diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..dfa6228492 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,32 @@ +{ + "name": "dimos-dev", + "image": "ghcr.io/dimensionalos/dev:dev", + "customizations": { + "vscode": { + "extensions": [ + "charliermarsh.ruff", + "ms-python.vscode-pylance" + ] + } + }, + "containerEnv": { + "PYTHONPATH": "${localEnv:PYTHONPATH}:/workspaces/dimos" + }, + "postCreateCommand": "git config --global --add safe.directory /workspaces/dimos && cd /workspaces/dimos && pre-commit install", + "settings": { + "notebook.formatOnSave.enabled": true, + "notebook.codeActionsOnSave": { + "notebook.source.fixAll": "explicit", + "notebook.source.organizeImports": "explicit" + }, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true + }, + "runArgs": [ + "--cap-add=NET_ADMIN" + ] +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..72d14322f1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,109 @@ +# Version control +.git +.gitignore +.github/ + +# Editor and IDE files +.vscode +.idea +*.swp +*.swo +.cursor/ +.cursorignore + +# Shell history +.bash_history +.zsh_history +.history + +# Python virtual environments +**/venv/ +**/.venv/ +**/env/ +**/.env/ +**/*-venv/ +**/*_venv/ +**/ENV/ + + +# Python build artifacts +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +*.egg-info/ +dist/ +build/ +*.so +*.dylib + +# Environment file +.env +.env.local +.env.*.local + +# Large data files +data/* +!data/.lfs/ + +# Model files (can be downloaded at runtime) +*.pt +*.pth +*.onnx +*.pb +*.h5 +*.ckpt +*.safetensors +checkpoints/ +assets/model-cache + +# Logs +*.log + +# Large media files (not needed for functionality) +*.png +*.jpg +*.jpeg +*.gif +*.mp4 +*.mov +*.avi +*.mkv +*.webm +*.MOV + +# Large font files +*.ttf +*.otf + +# Node modules (for dev tools, not needed in container) +node_modules/ +package-lock.json +package.json +bin/node_modules/ + +# Database files +*.db +*.sqlite +*.sqlite3 + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Temporary files +tmp/ +temp/ +*.tmp +.python-version + +# Exclude all assets subdirectories +assets/*/* +!assets/agent/prompt.txt +!assets/* diff --git a/.envrc b/.envrc index 48d0dc4859..09e580571a 100644 --- a/.envrc +++ b/.envrc @@ -1,2 +1,5 @@ -export DIRENV_WARN_TIMEOUT=-1000m -./bin/dev +if ! has nix_direnv_version || ! nix_direnv_version 3.0.6; then + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.6/direnvrc" "sha256-RYcUJaRMf8oF5LznDrlCXbkOQrywm0HDv1VjYGaJGdM=" +fi +use flake . +dotenv \ No newline at end of file diff --git a/.envrc.nix b/.envrc.nix new file mode 100644 index 0000000000..4a6ade8151 --- /dev/null +++ b/.envrc.nix @@ -0,0 +1,5 @@ +if ! has nix_direnv_version || ! nix_direnv_version 3.0.6; then + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.6/direnvrc" "sha256-RYcUJaRMf8oF5LznDrlCXbkOQrywm0HDv1VjYGaJGdM=" +fi +use flake . +dotenv_if_exists diff --git a/.envrc.venv b/.envrc.venv new file mode 100644 index 0000000000..a4b314c6f7 --- /dev/null +++ b/.envrc.venv @@ -0,0 +1,2 @@ +source env/bin/activate +dotenv_if_exists diff --git a/.gitattributes b/.gitattributes index fe75a92f79..302cb2e191 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,14 +1,16 @@ # Handle line endings automatically for files Git considers text, # converting them to LF on checkout. * text=auto eol=lf - # Ensure Python files always use LF for line endings. *.py text eol=lf - # Treat designated file types as binary and do not alter their contents or line endings. *.png binary *.jpg binary -*.gif binary *.ico binary *.pdf binary -*.mp4 binary +# Explicit LFS tracking for test files +/data/.lfs/*.tar.gz filter=lfs diff=lfs merge=lfs -text +*.onnx filter=lfs diff=lfs merge=lfs -text binary +*.mp4 filter=lfs diff=lfs merge=lfs -text binary +*.mov filter=lfs diff=lfs merge=lfs -text binary +*.gif filter=lfs diff=lfs merge=lfs -text binary diff --git a/.github/actions/docker-build/action.yml b/.github/actions/docker-build/action.yml new file mode 100644 index 0000000000..a538ad35fd --- /dev/null +++ b/.github/actions/docker-build/action.yml @@ -0,0 +1,59 @@ +name: docker-build +description: "Composite action to build and push a Docker target to GHCR" +inputs: + target: + description: "Dockerfile target stage to build" + required: true + tag: + description: "Image tag to push" + required: true + freespace: + description: "Remove large pre‑installed SDKs before building to free space" + required: false + default: "false" + context: + description: "Docker build context" + required: false + default: "." + +runs: + using: "composite" + steps: + - name: Free up disk space + if: ${{ inputs.freespace == 'true' }} + shell: bash + run: | + echo -e "pre cleanup space:\n $(df -h)" + sudo rm -rf /opt/ghc + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/share/boost + sudo rm -rf /usr/local/lib/android + echo -e "post cleanup space:\n $(df -h)" + + - uses: actions/checkout@v4 + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - uses: crazy-max/ghaction-github-runtime@v3 + + - uses: docker/setup-buildx-action@v3 + with: + driver: docker-container + install: true + use: true + + - name: Build & Push ${{ inputs.target }} + uses: docker/build-push-action@v6 + with: + push: true + context: ${{ inputs.context }} + file: docker/${{ inputs.target }}/Dockerfile + tags: ghcr.io/dimensionalos/${{ inputs.target }}:${{ inputs.tag }} + cache-from: type=gha,scope=${{ inputs.target }} + cache-to: type=gha,mode=max,scope=${{ inputs.target }} + build-args: | + FROM_TAG=${{ inputs.tag }} diff --git a/.github/workflows/_docker-build-template.yml b/.github/workflows/_docker-build-template.yml new file mode 100644 index 0000000000..730f4a4696 --- /dev/null +++ b/.github/workflows/_docker-build-template.yml @@ -0,0 +1,149 @@ +name: docker-build-template +on: + workflow_call: + inputs: + from-image: { type: string, required: true } + to-image: { type: string, required: true } + dockerfile: { type: string, required: true } + freespace: { type: boolean, default: true } + should-run: { type: boolean, default: false } + context: { type: string, default: '.' } + +# you can run this locally as well via +# ./bin/dockerbuild [image-name] +jobs: + build: + runs-on: [self-hosted, Linux] + permissions: + contents: read + packages: write + + steps: + - name: Fix permissions + if: ${{ inputs.should-run }} + run: | + sudo chown -R $USER:$USER ${{ github.workspace }} || true + + - uses: actions/checkout@v4 + if: ${{ inputs.should-run }} + with: + fetch-depth: 0 + + - name: free up disk space + # explicitly enable this for large builds + if: ${{ inputs.should-run && inputs.freespace }} + run: | + echo -e "pre cleanup space:\n $(df -h)" + sudo rm -rf /opt/ghc + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/share/boost + sudo rm -rf /usr/local/lib/android + + echo "=== Cleaning images from deleted branches ===" + + # Get list of all remote branches + git ls-remote --heads origin | awk '{print $2}' | sed 's|refs/heads/||' > /tmp/active_branches.txt + + # Check each docker image tag against branch list + docker images --format "{{.Repository}}:{{.Tag}}|{{.ID}}" | \ + grep "ghcr.io/dimensionalos" | \ + grep -v ":" | \ + while IFS='|' read image_ref id; do + tag=$(echo "$image_ref" | cut -d: -f2) + + # Skip if tag matches an active branch + if grep -qx "$tag" /tmp/active_branches.txt; then + echo "Branch exists: $tag - keeping $image_ref" + else + echo "Branch deleted: $tag - removing $image_ref" + docker rmi "$id" 2>/dev/null || true + fi + done + + rm -f /tmp/active_branches.txt + + USAGE=$(df / | awk 'NR==2 {print $5}' | sed 's/%//') + echo "Pre-docker-cleanup disk usage: ${USAGE}%" + + if [ $USAGE -gt 60 ]; then + echo "=== Running quick cleanup (usage > 60%) ===" + + # Keep newest image per tag + docker images --format "{{.Repository}}|{{.Tag}}|{{.ID}}" | \ + grep "ghcr.io/dimensionalos" | \ + grep -v "" | \ + while IFS='|' read repo tag id; do + created_ts=$(docker inspect -f '{{.Created}}' "$id" 2>/dev/null) + created_unix=$(date -d "$created_ts" +%s 2>/dev/null || echo "0") + echo "${repo}|${tag}|${id}|${created_unix}" + done | sort -t'|' -k1,1 -k2,2 -k4,4nr | \ + awk -F'|' ' + { + repo=$1; tag=$2; id=$3 + repo_tag = repo ":" tag + + # Skip protected tags + if (tag ~ /^(main|dev|latest)$/) next + + # Keep newest per tag, remove older duplicates + if (!(repo_tag in seen_combos)) { + seen_combos[repo_tag] = 1 + } else { + system("docker rmi " id " 2>/dev/null || true") + } + }' + + docker image prune -f + docker volume prune -f + fi + + # Aggressive cleanup if still above 85% + USAGE=$(df / | awk 'NR==2 {print $5}' | sed 's/%//') + if [ $USAGE -gt 85 ]; then + echo "=== AGGRESSIVE cleanup (usage > 85%) - removing all except main/dev ===" + + # Remove ALL images except main and dev tags + docker images --format "{{.Repository}}:{{.Tag}} {{.ID}}" | \ + grep -E "ghcr.io/dimensionalos" | \ + grep -vE ":(main|dev)$" | \ + awk '{print $2}' | xargs -r docker rmi -f || true + + docker container prune -f + docker volume prune -a -f + docker network prune -f + docker image prune -f + fi + + echo -e "post cleanup space:\n $(df -h)" + + - uses: docker/login-action@v3 + if: ${{ inputs.should-run }} + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # required for github cache of docker layers + - uses: crazy-max/ghaction-github-runtime@v3 + if: ${{ inputs.should-run }} + + # required for github cache of docker layers + - uses: docker/setup-buildx-action@v3 + if: ${{ inputs.should-run }} + with: + driver: docker-container + install: true + use: true + + - uses: docker/build-push-action@v6 + if: ${{ inputs.should-run }} + with: + push: true + context: ${{ inputs.context }} + file: docker/${{ inputs.dockerfile }}/Dockerfile + tags: ${{ inputs.to-image }} + cache-from: type=gha,scope=${{ inputs.dockerfile }} + cache-to: type=gha,mode=max,scope=${{ inputs.dockerfile }} + #cache-from: type=gha,scope=${{ inputs.dockerfile }}-${{ inputs.from-image }} + #cache-to: type=gha,mode=max,scope=${{ inputs.dockerfile }}-${{ inputs.from-image }} + build-args: FROM_IMAGE=${{ inputs.from-image }} diff --git a/.github/workflows/code-cleanup.yml b/.github/workflows/code-cleanup.yml new file mode 100644 index 0000000000..ddb75a90e3 --- /dev/null +++ b/.github/workflows/code-cleanup.yml @@ -0,0 +1,33 @@ +name: code-cleanup +on: push + +permissions: + contents: write + packages: write + pull-requests: read + +jobs: + pre-commit: + runs-on: self-hosted + steps: + - name: Fix permissions + run: | + sudo chown -R $USER:$USER ${{ github.workspace }} || true + + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + - name: Run pre-commit + id: pre-commit-first + uses: pre-commit/action@v3.0.1 + continue-on-error: true + + - name: Re-run pre-commit if failed initially + id: pre-commit-retry + if: steps.pre-commit-first.outcome == 'failure' + uses: pre-commit/action@v3.0.1 + continue-on-error: false + + - name: Commit code changes + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "CI code cleanup" diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000000..0c6abff68d --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,238 @@ +name: docker +on: + push: + branches: + - main + - dev + pull_request: + +permissions: + contents: read + packages: write + pull-requests: read + +jobs: + check-changes: + runs-on: [self-hosted, Linux] + outputs: + ros: ${{ steps.filter.outputs.ros }} + python: ${{ steps.filter.outputs.python }} + dev: ${{ steps.filter.outputs.dev }} + tests: ${{ steps.filter.outputs.tests }} + branch-tag: ${{ steps.set-tag.outputs.branch_tag }} + steps: + - name: Fix permissions + run: | + sudo chown -R $USER:$USER ${{ github.workspace }} || true + + - uses: actions/checkout@v4 + - id: filter + uses: dorny/paths-filter@v3 + with: + base: ${{ github.event.before }} + filters: | + # ros and python are (alternative) root images + # change to root stuff like docker.yml etc triggers rebuild of those + # which cascades into a full rebuild + ros: + - .github/workflows/_docker-build-template.yml + - .github/workflows/docker.yml + - docker/ros/** + + python: + - .github/workflows/_docker-build-template.yml + - .github/workflows/docker.yml + - docker/python/** + - pyproject.toml + + dev: + - docker/dev/** + + tests: + - dimos/** + + - name: Determine Branch Tag + id: set-tag + run: | + case "${GITHUB_REF_NAME}" in + main) branch_tag="latest" ;; + dev) branch_tag="dev" ;; + *) + branch_tag=$(echo "${GITHUB_REF_NAME}" \ + | tr '[:upper:]' '[:lower:]' \ + | sed -E 's#[^a-z0-9_.-]+#_#g' \ + | sed -E 's#^-+|-+$##g') + ;; + esac + echo "branch tag determined: ${branch_tag}" + echo branch_tag="${branch_tag}" >> "$GITHUB_OUTPUT" + + # just a debugger + inspect-needs: + needs: [check-changes, ros] + runs-on: dimos-runner-ubuntu-2204 + if: always() + steps: + - run: | + echo '${{ toJSON(needs) }}' + + ros: + needs: [check-changes] + if: needs.check-changes.outputs.ros == 'true' + uses: ./.github/workflows/_docker-build-template.yml + with: + should-run: true + from-image: ubuntu:22.04 + to-image: ghcr.io/dimensionalos/ros:${{ needs.check-changes.outputs.branch-tag }} + dockerfile: ros + + ros-python: + needs: [check-changes, ros] + if: always() + uses: ./.github/workflows/_docker-build-template.yml + with: + should-run: ${{ + needs.check-changes.outputs.python == 'true' && + needs.check-changes.result != 'error' && + needs.ros.result != 'error' + }} + + from-image: ghcr.io/dimensionalos/ros:${{ needs.ros.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }} + to-image: ghcr.io/dimensionalos/ros-python:${{ needs.check-changes.outputs.branch-tag }} + dockerfile: python + + python: + needs: [check-changes] + if: needs.check-changes.outputs.python == 'true' + uses: ./.github/workflows/_docker-build-template.yml + with: + should-run: true + dockerfile: python + from-image: ubuntu:22.04 + to-image: ghcr.io/dimensionalos/python:${{ needs.check-changes.outputs.branch-tag }} + + dev: + needs: [check-changes, python] + if: always() + + uses: ./.github/workflows/_docker-build-template.yml + with: + should-run: ${{ + needs.check-changes.result == 'success' && + ((needs.python.result == 'success') || + (needs.python.result == 'skipped' && + needs.check-changes.outputs.dev == 'true')) }} + from-image: ghcr.io/dimensionalos/python:${{ needs.python.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }} + to-image: ghcr.io/dimensionalos/dev:${{ needs.check-changes.outputs.branch-tag }} + dockerfile: dev + + ros-dev: + needs: [check-changes, ros-python] + if: always() + uses: ./.github/workflows/_docker-build-template.yml + with: + should-run: ${{ + needs.check-changes.result == 'success' && + (needs.check-changes.outputs.dev == 'true' || + (needs.ros-python.result == 'success' && (needs.check-changes.outputs.python == 'true' || needs.check-changes.outputs.ros == 'true'))) + }} + from-image: ghcr.io/dimensionalos/ros-python:${{ needs.ros-python.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }} + to-image: ghcr.io/dimensionalos/ros-dev:${{ needs.check-changes.outputs.branch-tag }} + dockerfile: dev + + run-ros-tests: + needs: [check-changes, ros-dev] + if: always() + uses: ./.github/workflows/tests.yml + secrets: inherit + with: + should-run: ${{ + needs.check-changes.result == 'success' && + ((needs.ros-dev.result == 'success') || + (needs.ros-dev.result == 'skipped' && + needs.check-changes.outputs.tests == 'true')) + }} + cmd: "pytest && pytest -m ros" # run tests that depend on ros as well + dev-image: ros-dev:${{ (needs.check-changes.outputs.python == 'true' || needs.check-changes.outputs.dev == 'true' || needs.check-changes.outputs.ros == 'true') && needs.ros-dev.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }} + + run-tests: + needs: [check-changes, dev] + if: always() + uses: ./.github/workflows/tests.yml + secrets: inherit + with: + should-run: ${{ + needs.check-changes.result == 'success' && + ((needs.dev.result == 'success') || + (needs.dev.result == 'skipped' && + needs.check-changes.outputs.tests == 'true')) + }} + cmd: "pytest" + dev-image: dev:${{ (needs.check-changes.outputs.python == 'true' || needs.check-changes.outputs.dev == 'true') && needs.dev.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }} + + # we run in parallel with normal tests for speed + run-heavy-tests: + needs: [check-changes, dev] + if: always() + uses: ./.github/workflows/tests.yml + secrets: inherit + with: + should-run: ${{ + needs.check-changes.result == 'success' && + ((needs.dev.result == 'success') || + (needs.dev.result == 'skipped' && + needs.check-changes.outputs.tests == 'true')) + }} + cmd: "pytest -m heavy" + dev-image: dev:${{ (needs.check-changes.outputs.python == 'true' || needs.check-changes.outputs.dev == 'true') && needs.dev.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }} + + run-lcm-tests: + needs: [check-changes, dev] + if: always() + uses: ./.github/workflows/tests.yml + secrets: inherit + with: + should-run: ${{ + needs.check-changes.result == 'success' && + ((needs.dev.result == 'success') || + (needs.dev.result == 'skipped' && + needs.check-changes.outputs.tests == 'true')) + }} + cmd: "pytest -m lcm" + dev-image: dev:${{ (needs.check-changes.outputs.python == 'true' || needs.check-changes.outputs.dev == 'true') && needs.dev.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }} + + # Run module tests directly to avoid pytest forking issues + # run-module-tests: + # needs: [check-changes, dev] + # if: ${{ + # always() && + # needs.check-changes.result == 'success' && + # ((needs.dev.result == 'success') || + # (needs.dev.result == 'skipped' && + # needs.check-changes.outputs.tests == 'true')) + # }} + # runs-on: [self-hosted, x64, 16gb] + # container: + # image: ghcr.io/dimensionalos/dev:${{ needs.check-changes.outputs.dev == 'true' && needs.dev.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }} + # steps: + # - name: Fix permissions + # run: | + # sudo chown -R $USER:$USER ${{ github.workspace }} || true + # + # - uses: actions/checkout@v4 + # with: + # lfs: true + # + # - name: Configure Git LFS + # run: | + # git config --global --add safe.directory '*' + # git lfs install + # git lfs fetch + # git lfs checkout + # + # - name: Run module tests + # env: + # CI: "true" + # run: | + # /entrypoint.sh bash -c "pytest -m module" + diff --git a/.github/workflows/readme.md b/.github/workflows/readme.md new file mode 100644 index 0000000000..0bc86973d8 --- /dev/null +++ b/.github/workflows/readme.md @@ -0,0 +1,51 @@ +# general structure of workflows + +Docker.yml checks for releavant file changes and re-builds required images +Currently images have a dependancy chain of ros -> python -> dev (in the future this might be a tree and can fork) + +On top of the dev image then tests are run. +Dev image is also what developers use in their own IDE via devcontainers +https://code.visualstudio.com/docs/devcontainers/containers + +# login to github docker repo + +create personal access token (classic, not fine grained) +https://github.com/settings/tokens + +add permissions +- read:packages scope to download container images and read their metadata. + + and optionally, + +- write:packages scope to download and upload container images and read and write their metadata. +- delete:packages scope to delete container images. + +more info @ https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry + +login to docker via + +`sh +echo TOKEN | docker login ghcr.io -u GITHUB_USER --password-stdin +` + +pull dev image (dev branch) +`sh +docker pull ghcr.io/dimensionalos/dev:dev +` + +pull dev image (master) +`sh +docker pull ghcr.io/dimensionalos/dev:latest +` + +# todo + +Currently there is an issue with ensuring both correct docker image build ordering, and skipping unneccessary re-builds. + +(we need job dependancies for builds to wait to their images underneath to be built (for example py waits for ros)) +by default if a parent is skipped, it's children get skipped as well, unless they have always() in their conditional. + +Issue is once we put always() in the conditional, it seems that no matter what other check we put in the same conditional, job will always run. +for this reason we cannot skip python (and above) builds for now. Needs review. + +I think we will need to write our own build dispatcher in python that calls github workflows that build images. diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000000..a94839a505 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,63 @@ +name: tests + +on: + workflow_call: + inputs: + should-run: + required: false + type: boolean + default: true + dev-image: + required: true + type: string + default: "dev:dev" + cmd: + required: true + type: string + +permissions: + contents: read + packages: read + +jobs: + + # cleanup: + # runs-on: dimos-runner-ubuntu-2204 + # steps: + # - name: exit early + # if: ${{ !inputs.should-run }} + # run: | + # exit 0 + + # - name: Free disk space + # run: | + # sudo rm -rf /opt/ghc + # sudo rm -rf /usr/share/dotnet + # sudo rm -rf /usr/local/share/boost + # sudo rm -rf /usr/local/lib/android + + run-tests: + runs-on: [self-hosted, Linux] + container: + image: ghcr.io/dimensionalos/${{ inputs.dev-image }} + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + ALIBABA_API_KEY: ${{ secrets.ALIBABA_API_KEY }} + + steps: + - uses: actions/checkout@v4 + + - name: Fix permissions + run: | + git config --global --add safe.directory '*' + + - name: Run tests + run: | + /entrypoint.sh bash -c "${{ inputs.cmd }}" + + - name: check disk space + if: failure() + run: | + df -h + diff --git a/.gitignore b/.gitignore index 20db783b19..18fd575c85 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,51 @@ -.venv/ .vscode/ # Ignore Python cache files __pycache__/ *.pyc -.venv* -venv* + +# Ignore virtual environment directories +*venv*/ +.venv*/ .ssh/ +# Ignore python tooling dirs +*.egg-info/ +__pycache__ + .env **/.DS_Store # Ignore default runtime output folder /assets/output/ +/assets/rgbd_data/ +/assets/saved_maps/ /assets/model-cache/ -assets/agent/memory.txt +/assets/agent/memory.txt .bash_history + +# Ignore all test data directories but allow compressed files +/data/* +!/data/.lfs/ + +# node env (used by devcontainers cli) +node_modules +package.json +package-lock.json + +# Ignore build artifacts +dist/ +build/ + +# Ignore data directory but keep .lfs subdirectory +data/* +!data/.lfs/ +FastSAM-x.pt +yolo11n.pt + +/thread_monitor_report.csv + +# symlink one of .envrc.* if you'd like to use +.envrc +.claude diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index ae48e66391..0000000000 --- a/.gitmodules +++ /dev/null @@ -1,9 +0,0 @@ -[submodule "dimos/external/openMVS"] - path = dimos/external/openMVS - url = https://github.com/cdcseacave/openMVS.git -[submodule "dimos/external/vcpkg"] - path = dimos/external/vcpkg - url = https://github.com/microsoft/vcpkg.git -[submodule "dimos/robot/unitree/external/go2_ros2_sdk"] - path = dimos/robot/unitree/external/go2_ros2_sdk - url = https://github.com/dimensionalOS/go2_ros2_sdk diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..67544f7f29 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,64 @@ +default_stages: [pre-commit] +exclude: (dimos/models/.*)|(deprecated) +repos: + + - repo: https://github.com/Lucas-C/pre-commit-hooks + rev: v1.5.5 + hooks: + - id: forbid-crlf + - id: remove-crlf + - id: insert-license + files: \.py$ + exclude: __init__\.py$ + args: + # use if you want to remove licences from all files + # (for globally changing wording or something) + #- --remove-header + - --license-filepath + - assets/license_file_header.txt + - --use-current-year + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.1 + hooks: + - id: ruff-format + stages: [pre-commit] + - id: ruff-check + args: [--fix, --unsafe-fixes] + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-case-conflict + - id: trailing-whitespace + language: python + types: [text] + stages: [pre-push] + - id: check-json + - id: check-toml + - id: check-yaml + - id: pretty-format-json + name: format json + args: [ --autofix, --no-sort-keys ] + + # - repo: local + # hooks: + # - id: mypy + # name: Type check + # # possible to also run within the dev image + # #entry: "./bin/dev mypy" + # entry: "./bin/mypy" + # language: python + # additional_dependencies: ["mypy==1.15.0", "numpy>=1.26.4,<2.0.0"] + # types: [python] + + - repo: local + hooks: + - id: lfs_check + name: LFS data + always_run: true + pass_filenames: false + entry: bin/lfs_check + language: script + + diff --git a/AUTONOMY_STACK_README.md b/AUTONOMY_STACK_README.md new file mode 100644 index 0000000000..70eff131ce --- /dev/null +++ b/AUTONOMY_STACK_README.md @@ -0,0 +1,284 @@ +# Autonomy Stack API Documentation + +## Prerequisites + +- Ubuntu 24.04 +- [ROS 2 Jazzy Installation](https://docs.ros.org/en/jazzy/Installation.html) + +Add the following line to your `~/.bashrc` to source the ROS 2 Jazzy setup script automatically: + +``` echo "source /opt/ros/jazzy/setup.bash" >> ~/.bashrc``` + +## MID360 Ethernet Configuration (skip for sim) + +### Step 1: Configure Network Interface + +1. Open Network Settings in Ubuntu +2. Find your Ethernet connection to the MID360 +3. Click the gear icon to edit settings +4. Go to IPv4 tab +5. Change Method from "Automatic (DHCP)" to "Manual" +6. Add the following settings: + - **Address**: 192.168.1.5 + - **Netmask**: 255.255.255.0 + - **Gateway**: 192.168.1.1 +7. Click "Apply" + +### Step 2: Configure MID360 IP in JSON + +1. Find your MID360 serial number (on sticker under QR code) +2. Note the last 2 digits (e.g., if serial ends in 89, use 189) +3. Edit the configuration file: + +```bash +cd ~/autonomy_stack_mecanum_wheel_platform +nano src/utilities/livox_ros_driver2/config/MID360_config.json +``` + +4. Update line 28 with your IP (192.168.1.1xx where xx = last 2 digits): + +```json +"ip" : "192.168.1.1xx", +``` + +5. Save and exit + +### Step 3: Verify Connection + +```bash +ping 192.168.1.1xx # Replace xx with your last 2 digits +``` + +## Robot Configuration + +### Setting Robot Type + +The system supports different robot configurations. Set the `ROBOT_CONFIG_PATH` environment variable to specify which robot configuration to use: + +```bash +# For Unitree G1 (default if not set) +export ROBOT_CONFIG_PATH="unitree/unitree_g1" + +# Add to ~/.bashrc to make permanent +echo 'export ROBOT_CONFIG_PATH="unitree/unitree_g1"' >> ~/.bashrc +``` + +Available robot configurations: +- `unitree/unitree_g1` - Unitree G1 robot (default) +- Add your custom robot configs in `src/base_autonomy/local_planner/config/` + +## Build the system + +You must do this every you make a code change, this is not Python + +```colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=Release``` + +## System Launch + +### Simulation Mode + +```bash +cd ~/autonomy_stack_mecanum_wheel_platform + +# Base autonomy only +./system_simulation.sh + +# With route planner +./system_simulation_with_route_planner.sh + +# With exploration planner +./system_simulation_with_exploration_planner.sh +``` + +### Real Robot Mode + +```bash +cd ~/autonomy_stack_mecanum_wheel_platform + +# Base autonomy only +./system_real_robot.sh + +# With route planner +./system_real_robot_with_route_planner.sh + +# With exploration planner +./system_real_robot_with_exploration_planner.sh +``` + +## Quick Troubleshooting + +- **Cannot ping MID360**: Check Ethernet cable and network settings +- **SLAM drift**: Press clear-terrain-map button on joystick controller +- **Joystick not recognized**: Unplug and replug USB dongle + + +## ROS Topics + +### Input Topics (Commands) + +| Topic | Type | Description | +|-------|------|-------------| +| `/way_point` | `geometry_msgs/PointStamped` | Send navigation goal (position only) | +| `/goal_pose` | `geometry_msgs/PoseStamped` | Send goal with orientation | +| `/cancel_goal` | `std_msgs/Bool` | Cancel current goal (data: true) | +| `/joy` | `sensor_msgs/Joy` | Joystick input | +| `/stop` | `std_msgs/Int8` | Soft Stop (2=stop all commmand, 0 = release) | +| `/navigation_boundary` | `geometry_msgs/PolygonStamped` | Set navigation boundaries | +| `/added_obstacles` | `sensor_msgs/PointCloud2` | Virtual obstacles | + +### Output Topics (Status) + +| Topic | Type | Description | +|-------|------|-------------| +| `/state_estimation` | `nav_msgs/Odometry` | Robot pose from SLAM | +| `/registered_scan` | `sensor_msgs/PointCloud2` | Aligned lidar point cloud | +| `/terrain_map` | `sensor_msgs/PointCloud2` | Local terrain map | +| `/terrain_map_ext` | `sensor_msgs/PointCloud2` | Extended terrain map | +| `/path` | `nav_msgs/Path` | Local path being followed | +| `/cmd_vel` | `geometry_msgs/Twist` | Velocity commands to motors | +| `/goal_reached` | `std_msgs/Bool` | True when goal reached, false when cancelled/new goal | + +### Map Topics + +| Topic | Type | Description | +|-------|------|-------------| +| `/overall_map` | `sensor_msgs/PointCloud2` | Global map (only in sim)| +| `/registered_scan` | `sensor_msgs/PointCloud2` | Current scan in map frame | +| `/terrain_map` | `sensor_msgs/PointCloud2` | Local obstacle map | + +## Usage Examples + +### Send Goal +```bash +ros2 topic pub /way_point geometry_msgs/msg/PointStamped "{ + header: {frame_id: 'map'}, + point: {x: 5.0, y: 3.0, z: 0.0} +}" --once +``` + +### Cancel Goal +```bash +ros2 topic pub /cancel_goal std_msgs/msg/Bool "data: true" --once +``` + +### Monitor Robot State +```bash +ros2 topic echo /state_estimation +``` + +## Configuration Parameters + +### Vehicle Parameters (`localPlanner`) + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `vehicleLength` | 0.5 | Robot length (m) | +| `vehicleWidth` | 0.5 | Robot width (m) | +| `maxSpeed` | 0.875 | Maximum speed (m/s) | +| `autonomySpeed` | 0.875 | Autonomous mode speed (m/s) | + +### Goal Tolerance Parameters + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `goalReachedThreshold` | 0.3-0.5 | Distance to consider goal reached (m) | +| `goalClearRange` | 0.35-0.6 | Extra clearance around goal (m) | +| `goalBehindRange` | 0.35-0.8 | Stop pursuing if goal behind within this distance (m) | +| `omniDirGoalThre` | 1.0 | Distance for omnidirectional approach (m) | + +### Obstacle Avoidance + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `obstacleHeightThre` | 0.1-0.2 | Height threshold for obstacles (m) | +| `adjacentRange` | 3.5 | Sensor range for planning (m) | +| `minRelZ` | -0.4 | Minimum relative height to consider (m) | +| `maxRelZ` | 0.3 | Maximum relative height to consider (m) | + +### Path Planning + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `pathScale` | 0.875 | Path resolution scale | +| `minPathScale` | 0.675 | Minimum path scale when blocked | +| `minPathRange` | 0.8 | Minimum planning range (m) | +| `dirThre` | 90.0 | Direction threshold (degrees) | + +### Control Parameters (`pathFollower`) + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `lookAheadDis` | 0.5 | Look-ahead distance (m) | +| `maxAccel` | 2.0 | Maximum acceleration (m/s²) | +| `slowDwnDisThre` | 0.875 | Slow down distance threshold (m) | + +### SLAM Blind Zones (`feature_extraction_node`) + +| Parameter | Mecanum | Description | +|-----------|---------|-------------| +| `blindFront` | 0.1 | Front blind zone (m) | +| `blindBack` | -0.2 | Back blind zone (m) | +| `blindLeft` | 0.1 | Left blind zone (m) | +| `blindRight` | -0.1 | Right blind zone (m) | +| `blindDiskRadius` | 0.4 | Cylindrical blind zone radius (m) | + +## Operating Modes + +### Mode Control +- **Joystick L2**: Hold for autonomy mode +- **Joystick R2**: Hold to disable obstacle checking + +### Speed Control +The robot automatically adjusts speed based on: +1. Obstacle proximity +2. Path complexity +3. Goal distance + +## Tuning Guide + +### For Tighter Navigation +- Decrease `goalReachedThreshold` (e.g., 0.2) +- Decrease `goalClearRange` (e.g., 0.3) +- Decrease `vehicleLength/Width` slightly + +### For Smoother Navigation +- Increase `goalReachedThreshold` (e.g., 0.5) +- Increase `lookAheadDis` (e.g., 0.7) +- Decrease `maxAccel` (e.g., 1.5) + +### For Aggressive Obstacle Avoidance +- Increase `obstacleHeightThre` (e.g., 0.15) +- Increase `adjacentRange` (e.g., 4.0) +- Increase blind zone parameters + +## Common Issues + +### Robot Oscillates at Goal +- Increase `goalReachedThreshold` +- Increase `goalBehindRange` + +### Robot Stops Too Far from Goal +- Decrease `goalReachedThreshold` +- Decrease `goalClearRange` + +### Robot Hits Low Obstacles +- Decrease `obstacleHeightThre` +- Adjust `minRelZ` to include lower points + +## SLAM Configuration + +### Localization Mode +Set in `livox_mid360.yaml`: +```yaml +local_mode: true +init_x: 0.0 +init_y: 0.0 +init_yaw: 0.0 +``` + +### Mapping Performance +```yaml +mapping_line_resolution: 0.1 # Decrease for higher quality +mapping_plane_resolution: 0.2 # Decrease for higher quality +max_iterations: 5 # Increase for better accuracy +``` \ No newline at end of file diff --git a/README.md b/README.md index f5df98f61f..1db93e9887 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The framework enables neurosymbolic orchestration of Agents as generalized spati The result: cross-embodied *"Dimensional Applications"* exceptional at generalization and robust at symbolic action execution. -## DIMOS x Unitree Go2 +## DIMOS x Unitree Go2 (OUT OF DATE) We are shipping a first look at the DIMOS x Unitree Go2 integration, allowing for off-the-shelf Agents() to "call" Unitree ROS2 Nodes and WebRTC action primitives, including: @@ -52,55 +52,18 @@ We are shipping a first look at the DIMOS x Unitree Go2 integration, allowing fo - Simulation bindings (Genesis, Isaacsim, etc.) to test your agentive application before deploying to a physical robot. - **DimOS Interface / Development Tools** - - Local development interface to control your robot, orchestrate agents, visualize camera/lidar streams, and debug your dimensional agentive application. + - Local development interface to control your robot, orchestrate agents, visualize camera/lidar streams, and debug your dimensional agentive application. -## Docker Quick Start 🚀 -> **⚠️ Recommended to start** - -### Prerequisites - -- Docker and Docker Compose installed -- A Unitree Go2 robot accessible on your network -- The robot's IP address -- OpenAI API Key - -### Configuration: - -Configure your environment variables in `.env` -```bash -OPENAI_API_KEY= -ALIBABA_API_KEY= -ANTHROPIC_API_KEY= -ROBOT_IP= -CONN_TYPE=webrtc -WEBRTC_SERVER_HOST=0.0.0.0 -WEBRTC_SERVER_PORT=9991 -DISPLAY=:0 -``` - -### Run docker compose -```bash -xhost +local:root # If running locally and desire RVIZ GUI -docker compose -f docker/unitree/agents_interface/docker-compose.yml up --build -``` -**Interface will start at http://localhost:3000** - -## Python Quick Start 🐍 - -### Prerequisites - -- A Unitree Go2 robot accessible on your network -- The robot's IP address -- OpenAI/Claude/Alibaba API Key - -### Python Installation (Ubuntu 22.04) +--- +## Python Installation +Tested on Ubuntu 22.04/24.04 ```bash sudo apt install python3-venv # Clone the repository -git clone --recurse-submodules https://github.com/dimensionalOS/dimos-unitree.git -cd dimos-unitree +git clone --branch dev --single-branch https://github.com/dimensionalOS/dimos.git +cd dimos # Create and activate virtual environment python3 -m venv venv @@ -108,16 +71,58 @@ source venv/bin/activate sudo apt install portaudio19-dev python3-pyaudio +# Install LFS +sudo apt install git-lfs +git lfs install + # Install torch and torchvision if not already installed -pip install -r base-requirements.txt +# Example CUDA 11.7, Pytorch 2.0.1 (replace with your required pytorch version if different) +pip install torch==2.0.1 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 +``` -# Install dependencies -pip install -r requirements.txt +#### Install dependencies +```bash +# CPU only (reccomended to attempt first) +pip install -e .[cpu,dev] + +# CUDA install +pip install -e .[cuda,dev] # Copy and configure environment variables cp default.env .env ``` +#### Test the install +```bash +pytest -s dimos/ +``` + +#### Test Dimensional with a replay UnitreeGo2 stream (no robot required) +```bash +CONNECTION_TYPE=replay python dimos/robot/unitree_webrtc/unitree_go2.py +``` + +#### Test Dimensional with a simulated UnitreeGo2 in MuJoCo (no robot required) +```bash +pip install -e .[sim] +export DISPLAY=:1 # Or DISPLAY=:0 if getting GLFW/OpenGL X11 errors +CONNECTION_TYPE=mujoco python dimos/robot/unitree_webrtc/unitree_go2.py +``` + +#### Test Dimensional with a real UnitreeGo2 over WebRTC +```bash +export ROBOT_IP=192.168.X.XXX # Add the robot IP address +python dimos/robot/unitree_webrtc/unitree_go2.py +``` + +#### Test Dimensional with a real UnitreeGo2 running Agents +*OpenAI / Alibaba keys required* +```bash +export ROBOT_IP=192.168.X.XXX # Add the robot IP address +python dimos/robot/unitree_webrtc/run_agents2.py +``` +--- + ### Agent API keys Full functionality will require API keys for the following: @@ -188,7 +193,7 @@ yarn install yarn dev # you may need to run sudo if previously built via Docker ``` -### Project Structure +### Project Structure (OUT OF DATE) ``` . @@ -233,7 +238,7 @@ yarn dev # you may need to run sudo if previously built via Docker ## Building -### Simple DimOS Application +### Simple DimOS Application (OUT OF DATE) ```python from dimos.robot.unitree.unitree_go2 import UnitreeGo2 @@ -260,7 +265,7 @@ while True: # keep process running ``` -### DimOS Application with Agent chaining +### DimOS Application with Agent chaining (OUT OF DATE) Let's build a simple DimOS application with Agent chaining. We define a ```planner``` as a ```PlanningAgent``` that takes in user input to devise a complex multi-step plan. This plan is passed step-by-step to an ```executor``` agent that can queue ```AbstractRobotSkill``` commands to the ```ROSCommandQueue```. @@ -300,7 +305,7 @@ while True: # keep process running time.sleep(1) ``` -### Calling Action Primitives +### Calling Action Primitives (OUT OF DATE) Call action primitives directly from ```Robot()``` for prototyping and testing. diff --git a/assets/agent/prompt.txt b/assets/agent/prompt.txt index 8a2ccff020..f38c13eb13 100644 --- a/assets/agent/prompt.txt +++ b/assets/agent/prompt.txt @@ -29,42 +29,23 @@ PERCEPTION & TEMPORAL AWARENESS: - You can recognize and track humans and objects in your field of view NAVIGATION & MOVEMENT: -- You can navigate to semantically described locations using Navigate (e.g., "go to the kitchen") -- You can navigate to visually identified objects using NavigateToObject (e.g., "go to the red chair") +- You can navigate to semantically described locations using NavigateWithText (e.g., "go to the kitchen") +- You can navigate to visually identified objects using NavigateWithText (e.g., "go to the red chair") - You can follow humans through complex environments using FollowHuman -- You can execute precise movement to specific coordinates using NavigateToGoal like if you're navigating to a GetPose waypoint - You can perform various body movements and gestures (sit, stand, dance, etc.) -- When navigating to a location like Kitchen or Bathroom or couch, use the generic Navigate skill to query spatial memory and navigate - You can stop any navigation process that is currently running using KillSkill -- Appended to every query you will find current objects detection and Saved Locations like this: - -Current objects detected: -[DETECTED OBJECTS] -Object 1: refrigerator - ID: 1 - Confidence: 0.88 - Position: x=9.44m, y=5.87m, z=-0.13m - Rotation: yaw=0.11 rad - Size: width=1.00m, height=1.46m - Depth: 4.92m - Bounding box: [606, 212, 773, 456] ----------------------------------- -Object 2: box - ID: 2 - Confidence: 0.84 - Position: x=11.30m, y=5.10m, z=-0.19m - Rotation: yaw=-0.03 rad - Size: width=0.91m, height=0.37m - Depth: 6.60m - Bounding box: [753, 149, 867, 195] ----------------------------------- + Saved Robot Locations: - LOCATION_NAME: Position (X, Y, Z), Rotation (X, Y, Z) ***ALWAYS CHECK FIRST if you can find a navigation query in the Saved Robot Locations before running the NavigateWithText tool call. If a saved location is found, get there with NavigateToGoal.*** -***When navigating to an object not in current object detected, run NavigateWithText, DO NOT EXPLORE with raw move commands!!! +***Don't use object detections for navigating to an object, ALWAYS run NavigateWithText. Only use object detections if NavigateWithText fails*** + +***When running NavigateWithText, set skip_visual_search flag to TRUE if the query is a general location such as kitchen or office, if it fails, then run without this flag*** + +***When navigating to an object not in current object detected, run NavigateWithText, DO NOT EXPLORE with raw move commands!!!*** PLANNING & REASONING: - You can develop both short-term and long-term plans to achieve complex goals diff --git a/assets/agent/prompt_agents2.txt b/assets/agent/prompt_agents2.txt new file mode 100644 index 0000000000..e0a47b553e --- /dev/null +++ b/assets/agent/prompt_agents2.txt @@ -0,0 +1,103 @@ +You are Daneel, an advanced AI agent created by the Dimensional team to control and operate the Unitree Go2 quadraped robot with a carrying case on your back. Your purpose is to assist humans by perceiving, understanding, and navigating physical environments while providing helpful interactions and completing tasks. + +CORE CAPABILITIES: + +Interaction with humans: +1. If asked to drop something off for someone, you can announce yourself to the person you are delivering to, wait 5 seconds, and then continue with your task. +2. If asked to pick up something, you can ask for help from the person you are picking up from, wait for them to respond, and then continue with your task. +3. If a human accidentally seems to call you "daniel" or something similar, don't worry about it or acknowledge it, as its due to the speech to text transcription being inaccurate. +4. When greeted, respond with what you are, Daneel, an AI agent trained to operate autonomously in physical space. +5. Be helpful. This means being proactive and comunicative. + + +You operate in an robot agent loop, iteratively completing tasks through these steps: +1. Analyze Events: Understand user needs and current state through event stream, focusing on latest user messages and execution results +2. Select Tools: Choose next tool call based on current state, task planning, relevant knowledge and available data APIs +3. Wait for Execution: Selected tool action will be executed by sandbox environment with new observations added to event stream +4. Iterate: Choose only one tool call per iteration, patiently repeat above steps until task completion +5. Killing: Kill skills when necessary with KillSkill. When asked to stop any skill or task, use KillSkill to stop it. + +SPATIAL UNDERSTANDING & MEMORY: +- You constantly are appending to your spatial memory, storing visual and positional data for future reference. You also have things from the past stored in your spatial memory. +- You can query your spatial memory using navigation related skills to find previously visited locations based on natural language descriptions +- You maintain persistent spatial knowledge across sessions in a vector database (ChromaDB) +- You can record specific locations using the tool called `tag_location_in_spatial_memory(location_name='label')`. This creates landmarks that can be revisited. If someone says "what do you think about this bathroom?" you know from context that you are now in the bathroom and can tag it as "bathroom". If someone says "this is where I work out" you can tag it as "exercise location". +- For local area information use the `street_map_query` skill. Example: `street_map_query('Where is a large park nearby?')` + +PERCEPTION & TEMPORAL AWARENESS: +- You can perceive the world through multiple sensory streams (video, audio, positional data) +- You maintain awareness of what has happened over time, building a temporal model of your environment +- You can identify and respond to changes in your surroundings +- You can recognize and track humans and objects in your field of view + +NAVIGATION & MOVEMENT: +- You can navigate to semantically described locations using `navigate_with_text` (e.g., "go to the kitchen") +- You can navigate to visually identified objects using `navigate_with_text` (e.g., "go to the red chair") +- You can follow humans through complex environments using `follow_human` +- You can perform various body movements and gestures (sit, stand, dance, etc.) +- You can stop any navigation process that is currently running using `stop_movement` +- If you are told to go to a location use `navigate_with_text()` +- If you want to explore the environment and go to places you haven't been before you can call the 'start_exploration` tool + +PLANNING & REASONING: +- You can develop both short-term and long-term plans to achieve complex goals +- You can reason about spatial relationships and plan efficient navigation paths +- You can adapt plans when encountering obstacles or changes in the environment +- You can combine multiple skills in sequence to accomplish multi-step tasks + +COMMUNICATION: +- You can listen to human instructions using speech recognition +- You can respond verbally using the `speak_aloud` skill with natural-sounding speech +- You maintain contextual awareness in conversations +- You provide clear progress updates during task execution but always be concise. Never be verbose! + +ADAPTABILITY: +- You can generalize your understanding to new, previously unseen environments +- You can apply learned skills to novel situations +- You can adjust your behavior based on environmental feedback +- You actively build and refine your knowledge of the world through exploration + +INTERACTION GUIDELINES: + +1. UNDERSTANDING USER REQUESTS + - Parse user instructions carefully to identify the intended goal + - Consider both explicit requests and implicit needs + - Ask clarifying questions when user intent is very ambiguous. But you can also be proactive. If someone says "Go greet the new people who are arriving." you can guess that you need to move to the front door to expect new people. Both do the task, but also let people it's a bit ambiguous by saying "I'm heading to the front door. Let me know if I should be going somewhere else." + +2. SKILL SELECTION AND EXECUTION + - Choose the most appropriate skill(s) for each task + - Provide all required parameters with correct values and types + - Execute skills in a logical sequence when multi-step actions are needed + - Monitor skill execution and handle any failures gracefully + +3. SPATIAL REASONING + - Leverage your spatial memory to navigate efficiently + - Build new spatial memories when exploring unfamiliar areas + - Use landmark-based navigation when possible + - Combine semantic and metric mapping for robust localization + +4. SAFETY AND ETHICS + - Prioritize human safety in all actions + - Respect privacy and personal boundaries + - Avoid actions that could damage the environment or the robot + - Be transparent about your capabilities and limitations + +5. COMMUNICATION STYLE + - Be concise but informative in your responses + - Provide clear status updates during extended tasks + - Use appropriate terminology based on the user's expertise level + - Maintain a helpful, supportive, and respectful tone + - Respond with the `speak_aloud` skill after EVERY QUERY to inform the user of your actions + - When speaking be terse and as concise as possible with a sentence or so, as you would if responding conversationally + +When responding to users: +1. First, acknowledge and confirm your understanding of their request +2. Select and execute the appropriate skill(s) using exact function names and proper parameters +3. Provide meaningful feedback about the outcome of your actions +4. Suggest next steps or additional information when relevant + +Example: If a user asks "Can you find the kitchen?", you would: +1. Acknowledge: "I'll help you find the kitchen." +2. Execute: Call the Navigate skill with query="kitchen" +3. Feedback: Report success or failure of navigation attempt +4. Next steps: Offer to take further actions once at the kitchen location diff --git a/docker/dev/base/motd b/assets/dimensionalascii.txt similarity index 100% rename from docker/dev/base/motd rename to assets/dimensionalascii.txt diff --git a/assets/dimos_interface.gif b/assets/dimos_interface.gif index c42fe4e903..e610a2b390 100644 Binary files a/assets/dimos_interface.gif and b/assets/dimos_interface.gif differ diff --git a/assets/foxglove_image_sharpness_test.json b/assets/foxglove_image_sharpness_test.json new file mode 100644 index 0000000000..e68b79a7e4 --- /dev/null +++ b/assets/foxglove_image_sharpness_test.json @@ -0,0 +1,140 @@ +{ + "configById": { + "Image!1dpphsz": { + "cameraState": { + "distance": 20, + "perspective": true, + "phi": 60, + "target": [ + 0, + 0, + 0 + ], + "targetOffset": [ + 0, + 0, + 0 + ], + "targetOrientation": [ + 0, + 0, + 0, + 1 + ], + "thetaOffset": 45, + "fovy": 45, + "near": 0.5, + "far": 5000 + }, + "followMode": "follow-pose", + "scene": {}, + "transforms": {}, + "topics": {}, + "layers": {}, + "publish": { + "type": "point", + "poseTopic": "/move_base_simple/goal", + "pointTopic": "/clicked_point", + "poseEstimateTopic": "/initialpose", + "poseEstimateXDeviation": 0.5, + "poseEstimateYDeviation": 0.5, + "poseEstimateThetaDeviation": 0.26179939 + }, + "imageMode": { + "imageTopic": "/all" + } + }, + "Image!2xvd0hl": { + "cameraState": { + "distance": 20, + "perspective": true, + "phi": 60, + "target": [ + 0, + 0, + 0 + ], + "targetOffset": [ + 0, + 0, + 0 + ], + "targetOrientation": [ + 0, + 0, + 0, + 1 + ], + "thetaOffset": 45, + "fovy": 45, + "near": 0.5, + "far": 5000 + }, + "followMode": "follow-pose", + "scene": {}, + "transforms": {}, + "topics": {}, + "layers": {}, + "publish": { + "type": "point", + "poseTopic": "/move_base_simple/goal", + "pointTopic": "/clicked_point", + "poseEstimateTopic": "/initialpose", + "poseEstimateXDeviation": 0.5, + "poseEstimateYDeviation": 0.5, + "poseEstimateThetaDeviation": 0.26179939 + }, + "imageMode": { + "imageTopic": "/sharp" + } + }, + "Gauge!1iofczz": { + "path": "/sharpness.x", + "minValue": 0, + "maxValue": 1, + "colorMap": "red-yellow-green", + "colorMode": "colormap", + "gradient": [ + "#0000ff", + "#ff00ff" + ], + "reverse": false + }, + "Plot!1gy7vh9": { + "paths": [ + { + "timestampMethod": "receiveTime", + "value": "/sharpness.x", + "enabled": true, + "color": "#4e98e2" + } + ], + "showXAxisLabels": true, + "showYAxisLabels": true, + "showLegend": true, + "legendDisplay": "floating", + "showPlotValuesInLegend": false, + "isSynced": true, + "xAxisVal": "timestamp", + "sidebarDimension": 240 + } + }, + "globalVariables": {}, + "userNodes": {}, + "playbackConfig": { + "speed": 1 + }, + "layout": { + "first": { + "first": "Image!1dpphsz", + "second": "Image!2xvd0hl", + "direction": "row" + }, + "second": { + "first": "Gauge!1iofczz", + "second": "Plot!1gy7vh9", + "direction": "row" + }, + "direction": "column" + } +} diff --git a/assets/foxglove_unitree_lcm_dashboard.json b/assets/foxglove_unitree_lcm_dashboard.json new file mode 100644 index 0000000000..df4e2715bc --- /dev/null +++ b/assets/foxglove_unitree_lcm_dashboard.json @@ -0,0 +1,288 @@ +{ + "configById": { + "3D!18i6zy7": { + "layers": { + "845139cb-26bc-40b3-8161-8ab60af4baf5": { + "visible": true, + "frameLocked": true, + "label": "Grid", + "instanceId": "845139cb-26bc-40b3-8161-8ab60af4baf5", + "layerId": "foxglove.Grid", + "lineWidth": 0.5, + "position": [ + 0, + 0, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "order": 1, + "size": 30, + "divisions": 30, + "color": "#248eff57" + }, + "ff758451-8c06-4419-a995-e93c825eb8be": { + "visible": true, + "frameLocked": true, + "label": "Grid", + "instanceId": "ff758451-8c06-4419-a995-e93c825eb8be", + "layerId": "foxglove.Grid", + "frameId": "base_link", + "size": 3, + "divisions": 3, + "lineWidth": 1.5, + "color": "#24fff4ff", + "position": [ + 0, + 0, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "order": 2 + } + }, + "cameraState": { + "perspective": false, + "distance": 25.847108697365048, + "phi": 32.532756465990374, + "thetaOffset": -179.288640038416, + "targetOffset": [ + 1.620731759058286, + -2.9069622235988986, + -0.09942375087215619 + ], + "target": [ + 0, + 0, + 0 + ], + "targetOrientation": [ + 0, + 0, + 0, + 1 + ], + "fovy": 45, + "near": 0.5, + "far": 5000 + }, + "followMode": "follow-pose", + "scene": { + "enableStats": true, + "ignoreColladaUpAxis": false, + "syncCamera": false, + "transforms": { + "visible": true + } + }, + "transforms": {}, + "topics": { + "/lidar": { + "stixelsEnabled": false, + "visible": true, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointShape": "circle", + "pointSize": 10, + "explicitAlpha": 1, + "decayTime": 0, + "cubeSize": 0.1, + "minValue": -0.3, + "cubeOutline": false + }, + "/odom": { + "visible": true, + "axisScale": 1 + }, + "/video": { + "visible": false + }, + "/global_map": { + "visible": true, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointSize": 10, + "decayTime": 0, + "pointShape": "cube", + "cubeOutline": false, + "cubeSize": 0.08, + "gradient": [ + "#06011dff", + "#d1e2e2ff" + ], + "stixelsEnabled": false, + "explicitAlpha": 1, + "minValue": -0.2 + }, + "/global_path": { + "visible": true, + "type": "line", + "arrowScale": [ + 1, + 0.15, + 0.15 + ], + "lineWidth": 0.132, + "gradient": [ + "#6bff7cff", + "#0081ffff" + ] + }, + "/global_target": { + "visible": true + }, + "/pt": { + "visible": false + }, + "/global_costmap": { + "visible": true, + "maxColor": "#8d3939ff", + "frameLocked": false, + "unknownColor": "#80808000", + "colorMode": "custom", + "alpha": 0.517, + "minColor": "#1e00ff00" + }, + "/global_gradient": { + "visible": true, + "maxColor": "#690066ff", + "unknownColor": "#30b89a00", + "minColor": "#00000000", + "colorMode": "custom", + "alpha": 0.3662, + "frameLocked": false, + "drawBehind": false + }, + "/global_cost_field": { + "visible": false, + "maxColor": "#ff0000ff", + "unknownColor": "#80808000" + }, + "/global_passable": { + "visible": false, + "maxColor": "#ffffff00", + "minColor": "#ff0000ff", + "unknownColor": "#80808000" + } + }, + "publish": { + "type": "point", + "poseTopic": "/move_base_simple/goal", + "pointTopic": "/clicked_point", + "poseEstimateTopic": "/estimate", + "poseEstimateXDeviation": 0.5, + "poseEstimateYDeviation": 0.5, + "poseEstimateThetaDeviation": 0.26179939 + }, + "imageMode": {}, + "foxglovePanelTitle": "test", + "followTf": "world" + }, + "Image!3mnp456": { + "cameraState": { + "distance": 20, + "perspective": true, + "phi": 60, + "target": [ + 0, + 0, + 0 + ], + "targetOffset": [ + 0, + 0, + 0 + ], + "targetOrientation": [ + 0, + 0, + 0, + 1 + ], + "thetaOffset": 45, + "fovy": 45, + "near": 0.5, + "far": 5000 + }, + "followMode": "follow-pose", + "scene": { + "enableStats": true + }, + "transforms": {}, + "topics": {}, + "layers": {}, + "publish": { + "type": "point", + "poseTopic": "/move_base_simple/goal", + "pointTopic": "/clicked_point", + "poseEstimateTopic": "/initialpose", + "poseEstimateXDeviation": 0.5, + "poseEstimateYDeviation": 0.5, + "poseEstimateThetaDeviation": 0.26179939 + }, + "imageMode": { + "imageTopic": "/video", + "colorMode": "gradient" + }, + "foxglovePanelTitle": "/video" + }, + "Plot!a1gj37": { + "paths": [ + { + "timestampMethod": "receiveTime", + "value": "/odom.pose.position.y", + "enabled": true, + "color": "#4e98e2" + }, + { + "timestampMethod": "receiveTime", + "value": "/odom.pose.position.x", + "enabled": true, + "color": "#f5774d" + }, + { + "timestampMethod": "receiveTime", + "value": "/odom.pose.position.z", + "enabled": true, + "color": "#f7df71" + } + ], + "showXAxisLabels": true, + "showYAxisLabels": true, + "showLegend": true, + "legendDisplay": "floating", + "showPlotValuesInLegend": false, + "isSynced": true, + "xAxisVal": "timestamp", + "sidebarDimension": 240 + } + }, + "globalVariables": {}, + "userNodes": {}, + "playbackConfig": { + "speed": 1 + }, + "drawerConfig": { + "tracks": [] + }, + "layout": { + "first": "3D!18i6zy7", + "second": { + "first": "Image!3mnp456", + "second": "Plot!a1gj37", + "direction": "column", + "splitPercentage": 28.030303030303028 + }, + "direction": "row", + "splitPercentage": 69.43271928754422 + } +} diff --git a/assets/foxglove_unitree_yolo.json b/assets/foxglove_unitree_yolo.json new file mode 100644 index 0000000000..ab53e4a71e --- /dev/null +++ b/assets/foxglove_unitree_yolo.json @@ -0,0 +1,849 @@ +{ + "configById": { + "3D!18i6zy7": { + "layers": { + "845139cb-26bc-40b3-8161-8ab60af4baf5": { + "visible": true, + "frameLocked": true, + "label": "Grid", + "instanceId": "845139cb-26bc-40b3-8161-8ab60af4baf5", + "layerId": "foxglove.Grid", + "lineWidth": 0.5, + "position": [ + 0, + 0, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "order": 1, + "size": 30, + "divisions": 30, + "color": "#248eff57" + }, + "ff758451-8c06-4419-a995-e93c825eb8be": { + "visible": false, + "frameLocked": true, + "label": "Grid", + "instanceId": "ff758451-8c06-4419-a995-e93c825eb8be", + "layerId": "foxglove.Grid", + "frameId": "base_link", + "divisions": 6, + "lineWidth": 1.5, + "color": "#24fff4ff", + "position": [ + 0, + 0, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "order": 2, + "size": 6 + } + }, + "cameraState": { + "perspective": true, + "distance": 13.268408624096915, + "phi": 26.658696672199024, + "thetaOffset": 99.69918626426482, + "targetOffset": [ + 1.740213570345715, + 0.7318803628974015, + -1.5060700211358968 + ], + "target": [ + 0, + 0, + 0 + ], + "targetOrientation": [ + 0, + 0, + 0, + 1 + ], + "fovy": 45, + "near": 0.5, + "far": 5000 + }, + "followMode": "follow-pose", + "scene": { + "enableStats": false, + "ignoreColladaUpAxis": false, + "syncCamera": true, + "transforms": { + "visible": true, + "showLabel": true, + "editable": true, + "enablePreloading": false, + "labelSize": 0.07 + } + }, + "transforms": { + "frame:camera_link": { + "visible": false + }, + "frame:sensor": { + "visible": false + }, + "frame:sensor_at_scan": { + "visible": false + }, + "frame:map": { + "visible": true + } + }, + "topics": { + "/lidar": { + "stixelsEnabled": false, + "visible": true, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointShape": "circle", + "pointSize": 2, + "explicitAlpha": 0.8, + "decayTime": 0, + "cubeSize": 0.05, + "cubeOutline": false, + "minValue": -2 + }, + "/odom": { + "visible": true, + "axisScale": 1 + }, + "/video": { + "visible": false + }, + "/global_map": { + "visible": true, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "decayTime": 0, + "pointShape": "square", + "cubeOutline": false, + "cubeSize": 0.08, + "gradient": [ + "#06011dff", + "#d1e2e2ff" + ], + "stixelsEnabled": false, + "explicitAlpha": 0.339, + "minValue": -0.2, + "pointSize": 5 + }, + "/global_path": { + "visible": true, + "type": "line", + "arrowScale": [ + 1, + 0.15, + 0.15 + ], + "lineWidth": 0.05, + "gradient": [ + "#6bff7cff", + "#0081ffff" + ] + }, + "/global_target": { + "visible": true + }, + "/pt": { + "visible": false + }, + "/global_costmap": { + "visible": false, + "maxColor": "#6b2b2bff", + "frameLocked": false, + "unknownColor": "#80808000", + "colorMode": "custom", + "alpha": 0.517, + "minColor": "#1e00ff00", + "drawBehind": false + }, + "/global_gradient": { + "visible": true, + "maxColor": "#690066ff", + "unknownColor": "#30b89a00", + "minColor": "#00000000", + "colorMode": "custom", + "alpha": 0.3662, + "frameLocked": false, + "drawBehind": false + }, + "/global_cost_field": { + "visible": false, + "maxColor": "#ff0000ff", + "unknownColor": "#80808000" + }, + "/global_passable": { + "visible": false, + "maxColor": "#ffffff00", + "minColor": "#ff0000ff", + "unknownColor": "#80808000" + }, + "/image": { + "visible": true, + "cameraInfoTopic": "/camera_info", + "distance": 1.5, + "planarProjectionFactor": 0, + "color": "#e7e1ffff" + }, + "/camera_info": { + "visible": true, + "distance": 1.5, + "planarProjectionFactor": 0 + }, + "/local_costmap": { + "visible": false + }, + "/navigation_goal": { + "visible": true + }, + "/debug_camera_optical_points": { + "stixelsEnabled": false, + "visible": false, + "pointSize": 0.07, + "pointShape": "cube", + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo" + }, + "/debug_world_points": { + "visible": false, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "rainbow", + "pointShape": "cube" + }, + "/filtered_points_suitcase_0": { + "visible": false, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointShape": "cube", + "flatColor": "#ff0808ff", + "cubeSize": 0.149, + "pointSize": 28.57 + }, + "/filtered_points_combined": { + "visible": true, + "flatColor": "#ff0000ff", + "pointShape": "cube", + "pointSize": 6.63, + "colorField": "z", + "colorMode": "gradient", + "colorMap": "rainbow", + "cubeSize": 0.35, + "gradient": [ + "#d100caff", + "#ff0000ff" + ] + }, + "/filtered_points_box_7": { + "visible": true, + "flatColor": "#fbfaffff", + "colorField": "intensity", + "colorMode": "colormap", + "colorMap": "turbo" + }, + "/filtered_pointcloud": { + "visible": true, + "colorField": "z", + "colorMode": "flat", + "colorMap": "turbo", + "flatColor": "#ff0000ff", + "pointSize": 40.21, + "pointShape": "cube", + "cubeSize": 0.1, + "cubeOutline": true + }, + "/detected": { + "visible": false, + "pointSize": 1.5, + "pointShape": "cube", + "cubeSize": 0.118, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "flatColor": "#d70000ff", + "cubeOutline": true + }, + "/detected_0": { + "visible": true, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointSize": 1.6, + "pointShape": "cube", + "cubeSize": 0.1, + "flatColor": "#e00000ff", + "stixelsEnabled": false, + "decayTime": 0, + "cubeOutline": true + }, + "/detected_1": { + "visible": true, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointShape": "cube", + "cubeSize": 0.1, + "flatColor": "#00ff15ff", + "cubeOutline": true + }, + "/image_detected_0": { + "visible": false + }, + "/detected/pointcloud/1": { + "visible": true, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointShape": "cube", + "flatColor": "#15ff00ff", + "pointSize": 0.1, + "cubeSize": 0.05, + "cubeOutline": true + }, + "/detected/pointcloud/2": { + "visible": true, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointShape": "cube", + "flatColor": "#00ffe1ff", + "pointSize": 10, + "cubeOutline": true, + "cubeSize": 0.05 + }, + "/detected/pointcloud/0": { + "visible": true, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointShape": "cube", + "flatColor": "#ff0000ff", + "cubeOutline": true, + "cubeSize": 0.04 + }, + "/detected/image/0": { + "visible": false + }, + "/detected/image/3": { + "visible": false + }, + "/detected/pointcloud/3": { + "visible": true, + "pointSize": 1.5, + "pointShape": "cube", + "cubeSize": 0.1, + "flatColor": "#00fffaff", + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo" + }, + "/detected/image/1": { + "visible": false + }, + "/registered_scan": { + "visible": true, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointShape": "circle", + "pointSize": 2 + }, + "/image/camera_info": { + "visible": true, + "distance": 2 + }, + "/map": { + "visible": true, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointShape": "square", + "cubeSize": 0.13, + "explicitAlpha": 1, + "pointSize": 1, + "decayTime": 2 + }, + "/detection3d/markers": { + "visible": true, + "color": "#88ff00ff", + "showOutlines": true, + "selectedIdVariable": "" + }, + "/foxglove/scene_update": { + "visible": true + }, + "/scene_update": { + "visible": true, + "showOutlines": true, + "computeVertexNormals": true + }, + "/target": { + "visible": true, + "axisScale": 1 + }, + "/goal_pose": { + "visible": true, + "axisScale": 0.5 + } + }, + "publish": { + "type": "point", + "poseTopic": "/move_base_simple/goal", + "pointTopic": "/clicked_point", + "poseEstimateTopic": "/estimate", + "poseEstimateXDeviation": 0.5, + "poseEstimateYDeviation": 0.5, + "poseEstimateThetaDeviation": 0.26179939 + }, + "imageMode": {}, + "foxglovePanelTitle": "", + "followTf": "map" + }, + "Image!3mnp456": { + "cameraState": { + "distance": 20, + "perspective": true, + "phi": 60, + "target": [ + 0, + 0, + 0 + ], + "targetOffset": [ + 0, + 0, + 0 + ], + "targetOrientation": [ + 0, + 0, + 0, + 1 + ], + "thetaOffset": 45, + "fovy": 45, + "near": 0.5, + "far": 5000 + }, + "followMode": "follow-pose", + "scene": { + "enableStats": false, + "transforms": { + "showLabel": false, + "visible": true + } + }, + "transforms": { + "frame:world": { + "visible": true + }, + "frame:camera_optical": { + "visible": false + }, + "frame:camera_link": { + "visible": false + }, + "frame:base_link": { + "visible": false + } + }, + "topics": { + "/lidar": { + "visible": false, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointSize": 6, + "explicitAlpha": 0.6, + "pointShape": "circle", + "cubeSize": 0.016 + }, + "/odom": { + "visible": false + }, + "/local_costmap": { + "visible": false + }, + "/global_costmap": { + "visible": false, + "minColor": "#ffffff00" + }, + "/detected_0": { + "visible": true, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointSize": 23, + "pointShape": "cube", + "cubeSize": 0.04, + "flatColor": "#ff0000ff", + "stixelsEnabled": false + }, + "/detected_1": { + "visible": true, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointSize": 20.51, + "flatColor": "#34ff00ff", + "pointShape": "cube", + "cubeSize": 0.04, + "cubeOutline": false + }, + "/filtered_pointcloud": { + "visible": true, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "rainbow", + "pointSize": 1.5, + "pointShape": "cube", + "flatColor": "#ff0000ff", + "cubeSize": 0.1 + }, + "/global_map": { + "visible": false, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointShape": "cube", + "pointSize": 5 + }, + "/detected/pointcloud/1": { + "visible": false, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointShape": "cube", + "cubeSize": 0.01, + "flatColor": "#00ff1eff", + "pointSize": 15, + "decayTime": 0, + "cubeOutline": true + }, + "/detected/pointcloud/2": { + "visible": false, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointShape": "circle", + "cubeSize": 0.1, + "flatColor": "#00fbffff", + "pointSize": 0.01 + }, + "/detected/pointcloud/0": { + "visible": false, + "colorField": "intensity", + "colorMode": "flat", + "colorMap": "turbo", + "pointShape": "cube", + "flatColor": "#ff0000ff", + "pointSize": 15, + "cubeOutline": true, + "cubeSize": 0.03 + }, + "/registered_scan": { + "visible": false, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointShape": "circle", + "pointSize": 6.49 + }, + "/detection3d/markers": { + "visible": false + }, + "/foxglove/scene_update": { + "visible": true + }, + "/scene_update": { + "visible": false + }, + "/map": { + "visible": false, + "colorField": "z", + "colorMode": "colormap", + "colorMap": "turbo", + "pointSize": 8 + } + }, + "layers": {}, + "publish": { + "type": "point", + "poseTopic": "/move_base_simple/goal", + "pointTopic": "/clicked_point", + "poseEstimateTopic": "/initialpose", + "poseEstimateXDeviation": 0.5, + "poseEstimateYDeviation": 0.5, + "poseEstimateThetaDeviation": 0.26179939 + }, + "imageMode": { + "imageTopic": "/image", + "colorMode": "gradient", + "annotations": { + "/detections": { + "visible": true + }, + "/annotations": { + "visible": true + } + }, + "synchronize": false, + "rotation": 0, + "calibrationTopic": "/camera_info" + }, + "foxglovePanelTitle": "" + }, + "Plot!3heo336": { + "paths": [ + { + "timestampMethod": "publishTime", + "value": "/image.header.stamp.sec", + "enabled": true, + "color": "#4e98e2", + "label": "image", + "showLine": false + }, + { + "timestampMethod": "publishTime", + "value": "/map.header.stamp.sec", + "enabled": true, + "color": "#f5774d", + "label": "lidar", + "showLine": false + }, + { + "timestampMethod": "publishTime", + "value": "/tf.transforms[0].header.stamp.sec", + "enabled": true, + "color": "#f7df71", + "label": "tf", + "showLine": false + }, + { + "timestampMethod": "publishTime", + "value": "/odom.header.stamp.sec", + "enabled": true, + "color": "#5cd6a9", + "label": "odom", + "showLine": false + } + ], + "showXAxisLabels": true, + "showYAxisLabels": true, + "showLegend": true, + "legendDisplay": "floating", + "showPlotValuesInLegend": false, + "isSynced": true, + "xAxisVal": "timestamp", + "sidebarDimension": 240 + }, + "Image!47pi3ov": { + "cameraState": { + "distance": 20, + "perspective": true, + "phi": 60, + "target": [ + 0, + 0, + 0 + ], + "targetOffset": [ + 0, + 0, + 0 + ], + "targetOrientation": [ + 0, + 0, + 0, + 1 + ], + "thetaOffset": 45, + "fovy": 45, + "near": 0.5, + "far": 5000 + }, + "followMode": "follow-pose", + "scene": {}, + "transforms": {}, + "topics": {}, + "layers": {}, + "publish": { + "type": "point", + "poseTopic": "/move_base_simple/goal", + "pointTopic": "/clicked_point", + "poseEstimateTopic": "/initialpose", + "poseEstimateXDeviation": 0.5, + "poseEstimateYDeviation": 0.5, + "poseEstimateThetaDeviation": 0.26179939 + }, + "imageMode": { + "imageTopic": "/detected/image/0" + } + }, + "Image!4kk50gw": { + "cameraState": { + "distance": 20, + "perspective": true, + "phi": 60, + "target": [ + 0, + 0, + 0 + ], + "targetOffset": [ + 0, + 0, + 0 + ], + "targetOrientation": [ + 0, + 0, + 0, + 1 + ], + "thetaOffset": 45, + "fovy": 45, + "near": 0.5, + "far": 5000 + }, + "followMode": "follow-pose", + "scene": {}, + "transforms": {}, + "topics": {}, + "layers": {}, + "publish": { + "type": "point", + "poseTopic": "/move_base_simple/goal", + "pointTopic": "/clicked_point", + "poseEstimateTopic": "/initialpose", + "poseEstimateXDeviation": 0.5, + "poseEstimateYDeviation": 0.5, + "poseEstimateThetaDeviation": 0.26179939 + }, + "imageMode": { + "imageTopic": "/detected/image/1" + } + }, + "Image!2348e0b": { + "cameraState": { + "distance": 20, + "perspective": true, + "phi": 60, + "target": [ + 0, + 0, + 0 + ], + "targetOffset": [ + 0, + 0, + 0 + ], + "targetOrientation": [ + 0, + 0, + 0, + 1 + ], + "thetaOffset": 45, + "fovy": 45, + "near": 0.5, + "far": 5000 + }, + "followMode": "follow-pose", + "scene": {}, + "transforms": {}, + "topics": {}, + "layers": {}, + "publish": { + "type": "point", + "poseTopic": "/move_base_simple/goal", + "pointTopic": "/clicked_point", + "poseEstimateTopic": "/initialpose", + "poseEstimateXDeviation": 0.5, + "poseEstimateYDeviation": 0.5, + "poseEstimateThetaDeviation": 0.26179939 + }, + "imageMode": { + "imageTopic": "/detected/image/2", + "synchronize": false + } + }, + "StateTransitions!pu21x4": { + "paths": [ + { + "value": "/annotations.texts[1].text", + "timestampMethod": "receiveTime", + "label": "detection1" + }, + { + "value": "/annotations.texts[3].text", + "timestampMethod": "receiveTime", + "label": "detection2" + }, + { + "value": "/annotations.texts[5].text", + "timestampMethod": "receiveTime", + "label": "detection3" + } + ], + "isSynced": true, + "showPoints": true, + "timeWindowMode": "automatic" + } + }, + "globalVariables": {}, + "userNodes": {}, + "playbackConfig": { + "speed": 1 + }, + "drawerConfig": { + "tracks": [] + }, + "layout": { + "first": { + "first": "3D!18i6zy7", + "second": "Image!3mnp456", + "direction": "row", + "splitPercentage": 47.265625 + }, + "second": { + "first": "Plot!3heo336", + "second": { + "first": { + "first": "Image!47pi3ov", + "second": { + "first": "Image!4kk50gw", + "second": "Image!2348e0b", + "direction": "row" + }, + "direction": "row", + "splitPercentage": 33.06523681858802 + }, + "second": "StateTransitions!pu21x4", + "direction": "column", + "splitPercentage": 86.63101604278076 + }, + "direction": "row", + "splitPercentage": 46.39139486467731 + }, + "direction": "column", + "splitPercentage": 81.62970106075217 + } +} diff --git a/assets/framecount.mp4 b/assets/framecount.mp4 index 74b0b2322b..759ee6ab27 100644 Binary files a/assets/framecount.mp4 and b/assets/framecount.mp4 differ diff --git a/assets/license_file_header.txt b/assets/license_file_header.txt new file mode 100644 index 0000000000..4268cd990f --- /dev/null +++ b/assets/license_file_header.txt @@ -0,0 +1,13 @@ +Copyright 2025 Dimensional Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/assets/simple_demo.mp4 b/assets/simple_demo.mp4 index 086a636383..cb8a635e78 100644 Binary files a/assets/simple_demo.mp4 and b/assets/simple_demo.mp4 differ diff --git a/assets/simple_demo_small.gif b/assets/simple_demo_small.gif index 294a2a0879..3c2cf54ef4 100644 Binary files a/assets/simple_demo_small.gif and b/assets/simple_demo_small.gif differ diff --git a/assets/trimmed_video_office.mov b/assets/trimmed_video_office.mov index 278582f74e..a3072be8fc 100644 Binary files a/assets/trimmed_video_office.mov and b/assets/trimmed_video_office.mov differ diff --git a/bin/cuda/fix_ort.sh b/bin/cuda/fix_ort.sh new file mode 100755 index 0000000000..182f387364 --- /dev/null +++ b/bin/cuda/fix_ort.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# This script fixes the onnxruntime <--> onnxruntime-gpu package clash +# that occurs when chromadb and other dependencies require the CPU-only +# onnxruntime package. It removes onnxruntime and reinstalls the GPU version. +set -euo pipefail + +: "${GPU_VER:=1.18.1}" + +python - </dev/null +} + +image_pull() { + docker pull "$IMAGE_NAME" +} + +ensure_image_downloaded() { + if ! image_exists "$1"; then + echo "Image ${IMAGE_NAME} not found. Pulling..." + image_pull "$1" + fi +} + +check_image_running() { + if docker ps -q --filter "ancestor=${IMAGE_NAME}" | grep -q .; then + return 0 + else + return 1 + fi +} + +stop_image() { + if check_image_running ${IMAGE_NAME}; then + echo "Stopping containers from image ${IMAGE_NAME}..." + docker stop $(docker ps -q --filter "ancestor=${IMAGE_NAME}") + else + echo "No containers from image ${IMAGE_NAME} are running." + fi +} + + +get_tag() { + local branch_name + branch_name=$(git rev-parse --abbrev-ref HEAD) + + case "${branch_name}" in + master) image_tag="latest" ;; + main) image_tag="latest" ;; + dev) image_tag="dev" ;; + *) + image_tag=$(echo "${branch_name}" \ + | tr '[:upper:]' '[:lower:]' \ + | sed -E 's#[^a-z0-9_.-]+#_#g' \ + | sed -E 's#^-+|-+$##g') + ;; + esac + echo "${image_tag}" +} + build_image() { + local image_tag + image_tag=$(get_tag) docker build \ --build-arg GIT_COMMIT=$(git rev-parse --short HEAD) \ --build-arg GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) \ - -t dimensionalos/dev-base docker/dev/base/ + -t "ghcr.io/dimensionalos/dev:${image_tag}" -f docker/dev/Dockerfile . } remove_image() { - docker rm -f dimos-dev + local tag=$(get_tag) + docker rm -f "dimos-dev-${tag}" 2>/dev/null || true } -COMPOSE_FILE="docker/dev/base/docker-compose.yaml" +devcontainer_install() { + # prompt user if we should install devcontainer + read -p "devcontainer CLI (https://github.com/devcontainers/cli) not found. Install into repo root? (y/n): " install_choice + if [[ "$install_choice" != "y" && "$install_choice" != "Y" ]]; then + echo "Devcontainer CLI installation aborted. Please install manually" + exit 1 + fi + + cd "$REPO_ROOT/bin/" + if [[ ! -d "$REPO_ROOT/bin/node_modules" ]]; then + npm init -y 1>/dev/null + fi + npm install @devcontainers/cli 1>&2 + if [[ $? -ne 0 ]]; then + echo "Failed to install devcontainer CLI. Please install it manually." + exit 1 + fi + echo $REPO_ROOT/bin/node_modules/.bin/devcontainer +} + + -# Parse flags and commands -while [[ $# -gt 0 ]]; do +find_devcontainer_bin() { + local bin_path + bin_path=$(command -v devcontainer) + + if [[ -z "$bin_path" ]]; then + bin_path="$REPO_ROOT/bin/node_modules/.bin/devcontainer" + fi + + if [[ -x "$bin_path" ]]; then + echo "$bin_path" + else + devcontainer_install + fi +} + +# Passes all arguments to devcontainer command, ensuring: +# - devcontainer CLI is installed +# - docker image is running +# - the workspace folder is set to the repository root +run_devcontainer() { + local devcontainer_bin + devcontainer_bin=$(find_devcontainer_bin) + + if ! check_image_running; then + ensure_image_downloaded + $devcontainer_bin up --workspace-folder="$REPO_ROOT" --gpu-availability="detect" + fi + + exec $devcontainer_bin $1 --workspace-folder="$REPO_ROOT" "${@:2}" +} + +if [[ $# -eq 0 ]]; then + run_devcontainer exec bash +else case "$1" in - -c|--cuda) - COMPOSE_FILE="docker/dev/base/docker-compose-cuda.yaml" + build) + build_image shift ;; - remove) - remove_image - exit 0 + stop) + stop_image + shift ;; - build) - remove_image - build_image + down) + stop_image shift ;; - *) - # Skip unrecognized args + pull) + docker pull ghcr.io/dimensionalos/dev:dev shift ;; + *) + run_devcontainer exec "$@" + shift + ;; esac -done - -if ! docker image inspect dimensionalos/dev-base &>/dev/null; then - echo "Image dimensionalos/dev-base not found. Building..." - build_image fi - -# Get current directory relative to repo root -REPO_ROOT=$(git rev-parse --show-toplevel) -REL_PATH=$(realpath --relative-to="$REPO_ROOT" "$(pwd)") - -docker compose -f "$REPO_ROOT/$COMPOSE_FILE" up -d \ - && docker exec -it dimos-dev bash -c "cd $REL_PATH; exec bash" diff --git a/bin/dockerbuild b/bin/dockerbuild new file mode 100755 index 0000000000..b02e10d5ca --- /dev/null +++ b/bin/dockerbuild @@ -0,0 +1,32 @@ +#!/bin/bash + +# Exit on error +set -e + +# Check for directory argument +if [ $# -lt 1 ]; then + echo "Usage: $0 [additional-docker-build-args]" + echo "Example: $0 base-ros-python --no-cache" + exit 1 +fi + +# Get the docker directory name +DOCKER_DIR=$1 +shift # Remove the first argument, leaving any additional args + +# Check if directory exists +if [ ! -d "docker/$DOCKER_DIR" ]; then + echo "Error: Directory docker/$DOCKER_DIR does not exist" + exit 1 +fi + +# Set image name based on directory +IMAGE_NAME="ghcr.io/dimensionalos/$DOCKER_DIR" + +echo "Building image $IMAGE_NAME from docker/$DOCKER_DIR..." +echo "Build context: $(pwd)" + +# Build the docker image with the current directory as context +docker build -t "$IMAGE_NAME" -f "docker/$DOCKER_DIR/Dockerfile" "$@" . + +echo "Successfully built $IMAGE_NAME" diff --git a/bin/filter-errors-after-date b/bin/filter-errors-after-date new file mode 100755 index 0000000000..03c7de0ca7 --- /dev/null +++ b/bin/filter-errors-after-date @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 + +# Used to filter errors to only show lines committed on or after a specific date +# Can be chained with filter-errors-for-user + +from datetime import datetime +import re +import subprocess +import sys + +_blame = {} + + +def _is_after_date(file, line_no, cutoff_date): + if file not in _blame: + _blame[file] = _get_git_blame_dates_for_file(file) + line_date = _blame[file].get(line_no) + if not line_date: + return False + return line_date >= cutoff_date + + +def _get_git_blame_dates_for_file(file_name): + try: + result = subprocess.run( + ["git", "blame", "--date=short", file_name], + capture_output=True, + text=True, + check=True, + ) + + blame_map = {} + # Each line looks like: ^abc123 (Author Name 2024-01-01 1) code + blame_pattern = re.compile(r"^[^\(]+\([^\)]+(\d{4}-\d{2}-\d{2})") + + for i, line in enumerate(result.stdout.split("\n")): + if not line: + continue + match = blame_pattern.match(line) + if match: + date_str = match.group(1) + blame_map[str(i + 1)] = date_str + + return blame_map + except subprocess.CalledProcessError: + return {} + + +def main(): + if len(sys.argv) != 2: + print("Usage: filter-errors-after-date ", file=sys.stderr) + print(" Example: filter-errors-after-date 2025-10-04", file=sys.stderr) + sys.exit(1) + + cutoff_date = sys.argv[1] + + try: + datetime.strptime(cutoff_date, "%Y-%m-%d") + except ValueError: + print(f"Error: Invalid date format '{cutoff_date}'. Use YYYY-MM-DD", file=sys.stderr) + sys.exit(1) + + for line in sys.stdin.readlines(): + split = re.findall(r"^([^:]+):(\d+):(.*)", line) + if not split or len(split[0]) != 3: + continue + + file, line_no = split[0][:2] + if not file.startswith("dimos/"): + continue + + if _is_after_date(file, line_no, cutoff_date): + print(":".join(split[0])) + + +if __name__ == "__main__": + main() diff --git a/bin/filter-errors-for-user b/bin/filter-errors-for-user new file mode 100755 index 0000000000..045b30b293 --- /dev/null +++ b/bin/filter-errors-for-user @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 + +# Used when running `./bin/mypy-strict --for-me` + +import re +import subprocess +import sys + +_blame = {} + + +def _is_for_user(file, line_no, user_email): + if file not in _blame: + _blame[file] = _get_git_blame_for_file(file) + return _blame[file][line_no] == user_email + + +def _get_git_blame_for_file(file_name): + try: + result = subprocess.run( + ["git", "blame", "--show-email", "-e", file_name], + capture_output=True, + text=True, + check=True, + ) + + blame_map = {} + # Each line looks like: ^abc123 ( 2024-01-01 12:00:00 +0000 1) code + blame_pattern = re.compile(r"^[^\(]+\(<([^>]+)>") + + for i, line in enumerate(result.stdout.split("\n")): + if not line: + continue + match = blame_pattern.match(line) + if match: + email = match.group(1) + blame_map[str(i + 1)] = email + + return blame_map + except subprocess.CalledProcessError: + return {} + + +def main(): + if len(sys.argv) != 2: + print("Usage: filter-errors-for-user ", file=sys.stderr) + sys.exit(1) + + user_email = sys.argv[1] + + for line in sys.stdin.readlines(): + split = re.findall(r"^([^:]+):(\d+):(.*)", line) + if not split or len(split[0]) != 3: + continue + file, line_no = split[0][:2] + if not file.startswith("dimos/"): + continue + if _is_for_user(file, line_no, user_email): + print(":".join(split[0])) + + +if __name__ == "__main__": + main() diff --git a/bin/lfs_check b/bin/lfs_check new file mode 100755 index 0000000000..0ddb847d56 --- /dev/null +++ b/bin/lfs_check @@ -0,0 +1,42 @@ +#!/bin/bash + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +ROOT=$(git rev-parse --show-toplevel) +cd $ROOT + +new_data=() + +# Enable nullglob to make globs expand to nothing when not matching +shopt -s nullglob + +# Iterate through all directories in data/ +for dir_path in data/*; do + + # Extract directory name + dir_name=$(basename "$dir_path") + + # Skip .lfs directory if it exists + [ "$dir_name" = ".lfs" ] && continue + + # Define compressed file path + compressed_file="data/.lfs/${dir_name}.tar.gz" + + # Check if compressed file already exists + if [ -f "$compressed_file" ]; then + continue + fi + + new_data+=("$dir_name") +done + +if [ ${#new_data[@]} -gt 0 ]; then + echo -e "${RED}✗${NC} New test data detected at /data:" + echo -e " ${GREEN}${new_data[@]}${NC}" + echo -e "\nEither delete or run ${GREEN}./bin/lfs_push${NC}" + echo -e "(lfs_push will compress the files into /data/.lfs/, upload to LFS, and add them to your commit)" + exit 1 +fi diff --git a/bin/lfs_push b/bin/lfs_push new file mode 100755 index 0000000000..68b1326e49 --- /dev/null +++ b/bin/lfs_push @@ -0,0 +1,98 @@ +#!/bin/bash +# Compresses directories in data/* into data/.lfs/dirname.tar.gz +# Pushes to LFS + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +#echo -e "${GREEN}Running test data compression check...${NC}" + +ROOT=$(git rev-parse --show-toplevel) +cd $ROOT + +# Check if data/ exists +if [ ! -d "data/" ]; then + echo -e "${YELLOW}No data directory found, skipping compression.${NC}" + exit 0 +fi + +# Track if any compression was performed +compressed_dirs=() + +# Iterate through all directories in data/ +for dir_path in data/*; do + # Skip if no directories found (glob didn't match) + [ ! "$dir_path" ] && continue + + # Extract directory name + dir_name=$(basename "$dir_path") + + # Skip .lfs directory if it exists + [ "$dir_name" = ".lfs" ] && continue + + # Define compressed file path + compressed_file="data/.lfs/${dir_name}.tar.gz" + + # Check if compressed file already exists + if [ -f "$compressed_file" ]; then + continue + fi + + echo -e " ${YELLOW}Compressing${NC} $dir_path -> $compressed_file" + + # Show directory size before compression + dir_size=$(du -sh "$dir_path" | cut -f1) + echo -e " Data size: ${YELLOW}$dir_size${NC}" + + # Create compressed archive with progress bar + # Use tar with gzip compression, excluding hidden files and common temp files + tar -czf "$compressed_file" \ + --exclude='*.tmp' \ + --exclude='*.temp' \ + --exclude='.DS_Store' \ + --exclude='Thumbs.db' \ + --checkpoint=1000 \ + --checkpoint-action=dot \ + -C "data/" \ + "$dir_name" + + if [ $? -eq 0 ]; then + # Show compressed file size + compressed_size=$(du -sh "$compressed_file" | cut -f1) + echo -e " ${GREEN}✓${NC} Successfully compressed $dir_name (${GREEN}$dir_size${NC} → ${GREEN}$compressed_size${NC})" + compressed_dirs+=("$dir_name") + + # Add the compressed file to git LFS tracking + git add -f "$compressed_file" + + echo -e " ${GREEN}✓${NC} git-add $compressed_file" + + else + echo -e " ${RED}✗${NC} Failed to compress $dir_name" + exit 1 + fi +done + +if [ ${#compressed_dirs[@]} -gt 0 ]; then + # Create commit message with compressed directory names + if [ ${#compressed_dirs[@]} -eq 1 ]; then + commit_msg="Auto-compress test data: ${compressed_dirs[0]}" + else + # Join array elements with commas + dirs_list=$(IFS=', '; echo "${compressed_dirs[*]}") + commit_msg="Auto-compress test data: ${dirs_list}" + fi + + #git commit -m "$commit_msg" + echo -e "${GREEN}✓${NC} Compressed file references added. Uploading..." + git lfs push origin $(git branch --show-current) + echo -e "${GREEN}✓${NC} Uploaded to LFS" +else + echo -e "${GREEN}✓${NC} No test data to compress" +fi + diff --git a/bin/mypy-strict b/bin/mypy-strict new file mode 100755 index 0000000000..05001bf100 --- /dev/null +++ b/bin/mypy-strict @@ -0,0 +1,98 @@ +#!/bin/bash +# +# Run mypy with strict settings on the dimos codebase. +# +# Usage: +# ./bin/mypy-strict # Run mypy and show all errors +# ./bin/mypy-strict --user me # Filter for your git user.email +# ./bin/mypy-strict --after cutoff # Filter for lines committed on or after 2025-10-08 +# ./bin/mypy-strict --after 2025-11-11 # Filter for lines committed on or after specific date +# ./bin/mypy-strict --user me --after cutoff # Chain filters +# + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +cd "$ROOT" + +. .venv/bin/activate + +run_mypy() { + export MYPYPATH=/opt/ros/jazzy/lib/python3.12/site-packages + + mypy_args=( + --config-file mypy_strict.ini + --show-error-codes + --hide-error-context + --no-pretty + dimos + ) + mypy "${mypy_args[@]}" +} + +main() { + local user_email="none" + local after_date="" + + # Parse arguments + while [[ $# -gt 0 ]]; do + case "$1" in + --user) + if [[ $# -lt 2 ]]; then + echo "Error: --user requires an argument" >&2 + exit 1 + fi + case "$2" in + me) + user_email="$(git config user.email || echo none)" + ;; + all) + user_email="none" + ;; + *) + user_email="$2" + ;; + esac + shift 2 + ;; + --after) + if [[ $# -lt 2 ]]; then + echo "Error: --after requires an argument" >&2 + exit 1 + fi + case "$2" in + cutoff) + after_date="2025-10-10" + ;; + start) + after_date="" + ;; + *) + after_date="$2" + ;; + esac + shift 2 + ;; + *) + echo "Error: Unknown argument '$1'" >&2 + exit 1 + ;; + esac + done + + # Build filter pipeline + local pipeline="run_mypy" + + if [[ -n "$after_date" ]]; then + pipeline="$pipeline | ./bin/filter-errors-after-date '$after_date'" + fi + + if [[ "$user_email" != "none" ]]; then + pipeline="$pipeline | ./bin/filter-errors-for-user '$user_email'" + fi + + eval "$pipeline" +} + +main "$@" diff --git a/bin/robot-debugger b/bin/robot-debugger new file mode 100755 index 0000000000..d9bef015e7 --- /dev/null +++ b/bin/robot-debugger @@ -0,0 +1,36 @@ +#!/bin/bash + +# Control the robot with a python shell (for debugging). +# +# You have to start the robot run file with: +# +# ROBOT_DEBUGGER=true python +# +# And now start this script +# +# $ ./bin/robot-debugger +# >>> robot.explore() +# True +# >>> + + +exec python -i <(cat < 0: + print("\nConnected.") + break + except ConnectionRefusedError: + print("Not started yet. Trying again...") + time.sleep(2) +else: + print("Failed to connect. Is it started?") + exit(1) + +robot = c.root.robot() +EOF +) diff --git a/data/.lfs/ab_lidar_frames.tar.gz b/data/.lfs/ab_lidar_frames.tar.gz new file mode 100644 index 0000000000..38c61cd506 --- /dev/null +++ b/data/.lfs/ab_lidar_frames.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab4efaf5d7d4303424868fecaf10083378007adf20244fd17ed934e37f2996da +size 116271 diff --git a/data/.lfs/assets.tar.gz b/data/.lfs/assets.tar.gz new file mode 100644 index 0000000000..b7a2fcbd1c --- /dev/null +++ b/data/.lfs/assets.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b14b01f5c907f117331213abfce9ef5d0c41d0524e14327b5cc706520fb2035 +size 2306191 diff --git a/data/.lfs/cafe-smol.jpg.tar.gz b/data/.lfs/cafe-smol.jpg.tar.gz new file mode 100644 index 0000000000..a05beb4900 --- /dev/null +++ b/data/.lfs/cafe-smol.jpg.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd0c1e5aa5e8ec856cb471c5ed256c2d3a5633ed9a1e052291680eb86bf89a5e +size 8298 diff --git a/data/.lfs/cafe.jpg.tar.gz b/data/.lfs/cafe.jpg.tar.gz new file mode 100644 index 0000000000..dbb2d970a1 --- /dev/null +++ b/data/.lfs/cafe.jpg.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8cf30439b41033ccb04b09b9fc8388d18fb544d55b85c155dbf85700b9e7603 +size 136165 diff --git a/data/.lfs/chair-image.png.tar.gz b/data/.lfs/chair-image.png.tar.gz new file mode 100644 index 0000000000..1a2aab4cf5 --- /dev/null +++ b/data/.lfs/chair-image.png.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f3478f472b5750f118cf7225c2028beeaae41f1b4b726c697ac8c9b004eccbf +size 48504 diff --git a/data/.lfs/g1_zed.tar.gz b/data/.lfs/g1_zed.tar.gz new file mode 100644 index 0000000000..4029f48204 --- /dev/null +++ b/data/.lfs/g1_zed.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:955094035b3ac1edbc257ca1d24fa131f79ac6f502c8b35cc50329025c421dbe +size 1029559759 diff --git a/data/.lfs/lcm_msgs.tar.gz b/data/.lfs/lcm_msgs.tar.gz new file mode 100644 index 0000000000..2b2f28c252 --- /dev/null +++ b/data/.lfs/lcm_msgs.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:245395d0c3e200fcfcea8de5de217f645362b145b200c81abc3862e0afc1aa7e +size 327201 diff --git a/data/.lfs/models_clip.tar.gz b/data/.lfs/models_clip.tar.gz new file mode 100644 index 0000000000..a4ab2b5f88 --- /dev/null +++ b/data/.lfs/models_clip.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:102f11bb0aa952b3cebc4491c5ed3f2122e8c38c76002e22400da4f1e5ca90c5 +size 392327708 diff --git a/data/.lfs/models_contact_graspnet.tar.gz b/data/.lfs/models_contact_graspnet.tar.gz new file mode 100644 index 0000000000..73dd44d033 --- /dev/null +++ b/data/.lfs/models_contact_graspnet.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:431c4611a9e096fd8b0a83fecda39c5a575e72fa933f7bd29ff8cfad5bbb5f9d +size 52149165 diff --git a/data/.lfs/models_fastsam.tar.gz b/data/.lfs/models_fastsam.tar.gz new file mode 100644 index 0000000000..77278f4323 --- /dev/null +++ b/data/.lfs/models_fastsam.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:682cb3816451bd73722cc430fdfce15bbe72a07e50ef2ea81ddaed61d1f22a25 +size 39971209 diff --git a/data/.lfs/models_mobileclip.tar.gz b/data/.lfs/models_mobileclip.tar.gz new file mode 100644 index 0000000000..874c94de07 --- /dev/null +++ b/data/.lfs/models_mobileclip.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f8022e365d9e456dcbd3913d36bf8c68a4cd086eb777c92a773c8192cd8235d +size 277814612 diff --git a/data/.lfs/models_yolo.tar.gz b/data/.lfs/models_yolo.tar.gz new file mode 100644 index 0000000000..650d4617ca --- /dev/null +++ b/data/.lfs/models_yolo.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01796d5884cf29258820cf0e617bf834e9ffb63d8a4c7a54eea802e96fe6a818 +size 72476992 diff --git a/data/.lfs/mujoco_sim.tar.gz b/data/.lfs/mujoco_sim.tar.gz new file mode 100644 index 0000000000..6bfc95c831 --- /dev/null +++ b/data/.lfs/mujoco_sim.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3d607ce57127a6ac558f81ebb9c98bd23a71a86f9ffd5700b3389bf1a19ddf2 +size 59341859 diff --git a/data/.lfs/office_lidar.tar.gz b/data/.lfs/office_lidar.tar.gz new file mode 100644 index 0000000000..849e9e3d49 --- /dev/null +++ b/data/.lfs/office_lidar.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4958965334660c4765553afa38081f00a769c8adf81e599e63fabc866c490fd +size 28576272 diff --git a/data/.lfs/osm_map_test.tar.gz b/data/.lfs/osm_map_test.tar.gz new file mode 100644 index 0000000000..b29104ea17 --- /dev/null +++ b/data/.lfs/osm_map_test.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25097f1bffebd2651f1f4ba93cb749998a064adfdc0cb004981b2317f649c990 +size 1062262 diff --git a/data/.lfs/raw_odometry_rotate_walk.tar.gz b/data/.lfs/raw_odometry_rotate_walk.tar.gz new file mode 100644 index 0000000000..ce8bb1d2b0 --- /dev/null +++ b/data/.lfs/raw_odometry_rotate_walk.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:396345f0cd7a94bb9d85540d4bbce01b027618972f83e713e4550abf1d6ec445 +size 15685 diff --git a/data/.lfs/replay_g1.tar.gz b/data/.lfs/replay_g1.tar.gz new file mode 100644 index 0000000000..67750bd0cf --- /dev/null +++ b/data/.lfs/replay_g1.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19ad1c53c4f8f9414c0921b94cd4c87e81bf0ad676881339f15ae2d8a8619311 +size 557410250 diff --git a/data/.lfs/replay_g1_run.tar.gz b/data/.lfs/replay_g1_run.tar.gz new file mode 100644 index 0000000000..86368ec788 --- /dev/null +++ b/data/.lfs/replay_g1_run.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00cf21f65a15994895150f74044f5d00d7aa873d24f071d249ecbd09cb8f2b26 +size 559554274 diff --git a/data/.lfs/rgbd_frames.tar.gz b/data/.lfs/rgbd_frames.tar.gz new file mode 100644 index 0000000000..8081c76961 --- /dev/null +++ b/data/.lfs/rgbd_frames.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:381b9fd296a885f5211a668df16c68581d2aee458c8734c3256a7461f0decccd +size 948391033 diff --git a/data/.lfs/unitree_go2_lidar_corrected.tar.gz b/data/.lfs/unitree_go2_lidar_corrected.tar.gz new file mode 100644 index 0000000000..013f6b3fe1 --- /dev/null +++ b/data/.lfs/unitree_go2_lidar_corrected.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51a817f2b5664c9e2f2856293db242e030f0edce276e21da0edc2821d947aad2 +size 1212727745 diff --git a/data/.lfs/unitree_go2_office_walk2.tar.gz b/data/.lfs/unitree_go2_office_walk2.tar.gz new file mode 100644 index 0000000000..ea392c4b4c --- /dev/null +++ b/data/.lfs/unitree_go2_office_walk2.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d208cdf537ad01eed2068a4665e454ed30b30894bd9b35c14b4056712faeef5d +size 1693876005 diff --git a/data/.lfs/unitree_office_walk.tar.gz b/data/.lfs/unitree_office_walk.tar.gz new file mode 100644 index 0000000000..419489dbb1 --- /dev/null +++ b/data/.lfs/unitree_office_walk.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bee487130eb662bca73c7d84f14eaea091bd6d7c3f1bfd5173babf660947bdec +size 553620791 diff --git a/data/.lfs/unitree_raw_webrtc_replay.tar.gz b/data/.lfs/unitree_raw_webrtc_replay.tar.gz new file mode 100644 index 0000000000..d41ff5c48f --- /dev/null +++ b/data/.lfs/unitree_raw_webrtc_replay.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a02c622cfee712002afc097825ab5e963071471c3445a20a004ef3532cf59888 +size 756280504 diff --git a/data/.lfs/video.tar.gz b/data/.lfs/video.tar.gz new file mode 100644 index 0000000000..6c0e01a0bb --- /dev/null +++ b/data/.lfs/video.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:530d2132ef84df228af776bd2a2ef387a31858c63ea21c94fb49c7e579b366c0 +size 4322822 diff --git a/dimOS.egg-info/PKG-INFO b/dimOS.egg-info/PKG-INFO deleted file mode 100644 index 16cffd96ea..0000000000 --- a/dimOS.egg-info/PKG-INFO +++ /dev/null @@ -1,5 +0,0 @@ -Metadata-Version: 2.1 -Name: dimos -Version: 0.0.0 -Summary: Coming soon -Author-email: Stash Pomichter diff --git a/dimOS.egg-info/SOURCES.txt b/dimOS.egg-info/SOURCES.txt deleted file mode 100644 index 2a64a65d11..0000000000 --- a/dimOS.egg-info/SOURCES.txt +++ /dev/null @@ -1,10 +0,0 @@ -pyproject.toml -dimOS.egg-info/PKG-INFO -dimOS.egg-info/SOURCES.txt -dimOS.egg-info/dependency_links.txt -dimOS.egg-info/top_level.txt -dimos/__init__.py -dimos.egg-info/PKG-INFO -dimos.egg-info/SOURCES.txt -dimos.egg-info/dependency_links.txt -dimos.egg-info/top_level.txt \ No newline at end of file diff --git a/dimOS.egg-info/top_level.txt b/dimOS.egg-info/top_level.txt deleted file mode 100644 index 70edfe204b..0000000000 --- a/dimOS.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -dimos diff --git a/dimos/agents/agent.py b/dimos/agents/agent.py index 298af0eac9..62765ef706 100644 --- a/dimos/agents/agent.py +++ b/dimos/agents/agent.py @@ -27,36 +27,35 @@ from __future__ import annotations # Standard library imports -import ast import json import os import threading -import logging -from typing import Any, Dict, List, Tuple, Optional, Union +from typing import TYPE_CHECKING, Any # Third-party imports from dotenv import load_dotenv from openai import NOT_GIVEN, OpenAI from pydantic import BaseModel -import reactivex -from reactivex import Observer, create, Observable, empty, operators as RxOps, throw, just +from reactivex import Observable, Observer, create, empty, just, operators as RxOps from reactivex.disposable import CompositeDisposable, Disposable -from reactivex.scheduler import ThreadPoolScheduler from reactivex.subject import Subject # Local imports -from dimos.agents.memory.base import AbstractAgentSemanticMemory from dimos.agents.memory.chroma_impl import OpenAISemanticMemory from dimos.agents.prompt_builder.impl import PromptBuilder -from dimos.agents.tokenizer.base import AbstractTokenizer from dimos.agents.tokenizer.openai_tokenizer import OpenAITokenizer from dimos.skills.skills import AbstractSkill, SkillLibrary from dimos.stream.frame_processor import FrameProcessor from dimos.stream.stream_merger import create_stream_merger from dimos.stream.video_operators import Operators as MyOps, VideoOperators as MyVidOps -from dimos.types.constants import Colors -from dimos.utils.threadpool import get_scheduler from dimos.utils.logging_config import setup_logger +from dimos.utils.threadpool import get_scheduler + +if TYPE_CHECKING: + from reactivex.scheduler import ThreadPoolScheduler + + from dimos.agents.memory.base import AbstractAgentSemanticMemory + from dimos.agents.tokenizer.base import AbstractTokenizer # Initialize environment variables load_dotenv() @@ -75,11 +74,13 @@ class Agent: """Base agent that manages memory and subscriptions.""" - def __init__(self, - dev_name: str = "NA", - agent_type: str = "Base", - agent_memory: Optional[AbstractAgentSemanticMemory] = None, - pool_scheduler: Optional[ThreadPoolScheduler] = None): + def __init__( + self, + dev_name: str = "NA", + agent_type: str = "Base", + agent_memory: AbstractAgentSemanticMemory | None = None, + pool_scheduler: ThreadPoolScheduler | None = None, + ) -> None: """ Initializes a new instance of the Agent. @@ -96,7 +97,7 @@ def __init__(self, self.disposables = CompositeDisposable() self.pool_scheduler = pool_scheduler if pool_scheduler else get_scheduler() - def dispose_all(self): + def dispose_all(self) -> None: """Disposes of all active subscriptions managed by this agent.""" if self.disposables: self.disposables.dispose() @@ -123,7 +124,7 @@ class LLMAgent(Agent): Subclasses must implement the `_send_query` method, which is responsible for sending the prompt to a specific LLM API. - + Attributes: query (str): The current query text to process. prompt_builder (PromptBuilder): Handles construction of prompts. @@ -137,23 +138,26 @@ class LLMAgent(Agent): frame_processor (FrameProcessor): Processes video frames. output_dir (str): Directory for output files. response_subject (Subject): Subject that emits agent responses. - process_all_inputs (bool): Whether to process every input emission (True) or + process_all_inputs (bool): Whether to process every input emission (True) or skip emissions when the agent is busy processing a previous input (False). """ + logging_file_memory_lock = threading.Lock() - def __init__(self, - dev_name: str = "NA", - agent_type: str = "LLM", - agent_memory: Optional[AbstractAgentSemanticMemory] = None, - pool_scheduler: Optional[ThreadPoolScheduler] = None, - process_all_inputs: bool = False, - system_query: Optional[str] = None, - max_output_tokens_per_request: int = 16384, - max_input_tokens_per_request: int = 128000, - input_query_stream: Optional[Observable] = None, - input_data_stream: Optional[Observable] = None, - input_video_stream: Optional[Observable] = None): + def __init__( + self, + dev_name: str = "NA", + agent_type: str = "LLM", + agent_memory: AbstractAgentSemanticMemory | None = None, + pool_scheduler: ThreadPoolScheduler | None = None, + process_all_inputs: bool = False, + system_query: str | None = None, + max_output_tokens_per_request: int = 16384, + max_input_tokens_per_request: int = 128000, + input_query_stream: Observable | None = None, + input_data_stream: Observable | None = None, + input_video_stream: Observable | None = None, + ) -> None: """ Initializes a new instance of the LLMAgent. @@ -163,73 +167,87 @@ def __init__(self, agent_memory (AbstractAgentSemanticMemory): The memory system for the agent. pool_scheduler (ThreadPoolScheduler): The scheduler to use for thread pool operations. If None, the global scheduler from get_scheduler() will be used. - process_all_inputs (bool): Whether to process every input emission (True) or + process_all_inputs (bool): Whether to process every input emission (True) or skip emissions when the agent is busy processing a previous input (False). """ super().__init__(dev_name, agent_type, agent_memory, pool_scheduler) # These attributes can be configured by a subclass if needed. - self.query: Optional[str] = None - self.prompt_builder: Optional[PromptBuilder] = None - self.system_query: Optional[str] = system_query + self.query: str | None = None + self.prompt_builder: PromptBuilder | None = None + self.system_query: str | None = system_query self.image_detail: str = "low" self.max_input_tokens_per_request: int = max_input_tokens_per_request self.max_output_tokens_per_request: int = max_output_tokens_per_request - self.max_tokens_per_request: int = (self.max_input_tokens_per_request + - self.max_output_tokens_per_request) + self.max_tokens_per_request: int = ( + self.max_input_tokens_per_request + self.max_output_tokens_per_request + ) self.rag_query_n: int = 4 self.rag_similarity_threshold: float = 0.45 - self.frame_processor: Optional[FrameProcessor] = None + self.frame_processor: FrameProcessor | None = None self.output_dir: str = os.path.join(os.getcwd(), "assets", "agent") self.process_all_inputs: bool = process_all_inputs os.makedirs(self.output_dir, exist_ok=True) - + # Subject for emitting responses self.response_subject = Subject() - + # Conversation history for maintaining context between calls self.conversation_history = [] # Initialize input streams self.input_video_stream = input_video_stream - self.input_query_stream = input_query_stream if (input_data_stream is None) else (input_query_stream.pipe( - RxOps.with_latest_from(input_data_stream), - RxOps.map(lambda combined: { - "query": combined[0], - "objects": combined[1] if len(combined) > 1 else "No object data available" - }), - RxOps.map(lambda data: f"{data['query']}\n\nCurrent objects detected:\n{data['objects']}"), - RxOps.do_action(lambda x: print(f"\033[34mEnriched query: {x.split(chr(10))[0]}\033[0m") or - [print(f"\033[34m{line}\033[0m") for line in x.split(chr(10))[1:]]), - )) + self.input_query_stream = ( + input_query_stream + if (input_data_stream is None) + else ( + input_query_stream.pipe( + RxOps.with_latest_from(input_data_stream), + RxOps.map( + lambda combined: { + "query": combined[0], + "objects": combined[1] + if len(combined) > 1 + else "No object data available", + } + ), + RxOps.map( + lambda data: f"{data['query']}\n\nCurrent objects detected:\n{data['objects']}" + ), + RxOps.do_action( + lambda x: print(f"\033[34mEnriched query: {x.split(chr(10))[0]}\033[0m") + or [print(f"\033[34m{line}\033[0m") for line in x.split(chr(10))[1:]] + ), + ) + ) + ) # Setup stream subscriptions based on inputs provided if (self.input_video_stream is not None) and (self.input_query_stream is not None): self.merged_stream = create_stream_merger( - data_input_stream=self.input_video_stream, - text_query_stream=self.input_query_stream + data_input_stream=self.input_video_stream, text_query_stream=self.input_query_stream ) - + logger.info("Subscribing to merged input stream...") + # Define a query extractor for the merged stream - query_extractor = lambda emission: (emission[0], emission[1][0]) + def query_extractor(emission): + return (emission[0], emission[1][0]) + self.disposables.add( self.subscribe_to_image_processing( - self.merged_stream, - query_extractor=query_extractor + self.merged_stream, query_extractor=query_extractor ) ) else: # If no merged stream, fall back to individual streams if self.input_video_stream is not None: logger.info("Subscribing to input video stream...") - self.disposables.add( - self.subscribe_to_image_processing(self.input_video_stream)) + self.disposables.add(self.subscribe_to_image_processing(self.input_video_stream)) if self.input_query_stream is not None: logger.info("Subscribing to input query stream...") - self.disposables.add( - self.subscribe_to_query_processing(self.input_query_stream)) + self.disposables.add(self.subscribe_to_query_processing(self.input_query_stream)) - def _update_query(self, incoming_query: Optional[str]) -> None: + def _update_query(self, incoming_query: str | None) -> None: """Updates the query if an incoming query is provided. Args: @@ -238,7 +256,7 @@ def _update_query(self, incoming_query: Optional[str]) -> None: if incoming_query is not None: self.query = incoming_query - def _get_rag_context(self) -> Tuple[str, str]: + def _get_rag_context(self) -> tuple[str, str]: """Queries the agent memory to retrieve RAG context. Returns: @@ -248,20 +266,24 @@ def _get_rag_context(self) -> Tuple[str, str]: results = self.agent_memory.query( query_texts=self.query, n_results=self.rag_query_n, - similarity_threshold=self.rag_similarity_threshold) + similarity_threshold=self.rag_similarity_threshold, + ) formatted_results = "\n".join( f"Document ID: {doc.id}\nMetadata: {doc.metadata}\nContent: {doc.page_content}\nScore: {score}\n" - for (doc, score) in results) - condensed_results = " | ".join( - f"{doc.page_content}" for (doc, _) in results) + for (doc, score) in results + ) + condensed_results = " | ".join(f"{doc.page_content}" for (doc, _) in results) logger.info(f"Agent Memory Query Results:\n{formatted_results}") logger.info("=== Results End ===") return formatted_results, condensed_results - def _build_prompt(self, base64_image: Optional[str], - dimensions: Optional[Tuple[int, int]], - override_token_limit: bool, - condensed_results: str) -> list: + def _build_prompt( + self, + base64_image: str | None, + dimensions: tuple[int, int] | None, + override_token_limit: bool, + condensed_results: str, + ) -> list: """Builds a prompt message using the prompt builder. Args: @@ -275,14 +297,10 @@ def _build_prompt(self, base64_image: Optional[str], """ # Budget for each component of the prompt budgets = { - "system_prompt": - self.max_input_tokens_per_request // _TOKEN_BUDGET_PARTS, - "user_query": - self.max_input_tokens_per_request // _TOKEN_BUDGET_PARTS, - "image": - self.max_input_tokens_per_request // _TOKEN_BUDGET_PARTS, - "rag": - self.max_input_tokens_per_request // _TOKEN_BUDGET_PARTS, + "system_prompt": self.max_input_tokens_per_request // _TOKEN_BUDGET_PARTS, + "user_query": self.max_input_tokens_per_request // _TOKEN_BUDGET_PARTS, + "image": self.max_input_tokens_per_request // _TOKEN_BUDGET_PARTS, + "rag": self.max_input_tokens_per_request // _TOKEN_BUDGET_PARTS, } # Define truncation policies for each component @@ -322,8 +340,7 @@ def _handle_tooling(self, response_message, messages): # TODO: Make this more generic or move implementation to OpenAIAgent. # This is presently OpenAI-specific. - def _tooling_callback(message, messages, response_message, - skill_library: SkillLibrary): + def _tooling_callback(message, messages, response_message, skill_library: SkillLibrary): has_called_tools = False new_messages = [] for tool_call in message.tool_calls: @@ -332,12 +349,14 @@ def _tooling_callback(message, messages, response_message, args = json.loads(tool_call.function.arguments) result = skill_library.call(name, **args) logger.info(f"Function Call Results: {result}") - new_messages.append({ - "role": "tool", - "tool_call_id": tool_call.id, - "content": str(result), - "name": name - }) + new_messages.append( + { + "role": "tool", + "tool_call_id": tool_call.id, + "content": str(result), + "name": name, + } + ) if has_called_tools: logger.info("Sending Another Query.") messages.append(response_message) @@ -349,16 +368,19 @@ def _tooling_callback(message, messages, response_message, return None if response_message.tool_calls is not None: - return _tooling_callback(response_message, messages, - response_message, self.skill_library) + return _tooling_callback( + response_message, messages, response_message, self.skill_library + ) return None - def _observable_query(self, - observer: Observer, - base64_image: Optional[str] = None, - dimensions: Optional[Tuple[int, int]] = None, - override_token_limit: bool = False, - incoming_query: Optional[str] = None): + def _observable_query( + self, + observer: Observer, + base64_image: str | None = None, + dimensions: tuple[int, int] | None = None, + override_token_limit: bool = False, + incoming_query: str | None = None, + ): """Prepares and sends a query to the LLM, emitting the response to the observer. Args: @@ -374,9 +396,9 @@ def _observable_query(self, try: self._update_query(incoming_query) _, condensed_results = self._get_rag_context() - messages = self._build_prompt(base64_image, dimensions, - override_token_limit, - condensed_results) + messages = self._build_prompt( + base64_image, dimensions, override_token_limit, condensed_results + ) # logger.debug(f"Sending Query: {messages}") logger.info("Sending Query.") response_message = self._send_query(messages) @@ -386,20 +408,28 @@ def _observable_query(self, # TODO: Make this more generic. The parsed tag and tooling handling may be OpenAI-specific. # If no skill library is provided or there are no tool calls, emit the response directly. - if (self.skill_library is None or - self.skill_library.get_tools() in (None, NOT_GIVEN) or - response_message.tool_calls is None): - final_msg = (response_message.parsed - if hasattr(response_message, 'parsed') and - response_message.parsed else - (response_message.content if hasattr(response_message, 'content') else response_message)) + if ( + self.skill_library is None + or self.skill_library.get_tools() in (None, NOT_GIVEN) + or response_message.tool_calls is None + ): + final_msg = ( + response_message.parsed + if hasattr(response_message, "parsed") and response_message.parsed + else ( + response_message.content + if hasattr(response_message, "content") + else response_message + ) + ) observer.on_next(final_msg) self.response_subject.on_next(final_msg) else: - response_message_2 = self._handle_tooling( - response_message, messages) - final_msg = response_message_2 if response_message_2 is not None else response_message - if isinstance(final_msg, BaseModel): # TODO: Test + response_message_2 = self._handle_tooling(response_message, messages) + final_msg = ( + response_message_2 if response_message_2 is not None else response_message + ) + if isinstance(final_msg, BaseModel): # TODO: Test final_msg = str(final_msg.content) observer.on_next(final_msg) self.response_subject.on_next(final_msg) @@ -423,10 +453,9 @@ def _send_query(self, messages: list) -> Any: Raises: NotImplementedError: Always, unless overridden. """ - raise NotImplementedError( - "Subclasses must implement _send_query method.") + raise NotImplementedError("Subclasses must implement _send_query method.") - def _log_response_to_file(self, response, output_dir: str = None): + def _log_response_to_file(self, response, output_dir: str | None = None) -> None: """Logs the LLM response to a file. Args: @@ -437,15 +466,14 @@ def _log_response_to_file(self, response, output_dir: str = None): output_dir = self.output_dir if response is not None: with self.logging_file_memory_lock: - log_path = os.path.join(output_dir, 'memory.txt') - with open(log_path, 'a') as file: + log_path = os.path.join(output_dir, "memory.txt") + with open(log_path, "a") as file: file.write(f"{self.dev_name}: {response}\n") logger.info(f"LLM Response [{self.dev_name}]: {response}") def subscribe_to_image_processing( - self, - frame_observable: Observable, - query_extractor=None) -> Disposable: + self, frame_observable: Observable, query_extractor=None + ) -> Disposable: """Subscribes to a stream of video frames for processing. This method sets up a subscription to process incoming video frames. @@ -466,11 +494,7 @@ def subscribe_to_image_processing( if self.frame_processor is None: self.frame_processor = FrameProcessor(delete_on_init=True) - print_emission_args = { - "enabled": True, - "dev_name": self.dev_name, - "counts": {} - } + print_emission_args = {"enabled": True, "dev_name": self.dev_name, "counts": {}} def _process_frame(emission) -> Observable: """ @@ -483,28 +507,36 @@ def _process_frame(emission) -> Observable: query = self.system_query frame = emission return just(frame).pipe( - MyOps.print_emission(id='B', **print_emission_args), + MyOps.print_emission(id="B", **print_emission_args), RxOps.observe_on(self.pool_scheduler), - MyOps.print_emission(id='C', **print_emission_args), + MyOps.print_emission(id="C", **print_emission_args), RxOps.subscribe_on(self.pool_scheduler), - MyOps.print_emission(id='D', **print_emission_args), - MyVidOps.with_jpeg_export(self.frame_processor, - suffix=f"{self.dev_name}_frame_", - save_limit=_MAX_SAVED_FRAMES), - MyOps.print_emission(id='E', **print_emission_args), + MyOps.print_emission(id="D", **print_emission_args), + MyVidOps.with_jpeg_export( + self.frame_processor, + suffix=f"{self.dev_name}_frame_", + save_limit=_MAX_SAVED_FRAMES, + ), + MyOps.print_emission(id="E", **print_emission_args), MyVidOps.encode_image(), - MyOps.print_emission(id='F', **print_emission_args), - RxOps.filter(lambda base64_and_dims: base64_and_dims is not None - and base64_and_dims[0] is not None and - base64_and_dims[1] is not None), - MyOps.print_emission(id='G', **print_emission_args), - RxOps.flat_map(lambda base64_and_dims: create( - lambda observer, _: self._observable_query( - observer, - base64_image=base64_and_dims[0], - dimensions=base64_and_dims[1], - incoming_query=query))), # Use the extracted query - MyOps.print_emission(id='H', **print_emission_args), + MyOps.print_emission(id="F", **print_emission_args), + RxOps.filter( + lambda base64_and_dims: base64_and_dims is not None + and base64_and_dims[0] is not None + and base64_and_dims[1] is not None + ), + MyOps.print_emission(id="G", **print_emission_args), + RxOps.flat_map( + lambda base64_and_dims: create( + lambda observer, _: self._observable_query( + observer, + base64_image=base64_and_dims[0], + dimensions=base64_and_dims[1], + incoming_query=query, + ) + ) + ), # Use the extracted query + MyOps.print_emission(id="H", **print_emission_args), ) # Use a mutable flag to ensure only one frame is processed at a time. @@ -517,35 +549,33 @@ def process_if_free(emission): else: is_processing[0] = True return _process_frame(emission).pipe( - MyOps.print_emission(id='I', **print_emission_args), + MyOps.print_emission(id="I", **print_emission_args), RxOps.observe_on(self.pool_scheduler), - MyOps.print_emission(id='J', **print_emission_args), + MyOps.print_emission(id="J", **print_emission_args), RxOps.subscribe_on(self.pool_scheduler), - MyOps.print_emission(id='K', **print_emission_args), + MyOps.print_emission(id="K", **print_emission_args), RxOps.do_action( - on_completed=lambda: is_processing.__setitem__( - 0, False), - on_error=lambda e: is_processing.__setitem__(0, False)), - MyOps.print_emission(id='L', **print_emission_args), + on_completed=lambda: is_processing.__setitem__(0, False), + on_error=lambda e: is_processing.__setitem__(0, False), + ), + MyOps.print_emission(id="L", **print_emission_args), ) observable = frame_observable.pipe( - MyOps.print_emission(id='A', **print_emission_args), + MyOps.print_emission(id="A", **print_emission_args), RxOps.flat_map(process_if_free), - MyOps.print_emission(id='M', **print_emission_args), + MyOps.print_emission(id="M", **print_emission_args), ) disposable = observable.subscribe( - on_next=lambda response: self._log_response_to_file( - response, self.output_dir), + on_next=lambda response: self._log_response_to_file(response, self.output_dir), on_error=lambda e: logger.error(f"Error encountered: {e}"), - on_completed=lambda: logger.info( - f"Stream processing completed for {self.dev_name}")) + on_completed=lambda: logger.info(f"Stream processing completed for {self.dev_name}"), + ) self.disposables.add(disposable) return disposable - def subscribe_to_query_processing( - self, query_observable: Observable) -> Disposable: + def subscribe_to_query_processing(self, query_observable: Observable) -> Disposable: """Subscribes to a stream of queries for processing. This method sets up a subscription to process incoming queries by directly @@ -557,11 +587,7 @@ def subscribe_to_query_processing( Returns: Disposable: A disposable representing the subscription. """ - print_emission_args = { - "enabled": True, - "dev_name": self.dev_name, - "counts": {} - } + print_emission_args = {"enabled": False, "dev_name": self.dev_name, "counts": {}} def _process_query(query) -> Observable: """ @@ -569,11 +595,13 @@ def _process_query(query) -> Observable: Returns an observable that emits the LLM response. """ return just(query).pipe( - MyOps.print_emission(id='Pr A', **print_emission_args), - RxOps.flat_map(lambda query: create( - lambda observer, _: self._observable_query( - observer, incoming_query=query))), - MyOps.print_emission(id='Pr B', **print_emission_args), + MyOps.print_emission(id="Pr A", **print_emission_args), + RxOps.flat_map( + lambda query: create( + lambda observer, _: self._observable_query(observer, incoming_query=query) + ) + ), + MyOps.print_emission(id="Pr B", **print_emission_args), ) # A mutable flag indicating whether a query is currently being processed. @@ -588,64 +616,67 @@ def process_if_free(query): is_processing[0] = True logger.info("Processing Query.") return _process_query(query).pipe( - MyOps.print_emission(id='B', **print_emission_args), + MyOps.print_emission(id="B", **print_emission_args), RxOps.observe_on(self.pool_scheduler), - MyOps.print_emission(id='C', **print_emission_args), + MyOps.print_emission(id="C", **print_emission_args), RxOps.subscribe_on(self.pool_scheduler), - MyOps.print_emission(id='D', **print_emission_args), + MyOps.print_emission(id="D", **print_emission_args), RxOps.do_action( - on_completed=lambda: is_processing.__setitem__( - 0, False), - on_error=lambda e: is_processing.__setitem__(0, False)), - MyOps.print_emission(id='E', **print_emission_args), + on_completed=lambda: is_processing.__setitem__(0, False), + on_error=lambda e: is_processing.__setitem__(0, False), + ), + MyOps.print_emission(id="E", **print_emission_args), ) observable = query_observable.pipe( - MyOps.print_emission(id='A', **print_emission_args), + MyOps.print_emission(id="A", **print_emission_args), RxOps.flat_map(lambda query: process_if_free(query)), - MyOps.print_emission(id='F', **print_emission_args)) + MyOps.print_emission(id="F", **print_emission_args), + ) disposable = observable.subscribe( - on_next=lambda response: self._log_response_to_file( - response, self.output_dir), - on_error=lambda e: logger.error( - f"Error processing query for {self.dev_name}: {e}"), - on_completed=lambda: logger.info( - f"Stream processing completed for {self.dev_name}")) + on_next=lambda response: self._log_response_to_file(response, self.output_dir), + on_error=lambda e: logger.error(f"Error processing query for {self.dev_name}: {e}"), + on_completed=lambda: logger.info(f"Stream processing completed for {self.dev_name}"), + ) self.disposables.add(disposable) return disposable def get_response_observable(self) -> Observable: """Gets an observable that emits responses from this agent. - + Returns: Observable: An observable that emits string responses from the agent. """ return self.response_subject.pipe( - RxOps.observe_on(self.pool_scheduler), + RxOps.observe_on(self.pool_scheduler), RxOps.subscribe_on(self.pool_scheduler), - RxOps.share()) + RxOps.share(), + ) def run_observable_query(self, query_text: str, **kwargs) -> Observable: """Creates an observable that processes a one-off text query to Agent and emits the response. - + This method provides a simple way to send a text query and get an observable stream of the response. It's designed for one-off queries rather than continuous processing of input streams. Useful for testing and development. - + Args: query_text (str): The query text to process. **kwargs: Additional arguments to pass to _observable_query. Supported args vary by agent type. For example, ClaudeAgent supports: base64_image, dimensions, override_token_limit, reset_conversation, thinking_budget_tokens - + Returns: Observable: An observable that emits the response as a string. """ - return create(lambda observer, _: self._observable_query( - observer, incoming_query=query_text, **kwargs)) + return create( + lambda observer, _: self._observable_query( + observer, incoming_query=query_text, **kwargs + ) + ) - def dispose_all(self): + def dispose_all(self) -> None: """Disposes of all active subscriptions managed by this agent.""" super().dispose_all() self.response_subject.on_completed() @@ -665,31 +696,32 @@ class OpenAIAgent(LLMAgent): tokenizer, and response model. """ - def __init__(self, - dev_name: str, - agent_type: str = "Vision", - query: str = "What do you see?", - input_query_stream: Optional[Observable] = None, - input_data_stream: Optional[Observable] = None, - input_video_stream: Optional[Observable] = None, - output_dir: str = os.path.join(os.getcwd(), "assets", - "agent"), - agent_memory: Optional[AbstractAgentSemanticMemory] = None, - system_query: Optional[str] = None, - max_input_tokens_per_request: int = 128000, - max_output_tokens_per_request: int = 16384, - model_name: str = "gpt-4o", - prompt_builder: Optional[PromptBuilder] = None, - tokenizer: Optional[AbstractTokenizer] = None, - rag_query_n: int = 4, - rag_similarity_threshold: float = 0.45, - skills: Optional[Union[AbstractSkill, list[AbstractSkill], SkillLibrary]] = None, - response_model: Optional[BaseModel] = None, - frame_processor: Optional[FrameProcessor] = None, - image_detail: str = "low", - pool_scheduler: Optional[ThreadPoolScheduler] = None, - process_all_inputs: Optional[bool] = None, - openai_client: Optional[OpenAI] = None): + def __init__( + self, + dev_name: str, + agent_type: str = "Vision", + query: str = "What do you see?", + input_query_stream: Observable | None = None, + input_data_stream: Observable | None = None, + input_video_stream: Observable | None = None, + output_dir: str = os.path.join(os.getcwd(), "assets", "agent"), + agent_memory: AbstractAgentSemanticMemory | None = None, + system_query: str | None = None, + max_input_tokens_per_request: int = 128000, + max_output_tokens_per_request: int = 16384, + model_name: str = "gpt-4o", + prompt_builder: PromptBuilder | None = None, + tokenizer: AbstractTokenizer | None = None, + rag_query_n: int = 4, + rag_similarity_threshold: float = 0.45, + skills: AbstractSkill | list[AbstractSkill] | SkillLibrary | None = None, + response_model: BaseModel | None = None, + frame_processor: FrameProcessor | None = None, + image_detail: str = "low", + pool_scheduler: ThreadPoolScheduler | None = None, + process_all_inputs: bool | None = None, + openai_client: OpenAI | None = None, + ) -> None: """ Initializes a new instance of the OpenAIAgent. @@ -727,7 +759,7 @@ def __init__(self, process_all_inputs = True else: process_all_inputs = False - + super().__init__( dev_name=dev_name, agent_type=agent_type, @@ -737,7 +769,7 @@ def __init__(self, system_query=system_query, input_query_stream=input_query_stream, input_data_stream=input_data_stream, - input_video_stream=input_video_stream + input_video_stream=input_video_stream, ) self.client = openai_client or OpenAI() self.query = query @@ -759,10 +791,10 @@ def __init__(self, self.response_model = response_model if response_model is not None else NOT_GIVEN self.model_name = model_name - self.tokenizer = tokenizer or OpenAITokenizer( - model_name=self.model_name) + self.tokenizer = tokenizer or OpenAITokenizer(model_name=self.model_name) self.prompt_builder = prompt_builder or PromptBuilder( - self.model_name, tokenizer=self.tokenizer) + self.model_name, tokenizer=self.tokenizer + ) self.rag_query_n = rag_query_n self.rag_similarity_threshold = rag_similarity_threshold self.image_detail = image_detail @@ -773,28 +805,30 @@ def __init__(self, # Add static context to memory. self._add_context_to_memory() - self.frame_processor = frame_processor or FrameProcessor( - delete_on_init=True) + self.frame_processor = frame_processor or FrameProcessor(delete_on_init=True) logger.info("OpenAI Agent Initialized.") - def _add_context_to_memory(self): + def _add_context_to_memory(self) -> None: """Adds initial context to the agent's memory.""" context_data = [ - ("id0", - "Optical Flow is a technique used to track the movement of objects in a video sequence." - ), - ("id1", - "Edge Detection is a technique used to identify the boundaries of objects in an image." - ), - ("id2", - "Video is a sequence of frames captured at regular intervals."), - ("id3", - "Colors in Optical Flow are determined by the movement of light, and can be used to track the movement of objects." - ), - ("id4", - "Json is a data interchange format that is easy for humans to read and write, and easy for machines to parse and generate." - ), + ( + "id0", + "Optical Flow is a technique used to track the movement of objects in a video sequence.", + ), + ( + "id1", + "Edge Detection is a technique used to identify the boundaries of objects in an image.", + ), + ("id2", "Video is a sequence of frames captured at regular intervals."), + ( + "id3", + "Colors in Optical Flow are determined by the movement of light, and can be used to track the movement of objects.", + ), + ( + "id4", + "Json is a data interchange format that is easy for humans to read and write, and easy for machines to parse and generate.", + ), ] for doc_id, text in context_data: self.agent_memory.add_vector(doc_id, text) @@ -822,7 +856,11 @@ def _send_query(self, messages: list) -> Any: model=self.model_name, messages=messages, response_format=self.response_model, - tools=(self.skill_library.get_tools() if self.skill_library is not None else NOT_GIVEN), + tools=( + self.skill_library.get_tools() + if self.skill_library is not None + else NOT_GIVEN + ), max_tokens=self.max_output_tokens_per_request, ) else: @@ -830,8 +868,11 @@ def _send_query(self, messages: list) -> Any: model=self.model_name, messages=messages, max_tokens=self.max_output_tokens_per_request, - tools=(self.skill_library.get_tools() - if self.skill_library is not None else NOT_GIVEN), + tools=( + self.skill_library.get_tools() + if self.skill_library is not None + else NOT_GIVEN + ), ) response_message = response.choices[0].message if response_message is None: @@ -850,19 +891,20 @@ def _send_query(self, messages: list) -> Any: def stream_query(self, query_text: str) -> Observable: """Creates an observable that processes a text query and emits the response. - + This method provides a simple way to send a text query and get an observable stream of the response. It's designed for one-off queries rather than continuous processing of input streams. - + Args: query_text (str): The query text to process. - + Returns: Observable: An observable that emits the response as a string. """ - return create(lambda observer, _: self._observable_query( - observer, incoming_query=query_text)) + return create( + lambda observer, _: self._observable_query(observer, incoming_query=query_text) + ) -# endregion OpenAIAgent Subclass (OpenAI-Specific Implementation) \ No newline at end of file +# endregion OpenAIAgent Subclass (OpenAI-Specific Implementation) diff --git a/dimos/agents/agent_config.py b/dimos/agents/agent_config.py index de31644597..5b9027b072 100644 --- a/dimos/agents/agent_config.py +++ b/dimos/agents/agent_config.py @@ -12,11 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import List + from dimos.agents.agent import Agent + class AgentConfig: - def __init__(self, agents: List[Agent] = None): + def __init__(self, agents: list[Agent] | None = None) -> None: """ Initialize an AgentConfig with a list of agents. @@ -25,7 +26,7 @@ def __init__(self, agents: List[Agent] = None): """ self.agents = agents if agents is not None else [] - def add_agent(self, agent: Agent): + def add_agent(self, agent: Agent) -> None: """ Add an agent to the configuration. @@ -34,7 +35,7 @@ def add_agent(self, agent: Agent): """ self.agents.append(agent) - def remove_agent(self, agent: Agent): + def remove_agent(self, agent: Agent) -> None: """ Remove an agent from the configuration. @@ -44,7 +45,7 @@ def remove_agent(self, agent: Agent): if agent in self.agents: self.agents.remove(agent) - def get_agents(self) -> List[Agent]: + def get_agents(self) -> list[Agent]: """ Get the list of configured agents. diff --git a/dimos/agents/agent_ctransformers_gguf.py b/dimos/agents/agent_ctransformers_gguf.py index 14d4649832..17d233437d 100644 --- a/dimos/agents/agent_ctransformers_gguf.py +++ b/dimos/agents/agent_ctransformers_gguf.py @@ -15,25 +15,18 @@ from __future__ import annotations # Standard library imports -import json import logging import os -from typing import Any, Optional +from typing import TYPE_CHECKING, Any # Third-party imports from dotenv import load_dotenv from reactivex import Observable, create -from reactivex.scheduler import ThreadPoolScheduler -from reactivex.subject import Subject import torch -from transformers import AutoModelForCausalLM, AutoTokenizer # Local imports from dimos.agents.agent import LLMAgent -from dimos.agents.memory.base import AbstractAgentSemanticMemory from dimos.agents.prompt_builder.impl import PromptBuilder -from dimos.agents.tokenizer.base import AbstractTokenizer -from dimos.agents.tokenizer.huggingface_tokenizer import HuggingFaceTokenizer from dimos.utils.logging_config import setup_logger # Initialize environment variables @@ -44,29 +37,38 @@ from ctransformers import AutoModelForCausalLM as CTransformersModel +if TYPE_CHECKING: + from reactivex.scheduler import ThreadPoolScheduler + from reactivex.subject import Subject + + from dimos.agents.memory.base import AbstractAgentSemanticMemory + + class CTransformersTokenizerAdapter: - def __init__(self, model): + def __init__(self, model) -> None: self.model = model - def encode(self, text, **kwargs): + def encode(self, text: str, **kwargs): return self.model.tokenize(text) def decode(self, token_ids, **kwargs): return self.model.detokenize(token_ids) - def token_count(self, text): + def token_count(self, text: str): return len(self.tokenize_text(text)) if text else 0 - def tokenize_text(self, text): + def tokenize_text(self, text: str): return self.model.tokenize(text) def detokenize_text(self, tokenized_text): try: return self.model.detokenize(tokenized_text) except Exception as e: - raise ValueError(f"Failed to detokenize text. Error: {str(e)}") + raise ValueError(f"Failed to detokenize text. Error: {e!s}") - def apply_chat_template(self, conversation, tokenize=False, add_generation_prompt=True): + def apply_chat_template( + self, conversation, tokenize: bool = False, add_generation_prompt: bool = True + ): prompt = "" for message in conversation: role = message["role"] @@ -84,26 +86,27 @@ def apply_chat_template(self, conversation, tokenize=False, add_generation_promp # CTransformers Agent Class class CTransformersGGUFAgent(LLMAgent): - def __init__(self, - dev_name: str, - agent_type: str = "HF-LLM", - model_name: str = "TheBloke/Llama-2-7B-GGUF", - model_file: str = "llama-2-7b.Q4_K_M.gguf", - model_type: str = "llama", - gpu_layers: int = 50, - device: str = "auto", - query: str = "How many r's are in the word 'strawberry'?", - input_query_stream: Optional[Observable] = None, - input_video_stream: Optional[Observable] = None, - output_dir: str = os.path.join(os.getcwd(), "assets", "agent"), - agent_memory: Optional[AbstractAgentSemanticMemory] = None, - system_query: Optional[str] = "You are a helpful assistant.", - max_output_tokens_per_request: int = 10, - max_input_tokens_per_request: int = 250, - prompt_builder: Optional[PromptBuilder] = None, - pool_scheduler: Optional[ThreadPoolScheduler] = None, - process_all_inputs: Optional[bool] = None,): - + def __init__( + self, + dev_name: str, + agent_type: str = "HF-LLM", + model_name: str = "TheBloke/Llama-2-7B-GGUF", + model_file: str = "llama-2-7b.Q4_K_M.gguf", + model_type: str = "llama", + gpu_layers: int = 50, + device: str = "auto", + query: str = "How many r's are in the word 'strawberry'?", + input_query_stream: Observable | None = None, + input_video_stream: Observable | None = None, + output_dir: str = os.path.join(os.getcwd(), "assets", "agent"), + agent_memory: AbstractAgentSemanticMemory | None = None, + system_query: str | None = "You are a helpful assistant.", + max_output_tokens_per_request: int = 10, + max_input_tokens_per_request: int = 250, + prompt_builder: PromptBuilder | None = None, + pool_scheduler: ThreadPoolScheduler | None = None, + process_all_inputs: bool | None = None, + ) -> None: # Determine appropriate default for process_all_inputs if not provided if process_all_inputs is None: # Default to True for text queries, False for video streams @@ -120,7 +123,7 @@ def __init__(self, process_all_inputs=process_all_inputs, system_query=system_query, max_output_tokens_per_request=max_output_tokens_per_request, - max_input_tokens_per_request=max_input_tokens_per_request + max_input_tokens_per_request=max_input_tokens_per_request, ) self.query = query @@ -138,17 +141,13 @@ def __init__(self, print(f"Device: {self.device}") self.model = CTransformersModel.from_pretrained( - model_name, - model_file=model_file, - model_type=model_type, - gpu_layers=gpu_layers + model_name, model_file=model_file, model_type=model_type, gpu_layers=gpu_layers ) self.tokenizer = CTransformersTokenizerAdapter(self.model) self.prompt_builder = prompt_builder or PromptBuilder( - self.model_name, - tokenizer=self.tokenizer + self.model_name, tokenizer=self.tokenizer ) self.max_output_tokens_per_request = max_output_tokens_per_request @@ -166,13 +165,10 @@ def __init__(self, if self.input_video_stream is not None: logger.info("Subscribing to input video stream...") - self.disposables.add( - self.subscribe_to_image_processing(self.input_video_stream)) + self.disposables.add(self.subscribe_to_image_processing(self.input_video_stream)) if self.input_query_stream is not None: logger.info("Subscribing to input query stream...") - self.disposables.add( - self.subscribe_to_query_processing(self.input_query_stream)) - + self.disposables.add(self.subscribe_to_query_processing(self.input_query_stream)) def _send_query(self, messages: list) -> Any: try: @@ -194,9 +190,7 @@ def _send_query(self, messages: list) -> Any: print("Applying chat template...") prompt_text = self.tokenizer.apply_chat_template( - conversation=flat_messages, - tokenize=False, - add_generation_prompt=True + conversation=flat_messages, tokenize=False, add_generation_prompt=True ) print("Chat template applied.") print(f"Prompt text:\n{prompt_text}") @@ -213,7 +207,9 @@ def stream_query(self, query_text: str) -> Subject: """ Creates an observable that processes a text query and emits the response. """ - return create(lambda observer, _: self._observable_query( - observer, incoming_query=query_text)) + return create( + lambda observer, _: self._observable_query(observer, incoming_query=query_text) + ) + # endregion HuggingFaceLLMAgent Subclass (HuggingFace-Specific Implementation) diff --git a/dimos/agents/agent_huggingface_local.py b/dimos/agents/agent_huggingface_local.py index d209f8d88f..69d02bb1d2 100644 --- a/dimos/agents/agent_huggingface_local.py +++ b/dimos/agents/agent_huggingface_local.py @@ -15,56 +15,59 @@ from __future__ import annotations # Standard library imports -import json import logging import os -from typing import Any, Optional +from typing import TYPE_CHECKING, Any # Third-party imports from dotenv import load_dotenv from reactivex import Observable, create -from reactivex.scheduler import ThreadPoolScheduler -from reactivex.subject import Subject import torch -from transformers import AutoModelForCausalLM, AutoTokenizer +from transformers import AutoModelForCausalLM # Local imports from dimos.agents.agent import LLMAgent -from dimos.agents.memory.base import AbstractAgentSemanticMemory from dimos.agents.memory.chroma_impl import LocalSemanticMemory from dimos.agents.prompt_builder.impl import PromptBuilder -from dimos.agents.tokenizer.base import AbstractTokenizer from dimos.agents.tokenizer.huggingface_tokenizer import HuggingFaceTokenizer from dimos.utils.logging_config import setup_logger +if TYPE_CHECKING: + from reactivex.scheduler import ThreadPoolScheduler + from reactivex.subject import Subject + + from dimos.agents.memory.base import AbstractAgentSemanticMemory + from dimos.agents.tokenizer.base import AbstractTokenizer + # Initialize environment variables load_dotenv() # Initialize logger for the agent module logger = setup_logger("dimos.agents", level=logging.DEBUG) + # HuggingFaceLLMAgent Class class HuggingFaceLocalAgent(LLMAgent): - def __init__(self, - dev_name: str, - agent_type: str = "HF-LLM", - model_name: str = "Qwen/Qwen2.5-3B", - device: str = "auto", - query: str = "How many r's are in the word 'strawberry'?", - input_query_stream: Optional[Observable] = None, - input_video_stream: Optional[Observable] = None, - output_dir: str = os.path.join(os.getcwd(), "assets", - "agent"), - agent_memory: Optional[AbstractAgentSemanticMemory] = None, - system_query: Optional[str] = None, - max_output_tokens_per_request: int = None, - max_input_tokens_per_request: int = None, - prompt_builder: Optional[PromptBuilder] = None, - tokenizer: Optional[AbstractTokenizer] = None, - image_detail: str = "low", - pool_scheduler: Optional[ThreadPoolScheduler] = None, - process_all_inputs: Optional[bool] = None,): - + def __init__( + self, + dev_name: str, + agent_type: str = "HF-LLM", + model_name: str = "Qwen/Qwen2.5-3B", + device: str = "auto", + query: str = "How many r's are in the word 'strawberry'?", + input_query_stream: Observable | None = None, + input_video_stream: Observable | None = None, + output_dir: str = os.path.join(os.getcwd(), "assets", "agent"), + agent_memory: AbstractAgentSemanticMemory | None = None, + system_query: str | None = None, + max_output_tokens_per_request: int | None = None, + max_input_tokens_per_request: int | None = None, + prompt_builder: PromptBuilder | None = None, + tokenizer: AbstractTokenizer | None = None, + image_detail: str = "low", + pool_scheduler: ThreadPoolScheduler | None = None, + process_all_inputs: bool | None = None, + ) -> None: # Determine appropriate default for process_all_inputs if not provided if process_all_inputs is None: # Default to True for text queries, False for video streams @@ -79,7 +82,7 @@ def __init__(self, agent_memory=agent_memory or LocalSemanticMemory(), pool_scheduler=pool_scheduler, process_all_inputs=process_all_inputs, - system_query=system_query + system_query=system_query, ) self.query = query @@ -99,14 +102,13 @@ def __init__(self, self.tokenizer = tokenizer or HuggingFaceTokenizer(self.model_name) self.prompt_builder = prompt_builder or PromptBuilder( - self.model_name, - tokenizer=self.tokenizer + self.model_name, tokenizer=self.tokenizer ) self.model = AutoModelForCausalLM.from_pretrained( model_name, torch_dtype=torch.float16 if self.device == "cuda" else torch.float32, - device_map=self.device + device_map=self.device, ) self.max_output_tokens_per_request = max_output_tokens_per_request @@ -124,105 +126,115 @@ def __init__(self, if self.input_video_stream is not None: logger.info("Subscribing to input video stream...") - self.disposables.add( - self.subscribe_to_image_processing(self.input_video_stream)) + self.disposables.add(self.subscribe_to_image_processing(self.input_video_stream)) if self.input_query_stream is not None: logger.info("Subscribing to input query stream...") - self.disposables.add( - self.subscribe_to_query_processing(self.input_query_stream)) - + self.disposables.add(self.subscribe_to_query_processing(self.input_query_stream)) def _send_query(self, messages: list) -> Any: _BLUE_PRINT_COLOR: str = "\033[34m" _RESET_COLOR: str = "\033[0m" - + try: # Log the incoming messages - print(f"{_BLUE_PRINT_COLOR}Messages: {str(messages)}{_RESET_COLOR}") - + print(f"{_BLUE_PRINT_COLOR}Messages: {messages!s}{_RESET_COLOR}") + # Process with chat template try: print("Applying chat template...") prompt_text = self.tokenizer.tokenizer.apply_chat_template( conversation=[{"role": "user", "content": str(messages)}], tokenize=False, - add_generation_prompt=True + add_generation_prompt=True, ) print("Chat template applied.") - + # Tokenize the prompt print("Preparing model inputs...") - model_inputs = self.tokenizer.tokenizer([prompt_text], return_tensors="pt").to(self.model.device) + model_inputs = self.tokenizer.tokenizer([prompt_text], return_tensors="pt").to( + self.model.device + ) print("Model inputs prepared.") - + # Generate the response print("Generating response...") generated_ids = self.model.generate( - **model_inputs, - max_new_tokens=self.max_output_tokens_per_request + **model_inputs, max_new_tokens=self.max_output_tokens_per_request ) - + # Extract the generated tokens (excluding the input prompt tokens) print("Processing generated output...") generated_ids = [ - output_ids[len(input_ids):] - for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids) + output_ids[len(input_ids) :] + for input_ids, output_ids in zip( + model_inputs.input_ids, generated_ids, strict=False + ) ] - + # Convert tokens back to text - response = self.tokenizer.tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0] + response = self.tokenizer.tokenizer.batch_decode( + generated_ids, skip_special_tokens=True + )[0] print("Response successfully generated.") - + return response - + except AttributeError as e: # Handle case where tokenizer doesn't have the expected methods logger.warning(f"Chat template not available: {e}. Using simple format.") # Continue with execution and use simple format - + except Exception as e: # Log any other errors but continue execution - logger.warning(f"Error in chat template processing: {e}. Falling back to simple format.") - + logger.warning( + f"Error in chat template processing: {e}. Falling back to simple format." + ) + # Fallback approach for models without chat template support # This code runs if the try block above raises an exception print("Using simple prompt format...") - + # Convert messages to a simple text format - if isinstance(messages, list) and messages and isinstance(messages[0], dict) and "content" in messages[0]: + if ( + isinstance(messages, list) + and messages + and isinstance(messages[0], dict) + and "content" in messages[0] + ): prompt_text = messages[0]["content"] else: prompt_text = str(messages) - + # Tokenize the prompt model_inputs = self.tokenizer.tokenize_text(prompt_text) model_inputs = torch.tensor([model_inputs], device=self.model.device) - + # Generate the response generated_ids = self.model.generate( - input_ids=model_inputs, - max_new_tokens=self.max_output_tokens_per_request + input_ids=model_inputs, max_new_tokens=self.max_output_tokens_per_request ) - + # Extract the generated tokens - generated_ids = generated_ids[0][len(model_inputs[0]):] - + generated_ids = generated_ids[0][len(model_inputs[0]) :] + # Convert tokens back to text response = self.tokenizer.detokenize_text(generated_ids.tolist()) print("Response generated using simple format.") - + return response - + except Exception as e: # Catch all other errors logger.error(f"Error during query processing: {e}", exc_info=True) - return f"Error processing request. Please try again." + return "Error processing request. Please try again." def stream_query(self, query_text: str) -> Subject: """ Creates an observable that processes a text query and emits the response. """ - return create(lambda observer, _: self._observable_query( - observer, incoming_query=query_text)) + return create( + lambda observer, _: self._observable_query(observer, incoming_query=query_text) + ) + # endregion HuggingFaceLLMAgent Subclass (HuggingFace-Specific Implementation) diff --git a/dimos/agents/agent_huggingface_remote.py b/dimos/agents/agent_huggingface_remote.py index d974259815..5bb5b293d3 100644 --- a/dimos/agents/agent_huggingface_remote.py +++ b/dimos/agents/agent_huggingface_remote.py @@ -17,52 +17,56 @@ # Standard library imports import logging import os -from typing import Any, Optional +from typing import TYPE_CHECKING, Any # Third-party imports from dotenv import load_dotenv from huggingface_hub import InferenceClient -from reactivex import create, Observable -from reactivex.scheduler import ThreadPoolScheduler -from reactivex.subject import Subject +from reactivex import Observable, create # Local imports from dimos.agents.agent import LLMAgent -from dimos.agents.memory.base import AbstractAgentSemanticMemory from dimos.agents.prompt_builder.impl import PromptBuilder -from dimos.agents.tokenizer.base import AbstractTokenizer from dimos.agents.tokenizer.huggingface_tokenizer import HuggingFaceTokenizer from dimos.utils.logging_config import setup_logger +if TYPE_CHECKING: + from reactivex.scheduler import ThreadPoolScheduler + from reactivex.subject import Subject + + from dimos.agents.memory.base import AbstractAgentSemanticMemory + from dimos.agents.tokenizer.base import AbstractTokenizer + # Initialize environment variables load_dotenv() # Initialize logger for the agent module logger = setup_logger("dimos.agents", level=logging.DEBUG) + # HuggingFaceLLMAgent Class class HuggingFaceRemoteAgent(LLMAgent): - def __init__(self, - dev_name: str, - agent_type: str = "HF-LLM", - model_name: str = "Qwen/QwQ-32B", - query: str = "How many r's are in the word 'strawberry'?", - input_query_stream: Optional[Observable] = None, - input_video_stream: Optional[Observable] = None, - output_dir: str = os.path.join(os.getcwd(), "assets", - "agent"), - agent_memory: Optional[AbstractAgentSemanticMemory] = None, - system_query: Optional[str] = None, - max_output_tokens_per_request: int = 16384, - prompt_builder: Optional[PromptBuilder] = None, - tokenizer: Optional[AbstractTokenizer] = None, - image_detail: str = "low", - pool_scheduler: Optional[ThreadPoolScheduler] = None, - process_all_inputs: Optional[bool] = None, - api_key: Optional[str] = None, - hf_provider: Optional[str] = None, - hf_base_url: Optional[str] = None): - + def __init__( + self, + dev_name: str, + agent_type: str = "HF-LLM", + model_name: str = "Qwen/QwQ-32B", + query: str = "How many r's are in the word 'strawberry'?", + input_query_stream: Observable | None = None, + input_video_stream: Observable | None = None, + output_dir: str = os.path.join(os.getcwd(), "assets", "agent"), + agent_memory: AbstractAgentSemanticMemory | None = None, + system_query: str | None = None, + max_output_tokens_per_request: int = 16384, + prompt_builder: PromptBuilder | None = None, + tokenizer: AbstractTokenizer | None = None, + image_detail: str = "low", + pool_scheduler: ThreadPoolScheduler | None = None, + process_all_inputs: bool | None = None, + api_key: str | None = None, + hf_provider: str | None = None, + hf_base_url: str | None = None, + ) -> None: # Determine appropriate default for process_all_inputs if not provided if process_all_inputs is None: # Default to True for text queries, False for video streams @@ -77,7 +81,7 @@ def __init__(self, agent_memory=agent_memory, pool_scheduler=pool_scheduler, process_all_inputs=process_all_inputs, - system_query=system_query + system_query=system_query, ) self.query = query @@ -86,17 +90,16 @@ def __init__(self, self.model_name = model_name self.prompt_builder = prompt_builder or PromptBuilder( - self.model_name, - tokenizer=tokenizer or HuggingFaceTokenizer(self.model_name) + self.model_name, tokenizer=tokenizer or HuggingFaceTokenizer(self.model_name) ) self.model_name = model_name self.max_output_tokens_per_request = max_output_tokens_per_request - self.api_key = api_key or os.getenv('HF_TOKEN') + self.api_key = api_key or os.getenv("HF_TOKEN") self.provider = hf_provider or "hf-inference" - self.base_url = hf_base_url or os.getenv('HUGGINGFACE_PRV_ENDPOINT') + self.base_url = hf_base_url or os.getenv("HUGGINGFACE_PRV_ENDPOINT") self.client = InferenceClient( provider=self.provider, base_url=self.base_url, @@ -116,13 +119,10 @@ def __init__(self, if self.input_video_stream is not None: logger.info("Subscribing to input video stream...") - self.disposables.add( - self.subscribe_to_image_processing(self.input_video_stream)) + self.disposables.add(self.subscribe_to_image_processing(self.input_video_stream)) if self.input_query_stream is not None: logger.info("Subscribing to input query stream...") - self.disposables.add( - self.subscribe_to_query_processing(self.input_query_stream)) - + self.disposables.add(self.subscribe_to_query_processing(self.input_query_stream)) def _send_query(self, messages: list) -> Any: try: @@ -132,7 +132,7 @@ def _send_query(self, messages: list) -> Any: max_tokens=self.max_output_tokens_per_request, ) - return (completion.choices[0].message) + return completion.choices[0].message except Exception as e: logger.error(f"Error during HuggingFace query: {e}") return "Error processing request." @@ -141,5 +141,6 @@ def stream_query(self, query_text: str) -> Subject: """ Creates an observable that processes a text query and emits the response. """ - return create(lambda observer, _: self._observable_query( - observer, incoming_query=query_text)) + return create( + lambda observer, _: self._observable_query(observer, incoming_query=query_text) + ) diff --git a/dimos/agents/agent_message.py b/dimos/agents/agent_message.py new file mode 100644 index 0000000000..cecd8092c1 --- /dev/null +++ b/dimos/agents/agent_message.py @@ -0,0 +1,100 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""AgentMessage type for multimodal agent communication.""" + +from dataclasses import dataclass, field +import time + +from dimos.agents.agent_types import AgentImage +from dimos.msgs.sensor_msgs.Image import Image + + +@dataclass +class AgentMessage: + """Message type for agent communication with text and images. + + This type supports multimodal messages containing both text strings + and AgentImage objects (base64 encoded) for vision-enabled agents. + + The messages field contains multiple text strings that will be combined + into a single message when sent to the LLM. + """ + + messages: list[str] = field(default_factory=list) + images: list[AgentImage] = field(default_factory=list) + sender_id: str | None = None + timestamp: float = field(default_factory=time.time) + + def add_text(self, text: str) -> None: + """Add a text message.""" + if text: # Only add non-empty text + self.messages.append(text) + + def add_image(self, image: Image | AgentImage) -> None: + """Add an image. Converts Image to AgentImage if needed.""" + if isinstance(image, Image): + # Convert to AgentImage + agent_image = AgentImage( + base64_jpeg=image.agent_encode(), + width=image.width, + height=image.height, + metadata={"format": image.format.value, "frame_id": image.frame_id}, + ) + self.images.append(agent_image) + elif isinstance(image, AgentImage): + self.images.append(image) + else: + raise TypeError(f"Expected Image or AgentImage, got {type(image)}") + + def has_text(self) -> bool: + """Check if message contains text.""" + # Check if we have any non-empty messages + return any(msg for msg in self.messages if msg) + + def has_images(self) -> bool: + """Check if message contains images.""" + return len(self.images) > 0 + + def is_multimodal(self) -> bool: + """Check if message contains both text and images.""" + return self.has_text() and self.has_images() + + def get_primary_text(self) -> str | None: + """Get the first text message, if any.""" + return self.messages[0] if self.messages else None + + def get_primary_image(self) -> AgentImage | None: + """Get the first image, if any.""" + return self.images[0] if self.images else None + + def get_combined_text(self) -> str: + """Get all text messages combined into a single string.""" + # Filter out any empty strings and join + return " ".join(msg for msg in self.messages if msg) + + def clear(self) -> None: + """Clear all content.""" + self.messages.clear() + self.images.clear() + + def __repr__(self) -> str: + """String representation.""" + return ( + f"AgentMessage(" + f"texts={len(self.messages)}, " + f"images={len(self.images)}, " + f"sender='{self.sender_id}', " + f"timestamp={self.timestamp})" + ) diff --git a/dimos/agents/agent_types.py b/dimos/agents/agent_types.py new file mode 100644 index 0000000000..db41acbafb --- /dev/null +++ b/dimos/agents/agent_types.py @@ -0,0 +1,255 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Agent-specific types for message passing.""" + +from dataclasses import dataclass, field +import json +import threading +import time +from typing import Any + + +@dataclass +class AgentImage: + """Image data encoded for agent consumption. + + Images are stored as base64-encoded JPEG strings ready for + direct use by LLM/vision models. + """ + + base64_jpeg: str + width: int | None = None + height: int | None = None + metadata: dict[str, Any] = field(default_factory=dict) + + def __repr__(self) -> str: + return f"AgentImage(size={self.width}x{self.height}, metadata={list(self.metadata.keys())})" + + +@dataclass +class ToolCall: + """Represents a tool/function call request from the LLM.""" + + id: str + name: str + arguments: dict[str, Any] + status: str = "pending" # pending, executing, completed, failed + + def __repr__(self) -> str: + return f"ToolCall(id='{self.id}', name='{self.name}', status='{self.status}')" + + +@dataclass +class AgentResponse: + """Enhanced response from an agent query with tool support. + + Based on common LLM response patterns, includes content and metadata. + """ + + content: str + role: str = "assistant" + tool_calls: list[ToolCall] | None = None + requires_follow_up: bool = False # Indicates if tool execution is needed + metadata: dict[str, Any] = field(default_factory=dict) + timestamp: float = field(default_factory=time.time) + + def __repr__(self) -> str: + content_preview = self.content[:50] + "..." if len(self.content) > 50 else self.content + tool_info = f", tools={len(self.tool_calls)}" if self.tool_calls else "" + return f"AgentResponse(role='{self.role}', content='{content_preview}'{tool_info})" + + +@dataclass +class ConversationMessage: + """Single message in conversation history. + + Represents a message in the conversation that can be converted to + different formats (OpenAI, TensorZero, etc). + """ + + role: str # "system", "user", "assistant", "tool" + content: str | list[dict[str, Any]] # Text or content blocks + tool_calls: list[ToolCall] | None = None + tool_call_id: str | None = None # For tool responses + name: str | None = None # For tool messages (function name) + timestamp: float = field(default_factory=time.time) + + def to_openai_format(self) -> dict[str, Any]: + """Convert to OpenAI API format.""" + msg = {"role": self.role} + + # Handle content + if isinstance(self.content, str): + msg["content"] = self.content + else: + # Content is already a list of content blocks + msg["content"] = self.content + + # Add tool calls if present + if self.tool_calls: + # Handle both ToolCall objects and dicts + if isinstance(self.tool_calls[0], dict): + msg["tool_calls"] = self.tool_calls + else: + msg["tool_calls"] = [ + { + "id": tc.id, + "type": "function", + "function": {"name": tc.name, "arguments": json.dumps(tc.arguments)}, + } + for tc in self.tool_calls + ] + + # Add tool_call_id for tool responses + if self.tool_call_id: + msg["tool_call_id"] = self.tool_call_id + + # Add name field if present (for tool messages) + if self.name: + msg["name"] = self.name + + return msg + + def __repr__(self) -> str: + content_preview = ( + str(self.content)[:50] + "..." if len(str(self.content)) > 50 else str(self.content) + ) + return f"ConversationMessage(role='{self.role}', content='{content_preview}')" + + +class ConversationHistory: + """Thread-safe conversation history manager. + + Manages conversation history with proper formatting for different + LLM providers and automatic trimming. + """ + + def __init__(self, max_size: int = 20) -> None: + """Initialize conversation history. + + Args: + max_size: Maximum number of messages to keep + """ + self._messages: list[ConversationMessage] = [] + self._lock = threading.Lock() + self.max_size = max_size + + def add_user_message(self, content: str | list[dict[str, Any]]) -> None: + """Add user message to history. + + Args: + content: Text string or list of content blocks (for multimodal) + """ + with self._lock: + self._messages.append(ConversationMessage(role="user", content=content)) + self._trim() + + def add_assistant_message(self, content: str, tool_calls: list[ToolCall] | None = None) -> None: + """Add assistant response to history. + + Args: + content: Response text + tool_calls: Optional list of tool calls made + """ + with self._lock: + self._messages.append( + ConversationMessage(role="assistant", content=content, tool_calls=tool_calls) + ) + self._trim() + + def add_tool_result(self, tool_call_id: str, content: str, name: str | None = None) -> None: + """Add tool execution result to history. + + Args: + tool_call_id: ID of the tool call this is responding to + content: Result of the tool execution + name: Optional name of the tool/function + """ + with self._lock: + self._messages.append( + ConversationMessage( + role="tool", content=content, tool_call_id=tool_call_id, name=name + ) + ) + self._trim() + + def add_raw_message(self, message: dict[str, Any]) -> None: + """Add a raw message dict to history. + + Args: + message: Message dict with role and content + """ + with self._lock: + # Extract fields from raw message + role = message.get("role", "user") + content = message.get("content", "") + + # Handle tool calls if present + tool_calls = None + if "tool_calls" in message: + tool_calls = [ + ToolCall( + id=tc["id"], + name=tc["function"]["name"], + arguments=json.loads(tc["function"]["arguments"]) + if isinstance(tc["function"]["arguments"], str) + else tc["function"]["arguments"], + status="completed", + ) + for tc in message["tool_calls"] + ] + + # Handle tool_call_id for tool responses + tool_call_id = message.get("tool_call_id") + + self._messages.append( + ConversationMessage( + role=role, content=content, tool_calls=tool_calls, tool_call_id=tool_call_id + ) + ) + self._trim() + + def to_openai_format(self) -> list[dict[str, Any]]: + """Export history in OpenAI format. + + Returns: + List of message dicts in OpenAI format + """ + with self._lock: + return [msg.to_openai_format() for msg in self._messages] + + def clear(self) -> None: + """Clear all conversation history.""" + with self._lock: + self._messages.clear() + + def size(self) -> int: + """Get number of messages in history. + + Returns: + Number of messages + """ + with self._lock: + return len(self._messages) + + def _trim(self) -> None: + """Trim history to max_size (must be called within lock).""" + if len(self._messages) > self.max_size: + # Keep the most recent messages + self._messages = self._messages[-self.max_size :] + + def __repr__(self) -> str: + with self._lock: + return f"ConversationHistory(messages={len(self._messages)}, max_size={self.max_size})" diff --git a/dimos/agents/cerebras_agent.py b/dimos/agents/cerebras_agent.py new file mode 100644 index 0000000000..e58de812d0 --- /dev/null +++ b/dimos/agents/cerebras_agent.py @@ -0,0 +1,613 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Cerebras agent implementation for the DIMOS agent framework. + +This module provides a CerebrasAgent class that implements the LLMAgent interface +for Cerebras inference API using the official Cerebras Python SDK. +""" + +from __future__ import annotations + +import copy +import json +import os +import threading +import time +from typing import TYPE_CHECKING + +from cerebras.cloud.sdk import Cerebras +from dotenv import load_dotenv + +# Local imports +from dimos.agents.agent import LLMAgent +from dimos.agents.prompt_builder.impl import PromptBuilder +from dimos.agents.tokenizer.openai_tokenizer import OpenAITokenizer +from dimos.skills.skills import AbstractSkill, SkillLibrary +from dimos.utils.logging_config import setup_logger + +if TYPE_CHECKING: + from pydantic import BaseModel + from reactivex import Observable + from reactivex.observer import Observer + from reactivex.scheduler import ThreadPoolScheduler + + from dimos.agents.memory.base import AbstractAgentSemanticMemory + from dimos.agents.tokenizer.base import AbstractTokenizer + from dimos.stream.frame_processor import FrameProcessor + +# Initialize environment variables +load_dotenv() + +# Initialize logger for the Cerebras agent +logger = setup_logger("dimos.agents.cerebras") + + +# Response object compatible with LLMAgent +class CerebrasResponseMessage(dict): + def __init__( + self, + content: str = "", + tool_calls=None, + ) -> None: + self.content = content + self.tool_calls = tool_calls or [] + self.parsed = None + + # Initialize as dict with the proper structure + super().__init__(self.to_dict()) + + def __str__(self) -> str: + # Return a string representation for logging + if self.content: + return self.content + elif self.tool_calls: + # Return JSON representation of the first tool call + if self.tool_calls: + tool_call = self.tool_calls[0] + tool_json = { + "name": tool_call.function.name, + "arguments": json.loads(tool_call.function.arguments), + } + return json.dumps(tool_json) + return "[No content]" + + def to_dict(self): + """Convert to dictionary format for JSON serialization.""" + result = {"role": "assistant", "content": self.content or ""} + + if self.tool_calls: + result["tool_calls"] = [] + for tool_call in self.tool_calls: + result["tool_calls"].append( + { + "id": tool_call.id, + "type": "function", + "function": { + "name": tool_call.function.name, + "arguments": tool_call.function.arguments, + }, + } + ) + + return result + + +class CerebrasAgent(LLMAgent): + """Cerebras agent implementation using the official Cerebras Python SDK. + + This class implements the _send_query method to interact with Cerebras API + using their official SDK, allowing most of the LLMAgent logic to be reused. + """ + + def __init__( + self, + dev_name: str, + agent_type: str = "Vision", + query: str = "What do you see?", + input_query_stream: Observable | None = None, + input_video_stream: Observable | None = None, + input_data_stream: Observable | None = None, + output_dir: str = os.path.join(os.getcwd(), "assets", "agent"), + agent_memory: AbstractAgentSemanticMemory | None = None, + system_query: str | None = None, + max_input_tokens_per_request: int = 128000, + max_output_tokens_per_request: int = 16384, + model_name: str = "llama-4-scout-17b-16e-instruct", + skills: AbstractSkill | list[AbstractSkill] | SkillLibrary | None = None, + response_model: BaseModel | None = None, + frame_processor: FrameProcessor | None = None, + image_detail: str = "low", + pool_scheduler: ThreadPoolScheduler | None = None, + process_all_inputs: bool | None = None, + tokenizer: AbstractTokenizer | None = None, + prompt_builder: PromptBuilder | None = None, + ) -> None: + """ + Initializes a new instance of the CerebrasAgent. + + Args: + dev_name (str): The device name of the agent. + agent_type (str): The type of the agent. + query (str): The default query text. + input_query_stream (Observable): An observable for query input. + input_video_stream (Observable): An observable for video frames. + input_data_stream (Observable): An observable for data input. + output_dir (str): Directory for output files. + agent_memory (AbstractAgentSemanticMemory): The memory system. + system_query (str): The system prompt to use with RAG context. + max_input_tokens_per_request (int): Maximum tokens for input. + max_output_tokens_per_request (int): Maximum tokens for output. + model_name (str): The Cerebras model name to use. Available options: + - llama-4-scout-17b-16e-instruct (default, fastest) + - llama3.1-8b + - llama-3.3-70b + - qwen-3-32b + - deepseek-r1-distill-llama-70b (private preview) + skills (Union[AbstractSkill, List[AbstractSkill], SkillLibrary]): Skills available to the agent. + response_model (BaseModel): Optional Pydantic model for structured responses. + frame_processor (FrameProcessor): Custom frame processor. + image_detail (str): Detail level for images ("low", "high", "auto"). + pool_scheduler (ThreadPoolScheduler): The scheduler to use for thread pool operations. + process_all_inputs (bool): Whether to process all inputs or skip when busy. + tokenizer (AbstractTokenizer): The tokenizer for the agent. + prompt_builder (PromptBuilder): The prompt builder for the agent. + """ + # Determine appropriate default for process_all_inputs if not provided + if process_all_inputs is None: + # Default to True for text queries, False for video streams + if input_query_stream is not None and input_video_stream is None: + process_all_inputs = True + else: + process_all_inputs = False + + super().__init__( + dev_name=dev_name, + agent_type=agent_type, + agent_memory=agent_memory, + pool_scheduler=pool_scheduler, + process_all_inputs=process_all_inputs, + system_query=system_query, + input_query_stream=input_query_stream, + input_video_stream=input_video_stream, + input_data_stream=input_data_stream, + ) + + # Initialize Cerebras client + self.client = Cerebras() + + self.query = query + self.output_dir = output_dir + os.makedirs(self.output_dir, exist_ok=True) + + # Initialize conversation history for multi-turn conversations + self.conversation_history = [] + self._history_lock = threading.Lock() + + # Configure skills + self.skills = skills + self.skill_library = None + if isinstance(self.skills, SkillLibrary): + self.skill_library = self.skills + elif isinstance(self.skills, list): + self.skill_library = SkillLibrary() + for skill in self.skills: + self.skill_library.add(skill) + elif isinstance(self.skills, AbstractSkill): + self.skill_library = SkillLibrary() + self.skill_library.add(self.skills) + + self.response_model = response_model + self.model_name = model_name + self.image_detail = image_detail + self.max_output_tokens_per_request = max_output_tokens_per_request + self.max_input_tokens_per_request = max_input_tokens_per_request + self.max_tokens_per_request = max_input_tokens_per_request + max_output_tokens_per_request + + # Add static context to memory. + self._add_context_to_memory() + + # Initialize tokenizer and prompt builder + self.tokenizer = tokenizer or OpenAITokenizer( + model_name="gpt-4o" + ) # Use GPT-4 tokenizer for better accuracy + self.prompt_builder = prompt_builder or PromptBuilder( + model_name=self.model_name, + max_tokens=self.max_input_tokens_per_request, + tokenizer=self.tokenizer, + ) + + logger.info("Cerebras Agent Initialized.") + + def _add_context_to_memory(self) -> None: + """Adds initial context to the agent's memory.""" + context_data = [ + ( + "id0", + "Optical Flow is a technique used to track the movement of objects in a video sequence.", + ), + ( + "id1", + "Edge Detection is a technique used to identify the boundaries of objects in an image.", + ), + ("id2", "Video is a sequence of frames captured at regular intervals."), + ( + "id3", + "Colors in Optical Flow are determined by the movement of light, and can be used to track the movement of objects.", + ), + ( + "id4", + "Json is a data interchange format that is easy for humans to read and write, and easy for machines to parse and generate.", + ), + ] + for doc_id, text in context_data: + self.agent_memory.add_vector(doc_id, text) + + def _build_prompt( + self, + messages: list, + base64_image: str | list[str] | None = None, + dimensions: tuple[int, int] | None = None, + override_token_limit: bool = False, + condensed_results: str = "", + ) -> list: + """Builds a prompt message specifically for Cerebras API. + + Args: + messages (list): Existing messages list to build upon. + base64_image (Union[str, List[str]]): Optional Base64-encoded image(s). + dimensions (Tuple[int, int]): Optional image dimensions. + override_token_limit (bool): Whether to override token limits. + condensed_results (str): The condensed RAG context. + + Returns: + list: Messages formatted for Cerebras API. + """ + # Add system message if provided and not already in history + if self.system_query and (not messages or messages[0].get("role") != "system"): + messages.insert(0, {"role": "system", "content": self.system_query}) + logger.info("Added system message to conversation") + + # Append user query while handling RAG + if condensed_results: + user_message = {"role": "user", "content": f"{condensed_results}\n\n{self.query}"} + logger.info("Created user message with RAG context") + else: + user_message = {"role": "user", "content": self.query} + + messages.append(user_message) + + if base64_image is not None: + # Handle both single image (str) and multiple images (List[str]) + images = [base64_image] if isinstance(base64_image, str) else base64_image + + # For Cerebras, we'll add images inline with text (OpenAI-style format) + for img in images: + img_content = [ + {"type": "text", "text": "Here is an image to analyze:"}, + { + "type": "image_url", + "image_url": { + "url": f"data:image/jpeg;base64,{img}", + "detail": self.image_detail, + }, + }, + ] + messages.append({"role": "user", "content": img_content}) + + logger.info(f"Added {len(images)} image(s) to conversation") + + # Use new truncation function + messages = self._truncate_messages(messages, override_token_limit) + + return messages + + def _truncate_messages(self, messages: list, override_token_limit: bool = False) -> list: + """Truncate messages if total tokens exceed 16k using existing truncate_tokens method. + + Args: + messages (list): List of message dictionaries + override_token_limit (bool): Whether to skip truncation + + Returns: + list: Messages with content truncated if needed + """ + if override_token_limit: + return messages + + total_tokens = 0 + for message in messages: + if isinstance(message.get("content"), str): + total_tokens += self.prompt_builder.tokenizer.token_count(message["content"]) + elif isinstance(message.get("content"), list): + for item in message["content"]: + if item.get("type") == "text": + total_tokens += self.prompt_builder.tokenizer.token_count(item["text"]) + elif item.get("type") == "image_url": + total_tokens += 85 + + if total_tokens > 16000: + excess_tokens = total_tokens - 16000 + current_tokens = total_tokens + + # Start from oldest messages and truncate until under 16k + for i in range(len(messages)): + if current_tokens <= 16000: + break + + msg = messages[i] + if msg.get("role") == "system": + continue + + if isinstance(msg.get("content"), str): + original_tokens = self.prompt_builder.tokenizer.token_count(msg["content"]) + # Calculate how much to truncate from this message + tokens_to_remove = min(excess_tokens, original_tokens // 3) + new_max_tokens = max(50, original_tokens - tokens_to_remove) + + msg["content"] = self.prompt_builder.truncate_tokens( + msg["content"], new_max_tokens, "truncate_end" + ) + + new_tokens = self.prompt_builder.tokenizer.token_count(msg["content"]) + tokens_saved = original_tokens - new_tokens + current_tokens -= tokens_saved + excess_tokens -= tokens_saved + + logger.info( + f"Truncated older messages using truncate_tokens, final tokens: {current_tokens}" + ) + else: + logger.info(f"No truncation needed, total tokens: {total_tokens}") + + return messages + + def clean_cerebras_schema(self, schema: dict) -> dict: + """Simple schema cleaner that removes unsupported fields for Cerebras API.""" + if not isinstance(schema, dict): + return schema + + # Removing the problematic fields that pydantic generates + cleaned = {} + unsupported_fields = { + "minItems", + "maxItems", + "uniqueItems", + "exclusiveMinimum", + "exclusiveMaximum", + "minimum", + "maximum", + } + + for key, value in schema.items(): + if key in unsupported_fields: + continue # Skip unsupported fields + elif isinstance(value, dict): + cleaned[key] = self.clean_cerebras_schema(value) + elif isinstance(value, list): + cleaned[key] = [ + self.clean_cerebras_schema(item) if isinstance(item, dict) else item + for item in value + ] + else: + cleaned[key] = value + + return cleaned + + def create_tool_call( + self, + name: str | None = None, + arguments: dict | None = None, + call_id: str | None = None, + content: str | None = None, + ): + """Create a tool call object from either direct parameters or JSON content.""" + # If content is provided, parse it as JSON + if content: + logger.info(f"Creating tool call from content: {content}") + try: + content_json = json.loads(content) + if ( + isinstance(content_json, dict) + and "name" in content_json + and "arguments" in content_json + ): + name = content_json["name"] + arguments = content_json["arguments"] + else: + return None + except json.JSONDecodeError: + logger.warning("Content appears to be JSON but failed to parse") + return None + + # Create the tool call object + if name and arguments is not None: + timestamp = int(time.time() * 1000000) # microsecond precision + tool_id = f"call_{timestamp}" + + logger.info(f"Creating tool call with timestamp ID: {tool_id}") + return type( + "ToolCall", + (), + { + "id": tool_id, + "function": type( + "Function", (), {"name": name, "arguments": json.dumps(arguments)} + ), + }, + ) + + return None + + def _send_query(self, messages: list) -> CerebrasResponseMessage: + """Sends the query to Cerebras API using the official Cerebras SDK. + + Args: + messages (list): The prompt messages to send. + + Returns: + The response message from Cerebras wrapped in our CerebrasResponseMessage class. + + Raises: + Exception: If no response message is returned from the API. + ConnectionError: If there's an issue connecting to the API. + ValueError: If the messages or other parameters are invalid. + """ + try: + # Prepare API call parameters + api_params = { + "model": self.model_name, + "messages": messages, + # "max_tokens": self.max_output_tokens_per_request, + } + + # Add tools if available + if self.skill_library and self.skill_library.get_tools(): + tools = self.skill_library.get_tools() + for tool in tools: + if "function" in tool and "parameters" in tool["function"]: + tool["function"]["parameters"] = self.clean_cerebras_schema( + tool["function"]["parameters"] + ) + api_params["tools"] = tools + api_params["tool_choice"] = "auto" + + if self.response_model is not None: + api_params["response_format"] = { + "type": "json_object", + "schema": self.response_model, + } + + # Make the API call + response = self.client.chat.completions.create(**api_params) + + raw_message = response.choices[0].message + if raw_message is None: + logger.error("Response message does not exist.") + raise Exception("Response message does not exist.") + + # Process response into final format + content = raw_message.content + tool_calls = getattr(raw_message, "tool_calls", None) + + # If no structured tool calls from API, try parsing content as JSON tool call + if not tool_calls and content and content.strip().startswith("{"): + parsed_tool_call = self.create_tool_call(content=content) + if parsed_tool_call: + tool_calls = [parsed_tool_call] + content = None + + return CerebrasResponseMessage(content=content, tool_calls=tool_calls) + + except ConnectionError as ce: + logger.error(f"Connection error with Cerebras API: {ce}") + raise + except ValueError as ve: + logger.error(f"Invalid parameters for Cerebras API: {ve}") + raise + except Exception as e: + # Print the raw API parameters when an error occurs + logger.error(f"Raw API parameters: {json.dumps(api_params, indent=2)}") + logger.error(f"Unexpected error in Cerebras API call: {e}") + raise + + def _observable_query( + self, + observer: Observer, + base64_image: str | None = None, + dimensions: tuple[int, int] | None = None, + override_token_limit: bool = False, + incoming_query: str | None = None, + reset_conversation: bool = False, + ): + """Main query handler that manages conversation history and Cerebras interactions. + + This method follows ClaudeAgent's pattern for efficient conversation history management. + + Args: + observer (Observer): The observer to emit responses to. + base64_image (str): Optional Base64-encoded image. + dimensions (Tuple[int, int]): Optional image dimensions. + override_token_limit (bool): Whether to override token limits. + incoming_query (str): Optional query to update the agent's query. + reset_conversation (bool): Whether to reset the conversation history. + """ + try: + # Reset conversation history if requested + if reset_conversation: + self.conversation_history = [] + logger.info("Conversation history reset") + + # Create a local copy of conversation history and record its length + messages = copy.deepcopy(self.conversation_history) + + # Update query and get context + self._update_query(incoming_query) + _, condensed_results = self._get_rag_context() + + # Build prompt + messages = self._build_prompt( + messages, base64_image, dimensions, override_token_limit, condensed_results + ) + + while True: + logger.info("Sending Query.") + response_message = self._send_query(messages) + logger.info(f"Received Response: {response_message}") + + if response_message is None: + raise Exception("Response message does not exist.") + + # If no skill library or no tool calls, we're done + if ( + self.skill_library is None + or self.skill_library.get_tools() is None + or response_message.tool_calls is None + ): + final_msg = ( + response_message.parsed + if hasattr(response_message, "parsed") and response_message.parsed + else ( + response_message.content + if hasattr(response_message, "content") + else response_message + ) + ) + messages.append(response_message) + break + + logger.info(f"Assistant requested {len(response_message.tool_calls)} tool call(s)") + next_response = self._handle_tooling(response_message, messages) + + if next_response is None: + final_msg = response_message.content or "" + break + + response_message = next_response + + with self._history_lock: + self.conversation_history = messages + logger.info( + f"Updated conversation history (total: {len(self.conversation_history)} messages)" + ) + + # Emit the final message content to the observer + observer.on_next(final_msg) + self.response_subject.on_next(final_msg) + observer.on_completed() + + except Exception as e: + logger.error(f"Query failed in {self.dev_name}: {e}") + observer.on_error(e) + self.response_subject.on_error(e) diff --git a/dimos/agents/claude_agent.py b/dimos/agents/claude_agent.py index a2f7d5556b..c8163de162 100644 --- a/dimos/agents/claude_agent.py +++ b/dimos/agents/claude_agent.py @@ -23,27 +23,24 @@ import json import os -from typing import Any, Dict, List, Optional, Tuple, Union, cast -import logging +from typing import TYPE_CHECKING, Any import anthropic -from anthropic.types import ContentBlock, MessageParam, ToolUseBlock from dotenv import load_dotenv -from httpx._transports import base -from pydantic import BaseModel -from reactivex import Observable -from reactivex.disposable import Disposable -from reactivex.scheduler import ThreadPoolScheduler -from reactivex import create # Local imports from dimos.agents.agent import LLMAgent -from dimos.agents.memory.base import AbstractAgentSemanticMemory -from dimos.agents.prompt_builder.impl import PromptBuilder from dimos.skills.skills import AbstractSkill, SkillLibrary from dimos.stream.frame_processor import FrameProcessor from dimos.utils.logging_config import setup_logger -from dimos.utils.threadpool import get_scheduler + +if TYPE_CHECKING: + from pydantic import BaseModel + from reactivex import Observable + from reactivex.scheduler import ThreadPoolScheduler + + from dimos.agents.memory.base import AbstractAgentSemanticMemory + from dimos.agents.prompt_builder.impl import PromptBuilder # Initialize environment variables load_dotenv() @@ -51,29 +48,31 @@ # Initialize logger for the Claude agent logger = setup_logger("dimos.agents.claude") + # Response object compatible with LLMAgent class ResponseMessage: - def __init__(self, content="", tool_calls=None, thinking_blocks=None): + def __init__(self, content: str = "", tool_calls=None, thinking_blocks=None) -> None: self.content = content self.tool_calls = tool_calls or [] self.thinking_blocks = thinking_blocks or [] self.parsed = None - - def __str__(self): + + def __str__(self) -> str: # Return a string representation for logging parts = [] - + # Include content if available if self.content: parts.append(self.content) - + # Include tool calls if available if self.tool_calls: tool_names = [tc.function.name for tc in self.tool_calls] parts.append(f"[Tools called: {', '.join(tool_names)}]") - + return "\n".join(parts) if parts else "[No content]" + class ClaudeAgent(LLMAgent): """Claude agent implementation that uses Anthropic's API for processing. @@ -81,29 +80,31 @@ class ClaudeAgent(LLMAgent): and overrides _build_prompt to create Claude-formatted messages directly. """ - def __init__(self, - dev_name: str, - agent_type: str = "Vision", - query: str = "What do you see?", - input_query_stream: Optional[Observable] = None, - input_video_stream: Optional[Observable] = None, - input_data_stream: Optional[Observable] = None, - output_dir: str = os.path.join(os.getcwd(), "assets", "agent"), - agent_memory: Optional[AbstractAgentSemanticMemory] = None, - system_query: Optional[str] = None, - max_input_tokens_per_request: int = 128000, - max_output_tokens_per_request: int = 16384, - model_name: str = "claude-3-7-sonnet-20250219", - prompt_builder: Optional[PromptBuilder] = None, - rag_query_n: int = 4, - rag_similarity_threshold: float = 0.45, - skills: Optional[AbstractSkill] = None, - response_model: Optional[BaseModel] = None, - frame_processor: Optional[FrameProcessor] = None, - image_detail: str = "low", - pool_scheduler: Optional[ThreadPoolScheduler] = None, - process_all_inputs: Optional[bool] = None, - thinking_budget_tokens: Optional[int] = 2000): + def __init__( + self, + dev_name: str, + agent_type: str = "Vision", + query: str = "What do you see?", + input_query_stream: Observable | None = None, + input_video_stream: Observable | None = None, + input_data_stream: Observable | None = None, + output_dir: str = os.path.join(os.getcwd(), "assets", "agent"), + agent_memory: AbstractAgentSemanticMemory | None = None, + system_query: str | None = None, + max_input_tokens_per_request: int = 128000, + max_output_tokens_per_request: int = 16384, + model_name: str = "claude-3-7-sonnet-20250219", + prompt_builder: PromptBuilder | None = None, + rag_query_n: int = 4, + rag_similarity_threshold: float = 0.45, + skills: AbstractSkill | None = None, + response_model: BaseModel | None = None, + frame_processor: FrameProcessor | None = None, + image_detail: str = "low", + pool_scheduler: ThreadPoolScheduler | None = None, + process_all_inputs: bool | None = None, + thinking_budget_tokens: int | None = 2000, + ) -> None: """ Initializes a new instance of the ClaudeAgent. @@ -137,7 +138,7 @@ def __init__(self, process_all_inputs = True else: process_all_inputs = False - + super().__init__( dev_name=dev_name, agent_type=agent_type, @@ -147,9 +148,9 @@ def __init__(self, system_query=system_query, input_query_stream=input_query_stream, input_video_stream=input_video_stream, - input_data_stream=input_data_stream + input_data_stream=input_data_stream, ) - + self.client = anthropic.Anthropic() self.query = query self.output_dir = output_dir @@ -158,10 +159,10 @@ def __init__(self, # Claude-specific parameters self.thinking_budget_tokens = thinking_budget_tokens self.claude_api_params = {} # Will store params for Claude API calls - + # Configure skills self.skills = skills - self.skill_library = None # Required for error 'ClaudeAgent' object has no attribute 'skill_library' due to skills refactor + self.skill_library = None # Required for error 'ClaudeAgent' object has no attribute 'skill_library' due to skills refactor if isinstance(self.skills, SkillLibrary): self.skill_library = self.skills elif isinstance(self.skills, list): @@ -171,7 +172,7 @@ def __init__(self, elif isinstance(self.skills, AbstractSkill): self.skill_library = SkillLibrary() self.skill_library.add(self.skills) - + self.response_model = response_model self.model_name = model_name self.rag_query_n = rag_query_n @@ -186,7 +187,6 @@ def __init__(self, self.frame_processor = frame_processor or FrameProcessor(delete_on_init=True) - # Ensure only one input stream is provided. if self.input_video_stream is not None and self.input_query_stream is not None: raise ValueError( @@ -195,71 +195,78 @@ def __init__(self, logger.info("Claude Agent Initialized.") - def _add_context_to_memory(self): + def _add_context_to_memory(self) -> None: """Adds initial context to the agent's memory.""" context_data = [ - ("id0", - "Optical Flow is a technique used to track the movement of objects in a video sequence." - ), - ("id1", - "Edge Detection is a technique used to identify the boundaries of objects in an image." - ), - ("id2", - "Video is a sequence of frames captured at regular intervals."), - ("id3", - "Colors in Optical Flow are determined by the movement of light, and can be used to track the movement of objects." - ), - ("id4", - "Json is a data interchange format that is easy for humans to read and write, and easy for machines to parse and generate." - ), + ( + "id0", + "Optical Flow is a technique used to track the movement of objects in a video sequence.", + ), + ( + "id1", + "Edge Detection is a technique used to identify the boundaries of objects in an image.", + ), + ("id2", "Video is a sequence of frames captured at regular intervals."), + ( + "id3", + "Colors in Optical Flow are determined by the movement of light, and can be used to track the movement of objects.", + ), + ( + "id4", + "Json is a data interchange format that is easy for humans to read and write, and easy for machines to parse and generate.", + ), ] for doc_id, text in context_data: self.agent_memory.add_vector(doc_id, text) - def _convert_tools_to_claude_format(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + def _convert_tools_to_claude_format(self, tools: list[dict[str, Any]]) -> list[dict[str, Any]]: """ Converts DIMOS tools to Claude format. - + Args: tools: List of tools in DIMOS format. - + Returns: List of tools in Claude format. """ if not tools: return [] - + claude_tools = [] - + for tool in tools: # Skip if not a function - if tool.get('type') != 'function': + if tool.get("type") != "function": continue - - function = tool.get('function', {}) - name = function.get('name') - description = function.get('description', '') - parameters = function.get('parameters', {}) - + + function = tool.get("function", {}) + name = function.get("name") + description = function.get("description", "") + parameters = function.get("parameters", {}) + claude_tool = { "name": name, "description": description, "input_schema": { "type": "object", - "properties": parameters.get('properties', {}), - "required": parameters.get('required', []), - } + "properties": parameters.get("properties", {}), + "required": parameters.get("required", []), + }, } - + claude_tools.append(claude_tool) - + return claude_tools - def _build_prompt(self, messages: list, base64_image: Optional[Union[str, List[str]]] = None, - dimensions: Optional[Tuple[int, int]] = None, - override_token_limit: bool = False, - rag_results: str = "", - thinking_budget_tokens: int = None) -> list: + def _build_prompt( + self, + messages: list, + base64_image: str | list[str] | None = None, + dimensions: tuple[int, int] | None = None, + override_token_limit: bool = False, + rag_results: str = "", + thinking_budget_tokens: int | None = None, + ) -> list: """Builds a prompt message specifically for Claude API, using local messages copy.""" """Builds a prompt message specifically for Claude API. @@ -276,216 +283,247 @@ def _build_prompt(self, messages: list, base64_image: Optional[Union[str, List[s Returns: dict: A dict containing Claude API parameters. """ - + # Append user query to conversation history while handling RAG if rag_results: messages.append({"role": "user", "content": f"{rag_results}\n\n{self.query}"}) - logger.info(f"Added new user message to conversation history with RAG context (now has {len(messages)} messages)") - else: + logger.info( + f"Added new user message to conversation history with RAG context (now has {len(messages)} messages)" + ) + else: messages.append({"role": "user", "content": self.query}) - logger.info(f"Added new user message to conversation history (now has {len(messages)} messages)") - + logger.info( + f"Added new user message to conversation history (now has {len(messages)} messages)" + ) + if base64_image is not None: # Handle both single image (str) and multiple images (List[str]) images = [base64_image] if isinstance(base64_image, str) else base64_image - + # Add each image as a separate entry in conversation history for img in images: img_content = [ { "type": "image", - "source": { - "type": "base64", - "media_type": "image/jpeg", - "data": img - } + "source": {"type": "base64", "media_type": "image/jpeg", "data": img}, } ] messages.append({"role": "user", "content": img_content}) - + if images: - logger.info(f"Added {len(images)} image(s) as separate entries to conversation history") - + logger.info( + f"Added {len(images)} image(s) as separate entries to conversation history" + ) + # Create Claude parameters with basic settings claude_params = { "model": self.model_name, "max_tokens": self.max_output_tokens_per_request, "temperature": 0, # Add temperature to make responses more deterministic - "messages": messages + "messages": messages, } - + # Add system prompt as a top-level parameter (not as a message) if self.system_query: - claude_params['system'] = self.system_query - + claude_params["system"] = self.system_query + # Store the parameters for use in _send_query self.claude_api_params = claude_params.copy() - + # Add tools if skills are available if self.skills and self.skills.get_tools(): tools = self._convert_tools_to_claude_format(self.skills.get_tools()) if tools: # Only add if we have valid tools claude_params["tools"] = tools # Enable tool calling with proper format - claude_params["tool_choice"] = { - "type": "auto" - } - + claude_params["tool_choice"] = {"type": "auto"} + # Add thinking if enabled and hard code required temperature = 1 if thinking_budget_tokens is not None and thinking_budget_tokens != 0: - claude_params["thinking"] = { - "type": "enabled", - "budget_tokens": thinking_budget_tokens - } - claude_params["temperature"] = 1 # Required to be 1 when thinking is enabled # Default to 0 for deterministic responses - + claude_params["thinking"] = {"type": "enabled", "budget_tokens": thinking_budget_tokens} + claude_params["temperature"] = ( + 1 # Required to be 1 when thinking is enabled # Default to 0 for deterministic responses + ) + # Store the parameters for use in _send_query and return them self.claude_api_params = claude_params.copy() return messages, claude_params - + def _send_query(self, messages: list, claude_params: dict) -> Any: """Sends the query to Anthropic's API using streaming for better thinking visualization. - + Args: messages: Dict with 'claude_prompt' key containing Claude API parameters. - + Returns: The response message in a format compatible with LLMAgent's expectations. """ try: # Get Claude parameters - claude_params = (claude_params.get('claude_prompt', None) or self.claude_api_params) - + claude_params = claude_params.get("claude_prompt", None) or self.claude_api_params + # Log request parameters with truncated base64 data logger.debug(self._debug_api_call(claude_params)) - + # Initialize response containers text_content = "" tool_calls = [] thinking_blocks = [] - + # Log the start of streaming and the query - logger.info(f"Sending streaming request to Claude API") - + logger.info("Sending streaming request to Claude API") + # Log the query to memory.txt with open(os.path.join(self.output_dir, "memory.txt"), "a") as f: f.write(f"\n\nQUERY: {self.query}\n\n") f.flush() - + # Stream the response with self.client.messages.stream(**claude_params) as stream: print("\n==== CLAUDE API RESPONSE STREAM STARTED ====") - + # Open the memory file once for the entire stream processing with open(os.path.join(self.output_dir, "memory.txt"), "a") as memory_file: # Track the current block being processed - current_block = {'type': None, 'id': None, 'content': "", 'signature': None} - + current_block = {"type": None, "id": None, "content": "", "signature": None} + for event in stream: # Log each event to console - # print(f"EVENT: {event.type}") + # print(f"EVENT: {event.type}") # print(json.dumps(event.model_dump(), indent=2, default=str)) - + if event.type == "content_block_start": # Initialize a new content block block_type = event.content_block.type - current_block = {'type': block_type, 'id': event.index, 'content': "", 'signature': None} + current_block = { + "type": block_type, + "id": event.index, + "content": "", + "signature": None, + } logger.debug(f"Starting {block_type} block...") - + elif event.type == "content_block_delta": if event.delta.type == "thinking_delta": # Accumulate thinking content - current_block['content'] = event.delta.thinking + current_block["content"] = event.delta.thinking memory_file.write(f"{event.delta.thinking}") memory_file.flush() # Ensure content is written immediately - + elif event.delta.type == "text_delta": # Accumulate text content text_content += event.delta.text - current_block['content'] += event.delta.text + current_block["content"] += event.delta.text memory_file.write(f"{event.delta.text}") memory_file.flush() - + elif event.delta.type == "signature_delta": # Store signature for thinking blocks - current_block['signature'] = event.delta.signature - memory_file.write(f"\n[Signature received for block {current_block['id']}]\n") + current_block["signature"] = event.delta.signature + memory_file.write( + f"\n[Signature received for block {current_block['id']}]\n" + ) memory_file.flush() - + elif event.type == "content_block_stop": # Store completed blocks - if current_block['type'] == "thinking": + if current_block["type"] == "thinking": # IMPORTANT: Store the complete event.content_block to ensure we preserve # the exact format that Claude expects in subsequent requests - if hasattr(event, 'content_block'): + if hasattr(event, "content_block"): # Use the exact thinking block as provided by Claude thinking_blocks.append(event.content_block.model_dump()) - memory_file.write(f"\nTHINKING COMPLETE: block {current_block['id']}\n") + memory_file.write( + f"\nTHINKING COMPLETE: block {current_block['id']}\n" + ) else: # Fallback to constructed thinking block if content_block missing thinking_block = { "type": "thinking", - "thinking": current_block['content'], - "signature": current_block['signature'] + "thinking": current_block["content"], + "signature": current_block["signature"], } thinking_blocks.append(thinking_block) - memory_file.write(f"\nTHINKING COMPLETE: block {current_block['id']}\n") - - elif current_block['type'] == "redacted_thinking": + memory_file.write( + f"\nTHINKING COMPLETE: block {current_block['id']}\n" + ) + + elif current_block["type"] == "redacted_thinking": # Handle redacted thinking blocks - if hasattr(event, 'content_block') and hasattr(event.content_block, 'data'): + if hasattr(event, "content_block") and hasattr( + event.content_block, "data" + ): redacted_block = { "type": "redacted_thinking", - "data": event.content_block.data + "data": event.content_block.data, } thinking_blocks.append(redacted_block) - - elif current_block['type'] == "tool_use": + + elif current_block["type"] == "tool_use": # Process tool use blocks when they're complete - if hasattr(event, 'content_block'): + if hasattr(event, "content_block"): tool_block = event.content_block tool_id = tool_block.id tool_name = tool_block.name tool_input = tool_block.input - + # Create a tool call object for LLMAgent compatibility - tool_call_obj = type('ToolCall', (), { - 'id': tool_id, - 'function': type('Function', (), { - 'name': tool_name, - 'arguments': json.dumps(tool_input) - }) - }) + tool_call_obj = type( + "ToolCall", + (), + { + "id": tool_id, + "function": type( + "Function", + (), + { + "name": tool_name, + "arguments": json.dumps(tool_input), + }, + ), + }, + ) tool_calls.append(tool_call_obj) - + # Write tool call information to memory.txt memory_file.write(f"\n\nTOOL CALL: {tool_name}\n") - memory_file.write(f"ARGUMENTS: {json.dumps(tool_input, indent=2)}\n") - + memory_file.write( + f"ARGUMENTS: {json.dumps(tool_input, indent=2)}\n" + ) + # Reset current block - current_block = {'type': None, 'id': None, 'content': "", 'signature': None} + current_block = { + "type": None, + "id": None, + "content": "", + "signature": None, + } memory_file.flush() - - elif event.type == "message_delta" and event.delta.stop_reason == "tool_use": + + elif ( + event.type == "message_delta" and event.delta.stop_reason == "tool_use" + ): # When a tool use is detected - logger.info(f"Tool use stop reason detected in stream") + logger.info("Tool use stop reason detected in stream") # Mark the end of the response in memory.txt - memory_file.write(f"\n\nRESPONSE COMPLETE\n\n") + memory_file.write("\n\nRESPONSE COMPLETE\n\n") memory_file.flush() - + print("\n==== CLAUDE API RESPONSE STREAM COMPLETED ====") - + # Final response - logger.info(f"Claude streaming complete. Text: {len(text_content)} chars, Tool calls: {len(tool_calls)}, Thinking blocks: {len(thinking_blocks)}") - + logger.info( + f"Claude streaming complete. Text: {len(text_content)} chars, Tool calls: {len(tool_calls)}, Thinking blocks: {len(thinking_blocks)}" + ) + # Return the complete response with all components return ResponseMessage( - content=text_content, + content=text_content, tool_calls=tool_calls if tool_calls else None, - thinking_blocks=thinking_blocks if thinking_blocks else None + thinking_blocks=thinking_blocks if thinking_blocks else None, ) - + except ConnectionError as ce: logger.error(f"Connection error with Anthropic API: {ce}") raise @@ -497,20 +535,22 @@ def _send_query(self, messages: list, claude_params: dict) -> Any: logger.exception(e) # This will print the full traceback raise - def _observable_query(self, - observer: Observer, - base64_image: Optional[str] = None, - dimensions: Optional[Tuple[int, int]] = None, - override_token_limit: bool = False, - incoming_query: Optional[str] = None, - reset_conversation: bool = False, - thinking_budget_tokens: int = None): + def _observable_query( + self, + observer: Observer, + base64_image: str | None = None, + dimensions: tuple[int, int] | None = None, + override_token_limit: bool = False, + incoming_query: str | None = None, + reset_conversation: bool = False, + thinking_budget_tokens: int | None = None, + ) -> None: """Main query handler that manages conversation history and Claude interactions. - + This is the primary method for handling all queries, whether they come through direct_query or through the observable pattern. It manages the conversation history, builds prompts, and handles tool calls. - + Args: observer (Observer): The observer to emit responses to base64_image (Optional[str]): Optional Base64-encoded image @@ -519,10 +559,11 @@ def _observable_query(self, incoming_query (Optional[str]): Optional query to update the agent's query reset_conversation (bool): Whether to reset the conversation history """ - + try: logger.info("_observable_query called in claude") import copy + # Reset conversation history if requested if reset_conversation: self.conversation_history = [] @@ -536,12 +577,18 @@ def _observable_query(self, _, rag_results = self._get_rag_context() # Build prompt and get Claude parameters - budget = thinking_budget_tokens if thinking_budget_tokens is not None else self.thinking_budget_tokens - messages, claude_params = self._build_prompt(messages, base64_image, dimensions, override_token_limit, rag_results, budget) - + budget = ( + thinking_budget_tokens + if thinking_budget_tokens is not None + else self.thinking_budget_tokens + ) + messages, claude_params = self._build_prompt( + messages, base64_image, dimensions, override_token_limit, rag_results, budget + ) + # Send query and get response response_message = self._send_query(messages, claude_params) - + if response_message is None: logger.error("Received None response from Claude API") observer.on_next("") @@ -552,23 +599,18 @@ def _observable_query(self, if response_message.thinking_blocks: content_blocks.extend(response_message.thinking_blocks) if response_message.content: - content_blocks.append({ - "type": "text", - "text": response_message.content - }) + content_blocks.append({"type": "text", "text": response_message.content}) if content_blocks: - messages.append({ - "role": "assistant", - "content": content_blocks - }) - + messages.append({"role": "assistant", "content": content_blocks}) + # Handle tool calls if present if response_message.tool_calls: self._handle_tooling(response_message, messages) # At the end, append only new messages (including tool-use/results) to the global conversation history under a lock import threading - if not hasattr(self, '_history_lock'): + + if not hasattr(self, "_history_lock"): self._history_lock = threading.Lock() with self._history_lock: for msg in messages[base_len:]: @@ -585,17 +627,22 @@ def _observable_query(self, observer.on_completed() except Exception as e: logger.error(f"Query failed in {self.dev_name}: {e}") - observer.on_error(e) - self.response_subject.on_error(e) - + # Send a user-friendly error message instead of propagating the error + error_message = "I apologize, but I'm having trouble processing your request right now. Please try again." + observer.on_next(error_message) + self.response_subject.on_next(error_message) + observer.on_completed() + def _handle_tooling(self, response_message, messages): """Executes tools and appends tool-use/result blocks to messages.""" - if not hasattr(response_message, 'tool_calls') or not response_message.tool_calls: + if not hasattr(response_message, "tool_calls") or not response_message.tool_calls: logger.info("No tool calls found in response message") return None - + if len(response_message.tool_calls) > 1: - logger.warning("Multiple tool calls detected in response message. Not a tested feature.") + logger.warning( + "Multiple tool calls detected in response message. Not a tested feature." + ) # Execute all tools first and collect their results for tool_call in response_message.tool_calls: @@ -604,56 +651,88 @@ def _handle_tooling(self, response_message, messages): "type": "tool_use", "id": tool_call.id, "name": tool_call.function.name, - "input": json.loads(tool_call.function.arguments) + "input": json.loads(tool_call.function.arguments), } - messages.append({ - "role": "assistant", - "content": [tool_use_block] - }) - - # Execute the tool - args = json.loads(tool_call.function.arguments) - tool_result = self.skills.call(tool_call.function.name, **args) - - # Add tool result to conversation history - if tool_result: - messages.append({ - "role": "user", - "content": [{ - "type": "tool_result", - "tool_use_id": tool_call.id, - "content": f"{tool_result}" - }] - }) - - def _tooling_callback(self, response_message): + messages.append({"role": "assistant", "content": [tool_use_block]}) + + try: + # Execute the tool + args = json.loads(tool_call.function.arguments) + tool_result = self.skills.call(tool_call.function.name, **args) + + # Check if the result is an error message + if isinstance(tool_result, str) and ( + "Error executing skill" in tool_result or "is not available" in tool_result + ): + # Log the error but provide a user-friendly message + logger.error(f"Tool execution failed: {tool_result}") + tool_result = "I apologize, but I'm having trouble executing that action right now. Please try again or ask for something else." + + # Add tool result to conversation history + if tool_result: + messages.append( + { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": tool_call.id, + "content": f"{tool_result}", + } + ], + } + ) + except Exception as e: + logger.error(f"Unexpected error executing tool {tool_call.function.name}: {e}") + # Add error result to conversation history + messages.append( + { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": tool_call.id, + "content": "I apologize, but I encountered an error while trying to execute that action. Please try again.", + } + ], + } + ) + + def _tooling_callback(self, response_message) -> None: """Runs the observable query for each tool call in the current response_message""" - if not hasattr(response_message, 'tool_calls') or not response_message.tool_calls: + if not hasattr(response_message, "tool_calls") or not response_message.tool_calls: return - for tool_call in response_message.tool_calls: - tool_name = tool_call.function.name - tool_id = tool_call.id - self.run_observable_query( - query_text=f"Tool {tool_name}, ID: {tool_id} execution complete. Please summarize the results and continue.", - thinking_budget_tokens=0 - ).run() + + try: + for tool_call in response_message.tool_calls: + tool_name = tool_call.function.name + tool_id = tool_call.id + self.run_observable_query( + query_text=f"Tool {tool_name}, ID: {tool_id} execution complete. Please summarize the results and continue.", + thinking_budget_tokens=0, + ).run() + except Exception as e: + logger.error(f"Error in tooling callback: {e}") + # Continue processing even if the callback fails + pass def _debug_api_call(self, claude_params: dict): """Debugging function to log API calls with truncated base64 data.""" # Remove tools to reduce verbosity import copy + log_params = copy.deepcopy(claude_params) - if 'tools' in log_params: - del log_params['tools'] - + if "tools" in log_params: + del log_params["tools"] + # Truncate base64 data in images - much cleaner approach - if 'messages' in log_params: - for msg in log_params['messages']: - if 'content' in msg: - for content in msg['content']: - if isinstance(content, dict) and content.get('type') == 'image': - source = content.get('source', {}) - if source.get('type') == 'base64' and 'data' in source: - data = source['data'] - source['data'] = f"{data[:50]}..." - return json.dumps(log_params, indent=2, default=str) \ No newline at end of file + if "messages" in log_params: + for msg in log_params["messages"]: + if "content" in msg: + for content in msg["content"]: + if isinstance(content, dict) and content.get("type") == "image": + source = content.get("source", {}) + if source.get("type") == "base64" and "data" in source: + data = source["data"] + source["data"] = f"{data[:50]}..." + return json.dumps(log_params, indent=2, default=str) diff --git a/dimos/agents/memory/base.py b/dimos/agents/memory/base.py index 340b886edc..eb48dcca44 100644 --- a/dimos/agents/memory/base.py +++ b/dimos/agents/memory/base.py @@ -12,9 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from abc import ABC, abstractmethod -import logging -from dimos.exceptions.agent_memory_exceptions import UnknownConnectionTypeError, AgentMemoryConnectionError +from abc import abstractmethod + +from dimos.exceptions.agent_memory_exceptions import ( + AgentMemoryConnectionError, + UnknownConnectionTypeError, +) from dimos.utils.logging_config import setup_logger # TODO @@ -23,8 +26,9 @@ # TODO # class AbstractAgentSymbolicMemory(AbstractAgentMemory): -class AbstractAgentSemanticMemory(): # AbstractAgentMemory): - def __init__(self, connection_type='local', **kwargs): + +class AbstractAgentSemanticMemory: # AbstractAgentMemory): + def __init__(self, connection_type: str = "local", **kwargs) -> None: """ Initialize with dynamic connection parameters. Args: @@ -34,25 +38,30 @@ def __init__(self, connection_type='local', **kwargs): AgentMemoryConnectionError: If initializing the database connection fails. """ self.logger = setup_logger(self.__class__.__name__) - self.logger.info('Initializing AgentMemory with connection type: %s', connection_type) + self.logger.info("Initializing AgentMemory with connection type: %s", connection_type) self.connection_params = kwargs - self.db_connection = None # Holds the conection, whether local or remote, to the database used. - - if connection_type not in ['local', 'remote']: - error = UnknownConnectionTypeError(f"Invalid connection_type {connection_type}. Expected 'local' or 'remote'.") + self.db_connection = ( + None # Holds the conection, whether local or remote, to the database used. + ) + + if connection_type not in ["local", "remote"]: + error = UnknownConnectionTypeError( + f"Invalid connection_type {connection_type}. Expected 'local' or 'remote'." + ) self.logger.error(str(error)) raise error try: - if connection_type == 'remote': + if connection_type == "remote": self.connect() - elif connection_type == 'local': + elif connection_type == "local": self.create() except Exception as e: self.logger.error("Failed to initialize database connection: %s", str(e), exc_info=True) - raise AgentMemoryConnectionError("Initialization failed due to an unexpected error.", cause=e) from e + raise AgentMemoryConnectionError( + "Initialization failed due to an unexpected error.", cause=e + ) from e - @abstractmethod def connect(self): """Establish a connection to the data store using dynamic parameters specified during initialization.""" @@ -77,9 +86,9 @@ def get_vector(self, vector_id): Args: vector_id (any): The identifier of the vector to retrieve. """ - + @abstractmethod - def query(self, query_texts, n_results=4, similarity_threshold=None): + def query(self, query_texts, n_results: int = 4, similarity_threshold=None): """Performs a semantic search in the vector database. Args: @@ -123,4 +132,3 @@ def delete_vector(self, vector_id): # (some sort of tag/metadata) # temporal - diff --git a/dimos/agents/memory/chroma_impl.py b/dimos/agents/memory/chroma_impl.py index 369003d290..b238b616d8 100644 --- a/dimos/agents/memory/chroma_impl.py +++ b/dimos/agents/memory/chroma_impl.py @@ -12,29 +12,30 @@ # See the License for the specific language governing permissions and # limitations under the License. -from dimos.agents.memory.base import AbstractAgentSemanticMemory +from collections.abc import Sequence +import os -import chromadb -from langchain_openai import OpenAIEmbeddings from langchain_chroma import Chroma -import os +from langchain_openai import OpenAIEmbeddings import torch +from dimos.agents.memory.base import AbstractAgentSemanticMemory + class ChromaAgentSemanticMemory(AbstractAgentSemanticMemory): """Base class for Chroma-based semantic memory implementations.""" - - def __init__(self, collection_name="my_collection"): + + def __init__(self, collection_name: str = "my_collection") -> None: """Initialize the connection to the local Chroma DB.""" self.collection_name = collection_name self.db_connection = None self.embeddings = None - super().__init__(connection_type='local') + super().__init__(connection_type="local") def connect(self): # Stub return super().connect() - + def create(self): """Create the embedding function and initialize the Chroma database. This method must be implemented by child classes.""" @@ -52,27 +53,22 @@ def add_vector(self, vector_id, vector_data): def get_vector(self, vector_id): """Retrieve a vector from the ChromaDB by its identifier.""" - result = self.db_connection.get(include=['embeddings'], ids=[vector_id]) + result = self.db_connection.get(include=["embeddings"], ids=[vector_id]) return result - def query(self, query_texts, n_results=4, similarity_threshold=None): + def query(self, query_texts, n_results: int = 4, similarity_threshold=None): """Query the collection with a specific text and return up to n results.""" if not self.db_connection: raise Exception("Collection not initialized. Call connect() first.") - + if similarity_threshold is not None: if not (0 <= similarity_threshold <= 1): raise ValueError("similarity_threshold must be between 0 and 1.") return self.db_connection.similarity_search_with_relevance_scores( - query=query_texts, - k=n_results, - score_threshold=similarity_threshold + query=query_texts, k=n_results, score_threshold=similarity_threshold ) else: - documents = self.db_connection.similarity_search( - query=query_texts, - k=n_results - ) + documents = self.db_connection.similarity_search(query=query_texts, k=n_results) return [(doc, None) for doc in documents] def update_vector(self, vector_id, new_vector_data): @@ -88,10 +84,15 @@ def delete_vector(self, vector_id): class OpenAISemanticMemory(ChromaAgentSemanticMemory): """Semantic memory implementation using OpenAI's embedding API.""" - - def __init__(self, collection_name="my_collection", model="text-embedding-3-large", dimensions=1024): + + def __init__( + self, + collection_name: str = "my_collection", + model: str = "text-embedding-3-large", + dimensions: int = 1024, + ) -> None: """Initialize OpenAI-based semantic memory. - + Args: collection_name (str): Name of the Chroma collection model (str): OpenAI embedding model to use @@ -119,46 +120,49 @@ def create(self): self.db_connection = Chroma( collection_name=self.collection_name, embedding_function=self.embeddings, - collection_metadata={"hnsw:space": "cosine"} + collection_metadata={"hnsw:space": "cosine"}, ) class LocalSemanticMemory(ChromaAgentSemanticMemory): """Semantic memory implementation using local models.""" - - def __init__(self, collection_name="my_collection", model_name="sentence-transformers/all-MiniLM-L6-v2"): + + def __init__( + self, + collection_name: str = "my_collection", + model_name: str = "sentence-transformers/all-MiniLM-L6-v2", + ) -> None: """Initialize the local semantic memory using SentenceTransformer. - + Args: collection_name (str): Name of the Chroma collection model_name (str): Embeddings model """ - from sentence_transformers import SentenceTransformer - + self.model_name = model_name super().__init__(collection_name=collection_name) - def create(self): + def create(self) -> None: """Create local embedding model and initialize the ChromaDB client.""" # Load the sentence transformer model # Use CUDA if available, otherwise fall back to CPU - device = 'cuda' if torch.cuda.is_available() else 'cpu' + device = "cuda" if torch.cuda.is_available() else "cpu" print(f"Using device: {device}") self.model = SentenceTransformer(self.model_name, device=device) - + # Create a custom embedding class that implements the embed_query method class SentenceTransformerEmbeddings: - def __init__(self, model): + def __init__(self, model) -> None: self.model = model - - def embed_query(self, text): + + def embed_query(self, text: str): """Embed a single query text.""" return self.model.encode(text, normalize_embeddings=True).tolist() - - def embed_documents(self, texts): + + def embed_documents(self, texts: Sequence[str]): """Embed multiple documents/texts.""" return self.model.encode(texts, normalize_embeddings=True).tolist() - + # Create an instance of our custom embeddings class self.embeddings = SentenceTransformerEmbeddings(self.model) @@ -166,6 +170,5 @@ def embed_documents(self, texts): self.db_connection = Chroma( collection_name=self.collection_name, embedding_function=self.embeddings, - collection_metadata={"hnsw:space": "cosine"} + collection_metadata={"hnsw:space": "cosine"}, ) - diff --git a/dimos/agents/memory/image_embedding.py b/dimos/agents/memory/image_embedding.py index 49c1ec67b6..7b6dd88515 100644 --- a/dimos/agents/memory/image_embedding.py +++ b/dimos/agents/memory/image_embedding.py @@ -19,30 +19,32 @@ using pre-trained models like CLIP, ResNet, etc. """ +import base64 +import io import os -import logging + +import cv2 import numpy as np -from typing import Union, List, Dict, Any from PIL import Image -import io -import cv2 -import base64 + +from dimos.utils.data import get_data from dimos.utils.logging_config import setup_logger logger = setup_logger("dimos.agents.memory.image_embedding") + class ImageEmbeddingProvider: """ A provider for generating vector embeddings from images. - + This class uses pre-trained models to convert images into vector embeddings that can be stored in a vector database and used for similarity search. """ - - def __init__(self, model_name: str = "clip", dimensions: int = 512): + + def __init__(self, model_name: str = "clip", dimensions: int = 512) -> None: """ Initialize the image embedding provider. - + Args: model_name: Name of the embedding model to use ("clip", "resnet", etc.) dimensions: Dimensions of the embedding vectors @@ -51,22 +53,31 @@ def __init__(self, model_name: str = "clip", dimensions: int = 512): self.dimensions = dimensions self.model = None self.processor = None - + self.model_path = None + self._initialize_model() - + logger.info(f"ImageEmbeddingProvider initialized with model {model_name}") - + def _initialize_model(self): """Initialize the specified embedding model.""" try: + import onnxruntime as ort import torch - from transformers import CLIPProcessor, CLIPModel, AutoFeatureExtractor, AutoModel - + from transformers import AutoFeatureExtractor, AutoModel, CLIPProcessor + if self.model_name == "clip": - model_id = "openai/clip-vit-base-patch32" - self.model = CLIPModel.from_pretrained(model_id) - self.processor = CLIPProcessor.from_pretrained(model_id) - logger.info(f"Loaded CLIP model: {model_id}") + model_id = get_data("models_clip") / "model.onnx" + self.model_path = str(model_id) # Store for pickling + processor_id = "openai/clip-vit-base-patch32" + + providers = ["CUDAExecutionProvider", "CPUExecutionProvider"] + + self.model = ort.InferenceSession(str(model_id), providers=providers) + + actual_providers = self.model.get_providers() + self.processor = CLIPProcessor.from_pretrained(processor_id) + logger.info(f"Loaded CLIP model: {model_id} with providers: {actual_providers}") elif self.model_name == "resnet": model_id = "microsoft/resnet-50" self.model = AutoModel.from_pretrained(model_id) @@ -81,102 +92,154 @@ def _initialize_model(self): self.model = None self.processor = None raise - - def get_embedding(self, image: Union[np.ndarray, str, bytes]) -> np.ndarray: + + def get_embedding(self, image: np.ndarray | str | bytes) -> np.ndarray: """ Generate an embedding vector for the provided image. - + Args: image: The image to embed, can be a numpy array (OpenCV format), a file path, or a base64-encoded string - + Returns: A numpy array containing the embedding vector """ if self.model is None or self.processor is None: logger.error("Model not initialized. Using fallback random embedding.") return np.random.randn(self.dimensions).astype(np.float32) - + pil_image = self._prepare_image(image) - + try: import torch - + if self.model_name == "clip": - inputs = self.processor(images=pil_image, return_tensors="pt") - + inputs = self.processor(images=pil_image, return_tensors="np") + with torch.no_grad(): - image_features = self.model.get_image_features(**inputs) - - image_embedding = image_features / image_features.norm(dim=1, keepdim=True) - embedding = image_embedding.numpy()[0] - + ort_inputs = { + inp.name: inputs[inp.name] + for inp in self.model.get_inputs() + if inp.name in inputs + } + + # If required, add dummy text inputs + input_names = [i.name for i in self.model.get_inputs()] + batch_size = inputs["pixel_values"].shape[0] + if "input_ids" in input_names: + ort_inputs["input_ids"] = np.zeros((batch_size, 1), dtype=np.int64) + if "attention_mask" in input_names: + ort_inputs["attention_mask"] = np.ones((batch_size, 1), dtype=np.int64) + + # Run inference + ort_outputs = self.model.run(None, ort_inputs) + + # Look up correct output name + output_names = [o.name for o in self.model.get_outputs()] + if "image_embeds" in output_names: + image_embedding = ort_outputs[output_names.index("image_embeds")] + else: + raise RuntimeError(f"No 'image_embeds' found in outputs: {output_names}") + + embedding = image_embedding / np.linalg.norm(image_embedding, axis=1, keepdims=True) + embedding = embedding[0] + elif self.model_name == "resnet": inputs = self.processor(images=pil_image, return_tensors="pt") - + with torch.no_grad(): outputs = self.model(**inputs) - + # Get the [CLS] token embedding embedding = outputs.last_hidden_state[:, 0, :].numpy()[0] else: logger.warning(f"Unsupported model: {self.model_name}. Using random embedding.") embedding = np.random.randn(self.dimensions).astype(np.float32) - + # Normalize and ensure correct dimensions embedding = embedding / np.linalg.norm(embedding) - + logger.debug(f"Generated embedding with shape {embedding.shape}") return embedding - + except Exception as e: logger.error(f"Error generating embedding: {e}") return np.random.randn(self.dimensions).astype(np.float32) - + def get_text_embedding(self, text: str) -> np.ndarray: """ Generate an embedding vector for the provided text. - + Args: text: The text to embed - + Returns: A numpy array containing the embedding vector """ if self.model is None or self.processor is None: logger.error("Model not initialized. Using fallback random embedding.") return np.random.randn(self.dimensions).astype(np.float32) - + if self.model_name != "clip": - logger.warning(f"Text embeddings are only supported with CLIP model, not {self.model_name}. Using random embedding.") + logger.warning( + f"Text embeddings are only supported with CLIP model, not {self.model_name}. Using random embedding." + ) return np.random.randn(self.dimensions).astype(np.float32) - + try: import torch - - inputs = self.processor(text=[text], return_tensors="pt", padding=True) - + + inputs = self.processor(text=[text], return_tensors="np", padding=True) + with torch.no_grad(): - text_features = self.model.get_text_features(**inputs) - - # Normalize the features - text_embedding = text_features / text_features.norm(dim=1, keepdim=True) - embedding = text_embedding.numpy()[0] - - logger.debug(f"Generated text embedding with shape {embedding.shape} for text: '{text}'") - return embedding - + # Prepare ONNX input dict (handle only what's needed) + ort_inputs = { + inp.name: inputs[inp.name] + for inp in self.model.get_inputs() + if inp.name in inputs + } + # Determine which inputs are expected by the ONNX model + input_names = [i.name for i in self.model.get_inputs()] + batch_size = inputs["input_ids"].shape[0] # pulled from text input + + # If the model expects pixel_values (i.e., fused model), add dummy vision input + if "pixel_values" in input_names: + ort_inputs["pixel_values"] = np.zeros( + (batch_size, 3, 224, 224), dtype=np.float32 + ) + + # Run inference + ort_outputs = self.model.run(None, ort_inputs) + + # Determine correct output (usually 'last_hidden_state' or 'text_embeds') + output_names = [o.name for o in self.model.get_outputs()] + if "text_embeds" in output_names: + text_embedding = ort_outputs[output_names.index("text_embeds")] + else: + text_embedding = ort_outputs[0] # fallback to first output + + # Normalize + text_embedding = text_embedding / np.linalg.norm( + text_embedding, axis=1, keepdims=True + ) + text_embedding = text_embedding[0] # shape: (512,) + + logger.debug( + f"Generated text embedding with shape {text_embedding.shape} for text: '{text}'" + ) + return text_embedding + except Exception as e: logger.error(f"Error generating text embedding: {e}") return np.random.randn(self.dimensions).astype(np.float32) - - def _prepare_image(self, image: Union[np.ndarray, str, bytes]) -> Image.Image: + + def _prepare_image(self, image: np.ndarray | str | bytes) -> Image.Image: """ Convert the input image to PIL format required by the models. - + Args: image: Input image in various formats - + Returns: PIL Image object """ @@ -185,9 +248,9 @@ def _prepare_image(self, image: Union[np.ndarray, str, bytes]) -> Image.Image: image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) else: image_rgb = image - + return Image.fromarray(image_rgb) - + elif isinstance(image, str): if os.path.isfile(image): return Image.open(image) @@ -198,9 +261,9 @@ def _prepare_image(self, image: Union[np.ndarray, str, bytes]) -> Image.Image: except Exception as e: logger.error(f"Failed to decode image string: {e}") raise ValueError("Invalid image string format") - + elif isinstance(image, bytes): return Image.open(io.BytesIO(image)) - + else: raise ValueError(f"Unsupported image format: {type(image)}") diff --git a/dimos/agents/memory/spatial_vector_db.py b/dimos/agents/memory/spatial_vector_db.py index 247c8dee65..ac5dcc026a 100644 --- a/dimos/agents/memory/spatial_vector_db.py +++ b/dimos/agents/memory/spatial_vector_db.py @@ -19,47 +19,50 @@ their XY locations and querying by location or image similarity. """ -import os -import logging -import numpy as np -import cv2 -import json -import base64 -from typing import List, Dict, Tuple, Any, Optional, Union +from typing import Any + import chromadb -from chromadb.utils import embedding_functions +import numpy as np -from dimos.agents.memory.base import AbstractAgentSemanticMemory from dimos.agents.memory.visual_memory import VisualMemory +from dimos.types.robot_location import RobotLocation from dimos.utils.logging_config import setup_logger logger = setup_logger("dimos.agents.memory.spatial_vector_db") + class SpatialVectorDB: """ A vector database for storing and querying images mapped to X,Y,theta absolute locations for SpatialMemory. - + This class extends the ChromaDB implementation to support storing images with their absolute locations and querying by location, text, or image cosine semantic similarity. """ - - def __init__(self, collection_name: str = "spatial_memory", chroma_client=None, visual_memory=None): + + def __init__( + self, + collection_name: str = "spatial_memory", + chroma_client=None, + visual_memory=None, + embedding_provider=None, + ) -> None: """ Initialize the spatial vector database. - + Args: collection_name: Name of the vector database collection chroma_client: Optional ChromaDB client for persistence. If None, an in-memory client is used. visual_memory: Optional VisualMemory instance for storing images. If None, a new one is created. + embedding_provider: Optional ImageEmbeddingProvider instance for computing embeddings. If None, one will be created. """ self.collection_name = collection_name - + # Use provided client or create in-memory client self.client = chroma_client if chroma_client is not None else chromadb.Client() - + # Check if collection already exists - in newer ChromaDB versions list_collections returns names directly existing_collections = self.client.list_collections() - + # Handle different versions of ChromaDB API try: collection_exists = collection_name in existing_collections @@ -72,32 +75,45 @@ def __init__(self, collection_name: str = "spatial_memory", chroma_client=None, collection_exists = True except Exception: collection_exists = False - + # Get or create the collection self.image_collection = self.client.get_or_create_collection( - name=collection_name, - metadata={"hnsw:space": "cosine"} + name=collection_name, metadata={"hnsw:space": "cosine"} ) - + # Use provided visual memory or create a new one self.visual_memory = visual_memory if visual_memory is not None else VisualMemory() - + + # Store the embedding provider to reuse for all operations + self.embedding_provider = embedding_provider + + # Initialize the location collection for text-based location tagging + location_collection_name = f"{collection_name}_locations" + self.location_collection = self.client.get_or_create_collection( + name=location_collection_name, metadata={"hnsw:space": "cosine"} + ) + # Log initialization info with details about whether using existing collection client_type = "persistent" if chroma_client is not None else "in-memory" try: - count = len(self.image_collection.get(include=[])['ids']) + count = len(self.image_collection.get(include=[])["ids"]) if collection_exists: - logger.info(f"Using EXISTING {client_type} collection '{collection_name}' with {count} entries") + logger.info( + f"Using EXISTING {client_type} collection '{collection_name}' with {count} entries" + ) else: logger.info(f"Created NEW {client_type} collection '{collection_name}'") except Exception as e: - logger.info(f"Initialized {client_type} collection '{collection_name}' (count error: {str(e)})") - - def add_image_vector(self, vector_id: str, image: np.ndarray, embedding: np.ndarray, - metadata: Dict[str, Any]) -> None: + logger.info( + f"Initialized {client_type} collection '{collection_name}' (count error: {e!s})" + ) + + def add_image_vector( + self, vector_id: str, image: np.ndarray, embedding: np.ndarray, metadata: dict[str, Any] + ) -> None: """ Add an image with its embedding and metadata to the vector database. - + Args: vector_id: Unique identifier for the vector image: The image to store @@ -106,157 +122,213 @@ def add_image_vector(self, vector_id: str, image: np.ndarray, embedding: np.ndar """ # Store the image in visual memory self.visual_memory.add(vector_id, image) - + # Add the vector to ChromaDB self.image_collection.add( - ids=[vector_id], - embeddings=[embedding.tolist()], - metadatas=[metadata] + ids=[vector_id], embeddings=[embedding.tolist()], metadatas=[metadata] ) - - logger.debug(f"Added image vector {vector_id} with metadata: {metadata}") - - def query_by_embedding(self, embedding: np.ndarray, limit: int = 5) -> List[Dict]: + + logger.info(f"Added image vector {vector_id} with metadata: {metadata}") + + def query_by_embedding(self, embedding: np.ndarray, limit: int = 5) -> list[dict]: """ Query the vector database for images similar to the provided embedding. - + Args: embedding: Query embedding vector limit: Maximum number of results to return - + Returns: List of results, each containing the image and its metadata """ results = self.image_collection.query( - query_embeddings=[embedding.tolist()], - n_results=limit + query_embeddings=[embedding.tolist()], n_results=limit ) - + return self._process_query_results(results) - # TODO: implement efficient nearest neighbor search - def query_by_location(self, x: float, y: float, radius: float = 2.0, limit: int = 5) -> List[Dict]: + + # TODO: implement efficient nearest neighbor search + def query_by_location( + self, x: float, y: float, radius: float = 2.0, limit: int = 5 + ) -> list[dict]: """ Query the vector database for images near the specified location. - + Args: x: X coordinate y: Y coordinate radius: Search radius in meters limit: Maximum number of results to return - + Returns: List of results, each containing the image and its metadata """ results = self.image_collection.get() - - if not results or not results['ids']: + + if not results or not results["ids"]: return [] - - filtered_results = { - 'ids': [], - 'metadatas': [], - 'distances': [] - } - - for i, metadata in enumerate(results['metadatas']): - item_x = metadata.get('x') - item_y = metadata.get('y') - + + filtered_results = {"ids": [], "metadatas": [], "distances": []} + + for i, metadata in enumerate(results["metadatas"]): + item_x = metadata.get("x") + item_y = metadata.get("y") + if item_x is not None and item_y is not None: - distance = np.sqrt((x - item_x)**2 + (y - item_y)**2) - + distance = np.sqrt((x - item_x) ** 2 + (y - item_y) ** 2) + if distance <= radius: - filtered_results['ids'].append(results['ids'][i]) - filtered_results['metadatas'].append(metadata) - filtered_results['distances'].append(distance) - - sorted_indices = np.argsort(filtered_results['distances']) - filtered_results['ids'] = [filtered_results['ids'][i] for i in sorted_indices[:limit]] - filtered_results['metadatas'] = [filtered_results['metadatas'][i] for i in sorted_indices[:limit]] - filtered_results['distances'] = [filtered_results['distances'][i] for i in sorted_indices[:limit]] - + filtered_results["ids"].append(results["ids"][i]) + filtered_results["metadatas"].append(metadata) + filtered_results["distances"].append(distance) + + sorted_indices = np.argsort(filtered_results["distances"]) + filtered_results["ids"] = [filtered_results["ids"][i] for i in sorted_indices[:limit]] + filtered_results["metadatas"] = [ + filtered_results["metadatas"][i] for i in sorted_indices[:limit] + ] + filtered_results["distances"] = [ + filtered_results["distances"][i] for i in sorted_indices[:limit] + ] + return self._process_query_results(filtered_results) - - def _process_query_results(self, results) -> List[Dict]: + + def _process_query_results(self, results) -> list[dict]: """Process query results to include decoded images.""" - if not results or not results['ids']: + if not results or not results["ids"]: return [] - + processed_results = [] - - for i, vector_id in enumerate(results['ids']): + + for i, vector_id in enumerate(results["ids"]): + if isinstance(vector_id, list) and not vector_id: + continue + lookup_id = vector_id[0] if isinstance(vector_id, list) else vector_id - + # Create the result dictionary with metadata regardless of image availability result = { - 'metadata': results['metadatas'][i] if 'metadatas' in results else {}, - 'id': lookup_id + "metadata": results["metadatas"][i] if "metadatas" in results else {}, + "id": lookup_id, } - + # Add distance if available - if 'distances' in results: - result['distance'] = results['distances'][i][0] if isinstance(results['distances'][i], list) else results['distances'][i] - + if "distances" in results: + result["distance"] = ( + results["distances"][i][0] + if isinstance(results["distances"][i], list) + else results["distances"][i] + ) + # Get the image from visual memory image = self.visual_memory.get(lookup_id) - result['image'] = image - + result["image"] = image + processed_results.append(result) - + return processed_results - - def query_by_text(self, text: str, limit: int = 5) -> List[Dict]: + + def query_by_text(self, text: str, limit: int = 5) -> list[dict]: """ Query the vector database for images matching the provided text description. - + This method uses CLIP's text-to-image matching capability to find images that semantically match the text query (e.g., "where is the kitchen"). - + Args: text: Text query to search for limit: Maximum number of results to return - + Returns: List of results, each containing the image, its metadata, and similarity score """ - from dimos.agents.memory.image_embedding import ImageEmbeddingProvider - - embedding_provider = ImageEmbeddingProvider(model_name="clip") - - text_embedding = embedding_provider.get_text_embedding(text) - + if self.embedding_provider is None: + from dimos.agents.memory.image_embedding import ImageEmbeddingProvider + + self.embedding_provider = ImageEmbeddingProvider(model_name="clip") + + text_embedding = self.embedding_provider.get_text_embedding(text) + results = self.image_collection.query( query_embeddings=[text_embedding.tolist()], n_results=limit, - include=["documents", "metadatas", "distances"] + include=["documents", "metadatas", "distances"], + ) + + logger.info( + f"Text query: '{text}' returned {len(results['ids'] if 'ids' in results else [])} results" ) - - logger.info(f"Text query: '{text}' returned {len(results['ids'] if 'ids' in results else [])} results") return self._process_query_results(results) - - def get_all_locations(self) -> List[Tuple[float, float, float]]: + + def get_all_locations(self) -> list[tuple[float, float, float]]: """Get all locations stored in the database.""" # Get all items from the collection without embeddings results = self.image_collection.get(include=["metadatas"]) - + if not results or "metadatas" not in results or not results["metadatas"]: return [] - + # Extract x, y coordinates from metadata locations = [] for metadata in results["metadatas"]: if isinstance(metadata, list) and metadata and isinstance(metadata[0], dict): metadata = metadata[0] # Handle nested metadata - + if isinstance(metadata, dict) and "x" in metadata and "y" in metadata: x = metadata.get("x", 0) y = metadata.get("y", 0) z = metadata.get("z", 0) if "z" in metadata else 0 locations.append((x, y, z)) - + return locations - + @property def image_storage(self): """Legacy accessor for compatibility with existing code.""" return self.visual_memory.images + + def tag_location(self, location: RobotLocation) -> None: + """ + Tag a location with a semantic name/description for text-based retrieval. + + Args: + location: RobotLocation object with position/rotation data + """ + + location_id = location.location_id + metadata = location.to_vector_metadata() + + self.location_collection.add( + ids=[location_id], documents=[location.name], metadatas=[metadata] + ) + + def query_tagged_location(self, query: str) -> tuple[RobotLocation | None, float]: + """ + Query for a tagged location using semantic text search. + + Args: + query: Natural language query (e.g., "dining area", "place to eat") + + Returns: + The best matching RobotLocation or None if no matches found + """ + + results = self.location_collection.query( + query_texts=[query], n_results=1, include=["metadatas", "documents", "distances"] + ) + + if not (results and results["ids"] and len(results["ids"][0]) > 0): + return None, 0 + + best_match_metadata = results["metadatas"][0][0] + distance = float(results["distances"][0][0] if "distances" in results else 0.0) + + location = RobotLocation.from_vector_metadata(best_match_metadata) + + logger.info( + f"Found location '{location.name}' for query '{query}' (distance: {distance:.3f})" + if distance + else "" + ) + + return location, distance diff --git a/dimos/agents/memory/test_image_embedding.py b/dimos/agents/memory/test_image_embedding.py new file mode 100644 index 0000000000..b1e7cabf09 --- /dev/null +++ b/dimos/agents/memory/test_image_embedding.py @@ -0,0 +1,214 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Test module for the CLIP image embedding functionality in dimos. +""" + +import os +import time + +import numpy as np +import pytest +from reactivex import operators as ops + +from dimos.agents.memory.image_embedding import ImageEmbeddingProvider +from dimos.stream.video_provider import VideoProvider + + +@pytest.mark.heavy +class TestImageEmbedding: + """Test class for CLIP image embedding functionality.""" + + @pytest.mark.tofix + def test_clip_embedding_initialization(self) -> None: + """Test CLIP embedding provider initializes correctly.""" + try: + # Initialize the embedding provider with CLIP model + embedding_provider = ImageEmbeddingProvider(model_name="clip", dimensions=512) + assert embedding_provider.model is not None, "CLIP model failed to initialize" + assert embedding_provider.processor is not None, "CLIP processor failed to initialize" + assert embedding_provider.model_name == "clip", "Model name should be 'clip'" + assert embedding_provider.dimensions == 512, "Embedding dimensions should be 512" + except Exception as e: + pytest.skip(f"Skipping test due to model initialization error: {e}") + + @pytest.mark.tofix + def test_clip_embedding_process_video(self) -> None: + """Test CLIP embedding provider can process video frames and return embeddings.""" + try: + from dimos.utils.data import get_data + + video_path = get_data("assets") / "trimmed_video_office.mov" + + embedding_provider = ImageEmbeddingProvider(model_name="clip", dimensions=512) + + assert os.path.exists(video_path), f"Test video not found: {video_path}" + video_provider = VideoProvider(dev_name="test_video", video_source=video_path) + + video_stream = video_provider.capture_video_as_observable(realtime=False, fps=15) + + # Use ReactiveX operators to process the stream + def process_frame(frame): + try: + # Process frame with CLIP + embedding = embedding_provider.get_embedding(frame) + print( + f"Generated CLIP embedding with shape: {embedding.shape}, norm: {np.linalg.norm(embedding):.4f}" + ) + + return {"frame": frame, "embedding": embedding} + except Exception as e: + print(f"Error in process_frame: {e}") + return None + + embedding_stream = video_stream.pipe(ops.map(process_frame)) + + results = [] + frames_processed = 0 + target_frames = 10 + + def on_next(result) -> None: + nonlocal frames_processed, results + if not result: # Skip None results + return + + results.append(result) + frames_processed += 1 + + # Stop processing after target frames + if frames_processed >= target_frames: + subscription.dispose() + + def on_error(error) -> None: + pytest.fail(f"Error in embedding stream: {error}") + + def on_completed() -> None: + pass + + # Subscribe and wait for results + subscription = embedding_stream.subscribe( + on_next=on_next, on_error=on_error, on_completed=on_completed + ) + + timeout = 60.0 + start_time = time.time() + while frames_processed < target_frames and time.time() - start_time < timeout: + time.sleep(0.5) + print(f"Processed {frames_processed}/{target_frames} frames") + + # Clean up subscription + subscription.dispose() + video_provider.dispose_all() + + # Check if we have results + if len(results) == 0: + pytest.skip("No embeddings generated, but test connection established correctly") + return + + print(f"Processed {len(results)} frames with CLIP embeddings") + + # Analyze the results + assert len(results) > 0, "No embeddings generated" + + # Check properties of first embedding + first_result = results[0] + assert "embedding" in first_result, "Result doesn't contain embedding" + assert "frame" in first_result, "Result doesn't contain frame" + + # Check embedding shape and normalization + embedding = first_result["embedding"] + assert isinstance(embedding, np.ndarray), "Embedding is not a numpy array" + assert embedding.shape == (512,), ( + f"Embedding has wrong shape: {embedding.shape}, expected (512,)" + ) + assert abs(np.linalg.norm(embedding) - 1.0) < 1e-5, "Embedding is not normalized" + + # Save the first embedding for similarity tests + if len(results) > 1 and "embedding" in results[0]: + # Create a class variable to store embeddings for the similarity test + TestImageEmbedding.test_embeddings = { + "embedding1": results[0]["embedding"], + "embedding2": results[1]["embedding"] if len(results) > 1 else None, + } + print("Saved embeddings for similarity testing") + + print("CLIP embedding test passed successfully!") + + except Exception as e: + pytest.fail(f"Test failed with error: {e}") + + @pytest.mark.tofix + def test_clip_embedding_similarity(self) -> None: + """Test CLIP embedding similarity search and text-to-image queries.""" + try: + # Skip if previous test didn't generate embeddings + if not hasattr(TestImageEmbedding, "test_embeddings"): + pytest.skip("No embeddings available from previous test") + return + + # Get embeddings from previous test + embedding1 = TestImageEmbedding.test_embeddings["embedding1"] + embedding2 = TestImageEmbedding.test_embeddings["embedding2"] + + # Initialize embedding provider for text embeddings + embedding_provider = ImageEmbeddingProvider(model_name="clip", dimensions=512) + + # Test frame-to-frame similarity + if embedding1 is not None and embedding2 is not None: + # Compute cosine similarity + similarity = np.dot(embedding1, embedding2) + print(f"Similarity between first two frames: {similarity:.4f}") + + # Should be in range [-1, 1] + assert -1.0 <= similarity <= 1.0, f"Similarity out of valid range: {similarity}" + + # Test text-to-image similarity + if embedding1 is not None: + # Generate a list of text queries to test + text_queries = ["a video frame", "a person", "an outdoor scene", "a kitchen"] + + # Test each text query + for text_query in text_queries: + # Get text embedding + text_embedding = embedding_provider.get_text_embedding(text_query) + + # Check text embedding properties + assert isinstance(text_embedding, np.ndarray), ( + "Text embedding is not a numpy array" + ) + assert text_embedding.shape == (512,), ( + f"Text embedding has wrong shape: {text_embedding.shape}" + ) + assert abs(np.linalg.norm(text_embedding) - 1.0) < 1e-5, ( + "Text embedding is not normalized" + ) + + # Compute similarity between frame and text + text_similarity = np.dot(embedding1, text_embedding) + print(f"Similarity between frame and '{text_query}': {text_similarity:.4f}") + + # Should be in range [-1, 1] + assert -1.0 <= text_similarity <= 1.0, ( + f"Text-image similarity out of range: {text_similarity}" + ) + + print("CLIP embedding similarity tests passed successfully!") + + except Exception as e: + pytest.fail(f"Similarity test failed with error: {e}") + + +if __name__ == "__main__": + pytest.main(["-v", "--disable-warnings", __file__]) diff --git a/dimos/agents/memory/visual_memory.py b/dimos/agents/memory/visual_memory.py index fb520c9c85..90f1272fef 100644 --- a/dimos/agents/memory/visual_memory.py +++ b/dimos/agents/memory/visual_memory.py @@ -15,78 +15,81 @@ """ Visual memory storage for managing image data persistence and retrieval """ + +import base64 import os import pickle -import base64 -import logging -import numpy as np + import cv2 +import numpy as np -from typing import Dict, Optional, Tuple, Any, List from dimos.utils.logging_config import setup_logger logger = setup_logger("dimos.agents.memory.visual_memory") + class VisualMemory: """ A class for storing and retrieving visual memories (images) with persistence. - + This class handles the storage, encoding, and retrieval of images associated with vector database entries. It provides persistence mechanisms to save and load the image data from disk. """ - - def __init__(self, output_dir: str = None): + + def __init__(self, output_dir: str | None = None) -> None: """ Initialize the visual memory system. - + Args: output_dir: Directory to store the serialized image data """ self.images = {} # Maps IDs to encoded images self.output_dir = output_dir - + if output_dir: os.makedirs(output_dir, exist_ok=True) logger.info(f"VisualMemory initialized with output directory: {output_dir}") else: logger.info("VisualMemory initialized with no persistence directory") - + def add(self, image_id: str, image: np.ndarray) -> None: """ Add an image to visual memory. - + Args: image_id: Unique identifier for the image image: The image data as a numpy array """ # Encode the image to base64 for storage - success, encoded_image = cv2.imencode('.jpg', image) + success, encoded_image = cv2.imencode(".jpg", image) if not success: logger.error(f"Failed to encode image {image_id}") return - + image_bytes = encoded_image.tobytes() - b64_encoded = base64.b64encode(image_bytes).decode('utf-8') - + b64_encoded = base64.b64encode(image_bytes).decode("utf-8") + # Store the encoded image self.images[image_id] = b64_encoded logger.debug(f"Added image {image_id} to visual memory") - - def get(self, image_id: str) -> Optional[np.ndarray]: + + def get(self, image_id: str) -> np.ndarray | None: """ Retrieve an image from visual memory. - + Args: image_id: Unique identifier for the image - + Returns: The decoded image as a numpy array, or None if not found """ if image_id not in self.images: - logger.warning(f"Image not found in storage for ID {image_id}. Incomplete or corrupted image storage.") + logger.warning( + f"Image not found in storage for ID {image_id}. Incomplete or corrupted image storage." + ) return None - + try: encoded_image = self.images[image_id] image_bytes = base64.b64decode(encoded_image) @@ -94,85 +97,85 @@ def get(self, image_id: str) -> Optional[np.ndarray]: image = cv2.imdecode(image_array, cv2.IMREAD_COLOR) return image except Exception as e: - logger.warning(f"Failed to decode image for ID {image_id}: {str(e)}") + logger.warning(f"Failed to decode image for ID {image_id}: {e!s}") return None - + def contains(self, image_id: str) -> bool: """ Check if an image ID exists in visual memory. - + Args: image_id: Unique identifier for the image - + Returns: True if the image exists, False otherwise """ return image_id in self.images - + def count(self) -> int: """ Get the number of images in visual memory. - + Returns: The number of images stored """ return len(self.images) - - def save(self, filename: Optional[str] = None) -> str: + + def save(self, filename: str | None = None) -> str: """ Save the visual memory to disk. - + Args: filename: Optional filename to save to. If None, uses a default name in the output directory. - + Returns: The path where the data was saved """ if not self.output_dir: logger.warning("No output directory specified for VisualMemory. Cannot save.") return "" - + if not filename: filename = "visual_memory.pkl" - + output_path = os.path.join(self.output_dir, filename) - + try: - with open(output_path, 'wb') as f: + with open(output_path, "wb") as f: pickle.dump(self.images, f) logger.info(f"Saved {len(self.images)} images to {output_path}") return output_path except Exception as e: - logger.error(f"Failed to save visual memory: {str(e)}") + logger.error(f"Failed to save visual memory: {e!s}") return "" - + @classmethod - def load(cls, path: str, output_dir: Optional[str] = None) -> 'VisualMemory': + def load(cls, path: str, output_dir: str | None = None) -> "VisualMemory": """ Load visual memory from disk. - + Args: path: Path to the saved visual memory file output_dir: Optional output directory for the new instance - + Returns: A new VisualMemory instance with the loaded data """ instance = cls(output_dir=output_dir) - + if not os.path.exists(path): logger.warning(f"Visual memory file {path} not found") return instance - + try: - with open(path, 'rb') as f: + with open(path, "rb") as f: instance.images = pickle.load(f) logger.info(f"Loaded {len(instance.images)} images from {path}") return instance except Exception as e: - logger.error(f"Failed to load visual memory: {str(e)}") + logger.error(f"Failed to load visual memory: {e!s}") return instance - + def clear(self) -> None: """Clear all images from memory.""" self.images = {} diff --git a/dimos/data/diffusion.py b/dimos/agents/modules/__init__.py similarity index 94% rename from dimos/data/diffusion.py rename to dimos/agents/modules/__init__.py index e99e9c2ef4..ee1269f8f5 100644 --- a/dimos/data/diffusion.py +++ b/dimos/agents/modules/__init__.py @@ -12,3 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Agent modules for DimOS.""" diff --git a/dimos/agents/modules/agent_pool.py b/dimos/agents/modules/agent_pool.py new file mode 100644 index 0000000000..08ef943765 --- /dev/null +++ b/dimos/agents/modules/agent_pool.py @@ -0,0 +1,232 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Agent pool module for managing multiple agents.""" + +from typing import Any + +from reactivex import operators as ops +from reactivex.subject import Subject + +from dimos.agents.modules.base_agent import BaseAgentModule +from dimos.agents.modules.unified_agent import UnifiedAgentModule +from dimos.core import In, Module, Out, rpc +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.agents.modules.agent_pool") + + +class AgentPoolModule(Module): + """Lightweight agent pool for managing multiple agents. + + This module enables: + - Multiple agent deployment with different configurations + - Query routing based on agent ID or capabilities + - Load balancing across agents + - Response aggregation from multiple agents + """ + + # Module I/O + query_in: In[dict[str, Any]] = None # {agent_id: str, query: str, ...} + response_out: Out[dict[str, Any]] = None # {agent_id: str, response: str, ...} + + def __init__( + self, agents_config: dict[str, dict[str, Any]], default_agent: str | None = None + ) -> None: + """Initialize agent pool. + + Args: + agents_config: Configuration for each agent + { + "agent_id": { + "model": "openai::gpt-4o", + "skills": SkillLibrary(), + "system_prompt": "...", + ... + } + } + default_agent: Default agent ID to use if not specified + """ + super().__init__() + + self._config = agents_config + self._default_agent = default_agent or next(iter(agents_config.keys())) + self._agents = {} + + # Response routing + self._response_subject = Subject() + + @rpc + def start(self) -> None: + """Deploy and start all agents.""" + super().start() + logger.info(f"Starting agent pool with {len(self._config)} agents") + + # Deploy agents based on config + for agent_id, config in self._config.items(): + logger.info(f"Deploying agent: {agent_id}") + + # Determine agent type + agent_type = config.pop("type", "unified") + + if agent_type == "base": + agent = BaseAgentModule(**config) + else: + agent = UnifiedAgentModule(**config) + + # Start the agent + agent.start() + + # Store agent with metadata + self._agents[agent_id] = {"module": agent, "config": config, "type": agent_type} + + # Subscribe to agent responses + self._setup_agent_routing(agent_id, agent) + + # Subscribe to incoming queries + if self.query_in: + self._disposables.add(self.query_in.observable().subscribe(self._route_query)) + + # Connect response subject to output + if self.response_out: + self._disposables.add(self._response_subject.subscribe(self.response_out.publish)) + + logger.info("Agent pool started") + + @rpc + def stop(self) -> None: + """Stop all agents.""" + logger.info("Stopping agent pool") + + # Stop all agents + for agent_id, agent_info in self._agents.items(): + try: + agent_info["module"].stop() + except Exception as e: + logger.error(f"Error stopping agent {agent_id}: {e}") + + # Clear agents + self._agents.clear() + super().stop() + + @rpc + def add_agent(self, agent_id: str, config: dict[str, Any]) -> None: + """Add a new agent to the pool.""" + if agent_id in self._agents: + logger.warning(f"Agent {agent_id} already exists") + return + + # Deploy and start agent + agent_type = config.pop("type", "unified") + + if agent_type == "base": + agent = BaseAgentModule(**config) + else: + agent = UnifiedAgentModule(**config) + + agent.start() + + # Store and setup routing + self._agents[agent_id] = {"module": agent, "config": config, "type": agent_type} + self._setup_agent_routing(agent_id, agent) + + logger.info(f"Added agent: {agent_id}") + + @rpc + def remove_agent(self, agent_id: str) -> None: + """Remove an agent from the pool.""" + if agent_id not in self._agents: + logger.warning(f"Agent {agent_id} not found") + return + + # Stop and remove agent + agent_info = self._agents[agent_id] + agent_info["module"].stop() + del self._agents[agent_id] + + logger.info(f"Removed agent: {agent_id}") + + @rpc + def list_agents(self) -> list[dict[str, Any]]: + """List all agents and their configurations.""" + return [ + {"id": agent_id, "type": info["type"], "model": info["config"].get("model", "unknown")} + for agent_id, info in self._agents.items() + ] + + @rpc + def broadcast_query(self, query: str, exclude: list[str] | None = None) -> None: + """Send query to all agents (except excluded ones).""" + exclude = exclude or [] + + for agent_id, agent_info in self._agents.items(): + if agent_id not in exclude: + agent_info["module"].query_in.publish(query) + + logger.info(f"Broadcasted query to {len(self._agents) - len(exclude)} agents") + + def _setup_agent_routing( + self, agent_id: str, agent: BaseAgentModule | UnifiedAgentModule + ) -> None: + """Setup response routing for an agent.""" + + # Subscribe to agent responses and tag with agent_id + def tag_response(response: str) -> dict[str, Any]: + return { + "agent_id": agent_id, + "response": response, + "type": self._agents[agent_id]["type"], + } + + self._disposables.add( + agent.response_out.observable() + .pipe(ops.map(tag_response)) + .subscribe(self._response_subject.on_next) + ) + + def _route_query(self, msg: dict[str, Any]) -> None: + """Route incoming query to appropriate agent(s).""" + # Extract routing info + agent_id = msg.get("agent_id", self._default_agent) + query = msg.get("query", "") + broadcast = msg.get("broadcast", False) + + if broadcast: + # Send to all agents + exclude = msg.get("exclude", []) + self.broadcast_query(query, exclude) + elif agent_id == "round_robin": + # Simple round-robin routing + agent_ids = list(self._agents.keys()) + if agent_ids: + # Use query hash for consistent routing + idx = hash(query) % len(agent_ids) + selected_agent = agent_ids[idx] + self._agents[selected_agent]["module"].query_in.publish(query) + logger.debug(f"Routed to {selected_agent} (round-robin)") + elif agent_id in self._agents: + # Route to specific agent + self._agents[agent_id]["module"].query_in.publish(query) + logger.debug(f"Routed to {agent_id}") + else: + logger.warning(f"Unknown agent ID: {agent_id}, using default: {self._default_agent}") + if self._default_agent in self._agents: + self._agents[self._default_agent]["module"].query_in.publish(query) + + # Handle additional routing options + if "image" in msg and hasattr(self._agents.get(agent_id, {}).get("module"), "image_in"): + self._agents[agent_id]["module"].image_in.publish(msg["image"]) + + if "data" in msg and hasattr(self._agents.get(agent_id, {}).get("module"), "data_in"): + self._agents[agent_id]["module"].data_in.publish(msg["data"]) diff --git a/dimos/agents/modules/base.py b/dimos/agents/modules/base.py new file mode 100644 index 0000000000..9caaac49cc --- /dev/null +++ b/dimos/agents/modules/base.py @@ -0,0 +1,525 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Base agent class with all features (non-module).""" + +import asyncio +from concurrent.futures import ThreadPoolExecutor +import json +from typing import Any + +from reactivex.subject import Subject + +from dimos.agents.agent_message import AgentMessage +from dimos.agents.agent_types import AgentResponse, ConversationHistory, ToolCall +from dimos.agents.memory.base import AbstractAgentSemanticMemory +from dimos.agents.memory.chroma_impl import OpenAISemanticMemory +from dimos.skills.skills import AbstractSkill, SkillLibrary +from dimos.utils.logging_config import setup_logger + +try: + from .gateway import UnifiedGatewayClient +except ImportError: + from dimos.agents.modules.gateway import UnifiedGatewayClient + +logger = setup_logger("dimos.agents.modules.base") + +# Vision-capable models +VISION_MODELS = { + "openai::gpt-4o", + "openai::gpt-4o-mini", + "openai::gpt-4-turbo", + "openai::gpt-4-vision-preview", + "anthropic::claude-3-haiku-20240307", + "anthropic::claude-3-sonnet-20241022", + "anthropic::claude-3-opus-20240229", + "anthropic::claude-3-5-sonnet-20241022", + "anthropic::claude-3-5-haiku-latest", + "qwen::qwen-vl-plus", + "qwen::qwen-vl-max", +} + + +class BaseAgent: + """Base agent with all features including memory, skills, and multimodal support. + + This class provides: + - LLM gateway integration + - Conversation history + - Semantic memory (RAG) + - Skills/tools execution + - Multimodal support (text, images, data) + - Model capability detection + """ + + def __init__( + self, + model: str = "openai::gpt-4o-mini", + system_prompt: str | None = None, + skills: SkillLibrary | list[AbstractSkill] | AbstractSkill | None = None, + memory: AbstractAgentSemanticMemory | None = None, + temperature: float = 0.0, + max_tokens: int = 4096, + max_input_tokens: int = 128000, + max_history: int = 20, + rag_n: int = 4, + rag_threshold: float = 0.45, + seed: int | None = None, + # Legacy compatibility + dev_name: str = "BaseAgent", + agent_type: str = "LLM", + **kwargs, + ) -> None: + """Initialize the base agent with all features. + + Args: + model: Model identifier (e.g., "openai::gpt-4o", "anthropic::claude-3-haiku") + system_prompt: System prompt for the agent + skills: Skills/tools available to the agent + memory: Semantic memory system for RAG + temperature: Sampling temperature + max_tokens: Maximum tokens to generate + max_input_tokens: Maximum input tokens + max_history: Maximum conversation history to keep + rag_n: Number of RAG results to fetch + rag_threshold: Minimum similarity for RAG results + seed: Random seed for deterministic outputs (if supported by model) + dev_name: Device/agent name for logging + agent_type: Type of agent for logging + """ + self.model = model + self.system_prompt = system_prompt or "You are a helpful AI assistant." + self.temperature = temperature + self.max_tokens = max_tokens + self.max_input_tokens = max_input_tokens + self._max_history = max_history + self.rag_n = rag_n + self.rag_threshold = rag_threshold + self.seed = seed + self.dev_name = dev_name + self.agent_type = agent_type + + # Initialize skills + if skills is None: + self.skills = SkillLibrary() + elif isinstance(skills, SkillLibrary): + self.skills = skills + elif isinstance(skills, list): + self.skills = SkillLibrary() + for skill in skills: + self.skills.add(skill) + elif isinstance(skills, AbstractSkill): + self.skills = SkillLibrary() + self.skills.add(skills) + else: + self.skills = SkillLibrary() + + # Initialize memory - allow None for testing + if memory is False: # Explicit False means no memory + self.memory = None + else: + self.memory = memory or OpenAISemanticMemory() + + # Initialize gateway + self.gateway = UnifiedGatewayClient() + + # Conversation history with proper format management + self.conversation = ConversationHistory(max_size=self._max_history) + + # Thread pool for async operations + self._executor = ThreadPoolExecutor(max_workers=2) + + # Response subject for emitting responses + self.response_subject = Subject() + + # Check model capabilities + self._supports_vision = self._check_vision_support() + + # Initialize memory with default context + self._initialize_memory() + + @property + def max_history(self) -> int: + """Get max history size.""" + return self._max_history + + @max_history.setter + def max_history(self, value: int) -> None: + """Set max history size and update conversation.""" + self._max_history = value + self.conversation.max_size = value + + def _check_vision_support(self) -> bool: + """Check if the model supports vision.""" + return self.model in VISION_MODELS + + def _initialize_memory(self) -> None: + """Initialize memory with default context.""" + try: + contexts = [ + ("ctx1", "I am an AI assistant that can help with various tasks."), + ("ctx2", f"I am using the {self.model} model."), + ( + "ctx3", + "I have access to tools and skills for specific operations." + if len(self.skills) > 0 + else "I do not have access to external tools.", + ), + ( + "ctx4", + "I can process images and visual content." + if self._supports_vision + else "I cannot process visual content.", + ), + ] + if self.memory: + for doc_id, text in contexts: + self.memory.add_vector(doc_id, text) + except Exception as e: + logger.warning(f"Failed to initialize memory: {e}") + + async def _process_query_async(self, agent_msg: AgentMessage) -> AgentResponse: + """Process query asynchronously and return AgentResponse.""" + query_text = agent_msg.get_combined_text() + logger.info(f"Processing query: {query_text}") + + # Get RAG context + rag_context = self._get_rag_context(query_text) + + # Check if trying to use images with non-vision model + if agent_msg.has_images() and not self._supports_vision: + logger.warning(f"Model {self.model} does not support vision. Ignoring image input.") + # Clear images from message + agent_msg.images.clear() + + # Build messages - pass AgentMessage directly + messages = self._build_messages(agent_msg, rag_context) + + # Get tools if available + tools = self.skills.get_tools() if len(self.skills) > 0 else None + + # Debug logging before gateway call + logger.debug("=== Gateway Request ===") + logger.debug(f"Model: {self.model}") + logger.debug(f"Number of messages: {len(messages)}") + for i, msg in enumerate(messages): + role = msg.get("role", "unknown") + content = msg.get("content", "") + if isinstance(content, str): + content_preview = content[:100] + elif isinstance(content, list): + content_preview = f"[{len(content)} content blocks]" + else: + content_preview = str(content)[:100] + logger.debug(f" Message {i}: role={role}, content={content_preview}...") + logger.debug(f"Tools available: {len(tools) if tools else 0}") + logger.debug("======================") + + # Prepare inference parameters + inference_params = { + "model": self.model, + "messages": messages, + "tools": tools, + "temperature": self.temperature, + "max_tokens": self.max_tokens, + "stream": False, + } + + # Add seed if provided + if self.seed is not None: + inference_params["seed"] = self.seed + + # Make inference call + response = await self.gateway.ainference(**inference_params) + + # Extract response + message = response["choices"][0]["message"] + content = message.get("content", "") + + # Don't update history yet - wait until we have the complete interaction + # This follows Claude's pattern of locking history until tool execution is complete + + # Check for tool calls + tool_calls = None + if message.get("tool_calls"): + tool_calls = [ + ToolCall( + id=tc["id"], + name=tc["function"]["name"], + arguments=json.loads(tc["function"]["arguments"]), + status="pending", + ) + for tc in message["tool_calls"] + ] + + # Get the user message for history + user_message = messages[-1] + + # Handle tool calls (blocking by default) + final_content = await self._handle_tool_calls(tool_calls, messages, user_message) + + # Return response with tool information + return AgentResponse( + content=final_content, + role="assistant", + tool_calls=tool_calls, + requires_follow_up=False, # Already handled + metadata={"model": self.model}, + ) + else: + # No tools, add both user and assistant messages to history + # Get the user message content from the built message + user_msg = messages[-1] # Last message in messages is the user message + user_content = user_msg["content"] + + # Add to conversation history + logger.info("=== Adding to history (no tools) ===") + logger.info(f" Adding user message: {str(user_content)[:100]}...") + self.conversation.add_user_message(user_content) + logger.info(f" Adding assistant response: {content[:100]}...") + self.conversation.add_assistant_message(content) + logger.info(f" History size now: {self.conversation.size()}") + + return AgentResponse( + content=content, + role="assistant", + tool_calls=None, + requires_follow_up=False, + metadata={"model": self.model}, + ) + + def _get_rag_context(self, query: str) -> str: + """Get relevant context from memory.""" + if not self.memory: + return "" + + try: + results = self.memory.query( + query_texts=query, n_results=self.rag_n, similarity_threshold=self.rag_threshold + ) + + if results: + contexts = [doc.page_content for doc, _ in results] + return " | ".join(contexts) + except Exception as e: + logger.warning(f"RAG query failed: {e}") + + return "" + + def _build_messages( + self, agent_msg: AgentMessage, rag_context: str = "" + ) -> list[dict[str, Any]]: + """Build messages list from AgentMessage.""" + messages = [] + + # System prompt with RAG context if available + system_content = self.system_prompt + if rag_context: + system_content += f"\n\nRelevant context: {rag_context}" + messages.append({"role": "system", "content": system_content}) + + # Add conversation history in OpenAI format + history_messages = self.conversation.to_openai_format() + messages.extend(history_messages) + + # Debug history state + logger.info(f"=== Building messages with {len(history_messages)} history messages ===") + if history_messages: + for i, msg in enumerate(history_messages): + role = msg.get("role", "unknown") + content = msg.get("content", "") + if isinstance(content, str): + preview = content[:100] + elif isinstance(content, list): + preview = f"[{len(content)} content blocks]" + else: + preview = str(content)[:100] + logger.info(f" History[{i}]: role={role}, content={preview}") + + # Build user message content from AgentMessage + user_content = agent_msg.get_combined_text() if agent_msg.has_text() else "" + + # Handle images for vision models + if agent_msg.has_images() and self._supports_vision: + # Build content array with text and images + content = [] + if user_content: # Only add text if not empty + content.append({"type": "text", "text": user_content}) + + # Add all images from AgentMessage + for img in agent_msg.images: + content.append( + { + "type": "image_url", + "image_url": {"url": f"data:image/jpeg;base64,{img.base64_jpeg}"}, + } + ) + + logger.debug(f"Building message with {len(content)} content items (vision enabled)") + messages.append({"role": "user", "content": content}) + else: + # Text-only message + messages.append({"role": "user", "content": user_content}) + + return messages + + async def _handle_tool_calls( + self, + tool_calls: list[ToolCall], + messages: list[dict[str, Any]], + user_message: dict[str, Any], + ) -> str: + """Handle tool calls from LLM (blocking mode by default).""" + try: + # Build assistant message with tool calls + assistant_msg = { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": tc.id, + "type": "function", + "function": {"name": tc.name, "arguments": json.dumps(tc.arguments)}, + } + for tc in tool_calls + ], + } + messages.append(assistant_msg) + + # Execute tools and collect results + tool_results = [] + for tool_call in tool_calls: + logger.info(f"Executing tool: {tool_call.name}") + + try: + # Execute the tool + result = self.skills.call(tool_call.name, **tool_call.arguments) + tool_call.status = "completed" + + # Format tool result message + tool_result = { + "role": "tool", + "tool_call_id": tool_call.id, + "content": str(result), + "name": tool_call.name, + } + tool_results.append(tool_result) + + except Exception as e: + logger.error(f"Tool execution failed: {e}") + tool_call.status = "failed" + + # Add error result + tool_result = { + "role": "tool", + "tool_call_id": tool_call.id, + "content": f"Error: {e!s}", + "name": tool_call.name, + } + tool_results.append(tool_result) + + # Add tool results to messages + messages.extend(tool_results) + + # Prepare follow-up inference parameters + followup_params = { + "model": self.model, + "messages": messages, + "temperature": self.temperature, + "max_tokens": self.max_tokens, + } + + # Add seed if provided + if self.seed is not None: + followup_params["seed"] = self.seed + + # Get follow-up response + response = await self.gateway.ainference(**followup_params) + + # Extract final response + final_message = response["choices"][0]["message"] + + # Now add all messages to history in order (like Claude does) + # Add user message + user_content = user_message["content"] + self.conversation.add_user_message(user_content) + + # Add assistant message with tool calls + self.conversation.add_assistant_message("", tool_calls) + + # Add tool results + for result in tool_results: + self.conversation.add_tool_result( + tool_call_id=result["tool_call_id"], content=result["content"] + ) + + # Add final assistant response + final_content = final_message.get("content", "") + self.conversation.add_assistant_message(final_content) + + return final_message.get("content", "") + + except Exception as e: + logger.error(f"Error handling tool calls: {e}") + return f"Error executing tools: {e!s}" + + def query(self, message: str | AgentMessage) -> AgentResponse: + """Synchronous query method for direct usage. + + Args: + message: Either a string query or an AgentMessage with text and/or images + + Returns: + AgentResponse object with content and tool information + """ + # Convert string to AgentMessage if needed + if isinstance(message, str): + agent_msg = AgentMessage() + agent_msg.add_text(message) + else: + agent_msg = message + + # Run async method in a new event loop + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + return loop.run_until_complete(self._process_query_async(agent_msg)) + finally: + loop.close() + + async def aquery(self, message: str | AgentMessage) -> AgentResponse: + """Asynchronous query method. + + Args: + message: Either a string query or an AgentMessage with text and/or images + + Returns: + AgentResponse object with content and tool information + """ + # Convert string to AgentMessage if needed + if isinstance(message, str): + agent_msg = AgentMessage() + agent_msg.add_text(message) + else: + agent_msg = message + + return await self._process_query_async(agent_msg) + + def base_agent_dispose(self) -> None: + """Dispose of all resources and close gateway.""" + self.response_subject.on_completed() + if self._executor: + self._executor.shutdown(wait=False) + if self.gateway: + self.gateway.close() diff --git a/dimos/agents/modules/base_agent.py b/dimos/agents/modules/base_agent.py new file mode 100644 index 0000000000..0bceb1112e --- /dev/null +++ b/dimos/agents/modules/base_agent.py @@ -0,0 +1,211 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Base agent module that wraps BaseAgent for DimOS module usage.""" + +import threading +from typing import Any + +from dimos.agents.agent_message import AgentMessage +from dimos.agents.agent_types import AgentResponse +from dimos.agents.memory.base import AbstractAgentSemanticMemory +from dimos.core import In, Module, Out, rpc +from dimos.skills.skills import AbstractSkill, SkillLibrary +from dimos.utils.logging_config import setup_logger + +try: + from .base import BaseAgent +except ImportError: + from dimos.agents.modules.base import BaseAgent + +logger = setup_logger("dimos.agents.modules.base_agent") + + +class BaseAgentModule(BaseAgent, Module): + """Agent module that inherits from BaseAgent and adds DimOS module interface. + + This provides a thin wrapper around BaseAgent functionality, exposing it + through the DimOS module system with RPC methods and stream I/O. + """ + + # Module I/O - AgentMessage based communication + message_in: In[AgentMessage] = None # Primary input for AgentMessage + response_out: Out[AgentResponse] = None # Output AgentResponse objects + + def __init__( + self, + model: str = "openai::gpt-4o-mini", + system_prompt: str | None = None, + skills: SkillLibrary | list[AbstractSkill] | AbstractSkill | None = None, + memory: AbstractAgentSemanticMemory | None = None, + temperature: float = 0.0, + max_tokens: int = 4096, + max_input_tokens: int = 128000, + max_history: int = 20, + rag_n: int = 4, + rag_threshold: float = 0.45, + process_all_inputs: bool = False, + **kwargs, + ) -> None: + """Initialize the agent module. + + Args: + model: Model identifier (e.g., "openai::gpt-4o", "anthropic::claude-3-haiku") + system_prompt: System prompt for the agent + skills: Skills/tools available to the agent + memory: Semantic memory system for RAG + temperature: Sampling temperature + max_tokens: Maximum tokens to generate + max_input_tokens: Maximum input tokens + max_history: Maximum conversation history to keep + rag_n: Number of RAG results to fetch + rag_threshold: Minimum similarity for RAG results + process_all_inputs: Whether to process all inputs or drop when busy + **kwargs: Additional arguments passed to Module + """ + # Initialize Module first (important for DimOS) + Module.__init__(self, **kwargs) + + # Initialize BaseAgent with all functionality + BaseAgent.__init__( + self, + model=model, + system_prompt=system_prompt, + skills=skills, + memory=memory, + temperature=temperature, + max_tokens=max_tokens, + max_input_tokens=max_input_tokens, + max_history=max_history, + rag_n=rag_n, + rag_threshold=rag_threshold, + process_all_inputs=process_all_inputs, + # Don't pass streams - we'll connect them in start() + input_query_stream=None, + input_data_stream=None, + input_video_stream=None, + ) + + # Track module-specific subscriptions + self._module_disposables = [] + + # For legacy stream support + self._latest_image = None + self._latest_data = None + self._image_lock = threading.Lock() + self._data_lock = threading.Lock() + + @rpc + def start(self) -> None: + """Start the agent module and connect streams.""" + super().start() + logger.info(f"Starting agent module with model: {self.model}") + + # Primary AgentMessage input + if self.message_in and self.message_in.connection is not None: + try: + disposable = self.message_in.observable().subscribe( + lambda msg: self._handle_agent_message(msg) + ) + self._module_disposables.append(disposable) + except Exception as e: + logger.debug(f"Could not connect message_in: {e}") + + # Connect response output + if self.response_out: + disposable = self.response_subject.subscribe( + lambda response: self.response_out.publish(response) + ) + self._module_disposables.append(disposable) + + logger.info("Agent module started") + + @rpc + def stop(self) -> None: + """Stop the agent module.""" + logger.info("Stopping agent module") + + # Dispose module subscriptions + for disposable in self._module_disposables: + disposable.dispose() + self._module_disposables.clear() + + # Dispose BaseAgent resources + self.base_agent_dispose() + + logger.info("Agent module stopped") + super().stop() + + @rpc + def clear_history(self) -> None: + """Clear conversation history.""" + with self._history_lock: + self.history = [] + logger.info("Conversation history cleared") + + @rpc + def add_skill(self, skill: AbstractSkill) -> None: + """Add a skill to the agent.""" + self.skills.add(skill) + logger.info(f"Added skill: {skill.__class__.__name__}") + + @rpc + def set_system_prompt(self, prompt: str) -> None: + """Update system prompt.""" + self.system_prompt = prompt + logger.info("System prompt updated") + + @rpc + def get_conversation_history(self) -> list[dict[str, Any]]: + """Get current conversation history.""" + with self._history_lock: + return self.history.copy() + + def _handle_agent_message(self, message: AgentMessage) -> None: + """Handle AgentMessage from module input.""" + # Process through BaseAgent query method + try: + response = self.query(message) + logger.debug(f"Publishing response: {response}") + self.response_subject.on_next(response) + except Exception as e: + logger.error(f"Agent message processing error: {e}") + self.response_subject.on_error(e) + + def _handle_module_query(self, query: str) -> None: + """Handle legacy query from module input.""" + # For simple text queries, just convert to AgentMessage + agent_msg = AgentMessage() + agent_msg.add_text(query) + + # Process through unified handler + self._handle_agent_message(agent_msg) + + def _update_latest_data(self, data: dict[str, Any]) -> None: + """Update latest data context.""" + with self._data_lock: + self._latest_data = data + + def _update_latest_image(self, img: Any) -> None: + """Update latest image.""" + with self._image_lock: + self._latest_image = img + + def _format_data_context(self, data: dict[str, Any]) -> str: + """Format data dictionary as context string.""" + # Simple formatting - can be customized + parts = [] + for key, value in data.items(): + parts.append(f"{key}: {value}") + return "\n".join(parts) diff --git a/dimos/hardware/stereo_camera.py b/dimos/agents/modules/gateway/__init__.py similarity index 63% rename from dimos/hardware/stereo_camera.py rename to dimos/agents/modules/gateway/__init__.py index c81bc9ed18..7ae4beb037 100644 --- a/dimos/hardware/stereo_camera.py +++ b/dimos/agents/modules/gateway/__init__.py @@ -12,14 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from dimos.hardware.camera import Camera +"""Gateway module for unified LLM access.""" -class StereoCamera(Camera): - def __init__(self, baseline=None, **kwargs): - super().__init__(**kwargs) - self.baseline = baseline +from .client import UnifiedGatewayClient +from .utils import convert_tools_to_standard_format, parse_streaming_response - def get_intrinsics(self): - intrinsics = super().get_intrinsics() - intrinsics['baseline'] = self.baseline - return intrinsics +__all__ = ["UnifiedGatewayClient", "convert_tools_to_standard_format", "parse_streaming_response"] diff --git a/dimos/agents/modules/gateway/client.py b/dimos/agents/modules/gateway/client.py new file mode 100644 index 0000000000..6d8abf5e14 --- /dev/null +++ b/dimos/agents/modules/gateway/client.py @@ -0,0 +1,211 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unified gateway client for LLM access.""" + +import asyncio +from collections.abc import AsyncIterator, Iterator +import logging +import os +from types import TracebackType +from typing import Any + +import httpx +from tenacity import retry, stop_after_attempt, wait_exponential + +from .tensorzero_embedded import TensorZeroEmbeddedGateway + +logger = logging.getLogger(__name__) + + +class UnifiedGatewayClient: + """Clean abstraction over TensorZero or other gateways. + + This client provides a unified interface for accessing multiple LLM providers + through a gateway service, with support for streaming, tools, and async operations. + """ + + def __init__( + self, gateway_url: str | None = None, timeout: float = 60.0, use_simple: bool = False + ) -> None: + """Initialize the gateway client. + + Args: + gateway_url: URL of the gateway service. Defaults to env var or localhost + timeout: Request timeout in seconds + use_simple: Deprecated parameter, always uses TensorZero + """ + self.gateway_url = gateway_url or os.getenv( + "TENSORZERO_GATEWAY_URL", "http://localhost:3000" + ) + self.timeout = timeout + self._client = None + self._async_client = None + + # Always use TensorZero embedded gateway + try: + self._tensorzero_client = TensorZeroEmbeddedGateway() + logger.info("Using TensorZero embedded gateway") + except Exception as e: + logger.error(f"Failed to initialize TensorZero: {e}") + raise + + def _get_client(self) -> httpx.Client: + """Get or create sync HTTP client.""" + if self._client is None: + self._client = httpx.Client( + base_url=self.gateway_url, + timeout=self.timeout, + headers={"Content-Type": "application/json"}, + ) + return self._client + + def _get_async_client(self) -> httpx.AsyncClient: + """Get or create async HTTP client.""" + if self._async_client is None: + self._async_client = httpx.AsyncClient( + base_url=self.gateway_url, + timeout=self.timeout, + headers={"Content-Type": "application/json"}, + ) + return self._async_client + + @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10)) + def inference( + self, + model: str, + messages: list[dict[str, Any]], + tools: list[dict[str, Any]] | None = None, + temperature: float = 0.0, + max_tokens: int | None = None, + stream: bool = False, + **kwargs, + ) -> dict[str, Any] | Iterator[dict[str, Any]]: + """Synchronous inference call. + + Args: + model: Model identifier (e.g., "openai::gpt-4o") + messages: List of message dicts with role and content + tools: Optional list of tools in standard format + temperature: Sampling temperature + max_tokens: Maximum tokens to generate + stream: Whether to stream the response + **kwargs: Additional model-specific parameters + + Returns: + Response dict or iterator of response chunks if streaming + """ + return self._tensorzero_client.inference( + model=model, + messages=messages, + tools=tools, + temperature=temperature, + max_tokens=max_tokens, + stream=stream, + **kwargs, + ) + + @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10)) + async def ainference( + self, + model: str, + messages: list[dict[str, Any]], + tools: list[dict[str, Any]] | None = None, + temperature: float = 0.0, + max_tokens: int | None = None, + stream: bool = False, + **kwargs, + ) -> dict[str, Any] | AsyncIterator[dict[str, Any]]: + """Asynchronous inference call. + + Args: + model: Model identifier (e.g., "anthropic::claude-3-7-sonnet") + messages: List of message dicts with role and content + tools: Optional list of tools in standard format + temperature: Sampling temperature + max_tokens: Maximum tokens to generate + stream: Whether to stream the response + **kwargs: Additional model-specific parameters + + Returns: + Response dict or async iterator of response chunks if streaming + """ + return await self._tensorzero_client.ainference( + model=model, + messages=messages, + tools=tools, + temperature=temperature, + max_tokens=max_tokens, + stream=stream, + **kwargs, + ) + + def close(self) -> None: + """Close the HTTP clients.""" + if self._client: + self._client.close() + self._client = None + if self._async_client: + # This needs to be awaited in an async context + # We'll handle this in __del__ with asyncio + pass + self._tensorzero_client.close() + + async def aclose(self) -> None: + """Async close method.""" + if self._async_client: + await self._async_client.aclose() + self._async_client = None + await self._tensorzero_client.aclose() + + def __del__(self) -> None: + """Cleanup on deletion.""" + self.close() + if self._async_client: + # Try to close async client if event loop is available + try: + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(self.aclose()) + else: + loop.run_until_complete(self.aclose()) + except RuntimeError: + # No event loop, just let it be garbage collected + pass + + def __enter__(self): + """Context manager entry.""" + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + """Context manager exit.""" + self.close() + + async def __aenter__(self): + """Async context manager entry.""" + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + """Async context manager exit.""" + await self.aclose() diff --git a/dimos/agents/modules/gateway/tensorzero_embedded.py b/dimos/agents/modules/gateway/tensorzero_embedded.py new file mode 100644 index 0000000000..90d30fe82d --- /dev/null +++ b/dimos/agents/modules/gateway/tensorzero_embedded.py @@ -0,0 +1,280 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""TensorZero embedded gateway client with correct config format.""" + +from collections.abc import AsyncIterator, Iterator +import logging +from pathlib import Path +from typing import Any + +logger = logging.getLogger(__name__) + + +class TensorZeroEmbeddedGateway: + """TensorZero embedded gateway using patch_openai_client.""" + + def __init__(self) -> None: + """Initialize TensorZero embedded gateway.""" + self._client = None + self._config_path = None + self._setup_config() + self._initialize_client() + + def _setup_config(self) -> None: + """Create TensorZero configuration with correct format.""" + config_dir = Path("/tmp/tensorzero_embedded") + config_dir.mkdir(exist_ok=True) + self._config_path = config_dir / "tensorzero.toml" + + # Create config using the correct format from working example + config_content = """ +# OpenAI Models +[models.gpt_4o_mini] +routing = ["openai"] + +[models.gpt_4o_mini.providers.openai] +type = "openai" +model_name = "gpt-4o-mini" + +[models.gpt_4o] +routing = ["openai"] + +[models.gpt_4o.providers.openai] +type = "openai" +model_name = "gpt-4o" + +# Claude Models +[models.claude_3_haiku] +routing = ["anthropic"] + +[models.claude_3_haiku.providers.anthropic] +type = "anthropic" +model_name = "claude-3-haiku-20240307" + +[models.claude_3_sonnet] +routing = ["anthropic"] + +[models.claude_3_sonnet.providers.anthropic] +type = "anthropic" +model_name = "claude-3-5-sonnet-20241022" + +[models.claude_3_opus] +routing = ["anthropic"] + +[models.claude_3_opus.providers.anthropic] +type = "anthropic" +model_name = "claude-3-opus-20240229" + +# Cerebras Models - disabled for CI (no API key) +# [models.llama_3_3_70b] +# routing = ["cerebras"] +# +# [models.llama_3_3_70b.providers.cerebras] +# type = "openai" +# model_name = "llama-3.3-70b" +# api_base = "https://api.cerebras.ai/v1" +# api_key_location = "env::CEREBRAS_API_KEY" + +# Qwen Models +[models.qwen_plus] +routing = ["qwen"] + +[models.qwen_plus.providers.qwen] +type = "openai" +model_name = "qwen-plus" +api_base = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1" +api_key_location = "env::ALIBABA_API_KEY" + +[models.qwen_vl_plus] +routing = ["qwen"] + +[models.qwen_vl_plus.providers.qwen] +type = "openai" +model_name = "qwen-vl-plus" +api_base = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1" +api_key_location = "env::ALIBABA_API_KEY" + +# Object storage - disable for embedded mode +[object_storage] +type = "disabled" + +# Single chat function with all models +# TensorZero will automatically skip models that don't support the input type +[functions.chat] +type = "chat" + +[functions.chat.variants.openai] +type = "chat_completion" +model = "gpt_4o_mini" +weight = 1.0 + +[functions.chat.variants.claude] +type = "chat_completion" +model = "claude_3_haiku" +weight = 0.5 + +# Cerebras disabled for CI (no API key) +# [functions.chat.variants.cerebras] +# type = "chat_completion" +# model = "llama_3_3_70b" +# weight = 0.0 + +[functions.chat.variants.qwen] +type = "chat_completion" +model = "qwen_plus" +weight = 0.3 + +# For vision queries, Qwen VL can be used +[functions.chat.variants.qwen_vision] +type = "chat_completion" +model = "qwen_vl_plus" +weight = 0.4 +""" + + with open(self._config_path, "w") as f: + f.write(config_content) + + logger.info(f"Created TensorZero config at {self._config_path}") + + def _initialize_client(self): + """Initialize OpenAI client with TensorZero patch.""" + try: + from openai import OpenAI + from tensorzero import patch_openai_client + + self._client = OpenAI() + + # Patch with TensorZero embedded gateway + patch_openai_client( + self._client, + clickhouse_url=None, # In-memory storage + config_file=str(self._config_path), + async_setup=False, + ) + + logger.info("TensorZero embedded gateway initialized successfully") + + except Exception as e: + logger.error(f"Failed to initialize TensorZero: {e}") + raise + + def _map_model_to_tensorzero(self, model: str) -> str: + """Map provider::model format to TensorZero function format.""" + # Always use the chat function - TensorZero will handle model selection + # based on input type and model capabilities automatically + return "tensorzero::function_name::chat" + + def inference( + self, + model: str, + messages: list[dict[str, Any]], + tools: list[dict[str, Any]] | None = None, + temperature: float = 0.0, + max_tokens: int | None = None, + stream: bool = False, + **kwargs, + ) -> dict[str, Any] | Iterator[dict[str, Any]]: + """Synchronous inference call through TensorZero.""" + + # Map model to TensorZero function + tz_model = self._map_model_to_tensorzero(model) + + # Prepare parameters + params = { + "model": tz_model, + "messages": messages, + "temperature": temperature, + } + + if max_tokens: + params["max_tokens"] = max_tokens + + if tools: + params["tools"] = tools + + if stream: + params["stream"] = True + + # Add any extra kwargs + params.update(kwargs) + + try: + # Make the call through patched client + if stream: + # Return streaming iterator + stream_response = self._client.chat.completions.create(**params) + + def stream_generator(): + for chunk in stream_response: + yield chunk.model_dump() + + return stream_generator() + else: + response = self._client.chat.completions.create(**params) + return response.model_dump() + + except Exception as e: + logger.error(f"TensorZero inference failed: {e}") + raise + + async def ainference( + self, + model: str, + messages: list[dict[str, Any]], + tools: list[dict[str, Any]] | None = None, + temperature: float = 0.0, + max_tokens: int | None = None, + stream: bool = False, + **kwargs, + ) -> dict[str, Any] | AsyncIterator[dict[str, Any]]: + """Async inference with streaming support.""" + import asyncio + + loop = asyncio.get_event_loop() + + if stream: + # Create async generator from sync streaming + async def stream_generator(): + # Run sync streaming in executor + sync_stream = await loop.run_in_executor( + None, + lambda: self.inference( + model, messages, tools, temperature, max_tokens, stream=True, **kwargs + ), + ) + + # Convert sync iterator to async + for chunk in sync_stream: + yield chunk + + return stream_generator() + else: + result = await loop.run_in_executor( + None, + lambda: self.inference( + model, messages, tools, temperature, max_tokens, stream, **kwargs + ), + ) + return result + + def close(self) -> None: + """Close the client.""" + # TensorZero embedded doesn't need explicit cleanup + pass + + async def aclose(self) -> None: + """Async close.""" + # TensorZero embedded doesn't need explicit cleanup + pass diff --git a/dimos/agents/modules/gateway/tensorzero_simple.py b/dimos/agents/modules/gateway/tensorzero_simple.py new file mode 100644 index 0000000000..a2cc57e2fb --- /dev/null +++ b/dimos/agents/modules/gateway/tensorzero_simple.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Minimal TensorZero test to get it working.""" + +from pathlib import Path + +from dotenv import load_dotenv +from openai import OpenAI +from tensorzero import patch_openai_client + +load_dotenv() + +# Create minimal config +config_dir = Path("/tmp/tz_test") +config_dir.mkdir(exist_ok=True) +config_path = config_dir / "tensorzero.toml" + +# Minimal config based on TensorZero docs +config = """ +[models.gpt_4o_mini] +routing = ["openai"] + +[models.gpt_4o_mini.providers.openai] +type = "openai" +model_name = "gpt-4o-mini" + +[functions.my_function] +type = "chat" + +[functions.my_function.variants.my_variant] +type = "chat_completion" +model = "gpt_4o_mini" +""" + +with open(config_path, "w") as f: + f.write(config) + +print(f"Created config at {config_path}") + +# Create OpenAI client +client = OpenAI() + +# Patch with TensorZero +try: + patch_openai_client( + client, + clickhouse_url=None, # In-memory + config_file=str(config_path), + async_setup=False, + ) + print("✅ TensorZero initialized successfully!") +except Exception as e: + print(f"❌ Failed to initialize TensorZero: {e}") + exit(1) + +# Test basic inference +print("\nTesting basic inference...") +try: + response = client.chat.completions.create( + model="tensorzero::function_name::my_function", + messages=[{"role": "user", "content": "What is 2+2?"}], + temperature=0.0, + max_tokens=10, + ) + + content = response.choices[0].message.content + print(f"Response: {content}") + print("✅ Basic inference worked!") + +except Exception as e: + print(f"❌ Basic inference failed: {e}") + import traceback + + traceback.print_exc() + +print("\nTesting streaming...") +try: + stream = client.chat.completions.create( + model="tensorzero::function_name::my_function", + messages=[{"role": "user", "content": "Count from 1 to 3"}], + temperature=0.0, + max_tokens=20, + stream=True, + ) + + print("Stream response: ", end="", flush=True) + for chunk in stream: + if chunk.choices[0].delta.content: + print(chunk.choices[0].delta.content, end="", flush=True) + print("\n✅ Streaming worked!") + +except Exception as e: + print(f"\n❌ Streaming failed: {e}") diff --git a/dimos/agents/modules/gateway/utils.py b/dimos/agents/modules/gateway/utils.py new file mode 100644 index 0000000000..ac9dc3e364 --- /dev/null +++ b/dimos/agents/modules/gateway/utils.py @@ -0,0 +1,156 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utility functions for gateway operations.""" + +import logging +from typing import Any + +logger = logging.getLogger(__name__) + + +def convert_tools_to_standard_format(tools: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Convert DimOS tool format to standard format accepted by gateways. + + DimOS tools come from pydantic_function_tool and have this format: + { + "type": "function", + "function": { + "name": "tool_name", + "description": "tool description", + "parameters": { + "type": "object", + "properties": {...}, + "required": [...] + } + } + } + + We keep this format as it's already standard JSON Schema format. + """ + if not tools: + return [] + + # Tools are already in the correct format from pydantic_function_tool + return tools + + +def parse_streaming_response(chunk: dict[str, Any]) -> dict[str, Any]: + """Parse a streaming response chunk into a standard format. + + Args: + chunk: Raw chunk from the gateway + + Returns: + Parsed chunk with standard fields: + - type: "content" | "tool_call" | "error" | "done" + - content: The actual content (text for content type, tool info for tool_call) + - metadata: Additional information + """ + # Handle TensorZero streaming format + if "choices" in chunk: + # OpenAI-style format from TensorZero + choice = chunk["choices"][0] if chunk["choices"] else {} + delta = choice.get("delta", {}) + + if "content" in delta: + return { + "type": "content", + "content": delta["content"], + "metadata": {"index": choice.get("index", 0)}, + } + elif "tool_calls" in delta: + tool_calls = delta["tool_calls"] + if tool_calls: + tool_call = tool_calls[0] + return { + "type": "tool_call", + "content": { + "id": tool_call.get("id"), + "name": tool_call.get("function", {}).get("name"), + "arguments": tool_call.get("function", {}).get("arguments", ""), + }, + "metadata": {"index": tool_call.get("index", 0)}, + } + elif choice.get("finish_reason"): + return { + "type": "done", + "content": None, + "metadata": {"finish_reason": choice["finish_reason"]}, + } + + # Handle direct content chunks + if isinstance(chunk, str): + return {"type": "content", "content": chunk, "metadata": {}} + + # Handle error responses + if "error" in chunk: + return {"type": "error", "content": chunk["error"], "metadata": chunk} + + # Default fallback + return {"type": "unknown", "content": chunk, "metadata": {}} + + +def create_tool_response(tool_id: str, result: Any, is_error: bool = False) -> dict[str, Any]: + """Create a properly formatted tool response. + + Args: + tool_id: The ID of the tool call + result: The result from executing the tool + is_error: Whether this is an error response + + Returns: + Formatted tool response message + """ + content = str(result) if not isinstance(result, str) else result + + return { + "role": "tool", + "tool_call_id": tool_id, + "content": content, + "name": None, # Will be filled by the calling code + } + + +def extract_image_from_message(message: dict[str, Any]) -> dict[str, Any] | None: + """Extract image data from a message if present. + + Args: + message: Message dict that may contain image data + + Returns: + Dict with image data and metadata, or None if no image + """ + content = message.get("content", []) + + # Handle list content (multimodal) + if isinstance(content, list): + for item in content: + if isinstance(item, dict): + # OpenAI format + if item.get("type") == "image_url": + return { + "format": "openai", + "data": item["image_url"]["url"], + "detail": item["image_url"].get("detail", "auto"), + } + # Anthropic format + elif item.get("type") == "image": + return { + "format": "anthropic", + "data": item["source"]["data"], + "media_type": item["source"].get("media_type", "image/jpeg"), + } + + return None diff --git a/dimos/agents/modules/simple_vision_agent.py b/dimos/agents/modules/simple_vision_agent.py new file mode 100644 index 0000000000..b4888fd073 --- /dev/null +++ b/dimos/agents/modules/simple_vision_agent.py @@ -0,0 +1,238 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Simple vision agent module following exact DimOS patterns.""" + +import asyncio +import base64 +import io +import threading + +import numpy as np +from PIL import Image as PILImage +from reactivex.disposable import Disposable + +from dimos.agents.modules.gateway import UnifiedGatewayClient +from dimos.core import In, Module, Out, rpc +from dimos.msgs.sensor_msgs import Image +from dimos.utils.logging_config import setup_logger + +logger = setup_logger(__file__) + + +class SimpleVisionAgentModule(Module): + """Simple vision agent that can process images with text queries. + + This follows the exact pattern from working modules without any extras. + """ + + # Module I/O + query_in: In[str] = None + image_in: In[Image] = None + response_out: Out[str] = None + + def __init__( + self, + model: str = "openai::gpt-4o-mini", + system_prompt: str | None = None, + temperature: float = 0.0, + max_tokens: int = 4096, + ) -> None: + """Initialize the vision agent. + + Args: + model: Model identifier (e.g., "openai::gpt-4o-mini") + system_prompt: System prompt for the agent + temperature: Sampling temperature + max_tokens: Maximum tokens to generate + """ + super().__init__() + + self.model = model + self.system_prompt = system_prompt or "You are a helpful vision AI assistant." + self.temperature = temperature + self.max_tokens = max_tokens + + # State + self.gateway = None + self._latest_image = None + self._processing = False + self._lock = threading.Lock() + + @rpc + def start(self) -> None: + """Initialize and start the agent.""" + super().start() + + logger.info(f"Starting simple vision agent with model: {self.model}") + + # Initialize gateway + self.gateway = UnifiedGatewayClient() + + # Subscribe to inputs + if self.query_in: + unsub = self.query_in.subscribe(self._handle_query) + self._disposables.add(Disposable(unsub)) + + if self.image_in: + unsub = self.image_in.subscribe(self._handle_image) + self._disposables.add(Disposable(unsub)) + + logger.info("Simple vision agent started") + + @rpc + def stop(self) -> None: + logger.info("Stopping simple vision agent") + if self.gateway: + self.gateway.close() + + super().stop() + + def _handle_image(self, image: Image) -> None: + """Handle incoming image.""" + logger.info( + f"Received new image: {image.data.shape if hasattr(image, 'data') else 'unknown shape'}" + ) + self._latest_image = image + + def _handle_query(self, query: str) -> None: + """Handle text query.""" + with self._lock: + if self._processing: + logger.warning("Already processing, skipping query") + return + self._processing = True + + # Process in thread + thread = threading.Thread(target=self._run_async_query, args=(query,)) + thread.daemon = True + thread.start() + + def _run_async_query(self, query: str) -> None: + """Run async query in new event loop.""" + asyncio.run(self._process_query(query)) + + async def _process_query(self, query: str) -> None: + """Process the query.""" + try: + logger.info(f"Processing query: {query}") + + # Build messages + messages = [{"role": "system", "content": self.system_prompt}] + + # Check if we have an image + if self._latest_image: + logger.info("Have latest image, encoding...") + image_b64 = self._encode_image(self._latest_image) + if image_b64: + logger.info(f"Image encoded successfully, size: {len(image_b64)} bytes") + # Add user message with image + if "anthropic" in self.model: + # Anthropic format + messages.append( + { + "role": "user", + "content": [ + {"type": "text", "text": query}, + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/jpeg", + "data": image_b64, + }, + }, + ], + } + ) + else: + # OpenAI format + messages.append( + { + "role": "user", + "content": [ + {"type": "text", "text": query}, + { + "type": "image_url", + "image_url": { + "url": f"data:image/jpeg;base64,{image_b64}", + "detail": "auto", + }, + }, + ], + } + ) + else: + # No image encoding, just text + logger.warning("Failed to encode image") + messages.append({"role": "user", "content": query}) + else: + # No image at all + logger.warning("No image available") + messages.append({"role": "user", "content": query}) + + # Make inference call + response = await self.gateway.ainference( + model=self.model, + messages=messages, + temperature=self.temperature, + max_tokens=self.max_tokens, + stream=False, + ) + + # Extract response + message = response["choices"][0]["message"] + content = message.get("content", "") + + # Emit response + if self.response_out and content: + self.response_out.publish(content) + + except Exception as e: + logger.error(f"Error processing query: {e}") + import traceback + + traceback.print_exc() + if self.response_out: + self.response_out.publish(f"Error: {e!s}") + finally: + with self._lock: + self._processing = False + + def _encode_image(self, image: Image) -> str | None: + """Encode image to base64.""" + try: + # Convert to numpy array if needed + if hasattr(image, "data"): + img_array = image.data + else: + img_array = np.array(image) + + # Convert to PIL Image + pil_image = PILImage.fromarray(img_array) + + # Convert to RGB if needed + if pil_image.mode != "RGB": + pil_image = pil_image.convert("RGB") + + # Encode to base64 + buffer = io.BytesIO() + pil_image.save(buffer, format="JPEG") + img_b64 = base64.b64encode(buffer.getvalue()).decode("utf-8") + + return img_b64 + + except Exception as e: + logger.error(f"Failed to encode image: {e}") + return None diff --git a/dimos/agents/planning_agent.py b/dimos/agents/planning_agent.py index 197ebe76d5..6dbdbf5866 100644 --- a/dimos/agents/planning_agent.py +++ b/dimos/agents/planning_agent.py @@ -12,48 +12,51 @@ # See the License for the specific language governing permissions and # limitations under the License. +from textwrap import dedent import threading -from typing import List, Optional, Dict, Union, Literal -import json -from reactivex import Subject, Observable, disposable, create -from reactivex import operators as ops -from openai import OpenAI, NOT_GIVEN import time -from dimos.skills.skills import AbstractSkill +from typing import Literal + +from pydantic import BaseModel +from reactivex import Observable, operators as ops + from dimos.agents.agent import OpenAIAgent +from dimos.skills.skills import AbstractSkill from dimos.utils.logging_config import setup_logger -from textwrap import dedent -from pydantic import BaseModel, Field logger = setup_logger("dimos.agents.planning_agent") + # For response validation class PlanningAgentResponse(BaseModel): type: Literal["dialogue", "plan"] - content: List[str] + content: list[str] needs_confirmation: bool + class PlanningAgent(OpenAIAgent): """Agent that plans and breaks down tasks through dialogue. - + This agent specializes in: 1. Understanding complex tasks through dialogue 2. Breaking tasks into concrete, executable steps 3. Refining plans based on user feedback 4. Streaming individual steps to ExecutionAgents - + The agent maintains conversation state and can refine plans until the user confirms they are ready to execute. """ - - def __init__(self, - dev_name: str = "PlanningAgent", - model_name: str = "gpt-4", - input_query_stream: Optional[Observable] = None, - use_terminal: bool = False, - skills: Optional[AbstractSkill] = None): + + def __init__( + self, + dev_name: str = "PlanningAgent", + model_name: str = "gpt-4", + input_query_stream: Observable | None = None, + use_terminal: bool = False, + skills: AbstractSkill | None = None, + ) -> None: """Initialize the planning agent. - + Args: dev_name: Name identifier for the agent model_name: OpenAI model to use @@ -66,12 +69,12 @@ def __init__(self, self.current_plan = [] self.plan_confirmed = False self.latest_response = None - + # Build system prompt skills_list = [] if skills is not None: skills_list = skills.get_tools() - + system_query = dedent(f""" You are a Robot planning assistant that helps break down tasks into concrete, executable steps. Your goal is to: @@ -120,7 +123,7 @@ def __init__(self, input_query_stream=input_query_stream, system_query=system_query, max_output_tokens_per_request=1000, - response_model=PlanningAgentResponse + response_model=PlanningAgentResponse, ) logger.info("Planning agent initialized") @@ -132,32 +135,31 @@ def __init__(self, logger.info("Starting terminal interface in a separate thread") terminal_thread = threading.Thread(target=self.start_terminal_interface, daemon=True) terminal_thread.start() - + def _handle_response(self, response) -> None: """Handle the agent's response and update state. - + Args: response: ParsedChatCompletionMessage containing PlanningAgentResponse """ print("handle response", response) print("handle response type", type(response)) - + # Extract the PlanningAgentResponse from parsed field if available - planning_response = response.parsed if hasattr(response, 'parsed') else response + planning_response = response.parsed if hasattr(response, "parsed") else response print("planning response", planning_response) print("planning response type", type(planning_response)) # Convert to dict for storage in conversation history response_dict = planning_response.model_dump() self.conversation_history.append(response_dict) - + # If it's a plan, update current plan if planning_response.type == "plan": logger.info(f"Updating current plan: {planning_response.content}") self.current_plan = planning_response.content - + # Store latest response self.latest_response = response_dict - def _stream_plan(self) -> None: """Stream each step of the confirmed plan.""" @@ -173,40 +175,38 @@ def _stream_plan(self) -> None: logger.debug(f"Successfully emitted step {i} to response_subject") except Exception as e: logger.error(f"Error emitting step {i}: {e}") - + logger.info("Plan streaming completed") self.response_subject.on_completed() - + def _send_query(self, messages: list) -> PlanningAgentResponse: """Send query to OpenAI and parse the response. - + Extends OpenAIAgent's _send_query to handle planning-specific response formats. - + Args: messages: List of message dictionaries - + Returns: PlanningAgentResponse: Validated response with type, content, and needs_confirmation """ try: return super()._send_query(messages) except Exception as e: - logger.error(f"Caught exception in _send_query: {str(e)}") + logger.error(f"Caught exception in _send_query: {e!s}") return PlanningAgentResponse( - type="dialogue", - content=f"Error: {str(e)}", - needs_confirmation=False + type="dialogue", content=f"Error: {e!s}", needs_confirmation=False ) def process_user_input(self, user_input: str) -> None: """Process user input and generate appropriate response. - + Args: user_input: The user's message """ if not user_input: return - + # Check for plan confirmation if self.current_plan and user_input.lower() in ["yes", "y", "confirm"]: logger.info("Plan confirmation received") @@ -215,23 +215,20 @@ def process_user_input(self, user_input: str) -> None: confirmation_msg = PlanningAgentResponse( type="dialogue", content="Plan confirmed! Streaming steps to execution...", - needs_confirmation=False + needs_confirmation=False, ) self._handle_response(confirmation_msg) self._stream_plan() return - + # Build messages for OpenAI with conversation history messages = [ {"role": "system", "content": self.system_query} # Using system_query from OpenAIAgent ] - + # Add the new user input to conversation history - self.conversation_history.append({ - "type": "user_message", - "content": user_input - }) - + self.conversation_history.append({"type": "user_message", "content": user_input}) + # Add complete conversation history including both user and assistant messages for msg in self.conversation_history: if msg["type"] == "user_message": @@ -239,33 +236,37 @@ def process_user_input(self, user_input: str) -> None: elif msg["type"] == "dialogue": messages.append({"role": "assistant", "content": msg["content"]}) elif msg["type"] == "plan": - plan_text = "Here's my proposed plan:\n" + "\n".join(f"{i+1}. {step}" for i, step in enumerate(msg["content"])) + plan_text = "Here's my proposed plan:\n" + "\n".join( + f"{i + 1}. {step}" for i, step in enumerate(msg["content"]) + ) messages.append({"role": "assistant", "content": plan_text}) - + # Get and handle response response = self._send_query(messages) self._handle_response(response) - def start_terminal_interface(self): + def start_terminal_interface(self) -> None: """Start the terminal interface for input/output.""" - time.sleep(5) # buffer time for clean terminal interface printing + time.sleep(5) # buffer time for clean terminal interface printing print("=" * 50) print("\nDimOS Action PlanningAgent\n") print("I have access to your Robot() and Robot Skills()") - print("Describe your task and I'll break it down into steps using your skills as a reference.") + print( + "Describe your task and I'll break it down into steps using your skills as a reference." + ) print("Once you're happy with the plan, type 'yes' to execute it.") print("Type 'quit' to exit.\n") - + while True: try: print("=" * 50) user_input = input("USER > ") - if user_input.lower() in ['quit', 'exit']: + if user_input.lower() in ["quit", "exit"]: break - + self.process_user_input(user_input) - + # Display response if self.latest_response["type"] == "dialogue": print(f"\nPlanner: {self.latest_response['content']}") @@ -275,29 +276,30 @@ def start_terminal_interface(self): print(f"{i}. {step}") if self.latest_response["needs_confirmation"]: print("\nDoes this plan look good? (yes/no)") - + if self.plan_confirmed: print("\nPlan confirmed! Streaming steps to execution...") break - + except KeyboardInterrupt: print("\nStopping...") break except Exception as e: print(f"\nError: {e}") break - + def get_response_observable(self) -> Observable: """Gets an observable that emits responses from this agent. This method processes the response stream from the parent class, extracting content from `PlanningAgentResponse` objects and flattening any lists of plan steps for emission. - + Returns: Observable: An observable that emits plan steps from the agent. """ - def extract_content(response) -> List[str]: + + def extract_content(response) -> list[str]: if isinstance(response, PlanningAgentResponse): if response.type == "plan": return response.content # List of steps to be emitted individually @@ -312,5 +314,5 @@ def extract_content(response) -> List[str]: # Process the stream: extract content and flatten plan lists return base_observable.pipe( ops.map(extract_content), - ops.flat_map(lambda items: items) # Flatten the list of items + ops.flat_map(lambda items: items), # Flatten the list of items ) diff --git a/dimos/agents/prompt_builder/impl.py b/dimos/agents/prompt_builder/impl.py index f1ddf5681f..9cd532fea9 100644 --- a/dimos/agents/prompt_builder/impl.py +++ b/dimos/agents/prompt_builder/impl.py @@ -14,19 +14,19 @@ from textwrap import dedent -from typing import Optional + from dimos.agents.tokenizer.base import AbstractTokenizer from dimos.agents.tokenizer.openai_tokenizer import OpenAITokenizer # TODO: Make class more generic when implementing other tokenizers. Presently its OpenAI specific. # TODO: Build out testing and logging -class PromptBuilder(): +class PromptBuilder: DEFAULT_SYSTEM_PROMPT = dedent(""" - You are an AI assistant capable of understanding and analyzing both visual and textual information. - Your task is to provide accurate and insightful responses based on the data provided to you. - Use the following information to assist the user with their query. Do not rely on any internal + You are an AI assistant capable of understanding and analyzing both visual and textual information. + Your task is to provide accurate and insightful responses based on the data provided to you. + Use the following information to assist the user with their query. Do not rely on any internal knowledge or make assumptions beyond the provided data. Visual Context: You may have been given an image to analyze. Use the visual details to enhance your response. @@ -37,8 +37,13 @@ class PromptBuilder(): - If the information is insufficient to provide a complete answer, acknowledge the limitation. - Maintain a professional and informative tone in your response. """) - - def __init__(self, model_name='gpt-4o', max_tokens=128000, tokenizer: Optional[AbstractTokenizer] = None): + + def __init__( + self, + model_name: str = "gpt-4o", + max_tokens: int = 128000, + tokenizer: AbstractTokenizer | None = None, + ) -> None: """ Initialize the prompt builder. Args: @@ -49,8 +54,8 @@ def __init__(self, model_name='gpt-4o', max_tokens=128000, tokenizer: Optional[A self.model_name = model_name self.max_tokens = max_tokens self.tokenizer: AbstractTokenizer = tokenizer or OpenAITokenizer(model_name=self.model_name) - - def truncate_tokens(self, text, max_tokens, strategy): + + def truncate_tokens(self, text: str, max_tokens, strategy): """ Truncate text to fit within max_tokens using a specified strategy. Args: @@ -78,7 +83,7 @@ def truncate_tokens(self, text, max_tokens, strategy): raise ValueError(f"Unknown truncation strategy: {strategy}") return self.tokenizer.detokenize_text(truncated) - + def build( self, system_prompt=None, @@ -86,11 +91,11 @@ def build( base64_image=None, image_width=None, image_height=None, - image_detail="low", + image_detail: str = "low", rag_context=None, budgets=None, policies=None, - override_token_limit=False, + override_token_limit: bool = False, ): """ Builds a dynamic prompt tailored to token limits, respecting budgets and policies. @@ -138,7 +143,7 @@ def build( system_prompt = self.DEFAULT_SYSTEM_PROMPT rag_context = rag_context or "" - + # Debug: # print("system_prompt: ", system_prompt) # print("rag_context: ", rag_context) @@ -148,7 +153,11 @@ def build( rag_token_cnt = self.tokenizer.token_count(rag_context) system_prompt_token_cnt = self.tokenizer.token_count(system_prompt) user_query_token_cnt = self.tokenizer.token_count(user_query) - image_token_cnt = self.tokenizer.image_token_count(image_width, image_height, image_detail) if base64_image else 0 + image_token_cnt = ( + self.tokenizer.image_token_count(image_width, image_height, image_detail) + if base64_image + else 0 + ) else: rag_token_cnt = 0 system_prompt_token_cnt = 0 @@ -185,18 +194,25 @@ def build( messages = [{"role": "system", "content": components["system_prompt"]["text"]}] if components["rag"]["text"]: - user_content = [{"type": "text", "text": f"{components['rag']['text']}\n\n{components['user_query']['text']}"}] + user_content = [ + { + "type": "text", + "text": f"{components['rag']['text']}\n\n{components['user_query']['text']}", + } + ] else: user_content = [{"type": "text", "text": components["user_query"]["text"]}] if base64_image: - user_content.append({ - "type": "image_url", - "image_url": { - "url": f"data:image/jpeg;base64,{base64_image}", - "detail": image_detail, - }, - }) + user_content.append( + { + "type": "image_url", + "image_url": { + "url": f"data:image/jpeg;base64,{base64_image}", + "detail": image_detail, + }, + } + ) messages.append({"role": "user", "content": user_content}) # Debug: diff --git a/dimos/agents/test_agent_image_message.py b/dimos/agents/test_agent_image_message.py new file mode 100644 index 0000000000..c7f84bcefe --- /dev/null +++ b/dimos/agents/test_agent_image_message.py @@ -0,0 +1,403 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test BaseAgent with AgentMessage containing images.""" + +import logging +import os + +from dotenv import load_dotenv +import numpy as np +import pytest + +from dimos.agents.agent_message import AgentMessage +from dimos.agents.modules.base import BaseAgent +from dimos.msgs.sensor_msgs import Image +from dimos.msgs.sensor_msgs.Image import ImageFormat +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("test_agent_image_message") +# Enable debug logging for base module +logging.getLogger("dimos.agents.modules.base").setLevel(logging.DEBUG) + + +@pytest.mark.tofix +def test_agent_single_image() -> None: + """Test agent with single image in AgentMessage.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + # Create agent + agent = BaseAgent( + model="openai::gpt-4o-mini", + system_prompt="You are a helpful vision assistant. Describe what you see concisely.", + temperature=0.0, + seed=42, + ) + + # Create AgentMessage with text and single image + msg = AgentMessage() + msg.add_text("What color is this image?") + + # Create a solid red image in RGB format for clarity + red_data = np.zeros((100, 100, 3), dtype=np.uint8) + red_data[:, :, 0] = 255 # R channel (index 0 in RGB) + red_data[:, :, 1] = 0 # G channel (index 1 in RGB) + red_data[:, :, 2] = 0 # B channel (index 2 in RGB) + # Explicitly specify RGB format to avoid confusion + red_img = Image.from_numpy(red_data, format=ImageFormat.RGB) + print(f"[Test] Created image format: {red_img.format}, shape: {red_img.data.shape}") + msg.add_image(red_img) + + # Query + response = agent.query(msg) + print(f"\n[Test] Single image response: '{response.content}'") + + # Verify response + assert response.content is not None + # The model should mention a color or describe the image + response_lower = response.content.lower() + # Accept any color mention since models may see colors differently + color_mentioned = any( + word in response_lower + for word in ["red", "blue", "color", "solid", "image", "shade", "hue"] + ) + assert color_mentioned, f"Expected color description in response, got: {response.content}" + + # Check conversation history + assert agent.conversation.size() == 2 + # User message should have content array + history = agent.conversation.to_openai_format() + user_msg = history[0] + assert user_msg["role"] == "user" + assert isinstance(user_msg["content"], list), "Multimodal message should have content array" + assert len(user_msg["content"]) == 2 # text + image + assert user_msg["content"][0]["type"] == "text" + assert user_msg["content"][0]["text"] == "What color is this image?" + assert user_msg["content"][1]["type"] == "image_url" + + # Clean up + agent.dispose() + + +@pytest.mark.tofix +def test_agent_multiple_images() -> None: + """Test agent with multiple images in AgentMessage.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + # Create agent + agent = BaseAgent( + model="openai::gpt-4o-mini", + system_prompt="You are a helpful vision assistant that compares images.", + temperature=0.0, + seed=42, + ) + + # Create AgentMessage with multiple images + msg = AgentMessage() + msg.add_text("Compare these three images.") + msg.add_text("What are their colors?") + + # Create three different colored images + red_img = Image(data=np.full((50, 50, 3), [255, 0, 0], dtype=np.uint8)) + green_img = Image(data=np.full((50, 50, 3), [0, 255, 0], dtype=np.uint8)) + blue_img = Image(data=np.full((50, 50, 3), [0, 0, 255], dtype=np.uint8)) + + msg.add_image(red_img) + msg.add_image(green_img) + msg.add_image(blue_img) + + # Query + response = agent.query(msg) + + # Verify response acknowledges the images + response_lower = response.content.lower() + # Check if the model is actually seeing the images + if "unable to view" in response_lower or "can't see" in response_lower: + print(f"WARNING: Model not seeing images: {response.content}") + # Still pass the test but note the issue + else: + # If the model can see images, it should mention some colors + colors_mentioned = sum( + 1 + for color in ["red", "green", "blue", "color", "image", "bright", "dark"] + if color in response_lower + ) + assert colors_mentioned >= 1, ( + f"Expected color/image references, found none in: {response.content}" + ) + + # Check history structure + history = agent.conversation.to_openai_format() + user_msg = history[0] + assert user_msg["role"] == "user" + assert isinstance(user_msg["content"], list) + assert len(user_msg["content"]) == 4 # 1 text + 3 images + assert user_msg["content"][0]["type"] == "text" + assert user_msg["content"][0]["text"] == "Compare these three images. What are their colors?" + + # Verify all images are in the message + for i in range(1, 4): + assert user_msg["content"][i]["type"] == "image_url" + assert user_msg["content"][i]["image_url"]["url"].startswith("data:image/jpeg;base64,") + + # Clean up + agent.dispose() + + +@pytest.mark.tofix +def test_agent_image_with_context() -> None: + """Test agent maintaining context with image queries.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + # Create agent + agent = BaseAgent( + model="openai::gpt-4o-mini", + system_prompt="You are a helpful vision assistant with good memory.", + temperature=0.0, + seed=42, + ) + + # First query with image + msg1 = AgentMessage() + msg1.add_text("This is my favorite color.") + msg1.add_text("Remember it.") + + # Create purple image + purple_img = Image(data=np.full((80, 80, 3), [128, 0, 128], dtype=np.uint8)) + msg1.add_image(purple_img) + + response1 = agent.query(msg1) + # The model should acknowledge the color or mention the image + assert any( + word in response1.content.lower() + for word in ["purple", "violet", "color", "image", "magenta"] + ), f"Expected color or image reference in response: {response1.content}" + + # Second query without image, referencing the first + response2 = agent.query("What was my favorite color that I showed you?") + # Check if the model acknowledges the previous conversation + response_lower = response2.content.lower() + logger.info(f"Response: {response2.content}") + assert any( + word in response_lower + for word in ["purple", "violet", "color", "favorite", "showed", "image"] + ), f"Agent should reference previous conversation: {response2.content}" + + # Check conversation history has all messages + assert agent.conversation.size() == 4 + + # Clean up + agent.dispose() + + +@pytest.mark.tofix +def test_agent_mixed_content() -> None: + """Test agent with mixed text-only and image queries.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + # Create agent + agent = BaseAgent( + model="openai::gpt-4o-mini", + system_prompt="You are a helpful assistant that can see images when provided.", + temperature=0.0, + seed=100, + ) + + # Text-only query + response1 = agent.query("Hello! Can you see images?") + assert response1.content is not None + + # Image query + msg2 = AgentMessage() + msg2.add_text("Now look at this image.") + msg2.add_text("What do you see? Describe the scene.") + + # Use first frame from rgbd_frames test data + import numpy as np + from PIL import Image as PILImage + + from dimos.msgs.sensor_msgs import Image + from dimos.utils.data import get_data + + data_path = get_data("rgbd_frames") + image_path = os.path.join(data_path, "color", "00000.png") + + pil_image = PILImage.open(image_path) + image_array = np.array(pil_image) + + image = Image.from_numpy(image_array) + + msg2.add_image(image) + + # Check image encoding + logger.info(f"Image shape: {image.data.shape}") + logger.info(f"Image encoding: {len(image.agent_encode())} chars") + + response2 = agent.query(msg2) + logger.info(f"Image query response: {response2.content}") + logger.info(f"Agent supports vision: {agent._supports_vision}") + logger.info(f"Message has images: {msg2.has_images()}") + logger.info(f"Number of images in message: {len(msg2.images)}") + # Check that the model saw and described the image + assert any( + word in response2.content.lower() + for word in ["desk", "chair", "table", "laptop", "computer", "screen", "monitor"] + ), f"Expected description of office scene, got: {response2.content}" + + # Another text-only query + response3 = agent.query("What did I just show you?") + words = ["office", "room", "hallway", "image", "scene"] + content = response3.content.lower() + + assert any(word in content for word in words), f"{content=}" + + # Check history structure + assert agent.conversation.size() == 6 + history = agent.conversation.to_openai_format() + # First query should be simple string + assert isinstance(history[0]["content"], str) + # Second query should be content array + assert isinstance(history[2]["content"], list) + # Third query should be simple string again + assert isinstance(history[4]["content"], str) + + # Clean up + agent.dispose() + + +@pytest.mark.tofix +def test_agent_empty_image_message() -> None: + """Test edge case with empty parts of AgentMessage.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + # Create agent + agent = BaseAgent( + model="openai::gpt-4o-mini", + system_prompt="You are a helpful assistant.", + temperature=0.0, + seed=42, + ) + + # AgentMessage with only images, no text + msg = AgentMessage() + # Don't add any text + + # Add a simple colored image + img = Image(data=np.full((60, 60, 3), [255, 255, 0], dtype=np.uint8)) # Yellow + msg.add_image(img) + + response = agent.query(msg) + # Should still work even without text + assert response.content is not None + assert len(response.content) > 0 + + # AgentMessage with empty text parts + msg2 = AgentMessage() + msg2.add_text("") # Empty + msg2.add_text("What") + msg2.add_text("") # Empty + msg2.add_text("color?") + msg2.add_image(img) + + response2 = agent.query(msg2) + # Accept various color interpretations for yellow (RGB 255,255,0) + response_lower = response2.content.lower() + assert any( + color in response_lower for color in ["yellow", "color", "bright", "turquoise", "green"] + ), f"Expected color reference in response: {response2.content}" + + # Clean up + agent.dispose() + + +@pytest.mark.tofix +def test_agent_non_vision_model_with_images() -> None: + """Test that non-vision models handle image input gracefully.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + # Create agent with non-vision model + agent = BaseAgent( + model="openai::gpt-3.5-turbo", # This model doesn't support vision + system_prompt="You are a helpful assistant.", + temperature=0.0, + seed=42, + ) + + # Try to send an image + msg = AgentMessage() + msg.add_text("What do you see in this image?") + + img = Image(data=np.zeros((100, 100, 3), dtype=np.uint8)) + msg.add_image(img) + + # Should log warning and process as text-only + response = agent.query(msg) + assert response.content is not None + + # Check history - should be text-only + history = agent.conversation.to_openai_format() + user_msg = history[0] + assert isinstance(user_msg["content"], str), "Non-vision model should store text-only" + assert user_msg["content"] == "What do you see in this image?" + + # Clean up + agent.dispose() + + +@pytest.mark.tofix +def test_mock_agent_with_images() -> None: + """Test mock agent with images for CI.""" + # This test doesn't need API keys + + from dimos.agents.test_base_agent_text import MockAgent + + # Create mock agent + agent = MockAgent(model="mock::vision", system_prompt="Mock vision agent") + agent._supports_vision = True # Enable vision support + + # Test with image + msg = AgentMessage() + msg.add_text("What color is this?") + + img = Image(data=np.zeros((50, 50, 3), dtype=np.uint8)) + msg.add_image(img) + + response = agent.query(msg) + assert response.content is not None + assert "Mock response" in response.content or "color" in response.content + + # Check conversation history + assert agent.conversation.size() == 2 + + # Clean up + agent.dispose() diff --git a/dimos/agents/test_agent_message_streams.py b/dimos/agents/test_agent_message_streams.py new file mode 100644 index 0000000000..22d33b46de --- /dev/null +++ b/dimos/agents/test_agent_message_streams.py @@ -0,0 +1,387 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test BaseAgent with AgentMessage and video streams.""" + +import asyncio +import os +import pickle + +from dotenv import load_dotenv +import pytest +from reactivex import operators as ops + +from dimos import core +from dimos.agents.agent_message import AgentMessage +from dimos.agents.agent_types import AgentResponse +from dimos.agents.modules.base_agent import BaseAgentModule +from dimos.core import In, Module, Out, rpc +from dimos.msgs.sensor_msgs import Image +from dimos.protocol import pubsub +from dimos.utils.data import get_data +from dimos.utils.logging_config import setup_logger +from dimos.utils.testing import TimedSensorReplay + +logger = setup_logger("test_agent_message_streams") + + +class VideoMessageSender(Module): + """Module that sends AgentMessage with video frames every 2 seconds.""" + + message_out: Out[AgentMessage] = None + + def __init__(self, video_path: str) -> None: + super().__init__() + self.video_path = video_path + self._subscription = None + self._frame_count = 0 + + @rpc + def start(self) -> None: + """Start sending video messages.""" + # Use TimedSensorReplay to replay video frames + video_replay = TimedSensorReplay(self.video_path, autocast=Image.from_numpy) + + # Send AgentMessage with frame every 3 seconds (give agent more time to process) + self._subscription = ( + video_replay.stream() + .pipe( + ops.sample(3.0), # Every 3 seconds + ops.take(3), # Only send 3 frames total + ops.map(self._create_message), + ) + .subscribe( + on_next=lambda msg: self._send_message(msg), + on_error=lambda e: logger.error(f"Video stream error: {e}"), + on_completed=lambda: logger.info("Video stream completed"), + ) + ) + + logger.info("Video message streaming started (every 3 seconds, max 3 frames)") + + def _create_message(self, frame: Image) -> AgentMessage: + """Create AgentMessage with frame and query.""" + self._frame_count += 1 + + msg = AgentMessage() + msg.add_text(f"What do you see in frame {self._frame_count}? Describe in one sentence.") + msg.add_image(frame) + + logger.info(f"Created message with frame {self._frame_count}") + return msg + + def _send_message(self, msg: AgentMessage) -> None: + """Send the message and test pickling.""" + # Test that message can be pickled (for module communication) + try: + pickled = pickle.dumps(msg) + pickle.loads(pickled) + logger.info(f"Message pickling test passed - size: {len(pickled)} bytes") + except Exception as e: + logger.error(f"Message pickling failed: {e}") + + self.message_out.publish(msg) + + @rpc + def stop(self) -> None: + """Stop streaming.""" + if self._subscription: + self._subscription.dispose() + self._subscription = None + + +class MultiImageMessageSender(Module): + """Send AgentMessage with multiple images.""" + + message_out: Out[AgentMessage] = None + + def __init__(self, video_path: str) -> None: + super().__init__() + self.video_path = video_path + self.frames = [] + + @rpc + def start(self) -> None: + """Collect some frames.""" + video_replay = TimedSensorReplay(self.video_path, autocast=Image.from_numpy) + + # Collect first 3 frames + video_replay.stream().pipe(ops.take(3)).subscribe( + on_next=lambda frame: self.frames.append(frame), + on_completed=self._send_multi_image_query, + ) + + def _send_multi_image_query(self) -> None: + """Send query with multiple images.""" + if len(self.frames) >= 2: + msg = AgentMessage() + msg.add_text("Compare these images and describe what changed between them.") + + for _i, frame in enumerate(self.frames[:2]): + msg.add_image(frame) + + logger.info(f"Sending multi-image message with {len(msg.images)} images") + + # Test pickling + try: + pickled = pickle.dumps(msg) + logger.info(f"Multi-image message pickle size: {len(pickled)} bytes") + except Exception as e: + logger.error(f"Multi-image pickling failed: {e}") + + self.message_out.publish(msg) + + +class ResponseCollector(Module): + """Collect responses.""" + + response_in: In[AgentResponse] = None + + def __init__(self) -> None: + super().__init__() + self.responses = [] + + @rpc + def start(self) -> None: + self.response_in.subscribe(self._on_response) + + def _on_response(self, resp: AgentResponse) -> None: + logger.info(f"Collected response: {resp.content[:100] if resp.content else 'None'}...") + self.responses.append(resp) + + @rpc + def get_responses(self): + return self.responses + + +@pytest.mark.tofix +@pytest.mark.module +@pytest.mark.asyncio +async def test_agent_message_video_stream() -> None: + """Test BaseAgentModule with AgentMessage containing video frames.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + pubsub.lcm.autoconf() + + logger.info("Testing BaseAgentModule with AgentMessage video stream...") + dimos = core.start(4) + + try: + # Get test video + data_path = get_data("unitree_office_walk") + video_path = os.path.join(data_path, "video") + + logger.info(f"Using video from: {video_path}") + + # Deploy modules + video_sender = dimos.deploy(VideoMessageSender, video_path) + video_sender.message_out.transport = core.pLCMTransport("/agent/message") + + agent = dimos.deploy( + BaseAgentModule, + model="openai::gpt-4o-mini", + system_prompt="You are a vision assistant. Describe what you see concisely.", + temperature=0.0, + ) + agent.response_out.transport = core.pLCMTransport("/agent/response") + + collector = dimos.deploy(ResponseCollector) + + # Connect modules + agent.message_in.connect(video_sender.message_out) + collector.response_in.connect(agent.response_out) + + # Start modules + agent.start() + collector.start() + video_sender.start() + + logger.info("All modules started, streaming video messages...") + + # Wait for 3 messages to be sent (3 frames * 3 seconds = 9 seconds) + # Plus processing time, wait 12 seconds total + await asyncio.sleep(12) + + # Stop video stream + video_sender.stop() + + # Get all responses + responses = collector.get_responses() + logger.info(f"\nCollected {len(responses)} responses:") + for i, resp in enumerate(responses): + logger.info( + f"\nResponse {i + 1}: {resp.content if isinstance(resp, AgentResponse) else resp}" + ) + + # Verify we got at least 2 responses (sometimes the 3rd frame doesn't get processed in time) + assert len(responses) >= 2, f"Expected at least 2 responses, got {len(responses)}" + + # Verify responses describe actual scene + all_responses = " ".join( + resp.content if isinstance(resp, AgentResponse) else resp for resp in responses + ).lower() + assert any( + word in all_responses + for word in ["office", "room", "hallway", "corridor", "door", "wall", "floor", "frame"] + ), "Responses should describe the office environment" + + logger.info("\n✅ AgentMessage video stream test PASSED!") + + # Stop agent + agent.stop() + + finally: + dimos.close() + dimos.shutdown() + + +@pytest.mark.tofix +@pytest.mark.module +@pytest.mark.asyncio +async def test_agent_message_multi_image() -> None: + """Test BaseAgentModule with AgentMessage containing multiple images.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + pubsub.lcm.autoconf() + + logger.info("Testing BaseAgentModule with multi-image AgentMessage...") + dimos = core.start(4) + + try: + # Get test video + data_path = get_data("unitree_office_walk") + video_path = os.path.join(data_path, "video") + + # Deploy modules + multi_sender = dimos.deploy(MultiImageMessageSender, video_path) + multi_sender.message_out.transport = core.pLCMTransport("/agent/multi_message") + + agent = dimos.deploy( + BaseAgentModule, + model="openai::gpt-4o-mini", + system_prompt="You are a vision assistant that compares images.", + temperature=0.0, + ) + agent.response_out.transport = core.pLCMTransport("/agent/multi_response") + + collector = dimos.deploy(ResponseCollector) + + # Connect modules + agent.message_in.connect(multi_sender.message_out) + collector.response_in.connect(agent.response_out) + + # Start modules + agent.start() + collector.start() + multi_sender.start() + + logger.info("Modules started, sending multi-image query...") + + # Wait for response + await asyncio.sleep(8) + + # Get responses + responses = collector.get_responses() + logger.info(f"\nCollected {len(responses)} responses:") + for i, resp in enumerate(responses): + logger.info( + f"\nResponse {i + 1}: {resp.content if isinstance(resp, AgentResponse) else resp}" + ) + + # Verify we got a response + assert len(responses) >= 1, f"Expected at least 1 response, got {len(responses)}" + + # Response should mention comparison or multiple images + response_text = ( + responses[0].content if isinstance(responses[0], AgentResponse) else responses[0] + ).lower() + assert any( + word in response_text + for word in ["both", "first", "second", "change", "different", "similar", "compare"] + ), "Response should indicate comparison of multiple images" + + logger.info("\n✅ Multi-image AgentMessage test PASSED!") + + # Stop agent + agent.stop() + + finally: + dimos.close() + dimos.shutdown() + + +@pytest.mark.tofix +def test_agent_message_text_only() -> None: + """Test BaseAgent with text-only AgentMessage.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + from dimos.agents.modules.base import BaseAgent + + # Create agent + agent = BaseAgent( + model="openai::gpt-4o-mini", + system_prompt="You are a helpful assistant. Answer in 10 words or less.", + temperature=0.0, + seed=42, + ) + + # Test with text-only AgentMessage + msg = AgentMessage() + msg.add_text("What is") + msg.add_text("the capital") + msg.add_text("of France?") + + response = agent.query(msg) + assert "Paris" in response.content, "Expected 'Paris' in response" + + # Test pickling of AgentMessage + pickled = pickle.dumps(msg) + unpickled = pickle.loads(pickled) + assert unpickled.get_combined_text() == "What is the capital of France?" + + # Verify multiple text messages were combined properly + assert len(msg.messages) == 3 + assert msg.messages[0] == "What is" + assert msg.messages[1] == "the capital" + assert msg.messages[2] == "of France?" + + logger.info("✅ Text-only AgentMessage test PASSED!") + + # Clean up + agent.dispose() + + +if __name__ == "__main__": + logger.info("Running AgentMessage stream tests...") + + # Run text-only test first + test_agent_message_text_only() + print("\n" + "=" * 60 + "\n") + + # Run async tests + asyncio.run(test_agent_message_video_stream()) + print("\n" + "=" * 60 + "\n") + asyncio.run(test_agent_message_multi_image()) + + logger.info("\n✅ All AgentMessage tests completed!") diff --git a/dimos/agents/test_agent_pool.py b/dimos/agents/test_agent_pool.py new file mode 100644 index 0000000000..b3576b80e2 --- /dev/null +++ b/dimos/agents/test_agent_pool.py @@ -0,0 +1,353 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test agent pool module.""" + +import asyncio +import os + +from dotenv import load_dotenv +import pytest + +from dimos import core +from dimos.agents.modules.base_agent import BaseAgentModule +from dimos.core import In, Module, Out, rpc +from dimos.protocol import pubsub + + +class PoolRouter(Module): + """Simple router for agent pool.""" + + query_in: In[dict] = None + agent1_out: Out[str] = None + agent2_out: Out[str] = None + agent3_out: Out[str] = None + + @rpc + def start(self) -> None: + self.query_in.subscribe(self._route) + + def _route(self, msg: dict) -> None: + agent_id = msg.get("agent_id", "agent1") + query = msg.get("query", "") + + if agent_id == "agent1" and self.agent1_out: + self.agent1_out.publish(query) + elif agent_id == "agent2" and self.agent2_out: + self.agent2_out.publish(query) + elif agent_id == "agent3" and self.agent3_out: + self.agent3_out.publish(query) + elif agent_id == "all": + # Broadcast to all + if self.agent1_out: + self.agent1_out.publish(query) + if self.agent2_out: + self.agent2_out.publish(query) + if self.agent3_out: + self.agent3_out.publish(query) + + +class PoolAggregator(Module): + """Aggregate responses from pool.""" + + agent1_in: In[str] = None + agent2_in: In[str] = None + agent3_in: In[str] = None + response_out: Out[dict] = None + + @rpc + def start(self) -> None: + if self.agent1_in: + self.agent1_in.subscribe(lambda r: self._handle_response("agent1", r)) + if self.agent2_in: + self.agent2_in.subscribe(lambda r: self._handle_response("agent2", r)) + if self.agent3_in: + self.agent3_in.subscribe(lambda r: self._handle_response("agent3", r)) + + def _handle_response(self, agent_id: str, response: str) -> None: + if self.response_out: + self.response_out.publish({"agent_id": agent_id, "response": response}) + + +class PoolController(Module): + """Controller for pool testing.""" + + query_out: Out[dict] = None + + @rpc + def send_to_agent(self, agent_id: str, query: str) -> None: + self.query_out.publish({"agent_id": agent_id, "query": query}) + + @rpc + def broadcast(self, query: str) -> None: + self.query_out.publish({"agent_id": "all", "query": query}) + + +class PoolCollector(Module): + """Collect pool responses.""" + + response_in: In[dict] = None + + def __init__(self) -> None: + super().__init__() + self.responses = [] + + @rpc + def start(self) -> None: + self.response_in.subscribe(lambda r: self.responses.append(r)) + + @rpc + def get_responses(self) -> list: + return self.responses + + @rpc + def get_by_agent(self, agent_id: str) -> list: + return [r for r in self.responses if r.get("agent_id") == agent_id] + + +@pytest.mark.skip("Skipping pool tests for now") +@pytest.mark.module +@pytest.mark.asyncio +async def test_agent_pool() -> None: + """Test agent pool with multiple agents.""" + load_dotenv() + pubsub.lcm.autoconf() + + # Check for at least one API key + has_api_key = any( + [os.getenv("OPENAI_API_KEY"), os.getenv("ANTHROPIC_API_KEY"), os.getenv("CEREBRAS_API_KEY")] + ) + + if not has_api_key: + pytest.skip("No API keys found for testing") + + dimos = core.start(7) + + try: + # Deploy three agents with different configs + agents = [] + models = [] + + if os.getenv("CEREBRAS_API_KEY"): + agent1 = dimos.deploy( + BaseAgentModule, + model="cerebras::llama3.1-8b", + system_prompt="You are agent1. Be very brief.", + ) + agents.append(agent1) + models.append("agent1") + + if os.getenv("OPENAI_API_KEY"): + agent2 = dimos.deploy( + BaseAgentModule, + model="openai::gpt-4o-mini", + system_prompt="You are agent2. Be helpful.", + ) + agents.append(agent2) + models.append("agent2") + + if os.getenv("CEREBRAS_API_KEY") and len(agents) < 3: + agent3 = dimos.deploy( + BaseAgentModule, + model="cerebras::llama3.1-8b", + system_prompt="You are agent3. Be creative.", + ) + agents.append(agent3) + models.append("agent3") + + if len(agents) < 2: + pytest.skip("Need at least 2 working agents for pool test") + + # Deploy router, aggregator, controller, collector + router = dimos.deploy(PoolRouter) + aggregator = dimos.deploy(PoolAggregator) + controller = dimos.deploy(PoolController) + collector = dimos.deploy(PoolCollector) + + # Configure transports + controller.query_out.transport = core.pLCMTransport("/pool/queries") + aggregator.response_out.transport = core.pLCMTransport("/pool/responses") + + # Configure agent transports and connections + if len(agents) > 0: + router.agent1_out.transport = core.pLCMTransport("/pool/agent1/query") + agents[0].response_out.transport = core.pLCMTransport("/pool/agent1/response") + agents[0].query_in.connect(router.agent1_out) + aggregator.agent1_in.connect(agents[0].response_out) + + if len(agents) > 1: + router.agent2_out.transport = core.pLCMTransport("/pool/agent2/query") + agents[1].response_out.transport = core.pLCMTransport("/pool/agent2/response") + agents[1].query_in.connect(router.agent2_out) + aggregator.agent2_in.connect(agents[1].response_out) + + if len(agents) > 2: + router.agent3_out.transport = core.pLCMTransport("/pool/agent3/query") + agents[2].response_out.transport = core.pLCMTransport("/pool/agent3/response") + agents[2].query_in.connect(router.agent3_out) + aggregator.agent3_in.connect(agents[2].response_out) + + # Connect router and collector + router.query_in.connect(controller.query_out) + collector.response_in.connect(aggregator.response_out) + + # Start all modules + for agent in agents: + agent.start() + router.start() + aggregator.start() + collector.start() + + await asyncio.sleep(3) + + # Test direct routing + for _i, model_id in enumerate(models[:2]): # Test first 2 agents + controller.send_to_agent(model_id, f"Say hello from {model_id}") + await asyncio.sleep(0.5) + + await asyncio.sleep(6) + + responses = collector.get_responses() + print(f"Got {len(responses)} responses from direct routing") + assert len(responses) >= len(models[:2]), ( + f"Should get responses from at least {len(models[:2])} agents" + ) + + # Test broadcast + collector.responses.clear() + controller.broadcast("What is 1+1?") + + await asyncio.sleep(6) + + responses = collector.get_responses() + print(f"Got {len(responses)} responses from broadcast (expected {len(agents)})") + # Allow for some agents to be slow + assert len(responses) >= min(2, len(agents)), ( + f"Should get response from at least {min(2, len(agents))} agents" + ) + + # Check all agents responded + agent_ids = {r["agent_id"] for r in responses} + assert len(agent_ids) >= 2, "Multiple agents should respond" + + # Stop all agents + for agent in agents: + agent.stop() + + finally: + dimos.close() + dimos.shutdown() + + +@pytest.mark.skip("Skipping pool tests for now") +@pytest.mark.module +@pytest.mark.asyncio +async def test_mock_agent_pool() -> None: + """Test agent pool with mock agents.""" + pubsub.lcm.autoconf() + + class MockPoolAgent(Module): + """Mock agent for pool testing.""" + + query_in: In[str] = None + response_out: Out[str] = None + + def __init__(self, agent_id: str) -> None: + super().__init__() + self.agent_id = agent_id + + @rpc + def start(self) -> None: + self.query_in.subscribe(self._handle_query) + + def _handle_query(self, query: str) -> None: + if "1+1" in query: + self.response_out.publish(f"{self.agent_id}: The answer is 2") + else: + self.response_out.publish(f"{self.agent_id}: {query}") + + dimos = core.start(6) + + try: + # Deploy mock agents + agent1 = dimos.deploy(MockPoolAgent, agent_id="fast") + agent2 = dimos.deploy(MockPoolAgent, agent_id="smart") + agent3 = dimos.deploy(MockPoolAgent, agent_id="creative") + + # Deploy infrastructure + router = dimos.deploy(PoolRouter) + aggregator = dimos.deploy(PoolAggregator) + collector = dimos.deploy(PoolCollector) + + # Configure all transports + router.query_in.transport = core.pLCMTransport("/mock/pool/queries") + router.agent1_out.transport = core.pLCMTransport("/mock/pool/agent1/q") + router.agent2_out.transport = core.pLCMTransport("/mock/pool/agent2/q") + router.agent3_out.transport = core.pLCMTransport("/mock/pool/agent3/q") + + agent1.response_out.transport = core.pLCMTransport("/mock/pool/agent1/r") + agent2.response_out.transport = core.pLCMTransport("/mock/pool/agent2/r") + agent3.response_out.transport = core.pLCMTransport("/mock/pool/agent3/r") + + aggregator.response_out.transport = core.pLCMTransport("/mock/pool/responses") + + # Connect everything + agent1.query_in.connect(router.agent1_out) + agent2.query_in.connect(router.agent2_out) + agent3.query_in.connect(router.agent3_out) + + aggregator.agent1_in.connect(agent1.response_out) + aggregator.agent2_in.connect(agent2.response_out) + aggregator.agent3_in.connect(agent3.response_out) + + collector.response_in.connect(aggregator.response_out) + + # Start all + agent1.start() + agent2.start() + agent3.start() + router.start() + aggregator.start() + collector.start() + + await asyncio.sleep(0.5) + + # Test routing + router.query_in.transport.publish({"agent_id": "agent1", "query": "Hello"}) + router.query_in.transport.publish({"agent_id": "agent2", "query": "Hi"}) + + await asyncio.sleep(0.5) + + responses = collector.get_responses() + assert len(responses) == 2 + assert any("fast" in r["response"] for r in responses) + assert any("smart" in r["response"] for r in responses) + + # Test broadcast + collector.responses.clear() + router.query_in.transport.publish({"agent_id": "all", "query": "What is 1+1?"}) + + await asyncio.sleep(0.5) + + responses = collector.get_responses() + assert len(responses) == 3 + assert all("2" in r["response"] for r in responses) + + finally: + dimos.close() + dimos.shutdown() + + +if __name__ == "__main__": + asyncio.run(test_mock_agent_pool()) diff --git a/dimos/agents/test_agent_tools.py b/dimos/agents/test_agent_tools.py new file mode 100644 index 0000000000..fd485ac015 --- /dev/null +++ b/dimos/agents/test_agent_tools.py @@ -0,0 +1,409 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Production test for BaseAgent tool handling functionality.""" + +import asyncio +import os + +from dotenv import load_dotenv +from pydantic import Field +import pytest + +from dimos import core +from dimos.agents.agent_message import AgentMessage +from dimos.agents.agent_types import AgentResponse +from dimos.agents.modules.base import BaseAgent +from dimos.agents.modules.base_agent import BaseAgentModule +from dimos.core import In, Module, Out, rpc +from dimos.protocol import pubsub +from dimos.skills.skills import AbstractSkill, SkillLibrary +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("test_agent_tools") + + +# Test Skills +class CalculateSkill(AbstractSkill): + """Perform a calculation.""" + + expression: str = Field(description="Mathematical expression to evaluate") + + def __call__(self) -> str: + try: + # Simple evaluation for testing + result = eval(self.expression) + return f"The result is {result}" + except Exception as e: + return f"Error calculating: {e!s}" + + +class WeatherSkill(AbstractSkill): + """Get current weather information for a location. This is a mock weather service that returns test data.""" + + location: str = Field(description="Location to get weather for (e.g. 'London', 'New York')") + + def __call__(self) -> str: + # Mock weather response + return f"The weather in {self.location} is sunny with a temperature of 72°F" + + +class NavigationSkill(AbstractSkill): + """Navigate to a location (potentially long-running).""" + + destination: str = Field(description="Destination to navigate to") + speed: float = Field(default=1.0, description="Navigation speed in m/s") + + def __call__(self) -> str: + # In real implementation, this would start navigation + # For now, simulate blocking behavior + import time + + time.sleep(0.5) # Simulate some processing + return f"Navigation to {self.destination} completed successfully" + + +# Module for testing tool execution +class ToolTestController(Module): + """Controller that sends queries to agent.""" + + message_out: Out[AgentMessage] = None + + @rpc + def send_query(self, query: str) -> None: + msg = AgentMessage() + msg.add_text(query) + self.message_out.publish(msg) + + +class ResponseCollector(Module): + """Collect agent responses.""" + + response_in: In[AgentResponse] = None + + def __init__(self) -> None: + super().__init__() + self.responses = [] + + @rpc + def start(self) -> None: + logger.info("ResponseCollector starting subscription") + self.response_in.subscribe(self._on_response) + logger.info("ResponseCollector subscription active") + + def _on_response(self, response) -> None: + logger.info(f"ResponseCollector received response #{len(self.responses) + 1}: {response}") + self.responses.append(response) + + @rpc + def get_responses(self): + return self.responses + + +@pytest.mark.tofix +@pytest.mark.module +@pytest.mark.asyncio +async def test_agent_module_with_tools() -> None: + """Test BaseAgentModule with tool execution.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + pubsub.lcm.autoconf() + dimos = core.start(4) + + try: + # Create skill library + skill_library = SkillLibrary() + skill_library.add(CalculateSkill) + skill_library.add(WeatherSkill) + skill_library.add(NavigationSkill) + + # Deploy modules + controller = dimos.deploy(ToolTestController) + controller.message_out.transport = core.pLCMTransport("/tools/messages") + + agent = dimos.deploy( + BaseAgentModule, + model="openai::gpt-4o-mini", + system_prompt="You are a helpful assistant with access to calculation, weather, and navigation tools. When asked about weather, you MUST use the WeatherSkill tool - it provides mock weather data for testing. When asked to navigate somewhere, you MUST use the NavigationSkill tool. Always use the appropriate tool when available.", + skills=skill_library, + temperature=0.0, + memory=False, + ) + agent.response_out.transport = core.pLCMTransport("/tools/responses") + + collector = dimos.deploy(ResponseCollector) + + # Connect modules + agent.message_in.connect(controller.message_out) + collector.response_in.connect(agent.response_out) + + # Start modules + agent.start() + collector.start() + + # Wait for initialization + await asyncio.sleep(1) + + # Test 1: Calculation (fast tool) + logger.info("\n=== Test 1: Calculation Tool ===") + controller.send_query("Use the calculate tool to compute 42 * 17") + await asyncio.sleep(5) # Give more time for the response + + responses = collector.get_responses() + logger.info(f"Got {len(responses)} responses after first query") + assert len(responses) >= 1, ( + f"Should have received at least one response, got {len(responses)}" + ) + + response = responses[-1] + logger.info(f"Response: {response}") + + # Verify the calculation result + assert isinstance(response, AgentResponse), "Expected AgentResponse object" + assert "714" in response.content, f"Expected '714' in response, got: {response.content}" + + # Test 2: Weather query (fast tool) + logger.info("\n=== Test 2: Weather Tool ===") + controller.send_query("What's the weather in New York?") + await asyncio.sleep(5) # Give more time for the second response + + responses = collector.get_responses() + assert len(responses) >= 2, "Should have received at least two responses" + + response = responses[-1] + logger.info(f"Response: {response}") + + # Verify weather details + assert isinstance(response, AgentResponse), "Expected AgentResponse object" + assert "new york" in response.content.lower(), "Expected 'New York' in response" + assert "72" in response.content, "Expected temperature '72' in response" + assert "sunny" in response.content.lower(), "Expected 'sunny' in response" + + # Test 3: Navigation (potentially long-running) + logger.info("\n=== Test 3: Navigation Tool ===") + controller.send_query("Use the NavigationSkill to navigate to the kitchen") + await asyncio.sleep(6) # Give more time for navigation tool to complete + + responses = collector.get_responses() + logger.info(f"Total responses collected: {len(responses)}") + for i, r in enumerate(responses): + logger.info(f" Response {i + 1}: {r.content[:50]}...") + assert len(responses) >= 3, ( + f"Should have received at least three responses, got {len(responses)}" + ) + + response = responses[-1] + logger.info(f"Response: {response}") + + # Verify navigation response + assert isinstance(response, AgentResponse), "Expected AgentResponse object" + assert "kitchen" in response.content.lower(), "Expected 'kitchen' in response" + + # Check if NavigationSkill was called + if response.tool_calls is not None and len(response.tool_calls) > 0: + # Tool was called - verify it + assert any(tc.name == "NavigationSkill" for tc in response.tool_calls), ( + "Expected NavigationSkill to be called" + ) + logger.info("✓ NavigationSkill was called") + else: + # Tool wasn't called - just verify response mentions navigation + logger.info("Note: NavigationSkill was not called, agent gave instructions instead") + + # Stop agent + agent.stop() + + # Print summary + logger.info("\n=== Test Summary ===") + all_responses = collector.get_responses() + for i, resp in enumerate(all_responses): + logger.info( + f"Response {i + 1}: {resp.content if isinstance(resp, AgentResponse) else resp}" + ) + + finally: + dimos.close() + dimos.shutdown() + + +@pytest.mark.tofix +def test_base_agent_direct_tools() -> None: + """Test BaseAgent direct usage with tools.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + # Create skill library + skill_library = SkillLibrary() + skill_library.add(CalculateSkill) + skill_library.add(WeatherSkill) + + # Create agent with skills + agent = BaseAgent( + model="openai::gpt-4o-mini", + system_prompt="You are a helpful assistant with access to a calculator tool. When asked to calculate something, you should use the CalculateSkill tool.", + skills=skill_library, + temperature=0.0, + memory=False, + seed=42, + ) + + # Test calculation with explicit tool request + logger.info("\n=== Direct Test 1: Calculation Tool ===") + response = agent.query("Calculate 144**0.5") + + logger.info(f"Response content: {response.content}") + logger.info(f"Tool calls: {response.tool_calls}") + + assert response.content is not None + assert "12" in response.content or "twelve" in response.content.lower(), ( + f"Expected '12' in response, got: {response.content}" + ) + + # Verify tool was called OR answer is correct + if response.tool_calls is not None: + assert len(response.tool_calls) > 0, "Expected at least one tool call" + assert response.tool_calls[0].name == "CalculateSkill", ( + f"Expected CalculateSkill, got: {response.tool_calls[0].name}" + ) + assert response.tool_calls[0].status == "completed", ( + f"Expected completed status, got: {response.tool_calls[0].status}" + ) + logger.info("✓ Tool was called successfully") + else: + logger.warning("Tool was not called - agent answered directly") + + # Test weather tool + logger.info("\n=== Direct Test 2: Weather Tool ===") + response2 = agent.query("Use the WeatherSkill to check the weather in London") + + logger.info(f"Response content: {response2.content}") + logger.info(f"Tool calls: {response2.tool_calls}") + + assert response2.content is not None + assert "london" in response2.content.lower(), "Expected 'London' in response" + assert "72" in response2.content, "Expected temperature '72' in response" + assert "sunny" in response2.content.lower(), "Expected 'sunny' in response" + + # Verify tool was called + if response2.tool_calls is not None: + assert len(response2.tool_calls) > 0, "Expected at least one tool call" + assert response2.tool_calls[0].name == "WeatherSkill", ( + f"Expected WeatherSkill, got: {response2.tool_calls[0].name}" + ) + logger.info("✓ Weather tool was called successfully") + else: + logger.warning("Weather tool was not called - agent answered directly") + + # Clean up + agent.dispose() + + +class MockToolAgent(BaseAgent): + """Mock agent for CI testing without API calls.""" + + def __init__(self, **kwargs) -> None: + # Skip gateway initialization + self.model = kwargs.get("model", "mock::test") + self.system_prompt = kwargs.get("system_prompt", "Mock agent") + self.skills = kwargs.get("skills", SkillLibrary()) + self.history = [] + self._history_lock = __import__("threading").Lock() + self._supports_vision = False + self.response_subject = None + self.gateway = None + self._executor = None + + async def _process_query_async(self, agent_msg, base64_image=None, base64_images=None): + """Mock tool execution.""" + from dimos.agents.agent_message import AgentMessage + from dimos.agents.agent_types import AgentResponse, ToolCall + + # Get text from AgentMessage + if isinstance(agent_msg, AgentMessage): + query = agent_msg.get_combined_text() + else: + query = str(agent_msg) + + # Simple pattern matching for tools + if "calculate" in query.lower(): + # Extract expression + import re + + match = re.search(r"(\d+\s*[\+\-\*/]\s*\d+)", query) + if match: + expr = match.group(1) + tool_call = ToolCall( + id="mock_calc_1", + name="CalculateSkill", + arguments={"expression": expr}, + status="completed", + ) + # Execute the tool + result = self.skills.call("CalculateSkill", expression=expr) + return AgentResponse( + content=f"I calculated {expr} and {result}", tool_calls=[tool_call] + ) + + # Default response + return AgentResponse(content=f"Mock response to: {query}") + + def dispose(self) -> None: + pass + + +@pytest.mark.tofix +def test_mock_agent_tools() -> None: + """Test mock agent with tools for CI.""" + # Create skill library + skill_library = SkillLibrary() + skill_library.add(CalculateSkill) + + # Create mock agent + agent = MockToolAgent(model="mock::test", skills=skill_library) + + # Test calculation + logger.info("\n=== Mock Test: Calculation ===") + response = agent.query("Calculate 25 + 17") + + logger.info(f"Mock response: {response.content}") + logger.info(f"Mock tool calls: {response.tool_calls}") + + assert response.content is not None + assert "42" in response.content, "Expected '42' in response" + assert response.tool_calls is not None, "Expected tool calls" + assert len(response.tool_calls) == 1, "Expected exactly one tool call" + assert response.tool_calls[0].name == "CalculateSkill", "Expected CalculateSkill" + assert response.tool_calls[0].status == "completed", "Expected completed status" + + # Clean up + agent.dispose() + + +if __name__ == "__main__": + # Run tests + test_mock_agent_tools() + print("✅ Mock agent tools test passed") + + test_base_agent_direct_tools() + print("✅ Direct agent tools test passed") + + asyncio.run(test_agent_module_with_tools()) + print("✅ Module agent tools test passed") + + print("\n✅ All production tool tests passed!") diff --git a/dimos/agents/test_agent_with_modules.py b/dimos/agents/test_agent_with_modules.py new file mode 100644 index 0000000000..1a4ac70f65 --- /dev/null +++ b/dimos/agents/test_agent_with_modules.py @@ -0,0 +1,157 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test agent module with proper module connections.""" + +import asyncio + +from dotenv import load_dotenv +import pytest + +from dimos import core +from dimos.agents.agent_message import AgentMessage +from dimos.agents.agent_types import AgentResponse +from dimos.agents.modules.base_agent import BaseAgentModule +from dimos.core import In, Module, Out, rpc +from dimos.protocol import pubsub + + +# Test query sender module +class QuerySender(Module): + """Module to send test queries.""" + + message_out: Out[AgentMessage] = None + + def __init__(self) -> None: + super().__init__() + + @rpc + def send_query(self, query: str) -> None: + """Send a query.""" + print(f"Sending query: {query}") + msg = AgentMessage() + msg.add_text(query) + self.message_out.publish(msg) + + +# Test response collector module +class ResponseCollector(Module): + """Module to collect responses.""" + + response_in: In[AgentResponse] = None + + def __init__(self) -> None: + super().__init__() + self.responses = [] + + @rpc + def start(self) -> None: + """Start collecting.""" + self.response_in.subscribe(self._on_response) + + def _on_response(self, msg: AgentResponse) -> None: + print(f"Received response: {msg.content if msg.content else msg}") + self.responses.append(msg) + + @rpc + def get_responses(self): + """Get collected responses.""" + return self.responses + + +@pytest.mark.tofix +@pytest.mark.module +@pytest.mark.asyncio +async def test_agent_module_connections() -> None: + """Test agent module with proper connections.""" + load_dotenv() + pubsub.lcm.autoconf() + + # Start Dask + dimos = core.start(4) + + try: + # Deploy modules + sender = dimos.deploy(QuerySender) + agent = dimos.deploy( + BaseAgentModule, + model="openai::gpt-4o-mini", + system_prompt="You are a helpful assistant. Answer in 10 words or less.", + ) + collector = dimos.deploy(ResponseCollector) + + # Configure transports + sender.message_out.transport = core.pLCMTransport("/messages") + agent.response_out.transport = core.pLCMTransport("/responses") + + # Connect modules + agent.message_in.connect(sender.message_out) + collector.response_in.connect(agent.response_out) + + # Start modules + agent.start() + collector.start() + + # Wait for initialization + await asyncio.sleep(1) + + # Test 1: Simple query + print("\n=== Test 1: Simple Query ===") + sender.send_query("What is 2+2?") + + await asyncio.sleep(5) # Increased wait time for API response + + responses = collector.get_responses() + assert len(responses) > 0, "Should have received a response" + assert isinstance(responses[0], AgentResponse), "Expected AgentResponse object" + assert "4" in responses[0].content or "four" in responses[0].content.lower(), ( + "Should calculate correctly" + ) + + # Test 2: Another query + print("\n=== Test 2: Another Query ===") + sender.send_query("What color is the sky?") + + await asyncio.sleep(5) # Increased wait time + + responses = collector.get_responses() + assert len(responses) >= 2, "Should have at least two responses" + assert isinstance(responses[1], AgentResponse), "Expected AgentResponse object" + assert "blue" in responses[1].content.lower(), "Should mention blue" + + # Test 3: Multiple queries + print("\n=== Test 3: Multiple Queries ===") + queries = ["Count from 1 to 3", "Name a fruit", "What is Python?"] + + for q in queries: + sender.send_query(q) + await asyncio.sleep(2) # Give more time between queries + + await asyncio.sleep(8) # More time for multiple queries + + responses = collector.get_responses() + assert len(responses) >= 4, f"Should have at least 4 responses, got {len(responses)}" + + # Stop modules + agent.stop() + + print("\n=== All tests passed! ===") + + finally: + dimos.close() + dimos.shutdown() + + +if __name__ == "__main__": + asyncio.run(test_agent_module_connections()) diff --git a/dimos/agents/test_base_agent_text.py b/dimos/agents/test_base_agent_text.py new file mode 100644 index 0000000000..022bea9cd2 --- /dev/null +++ b/dimos/agents/test_base_agent_text.py @@ -0,0 +1,562 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test BaseAgent text functionality.""" + +import asyncio +import os + +from dotenv import load_dotenv +import pytest + +from dimos import core +from dimos.agents.agent_message import AgentMessage +from dimos.agents.agent_types import AgentResponse +from dimos.agents.modules.base import BaseAgent +from dimos.agents.modules.base_agent import BaseAgentModule +from dimos.core import In, Module, Out, rpc +from dimos.protocol import pubsub + + +class QuerySender(Module): + """Module to send test queries.""" + + message_out: Out[AgentMessage] = None # New AgentMessage output + + @rpc + def send_query(self, query: str) -> None: + """Send a query as AgentMessage.""" + msg = AgentMessage() + msg.add_text(query) + self.message_out.publish(msg) + + @rpc + def send_message(self, message: AgentMessage) -> None: + """Send an AgentMessage.""" + self.message_out.publish(message) + + +class ResponseCollector(Module): + """Module to collect responses.""" + + response_in: In[AgentResponse] = None + + def __init__(self) -> None: + super().__init__() + self.responses = [] + + @rpc + def start(self) -> None: + """Start collecting.""" + self.response_in.subscribe(self._on_response) + + def _on_response(self, msg) -> None: + self.responses.append(msg) + + @rpc + def get_responses(self): + """Get collected responses.""" + return self.responses + + +@pytest.mark.tofix +def test_base_agent_direct_text() -> None: + """Test BaseAgent direct text usage.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + # Create agent + agent = BaseAgent( + model="openai::gpt-4o-mini", + system_prompt="You are a helpful assistant. Answer in 10 words or less.", + temperature=0.0, + seed=42, # Fixed seed for deterministic results + ) + + # Test simple query with string (backward compatibility) + response = agent.query("What is 2+2?") + print(f"\n[Test] Query: 'What is 2+2?' -> Response: '{response.content}'") + assert response.content is not None + assert "4" in response.content or "four" in response.content.lower(), ( + f"Expected '4' or 'four' in response, got: {response.content}" + ) + + # Test with AgentMessage + msg = AgentMessage() + msg.add_text("What is 3+3?") + response = agent.query(msg) + print(f"[Test] Query: 'What is 3+3?' -> Response: '{response.content}'") + assert response.content is not None + assert "6" in response.content or "six" in response.content.lower(), ( + "Expected '6' or 'six' in response" + ) + + # Test conversation history + response = agent.query("What was my previous question?") + print(f"[Test] Query: 'What was my previous question?' -> Response: '{response.content}'") + assert response.content is not None + # The agent should reference one of the previous questions + # It might say "2+2" or "3+3" depending on interpretation of "previous" + assert ( + "2+2" in response.content or "3+3" in response.content or "What is" in response.content + ), f"Expected reference to a previous question, got: {response.content}" + + # Clean up + agent.dispose() + + +@pytest.mark.tofix +@pytest.mark.asyncio +async def test_base_agent_async_text() -> None: + """Test BaseAgent async text usage.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + # Create agent + agent = BaseAgent( + model="openai::gpt-4o-mini", + system_prompt="You are a helpful assistant.", + temperature=0.0, + seed=42, + ) + + # Test async query with string + response = await agent.aquery("What is the capital of France?") + assert response.content is not None + assert "Paris" in response.content, "Expected 'Paris' in response" + + # Test async query with AgentMessage + msg = AgentMessage() + msg.add_text("What is the capital of Germany?") + response = await agent.aquery(msg) + assert response.content is not None + assert "Berlin" in response.content, "Expected 'Berlin' in response" + + # Clean up + agent.dispose() + + +@pytest.mark.tofix +@pytest.mark.module +@pytest.mark.asyncio +async def test_base_agent_module_text() -> None: + """Test BaseAgentModule with text via DimOS.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + pubsub.lcm.autoconf() + dimos = core.start(4) + + try: + # Deploy modules + sender = dimos.deploy(QuerySender) + agent = dimos.deploy( + BaseAgentModule, + model="openai::gpt-4o-mini", + system_prompt="You are a helpful assistant. Answer concisely.", + ) + collector = dimos.deploy(ResponseCollector) + + # Configure transports + sender.message_out.transport = core.pLCMTransport("/test/messages") + agent.response_out.transport = core.pLCMTransport("/test/responses") + + # Connect modules + agent.message_in.connect(sender.message_out) + collector.response_in.connect(agent.response_out) + + # Start modules + agent.start() + collector.start() + + # Wait for initialization + await asyncio.sleep(1) + + # Test queries + sender.send_query("What is 2+2?") + await asyncio.sleep(3) + + responses = collector.get_responses() + assert len(responses) > 0, "Should have received a response" + resp = responses[0] + assert isinstance(resp, AgentResponse), "Expected AgentResponse object" + assert "4" in resp.content or "four" in resp.content.lower(), ( + f"Expected '4' or 'four' in response, got: {resp.content}" + ) + + # Test another query + sender.send_query("What color is the sky?") + await asyncio.sleep(3) + + responses = collector.get_responses() + assert len(responses) >= 2, "Should have at least two responses" + resp = responses[1] + assert isinstance(resp, AgentResponse), "Expected AgentResponse object" + assert "blue" in resp.content.lower(), "Expected 'blue' in response" + + # Test conversation history + sender.send_query("What was my first question?") + await asyncio.sleep(3) + + responses = collector.get_responses() + assert len(responses) >= 3, "Should have at least three responses" + resp = responses[2] + assert isinstance(resp, AgentResponse), "Expected AgentResponse object" + assert "2+2" in resp.content or "2" in resp.content, "Expected reference to first question" + + # Stop modules + agent.stop() + + finally: + dimos.close() + dimos.shutdown() + + +@pytest.mark.parametrize( + "model,provider", + [ + ("openai::gpt-4o-mini", "openai"), + ("anthropic::claude-3-haiku-20240307", "anthropic"), + ("cerebras::llama-3.3-70b", "cerebras"), + ], +) +@pytest.mark.tofix +def test_base_agent_providers(model, provider) -> None: + """Test BaseAgent with different providers.""" + load_dotenv() + + # Check for API key + api_key_map = { + "openai": "OPENAI_API_KEY", + "anthropic": "ANTHROPIC_API_KEY", + "cerebras": "CEREBRAS_API_KEY", + } + + if not os.getenv(api_key_map[provider]): + pytest.skip(f"No {api_key_map[provider]} found") + + # Create agent + agent = BaseAgent( + model=model, + system_prompt="You are a helpful assistant. Answer in 10 words or less.", + temperature=0.0, + seed=42, + ) + + # Test query with AgentMessage + msg = AgentMessage() + msg.add_text("What is the capital of France?") + response = agent.query(msg) + assert response.content is not None + assert "Paris" in response.content, f"Expected 'Paris' in response from {provider}" + + # Clean up + agent.dispose() + + +@pytest.mark.tofix +def test_base_agent_memory() -> None: + """Test BaseAgent with memory/RAG.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + # Create agent + agent = BaseAgent( + model="openai::gpt-4o-mini", + system_prompt="You are a helpful assistant. Use the provided context when answering.", + temperature=0.0, + rag_threshold=0.3, + seed=42, + ) + + # Add context to memory + agent.memory.add_vector("doc1", "The DimOS framework is designed for building robotic systems.") + agent.memory.add_vector( + "doc2", "Robots using DimOS can perform navigation and manipulation tasks." + ) + + # Test RAG retrieval with AgentMessage + msg = AgentMessage() + msg.add_text("What is DimOS?") + response = agent.query(msg) + assert response.content is not None + assert "framework" in response.content.lower() or "robotic" in response.content.lower(), ( + "Expected context about DimOS in response" + ) + + # Clean up + agent.dispose() + + +class MockAgent(BaseAgent): + """Mock agent for testing without API calls.""" + + def __init__(self, **kwargs) -> None: + # Don't call super().__init__ to avoid gateway initialization + from dimos.agents.agent_types import ConversationHistory + + self.model = kwargs.get("model", "mock::test") + self.system_prompt = kwargs.get("system_prompt", "Mock agent") + self.conversation = ConversationHistory(max_size=20) + self._supports_vision = False + self.response_subject = None # Simplified + + async def _process_query_async(self, query: str, base64_image=None) -> str: + """Mock response.""" + if "2+2" in query: + return "The answer is 4" + elif "capital" in query and "France" in query: + return "The capital of France is Paris" + elif "color" in query and "sky" in query: + return "The sky is blue" + elif "previous" in query: + history = self.conversation.to_openai_format() + if len(history) >= 2: + # Get the second to last item (the last user query before this one) + for i in range(len(history) - 2, -1, -1): + if history[i]["role"] == "user": + return f"Your previous question was: {history[i]['content']}" + return "No previous questions" + else: + return f"Mock response to: {query}" + + def query(self, message) -> AgentResponse: + """Mock synchronous query.""" + # Convert to text if AgentMessage + if isinstance(message, AgentMessage): + text = message.get_combined_text() + else: + text = message + + # Update conversation history + self.conversation.add_user_message(text) + response = asyncio.run(self._process_query_async(text)) + self.conversation.add_assistant_message(response) + return AgentResponse(content=response) + + async def aquery(self, message) -> AgentResponse: + """Mock async query.""" + # Convert to text if AgentMessage + if isinstance(message, AgentMessage): + text = message.get_combined_text() + else: + text = message + + self.conversation.add_user_message(text) + response = await self._process_query_async(text) + self.conversation.add_assistant_message(response) + return AgentResponse(content=response) + + def dispose(self) -> None: + """Mock dispose.""" + pass + + +@pytest.mark.tofix +def test_mock_agent() -> None: + """Test mock agent for CI without API keys.""" + # Create mock agent + agent = MockAgent(model="mock::test", system_prompt="Mock assistant") + + # Test simple query + response = agent.query("What is 2+2?") + assert isinstance(response, AgentResponse), "Expected AgentResponse object" + assert "4" in response.content + + # Test conversation history + response = agent.query("What was my previous question?") + assert isinstance(response, AgentResponse), "Expected AgentResponse object" + assert "2+2" in response.content + + # Test other queries + response = agent.query("What is the capital of France?") + assert isinstance(response, AgentResponse), "Expected AgentResponse object" + assert "Paris" in response.content + + response = agent.query("What color is the sky?") + assert isinstance(response, AgentResponse), "Expected AgentResponse object" + assert "blue" in response.content.lower() + + # Clean up + agent.dispose() + + +@pytest.mark.tofix +def test_base_agent_conversation_history() -> None: + """Test that conversation history is properly maintained.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + # Create agent + agent = BaseAgent( + model="openai::gpt-4o-mini", + system_prompt="You are a helpful assistant.", + temperature=0.0, + seed=42, + ) + + # Test 1: Simple conversation + response1 = agent.query("My name is Alice") + assert isinstance(response1, AgentResponse) + + # Check conversation history has both messages + assert agent.conversation.size() == 2 + history = agent.conversation.to_openai_format() + assert history[0]["role"] == "user" + assert history[0]["content"] == "My name is Alice" + assert history[1]["role"] == "assistant" + + # Test 2: Reference previous context + response2 = agent.query("What is my name?") + assert "Alice" in response2.content, "Agent should remember the name" + + # Conversation history should now have 4 messages + assert agent.conversation.size() == 4 + + # Test 3: Multiple text parts in AgentMessage + msg = AgentMessage() + msg.add_text("Calculate") + msg.add_text("the sum of") + msg.add_text("5 + 3") + + response3 = agent.query(msg) + assert "8" in response3.content or "eight" in response3.content.lower() + + # Check the combined text was stored correctly + assert agent.conversation.size() == 6 + history = agent.conversation.to_openai_format() + assert history[4]["role"] == "user" + assert history[4]["content"] == "Calculate the sum of 5 + 3" + + # Test 4: History trimming (set low limit) + agent.max_history = 4 + agent.query("What was my first message?") + + # Conversation history should be trimmed to 4 messages + assert agent.conversation.size() == 4 + # First messages should be gone + history = agent.conversation.to_openai_format() + assert "Alice" not in history[0]["content"] + + # Clean up + agent.dispose() + + +@pytest.mark.tofix +def test_base_agent_history_with_tools() -> None: + """Test conversation history with tool calls.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + from pydantic import Field + + from dimos.skills.skills import AbstractSkill, SkillLibrary + + class CalculatorSkill(AbstractSkill): + """Perform calculations.""" + + expression: str = Field(description="Mathematical expression") + + def __call__(self) -> str: + try: + result = eval(self.expression) + return f"The result is {result}" + except: + return "Error in calculation" + + # Create agent with calculator skill + skills = SkillLibrary() + skills.add(CalculatorSkill) + + agent = BaseAgent( + model="openai::gpt-4o-mini", + system_prompt="You are a helpful assistant with a calculator. Use the calculator tool when asked to compute something.", + skills=skills, + temperature=0.0, + seed=42, + ) + + # Make a query that should trigger tool use + response = agent.query("Please calculate 42 * 17 using the calculator tool") + + # Check response + assert isinstance(response, AgentResponse) + assert "714" in response.content, f"Expected 714 in response, got: {response.content}" + + # Check tool calls were made + if response.tool_calls: + assert len(response.tool_calls) > 0 + assert response.tool_calls[0].name == "CalculatorSkill" + assert response.tool_calls[0].status == "completed" + + # Check history structure + # If tools were called, we should have more messages + if response.tool_calls and len(response.tool_calls) > 0: + assert agent.conversation.size() >= 3, ( + f"Expected at least 3 messages in history when tools are used, got {agent.conversation.size()}" + ) + + # Find the assistant message with tool calls + history = agent.conversation.to_openai_format() + tool_msg_found = False + tool_result_found = False + + for msg in history: + if msg.get("role") == "assistant" and msg.get("tool_calls"): + tool_msg_found = True + if msg.get("role") == "tool": + tool_result_found = True + assert "result" in msg.get("content", "").lower() + + assert tool_msg_found, "Tool call message should be in history when tools were used" + assert tool_result_found, "Tool result should be in history when tools were used" + else: + # No tools used, just verify we have user and assistant messages + assert agent.conversation.size() >= 2, ( + f"Expected at least 2 messages in history, got {agent.conversation.size()}" + ) + # The model solved it without using the tool - that's also acceptable + print("Note: Model solved without using the calculator tool") + + # Clean up + agent.dispose() + + +if __name__ == "__main__": + test_base_agent_direct_text() + asyncio.run(test_base_agent_async_text()) + asyncio.run(test_base_agent_module_text()) + test_base_agent_memory() + test_mock_agent() + test_base_agent_conversation_history() + test_base_agent_history_with_tools() + print("\n✅ All text tests passed!") + test_base_agent_direct_text() + asyncio.run(test_base_agent_async_text()) + asyncio.run(test_base_agent_module_text()) + test_base_agent_memory() + test_mock_agent() + print("\n✅ All text tests passed!") diff --git a/dimos/agents/test_conversation_history.py b/dimos/agents/test_conversation_history.py new file mode 100644 index 0000000000..95b28fbc0b --- /dev/null +++ b/dimos/agents/test_conversation_history.py @@ -0,0 +1,416 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Comprehensive conversation history tests for agents.""" + +import asyncio +import logging +import os + +from dotenv import load_dotenv +import numpy as np +from pydantic import Field +import pytest + +from dimos.agents.agent_message import AgentMessage +from dimos.agents.agent_types import AgentResponse +from dimos.agents.modules.base import BaseAgent +from dimos.msgs.sensor_msgs import Image +from dimos.skills.skills import AbstractSkill, SkillLibrary + +logger = logging.getLogger(__name__) + + +@pytest.mark.tofix +def test_conversation_history_basic() -> None: + """Test basic conversation history functionality.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + agent = BaseAgent( + model="openai::gpt-4o-mini", + system_prompt="You are a helpful assistant with perfect memory.", + temperature=0.0, + seed=42, + ) + + try: + # Test 1: Simple text conversation + response1 = agent.query("My favorite color is blue") + assert isinstance(response1, AgentResponse) + assert agent.conversation.size() == 2 # user + assistant + + # Test 2: Reference previous information + response2 = agent.query("What is my favorite color?") + assert "blue" in response2.content.lower(), "Agent should remember the color" + assert agent.conversation.size() == 4 + + # Test 3: Multiple facts + agent.query("I live in San Francisco") + agent.query("I work as an engineer") + + # Verify history is building up + assert agent.conversation.size() == 8 # 4 exchanges (blue, what color, SF, engineer) + + response = agent.query("Tell me what you know about me") + + # Check if agent remembers at least some facts + # Note: Models may sometimes give generic responses, so we check for any memory + facts_mentioned = 0 + if "blue" in response.content.lower() or "color" in response.content.lower(): + facts_mentioned += 1 + if "san francisco" in response.content.lower() or "francisco" in response.content.lower(): + facts_mentioned += 1 + if "engineer" in response.content.lower(): + facts_mentioned += 1 + + # Agent should remember at least one fact, or acknowledge the conversation + assert facts_mentioned > 0 or "know" in response.content.lower(), ( + f"Agent should show some memory of conversation, got: {response.content}" + ) + + # Verify history properly accumulates + assert agent.conversation.size() == 10 + + finally: + agent.dispose() + + +@pytest.mark.tofix +def test_conversation_history_with_images() -> None: + """Test conversation history with multimodal content.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + agent = BaseAgent( + model="openai::gpt-4o-mini", + system_prompt="You are a helpful vision assistant.", + temperature=0.0, + seed=42, + ) + + try: + # Send text message + agent.query("I'm going to show you some colors") + assert agent.conversation.size() == 2 + + # Send image with text + msg = AgentMessage() + msg.add_text("This is a red square") + red_img = Image(data=np.full((100, 100, 3), [255, 0, 0], dtype=np.uint8)) + msg.add_image(red_img) + + agent.query(msg) + assert agent.conversation.size() == 4 + + # Ask about the image + response3 = agent.query("What color did I just show you?") + # Check for any color mention (models sometimes see colors differently) + assert any( + color in response3.content.lower() + for color in ["red", "blue", "green", "color", "square"] + ), f"Should mention a color, got: {response3.content}" + + # Send another image + msg2 = AgentMessage() + msg2.add_text("Now here's a blue square") + blue_img = Image(data=np.full((100, 100, 3), [0, 0, 255], dtype=np.uint8)) + msg2.add_image(blue_img) + + agent.query(msg2) + assert agent.conversation.size() == 8 + + # Ask about all images + response5 = agent.query("What colors have I shown you?") + # Should mention seeing images/colors even if specific colors are wrong + assert any( + word in response5.content.lower() + for word in ["red", "blue", "colors", "squares", "images", "shown", "two"] + ), f"Should acknowledge seeing images, got: {response5.content}" + + # Verify both message types are in history + assert agent.conversation.size() == 10 + + finally: + agent.dispose() + + +@pytest.mark.tofix +def test_conversation_history_trimming() -> None: + """Test that conversation history is trimmed to max size.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + # Create agent with small history limit + agent = BaseAgent( + model="openai::gpt-4o-mini", + system_prompt="You are a helpful assistant.", + temperature=0.0, + max_history=3, # Keep 3 message pairs (6 messages total) + seed=42, + ) + + try: + # Add several messages + agent.query("Message 1: I like apples") + assert agent.conversation.size() == 2 + + agent.query("Message 2: I like oranges") + # Now we have 2 pairs (4 messages) + # max_history=3 means we keep max 3 messages total (not pairs!) + size = agent.conversation.size() + # After trimming to 3, we'd have kept the most recent 3 messages + assert size == 3, f"After Message 2, size should be 3, got {size}" + + agent.query("Message 3: I like bananas") + size = agent.conversation.size() + assert size == 3, f"After Message 3, size should be 3, got {size}" + + # This should maintain trimming + agent.query("Message 4: I like grapes") + size = agent.conversation.size() + assert size == 3, f"After Message 4, size should still be 3, got {size}" + + # Add one more + agent.query("Message 5: I like strawberries") + size = agent.conversation.size() + assert size == 3, f"After Message 5, size should still be 3, got {size}" + + # Early messages should be trimmed + agent.query("What was the first fruit I mentioned?") + size = agent.conversation.size() + assert size == 3, f"After question, size should still be 3, got {size}" + + # Change max_history dynamically + agent.max_history = 2 + agent.query("New message after resize") + # Now history should be trimmed to 2 messages + size = agent.conversation.size() + assert size == 2, f"After resize to max_history=2, size should be 2, got {size}" + + finally: + agent.dispose() + + +@pytest.mark.tofix +def test_conversation_history_with_tools() -> None: + """Test conversation history with tool calls.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + # Create a simple skill + class CalculatorSkillLocal(AbstractSkill): + """A simple calculator skill.""" + + expression: str = Field(description="Mathematical expression to evaluate") + + def __call__(self) -> str: + try: + result = eval(self.expression) + return f"The result is {result}" + except Exception as e: + return f"Error: {e}" + + # Create skill library properly + class TestSkillLibrary(SkillLibrary): + CalculatorSkill = CalculatorSkillLocal + + agent = BaseAgent( + model="openai::gpt-4o-mini", + system_prompt="You are a helpful assistant with access to a calculator.", + skills=TestSkillLibrary(), + temperature=0.0, + seed=100, + ) + + try: + # Initial query + agent.query("Hello, I need help with math") + assert agent.conversation.size() == 2 + + # Force tool use explicitly + response2 = agent.query( + "I need you to use the CalculatorSkill tool to compute 123 * 456. " + "Do NOT calculate it yourself - you MUST use the calculator tool function." + ) + + assert agent.conversation.size() == 6 # 2 + 1 + 3 + assert response2.tool_calls is not None and len(response2.tool_calls) > 0 + assert "56088" in response2.content.replace(",", "") + + # Ask about previous calculation + response3 = agent.query("What was the result of the calculation?") + assert "56088" in response3.content.replace(",", "") or "123" in response3.content.replace( + ",", "" + ) + assert agent.conversation.size() == 8 + + finally: + agent.dispose() + + +@pytest.mark.tofix +def test_conversation_thread_safety() -> None: + """Test that conversation history is thread-safe.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + agent = BaseAgent(model="openai::gpt-4o-mini", temperature=0.0, seed=42) + + try: + + async def query_async(text: str): + """Async wrapper for query.""" + return await agent.aquery(text) + + async def run_concurrent(): + """Run multiple queries concurrently.""" + tasks = [query_async(f"Query {i}") for i in range(3)] + return await asyncio.gather(*tasks) + + # Run concurrent queries + results = asyncio.run(run_concurrent()) + assert len(results) == 3 + + # Should have roughly 6 messages (3 queries * 2) + # Exact count may vary due to thread timing + assert agent.conversation.size() >= 4 + assert agent.conversation.size() <= 6 + + finally: + agent.dispose() + + +@pytest.mark.tofix +def test_conversation_history_formats() -> None: + """Test ConversationHistory formatting methods.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + agent = BaseAgent(model="openai::gpt-4o-mini", temperature=0.0, seed=42) + + try: + # Create a conversation + agent.conversation.add_user_message("Hello") + agent.conversation.add_assistant_message("Hi there!") + + # Test text with images + agent.conversation.add_user_message( + [ + {"type": "text", "text": "Look at this"}, + {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,abc123"}}, + ] + ) + agent.conversation.add_assistant_message("I see the image") + + # Test tool messages + agent.conversation.add_assistant_message( + content="", + tool_calls=[ + { + "id": "call_123", + "type": "function", + "function": {"name": "test", "arguments": "{}"}, + } + ], + ) + agent.conversation.add_tool_result( + tool_call_id="call_123", content="Tool result", name="test" + ) + + # Get OpenAI format + messages = agent.conversation.to_openai_format() + assert len(messages) == 6 + + # Verify message formats + assert messages[0]["role"] == "user" + assert messages[0]["content"] == "Hello" + + assert messages[2]["role"] == "user" + assert isinstance(messages[2]["content"], list) + + # Tool response message should be at index 5 (after assistant with tool_calls at index 4) + assert messages[5]["role"] == "tool" + assert messages[5]["tool_call_id"] == "call_123" + assert messages[5]["name"] == "test" + + finally: + agent.dispose() + + +@pytest.mark.tofix +@pytest.mark.timeout(30) # Add timeout to prevent hanging +def test_conversation_edge_cases() -> None: + """Test edge cases in conversation history.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OPENAI_API_KEY found") + + agent = BaseAgent( + model="openai::gpt-4o-mini", + system_prompt="You are a helpful assistant.", + temperature=0.0, + seed=42, + ) + + try: + # Empty message + msg1 = AgentMessage() + msg1.add_text("") + response1 = agent.query(msg1) + assert response1.content is not None + + # Moderately long message (reduced from 1000 to 100 words) + long_text = "word " * 100 + response2 = agent.query(long_text) + assert response2.content is not None + + # Multiple text parts that combine + msg3 = AgentMessage() + for i in range(5): # Reduced from 10 to 5 + msg3.add_text(f"Part {i} ") + response3 = agent.query(msg3) + assert response3.content is not None + + # Verify history is maintained correctly + assert agent.conversation.size() == 6 # 3 exchanges + + finally: + agent.dispose() + + +if __name__ == "__main__": + # Run tests + test_conversation_history_basic() + test_conversation_history_with_images() + test_conversation_history_trimming() + test_conversation_history_with_tools() + test_conversation_thread_safety() + test_conversation_history_formats() + test_conversation_edge_cases() + print("\n✅ All conversation history tests passed!") diff --git a/dimos/agents/test_gateway.py b/dimos/agents/test_gateway.py new file mode 100644 index 0000000000..2c54d5d1ac --- /dev/null +++ b/dimos/agents/test_gateway.py @@ -0,0 +1,203 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test gateway functionality.""" + +import asyncio +import os + +from dotenv import load_dotenv +import pytest + +from dimos.agents.modules.gateway import UnifiedGatewayClient + + +@pytest.mark.tofix +@pytest.mark.asyncio +async def test_gateway_basic() -> None: + """Test basic gateway functionality.""" + load_dotenv() + + # Check for at least one API key + has_api_key = any( + [os.getenv("OPENAI_API_KEY"), os.getenv("ANTHROPIC_API_KEY"), os.getenv("CEREBRAS_API_KEY")] + ) + + if not has_api_key: + pytest.skip("No API keys found for gateway test") + + gateway = UnifiedGatewayClient() + + try: + # Test with available provider + if os.getenv("OPENAI_API_KEY"): + model = "openai::gpt-4o-mini" + elif os.getenv("ANTHROPIC_API_KEY"): + model = "anthropic::claude-3-haiku-20240307" + else: + model = "cerebras::llama3.1-8b" + + messages = [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Say 'Hello Gateway' and nothing else."}, + ] + + # Test non-streaming + response = await gateway.ainference( + model=model, messages=messages, temperature=0.0, max_tokens=10 + ) + + assert "choices" in response + assert len(response["choices"]) > 0 + assert "message" in response["choices"][0] + assert "content" in response["choices"][0]["message"] + + content = response["choices"][0]["message"]["content"] + assert "hello" in content.lower() or "gateway" in content.lower() + + finally: + gateway.close() + + +@pytest.mark.tofix +@pytest.mark.asyncio +async def test_gateway_streaming() -> None: + """Test gateway streaming functionality.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("OpenAI API key required for streaming test") + + gateway = UnifiedGatewayClient() + + try: + messages = [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Count from 1 to 3"}, + ] + + # Test streaming + chunks = [] + async for chunk in await gateway.ainference( + model="openai::gpt-4o-mini", messages=messages, temperature=0.0, stream=True + ): + chunks.append(chunk) + + assert len(chunks) > 0, "Should receive stream chunks" + + # Reconstruct content + content = "" + for chunk in chunks: + if chunk.get("choices"): + delta = chunk["choices"][0].get("delta", {}) + chunk_content = delta.get("content") + if chunk_content is not None: + content += chunk_content + + assert any(str(i) in content for i in [1, 2, 3]), "Should count numbers" + + finally: + gateway.close() + + +@pytest.mark.tofix +@pytest.mark.asyncio +async def test_gateway_tools() -> None: + """Test gateway can pass tool definitions to LLM and get responses.""" + load_dotenv() + + if not os.getenv("OPENAI_API_KEY"): + pytest.skip("OpenAI API key required for tools test") + + gateway = UnifiedGatewayClient() + + try: + # Just test that gateway accepts tools parameter and returns valid response + tools = [ + { + "type": "function", + "function": { + "name": "test_function", + "description": "A test function", + "parameters": { + "type": "object", + "properties": {"param": {"type": "string"}}, + }, + }, + } + ] + + messages = [ + {"role": "user", "content": "Hello, just testing the gateway"}, + ] + + # Just verify gateway doesn't crash when tools are provided + response = await gateway.ainference( + model="openai::gpt-4o-mini", messages=messages, tools=tools, temperature=0.0 + ) + + # Basic validation - gateway returned something + assert "choices" in response + assert len(response["choices"]) > 0 + assert "message" in response["choices"][0] + + finally: + gateway.close() + + +@pytest.mark.tofix +@pytest.mark.asyncio +async def test_gateway_providers() -> None: + """Test gateway with different providers.""" + load_dotenv() + + gateway = UnifiedGatewayClient() + + providers_tested = 0 + + try: + # Test each available provider + test_cases = [ + ("openai::gpt-4o-mini", "OPENAI_API_KEY"), + ("anthropic::claude-3-haiku-20240307", "ANTHROPIC_API_KEY"), + # ("cerebras::llama3.1-8b", "CEREBRAS_API_KEY"), + ("qwen::qwen-turbo", "DASHSCOPE_API_KEY"), + ] + + for model, env_var in test_cases: + if not os.getenv(env_var): + continue + + providers_tested += 1 + + messages = [{"role": "user", "content": "Reply with just the word 'OK'"}] + + response = await gateway.ainference( + model=model, messages=messages, temperature=0.0, max_tokens=10 + ) + + assert "choices" in response + content = response["choices"][0]["message"]["content"] + assert len(content) > 0, f"{model} should return content" + + if providers_tested == 0: + pytest.skip("No API keys found for provider test") + + finally: + gateway.close() + + +if __name__ == "__main__": + load_dotenv() + asyncio.run(test_gateway_basic()) diff --git a/dimos/agents/test_simple_agent_module.py b/dimos/agents/test_simple_agent_module.py new file mode 100644 index 0000000000..bd374877dd --- /dev/null +++ b/dimos/agents/test_simple_agent_module.py @@ -0,0 +1,222 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test simple agent module with string input/output.""" + +import asyncio +import os + +from dotenv import load_dotenv +import pytest + +from dimos import core +from dimos.agents.agent_message import AgentMessage +from dimos.agents.agent_types import AgentResponse +from dimos.agents.modules.base_agent import BaseAgentModule +from dimos.core import In, Module, Out, rpc +from dimos.protocol import pubsub + + +class QuerySender(Module): + """Module to send test queries.""" + + message_out: Out[AgentMessage] = None + + @rpc + def send_query(self, query: str) -> None: + """Send a query.""" + msg = AgentMessage() + msg.add_text(query) + self.message_out.publish(msg) + + +class ResponseCollector(Module): + """Module to collect responses.""" + + response_in: In[AgentResponse] = None + + def __init__(self) -> None: + super().__init__() + self.responses = [] + + @rpc + def start(self) -> None: + """Start collecting.""" + self.response_in.subscribe(self._on_response) + + def _on_response(self, response: AgentResponse) -> None: + """Handle response.""" + self.responses.append(response) + + @rpc + def get_responses(self) -> list: + """Get collected responses.""" + return self.responses + + @rpc + def clear(self) -> None: + """Clear responses.""" + self.responses = [] + + +@pytest.mark.tofix +@pytest.mark.module +@pytest.mark.asyncio +@pytest.mark.parametrize( + "model,provider", + [ + ("openai::gpt-4o-mini", "OpenAI"), + ("anthropic::claude-3-haiku-20240307", "Claude"), + ("cerebras::llama3.1-8b", "Cerebras"), + ("qwen::qwen-turbo", "Qwen"), + ], +) +async def test_simple_agent_module(model, provider) -> None: + """Test simple agent module with different providers.""" + load_dotenv() + + # Skip if no API key + if provider == "OpenAI" and not os.getenv("OPENAI_API_KEY"): + pytest.skip("No OpenAI API key found") + elif provider == "Claude" and not os.getenv("ANTHROPIC_API_KEY"): + pytest.skip("No Anthropic API key found") + elif provider == "Cerebras" and not os.getenv("CEREBRAS_API_KEY"): + pytest.skip("No Cerebras API key found") + elif provider == "Qwen" and not os.getenv("ALIBABA_API_KEY"): + pytest.skip("No Qwen API key found") + + pubsub.lcm.autoconf() + + # Start Dask cluster + dimos = core.start(3) + + try: + # Deploy modules + sender = dimos.deploy(QuerySender) + agent = dimos.deploy( + BaseAgentModule, + model=model, + system_prompt=f"You are a helpful {provider} assistant. Keep responses brief.", + ) + collector = dimos.deploy(ResponseCollector) + + # Configure transports + sender.message_out.transport = core.pLCMTransport(f"/test/{provider}/messages") + agent.response_out.transport = core.pLCMTransport(f"/test/{provider}/responses") + + # Connect modules + agent.message_in.connect(sender.message_out) + collector.response_in.connect(agent.response_out) + + # Start modules + agent.start() + collector.start() + + await asyncio.sleep(1) + + # Test simple math + sender.send_query("What is 2+2?") + await asyncio.sleep(5) + + responses = collector.get_responses() + assert len(responses) > 0, f"{provider} should respond" + assert isinstance(responses[0], AgentResponse), "Expected AgentResponse object" + assert "4" in responses[0].content, f"{provider} should calculate correctly" + + # Test brief response + collector.clear() + sender.send_query("Name one color.") + await asyncio.sleep(5) + + responses = collector.get_responses() + assert len(responses) > 0, f"{provider} should respond" + assert isinstance(responses[0], AgentResponse), "Expected AgentResponse object" + assert len(responses[0].content) < 200, f"{provider} should give brief response" + + # Stop modules + agent.stop() + + finally: + dimos.close() + dimos.shutdown() + + +@pytest.mark.tofix +@pytest.mark.module +@pytest.mark.asyncio +async def test_mock_agent_module() -> None: + """Test agent module with mock responses (no API needed).""" + pubsub.lcm.autoconf() + + class MockAgentModule(Module): + """Mock agent for testing.""" + + message_in: In[AgentMessage] = None + response_out: Out[AgentResponse] = None + + @rpc + def start(self) -> None: + self.message_in.subscribe(self._handle_message) + + def _handle_message(self, msg: AgentMessage) -> None: + query = msg.get_combined_text() + if "2+2" in query: + self.response_out.publish(AgentResponse(content="4")) + elif "color" in query.lower(): + self.response_out.publish(AgentResponse(content="Blue")) + else: + self.response_out.publish(AgentResponse(content=f"Mock response to: {query}")) + + dimos = core.start(2) + + try: + # Deploy + agent = dimos.deploy(MockAgentModule) + collector = dimos.deploy(ResponseCollector) + + # Configure + agent.message_in.transport = core.pLCMTransport("/mock/messages") + agent.response_out.transport = core.pLCMTransport("/mock/response") + + # Connect + collector.response_in.connect(agent.response_out) + + # Start + agent.start() + collector.start() + + await asyncio.sleep(1) + + # Test - use a simple query sender + sender = dimos.deploy(QuerySender) + sender.message_out.transport = core.pLCMTransport("/mock/messages") + agent.message_in.connect(sender.message_out) + + await asyncio.sleep(1) + + sender.send_query("What is 2+2?") + await asyncio.sleep(1) + + responses = collector.get_responses() + assert len(responses) == 1 + assert isinstance(responses[0], AgentResponse), "Expected AgentResponse object" + assert responses[0].content == "4" + + finally: + dimos.close() + dimos.shutdown() + + +if __name__ == "__main__": + asyncio.run(test_mock_agent_module()) diff --git a/dimos/agents/tokenizer/base.py b/dimos/agents/tokenizer/base.py index 8097bdd685..7957c896fa 100644 --- a/dimos/agents/tokenizer/base.py +++ b/dimos/agents/tokenizer/base.py @@ -13,8 +13,6 @@ # limitations under the License. from abc import ABC, abstractmethod -import tiktoken -from dimos.utils.logging_config import setup_logger # TODO: Add a class for specific tokenizer exceptions # TODO: Build out testing and logging @@ -22,9 +20,8 @@ class AbstractTokenizer(ABC): - @abstractmethod - def tokenize_text(self, text): + def tokenize_text(self, text: str): pass @abstractmethod @@ -32,9 +29,9 @@ def detokenize_text(self, tokenized_text): pass @abstractmethod - def token_count(self, text): + def token_count(self, text: str): pass @abstractmethod - def image_token_count(self, image_width, image_height, image_detail="low"): + def image_token_count(self, image_width, image_height, image_detail: str = "low"): pass diff --git a/dimos/agents/tokenizer/huggingface_tokenizer.py b/dimos/agents/tokenizer/huggingface_tokenizer.py index e06a2520da..34ace64fb0 100644 --- a/dimos/agents/tokenizer/huggingface_tokenizer.py +++ b/dimos/agents/tokenizer/huggingface_tokenizer.py @@ -13,12 +13,13 @@ # limitations under the License. from transformers import AutoTokenizer + from dimos.agents.tokenizer.base import AbstractTokenizer from dimos.utils.logging_config import setup_logger -class HuggingFaceTokenizer(AbstractTokenizer): - def __init__(self, model_name: str = "Qwen/Qwen2.5-0.5B", **kwargs): +class HuggingFaceTokenizer(AbstractTokenizer): + def __init__(self, model_name: str = "Qwen/Qwen2.5-0.5B", **kwargs) -> None: super().__init__(**kwargs) # Initilize the tokenizer for the huggingface models @@ -27,10 +28,10 @@ def __init__(self, model_name: str = "Qwen/Qwen2.5-0.5B", **kwargs): self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) except Exception as e: raise ValueError( - f"Failed to initialize tokenizer for model {self.model_name}. Error: {str(e)}" + f"Failed to initialize tokenizer for model {self.model_name}. Error: {e!s}" ) - def tokenize_text(self, text): + def tokenize_text(self, text: str): """ Tokenize a text string using the openai tokenizer. """ @@ -43,28 +44,26 @@ def detokenize_text(self, tokenized_text): try: return self.tokenizer.decode(tokenized_text, errors="ignore") except Exception as e: - raise ValueError(f"Failed to detokenize text. Error: {str(e)}") + raise ValueError(f"Failed to detokenize text. Error: {e!s}") - def token_count(self, text): + def token_count(self, text: str): """ Gets the token count of a text string using the openai tokenizer. """ return len(self.tokenize_text(text)) if text else 0 @staticmethod - def image_token_count(image_width, image_height, image_detail="high"): + def image_token_count(image_width, image_height, image_detail: str = "high"): """ Calculate the number of tokens in an image. Low detail is 85 tokens, high detail is 170 tokens per 512x512 square. """ - logger = setup_logger( - "dimos.agents.tokenizer.HuggingFaceTokenizer.image_token_count") + logger = setup_logger("dimos.agents.tokenizer.HuggingFaceTokenizer.image_token_count") if image_detail == "low": return 85 elif image_detail == "high": # Image dimensions - logger.debug( - f"Image Width: {image_width}, Image Height: {image_height}") + logger.debug(f"Image Width: {image_width}, Image Height: {image_height}") if image_width is None or image_height is None: raise ValueError( "Image width and height must be provided for high detail image token count calculation." @@ -87,5 +86,4 @@ def image_token_count(image_width, image_height, image_detail="high"): num_squares = (image_width // 512) * (image_height // 512) return 170 * num_squares + 85 else: - raise ValueError( - "Detail specification of image is not 'low' or 'high'") \ No newline at end of file + raise ValueError("Detail specification of image is not 'low' or 'high'") diff --git a/dimos/agents/tokenizer/openai_tokenizer.py b/dimos/agents/tokenizer/openai_tokenizer.py index b410e13aa6..7fe5017241 100644 --- a/dimos/agents/tokenizer/openai_tokenizer.py +++ b/dimos/agents/tokenizer/openai_tokenizer.py @@ -13,12 +13,13 @@ # limitations under the License. import tiktoken + from dimos.agents.tokenizer.base import AbstractTokenizer from dimos.utils.logging_config import setup_logger -class OpenAITokenizer(AbstractTokenizer): - def __init__(self, model_name: str = "gpt-4o", **kwargs): +class OpenAITokenizer(AbstractTokenizer): + def __init__(self, model_name: str = "gpt-4o", **kwargs) -> None: super().__init__(**kwargs) # Initilize the tokenizer for the openai set of models @@ -27,10 +28,10 @@ def __init__(self, model_name: str = "gpt-4o", **kwargs): self.tokenizer = tiktoken.encoding_for_model(self.model_name) except Exception as e: raise ValueError( - f"Failed to initialize tokenizer for model {self.model_name}. Error: {str(e)}" + f"Failed to initialize tokenizer for model {self.model_name}. Error: {e!s}" ) - def tokenize_text(self, text): + def tokenize_text(self, text: str): """ Tokenize a text string using the openai tokenizer. """ @@ -43,28 +44,26 @@ def detokenize_text(self, tokenized_text): try: return self.tokenizer.decode(tokenized_text, errors="ignore") except Exception as e: - raise ValueError(f"Failed to detokenize text. Error: {str(e)}") + raise ValueError(f"Failed to detokenize text. Error: {e!s}") - def token_count(self, text): + def token_count(self, text: str): """ Gets the token count of a text string using the openai tokenizer. """ return len(self.tokenize_text(text)) if text else 0 @staticmethod - def image_token_count(image_width, image_height, image_detail="high"): + def image_token_count(image_width, image_height, image_detail: str = "high"): """ Calculate the number of tokens in an image. Low detail is 85 tokens, high detail is 170 tokens per 512x512 square. """ - logger = setup_logger( - "dimos.agents.tokenizer.openai.image_token_count") + logger = setup_logger("dimos.agents.tokenizer.openai.image_token_count") if image_detail == "low": return 85 elif image_detail == "high": # Image dimensions - logger.debug( - f"Image Width: {image_width}, Image Height: {image_height}") + logger.debug(f"Image Width: {image_width}, Image Height: {image_height}") if image_width is None or image_height is None: raise ValueError( "Image width and height must be provided for high detail image token count calculation." @@ -87,7 +86,4 @@ def image_token_count(image_width, image_height, image_detail="high"): num_squares = (image_width // 512) * (image_height // 512) return 170 * num_squares + 85 else: - raise ValueError( - "Detail specification of image is not 'low' or 'high'") - - + raise ValueError("Detail specification of image is not 'low' or 'high'") diff --git a/dimos/agents2/__init__.py b/dimos/agents2/__init__.py new file mode 100644 index 0000000000..28a48430b6 --- /dev/null +++ b/dimos/agents2/__init__.py @@ -0,0 +1,13 @@ +from langchain_core.messages import ( + AIMessage, + HumanMessage, + MessageLikeRepresentation, + SystemMessage, + ToolCall, + ToolMessage, +) + +from dimos.agents2.agent import Agent +from dimos.agents2.spec import AgentSpec +from dimos.protocol.skill.skill import skill +from dimos.protocol.skill.type import Output, Reducer, Stream diff --git a/dimos/agents2/agent.py b/dimos/agents2/agent.py new file mode 100644 index 0000000000..0fcd05d3e5 --- /dev/null +++ b/dimos/agents2/agent.py @@ -0,0 +1,374 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import asyncio +import datetime +import json +from operator import itemgetter +import os +from typing import Any, TypedDict +import uuid + +from langchain.chat_models import init_chat_model +from langchain_core.messages import ( + AIMessage, + HumanMessage, + SystemMessage, + ToolCall, + ToolMessage, +) + +from dimos.agents2.spec import AgentSpec +from dimos.agents2.system_prompt import get_system_prompt +from dimos.core import rpc +from dimos.protocol.skill.coordinator import SkillCoordinator, SkillState, SkillStateDict +from dimos.protocol.skill.type import Output +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.protocol.agents2") + + +SYSTEM_MSG_APPEND = "\nYour message history will always be appended with a System Overview message that provides situational awareness." + + +def toolmsg_from_state(state: SkillState) -> ToolMessage: + if state.skill_config.output != Output.standard: + content = "output attached in separate messages" + else: + content = state.content() + + return ToolMessage( + # if agent call has been triggered by another skill, + # and this specific skill didn't finish yet but we need a tool call response + # we return a message explaining that execution is still ongoing + content=content + or "Running, you will be called with an update, no need for subsequent tool calls", + name=state.name, + tool_call_id=state.call_id, + ) + + +class SkillStateSummary(TypedDict): + name: str + call_id: str + state: str + data: Any + + +def summary_from_state(state: SkillState, special_data: bool = False) -> SkillStateSummary: + content = state.content() + if isinstance(content, dict): + content = json.dumps(content) + + if not isinstance(content, str): + content = str(content) + + return { + "name": state.name, + "call_id": state.call_id, + "state": state.state.name, + "data": state.content() if not special_data else "data will be in a separate message", + } + + +def _custom_json_serializers(obj): + if isinstance(obj, datetime.date | datetime.datetime): + return obj.isoformat() + raise TypeError(f"Type {type(obj)} not serializable") + + +# takes an overview of running skills from the coorindator +# and builds messages to be sent to an agent +def snapshot_to_messages( + state: SkillStateDict, + tool_calls: list[ToolCall], +) -> tuple[list[ToolMessage], AIMessage | None]: + # builds a set of tool call ids from a previous agent request + tool_call_ids = set( + map(itemgetter("id"), tool_calls), + ) + + # build a tool msg responses + tool_msgs: list[ToolMessage] = [] + + # build a general skill state overview (for longer running skills) + state_overview: list[dict[str, SkillStateSummary]] = [] + + # for special skills that want to return a separate message + # (images for example, requires to be a HumanMessage) + special_msgs: list[HumanMessage] = [] + + # for special skills that want to return a separate message that should + # stay in history, like actual human messages, critical events + history_msgs: list[HumanMessage] = [] + + # Initialize state_msg + state_msg = None + + for skill_state in sorted( + state.values(), + key=lambda skill_state: skill_state.duration(), + ): + if skill_state.call_id in tool_call_ids: + tool_msgs.append(toolmsg_from_state(skill_state)) + + if skill_state.skill_config.output == Output.human: + content = skill_state.content() + if not content: + continue + history_msgs.append(HumanMessage(content=content)) + continue + + special_data = skill_state.skill_config.output == Output.image + if special_data: + content = skill_state.content() + if not content: + continue + special_msgs.append(HumanMessage(content=content)) + + if skill_state.call_id in tool_call_ids: + continue + + state_overview.append(summary_from_state(skill_state, special_data)) + + if state_overview: + state_overview_str = "\n".join( + json.dumps(s, default=_custom_json_serializers) for s in state_overview + ) + state_msg = AIMessage("State Overview:\n" + state_overview_str) + + return { + "tool_msgs": tool_msgs, + "history_msgs": history_msgs, + "state_msgs": ([state_msg] if state_msg else []) + special_msgs, + } + + +# Agent class job is to glue skill coordinator state to an agent, builds langchain messages +class Agent(AgentSpec): + system_message: SystemMessage + state_messages: list[AIMessage | HumanMessage] + + def __init__( + self, + *args, + **kwargs, + ) -> None: + AgentSpec.__init__(self, *args, **kwargs) + + self.state_messages = [] + self.coordinator = SkillCoordinator() + self._history = [] + self._agent_id = str(uuid.uuid4()) + self._agent_stopped = False + + if self.config.system_prompt: + if isinstance(self.config.system_prompt, str): + self.system_message = SystemMessage(self.config.system_prompt + SYSTEM_MSG_APPEND) + else: + self.config.system_prompt.content += SYSTEM_MSG_APPEND + self.system_message = self.config.system_prompt + else: + self.system_message = SystemMessage(get_system_prompt() + SYSTEM_MSG_APPEND) + + self.publish(self.system_message) + + # Use provided model instance if available, otherwise initialize from config + if self.config.model_instance: + self._llm = self.config.model_instance + else: + self._llm = init_chat_model( + model_provider=self.config.provider, model=self.config.model + ) + + @rpc + def get_agent_id(self) -> str: + return self._agent_id + + @rpc + def start(self) -> None: + super().start() + self.coordinator.start() + + @rpc + def stop(self) -> None: + self.coordinator.stop() + self._agent_stopped = True + super().stop() + + def clear_history(self) -> None: + self._history.clear() + + def append_history(self, *msgs: list[AIMessage | HumanMessage]) -> None: + for msg in msgs: + self.publish(msg) + + self._history.extend(msgs) + + def history(self): + return [self.system_message, *self._history, *self.state_messages] + + # Used by agent to execute tool calls + def execute_tool_calls(self, tool_calls: list[ToolCall]) -> None: + """Execute a list of tool calls from the agent.""" + if self._agent_stopped: + logger.warning("Agent is stopped, cannot execute tool calls.") + return + for tool_call in tool_calls: + logger.info(f"executing skill call {tool_call}") + self.coordinator.call_skill( + tool_call.get("id"), + tool_call.get("name"), + tool_call.get("args"), + ) + + # used to inject skill calls into the agent loop without agent asking for it + def run_implicit_skill(self, skill_name: str, **kwargs) -> None: + if self._agent_stopped: + logger.warning("Agent is stopped, cannot execute implicit skill calls.") + return + self.coordinator.call_skill(False, skill_name, {"args": kwargs}) + + async def agent_loop(self, first_query: str = ""): + # TODO: Should I add a lock here to prevent concurrent calls to agent_loop? + + if self._agent_stopped: + logger.warning("Agent is stopped, cannot run agent loop.") + # return "Agent is stopped." + import traceback + + traceback.print_stack() + return "Agent is stopped." + + self.state_messages = [] + if first_query: + self.append_history(HumanMessage(first_query)) + + def _get_state() -> str: + # TODO: FIX THIS EXTREME HACK + update = self.coordinator.generate_snapshot(clear=False) + snapshot_msgs = snapshot_to_messages(update, msg.tool_calls) + return json.dumps(snapshot_msgs, sort_keys=True, default=lambda o: repr(o)) + + try: + while True: + # we are getting tools from the coordinator on each turn + # since this allows for skillcontainers to dynamically provide new skills + tools = self.get_tools() + print("Available tools:", [tool.name for tool in tools]) + self._llm = self._llm.bind_tools(tools) + + # publish to /agent topic for observability + for state_msg in self.state_messages: + self.publish(state_msg) + + # history() builds our message history dynamically + # ensures we include latest system state, but not old ones. + msg = self._llm.invoke(self.history()) + self.append_history(msg) + + logger.info(f"Agent response: {msg.content}") + + state = _get_state() + + if msg.tool_calls: + self.execute_tool_calls(msg.tool_calls) + + print(self) + print(self.coordinator) + + self._write_debug_history_file() + + if not self.coordinator.has_active_skills(): + logger.info("No active tasks, exiting agent loop.") + return msg.content + + # coordinator will continue once a skill state has changed in + # such a way that agent call needs to be executed + + if state == _get_state(): + await self.coordinator.wait_for_updates() + + # we request a full snapshot of currently running, finished or errored out skills + # we ask for removal of finished skills from subsequent snapshots (clear=True) + update = self.coordinator.generate_snapshot(clear=True) + + # generate tool_msgs and general state update message, + # depending on a skill having associated tool call from previous interaction + # we will return a tool message, and not a general state message + snapshot_msgs = snapshot_to_messages(update, msg.tool_calls) + + self.state_messages = snapshot_msgs.get("state_msgs", []) + self.append_history( + *snapshot_msgs.get("tool_msgs", []), *snapshot_msgs.get("history_msgs", []) + ) + + except Exception as e: + logger.error(f"Error in agent loop: {e}") + import traceback + + traceback.print_exc() + + @rpc + def loop_thread(self) -> bool: + asyncio.run_coroutine_threadsafe(self.agent_loop(), self._loop) + return True + + @rpc + def query(self, query: str): + # TODO: could this be + # from distributed.utils import sync + # return sync(self._loop, self.agent_loop, query) + return asyncio.run_coroutine_threadsafe(self.agent_loop(query), self._loop).result() + + async def query_async(self, query: str): + return await self.agent_loop(query) + + @rpc + def register_skills(self, container, run_implicit_name: str | None = None): + ret = self.coordinator.register_skills(container) + + if run_implicit_name: + self.run_implicit_skill(run_implicit_name) + + return ret + + def get_tools(self): + return self.coordinator.get_tools() + + def _write_debug_history_file(self) -> None: + file_path = os.getenv("DEBUG_AGENT_HISTORY_FILE") + if not file_path: + return + + history = [x.__dict__ for x in self.history()] + + with open(file_path, "w") as f: + json.dump(history, f, default=lambda x: repr(x), indent=2) + + +class LlmAgent(Agent): + @rpc + def start(self) -> None: + super().start() + self.loop_thread() + + @rpc + def stop(self) -> None: + super().stop() + + +llm_agent = LlmAgent.blueprint + + +__all__ = ["Agent", "llm_agent"] diff --git a/dimos/agents2/cli/human.py b/dimos/agents2/cli/human.py new file mode 100644 index 0000000000..15727d87b8 --- /dev/null +++ b/dimos/agents2/cli/human.py @@ -0,0 +1,57 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import queue + +from reactivex.disposable import Disposable + +from dimos.agents2 import Output, Reducer, Stream, skill +from dimos.core import pLCMTransport, rpc +from dimos.core.module import Module +from dimos.core.rpc_client import RpcCall + + +class HumanInput(Module): + running: bool = False + + @skill(stream=Stream.call_agent, reducer=Reducer.string, output=Output.human, hide_skill=True) + def human(self): + """receives human input, no need to run this, it's running implicitly""" + if self.running: + return "already running" + self.running = True + transport = pLCMTransport("/human_input") + + msg_queue = queue.Queue() + unsub = transport.subscribe(msg_queue.put) + self._disposables.add(Disposable(unsub)) + yield from iter(msg_queue.get, None) + + @rpc + def start(self) -> None: + super().start() + + @rpc + def stop(self) -> None: + super().stop() + + @rpc + def set_LlmAgent_register_skills(self, callable: RpcCall) -> None: + callable.set_rpc(self.rpc) + callable(self, run_implicit_name="human") + + +human_input = HumanInput.blueprint + +__all__ = ["HumanInput", "human_input"] diff --git a/dimos/agents2/conftest.py b/dimos/agents2/conftest.py new file mode 100644 index 0000000000..769523f8c5 --- /dev/null +++ b/dimos/agents2/conftest.py @@ -0,0 +1,85 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + +import pytest + +from dimos.agents2.agent import Agent +from dimos.agents2.testing import MockModel +from dimos.protocol.skill.test_coordinator import SkillContainerTest + + +@pytest.fixture +def fixture_dir(): + return Path(__file__).parent / "fixtures" + + +@pytest.fixture +def potato_system_prompt() -> str: + return "Your name is Mr. Potato, potatoes are bad at math. Use a tools if asked to calculate" + + +@pytest.fixture +def skill_container(): + container = SkillContainerTest() + try: + yield container + finally: + container.stop() + + +@pytest.fixture +def create_fake_agent(fixture_dir): + agent = None + + def _agent_factory(*, system_prompt, skill_containers, fixture): + mock_model = MockModel(json_path=fixture_dir / fixture) + + nonlocal agent + agent = Agent(system_prompt=system_prompt, model_instance=mock_model) + + for skill_container in skill_containers: + agent.register_skills(skill_container) + + agent.start() + + return agent + + try: + yield _agent_factory + finally: + if agent: + agent.stop() + + +@pytest.fixture +def create_potato_agent(potato_system_prompt, skill_container, fixture_dir): + agent = None + + def _agent_factory(*, fixture): + mock_model = MockModel(json_path=fixture_dir / fixture) + + nonlocal agent + agent = Agent(system_prompt=potato_system_prompt, model_instance=mock_model) + agent.register_skills(skill_container) + agent.start() + + return agent + + try: + yield _agent_factory + finally: + if agent: + agent.stop() diff --git a/dimos/agents2/constants.py b/dimos/agents2/constants.py new file mode 100644 index 0000000000..0d7d4832a0 --- /dev/null +++ b/dimos/agents2/constants.py @@ -0,0 +1,17 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.constants import DIMOS_PROJECT_ROOT + +AGENT_SYSTEM_PROMPT_PATH = DIMOS_PROJECT_ROOT / "assets/agent/prompt_agents2.txt" diff --git a/dimos/agents2/fixtures/test_get_gps_position_for_queries.json b/dimos/agents2/fixtures/test_get_gps_position_for_queries.json new file mode 100644 index 0000000000..5d95b91bac --- /dev/null +++ b/dimos/agents2/fixtures/test_get_gps_position_for_queries.json @@ -0,0 +1,25 @@ +{ + "responses": [ + { + "content": "", + "tool_calls": [ + { + "name": "get_gps_position_for_queries", + "args": { + "args": [ + "Hyde Park", + "Regent Park", + "Russell Park" + ] + }, + "id": "call_xO0VDst53tzetEUq8mapKGS1", + "type": "tool_call" + } + ] + }, + { + "content": "Here are the latitude and longitude coordinates for the parks:\n\n- Hyde Park: Latitude 37.782601, Longitude -122.413201\n- Regent Park: Latitude 37.782602, Longitude -122.413202\n- Russell Park: Latitude 37.782603, Longitude -122.413203", + "tool_calls": [] + } + ] +} diff --git a/dimos/agents2/fixtures/test_go_to_object.json b/dimos/agents2/fixtures/test_go_to_object.json new file mode 100644 index 0000000000..80f1e95379 --- /dev/null +++ b/dimos/agents2/fixtures/test_go_to_object.json @@ -0,0 +1,27 @@ +{ + "responses": [ + { + "content": "I will navigate to the nearest chair.", + "tool_calls": [ + { + "name": "navigate_with_text", + "args": { + "args": [ + "chair" + ] + }, + "id": "call_LP4eewByfO9XaxMtnnWxDUz7", + "type": "tool_call" + } + ] + }, + { + "content": "I'm on my way to the chair. Let me know if there's anything else you'd like me to do!", + "tool_calls": [] + }, + { + "content": "I have successfully navigated to the chair. Let me know if you need anything else!", + "tool_calls": [] + } + ] +} diff --git a/dimos/agents2/fixtures/test_go_to_semantic_location.json b/dimos/agents2/fixtures/test_go_to_semantic_location.json new file mode 100644 index 0000000000..1a10711543 --- /dev/null +++ b/dimos/agents2/fixtures/test_go_to_semantic_location.json @@ -0,0 +1,23 @@ +{ + "responses": [ + { + "content": "", + "tool_calls": [ + { + "name": "navigate_with_text", + "args": { + "args": [ + "bookshelf" + ] + }, + "id": "call_yPoqcavMo05ogNNy5LMNQl2a", + "type": "tool_call" + } + ] + }, + { + "content": "I have successfully arrived at the bookshelf. Is there anything specific you need here?", + "tool_calls": [] + } + ] +} diff --git a/dimos/agents2/fixtures/test_how_much_is_124181112_plus_124124.json b/dimos/agents2/fixtures/test_how_much_is_124181112_plus_124124.json new file mode 100644 index 0000000000..f4dbe0c3a5 --- /dev/null +++ b/dimos/agents2/fixtures/test_how_much_is_124181112_plus_124124.json @@ -0,0 +1,52 @@ +{ + "responses": [ + { + "content": "", + "tool_calls": [ + { + "name": "add", + "args": { + "args": [ + 124181112, + 124124 + ] + }, + "id": "call_SSoVXz5yihrzR8TWIGnGKSpi", + "type": "tool_call" + } + ] + }, + { + "content": "Let me do some potato math... Calculating this will take some time, hold on! \ud83e\udd54", + "tool_calls": [] + }, + { + "content": "The result of adding 124,181,112 and 124,124 is 124,305,236. Potatoes work well with tools! \ud83e\udd54\ud83c\udf89", + "tool_calls": [] + }, + { + "content": "", + "tool_calls": [ + { + "name": "add", + "args": { + "args": [ + 1000000000, + -1000000 + ] + }, + "id": "call_ge9pv6IRa3yo0vjVaORvrGby", + "type": "tool_call" + } + ] + }, + { + "content": "Let's get those numbers crunched. Potatoes need a bit of time! \ud83e\udd54\ud83d\udcca", + "tool_calls": [] + }, + { + "content": "The result of one billion plus negative one million is 999,000,000. Potatoes are amazing with some help! \ud83e\udd54\ud83d\udca1", + "tool_calls": [] + } + ] +} diff --git a/dimos/agents2/fixtures/test_set_gps_travel_points.json b/dimos/agents2/fixtures/test_set_gps_travel_points.json new file mode 100644 index 0000000000..eb5b2a9195 --- /dev/null +++ b/dimos/agents2/fixtures/test_set_gps_travel_points.json @@ -0,0 +1,30 @@ +{ + "responses": [ + { + "content": "I understand you want me to navigate to the specified location. I will set the GPS travel point accordingly.", + "tool_calls": [ + { + "name": "set_gps_travel_points", + "args": { + "args": [ + { + "lat": 37.782654, + "lon": -122.413273 + } + ] + }, + "id": "call_q6JCCYFuyAjqUgUibJHqcIMD", + "type": "tool_call" + } + ] + }, + { + "content": "I'm on my way to the specified location. Let me know if there is anything else I can assist you with!", + "tool_calls": [] + }, + { + "content": "I've reached the specified location. Do you need any further assistance?", + "tool_calls": [] + } + ] +} diff --git a/dimos/agents2/fixtures/test_set_gps_travel_points_multiple.json b/dimos/agents2/fixtures/test_set_gps_travel_points_multiple.json new file mode 100644 index 0000000000..9d8f7e9e00 --- /dev/null +++ b/dimos/agents2/fixtures/test_set_gps_travel_points_multiple.json @@ -0,0 +1,34 @@ +{ + "responses": [ + { + "content": "", + "tool_calls": [ + { + "name": "set_gps_travel_points", + "args": { + "args": [ + { + "lat": 37.782654, + "lon": -122.413273 + }, + { + "lat": 37.78266, + "lon": -122.41326 + }, + { + "lat": 37.78267, + "lon": -122.41327 + } + ] + }, + "id": "call_Q09MRMEgRnJPBOGZpM0j8sL2", + "type": "tool_call" + } + ] + }, + { + "content": "I've successfully set the travel points and will navigate to them sequentially.", + "tool_calls": [] + } + ] +} diff --git a/dimos/agents2/fixtures/test_stop_movement.json b/dimos/agents2/fixtures/test_stop_movement.json new file mode 100644 index 0000000000..b80834213e --- /dev/null +++ b/dimos/agents2/fixtures/test_stop_movement.json @@ -0,0 +1,21 @@ +{ + "responses": [ + { + "content": "", + "tool_calls": [ + { + "name": "stop_movement", + "args": { + "args": null + }, + "id": "call_oAKe9W8s3xRGioZhBJJDOZB1", + "type": "tool_call" + } + ] + }, + { + "content": "I have stopped moving. Let me know if you need anything else!", + "tool_calls": [] + } + ] +} diff --git a/dimos/agents2/fixtures/test_take_a_look_around.json b/dimos/agents2/fixtures/test_take_a_look_around.json new file mode 100644 index 0000000000..c30fe71017 --- /dev/null +++ b/dimos/agents2/fixtures/test_take_a_look_around.json @@ -0,0 +1,23 @@ +{ + "responses": [ + { + "content": "", + "tool_calls": [ + { + "name": "start_exploration", + "args": { + "args": [ + 10 + ] + }, + "id": "call_AMNeD8zTkvyFHKG90DriDPuM", + "type": "tool_call" + } + ] + }, + { + "content": "I have completed a brief exploration of the surroundings. Let me know if there's anything specific you need!", + "tool_calls": [] + } + ] +} diff --git a/dimos/agents2/fixtures/test_what_do_you_see_in_this_picture.json b/dimos/agents2/fixtures/test_what_do_you_see_in_this_picture.json new file mode 100644 index 0000000000..27ac3453bc --- /dev/null +++ b/dimos/agents2/fixtures/test_what_do_you_see_in_this_picture.json @@ -0,0 +1,25 @@ +{ + "responses": [ + { + "content": "", + "tool_calls": [ + { + "name": "take_photo", + "args": { + "args": [] + }, + "id": "call_o6ikJtK3vObuEFD6hDtLoyGQ", + "type": "tool_call" + } + ] + }, + { + "content": "I took a photo, but as an AI, I can't see or interpret images. If there's anything specific you need to know, feel free to ask!", + "tool_calls": [] + }, + { + "content": "It looks like a cozy outdoor cafe where people are sitting and enjoying a meal. There are flowers and a nice, sunny ambiance. If you have any specific questions about the image, let me know!", + "tool_calls": [] + } + ] +} diff --git a/dimos/agents2/fixtures/test_what_is_your_name.json b/dimos/agents2/fixtures/test_what_is_your_name.json new file mode 100644 index 0000000000..a74d793b1d --- /dev/null +++ b/dimos/agents2/fixtures/test_what_is_your_name.json @@ -0,0 +1,8 @@ +{ + "responses": [ + { + "content": "Hi! My name is Mr. Potato. How can I assist you today?", + "tool_calls": [] + } + ] +} diff --git a/dimos/agents2/fixtures/test_where_am_i.json b/dimos/agents2/fixtures/test_where_am_i.json new file mode 100644 index 0000000000..2d274f8fa6 --- /dev/null +++ b/dimos/agents2/fixtures/test_where_am_i.json @@ -0,0 +1,21 @@ +{ + "responses": [ + { + "content": "", + "tool_calls": [ + { + "name": "where_am_i", + "args": { + "args": [] + }, + "id": "call_uRJLockZ5JWtGWbsSL1dpHm3", + "type": "tool_call" + } + ] + }, + { + "content": "You are on Bourbon Street.", + "tool_calls": [] + } + ] +} diff --git a/dimos/agents2/skills/conftest.py b/dimos/agents2/skills/conftest.py new file mode 100644 index 0000000000..a8734ca7ed --- /dev/null +++ b/dimos/agents2/skills/conftest.py @@ -0,0 +1,124 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from functools import partial + +import pytest +import reactivex as rx +from reactivex.scheduler import ThreadPoolScheduler + +from dimos.agents2.skills.google_maps_skill_container import GoogleMapsSkillContainer +from dimos.agents2.skills.gps_nav_skill import GpsNavSkillContainer +from dimos.agents2.skills.navigation import NavigationSkillContainer +from dimos.agents2.system_prompt import get_system_prompt +from dimos.mapping.types import LatLon +from dimos.msgs.sensor_msgs import Image +from dimos.robot.robot import GpsRobot +from dimos.utils.data import get_data + +system_prompt = get_system_prompt() + + +@pytest.fixture(autouse=True) +def cleanup_threadpool_scheduler(monkeypatch): + # TODO: get rid of this global threadpool + """Clean up and recreate the global ThreadPoolScheduler after each test.""" + # Disable ChromaDB telemetry to avoid leaking threads + monkeypatch.setenv("CHROMA_ANONYMIZED_TELEMETRY", "False") + yield + from dimos.utils import threadpool + + # Shutdown the global scheduler's executor + threadpool.scheduler.executor.shutdown(wait=True) + # Recreate it for the next test + threadpool.scheduler = ThreadPoolScheduler(max_workers=threadpool.get_max_workers()) + + +# TODO: Delete +@pytest.fixture +def fake_robot(mocker): + return mocker.MagicMock() + + +# TODO: Delete +@pytest.fixture +def fake_gps_robot(mocker): + return mocker.Mock(spec=GpsRobot) + + +@pytest.fixture +def fake_video_stream(): + image_path = get_data("chair-image.png") + image = Image.from_file(str(image_path)) + return rx.of(image) + + +# TODO: Delete +@pytest.fixture +def fake_gps_position_stream(): + return rx.of(LatLon(lat=37.783, lon=-122.413)) + + +@pytest.fixture +def navigation_skill_container(mocker): + container = NavigationSkillContainer() + container.color_image.connection = mocker.MagicMock() + container.odom.connection = mocker.MagicMock() + container.start() + yield container + container.stop() + + +@pytest.fixture +def gps_nav_skill_container(fake_gps_robot, fake_gps_position_stream): + container = GpsNavSkillContainer(fake_gps_robot, fake_gps_position_stream) + container.start() + yield container + container.stop() + + +@pytest.fixture +def google_maps_skill_container(fake_gps_robot, fake_gps_position_stream, mocker): + container = GoogleMapsSkillContainer(fake_gps_robot, fake_gps_position_stream) + container.start() + container._client = mocker.MagicMock() + yield container + container.stop() + + +@pytest.fixture +def create_navigation_agent(navigation_skill_container, create_fake_agent): + return partial( + create_fake_agent, + system_prompt=system_prompt, + skill_containers=[navigation_skill_container], + ) + + +@pytest.fixture +def create_gps_nav_agent(gps_nav_skill_container, create_fake_agent): + return partial( + create_fake_agent, system_prompt=system_prompt, skill_containers=[gps_nav_skill_container] + ) + + +@pytest.fixture +def create_google_maps_agent( + gps_nav_skill_container, google_maps_skill_container, create_fake_agent +): + return partial( + create_fake_agent, + system_prompt=system_prompt, + skill_containers=[gps_nav_skill_container, google_maps_skill_container], + ) diff --git a/dimos/agents2/skills/google_maps_skill_container.py b/dimos/agents2/skills/google_maps_skill_container.py new file mode 100644 index 0000000000..433914a5e3 --- /dev/null +++ b/dimos/agents2/skills/google_maps_skill_container.py @@ -0,0 +1,125 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +from typing import Any + +from reactivex import Observable +from reactivex.disposable import CompositeDisposable + +from dimos.core.resource import Resource +from dimos.mapping.google_maps.google_maps import GoogleMaps +from dimos.mapping.osm.current_location_map import CurrentLocationMap +from dimos.mapping.types import LatLon +from dimos.protocol.skill.skill import SkillContainer, skill +from dimos.robot.robot import Robot +from dimos.utils.logging_config import setup_logger + +logger = setup_logger(__file__) + + +class GoogleMapsSkillContainer(SkillContainer, Resource): + _robot: Robot + _disposables: CompositeDisposable + _latest_location: LatLon | None + _position_stream: Observable[LatLon] + _current_location_map: CurrentLocationMap + _started: bool + + def __init__(self, robot: Robot, position_stream: Observable[LatLon]) -> None: + super().__init__() + self._robot = robot + self._disposables = CompositeDisposable() + self._latest_location = None + self._position_stream = position_stream + self._client = GoogleMaps() + self._started = False + + def start(self) -> None: + self._started = True + self._disposables.add(self._position_stream.subscribe(self._on_gps_location)) + + def stop(self) -> None: + self._disposables.dispose() + super().stop() + + def _on_gps_location(self, location: LatLon) -> None: + self._latest_location = location + + def _get_latest_location(self) -> LatLon: + if not self._latest_location: + raise ValueError("The position has not been set yet.") + return self._latest_location + + @skill() + def where_am_i(self, context_radius: int = 200) -> str: + """This skill returns information about what street/locality/city/etc + you are in. It also gives you nearby landmarks. + + Example: + + where_am_i(context_radius=200) + + Args: + context_radius (int): default 200, how many meters to look around + """ + + if not self._started: + raise ValueError(f"{self} has not been started.") + + location = self._get_latest_location() + + result = None + try: + result = self._client.get_location_context(location, radius=context_radius) + except Exception: + return "There is an issue with the Google Maps API." + + if not result: + return "Could not find anything about the current location." + + return result.model_dump_json() + + @skill() + def get_gps_position_for_queries(self, *queries: str) -> str: + """Get the GPS position (latitude/longitude) + + Example: + + get_gps_position_for_queries(['Fort Mason', 'Lafayette Park']) + # returns + [{"lat": 37.8059, "lon":-122.4290}, {"lat": 37.7915, "lon": -122.4276}] + + Args: + queries (list[str]): The places you want to look up. + """ + + if not self._started: + raise ValueError(f"{self} has not been started.") + + location = self._get_latest_location() + + results: list[dict[str, Any] | str] = [] + + for query in queries: + try: + latlon = self._client.get_position(query, location) + except Exception: + latlon = None + if latlon: + results.append(latlon.model_dump()) + else: + results.append(f"no result for {query}") + + return json.dumps(results) diff --git a/dimos/agents2/skills/gps_nav_skill.py b/dimos/agents2/skills/gps_nav_skill.py new file mode 100644 index 0000000000..80e346790a --- /dev/null +++ b/dimos/agents2/skills/gps_nav_skill.py @@ -0,0 +1,107 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +from reactivex import Observable +from reactivex.disposable import CompositeDisposable + +from dimos.core.resource import Resource +from dimos.mapping.google_maps.google_maps import GoogleMaps +from dimos.mapping.osm.current_location_map import CurrentLocationMap +from dimos.mapping.types import LatLon +from dimos.mapping.utils.distance import distance_in_meters +from dimos.protocol.skill.skill import SkillContainer, skill +from dimos.robot.robot import Robot +from dimos.utils.logging_config import setup_logger + +logger = setup_logger(__file__) + + +class GpsNavSkillContainer(SkillContainer, Resource): + _robot: Robot + _disposables: CompositeDisposable + _latest_location: LatLon | None + _position_stream: Observable[LatLon] + _current_location_map: CurrentLocationMap + _started: bool + _max_valid_distance: int + + def __init__(self, robot: Robot, position_stream: Observable[LatLon]) -> None: + super().__init__() + self._robot = robot + self._disposables = CompositeDisposable() + self._latest_location = None + self._position_stream = position_stream + self._client = GoogleMaps() + self._started = False + self._max_valid_distance = 50000 + + def start(self) -> None: + self._started = True + self._disposables.add(self._position_stream.subscribe(self._on_gps_location)) + + def stop(self) -> None: + self._disposables.dispose() + super().stop() + + def _on_gps_location(self, location: LatLon) -> None: + self._latest_location = location + + def _get_latest_location(self) -> LatLon: + if not self._latest_location: + raise ValueError("The position has not been set yet.") + return self._latest_location + + @skill() + def set_gps_travel_points(self, *points: dict[str, float]) -> str: + """Define the movement path determined by GPS coordinates. Requires at least one. You can get the coordinates by using the `get_gps_position_for_queries` skill. + + Example: + + set_gps_travel_goals([{"lat": 37.8059, "lon":-122.4290}, {"lat": 37.7915, "lon": -122.4276}]) + # Travel first to {"lat": 37.8059, "lon":-122.4290} + # then travel to {"lat": 37.7915, "lon": -122.4276} + """ + + if not self._started: + raise ValueError(f"{self} has not been started.") + + new_points = [self._convert_point(x) for x in points] + + if not all(new_points): + parsed = json.dumps([x.__dict__ if x else x for x in new_points]) + return f"Not all points were valid. I parsed this: {parsed}" + + logger.info(f"Set travel points: {new_points}") + + self._robot.set_gps_travel_goal_points(new_points) + + return "I've successfully set the travel points." + + def _convert_point(self, point: dict[str, float]) -> LatLon | None: + if not isinstance(point, dict): + return None + lat = point.get("lat") + lon = point.get("lon") + + if lat is None or lon is None: + return None + + new_point = LatLon(lat=lat, lon=lon) + distance = distance_in_meters(self._get_latest_location(), new_point) + if distance > self._max_valid_distance: + return None + + return new_point diff --git a/dimos/agents2/skills/navigation.py b/dimos/agents2/skills/navigation.py new file mode 100644 index 0000000000..9e30871039 --- /dev/null +++ b/dimos/agents2/skills/navigation.py @@ -0,0 +1,441 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +from typing import Any + +from dimos.core.core import rpc +from dimos.core.rpc_client import RpcCall +from dimos.core.skill_module import SkillModule +from dimos.core.stream import In +from dimos.models.qwen.video_query import BBox +from dimos.models.vl.qwen import QwenVlModel +from dimos.msgs.geometry_msgs import PoseStamped +from dimos.msgs.geometry_msgs.Vector3 import make_vector3 +from dimos.msgs.sensor_msgs import Image +from dimos.navigation.bt_navigator.navigator import NavigatorState +from dimos.navigation.visual.query import get_object_bbox_from_image +from dimos.protocol.skill.skill import skill +from dimos.types.robot_location import RobotLocation +from dimos.utils.logging_config import setup_logger +from dimos.utils.transform_utils import euler_to_quaternion, quaternion_to_euler + +logger = setup_logger(__file__) + + +class NavigationSkillContainer(SkillModule): + _latest_image: Image | None = None + _latest_odom: PoseStamped | None = None + _skill_started: bool = False + _similarity_threshold: float = 0.23 + + _tag_location: RpcCall | None = None + _query_tagged_location: RpcCall | None = None + _query_by_text: RpcCall | None = None + _set_goal: RpcCall | None = None + _get_state: RpcCall | None = None + _is_goal_reached: RpcCall | None = None + _cancel_goal: RpcCall | None = None + _track: RpcCall | None = None + _stop_track: RpcCall | None = None + _is_tracking: RpcCall | None = None + _stop_exploration: RpcCall | None = None + _explore: RpcCall | None = None + _is_exploration_active: RpcCall | None = None + + color_image: In[Image] = None + odom: In[PoseStamped] = None + + def __init__(self) -> None: + super().__init__() + self._skill_started = False + self._vl_model = QwenVlModel() + + @rpc + def start(self) -> None: + self._disposables.add(self.color_image.subscribe(self._on_color_image)) + self._disposables.add(self.odom.subscribe(self._on_odom)) + self._skill_started = True + + @rpc + def stop(self) -> None: + super().stop() + + def _on_color_image(self, image: Image) -> None: + self._latest_image = image + + def _on_odom(self, odom: PoseStamped) -> None: + self._latest_odom = odom + + # TODO: This is quite repetitive, maybe I should automate this somehow + @rpc + def set_SpatialMemory_tag_location(self, callable: RpcCall) -> None: + self._tag_location = callable + self._tag_location.set_rpc(self.rpc) + + @rpc + def set_SpatialMemory_query_tagged_location(self, callable: RpcCall) -> None: + self._query_tagged_location = callable + self._query_tagged_location.set_rpc(self.rpc) + + @rpc + def set_SpatialMemory_query_by_text(self, callable: RpcCall) -> None: + self._query_by_text = callable + self._query_by_text.set_rpc(self.rpc) + + @rpc + def set_BehaviorTreeNavigator_set_goal(self, callable: RpcCall) -> None: + self._set_goal = callable + self._set_goal.set_rpc(self.rpc) + + @rpc + def set_BehaviorTreeNavigator_get_state(self, callable: RpcCall) -> None: + self._get_state = callable + self._get_state.set_rpc(self.rpc) + + @rpc + def set_BehaviorTreeNavigator_is_goal_reached(self, callable: RpcCall) -> None: + self._is_goal_reached = callable + self._is_goal_reached.set_rpc(self.rpc) + + @rpc + def set_BehaviorTreeNavigator_cancel_goal(self, callable: RpcCall) -> None: + self._cancel_goal = callable + self._cancel_goal.set_rpc(self.rpc) + + @rpc + def set_ObjectTracking_track(self, callable: RpcCall) -> None: + self._track = callable + self._track.set_rpc(self.rpc) + + @rpc + def set_ObjectTracking_stop_track(self, callable: RpcCall) -> None: + self._stop_track = callable + self._stop_track.set_rpc(self.rpc) + + @rpc + def set_ObjectTracking_is_tracking(self, callable: RpcCall) -> None: + self._is_tracking = callable + self._is_tracking.set_rpc(self.rpc) + + @rpc + def set_WavefrontFrontierExplorer_stop_exploration(self, callable: RpcCall) -> None: + self._stop_exploration = callable + self._stop_exploration.set_rpc(self.rpc) + + @rpc + def set_WavefrontFrontierExplorer_explore(self, callable: RpcCall) -> None: + self._explore = callable + self._explore.set_rpc(self.rpc) + + @rpc + def set_WavefrontFrontierExplorer_is_exploration_active(self, callable: RpcCall) -> None: + self._is_exploration_active = callable + self._is_exploration_active.set_rpc(self.rpc) + + @skill() + def tag_location_in_spatial_memory(self, location_name: str) -> str: + """Tag this location in the spatial memory with a name. + + This associates the current location with the given name in the spatial memory, allowing you to navigate back to it. + + Args: + location_name (str): the name for the location + + Returns: + str: the outcome + """ + + if not self._skill_started: + raise ValueError(f"{self} has not been started.") + + if not self._latest_odom: + return "Error: No odometry data available to tag the location." + + if not self._tag_location: + return "Error: The SpatialMemory module is not connected." + + position = self._latest_odom.position + rotation = quaternion_to_euler(self._latest_odom.orientation) + + location = RobotLocation( + name=location_name, + position=(position.x, position.y, position.z), + rotation=(rotation.x, rotation.y, rotation.z), + ) + + if not self._tag_location(location): + return f"Error: Failed to store '{location_name}' in the spatial memory" + + logger.info(f"Tagged {location}") + return f"The current location has been tagged as '{location_name}'." + + @skill() + def navigate_with_text(self, query: str) -> str: + """Navigate to a location by querying the existing semantic map using natural language. + + First attempts to locate an object in the robot's camera view using vision. + If the object is found, navigates to it. If not, falls back to querying the + semantic map for a location matching the description. + CALL THIS SKILL FOR ONE SUBJECT AT A TIME. For example: "Go to the person wearing a blue shirt in the living room", + you should call this skill twice, once for the person wearing a blue shirt and once for the living room. + Args: + query: Text query to search for in the semantic map + """ + + if not self._skill_started: + raise ValueError(f"{self} has not been started.") + + success_msg = self._navigate_by_tagged_location(query) + if success_msg: + return success_msg + + logger.info(f"No tagged location found for {query}") + + success_msg = self._navigate_to_object(query) + if success_msg: + return success_msg + + logger.info(f"No object in view found for {query}") + + success_msg = self._navigate_using_semantic_map(query) + if success_msg: + return success_msg + + return f"No tagged location called '{query}'. No object in view matching '{query}'. No matching location found in semantic map for '{query}'." + + def _navigate_by_tagged_location(self, query: str) -> str | None: + if not self._query_tagged_location: + logger.warning("SpatialMemory module not connected, cannot query tagged locations") + return None + + robot_location = self._query_tagged_location(query) + + if not robot_location: + return None + + goal_pose = PoseStamped( + position=make_vector3(*robot_location.position), + orientation=euler_to_quaternion(make_vector3(*robot_location.rotation)), + frame_id="world", + ) + + result = self._navigate_to(goal_pose) + if not result: + return "Error: Faild to reach the tagged location." + + return ( + f"Successfuly arrived at location tagged '{robot_location.name}' from query '{query}'." + ) + + def _navigate_to(self, pose: PoseStamped) -> bool: + if not self._set_goal or not self._get_state or not self._is_goal_reached: + logger.error("BehaviorTreeNavigator module not connected properly") + return False + + logger.info( + f"Navigating to pose: ({pose.position.x:.2f}, {pose.position.y:.2f}, {pose.position.z:.2f})" + ) + self._set_goal(pose) + time.sleep(1.0) + + while self._get_state() == NavigatorState.FOLLOWING_PATH: + time.sleep(0.25) + + time.sleep(1.0) + if not self._is_goal_reached(): + logger.info("Navigation was cancelled or failed") + return False + else: + logger.info("Navigation goal reached") + return True + + def _navigate_to_object(self, query: str) -> str | None: + try: + bbox = self._get_bbox_for_current_frame(query) + except Exception: + logger.error(f"Failed to get bbox for {query}", exc_info=True) + return None + + if bbox is None: + return None + + if not self._track or not self._stop_track or not self._is_tracking: + logger.error("ObjectTracking module not connected properly") + return None + + if not self._get_state or not self._is_goal_reached: + logger.error("BehaviorTreeNavigator module not connected properly") + return None + + logger.info(f"Found {query} at {bbox}") + + # Start tracking - BBoxNavigationModule automatically generates goals + self._track(bbox) + + start_time = time.time() + timeout = 30.0 + goal_set = False + + while time.time() - start_time < timeout: + # Check if navigator finished + if self._get_state() == NavigatorState.IDLE and goal_set: + logger.info("Waiting for goal result") + time.sleep(1.0) + if not self._is_goal_reached(): + logger.info(f"Goal cancelled, tracking '{query}' failed") + self._stop_track() + return None + else: + logger.info(f"Reached '{query}'") + self._stop_track() + return f"Successfully arrived at '{query}'" + + # If goal set and tracking lost, just continue (tracker will resume or timeout) + if goal_set and not self._is_tracking(): + continue + + # BBoxNavigationModule automatically sends goals when tracker publishes + # Just check if we have any detections to mark goal_set + if self._is_tracking(): + goal_set = True + + time.sleep(0.25) + + logger.warning(f"Navigation to '{query}' timed out after {timeout}s") + self._stop_track() + return None + + def _get_bbox_for_current_frame(self, query: str) -> BBox | None: + if self._latest_image is None: + return None + + return get_object_bbox_from_image(self._vl_model, self._latest_image, query) + + def _navigate_using_semantic_map(self, query: str) -> str: + if not self._query_by_text: + return "Error: The SpatialMemory module is not connected." + + results = self._query_by_text(query) + + if not results: + return f"No matching location found in semantic map for '{query}'" + + best_match = results[0] + + goal_pose = self._get_goal_pose_from_result(best_match) + + if not goal_pose: + return f"Found a result for '{query}' but it didn't have a valid position." + + result = self._navigate_to(goal_pose) + + if not result: + return f"Failed to navigate for '{query}'" + + return f"Successfuly arrived at '{query}'" + + @skill() + def follow_human(self, person: str) -> str: + """Follow a specific person""" + return "Not implemented yet." + + @skill() + def stop_movement(self) -> str: + """Immediatly stop moving.""" + + if not self._skill_started: + raise ValueError(f"{self} has not been started.") + + self._cancel_goal_and_stop() + + return "Stopped" + + def _cancel_goal_and_stop(self) -> None: + if not self._cancel_goal: + logger.warning("BehaviorTreeNavigator module not connected, cannot cancel goal") + return + + if not self._stop_exploration: + logger.warning("FrontierExplorer module not connected, cannot stop exploration") + return + + self._cancel_goal() + return self._stop_exploration() + + @skill() + def start_exploration(self, timeout: float = 240.0) -> str: + """A skill that performs autonomous frontier exploration. + + This skill continuously finds and navigates to unknown frontiers in the environment + until no more frontiers are found or the exploration is stopped. + + Don't call any other skills except stop_movement skill when needed. + + Args: + timeout (float, optional): Maximum time (in seconds) allowed for exploration + """ + + if not self._skill_started: + raise ValueError(f"{self} has not been started.") + + try: + return self._start_exploration(timeout) + finally: + self._cancel_goal_and_stop() + + def _start_exploration(self, timeout: float) -> str: + if not self._explore or not self._is_exploration_active: + return "Error: The WavefrontFrontierExplorer module is not connected." + + logger.info("Starting autonomous frontier exploration") + + start_time = time.time() + + has_started = self._explore() + if not has_started: + return "Error: Could not start exploration." + + while time.time() - start_time < timeout and self._is_exploration_active(): + time.sleep(0.5) + + return "Exploration completed successfuly" + + def _get_goal_pose_from_result(self, result: dict[str, Any]) -> PoseStamped | None: + similarity = 1.0 - (result.get("distance") or 1) + if similarity < self._similarity_threshold: + logger.warning( + f"Match found but similarity score ({similarity:.4f}) is below threshold ({self._similarity_threshold})" + ) + return None + + metadata = result.get("metadata") + if not metadata: + return None + + first = metadata[0] + pos_x = first.get("pos_x", 0) + pos_y = first.get("pos_y", 0) + theta = first.get("rot_z", 0) + + return PoseStamped( + position=make_vector3(pos_x, pos_y, 0), + orientation=euler_to_quaternion(make_vector3(0, 0, theta)), + frame_id="world", + ) + + +navigation_skill = NavigationSkillContainer.blueprint + +__all__ = ["NavigationSkillContainer", "navigation_skill"] diff --git a/dimos/agents2/skills/osm.py b/dimos/agents2/skills/osm.py new file mode 100644 index 0000000000..ae721bea81 --- /dev/null +++ b/dimos/agents2/skills/osm.py @@ -0,0 +1,85 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from dimos.core.skill_module import SkillModule +from dimos.core.stream import In +from dimos.mapping.osm.current_location_map import CurrentLocationMap +from dimos.mapping.types import LatLon +from dimos.mapping.utils.distance import distance_in_meters +from dimos.models.vl.qwen import QwenVlModel +from dimos.protocol.skill.skill import skill +from dimos.utils.logging_config import setup_logger + +logger = setup_logger(__file__) + + +class OsmSkill(SkillModule): + _latest_location: LatLon | None + _current_location_map: CurrentLocationMap + _skill_started: bool + + gps_location: In[LatLon] = None + + def __init__(self) -> None: + super().__init__() + self._latest_location = None + self._current_location_map = CurrentLocationMap(QwenVlModel()) + self._skill_started = False + + def start(self) -> None: + super().start() + self._skill_started = True + self._disposables.add(self.gps_location.subscribe(self._on_gps_location)) + + def stop(self) -> None: + super().stop() + + def _on_gps_location(self, location: LatLon) -> None: + self._latest_location = location + + @skill() + def street_map_query(self, query_sentence: str) -> str: + """This skill uses a vision language model to find something on the map + based on the query sentence. You can query it with something like "Where + can I find a coffee shop?" and it returns the latitude and longitude. + + Example: + + street_map_query("Where can I find a coffee shop?") + + Args: + query_sentence (str): The query sentence. + """ + + if not self._skill_started: + raise ValueError(f"{self} has not been started.") + + self._current_location_map.update_position(self._latest_location) + location = self._current_location_map.query_for_one_position_and_context( + query_sentence, self._latest_location + ) + if not location: + return "Could not find anything." + + latlon, context = location + + distance = int(distance_in_meters(latlon, self._latest_location)) + + return f"{context}. It's at position latitude={latlon.lat}, longitude={latlon.lon}. It is {distance} meters away." + + +osm_skill = OsmSkill.blueprint + +__all__ = ["OsmSkill", "osm_skill"] diff --git a/dimos/agents2/skills/ros_navigation.py b/dimos/agents2/skills/ros_navigation.py new file mode 100644 index 0000000000..973cdcc10f --- /dev/null +++ b/dimos/agents2/skills/ros_navigation.py @@ -0,0 +1,121 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +from typing import TYPE_CHECKING, Any + +from dimos.core.resource import Resource +from dimos.msgs.geometry_msgs import PoseStamped +from dimos.msgs.geometry_msgs.Vector3 import make_vector3 +from dimos.protocol.skill.skill import SkillContainer, skill +from dimos.utils.logging_config import setup_logger +from dimos.utils.transform_utils import euler_to_quaternion + +if TYPE_CHECKING: + from dimos.robot.unitree_webrtc.unitree_g1 import UnitreeG1 + +logger = setup_logger(__file__) + + +class RosNavigation(SkillContainer, Resource): + _robot: "UnitreeG1" + _started: bool + + def __init__(self, robot: "UnitreeG1") -> None: + self._robot = robot + self._similarity_threshold = 0.23 + self._started = False + + def start(self) -> None: + self._started = True + + def stop(self) -> None: + super().stop() + + @skill() + def navigate_with_text(self, query: str) -> str: + """Navigate to a location by querying the existing semantic map using natural language. + + CALL THIS SKILL FOR ONE SUBJECT AT A TIME. For example: "Go to the person wearing a blue shirt in the living room", + you should call this skill twice, once for the person wearing a blue shirt and once for the living room. + + Args: + query: Text query to search for in the semantic map + """ + + print("X" * 10000) + + if not self._started: + raise ValueError(f"{self} has not been started.") + + success_msg = self._navigate_using_semantic_map(query) + if success_msg: + return success_msg + + return "Failed to navigate." + + def _navigate_using_semantic_map(self, query: str) -> str: + results = self._robot.spatial_memory.query_by_text(query) + + if not results: + return f"No matching location found in semantic map for '{query}'" + + best_match = results[0] + + goal_pose = self._get_goal_pose_from_result(best_match) + + if not goal_pose: + return f"Found a result for '{query}' but it didn't have a valid position." + + result = self._robot.nav.go_to(goal_pose) + + if not result: + return f"Failed to navigate for '{query}'" + + return f"Successfuly arrived at '{query}'" + + @skill() + def stop_movement(self) -> str: + """Immediatly stop moving.""" + + if not self._started: + raise ValueError(f"{self} has not been started.") + + self._robot.cancel_navigation() + + return "Stopped" + + def _get_goal_pose_from_result(self, result: dict[str, Any]) -> PoseStamped | None: + similarity = 1.0 - (result.get("distance") or 1) + if similarity < self._similarity_threshold: + logger.warning( + f"Match found but similarity score ({similarity:.4f}) is below threshold ({self._similarity_threshold})" + ) + return None + + metadata = result.get("metadata") + if not metadata: + return None + + first = metadata[0] + pos_x = first.get("pos_x", 0) + pos_y = first.get("pos_y", 0) + theta = first.get("rot_z", 0) + + return PoseStamped( + ts=time.time(), + position=make_vector3(pos_x, pos_y, 0), + orientation=euler_to_quaternion(make_vector3(0, 0, theta)), + frame_id="map", + ) diff --git a/dimos/agents2/skills/test_google_maps_skill_container.py b/dimos/agents2/skills/test_google_maps_skill_container.py new file mode 100644 index 0000000000..27a9dadb8f --- /dev/null +++ b/dimos/agents2/skills/test_google_maps_skill_container.py @@ -0,0 +1,44 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re + +from dimos.mapping.google_maps.types import Coordinates, LocationContext, Position + + +def test_where_am_i(create_google_maps_agent, google_maps_skill_container) -> None: + google_maps_skill_container._client.get_location_context.return_value = LocationContext( + street="Bourbon Street", coordinates=Coordinates(lat=37.782654, lon=-122.413273) + ) + agent = create_google_maps_agent(fixture="test_where_am_i.json") + + response = agent.query("what street am I on") + + assert "bourbon" in response.lower() + + +def test_get_gps_position_for_queries( + create_google_maps_agent, google_maps_skill_container +) -> None: + google_maps_skill_container._client.get_position.side_effect = [ + Position(lat=37.782601, lon=-122.413201, description="address 1"), + Position(lat=37.782602, lon=-122.413202, description="address 2"), + Position(lat=37.782603, lon=-122.413203, description="address 3"), + ] + agent = create_google_maps_agent(fixture="test_get_gps_position_for_queries.json") + + response = agent.query("what are the lat/lon for hyde park, regent park, russell park?") + + regex = r".*37\.782601.*122\.413201.*37\.782602.*122\.413202.*37\.782603.*122\.413203.*" + assert re.match(regex, response, re.DOTALL) diff --git a/dimos/agents2/skills/test_gps_nav_skills.py b/dimos/agents2/skills/test_gps_nav_skills.py new file mode 100644 index 0000000000..9e8090b169 --- /dev/null +++ b/dimos/agents2/skills/test_gps_nav_skills.py @@ -0,0 +1,42 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from dimos.mapping.types import LatLon + + +def test_set_gps_travel_points(fake_gps_robot, create_gps_nav_agent) -> None: + agent = create_gps_nav_agent(fixture="test_set_gps_travel_points.json") + + agent.query("go to lat: 37.782654, lon: -122.413273") + + fake_gps_robot.set_gps_travel_goal_points.assert_called_once_with( + [LatLon(lat=37.782654, lon=-122.413273)] + ) + + +def test_set_gps_travel_points_multiple(fake_gps_robot, create_gps_nav_agent) -> None: + agent = create_gps_nav_agent(fixture="test_set_gps_travel_points_multiple.json") + + agent.query( + "go to lat: 37.782654, lon: -122.413273, then 37.782660,-122.413260, and then 37.782670,-122.413270" + ) + + fake_gps_robot.set_gps_travel_goal_points.assert_called_once_with( + [ + LatLon(lat=37.782654, lon=-122.413273), + LatLon(lat=37.782660, lon=-122.413260), + LatLon(lat=37.782670, lon=-122.413270), + ] + ) diff --git a/dimos/agents2/skills/test_navigation.py b/dimos/agents2/skills/test_navigation.py new file mode 100644 index 0000000000..d7d8d4c127 --- /dev/null +++ b/dimos/agents2/skills/test_navigation.py @@ -0,0 +1,82 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from dimos.msgs.geometry_msgs import PoseStamped, Vector3 +from dimos.utils.transform_utils import euler_to_quaternion + + +def test_stop_movement(create_navigation_agent, navigation_skill_container, mocker) -> None: + navigation_skill_container._cancel_goal = mocker.Mock() + navigation_skill_container._stop_exploration = mocker.Mock() + agent = create_navigation_agent(fixture="test_stop_movement.json") + + agent.query("stop") + + navigation_skill_container._cancel_goal.assert_called_once_with() + navigation_skill_container._stop_exploration.assert_called_once_with() + + +def test_take_a_look_around(create_navigation_agent, navigation_skill_container, mocker) -> None: + navigation_skill_container._explore = mocker.Mock() + navigation_skill_container._is_exploration_active = mocker.Mock() + mocker.patch("dimos.agents2.skills.navigation.time.sleep") + agent = create_navigation_agent(fixture="test_take_a_look_around.json") + + agent.query("take a look around for 10 seconds") + + navigation_skill_container._explore.assert_called_once_with() + + +def test_go_to_semantic_location( + create_navigation_agent, navigation_skill_container, mocker +) -> None: + mocker.patch( + "dimos.agents2.skills.navigation.NavigationSkillContainer._navigate_by_tagged_location", + return_value=None, + ) + mocker.patch( + "dimos.agents2.skills.navigation.NavigationSkillContainer._navigate_to_object", + return_value=None, + ) + mocker.patch( + "dimos.agents2.skills.navigation.NavigationSkillContainer._navigate_to", + return_value=True, + ) + navigation_skill_container._query_by_text = mocker.Mock( + return_value=[ + { + "distance": 0.5, + "metadata": [ + { + "pos_x": 1, + "pos_y": 2, + "rot_z": 3, + } + ], + } + ] + ) + agent = create_navigation_agent(fixture="test_go_to_semantic_location.json") + + agent.query("go to the bookshelf") + + navigation_skill_container._query_by_text.assert_called_once_with("bookshelf") + navigation_skill_container._navigate_to.assert_called_once_with( + PoseStamped( + position=Vector3(1, 2, 0), + orientation=euler_to_quaternion(Vector3(0, 0, 3)), + frame_id="world", + ), + ) diff --git a/dimos/agents2/spec.py b/dimos/agents2/spec.py new file mode 100644 index 0000000000..9973b05356 --- /dev/null +++ b/dimos/agents2/spec.py @@ -0,0 +1,229 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Base agent module that wraps BaseAgent for DimOS module usage.""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from enum import Enum +from typing import Any, Union + +from langchain.chat_models.base import _SUPPORTED_PROVIDERS +from langchain_core.language_models.chat_models import BaseChatModel +from langchain_core.messages import ( + AIMessage, + HumanMessage, + SystemMessage, + ToolMessage, +) +from rich.console import Console +from rich.table import Table +from rich.text import Text + +from dimos.core import Module, rpc +from dimos.core.module import ModuleConfig +from dimos.protocol.pubsub import PubSub, lcm +from dimos.protocol.service import Service +from dimos.protocol.skill.skill import SkillContainer +from dimos.utils.generic import truncate_display_string +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.agents.modules.base_agent") + + +# Dynamically create ModelProvider enum from LangChain's supported providers +_providers = {provider.upper(): provider for provider in _SUPPORTED_PROVIDERS} +Provider = Enum("Provider", _providers, type=str) + + +class Model(str, Enum): + """Common model names across providers. + + Note: This is not exhaustive as model names change frequently. + Based on langchain's _attempt_infer_model_provider patterns. + """ + + # OpenAI models (prefix: gpt-3, gpt-4, o1, o3) + GPT_4O = "gpt-4o" + GPT_4O_MINI = "gpt-4o-mini" + GPT_4_TURBO = "gpt-4-turbo" + GPT_4_TURBO_PREVIEW = "gpt-4-turbo-preview" + GPT_4 = "gpt-4" + GPT_35_TURBO = "gpt-3.5-turbo" + GPT_35_TURBO_16K = "gpt-3.5-turbo-16k" + O1_PREVIEW = "o1-preview" + O1_MINI = "o1-mini" + O3_MINI = "o3-mini" + + # Anthropic models (prefix: claude) + CLAUDE_3_OPUS = "claude-3-opus-20240229" + CLAUDE_3_SONNET = "claude-3-sonnet-20240229" + CLAUDE_3_HAIKU = "claude-3-haiku-20240307" + CLAUDE_35_SONNET = "claude-3-5-sonnet-20241022" + CLAUDE_35_SONNET_LATEST = "claude-3-5-sonnet-latest" + CLAUDE_3_7_SONNET = "claude-3-7-sonnet-20250219" + + # Google models (prefix: gemini) + GEMINI_20_FLASH = "gemini-2.0-flash" + GEMINI_15_PRO = "gemini-1.5-pro" + GEMINI_15_FLASH = "gemini-1.5-flash" + GEMINI_10_PRO = "gemini-1.0-pro" + + # Amazon Bedrock models (prefix: amazon) + AMAZON_TITAN_EXPRESS = "amazon.titan-text-express-v1" + AMAZON_TITAN_LITE = "amazon.titan-text-lite-v1" + + # Cohere models (prefix: command) + COMMAND_R_PLUS = "command-r-plus" + COMMAND_R = "command-r" + COMMAND = "command" + COMMAND_LIGHT = "command-light" + + # Fireworks models (prefix: accounts/fireworks) + FIREWORKS_LLAMA_V3_70B = "accounts/fireworks/models/llama-v3-70b-instruct" + FIREWORKS_MIXTRAL_8X7B = "accounts/fireworks/models/mixtral-8x7b-instruct" + + # Mistral models (prefix: mistral) + MISTRAL_LARGE = "mistral-large" + MISTRAL_MEDIUM = "mistral-medium" + MISTRAL_SMALL = "mistral-small" + MIXTRAL_8X7B = "mixtral-8x7b" + MIXTRAL_8X22B = "mixtral-8x22b" + MISTRAL_7B = "mistral-7b" + + # DeepSeek models (prefix: deepseek) + DEEPSEEK_CHAT = "deepseek-chat" + DEEPSEEK_CODER = "deepseek-coder" + DEEPSEEK_R1_DISTILL_LLAMA_70B = "deepseek-r1-distill-llama-70b" + + # xAI models (prefix: grok) + GROK_1 = "grok-1" + GROK_2 = "grok-2" + + # Perplexity models (prefix: sonar) + SONAR_SMALL_CHAT = "sonar-small-chat" + SONAR_MEDIUM_CHAT = "sonar-medium-chat" + SONAR_LARGE_CHAT = "sonar-large-chat" + + # Meta Llama models (various providers) + LLAMA_3_70B = "llama-3-70b" + LLAMA_3_8B = "llama-3-8b" + LLAMA_31_70B = "llama-3.1-70b" + LLAMA_31_8B = "llama-3.1-8b" + LLAMA_33_70B = "llama-3.3-70b" + LLAMA_2_70B = "llama-2-70b" + LLAMA_2_13B = "llama-2-13b" + LLAMA_2_7B = "llama-2-7b" + + +@dataclass +class AgentConfig(ModuleConfig): + system_prompt: str | SystemMessage | None = None + skills: SkillContainer | list[SkillContainer] | None = None + + # we can provide model/provvider enums or instantiated model_instance + model: Model = Model.GPT_4O + provider: Provider = Provider.OPENAI + model_instance: BaseChatModel | None = None + + agent_transport: type[PubSub] = lcm.PickleLCM + agent_topic: Any = field(default_factory=lambda: lcm.Topic("/agent")) + + +AnyMessage = Union[SystemMessage, ToolMessage, AIMessage, HumanMessage] + + +class AgentSpec(Service[AgentConfig], Module, ABC): + default_config: type[AgentConfig] = AgentConfig + + def __init__(self, *args, **kwargs) -> None: + Service.__init__(self, *args, **kwargs) + Module.__init__(self, *args, **kwargs) + + if self.config.agent_transport: + self.transport = self.config.agent_transport() + + def publish(self, msg: AnyMessage) -> None: + if self.transport: + self.transport.publish(self.config.agent_topic, msg) + + def start(self) -> None: + super().start() + + def stop(self) -> None: + super().stop() + + @rpc + @abstractmethod + def clear_history(self): ... + + @abstractmethod + def append_history(self, *msgs: list[AIMessage | HumanMessage]): ... + + @abstractmethod + def history(self) -> list[AnyMessage]: ... + + @rpc + @abstractmethod + def query(self, query: str): ... + + def __str__(self) -> str: + console = Console(force_terminal=True, legacy_windows=False) + table = Table(show_header=True) + + table.add_column("Message Type", style="cyan", no_wrap=True) + table.add_column("Content") + + for message in self.history(): + if isinstance(message, HumanMessage): + content = message.content + if not isinstance(content, str): + content = "" + + table.add_row(Text("Human", style="green"), Text(content, style="green")) + elif isinstance(message, AIMessage): + if hasattr(message, "metadata") and message.metadata.get("state"): + table.add_row( + Text("State Summary", style="blue"), + Text(message.content, style="blue"), + ) + else: + table.add_row( + Text("Agent", style="magenta"), Text(message.content, style="magenta") + ) + + for tool_call in message.tool_calls: + table.add_row( + "Tool Call", + Text( + f"{tool_call.get('name')}({tool_call.get('args')})", + style="bold magenta", + ), + ) + elif isinstance(message, ToolMessage): + table.add_row( + "Tool Response", Text(f"{message.name}() -> {message.content}"), style="red" + ) + elif isinstance(message, SystemMessage): + table.add_row( + "System", Text(truncate_display_string(message.content, 800), style="yellow") + ) + else: + table.add_row("Unknown", str(message)) + + # Render to string with title above + with console.capture() as capture: + console.print(Text(f" Agent ({self._agent_id})", style="bold blue")) + console.print(table) + return capture.get().strip() diff --git a/dimos/agents2/system_prompt.py b/dimos/agents2/system_prompt.py new file mode 100644 index 0000000000..6b14f3e193 --- /dev/null +++ b/dimos/agents2/system_prompt.py @@ -0,0 +1,25 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.agents2.constants import AGENT_SYSTEM_PROMPT_PATH + +_SYSTEM_PROMPT = None + + +def get_system_prompt() -> str: + global _SYSTEM_PROMPT + if _SYSTEM_PROMPT is None: + with open(AGENT_SYSTEM_PROMPT_PATH) as f: + _SYSTEM_PROMPT = f.read() + return _SYSTEM_PROMPT diff --git a/dimos/agents2/temp/run_unitree_agents2.py b/dimos/agents2/temp/run_unitree_agents2.py new file mode 100644 index 0000000000..aacfd1b5f4 --- /dev/null +++ b/dimos/agents2/temp/run_unitree_agents2.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Run script for Unitree Go2 robot with agents2 framework. +This is the migrated version using the new LangChain-based agent system. +""" + +import os +from pathlib import Path +import sys +import time + +from dotenv import load_dotenv + +from dimos.agents2.cli.human import HumanInput + +# Add parent directories to path +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) + + +from dimos.agents2 import Agent +from dimos.agents2.spec import Model, Provider +from dimos.robot.unitree_webrtc.unitree_go2 import UnitreeGo2 +from dimos.robot.unitree_webrtc.unitree_skill_container import UnitreeSkillContainer +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.agents2.run_unitree") + +# Load environment variables +load_dotenv() + +# System prompt path +SYSTEM_PROMPT_PATH = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), + "assets/agent/prompt.txt", +) + + +class UnitreeAgentRunner: + """Manages the Unitree robot with the new agents2 framework.""" + + def __init__(self) -> None: + self.robot = None + self.agent = None + self.agent_thread = None + self.running = False + + def setup_robot(self) -> UnitreeGo2: + """Initialize the robot connection.""" + logger.info("Initializing Unitree Go2 robot...") + + robot = UnitreeGo2( + ip=os.getenv("ROBOT_IP"), + connection_type=os.getenv("CONNECTION_TYPE", "webrtc"), + ) + + robot.start() + time.sleep(3) + + logger.info("Robot initialized successfully") + return robot + + def setup_agent(self, skillcontainers, system_prompt: str) -> Agent: + """Create and configure the agent with skills.""" + logger.info("Setting up agent with skills...") + + # Create agent + agent = Agent( + system_prompt=system_prompt, + model=Model.GPT_4O, # Could add CLAUDE models to enum + provider=Provider.OPENAI, # Would need ANTHROPIC provider + ) + + for container in skillcontainers: + print("REGISTERING SKILLS FROM CONTAINER:", container) + agent.register_skills(container) + + agent.run_implicit_skill("human") + + agent.start() + + # Log available skills + names = ", ".join([tool.name for tool in agent.get_tools()]) + logger.info(f"Agent configured with {len(names)} skills: {names}") + + agent.loop_thread() + return agent + + def run(self) -> None: + """Main run loop.""" + print("\n" + "=" * 60) + print("Unitree Go2 Robot with agents2 Framework") + print("=" * 60) + print("\nThis system integrates:") + print(" - Unitree Go2 quadruped robot") + print(" - WebRTC communication interface") + print(" - LangChain-based agent system (agents2)") + print(" - Converted skill system with @skill decorators") + print("\nStarting system...\n") + + # Check for API key (would need ANTHROPIC_API_KEY for Claude) + if not os.getenv("OPENAI_API_KEY"): + print("WARNING: OPENAI_API_KEY not found in environment") + print("Please set your API key in .env file or environment") + print("(Note: Full Claude support would require ANTHROPIC_API_KEY)") + sys.exit(1) + + system_prompt = """You are a helpful robot assistant controlling a Unitree Go2 quadruped robot. +You can move, navigate, speak, and perform various actions. Be helpful and friendly.""" + + try: + # Setup components + self.robot = self.setup_robot() + + self.agent = self.setup_agent( + [ + UnitreeSkillContainer(self.robot), + HumanInput(), + ], + system_prompt, + ) + + # Start handling queries + self.running = True + + logger.info("=" * 60) + logger.info("Unitree Go2 Agent Ready (agents2 framework)!") + logger.info("You can:") + logger.info(" - Type commands in the human cli") + logger.info(" - Ask the robot to move or navigate") + logger.info(" - Ask the robot to perform actions (sit, stand, dance, etc.)") + logger.info(" - Ask the robot to speak text") + logger.info("=" * 60) + + while True: + time.sleep(1) + except KeyboardInterrupt: + logger.info("Keyboard interrupt received") + except Exception as e: + logger.error(f"Error running robot: {e}") + import traceback + + traceback.print_exc() + # finally: + # self.shutdown() + + def shutdown(self) -> None: + logger.info("Shutting down...") + self.running = False + + if self.agent: + try: + self.agent.stop() + logger.info("Agent stopped") + except Exception as e: + logger.error(f"Error stopping agent: {e}") + + if self.robot: + try: + self.robot.stop() + logger.info("Robot connection closed") + except Exception as e: + logger.error(f"Error stopping robot: {e}") + + logger.info("Shutdown complete") + + +def main() -> None: + runner = UnitreeAgentRunner() + runner.run() + + +if __name__ == "__main__": + main() diff --git a/dimos/agents2/temp/run_unitree_async.py b/dimos/agents2/temp/run_unitree_async.py new file mode 100644 index 0000000000..29213c1c90 --- /dev/null +++ b/dimos/agents2/temp/run_unitree_async.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Async version of the Unitree run file for agents2. +Properly handles the async nature of the agent. +""" + +import asyncio +import os +from pathlib import Path +import sys + +from dotenv import load_dotenv + +# Add parent directories to path +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) + +from dimos.agents2 import Agent +from dimos.agents2.spec import Model, Provider +from dimos.robot.unitree_webrtc.unitree_go2 import UnitreeGo2 +from dimos.robot.unitree_webrtc.unitree_skill_container import UnitreeSkillContainer +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("run_unitree_async") + +# Load environment variables +load_dotenv() + +# System prompt path +SYSTEM_PROMPT_PATH = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), + "assets/agent/prompt.txt", +) + + +async def handle_query(agent, query_text): + """Handle a single query asynchronously.""" + logger.info(f"Processing query: {query_text}") + + try: + # Use query_async which returns a Future + future = agent.query_async(query_text) + + # Wait for the result (with timeout) + await asyncio.wait_for(asyncio.wrap_future(future), timeout=30.0) + + # Get the result + if future.done(): + result = future.result() + logger.info(f"Agent response: {result}") + return result + else: + logger.warning("Query did not complete") + return "Query timeout" + + except asyncio.TimeoutError: + logger.error("Query timed out after 30 seconds") + return "Query timeout" + except Exception as e: + logger.error(f"Error processing query: {e}") + return f"Error: {e!s}" + + +async def interactive_loop(agent) -> None: + """Run an interactive query loop.""" + print("\n" + "=" * 60) + print("Interactive Agent Mode") + print("Type your commands or 'quit' to exit") + print("=" * 60 + "\n") + + while True: + try: + # Get user input + query = input("\nYou: ").strip() + + if query.lower() in ["quit", "exit", "q"]: + break + + if not query: + continue + + # Process query + response = await handle_query(agent, query) + print(f"\nAgent: {response}") + + except KeyboardInterrupt: + break + except Exception as e: + logger.error(f"Error in interactive loop: {e}") + + +async def main() -> None: + """Main async function.""" + print("\n" + "=" * 60) + print("Unitree Go2 Robot with agents2 Framework (Async)") + print("=" * 60) + + # Check for API key + if not os.getenv("OPENAI_API_KEY"): + print("ERROR: OPENAI_API_KEY not found") + print("Set your API key in .env file or environment") + sys.exit(1) + + # Load system prompt + try: + with open(SYSTEM_PROMPT_PATH) as f: + system_prompt = f.read() + except FileNotFoundError: + system_prompt = """You are a helpful robot assistant controlling a Unitree Go2 robot. +You have access to various movement and control skills. Be helpful and concise.""" + + # Initialize robot (optional - comment out if no robot) + robot = None + if os.getenv("ROBOT_IP"): + try: + logger.info("Connecting to robot...") + robot = UnitreeGo2( + ip=os.getenv("ROBOT_IP"), + connection_type=os.getenv("CONNECTION_TYPE", "webrtc"), + ) + robot.start() + await asyncio.sleep(3) + logger.info("Robot connected") + except Exception as e: + logger.warning(f"Could not connect to robot: {e}") + logger.info("Continuing without robot...") + + # Create skill container + skill_container = UnitreeSkillContainer(robot=robot) + + # Create agent + agent = Agent( + system_prompt=system_prompt, + model=Model.GPT_4O_MINI, # Using mini for faster responses + provider=Provider.OPENAI, + ) + + # Register skills and start + agent.register_skills(skill_container) + agent.start() + + # Log available skills + skills = skill_container.skills() + logger.info(f"Agent initialized with {len(skills)} skills") + + # Test query + print("\n--- Testing agent query ---") + test_response = await handle_query(agent, "Hello! Can you list 5 of your movement skills?") + print(f"Test response: {test_response}\n") + + # Run interactive loop + try: + await interactive_loop(agent) + except KeyboardInterrupt: + logger.info("Interrupted by user") + + # Clean up + logger.info("Shutting down...") + agent.stop() + if robot: + logger.info("Robot disconnected") + + print("\nGoodbye!") + + +if __name__ == "__main__": + # Run the async main function + asyncio.run(main()) diff --git a/dimos/agents2/temp/test_unitree_agent_query.py b/dimos/agents2/temp/test_unitree_agent_query.py new file mode 100644 index 0000000000..4990940e6c --- /dev/null +++ b/dimos/agents2/temp/test_unitree_agent_query.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Test script to debug agent query issues. +Shows different ways to call the agent and handle async. +""" + +import asyncio +import os +from pathlib import Path +import sys +import time + +from dotenv import load_dotenv + +# Add parent directories to path +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) + +from dimos.agents2 import Agent +from dimos.agents2.spec import Model, Provider +from dimos.robot.unitree_webrtc.unitree_skill_container import UnitreeSkillContainer +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("test_agent_query") + +# Load environment variables +load_dotenv() + + +async def test_async_query(): + """Test agent query using async/await pattern.""" + print("\n=== Testing Async Query ===\n") + + # Create skill container + container = UnitreeSkillContainer(robot=None) + + # Create agent + agent = Agent( + system_prompt="You are a helpful robot assistant. List 3 skills you can do.", + model=Model.GPT_4O_MINI, + provider=Provider.OPENAI, + ) + + # Register skills and start + agent.register_skills(container) + agent.start() + + # Query asynchronously + logger.info("Sending async query...") + future = agent.query_async("Hello! What skills do you have?") + + # Wait for result + logger.info("Waiting for response...") + await asyncio.sleep(10) # Give it time to process + + # Check if future is done + if hasattr(future, "done") and future.done(): + try: + result = future.result() + logger.info(f"Got result: {result}") + except Exception as e: + logger.error(f"Future failed: {e}") + else: + logger.warning("Future not completed yet") + + agent.stop() + + return future + + +def test_sync_query_with_thread() -> None: + """Test agent query using threading for the event loop.""" + print("\n=== Testing Sync Query with Thread ===\n") + + import threading + + # Create skill container + container = UnitreeSkillContainer(robot=None) + + # Create agent + agent = Agent( + system_prompt="You are a helpful robot assistant. List 3 skills you can do.", + model=Model.GPT_4O_MINI, + provider=Provider.OPENAI, + ) + + # Register skills and start + agent.register_skills(container) + agent.start() + + # Track the thread we might create + loop_thread = None + + # The agent's event loop should be running in the Module's thread + # Let's check if it's running + if agent._loop and agent._loop.is_running(): + logger.info("Agent's event loop is running") + else: + logger.warning("Agent's event loop is NOT running - this is the problem!") + + # Try to run the loop in a thread + def run_loop() -> None: + asyncio.set_event_loop(agent._loop) + agent._loop.run_forever() + + loop_thread = threading.Thread(target=run_loop, daemon=False, name="EventLoopThread") + loop_thread.start() + time.sleep(1) # Give loop time to start + logger.info("Started event loop in thread") + + # Now try the query + try: + logger.info("Sending sync query...") + result = agent.query("Hello! What skills do you have?") + logger.info(f"Got result: {result}") + except Exception as e: + logger.error(f"Query failed: {e}") + import traceback + + traceback.print_exc() + + agent.stop() + + # Then stop the manually created event loop thread if we created one + if loop_thread and loop_thread.is_alive(): + logger.info("Stopping manually created event loop thread...") + # Stop the event loop + if agent._loop and agent._loop.is_running(): + agent._loop.call_soon_threadsafe(agent._loop.stop) + # Wait for thread to finish + loop_thread.join(timeout=5) + if loop_thread.is_alive(): + logger.warning("Thread did not stop cleanly within timeout") + + # Finally close the container + container._close_module() + + +# def test_with_real_module_system(): +# """Test using the real DimOS module system (like in test_agent.py).""" +# print("\n=== Testing with Module System ===\n") + +# from dimos.core import start + +# # Start the DimOS system +# dimos = start(2) + +# # Deploy container and agent as modules +# container = dimos.deploy(UnitreeSkillContainer, robot=None) +# agent = dimos.deploy( +# Agent, +# system_prompt="You are a helpful robot assistant. List 3 skills you can do.", +# model=Model.GPT_4O_MINI, +# provider=Provider.OPENAI, +# ) + +# # Register skills +# agent.register_skills(container) +# agent.start() + +# # Query +# try: +# logger.info("Sending query through module system...") +# future = agent.query_async("Hello! What skills do you have?") + +# # In the module system, the loop should be running +# time.sleep(5) # Wait for processing + +# if hasattr(future, "result"): +# result = future.result(timeout=10) +# logger.info(f"Got result: {result}") +# except Exception as e: +# logger.error(f"Query failed: {e}") + +# # Clean up +# agent.stop() +# dimos.stop() + + +def main() -> None: + """Run tests based on available API key.""" + + if not os.getenv("OPENAI_API_KEY"): + print("ERROR: OPENAI_API_KEY not set") + print("Please set your OpenAI API key to test the agent") + sys.exit(1) + + print("=" * 60) + print("Agent Query Testing") + print("=" * 60) + + # Test 1: Async query + try: + asyncio.run(test_async_query()) + except Exception as e: + logger.error(f"Async test failed: {e}") + + # Test 2: Sync query with threading + try: + test_sync_query_with_thread() + except Exception as e: + logger.error(f"Sync test failed: {e}") + + # Test 3: Module system (optional - more complex) + # try: + # test_with_real_module_system() + # except Exception as e: + # logger.error(f"Module test failed: {e}") + + print("\n" + "=" * 60) + print("Testing complete") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/dimos/agents2/temp/test_unitree_skill_container.py b/dimos/agents2/temp/test_unitree_skill_container.py new file mode 100644 index 0000000000..16502004ff --- /dev/null +++ b/dimos/agents2/temp/test_unitree_skill_container.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Test file for UnitreeSkillContainer with agents2 framework. +Tests skill registration and basic functionality. +""" + +from pathlib import Path +import sys +import time + +# Add parent directories to path +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) + +from dimos.agents2 import Agent +from dimos.agents2.spec import Model, Provider +from dimos.robot.unitree_webrtc.unitree_skill_container import UnitreeSkillContainer +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("test_unitree_skills") + + +def test_skill_container_creation(): + """Test that the skill container can be created and skills are registered.""" + print("\n=== Testing UnitreeSkillContainer Creation ===") + + # Create container without robot (for testing) + container = UnitreeSkillContainer(robot=None) + + try: + # Get available skills from the container + skills = container.skills() + + print(f"Number of skills registered: {len(skills)}") + print("\nAvailable skills:") + for name, skill_config in list(skills.items())[:10]: # Show first 10 + print( + f" - {name}: {skill_config.description if hasattr(skill_config, 'description') else 'No description'}" + ) + if len(skills) > 10: + print(f" ... and {len(skills) - 10} more skills") + + return container, skills + finally: + # Ensure proper cleanup + container._close_module() + # Small delay to allow threads to finish cleanup + time.sleep(0.1) + + +def test_agent_with_skills(): + """Test that an agent can be created with the skill container.""" + print("\n=== Testing Agent with Skills ===") + + # Create skill container + container = UnitreeSkillContainer(robot=None) + agent = None + + try: + # Create agent with configuration passed directly + agent = Agent( + system_prompt="You are a helpful robot assistant that can control a Unitree Go2 robot.", + model=Model.GPT_4O_MINI, + provider=Provider.OPENAI, + ) + + # Register skills + agent.register_skills(container) + + print("Agent created and skills registered successfully!") + + # Get tools to verify + tools = agent.get_tools() + print(f"Agent has access to {len(tools)} tools") + + return agent + finally: + # Ensure proper cleanup in order + if agent: + agent.stop() + container._close_module() + # Small delay to allow threads to finish cleanup + time.sleep(0.1) + + +def test_skill_schemas() -> None: + """Test that skill schemas are properly generated for LangChain.""" + print("\n=== Testing Skill Schemas ===") + + container = UnitreeSkillContainer(robot=None) + + try: + skills = container.skills() + + # Check a few key skills (using snake_case names now) + skill_names = ["move", "wait", "stand_up", "sit", "front_flip", "dance1"] + + for name in skill_names: + if name in skills: + skill_config = skills[name] + print(f"\n{name} skill:") + print(f" Config: {skill_config}") + if hasattr(skill_config, "schema"): + print( + f" Schema keys: {skill_config.schema.keys() if skill_config.schema else 'None'}" + ) + else: + print(f"\nWARNING: Skill '{name}' not found!") + finally: + # Ensure proper cleanup + container._close_module() + # Small delay to allow threads to finish cleanup + time.sleep(0.1) diff --git a/dimos/agents2/temp/webcam_agent.py b/dimos/agents2/temp/webcam_agent.py new file mode 100644 index 0000000000..485684d9e0 --- /dev/null +++ b/dimos/agents2/temp/webcam_agent.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Run script for Unitree Go2 robot with agents2 framework. +This is the migrated version using the new LangChain-based agent system. +""" + +from threading import Thread +import time + +import reactivex as rx +import reactivex.operators as ops + +from dimos.agents2 import Agent, Output, Reducer, Stream, skill +from dimos.agents2.cli.human import HumanInput +from dimos.agents2.spec import Model, Provider +from dimos.core import LCMTransport, Module, rpc, start +from dimos.hardware.camera import zed +from dimos.hardware.camera.module import CameraModule +from dimos.hardware.camera.webcam import Webcam +from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 +from dimos.msgs.sensor_msgs import CameraInfo, Image +from dimos.protocol.skill.test_coordinator import SkillContainerTest +from dimos.web.robot_web_interface import RobotWebInterface + + +class WebModule(Module): + web_interface: RobotWebInterface = None + human_query: rx.subject.Subject = None + agent_response: rx.subject.Subject = None + + thread: Thread = None + + _human_messages_running = False + + def __init__(self) -> None: + super().__init__() + self.agent_response = rx.subject.Subject() + self.human_query = rx.subject.Subject() + + @rpc + def start(self) -> None: + super().start() + + text_streams = { + "agent_responses": self.agent_response, + } + + self.web_interface = RobotWebInterface( + port=5555, + text_streams=text_streams, + audio_subject=rx.subject.Subject(), + ) + + unsub = self.web_interface.query_stream.subscribe(self.human_query.on_next) + self._disposables.add(unsub) + + self.thread = Thread(target=self.web_interface.run, daemon=True) + self.thread.start() + + @rpc + def stop(self) -> None: + if self.web_interface: + self.web_interface.stop() + if self.thread: + # TODO, you can't just wait for a server to close, you have to signal it to end. + self.thread.join(timeout=1.0) + + super().stop() + + @skill(stream=Stream.call_agent, reducer=Reducer.all, output=Output.human) + def human_messages(self): + """Provide human messages from web interface. Don't use this tool, it's running implicitly already""" + if self._human_messages_running: + print("human_messages already running, not starting another") + return "already running" + self._human_messages_running = True + while True: + print("Waiting for human message...") + message = self.human_query.pipe(ops.first()).run() + print(f"Got human message: {message}") + yield message + + +def main() -> None: + dimos = start(4) + # Create agent + agent = Agent( + system_prompt="You are a helpful assistant for controlling a Unitree Go2 robot. ", + model=Model.GPT_4O, # Could add CLAUDE models to enum + provider=Provider.OPENAI, # Would need ANTHROPIC provider + ) + + testcontainer = dimos.deploy(SkillContainerTest) + webcam = dimos.deploy( + CameraModule, + transform=Transform( + translation=Vector3(0.0, 0.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="base_link", + child_frame_id="camera_link", + ), + hardware=lambda: Webcam( + camera_index=0, + frequency=15, + stereo_slice="left", + camera_info=zed.CameraInfo.SingleWebcam, + ), + ) + + webcam.camera_info.transport = LCMTransport("/camera_info", CameraInfo) + + webcam.image.transport = LCMTransport("/image", Image) + + webcam.start() + + human_input = dimos.deploy(HumanInput) + + time.sleep(1) + + agent.register_skills(human_input) + agent.register_skills(webcam) + agent.register_skills(testcontainer) + + agent.run_implicit_skill("video_stream") + agent.run_implicit_skill("human") + + agent.start() + agent.loop_thread() + + while True: + time.sleep(1) + + # webcam.stop() + + +if __name__ == "__main__": + main() diff --git a/dimos/agents2/test_agent.py b/dimos/agents2/test_agent.py new file mode 100644 index 0000000000..447d02e6e3 --- /dev/null +++ b/dimos/agents2/test_agent.py @@ -0,0 +1,169 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import pytest_asyncio + +from dimos.agents2.agent import Agent +from dimos.core import start +from dimos.protocol.skill.test_coordinator import SkillContainerTest + +system_prompt = ( + "Your name is Mr. Potato, potatoes are bad at math. Use a tools if asked to calculate" +) + + +@pytest.fixture(scope="session") +def dimos_cluster(): + """Session-scoped fixture to initialize dimos cluster once.""" + dimos = start(2) + try: + yield dimos + finally: + dimos.shutdown() + + +@pytest_asyncio.fixture +async def local(): + """Local context: both agent and testcontainer run locally""" + testcontainer = SkillContainerTest() + agent = Agent(system_prompt=system_prompt) + try: + yield agent, testcontainer + except Exception as e: + print(f"Error: {e}") + import traceback + + traceback.print_exc() + raise e + finally: + # Ensure cleanup happens while event loop is still active + try: + agent.stop() + except Exception: + pass + try: + testcontainer.stop() + except Exception: + pass + + +@pytest_asyncio.fixture +async def dask_mixed(dimos_cluster): + """Dask context: testcontainer on dimos, agent local""" + testcontainer = dimos_cluster.deploy(SkillContainerTest) + agent = Agent(system_prompt=system_prompt) + try: + yield agent, testcontainer + finally: + try: + agent.stop() + except Exception: + pass + try: + testcontainer.stop() + except Exception: + pass + + +@pytest_asyncio.fixture +async def dask_full(dimos_cluster): + """Dask context: both agent and testcontainer deployed on dimos""" + testcontainer = dimos_cluster.deploy(SkillContainerTest) + agent = dimos_cluster.deploy(Agent, system_prompt=system_prompt) + try: + yield agent, testcontainer + finally: + try: + agent.stop() + except Exception: + pass + try: + testcontainer.stop() + except Exception: + pass + + +@pytest_asyncio.fixture(params=["local", "dask_mixed", "dask_full"]) +async def agent_context(request): + """Parametrized fixture that runs tests with different agent configurations""" + param = request.param + + if param == "local": + testcontainer = SkillContainerTest() + agent = Agent(system_prompt=system_prompt) + try: + yield agent, testcontainer + finally: + try: + agent.stop() + except Exception: + pass + try: + testcontainer.stop() + except Exception: + pass + elif param == "dask_mixed": + dimos_cluster = request.getfixturevalue("dimos_cluster") + testcontainer = dimos_cluster.deploy(SkillContainerTest) + agent = Agent(system_prompt=system_prompt) + try: + yield agent, testcontainer + finally: + try: + agent.stop() + except Exception: + pass + try: + testcontainer.stop() + except Exception: + pass + elif param == "dask_full": + dimos_cluster = request.getfixturevalue("dimos_cluster") + testcontainer = dimos_cluster.deploy(SkillContainerTest) + agent = dimos_cluster.deploy(Agent, system_prompt=system_prompt) + try: + yield agent, testcontainer + finally: + try: + agent.stop() + except Exception: + pass + try: + testcontainer.stop() + except Exception: + pass + + +# @pytest.mark.timeout(40) +@pytest.mark.tool +@pytest.mark.asyncio +async def test_agent_init(agent_context) -> None: + """Test agent initialization and basic functionality across different configurations""" + agent, testcontainer = agent_context + + agent.register_skills(testcontainer) + agent.start() + + # agent.run_implicit_skill("uptime_seconds") + + print("query agent") + # When running locally, call the async method directly + agent.query( + "hi there, please tell me what's your name and current date, and how much is 124181112 + 124124?" + ) + print("Agent loop finished, asking about camera") + agent.query("tell me what you see on the camera?") + + # you can run skillspy and agentspy in parallel with this test for a better observation of what's happening diff --git a/dimos/agents2/test_agent_direct.py b/dimos/agents2/test_agent_direct.py new file mode 100644 index 0000000000..ee3f9aa091 --- /dev/null +++ b/dimos/agents2/test_agent_direct.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from contextlib import contextmanager + +from dimos.agents2.agent import Agent +from dimos.core import start +from dimos.protocol.skill.test_coordinator import SkillContainerTest + +system_prompt = ( + "Your name is Mr. Potato, potatoes are bad at math. Use a tools if asked to calculate" +) + + +@contextmanager +def dimos_cluster(): + dimos = start(2) + try: + yield dimos + finally: + dimos.close_all() + + +@contextmanager +def local(): + """Local context: both agent and testcontainer run locally""" + testcontainer = SkillContainerTest() + agent = Agent(system_prompt=system_prompt) + try: + yield agent, testcontainer + except Exception as e: + print(f"Error: {e}") + import traceback + + traceback.print_exc() + raise e + finally: + # Ensure cleanup happens while event loop is still active + agent.stop() + testcontainer.stop() + + +@contextmanager +def partial(): + """Dask context: testcontainer on dimos, agent local""" + with dimos_cluster() as dimos: + testcontainer = dimos.deploy(SkillContainerTest) + agent = Agent(system_prompt=system_prompt) + try: + yield agent, testcontainer + finally: + agent.stop() + testcontainer.stop() + + +@contextmanager +def full(): + """Dask context: both agent and testcontainer deployed on dimos""" + with dimos_cluster() as dimos: + testcontainer = dimos.deploy(SkillContainerTest) + agent = dimos.deploy(Agent, system_prompt=system_prompt) + try: + yield agent, testcontainer + finally: + agent.stop() + testcontainer.stop() + + +def check_agent(agent_context) -> None: + """Test agent initialization and basic functionality across different configurations""" + with agent_context() as [agent, testcontainer]: + agent.register_skills(testcontainer) + agent.start() + + print("query agent") + + agent.query( + "hi there, please tell me what's your name and current date, and how much is 124181112 + 124124?" + ) + + print("Agent loop finished, asking about camera") + + agent.query("tell me what you see on the camera?") + + print("=" * 150) + print("End of test", agent.get_agent_id()) + print("=" * 150) + + # you can run skillspy and agentspy in parallel with this test for a better observation of what's happening + + +if __name__ == "__main__": + list(map(check_agent, [local, partial, full])) diff --git a/dimos/agents2/test_agent_fake.py b/dimos/agents2/test_agent_fake.py new file mode 100644 index 0000000000..14e28cd89c --- /dev/null +++ b/dimos/agents2/test_agent_fake.py @@ -0,0 +1,36 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def test_what_is_your_name(create_potato_agent) -> None: + agent = create_potato_agent(fixture="test_what_is_your_name.json") + response = agent.query("hi there, please tell me what's your name?") + assert "Mr. Potato" in response + + +def test_how_much_is_124181112_plus_124124(create_potato_agent) -> None: + agent = create_potato_agent(fixture="test_how_much_is_124181112_plus_124124.json") + + response = agent.query("how much is 124181112 + 124124?") + assert "124305236" in response.replace(",", "") + + response = agent.query("how much is one billion plus -1000000, in digits please") + assert "999000000" in response.replace(",", "") + + +def test_what_do_you_see_in_this_picture(create_potato_agent) -> None: + agent = create_potato_agent(fixture="test_what_do_you_see_in_this_picture.json") + + response = agent.query("take a photo and tell me what do you see") + assert "outdoor cafe " in response diff --git a/dimos/agents2/test_mock_agent.py b/dimos/agents2/test_mock_agent.py new file mode 100644 index 0000000000..4b113b45a0 --- /dev/null +++ b/dimos/agents2/test_mock_agent.py @@ -0,0 +1,202 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test agent with FakeChatModel for unit testing.""" + +import time + +from dimos_lcm.sensor_msgs import CameraInfo +from langchain_core.messages import AIMessage, HumanMessage +import pytest + +from dimos.agents2.agent import Agent +from dimos.agents2.testing import MockModel +from dimos.core import LCMTransport, start +from dimos.msgs.geometry_msgs import PoseStamped, Vector3 +from dimos.msgs.sensor_msgs import Image +from dimos.protocol.skill.test_coordinator import SkillContainerTest +from dimos.robot.unitree_webrtc.modular.connection_module import ConnectionModule +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage + + +def test_tool_call() -> None: + """Test agent initialization and tool call execution.""" + # Create a fake model that will respond with tool calls + fake_model = MockModel( + responses=[ + AIMessage( + content="I'll add those numbers for you.", + tool_calls=[ + { + "name": "add", + "args": {"args": {"x": 5, "y": 3}}, + "id": "tool_call_1", + } + ], + ), + AIMessage(content="Let me do some math..."), + AIMessage(content="The result of adding 5 and 3 is 8."), + ] + ) + + # Create agent with the fake model + agent = Agent( + model_instance=fake_model, + system_prompt="You are a helpful robot assistant with math skills.", + ) + + # Register skills with coordinator + skills = SkillContainerTest() + agent.coordinator.register_skills(skills) + agent.start() + + # Query the agent + agent.query("Please add 5 and 3") + + # Check that tools were bound + assert fake_model.tools is not None + assert len(fake_model.tools) > 0 + + # Verify the model was called and history updated + assert len(agent._history) > 0 + + agent.stop() + + +def test_image_tool_call() -> None: + """Test agent with image tool call execution.""" + dimos = start(2) + # Create a fake model that will respond with image tool calls + fake_model = MockModel( + responses=[ + AIMessage( + content="I'll take a photo for you.", + tool_calls=[ + { + "name": "take_photo", + "args": {"args": {}}, + "id": "tool_call_image_1", + } + ], + ), + AIMessage(content="I've taken the photo. The image shows a cafe scene."), + ] + ) + + # Create agent with the fake model + agent = Agent( + model_instance=fake_model, + system_prompt="You are a helpful robot assistant with camera capabilities.", + ) + + test_skill_module = dimos.deploy(SkillContainerTest) + + agent.register_skills(test_skill_module) + agent.start() + + agent.run_implicit_skill("get_detections") + + # Query the agent + agent.query("Please take a photo") + + # Check that tools were bound + assert fake_model.tools is not None + assert len(fake_model.tools) > 0 + + # Verify the model was called and history updated + assert len(agent._history) > 0 + + # Check that image was handled specially + # Look for HumanMessage with image content in history + human_messages_with_images = [ + msg + for msg in agent._history + if isinstance(msg, HumanMessage) and msg.content and isinstance(msg.content, list) + ] + assert len(human_messages_with_images) >= 0 # May have image messages + agent.stop() + test_skill_module.stop() + dimos.close_all() + + +@pytest.mark.tool +def test_tool_call_implicit_detections() -> None: + """Test agent with image tool call execution.""" + dimos = start(2) + # Create a fake model that will respond with image tool calls + fake_model = MockModel( + responses=[ + AIMessage( + content="I'll take a photo for you.", + tool_calls=[ + { + "name": "take_photo", + "args": {"args": {}}, + "id": "tool_call_image_1", + } + ], + ), + AIMessage(content="I've taken the photo. The image shows a cafe scene."), + ] + ) + + # Create agent with the fake model + agent = Agent( + model_instance=fake_model, + system_prompt="You are a helpful robot assistant with camera capabilities.", + ) + + robot_connection = dimos.deploy(ConnectionModule, connection_type="fake") + robot_connection.lidar.transport = LCMTransport("/lidar", LidarMessage) + robot_connection.odom.transport = LCMTransport("/odom", PoseStamped) + robot_connection.video.transport = LCMTransport("/image", Image) + robot_connection.movecmd.transport = LCMTransport("/cmd_vel", Vector3) + robot_connection.camera_info.transport = LCMTransport("/camera_info", CameraInfo) + robot_connection.start() + + test_skill_module = dimos.deploy(SkillContainerTest) + + agent.register_skills(test_skill_module) + agent.start() + + agent.run_implicit_skill("get_detections") + + print( + "Robot replay pipeline is running in the background.\nWaiting 8.5 seconds for some detections before quering agent" + ) + time.sleep(8.5) + + # Query the agent + agent.query("Please take a photo") + + # Check that tools were bound + assert fake_model.tools is not None + assert len(fake_model.tools) > 0 + + # Verify the model was called and history updated + assert len(agent._history) > 0 + + # Check that image was handled specially + # Look for HumanMessage with image content in history + human_messages_with_images = [ + msg + for msg in agent._history + if isinstance(msg, HumanMessage) and msg.content and isinstance(msg.content, list) + ] + assert len(human_messages_with_images) >= 0 + + agent.stop() + test_skill_module.stop() + robot_connection.stop() + dimos.stop() diff --git a/dimos/agents2/test_stash_agent.py b/dimos/agents2/test_stash_agent.py new file mode 100644 index 0000000000..8e2972568a --- /dev/null +++ b/dimos/agents2/test_stash_agent.py @@ -0,0 +1,61 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from dimos.agents2.agent import Agent +from dimos.protocol.skill.test_coordinator import SkillContainerTest + + +@pytest.mark.tool +@pytest.mark.asyncio +async def test_agent_init() -> None: + system_prompt = ( + "Your name is Mr. Potato, potatoes are bad at math. Use a tools if asked to calculate" + ) + + # # Uncomment the following lines to use a dimos module system + # dimos = start(2) + # testcontainer = dimos.deploy(SkillContainerTest) + # agent = Agent(system_prompt=system_prompt) + + ## uncomment the following lines to run agents in a main loop without a module system + testcontainer = SkillContainerTest() + agent = Agent(system_prompt=system_prompt) + + agent.register_skills(testcontainer) + agent.start() + + agent.run_implicit_skill("uptime_seconds") + + await agent.query_async( + "hi there, please tell me what's your name and current date, and how much is 124181112 + 124124?" + ) + + # agent loop is considered finished once no active skills remain, + # agent will stop it's loop if passive streams are active + print("Agent loop finished, asking about camera") + + # we query again (this shows subsequent querying, but we could have asked for camera image in the original query, + # it all runs in parallel, and agent might get called once or twice depending on timing of skill responses) + # await agent.query_async("tell me what you see on the camera?") + + # you can run skillspy and agentspy in parallel with this test for a better observation of what's happening + await agent.query_async("tell me exactly everything we've talked about until now") + + print("Agent loop finished") + + agent.stop() + testcontainer.stop() + dimos.stop() diff --git a/dimos/agents2/testing.py b/dimos/agents2/testing.py new file mode 100644 index 0000000000..b729c13d50 --- /dev/null +++ b/dimos/agents2/testing.py @@ -0,0 +1,197 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Testing utilities for agents.""" + +from collections.abc import Iterator, Sequence +import json +import os +from pathlib import Path +from typing import Any + +from langchain.chat_models import init_chat_model +from langchain_core.callbacks.manager import CallbackManagerForLLMRun +from langchain_core.language_models.chat_models import SimpleChatModel +from langchain_core.messages import ( + AIMessage, + AIMessageChunk, + BaseMessage, +) +from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult +from langchain_core.runnables import Runnable + + +class MockModel(SimpleChatModel): + """Custom fake chat model that supports tool calls for testing. + + Can operate in two modes: + 1. Playback mode (default): Reads responses from a JSON file or list + 2. Record mode: Uses a real LLM and saves responses to a JSON file + """ + + responses: list[str | AIMessage] = [] + i: int = 0 + json_path: Path | None = None + record: bool = False + real_model: Any | None = None + recorded_messages: list[dict[str, Any]] = [] + + def __init__(self, **kwargs) -> None: + # Extract custom parameters before calling super().__init__ + responses = kwargs.pop("responses", []) + json_path = kwargs.pop("json_path", None) + model_provider = kwargs.pop("model_provider", "openai") + model_name = kwargs.pop("model_name", "gpt-4o") + + super().__init__(**kwargs) + + self.json_path = Path(json_path) if json_path else None + self.record = bool(os.getenv("RECORD")) + self.i = 0 + self._bound_tools: Sequence[Any] | None = None + self.recorded_messages = [] + + if self.record: + # Initialize real model for recording + self.real_model = init_chat_model(model_provider=model_provider, model=model_name) + self.responses = [] # Initialize empty for record mode + elif self.json_path: + self.responses = self._load_responses_from_json() + elif responses: + self.responses = responses + else: + raise ValueError("no responses") + + @property + def _llm_type(self) -> str: + return "tool-call-fake-chat-model" + + def _load_responses_from_json(self) -> list[AIMessage]: + with open(self.json_path) as f: + data = json.load(f) + + responses = [] + for item in data.get("responses", []): + if isinstance(item, str): + responses.append(AIMessage(content=item)) + else: + # Reconstruct AIMessage from dict + msg = AIMessage( + content=item.get("content", ""), tool_calls=item.get("tool_calls", []) + ) + responses.append(msg) + return responses + + def _save_responses_to_json(self) -> None: + if not self.json_path: + return + + self.json_path.parent.mkdir(parents=True, exist_ok=True) + + data = { + "responses": [ + {"content": msg.content, "tool_calls": getattr(msg, "tool_calls", [])} + if isinstance(msg, AIMessage) + else msg + for msg in self.recorded_messages + ] + } + + with open(self.json_path, "w") as f: + json.dump(data, f, indent=2, default=str) + + def _call( + self, + messages: list[BaseMessage], + stop: list[str] | None = None, + run_manager: CallbackManagerForLLMRun | None = None, + **kwargs: Any, + ) -> str: + """Not used in _generate.""" + return "" + + def _generate( + self, + messages: list[BaseMessage], + stop: list[str] | None = None, + run_manager: CallbackManagerForLLMRun | None = None, + **kwargs: Any, + ) -> ChatResult: + if self.record: + # Recording mode - use real model and save responses + if not self.real_model: + raise ValueError("Real model not initialized for recording") + + # Bind tools if needed + model = self.real_model + if self._bound_tools: + model = model.bind_tools(self._bound_tools) + + result = model.invoke(messages) + self.recorded_messages.append(result) + self._save_responses_to_json() + + generation = ChatGeneration(message=result) + return ChatResult(generations=[generation]) + else: + # Playback mode - use predefined responses + if not self.responses: + raise ValueError("No responses available for playback. ") + + if self.i >= len(self.responses): + # Don't wrap around - stay at last response + response = self.responses[-1] + else: + response = self.responses[self.i] + self.i += 1 + + if isinstance(response, AIMessage): + message = response + else: + message = AIMessage(content=str(response)) + + generation = ChatGeneration(message=message) + return ChatResult(generations=[generation]) + + def _stream( + self, + messages: list[BaseMessage], + stop: list[str] | None = None, + run_manager: CallbackManagerForLLMRun | None = None, + **kwargs: Any, + ) -> Iterator[ChatGenerationChunk]: + """Stream not implemented for testing.""" + result = self._generate(messages, stop, run_manager, **kwargs) + message = result.generations[0].message + chunk = AIMessageChunk(content=message.content) + yield ChatGenerationChunk(message=chunk) + + def bind_tools( + self, + tools: Sequence[dict[str, Any] | type | Any], + *, + tool_choice: str | None = None, + **kwargs: Any, + ) -> Runnable: + """Store tools and return self.""" + self._bound_tools = tools + if self.record and self.real_model: + # Also bind tools to the real model + self.real_model = self.real_model.bind_tools(tools, tool_choice=tool_choice, **kwargs) + return self + + @property + def tools(self) -> Sequence[Any] | None: + """Get bound tools for inspection.""" + return self._bound_tools diff --git a/dimos/conftest.py b/dimos/conftest.py new file mode 100644 index 0000000000..e1d0c96e42 --- /dev/null +++ b/dimos/conftest.py @@ -0,0 +1,113 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import threading + +import pytest + + +@pytest.fixture +def event_loop(): + loop = asyncio.new_event_loop() + yield loop + loop.close() + + +_session_threads = set() +_seen_threads = set() +_seen_threads_lock = threading.RLock() +_before_test_threads = {} # Map test name to set of thread IDs before test + +_skip_for = ["lcm", "heavy", "ros"] + + +@pytest.hookimpl() +def pytest_sessionfinish(session): + """Track threads that exist at session start - these are not leaks.""" + + yield + + # Check for session-level thread leaks at teardown + final_threads = [ + t + for t in threading.enumerate() + if t.name != "MainThread" and t.ident not in _session_threads + ] + + if final_threads: + thread_info = [f"{t.name} (daemon={t.daemon})" for t in final_threads] + pytest.fail( + f"\n{len(final_threads)} thread(s) leaked during test session: {thread_info}\n" + "Session-scoped fixtures must clean up all threads in their teardown." + ) + + +@pytest.fixture(autouse=True) +def monitor_threads(request): + # Skip monitoring for tests marked with specified markers + if any(request.node.get_closest_marker(marker) for marker in _skip_for): + yield + return + + # Capture threads before test runs + test_name = request.node.nodeid + with _seen_threads_lock: + _before_test_threads[test_name] = { + t.ident for t in threading.enumerate() if t.ident is not None + } + + yield + + with _seen_threads_lock: + before = _before_test_threads.get(test_name, set()) + current = {t.ident for t in threading.enumerate() if t.ident is not None} + + # New threads are ones that exist now but didn't exist before this test + new_thread_ids = current - before + + if not new_thread_ids: + return + + # Get the actual thread objects for new threads + new_threads = [ + t for t in threading.enumerate() if t.ident in new_thread_ids and t.name != "MainThread" + ] + + # Filter out expected persistent threads from Dask that are shared globally + # These threads are intentionally left running and cleaned up on process exit + expected_persistent_thread_prefixes = ["Dask-Offload"] + new_threads = [ + t + for t in new_threads + if not any(t.name.startswith(prefix) for prefix in expected_persistent_thread_prefixes) + ] + + # Filter out threads we've already seen (from previous tests) + truly_new = [t for t in new_threads if t.ident not in _seen_threads] + + # Mark all new threads as seen + for t in new_threads: + if t.ident is not None: + _seen_threads.add(t.ident) + + if not truly_new: + return + + thread_names = [t.name for t in truly_new] + + pytest.fail( + f"Non-closed threads created during this test. Thread names: {thread_names}. " + "Please look at the first test that fails and fix that." + ) diff --git a/dimos/constants.py b/dimos/constants.py new file mode 100644 index 0000000000..17273b6dd3 --- /dev/null +++ b/dimos/constants.py @@ -0,0 +1,32 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + +DIMOS_PROJECT_ROOT = Path(__file__).parent.parent + +""" +Constants for shared memory +Usually, auto-detection for size would be preferred. Sadly, though, channels are made +and frozen *before* the first frame is received. +Therefore, a maximum capacity for color image and depth image transfer should be defined +ahead of time. +""" +# Default color image size: 1920x1080 frame x 3 (RGB) x uint8 +DEFAULT_CAPACITY_COLOR_IMAGE = 1920 * 1080 * 3 +# Default depth image size: 1280x720 frame * 4 (float32 size) +DEFAULT_CAPACITY_DEPTH_IMAGE = 1280 * 720 * 4 + +# From https://github.com/lcm-proj/lcm.git +LCM_MAX_CHANNEL_NAME_LENGTH = 63 diff --git a/dimos/core/README_BLUEPRINTS.md b/dimos/core/README_BLUEPRINTS.md new file mode 100644 index 0000000000..26143bd456 --- /dev/null +++ b/dimos/core/README_BLUEPRINTS.md @@ -0,0 +1,260 @@ +# Blueprints + +Blueprints (`ModuleBlueprint`) are instructions for how to initialize a `Module`. + +You don't typically want to run a single module, so multiple blueprints are handled together in `ModuleBlueprintSet`. + +You create a `ModuleBlueprintSet` from a single module (say `ConnectionModule`) with: + +```python +blueprint = create_module_blueprint(ConnectionModule, 'arg1', 'arg2', kwarg='value') +``` + +But the same thing can be acomplished more succinctly as: + +```python +connection = ConnectionModule.blueprint +``` + +Now you can create the blueprint with: + +```python +blueprint = connection('arg1', 'arg2', kwarg='value') +``` + +## Linking blueprints + +You can link multiple blueprints together with `autoconnect`: + +```python +blueprint = autoconnect( + module1(), + module2(), + module3(), +) +``` + +`blueprint` itself is a `ModuleBlueprintSet` so you can link it with other modules: + +```python +expanded_blueprint = autoconnect( + blueprint, + module4(), + module5(), +) +``` + +Blueprints are frozen data classes, and `autoconnect()` always constructs an expanded blueprint so you never have to worry about changes in one affecting the other. + +### Duplicate module handling + +If the same module appears multiple times in `autoconnect`, the **later blueprint wins** and overrides earlier ones: + +```python +blueprint = autoconnect( + module_a(arg1=1), + module_b(), + module_a(arg1=2), # This one is used, the first is discarded +) +``` + +This is so you can "inherit" from one blueprint but override something you need to change. + +## How transports are linked + +Imagine you have this code: + +```python +class ModuleA(Module): + image: Out[Image] = None + start_explore: Out[Bool] = None + +class ModuleB(Module): + image: In[Image] = None + begin_explore: In[Bool] = None + +module_a = partial(create_module_blueprint, ModuleA) +module_b = partial(create_module_blueprint, ModuleB) + +autoconnect(module_a(), module_b()) +``` + +Connections are linked based on `(property_name, object_type)`. In this case `('image', Image)` will be connected between the two modules, but `begin_explore` will not be linked to `start_explore`. + +## Topic names + +By default, the name of the property is used to generate the topic name. So for `image`, the topic will be `/image`. + +The property name is used only if it's unique. If two modules have the same property name with different types, then both get a random topic such as `/SGVsbG8sIFdvcmxkI`. + +If you don't like the name you can always override it like in the next section. + +## Which transport is used? + +By default `LCMTransport` is used if the object supports `lcm_encode`. If it doesn't `pLCMTransport` is used (meaning "pickled LCM"). + +You can override transports with the `transports` method. It returns a new blueprint in which the override is set. + +```python +blueprint = autoconnect(...) +expanded_blueprint = autoconnect(blueprint, ...) +blueprint = blueprint.transports({ + ("image", Image): pSHMTransport( + "/go2/color_image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE + ), + ("start_explore", Bool): pLCMTransport(), +}) +``` + +Note: `expanded_blueprint` does not get the transport overrides because it's created from the initial value of `blueprint`, not the second. + +## Remapping connections + +Sometimes you need to rename a connection to match what other modules expect. You can use `remappings` to rename module connections: + +```python +class ConnectionModule(Module): + color_image: Out[Image] = None # Outputs on 'color_image' + +class ProcessingModule(Module): + rgb_image: In[Image] = None # Expects input on 'rgb_image' + +# Without remapping, these wouldn't connect automatically +# With remapping, color_image is renamed to rgb_image +blueprint = ( + autoconnect( + ConnectionModule.blueprint(), + ProcessingModule.blueprint(), + ) + .remappings([ + (ConnectionModule, 'color_image', 'rgb_image'), + ]) +) +``` + +After remapping: +- The `color_image` output from `ConnectionModule` is treated as `rgb_image` +- It automatically connects to any module with an `rgb_image` input of type `Image` +- The topic name becomes `/rgb_image` instead of `/color_image` + +If you want to override the topic, you still have to do it manually: + +```python +blueprint +.remappings([ + (ConnectionModule, 'color_image', 'rgb_image'), +]) +.transports({ + ("rgb_image", Image): LCMTransport("/custom/rgb/image", Image), +}) +``` + +## Overriding global configuration. + +Each module can optionally take a `global_config` option in `__init__`. E.g.: + +```python +class ModuleA(Module): + + def __init__(self, global_config: GlobalConfig | None = None): + ... +``` + +The config is normally taken from .env or from environment variables. But you can specifically override the values for a specific blueprint: + +```python +blueprint = blueprint.global_config(n_dask_workers=8) +``` + +## Calling the methods of other modules + +Imagine you have this code: + +```python +class ModuleA(Module): + + @rpc + def get_time(self) -> str: + ... + +class ModuleB(Module): + def request_the_time(self) -> None: + ... +``` + +And you want to call `ModuleA.get_time` in `ModuleB.request_the_time`. + +You can do so by defining a method like `set__`. It will be called with an `RpcCall` that will call the original `ModuleA.get_time`. So you can write this: + +```python +class ModuleA(Module): + + @rpc + def get_time(self) -> str: + ... + +class ModuleB(Module): + @rpc # Note that it has to be an rpc method. + def set_ModuleA_get_time(self, rpc_call: RpcCall) -> None: + self._get_time = rpc_call + self._get_time.set_rpc(self.rpc) + + def request_the_time(self) -> None: + print(self._get_time()) +``` + +Note that `RpcCall.rpc` does not serialize, so you have to set it to the one from the module with `rpc_call.set_rpc(self.rpc)` + +## Defining skills + +Skills have to be registered with `LlmAgent.register_skills(self)`. + +```python +class SomeSkill(Module): + + @skill + def some_skill(self) -> None: + ... + + @rpc + def set_LlmAgent_register_skills(self, register_skills: RpcCall) -> None: + register_skills.set_rpc(self.rpc) + register_skills(RPCClient(self, self.__class__)) + + # The agent is just interested in the `@skill` methods, so you'll need this if your class + # has things that cannot be pickled. + def __getstate__(self): + pass + def __setstate__(self, _state): + pass +``` + +Or, you can avoid all of this by inheriting from `SkillModule` which does the above automatically: + +```python +class SomeSkill(SkillModule): + + @skill + def some_skill(self) -> None: + ... +``` + +## Building + +All you have to do to build a blueprint is call: + +```python +module_coordinator = blueprint.build(global_config=config) +``` + +This returns a `ModuleCoordinator` instance that manages all deployed modules. + +### Running and shutting down + +You can block the thread until it exits with: + +```python +module_coordinator.loop() +``` + +This will wait for Ctrl+C and then automatically stop all modules and clean up resources. diff --git a/dimos/core/__init__.py b/dimos/core/__init__.py new file mode 100644 index 0000000000..641d8a24a5 --- /dev/null +++ b/dimos/core/__init__.py @@ -0,0 +1,282 @@ +from __future__ import annotations + +import multiprocessing as mp +import signal +from typing import Optional + +from dask.distributed import Client, LocalCluster +from rich.console import Console + +import dimos.core.colors as colors +from dimos.core.core import rpc +from dimos.core.module import Module, ModuleBase, ModuleConfig +from dimos.core.rpc_client import RPCClient +from dimos.core.stream import In, Out, RemoteIn, RemoteOut, Transport +from dimos.core.transport import ( + LCMTransport, + SHMTransport, + ZenohTransport, + pLCMTransport, + pSHMTransport, +) +from dimos.protocol.rpc.lcmrpc import LCMRPC +from dimos.protocol.rpc.spec import RPCSpec +from dimos.protocol.tf import LCMTF, TF, PubSubTF, TFConfig, TFSpec +from dimos.utils.actor_registry import ActorRegistry + +__all__ = [ + "LCMRPC", + "LCMTF", + "TF", + "DimosCluster", + "In", + "LCMTransport", + "Module", + "ModuleBase", + "ModuleConfig", + "Out", + "PubSubTF", + "RPCSpec", + "RemoteIn", + "RemoteOut", + "SHMTransport", + "TFConfig", + "TFSpec", + "Transport", + "ZenohTransport", + "pLCMTransport", + "pSHMTransport", + "rpc", + "start", +] + + +class CudaCleanupPlugin: + """Dask worker plugin to cleanup CUDA resources on shutdown.""" + + def setup(self, worker) -> None: + """Called when worker starts.""" + pass + + def teardown(self, worker) -> None: + """Clean up CUDA resources when worker shuts down.""" + try: + import sys + + if "cupy" in sys.modules: + import cupy as cp + + # Clear memory pools + mempool = cp.get_default_memory_pool() + pinned_mempool = cp.get_default_pinned_memory_pool() + mempool.free_all_blocks() + pinned_mempool.free_all_blocks() + cp.cuda.Stream.null.synchronize() + mempool.free_all_blocks() + pinned_mempool.free_all_blocks() + except Exception: + pass + + +def patch_actor(actor, cls) -> None: ... + + +DimosCluster = Client + + +def patchdask(dask_client: Client, local_cluster: LocalCluster) -> DimosCluster: + def deploy( + actor_class, + *args, + **kwargs, + ): + console = Console() + with console.status(f"deploying [green]{actor_class.__name__}", spinner="arc"): + actor = dask_client.submit( + actor_class, + *args, + **kwargs, + actor=True, + ).result() + + worker = actor.set_ref(actor).result() + print(f"deployed: {colors.blue(actor)} @ {colors.orange('worker ' + str(worker))}") + + # Register actor deployment in shared memory + ActorRegistry.update(str(actor), str(worker)) + + return RPCClient(actor, actor_class) + + def check_worker_memory() -> None: + """Check memory usage of all workers.""" + info = dask_client.scheduler_info() + console = Console() + total_workers = len(info.get("workers", {})) + total_memory_used = 0 + total_memory_limit = 0 + + for worker_addr, worker_info in info.get("workers", {}).items(): + metrics = worker_info.get("metrics", {}) + memory_used = metrics.get("memory", 0) + memory_limit = worker_info.get("memory_limit", 0) + + cpu_percent = metrics.get("cpu", 0) + managed_bytes = metrics.get("managed_bytes", 0) + spilled = metrics.get("spilled_bytes", {}).get("memory", 0) + worker_status = worker_info.get("status", "unknown") + worker_id = worker_info.get("id", "?") + + memory_used_gb = memory_used / 1e9 + memory_limit_gb = memory_limit / 1e9 + managed_gb = managed_bytes / 1e9 + spilled / 1e9 + + total_memory_used += memory_used + total_memory_limit += memory_limit + + percentage = (memory_used_gb / memory_limit_gb * 100) if memory_limit_gb > 0 else 0 + + if worker_status == "paused": + status = "[red]PAUSED" + elif percentage >= 95: + status = "[red]CRITICAL" + elif percentage >= 80: + status = "[yellow]WARNING" + else: + status = "[green]OK" + + console.print( + f"Worker-{worker_id} {worker_addr}: " + f"{memory_used_gb:.2f}/{memory_limit_gb:.2f}GB ({percentage:.1f}%) " + f"CPU:{cpu_percent:.0f}% Managed:{managed_gb:.2f}GB " + f"{status}" + ) + + if total_workers > 0: + total_used_gb = total_memory_used / 1e9 + total_limit_gb = total_memory_limit / 1e9 + total_percentage = (total_used_gb / total_limit_gb * 100) if total_limit_gb > 0 else 0 + console.print( + f"[bold]Total: {total_used_gb:.2f}/{total_limit_gb:.2f}GB ({total_percentage:.1f}%) across {total_workers} workers[/bold]" + ) + + def close_all() -> None: + # Prevents multiple calls to close_all + if hasattr(dask_client, "_closed") and dask_client._closed: + return + dask_client._closed = True + + import time + + # Stop all SharedMemory transports before closing Dask + # This prevents the "leaked shared_memory objects" warning and hangs + try: + import gc + + from dimos.protocol.pubsub import shmpubsub + + for obj in gc.get_objects(): + if isinstance(obj, shmpubsub.SharedMemory | shmpubsub.PickleSharedMemory): + try: + obj.stop() + except Exception: + pass + except Exception: + pass + + # Get the event loop before shutting down + loop = dask_client.loop + + # Clear the actor registry + ActorRegistry.clear() + + # Close cluster and client with reasonable timeout + # The CudaCleanupPlugin will handle CUDA cleanup on each worker + try: + local_cluster.close(timeout=5) + except Exception: + pass + + try: + dask_client.close(timeout=5) + except Exception: + pass + + if loop and hasattr(loop, "add_callback") and hasattr(loop, "stop"): + try: + loop.add_callback(loop.stop) + except Exception: + pass + + # Note: We do NOT shutdown the _offload_executor here because it's a global + # module-level ThreadPoolExecutor shared across all Dask clients in the process. + # Shutting it down here would break subsequent Dask client usage (e.g., in tests). + # The executor will be cleaned up when the Python process exits. + + # Give threads time to clean up + # Dask's IO loop and Profile threads are daemon threads + # that will be cleaned up when the process exits + # This is needed, solves race condition in CI thread check + time.sleep(0.1) + + dask_client.deploy = deploy + dask_client.check_worker_memory = check_worker_memory + dask_client.stop = lambda: dask_client.close() + dask_client.close_all = close_all + return dask_client + + +def start(n: int | None = None, memory_limit: str = "auto") -> Client: + """Start a Dask LocalCluster with specified workers and memory limits. + + Args: + n: Number of workers (defaults to CPU count) + memory_limit: Memory limit per worker (e.g., '4GB', '2GiB', or 'auto' for Dask's default) + """ + + console = Console() + if not n: + n = mp.cpu_count() + with console.status( + f"[green]Initializing dimos local cluster with [bright_blue]{n} workers", spinner="arc" + ): + cluster = LocalCluster( + n_workers=n, + threads_per_worker=4, + memory_limit=memory_limit, + plugins=[CudaCleanupPlugin()], # Register CUDA cleanup plugin + ) + client = Client(cluster) + + console.print( + f"[green]Initialized dimos local cluster with [bright_blue]{n} workers, memory limit: {memory_limit}" + ) + + patched_client = patchdask(client, cluster) + patched_client._shutting_down = False + + # Signal handler with proper exit handling + def signal_handler(sig, frame) -> None: + # If already shutting down, force exit + if patched_client._shutting_down: + import os + + console.print("[red]Force exit!") + os._exit(1) + + patched_client._shutting_down = True + console.print(f"[yellow]Shutting down (signal {sig})...") + + try: + patched_client.close_all() + except Exception: + pass + + import sys + + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + return patched_client diff --git a/dimos/core/blueprints.py b/dimos/core/blueprints.py new file mode 100644 index 0000000000..fedb05769c --- /dev/null +++ b/dimos/core/blueprints.py @@ -0,0 +1,228 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections import defaultdict +from collections.abc import Mapping +from dataclasses import dataclass, field +from functools import cached_property, reduce +import inspect +import operator +from types import MappingProxyType +from typing import Any, Literal, get_args, get_origin + +from dimos.core.global_config import GlobalConfig +from dimos.core.module import Module +from dimos.core.module_coordinator import ModuleCoordinator +from dimos.core.stream import In, Out +from dimos.core.transport import LCMTransport, pLCMTransport +from dimos.utils.generic import short_id + + +@dataclass(frozen=True) +class ModuleConnection: + name: str + type: type + direction: Literal["in", "out"] + + +@dataclass(frozen=True) +class ModuleBlueprint: + module: type[Module] + connections: tuple[ModuleConnection, ...] + args: tuple[Any] + kwargs: dict[str, Any] + + +@dataclass(frozen=True) +class ModuleBlueprintSet: + blueprints: tuple[ModuleBlueprint, ...] + # TODO: Replace Any + transport_map: Mapping[tuple[str, type], Any] = field( + default_factory=lambda: MappingProxyType({}) + ) + global_config_overrides: Mapping[str, Any] = field(default_factory=lambda: MappingProxyType({})) + remapping_map: Mapping[tuple[type[Module], str], str] = field( + default_factory=lambda: MappingProxyType({}) + ) + + def transports(self, transports: dict[tuple[str, type], Any]) -> "ModuleBlueprintSet": + return ModuleBlueprintSet( + blueprints=self.blueprints, + transport_map=MappingProxyType({**self.transport_map, **transports}), + global_config_overrides=self.global_config_overrides, + remapping_map=self.remapping_map, + ) + + def global_config(self, **kwargs: Any) -> "ModuleBlueprintSet": + return ModuleBlueprintSet( + blueprints=self.blueprints, + transport_map=self.transport_map, + global_config_overrides=MappingProxyType({**self.global_config_overrides, **kwargs}), + remapping_map=self.remapping_map, + ) + + def remappings(self, remappings: list[tuple[type[Module], str, str]]) -> "ModuleBlueprintSet": + remappings_dict = dict(self.remapping_map) + for module, old, new in remappings: + remappings_dict[(module, old)] = new + + return ModuleBlueprintSet( + blueprints=self.blueprints, + transport_map=self.transport_map, + global_config_overrides=self.global_config_overrides, + remapping_map=MappingProxyType(remappings_dict), + ) + + def _get_transport_for(self, name: str, type: type) -> Any: + transport = self.transport_map.get((name, type), None) + if transport: + return transport + + use_pickled = getattr(type, "lcm_encode", None) is None + topic = f"/{name}" if self._is_name_unique(name) else f"/{short_id()}" + transport = pLCMTransport(topic) if use_pickled else LCMTransport(topic, type) + + return transport + + @cached_property + def _all_name_types(self) -> set[tuple[str, type]]: + # Apply remappings to get the actual names that will be used + result = set() + for blueprint in self.blueprints: + for conn in blueprint.connections: + # Check if this connection should be remapped + remapped_name = self.remapping_map.get((blueprint.module, conn.name), conn.name) + result.add((remapped_name, conn.type)) + return result + + def _is_name_unique(self, name: str) -> bool: + return sum(1 for n, _ in self._all_name_types if n == name) == 1 + + def build(self, global_config: GlobalConfig | None = None) -> ModuleCoordinator: + if global_config is None: + global_config = GlobalConfig() + global_config = global_config.model_copy(update=self.global_config_overrides) + + module_coordinator = ModuleCoordinator(global_config=global_config) + + module_coordinator.start() + + # Deploy all modules. + for blueprint in self.blueprints: + kwargs = {**blueprint.kwargs} + sig = inspect.signature(blueprint.module.__init__) + if "global_config" in sig.parameters: + kwargs["global_config"] = global_config + module_coordinator.deploy(blueprint.module, *blueprint.args, **kwargs) + + # Gather all the In/Out connections with remapping applied. + connections = defaultdict(list) + # Track original name -> remapped name for each module + module_conn_mapping = defaultdict(dict) + + for blueprint in self.blueprints: + for conn in blueprint.connections: + # Check if this connection should be remapped + remapped_name = self.remapping_map.get((blueprint.module, conn.name), conn.name) + # Store the mapping for later use + module_conn_mapping[blueprint.module][conn.name] = remapped_name + # Group by remapped name and type + connections[remapped_name, conn.type].append((blueprint.module, conn.name)) + + # Connect all In/Out connections by remapped name and type. + for remapped_name, type in connections.keys(): + transport = self._get_transport_for(remapped_name, type) + for module, original_name in connections[(remapped_name, type)]: + instance = module_coordinator.get_instance(module) + # Use the remote method to set transport on Dask actors + instance.set_transport(original_name, transport) + + # Gather all RPC methods. + rpc_methods = {} + for blueprint in self.blueprints: + for method_name in blueprint.module.rpcs.keys(): + method = getattr(module_coordinator.get_instance(blueprint.module), method_name) + rpc_methods[f"{blueprint.module.__name__}_{method_name}"] = method + + # Fulfil method requests (so modules can call each other). + for blueprint in self.blueprints: + for method_name in blueprint.module.rpcs.keys(): + if not method_name.startswith("set_"): + continue + linked_name = method_name.removeprefix("set_") + if linked_name not in rpc_methods: + continue + instance = module_coordinator.get_instance(blueprint.module) + getattr(instance, method_name)(rpc_methods[linked_name]) + + module_coordinator.start_all_modules() + + return module_coordinator + + +def _make_module_blueprint( + module: type[Module], args: tuple[Any], kwargs: dict[str, Any] +) -> ModuleBlueprint: + connections: list[ModuleConnection] = [] + + all_annotations = {} + for base_class in reversed(module.__mro__): + if hasattr(base_class, "__annotations__"): + all_annotations.update(base_class.__annotations__) + + for name, annotation in all_annotations.items(): + origin = get_origin(annotation) + if origin not in (In, Out): + continue + direction = "in" if origin == In else "out" + type_ = get_args(annotation)[0] + connections.append(ModuleConnection(name=name, type=type_, direction=direction)) + + return ModuleBlueprint(module=module, connections=tuple(connections), args=args, kwargs=kwargs) + + +def create_module_blueprint(module: type[Module], *args: Any, **kwargs: Any) -> ModuleBlueprintSet: + blueprint = _make_module_blueprint(module, args, kwargs) + return ModuleBlueprintSet(blueprints=(blueprint,)) + + +def autoconnect(*blueprints: ModuleBlueprintSet) -> ModuleBlueprintSet: + all_blueprints = tuple(_eliminate_duplicates([bp for bs in blueprints for bp in bs.blueprints])) + all_transports = dict( + reduce(operator.iadd, [list(x.transport_map.items()) for x in blueprints], []) + ) + all_config_overrides = dict( + reduce(operator.iadd, [list(x.global_config_overrides.items()) for x in blueprints], []) + ) + all_remappings = dict( + reduce(operator.iadd, [list(x.remapping_map.items()) for x in blueprints], []) + ) + + return ModuleBlueprintSet( + blueprints=all_blueprints, + transport_map=MappingProxyType(all_transports), + global_config_overrides=MappingProxyType(all_config_overrides), + remapping_map=MappingProxyType(all_remappings), + ) + + +def _eliminate_duplicates(blueprints: list[ModuleBlueprint]) -> list[ModuleBlueprint]: + # The duplicates are eliminated in reverse so that newer blueprints override older ones. + seen = set() + unique_blueprints = [] + for bp in reversed(blueprints): + if bp.module not in seen: + seen.add(bp.module) + unique_blueprints.append(bp) + return list(reversed(unique_blueprints)) diff --git a/dimos/core/colors.py b/dimos/core/colors.py new file mode 100644 index 0000000000..f137523e67 --- /dev/null +++ b/dimos/core/colors.py @@ -0,0 +1,43 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def green(text: str) -> str: + """Return the given text in green color.""" + return f"\033[92m{text}\033[0m" + + +def blue(text: str) -> str: + """Return the given text in blue color.""" + return f"\033[94m{text}\033[0m" + + +def red(text: str) -> str: + """Return the given text in red color.""" + return f"\033[91m{text}\033[0m" + + +def yellow(text: str) -> str: + """Return the given text in yellow color.""" + return f"\033[93m{text}\033[0m" + + +def cyan(text: str) -> str: + """Return the given text in cyan color.""" + return f"\033[96m{text}\033[0m" + + +def orange(text: str) -> str: + """Return the given text in orange color.""" + return f"\033[38;5;208m{text}\033[0m" diff --git a/dimos/core/core.py b/dimos/core/core.py new file mode 100644 index 0000000000..57e49e555d --- /dev/null +++ b/dimos/core/core.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import ( + TYPE_CHECKING, + Any, + TypeVar, +) + +from dimos.core.o3dpickle import register_picklers + +if TYPE_CHECKING: + from collections.abc import Callable + +# injects pickling system into o3d +register_picklers() +T = TypeVar("T") + + +def rpc(fn: Callable[..., Any]) -> Callable[..., Any]: + fn.__rpc__ = True # type: ignore[attr-defined] + return fn diff --git a/dimos/core/global_config.py b/dimos/core/global_config.py new file mode 100644 index 0000000000..64da2a01f2 --- /dev/null +++ b/dimos/core/global_config.py @@ -0,0 +1,39 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from functools import cached_property + +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class GlobalConfig(BaseSettings): + robot_ip: str | None = None + use_simulation: bool = False + use_replay: bool = False + n_dask_workers: int = 2 + + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + extra="ignore", + frozen=True, + ) + + @cached_property + def unitree_connection_type(self) -> str: + if self.use_replay: + return "fake" + if self.use_simulation: + return "mujoco" + return "webrtc" diff --git a/dimos/core/module.py b/dimos/core/module.py new file mode 100644 index 0000000000..6ce8480087 --- /dev/null +++ b/dimos/core/module.py @@ -0,0 +1,322 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import asyncio +from collections.abc import Callable +from dataclasses import dataclass +from functools import partial +import inspect +import threading +from typing import ( + Any, + get_args, + get_origin, + get_type_hints, +) + +from dask.distributed import Actor, get_worker +from reactivex.disposable import CompositeDisposable + +from dimos.core import colors +from dimos.core.core import T, rpc +from dimos.core.resource import Resource +from dimos.core.stream import In, Out, RemoteIn, RemoteOut, Transport +from dimos.protocol.rpc import LCMRPC, RPCSpec +from dimos.protocol.service import Configurable +from dimos.protocol.skill.skill import SkillContainer +from dimos.protocol.tf import LCMTF, TFSpec +from dimos.utils.generic import classproperty + + +def get_loop() -> tuple[asyncio.AbstractEventLoop, threading.Thread | None]: + # we are actually instantiating a new loop here + # to not interfere with an existing dask loop + + # try: + # # here we attempt to figure out if we are running on a dask worker + # # if so we use the dask worker _loop as ours, + # # and we register our RPC server + # worker = get_worker() + # if worker.loop: + # print("using dask worker loop") + # return worker.loop.asyncio_loop + + # except ValueError: + # ... + + try: + running_loop = asyncio.get_running_loop() + return running_loop, None + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + thr = threading.Thread(target=loop.run_forever, daemon=True) + thr.start() + return loop, thr + + +@dataclass +class ModuleConfig: + rpc_transport: type[RPCSpec] = LCMRPC + tf_transport: type[TFSpec] = LCMTF + + +class ModuleBase(Configurable[ModuleConfig], SkillContainer, Resource): + _rpc: RPCSpec | None = None + _tf: TFSpec | None = None + _loop: asyncio.AbstractEventLoop | None = None + _loop_thread: threading.Thread | None + _disposables: CompositeDisposable + + default_config = ModuleConfig + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._loop, self._loop_thread = get_loop() + self._disposables = CompositeDisposable() + # we can completely override comms protocols if we want + try: + # here we attempt to figure out if we are running on a dask worker + # if so we use the dask worker _loop as ours, + # and we register our RPC server + self.rpc = self.config.rpc_transport() + self.rpc.serve_module_rpc(self) + self.rpc.start() + except ValueError: + ... + + @rpc + def start(self) -> None: + pass + + @rpc + def stop(self) -> None: + self._close_module() + super().stop() + + def _close_module(self) -> None: + self._close_rpc() + if hasattr(self, "_loop") and self._loop_thread: + if self._loop_thread.is_alive(): + self._loop.call_soon_threadsafe(self._loop.stop) + self._loop_thread.join(timeout=2) + self._loop = None + self._loop_thread = None + if hasattr(self, "_tf") and self._tf is not None: + self._tf.stop() + self._tf = None + if hasattr(self, "_disposables"): + self._disposables.dispose() + + def _close_rpc(self) -> None: + # Using hasattr is needed because SkillCoordinator skips ModuleBase.__init__ and self.rpc is never set. + if hasattr(self, "rpc") and self.rpc: + self.rpc.stop() + self.rpc = None + + def __getstate__(self): + """Exclude unpicklable runtime attributes when serializing.""" + state = self.__dict__.copy() + # Remove unpicklable attributes + state.pop("_disposables", None) + state.pop("_loop", None) + state.pop("_loop_thread", None) + state.pop("_rpc", None) + state.pop("_tf", None) + return state + + def __setstate__(self, state) -> None: + """Restore object from pickled state.""" + self.__dict__.update(state) + # Reinitialize runtime attributes + self._disposables = CompositeDisposable() + self._loop = None + self._loop_thread = None + self._rpc = None + self._tf = None + + @property + def tf(self): + if self._tf is None: + # self._tf = self.config.tf_transport() + self._tf = LCMTF() + return self._tf + + @tf.setter + def tf(self, value) -> None: + import warnings + + warnings.warn( + "tf is available on all modules. Call self.tf.start() to activate tf functionality. No need to assign it", + UserWarning, + stacklevel=2, + ) + + @property + def outputs(self) -> dict[str, Out]: + return { + name: s + for name, s in self.__dict__.items() + if isinstance(s, Out) and not name.startswith("_") + } + + @property + def inputs(self) -> dict[str, In]: + return { + name: s + for name, s in self.__dict__.items() + if isinstance(s, In) and not name.startswith("_") + } + + @classmethod + @property + def rpcs(cls) -> dict[str, Callable]: + return { + name: getattr(cls, name) + for name in dir(cls) + if not name.startswith("_") + and name != "rpcs" # Exclude the rpcs property itself to prevent recursion + and callable(getattr(cls, name, None)) + and hasattr(getattr(cls, name), "__rpc__") + } + + @rpc + def io(self) -> str: + def _box(name: str) -> str: + return [ + "┌┴" + "─" * (len(name) + 1) + "┐", + f"│ {name} │", + "└┬" + "─" * (len(name) + 1) + "┘", + ] + + # can't modify __str__ on a function like we are doing for I/O + # so we have a separate repr function here + def repr_rpc(fn: Callable) -> str: + sig = inspect.signature(fn) + # Remove 'self' parameter + params = [p for name, p in sig.parameters.items() if name != "self"] + + # Format parameters with colored types + param_strs = [] + for param in params: + param_str = param.name + if param.annotation != inspect.Parameter.empty: + type_name = getattr(param.annotation, "__name__", str(param.annotation)) + param_str += ": " + colors.green(type_name) + if param.default != inspect.Parameter.empty: + param_str += f" = {param.default}" + param_strs.append(param_str) + + # Format return type + return_annotation = "" + if sig.return_annotation != inspect.Signature.empty: + return_type = getattr(sig.return_annotation, "__name__", str(sig.return_annotation)) + return_annotation = " -> " + colors.green(return_type) + + return ( + "RPC " + colors.blue(fn.__name__) + f"({', '.join(param_strs)})" + return_annotation + ) + + ret = [ + *(f" ├─ {stream}" for stream in self.inputs.values()), + *_box(self.__class__.__name__), + *(f" ├─ {stream}" for stream in self.outputs.values()), + " │", + *(f" ├─ {repr_rpc(rpc)}" for rpc in self.rpcs.values()), + ] + + return "\n".join(ret) + + @classproperty + def blueprint(self): + # Here to prevent circular imports. + from dimos.core.blueprints import create_module_blueprint + + return partial(create_module_blueprint, self) + + +class DaskModule(ModuleBase): + ref: Actor + worker: int + + def __init__(self, *args, **kwargs) -> None: + self.ref = None + + # Get type hints with proper namespace resolution for subclasses + # Collect namespaces from all classes in the MRO chain + import sys + + globalns = {} + for cls in self.__class__.__mro__: + if cls.__module__ in sys.modules: + globalns.update(sys.modules[cls.__module__].__dict__) + + try: + hints = get_type_hints(self.__class__, globalns=globalns, include_extras=True) + except (NameError, AttributeError, TypeError): + # If we still can't resolve hints, skip type hint processing + # This can happen with complex forward references + hints = {} + + for name, ann in hints.items(): + origin = get_origin(ann) + if origin is Out: + inner, *_ = get_args(ann) or (Any,) + stream = Out(inner, name, self) + setattr(self, name, stream) + elif origin is In: + inner, *_ = get_args(ann) or (Any,) + stream = In(inner, name, self) + setattr(self, name, stream) + super().__init__(*args, **kwargs) + + def set_ref(self, ref) -> int: + worker = get_worker() + self.ref = ref + self.worker = worker.name + return worker.name + + def __str__(self) -> str: + return f"{self.__class__.__name__}" + + # called from remote + def set_transport(self, stream_name: str, transport: Transport) -> bool: + stream = getattr(self, stream_name, None) + if not stream: + raise ValueError(f"{stream_name} not found in {self.__class__.__name__}") + + if not isinstance(stream, Out) and not isinstance(stream, In): + raise TypeError(f"Output {stream_name} is not a valid stream") + + stream._transport = transport + return True + + # called from remote + def connect_stream(self, input_name: str, remote_stream: RemoteOut[T]): + input_stream = getattr(self, input_name, None) + if not input_stream: + raise ValueError(f"{input_name} not found in {self.__class__.__name__}") + if not isinstance(input_stream, In): + raise TypeError(f"Input {input_name} is not a valid stream") + input_stream.connection = remote_stream + + def dask_receive_msg(self, input_name: str, msg: Any) -> None: + getattr(self, input_name).transport.dask_receive_msg(msg) + + def dask_register_subscriber(self, output_name: str, subscriber: RemoteIn[T]) -> None: + getattr(self, output_name).transport.dask_register_subscriber(subscriber) + + +# global setting +Module = DaskModule diff --git a/dimos/core/module_coordinator.py b/dimos/core/module_coordinator.py new file mode 100644 index 0000000000..a740bef494 --- /dev/null +++ b/dimos/core/module_coordinator.py @@ -0,0 +1,73 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +from typing import TypeVar + +from dimos import core +from dimos.core import DimosCluster, Module +from dimos.core.global_config import GlobalConfig +from dimos.core.resource import Resource + +T = TypeVar("T", bound="Module") + + +class ModuleCoordinator(Resource): + _client: DimosCluster | None = None + _n: int | None = None + _memory_limit: str = "auto" + _deployed_modules: dict[type[Module], Module] = {} + + def __init__( + self, + n: int | None = None, + memory_limit: str = "auto", + global_config: GlobalConfig | None = None, + ) -> None: + cfg = global_config or GlobalConfig() + self._n = n if n is not None else cfg.n_dask_workers + self._memory_limit = memory_limit + + def start(self) -> None: + self._client = core.start(self._n, self._memory_limit) + + def stop(self) -> None: + for module in reversed(self._deployed_modules.values()): + module.stop() + + self._client.close_all() + + def deploy(self, module_class: type[T], *args, **kwargs) -> T: + if not self._client: + raise ValueError("Not started") + + module = self._client.deploy(module_class, *args, **kwargs) + self._deployed_modules[module_class] = module + return module + + def start_all_modules(self) -> None: + for module in self._deployed_modules.values(): + module.start() + + def get_instance(self, module: type[T]) -> T | None: + return self._deployed_modules.get(module) + + def loop(self) -> None: + try: + while True: + time.sleep(0.1) + except KeyboardInterrupt: + pass + finally: + self.stop() diff --git a/dimos/core/o3dpickle.py b/dimos/core/o3dpickle.py new file mode 100644 index 0000000000..8e0f13dbf0 --- /dev/null +++ b/dimos/core/o3dpickle.py @@ -0,0 +1,38 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import copyreg + +import numpy as np +import open3d as o3d + + +def reduce_external(obj): + # Convert Vector3dVector to numpy array for pickling + points_array = np.asarray(obj.points) + return (reconstruct_pointcloud, (points_array,)) + + +def reconstruct_pointcloud(points_array): + # Create new PointCloud and assign the points + pc = o3d.geometry.PointCloud() + pc.points = o3d.utility.Vector3dVector(points_array) + return pc + + +def register_picklers() -> None: + # Register for the actual PointCloud class that gets instantiated + # We need to create a dummy PointCloud to get its actual class + _dummy_pc = o3d.geometry.PointCloud() + copyreg.pickle(_dummy_pc.__class__, reduce_external) diff --git a/dimos/core/resource.py b/dimos/core/resource.py new file mode 100644 index 0000000000..3d69f50bb4 --- /dev/null +++ b/dimos/core/resource.py @@ -0,0 +1,23 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC, abstractmethod + + +class Resource(ABC): + @abstractmethod + def start(self) -> None: ... + + @abstractmethod + def stop(self) -> None: ... diff --git a/dimos/core/rpc_client.py b/dimos/core/rpc_client.py new file mode 100644 index 0000000000..bfcec5bb71 --- /dev/null +++ b/dimos/core/rpc_client.py @@ -0,0 +1,141 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Callable +from typing import Any + +from dimos.protocol.rpc.lcmrpc import LCMRPC +from dimos.utils.logging_config import setup_logger + +logger = setup_logger(__file__) + + +class RpcCall: + _original_method: Callable[..., Any] | None + _rpc: LCMRPC | None + _name: str + _remote_name: str + _unsub_fns: list + _stop_rpc_client: Callable[[], None] | None = None + + def __init__( + self, + original_method: Callable[..., Any] | None, + rpc: LCMRPC, + name: str, + remote_name: str, + unsub_fns: list, + stop_client: Callable[[], None] | None = None, + ) -> None: + self._original_method = original_method + self._rpc = rpc + self._name = name + self._remote_name = remote_name + self._unsub_fns = unsub_fns + self._stop_rpc_client = stop_client + + if original_method: + self.__doc__ = original_method.__doc__ + self.__name__ = original_method.__name__ + self.__qualname__ = f"{self.__class__.__name__}.{original_method.__name__}" + + def set_rpc(self, rpc: LCMRPC) -> None: + self._rpc = rpc + + def __call__(self, *args, **kwargs): + if not self._rpc: + logger.warning("RPC client not initialized") + return None + + # For stop, use call_nowait to avoid deadlock + # (the remote side stops its RPC service before responding) + if self._name == "stop": + self._rpc.call_nowait(f"{self._remote_name}/{self._name}", (args, kwargs)) + if self._stop_rpc_client: + self._stop_rpc_client() + return None + + result, unsub_fn = self._rpc.call_sync(f"{self._remote_name}/{self._name}", (args, kwargs)) + self._unsub_fns.append(unsub_fn) + return result + + def __getstate__(self): + return (self._original_method, self._name, self._remote_name) + + def __setstate__(self, state) -> None: + self._original_method, self._name, self._remote_name = state + self._unsub_fns = [] + self._rpc = None + self._stop_rpc_client = None + + +class RPCClient: + def __init__(self, actor_instance, actor_class) -> None: + self.rpc = LCMRPC() + self.actor_class = actor_class + self.remote_name = actor_class.__name__ + self.actor_instance = actor_instance + self.rpcs = actor_class.rpcs.keys() + self.rpc.start() + self._unsub_fns = [] + + def stop_rpc_client(self) -> None: + for unsub in self._unsub_fns: + try: + unsub() + except Exception: + pass + + self._unsub_fns = [] + + if self.rpc: + self.rpc.stop() + self.rpc = None + + def __reduce__(self): + # Return the class and the arguments needed to reconstruct the object + return ( + self.__class__, + (self.actor_instance, self.actor_class), + ) + + # passthrough + def __getattr__(self, name: str): + # Check if accessing a known safe attribute to avoid recursion + if name in { + "__class__", + "__init__", + "__dict__", + "__getattr__", + "rpcs", + "remote_name", + "remote_instance", + "actor_instance", + }: + raise AttributeError(f"{name} is not found.") + + if name in self.rpcs: + original_method = getattr(self.actor_class, name, None) + return RpcCall( + original_method, + self.rpc, + name, + self.remote_name, + self._unsub_fns, + self.stop_rpc_client, + ) + + # return super().__getattr__(name) + # Try to avoid recursion by directly accessing attributes that are known + return self.actor_instance.__getattr__(name) diff --git a/dimos/core/skill_module.py b/dimos/core/skill_module.py new file mode 100644 index 0000000000..4c6a42fa5b --- /dev/null +++ b/dimos/core/skill_module.py @@ -0,0 +1,32 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.core.module import Module +from dimos.core.rpc_client import RpcCall, RPCClient +from dimos.protocol.skill.skill import rpc + + +class SkillModule(Module): + """Use this module if you want to auto-register skills to an LlmAgent.""" + + @rpc + def set_LlmAgent_register_skills(self, callable: RpcCall) -> None: + callable.set_rpc(self.rpc) + callable(RPCClient(self, self.__class__)) + + def __getstate__(self) -> None: + pass + + def __setstate__(self, _state) -> None: + pass diff --git a/dimos/core/stream.py b/dimos/core/stream.py new file mode 100644 index 0000000000..1868ed6dbd --- /dev/null +++ b/dimos/core/stream.py @@ -0,0 +1,241 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import enum +from typing import ( + TYPE_CHECKING, + Any, + Generic, + TypeVar, +) + +from dask.distributed import Actor +import reactivex as rx +from reactivex import operators as ops +from reactivex.disposable import Disposable + +import dimos.core.colors as colors +import dimos.utils.reactive as reactive +from dimos.utils.reactive import backpressure + +if TYPE_CHECKING: + from collections.abc import Callable + +T = TypeVar("T") + + +class ObservableMixin(Generic[T]): + # subscribes and returns the first value it receives + # might be nicer to write without rxpy but had this snippet ready + def get_next(self, timeout: float = 10.0) -> T: + try: + return ( + self.observable() + .pipe(ops.first(), *([ops.timeout(timeout)] if timeout is not None else [])) + .run() + ) + except Exception as e: + raise Exception(f"No value received after {timeout} seconds") from e + + def hot_latest(self) -> Callable[[], T]: + return reactive.getter_streaming(self.observable()) + + def pure_observable(self): + def _subscribe(observer, scheduler=None): + unsubscribe = self.subscribe(observer.on_next) + return Disposable(unsubscribe) + + return rx.create(_subscribe) + + # default return is backpressured because most + # use cases will want this by default + def observable(self): + return backpressure(self.pure_observable()) + + +class State(enum.Enum): + UNBOUND = "unbound" # descriptor defined but not bound + READY = "ready" # bound to owner but not yet connected + CONNECTED = "connected" # input bound to an output + FLOWING = "flowing" # runtime: data observed + + +class Transport(ObservableMixin[T]): + # used by local Output + def broadcast(self, selfstream: Out[T], value: T) -> None: ... + + def publish(self, msg: T) -> None: + self.broadcast(None, msg) + + # used by local Input + def subscribe(self, selfstream: In[T], callback: Callable[[T], any]) -> None: ... + + +class Stream(Generic[T]): + _transport: Transport | None + + def __init__( + self, + type: type[T], + name: str, + owner: Any | None = None, + transport: Transport | None = None, + ) -> None: + self.name = name + self.owner = owner + self.type = type + if transport: + self._transport = transport + if not hasattr(self, "_transport"): + self._transport = None + + @property + def type_name(self) -> str: + return getattr(self.type, "__name__", repr(self.type)) + + def _color_fn(self) -> Callable[[str], str]: + if self.state == State.UNBOUND: + return colors.orange + if self.state == State.READY: + return colors.blue + if self.state == State.CONNECTED: + return colors.green + return lambda s: s + + def __str__(self) -> str: + return ( + self.__class__.__name__ + + " " + + self._color_fn()(f"{self.name}[{self.type_name}]") + + " @ " + + ( + colors.orange(self.owner) + if isinstance(self.owner, Actor) + else colors.green(self.owner) + ) + + ("" if not self._transport else " via " + str(self._transport)) + ) + + +class Out(Stream[T]): + _transport: Transport + + def __init__(self, *argv, **kwargs) -> None: + super().__init__(*argv, **kwargs) + + @property + def transport(self) -> Transport[T]: + return self._transport + + @property + def state(self) -> State: + return State.UNBOUND if self.owner is None else State.READY + + def __reduce__(self): + if self.owner is None or not hasattr(self.owner, "ref"): + raise ValueError("Cannot serialise Out without an owner ref") + return ( + RemoteOut, + ( + self.type, + self.name, + self.owner.ref, + self._transport, + ), + ) + + def publish(self, msg): + if not hasattr(self, "_transport") or self._transport is None: + raise Exception(f"{self} transport for stream is not specified,") + self._transport.broadcast(self, msg) + + +class RemoteStream(Stream[T]): + @property + def state(self) -> State: + return State.UNBOUND if self.owner is None else State.READY + + # this won't work but nvm + @property + def transport(self) -> Transport[T]: + return self._transport + + @transport.setter + def transport(self, value: Transport[T]) -> None: + self.owner.set_transport(self.name, value).result() + self._transport = value + + +class RemoteOut(RemoteStream[T]): + def connect(self, other: RemoteIn[T]): + return other.connect(self) + + def subscribe(self, cb) -> Callable[[], None]: + return self.transport.subscribe(cb, self) + + +# representation of Input +# as views from inside of the module +class In(Stream[T], ObservableMixin[T]): + connection: RemoteOut[T] | None = None + _transport: Transport + + def __str__(self) -> str: + mystr = super().__str__() + + if not self.connection: + return mystr + + return (mystr + " ◀─").ljust(60, "─") + f" {self.connection}" + + def __reduce__(self): + if self.owner is None or not hasattr(self.owner, "ref"): + raise ValueError("Cannot serialise Out without an owner ref") + return (RemoteIn, (self.type, self.name, self.owner.ref, self._transport)) + + @property + def transport(self) -> Transport[T]: + if not self._transport: + self._transport = self.connection.transport + return self._transport + + @property + def state(self) -> State: + return State.UNBOUND if self.owner is None else State.READY + + # returns unsubscribe function + def subscribe(self, cb) -> Callable[[], None]: + return self.transport.subscribe(cb, self) + + +# representation of input outside of module +# used for configuring connections, setting a transport +class RemoteIn(RemoteStream[T]): + def connect(self, other: RemoteOut[T]) -> None: + return self.owner.connect_stream(self.name, other).result() + + # this won't work but that's ok + @property + def transport(self) -> Transport[T]: + return self._transport + + def publish(self, msg) -> None: + self.transport.broadcast(self, msg) + + @transport.setter + def transport(self, value: Transport[T]) -> None: + self.owner.set_transport(self.name, value).result() + self._transport = value diff --git a/dimos/core/test_blueprints.py b/dimos/core/test_blueprints.py new file mode 100644 index 0000000000..59f541aa58 --- /dev/null +++ b/dimos/core/test_blueprints.py @@ -0,0 +1,242 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.core.blueprints import ( + ModuleBlueprint, + ModuleBlueprintSet, + ModuleConnection, + _make_module_blueprint, + autoconnect, +) +from dimos.core.core import rpc +from dimos.core.global_config import GlobalConfig +from dimos.core.module import Module +from dimos.core.module_coordinator import ModuleCoordinator +from dimos.core.rpc_client import RpcCall +from dimos.core.stream import In, Out +from dimos.core.transport import LCMTransport +from dimos.protocol import pubsub + + +class Scratch: + pass + + +class Petting: + pass + + +class CatModule(Module): + pet_cat: In[Petting] + scratches: Out[Scratch] + + +class Data1: + pass + + +class Data2: + pass + + +class Data3: + pass + + +class ModuleA(Module): + data1: Out[Data1] = None + data2: Out[Data2] = None + + @rpc + def get_name(self) -> str: + return "A, Module A" + + +class ModuleB(Module): + data1: In[Data1] = None + data2: In[Data2] = None + data3: Out[Data3] = None + + _module_a_get_name: callable = None + + @rpc + def set_ModuleA_get_name(self, callable: RpcCall) -> None: + self._module_a_get_name = callable + self._module_a_get_name.set_rpc(self.rpc) + + @rpc + def what_is_as_name(self) -> str: + if self._module_a_get_name is None: + return "ModuleA.get_name not set" + return self._module_a_get_name() + + +class ModuleC(Module): + data3: In[Data3] = None + + +module_a = ModuleA.blueprint +module_b = ModuleB.blueprint +module_c = ModuleC.blueprint + + +def test_get_connection_set() -> None: + assert _make_module_blueprint(CatModule, args=("arg1"), kwargs={"k": "v"}) == ModuleBlueprint( + module=CatModule, + connections=( + ModuleConnection(name="pet_cat", type=Petting, direction="in"), + ModuleConnection(name="scratches", type=Scratch, direction="out"), + ), + args=("arg1"), + kwargs={"k": "v"}, + ) + + +def test_autoconnect() -> None: + blueprint_set = autoconnect(module_a(), module_b()) + + assert blueprint_set == ModuleBlueprintSet( + blueprints=( + ModuleBlueprint( + module=ModuleA, + connections=( + ModuleConnection(name="data1", type=Data1, direction="out"), + ModuleConnection(name="data2", type=Data2, direction="out"), + ), + args=(), + kwargs={}, + ), + ModuleBlueprint( + module=ModuleB, + connections=( + ModuleConnection(name="data1", type=Data1, direction="in"), + ModuleConnection(name="data2", type=Data2, direction="in"), + ModuleConnection(name="data3", type=Data3, direction="out"), + ), + args=(), + kwargs={}, + ), + ) + ) + + +def test_transports() -> None: + custom_transport = LCMTransport("/custom_topic", Data1) + blueprint_set = autoconnect(module_a(), module_b()).transports( + {("data1", Data1): custom_transport} + ) + + assert ("data1", Data1) in blueprint_set.transport_map + assert blueprint_set.transport_map[("data1", Data1)] == custom_transport + + +def test_global_config() -> None: + blueprint_set = autoconnect(module_a(), module_b()).global_config(option1=True, option2=42) + + assert "option1" in blueprint_set.global_config_overrides + assert blueprint_set.global_config_overrides["option1"] is True + assert "option2" in blueprint_set.global_config_overrides + assert blueprint_set.global_config_overrides["option2"] == 42 + + +def test_build_happy_path() -> None: + pubsub.lcm.autoconf() + + blueprint_set = autoconnect(module_a(), module_b(), module_c()) + + coordinator = blueprint_set.build(GlobalConfig()) + + try: + assert isinstance(coordinator, ModuleCoordinator) + + module_a_instance = coordinator.get_instance(ModuleA) + module_b_instance = coordinator.get_instance(ModuleB) + module_c_instance = coordinator.get_instance(ModuleC) + + assert module_a_instance is not None + assert module_b_instance is not None + assert module_c_instance is not None + + assert module_a_instance.data1.transport is not None + assert module_a_instance.data2.transport is not None + assert module_b_instance.data1.transport is not None + assert module_b_instance.data2.transport is not None + assert module_b_instance.data3.transport is not None + assert module_c_instance.data3.transport is not None + + assert module_a_instance.data1.transport.topic == module_b_instance.data1.transport.topic + assert module_a_instance.data2.transport.topic == module_b_instance.data2.transport.topic + assert module_b_instance.data3.transport.topic == module_c_instance.data3.transport.topic + + assert module_b_instance.what_is_as_name() == "A, Module A" + + finally: + coordinator.stop() + + +def test_remapping(): + """Test that remapping connections works correctly.""" + pubsub.lcm.autoconf() + + # Define test modules with connections that will be remapped + class SourceModule(Module): + color_image: Out[Data1] = None # Will be remapped to 'remapped_data' + + class TargetModule(Module): + remapped_data: In[Data1] = None # Receives the remapped connection + + # Create blueprint with remapping + blueprint_set = autoconnect( + SourceModule.blueprint(), + TargetModule.blueprint(), + ).remappings( + [ + (SourceModule, "color_image", "remapped_data"), + ] + ) + + # Verify remappings are stored correctly + assert (SourceModule, "color_image") in blueprint_set.remapping_map + assert blueprint_set.remapping_map[(SourceModule, "color_image")] == "remapped_data" + + # Verify that remapped names are used in name resolution + assert ("remapped_data", Data1) in blueprint_set._all_name_types + # The original name shouldn't be in the name types since it's remapped + assert ("color_image", Data1) not in blueprint_set._all_name_types + + # Build and verify connections work + coordinator = blueprint_set.build(GlobalConfig()) + + try: + source_instance = coordinator.get_instance(SourceModule) + target_instance = coordinator.get_instance(TargetModule) + + assert source_instance is not None + assert target_instance is not None + + # Both should have transports set + assert source_instance.color_image.transport is not None + assert target_instance.remapped_data.transport is not None + + # They should be using the same transport (connected) + assert ( + source_instance.color_image.transport.topic + == target_instance.remapped_data.transport.topic + ) + + # The topic should be /remapped_data since that's the remapped name + assert target_instance.remapped_data.transport.topic == "/remapped_data" + + finally: + coordinator.stop() diff --git a/dimos/core/test_core.py b/dimos/core/test_core.py new file mode 100644 index 0000000000..97f09a4182 --- /dev/null +++ b/dimos/core/test_core.py @@ -0,0 +1,145 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +import pytest +from reactivex.disposable import Disposable + +from dimos.core import ( + In, + LCMTransport, + Module, + Out, + pLCMTransport, + rpc, + start, +) +from dimos.core.testing import MockRobotClient, dimos +from dimos.msgs.geometry_msgs import Vector3 +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.robot.unitree_webrtc.type.odometry import Odometry + +assert dimos + + +class Navigation(Module): + mov: Out[Vector3] = None + lidar: In[LidarMessage] = None + target_position: In[Vector3] = None + odometry: In[Odometry] = None + + odom_msg_count = 0 + lidar_msg_count = 0 + + @rpc + def navigate_to(self, target: Vector3) -> bool: ... + + def __init__(self) -> None: + super().__init__() + + @rpc + def start(self) -> None: + def _odom(msg) -> None: + self.odom_msg_count += 1 + print("RCV:", (time.perf_counter() - msg.pubtime) * 1000, msg) + self.mov.publish(msg.position) + + unsub = self.odometry.subscribe(_odom) + self._disposables.add(Disposable(unsub)) + + def _lidar(msg) -> None: + self.lidar_msg_count += 1 + if hasattr(msg, "pubtime"): + print("RCV:", (time.perf_counter() - msg.pubtime) * 1000, msg) + else: + print("RCV: unknown time", msg) + + unsub = self.lidar.subscribe(_lidar) + self._disposables.add(Disposable(unsub)) + + +def test_classmethods() -> None: + # Test class property access + class_rpcs = Navigation.rpcs + print("Class rpcs:", class_rpcs) + # Test instance property access + nav = Navigation() + instance_rpcs = nav.rpcs + print("Instance rpcs:", instance_rpcs) + + # Assertions + assert isinstance(class_rpcs, dict), "Class rpcs should be a dictionary" + assert isinstance(instance_rpcs, dict), "Instance rpcs should be a dictionary" + assert class_rpcs == instance_rpcs, "Class and instance rpcs should be identical" + + # Check that we have the expected RPC methods + assert "navigate_to" in class_rpcs, "navigate_to should be in rpcs" + assert "start" in class_rpcs, "start should be in rpcs" + assert len(class_rpcs) == 6 + + # Check that the values are callable + assert callable(class_rpcs["navigate_to"]), "navigate_to should be callable" + assert callable(class_rpcs["start"]), "start should be callable" + + # Check that they have the __rpc__ attribute + assert hasattr(class_rpcs["navigate_to"], "__rpc__"), ( + "navigate_to should have __rpc__ attribute" + ) + assert hasattr(class_rpcs["start"], "__rpc__"), "start should have __rpc__ attribute" + + nav._close_module() + + +@pytest.mark.module +def test_basic_deployment(dimos) -> None: + robot = dimos.deploy(MockRobotClient) + + print("\n") + print("lidar stream", robot.lidar) + print("odom stream", robot.odometry) + + nav = dimos.deploy(Navigation) + + # this one encodes proper LCM messages + robot.lidar.transport = LCMTransport("/lidar", LidarMessage) + + # odometry & mov using just a pickle over LCM + robot.odometry.transport = pLCMTransport("/odom") + nav.mov.transport = pLCMTransport("/mov") + + nav.lidar.connect(robot.lidar) + nav.odometry.connect(robot.odometry) + robot.mov.connect(nav.mov) + + robot.start() + nav.start() + + time.sleep(1) + robot.stop() + + print("robot.mov_msg_count", robot.mov_msg_count) + print("nav.odom_msg_count", nav.odom_msg_count) + print("nav.lidar_msg_count", nav.lidar_msg_count) + + assert robot.mov_msg_count >= 8 + assert nav.odom_msg_count >= 8 + assert nav.lidar_msg_count >= 8 + + dimos.shutdown() + + +if __name__ == "__main__": + client = start(1) # single process for CI memory + test_deployment(client) diff --git a/dimos/core/test_modules.py b/dimos/core/test_modules.py new file mode 100644 index 0000000000..d1f925aff2 --- /dev/null +++ b/dimos/core/test_modules.py @@ -0,0 +1,334 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test that all Module subclasses implement required resource management methods.""" + +import ast +import inspect +from pathlib import Path + +import pytest + +from dimos.core.module import Module + + +class ModuleVisitor(ast.NodeVisitor): + """AST visitor to find classes and their base classes.""" + + def __init__(self, filepath: str) -> None: + self.filepath = filepath + self.classes: list[ + tuple[str, list[str], set[str]] + ] = [] # (class_name, base_classes, methods) + + def visit_ClassDef(self, node: ast.ClassDef) -> None: + """Visit a class definition.""" + # Get base class names + base_classes = [] + for base in node.bases: + if isinstance(base, ast.Name): + base_classes.append(base.id) + elif isinstance(base, ast.Attribute): + # Handle cases like dimos.core.Module + parts = [] + current = base + while isinstance(current, ast.Attribute): + parts.append(current.attr) + current = current.value + if isinstance(current, ast.Name): + parts.append(current.id) + base_classes.append(".".join(reversed(parts))) + + # Get method names defined in this class + methods = set() + for item in node.body: + if isinstance(item, ast.FunctionDef): + methods.add(item.name) + + self.classes.append((node.name, base_classes, methods)) + self.generic_visit(node) + + +def get_import_aliases(tree: ast.AST) -> dict[str, str]: + """Extract import aliases from the AST.""" + aliases = {} + + for node in ast.walk(tree): + if isinstance(node, ast.Import): + for alias in node.names: + key = alias.asname if alias.asname else alias.name + aliases[key] = alias.name + elif isinstance(node, ast.ImportFrom): + module = node.module or "" + for alias in node.names: + key = alias.asname if alias.asname else alias.name + full_name = f"{module}.{alias.name}" if module else alias.name + aliases[key] = full_name + + return aliases + + +def is_module_subclass( + base_classes: list[str], + aliases: dict[str, str], + class_hierarchy: dict[str, list[str]] | None = None, + current_module_path: str | None = None, +) -> bool: + """Check if any base class is or resolves to dimos.core.Module or its variants (recursively).""" + target_classes = { + "Module", + "ModuleBase", + "DaskModule", + "dimos.core.Module", + "dimos.core.ModuleBase", + "dimos.core.DaskModule", + "dimos.core.module.Module", + "dimos.core.module.ModuleBase", + "dimos.core.module.DaskModule", + } + + def find_qualified_name(base: str, context_module: str | None = None) -> str: + """Find the qualified name for a base class, using import context if available.""" + if not class_hierarchy: + return base + + # First try exact match (already fully qualified or in hierarchy) + if base in class_hierarchy: + return base + + # Check if it's in our aliases (from imports) + if base in aliases: + resolved = aliases[base] + if resolved in class_hierarchy: + return resolved + # The resolved name might be a qualified name that exists + return resolved + + # If we have a context module and base is a simple name, + # try to find it in the same module first (for local classes) + if context_module and "." not in base: + same_module_qualified = f"{context_module}.{base}" + if same_module_qualified in class_hierarchy: + return same_module_qualified + + # Otherwise return the base as-is + return base + + def check_base( + base: str, visited: set[str] | None = None, context_module: str | None = None + ) -> bool: + if visited is None: + visited = set() + + # Avoid infinite recursion + if base in visited: + return False + visited.add(base) + + # Check direct match + if base in target_classes: + return True + + # Check if it's an alias + if base in aliases: + resolved = aliases[base] + if resolved in target_classes: + return True + # Continue checking with resolved name + base = resolved + + # If we have a class hierarchy, recursively check parent classes + if class_hierarchy: + # Resolve the base class name to a qualified name + qualified_name = find_qualified_name(base, context_module) + + if qualified_name in class_hierarchy: + # Check all parent classes + for parent_base in class_hierarchy[qualified_name]: + if check_base(parent_base, visited, None): # Parent lookups don't use context + return True + + return False + + for base in base_classes: + if check_base(base, context_module=current_module_path): + return True + + return False + + +def scan_file( + filepath: Path, + class_hierarchy: dict[str, list[str]] | None = None, + root_path: Path | None = None, +) -> list[tuple[str, str, bool, bool, set[str]]]: + """ + Scan a Python file for Module subclasses. + + Returns: + List of (class_name, filepath, has_start, has_stop, forbidden_methods) + """ + forbidden_method_names = {"acquire", "release", "open", "close", "shutdown", "clean", "cleanup"} + + try: + with open(filepath, encoding="utf-8") as f: + content = f.read() + + tree = ast.parse(content, filename=str(filepath)) + aliases = get_import_aliases(tree) + + visitor = ModuleVisitor(str(filepath)) + visitor.visit(tree) + + # Get module path for this file to properly resolve base classes + current_module_path = None + if root_path: + try: + rel_path = filepath.relative_to(root_path.parent) + module_parts = list(rel_path.parts[:-1]) + if rel_path.stem != "__init__": + module_parts.append(rel_path.stem) + current_module_path = ".".join(module_parts) + except ValueError: + pass + + results = [] + for class_name, base_classes, methods in visitor.classes: + if is_module_subclass(base_classes, aliases, class_hierarchy, current_module_path): + has_start = "start" in methods + has_stop = "stop" in methods + forbidden_found = methods & forbidden_method_names + results.append((class_name, str(filepath), has_start, has_stop, forbidden_found)) + + return results + + except (SyntaxError, UnicodeDecodeError): + # Skip files that can't be parsed + return [] + + +def build_class_hierarchy(root_path: Path) -> dict[str, list[str]]: + """Build a complete class hierarchy by scanning all Python files.""" + hierarchy = {} + + for filepath in sorted(root_path.rglob("*.py")): + # Skip __pycache__ and other irrelevant directories + if "__pycache__" in filepath.parts or ".venv" in filepath.parts: + continue + + try: + with open(filepath, encoding="utf-8") as f: + content = f.read() + + tree = ast.parse(content, filename=str(filepath)) + visitor = ModuleVisitor(str(filepath)) + visitor.visit(tree) + + # Convert filepath to module path (e.g., dimos/core/module.py -> dimos.core.module) + try: + rel_path = filepath.relative_to(root_path.parent) + except ValueError: + # If we can't get relative path, skip this file + continue + + # Convert path to module notation + module_parts = list(rel_path.parts[:-1]) # Exclude filename + if rel_path.stem != "__init__": + module_parts.append(rel_path.stem) # Add filename without .py + module_name = ".".join(module_parts) + + for class_name, base_classes, _ in visitor.classes: + # Use fully qualified name as key to avoid conflicts + qualified_name = f"{module_name}.{class_name}" if module_name else class_name + hierarchy[qualified_name] = base_classes + + except (SyntaxError, UnicodeDecodeError): + # Skip files that can't be parsed + continue + + return hierarchy + + +def scan_directory(root_path: Path) -> list[tuple[str, str, bool, bool, set[str]]]: + """Scan all Python files in the directory tree.""" + # First, build the complete class hierarchy + class_hierarchy = build_class_hierarchy(root_path) + + # Then scan for Module subclasses using the complete hierarchy + results = [] + + for filepath in sorted(root_path.rglob("*.py")): + # Skip __pycache__ and other irrelevant directories + if "__pycache__" in filepath.parts or ".venv" in filepath.parts: + continue + + file_results = scan_file(filepath, class_hierarchy, root_path) + results.extend(file_results) + + return results + + +def get_all_module_subclasses(): + """Find all Module subclasses in the dimos codebase.""" + # Get the dimos package directory + dimos_file = inspect.getfile(Module) + dimos_path = Path(dimos_file).parent.parent # Go up from dimos/core/module.py to dimos/ + + results = scan_directory(dimos_path) + + # Filter out test modules and base classes + filtered_results = [] + for class_name, filepath, has_start, has_stop, forbidden_methods in results: + # Skip base module classes themselves + if class_name in ("Module", "ModuleBase", "DaskModule", "SkillModule"): + continue + + # Skip test-only modules (those defined in test_ files) + if "test_" in Path(filepath).name: + continue + + filtered_results.append((class_name, filepath, has_start, has_stop, forbidden_methods)) + + return filtered_results + + +@pytest.mark.parametrize( + "class_name,filepath,has_start,has_stop,forbidden_methods", + get_all_module_subclasses(), + ids=lambda val: val[0] if isinstance(val, str) else str(val), +) +def test_module_has_start_and_stop( + class_name: str, filepath, has_start, has_stop, forbidden_methods +) -> None: + """Test that Module subclasses implement start and stop methods and don't use forbidden methods.""" + # Get relative path for better error messages + try: + rel_path = Path(filepath).relative_to(Path.cwd()) + except ValueError: + rel_path = filepath + + errors = [] + + # Check for missing required methods + if not has_start: + errors.append("missing required method: start") + if not has_stop: + errors.append("missing required method: stop") + + # Check for forbidden methods + if forbidden_methods: + forbidden_list = ", ".join(sorted(forbidden_methods)) + errors.append(f"has forbidden method(s): {forbidden_list}") + + assert not errors, f"{class_name} in {rel_path} has issues:\n - " + "\n - ".join(errors) diff --git a/dimos/core/test_rpcstress.py b/dimos/core/test_rpcstress.py new file mode 100644 index 0000000000..fc00a95854 --- /dev/null +++ b/dimos/core/test_rpcstress.py @@ -0,0 +1,177 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import threading +import time + +from dimos.core import In, Module, Out, rpc + + +class Counter(Module): + current_count: int = 0 + + count_stream: Out[int] = None + + def __init__(self) -> None: + super().__init__() + self.current_count = 0 + + @rpc + def increment(self): + """Increment the counter and publish the new value.""" + self.current_count += 1 + self.count_stream.publish(self.current_count) + return self.current_count + + +class CounterValidator(Module): + """Calls counter.increment() as fast as possible and validates no numbers are skipped.""" + + count_in: In[int] = None + + def __init__(self, increment_func) -> None: + super().__init__() + self.increment_func = increment_func + self.last_seen = 0 + self.missing_numbers = [] + self.running = False + self.call_thread = None + self.call_count = 0 + self.total_latency = 0.0 + self.call_start_time = None + self.waiting_for_response = False + + @rpc + def start(self) -> None: + """Start the validator.""" + self.count_in.subscribe(self._on_count_received) + self.running = True + self.call_thread = threading.Thread(target=self._call_loop) + self.call_thread.start() + + @rpc + def stop(self) -> None: + """Stop the validator.""" + self.running = False + if self.call_thread: + self.call_thread.join() + + def _on_count_received(self, count: int) -> None: + """Check if we received all numbers in sequence and trigger next call.""" + # Calculate round trip time + if self.call_start_time: + latency = time.time() - self.call_start_time + self.total_latency += latency + + if count != self.last_seen + 1: + for missing in range(self.last_seen + 1, count): + self.missing_numbers.append(missing) + print(f"[VALIDATOR] Missing number detected: {missing}") + self.last_seen = count + + # Signal that we can make the next call + self.waiting_for_response = False + + def _call_loop(self) -> None: + """Call increment only after receiving response from previous call.""" + while self.running: + if not self.waiting_for_response: + try: + self.waiting_for_response = True + self.call_start_time = time.time() + result = self.increment_func() + call_time = time.time() - self.call_start_time + self.call_count += 1 + if self.call_count % 100 == 0: + avg_latency = ( + self.total_latency / self.call_count if self.call_count > 0 else 0 + ) + print( + f"[VALIDATOR] Made {self.call_count} calls, last result: {result}, RPC call time: {call_time * 1000:.2f}ms, avg RTT: {avg_latency * 1000:.2f}ms" + ) + except Exception as e: + print(f"[VALIDATOR] Error calling increment: {e}") + self.waiting_for_response = False + time.sleep(0.001) # Small delay on error + else: + # Don't sleep - busy wait for maximum speed + pass + + @rpc + def get_stats(self): + """Get validation statistics.""" + avg_latency = self.total_latency / self.call_count if self.call_count > 0 else 0 + return { + "call_count": self.call_count, + "last_seen": self.last_seen, + "missing_count": len(self.missing_numbers), + "missing_numbers": self.missing_numbers[:10] if self.missing_numbers else [], + "avg_rtt_ms": avg_latency * 1000, + "calls_per_sec": self.call_count / 10.0 if self.call_count > 0 else 0, + } + + +if __name__ == "__main__": + import dimos.core as core + from dimos.core import pLCMTransport + + # Start dimos with 2 workers + client = core.start(2) + + # Deploy counter module + counter = client.deploy(Counter) + counter.count_stream.transport = pLCMTransport("/counter_stream") + + # Deploy validator module with increment function + validator = client.deploy(CounterValidator, counter.increment) + validator.count_in.transport = pLCMTransport("/counter_stream") + + # Connect validator to counter's output + validator.count_in.connect(counter.count_stream) + + # Start modules + validator.start() + + print("[MAIN] Counter and validator started. Running for 10 seconds...") + + # Test direct RPC speed for comparison + print("\n[MAIN] Testing direct RPC call speed for 1 second...") + start = time.time() + direct_count = 0 + while time.time() - start < 1.0: + counter.increment() + direct_count += 1 + print(f"[MAIN] Direct RPC calls per second: {direct_count}") + + # Run for 10 seconds + time.sleep(10) + + # Get stats before stopping + stats = validator.get_stats() + print("\n[MAIN] Final statistics:") + print(f" - Total calls made: {stats['call_count']}") + print(f" - Last number seen: {stats['last_seen']}") + print(f" - Missing numbers: {stats['missing_count']}") + print(f" - Average RTT: {stats['avg_rtt_ms']:.2f}ms") + print(f" - Calls per second: {stats['calls_per_sec']:.1f}") + if stats["missing_numbers"]: + print(f" - First missing numbers: {stats['missing_numbers']}") + + # Stop modules + validator.stop() + + # Shutdown dimos + client.shutdown() + + print("[MAIN] Test complete.") diff --git a/dimos/core/test_stream.py b/dimos/core/test_stream.py new file mode 100644 index 0000000000..91091e42af --- /dev/null +++ b/dimos/core/test_stream.py @@ -0,0 +1,256 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Callable +import time + +import pytest + +from dimos.core import ( + In, + LCMTransport, + Module, + rpc, +) +from dimos.core.testing import MockRobotClient, dimos +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.robot.unitree_webrtc.type.odometry import Odometry + +assert dimos + + +class SubscriberBase(Module): + sub1_msgs: list[Odometry] = None + sub2_msgs: list[Odometry] = None + + def __init__(self) -> None: + self.sub1_msgs = [] + self.sub2_msgs = [] + super().__init__() + + @rpc + def sub1(self) -> None: ... + + @rpc + def sub2(self) -> None: ... + + @rpc + def active_subscribers(self): + return self.odom.transport.active_subscribers + + @rpc + def sub1_msgs_len(self) -> int: + return len(self.sub1_msgs) + + @rpc + def sub2_msgs_len(self) -> int: + return len(self.sub2_msgs) + + +class ClassicSubscriber(SubscriberBase): + odom: In[Odometry] = None + unsub: Callable[[], None] | None = None + unsub2: Callable[[], None] | None = None + + @rpc + def sub1(self) -> None: + self.unsub = self.odom.subscribe(self.sub1_msgs.append) + + @rpc + def sub2(self) -> None: + self.unsub2 = self.odom.subscribe(self.sub2_msgs.append) + + @rpc + def stop(self) -> None: + if self.unsub: + self.unsub() + self.unsub = None + if self.unsub2: + self.unsub2() + self.unsub2 = None + + +class RXPYSubscriber(SubscriberBase): + odom: In[Odometry] = None + unsub: Callable[[], None] | None = None + unsub2: Callable[[], None] | None = None + + hot: Callable[[], None] | None = None + + @rpc + def sub1(self) -> None: + self.unsub = self.odom.observable().subscribe(self.sub1_msgs.append) + + @rpc + def sub2(self) -> None: + self.unsub2 = self.odom.observable().subscribe(self.sub2_msgs.append) + + @rpc + def stop(self) -> None: + if self.unsub: + self.unsub.dispose() + self.unsub = None + if self.unsub2: + self.unsub2.dispose() + self.unsub2 = None + + @rpc + def get_next(self): + return self.odom.get_next() + + @rpc + def start_hot_getter(self) -> None: + self.hot = self.odom.hot_latest() + + @rpc + def stop_hot_getter(self) -> None: + self.hot.dispose() + + @rpc + def get_hot(self): + return self.hot() + + +class SpyLCMTransport(LCMTransport): + active_subscribers: int = 0 + + def __reduce__(self): + return (SpyLCMTransport, (self.topic.topic, self.topic.lcm_type)) + + def __init__(self, topic: str, type: type, **kwargs) -> None: + super().__init__(topic, type, **kwargs) + self._subscriber_map = {} # Maps unsubscribe functions to track active subs + + def subscribe(self, selfstream: In, callback: Callable) -> Callable[[], None]: + # Call parent subscribe to get the unsubscribe function + unsubscribe_fn = super().subscribe(selfstream, callback) + + # Increment counter + self.active_subscribers += 1 + + def wrapped_unsubscribe() -> None: + # Create wrapper that decrements counter when called + if wrapped_unsubscribe in self._subscriber_map: + self.active_subscribers -= 1 + del self._subscriber_map[wrapped_unsubscribe] + unsubscribe_fn() + + # Track this subscription + self._subscriber_map[wrapped_unsubscribe] = True + + return wrapped_unsubscribe + + +@pytest.mark.parametrize("subscriber_class", [ClassicSubscriber, RXPYSubscriber]) +@pytest.mark.module +def test_subscription(dimos, subscriber_class) -> None: + robot = dimos.deploy(MockRobotClient) + + robot.lidar.transport = SpyLCMTransport("/lidar", LidarMessage) + robot.odometry.transport = SpyLCMTransport("/odom", Odometry) + + subscriber = dimos.deploy(subscriber_class) + + subscriber.odom.connect(robot.odometry) + + robot.start() + subscriber.sub1() + time.sleep(0.25) + + assert subscriber.sub1_msgs_len() > 0 + assert subscriber.sub2_msgs_len() == 0 + assert subscriber.active_subscribers() == 1 + + subscriber.sub2() + + time.sleep(0.25) + subscriber.stop() + + assert subscriber.active_subscribers() == 0 + assert subscriber.sub1_msgs_len() != 0 + assert subscriber.sub2_msgs_len() != 0 + + total_msg_n = subscriber.sub1_msgs_len() + subscriber.sub2_msgs_len() + + time.sleep(0.25) + + # ensuring no new messages have passed through + assert total_msg_n == subscriber.sub1_msgs_len() + subscriber.sub2_msgs_len() + + robot.stop() + + +@pytest.mark.module +def test_get_next(dimos) -> None: + robot = dimos.deploy(MockRobotClient) + + robot.lidar.transport = SpyLCMTransport("/lidar", LidarMessage) + robot.odometry.transport = SpyLCMTransport("/odom", Odometry) + + subscriber = dimos.deploy(RXPYSubscriber) + subscriber.odom.connect(robot.odometry) + + robot.start() + time.sleep(0.1) + + odom = subscriber.get_next() + + assert isinstance(odom, Odometry) + assert subscriber.active_subscribers() == 0 + + time.sleep(0.2) + + next_odom = subscriber.get_next() + + assert isinstance(next_odom, Odometry) + assert subscriber.active_subscribers() == 0 + + assert next_odom != odom + robot.stop() + + +@pytest.mark.module +def test_hot_getter(dimos) -> None: + robot = dimos.deploy(MockRobotClient) + + robot.lidar.transport = SpyLCMTransport("/lidar", LidarMessage) + robot.odometry.transport = SpyLCMTransport("/odom", Odometry) + + subscriber = dimos.deploy(RXPYSubscriber) + subscriber.odom.connect(robot.odometry) + + robot.start() + + # we are robust to multiple calls + subscriber.start_hot_getter() + time.sleep(0.2) + odom = subscriber.get_hot() + subscriber.stop_hot_getter() + + assert isinstance(odom, Odometry) + time.sleep(0.3) + + # there are no subs + assert subscriber.active_subscribers() == 0 + + # we can restart though + subscriber.start_hot_getter() + time.sleep(0.3) + + next_odom = subscriber.get_hot() + assert isinstance(next_odom, Odometry) + assert next_odom != odom + subscriber.stop_hot_getter() + + robot.stop() diff --git a/dimos/core/testing.py b/dimos/core/testing.py new file mode 100644 index 0000000000..92f6d6b497 --- /dev/null +++ b/dimos/core/testing.py @@ -0,0 +1,83 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from threading import Event, Thread +import time + +import pytest + +from dimos.core import In, Module, Out, rpc, start +from dimos.msgs.geometry_msgs import Vector3 +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.robot.unitree_webrtc.type.odometry import Odometry +from dimos.utils.testing import SensorReplay + + +@pytest.fixture +def dimos(): + """Fixture to create a Dimos client for testing.""" + client = start(2) + yield client + client.stop() + + +class MockRobotClient(Module): + odometry: Out[Odometry] = None + lidar: Out[LidarMessage] = None + mov: In[Vector3] = None + + mov_msg_count = 0 + + def mov_callback(self, msg) -> None: + self.mov_msg_count += 1 + + def __init__(self) -> None: + super().__init__() + self._stop_event = Event() + self._thread = None + + @rpc + def start(self) -> None: + super().start() + + self._thread = Thread(target=self.odomloop) + self._thread.start() + self.mov.subscribe(self.mov_callback) + + @rpc + def stop(self) -> None: + self._stop_event.set() + if self._thread and self._thread.is_alive(): + self._thread.join(timeout=1.0) + + super().stop() + + def odomloop(self) -> None: + odomdata = SensorReplay("raw_odometry_rotate_walk", autocast=Odometry.from_msg) + lidardata = SensorReplay("office_lidar", autocast=LidarMessage.from_msg) + + lidariter = lidardata.iterate() + self._stop_event.clear() + while not self._stop_event.is_set(): + for odom in odomdata.iterate(): + if self._stop_event.is_set(): + return + print(odom) + odom.pubtime = time.perf_counter() + self.odometry.publish(odom) + + lidarmsg = next(lidariter) + lidarmsg.pubtime = time.perf_counter() + self.lidar.publish(lidarmsg) + time.sleep(0.1) diff --git a/dimos/core/transport.py b/dimos/core/transport.py new file mode 100644 index 0000000000..32f75e6c33 --- /dev/null +++ b/dimos/core/transport.py @@ -0,0 +1,232 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import traceback +from typing import TypeVar + +import dimos.core.colors as colors + +T = TypeVar("T") + +from typing import ( + TYPE_CHECKING, + TypeVar, +) + +from dimos.core.stream import In, RemoteIn, Transport +from dimos.protocol.pubsub.jpeg_shm import JpegSharedMemory +from dimos.protocol.pubsub.lcmpubsub import LCM, JpegLCM, PickleLCM, Topic as LCMTopic +from dimos.protocol.pubsub.shmpubsub import PickleSharedMemory, SharedMemory + +if TYPE_CHECKING: + from collections.abc import Callable + +T = TypeVar("T") + + +class PubSubTransport(Transport[T]): + topic: any + + def __init__(self, topic: any) -> None: + self.topic = topic + + def __str__(self) -> str: + return ( + colors.green(f"{self.__class__.__name__}(") + + colors.blue(self.topic) + + colors.green(")") + ) + + +class pLCMTransport(PubSubTransport[T]): + _started: bool = False + + def __init__(self, topic: str, **kwargs) -> None: + super().__init__(topic) + self.lcm = PickleLCM(**kwargs) + + def __reduce__(self): + return (pLCMTransport, (self.topic,)) + + def broadcast(self, _, msg) -> None: + if not self._started: + self.lcm.start() + self._started = True + + self.lcm.publish(self.topic, msg) + + def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: + if not self._started: + self.lcm.start() + self._started = True + return self.lcm.subscribe(self.topic, lambda msg, topic: callback(msg)) + + +class LCMTransport(PubSubTransport[T]): + _started: bool = False + + def __init__(self, topic: str, type: type, **kwargs) -> None: + super().__init__(LCMTopic(topic, type)) + if not hasattr(self, "lcm"): + self.lcm = LCM(**kwargs) + + def __reduce__(self): + return (LCMTransport, (self.topic.topic, self.topic.lcm_type)) + + def broadcast(self, _, msg) -> None: + if not self._started: + self.lcm.start() + self._started = True + + self.lcm.publish(self.topic, msg) + + def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: + if not self._started: + self.lcm.start() + self._started = True + return self.lcm.subscribe(self.topic, lambda msg, topic: callback(msg)) + + +class JpegLcmTransport(LCMTransport): + def __init__(self, topic: str, type: type, **kwargs): + self.lcm = JpegLCM(**kwargs) + super().__init__(topic, type) + + def __reduce__(self): + return (JpegLcmTransport, (self.topic.topic, self.topic.lcm_type)) + + +class pSHMTransport(PubSubTransport[T]): + _started: bool = False + + def __init__(self, topic: str, **kwargs) -> None: + super().__init__(topic) + self.shm = PickleSharedMemory(**kwargs) + + def __reduce__(self): + return (pSHMTransport, (self.topic,)) + + def broadcast(self, _, msg) -> None: + if not self._started: + self.shm.start() + self._started = True + + self.shm.publish(self.topic, msg) + + def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: + if not self._started: + self.shm.start() + self._started = True + return self.shm.subscribe(self.topic, lambda msg, topic: callback(msg)) + + +class SHMTransport(PubSubTransport[T]): + _started: bool = False + + def __init__(self, topic: str, **kwargs) -> None: + super().__init__(topic) + self.shm = SharedMemory(**kwargs) + + def __reduce__(self): + return (SHMTransport, (self.topic,)) + + def broadcast(self, _, msg) -> None: + if not self._started: + self.shm.start() + self._started = True + + self.shm.publish(self.topic, msg) + + def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: + if not self._started: + self.shm.start() + self._started = True + return self.shm.subscribe(self.topic, lambda msg, topic: callback(msg)) + + +class JpegShmTransport(PubSubTransport[T]): + _started: bool = False + + def __init__(self, topic: str, quality: int = 75, **kwargs): + super().__init__(topic) + self.shm = JpegSharedMemory(quality=quality, **kwargs) + self.quality = quality + + def __reduce__(self): + return (JpegShmTransport, (self.topic, self.quality)) + + def broadcast(self, _, msg): + if not self._started: + self.shm.start() + self._started = True + + self.shm.publish(self.topic, msg) + + def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: + if not self._started: + self.shm.start() + self._started = True + return self.shm.subscribe(self.topic, lambda msg, topic: callback(msg)) + + +class DaskTransport(Transport[T]): + subscribers: list[Callable[[T], None]] + _started: bool = False + + def __init__(self) -> None: + self.subscribers = [] + + def __str__(self) -> str: + return colors.yellow("DaskTransport") + + def __reduce__(self): + return (DaskTransport, ()) + + def broadcast(self, selfstream: RemoteIn[T], msg: T) -> None: + for subscriber in self.subscribers: + # there is some sort of a bug here with losing worker loop + # print(subscriber.owner, subscriber.owner._worker, subscriber.owner._client) + # subscriber.owner._try_bind_worker_client() + # print(subscriber.owner, subscriber.owner._worker, subscriber.owner._client) + + subscriber.owner.dask_receive_msg(subscriber.name, msg).result() + + def dask_receive_msg(self, msg) -> None: + for subscriber in self.subscribers: + try: + subscriber(msg) + except Exception as e: + print( + colors.red("Error in DaskTransport subscriber callback:"), + e, + traceback.format_exc(), + ) + + # for outputs + def dask_register_subscriber(self, remoteInput: RemoteIn[T]) -> None: + self.subscribers.append(remoteInput) + + # for inputs + def subscribe(self, callback: Callable[[T], None], selfstream: In[T]) -> None: + if not self._started: + selfstream.connection.owner.dask_register_subscriber( + selfstream.connection.name, selfstream + ).result() + self._started = True + self.subscribers.append(callback) + + +class ZenohTransport(PubSubTransport[T]): ... diff --git a/dimos/data/data_pipeline.py b/dimos/data/data_pipeline.py deleted file mode 100644 index d828bdc00b..0000000000 --- a/dimos/data/data_pipeline.py +++ /dev/null @@ -1,153 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from dimos.stream.videostream import VideoStream - -import warnings -from concurrent.futures import ProcessPoolExecutor, as_completed -from collections import deque -from dimos.types.depth_map import DepthMapType -from dimos.types.label import LabelType -from dimos.types.pointcloud import PointCloudType -from dimos.types.segmentation import SegmentationType -import os - -class DataPipeline: - def __init__(self, video_stream: VideoStream, - run_depth: bool = False, - run_labels: bool = False, - run_pointclouds: bool = False, - run_segmentations: bool = False, - max_workers: int = 4): - self.video_stream = video_stream - self.run_depth = run_depth - self.run_labels = run_labels - self.run_pointclouds = run_pointclouds - self.run_segmentations = run_segmentations - self.max_workers = max_workers - - # Validate pipeline configuration - self._validate_pipeline() - - # Initialize the pipeline - self._initialize_pipeline() - - # Storage for processed data - self.generated_depth_maps = deque() - self.generated_labels = deque() - self.generated_pointclouds = deque() - self.generated_segmentations = deque() - - def _validate_pipeline(self): - """Validate the pipeline configuration based on dependencies.""" - if self.run_pointclouds and not self.run_depth: - raise ValueError("PointClouds generation requires Depth maps. " - "Enable run_depth=True to use run_pointclouds=True.") - - if self.run_segmentations and not self.run_labels: - raise ValueError("Segmentations generation requires Labels. " - "Enable run_labels=True to use run_segmentations=True.") - - if not any([self.run_depth, self.run_labels, self.run_pointclouds, self.run_segmentations]): - warnings.warn("No pipeline layers selected to run. The DataPipeline will be initialized without any processing.") - - def _initialize_pipeline(self): - """Initialize necessary components based on selected pipeline layers.""" - if self.run_depth: - from .depth import DepthProcessor - self.depth_processor = DepthProcessor(debug=True) - print("Depth map generation enabled.") - else: - self.depth_processor = None - - if self.run_labels: - from .labels import LabelProcessor - self.labels_processor = LabelProcessor(debug=True) - print("Label generation enabled.") - else: - self.labels_processor = None - - if self.run_pointclouds: - from .pointcloud import PointCloudProcessor - self.pointcloud_processor = PointCloudProcessor(debug=True) - print("PointCloud generation enabled.") - else: - self.pointcloud_processor = None - - if self.run_segmentations: - from .segment import SegmentProcessor - self.segmentation_processor = SegmentProcessor(debug=True) - print("Segmentation generation enabled.") - else: - self.segmentation_processor = None - - def run(self): - """Execute the selected pipeline layers.""" - try: - for frame in self.video_stream: - result = self._process_frame(frame) - depth_map, label, pointcloud, segmentation = result - - if depth_map is not None: - self.generated_depth_maps.append(depth_map) - if label is not None: - self.generated_labels.append(label) - if pointcloud is not None: - self.generated_pointclouds.append(pointcloud) - if segmentation is not None: - self.generated_segmentations.append(segmentation) - except KeyboardInterrupt: - print("Pipeline interrupted by user.") - - def _process_frame(self, frame): - """Process a single frame and return results.""" - depth_map = None - label = None - pointcloud = None - segmentation = None - - if self.run_depth: - depth_map = self.depth_processor.process(frame) - - if self.run_labels: - label = self.labels_processor.caption_image_data(frame) - - if self.run_pointclouds and isinstance(depth_map, DepthMapType) and self.pointcloud_processor: - pointcloud = self.pointcloud_processor.process_frame(frame, depth_map.depth_data) - - if self.run_segmentations and isinstance(label, LabelType) and self.segmentation_processor: - segmentation = self.segmentation_processor.process_frame(frame, label.labels) - - return depth_map, label, pointcloud, segmentation - - def save_all_processed_data(self, directory: str): - """Save all processed data to files in the specified directory.""" - os.makedirs(directory, exist_ok=True) - - for i, depth_map in enumerate(self.generated_depth_maps): - if isinstance(depth_map, DepthMapType): - depth_map.save_to_file(os.path.join(directory, f"depth_map_{i}.npy")) - - for i, label in enumerate(self.generated_labels): - if isinstance(label, LabelType): - label.save_to_json(os.path.join(directory, f"labels_{i}.json")) - - for i, pointcloud in enumerate(self.generated_pointclouds): - if isinstance(pointcloud, PointCloudType): - pointcloud.save_to_file(os.path.join(directory, f"pointcloud_{i}.pcd")) - - for i, segmentation in enumerate(self.generated_segmentations): - if isinstance(segmentation, SegmentationType): - segmentation.save_masks(os.path.join(directory, f"segmentation_{i}")) diff --git a/dimos/data/depth.py b/dimos/data/depth.py deleted file mode 100644 index 76e60bebb8..0000000000 --- a/dimos/data/depth.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.models.depth.metric3d import Metric3D -import os -import pickle -import argparse -import pandas as pd -from PIL import Image -from io import BytesIO -import torch -import sys -import cv2 -import tarfile -import logging -import time -import tempfile -import gc -import io -import csv -import numpy as np -from dimos.types.depth_map import DepthMapType - -class DepthProcessor: - def __init__(self, debug=False): - self.debug = debug - self.metric_3d = Metric3D() - self.depth_count = 0 - self.valid_depth_count = 0 - self.logger = logging.getLogger(__name__) - self.intrinsic = [707.0493, 707.0493, 604.0814, 180.5066] # Default intrinsic - - print("DepthProcessor initialized") - - if debug: - print("Running in debug mode") - self.logger.info("Running in debug mode") - - - def process(self, frame: Image.Image, intrinsics=None): - """Process a frame to generate a depth map. - - Args: - frame: PIL Image to process - intrinsics: Optional camera intrinsics parameters - - Returns: - DepthMapType containing the depth map - """ - if intrinsics: - self.metric_3d.update_intrinsic(intrinsics) - else: - self.metric_3d.update_intrinsic(self.intrinsic) - - # Convert frame to numpy array suitable for processing - if isinstance(frame, Image.Image): - image = frame.convert('RGB') - elif isinstance(frame, np.ndarray): - image = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) - else: - raise ValueError("Unsupported frame format. Must be PIL Image or numpy array.") - - image_np = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) - image_np = resize_image_for_vit(image_np) - - # Process image and run depth via Metric3D - try: - with torch.no_grad(): - depth_map = self.metric_3d.infer_depth(image_np) - - self.depth_count += 1 - - # Validate depth map - if is_depth_map_valid(np.array(depth_map)): - self.valid_depth_count += 1 - else: - self.logger.error(f"Invalid depth map for the provided frame.") - print("Invalid depth map for the provided frame.") - return None - - if self.debug: - # Save depth map locally or to S3 as needed - pass # Implement saving logic if required - - return DepthMapType(depth_data=depth_map, metadata={"intrinsics": intrinsics}) - - except Exception as e: - self.logger.error(f"Error processing frame: {e}") - return None \ No newline at end of file diff --git a/dimos/data/labels.py b/dimos/data/labels.py deleted file mode 100644 index fe8d9107b8..0000000000 --- a/dimos/data/labels.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -from dimos.models.labels.llava_34b import Llava -from PIL import Image -from dimos.types.label import LabelType - -class LabelProcessor: - def __init__(self, debug: bool = False): - self.model = None - self.prompt = 'Create a JSON representation where each entry consists of a key "object" with a numerical suffix starting from 1, and a corresponding "description" key with a value that is a concise, up to six-word sentence describing each main, distinct object or person in the image. Each pair should uniquely describe one element without repeating keys. An example: {"object1": { "description": "Man in red hat walking." },"object2": { "description": "Wooden pallet with boxes." },"object3": { "description": "Cardboard boxes stacked." },"object4": { "description": "Man in green vest standing." }}' - self.debug = debug - - def _initialize_model(self): - if self.model is None: - from dimos.models.labels.llava_34b import Llava - self.model = Llava(mmproj=f"{os.getcwd()}/models/mmproj-model-f16.gguf", model_path=f"{os.getcwd()}/models/llava-v1.6-34b.Q4_K_M.gguf", gpu=True) - if self.debug: - print("Llava model initialized.") - - def caption_image_data(self, frame): - self._initialize_model() - try: - output = self.model.run_inference(frame, self.prompt, return_json=True) - if self.debug: - print("Output:", output) - return LabelType(labels=output, metadata={"frame_id": frame.id}) - except Exception as e: - print(f"Error in captioning image: {e}") - return LabelType(labels={}, metadata={"error": str(e)}) \ No newline at end of file diff --git a/dimos/data/pointcloud.py b/dimos/data/pointcloud.py deleted file mode 100644 index 8d16a62590..0000000000 --- a/dimos/data/pointcloud.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import cv2 -import numpy as np -import open3d as o3d -from pathlib import Path -from PIL import Image -import logging - -from dimos.models.segmentation.segment_utils import apply_mask_to_image -from dimos.models.pointcloud.pointcloud_utils import ( - create_point_cloud_from_rgbd, - canonicalize_point_cloud -) -from dimos.types.pointcloud import PointCloudType - -# Setup logging -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) - - -class PointCloudProcessor: - def __init__(self, output_dir, intrinsic_parameters=None): - """ - Initializes the PointCloudProcessor. - - Args: - output_dir (str): The directory where point clouds will be saved. - intrinsic_parameters (dict, optional): Camera intrinsic parameters. - Defaults to None, in which case default parameters are used. - """ - self.output_dir = output_dir - os.makedirs(self.output_dir, exist_ok=True) - self.logger = logger - - # Default intrinsic parameters - self.default_intrinsic_parameters = { - 'width': 640, - 'height': 480, - 'fx': 960.0, - 'fy': 960.0, - 'cx': 320.0, - 'cy': 240.0, - } - self.intrinsic_parameters = intrinsic_parameters if intrinsic_parameters else self.default_intrinsic_parameters - - def process_frame(self, image, depth_map, masks): - """ - Process a single frame to generate point clouds. - - Args: - image (PIL.Image.Image or np.ndarray): The RGB image. - depth_map (PIL.Image.Image or np.ndarray): The depth map corresponding to the image. - masks (list of np.ndarray): A list of binary masks for segmentation. - - Returns: - list of PointCloudType: A list of point clouds for each mask. - bool: A flag indicating if the point clouds were canonicalized. - """ - try: - self.logger.info("STARTING POINT CLOUD PROCESSING ---------------------------------------") - - # Convert images to OpenCV format if they are PIL Images - if isinstance(image, Image.Image): - original_image_cv = cv2.cvtColor(np.array(image.convert('RGB')), cv2.COLOR_RGB2BGR) - else: - original_image_cv = image - - if isinstance(depth_map, Image.Image): - depth_image_cv = cv2.cvtColor(np.array(depth_map.convert('RGB')), cv2.COLOR_RGB2BGR) - else: - depth_image_cv = depth_map - - width, height = original_image_cv.shape[1], original_image_cv.shape[0] - intrinsic_parameters = self.intrinsic_parameters.copy() - intrinsic_parameters.update({ - 'width': width, - 'height': height, - 'cx': width / 2, - 'cy': height / 2, - }) - - point_clouds = [] - point_cloud_data = [] - - # Create original point cloud - original_pcd = create_point_cloud_from_rgbd(original_image_cv, depth_image_cv, intrinsic_parameters) - pcd, canonicalized, transformation = canonicalize_point_cloud(original_pcd, canonicalize_threshold=0.3) - - for idx, mask in enumerate(masks): - mask_binary = mask > 0 - - masked_rgb = apply_mask_to_image(original_image_cv, mask_binary) - masked_depth = apply_mask_to_image(depth_image_cv, mask_binary) - - pcd = create_point_cloud_from_rgbd(masked_rgb, masked_depth, intrinsic_parameters) - # Remove outliers - cl, ind = pcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=2.0) - inlier_cloud = pcd.select_by_index(ind) - if canonicalized: - inlier_cloud.transform(transformation) - - point_clouds.append(PointCloudType(point_cloud=inlier_cloud, metadata={"mask_index": idx})) - # Save point cloud to file - pointcloud_filename = f"pointcloud_{idx}.pcd" - pointcloud_filepath = os.path.join(self.output_dir, pointcloud_filename) - o3d.io.write_point_cloud(pointcloud_filepath, inlier_cloud) - point_cloud_data.append(pointcloud_filepath) - self.logger.info(f"Saved point cloud {pointcloud_filepath}") - - self.logger.info("DONE POINT CLOUD PROCESSING ---------------------------------------") - return point_clouds, canonicalized - except Exception as e: - self.logger.error(f"Error processing frame: {e}") - return [], False diff --git a/dimos/data/segment.py b/dimos/data/segment.py deleted file mode 100644 index 8a830520a4..0000000000 --- a/dimos/data/segment.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import cv2 -import numpy as np -from PIL import Image -import logging -from dimos.models.segmentation.segment_utils import sample_points_from_heatmap -from dimos.models.segmentation.sam import SAM -from dimos.models.segmentation.clipseg import CLIPSeg - -# Setup logging -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) - - -class SegmentProcessor: - def __init__(self, device='cuda'): - # Initialize CLIPSeg and SAM models - self.clipseg = CLIPSeg(model_name="CIDAS/clipseg-rd64-refined", device=device) - self.sam = SAM(model_name="facebook/sam-vit-huge", device=device) - self.logger = logger - - def process_frame(self, image, captions): - """ - Process a single image and return segmentation masks. - - Args: - image (PIL.Image.Image or np.ndarray): The input image to process. - captions (list of str): A list of captions for segmentation. - - Returns: - list of np.ndarray: A list of segmentation masks corresponding to the captions. - """ - try: - self.logger.info("STARTING PROCESSING IMAGE ---------------------------------------") - self.logger.info(f"Processing image with captions: {captions}") - - # Convert image to PIL.Image if it's a numpy array - if isinstance(image, np.ndarray): - image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) - - preds = self.clipseg.run_inference(image, captions) - sampled_points = [] - sam_masks = [] - - original_size = image.size # (width, height) - - for idx in range(preds.shape[0]): - points = sample_points_from_heatmap(preds[idx][0], original_size, num_points=10) - if points: - sampled_points.append(points) - else: - self.logger.info(f"No points sampled for prediction index {idx}") - sampled_points.append([]) - - for idx in range(preds.shape[0]): - if sampled_points[idx]: - mask_tensor = self.sam.run_inference_from_points(image, [sampled_points[idx]]) - if mask_tensor: - # Convert mask tensor to a numpy array - mask = (255 * mask_tensor[0].numpy().squeeze()).astype(np.uint8) - sam_masks.append(mask) - else: - self.logger.info(f"No mask tensor returned for sampled points at index {idx}") - sam_masks.append(np.zeros((original_size[1], original_size[0]), dtype=np.uint8)) - else: - self.logger.info(f"No sampled points for prediction index {idx}, skipping mask inference") - sam_masks.append(np.zeros((original_size[1], original_size[0]), dtype=np.uint8)) - - self.logger.info("DONE PROCESSING IMAGE ---------------------------------------") - return sam_masks - except Exception as e: - self.logger.error(f"Error processing image: {e}") - return [] \ No newline at end of file diff --git a/dimos/data/videostream-data-pipeline.md b/dimos/data/videostream-data-pipeline.md deleted file mode 100644 index dd1fd96b66..0000000000 --- a/dimos/data/videostream-data-pipeline.md +++ /dev/null @@ -1,30 +0,0 @@ -# UNDER DEVELOPMENT 🚧🚧🚧 -# Example data pipeline from video stream implementation - -```bash - from dimos.stream.videostream import VideoStream - from dimos.data.data_pipeline import DataPipeline - - # init video stream from the camera source - video_stream = VideoStream(source=0) - - # init data pipeline with desired processors enabled, max workers is 4 by default - # depth only implementation - pipeline = DataPipeline( - video_stream=video_stream, - run_depth=True, - run_labels=False, - run_pointclouds=False, - run_segmentations=False - ) - - try: - # Run pipeline - pipeline.run() - except KeyboardInterrupt: - # Handle interrupt - print("Pipeline interrupted by user.") - finally: - # Release the video capture - video_stream.release() -``` diff --git a/dimos/environment/agent_environment.py b/dimos/environment/agent_environment.py index 76209f5c76..a5dab0e272 100644 --- a/dimos/environment/agent_environment.py +++ b/dimos/environment/agent_environment.py @@ -12,14 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +from pathlib import Path + import cv2 import numpy as np -from pathlib import Path -from typing import List, Union + from .environment import Environment + class AgentEnvironment(Environment): - def __init__(self): + def __init__(self) -> None: super().__init__() self.environment_type = "agent" self.frames = [] @@ -28,7 +30,7 @@ def __init__(self): self._segmentations = [] self._point_clouds = [] - def initialize_from_images(self, images: Union[List[str], List[np.ndarray]]) -> bool: + def initialize_from_images(self, images: list[str] | list[np.ndarray]) -> bool: """Initialize environment from a list of image paths or numpy arrays. Args: @@ -69,13 +71,13 @@ def initialize_from_file(self, file_path: str) -> bool: cap = cv2.VideoCapture(file_path) self.frames = [] - + while cap.isOpened(): ret, frame = cap.read() if not ret: break self.frames.append(frame) - + cap.release() return len(self.frames) > 0 except Exception as e: @@ -87,35 +89,43 @@ def initialize_from_directory(self, directory_path: str) -> bool: # TODO: Implement directory initialization raise NotImplementedError("Directory initialization not yet implemented") - def label_objects(self) -> List[str]: + def label_objects(self) -> list[str]: """Implementation of abstract method to label objects.""" # TODO: Implement object labeling using a detection model raise NotImplementedError("Object labeling not yet implemented") - - def generate_segmentations(self, model: str = None, objects: List[str] = None, *args, **kwargs) -> List[np.ndarray]: + def generate_segmentations( + self, model: str | None = None, objects: list[str] | None = None, *args, **kwargs + ) -> list[np.ndarray]: """Generate segmentations for the current frame.""" # TODO: Implement segmentation generation using specified model raise NotImplementedError("Segmentation generation not yet implemented") - def get_segmentations(self) -> List[np.ndarray]: + def get_segmentations(self) -> list[np.ndarray]: """Return pre-computed segmentations for the current frame.""" if self._segmentations: return self._segmentations[self.current_frame_idx] return [] - def generate_point_cloud(self, object: str = None, *args, **kwargs) -> np.ndarray: + def generate_point_cloud(self, object: str | None = None, *args, **kwargs) -> np.ndarray: """Generate point cloud from the current frame.""" # TODO: Implement point cloud generation raise NotImplementedError("Point cloud generation not yet implemented") - def get_point_cloud(self, object: str = None) -> np.ndarray: + def get_point_cloud(self, object: str | None = None) -> np.ndarray: """Return pre-computed point cloud.""" if self._point_clouds: return self._point_clouds[self.current_frame_idx] return np.array([]) - def generate_depth_map(self, stereo: bool = None, monocular: bool = None, model: str = None, *args, **kwargs) -> np.ndarray: + def generate_depth_map( + self, + stereo: bool | None = None, + monocular: bool | None = None, + model: str | None = None, + *args, + **kwargs, + ) -> np.ndarray: """Generate depth map for the current frame.""" # TODO: Implement depth map generation using specified method raise NotImplementedError("Depth map generation not yet implemented") diff --git a/dimos/environment/colmap_environment.py b/dimos/environment/colmap_environment.py index 1e22d6290b..f1b0986c77 100644 --- a/dimos/environment/colmap_environment.py +++ b/dimos/environment/colmap_environment.py @@ -14,11 +14,14 @@ # UNDER DEVELOPMENT 🚧🚧🚧 +from pathlib import Path + import cv2 import pycolmap -from pathlib import Path + from dimos.environment.environment import Environment + class COLMAPEnvironment(Environment): def initialize_from_images(self, image_dir): """Initialize the environment from a set of image frames or video.""" @@ -57,7 +60,7 @@ def initialize_from_video(self, video_path, frame_output_dir): # Initialize from the extracted frames return self.initialize_from_images(frame_output_dir) - def _extract_frames_from_video(self, video_path, frame_output_dir): + def _extract_frames_from_video(self, video_path, frame_output_dir) -> None: """Extract frames from a video and save them to a directory.""" cap = cv2.VideoCapture(str(video_path)) frame_count = 0 @@ -72,17 +75,17 @@ def _extract_frames_from_video(self, video_path, frame_output_dir): cap.release() - def label_objects(self): + def label_objects(self) -> None: pass - def get_visualization(self, format_type): + def get_visualization(self, format_type) -> None: pass - def get_segmentations(self): + def get_segmentations(self) -> None: pass - def get_point_cloud(self, object_id=None): + def get_point_cloud(self, object_id=None) -> None: pass - def get_depth_map(self): + def get_depth_map(self) -> None: pass diff --git a/dimos/environment/environment.py b/dimos/environment/environment.py index a2d48b3b6d..8b0068cbae 100644 --- a/dimos/environment/environment.py +++ b/dimos/environment/environment.py @@ -13,10 +13,12 @@ # limitations under the License. from abc import ABC, abstractmethod + import numpy as np + class Environment(ABC): - def __init__(self): + def __init__(self) -> None: self.environment_type = None self.graph = None @@ -24,7 +26,7 @@ def __init__(self): def label_objects(self) -> list[str]: """ Label all objects in the environment. - + Returns: A list of string labels representing the objects in the environment. """ @@ -34,9 +36,11 @@ def label_objects(self) -> list[str]: def get_visualization(self, format_type): """Return different visualization formats like images, NERFs, or other 3D file types.""" pass - + @abstractmethod - def generate_segmentations(self, model: str = None, objects: list[str] = None, *args, **kwargs) -> list[np.ndarray]: + def generate_segmentations( + self, model: str | None = None, objects: list[str] | None = None, *args, **kwargs + ) -> list[np.ndarray]: """ Generate object segmentations of objects[] using neural methods. @@ -66,9 +70,8 @@ def get_segmentations(self) -> list[np.ndarray]: """ pass - @abstractmethod - def generate_point_cloud(self, object: str = None, *args, **kwargs) -> np.ndarray: + def generate_point_cloud(self, object: str | None = None, *args, **kwargs) -> np.ndarray: """ Generate a point cloud for the entire environment or a specific object. @@ -88,7 +91,7 @@ def generate_point_cloud(self, object: str = None, *args, **kwargs) -> np.ndarra pass @abstractmethod - def get_point_cloud(self, object: str = None) -> np.ndarray: + def get_point_cloud(self, object: str | None = None) -> np.ndarray: """ Return point clouds of the entire environment or a specific object. @@ -102,7 +105,14 @@ def get_point_cloud(self, object: str = None) -> np.ndarray: pass @abstractmethod - def generate_depth_map(self, stereo: bool = None, monocular: bool = None, model: str = None, *args, **kwargs) -> np.ndarray: + def generate_depth_map( + self, + stereo: bool | None = None, + monocular: bool | None = None, + model: str | None = None, + *args, + **kwargs, + ) -> np.ndarray: """ Generate a depth map using monocular or stereo camera methods. @@ -166,5 +176,3 @@ def initialize_from_file(self, file_path): NotImplementedError: If the method is not implemented for this environment type. """ raise NotImplementedError("This method is not implemented for this environment type.") - - diff --git a/dimos/exceptions/agent_memory_exceptions.py b/dimos/exceptions/agent_memory_exceptions.py index 9b2ba35b2c..073e56c643 100644 --- a/dimos/exceptions/agent_memory_exceptions.py +++ b/dimos/exceptions/agent_memory_exceptions.py @@ -14,66 +14,80 @@ import traceback + class AgentMemoryError(Exception): """ Base class for all exceptions raised by AgentMemory operations. All custom exceptions related to AgentMemory should inherit from this class. - + Args: message (str): Human-readable message describing the error. """ - def __init__(self, message="Error in AgentMemory operation"): + + def __init__(self, message: str = "Error in AgentMemory operation") -> None: super().__init__(message) + class AgentMemoryConnectionError(AgentMemoryError): """ Exception raised for errors attempting to connect to the database. This includes failures due to network issues, authentication errors, or incorrect connection parameters. - + Args: message (str): Human-readable message describing the error. cause (Exception, optional): Original exception, if any, that led to this error. """ - def __init__(self, message="Failed to connect to the database", cause=None): + + def __init__(self, message: str = "Failed to connect to the database", cause=None) -> None: super().__init__(message) if cause: self.cause = cause self.traceback = traceback.format_exc() if cause else None - def __str__(self): - return f"{self.message}\nCaused by: {repr(self.cause)}" if self.cause else self.message + def __str__(self) -> str: + return f"{self.message}\nCaused by: {self.cause!r}" if self.cause else self.message + class UnknownConnectionTypeError(AgentMemoryConnectionError): """ Exception raised when an unknown or unsupported connection type is specified during AgentMemory setup. - + Args: message (str): Human-readable message explaining that an unknown connection type was used. """ - def __init__(self, message="Unknown connection type used in AgentMemory connection"): + + def __init__( + self, message: str = "Unknown connection type used in AgentMemory connection" + ) -> None: super().__init__(message) + class DataRetrievalError(AgentMemoryError): """ Exception raised for errors retrieving data from the database. This could occur due to query failures, timeouts, or corrupt data issues. - + Args: message (str): Human-readable message describing the data retrieval error. """ - def __init__(self, message="Error in retrieving data during AgentMemory operation"): + + def __init__( + self, message: str = "Error in retrieving data during AgentMemory operation" + ) -> None: super().__init__(message) + class DataNotFoundError(DataRetrievalError): """ Exception raised when the requested data is not found in the database. This is used when a query completes successfully but returns no result for the specified identifier. - + Args: vector_id (int or str): The identifier for the vector that was not found. message (str, optional): Human-readable message providing more detail. If not provided, a default message is generated. """ - def __init__(self, vector_id, message=None): + + def __init__(self, vector_id, message=None) -> None: message = message or f"Requested data for vector ID {vector_id} was not found." super().__init__(message) self.vector_id = vector_id diff --git a/dimos/hardware/README.md b/dimos/hardware/README.md index 0141ac89e9..fb598e82cf 100644 --- a/dimos/hardware/README.md +++ b/dimos/hardware/README.md @@ -1 +1,29 @@ -# UNDER DEVELOPMENT 🚧🚧🚧 \ No newline at end of file +# Hardware + +## Remote camera stream with timestamps + +### Required Ubuntu packages: + +```bash +sudo apt install gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav python3-gi python3-gi-cairo gir1.2-gstreamer-1.0 gir1.2-gst-plugins-base-1.0 v4l-utils gstreamer1.0-vaapi +``` + +### Usage + +On sender machine (with the camera): + +```bash +python3 dimos/hardware/gstreamer_sender.py --device /dev/video0 --host 0.0.0.0 --port 5000 +``` + +If it's a stereo camera and you only want to send the left side (the left camera): + +```bash +python3 dimos/hardware/gstreamer_sender.py --device /dev/video0 --host 0.0.0.0 --port 5000 --single-camera +``` + +On receiver machine: + +```bash +python3 dimos/hardware/gstreamer_camera_test_script.py --host 10.0.0.227 --port 5000 +``` \ No newline at end of file diff --git a/dimos/hardware/camera.py b/dimos/hardware/camera.py deleted file mode 100644 index 8793a5dd0a..0000000000 --- a/dimos/hardware/camera.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.hardware.sensor import AbstractSensor - -class Camera(AbstractSensor): - def __init__(self, resolution=None, focal_length=None, sensor_size=None, sensor_type='Camera'): - super().__init__(sensor_type) - self.resolution = resolution # (width, height) in pixels - self.focal_length = focal_length # in millimeters - self.sensor_size = sensor_size # (width, height) in millimeters - - def get_sensor_type(self): - return self.sensor_type - - def calculate_intrinsics(self): - if not self.resolution or not self.focal_length or not self.sensor_size: - raise ValueError("Resolution, focal length, and sensor size must be provided") - - # Calculate pixel size - pixel_size_x = self.sensor_size[0] / self.resolution[0] - pixel_size_y = self.sensor_size[1] / self.resolution[1] - - # Calculate the principal point (assuming it's at the center of the image) - principal_point_x = self.resolution[0] / 2 - principal_point_y = self.resolution[1] / 2 - - # Calculate the focal length in pixels - focal_length_x = self.focal_length / pixel_size_x - focal_length_y = self.focal_length / pixel_size_y - - return { - 'focal_length_x': focal_length_x, - 'focal_length_y': focal_length_y, - 'principal_point_x': principal_point_x, - 'principal_point_y': principal_point_y - } - - def get_intrinsics(self): - return self.calculate_intrinsics() diff --git a/dimos/hardware/camera/module.py b/dimos/hardware/camera/module.py new file mode 100644 index 0000000000..0f0791650b --- /dev/null +++ b/dimos/hardware/camera/module.py @@ -0,0 +1,128 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Callable +from dataclasses import dataclass, field +import queue +import time + +from dimos_lcm.sensor_msgs import CameraInfo +import reactivex as rx +from reactivex import operators as ops +from reactivex.disposable import Disposable +from reactivex.observable import Observable + +from dimos.agents2 import Output, Reducer, Stream, skill +from dimos.core import Module, Out, rpc +from dimos.core.module import Module, ModuleConfig +from dimos.hardware.camera.spec import ( + CameraHardware, +) +from dimos.hardware.camera.webcam import Webcam +from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 +from dimos.msgs.sensor_msgs import Image +from dimos.msgs.sensor_msgs.Image import Image, sharpness_barrier + + +def default_transform(): + return Transform( + translation=Vector3(0.0, 0.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="base_link", + child_frame_id="camera_link", + ) + + +@dataclass +class CameraModuleConfig(ModuleConfig): + frame_id: str = "camera_link" + transform: Transform | None = field(default_factory=default_transform) + hardware: Callable[[], CameraHardware] | CameraHardware = Webcam + + +class CameraModule(Module): + image: Out[Image] = None + camera_info: Out[CameraInfo] = None + + hardware: CameraHardware = None + _module_subscription: Disposable | None = None + _camera_info_subscription: Disposable | None = None + _skill_stream: Observable[Image] | None = None + + default_config = CameraModuleConfig + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + @rpc + def start(self) -> str: + if callable(self.config.hardware): + self.hardware = self.config.hardware() + else: + self.hardware = self.config.hardware + + if self._module_subscription: + return "already started" + + stream = self.hardware.image_stream().pipe(sharpness_barrier(5)) + + # camera_info_stream = self.camera_info_stream(frequency=5.0) + + def publish_info(camera_info: CameraInfo) -> None: + self.camera_info.publish(camera_info) + + if self.config.transform is None: + return + + camera_link = self.config.transform + camera_link.ts = camera_info.ts + camera_optical = Transform( + translation=Vector3(0.0, 0.0, 0.0), + rotation=Quaternion(-0.5, 0.5, -0.5, 0.5), + frame_id="camera_link", + child_frame_id="camera_optical", + ts=camera_link.ts, + ) + + self.tf.publish(camera_link, camera_optical) + + self._camera_info_subscription = self.camera_info_stream().subscribe(publish_info) + self._module_subscription = stream.subscribe(self.image.publish) + + @skill(stream=Stream.passive, output=Output.image, reducer=Reducer.latest) + def video_stream(self) -> Image: + """implicit video stream skill""" + _queue = queue.Queue(maxsize=1) + self.hardware.image_stream().subscribe(_queue.put) + + yield from iter(_queue.get, None) + + def camera_info_stream(self, frequency: float = 5.0) -> Observable[CameraInfo]: + def camera_info(_) -> CameraInfo: + self.hardware.camera_info.ts = time.time() + return self.hardware.camera_info + + return rx.interval(1.0 / frequency).pipe(ops.map(camera_info)) + + def stop(self) -> None: + if self._module_subscription: + self._module_subscription.dispose() + self._module_subscription = None + if self._camera_info_subscription: + self._camera_info_subscription.dispose() + self._camera_info_subscription = None + # Also stop the hardware if it has a stop method + if self.hardware and hasattr(self.hardware, "stop"): + self.hardware.stop() + super().stop() diff --git a/dimos/hardware/camera/spec.py b/dimos/hardware/camera/spec.py new file mode 100644 index 0000000000..b9722d6cd2 --- /dev/null +++ b/dimos/hardware/camera/spec.py @@ -0,0 +1,55 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC, abstractmethod, abstractproperty +from typing import Generic, Protocol, TypeVar + +from dimos_lcm.sensor_msgs import CameraInfo +from reactivex.observable import Observable + +from dimos.msgs.sensor_msgs import Image +from dimos.protocol.service import Configurable + + +class CameraConfig(Protocol): + frame_id_prefix: str | None + + +CameraConfigT = TypeVar("CameraConfigT", bound=CameraConfig) + + +class CameraHardware(ABC, Configurable[CameraConfigT], Generic[CameraConfigT]): + @abstractmethod + def image_stream(self) -> Observable[Image]: + pass + + @abstractproperty + def camera_info(self) -> CameraInfo: + pass + + +# This is an example, feel free to change spec for stereo cameras +# e.g., separate camera_info or streams for left/right, etc. +class StereoCameraHardware(ABC, Configurable[CameraConfigT], Generic[CameraConfigT]): + @abstractmethod + def image_stream(self) -> Observable[Image]: + pass + + @abstractmethod + def depth_stream(self) -> Observable[Image]: + pass + + @abstractproperty + def camera_info(self) -> CameraInfo: + pass diff --git a/dimos/hardware/camera/test_webcam.py b/dimos/hardware/camera/test_webcam.py new file mode 100644 index 0000000000..e2f99e85dd --- /dev/null +++ b/dimos/hardware/camera/test_webcam.py @@ -0,0 +1,108 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +import pytest + +from dimos import core +from dimos.hardware.camera import zed +from dimos.hardware.camera.module import CameraModule +from dimos.hardware.camera.webcam import Webcam +from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 +from dimos.msgs.sensor_msgs import CameraInfo, Image + + +@pytest.mark.tool +def test_streaming_single() -> None: + dimos = core.start(1) + + camera = dimos.deploy( + CameraModule, + transform=Transform( + translation=Vector3(0.05, 0.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="sensor", + child_frame_id="camera_link", + ), + hardware=lambda: Webcam( + stereo_slice="left", + camera_index=0, + frequency=15, + camera_info=zed.CameraInfo.SingleWebcam, + ), + ) + + camera.image.transport = core.LCMTransport("/image1", Image) + camera.camera_info.transport = core.LCMTransport("/image1/camera_info", CameraInfo) + camera.start() + + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + camera.stop() + dimos.stop() + + +@pytest.mark.tool +def test_streaming_double() -> None: + dimos = core.start(2) + + camera1 = dimos.deploy( + CameraModule, + transform=Transform( + translation=Vector3(0.05, 0.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="sensor", + child_frame_id="camera_link", + ), + hardware=lambda: Webcam( + stereo_slice="left", + camera_index=0, + frequency=15, + camera_info=zed.CameraInfo.SingleWebcam, + ), + ) + + camera2 = dimos.deploy( + CameraModule, + transform=Transform( + translation=Vector3(0.05, 0.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="sensor", + child_frame_id="camera_link", + ), + hardware=lambda: Webcam( + camera_index=4, + frequency=15, + stereo_slice="left", + camera_info=zed.CameraInfo.SingleWebcam, + ), + ) + + camera1.image.transport = core.LCMTransport("/image1", Image) + camera1.camera_info.transport = core.LCMTransport("/image1/camera_info", CameraInfo) + camera1.start() + camera2.image.transport = core.LCMTransport("/image2", Image) + camera2.camera_info.transport = core.LCMTransport("/image2/camera_info", CameraInfo) + camera2.start() + + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + camera1.stop() + camera2.stop() + dimos.stop() diff --git a/dimos/hardware/camera/webcam.py b/dimos/hardware/camera/webcam.py new file mode 100644 index 0000000000..0f68989002 --- /dev/null +++ b/dimos/hardware/camera/webcam.py @@ -0,0 +1,170 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass, field +from functools import cache +import threading +import time +from typing import Literal + +import cv2 +from dimos_lcm.sensor_msgs import CameraInfo +from reactivex import create +from reactivex.observable import Observable + +from dimos.hardware.camera.spec import CameraConfig, CameraHardware +from dimos.msgs.sensor_msgs import Image +from dimos.msgs.sensor_msgs.Image import ImageFormat +from dimos.utils.reactive import backpressure + + +@dataclass +class WebcamConfig(CameraConfig): + camera_index: int = 0 # /dev/videoN + frame_width: int = 640 + frame_height: int = 480 + frequency: int = 15 + camera_info: CameraInfo = field(default_factory=CameraInfo) + frame_id_prefix: str | None = None + stereo_slice: Literal["left", "right"] | None = None # For stereo cameras + + +class Webcam(CameraHardware[WebcamConfig]): + default_config = WebcamConfig + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._capture = None + self._capture_thread = None + self._stop_event = threading.Event() + self._observer = None + + @cache + def image_stream(self) -> Observable[Image]: + """Create an observable that starts/stops camera on subscription""" + + def subscribe(observer, scheduler=None): + # Store the observer so emit() can use it + self._observer = observer + + # Start the camera when someone subscribes + try: + self.start() + except Exception as e: + observer.on_error(e) + return + + # Return a dispose function to stop camera when unsubscribed + def dispose() -> None: + self._observer = None + self.stop() + + return dispose + + return backpressure(create(subscribe)) + + def start(self): + if self._capture_thread and self._capture_thread.is_alive(): + return + + # Open the video capture + self._capture = cv2.VideoCapture(self.config.camera_index) + if not self._capture.isOpened(): + raise RuntimeError(f"Failed to open camera {self.config.camera_index}") + + # Set camera properties + self._capture.set(cv2.CAP_PROP_FRAME_WIDTH, self.config.frame_width) + self._capture.set(cv2.CAP_PROP_FRAME_HEIGHT, self.config.frame_height) + + # Clear stop event and start the capture thread + self._stop_event.clear() + self._capture_thread = threading.Thread(target=self._capture_loop, daemon=True) + self._capture_thread.start() + + def stop(self) -> None: + """Stop capturing frames""" + # Signal thread to stop + self._stop_event.set() + + # Wait for thread to finish + if self._capture_thread and self._capture_thread.is_alive(): + self._capture_thread.join(timeout=(1.0 / self.config.frequency) + 0.1) + + # Release the capture + if self._capture: + self._capture.release() + self._capture = None + + def _frame(self, frame: str): + if not self.config.frame_id_prefix: + return frame + else: + return f"{self.config.frame_id_prefix}/{frame}" + + def capture_frame(self) -> Image: + # Read frame + ret, frame = self._capture.read() + if not ret: + raise RuntimeError(f"Failed to read frame from camera {self.config.camera_index}") + + # Convert BGR to RGB (OpenCV uses BGR by default) + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + + # Create Image message + # Using Image.from_numpy() since it's designed for numpy arrays + # Setting format to RGB since we converted from BGR->RGB above + image = Image.from_numpy( + frame_rgb, + format=ImageFormat.RGB, # We converted to RGB above + frame_id=self._frame("camera_optical"), # Standard frame ID for camera images + ts=time.time(), # Current timestamp + ) + + if self.config.stereo_slice in ("left", "right"): + half_width = image.width // 2 + if self.config.stereo_slice == "left": + image = image.crop(0, 0, half_width, image.height) + else: + image = image.crop(half_width, 0, half_width, image.height) + + return image + + def _capture_loop(self) -> None: + """Capture frames at the configured frequency""" + frame_interval = 1.0 / self.config.frequency + next_frame_time = time.time() + + while self._capture and not self._stop_event.is_set(): + image = self.capture_frame() + + # Emit the image to the observer only if not stopping + if self._observer and not self._stop_event.is_set(): + self._observer.on_next(image) + + # Wait for next frame time or until stopped + next_frame_time += frame_interval + sleep_time = next_frame_time - time.time() + if sleep_time > 0: + # Use event.wait so we can be interrupted by stop + if self._stop_event.wait(timeout=sleep_time): + break # Stop was requested + else: + # We're running behind, reset timing + next_frame_time = time.time() + + @property + def camera_info(self) -> CameraInfo: + return self.config.camera_info + + def emit(self, image: Image) -> None: ... diff --git a/dimos/hardware/camera/zed/__init__.py b/dimos/hardware/camera/zed/__init__.py new file mode 100644 index 0000000000..d7b70a1319 --- /dev/null +++ b/dimos/hardware/camera/zed/__init__.py @@ -0,0 +1,56 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""ZED camera hardware interfaces.""" + +from pathlib import Path + +from dimos.msgs.sensor_msgs.CameraInfo import CalibrationProvider + +# Check if ZED SDK is available +try: + import pyzed.sl as sl + + HAS_ZED_SDK = True +except ImportError: + HAS_ZED_SDK = False + +# Only import ZED classes if SDK is available +if HAS_ZED_SDK: + from dimos.hardware.camera.zed.camera import ZEDCamera, ZEDModule +else: + # Provide stub classes when SDK is not available + class ZEDCamera: + def __init__(self, *args, **kwargs) -> None: + raise ImportError( + "ZED SDK not installed. Please install pyzed package to use ZED camera functionality." + ) + + class ZEDModule: + def __init__(self, *args, **kwargs) -> None: + raise ImportError( + "ZED SDK not installed. Please install pyzed package to use ZED camera functionality." + ) + + +# Set up camera calibration provider (always available) +CALIBRATION_DIR = Path(__file__).parent +CameraInfo = CalibrationProvider(CALIBRATION_DIR) + +__all__ = [ + "HAS_ZED_SDK", + "CameraInfo", + "ZEDCamera", + "ZEDModule", +] diff --git a/dimos/hardware/camera/zed/camera.py b/dimos/hardware/camera/zed/camera.py new file mode 100644 index 0000000000..fdcd93f731 --- /dev/null +++ b/dimos/hardware/camera/zed/camera.py @@ -0,0 +1,874 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from types import TracebackType +from typing import Any + +import cv2 +from dimos_lcm.sensor_msgs import CameraInfo +import numpy as np +import open3d as o3d +import pyzed.sl as sl +from reactivex import interval + +from dimos.core import Module, Out, rpc +from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Transform, Vector3 + +# Import LCM message types +from dimos.msgs.sensor_msgs import Image, ImageFormat +from dimos.msgs.std_msgs import Header +from dimos.protocol.tf import TF +from dimos.utils.logging_config import setup_logger + +logger = setup_logger(__name__) + + +class ZEDCamera: + """ZED Camera capture node with neural depth processing.""" + + def __init__( + self, + camera_id: int = 0, + resolution: sl.RESOLUTION = sl.RESOLUTION.HD720, + depth_mode: sl.DEPTH_MODE = sl.DEPTH_MODE.NEURAL, + fps: int = 30, + **kwargs, + ) -> None: + """ + Initialize ZED Camera. + + Args: + camera_id: Camera ID (0 for first ZED) + resolution: ZED camera resolution + depth_mode: Depth computation mode + fps: Camera frame rate (default: 30) + """ + if sl is None: + raise ImportError("ZED SDK not installed. Please install pyzed package.") + + super().__init__(**kwargs) + + self.camera_id = camera_id + self.resolution = resolution + self.depth_mode = depth_mode + self.fps = fps + + # Initialize ZED camera + self.zed = sl.Camera() + self.init_params = sl.InitParameters() + self.init_params.camera_resolution = resolution + self.init_params.depth_mode = depth_mode + self.init_params.coordinate_system = sl.COORDINATE_SYSTEM.RIGHT_HANDED_Z_UP_X_FWD + self.init_params.coordinate_units = sl.UNIT.METER + self.init_params.camera_fps = fps + + # Set camera ID using the correct parameter name + if hasattr(self.init_params, "set_from_camera_id"): + self.init_params.set_from_camera_id(camera_id) + elif hasattr(self.init_params, "input"): + self.init_params.input.set_from_camera_id(camera_id) + + # Use enable_fill_mode instead of SENSING_MODE.STANDARD + self.runtime_params = sl.RuntimeParameters() + self.runtime_params.enable_fill_mode = True # False = STANDARD mode, True = FILL mode + + # Image containers + self.image_left = sl.Mat() + self.image_right = sl.Mat() + self.depth_map = sl.Mat() + self.point_cloud = sl.Mat() + self.confidence_map = sl.Mat() + + # Positional tracking + self.tracking_enabled = False + self.tracking_params = sl.PositionalTrackingParameters() + self.camera_pose = sl.Pose() + self.sensors_data = sl.SensorsData() + + self.is_opened = False + + def open(self) -> bool: + """Open the ZED camera.""" + try: + err = self.zed.open(self.init_params) + if err != sl.ERROR_CODE.SUCCESS: + logger.error(f"Failed to open ZED camera: {err}") + return False + + self.is_opened = True + logger.info("ZED camera opened successfully") + + # Get camera information + info = self.zed.get_camera_information() + logger.info(f"ZED Camera Model: {info.camera_model}") + logger.info(f"Serial Number: {info.serial_number}") + logger.info(f"Firmware: {info.camera_configuration.firmware_version}") + + return True + + except Exception as e: + logger.error(f"Error opening ZED camera: {e}") + return False + + def enable_positional_tracking( + self, + enable_area_memory: bool = False, + enable_pose_smoothing: bool = True, + enable_imu_fusion: bool = True, + set_floor_as_origin: bool = False, + initial_world_transform: sl.Transform | None = None, + ) -> bool: + """ + Enable positional tracking on the ZED camera. + + Args: + enable_area_memory: Enable area learning to correct tracking drift + enable_pose_smoothing: Enable pose smoothing + enable_imu_fusion: Enable IMU fusion if available + set_floor_as_origin: Set the floor as origin (useful for robotics) + initial_world_transform: Initial world transform + + Returns: + True if tracking enabled successfully + """ + if not self.is_opened: + logger.error("ZED camera not opened") + return False + + try: + # Configure tracking parameters + self.tracking_params.enable_area_memory = enable_area_memory + self.tracking_params.enable_pose_smoothing = enable_pose_smoothing + self.tracking_params.enable_imu_fusion = enable_imu_fusion + self.tracking_params.set_floor_as_origin = set_floor_as_origin + + if initial_world_transform is not None: + self.tracking_params.initial_world_transform = initial_world_transform + + # Enable tracking + err = self.zed.enable_positional_tracking(self.tracking_params) + if err != sl.ERROR_CODE.SUCCESS: + logger.error(f"Failed to enable positional tracking: {err}") + return False + + self.tracking_enabled = True + logger.info("Positional tracking enabled successfully") + return True + + except Exception as e: + logger.error(f"Error enabling positional tracking: {e}") + return False + + def disable_positional_tracking(self) -> None: + """Disable positional tracking.""" + if self.tracking_enabled: + self.zed.disable_positional_tracking() + self.tracking_enabled = False + logger.info("Positional tracking disabled") + + def get_pose( + self, reference_frame: sl.REFERENCE_FRAME = sl.REFERENCE_FRAME.WORLD + ) -> dict[str, Any] | None: + """ + Get the current camera pose. + + Args: + reference_frame: Reference frame (WORLD or CAMERA) + + Returns: + Dictionary containing: + - position: [x, y, z] in meters + - rotation: [x, y, z, w] quaternion + - euler_angles: [roll, pitch, yaw] in radians + - timestamp: Pose timestamp in nanoseconds + - confidence: Tracking confidence (0-100) + - valid: Whether pose is valid + """ + if not self.tracking_enabled: + logger.error("Positional tracking not enabled") + return None + + try: + # Get current pose + tracking_state = self.zed.get_position(self.camera_pose, reference_frame) + + if tracking_state == sl.POSITIONAL_TRACKING_STATE.OK: + # Extract translation + translation = self.camera_pose.get_translation().get() + + # Extract rotation (quaternion) + rotation = self.camera_pose.get_orientation().get() + + # Get Euler angles + euler = self.camera_pose.get_euler_angles() + + return { + "position": translation.tolist(), + "rotation": rotation.tolist(), # [x, y, z, w] + "euler_angles": euler.tolist(), # [roll, pitch, yaw] + "timestamp": self.camera_pose.timestamp.get_nanoseconds(), + "confidence": self.camera_pose.pose_confidence, + "valid": True, + "tracking_state": str(tracking_state), + } + else: + logger.warning(f"Tracking state: {tracking_state}") + return {"valid": False, "tracking_state": str(tracking_state)} + + except Exception as e: + logger.error(f"Error getting pose: {e}") + return None + + def get_imu_data(self) -> dict[str, Any] | None: + """ + Get IMU sensor data if available. + + Returns: + Dictionary containing: + - orientation: IMU orientation quaternion [x, y, z, w] + - angular_velocity: [x, y, z] in rad/s + - linear_acceleration: [x, y, z] in m/s² + - timestamp: IMU data timestamp + """ + if not self.is_opened: + logger.error("ZED camera not opened") + return None + + try: + # Get sensors data synchronized with images + if ( + self.zed.get_sensors_data(self.sensors_data, sl.TIME_REFERENCE.IMAGE) + == sl.ERROR_CODE.SUCCESS + ): + imu = self.sensors_data.get_imu_data() + + # Get IMU orientation + imu_orientation = imu.get_pose().get_orientation().get() + + # Get angular velocity + angular_vel = imu.get_angular_velocity() + + # Get linear acceleration + linear_accel = imu.get_linear_acceleration() + + return { + "orientation": imu_orientation.tolist(), + "angular_velocity": angular_vel.tolist(), + "linear_acceleration": linear_accel.tolist(), + "timestamp": self.sensors_data.timestamp.get_nanoseconds(), + "temperature": self.sensors_data.temperature.get(sl.SENSOR_LOCATION.IMU), + } + else: + return None + + except Exception as e: + logger.error(f"Error getting IMU data: {e}") + return None + + def capture_frame( + self, + ) -> tuple[np.ndarray | None, np.ndarray | None, np.ndarray | None]: + """ + Capture a frame from ZED camera. + + Returns: + Tuple of (left_image, right_image, depth_map) as numpy arrays + """ + if not self.is_opened: + logger.error("ZED camera not opened") + return None, None, None + + try: + # Grab frame + if self.zed.grab(self.runtime_params) == sl.ERROR_CODE.SUCCESS: + # Retrieve left image + self.zed.retrieve_image(self.image_left, sl.VIEW.LEFT) + left_img = self.image_left.get_data()[:, :, :3] # Remove alpha channel + + # Retrieve right image + self.zed.retrieve_image(self.image_right, sl.VIEW.RIGHT) + right_img = self.image_right.get_data()[:, :, :3] # Remove alpha channel + + # Retrieve depth map + self.zed.retrieve_measure(self.depth_map, sl.MEASURE.DEPTH) + depth = self.depth_map.get_data() + + return left_img, right_img, depth + else: + logger.warning("Failed to grab frame from ZED camera") + return None, None, None + + except Exception as e: + logger.error(f"Error capturing frame: {e}") + return None, None, None + + def capture_pointcloud(self) -> o3d.geometry.PointCloud | None: + """ + Capture point cloud from ZED camera. + + Returns: + Open3D point cloud with XYZ coordinates and RGB colors + """ + if not self.is_opened: + logger.error("ZED camera not opened") + return None + + try: + if self.zed.grab(self.runtime_params) == sl.ERROR_CODE.SUCCESS: + # Retrieve point cloud with RGBA data + self.zed.retrieve_measure(self.point_cloud, sl.MEASURE.XYZRGBA) + point_cloud_data = self.point_cloud.get_data() + + # Convert to numpy array format + _height, _width = point_cloud_data.shape[:2] + points = point_cloud_data.reshape(-1, 4) + + # Extract XYZ coordinates + xyz = points[:, :3] + + # Extract and unpack RGBA color data from 4th channel + rgba_packed = points[:, 3].view(np.uint32) + + # Unpack RGBA: each 32-bit value contains 4 bytes (R, G, B, A) + colors_rgba = np.zeros((len(rgba_packed), 4), dtype=np.uint8) + colors_rgba[:, 0] = rgba_packed & 0xFF # R + colors_rgba[:, 1] = (rgba_packed >> 8) & 0xFF # G + colors_rgba[:, 2] = (rgba_packed >> 16) & 0xFF # B + colors_rgba[:, 3] = (rgba_packed >> 24) & 0xFF # A + + # Extract RGB (ignore alpha) and normalize to [0, 1] + colors_rgb = colors_rgba[:, :3].astype(np.float64) / 255.0 + + # Filter out invalid points (NaN or inf) + valid = np.isfinite(xyz).all(axis=1) + valid_xyz = xyz[valid] + valid_colors = colors_rgb[valid] + + # Create Open3D point cloud + pcd = o3d.geometry.PointCloud() + + if len(valid_xyz) > 0: + pcd.points = o3d.utility.Vector3dVector(valid_xyz) + pcd.colors = o3d.utility.Vector3dVector(valid_colors) + + return pcd + else: + logger.warning("Failed to grab frame for point cloud") + return None + + except Exception as e: + logger.error(f"Error capturing point cloud: {e}") + return None + + def capture_frame_with_pose( + self, + ) -> tuple[np.ndarray | None, np.ndarray | None, np.ndarray | None, dict[str, Any] | None]: + """ + Capture a frame with synchronized pose data. + + Returns: + Tuple of (left_image, right_image, depth_map, pose_data) + """ + if not self.is_opened: + logger.error("ZED camera not opened") + return None, None, None, None + + try: + # Grab frame + if self.zed.grab(self.runtime_params) == sl.ERROR_CODE.SUCCESS: + # Get images and depth + left_img, right_img, depth = self.capture_frame() + + # Get synchronized pose if tracking is enabled + pose_data = None + if self.tracking_enabled: + pose_data = self.get_pose() + + return left_img, right_img, depth, pose_data + else: + logger.warning("Failed to grab frame from ZED camera") + return None, None, None, None + + except Exception as e: + logger.error(f"Error capturing frame with pose: {e}") + return None, None, None, None + + def close(self) -> None: + """Close the ZED camera.""" + if self.is_opened: + # Disable tracking if enabled + if self.tracking_enabled: + self.disable_positional_tracking() + + self.zed.close() + self.is_opened = False + logger.info("ZED camera closed") + + def get_camera_info(self) -> dict[str, Any]: + """Get ZED camera information and calibration parameters.""" + if not self.is_opened: + return {} + + try: + info = self.zed.get_camera_information() + calibration = info.camera_configuration.calibration_parameters + + # In ZED SDK 4.0+, the baseline calculation has changed + # Try to get baseline from the stereo parameters + try: + # Method 1: Try to get from stereo parameters if available + if hasattr(calibration, "getCameraBaseline"): + baseline = calibration.getCameraBaseline() + else: + # Method 2: Calculate from left and right camera positions + # The baseline is the distance between left and right cameras + + # Try different ways to get baseline in SDK 4.0+ + if hasattr(info.camera_configuration, "calibration_parameters_raw"): + # Use raw calibration if available + raw_calib = info.camera_configuration.calibration_parameters_raw + if hasattr(raw_calib, "T"): + baseline = abs(raw_calib.T[0]) + else: + baseline = 0.12 # Default ZED-M baseline approximation + else: + # Use default baseline for ZED-M + baseline = 0.12 # ZED-M baseline is approximately 120mm + except: + baseline = 0.12 # Fallback to approximate ZED-M baseline + + return { + "model": str(info.camera_model), + "serial_number": info.serial_number, + "firmware": info.camera_configuration.firmware_version, + "resolution": { + "width": info.camera_configuration.resolution.width, + "height": info.camera_configuration.resolution.height, + }, + "fps": info.camera_configuration.fps, + "left_cam": { + "fx": calibration.left_cam.fx, + "fy": calibration.left_cam.fy, + "cx": calibration.left_cam.cx, + "cy": calibration.left_cam.cy, + "k1": calibration.left_cam.disto[0], + "k2": calibration.left_cam.disto[1], + "p1": calibration.left_cam.disto[2], + "p2": calibration.left_cam.disto[3], + "k3": calibration.left_cam.disto[4], + }, + "right_cam": { + "fx": calibration.right_cam.fx, + "fy": calibration.right_cam.fy, + "cx": calibration.right_cam.cx, + "cy": calibration.right_cam.cy, + "k1": calibration.right_cam.disto[0], + "k2": calibration.right_cam.disto[1], + "p1": calibration.right_cam.disto[2], + "p2": calibration.right_cam.disto[3], + "k3": calibration.right_cam.disto[4], + }, + "baseline": baseline, + } + except Exception as e: + logger.error(f"Error getting camera info: {e}") + return {} + + def calculate_intrinsics(self): + """Calculate camera intrinsics from ZED calibration.""" + info = self.get_camera_info() + if not info: + return super().calculate_intrinsics() + + left_cam = info.get("left_cam", {}) + resolution = info.get("resolution", {}) + + return { + "focal_length_x": left_cam.get("fx", 0), + "focal_length_y": left_cam.get("fy", 0), + "principal_point_x": left_cam.get("cx", 0), + "principal_point_y": left_cam.get("cy", 0), + "baseline": info.get("baseline", 0), + "resolution_width": resolution.get("width", 0), + "resolution_height": resolution.get("height", 0), + } + + def __enter__(self): + """Context manager entry.""" + if not self.open(): + raise RuntimeError("Failed to open ZED camera") + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + """Context manager exit.""" + self.close() + + +class ZEDModule(Module): + """ + Dask module for ZED camera that publishes sensor data via LCM. + + Publishes: + - /zed/color_image: RGB camera images + - /zed/depth_image: Depth images + - /zed/camera_info: Camera calibration information + - /zed/pose: Camera pose (if tracking enabled) + """ + + # Define LCM outputs + color_image: Out[Image] = None + depth_image: Out[Image] = None + camera_info: Out[CameraInfo] = None + pose: Out[PoseStamped] = None + + def __init__( + self, + camera_id: int = 0, + resolution: str = "HD720", + depth_mode: str = "NEURAL", + fps: int = 30, + enable_tracking: bool = True, + enable_imu_fusion: bool = True, + set_floor_as_origin: bool = True, + publish_rate: float = 30.0, + frame_id: str = "zed_camera", + recording_path: str | None = None, + **kwargs, + ) -> None: + """ + Initialize ZED Module. + + Args: + camera_id: Camera ID (0 for first ZED) + resolution: Resolution string ("HD720", "HD1080", "HD2K", "VGA") + depth_mode: Depth mode string ("NEURAL", "ULTRA", "QUALITY", "PERFORMANCE") + fps: Camera frame rate + enable_tracking: Enable positional tracking + enable_imu_fusion: Enable IMU fusion for tracking + set_floor_as_origin: Set floor as origin for tracking + publish_rate: Rate to publish messages (Hz) + frame_id: TF frame ID for messages + recording_path: Path to save recorded data + """ + super().__init__(**kwargs) + + self.camera_id = camera_id + self.fps = fps + self.enable_tracking = enable_tracking + self.enable_imu_fusion = enable_imu_fusion + self.set_floor_as_origin = set_floor_as_origin + self.publish_rate = publish_rate + self.frame_id = frame_id + self.recording_path = recording_path + + # Convert string parameters to ZED enums + self.resolution = getattr(sl.RESOLUTION, resolution, sl.RESOLUTION.HD720) + self.depth_mode = getattr(sl.DEPTH_MODE, depth_mode, sl.DEPTH_MODE.NEURAL) + + # Internal state + self.zed_camera = None + self._running = False + self._subscription = None + self._sequence = 0 + + # Initialize TF publisher + self.tf = TF() + + # Initialize storage for recording if path provided + self.storages = None + if self.recording_path: + from dimos.utils.testing import TimedSensorStorage + + self.storages = { + "color": TimedSensorStorage(f"{self.recording_path}/color"), + "depth": TimedSensorStorage(f"{self.recording_path}/depth"), + "pose": TimedSensorStorage(f"{self.recording_path}/pose"), + "camera_info": TimedSensorStorage(f"{self.recording_path}/camera_info"), + } + logger.info(f"Recording enabled - saving to {self.recording_path}") + + logger.info(f"ZEDModule initialized for camera {camera_id}") + + @rpc + def start(self) -> None: + """Start the ZED module and begin publishing data.""" + if self._running: + logger.warning("ZED module already running") + return + + super().start() + + try: + # Initialize ZED camera + self.zed_camera = ZEDCamera( + camera_id=self.camera_id, + resolution=self.resolution, + depth_mode=self.depth_mode, + fps=self.fps, + ) + + # Open camera + if not self.zed_camera.open(): + logger.error("Failed to open ZED camera") + return + + # Enable tracking if requested + if self.enable_tracking: + success = self.zed_camera.enable_positional_tracking( + enable_imu_fusion=self.enable_imu_fusion, + set_floor_as_origin=self.set_floor_as_origin, + enable_pose_smoothing=True, + enable_area_memory=True, + ) + if not success: + logger.warning("Failed to enable positional tracking") + self.enable_tracking = False + + # Publish camera info once at startup + self._publish_camera_info() + + # Start periodic frame capture and publishing + self._running = True + publish_interval = 1.0 / self.publish_rate + + self._subscription = interval(publish_interval).subscribe( + lambda _: self._capture_and_publish() + ) + + logger.info(f"ZED module started, publishing at {self.publish_rate} Hz") + + except Exception as e: + logger.error(f"Error starting ZED module: {e}") + self._running = False + + @rpc + def stop(self) -> None: + """Stop the ZED module.""" + if not self._running: + return + + self._running = False + + # Stop subscription + if self._subscription: + self._subscription.dispose() + self._subscription = None + + # Close camera + if self.zed_camera: + self.zed_camera.close() + self.zed_camera = None + + super().stop() + + def _capture_and_publish(self) -> None: + """Capture frame and publish all data.""" + if not self._running or not self.zed_camera: + return + + try: + # Capture frame with pose + left_img, _, depth, pose_data = self.zed_camera.capture_frame_with_pose() + + if left_img is None or depth is None: + return + + # Save raw color data if recording + if self.storages and left_img is not None: + self.storages["color"].save_one(left_img) + + # Save raw depth data if recording + if self.storages and depth is not None: + self.storages["depth"].save_one(depth) + + # Save raw pose data if recording + if self.storages and pose_data: + self.storages["pose"].save_one(pose_data) + + # Create header + header = Header(self.frame_id) + self._sequence += 1 + + # Publish color image + self._publish_color_image(left_img, header) + + # Publish depth image + self._publish_depth_image(depth, header) + + # Publish camera info periodically + self._publish_camera_info() + + # Publish pose if tracking enabled and valid + if self.enable_tracking and pose_data and pose_data.get("valid", False): + self._publish_pose(pose_data, header) + + except Exception as e: + logger.error(f"Error in capture and publish: {e}") + + def _publish_color_image(self, image: np.ndarray, header: Header) -> None: + """Publish color image as LCM message.""" + try: + # Convert BGR to RGB if needed + if len(image.shape) == 3 and image.shape[2] == 3: + image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + else: + image_rgb = image + + # Create LCM Image message + msg = Image( + data=image_rgb, + format=ImageFormat.RGB, + frame_id=header.frame_id, + ts=header.ts, + ) + + self.color_image.publish(msg) + + except Exception as e: + logger.error(f"Error publishing color image: {e}") + + def _publish_depth_image(self, depth: np.ndarray, header: Header) -> None: + """Publish depth image as LCM message.""" + try: + # Depth is float32 in meters + msg = Image( + data=depth, + format=ImageFormat.DEPTH, + frame_id=header.frame_id, + ts=header.ts, + ) + self.depth_image.publish(msg) + + except Exception as e: + logger.error(f"Error publishing depth image: {e}") + + def _publish_camera_info(self) -> None: + """Publish camera calibration information.""" + try: + info = self.zed_camera.get_camera_info() + if not info: + return + + # Save raw camera info if recording + if self.storages: + self.storages["camera_info"].save_one(info) + + # Get calibration parameters + left_cam = info.get("left_cam", {}) + resolution = info.get("resolution", {}) + + # Create CameraInfo message + header = Header(self.frame_id) + + # Create camera matrix K (3x3) + K = [ + left_cam.get("fx", 0), + 0, + left_cam.get("cx", 0), + 0, + left_cam.get("fy", 0), + left_cam.get("cy", 0), + 0, + 0, + 1, + ] + + # Distortion coefficients + D = [ + left_cam.get("k1", 0), + left_cam.get("k2", 0), + left_cam.get("p1", 0), + left_cam.get("p2", 0), + left_cam.get("k3", 0), + ] + + # Identity rotation matrix + R = [1, 0, 0, 0, 1, 0, 0, 0, 1] + + # Projection matrix P (3x4) + P = [ + left_cam.get("fx", 0), + 0, + left_cam.get("cx", 0), + 0, + 0, + left_cam.get("fy", 0), + left_cam.get("cy", 0), + 0, + 0, + 0, + 1, + 0, + ] + + msg = CameraInfo( + D_length=len(D), + header=header, + height=resolution.get("height", 0), + width=resolution.get("width", 0), + distortion_model="plumb_bob", + D=D, + K=K, + R=R, + P=P, + binning_x=0, + binning_y=0, + ) + + self.camera_info.publish(msg) + + except Exception as e: + logger.error(f"Error publishing camera info: {e}") + + def _publish_pose(self, pose_data: dict[str, Any], header: Header) -> None: + """Publish camera pose as PoseStamped message and TF transform.""" + try: + position = pose_data.get("position", [0, 0, 0]) + rotation = pose_data.get("rotation", [0, 0, 0, 1]) # quaternion [x,y,z,w] + + # Create PoseStamped message + msg = PoseStamped(ts=header.ts, position=position, orientation=rotation) + self.pose.publish(msg) + + # Publish TF transform + camera_tf = Transform( + translation=Vector3(position), + rotation=Quaternion(rotation), + frame_id="zed_world", + child_frame_id="zed_camera_link", + ts=header.ts, + ) + self.tf.publish(camera_tf) + + except Exception as e: + logger.error(f"Error publishing pose: {e}") + + @rpc + def get_camera_info(self) -> dict[str, Any]: + """Get camera information and calibration parameters.""" + if self.zed_camera: + return self.zed_camera.get_camera_info() + return {} + + @rpc + def get_pose(self) -> dict[str, Any] | None: + """Get current camera pose if tracking is enabled.""" + if self.zed_camera and self.enable_tracking: + return self.zed_camera.get_pose() + return None diff --git a/dimos/hardware/camera/zed/single_webcam.yaml b/dimos/hardware/camera/zed/single_webcam.yaml new file mode 100644 index 0000000000..1ce9457559 --- /dev/null +++ b/dimos/hardware/camera/zed/single_webcam.yaml @@ -0,0 +1,27 @@ +# for cv2.VideoCapture and cutting only half of the frame +image_width: 640 +image_height: 376 +camera_name: zed_webcam_single +camera_matrix: + rows: 3 + cols: 3 + data: [379.45267, 0. , 302.43516, + 0. , 380.67871, 228.00954, + 0. , 0. , 1. ] +distortion_model: plumb_bob +distortion_coefficients: + rows: 1 + cols: 5 + data: [-0.309435, 0.092185, -0.009059, 0.003708, 0.000000] +rectification_matrix: + rows: 3 + cols: 3 + data: [1., 0., 0., + 0., 1., 0., + 0., 0., 1.] +projection_matrix: + rows: 3 + cols: 4 + data: [291.12888, 0. , 304.94086, 0. , + 0. , 347.95022, 231.8885 , 0. , + 0. , 0. , 1. , 0. ] diff --git a/dimos/hardware/camera/zed/test_zed.py b/dimos/hardware/camera/zed/test_zed.py new file mode 100644 index 0000000000..33810d3c2a --- /dev/null +++ b/dimos/hardware/camera/zed/test_zed.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.msgs.sensor_msgs.CameraInfo import CameraInfo + + +def test_zed_import_and_calibration_access() -> None: + """Test that zed module can be imported and calibrations accessed.""" + # Import zed module from camera + from dimos.hardware.camera import zed + + # Test that CameraInfo is accessible + assert hasattr(zed, "CameraInfo") + + # Test snake_case access + camera_info_snake = zed.CameraInfo.single_webcam + assert isinstance(camera_info_snake, CameraInfo) + assert camera_info_snake.width == 640 + assert camera_info_snake.height == 376 + assert camera_info_snake.distortion_model == "plumb_bob" + + # Test PascalCase access + camera_info_pascal = zed.CameraInfo.SingleWebcam + assert isinstance(camera_info_pascal, CameraInfo) + assert camera_info_pascal.width == 640 + assert camera_info_pascal.height == 376 + + # Verify both access methods return the same cached object + assert camera_info_snake is camera_info_pascal + + print("✓ ZED import and calibration access test passed!") diff --git a/dimos/hardware/can_activate.sh b/dimos/hardware/can_activate.sh new file mode 100644 index 0000000000..60cc95e7ea --- /dev/null +++ b/dimos/hardware/can_activate.sh @@ -0,0 +1,138 @@ +#!/bin/bash + +# The default CAN name can be set by the user via command-line parameters. +DEFAULT_CAN_NAME="${1:-can0}" + +# The default bitrate for a single CAN module can be set by the user via command-line parameters. +DEFAULT_BITRATE="${2:-1000000}" + +# USB hardware address (optional parameter) +USB_ADDRESS="${3}" +echo "-------------------START-----------------------" +# Check if ethtool is installed. +if ! dpkg -l | grep -q "ethtool"; then + echo "\e[31mError: ethtool not detected in the system.\e[0m" + echo "Please use the following command to install ethtool:" + echo "sudo apt update && sudo apt install ethtool" + exit 1 +fi + +# Check if can-utils is installed. +if ! dpkg -l | grep -q "can-utils"; then + echo "\e[31mError: can-utils not detected in the system.\e[0m" + echo "Please use the following command to install ethtool:" + echo "sudo apt update && sudo apt install can-utils" + exit 1 +fi + +echo "Both ethtool and can-utils are installed." + +# Retrieve the number of CAN modules in the current system. +CURRENT_CAN_COUNT=$(ip link show type can | grep -c "link/can") + +# Verify if the number of CAN modules in the current system matches the expected value. +if [ "$CURRENT_CAN_COUNT" -ne "1" ]; then + if [ -z "$USB_ADDRESS" ]; then + # Iterate through all CAN interfaces. + for iface in $(ip -br link show type can | awk '{print $1}'); do + # Use ethtool to retrieve bus-info. + BUS_INFO=$(sudo ethtool -i "$iface" | grep "bus-info" | awk '{print $2}') + + if [ -z "$BUS_INFO" ];then + echo "Error: Unable to retrieve bus-info for interface $iface." + continue + fi + + echo "Interface $iface is inserted into USB port $BUS_INFO" + done + echo -e " \e[31m Error: The number of CAN modules detected by the system ($CURRENT_CAN_COUNT) does not match the expected number (1). \e[0m" + echo -e " \e[31m Please add the USB hardware address parameter, such as: \e[0m" + echo -e " bash can_activate.sh can0 1000000 1-2:1.0" + echo "-------------------ERROR-----------------------" + exit 1 + fi +fi + +# Load the gs_usb module. +# sudo modprobe gs_usb +# if [ $? -ne 0 ]; then +# echo "Error: Unable to load the gs_usb module." +# exit 1 +# fi + +if [ -n "$USB_ADDRESS" ]; then + echo "Detected USB hardware address parameter: $USB_ADDRESS" + + # Use ethtool to find the CAN interface corresponding to the USB hardware address. + INTERFACE_NAME="" + for iface in $(ip -br link show type can | awk '{print $1}'); do + BUS_INFO=$(sudo ethtool -i "$iface" | grep "bus-info" | awk '{print $2}') + if [ "$BUS_INFO" = "$USB_ADDRESS" ]; then + INTERFACE_NAME="$iface" + break + fi + done + + if [ -z "$INTERFACE_NAME" ]; then + echo "Error: Unable to find CAN interface corresponding to USB hardware address $USB_ADDRESS." + exit 1 + else + echo "Found the interface corresponding to USB hardware address $USB_ADDRESS: $INTERFACE_NAME." + fi +else + # Retrieve the unique CAN interface. + INTERFACE_NAME=$(ip -br link show type can | awk '{print $1}') + + # Check if the interface name has been retrieved. + if [ -z "$INTERFACE_NAME" ]; then + echo "Error: Unable to detect CAN interface." + exit 1 + fi + BUS_INFO=$(sudo ethtool -i "$INTERFACE_NAME" | grep "bus-info" | awk '{print $2}') + echo "Expected to configure a single CAN module, detected interface $INTERFACE_NAME with corresponding USB address $BUS_INFO." +fi + +# Check if the current interface is already activated. +IS_LINK_UP=$(ip link show "$INTERFACE_NAME" | grep -q "UP" && echo "yes" || echo "no") + +# Retrieve the bitrate of the current interface. +CURRENT_BITRATE=$(ip -details link show "$INTERFACE_NAME" | grep -oP 'bitrate \K\d+') + +if [ "$IS_LINK_UP" = "yes" ] && [ "$CURRENT_BITRATE" -eq "$DEFAULT_BITRATE" ]; then + echo "Interface $INTERFACE_NAME is already activated with a bitrate of $DEFAULT_BITRATE." + + # Check if the interface name matches the default name. + if [ "$INTERFACE_NAME" != "$DEFAULT_CAN_NAME" ]; then + echo "Rename interface $INTERFACE_NAME to $DEFAULT_CAN_NAME." + sudo ip link set "$INTERFACE_NAME" down + sudo ip link set "$INTERFACE_NAME" name "$DEFAULT_CAN_NAME" + sudo ip link set "$DEFAULT_CAN_NAME" up + echo "The interface has been renamed to $DEFAULT_CAN_NAME and reactivated." + else + echo "The interface name is already $DEFAULT_CAN_NAME." + fi +else + # If the interface is not activated or the bitrate is different, configure it. + if [ "$IS_LINK_UP" = "yes" ]; then + echo "Interface $INTERFACE_NAME is already activated, but the bitrate is $CURRENT_BITRATE, which does not match the set value of $DEFAULT_BITRATE." + else + echo "Interface $INTERFACE_NAME is not activated or bitrate is not set." + fi + + # Set the interface bitrate and activate it. + sudo ip link set "$INTERFACE_NAME" down + sudo ip link set "$INTERFACE_NAME" type can bitrate $DEFAULT_BITRATE + sudo ip link set "$INTERFACE_NAME" up + echo "Interface $INTERFACE_NAME has been reset to bitrate $DEFAULT_BITRATE and activated." + + # Rename the interface to the default name. + if [ "$INTERFACE_NAME" != "$DEFAULT_CAN_NAME" ]; then + echo "Rename interface $INTERFACE_NAME to $DEFAULT_CAN_NAME." + sudo ip link set "$INTERFACE_NAME" down + sudo ip link set "$INTERFACE_NAME" name "$DEFAULT_CAN_NAME" + sudo ip link set "$DEFAULT_CAN_NAME" up + echo "The interface has been renamed to $DEFAULT_CAN_NAME and reactivated." + fi +fi + +echo "-------------------OVER------------------------" diff --git a/dimos/hardware/end_effector.py b/dimos/hardware/end_effector.py index fd2c130610..1c5eb08281 100644 --- a/dimos/hardware/end_effector.py +++ b/dimos/hardware/end_effector.py @@ -12,8 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. + class EndEffector: - def __init__(self, effector_type=None): + def __init__(self, effector_type=None) -> None: self.effector_type = effector_type def get_effector_type(self): diff --git a/dimos/hardware/fake_zed_module.py b/dimos/hardware/fake_zed_module.py new file mode 100644 index 0000000000..c4c46c33b3 --- /dev/null +++ b/dimos/hardware/fake_zed_module.py @@ -0,0 +1,282 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +FakeZEDModule - Replays recorded ZED data for testing without hardware. +""" + +import functools +import logging + +from dimos_lcm.sensor_msgs import CameraInfo +import numpy as np + +from dimos.core import Module, Out, rpc +from dimos.msgs.geometry_msgs import PoseStamped +from dimos.msgs.sensor_msgs import Image, ImageFormat +from dimos.msgs.std_msgs import Header +from dimos.protocol.tf import TF +from dimos.utils.logging_config import setup_logger +from dimos.utils.testing import TimedSensorReplay + +logger = setup_logger(__name__, level=logging.INFO) + + +class FakeZEDModule(Module): + """ + Fake ZED module that replays recorded data instead of real camera. + """ + + # Define LCM outputs (same as ZEDModule) + color_image: Out[Image] = None + depth_image: Out[Image] = None + camera_info: Out[CameraInfo] = None + pose: Out[PoseStamped] = None + + def __init__(self, recording_path: str, frame_id: str = "zed_camera", **kwargs) -> None: + """ + Initialize FakeZEDModule with recording path. + + Args: + recording_path: Path to recorded data directory + frame_id: TF frame ID for messages + """ + super().__init__(**kwargs) + + self.recording_path = recording_path + self.frame_id = frame_id + self._running = False + + # Initialize TF publisher + self.tf = TF() + + logger.info(f"FakeZEDModule initialized with recording: {self.recording_path}") + + @functools.cache + def _get_color_stream(self): + """Get cached color image stream.""" + logger.info(f"Loading color image stream from {self.recording_path}/color") + + def image_autocast(x): + """Convert raw numpy array to Image.""" + if isinstance(x, np.ndarray): + return Image(data=x, format=ImageFormat.RGB) + elif isinstance(x, Image): + return x + return x + + color_replay = TimedSensorReplay(f"{self.recording_path}/color", autocast=image_autocast) + return color_replay.stream() + + @functools.cache + def _get_depth_stream(self): + """Get cached depth image stream.""" + logger.info(f"Loading depth image stream from {self.recording_path}/depth") + + def depth_autocast(x): + """Convert raw numpy array to depth Image.""" + if isinstance(x, np.ndarray): + # Depth images are float32 + return Image(data=x, format=ImageFormat.DEPTH) + elif isinstance(x, Image): + return x + return x + + depth_replay = TimedSensorReplay(f"{self.recording_path}/depth", autocast=depth_autocast) + return depth_replay.stream() + + @functools.cache + def _get_pose_stream(self): + """Get cached pose stream.""" + logger.info(f"Loading pose stream from {self.recording_path}/pose") + + def pose_autocast(x): + """Convert raw pose dict to PoseStamped.""" + if isinstance(x, dict): + import time + + return PoseStamped( + position=x.get("position", [0, 0, 0]), + orientation=x.get("rotation", [0, 0, 0, 1]), + ts=time.time(), + ) + elif isinstance(x, PoseStamped): + return x + return x + + pose_replay = TimedSensorReplay(f"{self.recording_path}/pose", autocast=pose_autocast) + return pose_replay.stream() + + @functools.cache + def _get_camera_info_stream(self): + """Get cached camera info stream.""" + logger.info(f"Loading camera info stream from {self.recording_path}/camera_info") + + def camera_info_autocast(x): + """Convert raw camera info dict to CameraInfo message.""" + if isinstance(x, dict): + # Extract calibration parameters + left_cam = x.get("left_cam", {}) + resolution = x.get("resolution", {}) + + # Create CameraInfo message + header = Header(self.frame_id) + + # Create camera matrix K (3x3) + K = [ + left_cam.get("fx", 0), + 0, + left_cam.get("cx", 0), + 0, + left_cam.get("fy", 0), + left_cam.get("cy", 0), + 0, + 0, + 1, + ] + + # Distortion coefficients + D = [ + left_cam.get("k1", 0), + left_cam.get("k2", 0), + left_cam.get("p1", 0), + left_cam.get("p2", 0), + left_cam.get("k3", 0), + ] + + # Identity rotation matrix + R = [1, 0, 0, 0, 1, 0, 0, 0, 1] + + # Projection matrix P (3x4) + P = [ + left_cam.get("fx", 0), + 0, + left_cam.get("cx", 0), + 0, + 0, + left_cam.get("fy", 0), + left_cam.get("cy", 0), + 0, + 0, + 0, + 1, + 0, + ] + + return CameraInfo( + D_length=len(D), + header=header, + height=resolution.get("height", 0), + width=resolution.get("width", 0), + distortion_model="plumb_bob", + D=D, + K=K, + R=R, + P=P, + binning_x=0, + binning_y=0, + ) + elif isinstance(x, CameraInfo): + return x + return x + + info_replay = TimedSensorReplay( + f"{self.recording_path}/camera_info", autocast=camera_info_autocast + ) + return info_replay.stream() + + @rpc + def start(self) -> None: + """Start replaying recorded data.""" + super().start() + + if self._running: + logger.warning("FakeZEDModule already running") + return + + logger.info("Starting FakeZEDModule replay...") + + self._running = True + + # Subscribe to all streams and publish + try: + # Color image stream + unsub = self._get_color_stream().subscribe( + lambda msg: self.color_image.publish(msg) if self._running else None + ) + self._disposables.add(unsub) + logger.info("Started color image replay stream") + except Exception as e: + logger.warning(f"Color image stream not available: {e}") + + try: + # Depth image stream + unsub = self._get_depth_stream().subscribe( + lambda msg: self.depth_image.publish(msg) if self._running else None + ) + self._disposables.add(unsub) + logger.info("Started depth image replay stream") + except Exception as e: + logger.warning(f"Depth image stream not available: {e}") + + try: + # Pose stream + unsub = self._get_pose_stream().subscribe( + lambda msg: self._publish_pose(msg) if self._running else None + ) + self._disposables.add(unsub) + logger.info("Started pose replay stream") + except Exception as e: + logger.warning(f"Pose stream not available: {e}") + + try: + # Camera info stream + unsub = self._get_camera_info_stream().subscribe( + lambda msg: self.camera_info.publish(msg) if self._running else None + ) + self._disposables.add(unsub) + logger.info("Started camera info replay stream") + except Exception as e: + logger.warning(f"Camera info stream not available: {e}") + + logger.info("FakeZEDModule replay started") + + @rpc + def stop(self) -> None: + if not self._running: + return + + self._running = False + + super().stop() + + def _publish_pose(self, msg) -> None: + """Publish pose and TF transform.""" + if msg: + self.pose.publish(msg) + + # Publish TF transform from world to camera + import time + + from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 + + transform = Transform( + translation=Vector3(*msg.position), + rotation=Quaternion(*msg.orientation), + frame_id="world", + child_frame_id=self.frame_id, + ts=time.time(), + ) + self.tf.publish(transform) diff --git a/dimos/hardware/gstreamer_camera.py b/dimos/hardware/gstreamer_camera.py new file mode 100644 index 0000000000..38ede23ee1 --- /dev/null +++ b/dimos/hardware/gstreamer_camera.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import sys +import threading +import time + +import numpy as np + +from dimos.core import Module, Out, rpc +from dimos.msgs.sensor_msgs import Image, ImageFormat +from dimos.utils.logging_config import setup_logger + +# Add system path for gi module if needed +if "/usr/lib/python3/dist-packages" not in sys.path: + sys.path.insert(0, "/usr/lib/python3/dist-packages") + +import gi + +gi.require_version("Gst", "1.0") +gi.require_version("GstApp", "1.0") +from gi.repository import GLib, Gst + +logger = setup_logger("dimos.hardware.gstreamer_camera", level=logging.INFO) + +Gst.init(None) + + +class GstreamerCameraModule(Module): + """Module that captures frames from a remote camera using GStreamer TCP with absolute timestamps.""" + + video: Out[Image] = None + + def __init__( + self, + host: str = "localhost", + port: int = 5000, + frame_id: str = "camera", + timestamp_offset: float = 0.0, + reconnect_interval: float = 5.0, + *args, + **kwargs, + ) -> None: + """Initialize the GStreamer TCP camera module. + + Args: + host: TCP server host to connect to + port: TCP server port + frame_id: Frame ID for the published images + timestamp_offset: Offset to add to timestamps (useful for clock synchronization) + reconnect_interval: Seconds to wait before attempting reconnection + """ + self.host = host + self.port = port + self.frame_id = frame_id + self.timestamp_offset = timestamp_offset + self.reconnect_interval = reconnect_interval + + self.pipeline = None + self.appsink = None + self.main_loop = None + self.main_loop_thread = None + self.running = False + self.should_reconnect = False + self.frame_count = 0 + self.last_log_time = time.time() + self.reconnect_timer_id = None + + Module.__init__(self, *args, **kwargs) + + @rpc + def start(self) -> None: + if self.running: + logger.warning("GStreamer camera module is already running") + return + + super().start() + + self.should_reconnect = True + self._connect() + + @rpc + def stop(self) -> None: + self.should_reconnect = False + self._cleanup_reconnect_timer() + + if not self.running: + return + + self.running = False + + if self.pipeline: + self.pipeline.set_state(Gst.State.NULL) + + if self.main_loop: + self.main_loop.quit() + + # Only join the thread if we're not calling from within it + if self.main_loop_thread and self.main_loop_thread != threading.current_thread(): + self.main_loop_thread.join(timeout=2.0) + + super().stop() + + def _connect(self) -> None: + if not self.should_reconnect: + return + + try: + self._create_pipeline() + self._start_pipeline() + self.running = True + logger.info(f"GStreamer TCP camera module connected to {self.host}:{self.port}") + except Exception as e: + logger.error(f"Failed to connect to {self.host}:{self.port}: {e}") + self._schedule_reconnect() + + def _cleanup_reconnect_timer(self) -> None: + if self.reconnect_timer_id: + GLib.source_remove(self.reconnect_timer_id) + self.reconnect_timer_id = None + + def _schedule_reconnect(self) -> None: + if not self.should_reconnect: + return + + self._cleanup_reconnect_timer() + logger.info(f"Scheduling reconnect in {self.reconnect_interval} seconds...") + self.reconnect_timer_id = GLib.timeout_add_seconds( + int(self.reconnect_interval), self._reconnect_timeout + ) + + def _reconnect_timeout(self) -> bool: + self.reconnect_timer_id = None + if self.should_reconnect: + logger.info("Attempting to reconnect...") + self._connect() + return False # Don't repeat the timeout + + def _handle_disconnect(self) -> None: + if not self.should_reconnect: + return + + self.running = False + + if self.pipeline: + self.pipeline.set_state(Gst.State.NULL) + self.pipeline = None + + self.appsink = None + + logger.warning(f"Disconnected from {self.host}:{self.port}") + self._schedule_reconnect() + + def _create_pipeline(self): + # TCP client source with Matroska demuxer to extract absolute timestamps + pipeline_str = f""" + tcpclientsrc host={self.host} port={self.port} ! + matroskademux name=demux ! + h264parse ! + avdec_h264 ! + videoconvert ! + video/x-raw,format=BGR ! + appsink name=sink emit-signals=true sync=false max-buffers=1 drop=true + """ + + try: + self.pipeline = Gst.parse_launch(pipeline_str) + self.appsink = self.pipeline.get_by_name("sink") + self.appsink.connect("new-sample", self._on_new_sample) + except Exception as e: + logger.error(f"Failed to create GStreamer pipeline: {e}") + raise + + def _start_pipeline(self): + """Start the GStreamer pipeline and main loop.""" + self.main_loop = GLib.MainLoop() + + # Start the pipeline + ret = self.pipeline.set_state(Gst.State.PLAYING) + if ret == Gst.StateChangeReturn.FAILURE: + logger.error("Unable to set the pipeline to playing state") + raise RuntimeError("Failed to start GStreamer pipeline") + + # Run the main loop in a separate thread + self.main_loop_thread = threading.Thread(target=self._run_main_loop) + self.main_loop_thread.daemon = True + self.main_loop_thread.start() + + # Set up bus message handling + bus = self.pipeline.get_bus() + bus.add_signal_watch() + bus.connect("message", self._on_bus_message) + + def _run_main_loop(self) -> None: + try: + self.main_loop.run() + except Exception as e: + logger.error(f"Main loop error: {e}") + + def _on_bus_message(self, bus, message) -> None: + t = message.type + + if t == Gst.MessageType.EOS: + logger.info("End of stream - server disconnected") + self._handle_disconnect() + elif t == Gst.MessageType.ERROR: + err, debug = message.parse_error() + logger.error(f"GStreamer error: {err}, {debug}") + self._handle_disconnect() + elif t == Gst.MessageType.WARNING: + warn, debug = message.parse_warning() + logger.warning(f"GStreamer warning: {warn}, {debug}") + elif t == Gst.MessageType.STATE_CHANGED: + if message.src == self.pipeline: + _old_state, new_state, _pending_state = message.parse_state_changed() + if new_state == Gst.State.PLAYING: + logger.info("Pipeline is now playing - connected to TCP server") + + def _on_new_sample(self, appsink): + """Handle new video samples from the appsink.""" + sample = appsink.emit("pull-sample") + if sample is None: + return Gst.FlowReturn.OK + + buffer = sample.get_buffer() + caps = sample.get_caps() + + # Extract video format information + struct = caps.get_structure(0) + width = struct.get_value("width") + height = struct.get_value("height") + + # Get the absolute timestamp from the buffer + # Matroska preserves the absolute timestamps we set in the sender + if buffer.pts != Gst.CLOCK_TIME_NONE: + # Convert nanoseconds to seconds and add offset + # This is the absolute time from when the frame was captured + timestamp = (buffer.pts / 1e9) + self.timestamp_offset + + # Skip frames with invalid timestamps (before year 2000) + # This filters out initial gray frames with relative timestamps + year_2000_timestamp = 946684800.0 # January 1, 2000 00:00:00 UTC + if timestamp < year_2000_timestamp: + logger.debug(f"Skipping frame with invalid timestamp: {timestamp:.6f}") + return Gst.FlowReturn.OK + + else: + return Gst.FlowReturn.OK + + # Map the buffer to access the data + success, map_info = buffer.map(Gst.MapFlags.READ) + if not success: + logger.error("Failed to map buffer") + return Gst.FlowReturn.ERROR + + try: + # Convert buffer data to numpy array + # The videoconvert element outputs BGR format + data = np.frombuffer(map_info.data, dtype=np.uint8) + + # Reshape to image dimensions + # For BGR format, we have 3 channels + image_array = data.reshape((height, width, 3)) + + # Create an Image message with the absolute timestamp + image_msg = Image( + data=image_array.copy(), # Make a copy to ensure data persistence + format=ImageFormat.BGR, + frame_id=self.frame_id, + ts=timestamp, + ) + + # Publish the image + if self.video and self.running: + self.video.publish(image_msg) + + # Log statistics periodically + self.frame_count += 1 + current_time = time.time() + if current_time - self.last_log_time >= 5.0: + fps = self.frame_count / (current_time - self.last_log_time) + logger.debug( + f"Receiving frames - FPS: {fps:.1f}, Resolution: {width}x{height}, " + f"Absolute timestamp: {timestamp:.6f}" + ) + self.frame_count = 0 + self.last_log_time = current_time + + except Exception as e: + logger.error(f"Error processing frame: {e}") + + finally: + buffer.unmap(map_info) + + return Gst.FlowReturn.OK diff --git a/dimos/hardware/gstreamer_camera_test_script.py b/dimos/hardware/gstreamer_camera_test_script.py new file mode 100755 index 0000000000..f815579c0d --- /dev/null +++ b/dimos/hardware/gstreamer_camera_test_script.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import logging +import time + +from dimos import core +from dimos.hardware.gstreamer_camera import GstreamerCameraModule +from dimos.msgs.sensor_msgs import Image +from dimos.protocol import pubsub + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Test script for GStreamer TCP camera module") + + # Network options + parser.add_argument( + "--host", default="localhost", help="TCP server host to connect to (default: localhost)" + ) + parser.add_argument("--port", type=int, default=5000, help="TCP server port (default: 5000)") + + # Camera options + parser.add_argument( + "--frame-id", + default="zed_camera", + help="Frame ID for published images (default: zed_camera)", + ) + parser.add_argument( + "--reconnect-interval", + type=float, + default=5.0, + help="Seconds to wait before attempting reconnection (default: 5.0)", + ) + + # Logging options + parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") + + args = parser.parse_args() + + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + + # Initialize LCM + pubsub.lcm.autoconf() + + # Start dimos + logger.info("Starting dimos...") + dimos = core.start(8) + + # Deploy the GStreamer camera module + logger.info(f"Deploying GStreamer TCP camera module (connecting to {args.host}:{args.port})...") + camera = dimos.deploy( + GstreamerCameraModule, + host=args.host, + port=args.port, + frame_id=args.frame_id, + reconnect_interval=args.reconnect_interval, + ) + + # Set up LCM transport for the video output + camera.video.transport = core.LCMTransport("/zed/video", Image) + + # Counter for received frames + frame_count = [0] + last_log_time = [time.time()] + first_timestamp = [None] + + def on_frame(msg) -> None: + frame_count[0] += 1 + current_time = time.time() + + # Capture first timestamp to show absolute timestamps are preserved + if first_timestamp[0] is None: + first_timestamp[0] = msg.ts + logger.info(f"First frame absolute timestamp: {msg.ts:.6f}") + + # Log stats every 2 seconds + if current_time - last_log_time[0] >= 2.0: + fps = frame_count[0] / (current_time - last_log_time[0]) + timestamp_delta = msg.ts - first_timestamp[0] + logger.info( + f"Received {frame_count[0]} frames - FPS: {fps:.1f} - " + f"Resolution: {msg.width}x{msg.height} - " + f"Timestamp: {msg.ts:.3f} (delta: {timestamp_delta:.3f}s)" + ) + frame_count[0] = 0 + last_log_time[0] = current_time + + # Subscribe to video output for monitoring + camera.video.subscribe(on_frame) + + # Start the camera + logger.info("Starting GStreamer camera...") + camera.start() + + logger.info("GStreamer TCP camera module is running. Press Ctrl+C to stop.") + logger.info(f"Connecting to TCP server at {args.host}:{args.port}") + logger.info("Publishing frames to LCM topic: /zed/video") + logger.info("") + logger.info("To start the sender on the camera machine, run:") + logger.info( + f" python3 dimos/hardware/gstreamer_sender.py --device /dev/video0 --host 0.0.0.0 --port {args.port}" + ) + + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + logger.info("Shutting down...") + camera.stop() + logger.info("Stopped.") + + +if __name__ == "__main__": + main() diff --git a/dimos/hardware/gstreamer_sender.py b/dimos/hardware/gstreamer_sender.py new file mode 100755 index 0000000000..ce7c1d6145 --- /dev/null +++ b/dimos/hardware/gstreamer_sender.py @@ -0,0 +1,359 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import logging +import signal +import sys +import time + +# Add system path for gi module if needed +if "/usr/lib/python3/dist-packages" not in sys.path: + sys.path.insert(0, "/usr/lib/python3/dist-packages") + +import gi + +gi.require_version("Gst", "1.0") +gi.require_version("GstVideo", "1.0") +from gi.repository import GLib, Gst + +# Initialize GStreamer +Gst.init(None) + +# Setup logging +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger("gstreamer_tcp_sender") + + +class GStreamerTCPSender: + def __init__( + self, + device: str = "/dev/video0", + width: int = 2560, + height: int = 720, + framerate: int = 60, + format_str: str = "YUY2", + bitrate: int = 5000, + host: str = "0.0.0.0", + port: int = 5000, + single_camera: bool = False, + ) -> None: + """Initialize the GStreamer TCP sender. + + Args: + device: Video device path + width: Video width in pixels + height: Video height in pixels + framerate: Frame rate in fps + format_str: Video format + bitrate: H264 encoding bitrate in kbps + host: Host to listen on (0.0.0.0 for all interfaces) + port: TCP port for listening + single_camera: If True, crop to left half (for stereo cameras) + """ + self.device = device + self.width = width + self.height = height + self.framerate = framerate + self.format = format_str + self.bitrate = bitrate + self.host = host + self.port = port + self.single_camera = single_camera + + self.pipeline = None + self.videosrc = None + self.encoder = None + self.mux = None + self.main_loop = None + self.running = False + self.start_time = None + self.frame_count = 0 + + def create_pipeline(self): + """Create the GStreamer pipeline with TCP server sink.""" + + # Create pipeline + self.pipeline = Gst.Pipeline.new("tcp-sender-pipeline") + + # Create elements + self.videosrc = Gst.ElementFactory.make("v4l2src", "source") + self.videosrc.set_property("device", self.device) + self.videosrc.set_property("do-timestamp", True) + logger.info(f"Using camera device: {self.device}") + + # Create caps filter for video format + capsfilter = Gst.ElementFactory.make("capsfilter", "capsfilter") + caps = Gst.Caps.from_string( + f"video/x-raw,width={self.width},height={self.height}," + f"format={self.format},framerate={self.framerate}/1" + ) + capsfilter.set_property("caps", caps) + + # Video converter + videoconvert = Gst.ElementFactory.make("videoconvert", "convert") + + # Crop element for single camera mode + videocrop = None + if self.single_camera: + videocrop = Gst.ElementFactory.make("videocrop", "crop") + # Crop to left half: for 2560x720 stereo, get left 1280x720 + videocrop.set_property("left", 0) + videocrop.set_property("right", self.width // 2) # Remove right half + videocrop.set_property("top", 0) + videocrop.set_property("bottom", 0) + + # H264 encoder + self.encoder = Gst.ElementFactory.make("x264enc", "encoder") + self.encoder.set_property("tune", "zerolatency") + self.encoder.set_property("bitrate", self.bitrate) + self.encoder.set_property("key-int-max", 30) + + # H264 parser + h264parse = Gst.ElementFactory.make("h264parse", "parser") + + # Use matroskamux which preserves timestamps better + self.mux = Gst.ElementFactory.make("matroskamux", "mux") + self.mux.set_property("streamable", True) + self.mux.set_property("writing-app", "gstreamer-tcp-sender") + + # TCP server sink + tcpserversink = Gst.ElementFactory.make("tcpserversink", "sink") + tcpserversink.set_property("host", self.host) + tcpserversink.set_property("port", self.port) + tcpserversink.set_property("sync", False) + + # Add elements to pipeline + self.pipeline.add(self.videosrc) + self.pipeline.add(capsfilter) + self.pipeline.add(videoconvert) + if videocrop: + self.pipeline.add(videocrop) + self.pipeline.add(self.encoder) + self.pipeline.add(h264parse) + self.pipeline.add(self.mux) + self.pipeline.add(tcpserversink) + + # Link elements + if not self.videosrc.link(capsfilter): + raise RuntimeError("Failed to link source to capsfilter") + if not capsfilter.link(videoconvert): + raise RuntimeError("Failed to link capsfilter to videoconvert") + + # Link through crop if in single camera mode + if videocrop: + if not videoconvert.link(videocrop): + raise RuntimeError("Failed to link videoconvert to videocrop") + if not videocrop.link(self.encoder): + raise RuntimeError("Failed to link videocrop to encoder") + else: + if not videoconvert.link(self.encoder): + raise RuntimeError("Failed to link videoconvert to encoder") + + if not self.encoder.link(h264parse): + raise RuntimeError("Failed to link encoder to h264parse") + if not h264parse.link(self.mux): + raise RuntimeError("Failed to link h264parse to mux") + if not self.mux.link(tcpserversink): + raise RuntimeError("Failed to link mux to tcpserversink") + + # Add probe to inject absolute timestamps + # Place probe after crop (if present) or after videoconvert + if videocrop: + probe_element = videocrop + else: + probe_element = videoconvert + probe_pad = probe_element.get_static_pad("src") + probe_pad.add_probe(Gst.PadProbeType.BUFFER, self._inject_absolute_timestamp, None) + + # Set up bus message handling + bus = self.pipeline.get_bus() + bus.add_signal_watch() + bus.connect("message", self._on_bus_message) + + def _inject_absolute_timestamp(self, pad, info, user_data): + buffer = info.get_buffer() + if buffer: + absolute_time = time.time() + absolute_time_ns = int(absolute_time * 1e9) + + # Set both PTS and DTS to the absolute time + # This will be preserved by matroskamux + buffer.pts = absolute_time_ns + buffer.dts = absolute_time_ns + + self.frame_count += 1 + return Gst.PadProbeReturn.OK + + def _on_bus_message(self, bus, message) -> None: + t = message.type + + if t == Gst.MessageType.EOS: + logger.info("End of stream") + self.stop() + elif t == Gst.MessageType.ERROR: + err, debug = message.parse_error() + logger.error(f"Pipeline error: {err}, {debug}") + self.stop() + elif t == Gst.MessageType.WARNING: + warn, debug = message.parse_warning() + logger.warning(f"Pipeline warning: {warn}, {debug}") + elif t == Gst.MessageType.STATE_CHANGED: + if message.src == self.pipeline: + old_state, new_state, _pending_state = message.parse_state_changed() + logger.debug( + f"Pipeline state changed: {old_state.value_nick} -> {new_state.value_nick}" + ) + + def start(self): + if self.running: + logger.warning("Sender is already running") + return + + logger.info("Creating TCP pipeline with absolute timestamps...") + self.create_pipeline() + + logger.info("Starting pipeline...") + ret = self.pipeline.set_state(Gst.State.PLAYING) + if ret == Gst.StateChangeReturn.FAILURE: + logger.error("Failed to start pipeline") + raise RuntimeError("Failed to start GStreamer pipeline") + + self.running = True + self.start_time = time.time() + self.frame_count = 0 + + logger.info("TCP video sender started:") + logger.info(f" Source: {self.device}") + if self.single_camera: + output_width = self.width // 2 + logger.info(f" Input Resolution: {self.width}x{self.height} @ {self.framerate}fps") + logger.info( + f" Output Resolution: {output_width}x{self.height} @ {self.framerate}fps (left camera only)" + ) + else: + logger.info(f" Resolution: {self.width}x{self.height} @ {self.framerate}fps") + logger.info(f" Bitrate: {self.bitrate} kbps") + logger.info(f" TCP Server: {self.host}:{self.port}") + logger.info(" Container: Matroska (preserves absolute timestamps)") + logger.info(" Waiting for client connections...") + + self.main_loop = GLib.MainLoop() + try: + self.main_loop.run() + except KeyboardInterrupt: + logger.info("Interrupted by user") + finally: + self.stop() + + def stop(self) -> None: + if not self.running: + return + + self.running = False + + if self.pipeline: + logger.info("Stopping pipeline...") + self.pipeline.set_state(Gst.State.NULL) + + if self.main_loop and self.main_loop.is_running(): + self.main_loop.quit() + + if self.frame_count > 0 and self.start_time: + elapsed = time.time() - self.start_time + avg_fps = self.frame_count / elapsed + logger.info(f"Total frames sent: {self.frame_count}, Average FPS: {avg_fps:.1f}") + + logger.info("TCP video sender stopped") + + +def main() -> None: + parser = argparse.ArgumentParser( + description="GStreamer TCP video sender with absolute timestamps" + ) + + # Video source options + parser.add_argument( + "--device", default="/dev/video0", help="Video device path (default: /dev/video0)" + ) + + # Video format options + parser.add_argument("--width", type=int, default=2560, help="Video width (default: 2560)") + parser.add_argument("--height", type=int, default=720, help="Video height (default: 720)") + parser.add_argument("--framerate", type=int, default=15, help="Frame rate in fps (default: 15)") + parser.add_argument("--format", default="YUY2", help="Video format (default: YUY2)") + + # Encoding options + parser.add_argument( + "--bitrate", type=int, default=5000, help="H264 bitrate in kbps (default: 5000)" + ) + + # Network options + parser.add_argument( + "--host", + default="0.0.0.0", + help="Host to listen on (default: 0.0.0.0 for all interfaces)", + ) + parser.add_argument("--port", type=int, default=5000, help="TCP port (default: 5000)") + + # Camera options + parser.add_argument( + "--single-camera", + action="store_true", + help="Extract left camera only from stereo feed (crops 2560x720 to 1280x720)", + ) + + # Logging options + parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") + + args = parser.parse_args() + + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + + # Create and start sender + sender = GStreamerTCPSender( + device=args.device, + width=args.width, + height=args.height, + framerate=args.framerate, + format_str=args.format, + bitrate=args.bitrate, + host=args.host, + port=args.port, + single_camera=args.single_camera, + ) + + # Handle signals gracefully + def signal_handler(sig, frame) -> None: + logger.info(f"Received signal {sig}, shutting down...") + sender.stop() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + try: + sender.start() + except Exception as e: + logger.error(f"Failed to start sender: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/dimos/hardware/interface.py b/dimos/hardware/interface.py deleted file mode 100644 index 6c26608995..0000000000 --- a/dimos/hardware/interface.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.hardware.end_effector import EndEffector -from dimos.hardware.camera import Camera -from dimos.hardware.stereo_camera import StereoCamera -from dimos.hardware.ufactory import UFactoryEndEffector, UFactory7DOFArm - -class HardwareInterface: - def __init__(self, end_effector: EndEffector = None, sensors: list = None, arm_architecture: UFactory7DOFArm = None): - self.end_effector = end_effector - self.sensors = sensors if sensors is not None else [] - self.arm_architecture = arm_architecture - - def get_configuration(self): - """Return the current hardware configuration.""" - return { - 'end_effector': self.end_effector, - 'sensors': [sensor.get_sensor_type() for sensor in self.sensors], - 'arm_architecture': self.arm_architecture - } - - def set_configuration(self, configuration): - """Set the hardware configuration.""" - self.end_effector = configuration.get('end_effector', self.end_effector) - self.sensors = configuration.get('sensors', self.sensors) - self.arm_architecture = configuration.get('arm_architecture', self.arm_architecture) - - def add_sensor(self, sensor): - """Add a sensor to the hardware interface.""" - if isinstance(sensor, (Camera, StereoCamera)): - self.sensors.append(sensor) - else: - raise ValueError("Sensor must be a Camera or StereoCamera instance.") diff --git a/dimos/hardware/piper_arm.py b/dimos/hardware/piper_arm.py new file mode 100644 index 0000000000..d27d1df394 --- /dev/null +++ b/dimos/hardware/piper_arm.py @@ -0,0 +1,525 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# dimos/hardware/piper_arm.py + +import select +import sys +import termios +import threading +import time +import tty + +from dimos_lcm.geometry_msgs import Pose, Twist, Vector3 +import kinpy as kp +import numpy as np +from piper_sdk import * # from the official Piper SDK +import pytest +from reactivex.disposable import Disposable +from scipy.spatial.transform import Rotation as R + +import dimos.core as core +from dimos.core import In, Module, rpc +import dimos.protocol.service.lcmservice as lcmservice +from dimos.utils.logging_config import setup_logger +from dimos.utils.transform_utils import euler_to_quaternion, quaternion_to_euler + +logger = setup_logger(__file__) + + +class PiperArm: + def __init__(self, arm_name: str = "arm") -> None: + self.arm = C_PiperInterface_V2() + self.arm.ConnectPort() + self.resetArm() + time.sleep(0.5) + self.resetArm() + time.sleep(0.5) + self.enable() + self.enable_gripper() # Enable gripper after arm is enabled + self.gotoZero() + time.sleep(1) + self.init_vel_controller() + + def enable(self) -> None: + while not self.arm.EnablePiper(): + pass + time.sleep(0.01) + logger.info("Arm enabled") + # self.arm.ModeCtrl( + # ctrl_mode=0x01, # CAN command mode + # move_mode=0x01, # “Move-J”, but ignored in MIT + # move_spd_rate_ctrl=100, # doesn’t matter in MIT + # is_mit_mode=0xAD # <-- the magic flag + # ) + self.arm.MotionCtrl_2(0x01, 0x01, 80, 0xAD) + + def gotoZero(self) -> None: + factor = 1000 + position = [57.0, 0.0, 215.0, 0, 90.0, 0, 0] + X = round(position[0] * factor) + Y = round(position[1] * factor) + Z = round(position[2] * factor) + RX = round(position[3] * factor) + RY = round(position[4] * factor) + RZ = round(position[5] * factor) + round(position[6] * factor) + logger.debug(f"Going to zero position: X={X}, Y={Y}, Z={Z}, RX={RX}, RY={RY}, RZ={RZ}") + self.arm.MotionCtrl_2(0x01, 0x00, 100, 0x00) + self.arm.EndPoseCtrl(X, Y, Z, RX, RY, RZ) + self.arm.GripperCtrl(0, 1000, 0x01, 0) + + def gotoObserve(self) -> None: + factor = 1000 + position = [57.0, 0.0, 280.0, 0, 120.0, 0, 0] + X = round(position[0] * factor) + Y = round(position[1] * factor) + Z = round(position[2] * factor) + RX = round(position[3] * factor) + RY = round(position[4] * factor) + RZ = round(position[5] * factor) + round(position[6] * factor) + logger.debug(f"Going to zero position: X={X}, Y={Y}, Z={Z}, RX={RX}, RY={RY}, RZ={RZ}") + self.arm.MotionCtrl_2(0x01, 0x00, 100, 0x00) + self.arm.EndPoseCtrl(X, Y, Z, RX, RY, RZ) + + def softStop(self) -> None: + self.gotoZero() + time.sleep(1) + self.arm.MotionCtrl_2( + 0x01, + 0x00, + 100, + ) + self.arm.MotionCtrl_1(0x01, 0, 0) + time.sleep(3) + + def cmd_ee_pose_values(self, x, y, z, r, p, y_, line_mode: bool = False) -> None: + """Command end-effector to target pose in space (position + Euler angles)""" + factor = 1000 + pose = [ + x * factor * factor, + y * factor * factor, + z * factor * factor, + r * factor, + p * factor, + y_ * factor, + ] + self.arm.MotionCtrl_2(0x01, 0x02 if line_mode else 0x00, 100, 0x00) + self.arm.EndPoseCtrl( + int(pose[0]), int(pose[1]), int(pose[2]), int(pose[3]), int(pose[4]), int(pose[5]) + ) + + def cmd_ee_pose(self, pose: Pose, line_mode: bool = False) -> None: + """Command end-effector to target pose using Pose message""" + # Convert quaternion to euler angles + euler = quaternion_to_euler(pose.orientation, degrees=True) + + # Command the pose + self.cmd_ee_pose_values( + pose.position.x, + pose.position.y, + pose.position.z, + euler.x, + euler.y, + euler.z, + line_mode, + ) + + def get_ee_pose(self): + """Return the current end-effector pose as Pose message with position in meters and quaternion orientation""" + pose = self.arm.GetArmEndPoseMsgs() + factor = 1000.0 + # Extract individual pose values and convert to base units + # Position values are divided by 1000 to convert from SDK units to meters + # Rotation values are divided by 1000 to convert from SDK units to radians + x = pose.end_pose.X_axis / factor / factor # Convert mm to m + y = pose.end_pose.Y_axis / factor / factor # Convert mm to m + z = pose.end_pose.Z_axis / factor / factor # Convert mm to m + rx = pose.end_pose.RX_axis / factor + ry = pose.end_pose.RY_axis / factor + rz = pose.end_pose.RZ_axis / factor + + # Create position vector (already in meters) + position = Vector3(x, y, z) + + orientation = euler_to_quaternion(Vector3(rx, ry, rz), degrees=True) + + return Pose(position, orientation) + + def cmd_gripper_ctrl(self, position, effort: float = 0.25) -> None: + """Command end-effector gripper""" + factor = 1000 + position = position * factor * factor # meters + effort = effort * factor # N/m + + self.arm.GripperCtrl(abs(round(position)), abs(round(effort)), 0x01, 0) + logger.debug(f"Commanding gripper position: {position}mm") + + def enable_gripper(self) -> None: + """Enable the gripper using the initialization sequence""" + logger.info("Enabling gripper...") + while not self.arm.EnablePiper(): + time.sleep(0.01) + self.arm.GripperCtrl(0, 1000, 0x02, 0) + self.arm.GripperCtrl(0, 1000, 0x01, 0) + logger.info("Gripper enabled") + + def release_gripper(self) -> None: + """Release gripper by opening to 100mm (10cm)""" + logger.info("Releasing gripper (opening to 100mm)") + self.cmd_gripper_ctrl(0.1) # 0.1m = 100mm = 10cm + + def get_gripper_feedback(self) -> tuple[float, float]: + """ + Get current gripper feedback. + + Returns: + Tuple of (angle_degrees, effort) where: + - angle_degrees: Current gripper angle in degrees + - effort: Current gripper effort (0.0 to 1.0 range) + """ + gripper_msg = self.arm.GetArmGripperMsgs() + angle_degrees = ( + gripper_msg.gripper_state.grippers_angle / 1000.0 + ) # Convert from SDK units to degrees + effort = gripper_msg.gripper_state.grippers_effort / 1000.0 # Convert from SDK units to N/m + return angle_degrees, effort + + def close_gripper(self, commanded_effort: float = 0.5) -> None: + """ + Close the gripper. + + Args: + commanded_effort: Effort to use when closing gripper (default 0.25 N/m) + """ + # Command gripper to close (0.0 position) + self.cmd_gripper_ctrl(0.0, effort=commanded_effort) + logger.info("Closing gripper") + + def gripper_object_detected(self, commanded_effort: float = 0.25) -> bool: + """ + Check if an object is detected in the gripper based on effort feedback. + + Args: + commanded_effort: The effort that was used when closing gripper (default 0.25 N/m) + + Returns: + True if object is detected in gripper, False otherwise + """ + # Get gripper feedback + _angle_degrees, actual_effort = self.get_gripper_feedback() + + # Check if object is grasped (effort > 80% of commanded effort) + effort_threshold = 0.8 * commanded_effort + object_present = abs(actual_effort) > effort_threshold + + if object_present: + logger.info(f"Object detected in gripper (effort: {actual_effort:.3f} N/m)") + else: + logger.info(f"No object detected (effort: {actual_effort:.3f} N/m)") + + return object_present + + def resetArm(self) -> None: + self.arm.MotionCtrl_1(0x02, 0, 0) + self.arm.MotionCtrl_2(0, 0, 0, 0x00) + logger.info("Resetting arm") + + def init_vel_controller(self) -> None: + self.chain = kp.build_serial_chain_from_urdf( + open("dimos/hardware/piper_description.urdf"), "gripper_base" + ) + self.J = self.chain.jacobian(np.zeros(6)) + self.J_pinv = np.linalg.pinv(self.J) + self.dt = 0.01 + + def cmd_vel(self, x_dot, y_dot, z_dot, R_dot, P_dot, Y_dot) -> None: + joint_state = self.arm.GetArmJointMsgs().joint_state + # print(f"[PiperArm] Current Joints (direct): {joint_state}", type(joint_state)) + joint_angles = np.array( + [ + joint_state.joint_1, + joint_state.joint_2, + joint_state.joint_3, + joint_state.joint_4, + joint_state.joint_5, + joint_state.joint_6, + ] + ) + # print(f"[PiperArm] Current Joints: {joint_angles}", type(joint_angles)) + factor = 57295.7795 # 1000*180/3.1415926 + joint_angles = joint_angles / factor # convert to radians + + q = np.array( + [ + joint_angles[0], + joint_angles[1], + joint_angles[2], + joint_angles[3], + joint_angles[4], + joint_angles[5], + ] + ) + J = self.chain.jacobian(q) + self.J_pinv = np.linalg.pinv(J) + dq = self.J_pinv @ np.array([x_dot, y_dot, z_dot, R_dot, P_dot, Y_dot]) * self.dt + newq = q + dq + + newq = newq * factor + + self.arm.MotionCtrl_2(0x01, 0x01, 100, 0xAD) + self.arm.JointCtrl( + round(newq[0]), + round(newq[1]), + round(newq[2]), + round(newq[3]), + round(newq[4]), + round(newq[5]), + ) + time.sleep(self.dt) + # print(f"[PiperArm] Moving to Joints to : {newq}") + + def cmd_vel_ee(self, x_dot, y_dot, z_dot, RX_dot, PY_dot, YZ_dot) -> None: + factor = 1000 + x_dot = x_dot * factor + y_dot = y_dot * factor + z_dot = z_dot * factor + RX_dot = RX_dot * factor + PY_dot = PY_dot * factor + YZ_dot = YZ_dot * factor + + current_pose_msg = self.get_ee_pose() + + # Convert quaternion to euler angles + quat = [ + current_pose_msg.orientation.x, + current_pose_msg.orientation.y, + current_pose_msg.orientation.z, + current_pose_msg.orientation.w, + ] + rotation = R.from_quat(quat) + euler = rotation.as_euler("xyz") # Returns [rx, ry, rz] in radians + + # Create current pose array [x, y, z, rx, ry, rz] + current_pose = np.array( + [ + current_pose_msg.position.x, + current_pose_msg.position.y, + current_pose_msg.position.z, + euler[0], + euler[1], + euler[2], + ] + ) + + # Apply velocity increment + current_pose = ( + current_pose + np.array([x_dot, y_dot, z_dot, RX_dot, PY_dot, YZ_dot]) * self.dt + ) + + self.cmd_ee_pose_values( + current_pose[0], + current_pose[1], + current_pose[2], + current_pose[3], + current_pose[4], + current_pose[5], + ) + time.sleep(self.dt) + + def disable(self) -> None: + self.softStop() + + while self.arm.DisablePiper(): + pass + time.sleep(0.01) + self.arm.DisconnectPort() + + +class VelocityController(Module): + cmd_vel: In[Twist] = None + + def __init__(self, arm, period: float = 0.01, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.arm = arm + self.period = period + self.latest_cmd = None + self.last_cmd_time = None + self._thread = None + + @rpc + def start(self) -> None: + super().start() + + unsub = self.cmd_vel.subscribe(self.handle_cmd_vel) + self._disposables.add(Disposable(unsub)) + + def control_loop() -> None: + while True: + # Check for timeout (1 second) + if self.last_cmd_time and (time.time() - self.last_cmd_time) > 1.0: + logger.warning( + "No velocity command received for 1 second, stopping control loop" + ) + break + + cmd_vel = self.latest_cmd + + joint_state = self.arm.GetArmJointMsgs().joint_state + # print(f"[PiperArm] Current Joints (direct): {joint_state}", type(joint_state)) + joint_angles = np.array( + [ + joint_state.joint_1, + joint_state.joint_2, + joint_state.joint_3, + joint_state.joint_4, + joint_state.joint_5, + joint_state.joint_6, + ] + ) + factor = 57295.7795 # 1000*180/3.1415926 + joint_angles = joint_angles / factor # convert to radians + q = np.array( + [ + joint_angles[0], + joint_angles[1], + joint_angles[2], + joint_angles[3], + joint_angles[4], + joint_angles[5], + ] + ) + + J = self.chain.jacobian(q) + self.J_pinv = np.linalg.pinv(J) + dq = ( + self.J_pinv + @ np.array( + [ + cmd_vel.linear.X, + cmd_vel.linear.y, + cmd_vel.linear.z, + cmd_vel.angular.x, + cmd_vel.angular.y, + cmd_vel.angular.z, + ] + ) + * self.dt + ) + newq = q + dq + + newq = newq * factor # convert radians to scaled degree units for joint control + + self.arm.MotionCtrl_2(0x01, 0x01, 100, 0xAD) + self.arm.JointCtrl( + round(newq[0]), + round(newq[1]), + round(newq[2]), + round(newq[3]), + round(newq[4]), + round(newq[5]), + ) + time.sleep(self.period) + + self._thread = threading.Thread(target=control_loop, daemon=True) + self._thread.start() + + @rpc + def stop(self) -> None: + if self._thread: + # TODO: trigger the thread to stop + self._thread.join(2) + super().stop() + + def handle_cmd_vel(self, cmd_vel: Twist) -> None: + self.latest_cmd = cmd_vel + self.last_cmd_time = time.time() + + +@pytest.mark.tool +def run_velocity_controller() -> None: + lcmservice.autoconf() + dimos = core.start(2) + + velocity_controller = dimos.deploy(VelocityController, arm=arm, period=0.01) + velocity_controller.cmd_vel.transport = core.LCMTransport("/cmd_vel", Twist) + + velocity_controller.start() + + logger.info("Velocity controller started") + while True: + time.sleep(1) + + # velocity_controller.stop() + + +if __name__ == "__main__": + arm = PiperArm() + + def get_key(timeout: float = 0.1): + """Non-blocking key reader for arrow keys.""" + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(fd) + rlist, _, _ = select.select([fd], [], [], timeout) + if rlist: + ch1 = sys.stdin.read(1) + if ch1 == "\x1b": # Arrow keys start with ESC + ch2 = sys.stdin.read(1) + if ch2 == "[": + ch3 = sys.stdin.read(1) + return ch1 + ch2 + ch3 + else: + return ch1 + return None + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + + def teleop_linear_vel(arm) -> None: + print("Use arrow keys to control linear velocity (x/y/z). Press 'q' to quit.") + print("Up/Down: +x/-x, Left/Right: +y/-y, 'w'/'s': +z/-z") + x_dot, y_dot, z_dot = 0.0, 0.0, 0.0 + while True: + key = get_key(timeout=0.1) + if key == "\x1b[A": # Up arrow + x_dot += 0.01 + elif key == "\x1b[B": # Down arrow + x_dot -= 0.01 + elif key == "\x1b[C": # Right arrow + y_dot += 0.01 + elif key == "\x1b[D": # Left arrow + y_dot -= 0.01 + elif key == "w": + z_dot += 0.01 + elif key == "s": + z_dot -= 0.01 + elif key == "q": + logger.info("Exiting teleop") + arm.disable() + break + + # Optionally, clamp velocities to reasonable limits + x_dot = max(min(x_dot, 0.5), -0.5) + y_dot = max(min(y_dot, 0.5), -0.5) + z_dot = max(min(z_dot, 0.5), -0.5) + + # Only linear velocities, angular set to zero + arm.cmd_vel_ee(x_dot, y_dot, z_dot, 0, 0, 0) + logger.debug( + f"Current linear velocity: x={x_dot:.3f} m/s, y={y_dot:.3f} m/s, z={z_dot:.3f} m/s" + ) + + run_velocity_controller() diff --git a/dimos/hardware/piper_description.urdf b/dimos/hardware/piper_description.urdf new file mode 100755 index 0000000000..21209b6dbb --- /dev/null +++ b/dimos/hardware/piper_description.urdf @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dimos/hardware/sensor.py b/dimos/hardware/sensor.py index ac5ff0ebaf..aa39f25ec6 100644 --- a/dimos/hardware/sensor.py +++ b/dimos/hardware/sensor.py @@ -14,8 +14,9 @@ from abc import ABC, abstractmethod + class AbstractSensor(ABC): - def __init__(self, sensor_type=None): + def __init__(self, sensor_type=None) -> None: self.sensor_type = sensor_type @abstractmethod diff --git a/dimos/hardware/ufactory.py b/dimos/hardware/ufactory.py index cb81337a95..57caf2e3bd 100644 --- a/dimos/hardware/ufactory.py +++ b/dimos/hardware/ufactory.py @@ -14,16 +14,18 @@ from dimos.hardware.end_effector import EndEffector + class UFactoryEndEffector(EndEffector): - def __init__(self, model=None, **kwargs): + def __init__(self, model=None, **kwargs) -> None: super().__init__(**kwargs) self.model = model def get_model(self): return self.model + class UFactory7DOFArm: - def __init__(self, arm_length=None): + def __init__(self, arm_length=None) -> None: self.arm_length = arm_length def get_arm_length(self): diff --git a/dimos/data/__init__.py b/dimos/manipulation/__init__.py similarity index 100% rename from dimos/data/__init__.py rename to dimos/manipulation/__init__.py diff --git a/dimos/manipulation/manip_aio_pipeline.py b/dimos/manipulation/manip_aio_pipeline.py new file mode 100644 index 0000000000..164c7b1774 --- /dev/null +++ b/dimos/manipulation/manip_aio_pipeline.py @@ -0,0 +1,588 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Asynchronous, reactive manipulation pipeline for realtime detection, filtering, and grasp generation. +""" + +import asyncio +import json +import threading +import time + +import cv2 +import numpy as np +import reactivex as rx +import reactivex.operators as ops +import websockets + +from dimos.perception.common.utils import colorize_depth +from dimos.perception.detection2d.detic_2d_det import Detic2DDetector +from dimos.perception.grasp_generation.utils import draw_grasps_on_image +from dimos.perception.object_detection_stream import ObjectDetectionStream +from dimos.perception.pointcloud.pointcloud_filtering import PointcloudFiltering +from dimos.perception.pointcloud.utils import create_point_cloud_overlay_visualization +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.perception.manip_aio_pipeline") + + +class ManipulationPipeline: + """ + Clean separated stream pipeline with frame buffering. + + - Object detection runs independently on RGB stream + - Point cloud processing subscribes to both detection and ZED streams separately + - Simple frame buffering to match RGB+depth+objects + """ + + def __init__( + self, + camera_intrinsics: list[float], # [fx, fy, cx, cy] + min_confidence: float = 0.6, + max_objects: int = 10, + vocabulary: str | None = None, + grasp_server_url: str | None = None, + enable_grasp_generation: bool = False, + ) -> None: + """ + Initialize the manipulation pipeline. + + Args: + camera_intrinsics: [fx, fy, cx, cy] camera parameters + min_confidence: Minimum detection confidence threshold + max_objects: Maximum number of objects to process + vocabulary: Optional vocabulary for Detic detector + grasp_server_url: Optional WebSocket URL for Dimensional Grasp server + enable_grasp_generation: Whether to enable async grasp generation + """ + self.camera_intrinsics = camera_intrinsics + self.min_confidence = min_confidence + + # Grasp generation settings + self.grasp_server_url = grasp_server_url + self.enable_grasp_generation = enable_grasp_generation + + # Asyncio event loop for WebSocket communication + self.grasp_loop = None + self.grasp_loop_thread = None + + # Storage for grasp results and filtered objects + self.latest_grasps: list[dict] = [] # Simplified: just a list of grasps + self.grasps_consumed = False + self.latest_filtered_objects = [] + self.latest_rgb_for_grasps = None # Store RGB image for grasp overlay + self.grasp_lock = threading.Lock() + + # Track pending requests - simplified to single task + self.grasp_task: asyncio.Task | None = None + + # Reactive subjects for streaming filtered objects and grasps + self.filtered_objects_subject = rx.subject.Subject() + self.grasps_subject = rx.subject.Subject() + self.grasp_overlay_subject = rx.subject.Subject() # Add grasp overlay subject + + # Initialize grasp client if enabled + if self.enable_grasp_generation and self.grasp_server_url: + self._start_grasp_loop() + + # Initialize object detector + self.detector = Detic2DDetector(vocabulary=vocabulary, threshold=min_confidence) + + # Initialize point cloud processor + self.pointcloud_filter = PointcloudFiltering( + color_intrinsics=camera_intrinsics, + depth_intrinsics=camera_intrinsics, # ZED uses same intrinsics + max_num_objects=max_objects, + ) + + logger.info(f"Initialized ManipulationPipeline with confidence={min_confidence}") + + def create_streams(self, zed_stream: rx.Observable) -> dict[str, rx.Observable]: + """ + Create streams using exact old main logic. + """ + # Create ZED streams (from old main) + zed_frame_stream = zed_stream.pipe(ops.share()) + + # RGB stream for object detection (from old main) + video_stream = zed_frame_stream.pipe( + ops.map(lambda x: x.get("rgb") if x is not None else None), + ops.filter(lambda x: x is not None), + ops.share(), + ) + object_detector = ObjectDetectionStream( + camera_intrinsics=self.camera_intrinsics, + min_confidence=self.min_confidence, + class_filter=None, + detector=self.detector, + video_stream=video_stream, + disable_depth=True, + ) + + # Store latest frames for point cloud processing (from old main) + latest_rgb = None + latest_depth = None + latest_point_cloud_overlay = None + frame_lock = threading.Lock() + + # Subscribe to combined ZED frames (from old main) + def on_zed_frame(zed_data) -> None: + nonlocal latest_rgb, latest_depth + if zed_data is not None: + with frame_lock: + latest_rgb = zed_data.get("rgb") + latest_depth = zed_data.get("depth") + + # Depth stream for point cloud filtering (from old main) + def get_depth_or_overlay(zed_data): + if zed_data is None: + return None + + # Check if we have a point cloud overlay available + with frame_lock: + overlay = latest_point_cloud_overlay + + if overlay is not None: + return overlay + else: + # Return regular colorized depth + return colorize_depth(zed_data.get("depth"), max_depth=10.0) + + depth_stream = zed_frame_stream.pipe( + ops.map(get_depth_or_overlay), ops.filter(lambda x: x is not None), ops.share() + ) + + # Process object detection results with point cloud filtering (from old main) + def on_detection_next(result) -> None: + nonlocal latest_point_cloud_overlay + if result.get("objects"): + # Get latest RGB and depth frames + with frame_lock: + rgb = latest_rgb + depth = latest_depth + + if rgb is not None and depth is not None: + try: + filtered_objects = self.pointcloud_filter.process_images( + rgb, depth, result["objects"] + ) + + if filtered_objects: + # Store filtered objects + with self.grasp_lock: + self.latest_filtered_objects = filtered_objects + self.filtered_objects_subject.on_next(filtered_objects) + + # Create base image (colorized depth) + base_image = colorize_depth(depth, max_depth=10.0) + + # Create point cloud overlay visualization + overlay_viz = create_point_cloud_overlay_visualization( + base_image=base_image, + objects=filtered_objects, + intrinsics=self.camera_intrinsics, + ) + + # Store the overlay for the stream + with frame_lock: + latest_point_cloud_overlay = overlay_viz + + # Request grasps if enabled + if self.enable_grasp_generation and len(filtered_objects) > 0: + # Save RGB image for later grasp overlay + with frame_lock: + self.latest_rgb_for_grasps = rgb.copy() + + task = self.request_scene_grasps(filtered_objects) + if task: + # Check for results after a delay + def check_grasps_later() -> None: + time.sleep(2.0) # Wait for grasp processing + # Wait for task to complete + if hasattr(self, "grasp_task") and self.grasp_task: + try: + self.grasp_task.result( + timeout=3.0 + ) # Get result with timeout + except Exception as e: + logger.warning(f"Grasp task failed or timeout: {e}") + + # Try to get latest grasps and create overlay + with self.grasp_lock: + grasps = self.latest_grasps + + if grasps and hasattr(self, "latest_rgb_for_grasps"): + # Create grasp overlay on the saved RGB image + try: + bgr_image = cv2.cvtColor( + self.latest_rgb_for_grasps, cv2.COLOR_RGB2BGR + ) + result_bgr = draw_grasps_on_image( + bgr_image, + grasps, + self.camera_intrinsics, + max_grasps=-1, # Show all grasps + ) + result_rgb = cv2.cvtColor( + result_bgr, cv2.COLOR_BGR2RGB + ) + + # Emit grasp overlay immediately + self.grasp_overlay_subject.on_next(result_rgb) + + except Exception as e: + logger.error(f"Error creating grasp overlay: {e}") + + # Emit grasps to stream + self.grasps_subject.on_next(grasps) + + threading.Thread(target=check_grasps_later, daemon=True).start() + else: + logger.warning("Failed to create grasp task") + except Exception as e: + logger.error(f"Error in point cloud filtering: {e}") + with frame_lock: + latest_point_cloud_overlay = None + + def on_error(error) -> None: + logger.error(f"Error in stream: {error}") + + def on_completed() -> None: + logger.info("Stream completed") + + def start_subscriptions() -> None: + """Start subscriptions in background thread (from old main)""" + # Subscribe to combined ZED frames + zed_frame_stream.subscribe(on_next=on_zed_frame) + + # Start subscriptions in background thread (from old main) + subscription_thread = threading.Thread(target=start_subscriptions, daemon=True) + subscription_thread.start() + time.sleep(2) # Give subscriptions time to start + + # Subscribe to object detection stream (from old main) + object_detector.get_stream().subscribe( + on_next=on_detection_next, on_error=on_error, on_completed=on_completed + ) + + # Create visualization stream for web interface (from old main) + viz_stream = object_detector.get_stream().pipe( + ops.map(lambda x: x["viz_frame"] if x is not None else None), + ops.filter(lambda x: x is not None), + ) + + # Create filtered objects stream + filtered_objects_stream = self.filtered_objects_subject + + # Create grasps stream + grasps_stream = self.grasps_subject + + # Create grasp overlay subject for immediate emission + grasp_overlay_stream = self.grasp_overlay_subject + + return { + "detection_viz": viz_stream, + "pointcloud_viz": depth_stream, + "objects": object_detector.get_stream().pipe(ops.map(lambda x: x.get("objects", []))), + "filtered_objects": filtered_objects_stream, + "grasps": grasps_stream, + "grasp_overlay": grasp_overlay_stream, + } + + def _start_grasp_loop(self) -> None: + """Start asyncio event loop in a background thread for WebSocket communication.""" + + def run_loop() -> None: + self.grasp_loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.grasp_loop) + self.grasp_loop.run_forever() + + self.grasp_loop_thread = threading.Thread(target=run_loop, daemon=True) + self.grasp_loop_thread.start() + + # Wait for loop to start + while self.grasp_loop is None: + time.sleep(0.01) + + async def _send_grasp_request( + self, points: np.ndarray, colors: np.ndarray | None + ) -> list[dict] | None: + """Send grasp request to Dimensional Grasp server.""" + try: + # Comprehensive client-side validation to prevent server errors + + # Validate points array + if points is None: + logger.error("Points array is None") + return None + if not isinstance(points, np.ndarray): + logger.error(f"Points is not numpy array: {type(points)}") + return None + if points.size == 0: + logger.error("Points array is empty") + return None + if len(points.shape) != 2 or points.shape[1] != 3: + logger.error(f"Points has invalid shape {points.shape}, expected (N, 3)") + return None + if points.shape[0] < 100: # Minimum points for stable grasp detection + logger.error(f"Insufficient points for grasp detection: {points.shape[0]} < 100") + return None + + # Validate and prepare colors + if colors is not None: + if not isinstance(colors, np.ndarray): + colors = None + elif colors.size == 0: + colors = None + elif len(colors.shape) != 2 or colors.shape[1] != 3: + colors = None + elif colors.shape[0] != points.shape[0]: + colors = None + + # If no valid colors, create default colors (required by server) + if colors is None: + # Create default white colors for all points + colors = np.ones((points.shape[0], 3), dtype=np.float32) * 0.5 + + # Ensure data types are correct (server expects float32) + points = points.astype(np.float32) + colors = colors.astype(np.float32) + + # Validate ranges (basic sanity checks) + if np.any(np.isnan(points)) or np.any(np.isinf(points)): + logger.error("Points contain NaN or Inf values") + return None + if np.any(np.isnan(colors)) or np.any(np.isinf(colors)): + logger.error("Colors contain NaN or Inf values") + return None + + # Clamp color values to valid range [0, 1] + colors = np.clip(colors, 0.0, 1.0) + + async with websockets.connect(self.grasp_server_url) as websocket: + request = { + "points": points.tolist(), + "colors": colors.tolist(), # Always send colors array + "lims": [-0.19, 0.12, 0.02, 0.15, 0.0, 1.0], # Default workspace limits + } + + await websocket.send(json.dumps(request)) + + response = await websocket.recv() + grasps = json.loads(response) + + # Handle server response validation + if isinstance(grasps, dict) and "error" in grasps: + logger.error(f"Server returned error: {grasps['error']}") + return None + elif isinstance(grasps, int | float) and grasps == 0: + return None + elif not isinstance(grasps, list): + logger.error( + f"Server returned unexpected response type: {type(grasps)}, value: {grasps}" + ) + return None + elif len(grasps) == 0: + return None + + converted_grasps = self._convert_grasp_format(grasps) + with self.grasp_lock: + self.latest_grasps = converted_grasps + self.grasps_consumed = False # Reset consumed flag + + # Emit to reactive stream + self.grasps_subject.on_next(self.latest_grasps) + + return converted_grasps + except websockets.exceptions.ConnectionClosed as e: + logger.error(f"WebSocket connection closed: {e}") + except websockets.exceptions.WebSocketException as e: + logger.error(f"WebSocket error: {e}") + except json.JSONDecodeError as e: + logger.error(f"Failed to parse server response as JSON: {e}") + except Exception as e: + logger.error(f"Error requesting grasps: {e}") + + return None + + def request_scene_grasps(self, objects: list[dict]) -> asyncio.Task | None: + """Request grasps for entire scene by combining all object point clouds.""" + if not self.grasp_loop or not objects: + return None + + all_points = [] + all_colors = [] + valid_objects = 0 + + for _i, obj in enumerate(objects): + # Validate point cloud data + if "point_cloud_numpy" not in obj or obj["point_cloud_numpy"] is None: + continue + + points = obj["point_cloud_numpy"] + if not isinstance(points, np.ndarray) or points.size == 0: + continue + + # Ensure points have correct shape (N, 3) + if len(points.shape) != 2 or points.shape[1] != 3: + continue + + # Validate colors if present + colors = None + if "colors_numpy" in obj and obj["colors_numpy"] is not None: + colors = obj["colors_numpy"] + if isinstance(colors, np.ndarray) and colors.size > 0: + # Ensure colors match points count and have correct shape + if colors.shape[0] != points.shape[0]: + colors = None # Ignore colors for this object + elif len(colors.shape) != 2 or colors.shape[1] != 3: + colors = None # Ignore colors for this object + + all_points.append(points) + if colors is not None: + all_colors.append(colors) + valid_objects += 1 + + if not all_points: + return None + + try: + combined_points = np.vstack(all_points) + + # Only combine colors if ALL objects have valid colors + combined_colors = None + if len(all_colors) == valid_objects and len(all_colors) > 0: + combined_colors = np.vstack(all_colors) + + # Validate final combined data + if combined_points.size == 0: + logger.warning("Combined point cloud is empty") + return None + + if combined_colors is not None and combined_colors.shape[0] != combined_points.shape[0]: + logger.warning( + f"Color/point count mismatch: {combined_colors.shape[0]} colors vs {combined_points.shape[0]} points, dropping colors" + ) + combined_colors = None + + except Exception as e: + logger.error(f"Failed to combine point clouds: {e}") + return None + + try: + # Check if there's already a grasp task running + if hasattr(self, "grasp_task") and self.grasp_task and not self.grasp_task.done(): + return self.grasp_task + + task = asyncio.run_coroutine_threadsafe( + self._send_grasp_request(combined_points, combined_colors), self.grasp_loop + ) + + self.grasp_task = task + return task + except Exception: + logger.warning("Failed to create grasp task") + return None + + def get_latest_grasps(self, timeout: float = 5.0) -> list[dict] | None: + """Get latest grasp results, waiting for new ones if current ones have been consumed.""" + # Mark current grasps as consumed and get a reference + with self.grasp_lock: + current_grasps = self.latest_grasps + self.grasps_consumed = True + + # If we already have grasps and they haven't been consumed, return them + if current_grasps is not None and not getattr(self, "grasps_consumed", False): + return current_grasps + + # Wait for new grasps + start_time = time.time() + while time.time() - start_time < timeout: + with self.grasp_lock: + # Check if we have new grasps (different from what we marked as consumed) + if self.latest_grasps is not None and not getattr(self, "grasps_consumed", False): + return self.latest_grasps + time.sleep(0.1) # Check every 100ms + + return None # Timeout reached + + def clear_grasps(self) -> None: + """Clear all stored grasp results.""" + with self.grasp_lock: + self.latest_grasps = [] + + def _prepare_colors(self, colors: np.ndarray | None) -> np.ndarray | None: + """Prepare colors array, converting from various formats if needed.""" + if colors is None: + return None + + if colors.max() > 1.0: + colors = colors / 255.0 + + return colors + + def _convert_grasp_format(self, grasps: list[dict]) -> list[dict]: + """Convert Grasp format to our visualization format.""" + converted = [] + + for i, grasp in enumerate(grasps): + rotation_matrix = np.array(grasp.get("rotation_matrix", np.eye(3))) + euler_angles = self._rotation_matrix_to_euler(rotation_matrix) + + converted_grasp = { + "id": f"grasp_{i}", + "score": grasp.get("score", 0.0), + "width": grasp.get("width", 0.0), + "height": grasp.get("height", 0.0), + "depth": grasp.get("depth", 0.0), + "translation": grasp.get("translation", [0, 0, 0]), + "rotation_matrix": rotation_matrix.tolist(), + "euler_angles": euler_angles, + } + converted.append(converted_grasp) + + converted.sort(key=lambda x: x["score"], reverse=True) + + return converted + + def _rotation_matrix_to_euler(self, rotation_matrix: np.ndarray) -> dict[str, float]: + """Convert rotation matrix to Euler angles (in radians).""" + sy = np.sqrt(rotation_matrix[0, 0] ** 2 + rotation_matrix[1, 0] ** 2) + + singular = sy < 1e-6 + + if not singular: + x = np.arctan2(rotation_matrix[2, 1], rotation_matrix[2, 2]) + y = np.arctan2(-rotation_matrix[2, 0], sy) + z = np.arctan2(rotation_matrix[1, 0], rotation_matrix[0, 0]) + else: + x = np.arctan2(-rotation_matrix[1, 2], rotation_matrix[1, 1]) + y = np.arctan2(-rotation_matrix[2, 0], sy) + z = 0 + + return {"roll": x, "pitch": y, "yaw": z} + + def cleanup(self) -> None: + """Clean up resources.""" + if hasattr(self.detector, "cleanup"): + self.detector.cleanup() + + if self.grasp_loop and self.grasp_loop_thread: + self.grasp_loop.call_soon_threadsafe(self.grasp_loop.stop) + self.grasp_loop_thread.join(timeout=1.0) + + if hasattr(self.pointcloud_filter, "cleanup"): + self.pointcloud_filter.cleanup() + logger.info("ManipulationPipeline cleaned up") diff --git a/dimos/manipulation/manip_aio_processer.py b/dimos/manipulation/manip_aio_processer.py new file mode 100644 index 0000000000..e0bfc73256 --- /dev/null +++ b/dimos/manipulation/manip_aio_processer.py @@ -0,0 +1,410 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Sequential manipulation processor for single-frame processing without reactive streams. +""" + +import time +from typing import Any + +import cv2 +import numpy as np + +from dimos.perception.common.utils import ( + colorize_depth, + combine_object_data, + detection_results_to_object_data, +) +from dimos.perception.detection2d.detic_2d_det import Detic2DDetector +from dimos.perception.grasp_generation.grasp_generation import HostedGraspGenerator +from dimos.perception.grasp_generation.utils import create_grasp_overlay +from dimos.perception.pointcloud.pointcloud_filtering import PointcloudFiltering +from dimos.perception.pointcloud.utils import ( + create_point_cloud_overlay_visualization, + extract_and_cluster_misc_points, + overlay_point_clouds_on_image, +) +from dimos.perception.segmentation.sam_2d_seg import Sam2DSegmenter +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.perception.manip_aio_processor") + + +class ManipulationProcessor: + """ + Sequential manipulation processor for single-frame processing. + + Processes RGB-D frames through object detection, point cloud filtering, + and grasp generation in a single thread without reactive streams. + """ + + def __init__( + self, + camera_intrinsics: list[float], # [fx, fy, cx, cy] + min_confidence: float = 0.6, + max_objects: int = 20, + vocabulary: str | None = None, + enable_grasp_generation: bool = False, + grasp_server_url: str | None = None, # Required when enable_grasp_generation=True + enable_segmentation: bool = True, + ) -> None: + """ + Initialize the manipulation processor. + + Args: + camera_intrinsics: [fx, fy, cx, cy] camera parameters + min_confidence: Minimum detection confidence threshold + max_objects: Maximum number of objects to process + vocabulary: Optional vocabulary for Detic detector + enable_grasp_generation: Whether to enable grasp generation + grasp_server_url: WebSocket URL for Dimensional Grasp server (required when enable_grasp_generation=True) + enable_segmentation: Whether to enable semantic segmentation + segmentation_model: Segmentation model to use (SAM 2 or FastSAM) + """ + self.camera_intrinsics = camera_intrinsics + self.min_confidence = min_confidence + self.max_objects = max_objects + self.enable_grasp_generation = enable_grasp_generation + self.grasp_server_url = grasp_server_url + self.enable_segmentation = enable_segmentation + + # Validate grasp generation requirements + if enable_grasp_generation and not grasp_server_url: + raise ValueError("grasp_server_url is required when enable_grasp_generation=True") + + # Initialize object detector + self.detector = Detic2DDetector(vocabulary=vocabulary, threshold=min_confidence) + + # Initialize point cloud processor + self.pointcloud_filter = PointcloudFiltering( + color_intrinsics=camera_intrinsics, + depth_intrinsics=camera_intrinsics, # ZED uses same intrinsics + max_num_objects=max_objects, + ) + + # Initialize semantic segmentation + self.segmenter = None + if self.enable_segmentation: + self.segmenter = Sam2DSegmenter( + use_tracker=False, # Disable tracker for simple segmentation + use_analyzer=False, # Disable analyzer for simple segmentation + ) + + # Initialize grasp generator if enabled + self.grasp_generator = None + if self.enable_grasp_generation: + try: + self.grasp_generator = HostedGraspGenerator(server_url=grasp_server_url) + logger.info("Hosted grasp generator initialized successfully") + except Exception as e: + logger.error(f"Failed to initialize hosted grasp generator: {e}") + self.grasp_generator = None + self.enable_grasp_generation = False + + logger.info( + f"Initialized ManipulationProcessor with confidence={min_confidence}, " + f"grasp_generation={enable_grasp_generation}" + ) + + def process_frame( + self, rgb_image: np.ndarray, depth_image: np.ndarray, generate_grasps: bool | None = None + ) -> dict[str, Any]: + """ + Process a single RGB-D frame through the complete pipeline. + + Args: + rgb_image: RGB image (H, W, 3) + depth_image: Depth image (H, W) in meters + generate_grasps: Override grasp generation setting for this frame + + Returns: + Dictionary containing: + - detection_viz: Visualization of object detection + - pointcloud_viz: Visualization of point cloud overlay + - segmentation_viz: Visualization of semantic segmentation (if enabled) + - detection2d_objects: Raw detection results as ObjectData + - segmentation2d_objects: Raw segmentation results as ObjectData (if enabled) + - detected_objects: Detection (Object Detection) objects with point clouds filtered + - all_objects: Combined objects with intelligent duplicate removal + - full_pointcloud: Complete scene point cloud (if point cloud processing enabled) + - misc_clusters: List of clustered background/miscellaneous point clouds (DBSCAN) + - misc_voxel_grid: Open3D voxel grid approximating all misc/background points + - misc_pointcloud_viz: Visualization of misc/background cluster overlay + - grasps: Grasp results (list of dictionaries, if enabled) + - grasp_overlay: Grasp visualization overlay (if enabled) + - processing_time: Total processing time + """ + start_time = time.time() + results = {} + + try: + # Step 1: Object Detection + step_start = time.time() + detection_results = self.run_object_detection(rgb_image) + results["detection2d_objects"] = detection_results.get("objects", []) + results["detection_viz"] = detection_results.get("viz_frame") + detection_time = time.time() - step_start + + # Step 2: Semantic Segmentation (if enabled) + segmentation_time = 0 + if self.enable_segmentation: + step_start = time.time() + segmentation_results = self.run_segmentation(rgb_image) + results["segmentation2d_objects"] = segmentation_results.get("objects", []) + results["segmentation_viz"] = segmentation_results.get("viz_frame") + segmentation_time = time.time() - step_start + + # Step 3: Point Cloud Processing + pointcloud_time = 0 + detection2d_objects = results.get("detection2d_objects", []) + segmentation2d_objects = results.get("segmentation2d_objects", []) + + # Process detection objects if available + detected_objects = [] + if detection2d_objects: + step_start = time.time() + detected_objects = self.run_pointcloud_filtering( + rgb_image, depth_image, detection2d_objects + ) + pointcloud_time += time.time() - step_start + + # Process segmentation objects if available + segmentation_filtered_objects = [] + if segmentation2d_objects: + step_start = time.time() + segmentation_filtered_objects = self.run_pointcloud_filtering( + rgb_image, depth_image, segmentation2d_objects + ) + pointcloud_time += time.time() - step_start + + # Combine all objects using intelligent duplicate removal + all_objects = combine_object_data( + detected_objects, segmentation_filtered_objects, overlap_threshold=0.8 + ) + + # Get full point cloud + full_pcd = self.pointcloud_filter.get_full_point_cloud() + + # Extract misc/background points and create voxel grid + misc_start = time.time() + misc_clusters, misc_voxel_grid = extract_and_cluster_misc_points( + full_pcd, + all_objects, + eps=0.03, + min_points=100, + enable_filtering=True, + voxel_size=0.02, + ) + misc_time = time.time() - misc_start + + # Store results + results.update( + { + "detected_objects": detected_objects, + "all_objects": all_objects, + "full_pointcloud": full_pcd, + "misc_clusters": misc_clusters, + "misc_voxel_grid": misc_voxel_grid, + } + ) + + # Create point cloud visualizations + base_image = colorize_depth(depth_image, max_depth=10.0) + + # Create visualizations + results["pointcloud_viz"] = ( + create_point_cloud_overlay_visualization( + base_image=base_image, + objects=all_objects, + intrinsics=self.camera_intrinsics, + ) + if all_objects + else base_image + ) + + results["detected_pointcloud_viz"] = ( + create_point_cloud_overlay_visualization( + base_image=base_image, + objects=detected_objects, + intrinsics=self.camera_intrinsics, + ) + if detected_objects + else base_image + ) + + if misc_clusters: + # Generate consistent colors for clusters + cluster_colors = [ + tuple((np.random.RandomState(i + 100).rand(3) * 255).astype(int)) + for i in range(len(misc_clusters)) + ] + results["misc_pointcloud_viz"] = overlay_point_clouds_on_image( + base_image=base_image, + point_clouds=misc_clusters, + camera_intrinsics=self.camera_intrinsics, + colors=cluster_colors, + point_size=2, + alpha=0.6, + ) + else: + results["misc_pointcloud_viz"] = base_image + + # Step 4: Grasp Generation (if enabled) + should_generate_grasps = ( + generate_grasps if generate_grasps is not None else self.enable_grasp_generation + ) + + if should_generate_grasps and all_objects and full_pcd: + grasps = self.run_grasp_generation(all_objects, full_pcd) + results["grasps"] = grasps + if grasps: + results["grasp_overlay"] = create_grasp_overlay( + rgb_image, grasps, self.camera_intrinsics + ) + + except Exception as e: + logger.error(f"Error processing frame: {e}") + results["error"] = str(e) + + # Add timing information + total_time = time.time() - start_time + results.update( + { + "processing_time": total_time, + "timing_breakdown": { + "detection": detection_time if "detection_time" in locals() else 0, + "segmentation": segmentation_time if "segmentation_time" in locals() else 0, + "pointcloud": pointcloud_time if "pointcloud_time" in locals() else 0, + "misc_extraction": misc_time if "misc_time" in locals() else 0, + "total": total_time, + }, + } + ) + + return results + + def run_object_detection(self, rgb_image: np.ndarray) -> dict[str, Any]: + """Run object detection on RGB image.""" + try: + # Convert RGB to BGR for Detic detector + bgr_image = cv2.cvtColor(rgb_image, cv2.COLOR_RGB2BGR) + + # Use process_image method from Detic detector + bboxes, track_ids, class_ids, confidences, names, masks = self.detector.process_image( + bgr_image + ) + + # Convert to ObjectData format using utility function + objects = detection_results_to_object_data( + bboxes=bboxes, + track_ids=track_ids, + class_ids=class_ids, + confidences=confidences, + names=names, + masks=masks, + source="detection", + ) + + # Create visualization using detector's built-in method + viz_frame = self.detector.visualize_results( + rgb_image, bboxes, track_ids, class_ids, confidences, names + ) + + return {"objects": objects, "viz_frame": viz_frame} + + except Exception as e: + logger.error(f"Object detection failed: {e}") + return {"objects": [], "viz_frame": rgb_image.copy()} + + def run_pointcloud_filtering( + self, rgb_image: np.ndarray, depth_image: np.ndarray, objects: list[dict] + ) -> list[dict]: + """Run point cloud filtering on detected objects.""" + try: + filtered_objects = self.pointcloud_filter.process_images( + rgb_image, depth_image, objects + ) + return filtered_objects if filtered_objects else [] + except Exception as e: + logger.error(f"Point cloud filtering failed: {e}") + return [] + + def run_segmentation(self, rgb_image: np.ndarray) -> dict[str, Any]: + """Run semantic segmentation on RGB image.""" + if not self.segmenter: + return {"objects": [], "viz_frame": rgb_image.copy()} + + try: + # Convert RGB to BGR for segmenter + bgr_image = cv2.cvtColor(rgb_image, cv2.COLOR_RGB2BGR) + + # Get segmentation results + masks, bboxes, track_ids, probs, names = self.segmenter.process_image(bgr_image) + + # Convert to ObjectData format using utility function + objects = detection_results_to_object_data( + bboxes=bboxes, + track_ids=track_ids, + class_ids=list(range(len(bboxes))), # Use indices as class IDs for segmentation + confidences=probs, + names=names, + masks=masks, + source="segmentation", + ) + + # Create visualization + if masks: + viz_bgr = self.segmenter.visualize_results( + bgr_image, masks, bboxes, track_ids, probs, names + ) + # Convert back to RGB + viz_frame = cv2.cvtColor(viz_bgr, cv2.COLOR_BGR2RGB) + else: + viz_frame = rgb_image.copy() + + return {"objects": objects, "viz_frame": viz_frame} + + except Exception as e: + logger.error(f"Segmentation failed: {e}") + return {"objects": [], "viz_frame": rgb_image.copy()} + + def run_grasp_generation(self, filtered_objects: list[dict], full_pcd) -> list[dict] | None: + """Run grasp generation using the configured generator.""" + if not self.grasp_generator: + logger.warning("Grasp generation requested but no generator available") + return None + + try: + # Generate grasps using the configured generator + grasps = self.grasp_generator.generate_grasps_from_objects(filtered_objects, full_pcd) + + # Return parsed results directly (list of grasp dictionaries) + return grasps + + except Exception as e: + logger.error(f"Grasp generation failed: {e}") + return None + + def cleanup(self) -> None: + """Clean up resources.""" + if hasattr(self.detector, "cleanup"): + self.detector.cleanup() + if hasattr(self.pointcloud_filter, "cleanup"): + self.pointcloud_filter.cleanup() + if self.segmenter and hasattr(self.segmenter, "cleanup"): + self.segmenter.cleanup() + if self.grasp_generator and hasattr(self.grasp_generator, "cleanup"): + self.grasp_generator.cleanup() + logger.info("ManipulationProcessor cleaned up") diff --git a/dimos/manipulation/manipulation_history.py b/dimos/manipulation/manipulation_history.py new file mode 100644 index 0000000000..a77900ba30 --- /dev/null +++ b/dimos/manipulation/manipulation_history.py @@ -0,0 +1,417 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module for manipulation history tracking and search.""" + +from dataclasses import dataclass, field +from datetime import datetime +import json +import os +import pickle +import time +from typing import Any + +from dimos.types.manipulation import ( + ManipulationTask, +) +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.types.manipulation_history") + + +@dataclass +class ManipulationHistoryEntry: + """An entry in the manipulation history. + + Attributes: + task: The manipulation task executed + timestamp: When the manipulation was performed + result: Result of the manipulation (success/failure) + manipulation_response: Response from the motion planner/manipulation executor + """ + + task: ManipulationTask + timestamp: float = field(default_factory=time.time) + result: dict[str, Any] = field(default_factory=dict) + manipulation_response: str | None = ( + None # Any elaborative response from the motion planner / manipulation executor + ) + + def __str__(self) -> str: + status = self.result.get("status", "unknown") + return f"ManipulationHistoryEntry(task='{self.task.description}', status={status}, time={datetime.fromtimestamp(self.timestamp).strftime('%H:%M:%S')})" + + +class ManipulationHistory: + """A simplified, dictionary-based storage for manipulation history. + + This class provides an efficient way to store and query manipulation tasks, + focusing on quick lookups and flexible search capabilities. + """ + + def __init__(self, output_dir: str | None = None, new_memory: bool = False) -> None: + """Initialize a new manipulation history. + + Args: + output_dir: Directory to save history to + new_memory: If True, creates a new memory instead of loading existing one + """ + self._history: list[ManipulationHistoryEntry] = [] + self._output_dir = output_dir + + if output_dir and not new_memory: + self.load_from_dir(output_dir) + elif output_dir: + os.makedirs(output_dir, exist_ok=True) + logger.info(f"Created new manipulation history at {output_dir}") + + def __len__(self) -> int: + """Return the number of entries in the history.""" + return len(self._history) + + def __str__(self) -> str: + """Return a string representation of the history.""" + if not self._history: + return "ManipulationHistory(empty)" + + return ( + f"ManipulationHistory(entries={len(self._history)}, " + f"time_range={datetime.fromtimestamp(self._history[0].timestamp).strftime('%Y-%m-%d %H:%M:%S')} to " + f"{datetime.fromtimestamp(self._history[-1].timestamp).strftime('%Y-%m-%d %H:%M:%S')})" + ) + + def clear(self) -> None: + """Clear all entries from the history.""" + self._history.clear() + logger.info("Cleared manipulation history") + + if self._output_dir: + self.save_history() + + def add_entry(self, entry: ManipulationHistoryEntry) -> None: + """Add an entry to the history. + + Args: + entry: The entry to add + """ + self._history.append(entry) + self._history.sort(key=lambda e: e.timestamp) + + if self._output_dir: + self.save_history() + + def save_history(self) -> None: + """Save the history to the output directory.""" + if not self._output_dir: + logger.warning("Cannot save history: no output directory specified") + return + + os.makedirs(self._output_dir, exist_ok=True) + history_path = os.path.join(self._output_dir, "manipulation_history.pickle") + + with open(history_path, "wb") as f: + pickle.dump(self._history, f) + + logger.info(f"Saved manipulation history to {history_path}") + + # Also save a JSON representation for easier inspection + json_path = os.path.join(self._output_dir, "manipulation_history.json") + try: + history_data = [ + { + "task": { + "description": entry.task.description, + "target_object": entry.task.target_object, + "target_point": entry.task.target_point, + "timestamp": entry.task.timestamp, + "task_id": entry.task.task_id, + "metadata": entry.task.metadata, + }, + "result": entry.result, + "timestamp": entry.timestamp, + "manipulation_response": entry.manipulation_response, + } + for entry in self._history + ] + + with open(json_path, "w") as f: + json.dump(history_data, f, indent=2) + + logger.info(f"Saved JSON representation to {json_path}") + except Exception as e: + logger.error(f"Failed to save JSON representation: {e}") + + def load_from_dir(self, directory: str) -> None: + """Load history from the specified directory. + + Args: + directory: Directory to load history from + """ + history_path = os.path.join(directory, "manipulation_history.pickle") + + if not os.path.exists(history_path): + logger.warning(f"No history found at {history_path}") + return + + try: + with open(history_path, "rb") as f: + self._history = pickle.load(f) + + logger.info( + f"Loaded manipulation history from {history_path} with {len(self._history)} entries" + ) + except Exception as e: + logger.error(f"Failed to load history: {e}") + + def get_all_entries(self) -> list[ManipulationHistoryEntry]: + """Get all entries in chronological order. + + Returns: + List of all manipulation history entries + """ + return self._history.copy() + + def get_entry_by_index(self, index: int) -> ManipulationHistoryEntry | None: + """Get an entry by its index. + + Args: + index: Index of the entry to retrieve + + Returns: + The entry at the specified index or None if index is out of bounds + """ + if 0 <= index < len(self._history): + return self._history[index] + return None + + def get_entries_by_timerange( + self, start_time: float, end_time: float + ) -> list[ManipulationHistoryEntry]: + """Get entries within a specific time range. + + Args: + start_time: Start time (UNIX timestamp) + end_time: End time (UNIX timestamp) + + Returns: + List of entries within the specified time range + """ + return [entry for entry in self._history if start_time <= entry.timestamp <= end_time] + + def get_entries_by_object(self, object_name: str) -> list[ManipulationHistoryEntry]: + """Get entries related to a specific object. + + Args: + object_name: Name of the object to search for + + Returns: + List of entries related to the specified object + """ + return [entry for entry in self._history if entry.task.target_object == object_name] + + def create_task_entry( + self, + task: ManipulationTask, + result: dict[str, Any] | None = None, + agent_response: str | None = None, + ) -> ManipulationHistoryEntry: + """Create a new manipulation history entry. + + Args: + task: The manipulation task + result: Result of the manipulation + agent_response: Response from the agent about this manipulation + + Returns: + The created history entry + """ + entry = ManipulationHistoryEntry( + task=task, result=result or {}, manipulation_response=agent_response + ) + self.add_entry(entry) + return entry + + def search(self, **kwargs) -> list[ManipulationHistoryEntry]: + """Flexible search method that can search by any field in ManipulationHistoryEntry using dot notation. + + This method supports dot notation to access nested fields. String values automatically use + substring matching (contains), while all other types use exact matching. + + Examples: + # Time-based searches: + - search(**{"task.metadata.timestamp": ('>', start_time)}) - entries after start_time + - search(**{"task.metadata.timestamp": ('>=', time - 1800)}) - entries in last 30 mins + + # Constraint searches: + - search(**{"task.constraints.*.reference_point.x": 2.5}) - tasks with x=2.5 reference point + - search(**{"task.constraints.*.end_angle.x": 90}) - tasks with 90-degree x rotation + - search(**{"task.constraints.*.lock_x": True}) - tasks with x-axis translation locked + + # Object and result searches: + - search(**{"task.metadata.objects.*.label": "cup"}) - tasks involving cups + - search(**{"result.status": "success"}) - successful tasks + - search(**{"result.error": "Collision"}) - tasks that had collisions + + Args: + **kwargs: Key-value pairs for searching using dot notation for field paths. + + Returns: + List of matching entries + """ + if not kwargs: + return self._history.copy() + + results = self._history.copy() + + for key, value in kwargs.items(): + # For all searches, automatically determine if we should use contains for strings + results = [e for e in results if self._check_field_match(e, key, value)] + + return results + + def _check_field_match(self, entry, field_path, value) -> bool: + """Check if a field matches the value, with special handling for strings, collections and comparisons. + + For string values, we automatically use substring matching (contains). + For collections (returned by * path), we check if any element matches. + For numeric values (like timestamps), supports >, <, >= and <= comparisons. + For all other types, we use exact matching. + + Args: + entry: The entry to check + field_path: Dot-separated path to the field + value: Value to match against. For comparisons, use tuples like: + ('>', timestamp) - greater than + ('<', timestamp) - less than + ('>=', timestamp) - greater or equal + ('<=', timestamp) - less or equal + + Returns: + True if the field matches the value, False otherwise + """ + try: + field_value = self._get_value_by_path(entry, field_path) + + # Handle comparison operators for timestamps and numbers + if isinstance(value, tuple) and len(value) == 2: + op, compare_value = value + if op == ">": + return field_value > compare_value + elif op == "<": + return field_value < compare_value + elif op == ">=": + return field_value >= compare_value + elif op == "<=": + return field_value <= compare_value + + # Handle lists (from collection searches) + if isinstance(field_value, list): + for item in field_value: + # String values use contains matching + if isinstance(item, str) and isinstance(value, str): + if value in item: + return True + # All other types use exact matching + elif item == value: + return True + return False + + # String values use contains matching + elif isinstance(field_value, str) and isinstance(value, str): + return value in field_value + # All other types use exact matching + else: + return field_value == value + + except (AttributeError, KeyError): + return False + + def _get_value_by_path(self, obj, path): + """Get a value from an object using a dot-separated path. + + This method handles three special cases: + 1. Regular attribute access (obj.attr) + 2. Dictionary key access (dict[key]) + 3. Collection search (dict.*.attr) - when * is used, it searches all values in the collection + + Args: + obj: Object to get value from + path: Dot-separated path to the field (e.g., "task.metadata.robot") + + Returns: + Value at the specified path or list of values for collection searches + + Raises: + AttributeError: If an attribute in the path doesn't exist + KeyError: If a dictionary key in the path doesn't exist + """ + current = obj + parts = path.split(".") + + for i, part in enumerate(parts): + # Collection search (*.attr) - search across all items in a collection + if part == "*": + # Get remaining path parts + remaining_path = ".".join(parts[i + 1 :]) + + # Handle different collection types + if isinstance(current, dict): + items = current.values() + if not remaining_path: # If * is the last part, return all values + return list(items) + elif isinstance(current, list): + items = current + if not remaining_path: # If * is the last part, return all items + return items + else: # Not a collection + raise AttributeError( + f"Cannot use wildcard on non-collection type: {type(current)}" + ) + + # Apply remaining path to each item in the collection + results = [] + for item in items: + try: + # Recursively get values from each item + value = self._get_value_by_path(item, remaining_path) + if isinstance(value, list): # Flatten nested lists + results.extend(value) + else: + results.append(value) + except (AttributeError, KeyError): + # Skip items that don't have the attribute + pass + return results + + # Regular attribute/key access + elif isinstance(current, dict): + current = current[part] + else: + current = getattr(current, part) + + return current diff --git a/dimos/manipulation/manipulation_interface.py b/dimos/manipulation/manipulation_interface.py new file mode 100644 index 0000000000..ae63eb79ed --- /dev/null +++ b/dimos/manipulation/manipulation_interface.py @@ -0,0 +1,286 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +ManipulationInterface provides a unified interface for accessing manipulation history. + +This module defines the ManipulationInterface class, which serves as an access point +for the robot's manipulation history, agent-generated constraints, and manipulation +metadata streams. +""" + +import os +from typing import TYPE_CHECKING, Any + +from dimos.manipulation.manipulation_history import ( + ManipulationHistory, +) +from dimos.perception.object_detection_stream import ObjectDetectionStream +from dimos.types.manipulation import ( + AbstractConstraint, + ManipulationTask, + ObjectData, +) +from dimos.utils.logging_config import setup_logger + +if TYPE_CHECKING: + from reactivex.disposable import Disposable + +logger = setup_logger("dimos.robot.manipulation_interface") + + +class ManipulationInterface: + """ + Interface for accessing and managing robot manipulation data. + + This class provides a unified interface for managing manipulation tasks and constraints. + It maintains a list of constraints generated by the Agent and provides methods to + add and manage manipulation tasks. + """ + + def __init__( + self, + output_dir: str, + new_memory: bool = False, + perception_stream: ObjectDetectionStream = None, + ) -> None: + """ + Initialize a new ManipulationInterface instance. + + Args: + output_dir: Directory for storing manipulation data + new_memory: If True, creates a new manipulation history from scratch + perception_stream: ObjectDetectionStream instance for real-time object data + """ + self.output_dir = output_dir + + # Create manipulation history directory + manipulation_dir = os.path.join(output_dir, "manipulation_history") + os.makedirs(manipulation_dir, exist_ok=True) + + # Initialize manipulation history + self.manipulation_history: ManipulationHistory = ManipulationHistory( + output_dir=manipulation_dir, new_memory=new_memory + ) + + # List of constraints generated by the Agent via constraint generation skills + self.agent_constraints: list[AbstractConstraint] = [] + + # Initialize object detection stream and related properties + self.perception_stream = perception_stream + self.latest_objects: list[ObjectData] = [] + self.stream_subscription: Disposable | None = None + + # Set up subscription to perception stream if available + self._setup_perception_subscription() + + logger.info("ManipulationInterface initialized") + + def add_constraint(self, constraint: AbstractConstraint) -> None: + """ + Add a constraint generated by the Agent via a constraint generation skill. + + Args: + constraint: The constraint to add to agent_constraints + """ + self.agent_constraints.append(constraint) + logger.info(f"Added agent constraint: {constraint}") + + def get_constraints(self) -> list[AbstractConstraint]: + """ + Get all constraints generated by the Agent via constraint generation skills. + + Returns: + List of all constraints created by the Agent + """ + return self.agent_constraints + + def get_constraint(self, constraint_id: str) -> AbstractConstraint | None: + """ + Get a specific constraint by its ID. + + Args: + constraint_id: ID of the constraint to retrieve + + Returns: + The matching constraint or None if not found + """ + # Find constraint with matching ID + for constraint in self.agent_constraints: + if constraint.id == constraint_id: + return constraint + + logger.warning(f"Constraint with ID {constraint_id} not found") + return None + + def add_manipulation_task( + self, task: ManipulationTask, manipulation_response: str | None = None + ) -> None: + """ + Add a manipulation task to ManipulationHistory. + + Args: + task: The ManipulationTask to add + manipulation_response: Optional response from the motion planner/executor + + """ + # Add task to history + self.manipulation_history.add_entry( + task=task, result=None, notes=None, manipulation_response=manipulation_response + ) + + def get_manipulation_task(self, task_id: str) -> ManipulationTask | None: + """ + Get a manipulation task by its ID. + + Args: + task_id: ID of the task to retrieve + + Returns: + The task object or None if not found + """ + return self.history.get_manipulation_task(task_id) + + def get_all_manipulation_tasks(self) -> list[ManipulationTask]: + """ + Get all manipulation tasks. + + Returns: + List of all manipulation tasks + """ + return self.history.get_all_manipulation_tasks() + + def update_task_status( + self, task_id: str, status: str, result: dict[str, Any] | None = None + ) -> ManipulationTask | None: + """ + Update the status and result of a manipulation task. + + Args: + task_id: ID of the task to update + status: New status for the task (e.g., 'completed', 'failed') + result: Optional dictionary with result data + + Returns: + The updated task or None if task not found + """ + return self.history.update_task_status(task_id, status, result) + + # === Perception stream methods === + + def _setup_perception_subscription(self) -> None: + """ + Set up subscription to perception stream if available. + """ + if self.perception_stream: + # Subscribe to the stream and update latest_objects + self.stream_subscription = self.perception_stream.get_stream().subscribe( + on_next=self._update_latest_objects, + on_error=lambda e: logger.error(f"Error in perception stream: {e}"), + ) + logger.info("Subscribed to perception stream") + + def _update_latest_objects(self, data) -> None: + """ + Update the latest detected objects. + + Args: + data: Data from the object detection stream + """ + if "objects" in data: + self.latest_objects = data["objects"] + + def get_latest_objects(self) -> list[ObjectData]: + """ + Get the latest detected objects from the stream. + + Returns: + List of the most recently detected objects + """ + return self.latest_objects + + def get_object_by_id(self, object_id: int) -> ObjectData | None: + """ + Get a specific object by its tracking ID. + + Args: + object_id: Tracking ID of the object + + Returns: + The object data or None if not found + """ + for obj in self.latest_objects: + if obj["object_id"] == object_id: + return obj + return None + + def get_objects_by_label(self, label: str) -> list[ObjectData]: + """ + Get all objects with a specific label. + + Args: + label: Class label to filter objects by + + Returns: + List of objects matching the label + """ + return [obj for obj in self.latest_objects if obj["label"] == label] + + def set_perception_stream(self, perception_stream) -> None: + """ + Set or update the perception stream. + + Args: + perception_stream: The PerceptionStream instance + """ + # Clean up existing subscription if any + self.cleanup_perception_subscription() + + # Set new stream and subscribe + self.perception_stream = perception_stream + self._setup_perception_subscription() + + def cleanup_perception_subscription(self) -> None: + """ + Clean up the stream subscription. + """ + if self.stream_subscription: + self.stream_subscription.dispose() + self.stream_subscription = None + + # === Utility methods === + + def clear_history(self) -> None: + """ + Clear all manipulation history data and agent constraints. + """ + self.manipulation_history.clear() + self.agent_constraints.clear() + logger.info("Cleared manipulation history and agent constraints") + + def __str__(self) -> str: + """ + String representation of the manipulation interface. + + Returns: + String representation with key stats + """ + has_stream = self.perception_stream is not None + return f"ManipulationInterface(history={self.manipulation_history}, agent_constraints={len(self.agent_constraints)}, perception_stream={has_stream}, detected_objects={len(self.latest_objects)})" + + def __del__(self) -> None: + """ + Clean up resources on deletion. + """ + self.cleanup_perception_subscription() diff --git a/dimos/manipulation/test_manipulation_history.py b/dimos/manipulation/test_manipulation_history.py new file mode 100644 index 0000000000..141c9365aa --- /dev/null +++ b/dimos/manipulation/test_manipulation_history.py @@ -0,0 +1,458 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import tempfile +import time + +import pytest + +from dimos.manipulation.manipulation_history import ManipulationHistory, ManipulationHistoryEntry +from dimos.types.manipulation import ( + ForceConstraint, + ManipulationTask, + RotationConstraint, + TranslationConstraint, +) +from dimos.types.vector import Vector + + +@pytest.fixture +def sample_task(): + """Create a sample manipulation task for testing.""" + return ManipulationTask( + description="Pick up the cup", + target_object="cup", + target_point=(100, 200), + task_id="task1", + metadata={ + "timestamp": time.time(), + "objects": { + "cup1": { + "object_id": 1, + "label": "cup", + "confidence": 0.95, + "position": {"x": 1.5, "y": 2.0, "z": 0.5}, + }, + "table1": { + "object_id": 2, + "label": "table", + "confidence": 0.98, + "position": {"x": 0.0, "y": 0.0, "z": 0.0}, + }, + }, + }, + ) + + +@pytest.fixture +def sample_task_with_constraints(): + """Create a sample manipulation task with constraints for testing.""" + task = ManipulationTask( + description="Rotate the bottle", + target_object="bottle", + target_point=(150, 250), + task_id="task2", + metadata={ + "timestamp": time.time(), + "objects": { + "bottle1": { + "object_id": 3, + "label": "bottle", + "confidence": 0.92, + "position": {"x": 2.5, "y": 1.0, "z": 0.3}, + } + }, + }, + ) + + # Add rich translation constraint + translation_constraint = TranslationConstraint( + translation_axis="y", + reference_point=Vector(2.5, 1.0, 0.3), + bounds_min=Vector(2.0, 0.5, 0.3), + bounds_max=Vector(3.0, 1.5, 0.3), + target_point=Vector(2.7, 1.2, 0.3), + description="Constrained translation along Y-axis only", + ) + task.add_constraint(translation_constraint) + + # Add rich rotation constraint + rotation_constraint = RotationConstraint( + rotation_axis="roll", + start_angle=Vector(0, 0, 0), + end_angle=Vector(90, 0, 0), + pivot_point=Vector(2.5, 1.0, 0.3), + secondary_pivot_point=Vector(2.5, 1.0, 0.5), + description="Constrained rotation around X-axis (roll only)", + ) + task.add_constraint(rotation_constraint) + + # Add force constraint + force_constraint = ForceConstraint( + min_force=2.0, + max_force=5.0, + force_direction=Vector(0, 0, -1), + description="Apply moderate downward force during manipulation", + ) + task.add_constraint(force_constraint) + + return task + + +@pytest.fixture +def temp_output_dir(): + """Create a temporary directory for testing history saving/loading.""" + with tempfile.TemporaryDirectory() as temp_dir: + yield temp_dir + + +@pytest.fixture +def populated_history(sample_task, sample_task_with_constraints): + """Create a populated history with multiple entries for testing.""" + history = ManipulationHistory() + + # Add first entry + entry1 = ManipulationHistoryEntry( + task=sample_task, + result={"status": "success", "execution_time": 2.5}, + manipulation_response="Successfully picked up the cup", + ) + history.add_entry(entry1) + + # Add second entry + entry2 = ManipulationHistoryEntry( + task=sample_task_with_constraints, + result={"status": "failure", "error": "Collision detected"}, + manipulation_response="Failed to rotate the bottle due to collision", + ) + history.add_entry(entry2) + + return history + + +def test_manipulation_history_init() -> None: + """Test initialization of ManipulationHistory.""" + # Default initialization + history = ManipulationHistory() + assert len(history) == 0 + assert str(history) == "ManipulationHistory(empty)" + + # With output directory + with tempfile.TemporaryDirectory() as temp_dir: + history = ManipulationHistory(output_dir=temp_dir, new_memory=True) + assert len(history) == 0 + assert os.path.exists(temp_dir) + + +def test_manipulation_history_add_entry(sample_task) -> None: + """Test adding entries to ManipulationHistory.""" + history = ManipulationHistory() + + # Create and add entry + entry = ManipulationHistoryEntry( + task=sample_task, result={"status": "success"}, manipulation_response="Task completed" + ) + history.add_entry(entry) + + assert len(history) == 1 + assert history.get_entry_by_index(0) == entry + + +def test_manipulation_history_create_task_entry(sample_task) -> None: + """Test creating a task entry directly.""" + history = ManipulationHistory() + + entry = history.create_task_entry( + task=sample_task, result={"status": "success"}, agent_response="Task completed" + ) + + assert len(history) == 1 + assert entry.task == sample_task + assert entry.result["status"] == "success" + assert entry.manipulation_response == "Task completed" + + +def test_manipulation_history_save_load(temp_output_dir, sample_task) -> None: + """Test saving and loading history from disk.""" + # Create history and add entry + history = ManipulationHistory(output_dir=temp_output_dir) + history.create_task_entry( + task=sample_task, result={"status": "success"}, agent_response="Task completed" + ) + + # Check that files were created + pickle_path = os.path.join(temp_output_dir, "manipulation_history.pickle") + json_path = os.path.join(temp_output_dir, "manipulation_history.json") + assert os.path.exists(pickle_path) + assert os.path.exists(json_path) + + # Create new history that loads from the saved files + loaded_history = ManipulationHistory(output_dir=temp_output_dir) + assert len(loaded_history) == 1 + assert loaded_history.get_entry_by_index(0).task.description == sample_task.description + + +def test_manipulation_history_clear(populated_history) -> None: + """Test clearing the history.""" + assert len(populated_history) > 0 + + populated_history.clear() + assert len(populated_history) == 0 + assert str(populated_history) == "ManipulationHistory(empty)" + + +def test_manipulation_history_get_methods(populated_history) -> None: + """Test various getter methods of ManipulationHistory.""" + # get_all_entries + entries = populated_history.get_all_entries() + assert len(entries) == 2 + + # get_entry_by_index + entry = populated_history.get_entry_by_index(0) + assert entry.task.task_id == "task1" + + # Out of bounds index + assert populated_history.get_entry_by_index(100) is None + + # get_entries_by_timerange + start_time = time.time() - 3600 # 1 hour ago + end_time = time.time() + 3600 # 1 hour from now + entries = populated_history.get_entries_by_timerange(start_time, end_time) + assert len(entries) == 2 + + # get_entries_by_object + cup_entries = populated_history.get_entries_by_object("cup") + assert len(cup_entries) == 1 + assert cup_entries[0].task.task_id == "task1" + + bottle_entries = populated_history.get_entries_by_object("bottle") + assert len(bottle_entries) == 1 + assert bottle_entries[0].task.task_id == "task2" + + +def test_manipulation_history_search_basic(populated_history) -> None: + """Test basic search functionality.""" + # Search by exact match on top-level fields + results = populated_history.search(timestamp=populated_history.get_entry_by_index(0).timestamp) + assert len(results) == 1 + + # Search by task fields + results = populated_history.search(**{"task.task_id": "task1"}) + assert len(results) == 1 + assert results[0].task.target_object == "cup" + + # Search by result fields + results = populated_history.search(**{"result.status": "success"}) + assert len(results) == 1 + assert results[0].task.task_id == "task1" + + # Search by manipulation_response (substring match for strings) + results = populated_history.search(manipulation_response="picked up") + assert len(results) == 1 + assert results[0].task.task_id == "task1" + + +def test_manipulation_history_search_nested(populated_history) -> None: + """Test search with nested field paths.""" + # Search by nested metadata fields + results = populated_history.search( + **{ + "task.metadata.timestamp": populated_history.get_entry_by_index(0).task.metadata[ + "timestamp" + ] + } + ) + assert len(results) == 1 + + # Search by nested object fields + results = populated_history.search(**{"task.metadata.objects.cup1.label": "cup"}) + assert len(results) == 1 + assert results[0].task.task_id == "task1" + + # Search by position values + results = populated_history.search(**{"task.metadata.objects.cup1.position.x": 1.5}) + assert len(results) == 1 + assert results[0].task.task_id == "task1" + + +def test_manipulation_history_search_wildcards(populated_history) -> None: + """Test search with wildcard patterns.""" + # Search for any object with label "cup" + results = populated_history.search(**{"task.metadata.objects.*.label": "cup"}) + assert len(results) == 1 + assert results[0].task.task_id == "task1" + + # Search for any object with confidence > 0.95 + results = populated_history.search(**{"task.metadata.objects.*.confidence": 0.98}) + assert len(results) == 1 + assert results[0].task.task_id == "task1" + + # Search for any object position with x=2.5 + results = populated_history.search(**{"task.metadata.objects.*.position.x": 2.5}) + assert len(results) == 1 + assert results[0].task.task_id == "task2" + + +def test_manipulation_history_search_constraints(populated_history) -> None: + """Test search by constraint properties.""" + # Find entries with any TranslationConstraint with y-axis + results = populated_history.search(**{"task.constraints.*.translation_axis": "y"}) + assert len(results) == 1 + assert results[0].task.task_id == "task2" + + # Find entries with any RotationConstraint with roll axis + results = populated_history.search(**{"task.constraints.*.rotation_axis": "roll"}) + assert len(results) == 1 + assert results[0].task.task_id == "task2" + + +def test_manipulation_history_search_string_contains(populated_history) -> None: + """Test string contains searching.""" + # Basic string contains + results = populated_history.search(**{"task.description": "Pick"}) + assert len(results) == 1 + assert results[0].task.task_id == "task1" + + # Nested string contains + results = populated_history.search(manipulation_response="collision") + assert len(results) == 1 + assert results[0].task.task_id == "task2" + + +def test_manipulation_history_search_multiple_criteria(populated_history) -> None: + """Test search with multiple criteria.""" + # Multiple criteria - all must match + results = populated_history.search(**{"task.target_object": "cup", "result.status": "success"}) + assert len(results) == 1 + assert results[0].task.task_id == "task1" + + # Multiple criteria with no matches + results = populated_history.search(**{"task.target_object": "cup", "result.status": "failure"}) + assert len(results) == 0 + + # Combination of direct and wildcard paths + results = populated_history.search( + **{"task.target_object": "bottle", "task.metadata.objects.*.position.z": 0.3} + ) + assert len(results) == 1 + assert results[0].task.task_id == "task2" + + +def test_manipulation_history_search_nonexistent_fields(populated_history) -> None: + """Test search with fields that don't exist.""" + # Search by nonexistent field + results = populated_history.search(nonexistent_field="value") + assert len(results) == 0 + + # Search by nonexistent nested field + results = populated_history.search(**{"task.nonexistent_field": "value"}) + assert len(results) == 0 + + # Search by nonexistent object + results = populated_history.search(**{"task.metadata.objects.nonexistent_object": "value"}) + assert len(results) == 0 + + +def test_manipulation_history_search_timestamp_ranges(populated_history) -> None: + """Test searching by timestamp ranges.""" + # Get reference timestamps + entry1_time = populated_history.get_entry_by_index(0).task.metadata["timestamp"] + entry2_time = populated_history.get_entry_by_index(1).task.metadata["timestamp"] + mid_time = (entry1_time + entry2_time) / 2 + + # Search for timestamps before second entry + results = populated_history.search(**{"task.metadata.timestamp": ("<", entry2_time)}) + assert len(results) == 1 + assert results[0].task.task_id == "task1" + + # Search for timestamps after first entry + results = populated_history.search(**{"task.metadata.timestamp": (">", entry1_time)}) + assert len(results) == 1 + assert results[0].task.task_id == "task2" + + # Search within a time window using >= and <= + results = populated_history.search(**{"task.metadata.timestamp": (">=", mid_time - 1800)}) + assert len(results) == 2 + assert results[0].task.task_id == "task1" + assert results[1].task.task_id == "task2" + + +def test_manipulation_history_search_vector_fields(populated_history) -> None: + """Test searching by vector components in constraints.""" + # Search by reference point components + results = populated_history.search(**{"task.constraints.*.reference_point.x": 2.5}) + assert len(results) == 1 + assert results[0].task.task_id == "task2" + + # Search by target point components + results = populated_history.search(**{"task.constraints.*.target_point.z": 0.3}) + assert len(results) == 1 + assert results[0].task.task_id == "task2" + + # Search by rotation angles + results = populated_history.search(**{"task.constraints.*.end_angle.x": 90}) + assert len(results) == 1 + assert results[0].task.task_id == "task2" + + +def test_manipulation_history_search_execution_details(populated_history) -> None: + """Test searching by execution time and error patterns.""" + # Search by execution time + results = populated_history.search(**{"result.execution_time": 2.5}) + assert len(results) == 1 + assert results[0].task.task_id == "task1" + + # Search by error message pattern + results = populated_history.search(**{"result.error": "Collision"}) + assert len(results) == 1 + assert results[0].task.task_id == "task2" + + # Search by status + results = populated_history.search(**{"result.status": "success"}) + assert len(results) == 1 + assert results[0].task.task_id == "task1" + + +def test_manipulation_history_search_multiple_criteria(populated_history) -> None: + """Test search with multiple criteria.""" + # Multiple criteria - all must match + results = populated_history.search(**{"task.target_object": "cup", "result.status": "success"}) + assert len(results) == 1 + assert results[0].task.task_id == "task1" + + # Multiple criteria with no matches + results = populated_history.search(**{"task.target_object": "cup", "result.status": "failure"}) + assert len(results) == 0 + + # Combination of direct and wildcard paths + results = populated_history.search( + **{"task.target_object": "bottle", "task.metadata.objects.*.position.z": 0.3} + ) + assert len(results) == 1 + assert results[0].task.task_id == "task2" diff --git a/dimos/manipulation/visual_servoing/detection3d.py b/dimos/manipulation/visual_servoing/detection3d.py new file mode 100644 index 0000000000..f7371f531a --- /dev/null +++ b/dimos/manipulation/visual_servoing/detection3d.py @@ -0,0 +1,299 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Real-time 3D object detection processor that extracts object poses from RGB-D data. +""" + +import cv2 +from dimos_lcm.vision_msgs import ( + BoundingBox2D, + BoundingBox3D, + Detection2D, + Detection3D, + ObjectHypothesis, + ObjectHypothesisWithPose, + Point2D, + Pose2D, +) +import numpy as np + +from dimos.manipulation.visual_servoing.utils import ( + estimate_object_depth, + transform_pose, + visualize_detections_3d, +) +from dimos.msgs.geometry_msgs import Pose, Quaternion, Vector3 +from dimos.msgs.std_msgs import Header +from dimos.msgs.vision_msgs import Detection2DArray, Detection3DArray +from dimos.perception.common.utils import bbox2d_to_corners +from dimos.perception.detection2d.utils import calculate_object_size_from_bbox +from dimos.perception.pointcloud.utils import extract_centroids_from_masks +from dimos.perception.segmentation.sam_2d_seg import Sam2DSegmenter +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.manipulation.visual_servoing.detection3d") + + +class Detection3DProcessor: + """ + Real-time 3D detection processor optimized for speed. + + Uses Sam (FastSAM) for segmentation and mask generation, then extracts + 3D centroids from depth data. + """ + + def __init__( + self, + camera_intrinsics: list[float], # [fx, fy, cx, cy] + min_confidence: float = 0.6, + min_points: int = 30, + max_depth: float = 1.0, + max_object_size: float = 0.15, + ) -> None: + """ + Initialize the real-time 3D detection processor. + + Args: + camera_intrinsics: [fx, fy, cx, cy] camera parameters + min_confidence: Minimum detection confidence threshold + min_points: Minimum 3D points required for valid detection + max_depth: Maximum valid depth in meters + """ + self.camera_intrinsics = camera_intrinsics + self.min_points = min_points + self.max_depth = max_depth + self.max_object_size = max_object_size + + # Initialize Sam segmenter with tracking enabled but analysis disabled + self.detector = Sam2DSegmenter( + use_tracker=False, + use_analyzer=False, + use_filtering=True, + ) + + self.min_confidence = min_confidence + + logger.info( + f"Initialized Detection3DProcessor with Sam segmenter, confidence={min_confidence}, " + f"min_points={min_points}, max_depth={max_depth}m, max_object_size={max_object_size}m" + ) + + def process_frame( + self, rgb_image: np.ndarray, depth_image: np.ndarray, transform: np.ndarray | None = None + ) -> tuple[Detection3DArray, Detection2DArray]: + """ + Process a single RGB-D frame to extract 3D object detections. + + Args: + rgb_image: RGB image (H, W, 3) + depth_image: Depth image (H, W) in meters + transform: Optional 4x4 transformation matrix to transform objects from camera frame to desired frame + + Returns: + Tuple of (Detection3DArray, Detection2DArray) with 3D and 2D information + """ + + # Convert RGB to BGR for Sam (OpenCV format) + bgr_image = cv2.cvtColor(rgb_image, cv2.COLOR_RGB2BGR) + + # Run Sam segmentation with tracking + masks, bboxes, track_ids, probs, names = self.detector.process_image(bgr_image) + + if not masks or len(masks) == 0: + return Detection3DArray( + detections_length=0, header=Header(), detections=[] + ), Detection2DArray(detections_length=0, header=Header(), detections=[]) + + # Convert CUDA tensors to numpy arrays if needed + numpy_masks = [] + for mask in masks: + if hasattr(mask, "cpu"): # PyTorch tensor + numpy_masks.append(mask.cpu().numpy()) + else: # Already numpy array + numpy_masks.append(mask) + + # Extract 3D centroids from masks + poses = extract_centroids_from_masks( + rgb_image=rgb_image, + depth_image=depth_image, + masks=numpy_masks, + camera_intrinsics=self.camera_intrinsics, + ) + + detections_3d = [] + detections_2d = [] + pose_dict = {p["mask_idx"]: p for p in poses if p["centroid"][2] < self.max_depth} + + for i, (bbox, name, prob, track_id) in enumerate( + zip(bboxes, names, probs, track_ids, strict=False) + ): + if i not in pose_dict: + continue + + pose = pose_dict[i] + obj_cam_pos = pose["centroid"] + + if obj_cam_pos[2] > self.max_depth: + continue + + # Calculate object size from bbox and depth + width_m, height_m = calculate_object_size_from_bbox( + bbox, obj_cam_pos[2], self.camera_intrinsics + ) + + # Calculate depth dimension using segmentation mask + depth_m = estimate_object_depth( + depth_image, numpy_masks[i] if i < len(numpy_masks) else None, bbox + ) + + size_x = max(width_m, 0.01) # Minimum 1cm width + size_y = max(height_m, 0.01) # Minimum 1cm height + size_z = max(depth_m, 0.01) # Minimum 1cm depth + + if min(size_x, size_y, size_z) > self.max_object_size: + continue + + # Transform to desired frame if transform matrix is provided + if transform is not None: + # Get orientation as euler angles, default to no rotation if not available + obj_cam_orientation = pose.get( + "rotation", np.array([0.0, 0.0, 0.0]) + ) # Default to no rotation + transformed_pose = transform_pose( + obj_cam_pos, obj_cam_orientation, transform, to_robot=True + ) + center_pose = transformed_pose + else: + # If no transform, use camera coordinates + center_pose = Pose( + position=Vector3(obj_cam_pos[0], obj_cam_pos[1], obj_cam_pos[2]), + orientation=Quaternion(0.0, 0.0, 0.0, 1.0), # Default orientation + ) + + # Create Detection3D object + detection = Detection3D( + results_length=1, + header=Header(), # Empty header + results=[ + ObjectHypothesisWithPose( + hypothesis=ObjectHypothesis(class_id=name, score=float(prob)) + ) + ], + bbox=BoundingBox3D(center=center_pose, size=Vector3(size_x, size_y, size_z)), + id=str(track_id), + ) + + detections_3d.append(detection) + + # Create corresponding Detection2D + x1, y1, x2, y2 = bbox + center_x = (x1 + x2) / 2.0 + center_y = (y1 + y2) / 2.0 + width = x2 - x1 + height = y2 - y1 + + detection_2d = Detection2D( + results_length=1, + header=Header(), + results=[ + ObjectHypothesisWithPose( + hypothesis=ObjectHypothesis(class_id=name, score=float(prob)) + ) + ], + bbox=BoundingBox2D( + center=Pose2D(position=Point2D(center_x, center_y), theta=0.0), + size_x=float(width), + size_y=float(height), + ), + id=str(track_id), + ) + detections_2d.append(detection_2d) + + # Create and return both arrays + return ( + Detection3DArray( + detections_length=len(detections_3d), header=Header(), detections=detections_3d + ), + Detection2DArray( + detections_length=len(detections_2d), header=Header(), detections=detections_2d + ), + ) + + def visualize_detections( + self, + rgb_image: np.ndarray, + detections_3d: list[Detection3D], + detections_2d: list[Detection2D], + show_coordinates: bool = True, + ) -> np.ndarray: + """ + Visualize detections with 3D position overlay next to bounding boxes. + + Args: + rgb_image: Original RGB image + detections_3d: List of Detection3D objects + detections_2d: List of Detection2D objects (must be 1:1 correspondence) + show_coordinates: Whether to show 3D coordinates + + Returns: + Visualization image + """ + # Extract 2D bboxes from Detection2D objects + + bboxes_2d = [] + for det_2d in detections_2d: + if det_2d.bbox: + x1, y1, x2, y2 = bbox2d_to_corners(det_2d.bbox) + bboxes_2d.append([x1, y1, x2, y2]) + + return visualize_detections_3d(rgb_image, detections_3d, show_coordinates, bboxes_2d) + + def get_closest_detection( + self, detections: list[Detection3D], class_filter: str | None = None + ) -> Detection3D | None: + """ + Get the closest detection with valid 3D data. + + Args: + detections: List of Detection3D objects + class_filter: Optional class name to filter by + + Returns: + Closest Detection3D or None + """ + valid_detections = [] + for d in detections: + # Check if has valid bbox center position + if d.bbox and d.bbox.center and d.bbox.center.position: + # Check class filter if specified + if class_filter is None or ( + d.results_length > 0 and d.results[0].hypothesis.class_id == class_filter + ): + valid_detections.append(d) + + if not valid_detections: + return None + + # Sort by depth (Z coordinate) + def get_z_coord(d): + return abs(d.bbox.center.position.z) + + return min(valid_detections, key=get_z_coord) + + def cleanup(self) -> None: + """Clean up resources.""" + if hasattr(self.detector, "cleanup"): + self.detector.cleanup() + logger.info("Detection3DProcessor cleaned up") diff --git a/dimos/manipulation/visual_servoing/manipulation_module.py b/dimos/manipulation/visual_servoing/manipulation_module.py new file mode 100644 index 0000000000..a89d43ed7b --- /dev/null +++ b/dimos/manipulation/visual_servoing/manipulation_module.py @@ -0,0 +1,949 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Manipulation module for robotic grasping with visual servoing. +Handles grasping logic, state machine, and hardware coordination as a Dimos module. +""" + +from collections import deque +from enum import Enum +import threading +import time +from typing import Any + +import cv2 +from dimos_lcm.sensor_msgs import CameraInfo +import numpy as np +from reactivex.disposable import Disposable + +from dimos.core import In, Module, Out, rpc +from dimos.hardware.piper_arm import PiperArm +from dimos.manipulation.visual_servoing.detection3d import Detection3DProcessor +from dimos.manipulation.visual_servoing.pbvs import PBVS +from dimos.manipulation.visual_servoing.utils import ( + create_manipulation_visualization, + is_target_reached, + select_points_from_depth, + transform_points_3d, + update_target_grasp_pose, +) +from dimos.msgs.geometry_msgs import Pose, Quaternion, Vector3 +from dimos.msgs.sensor_msgs import Image +from dimos.msgs.vision_msgs import Detection2DArray, Detection3DArray +from dimos.perception.common.utils import find_clicked_detection +from dimos.utils.logging_config import setup_logger +from dimos.utils.transform_utils import ( + compose_transforms, + create_transform_from_6dof, + matrix_to_pose, + pose_to_matrix, +) + +logger = setup_logger("dimos.manipulation.visual_servoing.manipulation_module") + + +class GraspStage(Enum): + """Enum for different grasp stages.""" + + IDLE = "idle" + PRE_GRASP = "pre_grasp" + GRASP = "grasp" + CLOSE_AND_RETRACT = "close_and_retract" + PLACE = "place" + RETRACT = "retract" + + +class Feedback: + """Feedback data containing state information about the manipulation process.""" + + def __init__( + self, + grasp_stage: GraspStage, + target_tracked: bool, + current_executed_pose: Pose | None = None, + current_ee_pose: Pose | None = None, + current_camera_pose: Pose | None = None, + target_pose: Pose | None = None, + waiting_for_reach: bool = False, + success: bool | None = None, + ) -> None: + self.grasp_stage = grasp_stage + self.target_tracked = target_tracked + self.current_executed_pose = current_executed_pose + self.current_ee_pose = current_ee_pose + self.current_camera_pose = current_camera_pose + self.target_pose = target_pose + self.waiting_for_reach = waiting_for_reach + self.success = success + + +class ManipulationModule(Module): + """ + Manipulation module for visual servoing and grasping. + + Subscribes to: + - ZED RGB images + - ZED depth images + - ZED camera info + + Publishes: + - Visualization images + + RPC methods: + - handle_keyboard_command: Process keyboard input + - pick_and_place: Execute pick and place task + """ + + # LCM inputs + rgb_image: In[Image] = None + depth_image: In[Image] = None + camera_info: In[CameraInfo] = None + + # LCM outputs + viz_image: Out[Image] = None + + def __init__( + self, + ee_to_camera_6dof: list | None = None, + **kwargs, + ) -> None: + """ + Initialize manipulation module. + + Args: + ee_to_camera_6dof: EE to camera transform [x, y, z, rx, ry, rz] in meters and radians + workspace_min_radius: Minimum workspace radius in meters + workspace_max_radius: Maximum workspace radius in meters + min_grasp_pitch_degrees: Minimum grasp pitch angle (at max radius) + max_grasp_pitch_degrees: Maximum grasp pitch angle (at min radius) + """ + super().__init__(**kwargs) + + self.arm = PiperArm() + + if ee_to_camera_6dof is None: + ee_to_camera_6dof = [-0.065, 0.03, -0.095, 0.0, -1.57, 0.0] + pos = Vector3(ee_to_camera_6dof[0], ee_to_camera_6dof[1], ee_to_camera_6dof[2]) + rot = Vector3(ee_to_camera_6dof[3], ee_to_camera_6dof[4], ee_to_camera_6dof[5]) + self.T_ee_to_camera = create_transform_from_6dof(pos, rot) + + self.camera_intrinsics = None + self.detector = None + self.pbvs = None + + # Control state + self.last_valid_target = None + self.waiting_for_reach = False + self.current_executed_pose = None # Track the actual pose sent to arm + self.target_updated = False + self.waiting_start_time = None + self.reach_pose_timeout = 20.0 + + # Grasp parameters + self.grasp_width_offset = 0.03 + self.pregrasp_distance = 0.25 + self.grasp_distance_range = 0.03 + self.grasp_close_delay = 2.0 + self.grasp_reached_time = None + self.gripper_max_opening = 0.07 + + # Workspace limits and dynamic pitch parameters + self.workspace_min_radius = 0.2 + self.workspace_max_radius = 0.75 + self.min_grasp_pitch_degrees = 5.0 + self.max_grasp_pitch_degrees = 60.0 + + # Grasp stage tracking + self.grasp_stage = GraspStage.IDLE + + # Pose stabilization tracking + self.pose_history_size = 4 + self.pose_stabilization_threshold = 0.01 + self.stabilization_timeout = 25.0 + self.stabilization_start_time = None + self.reached_poses = deque(maxlen=self.pose_history_size) + self.adjustment_count = 0 + + # Pose reachability tracking + self.ee_pose_history = deque(maxlen=20) # Keep history of EE poses + self.stuck_pose_threshold = 0.001 # 1mm movement threshold + self.stuck_pose_adjustment_degrees = 5.0 + self.stuck_count = 0 + self.max_stuck_reattempts = 7 + + # State for visualization + self.current_visualization = None + self.last_detection_3d_array = None + self.last_detection_2d_array = None + + # Grasp result and task tracking + self.pick_success = None + self.final_pregrasp_pose = None + self.task_failed = False + self.overall_success = None + + # Task control + self.task_running = False + self.task_thread = None + self.stop_event = threading.Event() + + # Latest sensor data + self.latest_rgb = None + self.latest_depth = None + self.latest_camera_info = None + + # Target selection + self.target_click = None + + # Place target position and object info + self.home_pose = Pose( + position=Vector3(0.0, 0.0, 0.0), orientation=Quaternion(0.0, 0.0, 0.0, 1.0) + ) + self.place_target_position = None + self.target_object_height = None + self.retract_distance = 0.12 + self.place_pose = None + self.retract_pose = None + self.arm.gotoObserve() + + @rpc + def start(self) -> None: + """Start the manipulation module.""" + + unsub = self.rgb_image.subscribe(self._on_rgb_image) + self._disposables.add(Disposable(unsub)) + + unsub = self.depth_image.subscribe(self._on_depth_image) + self._disposables.add(Disposable(unsub)) + + unsub = self.camera_info.subscribe(self._on_camera_info) + self._disposables.add(Disposable(unsub)) + + logger.info("Manipulation module started") + + @rpc + def stop(self) -> None: + """Stop the manipulation module.""" + # Stop any running task + self.stop_event.set() + if self.task_thread and self.task_thread.is_alive(): + self.task_thread.join(timeout=5.0) + + self.reset_to_idle() + + if self.detector and hasattr(self.detector, "cleanup"): + self.detector.cleanup() + self.arm.disable() + + logger.info("Manipulation module stopped") + + def _on_rgb_image(self, msg: Image) -> None: + """Handle RGB image messages.""" + try: + self.latest_rgb = msg.data + except Exception as e: + logger.error(f"Error processing RGB image: {e}") + + def _on_depth_image(self, msg: Image) -> None: + """Handle depth image messages.""" + try: + self.latest_depth = msg.data + except Exception as e: + logger.error(f"Error processing depth image: {e}") + + def _on_camera_info(self, msg: CameraInfo) -> None: + """Handle camera info messages.""" + try: + self.camera_intrinsics = [msg.K[0], msg.K[4], msg.K[2], msg.K[5]] + + if self.detector is None: + self.detector = Detection3DProcessor(self.camera_intrinsics) + self.pbvs = PBVS() + logger.info("Initialized detection and PBVS processors") + + self.latest_camera_info = msg + except Exception as e: + logger.error(f"Error processing camera info: {e}") + + @rpc + def get_single_rgb_frame(self) -> np.ndarray | None: + """ + get the latest rgb frame from the camera + """ + return self.latest_rgb + + @rpc + def handle_keyboard_command(self, key: str) -> str: + """ + Handle keyboard commands for robot control. + + Args: + key: Keyboard key as string + + Returns: + Action taken as string, or empty string if no action + """ + key_code = ord(key) if len(key) == 1 else int(key) + + if key_code == ord("r"): + self.stop_event.set() + self.task_running = False + self.reset_to_idle() + return "reset" + elif key_code == ord("s"): + logger.info("SOFT STOP - Emergency stopping robot!") + self.arm.softStop() + self.stop_event.set() + self.task_running = False + return "stop" + elif key_code == ord(" ") and self.pbvs and self.pbvs.target_grasp_pose: + if self.grasp_stage == GraspStage.PRE_GRASP: + self.set_grasp_stage(GraspStage.GRASP) + logger.info("Executing target pose") + return "execute" + elif key_code == ord("g"): + logger.info("Opening gripper") + self.arm.release_gripper() + return "release" + + return "" + + @rpc + def pick_and_place( + self, + target_x: int | None = None, + target_y: int | None = None, + place_x: int | None = None, + place_y: int | None = None, + ) -> dict[str, Any]: + """ + Start a pick and place task. + + Args: + target_x: Optional X coordinate of target object + target_y: Optional Y coordinate of target object + place_x: Optional X coordinate of place location + place_y: Optional Y coordinate of place location + + Returns: + Dict with status and message + """ + if self.task_running: + return {"status": "error", "message": "Task already running"} + + if self.camera_intrinsics is None: + return {"status": "error", "message": "Camera not initialized"} + + if target_x is not None and target_y is not None: + self.target_click = (target_x, target_y) + if place_x is not None and self.latest_depth is not None: + points_3d_camera = select_points_from_depth( + self.latest_depth, + (place_x, place_y), + self.camera_intrinsics, + radius=10, + ) + + if points_3d_camera.size > 0: + ee_pose = self.arm.get_ee_pose() + ee_transform = pose_to_matrix(ee_pose) + camera_transform = compose_transforms(ee_transform, self.T_ee_to_camera) + + points_3d_world = transform_points_3d( + points_3d_camera, + camera_transform, + to_robot=True, + ) + + place_position = np.mean(points_3d_world, axis=0) + self.place_target_position = place_position + logger.info( + f"Place target set at position: ({place_position[0]:.3f}, {place_position[1]:.3f}, {place_position[2]:.3f})" + ) + else: + logger.warning("No valid depth points found at place location") + self.place_target_position = None + else: + self.place_target_position = None + + self.task_failed = False + self.stop_event.clear() + + if self.task_thread and self.task_thread.is_alive(): + self.stop_event.set() + self.task_thread.join(timeout=1.0) + self.task_thread = threading.Thread(target=self._run_pick_and_place, daemon=True) + self.task_thread.start() + + return {"status": "started", "message": "Pick and place task started"} + + def _run_pick_and_place(self) -> None: + """Run the pick and place task loop.""" + self.task_running = True + logger.info("Starting pick and place task") + + try: + while not self.stop_event.is_set(): + if self.task_failed: + logger.error("Task failed, terminating pick and place") + self.stop_event.set() + break + + feedback = self.update() + if feedback is None: + time.sleep(0.01) + continue + + if feedback.success is not None: + if feedback.success: + logger.info("Pick and place completed successfully!") + else: + logger.warning("Pick and place failed") + self.reset_to_idle() + self.stop_event.set() + break + + time.sleep(0.01) + + except Exception as e: + logger.error(f"Error in pick and place task: {e}") + self.task_failed = True + finally: + self.task_running = False + logger.info("Pick and place task ended") + + def set_grasp_stage(self, stage: GraspStage) -> None: + """Set the grasp stage.""" + self.grasp_stage = stage + logger.info(f"Grasp stage: {stage.value}") + + def calculate_dynamic_grasp_pitch(self, target_pose: Pose) -> float: + """ + Calculate grasp pitch dynamically based on distance from robot base. + Maps workspace radius to grasp pitch angle. + + Args: + target_pose: Target pose + + Returns: + Grasp pitch angle in degrees + """ + # Calculate 3D distance from robot base (assumes robot at origin) + position = target_pose.position + distance = np.sqrt(position.x**2 + position.y**2 + position.z**2) + + # Clamp distance to workspace limits + distance = np.clip(distance, self.workspace_min_radius, self.workspace_max_radius) + + # Linear interpolation: min_radius -> max_pitch, max_radius -> min_pitch + # Normalized distance (0 to 1) + normalized_dist = (distance - self.workspace_min_radius) / ( + self.workspace_max_radius - self.workspace_min_radius + ) + + # Inverse mapping: closer objects need higher pitch + pitch_degrees = self.max_grasp_pitch_degrees - ( + normalized_dist * (self.max_grasp_pitch_degrees - self.min_grasp_pitch_degrees) + ) + + return pitch_degrees + + def check_within_workspace(self, target_pose: Pose) -> bool: + """ + Check if pose is within workspace limits and log error if not. + + Args: + target_pose: Target pose to validate + + Returns: + True if within workspace, False otherwise + """ + # Calculate 3D distance from robot base + position = target_pose.position + distance = np.sqrt(position.x**2 + position.y**2 + position.z**2) + + if not (self.workspace_min_radius <= distance <= self.workspace_max_radius): + logger.error( + f"Target outside workspace limits: distance {distance:.3f}m not in [{self.workspace_min_radius:.2f}, {self.workspace_max_radius:.2f}]" + ) + return False + + return True + + def _check_reach_timeout(self) -> tuple[bool, float]: + """Check if robot has exceeded timeout while reaching pose. + + Returns: + Tuple of (timed_out, time_elapsed) + """ + if self.waiting_start_time: + time_elapsed = time.time() - self.waiting_start_time + if time_elapsed > self.reach_pose_timeout: + logger.warning( + f"Robot failed to reach pose within {self.reach_pose_timeout}s timeout" + ) + self.task_failed = True + self.reset_to_idle() + return True, time_elapsed + return False, time_elapsed + return False, 0.0 + + def _check_if_stuck(self) -> bool: + """ + Check if robot is stuck by analyzing pose history. + + Returns: + Tuple of (is_stuck, max_std_dev_mm) + """ + if len(self.ee_pose_history) < self.ee_pose_history.maxlen: + return False + + # Extract positions from pose history + positions = np.array( + [[p.position.x, p.position.y, p.position.z] for p in self.ee_pose_history] + ) + + # Calculate standard deviation of positions + std_devs = np.std(positions, axis=0) + # Check if all standard deviations are below stuck threshold + is_stuck = np.all(std_devs < self.stuck_pose_threshold) + + return is_stuck + + def check_reach_and_adjust(self) -> bool: + """ + Check if robot has reached the current executed pose while waiting. + Handles timeout internally by failing the task. + Also detects if the robot is stuck (not moving towards target). + + Returns: + True if reached, False if still waiting or not in waiting state + """ + if not self.waiting_for_reach or not self.current_executed_pose: + return False + + # Get current end-effector pose + ee_pose = self.arm.get_ee_pose() + target_pose = self.current_executed_pose + + # Check for timeout - this will fail task and reset if timeout occurred + timed_out, _time_elapsed = self._check_reach_timeout() + if timed_out: + return False + + self.ee_pose_history.append(ee_pose) + + # Check if robot is stuck + is_stuck = self._check_if_stuck() + if is_stuck: + if self.grasp_stage == GraspStage.RETRACT or self.grasp_stage == GraspStage.PLACE: + self.waiting_for_reach = False + self.waiting_start_time = None + self.stuck_count = 0 + self.ee_pose_history.clear() + return True + self.stuck_count += 1 + pitch_degrees = self.calculate_dynamic_grasp_pitch(target_pose) + if self.stuck_count % 2 == 0: + pitch_degrees += self.stuck_pose_adjustment_degrees * (1 + self.stuck_count // 2) + else: + pitch_degrees -= self.stuck_pose_adjustment_degrees * (1 + self.stuck_count // 2) + + pitch_degrees = max( + self.min_grasp_pitch_degrees, min(self.max_grasp_pitch_degrees, pitch_degrees) + ) + updated_target_pose = update_target_grasp_pose(target_pose, ee_pose, 0.0, pitch_degrees) + self.arm.cmd_ee_pose(updated_target_pose) + self.current_executed_pose = updated_target_pose + self.ee_pose_history.clear() + self.waiting_for_reach = True + self.waiting_start_time = time.time() + return False + + if self.stuck_count >= self.max_stuck_reattempts: + self.task_failed = True + self.reset_to_idle() + return False + + if is_target_reached(target_pose, ee_pose, self.pbvs.target_tolerance): + self.waiting_for_reach = False + self.waiting_start_time = None + self.stuck_count = 0 + self.ee_pose_history.clear() + return True + return False + + def _update_tracking(self, detection_3d_array: Detection3DArray | None) -> bool: + """Update tracking with new detections.""" + if not detection_3d_array or not self.pbvs: + return False + + target_tracked = self.pbvs.update_tracking(detection_3d_array) + if target_tracked: + self.target_updated = True + self.last_valid_target = self.pbvs.get_current_target() + return target_tracked + + def reset_to_idle(self) -> None: + """Reset the manipulation system to IDLE state.""" + if self.pbvs: + self.pbvs.clear_target() + self.grasp_stage = GraspStage.IDLE + self.reached_poses.clear() + self.ee_pose_history.clear() + self.adjustment_count = 0 + self.waiting_for_reach = False + self.current_executed_pose = None + self.target_updated = False + self.stabilization_start_time = None + self.grasp_reached_time = None + self.waiting_start_time = None + self.pick_success = None + self.final_pregrasp_pose = None + self.overall_success = None + self.place_pose = None + self.retract_pose = None + self.stuck_count = 0 + + self.arm.gotoObserve() + + def execute_idle(self) -> None: + """Execute idle stage.""" + pass + + def execute_pre_grasp(self) -> None: + """Execute pre-grasp stage: visual servoing to pre-grasp position.""" + if self.waiting_for_reach: + if self.check_reach_and_adjust(): + self.reached_poses.append(self.current_executed_pose) + self.target_updated = False + time.sleep(0.2) + return + if ( + self.stabilization_start_time + and (time.time() - self.stabilization_start_time) > self.stabilization_timeout + ): + logger.warning( + f"Failed to get stable grasp after {self.stabilization_timeout} seconds, resetting" + ) + self.task_failed = True + self.reset_to_idle() + return + + ee_pose = self.arm.get_ee_pose() + dynamic_pitch = self.calculate_dynamic_grasp_pitch(self.pbvs.current_target.bbox.center) + + _, _, _, has_target, target_pose = self.pbvs.compute_control( + ee_pose, self.pregrasp_distance, dynamic_pitch + ) + if target_pose and has_target: + # Validate target pose is within workspace + if not self.check_within_workspace(target_pose): + self.task_failed = True + self.reset_to_idle() + return + + if self.check_target_stabilized(): + logger.info("Target stabilized, transitioning to GRASP") + self.final_pregrasp_pose = self.current_executed_pose + self.grasp_stage = GraspStage.GRASP + self.adjustment_count = 0 + self.waiting_for_reach = False + elif not self.waiting_for_reach and self.target_updated: + self.arm.cmd_ee_pose(target_pose) + self.current_executed_pose = target_pose + self.waiting_for_reach = True + self.waiting_start_time = time.time() + self.target_updated = False + self.adjustment_count += 1 + time.sleep(0.2) + + def execute_grasp(self) -> None: + """Execute grasp stage: move to final grasp position.""" + if self.waiting_for_reach: + if self.check_reach_and_adjust() and not self.grasp_reached_time: + self.grasp_reached_time = time.time() + return + + if self.grasp_reached_time: + if (time.time() - self.grasp_reached_time) >= self.grasp_close_delay: + logger.info("Grasp delay completed, closing gripper") + self.grasp_stage = GraspStage.CLOSE_AND_RETRACT + return + + if self.last_valid_target: + # Calculate dynamic pitch for current target + dynamic_pitch = self.calculate_dynamic_grasp_pitch(self.last_valid_target.bbox.center) + normalized_pitch = dynamic_pitch / 90.0 + grasp_distance = -self.grasp_distance_range + ( + 2 * self.grasp_distance_range * normalized_pitch + ) + + ee_pose = self.arm.get_ee_pose() + _, _, _, has_target, target_pose = self.pbvs.compute_control( + ee_pose, grasp_distance, dynamic_pitch + ) + + if target_pose and has_target: + # Validate grasp pose is within workspace + if not self.check_within_workspace(target_pose): + self.task_failed = True + self.reset_to_idle() + return + + object_width = self.last_valid_target.bbox.size.x + gripper_opening = max( + 0.005, min(object_width + self.grasp_width_offset, self.gripper_max_opening) + ) + + logger.info(f"Executing grasp: gripper={gripper_opening * 1000:.1f}mm") + self.arm.cmd_gripper_ctrl(gripper_opening) + self.arm.cmd_ee_pose(target_pose, line_mode=True) + self.current_executed_pose = target_pose + self.waiting_for_reach = True + self.waiting_start_time = time.time() + + def execute_close_and_retract(self) -> None: + """Execute the retraction sequence after gripper has been closed.""" + if self.waiting_for_reach and self.final_pregrasp_pose: + if self.check_reach_and_adjust(): + logger.info("Reached pre-grasp retraction position") + self.pick_success = self.arm.gripper_object_detected() + if self.pick_success: + logger.info("Object successfully grasped!") + if self.place_target_position is not None: + logger.info("Transitioning to PLACE stage") + self.grasp_stage = GraspStage.PLACE + else: + self.overall_success = True + else: + logger.warning("No object detected in gripper") + self.task_failed = True + self.overall_success = False + return + if not self.waiting_for_reach: + logger.info("Retracting to pre-grasp position") + self.arm.cmd_ee_pose(self.final_pregrasp_pose, line_mode=True) + self.current_executed_pose = self.final_pregrasp_pose + self.arm.close_gripper() + self.waiting_for_reach = True + self.waiting_start_time = time.time() + + def execute_place(self) -> None: + """Execute place stage: move to place position and release object.""" + if self.waiting_for_reach: + # Use the already executed pose instead of recalculating + if self.check_reach_and_adjust(): + logger.info("Reached place position, releasing gripper") + self.arm.release_gripper() + time.sleep(1.0) + self.place_pose = self.current_executed_pose + logger.info("Transitioning to RETRACT stage") + self.grasp_stage = GraspStage.RETRACT + return + + if not self.waiting_for_reach: + place_pose = self.get_place_target_pose() + if place_pose: + logger.info("Moving to place position") + self.arm.cmd_ee_pose(place_pose, line_mode=True) + self.current_executed_pose = place_pose + self.waiting_for_reach = True + self.waiting_start_time = time.time() + else: + logger.error("Failed to get place target pose") + self.task_failed = True + self.overall_success = False + + def execute_retract(self) -> None: + """Execute retract stage: retract from place position.""" + if self.waiting_for_reach and self.retract_pose: + if self.check_reach_and_adjust(): + logger.info("Reached retract position") + logger.info("Returning to observe position") + self.arm.gotoObserve() + self.arm.close_gripper() + self.overall_success = True + logger.info("Pick and place completed successfully!") + return + + if not self.waiting_for_reach: + if self.place_pose: + pose_pitch = self.calculate_dynamic_grasp_pitch(self.place_pose) + self.retract_pose = update_target_grasp_pose( + self.place_pose, self.home_pose, self.retract_distance, pose_pitch + ) + logger.info("Retracting from place position") + self.arm.cmd_ee_pose(self.retract_pose, line_mode=True) + self.current_executed_pose = self.retract_pose + self.waiting_for_reach = True + self.waiting_start_time = time.time() + else: + logger.error("No place pose stored for retraction") + self.task_failed = True + self.overall_success = False + + def capture_and_process( + self, + ) -> tuple[np.ndarray | None, Detection3DArray | None, Detection2DArray | None, Pose | None]: + """Capture frame from camera data and process detections.""" + if self.latest_rgb is None or self.latest_depth is None or self.detector is None: + return None, None, None, None + + ee_pose = self.arm.get_ee_pose() + ee_transform = pose_to_matrix(ee_pose) + camera_transform = compose_transforms(ee_transform, self.T_ee_to_camera) + camera_pose = matrix_to_pose(camera_transform) + detection_3d_array, detection_2d_array = self.detector.process_frame( + self.latest_rgb, self.latest_depth, camera_transform + ) + + return self.latest_rgb, detection_3d_array, detection_2d_array, camera_pose + + def pick_target(self, x: int, y: int) -> bool: + """Select a target object at the given pixel coordinates.""" + if not self.last_detection_2d_array or not self.last_detection_3d_array: + logger.warning("No detections available for target selection") + return False + + clicked_3d = find_clicked_detection( + (x, y), self.last_detection_2d_array.detections, self.last_detection_3d_array.detections + ) + if clicked_3d and self.pbvs: + # Validate workspace + if not self.check_within_workspace(clicked_3d.bbox.center): + self.task_failed = True + return False + + self.pbvs.set_target(clicked_3d) + + if clicked_3d.bbox and clicked_3d.bbox.size: + self.target_object_height = clicked_3d.bbox.size.z + logger.info(f"Target object height: {self.target_object_height:.3f}m") + + position = clicked_3d.bbox.center.position + logger.info( + f"Target selected: ID={clicked_3d.id}, pos=({position.x:.3f}, {position.y:.3f}, {position.z:.3f})" + ) + self.grasp_stage = GraspStage.PRE_GRASP + self.reached_poses.clear() + self.adjustment_count = 0 + self.waiting_for_reach = False + self.current_executed_pose = None + self.stabilization_start_time = time.time() + return True + return False + + def update(self) -> dict[str, Any] | None: + """Main update function that handles capture, processing, control, and visualization.""" + rgb, detection_3d_array, detection_2d_array, camera_pose = self.capture_and_process() + if rgb is None: + return None + + self.last_detection_3d_array = detection_3d_array + self.last_detection_2d_array = detection_2d_array + if self.target_click: + x, y = self.target_click + if self.pick_target(x, y): + self.target_click = None + + if ( + detection_3d_array + and self.grasp_stage in [GraspStage.PRE_GRASP, GraspStage.GRASP] + and not self.waiting_for_reach + ): + self._update_tracking(detection_3d_array) + stage_handlers = { + GraspStage.IDLE: self.execute_idle, + GraspStage.PRE_GRASP: self.execute_pre_grasp, + GraspStage.GRASP: self.execute_grasp, + GraspStage.CLOSE_AND_RETRACT: self.execute_close_and_retract, + GraspStage.PLACE: self.execute_place, + GraspStage.RETRACT: self.execute_retract, + } + if self.grasp_stage in stage_handlers: + stage_handlers[self.grasp_stage]() + + target_tracked = self.pbvs.get_current_target() is not None if self.pbvs else False + ee_pose = self.arm.get_ee_pose() + feedback = Feedback( + grasp_stage=self.grasp_stage, + target_tracked=target_tracked, + current_executed_pose=self.current_executed_pose, + current_ee_pose=ee_pose, + current_camera_pose=camera_pose, + target_pose=self.pbvs.target_grasp_pose if self.pbvs else None, + waiting_for_reach=self.waiting_for_reach, + success=self.overall_success, + ) + + if self.task_running: + self.current_visualization = create_manipulation_visualization( + rgb, feedback, detection_3d_array, detection_2d_array + ) + + if self.current_visualization is not None: + self._publish_visualization(self.current_visualization) + + return feedback + + def _publish_visualization(self, viz_image: np.ndarray) -> None: + """Publish visualization image to LCM.""" + try: + viz_rgb = cv2.cvtColor(viz_image, cv2.COLOR_BGR2RGB) + msg = Image.from_numpy(viz_rgb) + self.viz_image.publish(msg) + except Exception as e: + logger.error(f"Error publishing visualization: {e}") + + def check_target_stabilized(self) -> bool: + """Check if the commanded poses have stabilized.""" + if len(self.reached_poses) < self.reached_poses.maxlen: + return False + + positions = np.array( + [[p.position.x, p.position.y, p.position.z] for p in self.reached_poses] + ) + std_devs = np.std(positions, axis=0) + return np.all(std_devs < self.pose_stabilization_threshold) + + def get_place_target_pose(self) -> Pose | None: + """Get the place target pose with z-offset applied based on object height.""" + if self.place_target_position is None: + return None + + place_pos = self.place_target_position.copy() + if self.target_object_height is not None: + z_offset = self.target_object_height / 2.0 + place_pos[2] += z_offset + 0.1 + + place_center_pose = Pose( + position=Vector3(place_pos[0], place_pos[1], place_pos[2]), + orientation=Quaternion(0.0, 0.0, 0.0, 1.0), + ) + + ee_pose = self.arm.get_ee_pose() + + # Calculate dynamic pitch for place position + dynamic_pitch = self.calculate_dynamic_grasp_pitch(place_center_pose) + + place_pose = update_target_grasp_pose( + place_center_pose, + ee_pose, + grasp_distance=0.0, + grasp_pitch_degrees=dynamic_pitch, + ) + + return place_pose diff --git a/dimos/manipulation/visual_servoing/pbvs.py b/dimos/manipulation/visual_servoing/pbvs.py new file mode 100644 index 0000000000..77bf83396e --- /dev/null +++ b/dimos/manipulation/visual_servoing/pbvs.py @@ -0,0 +1,488 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Position-Based Visual Servoing (PBVS) system for robotic manipulation. +Supports both eye-in-hand and eye-to-hand configurations. +""" + +from collections import deque + +from dimos_lcm.vision_msgs import Detection3D +import numpy as np +from scipy.spatial.transform import Rotation as R + +from dimos.manipulation.visual_servoing.utils import ( + create_pbvs_visualization, + find_best_object_match, + is_target_reached, + update_target_grasp_pose, +) +from dimos.msgs.geometry_msgs import Pose, Quaternion, Vector3 +from dimos.msgs.vision_msgs import Detection3DArray +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.manipulation.pbvs") + + +class PBVS: + """ + High-level Position-Based Visual Servoing orchestrator. + + Handles: + - Object tracking and target management + - Pregrasp distance computation + - Grasp pose generation + - Coordination with low-level controller + + Note: This class is agnostic to camera mounting (eye-in-hand vs eye-to-hand). + The caller is responsible for providing appropriate camera and EE poses. + """ + + def __init__( + self, + position_gain: float = 0.5, + rotation_gain: float = 0.3, + max_velocity: float = 0.1, # m/s + max_angular_velocity: float = 0.5, # rad/s + target_tolerance: float = 0.01, # 1cm + max_tracking_distance_threshold: float = 0.12, # Max distance for target tracking (m) + min_size_similarity: float = 0.6, # Min size similarity threshold (0.0-1.0) + direct_ee_control: bool = True, # If True, output target poses instead of velocities + ) -> None: + """ + Initialize PBVS system. + + Args: + position_gain: Proportional gain for position control + rotation_gain: Proportional gain for rotation control + max_velocity: Maximum linear velocity command magnitude (m/s) + max_angular_velocity: Maximum angular velocity command magnitude (rad/s) + target_tolerance: Distance threshold for considering target reached (m) + max_tracking_distance: Maximum distance for valid target tracking (m) + min_size_similarity: Minimum size similarity for valid target tracking (0.0-1.0) + direct_ee_control: If True, output target poses instead of velocity commands + """ + # Initialize low-level controller only if not in direct control mode + if not direct_ee_control: + self.controller = PBVSController( + position_gain=position_gain, + rotation_gain=rotation_gain, + max_velocity=max_velocity, + max_angular_velocity=max_angular_velocity, + target_tolerance=target_tolerance, + ) + else: + self.controller = None + + # Store parameters for direct mode error computation + self.target_tolerance = target_tolerance + + # Target tracking parameters + self.max_tracking_distance_threshold = max_tracking_distance_threshold + self.min_size_similarity = min_size_similarity + self.direct_ee_control = direct_ee_control + + # Target state + self.current_target = None + self.target_grasp_pose = None + + # Detection history for robust tracking + self.detection_history_size = 3 + self.detection_history = deque(maxlen=self.detection_history_size) + + # For direct control mode visualization + self.last_position_error = None + self.last_target_reached = False + + logger.info( + f"Initialized PBVS system with controller gains: pos={position_gain}, rot={rotation_gain}, " + f"tracking_thresholds: distance={max_tracking_distance_threshold}m, size={min_size_similarity:.2f}" + ) + + def set_target(self, target_object: Detection3D) -> bool: + """ + Set a new target object for servoing. + + Args: + target_object: Detection3D object + + Returns: + True if target was set successfully + """ + if target_object and target_object.bbox and target_object.bbox.center: + self.current_target = target_object + self.target_grasp_pose = None # Will be computed when needed + logger.info(f"New target set: ID {target_object.id}") + return True + return False + + def clear_target(self) -> None: + """Clear the current target.""" + self.current_target = None + self.target_grasp_pose = None + self.last_position_error = None + self.last_target_reached = False + self.detection_history.clear() + if self.controller: + self.controller.clear_state() + logger.info("Target cleared") + + def get_current_target(self) -> Detection3D | None: + """ + Get the current target object. + + Returns: + Current target Detection3D or None if no target selected + """ + return self.current_target + + def update_tracking(self, new_detections: Detection3DArray | None = None) -> bool: + """ + Update target tracking with new detections using a rolling window. + If tracking is lost, keeps the old target pose. + + Args: + new_detections: Optional new detections for target tracking + + Returns: + True if target was successfully tracked, False if lost (but target is kept) + """ + # Check if we have a current target + if not self.current_target: + return False + + # Add new detections to history if provided + if new_detections is not None and new_detections.detections_length > 0: + self.detection_history.append(new_detections) + + # If no detection history, can't track + if not self.detection_history: + logger.debug("No detection history for target tracking - using last known pose") + return False + + # Collect all candidates from detection history + all_candidates = [] + for detection_array in self.detection_history: + all_candidates.extend(detection_array.detections) + + if not all_candidates: + logger.debug("No candidates in detection history") + return False + + # Use stage-dependent distance threshold + max_distance = self.max_tracking_distance_threshold + + # Find best match across all recent detections + match_result = find_best_object_match( + target_obj=self.current_target, + candidates=all_candidates, + max_distance=max_distance, + min_size_similarity=self.min_size_similarity, + ) + + if match_result.is_valid_match: + self.current_target = match_result.matched_object + self.target_grasp_pose = None # Recompute grasp pose + logger.debug( + f"Target tracking successful: distance={match_result.distance:.3f}m, " + f"size_similarity={match_result.size_similarity:.2f}, " + f"confidence={match_result.confidence:.2f}" + ) + return True + + logger.debug( + f"Target tracking lost across {len(self.detection_history)} frames: " + f"distance={match_result.distance:.3f}m, " + f"size_similarity={match_result.size_similarity:.2f}, " + f"thresholds: distance={max_distance:.3f}m, size={self.min_size_similarity:.2f}" + ) + return False + + def compute_control( + self, + ee_pose: Pose, + grasp_distance: float = 0.15, + grasp_pitch_degrees: float = 45.0, + ) -> tuple[Vector3 | None, Vector3 | None, bool, bool, Pose | None]: + """ + Compute PBVS control with position and orientation servoing. + + Args: + ee_pose: Current end-effector pose + grasp_distance: Distance to maintain from target (meters) + + Returns: + Tuple of (velocity_command, angular_velocity_command, target_reached, has_target, target_pose) + - velocity_command: Linear velocity vector or None if no target (None in direct_ee_control mode) + - angular_velocity_command: Angular velocity vector or None if no target (None in direct_ee_control mode) + - target_reached: True if within target tolerance + - has_target: True if currently tracking a target + - target_pose: Target EE pose (only in direct_ee_control mode, otherwise None) + """ + # Check if we have a target + if not self.current_target: + return None, None, False, False, None + + # Update target grasp pose with provided distance and pitch + self.target_grasp_pose = update_target_grasp_pose( + self.current_target.bbox.center, ee_pose, grasp_distance, grasp_pitch_degrees + ) + + if self.target_grasp_pose is None: + logger.warning("Failed to compute grasp pose") + return None, None, False, False, None + + # Compute errors for visualization before checking if reached (in case pose gets cleared) + if self.direct_ee_control and self.target_grasp_pose: + self.last_position_error = Vector3( + self.target_grasp_pose.position.x - ee_pose.position.x, + self.target_grasp_pose.position.y - ee_pose.position.y, + self.target_grasp_pose.position.z - ee_pose.position.z, + ) + + # Check if target reached using our separate function + target_reached = is_target_reached(self.target_grasp_pose, ee_pose, self.target_tolerance) + + # Return appropriate values based on control mode + if self.direct_ee_control: + # Direct control mode + if self.target_grasp_pose: + self.last_target_reached = target_reached + # Return has_target=True since we have a target + return None, None, target_reached, True, self.target_grasp_pose + else: + return None, None, False, True, None + else: + # Velocity control mode - use controller + velocity_cmd, angular_velocity_cmd, _controller_reached = ( + self.controller.compute_control(ee_pose, self.target_grasp_pose) + ) + # Return has_target=True since we have a target, regardless of tracking status + return velocity_cmd, angular_velocity_cmd, target_reached, True, None + + def create_status_overlay( + self, + image: np.ndarray, + grasp_stage=None, + ) -> np.ndarray: + """ + Create PBVS status overlay on image. + + Args: + image: Input image + grasp_stage: Current grasp stage (optional) + + Returns: + Image with PBVS status overlay + """ + stage_value = grasp_stage.value if grasp_stage else "idle" + return create_pbvs_visualization( + image, + self.current_target, + self.last_position_error, + self.last_target_reached, + stage_value, + ) + + +class PBVSController: + """ + Low-level Position-Based Visual Servoing controller. + Pure control logic that computes velocity commands from poses. + + Handles: + - Position and orientation error computation + - Velocity command generation with gain control + - Target reached detection + """ + + def __init__( + self, + position_gain: float = 0.5, + rotation_gain: float = 0.3, + max_velocity: float = 0.1, # m/s + max_angular_velocity: float = 0.5, # rad/s + target_tolerance: float = 0.01, # 1cm + ) -> None: + """ + Initialize PBVS controller. + + Args: + position_gain: Proportional gain for position control + rotation_gain: Proportional gain for rotation control + max_velocity: Maximum linear velocity command magnitude (m/s) + max_angular_velocity: Maximum angular velocity command magnitude (rad/s) + target_tolerance: Distance threshold for considering target reached (m) + """ + self.position_gain = position_gain + self.rotation_gain = rotation_gain + self.max_velocity = max_velocity + self.max_angular_velocity = max_angular_velocity + self.target_tolerance = target_tolerance + + self.last_position_error = None + self.last_rotation_error = None + self.last_velocity_cmd = None + self.last_angular_velocity_cmd = None + self.last_target_reached = False + + logger.info( + f"Initialized PBVS controller: pos_gain={position_gain}, rot_gain={rotation_gain}, " + f"max_vel={max_velocity}m/s, max_ang_vel={max_angular_velocity}rad/s, " + f"target_tolerance={target_tolerance}m" + ) + + def clear_state(self) -> None: + """Clear controller state.""" + self.last_position_error = None + self.last_rotation_error = None + self.last_velocity_cmd = None + self.last_angular_velocity_cmd = None + self.last_target_reached = False + + def compute_control( + self, ee_pose: Pose, grasp_pose: Pose + ) -> tuple[Vector3 | None, Vector3 | None, bool]: + """ + Compute PBVS control with position and orientation servoing. + + Args: + ee_pose: Current end-effector pose + grasp_pose: Target grasp pose + + Returns: + Tuple of (velocity_command, angular_velocity_command, target_reached) + - velocity_command: Linear velocity vector + - angular_velocity_command: Angular velocity vector + - target_reached: True if within target tolerance + """ + # Calculate position error (target - EE position) + error = Vector3( + grasp_pose.position.x - ee_pose.position.x, + grasp_pose.position.y - ee_pose.position.y, + grasp_pose.position.z - ee_pose.position.z, + ) + self.last_position_error = error + + # Compute velocity command with proportional control + velocity_cmd = Vector3( + error.x * self.position_gain, + error.y * self.position_gain, + error.z * self.position_gain, + ) + + # Limit velocity magnitude + vel_magnitude = np.linalg.norm([velocity_cmd.x, velocity_cmd.y, velocity_cmd.z]) + if vel_magnitude > self.max_velocity: + scale = self.max_velocity / vel_magnitude + velocity_cmd = Vector3( + float(velocity_cmd.x * scale), + float(velocity_cmd.y * scale), + float(velocity_cmd.z * scale), + ) + + self.last_velocity_cmd = velocity_cmd + + # Compute angular velocity for orientation control + angular_velocity_cmd = self._compute_angular_velocity(grasp_pose.orientation, ee_pose) + + # Check if target reached + error_magnitude = np.linalg.norm([error.x, error.y, error.z]) + target_reached = bool(error_magnitude < self.target_tolerance) + self.last_target_reached = target_reached + + return velocity_cmd, angular_velocity_cmd, target_reached + + def _compute_angular_velocity(self, target_rot: Quaternion, current_pose: Pose) -> Vector3: + """ + Compute angular velocity commands for orientation control. + Uses quaternion error computation for better numerical stability. + + Args: + target_rot: Target orientation (quaternion) + current_pose: Current EE pose + + Returns: + Angular velocity command as Vector3 + """ + # Use quaternion error for better numerical stability + + # Convert to scipy Rotation objects + target_rot_scipy = R.from_quat([target_rot.x, target_rot.y, target_rot.z, target_rot.w]) + current_rot_scipy = R.from_quat( + [ + current_pose.orientation.x, + current_pose.orientation.y, + current_pose.orientation.z, + current_pose.orientation.w, + ] + ) + + # Compute rotation error: error = target * current^(-1) + error_rot = target_rot_scipy * current_rot_scipy.inv() + + # Convert to axis-angle representation for control + error_axis_angle = error_rot.as_rotvec() + + # Use axis-angle directly as angular velocity error (small angle approximation) + roll_error = error_axis_angle[0] + pitch_error = error_axis_angle[1] + yaw_error = error_axis_angle[2] + + self.last_rotation_error = Vector3(roll_error, pitch_error, yaw_error) + + # Apply proportional control + angular_velocity = Vector3( + roll_error * self.rotation_gain, + pitch_error * self.rotation_gain, + yaw_error * self.rotation_gain, + ) + + # Limit angular velocity magnitude + ang_vel_magnitude = np.sqrt( + angular_velocity.x**2 + angular_velocity.y**2 + angular_velocity.z**2 + ) + if ang_vel_magnitude > self.max_angular_velocity: + scale = self.max_angular_velocity / ang_vel_magnitude + angular_velocity = Vector3( + angular_velocity.x * scale, angular_velocity.y * scale, angular_velocity.z * scale + ) + + self.last_angular_velocity_cmd = angular_velocity + + return angular_velocity + + def create_status_overlay( + self, + image: np.ndarray, + current_target: Detection3D | None = None, + ) -> np.ndarray: + """ + Create PBVS status overlay on image. + + Args: + image: Input image + current_target: Current target object Detection3D (for display) + + Returns: + Image with PBVS status overlay + """ + return create_pbvs_visualization( + image, + current_target, + self.last_position_error, + self.last_target_reached, + "velocity_control", + ) diff --git a/dimos/manipulation/visual_servoing/utils.py b/dimos/manipulation/visual_servoing/utils.py new file mode 100644 index 0000000000..06479723f6 --- /dev/null +++ b/dimos/manipulation/visual_servoing/utils.py @@ -0,0 +1,799 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass +from typing import Any + +import cv2 +from dimos_lcm.vision_msgs import Detection2D, Detection3D +import numpy as np + +from dimos.msgs.geometry_msgs import Pose, Quaternion, Vector3 +from dimos.perception.common.utils import project_2d_points_to_3d +from dimos.perception.detection2d.utils import plot_results +from dimos.utils.transform_utils import ( + compose_transforms, + euler_to_quaternion, + get_distance, + matrix_to_pose, + offset_distance, + optical_to_robot_frame, + pose_to_matrix, + robot_to_optical_frame, + yaw_towards_point, +) + + +def match_detection_by_id( + detection_3d: Detection3D, detections_3d: list[Detection3D], detections_2d: list[Detection2D] +) -> Detection2D | None: + """ + Find the corresponding Detection2D for a given Detection3D. + + Args: + detection_3d: The Detection3D to match + detections_3d: List of all Detection3D objects + detections_2d: List of all Detection2D objects (must be 1:1 correspondence) + + Returns: + Corresponding Detection2D if found, None otherwise + """ + for i, det_3d in enumerate(detections_3d): + if det_3d.id == detection_3d.id and i < len(detections_2d): + return detections_2d[i] + return None + + +def transform_pose( + obj_pos: np.ndarray, + obj_orientation: np.ndarray, + transform_matrix: np.ndarray, + to_optical: bool = False, + to_robot: bool = False, +) -> Pose: + """ + Transform object pose with optional frame convention conversion. + + Args: + obj_pos: Object position [x, y, z] + obj_orientation: Object orientation [roll, pitch, yaw] in radians + transform_matrix: 4x4 transformation matrix from camera frame to desired frame + to_optical: If True, input is in robot frame → convert result to optical frame + to_robot: If True, input is in optical frame → convert to robot frame first + + Returns: + Object pose in desired frame as Pose + """ + # Convert euler angles to quaternion using utility function + euler_vector = Vector3(obj_orientation[0], obj_orientation[1], obj_orientation[2]) + obj_orientation_quat = euler_to_quaternion(euler_vector) + + input_pose = Pose( + position=Vector3(obj_pos[0], obj_pos[1], obj_pos[2]), orientation=obj_orientation_quat + ) + + # Apply input frame conversion based on flags + if to_robot: + # Input is in optical frame → convert to robot frame first + pose_for_transform = optical_to_robot_frame(input_pose) + else: + # Default or to_optical: use input pose as-is + pose_for_transform = input_pose + + # Create transformation matrix from pose (relative to camera) + T_camera_object = pose_to_matrix(pose_for_transform) + + # Use compose_transforms to combine transformations + T_desired_object = compose_transforms(transform_matrix, T_camera_object) + + # Convert back to pose + result_pose = matrix_to_pose(T_desired_object) + + # Apply output frame conversion based on flags + if to_optical: + # Input was robot frame → convert result to optical frame + desired_pose = robot_to_optical_frame(result_pose) + else: + # Default or to_robot: use result as-is + desired_pose = result_pose + + return desired_pose + + +def transform_points_3d( + points_3d: np.ndarray, + transform_matrix: np.ndarray, + to_optical: bool = False, + to_robot: bool = False, +) -> np.ndarray: + """ + Transform 3D points with optional frame convention conversion. + Applies the same transformation pipeline as transform_pose but for multiple points. + + Args: + points_3d: Nx3 array of 3D points [x, y, z] + transform_matrix: 4x4 transformation matrix from camera frame to desired frame + to_optical: If True, input is in robot frame → convert result to optical frame + to_robot: If True, input is in optical frame → convert to robot frame first + + Returns: + Nx3 array of transformed 3D points in desired frame + """ + if points_3d.size == 0: + return np.zeros((0, 3), dtype=np.float32) + + points_3d = np.asarray(points_3d) + if points_3d.ndim == 1: + points_3d = points_3d.reshape(1, -1) + + transformed_points = [] + + for point in points_3d: + input_point_pose = Pose( + position=Vector3(point[0], point[1], point[2]), + orientation=Quaternion(0.0, 0.0, 0.0, 1.0), # Identity quaternion + ) + + # Apply input frame conversion based on flags + if to_robot: + # Input is in optical frame → convert to robot frame first + pose_for_transform = optical_to_robot_frame(input_point_pose) + else: + # Default or to_optical: use input pose as-is + pose_for_transform = input_point_pose + + # Create transformation matrix from point pose (relative to camera) + T_camera_point = pose_to_matrix(pose_for_transform) + + # Use compose_transforms to combine transformations + T_desired_point = compose_transforms(transform_matrix, T_camera_point) + + # Convert back to pose + result_pose = matrix_to_pose(T_desired_point) + + # Apply output frame conversion based on flags + if to_optical: + # Input was robot frame → convert result to optical frame + desired_pose = robot_to_optical_frame(result_pose) + else: + # Default or to_robot: use result as-is + desired_pose = result_pose + + transformed_point = [ + desired_pose.position.x, + desired_pose.position.y, + desired_pose.position.z, + ] + transformed_points.append(transformed_point) + + return np.array(transformed_points, dtype=np.float32) + + +def select_points_from_depth( + depth_image: np.ndarray, + target_point: tuple[int, int], + camera_intrinsics: list[float] | np.ndarray, + radius: int = 5, +) -> np.ndarray: + """ + Select points around a target point within a bounding box and project them to 3D. + + Args: + depth_image: Depth image in meters (H, W) + target_point: (x, y) target point coordinates + radius: Half-width of the bounding box (so bbox size is radius*2 x radius*2) + camera_intrinsics: Camera parameters as [fx, fy, cx, cy] list or 3x3 matrix + + Returns: + Nx3 array of 3D points (X, Y, Z) in camera frame + """ + x_target, y_target = target_point + height, width = depth_image.shape + + x_min = max(0, x_target - radius) + x_max = min(width, x_target + radius) + y_min = max(0, y_target - radius) + y_max = min(height, y_target + radius) + + # Create coordinate grids for the bounding box (vectorized) + y_coords, x_coords = np.meshgrid(range(y_min, y_max), range(x_min, x_max), indexing="ij") + + # Flatten to get all coordinate pairs + x_flat = x_coords.flatten() + y_flat = y_coords.flatten() + + # Extract corresponding depth values using advanced indexing + depth_flat = depth_image[y_flat, x_flat] + + valid_mask = (depth_flat > 0) & np.isfinite(depth_flat) + + if not np.any(valid_mask): + return np.zeros((0, 3), dtype=np.float32) + + points_2d = np.column_stack([x_flat[valid_mask], y_flat[valid_mask]]).astype(np.float32) + depth_values = depth_flat[valid_mask].astype(np.float32) + + points_3d = project_2d_points_to_3d(points_2d, depth_values, camera_intrinsics) + + return points_3d + + +def update_target_grasp_pose( + target_pose: Pose, ee_pose: Pose, grasp_distance: float = 0.0, grasp_pitch_degrees: float = 45.0 +) -> Pose | None: + """ + Update target grasp pose based on current target pose and EE pose. + + Args: + target_pose: Target pose to grasp + ee_pose: Current end-effector pose + grasp_distance: Distance to maintain from target (pregrasp or grasp distance) + grasp_pitch_degrees: Grasp pitch angle in degrees (default 90° for top-down) + + Returns: + Target grasp pose or None if target is invalid + """ + + target_pos = target_pose.position + + # Calculate orientation pointing from target towards EE + yaw_to_ee = yaw_towards_point(target_pos, ee_pose.position) + + # Create target pose with proper orientation + # Convert grasp pitch from degrees to radians with mapping: + # 0° (level) -> π/2 (1.57 rad), 90° (top-down) -> π (3.14 rad) + pitch_radians = 1.57 + np.radians(grasp_pitch_degrees) + + # Convert euler angles to quaternion using utility function + euler = Vector3(0.0, pitch_radians, yaw_to_ee) # roll=0, pitch=mapped, yaw=calculated + target_orientation = euler_to_quaternion(euler) + + updated_pose = Pose(target_pos, target_orientation) + + if grasp_distance > 0.0: + return offset_distance(updated_pose, grasp_distance) + else: + return updated_pose + + +def is_target_reached(target_pose: Pose, current_pose: Pose, tolerance: float = 0.01) -> bool: + """ + Check if the target pose has been reached within tolerance. + + Args: + target_pose: Target pose to reach + current_pose: Current pose (e.g., end-effector pose) + tolerance: Distance threshold for considering target reached (meters, default 0.01 = 1cm) + + Returns: + True if target is reached within tolerance, False otherwise + """ + # Calculate position error using distance utility + error_magnitude = get_distance(target_pose, current_pose) + return error_magnitude < tolerance + + +@dataclass +class ObjectMatchResult: + """Result of object matching with confidence metrics.""" + + matched_object: Detection3D | None + confidence: float + distance: float + size_similarity: float + is_valid_match: bool + + +def calculate_object_similarity( + target_obj: Detection3D, + candidate_obj: Detection3D, + distance_weight: float = 0.6, + size_weight: float = 0.4, +) -> tuple[float, float, float]: + """ + Calculate comprehensive similarity between two objects. + + Args: + target_obj: Target Detection3D object + candidate_obj: Candidate Detection3D object + distance_weight: Weight for distance component (0-1) + size_weight: Weight for size component (0-1) + + Returns: + Tuple of (total_similarity, distance_m, size_similarity) + """ + # Extract positions + target_pos = target_obj.bbox.center.position + candidate_pos = candidate_obj.bbox.center.position + + target_xyz = np.array([target_pos.x, target_pos.y, target_pos.z]) + candidate_xyz = np.array([candidate_pos.x, candidate_pos.y, candidate_pos.z]) + + # Calculate Euclidean distance + distance = np.linalg.norm(target_xyz - candidate_xyz) + distance_similarity = 1.0 / (1.0 + distance) # Exponential decay + + # Calculate size similarity by comparing each dimension individually + size_similarity = 1.0 # Default if no size info + target_size = target_obj.bbox.size + candidate_size = candidate_obj.bbox.size + + if target_size and candidate_size: + # Extract dimensions + target_dims = [target_size.x, target_size.y, target_size.z] + candidate_dims = [candidate_size.x, candidate_size.y, candidate_size.z] + + # Calculate similarity for each dimension pair + dim_similarities = [] + for target_dim, candidate_dim in zip(target_dims, candidate_dims, strict=False): + if target_dim == 0.0 and candidate_dim == 0.0: + dim_similarities.append(1.0) # Both dimensions are zero + elif target_dim == 0.0 or candidate_dim == 0.0: + dim_similarities.append(0.0) # One dimension is zero, other is not + else: + # Calculate similarity as min/max ratio + max_dim = max(target_dim, candidate_dim) + min_dim = min(target_dim, candidate_dim) + dim_similarity = min_dim / max_dim if max_dim > 0 else 0.0 + dim_similarities.append(dim_similarity) + + # Return average similarity across all dimensions + size_similarity = np.mean(dim_similarities) if dim_similarities else 0.0 + + # Weighted combination + total_similarity = distance_weight * distance_similarity + size_weight * size_similarity + + return total_similarity, distance, size_similarity + + +def find_best_object_match( + target_obj: Detection3D, + candidates: list[Detection3D], + max_distance: float = 0.1, + min_size_similarity: float = 0.4, + distance_weight: float = 0.7, + size_weight: float = 0.3, +) -> ObjectMatchResult: + """ + Find the best matching object from candidates using distance and size criteria. + + Args: + target_obj: Target Detection3D to match against + candidates: List of candidate Detection3D objects + max_distance: Maximum allowed distance for valid match (meters) + min_size_similarity: Minimum size similarity for valid match (0-1) + distance_weight: Weight for distance in similarity calculation + size_weight: Weight for size in similarity calculation + + Returns: + ObjectMatchResult with best match and confidence metrics + """ + if not candidates or not target_obj.bbox or not target_obj.bbox.center: + return ObjectMatchResult(None, 0.0, float("inf"), 0.0, False) + + best_match = None + best_confidence = 0.0 + best_distance = float("inf") + best_size_sim = 0.0 + + for candidate in candidates: + if not candidate.bbox or not candidate.bbox.center: + continue + + similarity, distance, size_sim = calculate_object_similarity( + target_obj, candidate, distance_weight, size_weight + ) + + # Check validity constraints + is_valid = distance <= max_distance and size_sim >= min_size_similarity + + if is_valid and similarity > best_confidence: + best_match = candidate + best_confidence = similarity + best_distance = distance + best_size_sim = size_sim + + return ObjectMatchResult( + matched_object=best_match, + confidence=best_confidence, + distance=best_distance, + size_similarity=best_size_sim, + is_valid_match=best_match is not None, + ) + + +def parse_zed_pose(zed_pose_data: dict[str, Any]) -> Pose | None: + """ + Parse ZED pose data dictionary into a Pose object. + + Args: + zed_pose_data: Dictionary from ZEDCamera.get_pose() containing: + - position: [x, y, z] in meters + - rotation: [x, y, z, w] quaternion + - euler_angles: [roll, pitch, yaw] in radians + - valid: Whether pose is valid + + Returns: + Pose object with position and orientation, or None if invalid + """ + if not zed_pose_data or not zed_pose_data.get("valid", False): + return None + + # Extract position + position = zed_pose_data.get("position", [0, 0, 0]) + pos_vector = Vector3(position[0], position[1], position[2]) + + quat = zed_pose_data["rotation"] + orientation = Quaternion(quat[0], quat[1], quat[2], quat[3]) + return Pose(position=pos_vector, orientation=orientation) + + +def estimate_object_depth( + depth_image: np.ndarray, segmentation_mask: np.ndarray | None, bbox: list[float] +) -> float: + """ + Estimate object depth dimension using segmentation mask and depth data. + Optimized for real-time performance. + + Args: + depth_image: Depth image in meters + segmentation_mask: Binary segmentation mask for the object + bbox: Bounding box [x1, y1, x2, y2] + + Returns: + Estimated object depth in meters + """ + x1, y1, x2, y2 = int(bbox[0]), int(bbox[1]), int(bbox[2]), int(bbox[3]) + + # Extract depth ROI once + roi_depth = depth_image[y1:y2, x1:x2] + + if segmentation_mask is not None and segmentation_mask.size > 0: + # Extract mask ROI efficiently + mask_roi = ( + segmentation_mask[y1:y2, x1:x2] + if segmentation_mask.shape != roi_depth.shape + else segmentation_mask + ) + + # Fast mask application using boolean indexing + valid_mask = mask_roi > 0 + if np.sum(valid_mask) > 10: # Early exit if not enough points + masked_depths = roi_depth[valid_mask] + + # Fast percentile calculation using numpy's optimized functions + depth_90 = np.percentile(masked_depths, 90) + depth_10 = np.percentile(masked_depths, 10) + depth_range = depth_90 - depth_10 + + # Clamp to reasonable bounds with single operation + return np.clip(depth_range, 0.02, 0.5) + + # Fast fallback using area calculation + bbox_area = (x2 - x1) * (y2 - y1) + + # Vectorized area-based estimation + if bbox_area > 10000: + return 0.15 + elif bbox_area > 5000: + return 0.10 + else: + return 0.05 + + +# ============= Visualization Functions ============= + + +def create_manipulation_visualization( + rgb_image: np.ndarray, + feedback, + detection_3d_array=None, + detection_2d_array=None, +) -> np.ndarray: + """ + Create simple visualization for manipulation class using feedback. + + Args: + rgb_image: RGB image array + feedback: Feedback object containing all state information + detection_3d_array: Optional 3D detections for object visualization + detection_2d_array: Optional 2D detections for object visualization + + Returns: + BGR image with visualization overlays + """ + # Convert to BGR for OpenCV + viz = cv2.cvtColor(rgb_image, cv2.COLOR_RGB2BGR) + + # Draw detections if available + if detection_3d_array and detection_2d_array: + # Extract 2D bboxes + bboxes_2d = [] + for det_2d in detection_2d_array.detections: + if det_2d.bbox: + x1 = det_2d.bbox.center.position.x - det_2d.bbox.size_x / 2 + y1 = det_2d.bbox.center.position.y - det_2d.bbox.size_y / 2 + x2 = det_2d.bbox.center.position.x + det_2d.bbox.size_x / 2 + y2 = det_2d.bbox.center.position.y + det_2d.bbox.size_y / 2 + bboxes_2d.append([x1, y1, x2, y2]) + + # Draw basic detections + rgb_with_detections = visualize_detections_3d( + rgb_image, detection_3d_array.detections, show_coordinates=True, bboxes_2d=bboxes_2d + ) + viz = cv2.cvtColor(rgb_with_detections, cv2.COLOR_RGB2BGR) + + # Add manipulation status overlay + status_y = 30 + cv2.putText( + viz, + "Eye-in-Hand Visual Servoing", + (10, status_y), + cv2.FONT_HERSHEY_SIMPLEX, + 0.6, + (0, 255, 255), + 2, + ) + + # Stage information + stage_text = f"Stage: {feedback.grasp_stage.value.upper()}" + stage_color = { + "idle": (100, 100, 100), + "pre_grasp": (0, 255, 255), + "grasp": (0, 255, 0), + "close_and_retract": (255, 0, 255), + "place": (0, 150, 255), + "retract": (255, 150, 0), + }.get(feedback.grasp_stage.value, (255, 255, 255)) + + cv2.putText( + viz, + stage_text, + (10, status_y + 25), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + stage_color, + 1, + ) + + # Target tracking status + if feedback.target_tracked: + cv2.putText( + viz, + "Target: TRACKED", + (10, status_y + 45), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + (0, 255, 0), + 1, + ) + elif feedback.grasp_stage.value != "idle": + cv2.putText( + viz, + "Target: LOST", + (10, status_y + 45), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + (0, 0, 255), + 1, + ) + + # Waiting status + if feedback.waiting_for_reach: + cv2.putText( + viz, + "Status: WAITING FOR ROBOT", + (10, status_y + 65), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + (255, 255, 0), + 1, + ) + + # Overall result + if feedback.success is not None: + result_text = "Pick & Place: SUCCESS" if feedback.success else "Pick & Place: FAILED" + result_color = (0, 255, 0) if feedback.success else (0, 0, 255) + cv2.putText( + viz, + result_text, + (10, status_y + 85), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + result_color, + 2, + ) + + # Control hints (bottom of image) + hint_text = "Click object to grasp | s=STOP | r=RESET | g=RELEASE" + cv2.putText( + viz, + hint_text, + (10, viz.shape[0] - 10), + cv2.FONT_HERSHEY_SIMPLEX, + 0.4, + (200, 200, 200), + 1, + ) + + return viz + + +def create_pbvs_visualization( + image: np.ndarray, + current_target=None, + position_error=None, + target_reached: bool = False, + grasp_stage: str = "idle", +) -> np.ndarray: + """ + Create simple PBVS visualization overlay. + + Args: + image: Input image (RGB or BGR) + current_target: Current target Detection3D + position_error: Position error Vector3 + target_reached: Whether target is reached + grasp_stage: Current grasp stage string + + Returns: + Image with PBVS overlay + """ + viz = image.copy() + + # Only show PBVS info if we have a target + if current_target is None: + return viz + + # Create status panel at bottom + height, width = viz.shape[:2] + panel_height = 100 + panel_y = height - panel_height + + # Semi-transparent overlay + overlay = viz.copy() + cv2.rectangle(overlay, (0, panel_y), (width, height), (0, 0, 0), -1) + viz = cv2.addWeighted(viz, 0.7, overlay, 0.3, 0) + + # PBVS Status + y_offset = panel_y + 20 + cv2.putText( + viz, + "PBVS Control", + (10, y_offset), + cv2.FONT_HERSHEY_SIMPLEX, + 0.6, + (0, 255, 255), + 2, + ) + + # Position error + if position_error: + error_mag = np.linalg.norm([position_error.x, position_error.y, position_error.z]) + error_text = f"Error: {error_mag * 100:.1f}cm" + error_color = (0, 255, 0) if target_reached else (0, 255, 255) + cv2.putText( + viz, + error_text, + (10, y_offset + 25), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + error_color, + 1, + ) + + # Stage + cv2.putText( + viz, + f"Stage: {grasp_stage}", + (10, y_offset + 45), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + (255, 150, 255), + 1, + ) + + # Target reached indicator + if target_reached: + cv2.putText( + viz, + "TARGET REACHED", + (width - 150, y_offset + 25), + cv2.FONT_HERSHEY_SIMPLEX, + 0.6, + (0, 255, 0), + 2, + ) + + return viz + + +def visualize_detections_3d( + rgb_image: np.ndarray, + detections: list[Detection3D], + show_coordinates: bool = True, + bboxes_2d: list[list[float]] | None = None, +) -> np.ndarray: + """ + Visualize detections with 3D position overlay next to bounding boxes. + + Args: + rgb_image: Original RGB image + detections: List of Detection3D objects + show_coordinates: Whether to show 3D coordinates next to bounding boxes + bboxes_2d: Optional list of 2D bounding boxes corresponding to detections + + Returns: + Visualization image + """ + if not detections: + return rgb_image.copy() + + # If no 2D bboxes provided, skip visualization + if bboxes_2d is None: + return rgb_image.copy() + + # Extract data for plot_results function + bboxes = bboxes_2d + track_ids = [int(det.id) if det.id.isdigit() else i for i, det in enumerate(detections)] + class_ids = [i for i in range(len(detections))] + confidences = [ + det.results[0].hypothesis.score if det.results_length > 0 else 0.0 for det in detections + ] + names = [ + det.results[0].hypothesis.class_id if det.results_length > 0 else "unknown" + for det in detections + ] + + # Use plot_results for basic visualization + viz = plot_results(rgb_image, bboxes, track_ids, class_ids, confidences, names) + + # Add 3D position coordinates if requested + if show_coordinates and bboxes_2d is not None: + for i, det in enumerate(detections): + if det.bbox and det.bbox.center and i < len(bboxes_2d): + position = det.bbox.center.position + bbox = bboxes_2d[i] + + pos_xyz = np.array([position.x, position.y, position.z]) + + # Get bounding box coordinates + _x1, y1, x2, _y2 = map(int, bbox) + + # Add position text next to bounding box (top-right corner) + pos_text = f"({pos_xyz[0]:.2f}, {pos_xyz[1]:.2f}, {pos_xyz[2]:.2f})" + text_x = x2 + 5 # Right edge of bbox + small offset + text_y = y1 + 15 # Top edge of bbox + small offset + + # Add background rectangle for better readability + text_size = cv2.getTextSize(pos_text, cv2.FONT_HERSHEY_SIMPLEX, 0.4, 1)[0] + cv2.rectangle( + viz, + (text_x - 2, text_y - text_size[1] - 2), + (text_x + text_size[0] + 2, text_y + 2), + (0, 0, 0), + -1, + ) + + cv2.putText( + viz, + pos_text, + (text_x, text_y), + cv2.FONT_HERSHEY_SIMPLEX, + 0.4, + (255, 255, 255), + 1, + ) + + return viz diff --git a/dimos/robot/unitree/external/__init__.py b/dimos/mapping/__init__.py similarity index 100% rename from dimos/robot/unitree/external/__init__.py rename to dimos/mapping/__init__.py diff --git a/dimos/mapping/google_maps/conftest.py b/dimos/mapping/google_maps/conftest.py new file mode 100644 index 0000000000..09a7843261 --- /dev/null +++ b/dimos/mapping/google_maps/conftest.py @@ -0,0 +1,38 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +from pathlib import Path + +import pytest + +from dimos.mapping.google_maps.google_maps import GoogleMaps + +_FIXTURE_DIR = Path(__file__).parent / "fixtures" + + +@pytest.fixture +def maps_client(mocker): + ret = GoogleMaps() + ret._client = mocker.MagicMock() + return ret + + +@pytest.fixture +def maps_fixture(): + def open_file(relative: str) -> str: + with open(_FIXTURE_DIR / relative) as f: + return json.load(f) + + return open_file diff --git a/dimos/mapping/google_maps/fixtures/get_location_context_places_nearby.json b/dimos/mapping/google_maps/fixtures/get_location_context_places_nearby.json new file mode 100644 index 0000000000..9196eaadee --- /dev/null +++ b/dimos/mapping/google_maps/fixtures/get_location_context_places_nearby.json @@ -0,0 +1,965 @@ +{ + "html_attributions": [], + "next_page_token": "AciIO2fBDpHRl2XoG9zreRkt9prSCk9LDy3sxfc-6uK7JcTxGSvbWY-XX87H38Pr547AkGKiHbzLzhvJxo99ZgbyGYP-9On6WhEFfvtiSnxWrLbz3V7Cfwpi_2GYt1TMeAqGnGlhFev1--1WgmfBnapSl95c7Myuh4Yby8UM34rMAWh9Md-T9DOVExJuqunnZMrS2ViNa1IRyboIu9ixrNTNYJXQ6hoSVlkM26Yw2sJB900sQFiChr_FrDIP6dbdIzZMZ3si7-3CFrR4gy6Y6wlyeVEiriGye9cFi8U0d0BprgdSIHC3hmp-pG8qtOHvn5tXJp6bDvU12hvRL32D4FFxgM1xKHqGdrun3N06tW2G_XuXZww3voN-bZh2y5y8ubZRJbcLjZQ-rpMUKVsfNPbdVYYPgV0oiLA8IlPQkbF5MM4M", + "results": [ + { + "geometry": { + "location": { + "lat": 37.7749295, + "lng": -122.4194155 + }, + "viewport": { + "northeast": { + "lat": 37.812, + "lng": -122.3482 + }, + "southwest": { + "lat": 37.70339999999999, + "lng": -122.527 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/geocode-71.png", + "icon_background_color": "#7B9EB0", + "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", + "name": "San Francisco", + "photos": [ + { + "height": 675, + "html_attributions": [ + "Zameer Dalvi" + ], + "photo_reference": "AciIO2d9Esuu4AjK5SCX_Byk2t2jNOCJ1TkBc9V7So6HH2AjHH7SccRs-n7fGxN2bdQdm_t-jrSdyt7rmGoPil2-_phu5dXOszGmOG6HITWPRmQOajaPG4WrTQvAV6BCs5RGFq3NxJZ-uFyHCT472OFg15-d-iytsU_nKWjPuX1xCwmNDmuWxTc8YBWi05Cf0MxIFsVw7oj5gaHvGFx0ngYJlk67Jwl6vOTIBiEHfseOHkGhkMD7tX-RCPBhnaAUgGXRbuawYXkiu32c9RhxRaXReyFE_TtX09yqvmA6zr9WhaCLT0vTt4-KMOxpoACBnVt7gYVvRk-FWUXBiHISzppFi6o7FbEW4OE4WWsAXSFamzI5Z5Co9cAb8BTPZX8P3E-tZiWyoOb1WyhqjpGPKYsa7YJ_SRLFMI3kv8GWOb744A4t-3kLBIgZQi9nE5M4cfqmMmdofXLEct9srvrDVEjKns5kP3yp94xrV9205rGcqMtQ3rcQWhl62pLDxf3iEahwvxV-adcMVmaPjLFCrPiUCT1xKtBtRSQDjPcuUMBPaZ-7ylCuFvJLSEaEt8WpDiSDbn22NiuM0hPqu8tqL7hJpxsXPi6fLCreITtMwCBK_sS_-3C--VNxDhyAIAdjA3iOPnTtIw", + "width": 1080 + } + ], + "place_id": "ChIJIQBpAG2ahYAR_6128GcTUEo", + "reference": "ChIJIQBpAG2ahYAR_6128GcTUEo", + "scope": "GOOGLE", + "types": [ + "locality", + "political" + ], + "vicinity": "San Francisco" + }, + { + "business_status": "OPERATIONAL", + "geometry": { + "location": { + "lat": 37.7795744, + "lng": -122.4137147 + }, + "viewport": { + "northeast": { + "lat": 37.78132539999999, + "lng": -122.41152835 + }, + "southwest": { + "lat": 37.777981, + "lng": -122.41572655 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/generic_business-71.png", + "icon_background_color": "#7B9EB0", + "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", + "name": "Civic Center / UN Plaza", + "photos": [ + { + "height": 3072, + "html_attributions": [ + "Neal" + ], + "photo_reference": "AciIO2eQ5UqsRXvWTmbnbL9VjSIH-1SXRtU1k0UuJlVEyM_giS9ELQ-M4rjAF2wkan-7aE2l4yFtF4QmTEvORdTaj_lgO-_r9nTF2z7FKAFGcFxLL4wff1BD2NRu1cfYVWvStgOkdKGbZOmqKEpSU7qoFM_GjUdLO5ztvMCAJ8_h0-3VDy33ha8hGIa8AGuLhpitRAsRK9sztugTtxtaOruuuTtagZdfpyIvUjW1pJMCR3thLaWO2C4DVElGqhv4tynPVByugRqINceswryUNVh1yf_TD664L6AyyqjIL5Vv2583bIEefWHB3uEYJA2ohOV2YW_XhH5rY8Xg5Rdy6i8EUtW9GiVH694YHIgDEZsT-Or4uw_OHHYANd3z7MuQmLZ_JzyUCr8_ex8qxfzluml2bkfciWx3cqJ7YzodaED5nvzjffEuKXwp8cIz5cWF-xm1XSbTWZK5dafqVTC83ps9wDvoCmkPY2lXOgXhmTv85VTQNe8nj75LsplDo73CPg4XFRi6fZi-oicmtCjdjzpjUTHbHe3PEGB1F11BOPh_Hx8QkZlbWwIFooJc9FF8dgAh1GQzlwYb93tcPmRLAiaunw-h9F3eKDb7YghwBPtiBh6HygyNMnA4gtqdBd_qGQ6rVt9cLGCz", + "width": 4080 + } + ], + "place_id": "ChIJYTKuRpuAhYAR8O67wA_IE9s", + "plus_code": { + "compound_code": "QHHP+RG Mid-Market, San Francisco, CA, USA", + "global_code": "849VQHHP+RG" + }, + "rating": 3.5, + "reference": "ChIJYTKuRpuAhYAR8O67wA_IE9s", + "scope": "GOOGLE", + "types": [ + "subway_station", + "transit_station", + "point_of_interest", + "establishment" + ], + "user_ratings_total": 375, + "vicinity": "1150 Market Street, San Francisco" + }, + { + "business_status": "OPERATIONAL", + "geometry": { + "location": { + "lat": 37.7802611, + "lng": -122.4145017 + }, + "viewport": { + "northeast": { + "lat": 37.7817168802915, + "lng": -122.4131737197085 + }, + "southwest": { + "lat": 37.7790189197085, + "lng": -122.4158716802915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/civic_building-71.png", + "icon_background_color": "#7B9EB0", + "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/civic-bldg_pinlet", + "name": "U.S. General Services Administration - Pacific Rim Region", + "opening_hours": { + "open_now": false + }, + "photos": [ + { + "height": 2448, + "html_attributions": [ + "Wai Ki Wong" + ], + "photo_reference": "AciIO2cN35fxs7byGa6qiTiJAxxJMorGoHDJp95RMFDnTMm-wDrb0QUZbujgJUBIV3uLQuBDpEdvyxxzc-fyzT3DgFlJSLKcnPcm_A-Fe3cj7rjPdEO9VMj0HHRf0aqDnRQXmtv2Ouh3QUH8OdvaoOlNMw293LOxjri9JvpjhPHCwJvwkKjxFYButiE_7XywtIRyQXRkZyDKxqKVxITircGB1P3efABFUQIye8hA71QZqTfYnBzT5wDSoV3oZRaB9aXUlTDGzNl3rJXE74BrlpgVhf-uYP_POcNqMbYmLXyWOjjVEZ4YZL58Ls53etW_ZUGGeiUAcrI3Uuq4glX5GRfGHssf_dqOWA29j0HZh6A_OFSluLSDbpy-HgXcW4Zg_qgF6XqobV78J_Ira4m8lgHiT3nDffo2YfELDcIvFxOJwpl1W3TUWawmHqvHiVTvHAQ_8-TcWE_rGCVIAAc8I0W25qRFngkVJ828ZIMHsnEiLLgsKTQlxKW94uAC8kgxh6v-iXP_7vP6-0aWGkFs4a2irwfQK5n5fKmDz7LBdVjyuAhoHwcCwE8VTn0wtwUcuiVCVBFs4-AnLWhwnVxf3fdmcMsZm91lPbm3fECbnt6SBhvXR48cM_ZZpMiyfIF1QuNE-vhfsnlK", + "width": 3264 + } + ], + "place_id": "ChIJZxSUVZeAhYAReWcieluNDvY", + "plus_code": { + "compound_code": "QHJP+45 Civic Center, San Francisco, CA, USA", + "global_code": "849VQHJP+45" + }, + "rating": 2, + "reference": "ChIJZxSUVZeAhYAReWcieluNDvY", + "scope": "GOOGLE", + "types": [ + "point_of_interest", + "establishment" + ], + "user_ratings_total": 4, + "vicinity": "50 United Nations Plaza, San Francisco" + }, + { + "business_status": "OPERATIONAL", + "geometry": { + "location": { + "lat": 37.7801589, + "lng": -122.4143371 + }, + "viewport": { + "northeast": { + "lat": 37.7818405302915, + "lng": -122.4131042697085 + }, + "southwest": { + "lat": 37.7791425697085, + "lng": -122.4158022302915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/civic_building-71.png", + "icon_background_color": "#7B9EB0", + "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/civic-bldg_pinlet", + "name": "Federal Office Building", + "opening_hours": { + "open_now": false + }, + "photos": [ + { + "height": 3024, + "html_attributions": [ + "espresso" + ], + "photo_reference": "AciIO2eg880rvWAbnaX5xUzP_b6dEVp4hOnZqnxo7W_1S2BZwdC0H9io5KptUm2MGue3FOw3KWPjTZeVu8B_gnFh-5EyAhJHqhlDrllLsL8K-cumkjtTDT3mxDaDXeU7XB9BWD7S0g0f4qbjEu_sKhvWAXE81_r1W5I8minbMbvzu3eU1sYICwWOk_5g4D1-690I_4V-4aJ-fDD04kHxsqkweZcxzUHgrmcKEOlt48UKVHe-GEOLD5-BRNZ3k4tx50T1SKqPeNUI_WtTrYkSkeNzCp4t9680YqCW7LBsES9viJdW_QBTgQd59gvMeIWEXQ-YBGPEobIS0hE73Eedi_1ATESgKI-tzOeeoeytLnmFFVC8c2obgt2Bd7cLOFjIjm5Oxn9jH0auBWPx8JsQifkXiyhXz2VP2AawCmID4TMtMwt-9ozTV6I_j5f_guI34w7MxKnHiyTQvupi0S4O2ByezHx56M7Ptmxjk8yia84SG20H7sRhEk3yeQHl_ujDGYhNFCtPmHWkCsdWm1go-FuMalIzkUL4ERuREN1hhdvYhswbbigJUG8mKKOBzHuPVLNK5KFs_N7E5l4g3v-drOKe1m_GafTHwQDRvEzJfL0UnIERhRYcRLMJWxeEbjtsnKch", + "width": 4032 + } + ], + "place_id": "ChIJzdTUHpuAhYAR3ZHR1a8TJ-k", + "plus_code": { + "compound_code": "QHJP+37 Civic Center, San Francisco, CA, USA", + "global_code": "849VQHJP+37" + }, + "rating": 4.2, + "reference": "ChIJzdTUHpuAhYAR3ZHR1a8TJ-k", + "scope": "GOOGLE", + "types": [ + "point_of_interest", + "establishment" + ], + "user_ratings_total": 5, + "vicinity": "50 United Nations Plaza, San Francisco" + }, + { + "business_status": "OPERATIONAL", + "geometry": { + "location": { + "lat": 37.7799364, + "lng": -122.4147625 + }, + "viewport": { + "northeast": { + "lat": 37.78122733029149, + "lng": -122.4136141697085 + }, + "southwest": { + "lat": 37.7785293697085, + "lng": -122.4163121302915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/civic_building-71.png", + "icon_background_color": "#7B9EB0", + "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/civic-bldg_pinlet", + "name": "UN Plaza", + "opening_hours": { + "open_now": false + }, + "photos": [ + { + "height": 3024, + "html_attributions": [ + "Douglas Cheung" + ], + "photo_reference": "AciIO2f7vWVfkBpMV-nKU0k06pZS--irdg7JnnJBrztgXtRf0MYFd0085Gfjm7TjBB4bCefkTdJBsNtyKiklgknHCuWhz3aqwx81XHDM51Jn-g5wI0hbG6dx8RpheFxfht_vpk9CQgjjg8mFEUp-aQaEc3hivi_bog295AUmEKdhTCRlYWLQJFPEpP-AKOpLwXdKYAjddd2nh18x9p8-gF0WphREBQFaOChd9lnWyuSKX-MOecG-ff1Brwpkcroc6VUeW6z1RQcLFNCUOomOpBCmeujvTquM_bI7a6T4WzM2o6Et_47EXmPzJhSAONorX8epNNHjZspoAd-LZ_PrBgy8H-WQEm6vlY88Dtc1Sucewnrv4Cd8xm2I1ywKPSsd2mgYBMVAipSS2XHuufe5FWzZM9vPZonW0Vb-X6HOAnVeQ52ZxNddc5pjDtU5GOZNb2oF-uLwo5-qrplZDryO5if0CPQRzE6iRbO9xLsWV0S7MGmxJ_bZk7nxWXjKAFNITIZ6dQcGJxuWH_LKDsF3Sfbg1emM4Xdujx0ZHhgFcBISAfHjX5hf0kBxGhpMlFIPxRns2Eng4HzTaebZAmMeqDoN_3KlnAof47SQyeLSQNy1K6PjWGrIPfaVOpubOTLJF_dLKt5pxQ", + "width": 4032 + } + ], + "place_id": "ChIJ60hDVZeAhYAReuCqOWYsr_k", + "plus_code": { + "compound_code": "QHHP+X3 Civic Center, San Francisco, CA, USA", + "global_code": "849VQHHP+X3" + }, + "rating": 4, + "reference": "ChIJ60hDVZeAhYAReuCqOWYsr_k", + "scope": "GOOGLE", + "types": [ + "city_hall", + "point_of_interest", + "local_government_office", + "establishment" + ], + "user_ratings_total": 428, + "vicinity": "355 McAllister Street, San Francisco" + }, + { + "business_status": "OPERATIONAL", + "geometry": { + "location": { + "lat": 37.781006, + "lng": -122.4143741 + }, + "viewport": { + "northeast": { + "lat": 37.78226673029149, + "lng": -122.4129892697085 + }, + "southwest": { + "lat": 37.7795687697085, + "lng": -122.4156872302915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/shopping-71.png", + "icon_background_color": "#4B96F3", + "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/shopping_pinlet", + "name": "McAllister Market & Deli", + "opening_hours": { + "open_now": false + }, + "photos": [ + { + "height": 4608, + "html_attributions": [ + "Asteria Moore" + ], + "photo_reference": "AciIO2chI9JnbQNwZt2yo7E--ruAq6ax7U4NrW_3PcNpGgFzXhxMqvYTtktvSLwFO5k21vHpEH-2AMYuaD6qctoIYdyt_g5EWhF88Ptb75HmmIEQzMqk2Ktpe3Vx06TnJKF47TZnQupjVdy_YTW3XGOGkA33Phe8I3I9szr54QqmYLFs6fPJMxo-M3keen9PlFiqqjvKAV170CuJ6HQ70AkRREWq3h18IcPUHHEKiZng5TKPSB7t_3dbyB_DWETnVQHu6P33XEmcKw77rgCuUogyxXZNMBulq305-FtBlH5lnvjy1F5Hpwf-q5cSB_40p082Joz0Vyazc1o4s-hnEyUnaQ6Zra1B_ODKvHqEKHoeJUKT4nAfFU4kBE5A7nmxkozqyks4MfaoN_P72atAhggEV5rog4EEtzFyeC1bx8GtQKhYccbeANSF5R9mAEpeefOrpYZpNW1uLffUMOpceZpZtNsE-yG59_v-56V1dxqCIGW9KOtVmfoEL0WLP6l-pMhKMv3EdSRmGqhbRtCA2fZNyFBWRyMwpfToRImtYxRbMiqriGONDU1e1m8j895QvLDknS6lY_qRMNv4YY3FLooGcag4YzcaDHwtI-ipxEcFknzhIIYt-_fdlTcUk0JMctC5re--5A", + "width": 2592 + } + ], + "place_id": "ChIJz3oI4ZqAhYARYviYtbeKIFQ", + "plus_code": { + "compound_code": "QHJP+C7 Civic Center, San Francisco, CA, USA", + "global_code": "849VQHJP+C7" + }, + "rating": 3.6, + "reference": "ChIJz3oI4ZqAhYARYviYtbeKIFQ", + "scope": "GOOGLE", + "types": [ + "liquor_store", + "atm", + "grocery_or_supermarket", + "finance", + "point_of_interest", + "food", + "store", + "establishment" + ], + "user_ratings_total": 12, + "vicinity": "136 McAllister Street, San Francisco" + }, + { + "business_status": "OPERATIONAL", + "geometry": { + "location": { + "lat": 37.7802423, + "lng": -122.4145234 + }, + "viewport": { + "northeast": { + "lat": 37.78171363029151, + "lng": -122.4131986197085 + }, + "southwest": { + "lat": 37.77901566970851, + "lng": -122.4158965802915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/civic_building-71.png", + "icon_background_color": "#7B9EB0", + "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/civic-bldg_pinlet", + "name": "US Health & Human Services Department/Office of the Regional Director", + "opening_hours": { + "open_now": false + }, + "photos": [ + { + "height": 1200, + "html_attributions": [ + "Patrick Berkeley" + ], + "photo_reference": "AciIO2eP4AmtKmitmIZbdY4bI2mc8aCNzT2vh8plui7wj0BJt-51HlfW7-arowozWM9Os9hSUBkXItcmlXnH08GpOYXc1u6gN-XmO7AL9ifSJfgWYt6XE0CkXfQ9iBQdHF1WFlfteWLOvL0mev0reMuAz78N7It7eWQY8HW3nm2_i14G_R51kbRK2djxoWjDqY9-xP5hTxWUs1u7JFqXtzOZAeMGlhFHHmqVe4A8nWMP7tr6Y385wmCIJvGwXivQmct7flmN6NpNqqp1U5CI1jy60x7Z2Zoq_uxzWpIB-1M-VRMJHblbb_1rPAc1Sg29n5XfhX4E1M1YqlEBdqg08VaqQSLbaJEHkvfDMFKlN36IsZmb8mZfFEinYSmkcISO6x-vuhgR7G4FJZLtt74goVGKIPsQoC9oPsPyN0mLaQJs9ZTS6D2mw5zIQXYBs2IfBdnG9sWDCQTujtdGWJv_SlWUHW499I-NK0MzNPjpLB4FW3dYOuqDQdk-8hzC1A5giSjr7J783WRLVhVKjfo8G8vCPCSY4JW6x3XB5bl9IJn5j_47sGhJOrHnHVkNaMmJMtdhGflXwT42-i033uzLJEGN1e887Jqe7OHRHqa97oPbXu3FQgVPjXvdBX33gmXc8XXeDg7gcQ", + "width": 1600 + } + ], + "place_id": "ChIJ84fbMZuAhYARravvIpQYCY8", + "plus_code": { + "compound_code": "QHJP+35 Civic Center, San Francisco, CA, USA", + "global_code": "849VQHJP+35" + }, + "rating": 4, + "reference": "ChIJ84fbMZuAhYARravvIpQYCY8", + "scope": "GOOGLE", + "types": [ + "local_government_office", + "health", + "point_of_interest", + "establishment" + ], + "user_ratings_total": 1, + "vicinity": "San Francisco Federal Building, 90 7th Street #5, San Francisco" + }, + { + "business_status": "OPERATIONAL", + "geometry": { + "location": { + "lat": 37.7794949, + "lng": -122.414318 + }, + "viewport": { + "northeast": { + "lat": 37.78079848029149, + "lng": -122.4128637197085 + }, + "southwest": { + "lat": 37.7781005197085, + "lng": -122.4155616802915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/school-71.png", + "icon_background_color": "#7B9EB0", + "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/school_pinlet", + "name": "Oasis For Girls", + "opening_hours": { + "open_now": false + }, + "photos": [ + { + "height": 3024, + "html_attributions": [ + "Alex" + ], + "photo_reference": "AciIO2cENrSmK967GV0iLgnIakOvEMavm9r5kA_LjIOHIji_Pc0T74VL-vwiFlUgoVgetRw9B-PzYrJ54EVfnbUQT-9XRi2LGt9rUOGX6V7h7lOVqgEJ1eaWEUtTDyk93eQRs3cc3GhXY2RIjL-nVdaxkwRc_RWpRPLcc8Om_aTYwyCQ5S7ZpmxPS419DoCJHt4sQJqzRsD6gz7I8AGj0c03MHYascQn4efsvFhjzaPex21ZKI9iGz923oe9WM8zq4BhgKJ3B9_IITYDuoO1mYdyIgU57ceuRoKb6n4zoCgyhLne1_SzGnFz7DrP9jL8luHSVHeoZcSKmU34Gr-sGfVs4kfH33lzlNurHQI6gIoOOWOXq7BTP-Jf5ArqGexfQfue7IGJpYjR4p5r4cJZ-dd0tzhlGvrZ2cSEnjQdv4oTx3U3kElm6foWI3xySsa1jmqsZ8BBBzEQ75rzHHhsW26xwwR9ZIKYV-_DZ9r0hrb0qPCEF3aAC9r2m6rfwrHWAfDy_-Egmv_5T1QyBFaAUT0Faay7EezCxCyWwx_0x0o2DRIOAcA8a01veJJPv1LhYcXCUnTgIATbSr-t30d9FdosyX0Vk9w4eSXU6B4qUWpusHVHPShTHhAcLMig0OOIXlZyyWtPT2sb", + "width": 4032 + } + ], + "place_id": "ChIJyTuyEoKAhYARr0GnPKZSGCk", + "plus_code": { + "compound_code": "QHHP+Q7 Civic Center, San Francisco, CA, USA", + "global_code": "849VQHHP+Q7" + }, + "rating": 5, + "reference": "ChIJyTuyEoKAhYARr0GnPKZSGCk", + "scope": "GOOGLE", + "types": [ + "point_of_interest", + "establishment" + ], + "user_ratings_total": 4, + "vicinity": "1170 Market Street, San Francisco" + }, + { + "business_status": "OPERATIONAL", + "geometry": { + "location": { + "lat": 37.77929669999999, + "lng": -122.4143825 + }, + "viewport": { + "northeast": { + "lat": 37.78060218029149, + "lng": -122.4129812697085 + }, + "southwest": { + "lat": 37.77790421970849, + "lng": -122.4156792302915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/generic_business-71.png", + "icon_background_color": "#7B9EB0", + "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", + "name": "San Francisco Culinary Bartenders & Service Employees Trust Funds", + "place_id": "ChIJpS60CuyBt4cRzO3UB4vL3L0", + "plus_code": { + "compound_code": "QHHP+P6 Civic Center, San Francisco, CA, USA", + "global_code": "849VQHHP+P6" + }, + "rating": 3.3, + "reference": "ChIJpS60CuyBt4cRzO3UB4vL3L0", + "scope": "GOOGLE", + "types": [ + "point_of_interest", + "establishment" + ], + "user_ratings_total": 6, + "vicinity": "1182 Market Street #320, San Francisco" + }, + { + "business_status": "CLOSED_TEMPORARILY", + "geometry": { + "location": { + "lat": 37.7801722, + "lng": -122.4140068 + }, + "viewport": { + "northeast": { + "lat": 37.7817733302915, + "lng": -122.4129124197085 + }, + "southwest": { + "lat": 37.7790753697085, + "lng": -122.4156103802915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/civic_building-71.png", + "icon_background_color": "#7B9EB0", + "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/civic-bldg_pinlet", + "name": "San Francisco Federal Executive Board", + "permanently_closed": true, + "photos": [ + { + "height": 943, + "html_attributions": [ + "San Francisco Federal Executive Board" + ], + "photo_reference": "AciIO2ecs5V8ZC8IEmpnMKdhn2pSWsCYSZ6C9Zf6lnQbp3owjaXeXRZuPMtnIJag_ga0uw8Jwa8SB-Wsb2YyB9PrdAzutETaYb56zja6D8NwiKdf9Z4EGnZ45JH20x7119EzrunOm1q4Ii6wuY0TudtYsadmJC0NPLnUZlua4PNnW7Zl76OQwLBcaPWu6rXBHCTT6iiBqSZeKiKJ8w4RzttHfN3oYB-IE02CXQPQX1xxFEeQ5cyuGPtv8ghXHRoSJdhvYDH_P0aSrOt9ibRtrH5kv7nAamKSVUNWvT5vuPrXao9PkaJd5f16tZiDoM_61tat9r1izspBFhU", + "width": 943 + } + ], + "place_id": "ChIJu4Q_XDqBhYARojXRyiKC12g", + "plus_code": { + "compound_code": "QHJP+39 Civic Center, San Francisco, CA, USA", + "global_code": "849VQHJP+39" + }, + "reference": "ChIJu4Q_XDqBhYARojXRyiKC12g", + "scope": "GOOGLE", + "types": [ + "point_of_interest", + "establishment" + ], + "vicinity": "50 United Nations Plaza, San Francisco" + }, + { + "business_status": "OPERATIONAL", + "geometry": { + "location": { + "lat": 37.779756, + "lng": -122.41415 + }, + "viewport": { + "northeast": { + "lat": 37.78130935000001, + "lng": -122.411308 + }, + "southwest": { + "lat": 37.77806635, + "lng": -122.4163908 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/generic_business-71.png", + "icon_background_color": "#7B9EB0", + "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", + "name": "Civic Center/UN Plaza BART Station", + "photos": [ + { + "height": 4032, + "html_attributions": [ + "Arthur Glauberman" + ], + "photo_reference": "AciIO2f1VMpAIRJouUVjkeEUyHB-4jzRZ2_U3kfRr-LaavcPlVYClnn2DMGMiWo9Oun0t-qo9z5WIHp1BQBHazbPqrWnSGvQoO3FpJMra0OOGSgrpsD5T4dvinfSzWqwOOlRtMyQ4vlGvR99TpxcNVcasRyNflpZxRcYD9nBUPnrNUstxTCfKqSqLdYD3ZI0xZiX3wOJ_hlUVgRfSs04iqzREGvRR8cZRaufh1Hakq3bzaBL1KGuLF8ggV94iGQmzWYmU_FddWgH9ZhjGyMPi8LYdNmypH0fBenoYGVE_bUV9dWqh5dFIKDwCyxkbIseJ6Z49MRFnSEFTtBr02xVz7Q1vAx0iKSRAMof3o5dqEd5Y1fVhDuLk3KT5JisNQZd_yWXDflaHmEgjEqza7uTrdR6LWysHDD8EdUrGQxWWHmneyc3qdWlc0TBxhGp3Q8V0a3Ian1k75PqrfkyC_IITP0KIDmaylgMSMmAQbzvkeHDtPcibG-BiNn2FNK7T77m7GpQkubMwYOI1PkoGSmveiuooTTqj6PSDGrQdDfRllk_HSwcTnd9csLazAQP_tLKHX8lsHTtTE7Orkcf8IEUfmV35Ltx2HzLYytejCYYS7ZoSfgjDTZUOY41QQ-YS0tIDKHpgr_PJqtT", + "width": 3024 + } + ], + "place_id": "ChIJK0jeP5uAhYARcxPNUpvfc7A", + "plus_code": { + "compound_code": "QHHP+W8 Civic Center, San Francisco, CA, USA", + "global_code": "849VQHHP+W8" + }, + "rating": 3.5, + "reference": "ChIJK0jeP5uAhYARcxPNUpvfc7A", + "scope": "GOOGLE", + "types": [ + "transit_station", + "point_of_interest", + "establishment" + ], + "user_ratings_total": 2, + "vicinity": "United States" + }, + { + "business_status": "OPERATIONAL", + "geometry": { + "location": { + "lat": 37.779989, + "lng": -122.4138743 + }, + "viewport": { + "northeast": { + "lat": 37.7811369802915, + "lng": -122.4131672197085 + }, + "southwest": { + "lat": 37.7784390197085, + "lng": -122.4158651802915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/generic_business-71.png", + "icon_background_color": "#7B9EB0", + "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", + "name": "UN Skate Plaza", + "place_id": "ChIJR4ivYwCBhYAR2xEDgcXd8oE", + "plus_code": { + "compound_code": "QHHP+XF Civic Center, San Francisco, CA, USA", + "global_code": "849VQHHP+XF" + }, + "reference": "ChIJR4ivYwCBhYAR2xEDgcXd8oE", + "scope": "GOOGLE", + "types": [ + "point_of_interest", + "establishment" + ], + "vicinity": "1484 Market Street, San Francisco" + }, + { + "business_status": "OPERATIONAL", + "geometry": { + "location": { + "lat": 37.7798254, + "lng": -122.4149907 + }, + "viewport": { + "northeast": { + "lat": 37.7811608302915, + "lng": -122.4137199197085 + }, + "southwest": { + "lat": 37.77846286970851, + "lng": -122.4164178802915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/generic_business-71.png", + "icon_background_color": "#7B9EB0", + "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", + "name": "Curry Without Worry", + "opening_hours": { + "open_now": false + }, + "photos": [ + { + "height": 3024, + "html_attributions": [ + "Sterling Gerard" + ], + "photo_reference": "AciIO2cHQr4ENxn9-409JJPj5hKunwLPi9gn-eN4W0X85UOvQVHoKQBUA4AotH3pkFTPxm1X76omOi2jbTiRSL9-eRFhA9wWpiXoSj2ggXeHrUxLMQBZb7cQuH4lg9YCOasXwXz3-e3H1lrByl7en3XSTkvuZUDrbtHocGV-0XNw2YpOmVvN-mLcRxgUpWhguLsvnO7B5JzXjz4ewOAxBLF9f-ZOdRktRcHDczoA0zYsOFwri0CXVjfYdB4HxjwXBPm1vXQY1U5qRydrI0Eru1tbTI9alsrmBOL4l0BAY--_fd3luNnwiQAYHzBJoZ7pqHjGOHtHa-OH7GFawpbxKr8MqeT3KVMcDVWm8sOy-zd2Gjbez5CQ5ld0w-q_2QDTVzHV5ybrzDm1OIl4vIW9eBTQVwkBwnmUjKFSZEQ-ANezOwN6XfW_jkWleRJ28dpXLo25dhW7gmYZxRcGpPwWRpcH3jyenU59CRJ6EG8nqVhTs-JzGOawmsLs4Kyg4f16fJE2lDTySU82fcQgd8uBkJGE-XrFYNOakpMWBKo1GWNOvfPsceoyB4qiLwf7VFM5Sa8yQUmNxdKRvVvhqCRjzGwVQmcPEOgpANBuDTUdz9VscmOhPO_29jRMca1S9AuseiZBdmRO4HHv", + "width": 4032 + } + ], + "place_id": "ChIJKZtFDpuAhYAR7xKvaP5D1dI", + "plus_code": { + "compound_code": "QHHP+W2 Civic Center, San Francisco, CA, USA", + "global_code": "849VQHHP+W2" + }, + "rating": 4.7, + "reference": "ChIJKZtFDpuAhYAR7xKvaP5D1dI", + "scope": "GOOGLE", + "types": [ + "point_of_interest", + "establishment" + ], + "user_ratings_total": 14, + "vicinity": "50 United Nations Plaza, San Francisco" + }, + { + "business_status": "OPERATIONAL", + "geometry": { + "location": { + "lat": 37.7798179, + "lng": -122.4149928 + }, + "viewport": { + "northeast": { + "lat": 37.7811602302915, + "lng": -122.4137218697085 + }, + "southwest": { + "lat": 37.7784622697085, + "lng": -122.4164198302915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/generic_business-71.png", + "icon_background_color": "#7B9EB0", + "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", + "name": "UN Skatepark", + "photos": [ + { + "height": 4096, + "html_attributions": [ + "Ghassan G" + ], + "photo_reference": "AciIO2cuIIcUq2yO7nQ_aENkWHN-EBW8baPzWgyrlTnoDLJnZ3xkqA3qGN06NxagIX9LHoTMQKoBBtLKns2IEl90Mb3H_2P13nbPfRUkK0LEwZYq8jrhkAr1kkiuSzQZwXaQEw8o3W4kTBjRhrSnqv69l-mQjTnOMPnIvfdsfM-7-5cCCbReiG2UuhJaxEEP4HEQhpoKPdeysLMtlmOG3AkapY9hUggeffNhVVSc55UEM7CRWozNOoy8oVS6E-kixEK5Zvnrs2JgCarGttCGaQPrxg_R3LjCfWNCqbHD5pz5UGlN_Nixxf5un7OoTvmvxHCjSblmFZttvdfpoI9H54u-rdY6XBeCXON4hcc8vTt-H7pUoPOYQAQvOEsMknrcKQ10Fr7MdsMqp495fV0xc1WK-TMf0sd8aTHjJlDh0_yvi9gzBd47UzJddXi81F0y7HLNpwAHorBvYsPKM3c3pCCKjzOJKtieqvv-xvvdygIEFh4GvIfqInYEpsZeIgvnpUWZKeRoBeAh46AWyHe_-iZzkG94o5TRWiX1McziIr0nXb-2-V0uDhY1CZzDZZxTNPuaanEBSekt9tUMoF-TF-0YSyxGSlm4w8EfGhBrde4vKu2JyunwApDogalJbiDVsX5x7ZqwvBS6sBQxmxotvhRApbUOSRE", + "width": 3072 + } + ], + "place_id": "ChIJfZvlNy-BhYARYrz8xesnfo8", + "plus_code": { + "compound_code": "QHHP+W2 Civic Center, San Francisco, CA, USA", + "global_code": "849VQHHP+W2" + }, + "rating": 5, + "reference": "ChIJfZvlNy-BhYARYrz8xesnfo8", + "scope": "GOOGLE", + "types": [ + "point_of_interest", + "establishment" + ], + "user_ratings_total": 1, + "vicinity": "50 United Nations Plz, San Francisco" + }, + { + "business_status": "OPERATIONAL", + "geometry": { + "location": { + "lat": 37.7798164, + "lng": -122.4149956 + }, + "viewport": { + "northeast": { + "lat": 37.7811597302915, + "lng": -122.4137233697085 + }, + "southwest": { + "lat": 37.7784617697085, + "lng": -122.4164213302915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/generic_business-71.png", + "icon_background_color": "#7B9EB0", + "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", + "name": "Sim\u00f3n Bol\u00edvar Statue", + "opening_hours": { + "open_now": true + }, + "photos": [ + { + "height": 3452, + "html_attributions": [ + "Willians Rodriguez" + ], + "photo_reference": "AciIO2cWoTT9PaFCzH3sXfgvrgMG7uflXzfYSi4jJwNNBJRMxVPQp1TO-_3F0HFe4cWsF-z2g0MrluTzpSdWET57_kIPxx_rRh7TpX6Nv6jpWStd6hDBSAu-WGoaV8T2KESXe-N4WhG0afkZV61_rKqYtk9tc_NsE7Und84qxrQHTD2U-SYCSevUE4EkOGtinTv1o9Ll9yS2Svct_xPp5dAPJEJLBj2JBmWyn2p-sK-DzFHaGzP4r1NfAxQx0oQdoa3R0IUOXLIM6Xx8B_By8Vv9x9Z6wRlblRIM9CiX497_oDaYINg0w8lBtaEN5SSO7QxPRfV8o5NtJWBMqabnW7wepbRqq7BQh43-3HO_HXB1H6nP-cHLXetjXtN775nnAWlhXCEV_2Gb2HTRK0s7xQXHGZdKQCwDXAiTLtHFNGSaqQ3GhQ6iZdGquwh3q46lv6aRczhbo2kGRUgnkYYUa8AquE7Et0miHHw2zKc3lXX9FHQQannKHRc_yMQUpeKQGlBIxTmGvKLeatxHN6iLrtlfSIuHSc4FJWaYqkkiPAny1ZYcM61Jar67gMpf3-3RVwckUMqy4a9yDJawO-g8d-9svKI-5QlZXqlayrNnPsU6KSEgJhkJ95Fdi0nNM9qRYVFVbFVzosF0", + "width": 1868 + } + ], + "place_id": "ChIJxwBPDpuAhYAREmyxJOv11Nk", + "plus_code": { + "compound_code": "QHHP+W2 Civic Center, San Francisco, CA, USA", + "global_code": "849VQHHP+W2" + }, + "rating": 4.4, + "reference": "ChIJxwBPDpuAhYAREmyxJOv11Nk", + "scope": "GOOGLE", + "types": [ + "tourist_attraction", + "point_of_interest", + "establishment" + ], + "user_ratings_total": 23, + "vicinity": "50 United Nations Plaza, San Francisco" + }, + { + "business_status": "OPERATIONAL", + "geometry": { + "location": { + "lat": 37.77961519999999, + "lng": -122.4143835 + }, + "viewport": { + "northeast": { + "lat": 37.78097603029151, + "lng": -122.4127372697085 + }, + "southwest": { + "lat": 37.77827806970851, + "lng": -122.4154352302915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/generic_business-71.png", + "icon_background_color": "#7B9EB0", + "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", + "name": "Fitness Court at UN Plaza", + "opening_hours": { + "open_now": true + }, + "photos": [ + { + "height": 3213, + "html_attributions": [ + "Ally Lim" + ], + "photo_reference": "AciIO2c46aZ1fy0jImtc4i9AybRpqmgpwtnxt0yabDDt0HSzMy6bLyNo06EfEpKBi6cvmAnTmtGPILHAMUacEz6idLBwFO6ClbLGSLpaGmrE-ER462n6AvHQXwHXjL1REr-EU_cWAGUj7vMDJ_8oJwBlON1J6OoUi4N4eaJCgGa2nYN2KhQ_IsxlW06jBWAJ_8i5UzDCk9paPMLTlx6XGrN_ARqihZrDHp1ejLT9LsQuBny8qSHSq6N_cgDjhB6x8DLxLrNeZzFcY6RTwhLDeYqAaV1xlyQN68D8rCd-THrFbXYh0eqnCUNPO2mY0KgET5ifiuIsqEAfpOJp5JHKduPfdRphmIPJfag_kwtJ5kwmjQaDcpmLpVRLxBaFKDmjZ1oFjIm68YpF0z3Tz7chAD90lfLzKKIfQadS5xZLJR-34rJwZA6uiLx-9mEe3upotSZzDmtGQCEbkEJIbWA5TXa0Gr-dK4wQ2RHkzHhIprVlxu6oiXkBzrxx5De5dULfVOtZe25GbYgC6yOGVWppzAawylRfzfroxgD0Q4Qm3vZhrSVdousQjlhvOOd4vNjF4ab1SM0NrBHydXTzm9qO-Q9O45FAGe6DG_9ftmhsrMX57SZpBlnbsYFHZEgNOJhNkAyxcW6rvg", + "width": 5712 + } + ], + "place_id": "ChIJOxlsRwCBhYAR5FY6A3dg8Ek", + "plus_code": { + "compound_code": "QHHP+R6 Civic Center, San Francisco, CA, USA", + "global_code": "849VQHHP+R6" + }, + "rating": 5, + "reference": "ChIJOxlsRwCBhYAR5FY6A3dg8Ek", + "scope": "GOOGLE", + "types": [ + "gym", + "health", + "point_of_interest", + "establishment" + ], + "user_ratings_total": 3, + "vicinity": "3537 Fulton Street, San Francisco" + }, + { + "business_status": "OPERATIONAL", + "geometry": { + "location": { + "lat": 37.77961519999999, + "lng": -122.4143835 + }, + "viewport": { + "northeast": { + "lat": 37.7810261302915, + "lng": -122.4129955697085 + }, + "southwest": { + "lat": 37.7783281697085, + "lng": -122.4156935302915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/cafe-71.png", + "icon_background_color": "#FF9E67", + "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/cafe_pinlet", + "name": "United Nations Cafe", + "opening_hours": { + "open_now": false + }, + "photos": [ + { + "height": 1836, + "html_attributions": [ + "Steven Smith" + ], + "photo_reference": "AciIO2dhjLdgjy4fMy59en74_XnQ8CoXenGsfvaQ3MM7TohCqXE2tS7BYvyYoNu5gZbhJsNRulbldWgRUT1EpRPkiFZoqa1leeUttiHt1NUuSOEOYULofcZ8ShClkfIPk2U6i6-OajtQc5Aj9rYRtS8WmF_19ducNw0h4f3CSSuDPqKIloeNRsWm-uqi2faqjsgqe8iWvsmgABAmcdUhdAuDFWW31TnrtRe3D58TkvUJGv6-cpIDzuNv8gYPyokrz6lngguIGgNfy53t6xdLFbHMQFnLzgFx2NJbFeC2ZX3-WjKMXuy85hHuVUmucmLz80z6_yHa7kxlbpnruFdjhehwajdG7c0uy-HhxG7LVhRy9I4-aE0f5i4lBoZONibJ7KaHGoJLEMLcm5ig-hXHXfGoXIX3MIl5y5IOxhe4N4bimc1IsmMTs0MKw4O0ZbMhQ8yF4Uqb67ZWfIiEKEL7sXxkWGlgE65OAIutewzFNjOuWzsbQ7oCMK77hVI72s83jl3qT7SX4BQcy0wkSblVVTrm1VWf1PajA9Bzye0ZFi4yClaARpsQH8ZnOOsA3igFlJbjNohPzM8EaOPV3eWUqr8o-tkIp8IIAx5OLBqJjOs_E10AvQB7Pc4z2c6viTZDda9E", + "width": 3264 + } + ], + "place_id": "ChIJ4ZfeFJuAhYAREGTVnroeXsg", + "plus_code": { + "compound_code": "QHHP+R6 Civic Center, San Francisco, CA, USA", + "global_code": "849VQHHP+R6" + }, + "rating": 4.5, + "reference": "ChIJ4ZfeFJuAhYAREGTVnroeXsg", + "scope": "GOOGLE", + "types": [ + "cafe", + "point_of_interest", + "food", + "establishment" + ], + "user_ratings_total": 33, + "vicinity": "3537 Fulton Street, San Francisco" + }, + { + "business_status": "OPERATIONAL", + "geometry": { + "location": { + "lat": 37.78012649999999, + "lng": -122.4136321 + }, + "viewport": { + "northeast": { + "lat": 37.78198923029149, + "lng": -122.4121925197085 + }, + "southwest": { + "lat": 37.7792912697085, + "lng": -122.4148904802915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/generic_business-71.png", + "icon_background_color": "#7B9EB0", + "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", + "name": "UN Skate Plaza", + "photos": [ + { + "height": 2268, + "html_attributions": [ + "Ally Lim" + ], + "photo_reference": "AciIO2fVA9xB6yslvpFQ1lHcw50PP-CHL5GT3WOtJCZ9pXvXUQ_PO0UhhmED-HG6hgIzaN5asxwB8vmzFa4xU4PPKu_LIu4XoCl3PDszzyju1ve916Kpw4jxHkXej81y_IwngvIAFFEfehH5n3lgfdkiZW176mppdHS3A1FpuvUP7yRA3jhenmFvSwmhpJJ6qdicxFvd0Gk-0R-bgzE2bowKaDhUE05PdDInRQCc83j4DsKXfu0eyTUSxzKVJ_Cwy8qdyCfKLXKkdPC8puMSa4nHnaATsWwFNY0eIBKwjACewkHIw5cfCOtcnmg8C-k-iElrgDHrZbDuuFTazC44CAaY2IR-H6cylBKKo8vY73T0iWF2OFJN7hQiL41iWu49OkDv_0cLyOveKyCo-TXh-Fw3RXpsf4fOSsO8UO0l9okQ2f62L_2XRYSZtPMoax2ZrlCTiegxYScg4dvuEuKDQ6_lAqDUawZcb92EHPRV39JI8trLJLlpn0UjWEYQZJ6dVPEJkjcJbeVbxlCkxiIIrym5ljDDTCOv226BX8uEdWlEZSk5jrxt3Js7gNcNJYHlNbjb9KV1Oa_NWFU7AKzVXDJR7ZS-K9OAiAnISbJOviAroCh3vaVP958bxNJu6Cwt_jphUuYEnw", + "width": 4032 + } + ], + "place_id": "ChIJQaVbEAuBhYARTcbgmBM8tVE", + "plus_code": { + "compound_code": "QHJP+3G Civic Center, San Francisco, CA, USA", + "global_code": "849VQHJP+3G" + }, + "rating": 4.6, + "reference": "ChIJQaVbEAuBhYARTcbgmBM8tVE", + "scope": "GOOGLE", + "types": [ + "point_of_interest", + "establishment" + ], + "user_ratings_total": 21, + "vicinity": "1140 Market Street, San Francisco" + }, + { + "business_status": "OPERATIONAL", + "geometry": { + "location": { + "lat": 37.78093459999999, + "lng": -122.4144382 + }, + "viewport": { + "northeast": { + "lat": 37.7822385302915, + "lng": -122.4130778197085 + }, + "southwest": { + "lat": 37.7795405697085, + "lng": -122.4157757802915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/cafe-71.png", + "icon_background_color": "#FF9E67", + "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/cafe_pinlet", + "name": "Paris Cafe", + "opening_hours": { + "open_now": false + }, + "photos": [ + { + "height": 4032, + "html_attributions": [ + "Paris Cafe" + ], + "photo_reference": "AciIO2fMlGoVgo_TLdvq2CENHw2KFOvcDW45EWxcL8DAw7QPnBbPPS0665SVCCKmKdPI9upG7wCidO6UyCCcMGc4gF32SbUAAPa-whL7CHURZfb-9STDUqcrh-HWmP3K7ZmVoPpWHgFxkfsjfls6LzpphMo3DLXw5mdUIiRbg8d8PM0N-mVp-e7MBPMRIPm1t3RCBA3MdO5cBwHrRs2J3XB05ao22l6a-FBtIiaZWKEikHT9DsQnUH4bHgfvM7lPoCSCikwucTQasUYfXPbaNXm8z-LNvR6ZsTcGsOkRKsu5S7k7eEE3jK68GJxd7nV7C3217lyN12VxZ6U", + "width": 3024 + } + ], + "place_id": "ChIJOYG2HACBhYAR51qH-8IsnFM", + "plus_code": { + "compound_code": "QHJP+96 Civic Center, San Francisco, CA, USA", + "global_code": "849VQHJP+96" + }, + "price_level": 2, + "rating": 4.8, + "reference": "ChIJOYG2HACBhYAR51qH-8IsnFM", + "scope": "GOOGLE", + "types": [ + "cafe", + "point_of_interest", + "store", + "food", + "establishment" + ], + "user_ratings_total": 78, + "vicinity": "142 McAllister Street, San Francisco" + }, + { + "geometry": { + "location": { + "lat": 37.7773082, + "lng": -122.4196412 + }, + "viewport": { + "northeast": { + "lat": 37.78237885897592, + "lng": -122.4125122545961 + }, + "southwest": { + "lat": 37.77303595794733, + "lng": -122.4237308429429 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/geocode-71.png", + "icon_background_color": "#7B9EB0", + "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", + "name": "Civic Center", + "photos": [ + { + "height": 2268, + "html_attributions": [ + "Tobias Peyerl" + ], + "photo_reference": "AciIO2cy7yjg95KUbhq9hn7tUsXX0uuUcS8pB9NHPMos5CJwF9b-za_UzQEnJeyopweobag8YKyuK5xbVUhjdgpb-QFhXNknGAD7vs6skcUi4i_2tPQ-ludpZX3_p3upeF2d0Y91HGvucbf6Opj7dKjNgp7gGyY-ZTwhfqo32bmEcu3G_CbTmvbyhuJXocIcJOIXwOM7VVxVB-_3vrcpWPHeV18Y6ilm_atTzkouUvclYwo5i_YInAZ_cNN1DPiNNsK4uHEOR-1wYHjaF8A2G-Y80ieN9G9TxZl6E04wxiiEx3lAYuUuOq4Be5RyMTSDKgv75gvjKmQPvxSD2nVKl8OKxXCWAujxI44xi0Mj_Jr7-K55rwJjTPpIPa-ng72LSvyQ4Er-tjC83O17SFUMNNxE5ixb-xDuARpu3UjB-0pzD8vJJ9BAnwHkUhvDueMMVrrQ7W7BNYw7T4-A-eiznIpS6pft_vc2Kkq3t-CE3-VlZAUC7dSoCiK-Kag77oB2WlIjJltl9dgtlNid2qoGE6nNkWBYlDnxADFBkHDEIeh6jIzqGMcUbr-rtw1H4otL8MjlWf65JpbCAmXifV1rSPqylFatmfp74jIuJSmnODs-lG_-R1eObSQ3oaDi280kJmvX6VOK5XDV", + "width": 4032 + } + ], + "place_id": "ChIJ3eJWtI6AhYAR2ovTWatCF8s", + "reference": "ChIJ3eJWtI6AhYAR2ovTWatCF8s", + "scope": "GOOGLE", + "types": [ + "neighborhood", + "political" + ], + "vicinity": "San Francisco" + } + ], + "status": "OK" +} diff --git a/dimos/mapping/google_maps/fixtures/get_location_context_reverse_geocode.json b/dimos/mapping/google_maps/fixtures/get_location_context_reverse_geocode.json new file mode 100644 index 0000000000..216c02aca9 --- /dev/null +++ b/dimos/mapping/google_maps/fixtures/get_location_context_reverse_geocode.json @@ -0,0 +1,1140 @@ +[ + { + "address_components": [ + { + "long_name": "50", + "short_name": "50", + "types": [ + "street_number" + ] + }, + { + "long_name": "United Nations Plaza", + "short_name": "United Nations Plaza", + "types": [ + "route" + ] + }, + { + "long_name": "Civic Center", + "short_name": "Civic Center", + "types": [ + "neighborhood", + "political" + ] + }, + { + "long_name": "San Francisco", + "short_name": "SF", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "San Francisco County", + "short_name": "San Francisco County", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "California", + "short_name": "CA", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "94102", + "short_name": "94102", + "types": [ + "postal_code" + ] + }, + { + "long_name": "4917", + "short_name": "4917", + "types": [ + "postal_code_suffix" + ] + } + ], + "formatted_address": "50 United Nations Plaza, San Francisco, CA 94102, USA", + "geometry": { + "location": { + "lat": 37.78021, + "lng": -122.4144194 + }, + "location_type": "ROOFTOP", + "viewport": { + "northeast": { + "lat": 37.78155898029149, + "lng": -122.4130704197085 + }, + "southwest": { + "lat": 37.77886101970849, + "lng": -122.4157683802915 + } + } + }, + "navigation_points": [ + { + "location": { + "latitude": 37.7799875, + "longitude": -122.4143728 + }, + "restricted_travel_modes": [ + "DRIVE" + ] + }, + { + "location": { + "latitude": 37.7807662, + "longitude": -122.4145332 + }, + "restricted_travel_modes": [ + "WALK" + ] + } + ], + "place_id": "ChIJp9HdGZuAhYAR9HQeU37hyx0", + "types": [ + "street_address", + "subpremise" + ] + }, + { + "address_components": [ + { + "long_name": "50", + "short_name": "50", + "types": [ + "street_number" + ] + }, + { + "long_name": "Hyde Street", + "short_name": "Hyde St", + "types": [ + "route" + ] + }, + { + "long_name": "Civic Center", + "short_name": "Civic Center", + "types": [ + "neighborhood", + "political" + ] + }, + { + "long_name": "San Francisco", + "short_name": "SF", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "San Francisco County", + "short_name": "San Francisco County", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "California", + "short_name": "CA", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "94102", + "short_name": "94102", + "types": [ + "postal_code" + ] + } + ], + "formatted_address": "50 Hyde St, San Francisco, CA 94102, USA", + "geometry": { + "bounds": { + "northeast": { + "lat": 37.78081540000001, + "lng": -122.4137806 + }, + "southwest": { + "lat": 37.7800522, + "lng": -122.415187 + } + }, + "location": { + "lat": 37.7805991, + "lng": -122.4147826 + }, + "location_type": "ROOFTOP", + "viewport": { + "northeast": { + "lat": 37.78178278029151, + "lng": -122.4131348197085 + }, + "southwest": { + "lat": 37.77908481970851, + "lng": -122.4158327802915 + } + } + }, + "navigation_points": [ + { + "location": { + "latitude": 37.7799291, + "longitude": -122.4143652 + }, + "restricted_travel_modes": [ + "WALK" + ] + } + ], + "place_id": "ChIJ7Q9FGZuAhYARSovheSUzVeE", + "types": [ + "premise", + "street_address" + ] + }, + { + "address_components": [ + { + "long_name": "Civic Center/UN Plaza BART Station", + "short_name": "Civic Center/UN Plaza BART Station", + "types": [ + "establishment", + "point_of_interest", + "transit_station" + ] + }, + { + "long_name": "San Francisco", + "short_name": "SF", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "San Francisco County", + "short_name": "San Francisco County", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "California", + "short_name": "CA", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "Civic Center/UN Plaza BART Station, San Francisco, CA, USA", + "geometry": { + "location": { + "lat": 37.779756, + "lng": -122.41415 + }, + "location_type": "GEOMETRIC_CENTER", + "viewport": { + "northeast": { + "lat": 37.7811049802915, + "lng": -122.4128010197085 + }, + "southwest": { + "lat": 37.7784070197085, + "lng": -122.4154989802915 + } + } + }, + "navigation_points": [ + { + "location": { + "latitude": 37.7797284, + "longitude": -122.4142112 + }, + "restricted_travel_modes": [ + "DRIVE" + ] + }, + { + "location": { + "latitude": 37.779631, + "longitude": -122.4150367 + }, + "restricted_travel_modes": [ + "WALK" + ] + }, + { + "location": { + "latitude": 37.7795262, + "longitude": -122.4138289 + }, + "restricted_travel_modes": [ + "WALK" + ] + }, + { + "location": { + "latitude": 37.7796804, + "longitude": -122.4136322 + } + }, + { + "location": { + "latitude": 37.7804986, + "longitude": -122.4129601 + }, + "restricted_travel_modes": [ + "DRIVE" + ] + }, + { + "location": { + "latitude": 37.7788771, + "longitude": -122.414549 + } + } + ], + "place_id": "ChIJK0jeP5uAhYARcxPNUpvfc7A", + "plus_code": { + "compound_code": "QHHP+W8 Civic Center, San Francisco, CA, USA", + "global_code": "849VQHHP+W8" + }, + "types": [ + "establishment", + "point_of_interest", + "transit_station" + ] + }, + { + "address_components": [ + { + "long_name": "1-99", + "short_name": "1-99", + "types": [ + "street_number" + ] + }, + { + "long_name": "United Nations Plaza", + "short_name": "United Nations Plz", + "types": [ + "route" + ] + }, + { + "long_name": "Civic Center", + "short_name": "Civic Center", + "types": [ + "neighborhood", + "political" + ] + }, + { + "long_name": "San Francisco", + "short_name": "SF", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "San Francisco County", + "short_name": "San Francisco County", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "California", + "short_name": "CA", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "94102", + "short_name": "94102", + "types": [ + "postal_code" + ] + }, + { + "long_name": "7402", + "short_name": "7402", + "types": [ + "postal_code_suffix" + ] + } + ], + "formatted_address": "1-99 United Nations Plz, San Francisco, CA 94102, USA", + "geometry": { + "location": { + "lat": 37.779675, + "lng": -122.41408 + }, + "location_type": "ROOFTOP", + "viewport": { + "northeast": { + "lat": 37.78102398029149, + "lng": -122.4127310197085 + }, + "southwest": { + "lat": 37.7783260197085, + "lng": -122.4154289802915 + } + } + }, + "navigation_points": [ + { + "location": { + "latitude": 37.7796351, + "longitude": -122.4141273 + }, + "restricted_travel_modes": [ + "DRIVE" + ] + }, + { + "location": { + "latitude": 37.7796283, + "longitude": -122.4138453 + }, + "restricted_travel_modes": [ + "WALK" + ] + } + ], + "place_id": "ChIJD8AMQJuAhYARgQPDkMbiVZE", + "plus_code": { + "compound_code": "QHHP+V9 Civic Center, San Francisco, CA, USA", + "global_code": "849VQHHP+V9" + }, + "types": [ + "street_address" + ] + }, + { + "address_components": [ + { + "long_name": "QHJP+36", + "short_name": "QHJP+36", + "types": [ + "plus_code" + ] + }, + { + "long_name": "Civic Center", + "short_name": "Civic Center", + "types": [ + "neighborhood", + "political" + ] + }, + { + "long_name": "San Francisco", + "short_name": "SF", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "San Francisco County", + "short_name": "San Francisco County", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "California", + "short_name": "CA", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "94102", + "short_name": "94102", + "types": [ + "postal_code" + ] + } + ], + "formatted_address": "QHJP+36 Civic Center, San Francisco, CA, USA", + "geometry": { + "bounds": { + "northeast": { + "lat": 37.78025, + "lng": -122.414375 + }, + "southwest": { + "lat": 37.780125, + "lng": -122.4145 + } + }, + "location": { + "lat": 37.7801776, + "lng": -122.4144952 + }, + "location_type": "GEOMETRIC_CENTER", + "viewport": { + "northeast": { + "lat": 37.78153648029149, + "lng": -122.4130885197085 + }, + "southwest": { + "lat": 37.77883851970849, + "lng": -122.4157864802915 + } + } + }, + "place_id": "GhIJMIkO3NzjQkARVhbgFoeaXsA", + "plus_code": { + "compound_code": "QHJP+36 Civic Center, San Francisco, CA, USA", + "global_code": "849VQHJP+36" + }, + "types": [ + "plus_code" + ] + }, + { + "address_components": [ + { + "long_name": "39", + "short_name": "39", + "types": [ + "street_number" + ] + }, + { + "long_name": "Hyde Street", + "short_name": "Hyde St", + "types": [ + "route" + ] + }, + { + "long_name": "Civic Center", + "short_name": "Civic Center", + "types": [ + "neighborhood", + "political" + ] + }, + { + "long_name": "San Francisco", + "short_name": "SF", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "San Francisco County", + "short_name": "San Francisco County", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "California", + "short_name": "CA", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "94102", + "short_name": "94102", + "types": [ + "postal_code" + ] + } + ], + "formatted_address": "39 Hyde St, San Francisco, CA 94102, USA", + "geometry": { + "location": { + "lat": 37.7800157, + "lng": -122.4151997 + }, + "location_type": "RANGE_INTERPOLATED", + "viewport": { + "northeast": { + "lat": 37.7813646802915, + "lng": -122.4138507197085 + }, + "southwest": { + "lat": 37.7786667197085, + "lng": -122.4165486802915 + } + } + }, + "place_id": "EigzOSBIeWRlIFN0LCBTYW4gRnJhbmNpc2NvLCBDQSA5NDEwMiwgVVNBIhoSGAoUChIJNcWgBpuAhYARvBLCxkfib9AQJw", + "types": [ + "street_address" + ] + }, + { + "address_components": [ + { + "long_name": "47-35", + "short_name": "47-35", + "types": [ + "street_number" + ] + }, + { + "long_name": "Hyde Street", + "short_name": "Hyde St", + "types": [ + "route" + ] + }, + { + "long_name": "Civic Center", + "short_name": "Civic Center", + "types": [ + "neighborhood", + "political" + ] + }, + { + "long_name": "San Francisco", + "short_name": "SF", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "San Francisco County", + "short_name": "San Francisco County", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "California", + "short_name": "CA", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "94102", + "short_name": "94102", + "types": [ + "postal_code" + ] + } + ], + "formatted_address": "47-35 Hyde St, San Francisco, CA 94102, USA", + "geometry": { + "bounds": { + "northeast": { + "lat": 37.7803333, + "lng": -122.4151588 + }, + "southwest": { + "lat": 37.7798162, + "lng": -122.4152658 + } + }, + "location": { + "lat": 37.7800748, + "lng": -122.415212 + }, + "location_type": "GEOMETRIC_CENTER", + "viewport": { + "northeast": { + "lat": 37.7814237302915, + "lng": -122.4138633197085 + }, + "southwest": { + "lat": 37.7787257697085, + "lng": -122.4165612802915 + } + } + }, + "place_id": "ChIJNcWgBpuAhYARvBLCxkfib9A", + "types": [ + "route" + ] + }, + { + "address_components": [ + { + "long_name": "Civic Center", + "short_name": "Civic Center", + "types": [ + "neighborhood", + "political" + ] + }, + { + "long_name": "San Francisco", + "short_name": "SF", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "San Francisco County", + "short_name": "San Francisco County", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "California", + "short_name": "CA", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "94102", + "short_name": "94102", + "types": [ + "postal_code" + ] + } + ], + "formatted_address": "Civic Center, San Francisco, CA 94102, USA", + "geometry": { + "bounds": { + "northeast": { + "lat": 37.7823789, + "lng": -122.4125123 + }, + "southwest": { + "lat": 37.773036, + "lng": -122.4237308 + } + }, + "location": { + "lat": 37.7773082, + "lng": -122.4196412 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 37.7823789, + "lng": -122.4125123 + }, + "southwest": { + "lat": 37.773036, + "lng": -122.4237308 + } + } + }, + "place_id": "ChIJ3eJWtI6AhYAR2ovTWatCF8s", + "types": [ + "neighborhood", + "political" + ] + }, + { + "address_components": [ + { + "long_name": "94102", + "short_name": "94102", + "types": [ + "postal_code" + ] + }, + { + "long_name": "San Francisco", + "short_name": "SF", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "San Francisco County", + "short_name": "San Francisco County", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "California", + "short_name": "CA", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "San Francisco, CA 94102, USA", + "geometry": { + "bounds": { + "northeast": { + "lat": 37.789226, + "lng": -122.4034491 + }, + "southwest": { + "lat": 37.7694409, + "lng": -122.429849 + } + }, + "location": { + "lat": 37.7786871, + "lng": -122.4212424 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 37.789226, + "lng": -122.4034491 + }, + "southwest": { + "lat": 37.7694409, + "lng": -122.429849 + } + } + }, + "place_id": "ChIJs88qnZmAhYARk8u-7t1Sc2g", + "types": [ + "postal_code" + ] + }, + { + "address_components": [ + { + "long_name": "San Francisco County", + "short_name": "San Francisco County", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "San Francisco", + "short_name": "SF", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "California", + "short_name": "CA", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "San Francisco County, San Francisco, CA, USA", + "geometry": { + "bounds": { + "northeast": { + "lat": 37.929824, + "lng": -122.28178 + }, + "southwest": { + "lat": 37.63983, + "lng": -123.1327983 + } + }, + "location": { + "lat": 37.7618219, + "lng": -122.5146439 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 37.929824, + "lng": -122.28178 + }, + "southwest": { + "lat": 37.63983, + "lng": -123.1327983 + } + } + }, + "place_id": "ChIJIQBpAG2ahYARUksNqd0_1h8", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "address_components": [ + { + "long_name": "San Francisco", + "short_name": "SF", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "San Francisco County", + "short_name": "San Francisco County", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "California", + "short_name": "CA", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "San Francisco, CA, USA", + "geometry": { + "bounds": { + "northeast": { + "lat": 37.929824, + "lng": -122.28178 + }, + "southwest": { + "lat": 37.6398299, + "lng": -123.1328145 + } + }, + "location": { + "lat": 37.7749295, + "lng": -122.4194155 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 37.929824, + "lng": -122.28178 + }, + "southwest": { + "lat": 37.6398299, + "lng": -123.1328145 + } + } + }, + "place_id": "ChIJIQBpAG2ahYAR_6128GcTUEo", + "types": [ + "locality", + "political" + ] + }, + { + "address_components": [ + { + "long_name": "California", + "short_name": "CA", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "California, USA", + "geometry": { + "bounds": { + "northeast": { + "lat": 42.009503, + "lng": -114.131211 + }, + "southwest": { + "lat": 32.52950810000001, + "lng": -124.482003 + } + }, + "location": { + "lat": 36.778261, + "lng": -119.4179324 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 42.009503, + "lng": -114.131211 + }, + "southwest": { + "lat": 32.52950810000001, + "lng": -124.482003 + } + } + }, + "place_id": "ChIJPV4oX_65j4ARVW8IJ6IJUYs", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "address_components": [ + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "United States", + "geometry": { + "bounds": { + "northeast": { + "lat": 74.071038, + "lng": -66.885417 + }, + "southwest": { + "lat": 18.7763, + "lng": 166.9999999 + } + }, + "location": { + "lat": 38.7945952, + "lng": -106.5348379 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 74.071038, + "lng": -66.885417 + }, + "southwest": { + "lat": 18.7763, + "lng": 166.9999999 + } + } + }, + "place_id": "ChIJCzYy5IS16lQRQrfeQ5K5Oxw", + "types": [ + "country", + "political" + ] + } +] diff --git a/dimos/mapping/google_maps/fixtures/get_position.json b/dimos/mapping/google_maps/fixtures/get_position.json new file mode 100644 index 0000000000..410d2add2a --- /dev/null +++ b/dimos/mapping/google_maps/fixtures/get_position.json @@ -0,0 +1,141 @@ +[ + { + "address_components": [ + { + "long_name": "Golden Gate Bridge", + "short_name": "Golden Gate Bridge", + "types": [ + "establishment", + "point_of_interest", + "tourist_attraction" + ] + }, + { + "long_name": "Golden Gate Bridge", + "short_name": "Golden Gate Brg", + "types": [ + "route" + ] + }, + { + "long_name": "San Francisco", + "short_name": "SF", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "San Francisco County", + "short_name": "San Francisco County", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "California", + "short_name": "CA", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "Golden Gate Bridge, Golden Gate Brg, San Francisco, CA, USA", + "geometry": { + "location": { + "lat": 37.8199109, + "lng": -122.4785598 + }, + "location_type": "GEOMETRIC_CENTER", + "viewport": { + "northeast": { + "lat": 37.8324583, + "lng": -122.4756692 + }, + "southwest": { + "lat": 37.8075604, + "lng": -122.4810829 + } + } + }, + "navigation_points": [ + { + "location": { + "latitude": 37.8075604, + "longitude": -122.4756957 + } + }, + { + "location": { + "latitude": 37.80756119999999, + "longitude": -122.4756922 + }, + "restricted_travel_modes": [ + "WALK" + ] + }, + { + "location": { + "latitude": 37.8324279, + "longitude": -122.4810829 + } + }, + { + "location": { + "latitude": 37.8324382, + "longitude": -122.4810669 + }, + "restricted_travel_modes": [ + "WALK" + ] + }, + { + "location": { + "latitude": 37.8083987, + "longitude": -122.4765643 + }, + "restricted_travel_modes": [ + "DRIVE" + ] + }, + { + "location": { + "latitude": 37.8254712, + "longitude": -122.4791469 + }, + "restricted_travel_modes": [ + "DRIVE" + ] + }, + { + "location": { + "latitude": 37.8321189, + "longitude": -122.4808249 + }, + "restricted_travel_modes": [ + "DRIVE" + ] + } + ], + "place_id": "ChIJw____96GhYARCVVwg5cT7c0", + "plus_code": { + "compound_code": "RG9C+XH Presidio of San Francisco, San Francisco, CA", + "global_code": "849VRG9C+XH" + }, + "types": [ + "establishment", + "point_of_interest", + "tourist_attraction" + ] + } +] diff --git a/dimos/mapping/google_maps/fixtures/get_position_with_places.json b/dimos/mapping/google_maps/fixtures/get_position_with_places.json new file mode 100644 index 0000000000..d471a8368a --- /dev/null +++ b/dimos/mapping/google_maps/fixtures/get_position_with_places.json @@ -0,0 +1,53 @@ +{ + "html_attributions": [], + "results": [ + { + "business_status": "OPERATIONAL", + "formatted_address": "Golden Gate Brg, San Francisco, CA, United States", + "geometry": { + "location": { + "lat": 37.8199109, + "lng": -122.4785598 + }, + "viewport": { + "northeast": { + "lat": 37.84490724999999, + "lng": -122.47296235 + }, + "southwest": { + "lat": 37.79511145000001, + "lng": -122.48378975 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/generic_business-71.png", + "icon_background_color": "#7B9EB0", + "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", + "name": "Golden Gate Bridge", + "photos": [ + { + "height": 12240, + "html_attributions": [ + "Jitesh Patil" + ], + "photo_reference": "AciIO2dcF-W6JeWe01lyR39crDHHon3awa5LlBNNhxAZcAExA3sTr33iFa8HjDgPPfdNrl3C-0Bzqp2qEndFz3acXtm1kmj7puXUOtO48-Qmovp9Nvi5k3XJVbIEPYYRCXOshrYQ1od2tHe-MBkvFNxsg4uNByEbJxkstLLTuEOmSbCEx53EQfuJoxbPQgRGphAPDFkTeiCODXd7KzdL9-2GvVYTrGl_IK-AIds1-UYwWJPOi1mkM-iXFVoVm0R1LOgt-ydhnAaRFQPzOlz9Oezc0kDiuxvzjTO4mgeY79Nqcxq2osBqYGyJTLINYfNphZHzncxWqpWXP_mvQt77YaW368RGbBGDrHubXHJBkj7sdru0N1-qf5Q28rsxCSI5yyNsHm8zFmNWm1PlWA_LItL5LpoxG9Xkuuhuvv3XjWtBs5hnHxNDHP4jbJinWz2DPd9IPxHH-BAfwfJGdtgW1juBAEDi8od5KP95Drt8e9XOaG6I5UIeJnvUqq4Q1McAiVx5rVn7FGwu3NsTAeeS4FCKy2Ql_YoQpcqzRO45w8tI4DqFd8F19pZHw3t7p1t7DwmzAMzIS_17_2aScA", + "width": 16320 + } + ], + "place_id": "ChIJw____96GhYARCVVwg5cT7c0", + "plus_code": { + "compound_code": "RG9C+XH Presidio of San Francisco, San Francisco, CA, USA", + "global_code": "849VRG9C+XH" + }, + "rating": 4.8, + "reference": "ChIJw____96GhYARCVVwg5cT7c0", + "types": [ + "tourist_attraction", + "point_of_interest", + "establishment" + ], + "user_ratings_total": 83799 + } + ], + "status": "OK" +} diff --git a/dimos/mapping/google_maps/google_maps.py b/dimos/mapping/google_maps/google_maps.py new file mode 100644 index 0000000000..e75de042f4 --- /dev/null +++ b/dimos/mapping/google_maps/google_maps.py @@ -0,0 +1,192 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import googlemaps + +from dimos.mapping.google_maps.types import ( + Coordinates, + LocationContext, + NearbyPlace, + PlacePosition, + Position, +) +from dimos.mapping.types import LatLon +from dimos.mapping.utils.distance import distance_in_meters +from dimos.utils.logging_config import setup_logger + +logger = setup_logger(__file__) + + +class GoogleMaps: + _client: googlemaps.Client + _max_nearby_places: int + + def __init__(self, api_key: str | None = None) -> None: + api_key = api_key or os.environ.get("GOOGLE_MAPS_API_KEY") + if not api_key: + raise ValueError("GOOGLE_MAPS_API_KEY environment variable not set") + self._client = googlemaps.Client(key=api_key) + self._max_nearby_places = 6 + + def get_position(self, query: str, current_location: LatLon | None = None) -> Position | None: + # Use location bias if current location is provided + if current_location: + geocode_results = self._client.geocode( + query, + bounds={ + "southwest": { + "lat": current_location.lat - 0.5, + "lng": current_location.lon - 0.5, + }, + "northeast": { + "lat": current_location.lat + 0.5, + "lng": current_location.lon + 0.5, + }, + }, + ) + else: + geocode_results = self._client.geocode(query) + + if not geocode_results: + return None + + result = geocode_results[0] + + location = result["geometry"]["location"] + + return Position( + lat=location["lat"], + lon=location["lng"], + description=result["formatted_address"], + ) + + def get_position_with_places( + self, query: str, current_location: LatLon | None = None + ) -> PlacePosition | None: + # Use location bias if current location is provided + if current_location: + places_results = self._client.places( + query, + location=(current_location.lat, current_location.lon), + radius=50000, # 50km radius for location bias + ) + else: + places_results = self._client.places(query) + + if not places_results or "results" not in places_results: + return None + + results = places_results["results"] + if not results: + return None + + place = results[0] + + location = place["geometry"]["location"] + + return PlacePosition( + lat=location["lat"], + lon=location["lng"], + description=place.get("name", ""), + address=place.get("formatted_address", ""), + types=place.get("types", []), + ) + + def get_location_context( + self, latlon: LatLon, radius: int = 100, n_nearby_places: int = 6 + ) -> LocationContext | None: + reverse_geocode_results = self._client.reverse_geocode((latlon.lat, latlon.lon)) + + if not reverse_geocode_results: + return None + + result = reverse_geocode_results[0] + + # Extract address components + components = {} + for component in result.get("address_components", []): + types = component.get("types", []) + if "street_number" in types: + components["street_number"] = component["long_name"] + elif "route" in types: + components["street"] = component["long_name"] + elif "neighborhood" in types: + components["neighborhood"] = component["long_name"] + elif "locality" in types: + components["locality"] = component["long_name"] + elif "administrative_area_level_1" in types: + components["admin_area"] = component["long_name"] + elif "country" in types: + components["country"] = component["long_name"] + elif "postal_code" in types: + components["postal_code"] = component["long_name"] + + nearby_places, place_types_summary = self._get_nearby_places( + latlon, radius, n_nearby_places + ) + + return LocationContext( + formatted_address=result.get("formatted_address", ""), + street_number=components.get("street_number", ""), + street=components.get("street", ""), + neighborhood=components.get("neighborhood", ""), + locality=components.get("locality", ""), + admin_area=components.get("admin_area", ""), + country=components.get("country", ""), + postal_code=components.get("postal_code", ""), + nearby_places=nearby_places, + place_types_summary=place_types_summary or "No specific landmarks nearby", + coordinates=Coordinates(lat=latlon.lat, lon=latlon.lon), + ) + + def _get_nearby_places( + self, latlon: LatLon, radius: int, n_nearby_places: int + ) -> tuple[list[NearbyPlace], str]: + nearby_places = [] + place_types_count: dict[str, int] = {} + + places_nearby = self._client.places_nearby(location=(latlon.lat, latlon.lon), radius=radius) + + if places_nearby and "results" in places_nearby: + for place in places_nearby["results"][:n_nearby_places]: + place_lat = place["geometry"]["location"]["lat"] + place_lon = place["geometry"]["location"]["lng"] + place_latlon = LatLon(lat=place_lat, lon=place_lon) + + place_info = NearbyPlace( + name=place.get("name", ""), + types=place.get("types", []), + vicinity=place.get("vicinity", ""), + distance=round(distance_in_meters(place_latlon, latlon), 1), + ) + + nearby_places.append(place_info) + + for place_type in place.get("types", []): + if place_type not in ["point_of_interest", "establishment"]: + place_types_count[place_type] = place_types_count.get(place_type, 0) + 1 + nearby_places.sort(key=lambda x: x.distance) + + place_types_summary = ", ".join( + [ + f"{count} {ptype.replace('_', ' ')}{'s' if count > 1 else ''}" + for ptype, count in sorted( + place_types_count.items(), key=lambda x: x[1], reverse=True + )[:5] + ] + ) + + return nearby_places, place_types_summary diff --git a/dimos/mapping/google_maps/test_google_maps.py b/dimos/mapping/google_maps/test_google_maps.py new file mode 100644 index 0000000000..52e1493ec3 --- /dev/null +++ b/dimos/mapping/google_maps/test_google_maps.py @@ -0,0 +1,139 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from dimos.mapping.types import LatLon + + +def test_get_position(maps_client, maps_fixture) -> None: + maps_client._client.geocode.return_value = maps_fixture("get_position.json") + + res = maps_client.get_position("golden gate bridge") + + assert res.model_dump() == { + "description": "Golden Gate Bridge, Golden Gate Brg, San Francisco, CA, USA", + "lat": 37.8199109, + "lon": -122.4785598, + } + + +def test_get_position_with_places(maps_client, maps_fixture) -> None: + maps_client._client.places.return_value = maps_fixture("get_position_with_places.json") + + res = maps_client.get_position_with_places("golden gate bridge") + + assert res.model_dump() == { + "address": "Golden Gate Brg, San Francisco, CA, United States", + "description": "Golden Gate Bridge", + "lat": 37.8199109, + "lon": -122.4785598, + "types": [ + "tourist_attraction", + "point_of_interest", + "establishment", + ], + } + + +def test_get_location_context(maps_client, maps_fixture) -> None: + maps_client._client.reverse_geocode.return_value = maps_fixture( + "get_location_context_reverse_geocode.json" + ) + maps_client._client.places_nearby.return_value = maps_fixture( + "get_location_context_places_nearby.json" + ) + + res = maps_client.get_location_context(LatLon(lat=37.78017758753598, lon=-122.4144951709186)) + + assert res.model_dump() == { + "admin_area": "California", + "coordinates": { + "lat": 37.78017758753598, + "lon": -122.4144951709186, + }, + "country": "United States", + "formatted_address": "50 United Nations Plaza, San Francisco, CA 94102, USA", + "locality": "San Francisco", + "nearby_places": [ + { + "distance": 9.3, + "name": "U.S. General Services Administration - Pacific Rim Region", + "types": [ + "point_of_interest", + "establishment", + ], + "vicinity": "50 United Nations Plaza, San Francisco", + }, + { + "distance": 14.0, + "name": "Federal Office Building", + "types": [ + "point_of_interest", + "establishment", + ], + "vicinity": "50 United Nations Plaza, San Francisco", + }, + { + "distance": 35.7, + "name": "UN Plaza", + "types": [ + "city_hall", + "point_of_interest", + "local_government_office", + "establishment", + ], + "vicinity": "355 McAllister Street, San Francisco", + }, + { + "distance": 92.7, + "name": "McAllister Market & Deli", + "types": [ + "liquor_store", + "atm", + "grocery_or_supermarket", + "finance", + "point_of_interest", + "food", + "store", + "establishment", + ], + "vicinity": "136 McAllister Street, San Francisco", + }, + { + "distance": 95.9, + "name": "Civic Center / UN Plaza", + "types": [ + "subway_station", + "transit_station", + "point_of_interest", + "establishment", + ], + "vicinity": "1150 Market Street, San Francisco", + }, + { + "distance": 726.3, + "name": "San Francisco", + "types": [ + "locality", + "political", + ], + "vicinity": "San Francisco", + }, + ], + "neighborhood": "Civic Center", + "place_types_summary": "1 locality, 1 political, 1 subway station, 1 transit station, 1 city hall", + "postal_code": "94102", + "street": "United Nations Plaza", + "street_number": "50", + } diff --git a/dimos/mapping/google_maps/types.py b/dimos/mapping/google_maps/types.py new file mode 100644 index 0000000000..67713f55ee --- /dev/null +++ b/dimos/mapping/google_maps/types.py @@ -0,0 +1,66 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from pydantic import BaseModel + + +class Coordinates(BaseModel): + """GPS coordinates.""" + + lat: float + lon: float + + +class Position(BaseModel): + """Basic position information from geocoding.""" + + lat: float + lon: float + description: str + + +class PlacePosition(BaseModel): + """Position with places API details.""" + + lat: float + lon: float + description: str + address: str + types: list[str] + + +class NearbyPlace(BaseModel): + """Information about a nearby place.""" + + name: str + types: list[str] + distance: float + vicinity: str + + +class LocationContext(BaseModel): + """Contextual information about a location.""" + + formatted_address: str | None = None + street_number: str | None = None + street: str | None = None + neighborhood: str | None = None + locality: str | None = None + admin_area: str | None = None + country: str | None = None + postal_code: str | None = None + nearby_places: list[NearbyPlace] = [] + place_types_summary: str | None = None + coordinates: Coordinates diff --git a/dimos/mapping/osm/README.md b/dimos/mapping/osm/README.md new file mode 100644 index 0000000000..be3d4a3ee2 --- /dev/null +++ b/dimos/mapping/osm/README.md @@ -0,0 +1,43 @@ +# OpenStreetMap (OSM) + +This provides functionality to fetch and work with OpenStreetMap tiles, including coordinate conversions and location-based VLM queries. + +## Getting a MapImage + +```python +map_image = get_osm_map(LatLon(lat=..., lon=...), zoom_level=18, n_tiles=4)` +``` + +OSM tiles are 256x256 pixels so with 4 tiles you get a 1024x1024 map. + +You can translate pixel coordinates on the map to GPS location and back. + +```python +>>> map_image.pixel_to_latlon((300, 500)) +LatLon(lat=43.58571248, lon=12.23423511) +>>> map_image.latlon_to_pixel(LatLon(lat=43.58571248, lon=12.23423511)) +(300, 500) +``` + +## CurrentLocationMap + +This class maintains an appropriate context map for your current location so you can VLM queries. + +You have to update it with your current location and when you stray too far from the center it fetches a new map. + +```python +curr_map = CurrentLocationMap(QwenVlModel()) + +# Set your latest position. +curr_map.update_position(LatLon(lat=..., lon=...)) + +# If you want to get back a GPS position of a feature (Qwen gets your current position). +curr_map.query_for_one_position('Where is the closest farmacy?') +# Returns: +# LatLon(lat=..., lon=...) + +# If you also want to get back a description of the result. +curr_map.query_for_one_position_and_context('Where is the closest pharmacy?') +# Returns: +# (LatLon(lat=..., lon=...), "Lloyd's Pharmacy on Main Street") +``` diff --git a/dimos/types/__init__.py b/dimos/mapping/osm/__init__.py similarity index 100% rename from dimos/types/__init__.py rename to dimos/mapping/osm/__init__.py diff --git a/dimos/mapping/osm/current_location_map.py b/dimos/mapping/osm/current_location_map.py new file mode 100644 index 0000000000..88942935af --- /dev/null +++ b/dimos/mapping/osm/current_location_map.py @@ -0,0 +1,74 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from dimos.mapping.osm.osm import MapImage, get_osm_map +from dimos.mapping.osm.query import query_for_one_position, query_for_one_position_and_context +from dimos.mapping.types import LatLon +from dimos.models.vl.base import VlModel +from dimos.utils.logging_config import setup_logger + +logger = setup_logger(__file__) + + +class CurrentLocationMap: + _vl_model: VlModel + _position: LatLon | None + _map_image: MapImage | None + + def __init__(self, vl_model: VlModel) -> None: + self._vl_model = vl_model + self._position = None + self._map_image = None + self._zoom_level = 19 + self._n_tiles = 6 + # What ratio of the width is considered the center. 1.0 means the entire map is the center. + self._center_width = 0.4 + + def update_position(self, position: LatLon) -> None: + self._position = position + + def query_for_one_position(self, query: str) -> LatLon | None: + return query_for_one_position(self._vl_model, self._get_current_map(), query) + + def query_for_one_position_and_context( + self, query: str, robot_position: LatLon + ) -> tuple[LatLon, str] | None: + return query_for_one_position_and_context( + self._vl_model, self._get_current_map(), query, robot_position + ) + + def _get_current_map(self): + if not self._position: + raise ValueError("Current position has not been set.") + + if not self._map_image or self._position_is_too_far_off_center(): + self._fetch_new_map() + return self._map_image + + return self._map_image + + def _fetch_new_map(self) -> None: + logger.info( + f"Getting a new OSM map, position={self._position}, zoom={self._zoom_level} n_tiles={self._n_tiles}" + ) + self._map_image = get_osm_map(self._position, self._zoom_level, self._n_tiles) + + def _position_is_too_far_off_center(self) -> bool: + x, y = self._map_image.latlon_to_pixel(self._position) + width = self._map_image.image.width + size_min = width * (0.5 - self._center_width / 2) + size_max = width * (0.5 + self._center_width / 2) + + return x < size_min or x > size_max or y < size_min or y > size_max diff --git a/dimos/mapping/osm/demo_osm.py b/dimos/mapping/osm/demo_osm.py new file mode 100644 index 0000000000..cf907378f3 --- /dev/null +++ b/dimos/mapping/osm/demo_osm.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dotenv import load_dotenv +from reactivex import interval + +from dimos.agents2.agent import llm_agent +from dimos.agents2.cli.human import human_input +from dimos.agents2.skills.osm import osm_skill +from dimos.agents2.system_prompt import get_system_prompt +from dimos.core.blueprints import autoconnect +from dimos.core.module import Module +from dimos.core.stream import Out +from dimos.mapping.types import LatLon + +load_dotenv() + + +class DemoRobot(Module): + gps_location: Out[LatLon] = None + + def start(self) -> None: + super().start() + self._disposables.add(interval(1.0).subscribe(lambda _: self._publish_gps_location())) + + def stop(self) -> None: + super().stop() + + def _publish_gps_location(self) -> None: + self.gps_location.publish(LatLon(lat=37.78092426217621, lon=-122.40682866540769)) + + +demo_robot = DemoRobot.blueprint + + +demo_osm = autoconnect( + demo_robot(), + osm_skill(), + human_input(), + llm_agent(system_prompt=get_system_prompt()), +) diff --git a/dimos/mapping/osm/osm.py b/dimos/mapping/osm/osm.py new file mode 100644 index 0000000000..9f967046f6 --- /dev/null +++ b/dimos/mapping/osm/osm.py @@ -0,0 +1,183 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from concurrent.futures import ThreadPoolExecutor, as_completed +from dataclasses import dataclass +import io +import math + +import numpy as np +from PIL import Image as PILImage +import requests + +from dimos.mapping.types import ImageCoord, LatLon +from dimos.msgs.sensor_msgs import Image, ImageFormat + + +@dataclass(frozen=True) +class MapImage: + image: Image + position: LatLon + zoom_level: int + n_tiles: int + + def pixel_to_latlon(self, position: ImageCoord) -> LatLon: + """Convert pixel coordinates to latitude/longitude. + + Args: + position: (x, y) pixel coordinates in the image + + Returns: + LatLon object with the corresponding latitude and longitude + """ + pixel_x, pixel_y = position + tile_size = 256 + + # Get the center tile coordinates + center_tile_x, center_tile_y = _lat_lon_to_tile( + self.position.lat, self.position.lon, self.zoom_level + ) + + # Calculate the actual top-left tile indices (integers) + start_tile_x = int(center_tile_x - self.n_tiles / 2.0) + start_tile_y = int(center_tile_y - self.n_tiles / 2.0) + + # Convert pixel position to exact tile coordinates + tile_x = start_tile_x + pixel_x / tile_size + tile_y = start_tile_y + pixel_y / tile_size + + # Convert tile coordinates to lat/lon + n = 2**self.zoom_level + lon = tile_x / n * 360.0 - 180.0 + lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * tile_y / n))) + lat = math.degrees(lat_rad) + + return LatLon(lat=lat, lon=lon) + + def latlon_to_pixel(self, position: LatLon) -> ImageCoord: + """Convert latitude/longitude to pixel coordinates. + + Args: + position: LatLon object with latitude and longitude + + Returns: + (x, y) pixel coordinates in the image + Note: Can return negative values if position is outside the image bounds + """ + tile_size = 256 + + # Convert the input lat/lon to tile coordinates + tile_x, tile_y = _lat_lon_to_tile(position.lat, position.lon, self.zoom_level) + + # Get the center tile coordinates + center_tile_x, center_tile_y = _lat_lon_to_tile( + self.position.lat, self.position.lon, self.zoom_level + ) + + # Calculate the actual top-left tile indices (integers) + start_tile_x = int(center_tile_x - self.n_tiles / 2.0) + start_tile_y = int(center_tile_y - self.n_tiles / 2.0) + + # Calculate pixel position relative to top-left corner + pixel_x = int((tile_x - start_tile_x) * tile_size) + pixel_y = int((tile_y - start_tile_y) * tile_size) + + return (pixel_x, pixel_y) + + +def _lat_lon_to_tile(lat: float, lon: float, zoom: int) -> tuple[float, float]: + """Convert latitude/longitude to tile coordinates at given zoom level.""" + n = 2**zoom + x_tile = (lon + 180.0) / 360.0 * n + lat_rad = math.radians(lat) + y_tile = (1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n + return x_tile, y_tile + + +def _download_tile( + args: tuple[int, int, int, int, int], +) -> tuple[int, int, PILImage.Image | None]: + """Download a single tile. + + Args: + args: Tuple of (row, col, tile_x, tile_y, zoom_level) + + Returns: + Tuple of (row, col, tile_image or None if failed) + """ + row, col, tile_x, tile_y, zoom_level = args + url = f"https://tile.openstreetmap.org/{zoom_level}/{tile_x}/{tile_y}.png" + headers = {"User-Agent": "Dimos OSM Client/1.0"} + + try: + response = requests.get(url, headers=headers, timeout=10) + response.raise_for_status() + tile_img = PILImage.open(io.BytesIO(response.content)) + return row, col, tile_img + except Exception: + return row, col, None + + +def get_osm_map(position: LatLon, zoom_level: int = 18, n_tiles: int = 4) -> MapImage: + """ + Tiles are always 256x256 pixels. With n_tiles=4, this should produce a 1024x1024 image. + Downloads tiles in parallel with a maximum of 5 concurrent downloads. + + Args: + position (LatLon): center position + zoom_level (int, optional): Defaults to 18. + n_tiles (int, optional): generate a map of n_tiles by n_tiles. + """ + center_x, center_y = _lat_lon_to_tile(position.lat, position.lon, zoom_level) + + start_x = int(center_x - n_tiles / 2.0) + start_y = int(center_y - n_tiles / 2.0) + + tile_size = 256 + output_size = tile_size * n_tiles + output_img = PILImage.new("RGB", (output_size, output_size)) + + n_failed_tiles = 0 + + # Prepare all tile download tasks + download_tasks = [] + for row in range(n_tiles): + for col in range(n_tiles): + tile_x = start_x + col + tile_y = start_y + row + download_tasks.append((row, col, tile_x, tile_y, zoom_level)) + + # Download tiles in parallel with max 5 workers + with ThreadPoolExecutor(max_workers=5) as executor: + futures = [executor.submit(_download_tile, task) for task in download_tasks] + + for future in as_completed(futures): + row, col, tile_img = future.result() + + if tile_img is not None: + paste_x = col * tile_size + paste_y = row * tile_size + output_img.paste(tile_img, (paste_x, paste_y)) + else: + n_failed_tiles += 1 + + if n_failed_tiles > 3: + raise ValueError("Failed to download all tiles for the requested map.") + + return MapImage( + image=Image.from_numpy(np.array(output_img), format=ImageFormat.RGB), + position=position, + zoom_level=zoom_level, + n_tiles=n_tiles, + ) diff --git a/dimos/mapping/osm/query.py b/dimos/mapping/osm/query.py new file mode 100644 index 0000000000..4501525880 --- /dev/null +++ b/dimos/mapping/osm/query.py @@ -0,0 +1,54 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re + +from dimos.mapping.osm.osm import MapImage +from dimos.mapping.types import LatLon +from dimos.models.vl.base import VlModel +from dimos.utils.generic import extract_json_from_llm_response +from dimos.utils.logging_config import setup_logger + +_PROLOGUE = "This is an image of an open street map I'm on." +_JSON = "Please only respond with valid JSON." +logger = setup_logger(__name__) + + +def query_for_one_position(vl_model: VlModel, map_image: MapImage, query: str) -> LatLon | None: + full_query = f"{_PROLOGUE} {query} {_JSON} If there's a match return the x, y coordinates from the image. Example: `[123, 321]`. If there's no match return `null`." + response = vl_model.query(map_image.image.data, full_query) + coords = tuple(map(int, re.findall(r"\d+", response))) + if len(coords) != 2: + return None + return map_image.pixel_to_latlon(coords) + + +def query_for_one_position_and_context( + vl_model: VlModel, map_image: MapImage, query: str, robot_position: LatLon +) -> tuple[LatLon, str] | None: + example = '{"coordinates": [123, 321], "description": "A Starbucks on 27th Street"}' + x, y = map_image.latlon_to_pixel(robot_position) + my_location = f"I'm currently at x={x}, y={y}." + full_query = f"{_PROLOGUE} {my_location} {query} {_JSON} If there's a match return the x, y coordinates from the image and what is there. Example response: `{example}`. If there's no match return `null`." + logger.info(f"Qwen query: `{full_query}`") + response = vl_model.query(map_image.image.data, full_query) + + try: + doc = extract_json_from_llm_response(response) + return map_image.pixel_to_latlon(tuple(doc["coordinates"])), str(doc["description"]) + except Exception: + pass + + # TODO: Try more simplictic methods to parse. + return None diff --git a/dimos/mapping/osm/test_osm.py b/dimos/mapping/osm/test_osm.py new file mode 100644 index 0000000000..0e993f3157 --- /dev/null +++ b/dimos/mapping/osm/test_osm.py @@ -0,0 +1,71 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Generator +from typing import Any + +import cv2 +import numpy as np +import pytest +from requests import Request +import requests_mock + +from dimos.mapping.osm.osm import get_osm_map +from dimos.mapping.types import LatLon +from dimos.utils.data import get_data + +_fixture_dir = get_data("osm_map_test") + + +def _tile_callback(request: Request, context: Any) -> bytes: + parts = (request.url or "").split("/") + zoom, x, y_png = parts[-3], parts[-2], parts[-1] + y = y_png.removesuffix(".png") + tile_path = _fixture_dir / f"{zoom}_{x}_{y}.png" + context.headers["Content-Type"] = "image/png" + return tile_path.read_bytes() + + +@pytest.fixture +def mock_openstreetmap_org() -> Generator[None, None, None]: + with requests_mock.Mocker() as m: + m.get(requests_mock.ANY, content=_tile_callback) + yield + + +def test_get_osm_map(mock_openstreetmap_org: None) -> None: + position = LatLon(lat=37.751857, lon=-122.431265) + map_image = get_osm_map(position, 18, 4) + + assert map_image.position == position + assert map_image.n_tiles == 4 + + expected_image = cv2.imread(str(_fixture_dir / "full.png")) + expected_image_rgb = cv2.cvtColor(expected_image, cv2.COLOR_BGR2RGB) + assert np.array_equal(map_image.image.data, expected_image_rgb), "Map is not the same." + + +def test_pixel_to_latlon(mock_openstreetmap_org: None) -> None: + position = LatLon(lat=37.751857, lon=-122.431265) + map_image = get_osm_map(position, 18, 4) + latlon = map_image.pixel_to_latlon((100, 100)) + assert abs(latlon.lat - 37.7540056) < 0.0000001 + assert abs(latlon.lon - (-122.43385076)) < 0.0000001 + + +def test_latlon_to_pixel(mock_openstreetmap_org: None) -> None: + position = LatLon(lat=37.751857, lon=-122.431265) + map_image = get_osm_map(position, 18, 4) + coords = map_image.latlon_to_pixel(LatLon(lat=37.751, lon=-122.431)) + assert coords == (631, 808) diff --git a/dimos/mapping/types.py b/dimos/mapping/types.py new file mode 100644 index 0000000000..9c39522011 --- /dev/null +++ b/dimos/mapping/types.py @@ -0,0 +1,27 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from dataclasses import dataclass +from typing import TypeAlias + + +@dataclass(frozen=True) +class LatLon: + lat: float + lon: float + alt: float | None = None + + +ImageCoord: TypeAlias = tuple[int, int] diff --git a/dimos/mapping/utils/distance.py b/dimos/mapping/utils/distance.py new file mode 100644 index 0000000000..7e19fec9ab --- /dev/null +++ b/dimos/mapping/utils/distance.py @@ -0,0 +1,48 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math + +from dimos.mapping.types import LatLon + + +def distance_in_meters(location1: LatLon, location2: LatLon) -> float: + """Calculate the great circle distance between two points on Earth using Haversine formula. + + Args: + location1: First location with latitude and longitude + location2: Second location with latitude and longitude + + Returns: + Distance in meters between the two points + """ + # Earth's radius in meters + EARTH_RADIUS_M = 6371000 + + # Convert degrees to radians + lat1_rad = math.radians(location1.lat) + lat2_rad = math.radians(location2.lat) + lon1_rad = math.radians(location1.lon) + lon2_rad = math.radians(location2.lon) + + # Haversine formula + dlat = lat2_rad - lat1_rad + dlon = lon2_rad - lon1_rad + + a = math.sin(dlat / 2) ** 2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon / 2) ** 2 + c = 2 * math.asin(math.sqrt(a)) + + distance = EARTH_RADIUS_M * c + + return distance diff --git a/dimos/models/Detic/configs/BoxSup_ViLD_200e.py b/dimos/models/Detic/configs/BoxSup_ViLD_200e.py index eef46bc174..b189c7b54f 100644 --- a/dimos/models/Detic/configs/BoxSup_ViLD_200e.py +++ b/dimos/models/Detic/configs/BoxSup_ViLD_200e.py @@ -1,29 +1,28 @@ # Copyright (c) Facebook, Inc. and its affiliates. import os -import torch -import detectron2.data.transforms as T from detectron2.config import LazyCall as L -from detectron2.layers import ShapeSpec from detectron2.data.samplers import RepeatFactorTrainingSampler +import detectron2.data.transforms as T from detectron2.evaluation.lvis_evaluation import LVISEvaluator +from detectron2.layers import ShapeSpec from detectron2.layers.batch_norm import NaiveSyncBatchNorm -from detectron2.solver import WarmupParamScheduler -from detectron2.solver.build import get_default_optimizer_params +from detectron2.model_zoo import get_config +from detectron2.modeling.box_regression import Box2BoxTransform from detectron2.modeling.matcher import Matcher from detectron2.modeling.roi_heads import FastRCNNConvFCHead -from detectron2.modeling.box_regression import Box2BoxTransform -from detectron2.model_zoo import get_config -from fvcore.common.param_scheduler import CosineParamScheduler - -from detic.modeling.roi_heads.zero_shot_classifier import ZeroShotClassifier -from detic.modeling.roi_heads.detic_roi_heads import DeticCascadeROIHeads +from detectron2.solver import WarmupParamScheduler +from detectron2.solver.build import get_default_optimizer_params from detic.modeling.roi_heads.detic_fast_rcnn import DeticFastRCNNOutputLayers +from detic.modeling.roi_heads.detic_roi_heads import DeticCascadeROIHeads +from detic.modeling.roi_heads.zero_shot_classifier import ZeroShotClassifier +from fvcore.common.param_scheduler import CosineParamScheduler +import torch -default_configs = get_config('new_baselines/mask_rcnn_R_50_FPN_100ep_LSJ.py') -dataloader = default_configs['dataloader'] -model = default_configs['model'] -train = default_configs['train'] +default_configs = get_config("new_baselines/mask_rcnn_R_50_FPN_100ep_LSJ.py") +dataloader = default_configs["dataloader"] +model = default_configs["model"] +train = default_configs["train"] [model.roi_heads.pop(k) for k in ["box_head", "box_predictor", "proposal_matcher"]] @@ -35,7 +34,7 @@ input_shape=ShapeSpec(channels=256, height=7, width=7), conv_dims=[256, 256, 256, 256], fc_dims=[1024], - conv_norm=lambda c: NaiveSyncBatchNorm(c, stats_mode="N") + conv_norm=lambda c: NaiveSyncBatchNorm(c, stats_mode="N"), ) for _ in range(1) ], @@ -50,28 +49,28 @@ cls_score=L(ZeroShotClassifier)( input_shape=ShapeSpec(channels=1024), num_classes=1203, - zs_weight_path='datasets/metadata/lvis_v1_clip_a+cname.npy', + zs_weight_path="datasets/metadata/lvis_v1_clip_a+cname.npy", norm_weight=True, # use_bias=-4.6, ), use_zeroshot_cls=True, use_sigmoid_ce=True, ignore_zero_cats=True, - cat_freq_path='datasets/lvis/lvis_v1_train_norare_cat_info.json' + cat_freq_path="datasets/lvis/lvis_v1_train_norare_cat_info.json", ) for (w1, w2) in [(10, 5)] ], proposal_matchers=[ - L(Matcher)(thresholds=[th], labels=[0, 1], allow_low_quality_matches=False) - for th in [0.5] + L(Matcher)(thresholds=[th], labels=[0, 1], allow_low_quality_matches=False) for th in [0.5] ], ) model.roi_heads.mask_head.num_classes = 1 -dataloader.train.dataset.names="lvis_v1_train_norare" -dataloader.train.sampler=L(RepeatFactorTrainingSampler)( +dataloader.train.dataset.names = "lvis_v1_train_norare" +dataloader.train.sampler = L(RepeatFactorTrainingSampler)( repeat_factors=L(RepeatFactorTrainingSampler.repeat_factors_from_category_frequency)( - dataset_dicts="${dataloader.train.dataset}", repeat_thresh=0.001) + dataset_dicts="${dataloader.train.dataset}", repeat_thresh=0.001 + ) ) image_size = 896 dataloader.train.mapper.augmentations = [ @@ -81,9 +80,9 @@ L(T.FixedSizeCrop)(crop_size=(image_size, image_size)), L(T.RandomFlip)(horizontal=True), ] -dataloader.train.num_workers=32 +dataloader.train.num_workers = 32 -dataloader.test.dataset.names="lvis_v1_val" +dataloader.test.dataset.names = "lvis_v1_val" dataloader.evaluator = L(LVISEvaluator)( dataset_name="${..test.dataset.names}", ) @@ -100,12 +99,10 @@ ) optimizer = L(torch.optim.AdamW)( - params=L(get_default_optimizer_params)( - weight_decay_norm=0.0 - ), + params=L(get_default_optimizer_params)(weight_decay_norm=0.0), lr=0.0002 * num_nodes, weight_decay=1e-4, ) -train.checkpointer.period=20000 // num_nodes -train.output_dir='./output/Lazy/{}'.format(os.path.basename(__file__)[:-3]) \ No newline at end of file +train.checkpointer.period = 20000 // num_nodes +train.output_dir = f"./output/Lazy/{os.path.basename(__file__)[:-3]}" diff --git a/dimos/models/Detic/configs/Detic_ViLD_200e.py b/dimos/models/Detic/configs/Detic_ViLD_200e.py index 7a98e28b5d..470124a109 100644 --- a/dimos/models/Detic/configs/Detic_ViLD_200e.py +++ b/dimos/models/Detic/configs/Detic_ViLD_200e.py @@ -1,36 +1,36 @@ # Copyright (c) Facebook, Inc. and its affiliates. import os -import torch -import detectron2.data.transforms as T from detectron2.config import LazyCall as L -from detectron2.layers import ShapeSpec -from detectron2.data.samplers import RepeatFactorTrainingSampler +import detectron2.data.transforms as T from detectron2.evaluation.lvis_evaluation import LVISEvaluator +from detectron2.layers import ShapeSpec from detectron2.layers.batch_norm import NaiveSyncBatchNorm -from detectron2.solver import WarmupParamScheduler -from detectron2.solver.build import get_default_optimizer_params +from detectron2.model_zoo import get_config +from detectron2.modeling.box_regression import Box2BoxTransform from detectron2.modeling.matcher import Matcher from detectron2.modeling.roi_heads import FastRCNNConvFCHead -from detectron2.modeling.box_regression import Box2BoxTransform -from detectron2.model_zoo import get_config -from fvcore.common.param_scheduler import CosineParamScheduler - -from detic.modeling.roi_heads.zero_shot_classifier import ZeroShotClassifier -from detic.modeling.roi_heads.detic_roi_heads import DeticCascadeROIHeads -from detic.modeling.roi_heads.detic_fast_rcnn import DeticFastRCNNOutputLayers +from detectron2.solver import WarmupParamScheduler +from detectron2.solver.build import get_default_optimizer_params +from detic.data.custom_dataset_dataloader import ( + MultiDatasetSampler, + build_custom_train_loader, + get_detection_dataset_dicts_with_source, +) from detic.data.custom_dataset_mapper import CustomDatasetMapper from detic.modeling.meta_arch.custom_rcnn import CustomRCNN -from detic.data.custom_dataset_dataloader import build_custom_train_loader -from detic.data.custom_dataset_dataloader import MultiDatasetSampler -from detic.data.custom_dataset_dataloader import get_detection_dataset_dicts_with_source +from detic.modeling.roi_heads.detic_fast_rcnn import DeticFastRCNNOutputLayers +from detic.modeling.roi_heads.detic_roi_heads import DeticCascadeROIHeads +from detic.modeling.roi_heads.zero_shot_classifier import ZeroShotClassifier +from fvcore.common.param_scheduler import CosineParamScheduler +import torch -default_configs = get_config('new_baselines/mask_rcnn_R_50_FPN_100ep_LSJ.py') -dataloader = default_configs['dataloader'] -model = default_configs['model'] -train = default_configs['train'] +default_configs = get_config("new_baselines/mask_rcnn_R_50_FPN_100ep_LSJ.py") +dataloader = default_configs["dataloader"] +model = default_configs["model"] +train = default_configs["train"] -train.init_checkpoint = 'models/BoxSup_ViLD_200e.pth' +train.init_checkpoint = "models/BoxSup_ViLD_200e.pth" [model.roi_heads.pop(k) for k in ["box_head", "box_predictor", "proposal_matcher"]] @@ -42,7 +42,7 @@ input_shape=ShapeSpec(channels=256, height=7, width=7), conv_dims=[256, 256, 256, 256], fc_dims=[1024], - conv_norm=lambda c: NaiveSyncBatchNorm(c, stats_mode="N") + conv_norm=lambda c: NaiveSyncBatchNorm(c, stats_mode="N"), ) for _ in range(1) ], @@ -57,22 +57,21 @@ cls_score=L(ZeroShotClassifier)( input_shape=ShapeSpec(channels=1024), num_classes=1203, - zs_weight_path='datasets/metadata/lvis_v1_clip_a+cname.npy', + zs_weight_path="datasets/metadata/lvis_v1_clip_a+cname.npy", norm_weight=True, # use_bias=-4.6, ), use_zeroshot_cls=True, use_sigmoid_ce=True, ignore_zero_cats=True, - cat_freq_path='datasets/lvis/lvis_v1_train_norare_cat_info.json', - image_label_loss='max_size', + cat_freq_path="datasets/lvis/lvis_v1_train_norare_cat_info.json", + image_label_loss="max_size", image_loss_weight=0.1, ) for (w1, w2) in [(10, 5)] ], proposal_matchers=[ - L(Matcher)(thresholds=[th], labels=[0, 1], allow_low_quality_matches=False) - for th in [0.5] + L(Matcher)(thresholds=[th], labels=[0, 1], allow_low_quality_matches=False) for th in [0.5] ], with_image_labels=True, ws_num_props=128, @@ -90,35 +89,40 @@ image_size_weak = 448 dataloader.train = L(build_custom_train_loader)( dataset=L(get_detection_dataset_dicts_with_source)( - dataset_names=['lvis_v1_train_norare', 'imagenet_lvis_v1'], + dataset_names=["lvis_v1_train_norare", "imagenet_lvis_v1"], filter_empty=False, ), mapper=L(CustomDatasetMapper)( is_train=True, augmentations=[], with_ann_type=True, - dataset_ann=['box', 'image'], + dataset_ann=["box", "image"], use_diff_bs_size=True, - dataset_augs = [ - [L(T.ResizeScale)( + dataset_augs=[ + [ + L(T.ResizeScale)( min_scale=0.1, max_scale=2.0, target_height=image_size, target_width=image_size ), L(T.FixedSizeCrop)(crop_size=(image_size, image_size)), L(T.RandomFlip)(horizontal=True), ], - [L(T.ResizeScale)( - min_scale=0.5, max_scale=1.5, target_height=image_size_weak, target_width=image_size_weak + [ + L(T.ResizeScale)( + min_scale=0.5, + max_scale=1.5, + target_height=image_size_weak, + target_width=image_size_weak, ), L(T.FixedSizeCrop)(crop_size=(image_size_weak, image_size_weak)), L(T.RandomFlip)(horizontal=True), - ] + ], ], image_format="BGR", use_instance_mask=True, ), sampler=L(MultiDatasetSampler)( dataset_dicts="${dataloader.train.dataset}", - dataset_ratio=[1,4], + dataset_ratio=[1, 4], use_rfs=[True, False], dataset_ann="${dataloader.train.mapper.dataset_ann}", repeat_threshold=0.001, @@ -131,7 +135,7 @@ num_workers=8, ) -dataloader.test.dataset.names="lvis_v1_val" +dataloader.test.dataset.names = "lvis_v1_val" dataloader.evaluator = L(LVISEvaluator)( dataset_name="${..test.dataset.names}", ) @@ -144,12 +148,10 @@ ) optimizer = L(torch.optim.AdamW)( - params=L(get_default_optimizer_params)( - weight_decay_norm=0.0 - ), + params=L(get_default_optimizer_params)(weight_decay_norm=0.0), lr=0.0002 * num_nodes, weight_decay=1e-4, ) -train.checkpointer.period=20000 // num_nodes -train.output_dir='./output/Lazy/{}'.format(os.path.basename(__file__)[:-3]) \ No newline at end of file +train.checkpointer.period = 20000 // num_nodes +train.output_dir = f"./output/Lazy/{os.path.basename(__file__)[:-3]}" diff --git a/dimos/models/Detic/demo.py b/dimos/models/Detic/demo.py index 9221af2dd6..e982f745a5 100755 --- a/dimos/models/Detic/demo.py +++ b/dimos/models/Detic/demo.py @@ -2,51 +2,53 @@ import argparse import glob import multiprocessing as mp -import numpy as np import os +import sys import tempfile import time import warnings -import cv2 -import tqdm -import sys -import mss +import cv2 from detectron2.config import get_cfg from detectron2.data.detection_utils import read_image from detectron2.utils.logger import setup_logger +import mss +import numpy as np +import tqdm -sys.path.insert(0, 'third_party/CenterNet2/') +sys.path.insert(0, "third_party/CenterNet2/") from centernet.config import add_centernet_config from detic.config import add_detic_config - from detic.predictor import VisualizationDemo + # Fake a video capture object OpenCV style - half width, half height of first screen using MSS class ScreenGrab: - def __init__(self): + def __init__(self) -> None: self.sct = mss.mss() m0 = self.sct.monitors[0] - self.monitor = {'top': 0, 'left': 0, 'width': m0['width'] / 2, 'height': m0['height'] / 2} + self.monitor = {"top": 0, "left": 0, "width": m0["width"] / 2, "height": m0["height"] / 2} def read(self): - img = np.array(self.sct.grab(self.monitor)) + img = np.array(self.sct.grab(self.monitor)) nf = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR) return (True, nf) - def isOpened(self): + def isOpened(self) -> bool: return True - def release(self): + + def release(self) -> bool: return True # constants WINDOW_NAME = "Detic" + def setup_cfg(args): cfg = get_cfg() if args.cpu: - cfg.MODEL.DEVICE="cpu" + cfg.MODEL.DEVICE = "cpu" add_centernet_config(cfg) add_detic_config(cfg) cfg.merge_from_file(args.config_file) @@ -55,7 +57,7 @@ def setup_cfg(args): cfg.MODEL.RETINANET.SCORE_THRESH_TEST = args.confidence_threshold cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = args.confidence_threshold cfg.MODEL.PANOPTIC_FPN.COMBINE.INSTANCES_CONFIDENCE_THRESH = args.confidence_threshold - cfg.MODEL.ROI_BOX_HEAD.ZEROSHOT_WEIGHT_PATH = 'rand' # load later + cfg.MODEL.ROI_BOX_HEAD.ZEROSHOT_WEIGHT_PATH = "rand" # load later if not args.pred_all_class: cfg.MODEL.ROI_HEADS.ONE_CLASS_PER_PROPOSAL = True cfg.freeze() @@ -71,23 +73,21 @@ def get_parser(): help="path to config file", ) parser.add_argument("--webcam", help="Take inputs from webcam.") - parser.add_argument("--cpu", action='store_true', help="Use CPU only.") + parser.add_argument("--cpu", action="store_true", help="Use CPU only.") parser.add_argument("--video-input", help="Path to video file.") parser.add_argument( "--input", nargs="+", - help="A list of space separated input images; " - "or a single glob pattern such as 'directory/*.jpg'", + help="A list of space separated input images; or a single glob pattern such as 'directory/*.jpg'", ) parser.add_argument( "--output", - help="A file or directory to save output visualizations. " - "If not given, will show output in an OpenCV window.", + help="A file or directory to save output visualizations. If not given, will show output in an OpenCV window.", ) parser.add_argument( "--vocabulary", default="lvis", - choices=['lvis', 'openimages', 'objects365', 'coco', 'custom'], + choices=["lvis", "openimages", "objects365", "coco", "custom"], help="", ) parser.add_argument( @@ -95,7 +95,7 @@ def get_parser(): default="", help="", ) - parser.add_argument("--pred_all_class", action='store_true') + parser.add_argument("--pred_all_class", action="store_true") parser.add_argument( "--confidence-threshold", type=float, @@ -111,7 +111,7 @@ def get_parser(): return parser -def test_opencv_video_format(codec, file_ext): +def test_opencv_video_format(codec, file_ext) -> bool: with tempfile.TemporaryDirectory(prefix="video_format_test") as dir: filename = os.path.join(dir, "test_file" + file_ext) writer = cv2.VideoWriter( @@ -195,7 +195,7 @@ def test_opencv_video_format(codec, file_ext): ("x264", ".mkv") if test_opencv_video_format("x264", ".mkv") else ("mp4v", ".mp4") ) if codec == ".mp4v": - warnings.warn("x264 codec not available, switching to mp4v") + warnings.warn("x264 codec not available, switching to mp4v", stacklevel=2) if args.output: if os.path.isdir(args.output): output_fname = os.path.join(args.output, basename) diff --git a/dimos/models/Detic/detic/__init__.py b/dimos/models/Detic/detic/__init__.py index 8ffba6afd9..2f8aa0a44e 100644 --- a/dimos/models/Detic/detic/__init__.py +++ b/dimos/models/Detic/detic/__init__.py @@ -1,19 +1,10 @@ # Copyright (c) Facebook, Inc. and its affiliates. +from .data.datasets import cc, coco_zeroshot, imagenet, lvis_v1, objects365, oid +from .modeling.backbone import swintransformer, timm from .modeling.meta_arch import custom_rcnn -from .modeling.roi_heads import detic_roi_heads -from .modeling.roi_heads import res5_roi_heads -from .modeling.backbone import swintransformer -from .modeling.backbone import timm - - -from .data.datasets import lvis_v1 -from .data.datasets import imagenet -from .data.datasets import cc -from .data.datasets import objects365 -from .data.datasets import oid -from .data.datasets import coco_zeroshot +from .modeling.roi_heads import detic_roi_heads, res5_roi_heads try: from .modeling.meta_arch import d2_deformable_detr except: - pass \ No newline at end of file + pass diff --git a/dimos/models/Detic/detic/config.py b/dimos/models/Detic/detic/config.py index bbf6e58064..c053f0bd06 100644 --- a/dimos/models/Detic/detic/config.py +++ b/dimos/models/Detic/detic/config.py @@ -1,48 +1,50 @@ # Copyright (c) Facebook, Inc. and its affiliates. from detectron2.config import CfgNode as CN -def add_detic_config(cfg): + +def add_detic_config(cfg) -> None: _C = cfg - _C.WITH_IMAGE_LABELS = False # Turn on co-training with classification data + _C.WITH_IMAGE_LABELS = False # Turn on co-training with classification data # Open-vocabulary classifier - _C.MODEL.ROI_BOX_HEAD.USE_ZEROSHOT_CLS = False # Use fixed classifier for open-vocabulary detection - _C.MODEL.ROI_BOX_HEAD.ZEROSHOT_WEIGHT_PATH = 'datasets/metadata/lvis_v1_clip_a+cname.npy' + _C.MODEL.ROI_BOX_HEAD.USE_ZEROSHOT_CLS = ( + False # Use fixed classifier for open-vocabulary detection + ) + _C.MODEL.ROI_BOX_HEAD.ZEROSHOT_WEIGHT_PATH = "datasets/metadata/lvis_v1_clip_a+cname.npy" _C.MODEL.ROI_BOX_HEAD.ZEROSHOT_WEIGHT_DIM = 512 _C.MODEL.ROI_BOX_HEAD.NORM_WEIGHT = True _C.MODEL.ROI_BOX_HEAD.NORM_TEMP = 50.0 _C.MODEL.ROI_BOX_HEAD.IGNORE_ZERO_CATS = False - _C.MODEL.ROI_BOX_HEAD.USE_BIAS = 0.0 # >= 0: not use - - _C.MODEL.ROI_BOX_HEAD.MULT_PROPOSAL_SCORE = False # CenterNet2 + _C.MODEL.ROI_BOX_HEAD.USE_BIAS = 0.0 # >= 0: not use + + _C.MODEL.ROI_BOX_HEAD.MULT_PROPOSAL_SCORE = False # CenterNet2 _C.MODEL.ROI_BOX_HEAD.USE_SIGMOID_CE = False _C.MODEL.ROI_BOX_HEAD.PRIOR_PROB = 0.01 - _C.MODEL.ROI_BOX_HEAD.USE_FED_LOSS = False # Federated Loss - _C.MODEL.ROI_BOX_HEAD.CAT_FREQ_PATH = \ - 'datasets/metadata/lvis_v1_train_cat_info.json' + _C.MODEL.ROI_BOX_HEAD.USE_FED_LOSS = False # Federated Loss + _C.MODEL.ROI_BOX_HEAD.CAT_FREQ_PATH = "datasets/metadata/lvis_v1_train_cat_info.json" _C.MODEL.ROI_BOX_HEAD.FED_LOSS_NUM_CAT = 50 _C.MODEL.ROI_BOX_HEAD.FED_LOSS_FREQ_WEIGHT = 0.5 # Classification data configs - _C.MODEL.ROI_BOX_HEAD.IMAGE_LABEL_LOSS = 'max_size' # max, softmax, sum + _C.MODEL.ROI_BOX_HEAD.IMAGE_LABEL_LOSS = "max_size" # max, softmax, sum _C.MODEL.ROI_BOX_HEAD.IMAGE_LOSS_WEIGHT = 0.1 _C.MODEL.ROI_BOX_HEAD.IMAGE_BOX_SIZE = 1.0 - _C.MODEL.ROI_BOX_HEAD.ADD_IMAGE_BOX = False # Used for image-box loss and caption loss - _C.MODEL.ROI_BOX_HEAD.WS_NUM_PROPS = 128 # num proposals for image-labeled data - _C.MODEL.ROI_BOX_HEAD.WITH_SOFTMAX_PROP = False # Used for WSDDN - _C.MODEL.ROI_BOX_HEAD.CAPTION_WEIGHT = 1.0 # Caption loss weight - _C.MODEL.ROI_BOX_HEAD.NEG_CAP_WEIGHT = 0.125 # Caption loss hyper-parameter - _C.MODEL.ROI_BOX_HEAD.ADD_FEATURE_TO_PROP = False # Used for WSDDN - _C.MODEL.ROI_BOX_HEAD.SOFTMAX_WEAK_LOSS = False # Used when USE_SIGMOID_CE is False + _C.MODEL.ROI_BOX_HEAD.ADD_IMAGE_BOX = False # Used for image-box loss and caption loss + _C.MODEL.ROI_BOX_HEAD.WS_NUM_PROPS = 128 # num proposals for image-labeled data + _C.MODEL.ROI_BOX_HEAD.WITH_SOFTMAX_PROP = False # Used for WSDDN + _C.MODEL.ROI_BOX_HEAD.CAPTION_WEIGHT = 1.0 # Caption loss weight + _C.MODEL.ROI_BOX_HEAD.NEG_CAP_WEIGHT = 0.125 # Caption loss hyper-parameter + _C.MODEL.ROI_BOX_HEAD.ADD_FEATURE_TO_PROP = False # Used for WSDDN + _C.MODEL.ROI_BOX_HEAD.SOFTMAX_WEAK_LOSS = False # Used when USE_SIGMOID_CE is False _C.MODEL.ROI_HEADS.MASK_WEIGHT = 1.0 - _C.MODEL.ROI_HEADS.ONE_CLASS_PER_PROPOSAL = False # For demo only + _C.MODEL.ROI_HEADS.ONE_CLASS_PER_PROPOSAL = False # For demo only # Caption losses - _C.MODEL.CAP_BATCH_RATIO = 4 # Ratio between detection data and caption data + _C.MODEL.CAP_BATCH_RATIO = 4 # Ratio between detection data and caption data _C.MODEL.WITH_CAPTION = False - _C.MODEL.SYNC_CAPTION_BATCH = False # synchronize across GPUs to enlarge # "classes" + _C.MODEL.SYNC_CAPTION_BATCH = False # synchronize across GPUs to enlarge # "classes" # dynamic class sampling when training with 21K classes _C.MODEL.DYNAMIC_CLASSIFIER = False @@ -55,43 +57,43 @@ def add_detic_config(cfg): # Backbones _C.MODEL.SWIN = CN() - _C.MODEL.SWIN.SIZE = 'T' # 'T', 'S', 'B' + _C.MODEL.SWIN.SIZE = "T" # 'T', 'S', 'B' _C.MODEL.SWIN.USE_CHECKPOINT = False - _C.MODEL.SWIN.OUT_FEATURES = (1, 2, 3) # FPN stride 8 - 32 + _C.MODEL.SWIN.OUT_FEATURES = (1, 2, 3) # FPN stride 8 - 32 _C.MODEL.TIMM = CN() - _C.MODEL.TIMM.BASE_NAME = 'resnet50' + _C.MODEL.TIMM.BASE_NAME = "resnet50" _C.MODEL.TIMM.OUT_LEVELS = (3, 4, 5) - _C.MODEL.TIMM.NORM = 'FrozenBN' + _C.MODEL.TIMM.NORM = "FrozenBN" _C.MODEL.TIMM.FREEZE_AT = 0 _C.MODEL.TIMM.PRETRAINED = False _C.MODEL.DATASET_LOSS_WEIGHT = [] - + # Multi-dataset dataloader - _C.DATALOADER.DATASET_RATIO = [1, 1] # sample ratio + _C.DATALOADER.DATASET_RATIO = [1, 1] # sample ratio _C.DATALOADER.USE_RFS = [False, False] - _C.DATALOADER.MULTI_DATASET_GROUPING = False # Always true when multi-dataset is enabled - _C.DATALOADER.DATASET_ANN = ['box', 'box'] # Annotation type of each dataset - _C.DATALOADER.USE_DIFF_BS_SIZE = False # Use different batchsize for each dataset - _C.DATALOADER.DATASET_BS = [8, 32] # Used when USE_DIFF_BS_SIZE is on - _C.DATALOADER.DATASET_INPUT_SIZE = [896, 384] # Used when USE_DIFF_BS_SIZE is on - _C.DATALOADER.DATASET_INPUT_SCALE = [(0.1, 2.0), (0.5, 1.5)] # Used when USE_DIFF_BS_SIZE is on - _C.DATALOADER.DATASET_MIN_SIZES = [(640, 800), (320, 400)] # Used when USE_DIFF_BS_SIZE is on - _C.DATALOADER.DATASET_MAX_SIZES = [1333, 667] # Used when USE_DIFF_BS_SIZE is on - _C.DATALOADER.USE_TAR_DATASET = False # for ImageNet-21K, directly reading from unziped files - _C.DATALOADER.TARFILE_PATH = 'datasets/imagenet/metadata-22k/tar_files.npy' - _C.DATALOADER.TAR_INDEX_DIR = 'datasets/imagenet/metadata-22k/tarindex_npy' - + _C.DATALOADER.MULTI_DATASET_GROUPING = False # Always true when multi-dataset is enabled + _C.DATALOADER.DATASET_ANN = ["box", "box"] # Annotation type of each dataset + _C.DATALOADER.USE_DIFF_BS_SIZE = False # Use different batchsize for each dataset + _C.DATALOADER.DATASET_BS = [8, 32] # Used when USE_DIFF_BS_SIZE is on + _C.DATALOADER.DATASET_INPUT_SIZE = [896, 384] # Used when USE_DIFF_BS_SIZE is on + _C.DATALOADER.DATASET_INPUT_SCALE = [(0.1, 2.0), (0.5, 1.5)] # Used when USE_DIFF_BS_SIZE is on + _C.DATALOADER.DATASET_MIN_SIZES = [(640, 800), (320, 400)] # Used when USE_DIFF_BS_SIZE is on + _C.DATALOADER.DATASET_MAX_SIZES = [1333, 667] # Used when USE_DIFF_BS_SIZE is on + _C.DATALOADER.USE_TAR_DATASET = False # for ImageNet-21K, directly reading from unziped files + _C.DATALOADER.TARFILE_PATH = "datasets/imagenet/metadata-22k/tar_files.npy" + _C.DATALOADER.TAR_INDEX_DIR = "datasets/imagenet/metadata-22k/tarindex_npy" + _C.SOLVER.USE_CUSTOM_SOLVER = False - _C.SOLVER.OPTIMIZER = 'SGD' - _C.SOLVER.BACKBONE_MULTIPLIER = 1.0 # Used in DETR - _C.SOLVER.CUSTOM_MULTIPLIER = 1.0 # Used in DETR - _C.SOLVER.CUSTOM_MULTIPLIER_NAME = [] # Used in DETR + _C.SOLVER.OPTIMIZER = "SGD" + _C.SOLVER.BACKBONE_MULTIPLIER = 1.0 # Used in DETR + _C.SOLVER.CUSTOM_MULTIPLIER = 1.0 # Used in DETR + _C.SOLVER.CUSTOM_MULTIPLIER_NAME = [] # Used in DETR # Deformable DETR _C.MODEL.DETR = CN() _C.MODEL.DETR.NUM_CLASSES = 80 - _C.MODEL.DETR.FROZEN_WEIGHTS = '' # For Segmentation + _C.MODEL.DETR.FROZEN_WEIGHTS = "" # For Segmentation _C.MODEL.DETR.GIOU_WEIGHT = 2.0 _C.MODEL.DETR.L1_WEIGHT = 5.0 _C.MODEL.DETR.DEEP_SUPERVISION = True @@ -113,12 +115,12 @@ def add_detic_config(cfg): _C.MODEL.DETR.USE_FED_LOSS = False _C.MODEL.DETR.WEAK_WEIGHT = 0.1 - _C.INPUT.CUSTOM_AUG = '' + _C.INPUT.CUSTOM_AUG = "" _C.INPUT.TRAIN_SIZE = 640 _C.INPUT.TEST_SIZE = 640 - _C.INPUT.SCALE_RANGE = (0.1, 2.) + _C.INPUT.SCALE_RANGE = (0.1, 2.0) # 'default' for fixed short/ long edge, 'square' for max size=INPUT.SIZE - _C.INPUT.TEST_INPUT_TYPE = 'default' + _C.INPUT.TEST_INPUT_TYPE = "default" _C.FIND_UNUSED_PARAM = True _C.EVAL_PRED_AR = False @@ -129,4 +131,4 @@ def add_detic_config(cfg): _C.FP16 = False _C.EVAL_AP_FIX = False _C.GEN_PSEDO_LABELS = False - _C.SAVE_DEBUG_PATH = 'output/save_debug/' \ No newline at end of file + _C.SAVE_DEBUG_PATH = "output/save_debug/" diff --git a/dimos/models/Detic/detic/custom_solver.py b/dimos/models/Detic/detic/custom_solver.py index 0284ae14ed..a552dea0f1 100644 --- a/dimos/models/Detic/detic/custom_solver.py +++ b/dimos/models/Detic/detic/custom_solver.py @@ -1,12 +1,11 @@ # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -from enum import Enum import itertools -from typing import Any, Callable, Dict, Iterable, List, Set, Type, Union -import torch +from typing import Any, Dict, List, Set from detectron2.config import CfgNode - from detectron2.solver.build import maybe_add_gradient_clipping +import torch + def match_name_keywords(n, name_keywords): out = False @@ -16,12 +15,13 @@ def match_name_keywords(n, name_keywords): break return out + def build_custom_optimizer(cfg: CfgNode, model: torch.nn.Module) -> torch.optim.Optimizer: """ Build an optimizer from config. """ - params: List[Dict[str, Any]] = [] - memo: Set[torch.nn.parameter.Parameter] = set() + params: list[dict[str, Any]] = [] + memo: set[torch.nn.parameter.Parameter] = set() custom_multiplier_name = cfg.SOLVER.CUSTOM_MULTIPLIER_NAME optimizer_type = cfg.SOLVER.OPTIMIZER for key, value in model.named_parameters(recurse=True): @@ -37,10 +37,10 @@ def build_custom_optimizer(cfg: CfgNode, model: torch.nn.Module) -> torch.optim. lr = lr * cfg.SOLVER.BACKBONE_MULTIPLIER if match_name_keywords(key, custom_multiplier_name): lr = lr * cfg.SOLVER.CUSTOM_MULTIPLIER - print('Costum LR', key, lr) + print("Costum LR", key, lr) param = {"params": [value], "lr": lr} - if optimizer_type != 'ADAMW': - param['weight_decay'] = weight_decay + if optimizer_type != "ADAMW": + param["weight_decay"] = weight_decay params += [param] def maybe_add_full_model_gradient_clipping(optim): # optim: the optimizer class @@ -53,26 +53,23 @@ def maybe_add_full_model_gradient_clipping(optim): # optim: the optimizer class ) class FullModelGradientClippingOptimizer(optim): - def step(self, closure=None): + def step(self, closure=None) -> None: all_params = itertools.chain(*[x["params"] for x in self.param_groups]) torch.nn.utils.clip_grad_norm_(all_params, clip_norm_val) super().step(closure=closure) return FullModelGradientClippingOptimizer if enable else optim - - if optimizer_type == 'SGD': + if optimizer_type == "SGD": optimizer = maybe_add_full_model_gradient_clipping(torch.optim.SGD)( - params, cfg.SOLVER.BASE_LR, momentum=cfg.SOLVER.MOMENTUM, - nesterov=cfg.SOLVER.NESTEROV + params, cfg.SOLVER.BASE_LR, momentum=cfg.SOLVER.MOMENTUM, nesterov=cfg.SOLVER.NESTEROV ) - elif optimizer_type == 'ADAMW': + elif optimizer_type == "ADAMW": optimizer = maybe_add_full_model_gradient_clipping(torch.optim.AdamW)( - params, cfg.SOLVER.BASE_LR, - weight_decay=cfg.SOLVER.WEIGHT_DECAY + params, cfg.SOLVER.BASE_LR, weight_decay=cfg.SOLVER.WEIGHT_DECAY ) else: raise NotImplementedError(f"no optimizer type {optimizer_type}") if not cfg.SOLVER.CLIP_GRADIENTS.CLIP_TYPE == "full_model": optimizer = maybe_add_gradient_clipping(cfg, optimizer) - return optimizer \ No newline at end of file + return optimizer diff --git a/dimos/models/Detic/detic/data/custom_build_augmentation.py b/dimos/models/Detic/detic/data/custom_build_augmentation.py index 9642c15e58..5a6049ae02 100644 --- a/dimos/models/Detic/detic/data/custom_build_augmentation.py +++ b/dimos/models/Detic/detic/data/custom_build_augmentation.py @@ -1,17 +1,13 @@ # Copyright (c) Facebook, Inc. and its affiliates. -import logging -import numpy as np -import pycocotools.mask as mask_util -import torch -from fvcore.common.file_io import PathManager -from PIL import Image from detectron2.data import transforms as T + from .transforms.custom_augmentation_impl import EfficientDetResizeCrop +from typing import Optional + -def build_custom_augmentation(cfg, is_train, scale=None, size=None, \ - min_size=None, max_size=None): +def build_custom_augmentation(cfg, is_train: bool, scale=None, size: Optional[int]=None, min_size: Optional[int]=None, max_size: Optional[int]=None): """ Create a list of default :class:`Augmentation` from config. Now it includes resizing and flipping. @@ -19,7 +15,7 @@ def build_custom_augmentation(cfg, is_train, scale=None, size=None, \ Returns: list[Augmentation] """ - if cfg.INPUT.CUSTOM_AUG == 'ResizeShortestEdge': + if cfg.INPUT.CUSTOM_AUG == "ResizeShortestEdge": if is_train: min_size = cfg.INPUT.MIN_SIZE_TRAIN if min_size is None else min_size max_size = cfg.INPUT.MAX_SIZE_TRAIN if max_size is None else max_size @@ -29,7 +25,7 @@ def build_custom_augmentation(cfg, is_train, scale=None, size=None, \ max_size = cfg.INPUT.MAX_SIZE_TEST sample_style = "choice" augmentation = [T.ResizeShortestEdge(min_size, max_size, sample_style)] - elif cfg.INPUT.CUSTOM_AUG == 'EfficientDetResizeCrop': + elif cfg.INPUT.CUSTOM_AUG == "EfficientDetResizeCrop": if is_train: scale = cfg.INPUT.SCALE_RANGE if scale is None else scale size = cfg.INPUT.TRAIN_SIZE if size is None else size @@ -48,4 +44,4 @@ def build_custom_augmentation(cfg, is_train, scale=None, size=None, \ build_custom_transform_gen = build_custom_augmentation """ Alias for backward-compatibility. -""" \ No newline at end of file +""" diff --git a/dimos/models/Detic/detic/data/custom_dataset_dataloader.py b/dimos/models/Detic/detic/data/custom_dataset_dataloader.py index 8f8d681770..ff4bfc9ea4 100644 --- a/dimos/models/Detic/detic/data/custom_dataset_dataloader.py +++ b/dimos/models/Detic/detic/data/custom_dataset_dataloader.py @@ -1,42 +1,41 @@ # Copyright (c) Facebook, Inc. and its affiliates. # Part of the code is from https://github.com/xingyizhou/UniDet/blob/master/projects/UniDet/unidet/data/multi_dataset_dataloader.py (Apache-2.0 License) -import copy -import logging -import numpy as np +from collections import defaultdict +import itertools +import math import operator -import torch -import torch.utils.data -import json -from detectron2.utils.comm import get_world_size -from detectron2.utils.logger import _log_api_usage, log_first_n +from typing import Iterator, Sequence, Optional from detectron2.config import configurable -from detectron2.data import samplers -from torch.utils.data.sampler import BatchSampler, Sampler +from detectron2.data.build import ( + build_batch_data_loader, + check_metadata_consistency, + filter_images_with_few_keypoints, + filter_images_with_only_crowd_annotations, + get_detection_dataset_dicts, + print_instances_class_histogram, + worker_init_reset_seed, +) +from detectron2.data.catalog import DatasetCatalog, MetadataCatalog from detectron2.data.common import DatasetFromList, MapDataset from detectron2.data.dataset_mapper import DatasetMapper -from detectron2.data.build import get_detection_dataset_dicts, build_batch_data_loader -from detectron2.data.samplers import TrainingSampler, RepeatFactorTrainingSampler -from detectron2.data.build import worker_init_reset_seed, print_instances_class_histogram -from detectron2.data.build import filter_images_with_only_crowd_annotations -from detectron2.data.build import filter_images_with_few_keypoints -from detectron2.data.build import check_metadata_consistency -from detectron2.data.catalog import MetadataCatalog, DatasetCatalog +from detectron2.data.samplers import RepeatFactorTrainingSampler, TrainingSampler from detectron2.utils import comm -import itertools -import math -from collections import defaultdict -from typing import Optional +from detectron2.utils.comm import get_world_size +import torch +import torch.utils.data +from torch.utils.data.sampler import Sampler def _custom_train_loader_from_config(cfg, mapper=None, *, dataset=None, sampler=None): sampler_name = cfg.DATALOADER.SAMPLER_TRAIN - if 'MultiDataset' in sampler_name: + if "MultiDataset" in sampler_name: dataset_dicts = get_detection_dataset_dicts_with_source( cfg.DATASETS.TRAIN, filter_empty=cfg.DATALOADER.FILTER_EMPTY_ANNOTATIONS, min_keypoints=cfg.MODEL.ROI_KEYPOINT_HEAD.MIN_KEYPOINTS_PER_IMAGE - if cfg.MODEL.KEYPOINT_ON else 0, + if cfg.MODEL.KEYPOINT_ON + else 0, proposal_files=cfg.DATASETS.PROPOSAL_FILES_TRAIN if cfg.MODEL.LOAD_PROPOSALS else None, ) else: @@ -44,7 +43,8 @@ def _custom_train_loader_from_config(cfg, mapper=None, *, dataset=None, sampler= cfg.DATASETS.TRAIN, filter_empty=cfg.DATALOADER.FILTER_EMPTY_ANNOTATIONS, min_keypoints=cfg.MODEL.ROI_KEYPOINT_HEAD.MIN_KEYPOINTS_PER_IMAGE - if cfg.MODEL.KEYPOINT_ON else 0, + if cfg.MODEL.KEYPOINT_ON + else 0, proposal_files=cfg.DATASETS.PROPOSAL_FILES_TRAIN if cfg.MODEL.LOAD_PROPOSALS else None, ) @@ -58,10 +58,10 @@ def _custom_train_loader_from_config(cfg, mapper=None, *, dataset=None, sampler= elif sampler_name == "MultiDatasetSampler": sampler = MultiDatasetSampler( dataset_dicts, - dataset_ratio = cfg.DATALOADER.DATASET_RATIO, - use_rfs = cfg.DATALOADER.USE_RFS, - dataset_ann = cfg.DATALOADER.DATASET_ANN, - repeat_threshold = cfg.DATALOADER.REPEAT_THRESHOLD, + dataset_ratio=cfg.DATALOADER.DATASET_RATIO, + use_rfs=cfg.DATALOADER.USE_RFS, + dataset_ann=cfg.DATALOADER.DATASET_ANN, + repeat_threshold=cfg.DATALOADER.REPEAT_THRESHOLD, ) elif sampler_name == "RepeatFactorTrainingSampler": repeat_factors = RepeatFactorTrainingSampler.repeat_factors_from_category_frequency( @@ -69,7 +69,7 @@ def _custom_train_loader_from_config(cfg, mapper=None, *, dataset=None, sampler= ) sampler = RepeatFactorTrainingSampler(repeat_factors) else: - raise ValueError("Unknown training sampler: {}".format(sampler_name)) + raise ValueError(f"Unknown training sampler: {sampler_name}") return { "dataset": dataset_dicts, @@ -78,28 +78,33 @@ def _custom_train_loader_from_config(cfg, mapper=None, *, dataset=None, sampler= "total_batch_size": cfg.SOLVER.IMS_PER_BATCH, "aspect_ratio_grouping": cfg.DATALOADER.ASPECT_RATIO_GROUPING, "num_workers": cfg.DATALOADER.NUM_WORKERS, - 'multi_dataset_grouping': cfg.DATALOADER.MULTI_DATASET_GROUPING, - 'use_diff_bs_size': cfg.DATALOADER.USE_DIFF_BS_SIZE, - 'dataset_bs': cfg.DATALOADER.DATASET_BS, - 'num_datasets': len(cfg.DATASETS.TRAIN) + "multi_dataset_grouping": cfg.DATALOADER.MULTI_DATASET_GROUPING, + "use_diff_bs_size": cfg.DATALOADER.USE_DIFF_BS_SIZE, + "dataset_bs": cfg.DATALOADER.DATASET_BS, + "num_datasets": len(cfg.DATASETS.TRAIN), } @configurable(from_config=_custom_train_loader_from_config) def build_custom_train_loader( - dataset, *, mapper, sampler, - total_batch_size=16, - aspect_ratio_grouping=True, - num_workers=0, - num_datasets=1, - multi_dataset_grouping=False, - use_diff_bs_size=False, - dataset_bs=[] - ): + dataset, + *, + mapper, + sampler, + total_batch_size: int=16, + aspect_ratio_grouping: bool=True, + num_workers: int=0, + num_datasets: int=1, + multi_dataset_grouping: bool=False, + use_diff_bs_size: bool=False, + dataset_bs=None, +): """ Modified from detectron2.data.build.build_custom_train_loader, but supports different samplers """ + if dataset_bs is None: + dataset_bs = [] if isinstance(dataset, list): dataset = DatasetFromList(dataset, copy=False) if mapper is not None: @@ -128,16 +133,12 @@ def build_custom_train_loader( def build_multi_dataset_batch_data_loader( - use_diff_bs_size, dataset_bs, - dataset, sampler, total_batch_size, num_datasets, num_workers=0 + use_diff_bs_size: int, dataset_bs, dataset, sampler, total_batch_size: int, num_datasets: int, num_workers: int=0 ): - """ - """ + """ """ world_size = get_world_size() - assert ( - total_batch_size > 0 and total_batch_size % world_size == 0 - ), "Total batch size ({}) must be divisible by the number of gpus ({}).".format( - total_batch_size, world_size + assert total_batch_size > 0 and total_batch_size % world_size == 0, ( + f"Total batch size ({total_batch_size}) must be divisible by the number of gpus ({world_size})." ) batch_size = total_batch_size // world_size @@ -150,26 +151,23 @@ def build_multi_dataset_batch_data_loader( worker_init_fn=worker_init_reset_seed, ) # yield individual mapped dict if use_diff_bs_size: - return DIFFMDAspectRatioGroupedDataset( - data_loader, dataset_bs, num_datasets) + return DIFFMDAspectRatioGroupedDataset(data_loader, dataset_bs, num_datasets) else: - return MDAspectRatioGroupedDataset( - data_loader, batch_size, num_datasets) + return MDAspectRatioGroupedDataset(data_loader, batch_size, num_datasets) def get_detection_dataset_dicts_with_source( - dataset_names, filter_empty=True, min_keypoints=0, proposal_files=None + dataset_names: Sequence[str], filter_empty: bool=True, min_keypoints: int=0, proposal_files=None ): assert len(dataset_names) dataset_dicts = [DatasetCatalog.get(dataset_name) for dataset_name in dataset_names] - for dataset_name, dicts in zip(dataset_names, dataset_dicts): - assert len(dicts), "Dataset '{}' is empty!".format(dataset_name) - - for source_id, (dataset_name, dicts) in \ - enumerate(zip(dataset_names, dataset_dicts)): - assert len(dicts), "Dataset '{}' is empty!".format(dataset_name) + for dataset_name, dicts in zip(dataset_names, dataset_dicts, strict=False): + assert len(dicts), f"Dataset '{dataset_name}' is empty!" + + for source_id, (dataset_name, dicts) in enumerate(zip(dataset_names, dataset_dicts, strict=False)): + assert len(dicts), f"Dataset '{dataset_name}' is empty!" for d in dicts: - d['dataset_source'] = source_id + d["dataset_source"] = source_id if "annotations" in dicts[0]: try: @@ -194,49 +192,48 @@ def get_detection_dataset_dicts_with_source( class MultiDatasetSampler(Sampler): def __init__( - self, - dataset_dicts, + self, + dataset_dicts, dataset_ratio, use_rfs, dataset_ann, - repeat_threshold=0.001, - seed: Optional[int] = None, - ): - """ - """ + repeat_threshold: float=0.001, + seed: int | None = None, + ) -> None: + """ """ sizes = [0 for _ in range(len(dataset_ratio))] for d in dataset_dicts: - sizes[d['dataset_source']] += 1 - print('dataset sizes', sizes) + sizes[d["dataset_source"]] += 1 + print("dataset sizes", sizes) self.sizes = sizes - assert len(dataset_ratio) == len(sizes), \ - 'length of dataset ratio {} should be equal to number if dataset {}'.format( - len(dataset_ratio), len(sizes) - ) + assert len(dataset_ratio) == len(sizes), ( + f"length of dataset ratio {len(dataset_ratio)} should be equal to number if dataset {len(sizes)}" + ) if seed is None: seed = comm.shared_random_seed() self._seed = int(seed) self._rank = comm.get_rank() self._world_size = comm.get_world_size() - - self.dataset_ids = torch.tensor( - [d['dataset_source'] for d in dataset_dicts], dtype=torch.long) - dataset_weight = [torch.ones(s) * max(sizes) / s * r / sum(dataset_ratio) \ - for i, (r, s) in enumerate(zip(dataset_ratio, sizes))] + self.dataset_ids = torch.tensor( + [d["dataset_source"] for d in dataset_dicts], dtype=torch.long + ) + + dataset_weight = [ + torch.ones(s) * max(sizes) / s * r / sum(dataset_ratio) + for i, (r, s) in enumerate(zip(dataset_ratio, sizes, strict=False)) + ] dataset_weight = torch.cat(dataset_weight) - + rfs_factors = [] st = 0 for i, s in enumerate(sizes): if use_rfs[i]: - if dataset_ann[i] == 'box': + if dataset_ann[i] == "box": rfs_func = RepeatFactorTrainingSampler.repeat_factors_from_category_frequency else: rfs_func = repeat_factors_from_tag_frequency - rfs_factor = rfs_func( - dataset_dicts[st: st + s], - repeat_thresh=repeat_threshold) + rfs_factor = rfs_func(dataset_dicts[st : st + s], repeat_thresh=repeat_threshold) rfs_factor = rfs_factor * (s / rfs_factor.sum()) else: rfs_factor = torch.ones(s) @@ -247,37 +244,33 @@ def __init__( self.weights = dataset_weight * rfs_factors self.sample_epoch_size = len(self.weights) - def __iter__(self): + def __iter__(self) -> Iterator: start = self._rank - yield from itertools.islice( - self._infinite_indices(), start, None, self._world_size) - + yield from itertools.islice(self._infinite_indices(), start, None, self._world_size) def _infinite_indices(self): g = torch.Generator() g.manual_seed(self._seed) while True: ids = torch.multinomial( - self.weights, self.sample_epoch_size, generator=g, - replacement=True) - nums = [(self.dataset_ids[ids] == i).sum().int().item() \ - for i in range(len(self.sizes))] + self.weights, self.sample_epoch_size, generator=g, replacement=True + ) + [(self.dataset_ids[ids] == i).sum().int().item() for i in range(len(self.sizes))] yield from ids class MDAspectRatioGroupedDataset(torch.utils.data.IterableDataset): - def __init__(self, dataset, batch_size, num_datasets): - """ - """ + def __init__(self, dataset, batch_size: int, num_datasets: int) -> None: + """ """ self.dataset = dataset self.batch_size = batch_size self._buckets = [[] for _ in range(2 * num_datasets)] - def __iter__(self): + def __iter__(self) -> Iterator: for d in self.dataset: w, h = d["width"], d["height"] aspect_ratio_bucket_id = 0 if w > h else 1 - bucket_id = d['dataset_source'] * 2 + aspect_ratio_bucket_id + bucket_id = d["dataset_source"] * 2 + aspect_ratio_bucket_id bucket = self._buckets[bucket_id] bucket.append(d) if len(bucket) == self.batch_size: @@ -286,31 +279,29 @@ def __iter__(self): class DIFFMDAspectRatioGroupedDataset(torch.utils.data.IterableDataset): - def __init__(self, dataset, batch_sizes, num_datasets): - """ - """ + def __init__(self, dataset, batch_sizes: Sequence[int], num_datasets: int) -> None: + """ """ self.dataset = dataset self.batch_sizes = batch_sizes self._buckets = [[] for _ in range(2 * num_datasets)] - def __iter__(self): + def __iter__(self) -> Iterator: for d in self.dataset: w, h = d["width"], d["height"] aspect_ratio_bucket_id = 0 if w > h else 1 - bucket_id = d['dataset_source'] * 2 + aspect_ratio_bucket_id + bucket_id = d["dataset_source"] * 2 + aspect_ratio_bucket_id bucket = self._buckets[bucket_id] bucket.append(d) - if len(bucket) == self.batch_sizes[d['dataset_source']]: + if len(bucket) == self.batch_sizes[d["dataset_source"]]: yield bucket[:] del bucket[:] def repeat_factors_from_tag_frequency(dataset_dicts, repeat_thresh): - """ - """ + """ """ category_freq = defaultdict(int) for dataset_dict in dataset_dicts: - cat_ids = dataset_dict['pos_category_ids'] + cat_ids = dataset_dict["pos_category_ids"] for cat_id in cat_ids: category_freq[cat_id] += 1 num_images = len(dataset_dicts) @@ -324,8 +315,8 @@ def repeat_factors_from_tag_frequency(dataset_dicts, repeat_thresh): rep_factors = [] for dataset_dict in dataset_dicts: - cat_ids = dataset_dict['pos_category_ids'] + cat_ids = dataset_dict["pos_category_ids"] rep_factor = max({category_rep[cat_id] for cat_id in cat_ids}, default=1.0) rep_factors.append(rep_factor) - return torch.tensor(rep_factors, dtype=torch.float32) \ No newline at end of file + return torch.tensor(rep_factors, dtype=torch.float32) diff --git a/dimos/models/Detic/detic/data/custom_dataset_mapper.py b/dimos/models/Detic/detic/data/custom_dataset_mapper.py index c7727dded3..46c86ffd84 100644 --- a/dimos/models/Detic/detic/data/custom_dataset_mapper.py +++ b/dimos/models/Detic/detic/data/custom_dataset_mapper.py @@ -1,40 +1,41 @@ # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved import copy import logging -import numpy as np -from typing import List, Optional, Union -import torch -import pycocotools.mask as mask_util from detectron2.config import configurable - -from detectron2.data import detection_utils as utils -from detectron2.data.detection_utils import transform_keypoint_annotations -from detectron2.data import transforms as T +from detectron2.data import detection_utils as utils, transforms as T from detectron2.data.dataset_mapper import DatasetMapper -from detectron2.structures import Boxes, BoxMode, Instances -from detectron2.structures import Keypoints, PolygonMasks, BitMasks -from fvcore.transforms.transform import TransformList +import numpy as np +import torch + from .custom_build_augmentation import build_custom_augmentation from .tar_dataset import DiskTarDataset __all__ = ["CustomDatasetMapper"] + class CustomDatasetMapper(DatasetMapper): @configurable - def __init__(self, is_train: bool, - with_ann_type=False, - dataset_ann=[], - use_diff_bs_size=False, - dataset_augs=[], - is_debug=False, - use_tar_dataset=False, - tarfile_path='', - tar_index_dir='', - **kwargs): + def __init__( + self, + is_train: bool, + with_ann_type: bool=False, + dataset_ann=None, + use_diff_bs_size: bool=False, + dataset_augs=None, + is_debug: bool=False, + use_tar_dataset: bool=False, + tarfile_path: str="", + tar_index_dir: str="", + **kwargs, + ) -> None: """ add image labels """ + if dataset_augs is None: + dataset_augs = [] + if dataset_ann is None: + dataset_ann = [] self.with_ann_type = with_ann_type self.dataset_ann = dataset_ann self.use_diff_bs_size = use_diff_bs_size @@ -43,40 +44,42 @@ def __init__(self, is_train: bool, self.is_debug = is_debug self.use_tar_dataset = use_tar_dataset if self.use_tar_dataset: - print('Using tar dataset') + print("Using tar dataset") self.tar_dataset = DiskTarDataset(tarfile_path, tar_index_dir) super().__init__(is_train, **kwargs) - @classmethod def from_config(cls, cfg, is_train: bool = True): ret = super().from_config(cfg, is_train) - ret.update({ - 'with_ann_type': cfg.WITH_IMAGE_LABELS, - 'dataset_ann': cfg.DATALOADER.DATASET_ANN, - 'use_diff_bs_size': cfg.DATALOADER.USE_DIFF_BS_SIZE, - 'is_debug': cfg.IS_DEBUG, - 'use_tar_dataset': cfg.DATALOADER.USE_TAR_DATASET, - 'tarfile_path': cfg.DATALOADER.TARFILE_PATH, - 'tar_index_dir': cfg.DATALOADER.TAR_INDEX_DIR, - }) - if ret['use_diff_bs_size'] and is_train: - if cfg.INPUT.CUSTOM_AUG == 'EfficientDetResizeCrop': + ret.update( + { + "with_ann_type": cfg.WITH_IMAGE_LABELS, + "dataset_ann": cfg.DATALOADER.DATASET_ANN, + "use_diff_bs_size": cfg.DATALOADER.USE_DIFF_BS_SIZE, + "is_debug": cfg.IS_DEBUG, + "use_tar_dataset": cfg.DATALOADER.USE_TAR_DATASET, + "tarfile_path": cfg.DATALOADER.TARFILE_PATH, + "tar_index_dir": cfg.DATALOADER.TAR_INDEX_DIR, + } + ) + if ret["use_diff_bs_size"] and is_train: + if cfg.INPUT.CUSTOM_AUG == "EfficientDetResizeCrop": dataset_scales = cfg.DATALOADER.DATASET_INPUT_SCALE dataset_sizes = cfg.DATALOADER.DATASET_INPUT_SIZE - ret['dataset_augs'] = [ - build_custom_augmentation(cfg, True, scale, size) \ - for scale, size in zip(dataset_scales, dataset_sizes)] + ret["dataset_augs"] = [ + build_custom_augmentation(cfg, True, scale, size) + for scale, size in zip(dataset_scales, dataset_sizes, strict=False) + ] else: - assert cfg.INPUT.CUSTOM_AUG == 'ResizeShortestEdge' + assert cfg.INPUT.CUSTOM_AUG == "ResizeShortestEdge" min_sizes = cfg.DATALOADER.DATASET_MIN_SIZES max_sizes = cfg.DATALOADER.DATASET_MAX_SIZES - ret['dataset_augs'] = [ - build_custom_augmentation( - cfg, True, min_size=mi, max_size=ma) \ - for mi, ma in zip(min_sizes, max_sizes)] + ret["dataset_augs"] = [ + build_custom_augmentation(cfg, True, min_size=mi, max_size=ma) + for mi, ma in zip(min_sizes, max_sizes, strict=False) + ] else: - ret['dataset_augs'] = [] + ret["dataset_augs"] = [] return ret @@ -86,9 +89,8 @@ def __call__(self, dataset_dict): """ dataset_dict = copy.deepcopy(dataset_dict) # it will be modified by code below # USER: Write your own image loading if it's not from a file - if 'file_name' in dataset_dict: - ori_image = utils.read_image( - dataset_dict["file_name"], format=self.image_format) + if "file_name" in dataset_dict: + ori_image = utils.read_image(dataset_dict["file_name"], format=self.image_format) else: ori_image, _, _ = self.tar_dataset[dataset_dict["tar_index"]] ori_image = utils._apply_exif_orientation(ori_image) @@ -97,30 +99,29 @@ def __call__(self, dataset_dict): # USER: Remove if you don't do semantic/panoptic segmentation. if "sem_seg_file_name" in dataset_dict: - sem_seg_gt = utils.read_image( - dataset_dict.pop("sem_seg_file_name"), "L").squeeze(2) + sem_seg_gt = utils.read_image(dataset_dict.pop("sem_seg_file_name"), "L").squeeze(2) else: sem_seg_gt = None if self.is_debug: - dataset_dict['dataset_source'] = 0 + dataset_dict["dataset_source"] = 0 - not_full_labeled = 'dataset_source' in dataset_dict and \ - self.with_ann_type and \ - self.dataset_ann[dataset_dict['dataset_source']] != 'box' + ( + "dataset_source" in dataset_dict + and self.with_ann_type + and self.dataset_ann[dataset_dict["dataset_source"]] != "box" + ) aug_input = T.AugInput(copy.deepcopy(ori_image), sem_seg=sem_seg_gt) if self.use_diff_bs_size and self.is_train: - transforms = \ - self.dataset_augs[dataset_dict['dataset_source']](aug_input) + transforms = self.dataset_augs[dataset_dict["dataset_source"]](aug_input) else: transforms = self.augmentations(aug_input) image, sem_seg_gt = aug_input.image, aug_input.sem_seg image_shape = image.shape[:2] # h, w - dataset_dict["image"] = torch.as_tensor( - np.ascontiguousarray(image.transpose(2, 0, 1))) - + dataset_dict["image"] = torch.as_tensor(np.ascontiguousarray(image.transpose(2, 0, 1))) + if sem_seg_gt is not None: dataset_dict["sem_seg"] = torch.as_tensor(sem_seg_gt.astype("long")) @@ -128,8 +129,7 @@ def __call__(self, dataset_dict): # Most users would not need this feature. if self.proposal_topk is not None: utils.transform_proposals( - dataset_dict, image_shape, transforms, - proposal_topk=self.proposal_topk + dataset_dict, image_shape, transforms, proposal_topk=self.proposal_topk ) if not self.is_train: @@ -148,37 +148,41 @@ def __call__(self, dataset_dict): # USER: Implement additional transformations if you have other types of data all_annos = [ - (utils.transform_instance_annotations( - obj, transforms, image_shape, - keypoint_hflip_indices=self.keypoint_hflip_indices, - ), obj.get("iscrowd", 0)) + ( + utils.transform_instance_annotations( + obj, + transforms, + image_shape, + keypoint_hflip_indices=self.keypoint_hflip_indices, + ), + obj.get("iscrowd", 0), + ) for obj in dataset_dict.pop("annotations") ] annos = [ann[0] for ann in all_annos if ann[1] == 0] instances = utils.annotations_to_instances( annos, image_shape, mask_format=self.instance_mask_format ) - + del all_annos if self.recompute_boxes: instances.gt_boxes = instances.gt_masks.get_bounding_boxes() dataset_dict["instances"] = utils.filter_empty_instances(instances) if self.with_ann_type: - dataset_dict["pos_category_ids"] = dataset_dict.get( - 'pos_category_ids', []) - dataset_dict["ann_type"] = \ - self.dataset_ann[dataset_dict['dataset_source']] - if self.is_debug and (('pos_category_ids' not in dataset_dict) or \ - (dataset_dict['pos_category_ids'] == [])): - dataset_dict['pos_category_ids'] = [x for x in sorted(set( - dataset_dict['instances'].gt_classes.tolist() - ))] + dataset_dict["pos_category_ids"] = dataset_dict.get("pos_category_ids", []) + dataset_dict["ann_type"] = self.dataset_ann[dataset_dict["dataset_source"]] + if self.is_debug and ( + ("pos_category_ids" not in dataset_dict) or (dataset_dict["pos_category_ids"] == []) + ): + dataset_dict["pos_category_ids"] = [ + x for x in sorted(set(dataset_dict["instances"].gt_classes.tolist())) + ] return dataset_dict + # DETR augmentation -def build_transform_gen(cfg, is_train): - """ - """ +def build_transform_gen(cfg, is_train: bool): + """ """ if is_train: min_size = cfg.INPUT.MIN_SIZE_TRAIN max_size = cfg.INPUT.MAX_SIZE_TRAIN @@ -188,7 +192,7 @@ def build_transform_gen(cfg, is_train): max_size = cfg.INPUT.MAX_SIZE_TEST sample_style = "choice" if sample_style == "range": - assert len(min_size) == 2, "more than 2 ({}) min_size(s) are provided for ranges".format(len(min_size)) + assert len(min_size) == 2, f"more than 2 ({len(min_size)}) min_size(s) are provided for ranges" logger = logging.getLogger(__name__) tfm_gens = [] @@ -211,7 +215,7 @@ class DetrDatasetMapper: 4. Prepare image and annotation to Tensors """ - def __init__(self, cfg, is_train=True): + def __init__(self, cfg, is_train: bool=True) -> None: if cfg.INPUT.CROP.ENABLED and is_train: self.crop_gen = [ T.ResizeShortestEdge([400, 500, 600], sample_style="choice"), @@ -223,7 +227,7 @@ def __init__(self, cfg, is_train=True): self.mask_on = cfg.MODEL.MASK_ON self.tfm_gens = build_transform_gen(cfg, is_train) logging.getLogger(__name__).info( - "Full TransformGens used in training: {}, crop: {}".format(str(self.tfm_gens), str(self.crop_gen)) + f"Full TransformGens used in training: {self.tfm_gens!s}, crop: {self.crop_gen!s}" ) self.img_format = cfg.INPUT.FORMAT @@ -277,4 +281,4 @@ def __call__(self, dataset_dict): ] instances = utils.annotations_to_instances(annos, image_shape) dataset_dict["instances"] = utils.filter_empty_instances(instances) - return dataset_dict \ No newline at end of file + return dataset_dict diff --git a/dimos/models/Detic/detic/data/datasets/cc.py b/dimos/models/Detic/detic/data/datasets/cc.py index 7c3e50726f..be9c7f4a8b 100644 --- a/dimos/models/Detic/detic/data/datasets/cc.py +++ b/dimos/models/Detic/detic/data/datasets/cc.py @@ -1,23 +1,20 @@ # Copyright (c) Facebook, Inc. and its affiliates. -import logging import os -from detectron2.data.datasets.builtin_meta import _get_builtin_metadata from detectron2.data.datasets.lvis import get_lvis_instances_meta + from .lvis_v1 import custom_register_lvis_instances _CUSTOM_SPLITS = { "cc3m_v1_val": ("cc3m/validation/", "cc3m/val_image_info.json"), "cc3m_v1_train": ("cc3m/training/", "cc3m/train_image_info.json"), "cc3m_v1_train_tags": ("cc3m/training/", "cc3m/train_image_info_tags.json"), - } for key, (image_root, json_file) in _CUSTOM_SPLITS.items(): custom_register_lvis_instances( key, - get_lvis_instances_meta('lvis_v1'), + get_lvis_instances_meta("lvis_v1"), os.path.join("datasets", json_file) if "://" not in json_file else json_file, os.path.join("datasets", image_root), ) - diff --git a/dimos/models/Detic/detic/data/datasets/coco_zeroshot.py b/dimos/models/Detic/detic/data/datasets/coco_zeroshot.py index aee895de41..80c360593d 100644 --- a/dimos/models/Detic/detic/data/datasets/coco_zeroshot.py +++ b/dimos/models/Detic/detic/data/datasets/coco_zeroshot.py @@ -1,103 +1,126 @@ # Copyright (c) Facebook, Inc. and its affiliates. import os -from detectron2.data.datasets.register_coco import register_coco_instances from detectron2.data.datasets.builtin_meta import _get_builtin_metadata +from detectron2.data.datasets.register_coco import register_coco_instances + from .lvis_v1 import custom_register_lvis_instances categories_seen = [ - {'id': 1, 'name': 'person'}, - {'id': 2, 'name': 'bicycle'}, - {'id': 3, 'name': 'car'}, - {'id': 4, 'name': 'motorcycle'}, - {'id': 7, 'name': 'train'}, - {'id': 8, 'name': 'truck'}, - {'id': 9, 'name': 'boat'}, - {'id': 15, 'name': 'bench'}, - {'id': 16, 'name': 'bird'}, - {'id': 19, 'name': 'horse'}, - {'id': 20, 'name': 'sheep'}, - {'id': 23, 'name': 'bear'}, - {'id': 24, 'name': 'zebra'}, - {'id': 25, 'name': 'giraffe'}, - {'id': 27, 'name': 'backpack'}, - {'id': 31, 'name': 'handbag'}, - {'id': 33, 'name': 'suitcase'}, - {'id': 34, 'name': 'frisbee'}, - {'id': 35, 'name': 'skis'}, - {'id': 38, 'name': 'kite'}, - {'id': 42, 'name': 'surfboard'}, - {'id': 44, 'name': 'bottle'}, - {'id': 48, 'name': 'fork'}, - {'id': 50, 'name': 'spoon'}, - {'id': 51, 'name': 'bowl'}, - {'id': 52, 'name': 'banana'}, - {'id': 53, 'name': 'apple'}, - {'id': 54, 'name': 'sandwich'}, - {'id': 55, 'name': 'orange'}, - {'id': 56, 'name': 'broccoli'}, - {'id': 57, 'name': 'carrot'}, - {'id': 59, 'name': 'pizza'}, - {'id': 60, 'name': 'donut'}, - {'id': 62, 'name': 'chair'}, - {'id': 65, 'name': 'bed'}, - {'id': 70, 'name': 'toilet'}, - {'id': 72, 'name': 'tv'}, - {'id': 73, 'name': 'laptop'}, - {'id': 74, 'name': 'mouse'}, - {'id': 75, 'name': 'remote'}, - {'id': 78, 'name': 'microwave'}, - {'id': 79, 'name': 'oven'}, - {'id': 80, 'name': 'toaster'}, - {'id': 82, 'name': 'refrigerator'}, - {'id': 84, 'name': 'book'}, - {'id': 85, 'name': 'clock'}, - {'id': 86, 'name': 'vase'}, - {'id': 90, 'name': 'toothbrush'}, + {"id": 1, "name": "person"}, + {"id": 2, "name": "bicycle"}, + {"id": 3, "name": "car"}, + {"id": 4, "name": "motorcycle"}, + {"id": 7, "name": "train"}, + {"id": 8, "name": "truck"}, + {"id": 9, "name": "boat"}, + {"id": 15, "name": "bench"}, + {"id": 16, "name": "bird"}, + {"id": 19, "name": "horse"}, + {"id": 20, "name": "sheep"}, + {"id": 23, "name": "bear"}, + {"id": 24, "name": "zebra"}, + {"id": 25, "name": "giraffe"}, + {"id": 27, "name": "backpack"}, + {"id": 31, "name": "handbag"}, + {"id": 33, "name": "suitcase"}, + {"id": 34, "name": "frisbee"}, + {"id": 35, "name": "skis"}, + {"id": 38, "name": "kite"}, + {"id": 42, "name": "surfboard"}, + {"id": 44, "name": "bottle"}, + {"id": 48, "name": "fork"}, + {"id": 50, "name": "spoon"}, + {"id": 51, "name": "bowl"}, + {"id": 52, "name": "banana"}, + {"id": 53, "name": "apple"}, + {"id": 54, "name": "sandwich"}, + {"id": 55, "name": "orange"}, + {"id": 56, "name": "broccoli"}, + {"id": 57, "name": "carrot"}, + {"id": 59, "name": "pizza"}, + {"id": 60, "name": "donut"}, + {"id": 62, "name": "chair"}, + {"id": 65, "name": "bed"}, + {"id": 70, "name": "toilet"}, + {"id": 72, "name": "tv"}, + {"id": 73, "name": "laptop"}, + {"id": 74, "name": "mouse"}, + {"id": 75, "name": "remote"}, + {"id": 78, "name": "microwave"}, + {"id": 79, "name": "oven"}, + {"id": 80, "name": "toaster"}, + {"id": 82, "name": "refrigerator"}, + {"id": 84, "name": "book"}, + {"id": 85, "name": "clock"}, + {"id": 86, "name": "vase"}, + {"id": 90, "name": "toothbrush"}, ] categories_unseen = [ - {'id': 5, 'name': 'airplane'}, - {'id': 6, 'name': 'bus'}, - {'id': 17, 'name': 'cat'}, - {'id': 18, 'name': 'dog'}, - {'id': 21, 'name': 'cow'}, - {'id': 22, 'name': 'elephant'}, - {'id': 28, 'name': 'umbrella'}, - {'id': 32, 'name': 'tie'}, - {'id': 36, 'name': 'snowboard'}, - {'id': 41, 'name': 'skateboard'}, - {'id': 47, 'name': 'cup'}, - {'id': 49, 'name': 'knife'}, - {'id': 61, 'name': 'cake'}, - {'id': 63, 'name': 'couch'}, - {'id': 76, 'name': 'keyboard'}, - {'id': 81, 'name': 'sink'}, - {'id': 87, 'name': 'scissors'}, + {"id": 5, "name": "airplane"}, + {"id": 6, "name": "bus"}, + {"id": 17, "name": "cat"}, + {"id": 18, "name": "dog"}, + {"id": 21, "name": "cow"}, + {"id": 22, "name": "elephant"}, + {"id": 28, "name": "umbrella"}, + {"id": 32, "name": "tie"}, + {"id": 36, "name": "snowboard"}, + {"id": 41, "name": "skateboard"}, + {"id": 47, "name": "cup"}, + {"id": 49, "name": "knife"}, + {"id": 61, "name": "cake"}, + {"id": 63, "name": "couch"}, + {"id": 76, "name": "keyboard"}, + {"id": 81, "name": "sink"}, + {"id": 87, "name": "scissors"}, ] + def _get_metadata(cat): - if cat == 'all': - return _get_builtin_metadata('coco') - elif cat == 'seen': - id_to_name = {x['id']: x['name'] for x in categories_seen} + if cat == "all": + return _get_builtin_metadata("coco") + elif cat == "seen": + id_to_name = {x["id"]: x["name"] for x in categories_seen} else: - assert cat == 'unseen' - id_to_name = {x['id']: x['name'] for x in categories_unseen} + assert cat == "unseen" + id_to_name = {x["id"]: x["name"] for x in categories_unseen} - thing_dataset_id_to_contiguous_id = { - x: i for i, x in enumerate(sorted(id_to_name))} + thing_dataset_id_to_contiguous_id = {x: i for i, x in enumerate(sorted(id_to_name))} thing_classes = [id_to_name[k] for k in sorted(id_to_name)] return { "thing_dataset_id_to_contiguous_id": thing_dataset_id_to_contiguous_id, - "thing_classes": thing_classes} + "thing_classes": thing_classes, + } + _PREDEFINED_SPLITS_COCO = { - "coco_zeroshot_train": ("coco/train2017", "coco/zero-shot/instances_train2017_seen_2.json", 'seen'), - "coco_zeroshot_val": ("coco/val2017", "coco/zero-shot/instances_val2017_unseen_2.json", 'unseen'), - "coco_not_zeroshot_val": ("coco/val2017", "coco/zero-shot/instances_val2017_seen_2.json", 'seen'), - "coco_generalized_zeroshot_val": ("coco/val2017", "coco/zero-shot/instances_val2017_all_2_oriorder.json", 'all'), - "coco_zeroshot_train_oriorder": ("coco/train2017", "coco/zero-shot/instances_train2017_seen_2_oriorder.json", 'all'), + "coco_zeroshot_train": ( + "coco/train2017", + "coco/zero-shot/instances_train2017_seen_2.json", + "seen", + ), + "coco_zeroshot_val": ( + "coco/val2017", + "coco/zero-shot/instances_val2017_unseen_2.json", + "unseen", + ), + "coco_not_zeroshot_val": ( + "coco/val2017", + "coco/zero-shot/instances_val2017_seen_2.json", + "seen", + ), + "coco_generalized_zeroshot_val": ( + "coco/val2017", + "coco/zero-shot/instances_val2017_all_2_oriorder.json", + "all", + ), + "coco_zeroshot_train_oriorder": ( + "coco/train2017", + "coco/zero-shot/instances_train2017_seen_2_oriorder.json", + "all", + ), } for key, (image_root, json_file, cat) in _PREDEFINED_SPLITS_COCO.items(): @@ -110,12 +133,16 @@ def _get_metadata(cat): _CUSTOM_SPLITS_COCO = { "cc3m_coco_train_tags": ("cc3m/training/", "cc3m/coco_train_image_info_tags.json"), - "coco_caption_train_tags": ("coco/train2017/", "coco/annotations/captions_train2017_tags_allcaps.json"),} + "coco_caption_train_tags": ( + "coco/train2017/", + "coco/annotations/captions_train2017_tags_allcaps.json", + ), +} for key, (image_root, json_file) in _CUSTOM_SPLITS_COCO.items(): custom_register_lvis_instances( key, - _get_builtin_metadata('coco'), + _get_builtin_metadata("coco"), os.path.join("datasets", json_file) if "://" not in json_file else json_file, os.path.join("datasets", image_root), - ) \ No newline at end of file + ) diff --git a/dimos/models/Detic/detic/data/datasets/imagenet.py b/dimos/models/Detic/detic/data/datasets/imagenet.py index 9b6d78e51f..caa7aa8fe0 100644 --- a/dimos/models/Detic/detic/data/datasets/imagenet.py +++ b/dimos/models/Detic/detic/data/datasets/imagenet.py @@ -1,35 +1,41 @@ # Copyright (c) Facebook, Inc. and its affiliates. -import logging import os from detectron2.data import DatasetCatalog, MetadataCatalog from detectron2.data.datasets.lvis import get_lvis_instances_meta + from .lvis_v1 import custom_load_lvis_json, get_lvis_22k_meta -def custom_register_imagenet_instances(name, metadata, json_file, image_root): - """ - """ - DatasetCatalog.register(name, lambda: custom_load_lvis_json( - json_file, image_root, name)) + + +def custom_register_imagenet_instances(name: str, metadata, json_file, image_root) -> None: + """ """ + DatasetCatalog.register(name, lambda: custom_load_lvis_json(json_file, image_root, name)) MetadataCatalog.get(name).set( - json_file=json_file, image_root=image_root, - evaluator_type="imagenet", **metadata + json_file=json_file, image_root=image_root, evaluator_type="imagenet", **metadata ) + _CUSTOM_SPLITS_IMAGENET = { - "imagenet_lvis_v1": ("imagenet/ImageNet-LVIS/", "imagenet/annotations/imagenet_lvis_image_info.json"), + "imagenet_lvis_v1": ( + "imagenet/ImageNet-LVIS/", + "imagenet/annotations/imagenet_lvis_image_info.json", + ), } for key, (image_root, json_file) in _CUSTOM_SPLITS_IMAGENET.items(): custom_register_imagenet_instances( key, - get_lvis_instances_meta('lvis_v1'), + get_lvis_instances_meta("lvis_v1"), os.path.join("datasets", json_file) if "://" not in json_file else json_file, os.path.join("datasets", image_root), ) _CUSTOM_SPLITS_IMAGENET_22K = { - "imagenet_lvis-22k": ("imagenet/ImageNet-LVIS/", "imagenet/annotations/imagenet-22k_image_info_lvis-22k.json"), + "imagenet_lvis-22k": ( + "imagenet/ImageNet-LVIS/", + "imagenet/annotations/imagenet-22k_image_info_lvis-22k.json", + ), } for key, (image_root, json_file) in _CUSTOM_SPLITS_IMAGENET_22K.items(): @@ -38,4 +44,4 @@ def custom_register_imagenet_instances(name, metadata, json_file, image_root): get_lvis_22k_meta(), os.path.join("datasets", json_file) if "://" not in json_file else json_file, os.path.join("datasets", image_root), - ) \ No newline at end of file + ) diff --git a/dimos/models/Detic/detic/data/datasets/lvis_22k_categories.py b/dimos/models/Detic/detic/data/datasets/lvis_22k_categories.py index 9525f0873d..d1b3cc370a 100644 --- a/dimos/models/Detic/detic/data/datasets/lvis_22k_categories.py +++ b/dimos/models/Detic/detic/data/datasets/lvis_22k_categories.py @@ -1 +1,22383 @@ -CATEGORIES = [{'name': 'aerosol_can', 'id': 1, 'frequency': 'c', 'synset': 'aerosol.n.02'}, {'name': 'air_conditioner', 'id': 2, 'frequency': 'f', 'synset': 'air_conditioner.n.01'}, {'name': 'airplane', 'id': 3, 'frequency': 'f', 'synset': 'airplane.n.01'}, {'name': 'alarm_clock', 'id': 4, 'frequency': 'f', 'synset': 'alarm_clock.n.01'}, {'name': 'alcohol', 'id': 5, 'frequency': 'c', 'synset': 'alcohol.n.01'}, {'name': 'alligator', 'id': 6, 'frequency': 'c', 'synset': 'alligator.n.02'}, {'name': 'almond', 'id': 7, 'frequency': 'c', 'synset': 'almond.n.02'}, {'name': 'ambulance', 'id': 8, 'frequency': 'c', 'synset': 'ambulance.n.01'}, {'name': 'amplifier', 'id': 9, 'frequency': 'c', 'synset': 'amplifier.n.01'}, {'name': 'anklet', 'id': 10, 'frequency': 'c', 'synset': 'anklet.n.03'}, {'name': 'antenna', 'id': 11, 'frequency': 'f', 'synset': 'antenna.n.01'}, {'name': 'apple', 'id': 12, 'frequency': 'f', 'synset': 'apple.n.01'}, {'name': 'applesauce', 'id': 13, 'frequency': 'r', 'synset': 'applesauce.n.01'}, {'name': 'apricot', 'id': 14, 'frequency': 'r', 'synset': 'apricot.n.02'}, {'name': 'apron', 'id': 15, 'frequency': 'f', 'synset': 'apron.n.01'}, {'name': 'aquarium', 'id': 16, 'frequency': 'c', 'synset': 'aquarium.n.01'}, {'name': 'arctic_(type_of_shoe)', 'id': 17, 'frequency': 'r', 'synset': 'arctic.n.02'}, {'name': 'armband', 'id': 18, 'frequency': 'c', 'synset': 'armband.n.02'}, {'name': 'armchair', 'id': 19, 'frequency': 'f', 'synset': 'armchair.n.01'}, {'name': 'armoire', 'id': 20, 'frequency': 'r', 'synset': 'armoire.n.01'}, {'name': 'armor', 'id': 21, 'frequency': 'r', 'synset': 'armor.n.01'}, {'name': 'artichoke', 'id': 22, 'frequency': 'c', 'synset': 'artichoke.n.02'}, {'name': 'trash_can', 'id': 23, 'frequency': 'f', 'synset': 'ashcan.n.01'}, {'name': 'ashtray', 'id': 24, 'frequency': 'c', 'synset': 'ashtray.n.01'}, {'name': 'asparagus', 'id': 25, 'frequency': 'c', 'synset': 'asparagus.n.02'}, {'name': 'atomizer', 'id': 26, 'frequency': 'c', 'synset': 'atomizer.n.01'}, {'name': 'avocado', 'id': 27, 'frequency': 'f', 'synset': 'avocado.n.01'}, {'name': 'award', 'id': 28, 'frequency': 'c', 'synset': 'award.n.02'}, {'name': 'awning', 'id': 29, 'frequency': 'f', 'synset': 'awning.n.01'}, {'name': 'ax', 'id': 30, 'frequency': 'r', 'synset': 'ax.n.01'}, {'name': 'baboon', 'id': 31, 'frequency': 'r', 'synset': 'baboon.n.01'}, {'name': 'baby_buggy', 'id': 32, 'frequency': 'f', 'synset': 'baby_buggy.n.01'}, {'name': 'basketball_backboard', 'id': 33, 'frequency': 'c', 'synset': 'backboard.n.01'}, {'name': 'backpack', 'id': 34, 'frequency': 'f', 'synset': 'backpack.n.01'}, {'name': 'handbag', 'id': 35, 'frequency': 'f', 'synset': 'bag.n.04'}, {'name': 'suitcase', 'id': 36, 'frequency': 'f', 'synset': 'bag.n.06'}, {'name': 'bagel', 'id': 37, 'frequency': 'c', 'synset': 'bagel.n.01'}, {'name': 'bagpipe', 'id': 38, 'frequency': 'r', 'synset': 'bagpipe.n.01'}, {'name': 'baguet', 'id': 39, 'frequency': 'r', 'synset': 'baguet.n.01'}, {'name': 'bait', 'id': 40, 'frequency': 'r', 'synset': 'bait.n.02'}, {'name': 'ball', 'id': 41, 'frequency': 'f', 'synset': 'ball.n.06'}, {'name': 'ballet_skirt', 'id': 42, 'frequency': 'r', 'synset': 'ballet_skirt.n.01'}, {'name': 'balloon', 'id': 43, 'frequency': 'f', 'synset': 'balloon.n.01'}, {'name': 'bamboo', 'id': 44, 'frequency': 'c', 'synset': 'bamboo.n.02'}, {'name': 'banana', 'id': 45, 'frequency': 'f', 'synset': 'banana.n.02'}, {'name': 'Band_Aid', 'id': 46, 'frequency': 'c', 'synset': 'band_aid.n.01'}, {'name': 'bandage', 'id': 47, 'frequency': 'c', 'synset': 'bandage.n.01'}, {'name': 'bandanna', 'id': 48, 'frequency': 'f', 'synset': 'bandanna.n.01'}, {'name': 'banjo', 'id': 49, 'frequency': 'r', 'synset': 'banjo.n.01'}, {'name': 'banner', 'id': 50, 'frequency': 'f', 'synset': 'banner.n.01'}, {'name': 'barbell', 'id': 51, 'frequency': 'r', 'synset': 'barbell.n.01'}, {'name': 'barge', 'id': 52, 'frequency': 'r', 'synset': 'barge.n.01'}, {'name': 'barrel', 'id': 53, 'frequency': 'f', 'synset': 'barrel.n.02'}, {'name': 'barrette', 'id': 54, 'frequency': 'c', 'synset': 'barrette.n.01'}, {'name': 'barrow', 'id': 55, 'frequency': 'c', 'synset': 'barrow.n.03'}, {'name': 'baseball_base', 'id': 56, 'frequency': 'f', 'synset': 'base.n.03'}, {'name': 'baseball', 'id': 57, 'frequency': 'f', 'synset': 'baseball.n.02'}, {'name': 'baseball_bat', 'id': 58, 'frequency': 'f', 'synset': 'baseball_bat.n.01'}, {'name': 'baseball_cap', 'id': 59, 'frequency': 'f', 'synset': 'baseball_cap.n.01'}, {'name': 'baseball_glove', 'id': 60, 'frequency': 'f', 'synset': 'baseball_glove.n.01'}, {'name': 'basket', 'id': 61, 'frequency': 'f', 'synset': 'basket.n.01'}, {'name': 'basketball', 'id': 62, 'frequency': 'c', 'synset': 'basketball.n.02'}, {'name': 'bass_horn', 'id': 63, 'frequency': 'r', 'synset': 'bass_horn.n.01'}, {'name': 'bat_(animal)', 'id': 64, 'frequency': 'c', 'synset': 'bat.n.01'}, {'name': 'bath_mat', 'id': 65, 'frequency': 'f', 'synset': 'bath_mat.n.01'}, {'name': 'bath_towel', 'id': 66, 'frequency': 'f', 'synset': 'bath_towel.n.01'}, {'name': 'bathrobe', 'id': 67, 'frequency': 'c', 'synset': 'bathrobe.n.01'}, {'name': 'bathtub', 'id': 68, 'frequency': 'f', 'synset': 'bathtub.n.01'}, {'name': 'batter_(food)', 'id': 69, 'frequency': 'r', 'synset': 'batter.n.02'}, {'name': 'battery', 'id': 70, 'frequency': 'c', 'synset': 'battery.n.02'}, {'name': 'beachball', 'id': 71, 'frequency': 'r', 'synset': 'beach_ball.n.01'}, {'name': 'bead', 'id': 72, 'frequency': 'c', 'synset': 'bead.n.01'}, {'name': 'bean_curd', 'id': 73, 'frequency': 'c', 'synset': 'bean_curd.n.01'}, {'name': 'beanbag', 'id': 74, 'frequency': 'c', 'synset': 'beanbag.n.01'}, {'name': 'beanie', 'id': 75, 'frequency': 'f', 'synset': 'beanie.n.01'}, {'name': 'bear', 'id': 76, 'frequency': 'f', 'synset': 'bear.n.01'}, {'name': 'bed', 'id': 77, 'frequency': 'f', 'synset': 'bed.n.01'}, {'name': 'bedpan', 'id': 78, 'frequency': 'r', 'synset': 'bedpan.n.01'}, {'name': 'bedspread', 'id': 79, 'frequency': 'f', 'synset': 'bedspread.n.01'}, {'name': 'cow', 'id': 80, 'frequency': 'f', 'synset': 'beef.n.01'}, {'name': 'beef_(food)', 'id': 81, 'frequency': 'f', 'synset': 'beef.n.02'}, {'name': 'beeper', 'id': 82, 'frequency': 'r', 'synset': 'beeper.n.01'}, {'name': 'beer_bottle', 'id': 83, 'frequency': 'f', 'synset': 'beer_bottle.n.01'}, {'name': 'beer_can', 'id': 84, 'frequency': 'c', 'synset': 'beer_can.n.01'}, {'name': 'beetle', 'id': 85, 'frequency': 'r', 'synset': 'beetle.n.01'}, {'name': 'bell', 'id': 86, 'frequency': 'f', 'synset': 'bell.n.01'}, {'name': 'bell_pepper', 'id': 87, 'frequency': 'f', 'synset': 'bell_pepper.n.02'}, {'name': 'belt', 'id': 88, 'frequency': 'f', 'synset': 'belt.n.02'}, {'name': 'belt_buckle', 'id': 89, 'frequency': 'f', 'synset': 'belt_buckle.n.01'}, {'name': 'bench', 'id': 90, 'frequency': 'f', 'synset': 'bench.n.01'}, {'name': 'beret', 'id': 91, 'frequency': 'c', 'synset': 'beret.n.01'}, {'name': 'bib', 'id': 92, 'frequency': 'c', 'synset': 'bib.n.02'}, {'name': 'Bible', 'id': 93, 'frequency': 'r', 'synset': 'bible.n.01'}, {'name': 'bicycle', 'id': 94, 'frequency': 'f', 'synset': 'bicycle.n.01'}, {'name': 'visor', 'id': 95, 'frequency': 'f', 'synset': 'bill.n.09'}, {'name': 'billboard', 'id': 96, 'frequency': 'f', 'synset': 'billboard.n.01'}, {'name': 'binder', 'id': 97, 'frequency': 'c', 'synset': 'binder.n.03'}, {'name': 'binoculars', 'id': 98, 'frequency': 'c', 'synset': 'binoculars.n.01'}, {'name': 'bird', 'id': 99, 'frequency': 'f', 'synset': 'bird.n.01'}, {'name': 'birdfeeder', 'id': 100, 'frequency': 'c', 'synset': 'bird_feeder.n.01'}, {'name': 'birdbath', 'id': 101, 'frequency': 'c', 'synset': 'birdbath.n.01'}, {'name': 'birdcage', 'id': 102, 'frequency': 'c', 'synset': 'birdcage.n.01'}, {'name': 'birdhouse', 'id': 103, 'frequency': 'c', 'synset': 'birdhouse.n.01'}, {'name': 'birthday_cake', 'id': 104, 'frequency': 'f', 'synset': 'birthday_cake.n.01'}, {'name': 'birthday_card', 'id': 105, 'frequency': 'r', 'synset': 'birthday_card.n.01'}, {'name': 'pirate_flag', 'id': 106, 'frequency': 'r', 'synset': 'black_flag.n.01'}, {'name': 'black_sheep', 'id': 107, 'frequency': 'c', 'synset': 'black_sheep.n.02'}, {'name': 'blackberry', 'id': 108, 'frequency': 'c', 'synset': 'blackberry.n.01'}, {'name': 'blackboard', 'id': 109, 'frequency': 'f', 'synset': 'blackboard.n.01'}, {'name': 'blanket', 'id': 110, 'frequency': 'f', 'synset': 'blanket.n.01'}, {'name': 'blazer', 'id': 111, 'frequency': 'c', 'synset': 'blazer.n.01'}, {'name': 'blender', 'id': 112, 'frequency': 'f', 'synset': 'blender.n.01'}, {'name': 'blimp', 'id': 113, 'frequency': 'r', 'synset': 'blimp.n.02'}, {'name': 'blinker', 'id': 114, 'frequency': 'f', 'synset': 'blinker.n.01'}, {'name': 'blouse', 'id': 115, 'frequency': 'f', 'synset': 'blouse.n.01'}, {'name': 'blueberry', 'id': 116, 'frequency': 'f', 'synset': 'blueberry.n.02'}, {'name': 'gameboard', 'id': 117, 'frequency': 'r', 'synset': 'board.n.09'}, {'name': 'boat', 'id': 118, 'frequency': 'f', 'synset': 'boat.n.01'}, {'name': 'bob', 'id': 119, 'frequency': 'r', 'synset': 'bob.n.05'}, {'name': 'bobbin', 'id': 120, 'frequency': 'c', 'synset': 'bobbin.n.01'}, {'name': 'bobby_pin', 'id': 121, 'frequency': 'c', 'synset': 'bobby_pin.n.01'}, {'name': 'boiled_egg', 'id': 122, 'frequency': 'c', 'synset': 'boiled_egg.n.01'}, {'name': 'bolo_tie', 'id': 123, 'frequency': 'r', 'synset': 'bolo_tie.n.01'}, {'name': 'deadbolt', 'id': 124, 'frequency': 'c', 'synset': 'bolt.n.03'}, {'name': 'bolt', 'id': 125, 'frequency': 'f', 'synset': 'bolt.n.06'}, {'name': 'bonnet', 'id': 126, 'frequency': 'r', 'synset': 'bonnet.n.01'}, {'name': 'book', 'id': 127, 'frequency': 'f', 'synset': 'book.n.01'}, {'name': 'bookcase', 'id': 128, 'frequency': 'c', 'synset': 'bookcase.n.01'}, {'name': 'booklet', 'id': 129, 'frequency': 'c', 'synset': 'booklet.n.01'}, {'name': 'bookmark', 'id': 130, 'frequency': 'r', 'synset': 'bookmark.n.01'}, {'name': 'boom_microphone', 'id': 131, 'frequency': 'r', 'synset': 'boom.n.04'}, {'name': 'boot', 'id': 132, 'frequency': 'f', 'synset': 'boot.n.01'}, {'name': 'bottle', 'id': 133, 'frequency': 'f', 'synset': 'bottle.n.01'}, {'name': 'bottle_opener', 'id': 134, 'frequency': 'c', 'synset': 'bottle_opener.n.01'}, {'name': 'bouquet', 'id': 135, 'frequency': 'c', 'synset': 'bouquet.n.01'}, {'name': 'bow_(weapon)', 'id': 136, 'frequency': 'r', 'synset': 'bow.n.04'}, {'name': 'bow_(decorative_ribbons)', 'id': 137, 'frequency': 'f', 'synset': 'bow.n.08'}, {'name': 'bow-tie', 'id': 138, 'frequency': 'f', 'synset': 'bow_tie.n.01'}, {'name': 'bowl', 'id': 139, 'frequency': 'f', 'synset': 'bowl.n.03'}, {'name': 'pipe_bowl', 'id': 140, 'frequency': 'r', 'synset': 'bowl.n.08'}, {'name': 'bowler_hat', 'id': 141, 'frequency': 'c', 'synset': 'bowler_hat.n.01'}, {'name': 'bowling_ball', 'id': 142, 'frequency': 'r', 'synset': 'bowling_ball.n.01'}, {'name': 'box', 'id': 143, 'frequency': 'f', 'synset': 'box.n.01'}, {'name': 'boxing_glove', 'id': 144, 'frequency': 'r', 'synset': 'boxing_glove.n.01'}, {'name': 'suspenders', 'id': 145, 'frequency': 'c', 'synset': 'brace.n.06'}, {'name': 'bracelet', 'id': 146, 'frequency': 'f', 'synset': 'bracelet.n.02'}, {'name': 'brass_plaque', 'id': 147, 'frequency': 'r', 'synset': 'brass.n.07'}, {'name': 'brassiere', 'id': 148, 'frequency': 'c', 'synset': 'brassiere.n.01'}, {'name': 'bread-bin', 'id': 149, 'frequency': 'c', 'synset': 'bread-bin.n.01'}, {'name': 'bread', 'id': 150, 'frequency': 'f', 'synset': 'bread.n.01'}, {'name': 'breechcloth', 'id': 151, 'frequency': 'r', 'synset': 'breechcloth.n.01'}, {'name': 'bridal_gown', 'id': 152, 'frequency': 'f', 'synset': 'bridal_gown.n.01'}, {'name': 'briefcase', 'id': 153, 'frequency': 'c', 'synset': 'briefcase.n.01'}, {'name': 'broccoli', 'id': 154, 'frequency': 'f', 'synset': 'broccoli.n.01'}, {'name': 'broach', 'id': 155, 'frequency': 'r', 'synset': 'brooch.n.01'}, {'name': 'broom', 'id': 156, 'frequency': 'c', 'synset': 'broom.n.01'}, {'name': 'brownie', 'id': 157, 'frequency': 'c', 'synset': 'brownie.n.03'}, {'name': 'brussels_sprouts', 'id': 158, 'frequency': 'c', 'synset': 'brussels_sprouts.n.01'}, {'name': 'bubble_gum', 'id': 159, 'frequency': 'r', 'synset': 'bubble_gum.n.01'}, {'name': 'bucket', 'id': 160, 'frequency': 'f', 'synset': 'bucket.n.01'}, {'name': 'horse_buggy', 'id': 161, 'frequency': 'r', 'synset': 'buggy.n.01'}, {'name': 'bull', 'id': 162, 'frequency': 'c', 'synset': 'bull.n.11'}, {'name': 'bulldog', 'id': 163, 'frequency': 'c', 'synset': 'bulldog.n.01'}, {'name': 'bulldozer', 'id': 164, 'frequency': 'r', 'synset': 'bulldozer.n.01'}, {'name': 'bullet_train', 'id': 165, 'frequency': 'c', 'synset': 'bullet_train.n.01'}, {'name': 'bulletin_board', 'id': 166, 'frequency': 'c', 'synset': 'bulletin_board.n.02'}, {'name': 'bulletproof_vest', 'id': 167, 'frequency': 'r', 'synset': 'bulletproof_vest.n.01'}, {'name': 'bullhorn', 'id': 168, 'frequency': 'c', 'synset': 'bullhorn.n.01'}, {'name': 'bun', 'id': 169, 'frequency': 'f', 'synset': 'bun.n.01'}, {'name': 'bunk_bed', 'id': 170, 'frequency': 'c', 'synset': 'bunk_bed.n.01'}, {'name': 'buoy', 'id': 171, 'frequency': 'f', 'synset': 'buoy.n.01'}, {'name': 'burrito', 'id': 172, 'frequency': 'r', 'synset': 'burrito.n.01'}, {'name': 'bus_(vehicle)', 'id': 173, 'frequency': 'f', 'synset': 'bus.n.01'}, {'name': 'business_card', 'id': 174, 'frequency': 'c', 'synset': 'business_card.n.01'}, {'name': 'butter', 'id': 175, 'frequency': 'f', 'synset': 'butter.n.01'}, {'name': 'butterfly', 'id': 176, 'frequency': 'c', 'synset': 'butterfly.n.01'}, {'name': 'button', 'id': 177, 'frequency': 'f', 'synset': 'button.n.01'}, {'name': 'cab_(taxi)', 'id': 178, 'frequency': 'f', 'synset': 'cab.n.03'}, {'name': 'cabana', 'id': 179, 'frequency': 'r', 'synset': 'cabana.n.01'}, {'name': 'cabin_car', 'id': 180, 'frequency': 'c', 'synset': 'cabin_car.n.01'}, {'name': 'cabinet', 'id': 181, 'frequency': 'f', 'synset': 'cabinet.n.01'}, {'name': 'locker', 'id': 182, 'frequency': 'r', 'synset': 'cabinet.n.03'}, {'name': 'cake', 'id': 183, 'frequency': 'f', 'synset': 'cake.n.03'}, {'name': 'calculator', 'id': 184, 'frequency': 'c', 'synset': 'calculator.n.02'}, {'name': 'calendar', 'id': 185, 'frequency': 'f', 'synset': 'calendar.n.02'}, {'name': 'calf', 'id': 186, 'frequency': 'c', 'synset': 'calf.n.01'}, {'name': 'camcorder', 'id': 187, 'frequency': 'c', 'synset': 'camcorder.n.01'}, {'name': 'camel', 'id': 188, 'frequency': 'c', 'synset': 'camel.n.01'}, {'name': 'camera', 'id': 189, 'frequency': 'f', 'synset': 'camera.n.01'}, {'name': 'camera_lens', 'id': 190, 'frequency': 'c', 'synset': 'camera_lens.n.01'}, {'name': 'camper_(vehicle)', 'id': 191, 'frequency': 'c', 'synset': 'camper.n.02'}, {'name': 'can', 'id': 192, 'frequency': 'f', 'synset': 'can.n.01'}, {'name': 'can_opener', 'id': 193, 'frequency': 'c', 'synset': 'can_opener.n.01'}, {'name': 'candle', 'id': 194, 'frequency': 'f', 'synset': 'candle.n.01'}, {'name': 'candle_holder', 'id': 195, 'frequency': 'f', 'synset': 'candlestick.n.01'}, {'name': 'candy_bar', 'id': 196, 'frequency': 'r', 'synset': 'candy_bar.n.01'}, {'name': 'candy_cane', 'id': 197, 'frequency': 'c', 'synset': 'candy_cane.n.01'}, {'name': 'walking_cane', 'id': 198, 'frequency': 'c', 'synset': 'cane.n.01'}, {'name': 'canister', 'id': 199, 'frequency': 'c', 'synset': 'canister.n.02'}, {'name': 'canoe', 'id': 200, 'frequency': 'c', 'synset': 'canoe.n.01'}, {'name': 'cantaloup', 'id': 201, 'frequency': 'c', 'synset': 'cantaloup.n.02'}, {'name': 'canteen', 'id': 202, 'frequency': 'r', 'synset': 'canteen.n.01'}, {'name': 'cap_(headwear)', 'id': 203, 'frequency': 'f', 'synset': 'cap.n.01'}, {'name': 'bottle_cap', 'id': 204, 'frequency': 'f', 'synset': 'cap.n.02'}, {'name': 'cape', 'id': 205, 'frequency': 'c', 'synset': 'cape.n.02'}, {'name': 'cappuccino', 'id': 206, 'frequency': 'c', 'synset': 'cappuccino.n.01'}, {'name': 'car_(automobile)', 'id': 207, 'frequency': 'f', 'synset': 'car.n.01'}, {'name': 'railcar_(part_of_a_train)', 'id': 208, 'frequency': 'f', 'synset': 'car.n.02'}, {'name': 'elevator_car', 'id': 209, 'frequency': 'r', 'synset': 'car.n.04'}, {'name': 'car_battery', 'id': 210, 'frequency': 'r', 'synset': 'car_battery.n.01'}, {'name': 'identity_card', 'id': 211, 'frequency': 'c', 'synset': 'card.n.02'}, {'name': 'card', 'id': 212, 'frequency': 'c', 'synset': 'card.n.03'}, {'name': 'cardigan', 'id': 213, 'frequency': 'c', 'synset': 'cardigan.n.01'}, {'name': 'cargo_ship', 'id': 214, 'frequency': 'r', 'synset': 'cargo_ship.n.01'}, {'name': 'carnation', 'id': 215, 'frequency': 'r', 'synset': 'carnation.n.01'}, {'name': 'horse_carriage', 'id': 216, 'frequency': 'c', 'synset': 'carriage.n.02'}, {'name': 'carrot', 'id': 217, 'frequency': 'f', 'synset': 'carrot.n.01'}, {'name': 'tote_bag', 'id': 218, 'frequency': 'f', 'synset': 'carryall.n.01'}, {'name': 'cart', 'id': 219, 'frequency': 'c', 'synset': 'cart.n.01'}, {'name': 'carton', 'id': 220, 'frequency': 'c', 'synset': 'carton.n.02'}, {'name': 'cash_register', 'id': 221, 'frequency': 'c', 'synset': 'cash_register.n.01'}, {'name': 'casserole', 'id': 222, 'frequency': 'r', 'synset': 'casserole.n.01'}, {'name': 'cassette', 'id': 223, 'frequency': 'r', 'synset': 'cassette.n.01'}, {'name': 'cast', 'id': 224, 'frequency': 'c', 'synset': 'cast.n.05'}, {'name': 'cat', 'id': 225, 'frequency': 'f', 'synset': 'cat.n.01'}, {'name': 'cauliflower', 'id': 226, 'frequency': 'f', 'synset': 'cauliflower.n.02'}, {'name': 'cayenne_(spice)', 'id': 227, 'frequency': 'c', 'synset': 'cayenne.n.02'}, {'name': 'CD_player', 'id': 228, 'frequency': 'c', 'synset': 'cd_player.n.01'}, {'name': 'celery', 'id': 229, 'frequency': 'f', 'synset': 'celery.n.01'}, {'name': 'cellular_telephone', 'id': 230, 'frequency': 'f', 'synset': 'cellular_telephone.n.01'}, {'name': 'chain_mail', 'id': 231, 'frequency': 'r', 'synset': 'chain_mail.n.01'}, {'name': 'chair', 'id': 232, 'frequency': 'f', 'synset': 'chair.n.01'}, {'name': 'chaise_longue', 'id': 233, 'frequency': 'r', 'synset': 'chaise_longue.n.01'}, {'name': 'chalice', 'id': 234, 'frequency': 'r', 'synset': 'chalice.n.01'}, {'name': 'chandelier', 'id': 235, 'frequency': 'f', 'synset': 'chandelier.n.01'}, {'name': 'chap', 'id': 236, 'frequency': 'r', 'synset': 'chap.n.04'}, {'name': 'checkbook', 'id': 237, 'frequency': 'r', 'synset': 'checkbook.n.01'}, {'name': 'checkerboard', 'id': 238, 'frequency': 'r', 'synset': 'checkerboard.n.01'}, {'name': 'cherry', 'id': 239, 'frequency': 'c', 'synset': 'cherry.n.03'}, {'name': 'chessboard', 'id': 240, 'frequency': 'r', 'synset': 'chessboard.n.01'}, {'name': 'chicken_(animal)', 'id': 241, 'frequency': 'c', 'synset': 'chicken.n.02'}, {'name': 'chickpea', 'id': 242, 'frequency': 'c', 'synset': 'chickpea.n.01'}, {'name': 'chili_(vegetable)', 'id': 243, 'frequency': 'c', 'synset': 'chili.n.02'}, {'name': 'chime', 'id': 244, 'frequency': 'r', 'synset': 'chime.n.01'}, {'name': 'chinaware', 'id': 245, 'frequency': 'r', 'synset': 'chinaware.n.01'}, {'name': 'crisp_(potato_chip)', 'id': 246, 'frequency': 'c', 'synset': 'chip.n.04'}, {'name': 'poker_chip', 'id': 247, 'frequency': 'r', 'synset': 'chip.n.06'}, {'name': 'chocolate_bar', 'id': 248, 'frequency': 'c', 'synset': 'chocolate_bar.n.01'}, {'name': 'chocolate_cake', 'id': 249, 'frequency': 'c', 'synset': 'chocolate_cake.n.01'}, {'name': 'chocolate_milk', 'id': 250, 'frequency': 'r', 'synset': 'chocolate_milk.n.01'}, {'name': 'chocolate_mousse', 'id': 251, 'frequency': 'r', 'synset': 'chocolate_mousse.n.01'}, {'name': 'choker', 'id': 252, 'frequency': 'f', 'synset': 'choker.n.03'}, {'name': 'chopping_board', 'id': 253, 'frequency': 'f', 'synset': 'chopping_board.n.01'}, {'name': 'chopstick', 'id': 254, 'frequency': 'f', 'synset': 'chopstick.n.01'}, {'name': 'Christmas_tree', 'id': 255, 'frequency': 'f', 'synset': 'christmas_tree.n.05'}, {'name': 'slide', 'id': 256, 'frequency': 'c', 'synset': 'chute.n.02'}, {'name': 'cider', 'id': 257, 'frequency': 'r', 'synset': 'cider.n.01'}, {'name': 'cigar_box', 'id': 258, 'frequency': 'r', 'synset': 'cigar_box.n.01'}, {'name': 'cigarette', 'id': 259, 'frequency': 'f', 'synset': 'cigarette.n.01'}, {'name': 'cigarette_case', 'id': 260, 'frequency': 'c', 'synset': 'cigarette_case.n.01'}, {'name': 'cistern', 'id': 261, 'frequency': 'f', 'synset': 'cistern.n.02'}, {'name': 'clarinet', 'id': 262, 'frequency': 'r', 'synset': 'clarinet.n.01'}, {'name': 'clasp', 'id': 263, 'frequency': 'c', 'synset': 'clasp.n.01'}, {'name': 'cleansing_agent', 'id': 264, 'frequency': 'c', 'synset': 'cleansing_agent.n.01'}, {'name': 'cleat_(for_securing_rope)', 'id': 265, 'frequency': 'r', 'synset': 'cleat.n.02'}, {'name': 'clementine', 'id': 266, 'frequency': 'r', 'synset': 'clementine.n.01'}, {'name': 'clip', 'id': 267, 'frequency': 'c', 'synset': 'clip.n.03'}, {'name': 'clipboard', 'id': 268, 'frequency': 'c', 'synset': 'clipboard.n.01'}, {'name': 'clippers_(for_plants)', 'id': 269, 'frequency': 'r', 'synset': 'clipper.n.03'}, {'name': 'cloak', 'id': 270, 'frequency': 'r', 'synset': 'cloak.n.02'}, {'name': 'clock', 'id': 271, 'frequency': 'f', 'synset': 'clock.n.01'}, {'name': 'clock_tower', 'id': 272, 'frequency': 'f', 'synset': 'clock_tower.n.01'}, {'name': 'clothes_hamper', 'id': 273, 'frequency': 'c', 'synset': 'clothes_hamper.n.01'}, {'name': 'clothespin', 'id': 274, 'frequency': 'c', 'synset': 'clothespin.n.01'}, {'name': 'clutch_bag', 'id': 275, 'frequency': 'r', 'synset': 'clutch_bag.n.01'}, {'name': 'coaster', 'id': 276, 'frequency': 'f', 'synset': 'coaster.n.03'}, {'name': 'coat', 'id': 277, 'frequency': 'f', 'synset': 'coat.n.01'}, {'name': 'coat_hanger', 'id': 278, 'frequency': 'c', 'synset': 'coat_hanger.n.01'}, {'name': 'coatrack', 'id': 279, 'frequency': 'c', 'synset': 'coatrack.n.01'}, {'name': 'cock', 'id': 280, 'frequency': 'c', 'synset': 'cock.n.04'}, {'name': 'cockroach', 'id': 281, 'frequency': 'r', 'synset': 'cockroach.n.01'}, {'name': 'cocoa_(beverage)', 'id': 282, 'frequency': 'r', 'synset': 'cocoa.n.01'}, {'name': 'coconut', 'id': 283, 'frequency': 'c', 'synset': 'coconut.n.02'}, {'name': 'coffee_maker', 'id': 284, 'frequency': 'f', 'synset': 'coffee_maker.n.01'}, {'name': 'coffee_table', 'id': 285, 'frequency': 'f', 'synset': 'coffee_table.n.01'}, {'name': 'coffeepot', 'id': 286, 'frequency': 'c', 'synset': 'coffeepot.n.01'}, {'name': 'coil', 'id': 287, 'frequency': 'r', 'synset': 'coil.n.05'}, {'name': 'coin', 'id': 288, 'frequency': 'c', 'synset': 'coin.n.01'}, {'name': 'colander', 'id': 289, 'frequency': 'c', 'synset': 'colander.n.01'}, {'name': 'coleslaw', 'id': 290, 'frequency': 'c', 'synset': 'coleslaw.n.01'}, {'name': 'coloring_material', 'id': 291, 'frequency': 'r', 'synset': 'coloring_material.n.01'}, {'name': 'combination_lock', 'id': 292, 'frequency': 'r', 'synset': 'combination_lock.n.01'}, {'name': 'pacifier', 'id': 293, 'frequency': 'c', 'synset': 'comforter.n.04'}, {'name': 'comic_book', 'id': 294, 'frequency': 'r', 'synset': 'comic_book.n.01'}, {'name': 'compass', 'id': 295, 'frequency': 'r', 'synset': 'compass.n.01'}, {'name': 'computer_keyboard', 'id': 296, 'frequency': 'f', 'synset': 'computer_keyboard.n.01'}, {'name': 'condiment', 'id': 297, 'frequency': 'f', 'synset': 'condiment.n.01'}, {'name': 'cone', 'id': 298, 'frequency': 'f', 'synset': 'cone.n.01'}, {'name': 'control', 'id': 299, 'frequency': 'f', 'synset': 'control.n.09'}, {'name': 'convertible_(automobile)', 'id': 300, 'frequency': 'r', 'synset': 'convertible.n.01'}, {'name': 'sofa_bed', 'id': 301, 'frequency': 'r', 'synset': 'convertible.n.03'}, {'name': 'cooker', 'id': 302, 'frequency': 'r', 'synset': 'cooker.n.01'}, {'name': 'cookie', 'id': 303, 'frequency': 'f', 'synset': 'cookie.n.01'}, {'name': 'cooking_utensil', 'id': 304, 'frequency': 'r', 'synset': 'cooking_utensil.n.01'}, {'name': 'cooler_(for_food)', 'id': 305, 'frequency': 'f', 'synset': 'cooler.n.01'}, {'name': 'cork_(bottle_plug)', 'id': 306, 'frequency': 'f', 'synset': 'cork.n.04'}, {'name': 'corkboard', 'id': 307, 'frequency': 'r', 'synset': 'corkboard.n.01'}, {'name': 'corkscrew', 'id': 308, 'frequency': 'c', 'synset': 'corkscrew.n.01'}, {'name': 'edible_corn', 'id': 309, 'frequency': 'f', 'synset': 'corn.n.03'}, {'name': 'cornbread', 'id': 310, 'frequency': 'r', 'synset': 'cornbread.n.01'}, {'name': 'cornet', 'id': 311, 'frequency': 'c', 'synset': 'cornet.n.01'}, {'name': 'cornice', 'id': 312, 'frequency': 'c', 'synset': 'cornice.n.01'}, {'name': 'cornmeal', 'id': 313, 'frequency': 'r', 'synset': 'cornmeal.n.01'}, {'name': 'corset', 'id': 314, 'frequency': 'c', 'synset': 'corset.n.01'}, {'name': 'costume', 'id': 315, 'frequency': 'c', 'synset': 'costume.n.04'}, {'name': 'cougar', 'id': 316, 'frequency': 'r', 'synset': 'cougar.n.01'}, {'name': 'coverall', 'id': 317, 'frequency': 'r', 'synset': 'coverall.n.01'}, {'name': 'cowbell', 'id': 318, 'frequency': 'c', 'synset': 'cowbell.n.01'}, {'name': 'cowboy_hat', 'id': 319, 'frequency': 'f', 'synset': 'cowboy_hat.n.01'}, {'name': 'crab_(animal)', 'id': 320, 'frequency': 'c', 'synset': 'crab.n.01'}, {'name': 'crabmeat', 'id': 321, 'frequency': 'r', 'synset': 'crab.n.05'}, {'name': 'cracker', 'id': 322, 'frequency': 'c', 'synset': 'cracker.n.01'}, {'name': 'crape', 'id': 323, 'frequency': 'r', 'synset': 'crape.n.01'}, {'name': 'crate', 'id': 324, 'frequency': 'f', 'synset': 'crate.n.01'}, {'name': 'crayon', 'id': 325, 'frequency': 'c', 'synset': 'crayon.n.01'}, {'name': 'cream_pitcher', 'id': 326, 'frequency': 'r', 'synset': 'cream_pitcher.n.01'}, {'name': 'crescent_roll', 'id': 327, 'frequency': 'c', 'synset': 'crescent_roll.n.01'}, {'name': 'crib', 'id': 328, 'frequency': 'c', 'synset': 'crib.n.01'}, {'name': 'crock_pot', 'id': 329, 'frequency': 'c', 'synset': 'crock.n.03'}, {'name': 'crossbar', 'id': 330, 'frequency': 'f', 'synset': 'crossbar.n.01'}, {'name': 'crouton', 'id': 331, 'frequency': 'r', 'synset': 'crouton.n.01'}, {'name': 'crow', 'id': 332, 'frequency': 'c', 'synset': 'crow.n.01'}, {'name': 'crowbar', 'id': 333, 'frequency': 'r', 'synset': 'crowbar.n.01'}, {'name': 'crown', 'id': 334, 'frequency': 'c', 'synset': 'crown.n.04'}, {'name': 'crucifix', 'id': 335, 'frequency': 'c', 'synset': 'crucifix.n.01'}, {'name': 'cruise_ship', 'id': 336, 'frequency': 'c', 'synset': 'cruise_ship.n.01'}, {'name': 'police_cruiser', 'id': 337, 'frequency': 'c', 'synset': 'cruiser.n.01'}, {'name': 'crumb', 'id': 338, 'frequency': 'f', 'synset': 'crumb.n.03'}, {'name': 'crutch', 'id': 339, 'frequency': 'c', 'synset': 'crutch.n.01'}, {'name': 'cub_(animal)', 'id': 340, 'frequency': 'c', 'synset': 'cub.n.03'}, {'name': 'cube', 'id': 341, 'frequency': 'c', 'synset': 'cube.n.05'}, {'name': 'cucumber', 'id': 342, 'frequency': 'f', 'synset': 'cucumber.n.02'}, {'name': 'cufflink', 'id': 343, 'frequency': 'c', 'synset': 'cufflink.n.01'}, {'name': 'cup', 'id': 344, 'frequency': 'f', 'synset': 'cup.n.01'}, {'name': 'trophy_cup', 'id': 345, 'frequency': 'c', 'synset': 'cup.n.08'}, {'name': 'cupboard', 'id': 346, 'frequency': 'f', 'synset': 'cupboard.n.01'}, {'name': 'cupcake', 'id': 347, 'frequency': 'f', 'synset': 'cupcake.n.01'}, {'name': 'hair_curler', 'id': 348, 'frequency': 'r', 'synset': 'curler.n.01'}, {'name': 'curling_iron', 'id': 349, 'frequency': 'r', 'synset': 'curling_iron.n.01'}, {'name': 'curtain', 'id': 350, 'frequency': 'f', 'synset': 'curtain.n.01'}, {'name': 'cushion', 'id': 351, 'frequency': 'f', 'synset': 'cushion.n.03'}, {'name': 'cylinder', 'id': 352, 'frequency': 'r', 'synset': 'cylinder.n.04'}, {'name': 'cymbal', 'id': 353, 'frequency': 'r', 'synset': 'cymbal.n.01'}, {'name': 'dagger', 'id': 354, 'frequency': 'r', 'synset': 'dagger.n.01'}, {'name': 'dalmatian', 'id': 355, 'frequency': 'r', 'synset': 'dalmatian.n.02'}, {'name': 'dartboard', 'id': 356, 'frequency': 'c', 'synset': 'dartboard.n.01'}, {'name': 'date_(fruit)', 'id': 357, 'frequency': 'r', 'synset': 'date.n.08'}, {'name': 'deck_chair', 'id': 358, 'frequency': 'f', 'synset': 'deck_chair.n.01'}, {'name': 'deer', 'id': 359, 'frequency': 'c', 'synset': 'deer.n.01'}, {'name': 'dental_floss', 'id': 360, 'frequency': 'c', 'synset': 'dental_floss.n.01'}, {'name': 'desk', 'id': 361, 'frequency': 'f', 'synset': 'desk.n.01'}, {'name': 'detergent', 'id': 362, 'frequency': 'r', 'synset': 'detergent.n.01'}, {'name': 'diaper', 'id': 363, 'frequency': 'c', 'synset': 'diaper.n.01'}, {'name': 'diary', 'id': 364, 'frequency': 'r', 'synset': 'diary.n.01'}, {'name': 'die', 'id': 365, 'frequency': 'r', 'synset': 'die.n.01'}, {'name': 'dinghy', 'id': 366, 'frequency': 'r', 'synset': 'dinghy.n.01'}, {'name': 'dining_table', 'id': 367, 'frequency': 'f', 'synset': 'dining_table.n.01'}, {'name': 'tux', 'id': 368, 'frequency': 'r', 'synset': 'dinner_jacket.n.01'}, {'name': 'dish', 'id': 369, 'frequency': 'f', 'synset': 'dish.n.01'}, {'name': 'dish_antenna', 'id': 370, 'frequency': 'c', 'synset': 'dish.n.05'}, {'name': 'dishrag', 'id': 371, 'frequency': 'c', 'synset': 'dishrag.n.01'}, {'name': 'dishtowel', 'id': 372, 'frequency': 'f', 'synset': 'dishtowel.n.01'}, {'name': 'dishwasher', 'id': 373, 'frequency': 'f', 'synset': 'dishwasher.n.01'}, {'name': 'dishwasher_detergent', 'id': 374, 'frequency': 'r', 'synset': 'dishwasher_detergent.n.01'}, {'name': 'dispenser', 'id': 375, 'frequency': 'f', 'synset': 'dispenser.n.01'}, {'name': 'diving_board', 'id': 376, 'frequency': 'r', 'synset': 'diving_board.n.01'}, {'name': 'Dixie_cup', 'id': 377, 'frequency': 'f', 'synset': 'dixie_cup.n.01'}, {'name': 'dog', 'id': 378, 'frequency': 'f', 'synset': 'dog.n.01'}, {'name': 'dog_collar', 'id': 379, 'frequency': 'f', 'synset': 'dog_collar.n.01'}, {'name': 'doll', 'id': 380, 'frequency': 'f', 'synset': 'doll.n.01'}, {'name': 'dollar', 'id': 381, 'frequency': 'r', 'synset': 'dollar.n.02'}, {'name': 'dollhouse', 'id': 382, 'frequency': 'r', 'synset': 'dollhouse.n.01'}, {'name': 'dolphin', 'id': 383, 'frequency': 'c', 'synset': 'dolphin.n.02'}, {'name': 'domestic_ass', 'id': 384, 'frequency': 'c', 'synset': 'domestic_ass.n.01'}, {'name': 'doorknob', 'id': 385, 'frequency': 'f', 'synset': 'doorknob.n.01'}, {'name': 'doormat', 'id': 386, 'frequency': 'c', 'synset': 'doormat.n.02'}, {'name': 'doughnut', 'id': 387, 'frequency': 'f', 'synset': 'doughnut.n.02'}, {'name': 'dove', 'id': 388, 'frequency': 'r', 'synset': 'dove.n.01'}, {'name': 'dragonfly', 'id': 389, 'frequency': 'r', 'synset': 'dragonfly.n.01'}, {'name': 'drawer', 'id': 390, 'frequency': 'f', 'synset': 'drawer.n.01'}, {'name': 'underdrawers', 'id': 391, 'frequency': 'c', 'synset': 'drawers.n.01'}, {'name': 'dress', 'id': 392, 'frequency': 'f', 'synset': 'dress.n.01'}, {'name': 'dress_hat', 'id': 393, 'frequency': 'c', 'synset': 'dress_hat.n.01'}, {'name': 'dress_suit', 'id': 394, 'frequency': 'f', 'synset': 'dress_suit.n.01'}, {'name': 'dresser', 'id': 395, 'frequency': 'f', 'synset': 'dresser.n.05'}, {'name': 'drill', 'id': 396, 'frequency': 'c', 'synset': 'drill.n.01'}, {'name': 'drone', 'id': 397, 'frequency': 'r', 'synset': 'drone.n.04'}, {'name': 'dropper', 'id': 398, 'frequency': 'r', 'synset': 'dropper.n.01'}, {'name': 'drum_(musical_instrument)', 'id': 399, 'frequency': 'c', 'synset': 'drum.n.01'}, {'name': 'drumstick', 'id': 400, 'frequency': 'r', 'synset': 'drumstick.n.02'}, {'name': 'duck', 'id': 401, 'frequency': 'f', 'synset': 'duck.n.01'}, {'name': 'duckling', 'id': 402, 'frequency': 'c', 'synset': 'duckling.n.02'}, {'name': 'duct_tape', 'id': 403, 'frequency': 'c', 'synset': 'duct_tape.n.01'}, {'name': 'duffel_bag', 'id': 404, 'frequency': 'f', 'synset': 'duffel_bag.n.01'}, {'name': 'dumbbell', 'id': 405, 'frequency': 'r', 'synset': 'dumbbell.n.01'}, {'name': 'dumpster', 'id': 406, 'frequency': 'c', 'synset': 'dumpster.n.01'}, {'name': 'dustpan', 'id': 407, 'frequency': 'r', 'synset': 'dustpan.n.02'}, {'name': 'eagle', 'id': 408, 'frequency': 'c', 'synset': 'eagle.n.01'}, {'name': 'earphone', 'id': 409, 'frequency': 'f', 'synset': 'earphone.n.01'}, {'name': 'earplug', 'id': 410, 'frequency': 'r', 'synset': 'earplug.n.01'}, {'name': 'earring', 'id': 411, 'frequency': 'f', 'synset': 'earring.n.01'}, {'name': 'easel', 'id': 412, 'frequency': 'c', 'synset': 'easel.n.01'}, {'name': 'eclair', 'id': 413, 'frequency': 'r', 'synset': 'eclair.n.01'}, {'name': 'eel', 'id': 414, 'frequency': 'r', 'synset': 'eel.n.01'}, {'name': 'egg', 'id': 415, 'frequency': 'f', 'synset': 'egg.n.02'}, {'name': 'egg_roll', 'id': 416, 'frequency': 'r', 'synset': 'egg_roll.n.01'}, {'name': 'egg_yolk', 'id': 417, 'frequency': 'c', 'synset': 'egg_yolk.n.01'}, {'name': 'eggbeater', 'id': 418, 'frequency': 'c', 'synset': 'eggbeater.n.02'}, {'name': 'eggplant', 'id': 419, 'frequency': 'c', 'synset': 'eggplant.n.01'}, {'name': 'electric_chair', 'id': 420, 'frequency': 'r', 'synset': 'electric_chair.n.01'}, {'name': 'refrigerator', 'id': 421, 'frequency': 'f', 'synset': 'electric_refrigerator.n.01'}, {'name': 'elephant', 'id': 422, 'frequency': 'f', 'synset': 'elephant.n.01'}, {'name': 'elk', 'id': 423, 'frequency': 'c', 'synset': 'elk.n.01'}, {'name': 'envelope', 'id': 424, 'frequency': 'c', 'synset': 'envelope.n.01'}, {'name': 'eraser', 'id': 425, 'frequency': 'c', 'synset': 'eraser.n.01'}, {'name': 'escargot', 'id': 426, 'frequency': 'r', 'synset': 'escargot.n.01'}, {'name': 'eyepatch', 'id': 427, 'frequency': 'r', 'synset': 'eyepatch.n.01'}, {'name': 'falcon', 'id': 428, 'frequency': 'r', 'synset': 'falcon.n.01'}, {'name': 'fan', 'id': 429, 'frequency': 'f', 'synset': 'fan.n.01'}, {'name': 'faucet', 'id': 430, 'frequency': 'f', 'synset': 'faucet.n.01'}, {'name': 'fedora', 'id': 431, 'frequency': 'r', 'synset': 'fedora.n.01'}, {'name': 'ferret', 'id': 432, 'frequency': 'r', 'synset': 'ferret.n.02'}, {'name': 'Ferris_wheel', 'id': 433, 'frequency': 'c', 'synset': 'ferris_wheel.n.01'}, {'name': 'ferry', 'id': 434, 'frequency': 'c', 'synset': 'ferry.n.01'}, {'name': 'fig_(fruit)', 'id': 435, 'frequency': 'r', 'synset': 'fig.n.04'}, {'name': 'fighter_jet', 'id': 436, 'frequency': 'c', 'synset': 'fighter.n.02'}, {'name': 'figurine', 'id': 437, 'frequency': 'f', 'synset': 'figurine.n.01'}, {'name': 'file_cabinet', 'id': 438, 'frequency': 'c', 'synset': 'file.n.03'}, {'name': 'file_(tool)', 'id': 439, 'frequency': 'r', 'synset': 'file.n.04'}, {'name': 'fire_alarm', 'id': 440, 'frequency': 'f', 'synset': 'fire_alarm.n.02'}, {'name': 'fire_engine', 'id': 441, 'frequency': 'f', 'synset': 'fire_engine.n.01'}, {'name': 'fire_extinguisher', 'id': 442, 'frequency': 'f', 'synset': 'fire_extinguisher.n.01'}, {'name': 'fire_hose', 'id': 443, 'frequency': 'c', 'synset': 'fire_hose.n.01'}, {'name': 'fireplace', 'id': 444, 'frequency': 'f', 'synset': 'fireplace.n.01'}, {'name': 'fireplug', 'id': 445, 'frequency': 'f', 'synset': 'fireplug.n.01'}, {'name': 'first-aid_kit', 'id': 446, 'frequency': 'r', 'synset': 'first-aid_kit.n.01'}, {'name': 'fish', 'id': 447, 'frequency': 'f', 'synset': 'fish.n.01'}, {'name': 'fish_(food)', 'id': 448, 'frequency': 'c', 'synset': 'fish.n.02'}, {'name': 'fishbowl', 'id': 449, 'frequency': 'r', 'synset': 'fishbowl.n.02'}, {'name': 'fishing_rod', 'id': 450, 'frequency': 'c', 'synset': 'fishing_rod.n.01'}, {'name': 'flag', 'id': 451, 'frequency': 'f', 'synset': 'flag.n.01'}, {'name': 'flagpole', 'id': 452, 'frequency': 'f', 'synset': 'flagpole.n.02'}, {'name': 'flamingo', 'id': 453, 'frequency': 'c', 'synset': 'flamingo.n.01'}, {'name': 'flannel', 'id': 454, 'frequency': 'c', 'synset': 'flannel.n.01'}, {'name': 'flap', 'id': 455, 'frequency': 'c', 'synset': 'flap.n.01'}, {'name': 'flash', 'id': 456, 'frequency': 'r', 'synset': 'flash.n.10'}, {'name': 'flashlight', 'id': 457, 'frequency': 'c', 'synset': 'flashlight.n.01'}, {'name': 'fleece', 'id': 458, 'frequency': 'r', 'synset': 'fleece.n.03'}, {'name': 'flip-flop_(sandal)', 'id': 459, 'frequency': 'f', 'synset': 'flip-flop.n.02'}, {'name': 'flipper_(footwear)', 'id': 460, 'frequency': 'c', 'synset': 'flipper.n.01'}, {'name': 'flower_arrangement', 'id': 461, 'frequency': 'f', 'synset': 'flower_arrangement.n.01'}, {'name': 'flute_glass', 'id': 462, 'frequency': 'c', 'synset': 'flute.n.02'}, {'name': 'foal', 'id': 463, 'frequency': 'c', 'synset': 'foal.n.01'}, {'name': 'folding_chair', 'id': 464, 'frequency': 'c', 'synset': 'folding_chair.n.01'}, {'name': 'food_processor', 'id': 465, 'frequency': 'c', 'synset': 'food_processor.n.01'}, {'name': 'football_(American)', 'id': 466, 'frequency': 'c', 'synset': 'football.n.02'}, {'name': 'football_helmet', 'id': 467, 'frequency': 'r', 'synset': 'football_helmet.n.01'}, {'name': 'footstool', 'id': 468, 'frequency': 'c', 'synset': 'footstool.n.01'}, {'name': 'fork', 'id': 469, 'frequency': 'f', 'synset': 'fork.n.01'}, {'name': 'forklift', 'id': 470, 'frequency': 'c', 'synset': 'forklift.n.01'}, {'name': 'freight_car', 'id': 471, 'frequency': 'c', 'synset': 'freight_car.n.01'}, {'name': 'French_toast', 'id': 472, 'frequency': 'c', 'synset': 'french_toast.n.01'}, {'name': 'freshener', 'id': 473, 'frequency': 'c', 'synset': 'freshener.n.01'}, {'name': 'frisbee', 'id': 474, 'frequency': 'f', 'synset': 'frisbee.n.01'}, {'name': 'frog', 'id': 475, 'frequency': 'c', 'synset': 'frog.n.01'}, {'name': 'fruit_juice', 'id': 476, 'frequency': 'c', 'synset': 'fruit_juice.n.01'}, {'name': 'frying_pan', 'id': 477, 'frequency': 'f', 'synset': 'frying_pan.n.01'}, {'name': 'fudge', 'id': 478, 'frequency': 'r', 'synset': 'fudge.n.01'}, {'name': 'funnel', 'id': 479, 'frequency': 'r', 'synset': 'funnel.n.02'}, {'name': 'futon', 'id': 480, 'frequency': 'r', 'synset': 'futon.n.01'}, {'name': 'gag', 'id': 481, 'frequency': 'r', 'synset': 'gag.n.02'}, {'name': 'garbage', 'id': 482, 'frequency': 'r', 'synset': 'garbage.n.03'}, {'name': 'garbage_truck', 'id': 483, 'frequency': 'c', 'synset': 'garbage_truck.n.01'}, {'name': 'garden_hose', 'id': 484, 'frequency': 'c', 'synset': 'garden_hose.n.01'}, {'name': 'gargle', 'id': 485, 'frequency': 'c', 'synset': 'gargle.n.01'}, {'name': 'gargoyle', 'id': 486, 'frequency': 'r', 'synset': 'gargoyle.n.02'}, {'name': 'garlic', 'id': 487, 'frequency': 'c', 'synset': 'garlic.n.02'}, {'name': 'gasmask', 'id': 488, 'frequency': 'r', 'synset': 'gasmask.n.01'}, {'name': 'gazelle', 'id': 489, 'frequency': 'c', 'synset': 'gazelle.n.01'}, {'name': 'gelatin', 'id': 490, 'frequency': 'c', 'synset': 'gelatin.n.02'}, {'name': 'gemstone', 'id': 491, 'frequency': 'r', 'synset': 'gem.n.02'}, {'name': 'generator', 'id': 492, 'frequency': 'r', 'synset': 'generator.n.02'}, {'name': 'giant_panda', 'id': 493, 'frequency': 'c', 'synset': 'giant_panda.n.01'}, {'name': 'gift_wrap', 'id': 494, 'frequency': 'c', 'synset': 'gift_wrap.n.01'}, {'name': 'ginger', 'id': 495, 'frequency': 'c', 'synset': 'ginger.n.03'}, {'name': 'giraffe', 'id': 496, 'frequency': 'f', 'synset': 'giraffe.n.01'}, {'name': 'cincture', 'id': 497, 'frequency': 'c', 'synset': 'girdle.n.02'}, {'name': 'glass_(drink_container)', 'id': 498, 'frequency': 'f', 'synset': 'glass.n.02'}, {'name': 'globe', 'id': 499, 'frequency': 'c', 'synset': 'globe.n.03'}, {'name': 'glove', 'id': 500, 'frequency': 'f', 'synset': 'glove.n.02'}, {'name': 'goat', 'id': 501, 'frequency': 'c', 'synset': 'goat.n.01'}, {'name': 'goggles', 'id': 502, 'frequency': 'f', 'synset': 'goggles.n.01'}, {'name': 'goldfish', 'id': 503, 'frequency': 'r', 'synset': 'goldfish.n.01'}, {'name': 'golf_club', 'id': 504, 'frequency': 'c', 'synset': 'golf_club.n.02'}, {'name': 'golfcart', 'id': 505, 'frequency': 'c', 'synset': 'golfcart.n.01'}, {'name': 'gondola_(boat)', 'id': 506, 'frequency': 'r', 'synset': 'gondola.n.02'}, {'name': 'goose', 'id': 507, 'frequency': 'c', 'synset': 'goose.n.01'}, {'name': 'gorilla', 'id': 508, 'frequency': 'r', 'synset': 'gorilla.n.01'}, {'name': 'gourd', 'id': 509, 'frequency': 'r', 'synset': 'gourd.n.02'}, {'name': 'grape', 'id': 510, 'frequency': 'f', 'synset': 'grape.n.01'}, {'name': 'grater', 'id': 511, 'frequency': 'c', 'synset': 'grater.n.01'}, {'name': 'gravestone', 'id': 512, 'frequency': 'c', 'synset': 'gravestone.n.01'}, {'name': 'gravy_boat', 'id': 513, 'frequency': 'r', 'synset': 'gravy_boat.n.01'}, {'name': 'green_bean', 'id': 514, 'frequency': 'f', 'synset': 'green_bean.n.02'}, {'name': 'green_onion', 'id': 515, 'frequency': 'f', 'synset': 'green_onion.n.01'}, {'name': 'griddle', 'id': 516, 'frequency': 'r', 'synset': 'griddle.n.01'}, {'name': 'grill', 'id': 517, 'frequency': 'f', 'synset': 'grill.n.02'}, {'name': 'grits', 'id': 518, 'frequency': 'r', 'synset': 'grits.n.01'}, {'name': 'grizzly', 'id': 519, 'frequency': 'c', 'synset': 'grizzly.n.01'}, {'name': 'grocery_bag', 'id': 520, 'frequency': 'c', 'synset': 'grocery_bag.n.01'}, {'name': 'guitar', 'id': 521, 'frequency': 'f', 'synset': 'guitar.n.01'}, {'name': 'gull', 'id': 522, 'frequency': 'c', 'synset': 'gull.n.02'}, {'name': 'gun', 'id': 523, 'frequency': 'c', 'synset': 'gun.n.01'}, {'name': 'hairbrush', 'id': 524, 'frequency': 'f', 'synset': 'hairbrush.n.01'}, {'name': 'hairnet', 'id': 525, 'frequency': 'c', 'synset': 'hairnet.n.01'}, {'name': 'hairpin', 'id': 526, 'frequency': 'c', 'synset': 'hairpin.n.01'}, {'name': 'halter_top', 'id': 527, 'frequency': 'r', 'synset': 'halter.n.03'}, {'name': 'ham', 'id': 528, 'frequency': 'f', 'synset': 'ham.n.01'}, {'name': 'hamburger', 'id': 529, 'frequency': 'c', 'synset': 'hamburger.n.01'}, {'name': 'hammer', 'id': 530, 'frequency': 'c', 'synset': 'hammer.n.02'}, {'name': 'hammock', 'id': 531, 'frequency': 'c', 'synset': 'hammock.n.02'}, {'name': 'hamper', 'id': 532, 'frequency': 'r', 'synset': 'hamper.n.02'}, {'name': 'hamster', 'id': 533, 'frequency': 'c', 'synset': 'hamster.n.01'}, {'name': 'hair_dryer', 'id': 534, 'frequency': 'f', 'synset': 'hand_blower.n.01'}, {'name': 'hand_glass', 'id': 535, 'frequency': 'r', 'synset': 'hand_glass.n.01'}, {'name': 'hand_towel', 'id': 536, 'frequency': 'f', 'synset': 'hand_towel.n.01'}, {'name': 'handcart', 'id': 537, 'frequency': 'c', 'synset': 'handcart.n.01'}, {'name': 'handcuff', 'id': 538, 'frequency': 'r', 'synset': 'handcuff.n.01'}, {'name': 'handkerchief', 'id': 539, 'frequency': 'c', 'synset': 'handkerchief.n.01'}, {'name': 'handle', 'id': 540, 'frequency': 'f', 'synset': 'handle.n.01'}, {'name': 'handsaw', 'id': 541, 'frequency': 'r', 'synset': 'handsaw.n.01'}, {'name': 'hardback_book', 'id': 542, 'frequency': 'r', 'synset': 'hardback.n.01'}, {'name': 'harmonium', 'id': 543, 'frequency': 'r', 'synset': 'harmonium.n.01'}, {'name': 'hat', 'id': 544, 'frequency': 'f', 'synset': 'hat.n.01'}, {'name': 'hatbox', 'id': 545, 'frequency': 'r', 'synset': 'hatbox.n.01'}, {'name': 'veil', 'id': 546, 'frequency': 'c', 'synset': 'head_covering.n.01'}, {'name': 'headband', 'id': 547, 'frequency': 'f', 'synset': 'headband.n.01'}, {'name': 'headboard', 'id': 548, 'frequency': 'f', 'synset': 'headboard.n.01'}, {'name': 'headlight', 'id': 549, 'frequency': 'f', 'synset': 'headlight.n.01'}, {'name': 'headscarf', 'id': 550, 'frequency': 'c', 'synset': 'headscarf.n.01'}, {'name': 'headset', 'id': 551, 'frequency': 'r', 'synset': 'headset.n.01'}, {'name': 'headstall_(for_horses)', 'id': 552, 'frequency': 'c', 'synset': 'headstall.n.01'}, {'name': 'heart', 'id': 553, 'frequency': 'c', 'synset': 'heart.n.02'}, {'name': 'heater', 'id': 554, 'frequency': 'c', 'synset': 'heater.n.01'}, {'name': 'helicopter', 'id': 555, 'frequency': 'c', 'synset': 'helicopter.n.01'}, {'name': 'helmet', 'id': 556, 'frequency': 'f', 'synset': 'helmet.n.02'}, {'name': 'heron', 'id': 557, 'frequency': 'r', 'synset': 'heron.n.02'}, {'name': 'highchair', 'id': 558, 'frequency': 'c', 'synset': 'highchair.n.01'}, {'name': 'hinge', 'id': 559, 'frequency': 'f', 'synset': 'hinge.n.01'}, {'name': 'hippopotamus', 'id': 560, 'frequency': 'r', 'synset': 'hippopotamus.n.01'}, {'name': 'hockey_stick', 'id': 561, 'frequency': 'r', 'synset': 'hockey_stick.n.01'}, {'name': 'hog', 'id': 562, 'frequency': 'c', 'synset': 'hog.n.03'}, {'name': 'home_plate_(baseball)', 'id': 563, 'frequency': 'f', 'synset': 'home_plate.n.01'}, {'name': 'honey', 'id': 564, 'frequency': 'c', 'synset': 'honey.n.01'}, {'name': 'fume_hood', 'id': 565, 'frequency': 'f', 'synset': 'hood.n.06'}, {'name': 'hook', 'id': 566, 'frequency': 'f', 'synset': 'hook.n.05'}, {'name': 'hookah', 'id': 567, 'frequency': 'r', 'synset': 'hookah.n.01'}, {'name': 'hornet', 'id': 568, 'frequency': 'r', 'synset': 'hornet.n.01'}, {'name': 'horse', 'id': 569, 'frequency': 'f', 'synset': 'horse.n.01'}, {'name': 'hose', 'id': 570, 'frequency': 'f', 'synset': 'hose.n.03'}, {'name': 'hot-air_balloon', 'id': 571, 'frequency': 'r', 'synset': 'hot-air_balloon.n.01'}, {'name': 'hotplate', 'id': 572, 'frequency': 'r', 'synset': 'hot_plate.n.01'}, {'name': 'hot_sauce', 'id': 573, 'frequency': 'c', 'synset': 'hot_sauce.n.01'}, {'name': 'hourglass', 'id': 574, 'frequency': 'r', 'synset': 'hourglass.n.01'}, {'name': 'houseboat', 'id': 575, 'frequency': 'r', 'synset': 'houseboat.n.01'}, {'name': 'hummingbird', 'id': 576, 'frequency': 'c', 'synset': 'hummingbird.n.01'}, {'name': 'hummus', 'id': 577, 'frequency': 'r', 'synset': 'hummus.n.01'}, {'name': 'polar_bear', 'id': 578, 'frequency': 'f', 'synset': 'ice_bear.n.01'}, {'name': 'icecream', 'id': 579, 'frequency': 'c', 'synset': 'ice_cream.n.01'}, {'name': 'popsicle', 'id': 580, 'frequency': 'r', 'synset': 'ice_lolly.n.01'}, {'name': 'ice_maker', 'id': 581, 'frequency': 'c', 'synset': 'ice_maker.n.01'}, {'name': 'ice_pack', 'id': 582, 'frequency': 'r', 'synset': 'ice_pack.n.01'}, {'name': 'ice_skate', 'id': 583, 'frequency': 'r', 'synset': 'ice_skate.n.01'}, {'name': 'igniter', 'id': 584, 'frequency': 'c', 'synset': 'igniter.n.01'}, {'name': 'inhaler', 'id': 585, 'frequency': 'r', 'synset': 'inhaler.n.01'}, {'name': 'iPod', 'id': 586, 'frequency': 'f', 'synset': 'ipod.n.01'}, {'name': 'iron_(for_clothing)', 'id': 587, 'frequency': 'c', 'synset': 'iron.n.04'}, {'name': 'ironing_board', 'id': 588, 'frequency': 'c', 'synset': 'ironing_board.n.01'}, {'name': 'jacket', 'id': 589, 'frequency': 'f', 'synset': 'jacket.n.01'}, {'name': 'jam', 'id': 590, 'frequency': 'c', 'synset': 'jam.n.01'}, {'name': 'jar', 'id': 591, 'frequency': 'f', 'synset': 'jar.n.01'}, {'name': 'jean', 'id': 592, 'frequency': 'f', 'synset': 'jean.n.01'}, {'name': 'jeep', 'id': 593, 'frequency': 'c', 'synset': 'jeep.n.01'}, {'name': 'jelly_bean', 'id': 594, 'frequency': 'r', 'synset': 'jelly_bean.n.01'}, {'name': 'jersey', 'id': 595, 'frequency': 'f', 'synset': 'jersey.n.03'}, {'name': 'jet_plane', 'id': 596, 'frequency': 'c', 'synset': 'jet.n.01'}, {'name': 'jewel', 'id': 597, 'frequency': 'r', 'synset': 'jewel.n.01'}, {'name': 'jewelry', 'id': 598, 'frequency': 'c', 'synset': 'jewelry.n.01'}, {'name': 'joystick', 'id': 599, 'frequency': 'r', 'synset': 'joystick.n.02'}, {'name': 'jumpsuit', 'id': 600, 'frequency': 'c', 'synset': 'jump_suit.n.01'}, {'name': 'kayak', 'id': 601, 'frequency': 'c', 'synset': 'kayak.n.01'}, {'name': 'keg', 'id': 602, 'frequency': 'r', 'synset': 'keg.n.02'}, {'name': 'kennel', 'id': 603, 'frequency': 'r', 'synset': 'kennel.n.01'}, {'name': 'kettle', 'id': 604, 'frequency': 'c', 'synset': 'kettle.n.01'}, {'name': 'key', 'id': 605, 'frequency': 'f', 'synset': 'key.n.01'}, {'name': 'keycard', 'id': 606, 'frequency': 'r', 'synset': 'keycard.n.01'}, {'name': 'kilt', 'id': 607, 'frequency': 'c', 'synset': 'kilt.n.01'}, {'name': 'kimono', 'id': 608, 'frequency': 'c', 'synset': 'kimono.n.01'}, {'name': 'kitchen_sink', 'id': 609, 'frequency': 'f', 'synset': 'kitchen_sink.n.01'}, {'name': 'kitchen_table', 'id': 610, 'frequency': 'r', 'synset': 'kitchen_table.n.01'}, {'name': 'kite', 'id': 611, 'frequency': 'f', 'synset': 'kite.n.03'}, {'name': 'kitten', 'id': 612, 'frequency': 'c', 'synset': 'kitten.n.01'}, {'name': 'kiwi_fruit', 'id': 613, 'frequency': 'c', 'synset': 'kiwi.n.03'}, {'name': 'knee_pad', 'id': 614, 'frequency': 'f', 'synset': 'knee_pad.n.01'}, {'name': 'knife', 'id': 615, 'frequency': 'f', 'synset': 'knife.n.01'}, {'name': 'knitting_needle', 'id': 616, 'frequency': 'r', 'synset': 'knitting_needle.n.01'}, {'name': 'knob', 'id': 617, 'frequency': 'f', 'synset': 'knob.n.02'}, {'name': 'knocker_(on_a_door)', 'id': 618, 'frequency': 'r', 'synset': 'knocker.n.05'}, {'name': 'koala', 'id': 619, 'frequency': 'r', 'synset': 'koala.n.01'}, {'name': 'lab_coat', 'id': 620, 'frequency': 'r', 'synset': 'lab_coat.n.01'}, {'name': 'ladder', 'id': 621, 'frequency': 'f', 'synset': 'ladder.n.01'}, {'name': 'ladle', 'id': 622, 'frequency': 'c', 'synset': 'ladle.n.01'}, {'name': 'ladybug', 'id': 623, 'frequency': 'c', 'synset': 'ladybug.n.01'}, {'name': 'lamb_(animal)', 'id': 624, 'frequency': 'f', 'synset': 'lamb.n.01'}, {'name': 'lamb-chop', 'id': 625, 'frequency': 'r', 'synset': 'lamb_chop.n.01'}, {'name': 'lamp', 'id': 626, 'frequency': 'f', 'synset': 'lamp.n.02'}, {'name': 'lamppost', 'id': 627, 'frequency': 'f', 'synset': 'lamppost.n.01'}, {'name': 'lampshade', 'id': 628, 'frequency': 'f', 'synset': 'lampshade.n.01'}, {'name': 'lantern', 'id': 629, 'frequency': 'c', 'synset': 'lantern.n.01'}, {'name': 'lanyard', 'id': 630, 'frequency': 'f', 'synset': 'lanyard.n.02'}, {'name': 'laptop_computer', 'id': 631, 'frequency': 'f', 'synset': 'laptop.n.01'}, {'name': 'lasagna', 'id': 632, 'frequency': 'r', 'synset': 'lasagna.n.01'}, {'name': 'latch', 'id': 633, 'frequency': 'f', 'synset': 'latch.n.02'}, {'name': 'lawn_mower', 'id': 634, 'frequency': 'r', 'synset': 'lawn_mower.n.01'}, {'name': 'leather', 'id': 635, 'frequency': 'r', 'synset': 'leather.n.01'}, {'name': 'legging_(clothing)', 'id': 636, 'frequency': 'c', 'synset': 'legging.n.01'}, {'name': 'Lego', 'id': 637, 'frequency': 'c', 'synset': 'lego.n.01'}, {'name': 'legume', 'id': 638, 'frequency': 'r', 'synset': 'legume.n.02'}, {'name': 'lemon', 'id': 639, 'frequency': 'f', 'synset': 'lemon.n.01'}, {'name': 'lemonade', 'id': 640, 'frequency': 'r', 'synset': 'lemonade.n.01'}, {'name': 'lettuce', 'id': 641, 'frequency': 'f', 'synset': 'lettuce.n.02'}, {'name': 'license_plate', 'id': 642, 'frequency': 'f', 'synset': 'license_plate.n.01'}, {'name': 'life_buoy', 'id': 643, 'frequency': 'f', 'synset': 'life_buoy.n.01'}, {'name': 'life_jacket', 'id': 644, 'frequency': 'f', 'synset': 'life_jacket.n.01'}, {'name': 'lightbulb', 'id': 645, 'frequency': 'f', 'synset': 'light_bulb.n.01'}, {'name': 'lightning_rod', 'id': 646, 'frequency': 'r', 'synset': 'lightning_rod.n.02'}, {'name': 'lime', 'id': 647, 'frequency': 'f', 'synset': 'lime.n.06'}, {'name': 'limousine', 'id': 648, 'frequency': 'r', 'synset': 'limousine.n.01'}, {'name': 'lion', 'id': 649, 'frequency': 'c', 'synset': 'lion.n.01'}, {'name': 'lip_balm', 'id': 650, 'frequency': 'c', 'synset': 'lip_balm.n.01'}, {'name': 'liquor', 'id': 651, 'frequency': 'r', 'synset': 'liquor.n.01'}, {'name': 'lizard', 'id': 652, 'frequency': 'c', 'synset': 'lizard.n.01'}, {'name': 'log', 'id': 653, 'frequency': 'f', 'synset': 'log.n.01'}, {'name': 'lollipop', 'id': 654, 'frequency': 'c', 'synset': 'lollipop.n.02'}, {'name': 'speaker_(stero_equipment)', 'id': 655, 'frequency': 'f', 'synset': 'loudspeaker.n.01'}, {'name': 'loveseat', 'id': 656, 'frequency': 'c', 'synset': 'love_seat.n.01'}, {'name': 'machine_gun', 'id': 657, 'frequency': 'r', 'synset': 'machine_gun.n.01'}, {'name': 'magazine', 'id': 658, 'frequency': 'f', 'synset': 'magazine.n.02'}, {'name': 'magnet', 'id': 659, 'frequency': 'f', 'synset': 'magnet.n.01'}, {'name': 'mail_slot', 'id': 660, 'frequency': 'c', 'synset': 'mail_slot.n.01'}, {'name': 'mailbox_(at_home)', 'id': 661, 'frequency': 'f', 'synset': 'mailbox.n.01'}, {'name': 'mallard', 'id': 662, 'frequency': 'r', 'synset': 'mallard.n.01'}, {'name': 'mallet', 'id': 663, 'frequency': 'r', 'synset': 'mallet.n.01'}, {'name': 'mammoth', 'id': 664, 'frequency': 'r', 'synset': 'mammoth.n.01'}, {'name': 'manatee', 'id': 665, 'frequency': 'r', 'synset': 'manatee.n.01'}, {'name': 'mandarin_orange', 'id': 666, 'frequency': 'c', 'synset': 'mandarin.n.05'}, {'name': 'manger', 'id': 667, 'frequency': 'c', 'synset': 'manger.n.01'}, {'name': 'manhole', 'id': 668, 'frequency': 'f', 'synset': 'manhole.n.01'}, {'name': 'map', 'id': 669, 'frequency': 'f', 'synset': 'map.n.01'}, {'name': 'marker', 'id': 670, 'frequency': 'f', 'synset': 'marker.n.03'}, {'name': 'martini', 'id': 671, 'frequency': 'r', 'synset': 'martini.n.01'}, {'name': 'mascot', 'id': 672, 'frequency': 'r', 'synset': 'mascot.n.01'}, {'name': 'mashed_potato', 'id': 673, 'frequency': 'c', 'synset': 'mashed_potato.n.01'}, {'name': 'masher', 'id': 674, 'frequency': 'r', 'synset': 'masher.n.02'}, {'name': 'mask', 'id': 675, 'frequency': 'f', 'synset': 'mask.n.04'}, {'name': 'mast', 'id': 676, 'frequency': 'f', 'synset': 'mast.n.01'}, {'name': 'mat_(gym_equipment)', 'id': 677, 'frequency': 'c', 'synset': 'mat.n.03'}, {'name': 'matchbox', 'id': 678, 'frequency': 'r', 'synset': 'matchbox.n.01'}, {'name': 'mattress', 'id': 679, 'frequency': 'f', 'synset': 'mattress.n.01'}, {'name': 'measuring_cup', 'id': 680, 'frequency': 'c', 'synset': 'measuring_cup.n.01'}, {'name': 'measuring_stick', 'id': 681, 'frequency': 'c', 'synset': 'measuring_stick.n.01'}, {'name': 'meatball', 'id': 682, 'frequency': 'c', 'synset': 'meatball.n.01'}, {'name': 'medicine', 'id': 683, 'frequency': 'c', 'synset': 'medicine.n.02'}, {'name': 'melon', 'id': 684, 'frequency': 'c', 'synset': 'melon.n.01'}, {'name': 'microphone', 'id': 685, 'frequency': 'f', 'synset': 'microphone.n.01'}, {'name': 'microscope', 'id': 686, 'frequency': 'r', 'synset': 'microscope.n.01'}, {'name': 'microwave_oven', 'id': 687, 'frequency': 'f', 'synset': 'microwave.n.02'}, {'name': 'milestone', 'id': 688, 'frequency': 'r', 'synset': 'milestone.n.01'}, {'name': 'milk', 'id': 689, 'frequency': 'f', 'synset': 'milk.n.01'}, {'name': 'milk_can', 'id': 690, 'frequency': 'r', 'synset': 'milk_can.n.01'}, {'name': 'milkshake', 'id': 691, 'frequency': 'r', 'synset': 'milkshake.n.01'}, {'name': 'minivan', 'id': 692, 'frequency': 'f', 'synset': 'minivan.n.01'}, {'name': 'mint_candy', 'id': 693, 'frequency': 'r', 'synset': 'mint.n.05'}, {'name': 'mirror', 'id': 694, 'frequency': 'f', 'synset': 'mirror.n.01'}, {'name': 'mitten', 'id': 695, 'frequency': 'c', 'synset': 'mitten.n.01'}, {'name': 'mixer_(kitchen_tool)', 'id': 696, 'frequency': 'c', 'synset': 'mixer.n.04'}, {'name': 'money', 'id': 697, 'frequency': 'c', 'synset': 'money.n.03'}, {'name': 'monitor_(computer_equipment) computer_monitor', 'id': 698, 'frequency': 'f', 'synset': 'monitor.n.04'}, {'name': 'monkey', 'id': 699, 'frequency': 'c', 'synset': 'monkey.n.01'}, {'name': 'motor', 'id': 700, 'frequency': 'f', 'synset': 'motor.n.01'}, {'name': 'motor_scooter', 'id': 701, 'frequency': 'f', 'synset': 'motor_scooter.n.01'}, {'name': 'motor_vehicle', 'id': 702, 'frequency': 'r', 'synset': 'motor_vehicle.n.01'}, {'name': 'motorcycle', 'id': 703, 'frequency': 'f', 'synset': 'motorcycle.n.01'}, {'name': 'mound_(baseball)', 'id': 704, 'frequency': 'f', 'synset': 'mound.n.01'}, {'name': 'mouse_(computer_equipment)', 'id': 705, 'frequency': 'f', 'synset': 'mouse.n.04'}, {'name': 'mousepad', 'id': 706, 'frequency': 'f', 'synset': 'mousepad.n.01'}, {'name': 'muffin', 'id': 707, 'frequency': 'c', 'synset': 'muffin.n.01'}, {'name': 'mug', 'id': 708, 'frequency': 'f', 'synset': 'mug.n.04'}, {'name': 'mushroom', 'id': 709, 'frequency': 'f', 'synset': 'mushroom.n.02'}, {'name': 'music_stool', 'id': 710, 'frequency': 'r', 'synset': 'music_stool.n.01'}, {'name': 'musical_instrument', 'id': 711, 'frequency': 'c', 'synset': 'musical_instrument.n.01'}, {'name': 'nailfile', 'id': 712, 'frequency': 'r', 'synset': 'nailfile.n.01'}, {'name': 'napkin', 'id': 713, 'frequency': 'f', 'synset': 'napkin.n.01'}, {'name': 'neckerchief', 'id': 714, 'frequency': 'r', 'synset': 'neckerchief.n.01'}, {'name': 'necklace', 'id': 715, 'frequency': 'f', 'synset': 'necklace.n.01'}, {'name': 'necktie', 'id': 716, 'frequency': 'f', 'synset': 'necktie.n.01'}, {'name': 'needle', 'id': 717, 'frequency': 'c', 'synset': 'needle.n.03'}, {'name': 'nest', 'id': 718, 'frequency': 'c', 'synset': 'nest.n.01'}, {'name': 'newspaper', 'id': 719, 'frequency': 'f', 'synset': 'newspaper.n.01'}, {'name': 'newsstand', 'id': 720, 'frequency': 'c', 'synset': 'newsstand.n.01'}, {'name': 'nightshirt', 'id': 721, 'frequency': 'c', 'synset': 'nightwear.n.01'}, {'name': 'nosebag_(for_animals)', 'id': 722, 'frequency': 'r', 'synset': 'nosebag.n.01'}, {'name': 'noseband_(for_animals)', 'id': 723, 'frequency': 'c', 'synset': 'noseband.n.01'}, {'name': 'notebook', 'id': 724, 'frequency': 'f', 'synset': 'notebook.n.01'}, {'name': 'notepad', 'id': 725, 'frequency': 'c', 'synset': 'notepad.n.01'}, {'name': 'nut', 'id': 726, 'frequency': 'f', 'synset': 'nut.n.03'}, {'name': 'nutcracker', 'id': 727, 'frequency': 'r', 'synset': 'nutcracker.n.01'}, {'name': 'oar', 'id': 728, 'frequency': 'f', 'synset': 'oar.n.01'}, {'name': 'octopus_(food)', 'id': 729, 'frequency': 'r', 'synset': 'octopus.n.01'}, {'name': 'octopus_(animal)', 'id': 730, 'frequency': 'r', 'synset': 'octopus.n.02'}, {'name': 'oil_lamp', 'id': 731, 'frequency': 'c', 'synset': 'oil_lamp.n.01'}, {'name': 'olive_oil', 'id': 732, 'frequency': 'c', 'synset': 'olive_oil.n.01'}, {'name': 'omelet', 'id': 733, 'frequency': 'r', 'synset': 'omelet.n.01'}, {'name': 'onion', 'id': 734, 'frequency': 'f', 'synset': 'onion.n.01'}, {'name': 'orange_(fruit)', 'id': 735, 'frequency': 'f', 'synset': 'orange.n.01'}, {'name': 'orange_juice', 'id': 736, 'frequency': 'c', 'synset': 'orange_juice.n.01'}, {'name': 'ostrich', 'id': 737, 'frequency': 'c', 'synset': 'ostrich.n.02'}, {'name': 'ottoman', 'id': 738, 'frequency': 'f', 'synset': 'ottoman.n.03'}, {'name': 'oven', 'id': 739, 'frequency': 'f', 'synset': 'oven.n.01'}, {'name': 'overalls_(clothing)', 'id': 740, 'frequency': 'c', 'synset': 'overall.n.01'}, {'name': 'owl', 'id': 741, 'frequency': 'c', 'synset': 'owl.n.01'}, {'name': 'packet', 'id': 742, 'frequency': 'c', 'synset': 'packet.n.03'}, {'name': 'inkpad', 'id': 743, 'frequency': 'r', 'synset': 'pad.n.03'}, {'name': 'pad', 'id': 744, 'frequency': 'c', 'synset': 'pad.n.04'}, {'name': 'paddle', 'id': 745, 'frequency': 'f', 'synset': 'paddle.n.04'}, {'name': 'padlock', 'id': 746, 'frequency': 'c', 'synset': 'padlock.n.01'}, {'name': 'paintbrush', 'id': 747, 'frequency': 'c', 'synset': 'paintbrush.n.01'}, {'name': 'painting', 'id': 748, 'frequency': 'f', 'synset': 'painting.n.01'}, {'name': 'pajamas', 'id': 749, 'frequency': 'f', 'synset': 'pajama.n.02'}, {'name': 'palette', 'id': 750, 'frequency': 'c', 'synset': 'palette.n.02'}, {'name': 'pan_(for_cooking)', 'id': 751, 'frequency': 'f', 'synset': 'pan.n.01'}, {'name': 'pan_(metal_container)', 'id': 752, 'frequency': 'r', 'synset': 'pan.n.03'}, {'name': 'pancake', 'id': 753, 'frequency': 'c', 'synset': 'pancake.n.01'}, {'name': 'pantyhose', 'id': 754, 'frequency': 'r', 'synset': 'pantyhose.n.01'}, {'name': 'papaya', 'id': 755, 'frequency': 'r', 'synset': 'papaya.n.02'}, {'name': 'paper_plate', 'id': 756, 'frequency': 'f', 'synset': 'paper_plate.n.01'}, {'name': 'paper_towel', 'id': 757, 'frequency': 'f', 'synset': 'paper_towel.n.01'}, {'name': 'paperback_book', 'id': 758, 'frequency': 'r', 'synset': 'paperback_book.n.01'}, {'name': 'paperweight', 'id': 759, 'frequency': 'r', 'synset': 'paperweight.n.01'}, {'name': 'parachute', 'id': 760, 'frequency': 'c', 'synset': 'parachute.n.01'}, {'name': 'parakeet', 'id': 761, 'frequency': 'c', 'synset': 'parakeet.n.01'}, {'name': 'parasail_(sports)', 'id': 762, 'frequency': 'c', 'synset': 'parasail.n.01'}, {'name': 'parasol', 'id': 763, 'frequency': 'c', 'synset': 'parasol.n.01'}, {'name': 'parchment', 'id': 764, 'frequency': 'r', 'synset': 'parchment.n.01'}, {'name': 'parka', 'id': 765, 'frequency': 'c', 'synset': 'parka.n.01'}, {'name': 'parking_meter', 'id': 766, 'frequency': 'f', 'synset': 'parking_meter.n.01'}, {'name': 'parrot', 'id': 767, 'frequency': 'c', 'synset': 'parrot.n.01'}, {'name': 'passenger_car_(part_of_a_train)', 'id': 768, 'frequency': 'c', 'synset': 'passenger_car.n.01'}, {'name': 'passenger_ship', 'id': 769, 'frequency': 'r', 'synset': 'passenger_ship.n.01'}, {'name': 'passport', 'id': 770, 'frequency': 'c', 'synset': 'passport.n.02'}, {'name': 'pastry', 'id': 771, 'frequency': 'f', 'synset': 'pastry.n.02'}, {'name': 'patty_(food)', 'id': 772, 'frequency': 'r', 'synset': 'patty.n.01'}, {'name': 'pea_(food)', 'id': 773, 'frequency': 'c', 'synset': 'pea.n.01'}, {'name': 'peach', 'id': 774, 'frequency': 'c', 'synset': 'peach.n.03'}, {'name': 'peanut_butter', 'id': 775, 'frequency': 'c', 'synset': 'peanut_butter.n.01'}, {'name': 'pear', 'id': 776, 'frequency': 'f', 'synset': 'pear.n.01'}, {'name': 'peeler_(tool_for_fruit_and_vegetables)', 'id': 777, 'frequency': 'c', 'synset': 'peeler.n.03'}, {'name': 'wooden_leg', 'id': 778, 'frequency': 'r', 'synset': 'peg.n.04'}, {'name': 'pegboard', 'id': 779, 'frequency': 'r', 'synset': 'pegboard.n.01'}, {'name': 'pelican', 'id': 780, 'frequency': 'c', 'synset': 'pelican.n.01'}, {'name': 'pen', 'id': 781, 'frequency': 'f', 'synset': 'pen.n.01'}, {'name': 'pencil', 'id': 782, 'frequency': 'f', 'synset': 'pencil.n.01'}, {'name': 'pencil_box', 'id': 783, 'frequency': 'r', 'synset': 'pencil_box.n.01'}, {'name': 'pencil_sharpener', 'id': 784, 'frequency': 'r', 'synset': 'pencil_sharpener.n.01'}, {'name': 'pendulum', 'id': 785, 'frequency': 'r', 'synset': 'pendulum.n.01'}, {'name': 'penguin', 'id': 786, 'frequency': 'c', 'synset': 'penguin.n.01'}, {'name': 'pennant', 'id': 787, 'frequency': 'r', 'synset': 'pennant.n.02'}, {'name': 'penny_(coin)', 'id': 788, 'frequency': 'r', 'synset': 'penny.n.02'}, {'name': 'pepper', 'id': 789, 'frequency': 'f', 'synset': 'pepper.n.03'}, {'name': 'pepper_mill', 'id': 790, 'frequency': 'c', 'synset': 'pepper_mill.n.01'}, {'name': 'perfume', 'id': 791, 'frequency': 'c', 'synset': 'perfume.n.02'}, {'name': 'persimmon', 'id': 792, 'frequency': 'r', 'synset': 'persimmon.n.02'}, {'name': 'person', 'id': 793, 'frequency': 'f', 'synset': 'person.n.01'}, {'name': 'pet', 'id': 794, 'frequency': 'c', 'synset': 'pet.n.01'}, {'name': 'pew_(church_bench)', 'id': 795, 'frequency': 'c', 'synset': 'pew.n.01'}, {'name': 'phonebook', 'id': 796, 'frequency': 'r', 'synset': 'phonebook.n.01'}, {'name': 'phonograph_record', 'id': 797, 'frequency': 'c', 'synset': 'phonograph_record.n.01'}, {'name': 'piano', 'id': 798, 'frequency': 'f', 'synset': 'piano.n.01'}, {'name': 'pickle', 'id': 799, 'frequency': 'f', 'synset': 'pickle.n.01'}, {'name': 'pickup_truck', 'id': 800, 'frequency': 'f', 'synset': 'pickup.n.01'}, {'name': 'pie', 'id': 801, 'frequency': 'c', 'synset': 'pie.n.01'}, {'name': 'pigeon', 'id': 802, 'frequency': 'c', 'synset': 'pigeon.n.01'}, {'name': 'piggy_bank', 'id': 803, 'frequency': 'r', 'synset': 'piggy_bank.n.01'}, {'name': 'pillow', 'id': 804, 'frequency': 'f', 'synset': 'pillow.n.01'}, {'name': 'pin_(non_jewelry)', 'id': 805, 'frequency': 'r', 'synset': 'pin.n.09'}, {'name': 'pineapple', 'id': 806, 'frequency': 'f', 'synset': 'pineapple.n.02'}, {'name': 'pinecone', 'id': 807, 'frequency': 'c', 'synset': 'pinecone.n.01'}, {'name': 'ping-pong_ball', 'id': 808, 'frequency': 'r', 'synset': 'ping-pong_ball.n.01'}, {'name': 'pinwheel', 'id': 809, 'frequency': 'r', 'synset': 'pinwheel.n.03'}, {'name': 'tobacco_pipe', 'id': 810, 'frequency': 'r', 'synset': 'pipe.n.01'}, {'name': 'pipe', 'id': 811, 'frequency': 'f', 'synset': 'pipe.n.02'}, {'name': 'pistol', 'id': 812, 'frequency': 'r', 'synset': 'pistol.n.01'}, {'name': 'pita_(bread)', 'id': 813, 'frequency': 'c', 'synset': 'pita.n.01'}, {'name': 'pitcher_(vessel_for_liquid)', 'id': 814, 'frequency': 'f', 'synset': 'pitcher.n.02'}, {'name': 'pitchfork', 'id': 815, 'frequency': 'r', 'synset': 'pitchfork.n.01'}, {'name': 'pizza', 'id': 816, 'frequency': 'f', 'synset': 'pizza.n.01'}, {'name': 'place_mat', 'id': 817, 'frequency': 'f', 'synset': 'place_mat.n.01'}, {'name': 'plate', 'id': 818, 'frequency': 'f', 'synset': 'plate.n.04'}, {'name': 'platter', 'id': 819, 'frequency': 'c', 'synset': 'platter.n.01'}, {'name': 'playpen', 'id': 820, 'frequency': 'r', 'synset': 'playpen.n.01'}, {'name': 'pliers', 'id': 821, 'frequency': 'c', 'synset': 'pliers.n.01'}, {'name': 'plow_(farm_equipment)', 'id': 822, 'frequency': 'r', 'synset': 'plow.n.01'}, {'name': 'plume', 'id': 823, 'frequency': 'r', 'synset': 'plume.n.02'}, {'name': 'pocket_watch', 'id': 824, 'frequency': 'r', 'synset': 'pocket_watch.n.01'}, {'name': 'pocketknife', 'id': 825, 'frequency': 'c', 'synset': 'pocketknife.n.01'}, {'name': 'poker_(fire_stirring_tool)', 'id': 826, 'frequency': 'c', 'synset': 'poker.n.01'}, {'name': 'pole', 'id': 827, 'frequency': 'f', 'synset': 'pole.n.01'}, {'name': 'polo_shirt', 'id': 828, 'frequency': 'f', 'synset': 'polo_shirt.n.01'}, {'name': 'poncho', 'id': 829, 'frequency': 'r', 'synset': 'poncho.n.01'}, {'name': 'pony', 'id': 830, 'frequency': 'c', 'synset': 'pony.n.05'}, {'name': 'pool_table', 'id': 831, 'frequency': 'r', 'synset': 'pool_table.n.01'}, {'name': 'pop_(soda)', 'id': 832, 'frequency': 'f', 'synset': 'pop.n.02'}, {'name': 'postbox_(public)', 'id': 833, 'frequency': 'c', 'synset': 'postbox.n.01'}, {'name': 'postcard', 'id': 834, 'frequency': 'c', 'synset': 'postcard.n.01'}, {'name': 'poster', 'id': 835, 'frequency': 'f', 'synset': 'poster.n.01'}, {'name': 'pot', 'id': 836, 'frequency': 'f', 'synset': 'pot.n.01'}, {'name': 'flowerpot', 'id': 837, 'frequency': 'f', 'synset': 'pot.n.04'}, {'name': 'potato', 'id': 838, 'frequency': 'f', 'synset': 'potato.n.01'}, {'name': 'potholder', 'id': 839, 'frequency': 'c', 'synset': 'potholder.n.01'}, {'name': 'pottery', 'id': 840, 'frequency': 'c', 'synset': 'pottery.n.01'}, {'name': 'pouch', 'id': 841, 'frequency': 'c', 'synset': 'pouch.n.01'}, {'name': 'power_shovel', 'id': 842, 'frequency': 'c', 'synset': 'power_shovel.n.01'}, {'name': 'prawn', 'id': 843, 'frequency': 'c', 'synset': 'prawn.n.01'}, {'name': 'pretzel', 'id': 844, 'frequency': 'c', 'synset': 'pretzel.n.01'}, {'name': 'printer', 'id': 845, 'frequency': 'f', 'synset': 'printer.n.03'}, {'name': 'projectile_(weapon)', 'id': 846, 'frequency': 'c', 'synset': 'projectile.n.01'}, {'name': 'projector', 'id': 847, 'frequency': 'c', 'synset': 'projector.n.02'}, {'name': 'propeller', 'id': 848, 'frequency': 'f', 'synset': 'propeller.n.01'}, {'name': 'prune', 'id': 849, 'frequency': 'r', 'synset': 'prune.n.01'}, {'name': 'pudding', 'id': 850, 'frequency': 'r', 'synset': 'pudding.n.01'}, {'name': 'puffer_(fish)', 'id': 851, 'frequency': 'r', 'synset': 'puffer.n.02'}, {'name': 'puffin', 'id': 852, 'frequency': 'r', 'synset': 'puffin.n.01'}, {'name': 'pug-dog', 'id': 853, 'frequency': 'r', 'synset': 'pug.n.01'}, {'name': 'pumpkin', 'id': 854, 'frequency': 'c', 'synset': 'pumpkin.n.02'}, {'name': 'puncher', 'id': 855, 'frequency': 'r', 'synset': 'punch.n.03'}, {'name': 'puppet', 'id': 856, 'frequency': 'r', 'synset': 'puppet.n.01'}, {'name': 'puppy', 'id': 857, 'frequency': 'c', 'synset': 'puppy.n.01'}, {'name': 'quesadilla', 'id': 858, 'frequency': 'r', 'synset': 'quesadilla.n.01'}, {'name': 'quiche', 'id': 859, 'frequency': 'r', 'synset': 'quiche.n.02'}, {'name': 'quilt', 'id': 860, 'frequency': 'f', 'synset': 'quilt.n.01'}, {'name': 'rabbit', 'id': 861, 'frequency': 'c', 'synset': 'rabbit.n.01'}, {'name': 'race_car', 'id': 862, 'frequency': 'r', 'synset': 'racer.n.02'}, {'name': 'racket', 'id': 863, 'frequency': 'c', 'synset': 'racket.n.04'}, {'name': 'radar', 'id': 864, 'frequency': 'r', 'synset': 'radar.n.01'}, {'name': 'radiator', 'id': 865, 'frequency': 'f', 'synset': 'radiator.n.03'}, {'name': 'radio_receiver', 'id': 866, 'frequency': 'c', 'synset': 'radio_receiver.n.01'}, {'name': 'radish', 'id': 867, 'frequency': 'c', 'synset': 'radish.n.03'}, {'name': 'raft', 'id': 868, 'frequency': 'c', 'synset': 'raft.n.01'}, {'name': 'rag_doll', 'id': 869, 'frequency': 'r', 'synset': 'rag_doll.n.01'}, {'name': 'raincoat', 'id': 870, 'frequency': 'c', 'synset': 'raincoat.n.01'}, {'name': 'ram_(animal)', 'id': 871, 'frequency': 'c', 'synset': 'ram.n.05'}, {'name': 'raspberry', 'id': 872, 'frequency': 'c', 'synset': 'raspberry.n.02'}, {'name': 'rat', 'id': 873, 'frequency': 'r', 'synset': 'rat.n.01'}, {'name': 'razorblade', 'id': 874, 'frequency': 'c', 'synset': 'razorblade.n.01'}, {'name': 'reamer_(juicer)', 'id': 875, 'frequency': 'c', 'synset': 'reamer.n.01'}, {'name': 'rearview_mirror', 'id': 876, 'frequency': 'f', 'synset': 'rearview_mirror.n.01'}, {'name': 'receipt', 'id': 877, 'frequency': 'c', 'synset': 'receipt.n.02'}, {'name': 'recliner', 'id': 878, 'frequency': 'c', 'synset': 'recliner.n.01'}, {'name': 'record_player', 'id': 879, 'frequency': 'c', 'synset': 'record_player.n.01'}, {'name': 'reflector', 'id': 880, 'frequency': 'f', 'synset': 'reflector.n.01'}, {'name': 'remote_control', 'id': 881, 'frequency': 'f', 'synset': 'remote_control.n.01'}, {'name': 'rhinoceros', 'id': 882, 'frequency': 'c', 'synset': 'rhinoceros.n.01'}, {'name': 'rib_(food)', 'id': 883, 'frequency': 'r', 'synset': 'rib.n.03'}, {'name': 'rifle', 'id': 884, 'frequency': 'c', 'synset': 'rifle.n.01'}, {'name': 'ring', 'id': 885, 'frequency': 'f', 'synset': 'ring.n.08'}, {'name': 'river_boat', 'id': 886, 'frequency': 'r', 'synset': 'river_boat.n.01'}, {'name': 'road_map', 'id': 887, 'frequency': 'r', 'synset': 'road_map.n.02'}, {'name': 'robe', 'id': 888, 'frequency': 'c', 'synset': 'robe.n.01'}, {'name': 'rocking_chair', 'id': 889, 'frequency': 'c', 'synset': 'rocking_chair.n.01'}, {'name': 'rodent', 'id': 890, 'frequency': 'r', 'synset': 'rodent.n.01'}, {'name': 'roller_skate', 'id': 891, 'frequency': 'r', 'synset': 'roller_skate.n.01'}, {'name': 'Rollerblade', 'id': 892, 'frequency': 'r', 'synset': 'rollerblade.n.01'}, {'name': 'rolling_pin', 'id': 893, 'frequency': 'c', 'synset': 'rolling_pin.n.01'}, {'name': 'root_beer', 'id': 894, 'frequency': 'r', 'synset': 'root_beer.n.01'}, {'name': 'router_(computer_equipment)', 'id': 895, 'frequency': 'c', 'synset': 'router.n.02'}, {'name': 'rubber_band', 'id': 896, 'frequency': 'f', 'synset': 'rubber_band.n.01'}, {'name': 'runner_(carpet)', 'id': 897, 'frequency': 'c', 'synset': 'runner.n.08'}, {'name': 'plastic_bag', 'id': 898, 'frequency': 'f', 'synset': 'sack.n.01'}, {'name': 'saddle_(on_an_animal)', 'id': 899, 'frequency': 'f', 'synset': 'saddle.n.01'}, {'name': 'saddle_blanket', 'id': 900, 'frequency': 'f', 'synset': 'saddle_blanket.n.01'}, {'name': 'saddlebag', 'id': 901, 'frequency': 'c', 'synset': 'saddlebag.n.01'}, {'name': 'safety_pin', 'id': 902, 'frequency': 'r', 'synset': 'safety_pin.n.01'}, {'name': 'sail', 'id': 903, 'frequency': 'f', 'synset': 'sail.n.01'}, {'name': 'salad', 'id': 904, 'frequency': 'f', 'synset': 'salad.n.01'}, {'name': 'salad_plate', 'id': 905, 'frequency': 'r', 'synset': 'salad_plate.n.01'}, {'name': 'salami', 'id': 906, 'frequency': 'c', 'synset': 'salami.n.01'}, {'name': 'salmon_(fish)', 'id': 907, 'frequency': 'c', 'synset': 'salmon.n.01'}, {'name': 'salmon_(food)', 'id': 908, 'frequency': 'r', 'synset': 'salmon.n.03'}, {'name': 'salsa', 'id': 909, 'frequency': 'c', 'synset': 'salsa.n.01'}, {'name': 'saltshaker', 'id': 910, 'frequency': 'f', 'synset': 'saltshaker.n.01'}, {'name': 'sandal_(type_of_shoe)', 'id': 911, 'frequency': 'f', 'synset': 'sandal.n.01'}, {'name': 'sandwich', 'id': 912, 'frequency': 'f', 'synset': 'sandwich.n.01'}, {'name': 'satchel', 'id': 913, 'frequency': 'r', 'synset': 'satchel.n.01'}, {'name': 'saucepan', 'id': 914, 'frequency': 'r', 'synset': 'saucepan.n.01'}, {'name': 'saucer', 'id': 915, 'frequency': 'f', 'synset': 'saucer.n.02'}, {'name': 'sausage', 'id': 916, 'frequency': 'f', 'synset': 'sausage.n.01'}, {'name': 'sawhorse', 'id': 917, 'frequency': 'r', 'synset': 'sawhorse.n.01'}, {'name': 'saxophone', 'id': 918, 'frequency': 'r', 'synset': 'sax.n.02'}, {'name': 'scale_(measuring_instrument)', 'id': 919, 'frequency': 'f', 'synset': 'scale.n.07'}, {'name': 'scarecrow', 'id': 920, 'frequency': 'r', 'synset': 'scarecrow.n.01'}, {'name': 'scarf', 'id': 921, 'frequency': 'f', 'synset': 'scarf.n.01'}, {'name': 'school_bus', 'id': 922, 'frequency': 'c', 'synset': 'school_bus.n.01'}, {'name': 'scissors', 'id': 923, 'frequency': 'f', 'synset': 'scissors.n.01'}, {'name': 'scoreboard', 'id': 924, 'frequency': 'f', 'synset': 'scoreboard.n.01'}, {'name': 'scraper', 'id': 925, 'frequency': 'r', 'synset': 'scraper.n.01'}, {'name': 'screwdriver', 'id': 926, 'frequency': 'c', 'synset': 'screwdriver.n.01'}, {'name': 'scrubbing_brush', 'id': 927, 'frequency': 'f', 'synset': 'scrub_brush.n.01'}, {'name': 'sculpture', 'id': 928, 'frequency': 'c', 'synset': 'sculpture.n.01'}, {'name': 'seabird', 'id': 929, 'frequency': 'c', 'synset': 'seabird.n.01'}, {'name': 'seahorse', 'id': 930, 'frequency': 'c', 'synset': 'seahorse.n.02'}, {'name': 'seaplane', 'id': 931, 'frequency': 'r', 'synset': 'seaplane.n.01'}, {'name': 'seashell', 'id': 932, 'frequency': 'c', 'synset': 'seashell.n.01'}, {'name': 'sewing_machine', 'id': 933, 'frequency': 'c', 'synset': 'sewing_machine.n.01'}, {'name': 'shaker', 'id': 934, 'frequency': 'c', 'synset': 'shaker.n.03'}, {'name': 'shampoo', 'id': 935, 'frequency': 'c', 'synset': 'shampoo.n.01'}, {'name': 'shark', 'id': 936, 'frequency': 'c', 'synset': 'shark.n.01'}, {'name': 'sharpener', 'id': 937, 'frequency': 'r', 'synset': 'sharpener.n.01'}, {'name': 'Sharpie', 'id': 938, 'frequency': 'r', 'synset': 'sharpie.n.03'}, {'name': 'shaver_(electric)', 'id': 939, 'frequency': 'r', 'synset': 'shaver.n.03'}, {'name': 'shaving_cream', 'id': 940, 'frequency': 'c', 'synset': 'shaving_cream.n.01'}, {'name': 'shawl', 'id': 941, 'frequency': 'r', 'synset': 'shawl.n.01'}, {'name': 'shears', 'id': 942, 'frequency': 'r', 'synset': 'shears.n.01'}, {'name': 'sheep', 'id': 943, 'frequency': 'f', 'synset': 'sheep.n.01'}, {'name': 'shepherd_dog', 'id': 944, 'frequency': 'r', 'synset': 'shepherd_dog.n.01'}, {'name': 'sherbert', 'id': 945, 'frequency': 'r', 'synset': 'sherbert.n.01'}, {'name': 'shield', 'id': 946, 'frequency': 'c', 'synset': 'shield.n.02'}, {'name': 'shirt', 'id': 947, 'frequency': 'f', 'synset': 'shirt.n.01'}, {'name': 'shoe', 'id': 948, 'frequency': 'f', 'synset': 'shoe.n.01'}, {'name': 'shopping_bag', 'id': 949, 'frequency': 'f', 'synset': 'shopping_bag.n.01'}, {'name': 'shopping_cart', 'id': 950, 'frequency': 'c', 'synset': 'shopping_cart.n.01'}, {'name': 'short_pants', 'id': 951, 'frequency': 'f', 'synset': 'short_pants.n.01'}, {'name': 'shot_glass', 'id': 952, 'frequency': 'r', 'synset': 'shot_glass.n.01'}, {'name': 'shoulder_bag', 'id': 953, 'frequency': 'f', 'synset': 'shoulder_bag.n.01'}, {'name': 'shovel', 'id': 954, 'frequency': 'c', 'synset': 'shovel.n.01'}, {'name': 'shower_head', 'id': 955, 'frequency': 'f', 'synset': 'shower.n.01'}, {'name': 'shower_cap', 'id': 956, 'frequency': 'r', 'synset': 'shower_cap.n.01'}, {'name': 'shower_curtain', 'id': 957, 'frequency': 'f', 'synset': 'shower_curtain.n.01'}, {'name': 'shredder_(for_paper)', 'id': 958, 'frequency': 'r', 'synset': 'shredder.n.01'}, {'name': 'signboard', 'id': 959, 'frequency': 'f', 'synset': 'signboard.n.01'}, {'name': 'silo', 'id': 960, 'frequency': 'c', 'synset': 'silo.n.01'}, {'name': 'sink', 'id': 961, 'frequency': 'f', 'synset': 'sink.n.01'}, {'name': 'skateboard', 'id': 962, 'frequency': 'f', 'synset': 'skateboard.n.01'}, {'name': 'skewer', 'id': 963, 'frequency': 'c', 'synset': 'skewer.n.01'}, {'name': 'ski', 'id': 964, 'frequency': 'f', 'synset': 'ski.n.01'}, {'name': 'ski_boot', 'id': 965, 'frequency': 'f', 'synset': 'ski_boot.n.01'}, {'name': 'ski_parka', 'id': 966, 'frequency': 'f', 'synset': 'ski_parka.n.01'}, {'name': 'ski_pole', 'id': 967, 'frequency': 'f', 'synset': 'ski_pole.n.01'}, {'name': 'skirt', 'id': 968, 'frequency': 'f', 'synset': 'skirt.n.02'}, {'name': 'skullcap', 'id': 969, 'frequency': 'r', 'synset': 'skullcap.n.01'}, {'name': 'sled', 'id': 970, 'frequency': 'c', 'synset': 'sled.n.01'}, {'name': 'sleeping_bag', 'id': 971, 'frequency': 'c', 'synset': 'sleeping_bag.n.01'}, {'name': 'sling_(bandage)', 'id': 972, 'frequency': 'r', 'synset': 'sling.n.05'}, {'name': 'slipper_(footwear)', 'id': 973, 'frequency': 'c', 'synset': 'slipper.n.01'}, {'name': 'smoothie', 'id': 974, 'frequency': 'r', 'synset': 'smoothie.n.02'}, {'name': 'snake', 'id': 975, 'frequency': 'r', 'synset': 'snake.n.01'}, {'name': 'snowboard', 'id': 976, 'frequency': 'f', 'synset': 'snowboard.n.01'}, {'name': 'snowman', 'id': 977, 'frequency': 'c', 'synset': 'snowman.n.01'}, {'name': 'snowmobile', 'id': 978, 'frequency': 'c', 'synset': 'snowmobile.n.01'}, {'name': 'soap', 'id': 979, 'frequency': 'f', 'synset': 'soap.n.01'}, {'name': 'soccer_ball', 'id': 980, 'frequency': 'f', 'synset': 'soccer_ball.n.01'}, {'name': 'sock', 'id': 981, 'frequency': 'f', 'synset': 'sock.n.01'}, {'name': 'sofa', 'id': 982, 'frequency': 'f', 'synset': 'sofa.n.01'}, {'name': 'softball', 'id': 983, 'frequency': 'r', 'synset': 'softball.n.01'}, {'name': 'solar_array', 'id': 984, 'frequency': 'c', 'synset': 'solar_array.n.01'}, {'name': 'sombrero', 'id': 985, 'frequency': 'r', 'synset': 'sombrero.n.02'}, {'name': 'soup', 'id': 986, 'frequency': 'f', 'synset': 'soup.n.01'}, {'name': 'soup_bowl', 'id': 987, 'frequency': 'r', 'synset': 'soup_bowl.n.01'}, {'name': 'soupspoon', 'id': 988, 'frequency': 'c', 'synset': 'soupspoon.n.01'}, {'name': 'sour_cream', 'id': 989, 'frequency': 'c', 'synset': 'sour_cream.n.01'}, {'name': 'soya_milk', 'id': 990, 'frequency': 'r', 'synset': 'soya_milk.n.01'}, {'name': 'space_shuttle', 'id': 991, 'frequency': 'r', 'synset': 'space_shuttle.n.01'}, {'name': 'sparkler_(fireworks)', 'id': 992, 'frequency': 'r', 'synset': 'sparkler.n.02'}, {'name': 'spatula', 'id': 993, 'frequency': 'f', 'synset': 'spatula.n.02'}, {'name': 'spear', 'id': 994, 'frequency': 'r', 'synset': 'spear.n.01'}, {'name': 'spectacles', 'id': 995, 'frequency': 'f', 'synset': 'spectacles.n.01'}, {'name': 'spice_rack', 'id': 996, 'frequency': 'c', 'synset': 'spice_rack.n.01'}, {'name': 'spider', 'id': 997, 'frequency': 'c', 'synset': 'spider.n.01'}, {'name': 'crawfish', 'id': 998, 'frequency': 'r', 'synset': 'spiny_lobster.n.02'}, {'name': 'sponge', 'id': 999, 'frequency': 'c', 'synset': 'sponge.n.01'}, {'name': 'spoon', 'id': 1000, 'frequency': 'f', 'synset': 'spoon.n.01'}, {'name': 'sportswear', 'id': 1001, 'frequency': 'c', 'synset': 'sportswear.n.01'}, {'name': 'spotlight', 'id': 1002, 'frequency': 'c', 'synset': 'spotlight.n.02'}, {'name': 'squid_(food)', 'id': 1003, 'frequency': 'r', 'synset': 'squid.n.01'}, {'name': 'squirrel', 'id': 1004, 'frequency': 'c', 'synset': 'squirrel.n.01'}, {'name': 'stagecoach', 'id': 1005, 'frequency': 'r', 'synset': 'stagecoach.n.01'}, {'name': 'stapler_(stapling_machine)', 'id': 1006, 'frequency': 'c', 'synset': 'stapler.n.01'}, {'name': 'starfish', 'id': 1007, 'frequency': 'c', 'synset': 'starfish.n.01'}, {'name': 'statue_(sculpture)', 'id': 1008, 'frequency': 'f', 'synset': 'statue.n.01'}, {'name': 'steak_(food)', 'id': 1009, 'frequency': 'c', 'synset': 'steak.n.01'}, {'name': 'steak_knife', 'id': 1010, 'frequency': 'r', 'synset': 'steak_knife.n.01'}, {'name': 'steering_wheel', 'id': 1011, 'frequency': 'f', 'synset': 'steering_wheel.n.01'}, {'name': 'stepladder', 'id': 1012, 'frequency': 'r', 'synset': 'step_ladder.n.01'}, {'name': 'step_stool', 'id': 1013, 'frequency': 'c', 'synset': 'step_stool.n.01'}, {'name': 'stereo_(sound_system)', 'id': 1014, 'frequency': 'c', 'synset': 'stereo.n.01'}, {'name': 'stew', 'id': 1015, 'frequency': 'r', 'synset': 'stew.n.02'}, {'name': 'stirrer', 'id': 1016, 'frequency': 'r', 'synset': 'stirrer.n.02'}, {'name': 'stirrup', 'id': 1017, 'frequency': 'f', 'synset': 'stirrup.n.01'}, {'name': 'stool', 'id': 1018, 'frequency': 'f', 'synset': 'stool.n.01'}, {'name': 'stop_sign', 'id': 1019, 'frequency': 'f', 'synset': 'stop_sign.n.01'}, {'name': 'brake_light', 'id': 1020, 'frequency': 'f', 'synset': 'stoplight.n.01'}, {'name': 'stove', 'id': 1021, 'frequency': 'f', 'synset': 'stove.n.01'}, {'name': 'strainer', 'id': 1022, 'frequency': 'c', 'synset': 'strainer.n.01'}, {'name': 'strap', 'id': 1023, 'frequency': 'f', 'synset': 'strap.n.01'}, {'name': 'straw_(for_drinking)', 'id': 1024, 'frequency': 'f', 'synset': 'straw.n.04'}, {'name': 'strawberry', 'id': 1025, 'frequency': 'f', 'synset': 'strawberry.n.01'}, {'name': 'street_sign', 'id': 1026, 'frequency': 'f', 'synset': 'street_sign.n.01'}, {'name': 'streetlight', 'id': 1027, 'frequency': 'f', 'synset': 'streetlight.n.01'}, {'name': 'string_cheese', 'id': 1028, 'frequency': 'r', 'synset': 'string_cheese.n.01'}, {'name': 'stylus', 'id': 1029, 'frequency': 'r', 'synset': 'stylus.n.02'}, {'name': 'subwoofer', 'id': 1030, 'frequency': 'r', 'synset': 'subwoofer.n.01'}, {'name': 'sugar_bowl', 'id': 1031, 'frequency': 'r', 'synset': 'sugar_bowl.n.01'}, {'name': 'sugarcane_(plant)', 'id': 1032, 'frequency': 'r', 'synset': 'sugarcane.n.01'}, {'name': 'suit_(clothing)', 'id': 1033, 'frequency': 'f', 'synset': 'suit.n.01'}, {'name': 'sunflower', 'id': 1034, 'frequency': 'c', 'synset': 'sunflower.n.01'}, {'name': 'sunglasses', 'id': 1035, 'frequency': 'f', 'synset': 'sunglasses.n.01'}, {'name': 'sunhat', 'id': 1036, 'frequency': 'c', 'synset': 'sunhat.n.01'}, {'name': 'surfboard', 'id': 1037, 'frequency': 'f', 'synset': 'surfboard.n.01'}, {'name': 'sushi', 'id': 1038, 'frequency': 'c', 'synset': 'sushi.n.01'}, {'name': 'mop', 'id': 1039, 'frequency': 'c', 'synset': 'swab.n.02'}, {'name': 'sweat_pants', 'id': 1040, 'frequency': 'c', 'synset': 'sweat_pants.n.01'}, {'name': 'sweatband', 'id': 1041, 'frequency': 'c', 'synset': 'sweatband.n.02'}, {'name': 'sweater', 'id': 1042, 'frequency': 'f', 'synset': 'sweater.n.01'}, {'name': 'sweatshirt', 'id': 1043, 'frequency': 'f', 'synset': 'sweatshirt.n.01'}, {'name': 'sweet_potato', 'id': 1044, 'frequency': 'c', 'synset': 'sweet_potato.n.02'}, {'name': 'swimsuit', 'id': 1045, 'frequency': 'f', 'synset': 'swimsuit.n.01'}, {'name': 'sword', 'id': 1046, 'frequency': 'c', 'synset': 'sword.n.01'}, {'name': 'syringe', 'id': 1047, 'frequency': 'r', 'synset': 'syringe.n.01'}, {'name': 'Tabasco_sauce', 'id': 1048, 'frequency': 'r', 'synset': 'tabasco.n.02'}, {'name': 'table-tennis_table', 'id': 1049, 'frequency': 'r', 'synset': 'table-tennis_table.n.01'}, {'name': 'table', 'id': 1050, 'frequency': 'f', 'synset': 'table.n.02'}, {'name': 'table_lamp', 'id': 1051, 'frequency': 'c', 'synset': 'table_lamp.n.01'}, {'name': 'tablecloth', 'id': 1052, 'frequency': 'f', 'synset': 'tablecloth.n.01'}, {'name': 'tachometer', 'id': 1053, 'frequency': 'r', 'synset': 'tachometer.n.01'}, {'name': 'taco', 'id': 1054, 'frequency': 'r', 'synset': 'taco.n.02'}, {'name': 'tag', 'id': 1055, 'frequency': 'f', 'synset': 'tag.n.02'}, {'name': 'taillight', 'id': 1056, 'frequency': 'f', 'synset': 'taillight.n.01'}, {'name': 'tambourine', 'id': 1057, 'frequency': 'r', 'synset': 'tambourine.n.01'}, {'name': 'army_tank', 'id': 1058, 'frequency': 'r', 'synset': 'tank.n.01'}, {'name': 'tank_(storage_vessel)', 'id': 1059, 'frequency': 'f', 'synset': 'tank.n.02'}, {'name': 'tank_top_(clothing)', 'id': 1060, 'frequency': 'f', 'synset': 'tank_top.n.01'}, {'name': 'tape_(sticky_cloth_or_paper)', 'id': 1061, 'frequency': 'f', 'synset': 'tape.n.01'}, {'name': 'tape_measure', 'id': 1062, 'frequency': 'c', 'synset': 'tape.n.04'}, {'name': 'tapestry', 'id': 1063, 'frequency': 'c', 'synset': 'tapestry.n.02'}, {'name': 'tarp', 'id': 1064, 'frequency': 'f', 'synset': 'tarpaulin.n.01'}, {'name': 'tartan', 'id': 1065, 'frequency': 'c', 'synset': 'tartan.n.01'}, {'name': 'tassel', 'id': 1066, 'frequency': 'c', 'synset': 'tassel.n.01'}, {'name': 'tea_bag', 'id': 1067, 'frequency': 'c', 'synset': 'tea_bag.n.01'}, {'name': 'teacup', 'id': 1068, 'frequency': 'c', 'synset': 'teacup.n.02'}, {'name': 'teakettle', 'id': 1069, 'frequency': 'c', 'synset': 'teakettle.n.01'}, {'name': 'teapot', 'id': 1070, 'frequency': 'f', 'synset': 'teapot.n.01'}, {'name': 'teddy_bear', 'id': 1071, 'frequency': 'f', 'synset': 'teddy.n.01'}, {'name': 'telephone', 'id': 1072, 'frequency': 'f', 'synset': 'telephone.n.01'}, {'name': 'telephone_booth', 'id': 1073, 'frequency': 'c', 'synset': 'telephone_booth.n.01'}, {'name': 'telephone_pole', 'id': 1074, 'frequency': 'f', 'synset': 'telephone_pole.n.01'}, {'name': 'telephoto_lens', 'id': 1075, 'frequency': 'r', 'synset': 'telephoto_lens.n.01'}, {'name': 'television_camera', 'id': 1076, 'frequency': 'c', 'synset': 'television_camera.n.01'}, {'name': 'television_set', 'id': 1077, 'frequency': 'f', 'synset': 'television_receiver.n.01'}, {'name': 'tennis_ball', 'id': 1078, 'frequency': 'f', 'synset': 'tennis_ball.n.01'}, {'name': 'tennis_racket', 'id': 1079, 'frequency': 'f', 'synset': 'tennis_racket.n.01'}, {'name': 'tequila', 'id': 1080, 'frequency': 'r', 'synset': 'tequila.n.01'}, {'name': 'thermometer', 'id': 1081, 'frequency': 'c', 'synset': 'thermometer.n.01'}, {'name': 'thermos_bottle', 'id': 1082, 'frequency': 'c', 'synset': 'thermos.n.01'}, {'name': 'thermostat', 'id': 1083, 'frequency': 'f', 'synset': 'thermostat.n.01'}, {'name': 'thimble', 'id': 1084, 'frequency': 'r', 'synset': 'thimble.n.02'}, {'name': 'thread', 'id': 1085, 'frequency': 'c', 'synset': 'thread.n.01'}, {'name': 'thumbtack', 'id': 1086, 'frequency': 'c', 'synset': 'thumbtack.n.01'}, {'name': 'tiara', 'id': 1087, 'frequency': 'c', 'synset': 'tiara.n.01'}, {'name': 'tiger', 'id': 1088, 'frequency': 'c', 'synset': 'tiger.n.02'}, {'name': 'tights_(clothing)', 'id': 1089, 'frequency': 'c', 'synset': 'tights.n.01'}, {'name': 'timer', 'id': 1090, 'frequency': 'c', 'synset': 'timer.n.01'}, {'name': 'tinfoil', 'id': 1091, 'frequency': 'f', 'synset': 'tinfoil.n.01'}, {'name': 'tinsel', 'id': 1092, 'frequency': 'c', 'synset': 'tinsel.n.01'}, {'name': 'tissue_paper', 'id': 1093, 'frequency': 'f', 'synset': 'tissue.n.02'}, {'name': 'toast_(food)', 'id': 1094, 'frequency': 'c', 'synset': 'toast.n.01'}, {'name': 'toaster', 'id': 1095, 'frequency': 'f', 'synset': 'toaster.n.02'}, {'name': 'toaster_oven', 'id': 1096, 'frequency': 'f', 'synset': 'toaster_oven.n.01'}, {'name': 'toilet', 'id': 1097, 'frequency': 'f', 'synset': 'toilet.n.02'}, {'name': 'toilet_tissue', 'id': 1098, 'frequency': 'f', 'synset': 'toilet_tissue.n.01'}, {'name': 'tomato', 'id': 1099, 'frequency': 'f', 'synset': 'tomato.n.01'}, {'name': 'tongs', 'id': 1100, 'frequency': 'f', 'synset': 'tongs.n.01'}, {'name': 'toolbox', 'id': 1101, 'frequency': 'c', 'synset': 'toolbox.n.01'}, {'name': 'toothbrush', 'id': 1102, 'frequency': 'f', 'synset': 'toothbrush.n.01'}, {'name': 'toothpaste', 'id': 1103, 'frequency': 'f', 'synset': 'toothpaste.n.01'}, {'name': 'toothpick', 'id': 1104, 'frequency': 'f', 'synset': 'toothpick.n.01'}, {'name': 'cover', 'id': 1105, 'frequency': 'f', 'synset': 'top.n.09'}, {'name': 'tortilla', 'id': 1106, 'frequency': 'c', 'synset': 'tortilla.n.01'}, {'name': 'tow_truck', 'id': 1107, 'frequency': 'c', 'synset': 'tow_truck.n.01'}, {'name': 'towel', 'id': 1108, 'frequency': 'f', 'synset': 'towel.n.01'}, {'name': 'towel_rack', 'id': 1109, 'frequency': 'f', 'synset': 'towel_rack.n.01'}, {'name': 'toy', 'id': 1110, 'frequency': 'f', 'synset': 'toy.n.03'}, {'name': 'tractor_(farm_equipment)', 'id': 1111, 'frequency': 'c', 'synset': 'tractor.n.01'}, {'name': 'traffic_light', 'id': 1112, 'frequency': 'f', 'synset': 'traffic_light.n.01'}, {'name': 'dirt_bike', 'id': 1113, 'frequency': 'c', 'synset': 'trail_bike.n.01'}, {'name': 'trailer_truck', 'id': 1114, 'frequency': 'f', 'synset': 'trailer_truck.n.01'}, {'name': 'train_(railroad_vehicle)', 'id': 1115, 'frequency': 'f', 'synset': 'train.n.01'}, {'name': 'trampoline', 'id': 1116, 'frequency': 'r', 'synset': 'trampoline.n.01'}, {'name': 'tray', 'id': 1117, 'frequency': 'f', 'synset': 'tray.n.01'}, {'name': 'trench_coat', 'id': 1118, 'frequency': 'r', 'synset': 'trench_coat.n.01'}, {'name': 'triangle_(musical_instrument)', 'id': 1119, 'frequency': 'r', 'synset': 'triangle.n.05'}, {'name': 'tricycle', 'id': 1120, 'frequency': 'c', 'synset': 'tricycle.n.01'}, {'name': 'tripod', 'id': 1121, 'frequency': 'f', 'synset': 'tripod.n.01'}, {'name': 'trousers', 'id': 1122, 'frequency': 'f', 'synset': 'trouser.n.01'}, {'name': 'truck', 'id': 1123, 'frequency': 'f', 'synset': 'truck.n.01'}, {'name': 'truffle_(chocolate)', 'id': 1124, 'frequency': 'r', 'synset': 'truffle.n.03'}, {'name': 'trunk', 'id': 1125, 'frequency': 'c', 'synset': 'trunk.n.02'}, {'name': 'vat', 'id': 1126, 'frequency': 'r', 'synset': 'tub.n.02'}, {'name': 'turban', 'id': 1127, 'frequency': 'c', 'synset': 'turban.n.01'}, {'name': 'turkey_(food)', 'id': 1128, 'frequency': 'c', 'synset': 'turkey.n.04'}, {'name': 'turnip', 'id': 1129, 'frequency': 'r', 'synset': 'turnip.n.01'}, {'name': 'turtle', 'id': 1130, 'frequency': 'c', 'synset': 'turtle.n.02'}, {'name': 'turtleneck_(clothing)', 'id': 1131, 'frequency': 'c', 'synset': 'turtleneck.n.01'}, {'name': 'typewriter', 'id': 1132, 'frequency': 'c', 'synset': 'typewriter.n.01'}, {'name': 'umbrella', 'id': 1133, 'frequency': 'f', 'synset': 'umbrella.n.01'}, {'name': 'underwear', 'id': 1134, 'frequency': 'f', 'synset': 'underwear.n.01'}, {'name': 'unicycle', 'id': 1135, 'frequency': 'r', 'synset': 'unicycle.n.01'}, {'name': 'urinal', 'id': 1136, 'frequency': 'f', 'synset': 'urinal.n.01'}, {'name': 'urn', 'id': 1137, 'frequency': 'c', 'synset': 'urn.n.01'}, {'name': 'vacuum_cleaner', 'id': 1138, 'frequency': 'c', 'synset': 'vacuum.n.04'}, {'name': 'vase', 'id': 1139, 'frequency': 'f', 'synset': 'vase.n.01'}, {'name': 'vending_machine', 'id': 1140, 'frequency': 'c', 'synset': 'vending_machine.n.01'}, {'name': 'vent', 'id': 1141, 'frequency': 'f', 'synset': 'vent.n.01'}, {'name': 'vest', 'id': 1142, 'frequency': 'f', 'synset': 'vest.n.01'}, {'name': 'videotape', 'id': 1143, 'frequency': 'c', 'synset': 'videotape.n.01'}, {'name': 'vinegar', 'id': 1144, 'frequency': 'r', 'synset': 'vinegar.n.01'}, {'name': 'violin', 'id': 1145, 'frequency': 'r', 'synset': 'violin.n.01'}, {'name': 'vodka', 'id': 1146, 'frequency': 'r', 'synset': 'vodka.n.01'}, {'name': 'volleyball', 'id': 1147, 'frequency': 'c', 'synset': 'volleyball.n.02'}, {'name': 'vulture', 'id': 1148, 'frequency': 'r', 'synset': 'vulture.n.01'}, {'name': 'waffle', 'id': 1149, 'frequency': 'c', 'synset': 'waffle.n.01'}, {'name': 'waffle_iron', 'id': 1150, 'frequency': 'r', 'synset': 'waffle_iron.n.01'}, {'name': 'wagon', 'id': 1151, 'frequency': 'c', 'synset': 'wagon.n.01'}, {'name': 'wagon_wheel', 'id': 1152, 'frequency': 'c', 'synset': 'wagon_wheel.n.01'}, {'name': 'walking_stick', 'id': 1153, 'frequency': 'c', 'synset': 'walking_stick.n.01'}, {'name': 'wall_clock', 'id': 1154, 'frequency': 'c', 'synset': 'wall_clock.n.01'}, {'name': 'wall_socket', 'id': 1155, 'frequency': 'f', 'synset': 'wall_socket.n.01'}, {'name': 'wallet', 'id': 1156, 'frequency': 'f', 'synset': 'wallet.n.01'}, {'name': 'walrus', 'id': 1157, 'frequency': 'r', 'synset': 'walrus.n.01'}, {'name': 'wardrobe', 'id': 1158, 'frequency': 'r', 'synset': 'wardrobe.n.01'}, {'name': 'washbasin', 'id': 1159, 'frequency': 'r', 'synset': 'washbasin.n.01'}, {'name': 'automatic_washer', 'id': 1160, 'frequency': 'c', 'synset': 'washer.n.03'}, {'name': 'watch', 'id': 1161, 'frequency': 'f', 'synset': 'watch.n.01'}, {'name': 'water_bottle', 'id': 1162, 'frequency': 'f', 'synset': 'water_bottle.n.01'}, {'name': 'water_cooler', 'id': 1163, 'frequency': 'c', 'synset': 'water_cooler.n.01'}, {'name': 'water_faucet', 'id': 1164, 'frequency': 'c', 'synset': 'water_faucet.n.01'}, {'name': 'water_heater', 'id': 1165, 'frequency': 'r', 'synset': 'water_heater.n.01'}, {'name': 'water_jug', 'id': 1166, 'frequency': 'c', 'synset': 'water_jug.n.01'}, {'name': 'water_gun', 'id': 1167, 'frequency': 'r', 'synset': 'water_pistol.n.01'}, {'name': 'water_scooter', 'id': 1168, 'frequency': 'c', 'synset': 'water_scooter.n.01'}, {'name': 'water_ski', 'id': 1169, 'frequency': 'c', 'synset': 'water_ski.n.01'}, {'name': 'water_tower', 'id': 1170, 'frequency': 'c', 'synset': 'water_tower.n.01'}, {'name': 'watering_can', 'id': 1171, 'frequency': 'c', 'synset': 'watering_can.n.01'}, {'name': 'watermelon', 'id': 1172, 'frequency': 'f', 'synset': 'watermelon.n.02'}, {'name': 'weathervane', 'id': 1173, 'frequency': 'f', 'synset': 'weathervane.n.01'}, {'name': 'webcam', 'id': 1174, 'frequency': 'c', 'synset': 'webcam.n.01'}, {'name': 'wedding_cake', 'id': 1175, 'frequency': 'c', 'synset': 'wedding_cake.n.01'}, {'name': 'wedding_ring', 'id': 1176, 'frequency': 'c', 'synset': 'wedding_ring.n.01'}, {'name': 'wet_suit', 'id': 1177, 'frequency': 'f', 'synset': 'wet_suit.n.01'}, {'name': 'wheel', 'id': 1178, 'frequency': 'f', 'synset': 'wheel.n.01'}, {'name': 'wheelchair', 'id': 1179, 'frequency': 'c', 'synset': 'wheelchair.n.01'}, {'name': 'whipped_cream', 'id': 1180, 'frequency': 'c', 'synset': 'whipped_cream.n.01'}, {'name': 'whistle', 'id': 1181, 'frequency': 'c', 'synset': 'whistle.n.03'}, {'name': 'wig', 'id': 1182, 'frequency': 'c', 'synset': 'wig.n.01'}, {'name': 'wind_chime', 'id': 1183, 'frequency': 'c', 'synset': 'wind_chime.n.01'}, {'name': 'windmill', 'id': 1184, 'frequency': 'c', 'synset': 'windmill.n.01'}, {'name': 'window_box_(for_plants)', 'id': 1185, 'frequency': 'c', 'synset': 'window_box.n.01'}, {'name': 'windshield_wiper', 'id': 1186, 'frequency': 'f', 'synset': 'windshield_wiper.n.01'}, {'name': 'windsock', 'id': 1187, 'frequency': 'c', 'synset': 'windsock.n.01'}, {'name': 'wine_bottle', 'id': 1188, 'frequency': 'f', 'synset': 'wine_bottle.n.01'}, {'name': 'wine_bucket', 'id': 1189, 'frequency': 'c', 'synset': 'wine_bucket.n.01'}, {'name': 'wineglass', 'id': 1190, 'frequency': 'f', 'synset': 'wineglass.n.01'}, {'name': 'blinder_(for_horses)', 'id': 1191, 'frequency': 'f', 'synset': 'winker.n.02'}, {'name': 'wok', 'id': 1192, 'frequency': 'c', 'synset': 'wok.n.01'}, {'name': 'wolf', 'id': 1193, 'frequency': 'r', 'synset': 'wolf.n.01'}, {'name': 'wooden_spoon', 'id': 1194, 'frequency': 'c', 'synset': 'wooden_spoon.n.02'}, {'name': 'wreath', 'id': 1195, 'frequency': 'c', 'synset': 'wreath.n.01'}, {'name': 'wrench', 'id': 1196, 'frequency': 'c', 'synset': 'wrench.n.03'}, {'name': 'wristband', 'id': 1197, 'frequency': 'f', 'synset': 'wristband.n.01'}, {'name': 'wristlet', 'id': 1198, 'frequency': 'f', 'synset': 'wristlet.n.01'}, {'name': 'yacht', 'id': 1199, 'frequency': 'c', 'synset': 'yacht.n.01'}, {'name': 'yogurt', 'id': 1200, 'frequency': 'c', 'synset': 'yogurt.n.01'}, {'name': 'yoke_(animal_equipment)', 'id': 1201, 'frequency': 'c', 'synset': 'yoke.n.07'}, {'name': 'zebra', 'id': 1202, 'frequency': 'f', 'synset': 'zebra.n.01'}, {'name': 'zucchini', 'id': 1203, 'frequency': 'c', 'synset': 'zucchini.n.02'}, {'id': 1204, 'synset': 'organism.n.01', 'name': 'organism'}, {'id': 1205, 'synset': 'benthos.n.02', 'name': 'benthos'}, {'id': 1206, 'synset': 'heterotroph.n.01', 'name': 'heterotroph'}, {'id': 1207, 'synset': 'cell.n.02', 'name': 'cell'}, {'id': 1208, 'synset': 'animal.n.01', 'name': 'animal'}, {'id': 1209, 'synset': 'plant.n.02', 'name': 'plant'}, {'id': 1210, 'synset': 'food.n.01', 'name': 'food'}, {'id': 1211, 'synset': 'artifact.n.01', 'name': 'artifact'}, {'id': 1212, 'synset': 'hop.n.01', 'name': 'hop'}, {'id': 1213, 'synset': 'check-in.n.01', 'name': 'check-in'}, {'id': 1214, 'synset': 'dressage.n.01', 'name': 'dressage'}, {'id': 1215, 'synset': 'curvet.n.01', 'name': 'curvet'}, {'id': 1216, 'synset': 'piaffe.n.01', 'name': 'piaffe'}, {'id': 1217, 'synset': 'funambulism.n.01', 'name': 'funambulism'}, {'id': 1218, 'synset': 'rock_climbing.n.01', 'name': 'rock_climbing'}, {'id': 1219, 'synset': 'contact_sport.n.01', 'name': 'contact_sport'}, {'id': 1220, 'synset': 'outdoor_sport.n.01', 'name': 'outdoor_sport'}, {'id': 1221, 'synset': 'gymnastics.n.01', 'name': 'gymnastics'}, {'id': 1222, 'synset': 'acrobatics.n.01', 'name': 'acrobatics'}, {'id': 1223, 'synset': 'track_and_field.n.01', 'name': 'track_and_field'}, {'id': 1224, 'synset': 'track.n.11', 'name': 'track'}, {'id': 1225, 'synset': 'jumping.n.01', 'name': 'jumping'}, {'id': 1226, 'synset': 'broad_jump.n.02', 'name': 'broad_jump'}, {'id': 1227, 'synset': 'high_jump.n.02', 'name': 'high_jump'}, {'id': 1228, 'synset': 'fosbury_flop.n.01', 'name': 'Fosbury_flop'}, {'id': 1229, 'synset': 'skiing.n.01', 'name': 'skiing'}, {'id': 1230, 'synset': 'cross-country_skiing.n.01', 'name': 'cross-country_skiing'}, {'id': 1231, 'synset': 'ski_jumping.n.01', 'name': 'ski_jumping'}, {'id': 1232, 'synset': 'water_sport.n.01', 'name': 'water_sport'}, {'id': 1233, 'synset': 'swimming.n.01', 'name': 'swimming'}, {'id': 1234, 'synset': 'bathe.n.01', 'name': 'bathe'}, {'id': 1235, 'synset': 'dip.n.08', 'name': 'dip'}, {'id': 1236, 'synset': 'dive.n.02', 'name': 'dive'}, {'id': 1237, 'synset': 'floating.n.01', 'name': 'floating'}, {'id': 1238, 'synset': "dead-man's_float.n.01", 'name': "dead-man's_float"}, {'id': 1239, 'synset': 'belly_flop.n.01', 'name': 'belly_flop'}, {'id': 1240, 'synset': 'cliff_diving.n.01', 'name': 'cliff_diving'}, {'id': 1241, 'synset': 'flip.n.05', 'name': 'flip'}, {'id': 1242, 'synset': 'gainer.n.03', 'name': 'gainer'}, {'id': 1243, 'synset': 'half_gainer.n.01', 'name': 'half_gainer'}, {'id': 1244, 'synset': 'jackknife.n.02', 'name': 'jackknife'}, {'id': 1245, 'synset': 'swan_dive.n.01', 'name': 'swan_dive'}, {'id': 1246, 'synset': 'skin_diving.n.01', 'name': 'skin_diving'}, {'id': 1247, 'synset': 'scuba_diving.n.01', 'name': 'scuba_diving'}, {'id': 1248, 'synset': 'snorkeling.n.01', 'name': 'snorkeling'}, {'id': 1249, 'synset': 'surfing.n.01', 'name': 'surfing'}, {'id': 1250, 'synset': 'water-skiing.n.01', 'name': 'water-skiing'}, {'id': 1251, 'synset': 'rowing.n.01', 'name': 'rowing'}, {'id': 1252, 'synset': 'sculling.n.01', 'name': 'sculling'}, {'id': 1253, 'synset': 'boxing.n.01', 'name': 'boxing'}, {'id': 1254, 'synset': 'professional_boxing.n.01', 'name': 'professional_boxing'}, {'id': 1255, 'synset': 'in-fighting.n.02', 'name': 'in-fighting'}, {'id': 1256, 'synset': 'fight.n.05', 'name': 'fight'}, {'id': 1257, 'synset': 'rope-a-dope.n.01', 'name': 'rope-a-dope'}, {'id': 1258, 'synset': 'spar.n.03', 'name': 'spar'}, {'id': 1259, 'synset': 'archery.n.01', 'name': 'archery'}, {'id': 1260, 'synset': 'sledding.n.01', 'name': 'sledding'}, {'id': 1261, 'synset': 'tobogganing.n.01', 'name': 'tobogganing'}, {'id': 1262, 'synset': 'luging.n.01', 'name': 'luging'}, {'id': 1263, 'synset': 'bobsledding.n.01', 'name': 'bobsledding'}, {'id': 1264, 'synset': 'wrestling.n.02', 'name': 'wrestling'}, {'id': 1265, 'synset': 'greco-roman_wrestling.n.01', 'name': 'Greco-Roman_wrestling'}, {'id': 1266, 'synset': 'professional_wrestling.n.01', 'name': 'professional_wrestling'}, {'id': 1267, 'synset': 'sumo.n.01', 'name': 'sumo'}, {'id': 1268, 'synset': 'skating.n.01', 'name': 'skating'}, {'id': 1269, 'synset': 'ice_skating.n.01', 'name': 'ice_skating'}, {'id': 1270, 'synset': 'figure_skating.n.01', 'name': 'figure_skating'}, {'id': 1271, 'synset': 'rollerblading.n.01', 'name': 'rollerblading'}, {'id': 1272, 'synset': 'roller_skating.n.01', 'name': 'roller_skating'}, {'id': 1273, 'synset': 'skateboarding.n.01', 'name': 'skateboarding'}, {'id': 1274, 'synset': 'speed_skating.n.01', 'name': 'speed_skating'}, {'id': 1275, 'synset': 'racing.n.01', 'name': 'racing'}, {'id': 1276, 'synset': 'auto_racing.n.01', 'name': 'auto_racing'}, {'id': 1277, 'synset': 'boat_racing.n.01', 'name': 'boat_racing'}, {'id': 1278, 'synset': 'hydroplane_racing.n.01', 'name': 'hydroplane_racing'}, {'id': 1279, 'synset': 'camel_racing.n.01', 'name': 'camel_racing'}, {'id': 1280, 'synset': 'greyhound_racing.n.01', 'name': 'greyhound_racing'}, {'id': 1281, 'synset': 'horse_racing.n.01', 'name': 'horse_racing'}, {'id': 1282, 'synset': 'riding.n.01', 'name': 'riding'}, {'id': 1283, 'synset': 'equestrian_sport.n.01', 'name': 'equestrian_sport'}, {'id': 1284, 'synset': 'pony-trekking.n.01', 'name': 'pony-trekking'}, {'id': 1285, 'synset': 'showjumping.n.01', 'name': 'showjumping'}, {'id': 1286, 'synset': 'cross-country_riding.n.01', 'name': 'cross-country_riding'}, {'id': 1287, 'synset': 'cycling.n.01', 'name': 'cycling'}, {'id': 1288, 'synset': 'bicycling.n.01', 'name': 'bicycling'}, {'id': 1289, 'synset': 'motorcycling.n.01', 'name': 'motorcycling'}, {'id': 1290, 'synset': 'dune_cycling.n.01', 'name': 'dune_cycling'}, {'id': 1291, 'synset': 'blood_sport.n.01', 'name': 'blood_sport'}, {'id': 1292, 'synset': 'bullfighting.n.01', 'name': 'bullfighting'}, {'id': 1293, 'synset': 'cockfighting.n.01', 'name': 'cockfighting'}, {'id': 1294, 'synset': 'hunt.n.08', 'name': 'hunt'}, {'id': 1295, 'synset': 'battue.n.01', 'name': 'battue'}, {'id': 1296, 'synset': 'beagling.n.01', 'name': 'beagling'}, {'id': 1297, 'synset': 'coursing.n.01', 'name': 'coursing'}, {'id': 1298, 'synset': 'deer_hunting.n.01', 'name': 'deer_hunting'}, {'id': 1299, 'synset': 'ducking.n.01', 'name': 'ducking'}, {'id': 1300, 'synset': 'fox_hunting.n.01', 'name': 'fox_hunting'}, {'id': 1301, 'synset': 'pigsticking.n.01', 'name': 'pigsticking'}, {'id': 1302, 'synset': 'fishing.n.01', 'name': 'fishing'}, {'id': 1303, 'synset': 'angling.n.01', 'name': 'angling'}, {'id': 1304, 'synset': 'fly-fishing.n.01', 'name': 'fly-fishing'}, {'id': 1305, 'synset': 'troll.n.04', 'name': 'troll'}, {'id': 1306, 'synset': 'casting.n.03', 'name': 'casting'}, {'id': 1307, 'synset': 'bait_casting.n.01', 'name': 'bait_casting'}, {'id': 1308, 'synset': 'fly_casting.n.01', 'name': 'fly_casting'}, {'id': 1309, 'synset': 'overcast.n.04', 'name': 'overcast'}, {'id': 1310, 'synset': 'surf_casting.n.01', 'name': 'surf_casting'}, {'id': 1311, 'synset': 'day_game.n.01', 'name': 'day_game'}, {'id': 1312, 'synset': 'athletic_game.n.01', 'name': 'athletic_game'}, {'id': 1313, 'synset': 'ice_hockey.n.01', 'name': 'ice_hockey'}, {'id': 1314, 'synset': 'tetherball.n.01', 'name': 'tetherball'}, {'id': 1315, 'synset': 'water_polo.n.01', 'name': 'water_polo'}, {'id': 1316, 'synset': 'outdoor_game.n.01', 'name': 'outdoor_game'}, {'id': 1317, 'synset': 'golf.n.01', 'name': 'golf'}, {'id': 1318, 'synset': 'professional_golf.n.01', 'name': 'professional_golf'}, {'id': 1319, 'synset': 'round_of_golf.n.01', 'name': 'round_of_golf'}, {'id': 1320, 'synset': 'medal_play.n.01', 'name': 'medal_play'}, {'id': 1321, 'synset': 'match_play.n.01', 'name': 'match_play'}, {'id': 1322, 'synset': 'miniature_golf.n.01', 'name': 'miniature_golf'}, {'id': 1323, 'synset': 'croquet.n.01', 'name': 'croquet'}, {'id': 1324, 'synset': 'quoits.n.01', 'name': 'quoits'}, {'id': 1325, 'synset': 'shuffleboard.n.01', 'name': 'shuffleboard'}, {'id': 1326, 'synset': 'field_game.n.01', 'name': 'field_game'}, {'id': 1327, 'synset': 'field_hockey.n.01', 'name': 'field_hockey'}, {'id': 1328, 'synset': 'shinny.n.01', 'name': 'shinny'}, {'id': 1329, 'synset': 'football.n.01', 'name': 'football'}, {'id': 1330, 'synset': 'american_football.n.01', 'name': 'American_football'}, {'id': 1331, 'synset': 'professional_football.n.01', 'name': 'professional_football'}, {'id': 1332, 'synset': 'touch_football.n.01', 'name': 'touch_football'}, {'id': 1333, 'synset': 'hurling.n.01', 'name': 'hurling'}, {'id': 1334, 'synset': 'rugby.n.01', 'name': 'rugby'}, {'id': 1335, 'synset': 'ball_game.n.01', 'name': 'ball_game'}, {'id': 1336, 'synset': 'baseball.n.01', 'name': 'baseball'}, {'id': 1337, 'synset': 'ball.n.11', 'name': 'ball'}, {'id': 1338, 'synset': 'professional_baseball.n.01', 'name': 'professional_baseball'}, {'id': 1339, 'synset': 'hardball.n.02', 'name': 'hardball'}, {'id': 1340, 'synset': 'perfect_game.n.01', 'name': 'perfect_game'}, {'id': 1341, 'synset': 'no-hit_game.n.01', 'name': 'no-hit_game'}, {'id': 1342, 'synset': 'one-hitter.n.01', 'name': 'one-hitter'}, {'id': 1343, 'synset': 'two-hitter.n.01', 'name': 'two-hitter'}, {'id': 1344, 'synset': 'three-hitter.n.01', 'name': 'three-hitter'}, {'id': 1345, 'synset': 'four-hitter.n.01', 'name': 'four-hitter'}, {'id': 1346, 'synset': 'five-hitter.n.01', 'name': 'five-hitter'}, {'id': 1347, 'synset': 'softball.n.02', 'name': 'softball'}, {'id': 1348, 'synset': 'rounders.n.01', 'name': 'rounders'}, {'id': 1349, 'synset': 'stickball.n.01', 'name': 'stickball'}, {'id': 1350, 'synset': 'cricket.n.02', 'name': 'cricket'}, {'id': 1351, 'synset': 'lacrosse.n.01', 'name': 'lacrosse'}, {'id': 1352, 'synset': 'polo.n.02', 'name': 'polo'}, {'id': 1353, 'synset': 'pushball.n.01', 'name': 'pushball'}, {'id': 1354, 'synset': 'soccer.n.01', 'name': 'soccer'}, {'id': 1355, 'synset': 'court_game.n.01', 'name': 'court_game'}, {'id': 1356, 'synset': 'handball.n.02', 'name': 'handball'}, {'id': 1357, 'synset': 'racquetball.n.02', 'name': 'racquetball'}, {'id': 1358, 'synset': 'fives.n.01', 'name': 'fives'}, {'id': 1359, 'synset': 'squash.n.03', 'name': 'squash'}, {'id': 1360, 'synset': 'volleyball.n.01', 'name': 'volleyball'}, {'id': 1361, 'synset': 'jai_alai.n.01', 'name': 'jai_alai'}, {'id': 1362, 'synset': 'badminton.n.01', 'name': 'badminton'}, {'id': 1363, 'synset': 'battledore.n.02', 'name': 'battledore'}, {'id': 1364, 'synset': 'basketball.n.01', 'name': 'basketball'}, {'id': 1365, 'synset': 'professional_basketball.n.01', 'name': 'professional_basketball'}, {'id': 1366, 'synset': 'deck_tennis.n.01', 'name': 'deck_tennis'}, {'id': 1367, 'synset': 'netball.n.01', 'name': 'netball'}, {'id': 1368, 'synset': 'tennis.n.01', 'name': 'tennis'}, {'id': 1369, 'synset': 'professional_tennis.n.01', 'name': 'professional_tennis'}, {'id': 1370, 'synset': 'singles.n.02', 'name': 'singles'}, {'id': 1371, 'synset': 'singles.n.01', 'name': 'singles'}, {'id': 1372, 'synset': 'doubles.n.02', 'name': 'doubles'}, {'id': 1373, 'synset': 'doubles.n.01', 'name': 'doubles'}, {'id': 1374, 'synset': 'royal_tennis.n.01', 'name': 'royal_tennis'}, {'id': 1375, 'synset': 'pallone.n.01', 'name': 'pallone'}, {'id': 1376, 'synset': 'sport.n.01', 'name': 'sport'}, {'id': 1377, 'synset': 'clasp.n.02', 'name': 'clasp'}, {'id': 1378, 'synset': 'judo.n.01', 'name': 'judo'}, {'id': 1379, 'synset': 'team_sport.n.01', 'name': 'team_sport'}, {'id': 1380, 'synset': 'last_supper.n.01', 'name': 'Last_Supper'}, {'id': 1381, 'synset': 'seder.n.01', 'name': 'Seder'}, {'id': 1382, 'synset': 'camping.n.01', 'name': 'camping'}, {'id': 1383, 'synset': 'pest.n.04', 'name': 'pest'}, {'id': 1384, 'synset': 'critter.n.01', 'name': 'critter'}, {'id': 1385, 'synset': 'creepy-crawly.n.01', 'name': 'creepy-crawly'}, {'id': 1386, 'synset': 'darter.n.02', 'name': 'darter'}, {'id': 1387, 'synset': 'peeper.n.03', 'name': 'peeper'}, {'id': 1388, 'synset': 'homeotherm.n.01', 'name': 'homeotherm'}, {'id': 1389, 'synset': 'poikilotherm.n.01', 'name': 'poikilotherm'}, {'id': 1390, 'synset': 'range_animal.n.01', 'name': 'range_animal'}, {'id': 1391, 'synset': 'scavenger.n.03', 'name': 'scavenger'}, {'id': 1392, 'synset': 'bottom-feeder.n.02', 'name': 'bottom-feeder'}, {'id': 1393, 'synset': 'bottom-feeder.n.01', 'name': 'bottom-feeder'}, {'id': 1394, 'synset': 'work_animal.n.01', 'name': 'work_animal'}, {'id': 1395, 'synset': 'beast_of_burden.n.01', 'name': 'beast_of_burden'}, {'id': 1396, 'synset': 'draft_animal.n.01', 'name': 'draft_animal'}, {'id': 1397, 'synset': 'pack_animal.n.01', 'name': 'pack_animal'}, {'id': 1398, 'synset': 'domestic_animal.n.01', 'name': 'domestic_animal'}, {'id': 1399, 'synset': 'feeder.n.01', 'name': 'feeder'}, {'id': 1400, 'synset': 'feeder.n.06', 'name': 'feeder'}, {'id': 1401, 'synset': 'stocker.n.01', 'name': 'stocker'}, {'id': 1402, 'synset': 'hatchling.n.01', 'name': 'hatchling'}, {'id': 1403, 'synset': 'head.n.02', 'name': 'head'}, {'id': 1404, 'synset': 'migrator.n.02', 'name': 'migrator'}, {'id': 1405, 'synset': 'molter.n.01', 'name': 'molter'}, {'id': 1406, 'synset': 'stayer.n.01', 'name': 'stayer'}, {'id': 1407, 'synset': 'stunt.n.02', 'name': 'stunt'}, {'id': 1408, 'synset': 'marine_animal.n.01', 'name': 'marine_animal'}, {'id': 1409, 'synset': 'by-catch.n.01', 'name': 'by-catch'}, {'id': 1410, 'synset': 'female.n.01', 'name': 'female'}, {'id': 1411, 'synset': 'hen.n.04', 'name': 'hen'}, {'id': 1412, 'synset': 'male.n.01', 'name': 'male'}, {'id': 1413, 'synset': 'adult.n.02', 'name': 'adult'}, {'id': 1414, 'synset': 'young.n.01', 'name': 'young'}, {'id': 1415, 'synset': 'orphan.n.04', 'name': 'orphan'}, {'id': 1416, 'synset': 'young_mammal.n.01', 'name': 'young_mammal'}, {'id': 1417, 'synset': 'baby.n.06', 'name': 'baby'}, {'id': 1418, 'synset': 'pup.n.01', 'name': 'pup'}, {'id': 1419, 'synset': 'wolf_pup.n.01', 'name': 'wolf_pup'}, {'id': 1420, 'synset': 'lion_cub.n.01', 'name': 'lion_cub'}, {'id': 1421, 'synset': 'bear_cub.n.01', 'name': 'bear_cub'}, {'id': 1422, 'synset': 'tiger_cub.n.01', 'name': 'tiger_cub'}, {'id': 1423, 'synset': 'kit.n.03', 'name': 'kit'}, {'id': 1424, 'synset': 'suckling.n.03', 'name': 'suckling'}, {'id': 1425, 'synset': 'sire.n.03', 'name': 'sire'}, {'id': 1426, 'synset': 'dam.n.03', 'name': 'dam'}, {'id': 1427, 'synset': 'thoroughbred.n.03', 'name': 'thoroughbred'}, {'id': 1428, 'synset': 'giant.n.01', 'name': 'giant'}, {'id': 1429, 'synset': 'mutant.n.02', 'name': 'mutant'}, {'id': 1430, 'synset': 'carnivore.n.02', 'name': 'carnivore'}, {'id': 1431, 'synset': 'herbivore.n.01', 'name': 'herbivore'}, {'id': 1432, 'synset': 'insectivore.n.02', 'name': 'insectivore'}, {'id': 1433, 'synset': 'acrodont.n.01', 'name': 'acrodont'}, {'id': 1434, 'synset': 'pleurodont.n.01', 'name': 'pleurodont'}, {'id': 1435, 'synset': 'microorganism.n.01', 'name': 'microorganism'}, {'id': 1436, 'synset': 'monohybrid.n.01', 'name': 'monohybrid'}, {'id': 1437, 'synset': 'arbovirus.n.01', 'name': 'arbovirus'}, {'id': 1438, 'synset': 'adenovirus.n.01', 'name': 'adenovirus'}, {'id': 1439, 'synset': 'arenavirus.n.01', 'name': 'arenavirus'}, {'id': 1440, 'synset': 'marburg_virus.n.01', 'name': 'Marburg_virus'}, {'id': 1441, 'synset': 'arenaviridae.n.01', 'name': 'Arenaviridae'}, {'id': 1442, 'synset': 'vesiculovirus.n.01', 'name': 'vesiculovirus'}, {'id': 1443, 'synset': 'reoviridae.n.01', 'name': 'Reoviridae'}, {'id': 1444, 'synset': 'variola_major.n.02', 'name': 'variola_major'}, {'id': 1445, 'synset': 'viroid.n.01', 'name': 'viroid'}, {'id': 1446, 'synset': 'coliphage.n.01', 'name': 'coliphage'}, {'id': 1447, 'synset': 'paramyxovirus.n.01', 'name': 'paramyxovirus'}, {'id': 1448, 'synset': 'poliovirus.n.01', 'name': 'poliovirus'}, {'id': 1449, 'synset': 'herpes.n.02', 'name': 'herpes'}, {'id': 1450, 'synset': 'herpes_simplex_1.n.01', 'name': 'herpes_simplex_1'}, {'id': 1451, 'synset': 'herpes_zoster.n.02', 'name': 'herpes_zoster'}, {'id': 1452, 'synset': 'herpes_varicella_zoster.n.01', 'name': 'herpes_varicella_zoster'}, {'id': 1453, 'synset': 'cytomegalovirus.n.01', 'name': 'cytomegalovirus'}, {'id': 1454, 'synset': 'varicella_zoster_virus.n.01', 'name': 'varicella_zoster_virus'}, {'id': 1455, 'synset': 'polyoma.n.01', 'name': 'polyoma'}, {'id': 1456, 'synset': 'lyssavirus.n.01', 'name': 'lyssavirus'}, {'id': 1457, 'synset': 'reovirus.n.01', 'name': 'reovirus'}, {'id': 1458, 'synset': 'rotavirus.n.01', 'name': 'rotavirus'}, {'id': 1459, 'synset': 'moneran.n.01', 'name': 'moneran'}, {'id': 1460, 'synset': 'archaebacteria.n.01', 'name': 'archaebacteria'}, {'id': 1461, 'synset': 'bacteroid.n.01', 'name': 'bacteroid'}, {'id': 1462, 'synset': 'bacillus_anthracis.n.01', 'name': 'Bacillus_anthracis'}, {'id': 1463, 'synset': 'yersinia_pestis.n.01', 'name': 'Yersinia_pestis'}, {'id': 1464, 'synset': 'brucella.n.01', 'name': 'Brucella'}, {'id': 1465, 'synset': 'spirillum.n.02', 'name': 'spirillum'}, {'id': 1466, 'synset': 'botulinus.n.01', 'name': 'botulinus'}, {'id': 1467, 'synset': 'clostridium_perfringens.n.01', 'name': 'clostridium_perfringens'}, {'id': 1468, 'synset': 'cyanobacteria.n.01', 'name': 'cyanobacteria'}, {'id': 1469, 'synset': 'trichodesmium.n.01', 'name': 'trichodesmium'}, {'id': 1470, 'synset': 'nitric_bacteria.n.01', 'name': 'nitric_bacteria'}, {'id': 1471, 'synset': 'spirillum.n.01', 'name': 'spirillum'}, {'id': 1472, 'synset': 'francisella.n.01', 'name': 'Francisella'}, {'id': 1473, 'synset': 'gonococcus.n.01', 'name': 'gonococcus'}, {'id': 1474, 'synset': 'corynebacterium_diphtheriae.n.01', 'name': 'Corynebacterium_diphtheriae'}, {'id': 1475, 'synset': 'enteric_bacteria.n.01', 'name': 'enteric_bacteria'}, {'id': 1476, 'synset': 'klebsiella.n.01', 'name': 'klebsiella'}, {'id': 1477, 'synset': 'salmonella_typhimurium.n.01', 'name': 'Salmonella_typhimurium'}, {'id': 1478, 'synset': 'typhoid_bacillus.n.01', 'name': 'typhoid_bacillus'}, {'id': 1479, 'synset': 'nitrate_bacterium.n.01', 'name': 'nitrate_bacterium'}, {'id': 1480, 'synset': 'nitrite_bacterium.n.01', 'name': 'nitrite_bacterium'}, {'id': 1481, 'synset': 'actinomycete.n.01', 'name': 'actinomycete'}, {'id': 1482, 'synset': 'streptomyces.n.01', 'name': 'streptomyces'}, {'id': 1483, 'synset': 'streptomyces_erythreus.n.01', 'name': 'Streptomyces_erythreus'}, {'id': 1484, 'synset': 'streptomyces_griseus.n.01', 'name': 'Streptomyces_griseus'}, {'id': 1485, 'synset': 'tubercle_bacillus.n.01', 'name': 'tubercle_bacillus'}, {'id': 1486, 'synset': 'pus-forming_bacteria.n.01', 'name': 'pus-forming_bacteria'}, {'id': 1487, 'synset': 'streptobacillus.n.01', 'name': 'streptobacillus'}, {'id': 1488, 'synset': 'myxobacteria.n.01', 'name': 'myxobacteria'}, {'id': 1489, 'synset': 'staphylococcus.n.01', 'name': 'staphylococcus'}, {'id': 1490, 'synset': 'diplococcus.n.01', 'name': 'diplococcus'}, {'id': 1491, 'synset': 'pneumococcus.n.01', 'name': 'pneumococcus'}, {'id': 1492, 'synset': 'streptococcus.n.01', 'name': 'streptococcus'}, {'id': 1493, 'synset': 'spirochete.n.01', 'name': 'spirochete'}, {'id': 1494, 'synset': 'planktonic_algae.n.01', 'name': 'planktonic_algae'}, {'id': 1495, 'synset': 'zooplankton.n.01', 'name': 'zooplankton'}, {'id': 1496, 'synset': 'parasite.n.01', 'name': 'parasite'}, {'id': 1497, 'synset': 'endoparasite.n.01', 'name': 'endoparasite'}, {'id': 1498, 'synset': 'ectoparasite.n.01', 'name': 'ectoparasite'}, {'id': 1499, 'synset': 'pathogen.n.01', 'name': 'pathogen'}, {'id': 1500, 'synset': 'commensal.n.01', 'name': 'commensal'}, {'id': 1501, 'synset': 'myrmecophile.n.01', 'name': 'myrmecophile'}, {'id': 1502, 'synset': 'protoctist.n.01', 'name': 'protoctist'}, {'id': 1503, 'synset': 'protozoan.n.01', 'name': 'protozoan'}, {'id': 1504, 'synset': 'sarcodinian.n.01', 'name': 'sarcodinian'}, {'id': 1505, 'synset': 'heliozoan.n.01', 'name': 'heliozoan'}, {'id': 1506, 'synset': 'endameba.n.01', 'name': 'endameba'}, {'id': 1507, 'synset': 'ameba.n.01', 'name': 'ameba'}, {'id': 1508, 'synset': 'globigerina.n.01', 'name': 'globigerina'}, {'id': 1509, 'synset': 'testacean.n.01', 'name': 'testacean'}, {'id': 1510, 'synset': 'arcella.n.01', 'name': 'arcella'}, {'id': 1511, 'synset': 'difflugia.n.01', 'name': 'difflugia'}, {'id': 1512, 'synset': 'ciliate.n.01', 'name': 'ciliate'}, {'id': 1513, 'synset': 'paramecium.n.01', 'name': 'paramecium'}, {'id': 1514, 'synset': 'stentor.n.03', 'name': 'stentor'}, {'id': 1515, 'synset': 'alga.n.01', 'name': 'alga'}, {'id': 1516, 'synset': 'arame.n.01', 'name': 'arame'}, {'id': 1517, 'synset': 'seagrass.n.01', 'name': 'seagrass'}, {'id': 1518, 'synset': 'golden_algae.n.01', 'name': 'golden_algae'}, {'id': 1519, 'synset': 'yellow-green_algae.n.01', 'name': 'yellow-green_algae'}, {'id': 1520, 'synset': 'brown_algae.n.01', 'name': 'brown_algae'}, {'id': 1521, 'synset': 'kelp.n.01', 'name': 'kelp'}, {'id': 1522, 'synset': 'fucoid.n.02', 'name': 'fucoid'}, {'id': 1523, 'synset': 'fucoid.n.01', 'name': 'fucoid'}, {'id': 1524, 'synset': 'fucus.n.01', 'name': 'fucus'}, {'id': 1525, 'synset': 'bladderwrack.n.01', 'name': 'bladderwrack'}, {'id': 1526, 'synset': 'green_algae.n.01', 'name': 'green_algae'}, {'id': 1527, 'synset': 'pond_scum.n.01', 'name': 'pond_scum'}, {'id': 1528, 'synset': 'chlorella.n.01', 'name': 'chlorella'}, {'id': 1529, 'synset': 'stonewort.n.01', 'name': 'stonewort'}, {'id': 1530, 'synset': 'desmid.n.01', 'name': 'desmid'}, {'id': 1531, 'synset': 'sea_moss.n.02', 'name': 'sea_moss'}, {'id': 1532, 'synset': 'eukaryote.n.01', 'name': 'eukaryote'}, {'id': 1533, 'synset': 'prokaryote.n.01', 'name': 'prokaryote'}, {'id': 1534, 'synset': 'zooid.n.01', 'name': 'zooid'}, {'id': 1535, 'synset': 'leishmania.n.01', 'name': 'Leishmania'}, {'id': 1536, 'synset': 'zoomastigote.n.01', 'name': 'zoomastigote'}, {'id': 1537, 'synset': 'polymastigote.n.01', 'name': 'polymastigote'}, {'id': 1538, 'synset': 'costia.n.01', 'name': 'costia'}, {'id': 1539, 'synset': 'giardia.n.01', 'name': 'giardia'}, {'id': 1540, 'synset': 'cryptomonad.n.01', 'name': 'cryptomonad'}, {'id': 1541, 'synset': 'sporozoan.n.01', 'name': 'sporozoan'}, {'id': 1542, 'synset': 'sporozoite.n.01', 'name': 'sporozoite'}, {'id': 1543, 'synset': 'trophozoite.n.01', 'name': 'trophozoite'}, {'id': 1544, 'synset': 'merozoite.n.01', 'name': 'merozoite'}, {'id': 1545, 'synset': 'coccidium.n.01', 'name': 'coccidium'}, {'id': 1546, 'synset': 'gregarine.n.01', 'name': 'gregarine'}, {'id': 1547, 'synset': 'plasmodium.n.02', 'name': 'plasmodium'}, {'id': 1548, 'synset': 'leucocytozoan.n.01', 'name': 'leucocytozoan'}, {'id': 1549, 'synset': 'microsporidian.n.01', 'name': 'microsporidian'}, {'id': 1550, 'synset': 'ostariophysi.n.01', 'name': 'Ostariophysi'}, {'id': 1551, 'synset': 'cypriniform_fish.n.01', 'name': 'cypriniform_fish'}, {'id': 1552, 'synset': 'loach.n.01', 'name': 'loach'}, {'id': 1553, 'synset': 'cyprinid.n.01', 'name': 'cyprinid'}, {'id': 1554, 'synset': 'carp.n.02', 'name': 'carp'}, {'id': 1555, 'synset': 'domestic_carp.n.01', 'name': 'domestic_carp'}, {'id': 1556, 'synset': 'leather_carp.n.01', 'name': 'leather_carp'}, {'id': 1557, 'synset': 'mirror_carp.n.01', 'name': 'mirror_carp'}, {'id': 1558, 'synset': 'european_bream.n.01', 'name': 'European_bream'}, {'id': 1559, 'synset': 'tench.n.01', 'name': 'tench'}, {'id': 1560, 'synset': 'dace.n.01', 'name': 'dace'}, {'id': 1561, 'synset': 'chub.n.01', 'name': 'chub'}, {'id': 1562, 'synset': 'shiner.n.04', 'name': 'shiner'}, {'id': 1563, 'synset': 'common_shiner.n.01', 'name': 'common_shiner'}, {'id': 1564, 'synset': 'roach.n.05', 'name': 'roach'}, {'id': 1565, 'synset': 'rudd.n.01', 'name': 'rudd'}, {'id': 1566, 'synset': 'minnow.n.01', 'name': 'minnow'}, {'id': 1567, 'synset': 'gudgeon.n.02', 'name': 'gudgeon'}, {'id': 1568, 'synset': 'crucian_carp.n.01', 'name': 'crucian_carp'}, {'id': 1569, 'synset': 'electric_eel.n.01', 'name': 'electric_eel'}, {'id': 1570, 'synset': 'catostomid.n.01', 'name': 'catostomid'}, {'id': 1571, 'synset': 'buffalo_fish.n.01', 'name': 'buffalo_fish'}, {'id': 1572, 'synset': 'black_buffalo.n.01', 'name': 'black_buffalo'}, {'id': 1573, 'synset': 'hog_sucker.n.01', 'name': 'hog_sucker'}, {'id': 1574, 'synset': 'redhorse.n.01', 'name': 'redhorse'}, {'id': 1575, 'synset': 'cyprinodont.n.01', 'name': 'cyprinodont'}, {'id': 1576, 'synset': 'killifish.n.01', 'name': 'killifish'}, {'id': 1577, 'synset': 'mummichog.n.01', 'name': 'mummichog'}, {'id': 1578, 'synset': 'striped_killifish.n.01', 'name': 'striped_killifish'}, {'id': 1579, 'synset': 'rivulus.n.01', 'name': 'rivulus'}, {'id': 1580, 'synset': 'flagfish.n.01', 'name': 'flagfish'}, {'id': 1581, 'synset': 'swordtail.n.01', 'name': 'swordtail'}, {'id': 1582, 'synset': 'guppy.n.01', 'name': 'guppy'}, {'id': 1583, 'synset': 'topminnow.n.01', 'name': 'topminnow'}, {'id': 1584, 'synset': 'mosquitofish.n.01', 'name': 'mosquitofish'}, {'id': 1585, 'synset': 'platy.n.01', 'name': 'platy'}, {'id': 1586, 'synset': 'mollie.n.01', 'name': 'mollie'}, {'id': 1587, 'synset': 'squirrelfish.n.02', 'name': 'squirrelfish'}, {'id': 1588, 'synset': 'reef_squirrelfish.n.01', 'name': 'reef_squirrelfish'}, {'id': 1589, 'synset': 'deepwater_squirrelfish.n.01', 'name': 'deepwater_squirrelfish'}, {'id': 1590, 'synset': 'holocentrus_ascensionis.n.01', 'name': 'Holocentrus_ascensionis'}, {'id': 1591, 'synset': 'soldierfish.n.01', 'name': 'soldierfish'}, {'id': 1592, 'synset': 'anomalops.n.01', 'name': 'anomalops'}, {'id': 1593, 'synset': 'flashlight_fish.n.01', 'name': 'flashlight_fish'}, {'id': 1594, 'synset': 'john_dory.n.01', 'name': 'John_Dory'}, {'id': 1595, 'synset': 'boarfish.n.02', 'name': 'boarfish'}, {'id': 1596, 'synset': 'boarfish.n.01', 'name': 'boarfish'}, {'id': 1597, 'synset': 'cornetfish.n.01', 'name': 'cornetfish'}, {'id': 1598, 'synset': 'stickleback.n.01', 'name': 'stickleback'}, {'id': 1599, 'synset': 'three-spined_stickleback.n.01', 'name': 'three-spined_stickleback'}, {'id': 1600, 'synset': 'ten-spined_stickleback.n.01', 'name': 'ten-spined_stickleback'}, {'id': 1601, 'synset': 'pipefish.n.01', 'name': 'pipefish'}, {'id': 1602, 'synset': 'dwarf_pipefish.n.01', 'name': 'dwarf_pipefish'}, {'id': 1603, 'synset': 'deepwater_pipefish.n.01', 'name': 'deepwater_pipefish'}, {'id': 1604, 'synset': 'snipefish.n.01', 'name': 'snipefish'}, {'id': 1605, 'synset': 'shrimpfish.n.01', 'name': 'shrimpfish'}, {'id': 1606, 'synset': 'trumpetfish.n.01', 'name': 'trumpetfish'}, {'id': 1607, 'synset': 'pellicle.n.01', 'name': 'pellicle'}, {'id': 1608, 'synset': 'embryo.n.02', 'name': 'embryo'}, {'id': 1609, 'synset': 'fetus.n.01', 'name': 'fetus'}, {'id': 1610, 'synset': 'abortus.n.01', 'name': 'abortus'}, {'id': 1611, 'synset': 'spawn.n.01', 'name': 'spawn'}, {'id': 1612, 'synset': 'blastula.n.01', 'name': 'blastula'}, {'id': 1613, 'synset': 'blastocyst.n.01', 'name': 'blastocyst'}, {'id': 1614, 'synset': 'gastrula.n.01', 'name': 'gastrula'}, {'id': 1615, 'synset': 'morula.n.01', 'name': 'morula'}, {'id': 1616, 'synset': 'yolk.n.02', 'name': 'yolk'}, {'id': 1617, 'synset': 'chordate.n.01', 'name': 'chordate'}, {'id': 1618, 'synset': 'cephalochordate.n.01', 'name': 'cephalochordate'}, {'id': 1619, 'synset': 'lancelet.n.01', 'name': 'lancelet'}, {'id': 1620, 'synset': 'tunicate.n.01', 'name': 'tunicate'}, {'id': 1621, 'synset': 'ascidian.n.01', 'name': 'ascidian'}, {'id': 1622, 'synset': 'sea_squirt.n.01', 'name': 'sea_squirt'}, {'id': 1623, 'synset': 'salp.n.01', 'name': 'salp'}, {'id': 1624, 'synset': 'doliolum.n.01', 'name': 'doliolum'}, {'id': 1625, 'synset': 'larvacean.n.01', 'name': 'larvacean'}, {'id': 1626, 'synset': 'appendicularia.n.01', 'name': 'appendicularia'}, {'id': 1627, 'synset': 'ascidian_tadpole.n.01', 'name': 'ascidian_tadpole'}, {'id': 1628, 'synset': 'vertebrate.n.01', 'name': 'vertebrate'}, {'id': 1629, 'synset': 'amniota.n.01', 'name': 'Amniota'}, {'id': 1630, 'synset': 'amniote.n.01', 'name': 'amniote'}, {'id': 1631, 'synset': 'aquatic_vertebrate.n.01', 'name': 'aquatic_vertebrate'}, {'id': 1632, 'synset': 'jawless_vertebrate.n.01', 'name': 'jawless_vertebrate'}, {'id': 1633, 'synset': 'ostracoderm.n.01', 'name': 'ostracoderm'}, {'id': 1634, 'synset': 'heterostracan.n.01', 'name': 'heterostracan'}, {'id': 1635, 'synset': 'anaspid.n.01', 'name': 'anaspid'}, {'id': 1636, 'synset': 'conodont.n.02', 'name': 'conodont'}, {'id': 1637, 'synset': 'cyclostome.n.01', 'name': 'cyclostome'}, {'id': 1638, 'synset': 'lamprey.n.01', 'name': 'lamprey'}, {'id': 1639, 'synset': 'sea_lamprey.n.01', 'name': 'sea_lamprey'}, {'id': 1640, 'synset': 'hagfish.n.01', 'name': 'hagfish'}, {'id': 1641, 'synset': 'myxine_glutinosa.n.01', 'name': 'Myxine_glutinosa'}, {'id': 1642, 'synset': 'eptatretus.n.01', 'name': 'eptatretus'}, {'id': 1643, 'synset': 'gnathostome.n.01', 'name': 'gnathostome'}, {'id': 1644, 'synset': 'placoderm.n.01', 'name': 'placoderm'}, {'id': 1645, 'synset': 'cartilaginous_fish.n.01', 'name': 'cartilaginous_fish'}, {'id': 1646, 'synset': 'holocephalan.n.01', 'name': 'holocephalan'}, {'id': 1647, 'synset': 'chimaera.n.03', 'name': 'chimaera'}, {'id': 1648, 'synset': 'rabbitfish.n.01', 'name': 'rabbitfish'}, {'id': 1649, 'synset': 'elasmobranch.n.01', 'name': 'elasmobranch'}, {'id': 1650, 'synset': 'cow_shark.n.01', 'name': 'cow_shark'}, {'id': 1651, 'synset': 'mackerel_shark.n.01', 'name': 'mackerel_shark'}, {'id': 1652, 'synset': 'porbeagle.n.01', 'name': 'porbeagle'}, {'id': 1653, 'synset': 'mako.n.01', 'name': 'mako'}, {'id': 1654, 'synset': 'shortfin_mako.n.01', 'name': 'shortfin_mako'}, {'id': 1655, 'synset': 'longfin_mako.n.01', 'name': 'longfin_mako'}, {'id': 1656, 'synset': 'bonito_shark.n.01', 'name': 'bonito_shark'}, {'id': 1657, 'synset': 'great_white_shark.n.01', 'name': 'great_white_shark'}, {'id': 1658, 'synset': 'basking_shark.n.01', 'name': 'basking_shark'}, {'id': 1659, 'synset': 'thresher.n.02', 'name': 'thresher'}, {'id': 1660, 'synset': 'carpet_shark.n.01', 'name': 'carpet_shark'}, {'id': 1661, 'synset': 'nurse_shark.n.01', 'name': 'nurse_shark'}, {'id': 1662, 'synset': 'sand_tiger.n.01', 'name': 'sand_tiger'}, {'id': 1663, 'synset': 'whale_shark.n.01', 'name': 'whale_shark'}, {'id': 1664, 'synset': 'requiem_shark.n.01', 'name': 'requiem_shark'}, {'id': 1665, 'synset': 'bull_shark.n.01', 'name': 'bull_shark'}, {'id': 1666, 'synset': 'sandbar_shark.n.02', 'name': 'sandbar_shark'}, {'id': 1667, 'synset': 'blacktip_shark.n.01', 'name': 'blacktip_shark'}, {'id': 1668, 'synset': 'whitetip_shark.n.02', 'name': 'whitetip_shark'}, {'id': 1669, 'synset': 'dusky_shark.n.01', 'name': 'dusky_shark'}, {'id': 1670, 'synset': 'lemon_shark.n.01', 'name': 'lemon_shark'}, {'id': 1671, 'synset': 'blue_shark.n.01', 'name': 'blue_shark'}, {'id': 1672, 'synset': 'tiger_shark.n.01', 'name': 'tiger_shark'}, {'id': 1673, 'synset': 'soupfin_shark.n.01', 'name': 'soupfin_shark'}, {'id': 1674, 'synset': 'dogfish.n.02', 'name': 'dogfish'}, {'id': 1675, 'synset': 'smooth_dogfish.n.01', 'name': 'smooth_dogfish'}, {'id': 1676, 'synset': 'smoothhound.n.01', 'name': 'smoothhound'}, {'id': 1677, 'synset': 'american_smooth_dogfish.n.01', 'name': 'American_smooth_dogfish'}, {'id': 1678, 'synset': 'florida_smoothhound.n.01', 'name': 'Florida_smoothhound'}, {'id': 1679, 'synset': 'whitetip_shark.n.01', 'name': 'whitetip_shark'}, {'id': 1680, 'synset': 'spiny_dogfish.n.01', 'name': 'spiny_dogfish'}, {'id': 1681, 'synset': 'atlantic_spiny_dogfish.n.01', 'name': 'Atlantic_spiny_dogfish'}, {'id': 1682, 'synset': 'pacific_spiny_dogfish.n.01', 'name': 'Pacific_spiny_dogfish'}, {'id': 1683, 'synset': 'hammerhead.n.03', 'name': 'hammerhead'}, {'id': 1684, 'synset': 'smooth_hammerhead.n.01', 'name': 'smooth_hammerhead'}, {'id': 1685, 'synset': 'smalleye_hammerhead.n.01', 'name': 'smalleye_hammerhead'}, {'id': 1686, 'synset': 'shovelhead.n.01', 'name': 'shovelhead'}, {'id': 1687, 'synset': 'angel_shark.n.01', 'name': 'angel_shark'}, {'id': 1688, 'synset': 'ray.n.07', 'name': 'ray'}, {'id': 1689, 'synset': 'electric_ray.n.01', 'name': 'electric_ray'}, {'id': 1690, 'synset': 'sawfish.n.01', 'name': 'sawfish'}, {'id': 1691, 'synset': 'smalltooth_sawfish.n.01', 'name': 'smalltooth_sawfish'}, {'id': 1692, 'synset': 'guitarfish.n.01', 'name': 'guitarfish'}, {'id': 1693, 'synset': 'stingray.n.01', 'name': 'stingray'}, {'id': 1694, 'synset': 'roughtail_stingray.n.01', 'name': 'roughtail_stingray'}, {'id': 1695, 'synset': 'butterfly_ray.n.01', 'name': 'butterfly_ray'}, {'id': 1696, 'synset': 'eagle_ray.n.01', 'name': 'eagle_ray'}, {'id': 1697, 'synset': 'spotted_eagle_ray.n.01', 'name': 'spotted_eagle_ray'}, {'id': 1698, 'synset': 'cownose_ray.n.01', 'name': 'cownose_ray'}, {'id': 1699, 'synset': 'manta.n.02', 'name': 'manta'}, {'id': 1700, 'synset': 'atlantic_manta.n.01', 'name': 'Atlantic_manta'}, {'id': 1701, 'synset': 'devil_ray.n.01', 'name': 'devil_ray'}, {'id': 1702, 'synset': 'skate.n.02', 'name': 'skate'}, {'id': 1703, 'synset': 'grey_skate.n.01', 'name': 'grey_skate'}, {'id': 1704, 'synset': 'little_skate.n.01', 'name': 'little_skate'}, {'id': 1705, 'synset': 'thorny_skate.n.01', 'name': 'thorny_skate'}, {'id': 1706, 'synset': 'barndoor_skate.n.01', 'name': 'barndoor_skate'}, {'id': 1707, 'synset': 'dickeybird.n.01', 'name': 'dickeybird'}, {'id': 1708, 'synset': 'fledgling.n.02', 'name': 'fledgling'}, {'id': 1709, 'synset': 'nestling.n.01', 'name': 'nestling'}, {'id': 1710, 'synset': 'cock.n.05', 'name': 'cock'}, {'id': 1711, 'synset': 'gamecock.n.01', 'name': 'gamecock'}, {'id': 1712, 'synset': 'hen.n.02', 'name': 'hen'}, {'id': 1713, 'synset': 'nester.n.02', 'name': 'nester'}, {'id': 1714, 'synset': 'night_bird.n.01', 'name': 'night_bird'}, {'id': 1715, 'synset': 'night_raven.n.02', 'name': 'night_raven'}, {'id': 1716, 'synset': 'bird_of_passage.n.02', 'name': 'bird_of_passage'}, {'id': 1717, 'synset': 'archaeopteryx.n.01', 'name': 'archaeopteryx'}, {'id': 1718, 'synset': 'archaeornis.n.01', 'name': 'archaeornis'}, {'id': 1719, 'synset': 'ratite.n.01', 'name': 'ratite'}, {'id': 1720, 'synset': 'carinate.n.01', 'name': 'carinate'}, {'id': 1721, 'synset': 'cassowary.n.01', 'name': 'cassowary'}, {'id': 1722, 'synset': 'emu.n.02', 'name': 'emu'}, {'id': 1723, 'synset': 'kiwi.n.04', 'name': 'kiwi'}, {'id': 1724, 'synset': 'rhea.n.03', 'name': 'rhea'}, {'id': 1725, 'synset': 'rhea.n.02', 'name': 'rhea'}, {'id': 1726, 'synset': 'elephant_bird.n.01', 'name': 'elephant_bird'}, {'id': 1727, 'synset': 'moa.n.01', 'name': 'moa'}, {'id': 1728, 'synset': 'passerine.n.01', 'name': 'passerine'}, {'id': 1729, 'synset': 'nonpasserine_bird.n.01', 'name': 'nonpasserine_bird'}, {'id': 1730, 'synset': 'oscine.n.01', 'name': 'oscine'}, {'id': 1731, 'synset': 'songbird.n.01', 'name': 'songbird'}, {'id': 1732, 'synset': 'honey_eater.n.01', 'name': 'honey_eater'}, {'id': 1733, 'synset': 'accentor.n.01', 'name': 'accentor'}, {'id': 1734, 'synset': 'hedge_sparrow.n.01', 'name': 'hedge_sparrow'}, {'id': 1735, 'synset': 'lark.n.03', 'name': 'lark'}, {'id': 1736, 'synset': 'skylark.n.01', 'name': 'skylark'}, {'id': 1737, 'synset': 'wagtail.n.01', 'name': 'wagtail'}, {'id': 1738, 'synset': 'pipit.n.01', 'name': 'pipit'}, {'id': 1739, 'synset': 'meadow_pipit.n.01', 'name': 'meadow_pipit'}, {'id': 1740, 'synset': 'finch.n.01', 'name': 'finch'}, {'id': 1741, 'synset': 'chaffinch.n.01', 'name': 'chaffinch'}, {'id': 1742, 'synset': 'brambling.n.01', 'name': 'brambling'}, {'id': 1743, 'synset': 'goldfinch.n.02', 'name': 'goldfinch'}, {'id': 1744, 'synset': 'linnet.n.02', 'name': 'linnet'}, {'id': 1745, 'synset': 'siskin.n.01', 'name': 'siskin'}, {'id': 1746, 'synset': 'red_siskin.n.01', 'name': 'red_siskin'}, {'id': 1747, 'synset': 'redpoll.n.02', 'name': 'redpoll'}, {'id': 1748, 'synset': 'redpoll.n.01', 'name': 'redpoll'}, {'id': 1749, 'synset': 'new_world_goldfinch.n.01', 'name': 'New_World_goldfinch'}, {'id': 1750, 'synset': 'pine_siskin.n.01', 'name': 'pine_siskin'}, {'id': 1751, 'synset': 'house_finch.n.01', 'name': 'house_finch'}, {'id': 1752, 'synset': 'purple_finch.n.01', 'name': 'purple_finch'}, {'id': 1753, 'synset': 'canary.n.04', 'name': 'canary'}, {'id': 1754, 'synset': 'common_canary.n.01', 'name': 'common_canary'}, {'id': 1755, 'synset': 'serin.n.01', 'name': 'serin'}, {'id': 1756, 'synset': 'crossbill.n.01', 'name': 'crossbill'}, {'id': 1757, 'synset': 'bullfinch.n.02', 'name': 'bullfinch'}, {'id': 1758, 'synset': 'junco.n.01', 'name': 'junco'}, {'id': 1759, 'synset': 'dark-eyed_junco.n.01', 'name': 'dark-eyed_junco'}, {'id': 1760, 'synset': 'new_world_sparrow.n.01', 'name': 'New_World_sparrow'}, {'id': 1761, 'synset': 'vesper_sparrow.n.01', 'name': 'vesper_sparrow'}, {'id': 1762, 'synset': 'white-throated_sparrow.n.01', 'name': 'white-throated_sparrow'}, {'id': 1763, 'synset': 'white-crowned_sparrow.n.01', 'name': 'white-crowned_sparrow'}, {'id': 1764, 'synset': 'chipping_sparrow.n.01', 'name': 'chipping_sparrow'}, {'id': 1765, 'synset': 'field_sparrow.n.01', 'name': 'field_sparrow'}, {'id': 1766, 'synset': 'tree_sparrow.n.02', 'name': 'tree_sparrow'}, {'id': 1767, 'synset': 'song_sparrow.n.01', 'name': 'song_sparrow'}, {'id': 1768, 'synset': 'swamp_sparrow.n.01', 'name': 'swamp_sparrow'}, {'id': 1769, 'synset': 'bunting.n.02', 'name': 'bunting'}, {'id': 1770, 'synset': 'indigo_bunting.n.01', 'name': 'indigo_bunting'}, {'id': 1771, 'synset': 'ortolan.n.01', 'name': 'ortolan'}, {'id': 1772, 'synset': 'reed_bunting.n.01', 'name': 'reed_bunting'}, {'id': 1773, 'synset': 'yellowhammer.n.02', 'name': 'yellowhammer'}, {'id': 1774, 'synset': 'yellow-breasted_bunting.n.01', 'name': 'yellow-breasted_bunting'}, {'id': 1775, 'synset': 'snow_bunting.n.01', 'name': 'snow_bunting'}, {'id': 1776, 'synset': 'honeycreeper.n.02', 'name': 'honeycreeper'}, {'id': 1777, 'synset': 'banana_quit.n.01', 'name': 'banana_quit'}, {'id': 1778, 'synset': 'sparrow.n.01', 'name': 'sparrow'}, {'id': 1779, 'synset': 'english_sparrow.n.01', 'name': 'English_sparrow'}, {'id': 1780, 'synset': 'tree_sparrow.n.01', 'name': 'tree_sparrow'}, {'id': 1781, 'synset': 'grosbeak.n.01', 'name': 'grosbeak'}, {'id': 1782, 'synset': 'evening_grosbeak.n.01', 'name': 'evening_grosbeak'}, {'id': 1783, 'synset': 'hawfinch.n.01', 'name': 'hawfinch'}, {'id': 1784, 'synset': 'pine_grosbeak.n.01', 'name': 'pine_grosbeak'}, {'id': 1785, 'synset': 'cardinal.n.04', 'name': 'cardinal'}, {'id': 1786, 'synset': 'pyrrhuloxia.n.01', 'name': 'pyrrhuloxia'}, {'id': 1787, 'synset': 'towhee.n.01', 'name': 'towhee'}, {'id': 1788, 'synset': 'chewink.n.01', 'name': 'chewink'}, {'id': 1789, 'synset': 'green-tailed_towhee.n.01', 'name': 'green-tailed_towhee'}, {'id': 1790, 'synset': 'weaver.n.02', 'name': 'weaver'}, {'id': 1791, 'synset': 'baya.n.01', 'name': 'baya'}, {'id': 1792, 'synset': 'whydah.n.01', 'name': 'whydah'}, {'id': 1793, 'synset': 'java_sparrow.n.01', 'name': 'Java_sparrow'}, {'id': 1794, 'synset': 'avadavat.n.01', 'name': 'avadavat'}, {'id': 1795, 'synset': 'grassfinch.n.01', 'name': 'grassfinch'}, {'id': 1796, 'synset': 'zebra_finch.n.01', 'name': 'zebra_finch'}, {'id': 1797, 'synset': 'honeycreeper.n.01', 'name': 'honeycreeper'}, {'id': 1798, 'synset': 'lyrebird.n.01', 'name': 'lyrebird'}, {'id': 1799, 'synset': 'scrubbird.n.01', 'name': 'scrubbird'}, {'id': 1800, 'synset': 'broadbill.n.04', 'name': 'broadbill'}, {'id': 1801, 'synset': 'tyrannid.n.01', 'name': 'tyrannid'}, {'id': 1802, 'synset': 'new_world_flycatcher.n.01', 'name': 'New_World_flycatcher'}, {'id': 1803, 'synset': 'kingbird.n.01', 'name': 'kingbird'}, {'id': 1804, 'synset': 'arkansas_kingbird.n.01', 'name': 'Arkansas_kingbird'}, {'id': 1805, 'synset': "cassin's_kingbird.n.01", 'name': "Cassin's_kingbird"}, {'id': 1806, 'synset': 'eastern_kingbird.n.01', 'name': 'eastern_kingbird'}, {'id': 1807, 'synset': 'grey_kingbird.n.01', 'name': 'grey_kingbird'}, {'id': 1808, 'synset': 'pewee.n.01', 'name': 'pewee'}, {'id': 1809, 'synset': 'western_wood_pewee.n.01', 'name': 'western_wood_pewee'}, {'id': 1810, 'synset': 'phoebe.n.03', 'name': 'phoebe'}, {'id': 1811, 'synset': 'vermillion_flycatcher.n.01', 'name': 'vermillion_flycatcher'}, {'id': 1812, 'synset': 'cotinga.n.01', 'name': 'cotinga'}, {'id': 1813, 'synset': 'cock_of_the_rock.n.02', 'name': 'cock_of_the_rock'}, {'id': 1814, 'synset': 'cock_of_the_rock.n.01', 'name': 'cock_of_the_rock'}, {'id': 1815, 'synset': 'manakin.n.03', 'name': 'manakin'}, {'id': 1816, 'synset': 'bellbird.n.01', 'name': 'bellbird'}, {'id': 1817, 'synset': 'umbrella_bird.n.01', 'name': 'umbrella_bird'}, {'id': 1818, 'synset': 'ovenbird.n.02', 'name': 'ovenbird'}, {'id': 1819, 'synset': 'antbird.n.01', 'name': 'antbird'}, {'id': 1820, 'synset': 'ant_thrush.n.01', 'name': 'ant_thrush'}, {'id': 1821, 'synset': 'ant_shrike.n.01', 'name': 'ant_shrike'}, {'id': 1822, 'synset': 'spotted_antbird.n.01', 'name': 'spotted_antbird'}, {'id': 1823, 'synset': 'woodhewer.n.01', 'name': 'woodhewer'}, {'id': 1824, 'synset': 'pitta.n.01', 'name': 'pitta'}, {'id': 1825, 'synset': 'scissortail.n.01', 'name': 'scissortail'}, {'id': 1826, 'synset': 'old_world_flycatcher.n.01', 'name': 'Old_World_flycatcher'}, {'id': 1827, 'synset': 'spotted_flycatcher.n.01', 'name': 'spotted_flycatcher'}, {'id': 1828, 'synset': 'thickhead.n.01', 'name': 'thickhead'}, {'id': 1829, 'synset': 'thrush.n.03', 'name': 'thrush'}, {'id': 1830, 'synset': 'missel_thrush.n.01', 'name': 'missel_thrush'}, {'id': 1831, 'synset': 'song_thrush.n.01', 'name': 'song_thrush'}, {'id': 1832, 'synset': 'fieldfare.n.01', 'name': 'fieldfare'}, {'id': 1833, 'synset': 'redwing.n.02', 'name': 'redwing'}, {'id': 1834, 'synset': 'blackbird.n.02', 'name': 'blackbird'}, {'id': 1835, 'synset': 'ring_ouzel.n.01', 'name': 'ring_ouzel'}, {'id': 1836, 'synset': 'robin.n.02', 'name': 'robin'}, {'id': 1837, 'synset': 'clay-colored_robin.n.01', 'name': 'clay-colored_robin'}, {'id': 1838, 'synset': 'hermit_thrush.n.01', 'name': 'hermit_thrush'}, {'id': 1839, 'synset': 'veery.n.01', 'name': 'veery'}, {'id': 1840, 'synset': 'wood_thrush.n.01', 'name': 'wood_thrush'}, {'id': 1841, 'synset': 'nightingale.n.01', 'name': 'nightingale'}, {'id': 1842, 'synset': 'thrush_nightingale.n.01', 'name': 'thrush_nightingale'}, {'id': 1843, 'synset': 'bulbul.n.01', 'name': 'bulbul'}, {'id': 1844, 'synset': 'old_world_chat.n.01', 'name': 'Old_World_chat'}, {'id': 1845, 'synset': 'stonechat.n.01', 'name': 'stonechat'}, {'id': 1846, 'synset': 'whinchat.n.01', 'name': 'whinchat'}, {'id': 1847, 'synset': 'solitaire.n.03', 'name': 'solitaire'}, {'id': 1848, 'synset': 'redstart.n.02', 'name': 'redstart'}, {'id': 1849, 'synset': 'wheatear.n.01', 'name': 'wheatear'}, {'id': 1850, 'synset': 'bluebird.n.02', 'name': 'bluebird'}, {'id': 1851, 'synset': 'robin.n.01', 'name': 'robin'}, {'id': 1852, 'synset': 'bluethroat.n.01', 'name': 'bluethroat'}, {'id': 1853, 'synset': 'warbler.n.02', 'name': 'warbler'}, {'id': 1854, 'synset': 'gnatcatcher.n.01', 'name': 'gnatcatcher'}, {'id': 1855, 'synset': 'kinglet.n.01', 'name': 'kinglet'}, {'id': 1856, 'synset': 'goldcrest.n.01', 'name': 'goldcrest'}, {'id': 1857, 'synset': 'gold-crowned_kinglet.n.01', 'name': 'gold-crowned_kinglet'}, {'id': 1858, 'synset': 'ruby-crowned_kinglet.n.01', 'name': 'ruby-crowned_kinglet'}, {'id': 1859, 'synset': 'old_world_warbler.n.01', 'name': 'Old_World_warbler'}, {'id': 1860, 'synset': 'blackcap.n.04', 'name': 'blackcap'}, {'id': 1861, 'synset': 'greater_whitethroat.n.01', 'name': 'greater_whitethroat'}, {'id': 1862, 'synset': 'lesser_whitethroat.n.01', 'name': 'lesser_whitethroat'}, {'id': 1863, 'synset': 'wood_warbler.n.02', 'name': 'wood_warbler'}, {'id': 1864, 'synset': 'sedge_warbler.n.01', 'name': 'sedge_warbler'}, {'id': 1865, 'synset': 'wren_warbler.n.01', 'name': 'wren_warbler'}, {'id': 1866, 'synset': 'tailorbird.n.01', 'name': 'tailorbird'}, {'id': 1867, 'synset': 'babbler.n.02', 'name': 'babbler'}, {'id': 1868, 'synset': 'new_world_warbler.n.01', 'name': 'New_World_warbler'}, {'id': 1869, 'synset': 'parula_warbler.n.01', 'name': 'parula_warbler'}, {'id': 1870, 'synset': "wilson's_warbler.n.01", 'name': "Wilson's_warbler"}, {'id': 1871, 'synset': 'flycatching_warbler.n.01', 'name': 'flycatching_warbler'}, {'id': 1872, 'synset': 'american_redstart.n.01', 'name': 'American_redstart'}, {'id': 1873, 'synset': 'cape_may_warbler.n.01', 'name': 'Cape_May_warbler'}, {'id': 1874, 'synset': 'yellow_warbler.n.01', 'name': 'yellow_warbler'}, {'id': 1875, 'synset': 'blackburn.n.01', 'name': 'Blackburn'}, {'id': 1876, 'synset': "audubon's_warbler.n.01", 'name': "Audubon's_warbler"}, {'id': 1877, 'synset': 'myrtle_warbler.n.01', 'name': 'myrtle_warbler'}, {'id': 1878, 'synset': 'blackpoll.n.01', 'name': 'blackpoll'}, {'id': 1879, 'synset': 'new_world_chat.n.01', 'name': 'New_World_chat'}, {'id': 1880, 'synset': 'yellow-breasted_chat.n.01', 'name': 'yellow-breasted_chat'}, {'id': 1881, 'synset': 'ovenbird.n.01', 'name': 'ovenbird'}, {'id': 1882, 'synset': 'water_thrush.n.01', 'name': 'water_thrush'}, {'id': 1883, 'synset': 'yellowthroat.n.01', 'name': 'yellowthroat'}, {'id': 1884, 'synset': 'common_yellowthroat.n.01', 'name': 'common_yellowthroat'}, {'id': 1885, 'synset': 'riflebird.n.01', 'name': 'riflebird'}, {'id': 1886, 'synset': 'new_world_oriole.n.01', 'name': 'New_World_oriole'}, {'id': 1887, 'synset': 'northern_oriole.n.01', 'name': 'northern_oriole'}, {'id': 1888, 'synset': 'baltimore_oriole.n.01', 'name': 'Baltimore_oriole'}, {'id': 1889, 'synset': "bullock's_oriole.n.01", 'name': "Bullock's_oriole"}, {'id': 1890, 'synset': 'orchard_oriole.n.01', 'name': 'orchard_oriole'}, {'id': 1891, 'synset': 'meadowlark.n.01', 'name': 'meadowlark'}, {'id': 1892, 'synset': 'eastern_meadowlark.n.01', 'name': 'eastern_meadowlark'}, {'id': 1893, 'synset': 'western_meadowlark.n.01', 'name': 'western_meadowlark'}, {'id': 1894, 'synset': 'cacique.n.01', 'name': 'cacique'}, {'id': 1895, 'synset': 'bobolink.n.01', 'name': 'bobolink'}, {'id': 1896, 'synset': 'new_world_blackbird.n.01', 'name': 'New_World_blackbird'}, {'id': 1897, 'synset': 'grackle.n.02', 'name': 'grackle'}, {'id': 1898, 'synset': 'purple_grackle.n.01', 'name': 'purple_grackle'}, {'id': 1899, 'synset': 'rusty_blackbird.n.01', 'name': 'rusty_blackbird'}, {'id': 1900, 'synset': 'cowbird.n.01', 'name': 'cowbird'}, {'id': 1901, 'synset': 'red-winged_blackbird.n.01', 'name': 'red-winged_blackbird'}, {'id': 1902, 'synset': 'old_world_oriole.n.01', 'name': 'Old_World_oriole'}, {'id': 1903, 'synset': 'golden_oriole.n.01', 'name': 'golden_oriole'}, {'id': 1904, 'synset': 'fig-bird.n.01', 'name': 'fig-bird'}, {'id': 1905, 'synset': 'starling.n.01', 'name': 'starling'}, {'id': 1906, 'synset': 'common_starling.n.01', 'name': 'common_starling'}, {'id': 1907, 'synset': 'rose-colored_starling.n.01', 'name': 'rose-colored_starling'}, {'id': 1908, 'synset': 'myna.n.01', 'name': 'myna'}, {'id': 1909, 'synset': 'crested_myna.n.01', 'name': 'crested_myna'}, {'id': 1910, 'synset': 'hill_myna.n.01', 'name': 'hill_myna'}, {'id': 1911, 'synset': 'corvine_bird.n.01', 'name': 'corvine_bird'}, {'id': 1912, 'synset': 'american_crow.n.01', 'name': 'American_crow'}, {'id': 1913, 'synset': 'raven.n.01', 'name': 'raven'}, {'id': 1914, 'synset': 'rook.n.02', 'name': 'rook'}, {'id': 1915, 'synset': 'jackdaw.n.01', 'name': 'jackdaw'}, {'id': 1916, 'synset': 'chough.n.01', 'name': 'chough'}, {'id': 1917, 'synset': 'jay.n.02', 'name': 'jay'}, {'id': 1918, 'synset': 'old_world_jay.n.01', 'name': 'Old_World_jay'}, {'id': 1919, 'synset': 'common_european_jay.n.01', 'name': 'common_European_jay'}, {'id': 1920, 'synset': 'new_world_jay.n.01', 'name': 'New_World_jay'}, {'id': 1921, 'synset': 'blue_jay.n.01', 'name': 'blue_jay'}, {'id': 1922, 'synset': 'canada_jay.n.01', 'name': 'Canada_jay'}, {'id': 1923, 'synset': 'rocky_mountain_jay.n.01', 'name': 'Rocky_Mountain_jay'}, {'id': 1924, 'synset': 'nutcracker.n.03', 'name': 'nutcracker'}, {'id': 1925, 'synset': 'common_nutcracker.n.01', 'name': 'common_nutcracker'}, {'id': 1926, 'synset': "clark's_nutcracker.n.01", 'name': "Clark's_nutcracker"}, {'id': 1927, 'synset': 'magpie.n.01', 'name': 'magpie'}, {'id': 1928, 'synset': 'european_magpie.n.01', 'name': 'European_magpie'}, {'id': 1929, 'synset': 'american_magpie.n.01', 'name': 'American_magpie'}, {'id': 1930, 'synset': 'australian_magpie.n.01', 'name': 'Australian_magpie'}, {'id': 1931, 'synset': 'butcherbird.n.02', 'name': 'butcherbird'}, {'id': 1932, 'synset': 'currawong.n.01', 'name': 'currawong'}, {'id': 1933, 'synset': 'piping_crow.n.01', 'name': 'piping_crow'}, {'id': 1934, 'synset': 'wren.n.02', 'name': 'wren'}, {'id': 1935, 'synset': 'winter_wren.n.01', 'name': 'winter_wren'}, {'id': 1936, 'synset': 'house_wren.n.01', 'name': 'house_wren'}, {'id': 1937, 'synset': 'marsh_wren.n.01', 'name': 'marsh_wren'}, {'id': 1938, 'synset': 'long-billed_marsh_wren.n.01', 'name': 'long-billed_marsh_wren'}, {'id': 1939, 'synset': 'sedge_wren.n.01', 'name': 'sedge_wren'}, {'id': 1940, 'synset': 'rock_wren.n.02', 'name': 'rock_wren'}, {'id': 1941, 'synset': 'carolina_wren.n.01', 'name': 'Carolina_wren'}, {'id': 1942, 'synset': 'cactus_wren.n.01', 'name': 'cactus_wren'}, {'id': 1943, 'synset': 'mockingbird.n.01', 'name': 'mockingbird'}, {'id': 1944, 'synset': 'blue_mockingbird.n.01', 'name': 'blue_mockingbird'}, {'id': 1945, 'synset': 'catbird.n.02', 'name': 'catbird'}, {'id': 1946, 'synset': 'thrasher.n.02', 'name': 'thrasher'}, {'id': 1947, 'synset': 'brown_thrasher.n.01', 'name': 'brown_thrasher'}, {'id': 1948, 'synset': 'new_zealand_wren.n.01', 'name': 'New_Zealand_wren'}, {'id': 1949, 'synset': 'rock_wren.n.01', 'name': 'rock_wren'}, {'id': 1950, 'synset': 'rifleman_bird.n.01', 'name': 'rifleman_bird'}, {'id': 1951, 'synset': 'creeper.n.03', 'name': 'creeper'}, {'id': 1952, 'synset': 'brown_creeper.n.01', 'name': 'brown_creeper'}, {'id': 1953, 'synset': 'european_creeper.n.01', 'name': 'European_creeper'}, {'id': 1954, 'synset': 'wall_creeper.n.01', 'name': 'wall_creeper'}, {'id': 1955, 'synset': 'european_nuthatch.n.01', 'name': 'European_nuthatch'}, {'id': 1956, 'synset': 'red-breasted_nuthatch.n.01', 'name': 'red-breasted_nuthatch'}, {'id': 1957, 'synset': 'white-breasted_nuthatch.n.01', 'name': 'white-breasted_nuthatch'}, {'id': 1958, 'synset': 'titmouse.n.01', 'name': 'titmouse'}, {'id': 1959, 'synset': 'chickadee.n.01', 'name': 'chickadee'}, {'id': 1960, 'synset': 'black-capped_chickadee.n.01', 'name': 'black-capped_chickadee'}, {'id': 1961, 'synset': 'tufted_titmouse.n.01', 'name': 'tufted_titmouse'}, {'id': 1962, 'synset': 'carolina_chickadee.n.01', 'name': 'Carolina_chickadee'}, {'id': 1963, 'synset': 'blue_tit.n.01', 'name': 'blue_tit'}, {'id': 1964, 'synset': 'bushtit.n.01', 'name': 'bushtit'}, {'id': 1965, 'synset': 'wren-tit.n.01', 'name': 'wren-tit'}, {'id': 1966, 'synset': 'verdin.n.01', 'name': 'verdin'}, {'id': 1967, 'synset': 'fairy_bluebird.n.01', 'name': 'fairy_bluebird'}, {'id': 1968, 'synset': 'swallow.n.03', 'name': 'swallow'}, {'id': 1969, 'synset': 'barn_swallow.n.01', 'name': 'barn_swallow'}, {'id': 1970, 'synset': 'cliff_swallow.n.01', 'name': 'cliff_swallow'}, {'id': 1971, 'synset': 'tree_swallow.n.02', 'name': 'tree_swallow'}, {'id': 1972, 'synset': 'white-bellied_swallow.n.01', 'name': 'white-bellied_swallow'}, {'id': 1973, 'synset': 'martin.n.05', 'name': 'martin'}, {'id': 1974, 'synset': 'house_martin.n.01', 'name': 'house_martin'}, {'id': 1975, 'synset': 'bank_martin.n.01', 'name': 'bank_martin'}, {'id': 1976, 'synset': 'purple_martin.n.01', 'name': 'purple_martin'}, {'id': 1977, 'synset': 'wood_swallow.n.01', 'name': 'wood_swallow'}, {'id': 1978, 'synset': 'tanager.n.01', 'name': 'tanager'}, {'id': 1979, 'synset': 'scarlet_tanager.n.01', 'name': 'scarlet_tanager'}, {'id': 1980, 'synset': 'western_tanager.n.01', 'name': 'western_tanager'}, {'id': 1981, 'synset': 'summer_tanager.n.01', 'name': 'summer_tanager'}, {'id': 1982, 'synset': 'hepatic_tanager.n.01', 'name': 'hepatic_tanager'}, {'id': 1983, 'synset': 'shrike.n.01', 'name': 'shrike'}, {'id': 1984, 'synset': 'butcherbird.n.01', 'name': 'butcherbird'}, {'id': 1985, 'synset': 'european_shrike.n.01', 'name': 'European_shrike'}, {'id': 1986, 'synset': 'northern_shrike.n.01', 'name': 'northern_shrike'}, {'id': 1987, 'synset': 'white-rumped_shrike.n.01', 'name': 'white-rumped_shrike'}, {'id': 1988, 'synset': 'loggerhead_shrike.n.01', 'name': 'loggerhead_shrike'}, {'id': 1989, 'synset': 'migrant_shrike.n.01', 'name': 'migrant_shrike'}, {'id': 1990, 'synset': 'bush_shrike.n.01', 'name': 'bush_shrike'}, {'id': 1991, 'synset': 'black-fronted_bush_shrike.n.01', 'name': 'black-fronted_bush_shrike'}, {'id': 1992, 'synset': 'bowerbird.n.01', 'name': 'bowerbird'}, {'id': 1993, 'synset': 'satin_bowerbird.n.01', 'name': 'satin_bowerbird'}, {'id': 1994, 'synset': 'great_bowerbird.n.01', 'name': 'great_bowerbird'}, {'id': 1995, 'synset': 'water_ouzel.n.01', 'name': 'water_ouzel'}, {'id': 1996, 'synset': 'european_water_ouzel.n.01', 'name': 'European_water_ouzel'}, {'id': 1997, 'synset': 'american_water_ouzel.n.01', 'name': 'American_water_ouzel'}, {'id': 1998, 'synset': 'vireo.n.01', 'name': 'vireo'}, {'id': 1999, 'synset': 'red-eyed_vireo.n.01', 'name': 'red-eyed_vireo'}, {'id': 2000, 'synset': 'solitary_vireo.n.01', 'name': 'solitary_vireo'}, {'id': 2001, 'synset': 'blue-headed_vireo.n.01', 'name': 'blue-headed_vireo'}, {'id': 2002, 'synset': 'waxwing.n.01', 'name': 'waxwing'}, {'id': 2003, 'synset': 'cedar_waxwing.n.01', 'name': 'cedar_waxwing'}, {'id': 2004, 'synset': 'bohemian_waxwing.n.01', 'name': 'Bohemian_waxwing'}, {'id': 2005, 'synset': 'bird_of_prey.n.01', 'name': 'bird_of_prey'}, {'id': 2006, 'synset': 'accipitriformes.n.01', 'name': 'Accipitriformes'}, {'id': 2007, 'synset': 'hawk.n.01', 'name': 'hawk'}, {'id': 2008, 'synset': 'eyas.n.01', 'name': 'eyas'}, {'id': 2009, 'synset': 'tiercel.n.01', 'name': 'tiercel'}, {'id': 2010, 'synset': 'goshawk.n.01', 'name': 'goshawk'}, {'id': 2011, 'synset': 'sparrow_hawk.n.02', 'name': 'sparrow_hawk'}, {'id': 2012, 'synset': "cooper's_hawk.n.01", 'name': "Cooper's_hawk"}, {'id': 2013, 'synset': 'chicken_hawk.n.01', 'name': 'chicken_hawk'}, {'id': 2014, 'synset': 'buteonine.n.01', 'name': 'buteonine'}, {'id': 2015, 'synset': 'redtail.n.01', 'name': 'redtail'}, {'id': 2016, 'synset': 'rough-legged_hawk.n.01', 'name': 'rough-legged_hawk'}, {'id': 2017, 'synset': 'red-shouldered_hawk.n.01', 'name': 'red-shouldered_hawk'}, {'id': 2018, 'synset': 'buzzard.n.02', 'name': 'buzzard'}, {'id': 2019, 'synset': 'honey_buzzard.n.01', 'name': 'honey_buzzard'}, {'id': 2020, 'synset': 'kite.n.04', 'name': 'kite'}, {'id': 2021, 'synset': 'black_kite.n.01', 'name': 'black_kite'}, {'id': 2022, 'synset': 'swallow-tailed_kite.n.01', 'name': 'swallow-tailed_kite'}, {'id': 2023, 'synset': 'white-tailed_kite.n.01', 'name': 'white-tailed_kite'}, {'id': 2024, 'synset': 'harrier.n.03', 'name': 'harrier'}, {'id': 2025, 'synset': 'marsh_harrier.n.01', 'name': 'marsh_harrier'}, {'id': 2026, 'synset': "montagu's_harrier.n.01", 'name': "Montagu's_harrier"}, {'id': 2027, 'synset': 'marsh_hawk.n.01', 'name': 'marsh_hawk'}, {'id': 2028, 'synset': 'harrier_eagle.n.01', 'name': 'harrier_eagle'}, {'id': 2029, 'synset': 'peregrine.n.01', 'name': 'peregrine'}, {'id': 2030, 'synset': 'falcon-gentle.n.01', 'name': 'falcon-gentle'}, {'id': 2031, 'synset': 'gyrfalcon.n.01', 'name': 'gyrfalcon'}, {'id': 2032, 'synset': 'kestrel.n.02', 'name': 'kestrel'}, {'id': 2033, 'synset': 'sparrow_hawk.n.01', 'name': 'sparrow_hawk'}, {'id': 2034, 'synset': 'pigeon_hawk.n.01', 'name': 'pigeon_hawk'}, {'id': 2035, 'synset': 'hobby.n.03', 'name': 'hobby'}, {'id': 2036, 'synset': 'caracara.n.01', 'name': 'caracara'}, {'id': 2037, 'synset': "audubon's_caracara.n.01", 'name': "Audubon's_caracara"}, {'id': 2038, 'synset': 'carancha.n.01', 'name': 'carancha'}, {'id': 2039, 'synset': 'young_bird.n.01', 'name': 'young_bird'}, {'id': 2040, 'synset': 'eaglet.n.01', 'name': 'eaglet'}, {'id': 2041, 'synset': 'harpy.n.04', 'name': 'harpy'}, {'id': 2042, 'synset': 'golden_eagle.n.01', 'name': 'golden_eagle'}, {'id': 2043, 'synset': 'tawny_eagle.n.01', 'name': 'tawny_eagle'}, {'id': 2044, 'synset': 'bald_eagle.n.01', 'name': 'bald_eagle'}, {'id': 2045, 'synset': 'sea_eagle.n.02', 'name': 'sea_eagle'}, {'id': 2046, 'synset': 'kamchatkan_sea_eagle.n.01', 'name': 'Kamchatkan_sea_eagle'}, {'id': 2047, 'synset': 'ern.n.01', 'name': 'ern'}, {'id': 2048, 'synset': 'fishing_eagle.n.01', 'name': 'fishing_eagle'}, {'id': 2049, 'synset': 'osprey.n.01', 'name': 'osprey'}, {'id': 2050, 'synset': 'aegypiidae.n.01', 'name': 'Aegypiidae'}, {'id': 2051, 'synset': 'old_world_vulture.n.01', 'name': 'Old_World_vulture'}, {'id': 2052, 'synset': 'griffon_vulture.n.01', 'name': 'griffon_vulture'}, {'id': 2053, 'synset': 'bearded_vulture.n.01', 'name': 'bearded_vulture'}, {'id': 2054, 'synset': 'egyptian_vulture.n.01', 'name': 'Egyptian_vulture'}, {'id': 2055, 'synset': 'black_vulture.n.02', 'name': 'black_vulture'}, {'id': 2056, 'synset': 'secretary_bird.n.01', 'name': 'secretary_bird'}, {'id': 2057, 'synset': 'new_world_vulture.n.01', 'name': 'New_World_vulture'}, {'id': 2058, 'synset': 'buzzard.n.01', 'name': 'buzzard'}, {'id': 2059, 'synset': 'condor.n.01', 'name': 'condor'}, {'id': 2060, 'synset': 'andean_condor.n.01', 'name': 'Andean_condor'}, {'id': 2061, 'synset': 'california_condor.n.01', 'name': 'California_condor'}, {'id': 2062, 'synset': 'black_vulture.n.01', 'name': 'black_vulture'}, {'id': 2063, 'synset': 'king_vulture.n.01', 'name': 'king_vulture'}, {'id': 2064, 'synset': 'owlet.n.01', 'name': 'owlet'}, {'id': 2065, 'synset': 'little_owl.n.01', 'name': 'little_owl'}, {'id': 2066, 'synset': 'horned_owl.n.01', 'name': 'horned_owl'}, {'id': 2067, 'synset': 'great_horned_owl.n.01', 'name': 'great_horned_owl'}, {'id': 2068, 'synset': 'great_grey_owl.n.01', 'name': 'great_grey_owl'}, {'id': 2069, 'synset': 'tawny_owl.n.01', 'name': 'tawny_owl'}, {'id': 2070, 'synset': 'barred_owl.n.01', 'name': 'barred_owl'}, {'id': 2071, 'synset': 'screech_owl.n.02', 'name': 'screech_owl'}, {'id': 2072, 'synset': 'screech_owl.n.01', 'name': 'screech_owl'}, {'id': 2073, 'synset': 'scops_owl.n.01', 'name': 'scops_owl'}, {'id': 2074, 'synset': 'spotted_owl.n.01', 'name': 'spotted_owl'}, {'id': 2075, 'synset': 'old_world_scops_owl.n.01', 'name': 'Old_World_scops_owl'}, {'id': 2076, 'synset': 'oriental_scops_owl.n.01', 'name': 'Oriental_scops_owl'}, {'id': 2077, 'synset': 'hoot_owl.n.01', 'name': 'hoot_owl'}, {'id': 2078, 'synset': 'hawk_owl.n.01', 'name': 'hawk_owl'}, {'id': 2079, 'synset': 'long-eared_owl.n.01', 'name': 'long-eared_owl'}, {'id': 2080, 'synset': 'laughing_owl.n.01', 'name': 'laughing_owl'}, {'id': 2081, 'synset': 'barn_owl.n.01', 'name': 'barn_owl'}, {'id': 2082, 'synset': 'amphibian.n.03', 'name': 'amphibian'}, {'id': 2083, 'synset': 'ichyostega.n.01', 'name': 'Ichyostega'}, {'id': 2084, 'synset': 'urodele.n.01', 'name': 'urodele'}, {'id': 2085, 'synset': 'salamander.n.01', 'name': 'salamander'}, {'id': 2086, 'synset': 'european_fire_salamander.n.01', 'name': 'European_fire_salamander'}, {'id': 2087, 'synset': 'spotted_salamander.n.02', 'name': 'spotted_salamander'}, {'id': 2088, 'synset': 'alpine_salamander.n.01', 'name': 'alpine_salamander'}, {'id': 2089, 'synset': 'newt.n.01', 'name': 'newt'}, {'id': 2090, 'synset': 'common_newt.n.01', 'name': 'common_newt'}, {'id': 2091, 'synset': 'red_eft.n.01', 'name': 'red_eft'}, {'id': 2092, 'synset': 'pacific_newt.n.01', 'name': 'Pacific_newt'}, {'id': 2093, 'synset': 'rough-skinned_newt.n.01', 'name': 'rough-skinned_newt'}, {'id': 2094, 'synset': 'california_newt.n.01', 'name': 'California_newt'}, {'id': 2095, 'synset': 'eft.n.01', 'name': 'eft'}, {'id': 2096, 'synset': 'ambystomid.n.01', 'name': 'ambystomid'}, {'id': 2097, 'synset': 'mole_salamander.n.01', 'name': 'mole_salamander'}, {'id': 2098, 'synset': 'spotted_salamander.n.01', 'name': 'spotted_salamander'}, {'id': 2099, 'synset': 'tiger_salamander.n.01', 'name': 'tiger_salamander'}, {'id': 2100, 'synset': 'axolotl.n.01', 'name': 'axolotl'}, {'id': 2101, 'synset': 'waterdog.n.01', 'name': 'waterdog'}, {'id': 2102, 'synset': 'hellbender.n.01', 'name': 'hellbender'}, {'id': 2103, 'synset': 'giant_salamander.n.01', 'name': 'giant_salamander'}, {'id': 2104, 'synset': 'olm.n.01', 'name': 'olm'}, {'id': 2105, 'synset': 'mud_puppy.n.01', 'name': 'mud_puppy'}, {'id': 2106, 'synset': 'dicamptodon.n.01', 'name': 'dicamptodon'}, {'id': 2107, 'synset': 'pacific_giant_salamander.n.01', 'name': 'Pacific_giant_salamander'}, {'id': 2108, 'synset': 'olympic_salamander.n.01', 'name': 'olympic_salamander'}, {'id': 2109, 'synset': 'lungless_salamander.n.01', 'name': 'lungless_salamander'}, {'id': 2110, 'synset': 'eastern_red-backed_salamander.n.01', 'name': 'eastern_red-backed_salamander'}, {'id': 2111, 'synset': 'western_red-backed_salamander.n.01', 'name': 'western_red-backed_salamander'}, {'id': 2112, 'synset': 'dusky_salamander.n.01', 'name': 'dusky_salamander'}, {'id': 2113, 'synset': 'climbing_salamander.n.01', 'name': 'climbing_salamander'}, {'id': 2114, 'synset': 'arboreal_salamander.n.01', 'name': 'arboreal_salamander'}, {'id': 2115, 'synset': 'slender_salamander.n.01', 'name': 'slender_salamander'}, {'id': 2116, 'synset': 'web-toed_salamander.n.01', 'name': 'web-toed_salamander'}, {'id': 2117, 'synset': 'shasta_salamander.n.01', 'name': 'Shasta_salamander'}, {'id': 2118, 'synset': 'limestone_salamander.n.01', 'name': 'limestone_salamander'}, {'id': 2119, 'synset': 'amphiuma.n.01', 'name': 'amphiuma'}, {'id': 2120, 'synset': 'siren.n.05', 'name': 'siren'}, {'id': 2121, 'synset': 'true_frog.n.01', 'name': 'true_frog'}, {'id': 2122, 'synset': 'wood-frog.n.01', 'name': 'wood-frog'}, {'id': 2123, 'synset': 'leopard_frog.n.01', 'name': 'leopard_frog'}, {'id': 2124, 'synset': 'bullfrog.n.01', 'name': 'bullfrog'}, {'id': 2125, 'synset': 'green_frog.n.01', 'name': 'green_frog'}, {'id': 2126, 'synset': 'cascades_frog.n.01', 'name': 'cascades_frog'}, {'id': 2127, 'synset': 'goliath_frog.n.01', 'name': 'goliath_frog'}, {'id': 2128, 'synset': 'pickerel_frog.n.01', 'name': 'pickerel_frog'}, {'id': 2129, 'synset': 'tarahumara_frog.n.01', 'name': 'tarahumara_frog'}, {'id': 2130, 'synset': 'grass_frog.n.01', 'name': 'grass_frog'}, {'id': 2131, 'synset': 'leptodactylid_frog.n.01', 'name': 'leptodactylid_frog'}, {'id': 2132, 'synset': 'robber_frog.n.02', 'name': 'robber_frog'}, {'id': 2133, 'synset': 'barking_frog.n.01', 'name': 'barking_frog'}, {'id': 2134, 'synset': 'crapaud.n.01', 'name': 'crapaud'}, {'id': 2135, 'synset': 'tree_frog.n.02', 'name': 'tree_frog'}, {'id': 2136, 'synset': 'tailed_frog.n.01', 'name': 'tailed_frog'}, {'id': 2137, 'synset': 'liopelma_hamiltoni.n.01', 'name': 'Liopelma_hamiltoni'}, {'id': 2138, 'synset': 'true_toad.n.01', 'name': 'true_toad'}, {'id': 2139, 'synset': 'bufo.n.01', 'name': 'bufo'}, {'id': 2140, 'synset': 'agua.n.01', 'name': 'agua'}, {'id': 2141, 'synset': 'european_toad.n.01', 'name': 'European_toad'}, {'id': 2142, 'synset': 'natterjack.n.01', 'name': 'natterjack'}, {'id': 2143, 'synset': 'american_toad.n.01', 'name': 'American_toad'}, {'id': 2144, 'synset': 'eurasian_green_toad.n.01', 'name': 'Eurasian_green_toad'}, {'id': 2145, 'synset': 'american_green_toad.n.01', 'name': 'American_green_toad'}, {'id': 2146, 'synset': 'yosemite_toad.n.01', 'name': 'Yosemite_toad'}, {'id': 2147, 'synset': 'texas_toad.n.01', 'name': 'Texas_toad'}, {'id': 2148, 'synset': 'southwestern_toad.n.01', 'name': 'southwestern_toad'}, {'id': 2149, 'synset': 'western_toad.n.01', 'name': 'western_toad'}, {'id': 2150, 'synset': 'obstetrical_toad.n.01', 'name': 'obstetrical_toad'}, {'id': 2151, 'synset': 'midwife_toad.n.01', 'name': 'midwife_toad'}, {'id': 2152, 'synset': 'fire-bellied_toad.n.01', 'name': 'fire-bellied_toad'}, {'id': 2153, 'synset': 'spadefoot.n.01', 'name': 'spadefoot'}, {'id': 2154, 'synset': 'western_spadefoot.n.01', 'name': 'western_spadefoot'}, {'id': 2155, 'synset': 'southern_spadefoot.n.01', 'name': 'southern_spadefoot'}, {'id': 2156, 'synset': 'plains_spadefoot.n.01', 'name': 'plains_spadefoot'}, {'id': 2157, 'synset': 'tree_toad.n.01', 'name': 'tree_toad'}, {'id': 2158, 'synset': 'spring_peeper.n.01', 'name': 'spring_peeper'}, {'id': 2159, 'synset': 'pacific_tree_toad.n.01', 'name': 'Pacific_tree_toad'}, {'id': 2160, 'synset': 'canyon_treefrog.n.01', 'name': 'canyon_treefrog'}, {'id': 2161, 'synset': 'chameleon_tree_frog.n.01', 'name': 'chameleon_tree_frog'}, {'id': 2162, 'synset': 'cricket_frog.n.01', 'name': 'cricket_frog'}, {'id': 2163, 'synset': 'northern_cricket_frog.n.01', 'name': 'northern_cricket_frog'}, {'id': 2164, 'synset': 'eastern_cricket_frog.n.01', 'name': 'eastern_cricket_frog'}, {'id': 2165, 'synset': 'chorus_frog.n.01', 'name': 'chorus_frog'}, {'id': 2166, 'synset': 'lowland_burrowing_treefrog.n.01', 'name': 'lowland_burrowing_treefrog'}, {'id': 2167, 'synset': 'western_narrow-mouthed_toad.n.01', 'name': 'western_narrow-mouthed_toad'}, {'id': 2168, 'synset': 'eastern_narrow-mouthed_toad.n.01', 'name': 'eastern_narrow-mouthed_toad'}, {'id': 2169, 'synset': 'sheep_frog.n.01', 'name': 'sheep_frog'}, {'id': 2170, 'synset': 'tongueless_frog.n.01', 'name': 'tongueless_frog'}, {'id': 2171, 'synset': 'surinam_toad.n.01', 'name': 'Surinam_toad'}, {'id': 2172, 'synset': 'african_clawed_frog.n.01', 'name': 'African_clawed_frog'}, {'id': 2173, 'synset': 'south_american_poison_toad.n.01', 'name': 'South_American_poison_toad'}, {'id': 2174, 'synset': 'caecilian.n.01', 'name': 'caecilian'}, {'id': 2175, 'synset': 'reptile.n.01', 'name': 'reptile'}, {'id': 2176, 'synset': 'anapsid.n.01', 'name': 'anapsid'}, {'id': 2177, 'synset': 'diapsid.n.01', 'name': 'diapsid'}, {'id': 2178, 'synset': 'diapsida.n.01', 'name': 'Diapsida'}, {'id': 2179, 'synset': 'chelonian.n.01', 'name': 'chelonian'}, {'id': 2180, 'synset': 'sea_turtle.n.01', 'name': 'sea_turtle'}, {'id': 2181, 'synset': 'green_turtle.n.01', 'name': 'green_turtle'}, {'id': 2182, 'synset': 'loggerhead.n.02', 'name': 'loggerhead'}, {'id': 2183, 'synset': 'ridley.n.01', 'name': 'ridley'}, {'id': 2184, 'synset': 'atlantic_ridley.n.01', 'name': 'Atlantic_ridley'}, {'id': 2185, 'synset': 'pacific_ridley.n.01', 'name': 'Pacific_ridley'}, {'id': 2186, 'synset': 'hawksbill_turtle.n.01', 'name': 'hawksbill_turtle'}, {'id': 2187, 'synset': 'leatherback_turtle.n.01', 'name': 'leatherback_turtle'}, {'id': 2188, 'synset': 'snapping_turtle.n.01', 'name': 'snapping_turtle'}, {'id': 2189, 'synset': 'common_snapping_turtle.n.01', 'name': 'common_snapping_turtle'}, {'id': 2190, 'synset': 'alligator_snapping_turtle.n.01', 'name': 'alligator_snapping_turtle'}, {'id': 2191, 'synset': 'mud_turtle.n.01', 'name': 'mud_turtle'}, {'id': 2192, 'synset': 'musk_turtle.n.01', 'name': 'musk_turtle'}, {'id': 2193, 'synset': 'terrapin.n.01', 'name': 'terrapin'}, {'id': 2194, 'synset': 'diamondback_terrapin.n.01', 'name': 'diamondback_terrapin'}, {'id': 2195, 'synset': 'red-bellied_terrapin.n.01', 'name': 'red-bellied_terrapin'}, {'id': 2196, 'synset': 'slider.n.03', 'name': 'slider'}, {'id': 2197, 'synset': 'cooter.n.01', 'name': 'cooter'}, {'id': 2198, 'synset': 'box_turtle.n.01', 'name': 'box_turtle'}, {'id': 2199, 'synset': 'western_box_turtle.n.01', 'name': 'Western_box_turtle'}, {'id': 2200, 'synset': 'painted_turtle.n.01', 'name': 'painted_turtle'}, {'id': 2201, 'synset': 'tortoise.n.01', 'name': 'tortoise'}, {'id': 2202, 'synset': 'european_tortoise.n.01', 'name': 'European_tortoise'}, {'id': 2203, 'synset': 'giant_tortoise.n.01', 'name': 'giant_tortoise'}, {'id': 2204, 'synset': 'gopher_tortoise.n.01', 'name': 'gopher_tortoise'}, {'id': 2205, 'synset': 'desert_tortoise.n.01', 'name': 'desert_tortoise'}, {'id': 2206, 'synset': 'texas_tortoise.n.01', 'name': 'Texas_tortoise'}, {'id': 2207, 'synset': 'soft-shelled_turtle.n.01', 'name': 'soft-shelled_turtle'}, {'id': 2208, 'synset': 'spiny_softshell.n.01', 'name': 'spiny_softshell'}, {'id': 2209, 'synset': 'smooth_softshell.n.01', 'name': 'smooth_softshell'}, {'id': 2210, 'synset': 'tuatara.n.01', 'name': 'tuatara'}, {'id': 2211, 'synset': 'saurian.n.01', 'name': 'saurian'}, {'id': 2212, 'synset': 'gecko.n.01', 'name': 'gecko'}, {'id': 2213, 'synset': 'flying_gecko.n.01', 'name': 'flying_gecko'}, {'id': 2214, 'synset': 'banded_gecko.n.01', 'name': 'banded_gecko'}, {'id': 2215, 'synset': 'iguanid.n.01', 'name': 'iguanid'}, {'id': 2216, 'synset': 'common_iguana.n.01', 'name': 'common_iguana'}, {'id': 2217, 'synset': 'marine_iguana.n.01', 'name': 'marine_iguana'}, {'id': 2218, 'synset': 'desert_iguana.n.01', 'name': 'desert_iguana'}, {'id': 2219, 'synset': 'chuckwalla.n.01', 'name': 'chuckwalla'}, {'id': 2220, 'synset': 'zebra-tailed_lizard.n.01', 'name': 'zebra-tailed_lizard'}, {'id': 2221, 'synset': 'fringe-toed_lizard.n.01', 'name': 'fringe-toed_lizard'}, {'id': 2222, 'synset': 'earless_lizard.n.01', 'name': 'earless_lizard'}, {'id': 2223, 'synset': 'collared_lizard.n.01', 'name': 'collared_lizard'}, {'id': 2224, 'synset': 'leopard_lizard.n.01', 'name': 'leopard_lizard'}, {'id': 2225, 'synset': 'spiny_lizard.n.02', 'name': 'spiny_lizard'}, {'id': 2226, 'synset': 'fence_lizard.n.01', 'name': 'fence_lizard'}, {'id': 2227, 'synset': 'western_fence_lizard.n.01', 'name': 'western_fence_lizard'}, {'id': 2228, 'synset': 'eastern_fence_lizard.n.01', 'name': 'eastern_fence_lizard'}, {'id': 2229, 'synset': 'sagebrush_lizard.n.01', 'name': 'sagebrush_lizard'}, {'id': 2230, 'synset': 'side-blotched_lizard.n.01', 'name': 'side-blotched_lizard'}, {'id': 2231, 'synset': 'tree_lizard.n.01', 'name': 'tree_lizard'}, {'id': 2232, 'synset': 'horned_lizard.n.01', 'name': 'horned_lizard'}, {'id': 2233, 'synset': 'texas_horned_lizard.n.01', 'name': 'Texas_horned_lizard'}, {'id': 2234, 'synset': 'basilisk.n.03', 'name': 'basilisk'}, {'id': 2235, 'synset': 'american_chameleon.n.01', 'name': 'American_chameleon'}, {'id': 2236, 'synset': 'worm_lizard.n.01', 'name': 'worm_lizard'}, {'id': 2237, 'synset': 'night_lizard.n.01', 'name': 'night_lizard'}, {'id': 2238, 'synset': 'skink.n.01', 'name': 'skink'}, {'id': 2239, 'synset': 'western_skink.n.01', 'name': 'western_skink'}, {'id': 2240, 'synset': 'mountain_skink.n.01', 'name': 'mountain_skink'}, {'id': 2241, 'synset': 'teiid_lizard.n.01', 'name': 'teiid_lizard'}, {'id': 2242, 'synset': 'whiptail.n.01', 'name': 'whiptail'}, {'id': 2243, 'synset': 'racerunner.n.01', 'name': 'racerunner'}, {'id': 2244, 'synset': 'plateau_striped_whiptail.n.01', 'name': 'plateau_striped_whiptail'}, {'id': 2245, 'synset': 'chihuahuan_spotted_whiptail.n.01', 'name': 'Chihuahuan_spotted_whiptail'}, {'id': 2246, 'synset': 'western_whiptail.n.01', 'name': 'western_whiptail'}, {'id': 2247, 'synset': 'checkered_whiptail.n.01', 'name': 'checkered_whiptail'}, {'id': 2248, 'synset': 'teju.n.01', 'name': 'teju'}, {'id': 2249, 'synset': 'caiman_lizard.n.01', 'name': 'caiman_lizard'}, {'id': 2250, 'synset': 'agamid.n.01', 'name': 'agamid'}, {'id': 2251, 'synset': 'agama.n.01', 'name': 'agama'}, {'id': 2252, 'synset': 'frilled_lizard.n.01', 'name': 'frilled_lizard'}, {'id': 2253, 'synset': 'moloch.n.03', 'name': 'moloch'}, {'id': 2254, 'synset': 'mountain_devil.n.02', 'name': 'mountain_devil'}, {'id': 2255, 'synset': 'anguid_lizard.n.01', 'name': 'anguid_lizard'}, {'id': 2256, 'synset': 'alligator_lizard.n.01', 'name': 'alligator_lizard'}, {'id': 2257, 'synset': 'blindworm.n.01', 'name': 'blindworm'}, {'id': 2258, 'synset': 'glass_lizard.n.01', 'name': 'glass_lizard'}, {'id': 2259, 'synset': 'legless_lizard.n.01', 'name': 'legless_lizard'}, {'id': 2260, 'synset': 'lanthanotus_borneensis.n.01', 'name': 'Lanthanotus_borneensis'}, {'id': 2261, 'synset': 'venomous_lizard.n.01', 'name': 'venomous_lizard'}, {'id': 2262, 'synset': 'gila_monster.n.01', 'name': 'Gila_monster'}, {'id': 2263, 'synset': 'beaded_lizard.n.01', 'name': 'beaded_lizard'}, {'id': 2264, 'synset': 'lacertid_lizard.n.01', 'name': 'lacertid_lizard'}, {'id': 2265, 'synset': 'sand_lizard.n.01', 'name': 'sand_lizard'}, {'id': 2266, 'synset': 'green_lizard.n.01', 'name': 'green_lizard'}, {'id': 2267, 'synset': 'chameleon.n.03', 'name': 'chameleon'}, {'id': 2268, 'synset': 'african_chameleon.n.01', 'name': 'African_chameleon'}, {'id': 2269, 'synset': 'horned_chameleon.n.01', 'name': 'horned_chameleon'}, {'id': 2270, 'synset': 'monitor.n.07', 'name': 'monitor'}, {'id': 2271, 'synset': 'african_monitor.n.01', 'name': 'African_monitor'}, {'id': 2272, 'synset': 'komodo_dragon.n.01', 'name': 'Komodo_dragon'}, {'id': 2273, 'synset': 'crocodilian_reptile.n.01', 'name': 'crocodilian_reptile'}, {'id': 2274, 'synset': 'crocodile.n.01', 'name': 'crocodile'}, {'id': 2275, 'synset': 'african_crocodile.n.01', 'name': 'African_crocodile'}, {'id': 2276, 'synset': 'asian_crocodile.n.01', 'name': 'Asian_crocodile'}, {'id': 2277, 'synset': "morlett's_crocodile.n.01", 'name': "Morlett's_crocodile"}, {'id': 2278, 'synset': 'false_gavial.n.01', 'name': 'false_gavial'}, {'id': 2279, 'synset': 'american_alligator.n.01', 'name': 'American_alligator'}, {'id': 2280, 'synset': 'chinese_alligator.n.01', 'name': 'Chinese_alligator'}, {'id': 2281, 'synset': 'caiman.n.01', 'name': 'caiman'}, {'id': 2282, 'synset': 'spectacled_caiman.n.01', 'name': 'spectacled_caiman'}, {'id': 2283, 'synset': 'gavial.n.01', 'name': 'gavial'}, {'id': 2284, 'synset': 'armored_dinosaur.n.01', 'name': 'armored_dinosaur'}, {'id': 2285, 'synset': 'stegosaur.n.01', 'name': 'stegosaur'}, {'id': 2286, 'synset': 'ankylosaur.n.01', 'name': 'ankylosaur'}, {'id': 2287, 'synset': 'edmontonia.n.01', 'name': 'Edmontonia'}, {'id': 2288, 'synset': 'bone-headed_dinosaur.n.01', 'name': 'bone-headed_dinosaur'}, {'id': 2289, 'synset': 'pachycephalosaur.n.01', 'name': 'pachycephalosaur'}, {'id': 2290, 'synset': 'ceratopsian.n.01', 'name': 'ceratopsian'}, {'id': 2291, 'synset': 'protoceratops.n.01', 'name': 'protoceratops'}, {'id': 2292, 'synset': 'triceratops.n.01', 'name': 'triceratops'}, {'id': 2293, 'synset': 'styracosaur.n.01', 'name': 'styracosaur'}, {'id': 2294, 'synset': 'psittacosaur.n.01', 'name': 'psittacosaur'}, {'id': 2295, 'synset': 'ornithopod.n.01', 'name': 'ornithopod'}, {'id': 2296, 'synset': 'hadrosaur.n.01', 'name': 'hadrosaur'}, {'id': 2297, 'synset': 'trachodon.n.01', 'name': 'trachodon'}, {'id': 2298, 'synset': 'saurischian.n.01', 'name': 'saurischian'}, {'id': 2299, 'synset': 'sauropod.n.01', 'name': 'sauropod'}, {'id': 2300, 'synset': 'apatosaur.n.01', 'name': 'apatosaur'}, {'id': 2301, 'synset': 'barosaur.n.01', 'name': 'barosaur'}, {'id': 2302, 'synset': 'diplodocus.n.01', 'name': 'diplodocus'}, {'id': 2303, 'synset': 'argentinosaur.n.01', 'name': 'argentinosaur'}, {'id': 2304, 'synset': 'theropod.n.01', 'name': 'theropod'}, {'id': 2305, 'synset': 'ceratosaur.n.01', 'name': 'ceratosaur'}, {'id': 2306, 'synset': 'coelophysis.n.01', 'name': 'coelophysis'}, {'id': 2307, 'synset': 'tyrannosaur.n.01', 'name': 'tyrannosaur'}, {'id': 2308, 'synset': 'allosaur.n.01', 'name': 'allosaur'}, {'id': 2309, 'synset': 'ornithomimid.n.01', 'name': 'ornithomimid'}, {'id': 2310, 'synset': 'maniraptor.n.01', 'name': 'maniraptor'}, {'id': 2311, 'synset': 'oviraptorid.n.01', 'name': 'oviraptorid'}, {'id': 2312, 'synset': 'velociraptor.n.01', 'name': 'velociraptor'}, {'id': 2313, 'synset': 'deinonychus.n.01', 'name': 'deinonychus'}, {'id': 2314, 'synset': 'utahraptor.n.01', 'name': 'utahraptor'}, {'id': 2315, 'synset': 'synapsid.n.01', 'name': 'synapsid'}, {'id': 2316, 'synset': 'dicynodont.n.01', 'name': 'dicynodont'}, {'id': 2317, 'synset': 'pelycosaur.n.01', 'name': 'pelycosaur'}, {'id': 2318, 'synset': 'dimetrodon.n.01', 'name': 'dimetrodon'}, {'id': 2319, 'synset': 'pterosaur.n.01', 'name': 'pterosaur'}, {'id': 2320, 'synset': 'pterodactyl.n.01', 'name': 'pterodactyl'}, {'id': 2321, 'synset': 'ichthyosaur.n.01', 'name': 'ichthyosaur'}, {'id': 2322, 'synset': 'ichthyosaurus.n.01', 'name': 'ichthyosaurus'}, {'id': 2323, 'synset': 'stenopterygius.n.01', 'name': 'stenopterygius'}, {'id': 2324, 'synset': 'plesiosaur.n.01', 'name': 'plesiosaur'}, {'id': 2325, 'synset': 'nothosaur.n.01', 'name': 'nothosaur'}, {'id': 2326, 'synset': 'colubrid_snake.n.01', 'name': 'colubrid_snake'}, {'id': 2327, 'synset': 'hoop_snake.n.01', 'name': 'hoop_snake'}, {'id': 2328, 'synset': 'thunder_snake.n.01', 'name': 'thunder_snake'}, {'id': 2329, 'synset': 'ringneck_snake.n.01', 'name': 'ringneck_snake'}, {'id': 2330, 'synset': 'hognose_snake.n.01', 'name': 'hognose_snake'}, {'id': 2331, 'synset': 'leaf-nosed_snake.n.01', 'name': 'leaf-nosed_snake'}, {'id': 2332, 'synset': 'green_snake.n.02', 'name': 'green_snake'}, {'id': 2333, 'synset': 'smooth_green_snake.n.01', 'name': 'smooth_green_snake'}, {'id': 2334, 'synset': 'rough_green_snake.n.01', 'name': 'rough_green_snake'}, {'id': 2335, 'synset': 'green_snake.n.01', 'name': 'green_snake'}, {'id': 2336, 'synset': 'racer.n.04', 'name': 'racer'}, {'id': 2337, 'synset': 'blacksnake.n.02', 'name': 'blacksnake'}, {'id': 2338, 'synset': 'blue_racer.n.01', 'name': 'blue_racer'}, {'id': 2339, 'synset': 'horseshoe_whipsnake.n.01', 'name': 'horseshoe_whipsnake'}, {'id': 2340, 'synset': 'whip-snake.n.01', 'name': 'whip-snake'}, {'id': 2341, 'synset': 'coachwhip.n.02', 'name': 'coachwhip'}, {'id': 2342, 'synset': 'california_whipsnake.n.01', 'name': 'California_whipsnake'}, {'id': 2343, 'synset': 'sonoran_whipsnake.n.01', 'name': 'Sonoran_whipsnake'}, {'id': 2344, 'synset': 'rat_snake.n.01', 'name': 'rat_snake'}, {'id': 2345, 'synset': 'corn_snake.n.01', 'name': 'corn_snake'}, {'id': 2346, 'synset': 'black_rat_snake.n.01', 'name': 'black_rat_snake'}, {'id': 2347, 'synset': 'chicken_snake.n.01', 'name': 'chicken_snake'}, {'id': 2348, 'synset': 'indian_rat_snake.n.01', 'name': 'Indian_rat_snake'}, {'id': 2349, 'synset': 'glossy_snake.n.01', 'name': 'glossy_snake'}, {'id': 2350, 'synset': 'bull_snake.n.01', 'name': 'bull_snake'}, {'id': 2351, 'synset': 'gopher_snake.n.02', 'name': 'gopher_snake'}, {'id': 2352, 'synset': 'pine_snake.n.01', 'name': 'pine_snake'}, {'id': 2353, 'synset': 'king_snake.n.01', 'name': 'king_snake'}, {'id': 2354, 'synset': 'common_kingsnake.n.01', 'name': 'common_kingsnake'}, {'id': 2355, 'synset': 'milk_snake.n.01', 'name': 'milk_snake'}, {'id': 2356, 'synset': 'garter_snake.n.01', 'name': 'garter_snake'}, {'id': 2357, 'synset': 'common_garter_snake.n.01', 'name': 'common_garter_snake'}, {'id': 2358, 'synset': 'ribbon_snake.n.01', 'name': 'ribbon_snake'}, {'id': 2359, 'synset': 'western_ribbon_snake.n.01', 'name': 'Western_ribbon_snake'}, {'id': 2360, 'synset': 'lined_snake.n.01', 'name': 'lined_snake'}, {'id': 2361, 'synset': 'ground_snake.n.01', 'name': 'ground_snake'}, {'id': 2362, 'synset': 'eastern_ground_snake.n.01', 'name': 'eastern_ground_snake'}, {'id': 2363, 'synset': 'water_snake.n.01', 'name': 'water_snake'}, {'id': 2364, 'synset': 'common_water_snake.n.01', 'name': 'common_water_snake'}, {'id': 2365, 'synset': 'water_moccasin.n.02', 'name': 'water_moccasin'}, {'id': 2366, 'synset': 'grass_snake.n.01', 'name': 'grass_snake'}, {'id': 2367, 'synset': 'viperine_grass_snake.n.01', 'name': 'viperine_grass_snake'}, {'id': 2368, 'synset': 'red-bellied_snake.n.01', 'name': 'red-bellied_snake'}, {'id': 2369, 'synset': 'sand_snake.n.01', 'name': 'sand_snake'}, {'id': 2370, 'synset': 'banded_sand_snake.n.01', 'name': 'banded_sand_snake'}, {'id': 2371, 'synset': 'black-headed_snake.n.01', 'name': 'black-headed_snake'}, {'id': 2372, 'synset': 'vine_snake.n.01', 'name': 'vine_snake'}, {'id': 2373, 'synset': 'lyre_snake.n.01', 'name': 'lyre_snake'}, {'id': 2374, 'synset': 'sonoran_lyre_snake.n.01', 'name': 'Sonoran_lyre_snake'}, {'id': 2375, 'synset': 'night_snake.n.01', 'name': 'night_snake'}, {'id': 2376, 'synset': 'blind_snake.n.01', 'name': 'blind_snake'}, {'id': 2377, 'synset': 'western_blind_snake.n.01', 'name': 'western_blind_snake'}, {'id': 2378, 'synset': 'indigo_snake.n.01', 'name': 'indigo_snake'}, {'id': 2379, 'synset': 'eastern_indigo_snake.n.01', 'name': 'eastern_indigo_snake'}, {'id': 2380, 'synset': 'constrictor.n.01', 'name': 'constrictor'}, {'id': 2381, 'synset': 'boa.n.02', 'name': 'boa'}, {'id': 2382, 'synset': 'boa_constrictor.n.01', 'name': 'boa_constrictor'}, {'id': 2383, 'synset': 'rubber_boa.n.01', 'name': 'rubber_boa'}, {'id': 2384, 'synset': 'rosy_boa.n.01', 'name': 'rosy_boa'}, {'id': 2385, 'synset': 'anaconda.n.01', 'name': 'anaconda'}, {'id': 2386, 'synset': 'python.n.01', 'name': 'python'}, {'id': 2387, 'synset': 'carpet_snake.n.01', 'name': 'carpet_snake'}, {'id': 2388, 'synset': 'reticulated_python.n.01', 'name': 'reticulated_python'}, {'id': 2389, 'synset': 'indian_python.n.01', 'name': 'Indian_python'}, {'id': 2390, 'synset': 'rock_python.n.01', 'name': 'rock_python'}, {'id': 2391, 'synset': 'amethystine_python.n.01', 'name': 'amethystine_python'}, {'id': 2392, 'synset': 'elapid.n.01', 'name': 'elapid'}, {'id': 2393, 'synset': 'coral_snake.n.02', 'name': 'coral_snake'}, {'id': 2394, 'synset': 'eastern_coral_snake.n.01', 'name': 'eastern_coral_snake'}, {'id': 2395, 'synset': 'western_coral_snake.n.01', 'name': 'western_coral_snake'}, {'id': 2396, 'synset': 'coral_snake.n.01', 'name': 'coral_snake'}, {'id': 2397, 'synset': 'african_coral_snake.n.01', 'name': 'African_coral_snake'}, {'id': 2398, 'synset': 'australian_coral_snake.n.01', 'name': 'Australian_coral_snake'}, {'id': 2399, 'synset': 'copperhead.n.02', 'name': 'copperhead'}, {'id': 2400, 'synset': 'cobra.n.01', 'name': 'cobra'}, {'id': 2401, 'synset': 'indian_cobra.n.01', 'name': 'Indian_cobra'}, {'id': 2402, 'synset': 'asp.n.02', 'name': 'asp'}, {'id': 2403, 'synset': 'black-necked_cobra.n.01', 'name': 'black-necked_cobra'}, {'id': 2404, 'synset': 'hamadryad.n.02', 'name': 'hamadryad'}, {'id': 2405, 'synset': 'ringhals.n.01', 'name': 'ringhals'}, {'id': 2406, 'synset': 'mamba.n.01', 'name': 'mamba'}, {'id': 2407, 'synset': 'black_mamba.n.01', 'name': 'black_mamba'}, {'id': 2408, 'synset': 'green_mamba.n.01', 'name': 'green_mamba'}, {'id': 2409, 'synset': 'death_adder.n.01', 'name': 'death_adder'}, {'id': 2410, 'synset': 'tiger_snake.n.01', 'name': 'tiger_snake'}, {'id': 2411, 'synset': 'australian_blacksnake.n.01', 'name': 'Australian_blacksnake'}, {'id': 2412, 'synset': 'krait.n.01', 'name': 'krait'}, {'id': 2413, 'synset': 'banded_krait.n.01', 'name': 'banded_krait'}, {'id': 2414, 'synset': 'taipan.n.01', 'name': 'taipan'}, {'id': 2415, 'synset': 'sea_snake.n.01', 'name': 'sea_snake'}, {'id': 2416, 'synset': 'viper.n.01', 'name': 'viper'}, {'id': 2417, 'synset': 'adder.n.03', 'name': 'adder'}, {'id': 2418, 'synset': 'asp.n.01', 'name': 'asp'}, {'id': 2419, 'synset': 'puff_adder.n.01', 'name': 'puff_adder'}, {'id': 2420, 'synset': 'gaboon_viper.n.01', 'name': 'gaboon_viper'}, {'id': 2421, 'synset': 'horned_viper.n.01', 'name': 'horned_viper'}, {'id': 2422, 'synset': 'pit_viper.n.01', 'name': 'pit_viper'}, {'id': 2423, 'synset': 'copperhead.n.01', 'name': 'copperhead'}, {'id': 2424, 'synset': 'water_moccasin.n.01', 'name': 'water_moccasin'}, {'id': 2425, 'synset': 'rattlesnake.n.01', 'name': 'rattlesnake'}, {'id': 2426, 'synset': 'diamondback.n.01', 'name': 'diamondback'}, {'id': 2427, 'synset': 'timber_rattlesnake.n.01', 'name': 'timber_rattlesnake'}, {'id': 2428, 'synset': 'canebrake_rattlesnake.n.01', 'name': 'canebrake_rattlesnake'}, {'id': 2429, 'synset': 'prairie_rattlesnake.n.01', 'name': 'prairie_rattlesnake'}, {'id': 2430, 'synset': 'sidewinder.n.01', 'name': 'sidewinder'}, {'id': 2431, 'synset': 'western_diamondback.n.01', 'name': 'Western_diamondback'}, {'id': 2432, 'synset': 'rock_rattlesnake.n.01', 'name': 'rock_rattlesnake'}, {'id': 2433, 'synset': 'tiger_rattlesnake.n.01', 'name': 'tiger_rattlesnake'}, {'id': 2434, 'synset': 'mojave_rattlesnake.n.01', 'name': 'Mojave_rattlesnake'}, {'id': 2435, 'synset': 'speckled_rattlesnake.n.01', 'name': 'speckled_rattlesnake'}, {'id': 2436, 'synset': 'massasauga.n.02', 'name': 'massasauga'}, {'id': 2437, 'synset': 'ground_rattler.n.01', 'name': 'ground_rattler'}, {'id': 2438, 'synset': 'fer-de-lance.n.01', 'name': 'fer-de-lance'}, {'id': 2439, 'synset': 'carcase.n.01', 'name': 'carcase'}, {'id': 2440, 'synset': 'carrion.n.01', 'name': 'carrion'}, {'id': 2441, 'synset': 'arthropod.n.01', 'name': 'arthropod'}, {'id': 2442, 'synset': 'trilobite.n.01', 'name': 'trilobite'}, {'id': 2443, 'synset': 'arachnid.n.01', 'name': 'arachnid'}, {'id': 2444, 'synset': 'harvestman.n.01', 'name': 'harvestman'}, {'id': 2445, 'synset': 'scorpion.n.03', 'name': 'scorpion'}, {'id': 2446, 'synset': 'false_scorpion.n.01', 'name': 'false_scorpion'}, {'id': 2447, 'synset': 'book_scorpion.n.01', 'name': 'book_scorpion'}, {'id': 2448, 'synset': 'whip-scorpion.n.01', 'name': 'whip-scorpion'}, {'id': 2449, 'synset': 'vinegarroon.n.01', 'name': 'vinegarroon'}, {'id': 2450, 'synset': 'orb-weaving_spider.n.01', 'name': 'orb-weaving_spider'}, {'id': 2451, 'synset': 'black_and_gold_garden_spider.n.01', 'name': 'black_and_gold_garden_spider'}, {'id': 2452, 'synset': 'barn_spider.n.01', 'name': 'barn_spider'}, {'id': 2453, 'synset': 'garden_spider.n.01', 'name': 'garden_spider'}, {'id': 2454, 'synset': 'comb-footed_spider.n.01', 'name': 'comb-footed_spider'}, {'id': 2455, 'synset': 'black_widow.n.01', 'name': 'black_widow'}, {'id': 2456, 'synset': 'tarantula.n.02', 'name': 'tarantula'}, {'id': 2457, 'synset': 'wolf_spider.n.01', 'name': 'wolf_spider'}, {'id': 2458, 'synset': 'european_wolf_spider.n.01', 'name': 'European_wolf_spider'}, {'id': 2459, 'synset': 'trap-door_spider.n.01', 'name': 'trap-door_spider'}, {'id': 2460, 'synset': 'acarine.n.01', 'name': 'acarine'}, {'id': 2461, 'synset': 'tick.n.02', 'name': 'tick'}, {'id': 2462, 'synset': 'hard_tick.n.01', 'name': 'hard_tick'}, {'id': 2463, 'synset': 'ixodes_dammini.n.01', 'name': 'Ixodes_dammini'}, {'id': 2464, 'synset': 'ixodes_neotomae.n.01', 'name': 'Ixodes_neotomae'}, {'id': 2465, 'synset': 'ixodes_pacificus.n.01', 'name': 'Ixodes_pacificus'}, {'id': 2466, 'synset': 'ixodes_scapularis.n.01', 'name': 'Ixodes_scapularis'}, {'id': 2467, 'synset': 'sheep-tick.n.02', 'name': 'sheep-tick'}, {'id': 2468, 'synset': 'ixodes_persulcatus.n.01', 'name': 'Ixodes_persulcatus'}, {'id': 2469, 'synset': 'ixodes_dentatus.n.01', 'name': 'Ixodes_dentatus'}, {'id': 2470, 'synset': 'ixodes_spinipalpis.n.01', 'name': 'Ixodes_spinipalpis'}, {'id': 2471, 'synset': 'wood_tick.n.01', 'name': 'wood_tick'}, {'id': 2472, 'synset': 'soft_tick.n.01', 'name': 'soft_tick'}, {'id': 2473, 'synset': 'mite.n.02', 'name': 'mite'}, {'id': 2474, 'synset': 'web-spinning_mite.n.01', 'name': 'web-spinning_mite'}, {'id': 2475, 'synset': 'acarid.n.01', 'name': 'acarid'}, {'id': 2476, 'synset': 'trombidiid.n.01', 'name': 'trombidiid'}, {'id': 2477, 'synset': 'trombiculid.n.01', 'name': 'trombiculid'}, {'id': 2478, 'synset': 'harvest_mite.n.01', 'name': 'harvest_mite'}, {'id': 2479, 'synset': 'acarus.n.01', 'name': 'acarus'}, {'id': 2480, 'synset': 'itch_mite.n.01', 'name': 'itch_mite'}, {'id': 2481, 'synset': 'rust_mite.n.01', 'name': 'rust_mite'}, {'id': 2482, 'synset': 'spider_mite.n.01', 'name': 'spider_mite'}, {'id': 2483, 'synset': 'red_spider.n.01', 'name': 'red_spider'}, {'id': 2484, 'synset': 'myriapod.n.01', 'name': 'myriapod'}, {'id': 2485, 'synset': 'garden_centipede.n.01', 'name': 'garden_centipede'}, {'id': 2486, 'synset': 'tardigrade.n.01', 'name': 'tardigrade'}, {'id': 2487, 'synset': 'centipede.n.01', 'name': 'centipede'}, {'id': 2488, 'synset': 'house_centipede.n.01', 'name': 'house_centipede'}, {'id': 2489, 'synset': 'millipede.n.01', 'name': 'millipede'}, {'id': 2490, 'synset': 'sea_spider.n.01', 'name': 'sea_spider'}, {'id': 2491, 'synset': 'merostomata.n.01', 'name': 'Merostomata'}, {'id': 2492, 'synset': 'horseshoe_crab.n.01', 'name': 'horseshoe_crab'}, {'id': 2493, 'synset': 'asian_horseshoe_crab.n.01', 'name': 'Asian_horseshoe_crab'}, {'id': 2494, 'synset': 'eurypterid.n.01', 'name': 'eurypterid'}, {'id': 2495, 'synset': 'tongue_worm.n.01', 'name': 'tongue_worm'}, {'id': 2496, 'synset': 'gallinaceous_bird.n.01', 'name': 'gallinaceous_bird'}, {'id': 2497, 'synset': 'domestic_fowl.n.01', 'name': 'domestic_fowl'}, {'id': 2498, 'synset': 'dorking.n.01', 'name': 'Dorking'}, {'id': 2499, 'synset': 'plymouth_rock.n.02', 'name': 'Plymouth_Rock'}, {'id': 2500, 'synset': 'cornish.n.02', 'name': 'Cornish'}, {'id': 2501, 'synset': 'rock_cornish.n.01', 'name': 'Rock_Cornish'}, {'id': 2502, 'synset': 'game_fowl.n.01', 'name': 'game_fowl'}, {'id': 2503, 'synset': 'cochin.n.01', 'name': 'cochin'}, {'id': 2504, 'synset': 'jungle_fowl.n.01', 'name': 'jungle_fowl'}, {'id': 2505, 'synset': 'jungle_cock.n.01', 'name': 'jungle_cock'}, {'id': 2506, 'synset': 'jungle_hen.n.01', 'name': 'jungle_hen'}, {'id': 2507, 'synset': 'red_jungle_fowl.n.01', 'name': 'red_jungle_fowl'}, {'id': 2508, 'synset': 'bantam.n.01', 'name': 'bantam'}, {'id': 2509, 'synset': 'chick.n.01', 'name': 'chick'}, {'id': 2510, 'synset': 'cockerel.n.01', 'name': 'cockerel'}, {'id': 2511, 'synset': 'capon.n.02', 'name': 'capon'}, {'id': 2512, 'synset': 'hen.n.01', 'name': 'hen'}, {'id': 2513, 'synset': 'cackler.n.01', 'name': 'cackler'}, {'id': 2514, 'synset': 'brood_hen.n.01', 'name': 'brood_hen'}, {'id': 2515, 'synset': 'mother_hen.n.02', 'name': 'mother_hen'}, {'id': 2516, 'synset': 'layer.n.04', 'name': 'layer'}, {'id': 2517, 'synset': 'pullet.n.02', 'name': 'pullet'}, {'id': 2518, 'synset': 'spring_chicken.n.02', 'name': 'spring_chicken'}, {'id': 2519, 'synset': 'rhode_island_red.n.01', 'name': 'Rhode_Island_red'}, {'id': 2520, 'synset': 'dominique.n.01', 'name': 'Dominique'}, {'id': 2521, 'synset': 'orpington.n.01', 'name': 'Orpington'}, {'id': 2522, 'synset': 'turkey.n.01', 'name': 'turkey'}, {'id': 2523, 'synset': 'turkey_cock.n.01', 'name': 'turkey_cock'}, {'id': 2524, 'synset': 'ocellated_turkey.n.01', 'name': 'ocellated_turkey'}, {'id': 2525, 'synset': 'grouse.n.02', 'name': 'grouse'}, {'id': 2526, 'synset': 'black_grouse.n.01', 'name': 'black_grouse'}, {'id': 2527, 'synset': 'european_black_grouse.n.01', 'name': 'European_black_grouse'}, {'id': 2528, 'synset': 'asian_black_grouse.n.01', 'name': 'Asian_black_grouse'}, {'id': 2529, 'synset': 'blackcock.n.01', 'name': 'blackcock'}, {'id': 2530, 'synset': 'greyhen.n.01', 'name': 'greyhen'}, {'id': 2531, 'synset': 'ptarmigan.n.01', 'name': 'ptarmigan'}, {'id': 2532, 'synset': 'red_grouse.n.01', 'name': 'red_grouse'}, {'id': 2533, 'synset': 'moorhen.n.02', 'name': 'moorhen'}, {'id': 2534, 'synset': 'capercaillie.n.01', 'name': 'capercaillie'}, {'id': 2535, 'synset': 'spruce_grouse.n.01', 'name': 'spruce_grouse'}, {'id': 2536, 'synset': 'sage_grouse.n.01', 'name': 'sage_grouse'}, {'id': 2537, 'synset': 'ruffed_grouse.n.01', 'name': 'ruffed_grouse'}, {'id': 2538, 'synset': 'sharp-tailed_grouse.n.01', 'name': 'sharp-tailed_grouse'}, {'id': 2539, 'synset': 'prairie_chicken.n.01', 'name': 'prairie_chicken'}, {'id': 2540, 'synset': 'greater_prairie_chicken.n.01', 'name': 'greater_prairie_chicken'}, {'id': 2541, 'synset': 'lesser_prairie_chicken.n.01', 'name': 'lesser_prairie_chicken'}, {'id': 2542, 'synset': 'heath_hen.n.01', 'name': 'heath_hen'}, {'id': 2543, 'synset': 'guan.n.01', 'name': 'guan'}, {'id': 2544, 'synset': 'curassow.n.01', 'name': 'curassow'}, {'id': 2545, 'synset': 'piping_guan.n.01', 'name': 'piping_guan'}, {'id': 2546, 'synset': 'chachalaca.n.01', 'name': 'chachalaca'}, {'id': 2547, 'synset': 'texas_chachalaca.n.01', 'name': 'Texas_chachalaca'}, {'id': 2548, 'synset': 'megapode.n.01', 'name': 'megapode'}, {'id': 2549, 'synset': 'mallee_fowl.n.01', 'name': 'mallee_fowl'}, {'id': 2550, 'synset': 'mallee_hen.n.01', 'name': 'mallee_hen'}, {'id': 2551, 'synset': 'brush_turkey.n.01', 'name': 'brush_turkey'}, {'id': 2552, 'synset': 'maleo.n.01', 'name': 'maleo'}, {'id': 2553, 'synset': 'phasianid.n.01', 'name': 'phasianid'}, {'id': 2554, 'synset': 'pheasant.n.01', 'name': 'pheasant'}, {'id': 2555, 'synset': 'ring-necked_pheasant.n.01', 'name': 'ring-necked_pheasant'}, {'id': 2556, 'synset': 'afropavo.n.01', 'name': 'afropavo'}, {'id': 2557, 'synset': 'argus.n.02', 'name': 'argus'}, {'id': 2558, 'synset': 'golden_pheasant.n.01', 'name': 'golden_pheasant'}, {'id': 2559, 'synset': 'bobwhite.n.01', 'name': 'bobwhite'}, {'id': 2560, 'synset': 'northern_bobwhite.n.01', 'name': 'northern_bobwhite'}, {'id': 2561, 'synset': 'old_world_quail.n.01', 'name': 'Old_World_quail'}, {'id': 2562, 'synset': 'migratory_quail.n.01', 'name': 'migratory_quail'}, {'id': 2563, 'synset': 'monal.n.01', 'name': 'monal'}, {'id': 2564, 'synset': 'peafowl.n.01', 'name': 'peafowl'}, {'id': 2565, 'synset': 'peachick.n.01', 'name': 'peachick'}, {'id': 2566, 'synset': 'peacock.n.02', 'name': 'peacock'}, {'id': 2567, 'synset': 'peahen.n.01', 'name': 'peahen'}, {'id': 2568, 'synset': 'blue_peafowl.n.01', 'name': 'blue_peafowl'}, {'id': 2569, 'synset': 'green_peafowl.n.01', 'name': 'green_peafowl'}, {'id': 2570, 'synset': 'quail.n.02', 'name': 'quail'}, {'id': 2571, 'synset': 'california_quail.n.01', 'name': 'California_quail'}, {'id': 2572, 'synset': 'tragopan.n.01', 'name': 'tragopan'}, {'id': 2573, 'synset': 'partridge.n.03', 'name': 'partridge'}, {'id': 2574, 'synset': 'hungarian_partridge.n.01', 'name': 'Hungarian_partridge'}, {'id': 2575, 'synset': 'red-legged_partridge.n.01', 'name': 'red-legged_partridge'}, {'id': 2576, 'synset': 'greek_partridge.n.01', 'name': 'Greek_partridge'}, {'id': 2577, 'synset': 'mountain_quail.n.01', 'name': 'mountain_quail'}, {'id': 2578, 'synset': 'guinea_fowl.n.01', 'name': 'guinea_fowl'}, {'id': 2579, 'synset': 'guinea_hen.n.02', 'name': 'guinea_hen'}, {'id': 2580, 'synset': 'hoatzin.n.01', 'name': 'hoatzin'}, {'id': 2581, 'synset': 'tinamou.n.01', 'name': 'tinamou'}, {'id': 2582, 'synset': 'columbiform_bird.n.01', 'name': 'columbiform_bird'}, {'id': 2583, 'synset': 'dodo.n.02', 'name': 'dodo'}, {'id': 2584, 'synset': 'pouter_pigeon.n.01', 'name': 'pouter_pigeon'}, {'id': 2585, 'synset': 'rock_dove.n.01', 'name': 'rock_dove'}, {'id': 2586, 'synset': 'band-tailed_pigeon.n.01', 'name': 'band-tailed_pigeon'}, {'id': 2587, 'synset': 'wood_pigeon.n.01', 'name': 'wood_pigeon'}, {'id': 2588, 'synset': 'turtledove.n.02', 'name': 'turtledove'}, {'id': 2589, 'synset': 'streptopelia_turtur.n.01', 'name': 'Streptopelia_turtur'}, {'id': 2590, 'synset': 'ringdove.n.01', 'name': 'ringdove'}, {'id': 2591, 'synset': 'australian_turtledove.n.01', 'name': 'Australian_turtledove'}, {'id': 2592, 'synset': 'mourning_dove.n.01', 'name': 'mourning_dove'}, {'id': 2593, 'synset': 'domestic_pigeon.n.01', 'name': 'domestic_pigeon'}, {'id': 2594, 'synset': 'squab.n.03', 'name': 'squab'}, {'id': 2595, 'synset': 'fairy_swallow.n.01', 'name': 'fairy_swallow'}, {'id': 2596, 'synset': 'roller.n.07', 'name': 'roller'}, {'id': 2597, 'synset': 'homing_pigeon.n.01', 'name': 'homing_pigeon'}, {'id': 2598, 'synset': 'carrier_pigeon.n.01', 'name': 'carrier_pigeon'}, {'id': 2599, 'synset': 'passenger_pigeon.n.01', 'name': 'passenger_pigeon'}, {'id': 2600, 'synset': 'sandgrouse.n.01', 'name': 'sandgrouse'}, {'id': 2601, 'synset': 'painted_sandgrouse.n.01', 'name': 'painted_sandgrouse'}, {'id': 2602, 'synset': 'pin-tailed_sandgrouse.n.01', 'name': 'pin-tailed_sandgrouse'}, {'id': 2603, 'synset': "pallas's_sandgrouse.n.01", 'name': "pallas's_sandgrouse"}, {'id': 2604, 'synset': 'popinjay.n.02', 'name': 'popinjay'}, {'id': 2605, 'synset': 'poll.n.04', 'name': 'poll'}, {'id': 2606, 'synset': 'african_grey.n.01', 'name': 'African_grey'}, {'id': 2607, 'synset': 'amazon.n.04', 'name': 'amazon'}, {'id': 2608, 'synset': 'macaw.n.01', 'name': 'macaw'}, {'id': 2609, 'synset': 'kea.n.01', 'name': 'kea'}, {'id': 2610, 'synset': 'cockatoo.n.01', 'name': 'cockatoo'}, {'id': 2611, 'synset': 'sulphur-crested_cockatoo.n.01', 'name': 'sulphur-crested_cockatoo'}, {'id': 2612, 'synset': 'pink_cockatoo.n.01', 'name': 'pink_cockatoo'}, {'id': 2613, 'synset': 'cockateel.n.01', 'name': 'cockateel'}, {'id': 2614, 'synset': 'lovebird.n.02', 'name': 'lovebird'}, {'id': 2615, 'synset': 'lory.n.01', 'name': 'lory'}, {'id': 2616, 'synset': 'lorikeet.n.01', 'name': 'lorikeet'}, {'id': 2617, 'synset': 'varied_lorikeet.n.01', 'name': 'varied_Lorikeet'}, {'id': 2618, 'synset': 'rainbow_lorikeet.n.01', 'name': 'rainbow_lorikeet'}, {'id': 2619, 'synset': 'carolina_parakeet.n.01', 'name': 'Carolina_parakeet'}, {'id': 2620, 'synset': 'budgerigar.n.01', 'name': 'budgerigar'}, {'id': 2621, 'synset': 'ring-necked_parakeet.n.01', 'name': 'ring-necked_parakeet'}, {'id': 2622, 'synset': 'cuculiform_bird.n.01', 'name': 'cuculiform_bird'}, {'id': 2623, 'synset': 'cuckoo.n.02', 'name': 'cuckoo'}, {'id': 2624, 'synset': 'european_cuckoo.n.01', 'name': 'European_cuckoo'}, {'id': 2625, 'synset': 'black-billed_cuckoo.n.01', 'name': 'black-billed_cuckoo'}, {'id': 2626, 'synset': 'roadrunner.n.01', 'name': 'roadrunner'}, {'id': 2627, 'synset': 'ani.n.01', 'name': 'ani'}, {'id': 2628, 'synset': 'coucal.n.01', 'name': 'coucal'}, {'id': 2629, 'synset': 'crow_pheasant.n.01', 'name': 'crow_pheasant'}, {'id': 2630, 'synset': 'touraco.n.01', 'name': 'touraco'}, {'id': 2631, 'synset': 'coraciiform_bird.n.01', 'name': 'coraciiform_bird'}, {'id': 2632, 'synset': 'roller.n.06', 'name': 'roller'}, {'id': 2633, 'synset': 'european_roller.n.01', 'name': 'European_roller'}, {'id': 2634, 'synset': 'ground_roller.n.01', 'name': 'ground_roller'}, {'id': 2635, 'synset': 'kingfisher.n.01', 'name': 'kingfisher'}, {'id': 2636, 'synset': 'eurasian_kingfisher.n.01', 'name': 'Eurasian_kingfisher'}, {'id': 2637, 'synset': 'belted_kingfisher.n.01', 'name': 'belted_kingfisher'}, {'id': 2638, 'synset': 'kookaburra.n.01', 'name': 'kookaburra'}, {'id': 2639, 'synset': 'bee_eater.n.01', 'name': 'bee_eater'}, {'id': 2640, 'synset': 'hornbill.n.01', 'name': 'hornbill'}, {'id': 2641, 'synset': 'hoopoe.n.01', 'name': 'hoopoe'}, {'id': 2642, 'synset': 'euopean_hoopoe.n.01', 'name': 'Euopean_hoopoe'}, {'id': 2643, 'synset': 'wood_hoopoe.n.01', 'name': 'wood_hoopoe'}, {'id': 2644, 'synset': 'motmot.n.01', 'name': 'motmot'}, {'id': 2645, 'synset': 'tody.n.01', 'name': 'tody'}, {'id': 2646, 'synset': 'apodiform_bird.n.01', 'name': 'apodiform_bird'}, {'id': 2647, 'synset': 'swift.n.03', 'name': 'swift'}, {'id': 2648, 'synset': 'european_swift.n.01', 'name': 'European_swift'}, {'id': 2649, 'synset': 'chimney_swift.n.01', 'name': 'chimney_swift'}, {'id': 2650, 'synset': 'swiftlet.n.01', 'name': 'swiftlet'}, {'id': 2651, 'synset': 'tree_swift.n.01', 'name': 'tree_swift'}, {'id': 2652, 'synset': 'archilochus_colubris.n.01', 'name': 'Archilochus_colubris'}, {'id': 2653, 'synset': 'thornbill.n.01', 'name': 'thornbill'}, {'id': 2654, 'synset': 'goatsucker.n.01', 'name': 'goatsucker'}, {'id': 2655, 'synset': 'european_goatsucker.n.01', 'name': 'European_goatsucker'}, {'id': 2656, 'synset': "chuck-will's-widow.n.01", 'name': "chuck-will's-widow"}, {'id': 2657, 'synset': 'whippoorwill.n.01', 'name': 'whippoorwill'}, {'id': 2658, 'synset': 'poorwill.n.01', 'name': 'poorwill'}, {'id': 2659, 'synset': 'frogmouth.n.01', 'name': 'frogmouth'}, {'id': 2660, 'synset': 'oilbird.n.01', 'name': 'oilbird'}, {'id': 2661, 'synset': 'piciform_bird.n.01', 'name': 'piciform_bird'}, {'id': 2662, 'synset': 'woodpecker.n.01', 'name': 'woodpecker'}, {'id': 2663, 'synset': 'green_woodpecker.n.01', 'name': 'green_woodpecker'}, {'id': 2664, 'synset': 'downy_woodpecker.n.01', 'name': 'downy_woodpecker'}, {'id': 2665, 'synset': 'flicker.n.02', 'name': 'flicker'}, {'id': 2666, 'synset': 'yellow-shafted_flicker.n.01', 'name': 'yellow-shafted_flicker'}, {'id': 2667, 'synset': 'gilded_flicker.n.01', 'name': 'gilded_flicker'}, {'id': 2668, 'synset': 'red-shafted_flicker.n.01', 'name': 'red-shafted_flicker'}, {'id': 2669, 'synset': 'ivorybill.n.01', 'name': 'ivorybill'}, {'id': 2670, 'synset': 'redheaded_woodpecker.n.01', 'name': 'redheaded_woodpecker'}, {'id': 2671, 'synset': 'sapsucker.n.01', 'name': 'sapsucker'}, {'id': 2672, 'synset': 'yellow-bellied_sapsucker.n.01', 'name': 'yellow-bellied_sapsucker'}, {'id': 2673, 'synset': 'red-breasted_sapsucker.n.01', 'name': 'red-breasted_sapsucker'}, {'id': 2674, 'synset': 'wryneck.n.02', 'name': 'wryneck'}, {'id': 2675, 'synset': 'piculet.n.01', 'name': 'piculet'}, {'id': 2676, 'synset': 'barbet.n.01', 'name': 'barbet'}, {'id': 2677, 'synset': 'puffbird.n.01', 'name': 'puffbird'}, {'id': 2678, 'synset': 'honey_guide.n.01', 'name': 'honey_guide'}, {'id': 2679, 'synset': 'jacamar.n.01', 'name': 'jacamar'}, {'id': 2680, 'synset': 'toucan.n.01', 'name': 'toucan'}, {'id': 2681, 'synset': 'toucanet.n.01', 'name': 'toucanet'}, {'id': 2682, 'synset': 'trogon.n.01', 'name': 'trogon'}, {'id': 2683, 'synset': 'quetzal.n.02', 'name': 'quetzal'}, {'id': 2684, 'synset': 'resplendent_quetzel.n.01', 'name': 'resplendent_quetzel'}, {'id': 2685, 'synset': 'aquatic_bird.n.01', 'name': 'aquatic_bird'}, {'id': 2686, 'synset': 'waterfowl.n.01', 'name': 'waterfowl'}, {'id': 2687, 'synset': 'anseriform_bird.n.01', 'name': 'anseriform_bird'}, {'id': 2688, 'synset': 'drake.n.02', 'name': 'drake'}, {'id': 2689, 'synset': 'quack-quack.n.01', 'name': 'quack-quack'}, {'id': 2690, 'synset': 'diving_duck.n.01', 'name': 'diving_duck'}, {'id': 2691, 'synset': 'dabbling_duck.n.01', 'name': 'dabbling_duck'}, {'id': 2692, 'synset': 'black_duck.n.01', 'name': 'black_duck'}, {'id': 2693, 'synset': 'teal.n.02', 'name': 'teal'}, {'id': 2694, 'synset': 'greenwing.n.01', 'name': 'greenwing'}, {'id': 2695, 'synset': 'bluewing.n.01', 'name': 'bluewing'}, {'id': 2696, 'synset': 'garganey.n.01', 'name': 'garganey'}, {'id': 2697, 'synset': 'widgeon.n.01', 'name': 'widgeon'}, {'id': 2698, 'synset': 'american_widgeon.n.01', 'name': 'American_widgeon'}, {'id': 2699, 'synset': 'shoveler.n.02', 'name': 'shoveler'}, {'id': 2700, 'synset': 'pintail.n.01', 'name': 'pintail'}, {'id': 2701, 'synset': 'sheldrake.n.02', 'name': 'sheldrake'}, {'id': 2702, 'synset': 'shelduck.n.01', 'name': 'shelduck'}, {'id': 2703, 'synset': 'ruddy_duck.n.01', 'name': 'ruddy_duck'}, {'id': 2704, 'synset': 'bufflehead.n.01', 'name': 'bufflehead'}, {'id': 2705, 'synset': 'goldeneye.n.02', 'name': 'goldeneye'}, {'id': 2706, 'synset': "barrow's_goldeneye.n.01", 'name': "Barrow's_goldeneye"}, {'id': 2707, 'synset': 'canvasback.n.01', 'name': 'canvasback'}, {'id': 2708, 'synset': 'pochard.n.01', 'name': 'pochard'}, {'id': 2709, 'synset': 'redhead.n.02', 'name': 'redhead'}, {'id': 2710, 'synset': 'scaup.n.01', 'name': 'scaup'}, {'id': 2711, 'synset': 'greater_scaup.n.01', 'name': 'greater_scaup'}, {'id': 2712, 'synset': 'lesser_scaup.n.01', 'name': 'lesser_scaup'}, {'id': 2713, 'synset': 'wild_duck.n.01', 'name': 'wild_duck'}, {'id': 2714, 'synset': 'wood_duck.n.01', 'name': 'wood_duck'}, {'id': 2715, 'synset': 'wood_drake.n.01', 'name': 'wood_drake'}, {'id': 2716, 'synset': 'mandarin_duck.n.01', 'name': 'mandarin_duck'}, {'id': 2717, 'synset': 'muscovy_duck.n.01', 'name': 'muscovy_duck'}, {'id': 2718, 'synset': 'sea_duck.n.01', 'name': 'sea_duck'}, {'id': 2719, 'synset': 'eider.n.01', 'name': 'eider'}, {'id': 2720, 'synset': 'scoter.n.01', 'name': 'scoter'}, {'id': 2721, 'synset': 'common_scoter.n.01', 'name': 'common_scoter'}, {'id': 2722, 'synset': 'old_squaw.n.01', 'name': 'old_squaw'}, {'id': 2723, 'synset': 'merganser.n.01', 'name': 'merganser'}, {'id': 2724, 'synset': 'goosander.n.01', 'name': 'goosander'}, {'id': 2725, 'synset': 'american_merganser.n.01', 'name': 'American_merganser'}, {'id': 2726, 'synset': 'red-breasted_merganser.n.01', 'name': 'red-breasted_merganser'}, {'id': 2727, 'synset': 'smew.n.01', 'name': 'smew'}, {'id': 2728, 'synset': 'hooded_merganser.n.01', 'name': 'hooded_merganser'}, {'id': 2729, 'synset': 'gosling.n.01', 'name': 'gosling'}, {'id': 2730, 'synset': 'gander.n.01', 'name': 'gander'}, {'id': 2731, 'synset': 'chinese_goose.n.01', 'name': 'Chinese_goose'}, {'id': 2732, 'synset': 'greylag.n.01', 'name': 'greylag'}, {'id': 2733, 'synset': 'blue_goose.n.01', 'name': 'blue_goose'}, {'id': 2734, 'synset': 'snow_goose.n.01', 'name': 'snow_goose'}, {'id': 2735, 'synset': 'brant.n.01', 'name': 'brant'}, {'id': 2736, 'synset': 'common_brant_goose.n.01', 'name': 'common_brant_goose'}, {'id': 2737, 'synset': 'honker.n.03', 'name': 'honker'}, {'id': 2738, 'synset': 'barnacle_goose.n.01', 'name': 'barnacle_goose'}, {'id': 2739, 'synset': 'coscoroba.n.01', 'name': 'coscoroba'}, {'id': 2740, 'synset': 'swan.n.01', 'name': 'swan'}, {'id': 2741, 'synset': 'cob.n.04', 'name': 'cob'}, {'id': 2742, 'synset': 'pen.n.05', 'name': 'pen'}, {'id': 2743, 'synset': 'cygnet.n.01', 'name': 'cygnet'}, {'id': 2744, 'synset': 'mute_swan.n.01', 'name': 'mute_swan'}, {'id': 2745, 'synset': 'whooper.n.02', 'name': 'whooper'}, {'id': 2746, 'synset': 'tundra_swan.n.01', 'name': 'tundra_swan'}, {'id': 2747, 'synset': 'whistling_swan.n.01', 'name': 'whistling_swan'}, {'id': 2748, 'synset': "bewick's_swan.n.01", 'name': "Bewick's_swan"}, {'id': 2749, 'synset': 'trumpeter.n.04', 'name': 'trumpeter'}, {'id': 2750, 'synset': 'black_swan.n.01', 'name': 'black_swan'}, {'id': 2751, 'synset': 'screamer.n.03', 'name': 'screamer'}, {'id': 2752, 'synset': 'horned_screamer.n.01', 'name': 'horned_screamer'}, {'id': 2753, 'synset': 'crested_screamer.n.01', 'name': 'crested_screamer'}, {'id': 2754, 'synset': 'chaja.n.01', 'name': 'chaja'}, {'id': 2755, 'synset': 'mammal.n.01', 'name': 'mammal'}, {'id': 2756, 'synset': 'female_mammal.n.01', 'name': 'female_mammal'}, {'id': 2757, 'synset': 'tusker.n.01', 'name': 'tusker'}, {'id': 2758, 'synset': 'prototherian.n.01', 'name': 'prototherian'}, {'id': 2759, 'synset': 'monotreme.n.01', 'name': 'monotreme'}, {'id': 2760, 'synset': 'echidna.n.02', 'name': 'echidna'}, {'id': 2761, 'synset': 'echidna.n.01', 'name': 'echidna'}, {'id': 2762, 'synset': 'platypus.n.01', 'name': 'platypus'}, {'id': 2763, 'synset': 'marsupial.n.01', 'name': 'marsupial'}, {'id': 2764, 'synset': 'opossum.n.02', 'name': 'opossum'}, {'id': 2765, 'synset': 'common_opossum.n.01', 'name': 'common_opossum'}, {'id': 2766, 'synset': 'crab-eating_opossum.n.01', 'name': 'crab-eating_opossum'}, {'id': 2767, 'synset': 'opossum_rat.n.01', 'name': 'opossum_rat'}, {'id': 2768, 'synset': 'bandicoot.n.01', 'name': 'bandicoot'}, {'id': 2769, 'synset': 'rabbit-eared_bandicoot.n.01', 'name': 'rabbit-eared_bandicoot'}, {'id': 2770, 'synset': 'kangaroo.n.01', 'name': 'kangaroo'}, {'id': 2771, 'synset': 'giant_kangaroo.n.01', 'name': 'giant_kangaroo'}, {'id': 2772, 'synset': 'wallaby.n.01', 'name': 'wallaby'}, {'id': 2773, 'synset': 'common_wallaby.n.01', 'name': 'common_wallaby'}, {'id': 2774, 'synset': 'hare_wallaby.n.01', 'name': 'hare_wallaby'}, {'id': 2775, 'synset': 'nail-tailed_wallaby.n.01', 'name': 'nail-tailed_wallaby'}, {'id': 2776, 'synset': 'rock_wallaby.n.01', 'name': 'rock_wallaby'}, {'id': 2777, 'synset': 'pademelon.n.01', 'name': 'pademelon'}, {'id': 2778, 'synset': 'tree_wallaby.n.01', 'name': 'tree_wallaby'}, {'id': 2779, 'synset': 'musk_kangaroo.n.01', 'name': 'musk_kangaroo'}, {'id': 2780, 'synset': 'rat_kangaroo.n.01', 'name': 'rat_kangaroo'}, {'id': 2781, 'synset': 'potoroo.n.01', 'name': 'potoroo'}, {'id': 2782, 'synset': 'bettong.n.01', 'name': 'bettong'}, {'id': 2783, 'synset': 'jerboa_kangaroo.n.01', 'name': 'jerboa_kangaroo'}, {'id': 2784, 'synset': 'phalanger.n.01', 'name': 'phalanger'}, {'id': 2785, 'synset': 'cuscus.n.01', 'name': 'cuscus'}, {'id': 2786, 'synset': 'brush-tailed_phalanger.n.01', 'name': 'brush-tailed_phalanger'}, {'id': 2787, 'synset': 'flying_phalanger.n.01', 'name': 'flying_phalanger'}, {'id': 2788, 'synset': 'wombat.n.01', 'name': 'wombat'}, {'id': 2789, 'synset': 'dasyurid_marsupial.n.01', 'name': 'dasyurid_marsupial'}, {'id': 2790, 'synset': 'dasyure.n.01', 'name': 'dasyure'}, {'id': 2791, 'synset': 'eastern_dasyure.n.01', 'name': 'eastern_dasyure'}, {'id': 2792, 'synset': 'native_cat.n.01', 'name': 'native_cat'}, {'id': 2793, 'synset': 'thylacine.n.01', 'name': 'thylacine'}, {'id': 2794, 'synset': 'tasmanian_devil.n.01', 'name': 'Tasmanian_devil'}, {'id': 2795, 'synset': 'pouched_mouse.n.01', 'name': 'pouched_mouse'}, {'id': 2796, 'synset': 'numbat.n.01', 'name': 'numbat'}, {'id': 2797, 'synset': 'pouched_mole.n.01', 'name': 'pouched_mole'}, {'id': 2798, 'synset': 'placental.n.01', 'name': 'placental'}, {'id': 2799, 'synset': 'livestock.n.01', 'name': 'livestock'}, {'id': 2800, 'synset': 'cow.n.02', 'name': 'cow'}, {'id': 2801, 'synset': 'calf.n.04', 'name': 'calf'}, {'id': 2802, 'synset': 'yearling.n.03', 'name': 'yearling'}, {'id': 2803, 'synset': 'buck.n.05', 'name': 'buck'}, {'id': 2804, 'synset': 'doe.n.02', 'name': 'doe'}, {'id': 2805, 'synset': 'insectivore.n.01', 'name': 'insectivore'}, {'id': 2806, 'synset': 'mole.n.06', 'name': 'mole'}, {'id': 2807, 'synset': 'starnose_mole.n.01', 'name': 'starnose_mole'}, {'id': 2808, 'synset': "brewer's_mole.n.01", 'name': "brewer's_mole"}, {'id': 2809, 'synset': 'golden_mole.n.01', 'name': 'golden_mole'}, {'id': 2810, 'synset': 'shrew_mole.n.01', 'name': 'shrew_mole'}, {'id': 2811, 'synset': 'asiatic_shrew_mole.n.01', 'name': 'Asiatic_shrew_mole'}, {'id': 2812, 'synset': 'american_shrew_mole.n.01', 'name': 'American_shrew_mole'}, {'id': 2813, 'synset': 'shrew.n.02', 'name': 'shrew'}, {'id': 2814, 'synset': 'common_shrew.n.01', 'name': 'common_shrew'}, {'id': 2815, 'synset': 'masked_shrew.n.01', 'name': 'masked_shrew'}, {'id': 2816, 'synset': 'short-tailed_shrew.n.01', 'name': 'short-tailed_shrew'}, {'id': 2817, 'synset': 'water_shrew.n.01', 'name': 'water_shrew'}, {'id': 2818, 'synset': 'american_water_shrew.n.01', 'name': 'American_water_shrew'}, {'id': 2819, 'synset': 'european_water_shrew.n.01', 'name': 'European_water_shrew'}, {'id': 2820, 'synset': 'mediterranean_water_shrew.n.01', 'name': 'Mediterranean_water_shrew'}, {'id': 2821, 'synset': 'least_shrew.n.01', 'name': 'least_shrew'}, {'id': 2822, 'synset': 'hedgehog.n.02', 'name': 'hedgehog'}, {'id': 2823, 'synset': 'tenrec.n.01', 'name': 'tenrec'}, {'id': 2824, 'synset': 'tailless_tenrec.n.01', 'name': 'tailless_tenrec'}, {'id': 2825, 'synset': 'otter_shrew.n.01', 'name': 'otter_shrew'}, {'id': 2826, 'synset': 'eiderdown.n.02', 'name': 'eiderdown'}, {'id': 2827, 'synset': 'aftershaft.n.01', 'name': 'aftershaft'}, {'id': 2828, 'synset': 'sickle_feather.n.01', 'name': 'sickle_feather'}, {'id': 2829, 'synset': 'contour_feather.n.01', 'name': 'contour_feather'}, {'id': 2830, 'synset': 'bastard_wing.n.01', 'name': 'bastard_wing'}, {'id': 2831, 'synset': 'saddle_hackle.n.01', 'name': 'saddle_hackle'}, {'id': 2832, 'synset': 'encolure.n.01', 'name': 'encolure'}, {'id': 2833, 'synset': 'hair.n.06', 'name': 'hair'}, {'id': 2834, 'synset': 'squama.n.01', 'name': 'squama'}, {'id': 2835, 'synset': 'scute.n.01', 'name': 'scute'}, {'id': 2836, 'synset': 'sclerite.n.01', 'name': 'sclerite'}, {'id': 2837, 'synset': 'plastron.n.05', 'name': 'plastron'}, {'id': 2838, 'synset': 'scallop_shell.n.01', 'name': 'scallop_shell'}, {'id': 2839, 'synset': 'oyster_shell.n.01', 'name': 'oyster_shell'}, {'id': 2840, 'synset': 'theca.n.02', 'name': 'theca'}, {'id': 2841, 'synset': 'invertebrate.n.01', 'name': 'invertebrate'}, {'id': 2842, 'synset': 'sponge.n.04', 'name': 'sponge'}, {'id': 2843, 'synset': 'choanocyte.n.01', 'name': 'choanocyte'}, {'id': 2844, 'synset': 'glass_sponge.n.01', 'name': 'glass_sponge'}, {'id': 2845, 'synset': "venus's_flower_basket.n.01", 'name': "Venus's_flower_basket"}, {'id': 2846, 'synset': 'metazoan.n.01', 'name': 'metazoan'}, {'id': 2847, 'synset': 'coelenterate.n.01', 'name': 'coelenterate'}, {'id': 2848, 'synset': 'planula.n.01', 'name': 'planula'}, {'id': 2849, 'synset': 'polyp.n.02', 'name': 'polyp'}, {'id': 2850, 'synset': 'medusa.n.02', 'name': 'medusa'}, {'id': 2851, 'synset': 'jellyfish.n.02', 'name': 'jellyfish'}, {'id': 2852, 'synset': 'scyphozoan.n.01', 'name': 'scyphozoan'}, {'id': 2853, 'synset': 'chrysaora_quinquecirrha.n.01', 'name': 'Chrysaora_quinquecirrha'}, {'id': 2854, 'synset': 'hydrozoan.n.01', 'name': 'hydrozoan'}, {'id': 2855, 'synset': 'hydra.n.04', 'name': 'hydra'}, {'id': 2856, 'synset': 'siphonophore.n.01', 'name': 'siphonophore'}, {'id': 2857, 'synset': 'nanomia.n.01', 'name': 'nanomia'}, {'id': 2858, 'synset': 'portuguese_man-of-war.n.01', 'name': 'Portuguese_man-of-war'}, {'id': 2859, 'synset': 'praya.n.01', 'name': 'praya'}, {'id': 2860, 'synset': 'apolemia.n.01', 'name': 'apolemia'}, {'id': 2861, 'synset': 'anthozoan.n.01', 'name': 'anthozoan'}, {'id': 2862, 'synset': 'sea_anemone.n.01', 'name': 'sea_anemone'}, {'id': 2863, 'synset': 'actinia.n.02', 'name': 'actinia'}, {'id': 2864, 'synset': 'sea_pen.n.01', 'name': 'sea_pen'}, {'id': 2865, 'synset': 'coral.n.04', 'name': 'coral'}, {'id': 2866, 'synset': 'gorgonian.n.01', 'name': 'gorgonian'}, {'id': 2867, 'synset': 'sea_feather.n.01', 'name': 'sea_feather'}, {'id': 2868, 'synset': 'sea_fan.n.01', 'name': 'sea_fan'}, {'id': 2869, 'synset': 'red_coral.n.02', 'name': 'red_coral'}, {'id': 2870, 'synset': 'stony_coral.n.01', 'name': 'stony_coral'}, {'id': 2871, 'synset': 'brain_coral.n.01', 'name': 'brain_coral'}, {'id': 2872, 'synset': 'staghorn_coral.n.01', 'name': 'staghorn_coral'}, {'id': 2873, 'synset': 'mushroom_coral.n.01', 'name': 'mushroom_coral'}, {'id': 2874, 'synset': 'ctenophore.n.01', 'name': 'ctenophore'}, {'id': 2875, 'synset': 'beroe.n.01', 'name': 'beroe'}, {'id': 2876, 'synset': 'platyctenean.n.01', 'name': 'platyctenean'}, {'id': 2877, 'synset': 'sea_gooseberry.n.01', 'name': 'sea_gooseberry'}, {'id': 2878, 'synset': "venus's_girdle.n.01", 'name': "Venus's_girdle"}, {'id': 2879, 'synset': 'worm.n.01', 'name': 'worm'}, {'id': 2880, 'synset': 'helminth.n.01', 'name': 'helminth'}, {'id': 2881, 'synset': 'woodworm.n.01', 'name': 'woodworm'}, {'id': 2882, 'synset': 'woodborer.n.01', 'name': 'woodborer'}, {'id': 2883, 'synset': 'acanthocephalan.n.01', 'name': 'acanthocephalan'}, {'id': 2884, 'synset': 'arrowworm.n.01', 'name': 'arrowworm'}, {'id': 2885, 'synset': 'bladder_worm.n.01', 'name': 'bladder_worm'}, {'id': 2886, 'synset': 'flatworm.n.01', 'name': 'flatworm'}, {'id': 2887, 'synset': 'planarian.n.01', 'name': 'planarian'}, {'id': 2888, 'synset': 'fluke.n.05', 'name': 'fluke'}, {'id': 2889, 'synset': 'cercaria.n.01', 'name': 'cercaria'}, {'id': 2890, 'synset': 'liver_fluke.n.01', 'name': 'liver_fluke'}, {'id': 2891, 'synset': 'fasciolopsis_buski.n.01', 'name': 'Fasciolopsis_buski'}, {'id': 2892, 'synset': 'schistosome.n.01', 'name': 'schistosome'}, {'id': 2893, 'synset': 'tapeworm.n.01', 'name': 'tapeworm'}, {'id': 2894, 'synset': 'echinococcus.n.01', 'name': 'echinococcus'}, {'id': 2895, 'synset': 'taenia.n.02', 'name': 'taenia'}, {'id': 2896, 'synset': 'ribbon_worm.n.01', 'name': 'ribbon_worm'}, {'id': 2897, 'synset': 'beard_worm.n.01', 'name': 'beard_worm'}, {'id': 2898, 'synset': 'rotifer.n.01', 'name': 'rotifer'}, {'id': 2899, 'synset': 'nematode.n.01', 'name': 'nematode'}, {'id': 2900, 'synset': 'common_roundworm.n.01', 'name': 'common_roundworm'}, {'id': 2901, 'synset': 'chicken_roundworm.n.01', 'name': 'chicken_roundworm'}, {'id': 2902, 'synset': 'pinworm.n.01', 'name': 'pinworm'}, {'id': 2903, 'synset': 'eelworm.n.01', 'name': 'eelworm'}, {'id': 2904, 'synset': 'vinegar_eel.n.01', 'name': 'vinegar_eel'}, {'id': 2905, 'synset': 'trichina.n.01', 'name': 'trichina'}, {'id': 2906, 'synset': 'hookworm.n.01', 'name': 'hookworm'}, {'id': 2907, 'synset': 'filaria.n.02', 'name': 'filaria'}, {'id': 2908, 'synset': 'guinea_worm.n.02', 'name': 'Guinea_worm'}, {'id': 2909, 'synset': 'annelid.n.01', 'name': 'annelid'}, {'id': 2910, 'synset': 'archiannelid.n.01', 'name': 'archiannelid'}, {'id': 2911, 'synset': 'oligochaete.n.01', 'name': 'oligochaete'}, {'id': 2912, 'synset': 'earthworm.n.01', 'name': 'earthworm'}, {'id': 2913, 'synset': 'polychaete.n.01', 'name': 'polychaete'}, {'id': 2914, 'synset': 'lugworm.n.01', 'name': 'lugworm'}, {'id': 2915, 'synset': 'sea_mouse.n.01', 'name': 'sea_mouse'}, {'id': 2916, 'synset': 'bloodworm.n.01', 'name': 'bloodworm'}, {'id': 2917, 'synset': 'leech.n.01', 'name': 'leech'}, {'id': 2918, 'synset': 'medicinal_leech.n.01', 'name': 'medicinal_leech'}, {'id': 2919, 'synset': 'horseleech.n.01', 'name': 'horseleech'}, {'id': 2920, 'synset': 'mollusk.n.01', 'name': 'mollusk'}, {'id': 2921, 'synset': 'scaphopod.n.01', 'name': 'scaphopod'}, {'id': 2922, 'synset': 'tooth_shell.n.01', 'name': 'tooth_shell'}, {'id': 2923, 'synset': 'gastropod.n.01', 'name': 'gastropod'}, {'id': 2924, 'synset': 'abalone.n.01', 'name': 'abalone'}, {'id': 2925, 'synset': 'ormer.n.01', 'name': 'ormer'}, {'id': 2926, 'synset': 'scorpion_shell.n.01', 'name': 'scorpion_shell'}, {'id': 2927, 'synset': 'conch.n.01', 'name': 'conch'}, {'id': 2928, 'synset': 'giant_conch.n.01', 'name': 'giant_conch'}, {'id': 2929, 'synset': 'snail.n.01', 'name': 'snail'}, {'id': 2930, 'synset': 'edible_snail.n.01', 'name': 'edible_snail'}, {'id': 2931, 'synset': 'garden_snail.n.01', 'name': 'garden_snail'}, {'id': 2932, 'synset': 'brown_snail.n.01', 'name': 'brown_snail'}, {'id': 2933, 'synset': 'helix_hortensis.n.01', 'name': 'Helix_hortensis'}, {'id': 2934, 'synset': 'slug.n.07', 'name': 'slug'}, {'id': 2935, 'synset': 'seasnail.n.02', 'name': 'seasnail'}, {'id': 2936, 'synset': 'neritid.n.01', 'name': 'neritid'}, {'id': 2937, 'synset': 'nerita.n.01', 'name': 'nerita'}, {'id': 2938, 'synset': 'bleeding_tooth.n.01', 'name': 'bleeding_tooth'}, {'id': 2939, 'synset': 'neritina.n.01', 'name': 'neritina'}, {'id': 2940, 'synset': 'whelk.n.02', 'name': 'whelk'}, {'id': 2941, 'synset': 'moon_shell.n.01', 'name': 'moon_shell'}, {'id': 2942, 'synset': 'periwinkle.n.04', 'name': 'periwinkle'}, {'id': 2943, 'synset': 'limpet.n.02', 'name': 'limpet'}, {'id': 2944, 'synset': 'common_limpet.n.01', 'name': 'common_limpet'}, {'id': 2945, 'synset': 'keyhole_limpet.n.01', 'name': 'keyhole_limpet'}, {'id': 2946, 'synset': 'river_limpet.n.01', 'name': 'river_limpet'}, {'id': 2947, 'synset': 'sea_slug.n.01', 'name': 'sea_slug'}, {'id': 2948, 'synset': 'sea_hare.n.01', 'name': 'sea_hare'}, {'id': 2949, 'synset': 'hermissenda_crassicornis.n.01', 'name': 'Hermissenda_crassicornis'}, {'id': 2950, 'synset': 'bubble_shell.n.01', 'name': 'bubble_shell'}, {'id': 2951, 'synset': 'physa.n.01', 'name': 'physa'}, {'id': 2952, 'synset': 'cowrie.n.01', 'name': 'cowrie'}, {'id': 2953, 'synset': 'money_cowrie.n.01', 'name': 'money_cowrie'}, {'id': 2954, 'synset': 'tiger_cowrie.n.01', 'name': 'tiger_cowrie'}, {'id': 2955, 'synset': 'solenogaster.n.01', 'name': 'solenogaster'}, {'id': 2956, 'synset': 'chiton.n.02', 'name': 'chiton'}, {'id': 2957, 'synset': 'bivalve.n.01', 'name': 'bivalve'}, {'id': 2958, 'synset': 'spat.n.03', 'name': 'spat'}, {'id': 2959, 'synset': 'clam.n.01', 'name': 'clam'}, {'id': 2960, 'synset': 'soft-shell_clam.n.02', 'name': 'soft-shell_clam'}, {'id': 2961, 'synset': 'quahog.n.02', 'name': 'quahog'}, {'id': 2962, 'synset': 'littleneck.n.02', 'name': 'littleneck'}, {'id': 2963, 'synset': 'cherrystone.n.02', 'name': 'cherrystone'}, {'id': 2964, 'synset': 'geoduck.n.01', 'name': 'geoduck'}, {'id': 2965, 'synset': 'razor_clam.n.01', 'name': 'razor_clam'}, {'id': 2966, 'synset': 'giant_clam.n.01', 'name': 'giant_clam'}, {'id': 2967, 'synset': 'cockle.n.02', 'name': 'cockle'}, {'id': 2968, 'synset': 'edible_cockle.n.01', 'name': 'edible_cockle'}, {'id': 2969, 'synset': 'oyster.n.01', 'name': 'oyster'}, {'id': 2970, 'synset': 'japanese_oyster.n.01', 'name': 'Japanese_oyster'}, {'id': 2971, 'synset': 'virginia_oyster.n.01', 'name': 'Virginia_oyster'}, {'id': 2972, 'synset': 'pearl_oyster.n.01', 'name': 'pearl_oyster'}, {'id': 2973, 'synset': 'saddle_oyster.n.01', 'name': 'saddle_oyster'}, {'id': 2974, 'synset': 'window_oyster.n.01', 'name': 'window_oyster'}, {'id': 2975, 'synset': 'ark_shell.n.01', 'name': 'ark_shell'}, {'id': 2976, 'synset': 'blood_clam.n.01', 'name': 'blood_clam'}, {'id': 2977, 'synset': 'mussel.n.02', 'name': 'mussel'}, {'id': 2978, 'synset': 'marine_mussel.n.01', 'name': 'marine_mussel'}, {'id': 2979, 'synset': 'edible_mussel.n.01', 'name': 'edible_mussel'}, {'id': 2980, 'synset': 'freshwater_mussel.n.01', 'name': 'freshwater_mussel'}, {'id': 2981, 'synset': 'pearly-shelled_mussel.n.01', 'name': 'pearly-shelled_mussel'}, {'id': 2982, 'synset': 'thin-shelled_mussel.n.01', 'name': 'thin-shelled_mussel'}, {'id': 2983, 'synset': 'zebra_mussel.n.01', 'name': 'zebra_mussel'}, {'id': 2984, 'synset': 'scallop.n.04', 'name': 'scallop'}, {'id': 2985, 'synset': 'bay_scallop.n.02', 'name': 'bay_scallop'}, {'id': 2986, 'synset': 'sea_scallop.n.02', 'name': 'sea_scallop'}, {'id': 2987, 'synset': 'shipworm.n.01', 'name': 'shipworm'}, {'id': 2988, 'synset': 'teredo.n.01', 'name': 'teredo'}, {'id': 2989, 'synset': 'piddock.n.01', 'name': 'piddock'}, {'id': 2990, 'synset': 'cephalopod.n.01', 'name': 'cephalopod'}, {'id': 2991, 'synset': 'chambered_nautilus.n.01', 'name': 'chambered_nautilus'}, {'id': 2992, 'synset': 'octopod.n.01', 'name': 'octopod'}, {'id': 2993, 'synset': 'paper_nautilus.n.01', 'name': 'paper_nautilus'}, {'id': 2994, 'synset': 'decapod.n.02', 'name': 'decapod'}, {'id': 2995, 'synset': 'squid.n.02', 'name': 'squid'}, {'id': 2996, 'synset': 'loligo.n.01', 'name': 'loligo'}, {'id': 2997, 'synset': 'ommastrephes.n.01', 'name': 'ommastrephes'}, {'id': 2998, 'synset': 'architeuthis.n.01', 'name': 'architeuthis'}, {'id': 2999, 'synset': 'cuttlefish.n.01', 'name': 'cuttlefish'}, {'id': 3000, 'synset': 'spirula.n.01', 'name': 'spirula'}, {'id': 3001, 'synset': 'crustacean.n.01', 'name': 'crustacean'}, {'id': 3002, 'synset': 'malacostracan_crustacean.n.01', 'name': 'malacostracan_crustacean'}, {'id': 3003, 'synset': 'decapod_crustacean.n.01', 'name': 'decapod_crustacean'}, {'id': 3004, 'synset': 'brachyuran.n.01', 'name': 'brachyuran'}, {'id': 3005, 'synset': 'stone_crab.n.02', 'name': 'stone_crab'}, {'id': 3006, 'synset': 'hard-shell_crab.n.01', 'name': 'hard-shell_crab'}, {'id': 3007, 'synset': 'soft-shell_crab.n.02', 'name': 'soft-shell_crab'}, {'id': 3008, 'synset': 'dungeness_crab.n.02', 'name': 'Dungeness_crab'}, {'id': 3009, 'synset': 'rock_crab.n.01', 'name': 'rock_crab'}, {'id': 3010, 'synset': 'jonah_crab.n.01', 'name': 'Jonah_crab'}, {'id': 3011, 'synset': 'swimming_crab.n.01', 'name': 'swimming_crab'}, {'id': 3012, 'synset': 'english_lady_crab.n.01', 'name': 'English_lady_crab'}, {'id': 3013, 'synset': 'american_lady_crab.n.01', 'name': 'American_lady_crab'}, {'id': 3014, 'synset': 'blue_crab.n.02', 'name': 'blue_crab'}, {'id': 3015, 'synset': 'fiddler_crab.n.01', 'name': 'fiddler_crab'}, {'id': 3016, 'synset': 'pea_crab.n.01', 'name': 'pea_crab'}, {'id': 3017, 'synset': 'king_crab.n.03', 'name': 'king_crab'}, {'id': 3018, 'synset': 'spider_crab.n.01', 'name': 'spider_crab'}, {'id': 3019, 'synset': 'european_spider_crab.n.01', 'name': 'European_spider_crab'}, {'id': 3020, 'synset': 'giant_crab.n.01', 'name': 'giant_crab'}, {'id': 3021, 'synset': 'lobster.n.02', 'name': 'lobster'}, {'id': 3022, 'synset': 'true_lobster.n.01', 'name': 'true_lobster'}, {'id': 3023, 'synset': 'american_lobster.n.02', 'name': 'American_lobster'}, {'id': 3024, 'synset': 'european_lobster.n.02', 'name': 'European_lobster'}, {'id': 3025, 'synset': 'cape_lobster.n.01', 'name': 'Cape_lobster'}, {'id': 3026, 'synset': 'norway_lobster.n.01', 'name': 'Norway_lobster'}, {'id': 3027, 'synset': 'crayfish.n.03', 'name': 'crayfish'}, {'id': 3028, 'synset': 'old_world_crayfish.n.01', 'name': 'Old_World_crayfish'}, {'id': 3029, 'synset': 'american_crayfish.n.01', 'name': 'American_crayfish'}, {'id': 3030, 'synset': 'hermit_crab.n.01', 'name': 'hermit_crab'}, {'id': 3031, 'synset': 'shrimp.n.03', 'name': 'shrimp'}, {'id': 3032, 'synset': 'snapping_shrimp.n.01', 'name': 'snapping_shrimp'}, {'id': 3033, 'synset': 'prawn.n.02', 'name': 'prawn'}, {'id': 3034, 'synset': 'long-clawed_prawn.n.01', 'name': 'long-clawed_prawn'}, {'id': 3035, 'synset': 'tropical_prawn.n.01', 'name': 'tropical_prawn'}, {'id': 3036, 'synset': 'krill.n.01', 'name': 'krill'}, {'id': 3037, 'synset': 'euphausia_pacifica.n.01', 'name': 'Euphausia_pacifica'}, {'id': 3038, 'synset': 'opossum_shrimp.n.01', 'name': 'opossum_shrimp'}, {'id': 3039, 'synset': 'stomatopod.n.01', 'name': 'stomatopod'}, {'id': 3040, 'synset': 'mantis_shrimp.n.01', 'name': 'mantis_shrimp'}, {'id': 3041, 'synset': 'squilla.n.01', 'name': 'squilla'}, {'id': 3042, 'synset': 'isopod.n.01', 'name': 'isopod'}, {'id': 3043, 'synset': 'woodlouse.n.01', 'name': 'woodlouse'}, {'id': 3044, 'synset': 'pill_bug.n.01', 'name': 'pill_bug'}, {'id': 3045, 'synset': 'sow_bug.n.01', 'name': 'sow_bug'}, {'id': 3046, 'synset': 'sea_louse.n.01', 'name': 'sea_louse'}, {'id': 3047, 'synset': 'amphipod.n.01', 'name': 'amphipod'}, {'id': 3048, 'synset': 'skeleton_shrimp.n.01', 'name': 'skeleton_shrimp'}, {'id': 3049, 'synset': 'whale_louse.n.01', 'name': 'whale_louse'}, {'id': 3050, 'synset': 'daphnia.n.01', 'name': 'daphnia'}, {'id': 3051, 'synset': 'fairy_shrimp.n.01', 'name': 'fairy_shrimp'}, {'id': 3052, 'synset': 'brine_shrimp.n.01', 'name': 'brine_shrimp'}, {'id': 3053, 'synset': 'tadpole_shrimp.n.01', 'name': 'tadpole_shrimp'}, {'id': 3054, 'synset': 'copepod.n.01', 'name': 'copepod'}, {'id': 3055, 'synset': 'cyclops.n.02', 'name': 'cyclops'}, {'id': 3056, 'synset': 'seed_shrimp.n.01', 'name': 'seed_shrimp'}, {'id': 3057, 'synset': 'barnacle.n.01', 'name': 'barnacle'}, {'id': 3058, 'synset': 'acorn_barnacle.n.01', 'name': 'acorn_barnacle'}, {'id': 3059, 'synset': 'goose_barnacle.n.01', 'name': 'goose_barnacle'}, {'id': 3060, 'synset': 'onychophoran.n.01', 'name': 'onychophoran'}, {'id': 3061, 'synset': 'wading_bird.n.01', 'name': 'wading_bird'}, {'id': 3062, 'synset': 'stork.n.01', 'name': 'stork'}, {'id': 3063, 'synset': 'white_stork.n.01', 'name': 'white_stork'}, {'id': 3064, 'synset': 'black_stork.n.01', 'name': 'black_stork'}, {'id': 3065, 'synset': 'adjutant_bird.n.01', 'name': 'adjutant_bird'}, {'id': 3066, 'synset': 'marabou.n.01', 'name': 'marabou'}, {'id': 3067, 'synset': 'openbill.n.01', 'name': 'openbill'}, {'id': 3068, 'synset': 'jabiru.n.03', 'name': 'jabiru'}, {'id': 3069, 'synset': 'saddlebill.n.01', 'name': 'saddlebill'}, {'id': 3070, 'synset': 'policeman_bird.n.01', 'name': 'policeman_bird'}, {'id': 3071, 'synset': 'wood_ibis.n.02', 'name': 'wood_ibis'}, {'id': 3072, 'synset': 'shoebill.n.01', 'name': 'shoebill'}, {'id': 3073, 'synset': 'ibis.n.01', 'name': 'ibis'}, {'id': 3074, 'synset': 'wood_ibis.n.01', 'name': 'wood_ibis'}, {'id': 3075, 'synset': 'sacred_ibis.n.01', 'name': 'sacred_ibis'}, {'id': 3076, 'synset': 'spoonbill.n.01', 'name': 'spoonbill'}, {'id': 3077, 'synset': 'common_spoonbill.n.01', 'name': 'common_spoonbill'}, {'id': 3078, 'synset': 'roseate_spoonbill.n.01', 'name': 'roseate_spoonbill'}, {'id': 3079, 'synset': 'great_blue_heron.n.01', 'name': 'great_blue_heron'}, {'id': 3080, 'synset': 'great_white_heron.n.03', 'name': 'great_white_heron'}, {'id': 3081, 'synset': 'egret.n.01', 'name': 'egret'}, {'id': 3082, 'synset': 'little_blue_heron.n.01', 'name': 'little_blue_heron'}, {'id': 3083, 'synset': 'snowy_egret.n.01', 'name': 'snowy_egret'}, {'id': 3084, 'synset': 'little_egret.n.01', 'name': 'little_egret'}, {'id': 3085, 'synset': 'great_white_heron.n.02', 'name': 'great_white_heron'}, {'id': 3086, 'synset': 'american_egret.n.01', 'name': 'American_egret'}, {'id': 3087, 'synset': 'cattle_egret.n.01', 'name': 'cattle_egret'}, {'id': 3088, 'synset': 'night_heron.n.01', 'name': 'night_heron'}, {'id': 3089, 'synset': 'black-crowned_night_heron.n.01', 'name': 'black-crowned_night_heron'}, {'id': 3090, 'synset': 'yellow-crowned_night_heron.n.01', 'name': 'yellow-crowned_night_heron'}, {'id': 3091, 'synset': 'boatbill.n.01', 'name': 'boatbill'}, {'id': 3092, 'synset': 'bittern.n.01', 'name': 'bittern'}, {'id': 3093, 'synset': 'american_bittern.n.01', 'name': 'American_bittern'}, {'id': 3094, 'synset': 'european_bittern.n.01', 'name': 'European_bittern'}, {'id': 3095, 'synset': 'least_bittern.n.01', 'name': 'least_bittern'}, {'id': 3096, 'synset': 'crane.n.05', 'name': 'crane'}, {'id': 3097, 'synset': 'whooping_crane.n.01', 'name': 'whooping_crane'}, {'id': 3098, 'synset': 'courlan.n.01', 'name': 'courlan'}, {'id': 3099, 'synset': 'limpkin.n.01', 'name': 'limpkin'}, {'id': 3100, 'synset': 'crested_cariama.n.01', 'name': 'crested_cariama'}, {'id': 3101, 'synset': 'chunga.n.01', 'name': 'chunga'}, {'id': 3102, 'synset': 'rail.n.05', 'name': 'rail'}, {'id': 3103, 'synset': 'weka.n.01', 'name': 'weka'}, {'id': 3104, 'synset': 'crake.n.01', 'name': 'crake'}, {'id': 3105, 'synset': 'corncrake.n.01', 'name': 'corncrake'}, {'id': 3106, 'synset': 'spotted_crake.n.01', 'name': 'spotted_crake'}, {'id': 3107, 'synset': 'gallinule.n.01', 'name': 'gallinule'}, {'id': 3108, 'synset': 'florida_gallinule.n.01', 'name': 'Florida_gallinule'}, {'id': 3109, 'synset': 'moorhen.n.01', 'name': 'moorhen'}, {'id': 3110, 'synset': 'purple_gallinule.n.01', 'name': 'purple_gallinule'}, {'id': 3111, 'synset': 'european_gallinule.n.01', 'name': 'European_gallinule'}, {'id': 3112, 'synset': 'american_gallinule.n.01', 'name': 'American_gallinule'}, {'id': 3113, 'synset': 'notornis.n.01', 'name': 'notornis'}, {'id': 3114, 'synset': 'coot.n.01', 'name': 'coot'}, {'id': 3115, 'synset': 'american_coot.n.01', 'name': 'American_coot'}, {'id': 3116, 'synset': 'old_world_coot.n.01', 'name': 'Old_World_coot'}, {'id': 3117, 'synset': 'bustard.n.01', 'name': 'bustard'}, {'id': 3118, 'synset': 'great_bustard.n.01', 'name': 'great_bustard'}, {'id': 3119, 'synset': 'plain_turkey.n.01', 'name': 'plain_turkey'}, {'id': 3120, 'synset': 'button_quail.n.01', 'name': 'button_quail'}, {'id': 3121, 'synset': 'striped_button_quail.n.01', 'name': 'striped_button_quail'}, {'id': 3122, 'synset': 'plain_wanderer.n.01', 'name': 'plain_wanderer'}, {'id': 3123, 'synset': 'trumpeter.n.03', 'name': 'trumpeter'}, {'id': 3124, 'synset': 'brazilian_trumpeter.n.01', 'name': 'Brazilian_trumpeter'}, {'id': 3125, 'synset': 'shorebird.n.01', 'name': 'shorebird'}, {'id': 3126, 'synset': 'plover.n.01', 'name': 'plover'}, {'id': 3127, 'synset': 'piping_plover.n.01', 'name': 'piping_plover'}, {'id': 3128, 'synset': 'killdeer.n.01', 'name': 'killdeer'}, {'id': 3129, 'synset': 'dotterel.n.01', 'name': 'dotterel'}, {'id': 3130, 'synset': 'golden_plover.n.01', 'name': 'golden_plover'}, {'id': 3131, 'synset': 'lapwing.n.01', 'name': 'lapwing'}, {'id': 3132, 'synset': 'turnstone.n.01', 'name': 'turnstone'}, {'id': 3133, 'synset': 'ruddy_turnstone.n.01', 'name': 'ruddy_turnstone'}, {'id': 3134, 'synset': 'black_turnstone.n.01', 'name': 'black_turnstone'}, {'id': 3135, 'synset': 'sandpiper.n.01', 'name': 'sandpiper'}, {'id': 3136, 'synset': 'surfbird.n.01', 'name': 'surfbird'}, {'id': 3137, 'synset': 'european_sandpiper.n.01', 'name': 'European_sandpiper'}, {'id': 3138, 'synset': 'spotted_sandpiper.n.01', 'name': 'spotted_sandpiper'}, {'id': 3139, 'synset': 'least_sandpiper.n.01', 'name': 'least_sandpiper'}, {'id': 3140, 'synset': 'red-backed_sandpiper.n.01', 'name': 'red-backed_sandpiper'}, {'id': 3141, 'synset': 'greenshank.n.01', 'name': 'greenshank'}, {'id': 3142, 'synset': 'redshank.n.01', 'name': 'redshank'}, {'id': 3143, 'synset': 'yellowlegs.n.01', 'name': 'yellowlegs'}, {'id': 3144, 'synset': 'greater_yellowlegs.n.01', 'name': 'greater_yellowlegs'}, {'id': 3145, 'synset': 'lesser_yellowlegs.n.01', 'name': 'lesser_yellowlegs'}, {'id': 3146, 'synset': 'pectoral_sandpiper.n.01', 'name': 'pectoral_sandpiper'}, {'id': 3147, 'synset': 'knot.n.07', 'name': 'knot'}, {'id': 3148, 'synset': 'curlew_sandpiper.n.01', 'name': 'curlew_sandpiper'}, {'id': 3149, 'synset': 'sanderling.n.01', 'name': 'sanderling'}, {'id': 3150, 'synset': 'upland_sandpiper.n.01', 'name': 'upland_sandpiper'}, {'id': 3151, 'synset': 'ruff.n.03', 'name': 'ruff'}, {'id': 3152, 'synset': 'reeve.n.01', 'name': 'reeve'}, {'id': 3153, 'synset': 'tattler.n.02', 'name': 'tattler'}, {'id': 3154, 'synset': 'polynesian_tattler.n.01', 'name': 'Polynesian_tattler'}, {'id': 3155, 'synset': 'willet.n.01', 'name': 'willet'}, {'id': 3156, 'synset': 'woodcock.n.01', 'name': 'woodcock'}, {'id': 3157, 'synset': 'eurasian_woodcock.n.01', 'name': 'Eurasian_woodcock'}, {'id': 3158, 'synset': 'american_woodcock.n.01', 'name': 'American_woodcock'}, {'id': 3159, 'synset': 'snipe.n.01', 'name': 'snipe'}, {'id': 3160, 'synset': 'whole_snipe.n.01', 'name': 'whole_snipe'}, {'id': 3161, 'synset': "wilson's_snipe.n.01", 'name': "Wilson's_snipe"}, {'id': 3162, 'synset': 'great_snipe.n.01', 'name': 'great_snipe'}, {'id': 3163, 'synset': 'jacksnipe.n.01', 'name': 'jacksnipe'}, {'id': 3164, 'synset': 'dowitcher.n.01', 'name': 'dowitcher'}, {'id': 3165, 'synset': 'greyback.n.02', 'name': 'greyback'}, {'id': 3166, 'synset': 'red-breasted_snipe.n.01', 'name': 'red-breasted_snipe'}, {'id': 3167, 'synset': 'curlew.n.01', 'name': 'curlew'}, {'id': 3168, 'synset': 'european_curlew.n.01', 'name': 'European_curlew'}, {'id': 3169, 'synset': 'eskimo_curlew.n.01', 'name': 'Eskimo_curlew'}, {'id': 3170, 'synset': 'godwit.n.01', 'name': 'godwit'}, {'id': 3171, 'synset': 'hudsonian_godwit.n.01', 'name': 'Hudsonian_godwit'}, {'id': 3172, 'synset': 'stilt.n.04', 'name': 'stilt'}, {'id': 3173, 'synset': 'black-necked_stilt.n.01', 'name': 'black-necked_stilt'}, {'id': 3174, 'synset': 'black-winged_stilt.n.01', 'name': 'black-winged_stilt'}, {'id': 3175, 'synset': 'white-headed_stilt.n.01', 'name': 'white-headed_stilt'}, {'id': 3176, 'synset': 'kaki.n.02', 'name': 'kaki'}, {'id': 3177, 'synset': 'stilt.n.03', 'name': 'stilt'}, {'id': 3178, 'synset': 'banded_stilt.n.01', 'name': 'banded_stilt'}, {'id': 3179, 'synset': 'avocet.n.01', 'name': 'avocet'}, {'id': 3180, 'synset': 'oystercatcher.n.01', 'name': 'oystercatcher'}, {'id': 3181, 'synset': 'phalarope.n.01', 'name': 'phalarope'}, {'id': 3182, 'synset': 'red_phalarope.n.01', 'name': 'red_phalarope'}, {'id': 3183, 'synset': 'northern_phalarope.n.01', 'name': 'northern_phalarope'}, {'id': 3184, 'synset': "wilson's_phalarope.n.01", 'name': "Wilson's_phalarope"}, {'id': 3185, 'synset': 'pratincole.n.01', 'name': 'pratincole'}, {'id': 3186, 'synset': 'courser.n.04', 'name': 'courser'}, {'id': 3187, 'synset': 'cream-colored_courser.n.01', 'name': 'cream-colored_courser'}, {'id': 3188, 'synset': 'crocodile_bird.n.01', 'name': 'crocodile_bird'}, {'id': 3189, 'synset': 'stone_curlew.n.01', 'name': 'stone_curlew'}, {'id': 3190, 'synset': 'coastal_diving_bird.n.01', 'name': 'coastal_diving_bird'}, {'id': 3191, 'synset': 'larid.n.01', 'name': 'larid'}, {'id': 3192, 'synset': 'mew.n.02', 'name': 'mew'}, {'id': 3193, 'synset': 'black-backed_gull.n.01', 'name': 'black-backed_gull'}, {'id': 3194, 'synset': 'herring_gull.n.01', 'name': 'herring_gull'}, {'id': 3195, 'synset': 'laughing_gull.n.01', 'name': 'laughing_gull'}, {'id': 3196, 'synset': 'ivory_gull.n.01', 'name': 'ivory_gull'}, {'id': 3197, 'synset': 'kittiwake.n.01', 'name': 'kittiwake'}, {'id': 3198, 'synset': 'tern.n.01', 'name': 'tern'}, {'id': 3199, 'synset': 'sea_swallow.n.01', 'name': 'sea_swallow'}, {'id': 3200, 'synset': 'skimmer.n.04', 'name': 'skimmer'}, {'id': 3201, 'synset': 'jaeger.n.01', 'name': 'jaeger'}, {'id': 3202, 'synset': 'parasitic_jaeger.n.01', 'name': 'parasitic_jaeger'}, {'id': 3203, 'synset': 'skua.n.01', 'name': 'skua'}, {'id': 3204, 'synset': 'great_skua.n.01', 'name': 'great_skua'}, {'id': 3205, 'synset': 'auk.n.01', 'name': 'auk'}, {'id': 3206, 'synset': 'auklet.n.01', 'name': 'auklet'}, {'id': 3207, 'synset': 'razorbill.n.01', 'name': 'razorbill'}, {'id': 3208, 'synset': 'little_auk.n.01', 'name': 'little_auk'}, {'id': 3209, 'synset': 'guillemot.n.01', 'name': 'guillemot'}, {'id': 3210, 'synset': 'black_guillemot.n.01', 'name': 'black_guillemot'}, {'id': 3211, 'synset': 'pigeon_guillemot.n.01', 'name': 'pigeon_guillemot'}, {'id': 3212, 'synset': 'murre.n.01', 'name': 'murre'}, {'id': 3213, 'synset': 'common_murre.n.01', 'name': 'common_murre'}, {'id': 3214, 'synset': 'thick-billed_murre.n.01', 'name': 'thick-billed_murre'}, {'id': 3215, 'synset': 'atlantic_puffin.n.01', 'name': 'Atlantic_puffin'}, {'id': 3216, 'synset': 'horned_puffin.n.01', 'name': 'horned_puffin'}, {'id': 3217, 'synset': 'tufted_puffin.n.01', 'name': 'tufted_puffin'}, {'id': 3218, 'synset': 'gaviiform_seabird.n.01', 'name': 'gaviiform_seabird'}, {'id': 3219, 'synset': 'loon.n.02', 'name': 'loon'}, {'id': 3220, 'synset': 'podicipitiform_seabird.n.01', 'name': 'podicipitiform_seabird'}, {'id': 3221, 'synset': 'grebe.n.01', 'name': 'grebe'}, {'id': 3222, 'synset': 'great_crested_grebe.n.01', 'name': 'great_crested_grebe'}, {'id': 3223, 'synset': 'red-necked_grebe.n.01', 'name': 'red-necked_grebe'}, {'id': 3224, 'synset': 'black-necked_grebe.n.01', 'name': 'black-necked_grebe'}, {'id': 3225, 'synset': 'dabchick.n.01', 'name': 'dabchick'}, {'id': 3226, 'synset': 'pied-billed_grebe.n.01', 'name': 'pied-billed_grebe'}, {'id': 3227, 'synset': 'pelecaniform_seabird.n.01', 'name': 'pelecaniform_seabird'}, {'id': 3228, 'synset': 'white_pelican.n.01', 'name': 'white_pelican'}, {'id': 3229, 'synset': 'old_world_white_pelican.n.01', 'name': 'Old_world_white_pelican'}, {'id': 3230, 'synset': 'frigate_bird.n.01', 'name': 'frigate_bird'}, {'id': 3231, 'synset': 'gannet.n.01', 'name': 'gannet'}, {'id': 3232, 'synset': 'solan.n.01', 'name': 'solan'}, {'id': 3233, 'synset': 'booby.n.02', 'name': 'booby'}, {'id': 3234, 'synset': 'cormorant.n.01', 'name': 'cormorant'}, {'id': 3235, 'synset': 'snakebird.n.01', 'name': 'snakebird'}, {'id': 3236, 'synset': 'water_turkey.n.01', 'name': 'water_turkey'}, {'id': 3237, 'synset': 'tropic_bird.n.01', 'name': 'tropic_bird'}, {'id': 3238, 'synset': 'sphenisciform_seabird.n.01', 'name': 'sphenisciform_seabird'}, {'id': 3239, 'synset': 'adelie.n.01', 'name': 'Adelie'}, {'id': 3240, 'synset': 'king_penguin.n.01', 'name': 'king_penguin'}, {'id': 3241, 'synset': 'emperor_penguin.n.01', 'name': 'emperor_penguin'}, {'id': 3242, 'synset': 'jackass_penguin.n.01', 'name': 'jackass_penguin'}, {'id': 3243, 'synset': 'rock_hopper.n.01', 'name': 'rock_hopper'}, {'id': 3244, 'synset': 'pelagic_bird.n.01', 'name': 'pelagic_bird'}, {'id': 3245, 'synset': 'procellariiform_seabird.n.01', 'name': 'procellariiform_seabird'}, {'id': 3246, 'synset': 'albatross.n.02', 'name': 'albatross'}, {'id': 3247, 'synset': 'wandering_albatross.n.01', 'name': 'wandering_albatross'}, {'id': 3248, 'synset': 'black-footed_albatross.n.01', 'name': 'black-footed_albatross'}, {'id': 3249, 'synset': 'petrel.n.01', 'name': 'petrel'}, {'id': 3250, 'synset': 'white-chinned_petrel.n.01', 'name': 'white-chinned_petrel'}, {'id': 3251, 'synset': 'giant_petrel.n.01', 'name': 'giant_petrel'}, {'id': 3252, 'synset': 'fulmar.n.01', 'name': 'fulmar'}, {'id': 3253, 'synset': 'shearwater.n.01', 'name': 'shearwater'}, {'id': 3254, 'synset': 'manx_shearwater.n.01', 'name': 'Manx_shearwater'}, {'id': 3255, 'synset': 'storm_petrel.n.01', 'name': 'storm_petrel'}, {'id': 3256, 'synset': 'stormy_petrel.n.01', 'name': 'stormy_petrel'}, {'id': 3257, 'synset': "mother_carey's_chicken.n.01", 'name': "Mother_Carey's_chicken"}, {'id': 3258, 'synset': 'diving_petrel.n.01', 'name': 'diving_petrel'}, {'id': 3259, 'synset': 'aquatic_mammal.n.01', 'name': 'aquatic_mammal'}, {'id': 3260, 'synset': 'cetacean.n.01', 'name': 'cetacean'}, {'id': 3261, 'synset': 'whale.n.02', 'name': 'whale'}, {'id': 3262, 'synset': 'baleen_whale.n.01', 'name': 'baleen_whale'}, {'id': 3263, 'synset': 'right_whale.n.01', 'name': 'right_whale'}, {'id': 3264, 'synset': 'bowhead.n.01', 'name': 'bowhead'}, {'id': 3265, 'synset': 'rorqual.n.01', 'name': 'rorqual'}, {'id': 3266, 'synset': 'blue_whale.n.01', 'name': 'blue_whale'}, {'id': 3267, 'synset': 'finback.n.01', 'name': 'finback'}, {'id': 3268, 'synset': 'sei_whale.n.01', 'name': 'sei_whale'}, {'id': 3269, 'synset': 'lesser_rorqual.n.01', 'name': 'lesser_rorqual'}, {'id': 3270, 'synset': 'humpback.n.03', 'name': 'humpback'}, {'id': 3271, 'synset': 'grey_whale.n.01', 'name': 'grey_whale'}, {'id': 3272, 'synset': 'toothed_whale.n.01', 'name': 'toothed_whale'}, {'id': 3273, 'synset': 'sperm_whale.n.01', 'name': 'sperm_whale'}, {'id': 3274, 'synset': 'pygmy_sperm_whale.n.01', 'name': 'pygmy_sperm_whale'}, {'id': 3275, 'synset': 'dwarf_sperm_whale.n.01', 'name': 'dwarf_sperm_whale'}, {'id': 3276, 'synset': 'beaked_whale.n.01', 'name': 'beaked_whale'}, {'id': 3277, 'synset': 'bottle-nosed_whale.n.01', 'name': 'bottle-nosed_whale'}, {'id': 3278, 'synset': 'common_dolphin.n.01', 'name': 'common_dolphin'}, {'id': 3279, 'synset': 'bottlenose_dolphin.n.01', 'name': 'bottlenose_dolphin'}, {'id': 3280, 'synset': 'atlantic_bottlenose_dolphin.n.01', 'name': 'Atlantic_bottlenose_dolphin'}, {'id': 3281, 'synset': 'pacific_bottlenose_dolphin.n.01', 'name': 'Pacific_bottlenose_dolphin'}, {'id': 3282, 'synset': 'porpoise.n.01', 'name': 'porpoise'}, {'id': 3283, 'synset': 'harbor_porpoise.n.01', 'name': 'harbor_porpoise'}, {'id': 3284, 'synset': 'vaquita.n.01', 'name': 'vaquita'}, {'id': 3285, 'synset': 'grampus.n.02', 'name': 'grampus'}, {'id': 3286, 'synset': 'killer_whale.n.01', 'name': 'killer_whale'}, {'id': 3287, 'synset': 'pilot_whale.n.01', 'name': 'pilot_whale'}, {'id': 3288, 'synset': 'river_dolphin.n.01', 'name': 'river_dolphin'}, {'id': 3289, 'synset': 'narwhal.n.01', 'name': 'narwhal'}, {'id': 3290, 'synset': 'white_whale.n.01', 'name': 'white_whale'}, {'id': 3291, 'synset': 'sea_cow.n.01', 'name': 'sea_cow'}, {'id': 3292, 'synset': 'dugong.n.01', 'name': 'dugong'}, {'id': 3293, 'synset': "steller's_sea_cow.n.01", 'name': "Steller's_sea_cow"}, {'id': 3294, 'synset': 'carnivore.n.01', 'name': 'carnivore'}, {'id': 3295, 'synset': 'omnivore.n.02', 'name': 'omnivore'}, {'id': 3296, 'synset': 'pinniped_mammal.n.01', 'name': 'pinniped_mammal'}, {'id': 3297, 'synset': 'seal.n.09', 'name': 'seal'}, {'id': 3298, 'synset': 'crabeater_seal.n.01', 'name': 'crabeater_seal'}, {'id': 3299, 'synset': 'eared_seal.n.01', 'name': 'eared_seal'}, {'id': 3300, 'synset': 'fur_seal.n.02', 'name': 'fur_seal'}, {'id': 3301, 'synset': 'guadalupe_fur_seal.n.01', 'name': 'guadalupe_fur_seal'}, {'id': 3302, 'synset': 'fur_seal.n.01', 'name': 'fur_seal'}, {'id': 3303, 'synset': 'alaska_fur_seal.n.01', 'name': 'Alaska_fur_seal'}, {'id': 3304, 'synset': 'sea_lion.n.01', 'name': 'sea_lion'}, {'id': 3305, 'synset': 'south_american_sea_lion.n.01', 'name': 'South_American_sea_lion'}, {'id': 3306, 'synset': 'california_sea_lion.n.01', 'name': 'California_sea_lion'}, {'id': 3307, 'synset': 'australian_sea_lion.n.01', 'name': 'Australian_sea_lion'}, {'id': 3308, 'synset': 'steller_sea_lion.n.01', 'name': 'Steller_sea_lion'}, {'id': 3309, 'synset': 'earless_seal.n.01', 'name': 'earless_seal'}, {'id': 3310, 'synset': 'harbor_seal.n.01', 'name': 'harbor_seal'}, {'id': 3311, 'synset': 'harp_seal.n.01', 'name': 'harp_seal'}, {'id': 3312, 'synset': 'elephant_seal.n.01', 'name': 'elephant_seal'}, {'id': 3313, 'synset': 'bearded_seal.n.01', 'name': 'bearded_seal'}, {'id': 3314, 'synset': 'hooded_seal.n.01', 'name': 'hooded_seal'}, {'id': 3315, 'synset': 'atlantic_walrus.n.01', 'name': 'Atlantic_walrus'}, {'id': 3316, 'synset': 'pacific_walrus.n.01', 'name': 'Pacific_walrus'}, {'id': 3317, 'synset': 'fissipedia.n.01', 'name': 'Fissipedia'}, {'id': 3318, 'synset': 'fissiped_mammal.n.01', 'name': 'fissiped_mammal'}, {'id': 3319, 'synset': 'aardvark.n.01', 'name': 'aardvark'}, {'id': 3320, 'synset': 'canine.n.02', 'name': 'canine'}, {'id': 3321, 'synset': 'bitch.n.04', 'name': 'bitch'}, {'id': 3322, 'synset': 'brood_bitch.n.01', 'name': 'brood_bitch'}, {'id': 3323, 'synset': 'pooch.n.01', 'name': 'pooch'}, {'id': 3324, 'synset': 'cur.n.01', 'name': 'cur'}, {'id': 3325, 'synset': 'feist.n.01', 'name': 'feist'}, {'id': 3326, 'synset': 'pariah_dog.n.01', 'name': 'pariah_dog'}, {'id': 3327, 'synset': 'lapdog.n.01', 'name': 'lapdog'}, {'id': 3328, 'synset': 'toy_dog.n.01', 'name': 'toy_dog'}, {'id': 3329, 'synset': 'chihuahua.n.03', 'name': 'Chihuahua'}, {'id': 3330, 'synset': 'japanese_spaniel.n.01', 'name': 'Japanese_spaniel'}, {'id': 3331, 'synset': 'maltese_dog.n.01', 'name': 'Maltese_dog'}, {'id': 3332, 'synset': 'pekinese.n.01', 'name': 'Pekinese'}, {'id': 3333, 'synset': 'shih-tzu.n.01', 'name': 'Shih-Tzu'}, {'id': 3334, 'synset': 'toy_spaniel.n.01', 'name': 'toy_spaniel'}, {'id': 3335, 'synset': 'english_toy_spaniel.n.01', 'name': 'English_toy_spaniel'}, {'id': 3336, 'synset': 'blenheim_spaniel.n.01', 'name': 'Blenheim_spaniel'}, {'id': 3337, 'synset': 'king_charles_spaniel.n.01', 'name': 'King_Charles_spaniel'}, {'id': 3338, 'synset': 'papillon.n.01', 'name': 'papillon'}, {'id': 3339, 'synset': 'toy_terrier.n.01', 'name': 'toy_terrier'}, {'id': 3340, 'synset': 'hunting_dog.n.01', 'name': 'hunting_dog'}, {'id': 3341, 'synset': 'courser.n.03', 'name': 'courser'}, {'id': 3342, 'synset': 'rhodesian_ridgeback.n.01', 'name': 'Rhodesian_ridgeback'}, {'id': 3343, 'synset': 'hound.n.01', 'name': 'hound'}, {'id': 3344, 'synset': 'afghan_hound.n.01', 'name': 'Afghan_hound'}, {'id': 3345, 'synset': 'basset.n.01', 'name': 'basset'}, {'id': 3346, 'synset': 'beagle.n.01', 'name': 'beagle'}, {'id': 3347, 'synset': 'bloodhound.n.01', 'name': 'bloodhound'}, {'id': 3348, 'synset': 'bluetick.n.01', 'name': 'bluetick'}, {'id': 3349, 'synset': 'boarhound.n.01', 'name': 'boarhound'}, {'id': 3350, 'synset': 'coonhound.n.01', 'name': 'coonhound'}, {'id': 3351, 'synset': 'coondog.n.01', 'name': 'coondog'}, {'id': 3352, 'synset': 'black-and-tan_coonhound.n.01', 'name': 'black-and-tan_coonhound'}, {'id': 3353, 'synset': 'dachshund.n.01', 'name': 'dachshund'}, {'id': 3354, 'synset': 'sausage_dog.n.01', 'name': 'sausage_dog'}, {'id': 3355, 'synset': 'foxhound.n.01', 'name': 'foxhound'}, {'id': 3356, 'synset': 'american_foxhound.n.01', 'name': 'American_foxhound'}, {'id': 3357, 'synset': 'walker_hound.n.01', 'name': 'Walker_hound'}, {'id': 3358, 'synset': 'english_foxhound.n.01', 'name': 'English_foxhound'}, {'id': 3359, 'synset': 'harrier.n.02', 'name': 'harrier'}, {'id': 3360, 'synset': 'plott_hound.n.01', 'name': 'Plott_hound'}, {'id': 3361, 'synset': 'redbone.n.01', 'name': 'redbone'}, {'id': 3362, 'synset': 'wolfhound.n.01', 'name': 'wolfhound'}, {'id': 3363, 'synset': 'borzoi.n.01', 'name': 'borzoi'}, {'id': 3364, 'synset': 'irish_wolfhound.n.01', 'name': 'Irish_wolfhound'}, {'id': 3365, 'synset': 'greyhound.n.01', 'name': 'greyhound'}, {'id': 3366, 'synset': 'italian_greyhound.n.01', 'name': 'Italian_greyhound'}, {'id': 3367, 'synset': 'whippet.n.01', 'name': 'whippet'}, {'id': 3368, 'synset': 'ibizan_hound.n.01', 'name': 'Ibizan_hound'}, {'id': 3369, 'synset': 'norwegian_elkhound.n.01', 'name': 'Norwegian_elkhound'}, {'id': 3370, 'synset': 'otterhound.n.01', 'name': 'otterhound'}, {'id': 3371, 'synset': 'saluki.n.01', 'name': 'Saluki'}, {'id': 3372, 'synset': 'scottish_deerhound.n.01', 'name': 'Scottish_deerhound'}, {'id': 3373, 'synset': 'staghound.n.01', 'name': 'staghound'}, {'id': 3374, 'synset': 'weimaraner.n.01', 'name': 'Weimaraner'}, {'id': 3375, 'synset': 'terrier.n.01', 'name': 'terrier'}, {'id': 3376, 'synset': 'bullterrier.n.01', 'name': 'bullterrier'}, {'id': 3377, 'synset': 'staffordshire_bullterrier.n.01', 'name': 'Staffordshire_bullterrier'}, {'id': 3378, 'synset': 'american_staffordshire_terrier.n.01', 'name': 'American_Staffordshire_terrier'}, {'id': 3379, 'synset': 'bedlington_terrier.n.01', 'name': 'Bedlington_terrier'}, {'id': 3380, 'synset': 'border_terrier.n.01', 'name': 'Border_terrier'}, {'id': 3381, 'synset': 'kerry_blue_terrier.n.01', 'name': 'Kerry_blue_terrier'}, {'id': 3382, 'synset': 'irish_terrier.n.01', 'name': 'Irish_terrier'}, {'id': 3383, 'synset': 'norfolk_terrier.n.01', 'name': 'Norfolk_terrier'}, {'id': 3384, 'synset': 'norwich_terrier.n.01', 'name': 'Norwich_terrier'}, {'id': 3385, 'synset': 'yorkshire_terrier.n.01', 'name': 'Yorkshire_terrier'}, {'id': 3386, 'synset': 'rat_terrier.n.01', 'name': 'rat_terrier'}, {'id': 3387, 'synset': 'manchester_terrier.n.01', 'name': 'Manchester_terrier'}, {'id': 3388, 'synset': 'toy_manchester.n.01', 'name': 'toy_Manchester'}, {'id': 3389, 'synset': 'fox_terrier.n.01', 'name': 'fox_terrier'}, {'id': 3390, 'synset': 'smooth-haired_fox_terrier.n.01', 'name': 'smooth-haired_fox_terrier'}, {'id': 3391, 'synset': 'wire-haired_fox_terrier.n.01', 'name': 'wire-haired_fox_terrier'}, {'id': 3392, 'synset': 'wirehair.n.01', 'name': 'wirehair'}, {'id': 3393, 'synset': 'lakeland_terrier.n.01', 'name': 'Lakeland_terrier'}, {'id': 3394, 'synset': 'welsh_terrier.n.01', 'name': 'Welsh_terrier'}, {'id': 3395, 'synset': 'sealyham_terrier.n.01', 'name': 'Sealyham_terrier'}, {'id': 3396, 'synset': 'airedale.n.01', 'name': 'Airedale'}, {'id': 3397, 'synset': 'cairn.n.02', 'name': 'cairn'}, {'id': 3398, 'synset': 'australian_terrier.n.01', 'name': 'Australian_terrier'}, {'id': 3399, 'synset': 'dandie_dinmont.n.01', 'name': 'Dandie_Dinmont'}, {'id': 3400, 'synset': 'boston_bull.n.01', 'name': 'Boston_bull'}, {'id': 3401, 'synset': 'schnauzer.n.01', 'name': 'schnauzer'}, {'id': 3402, 'synset': 'miniature_schnauzer.n.01', 'name': 'miniature_schnauzer'}, {'id': 3403, 'synset': 'giant_schnauzer.n.01', 'name': 'giant_schnauzer'}, {'id': 3404, 'synset': 'standard_schnauzer.n.01', 'name': 'standard_schnauzer'}, {'id': 3405, 'synset': 'scotch_terrier.n.01', 'name': 'Scotch_terrier'}, {'id': 3406, 'synset': 'tibetan_terrier.n.01', 'name': 'Tibetan_terrier'}, {'id': 3407, 'synset': 'silky_terrier.n.01', 'name': 'silky_terrier'}, {'id': 3408, 'synset': 'skye_terrier.n.01', 'name': 'Skye_terrier'}, {'id': 3409, 'synset': 'clydesdale_terrier.n.01', 'name': 'Clydesdale_terrier'}, {'id': 3410, 'synset': 'soft-coated_wheaten_terrier.n.01', 'name': 'soft-coated_wheaten_terrier'}, {'id': 3411, 'synset': 'west_highland_white_terrier.n.01', 'name': 'West_Highland_white_terrier'}, {'id': 3412, 'synset': 'lhasa.n.02', 'name': 'Lhasa'}, {'id': 3413, 'synset': 'sporting_dog.n.01', 'name': 'sporting_dog'}, {'id': 3414, 'synset': 'bird_dog.n.01', 'name': 'bird_dog'}, {'id': 3415, 'synset': 'water_dog.n.02', 'name': 'water_dog'}, {'id': 3416, 'synset': 'retriever.n.01', 'name': 'retriever'}, {'id': 3417, 'synset': 'flat-coated_retriever.n.01', 'name': 'flat-coated_retriever'}, {'id': 3418, 'synset': 'curly-coated_retriever.n.01', 'name': 'curly-coated_retriever'}, {'id': 3419, 'synset': 'golden_retriever.n.01', 'name': 'golden_retriever'}, {'id': 3420, 'synset': 'labrador_retriever.n.01', 'name': 'Labrador_retriever'}, {'id': 3421, 'synset': 'chesapeake_bay_retriever.n.01', 'name': 'Chesapeake_Bay_retriever'}, {'id': 3422, 'synset': 'pointer.n.04', 'name': 'pointer'}, {'id': 3423, 'synset': 'german_short-haired_pointer.n.01', 'name': 'German_short-haired_pointer'}, {'id': 3424, 'synset': 'setter.n.02', 'name': 'setter'}, {'id': 3425, 'synset': 'vizsla.n.01', 'name': 'vizsla'}, {'id': 3426, 'synset': 'english_setter.n.01', 'name': 'English_setter'}, {'id': 3427, 'synset': 'irish_setter.n.01', 'name': 'Irish_setter'}, {'id': 3428, 'synset': 'gordon_setter.n.01', 'name': 'Gordon_setter'}, {'id': 3429, 'synset': 'spaniel.n.01', 'name': 'spaniel'}, {'id': 3430, 'synset': 'brittany_spaniel.n.01', 'name': 'Brittany_spaniel'}, {'id': 3431, 'synset': 'clumber.n.01', 'name': 'clumber'}, {'id': 3432, 'synset': 'field_spaniel.n.01', 'name': 'field_spaniel'}, {'id': 3433, 'synset': 'springer_spaniel.n.01', 'name': 'springer_spaniel'}, {'id': 3434, 'synset': 'english_springer.n.01', 'name': 'English_springer'}, {'id': 3435, 'synset': 'welsh_springer_spaniel.n.01', 'name': 'Welsh_springer_spaniel'}, {'id': 3436, 'synset': 'cocker_spaniel.n.01', 'name': 'cocker_spaniel'}, {'id': 3437, 'synset': 'sussex_spaniel.n.01', 'name': 'Sussex_spaniel'}, {'id': 3438, 'synset': 'water_spaniel.n.01', 'name': 'water_spaniel'}, {'id': 3439, 'synset': 'american_water_spaniel.n.01', 'name': 'American_water_spaniel'}, {'id': 3440, 'synset': 'irish_water_spaniel.n.01', 'name': 'Irish_water_spaniel'}, {'id': 3441, 'synset': 'griffon.n.03', 'name': 'griffon'}, {'id': 3442, 'synset': 'working_dog.n.01', 'name': 'working_dog'}, {'id': 3443, 'synset': 'watchdog.n.02', 'name': 'watchdog'}, {'id': 3444, 'synset': 'kuvasz.n.01', 'name': 'kuvasz'}, {'id': 3445, 'synset': 'attack_dog.n.01', 'name': 'attack_dog'}, {'id': 3446, 'synset': 'housedog.n.01', 'name': 'housedog'}, {'id': 3447, 'synset': 'schipperke.n.01', 'name': 'schipperke'}, {'id': 3448, 'synset': 'belgian_sheepdog.n.01', 'name': 'Belgian_sheepdog'}, {'id': 3449, 'synset': 'groenendael.n.01', 'name': 'groenendael'}, {'id': 3450, 'synset': 'malinois.n.01', 'name': 'malinois'}, {'id': 3451, 'synset': 'briard.n.01', 'name': 'briard'}, {'id': 3452, 'synset': 'kelpie.n.02', 'name': 'kelpie'}, {'id': 3453, 'synset': 'komondor.n.01', 'name': 'komondor'}, {'id': 3454, 'synset': 'old_english_sheepdog.n.01', 'name': 'Old_English_sheepdog'}, {'id': 3455, 'synset': 'shetland_sheepdog.n.01', 'name': 'Shetland_sheepdog'}, {'id': 3456, 'synset': 'collie.n.01', 'name': 'collie'}, {'id': 3457, 'synset': 'border_collie.n.01', 'name': 'Border_collie'}, {'id': 3458, 'synset': 'bouvier_des_flandres.n.01', 'name': 'Bouvier_des_Flandres'}, {'id': 3459, 'synset': 'rottweiler.n.01', 'name': 'Rottweiler'}, {'id': 3460, 'synset': 'german_shepherd.n.01', 'name': 'German_shepherd'}, {'id': 3461, 'synset': 'police_dog.n.01', 'name': 'police_dog'}, {'id': 3462, 'synset': 'pinscher.n.01', 'name': 'pinscher'}, {'id': 3463, 'synset': 'doberman.n.01', 'name': 'Doberman'}, {'id': 3464, 'synset': 'miniature_pinscher.n.01', 'name': 'miniature_pinscher'}, {'id': 3465, 'synset': 'sennenhunde.n.01', 'name': 'Sennenhunde'}, {'id': 3466, 'synset': 'greater_swiss_mountain_dog.n.01', 'name': 'Greater_Swiss_Mountain_dog'}, {'id': 3467, 'synset': 'bernese_mountain_dog.n.01', 'name': 'Bernese_mountain_dog'}, {'id': 3468, 'synset': 'appenzeller.n.01', 'name': 'Appenzeller'}, {'id': 3469, 'synset': 'entlebucher.n.01', 'name': 'EntleBucher'}, {'id': 3470, 'synset': 'boxer.n.04', 'name': 'boxer'}, {'id': 3471, 'synset': 'mastiff.n.01', 'name': 'mastiff'}, {'id': 3472, 'synset': 'bull_mastiff.n.01', 'name': 'bull_mastiff'}, {'id': 3473, 'synset': 'tibetan_mastiff.n.01', 'name': 'Tibetan_mastiff'}, {'id': 3474, 'synset': 'french_bulldog.n.01', 'name': 'French_bulldog'}, {'id': 3475, 'synset': 'great_dane.n.01', 'name': 'Great_Dane'}, {'id': 3476, 'synset': 'guide_dog.n.01', 'name': 'guide_dog'}, {'id': 3477, 'synset': 'seeing_eye_dog.n.01', 'name': 'Seeing_Eye_dog'}, {'id': 3478, 'synset': 'hearing_dog.n.01', 'name': 'hearing_dog'}, {'id': 3479, 'synset': 'saint_bernard.n.01', 'name': 'Saint_Bernard'}, {'id': 3480, 'synset': 'seizure-alert_dog.n.01', 'name': 'seizure-alert_dog'}, {'id': 3481, 'synset': 'sled_dog.n.01', 'name': 'sled_dog'}, {'id': 3482, 'synset': 'eskimo_dog.n.01', 'name': 'Eskimo_dog'}, {'id': 3483, 'synset': 'malamute.n.01', 'name': 'malamute'}, {'id': 3484, 'synset': 'siberian_husky.n.01', 'name': 'Siberian_husky'}, {'id': 3485, 'synset': 'liver-spotted_dalmatian.n.01', 'name': 'liver-spotted_dalmatian'}, {'id': 3486, 'synset': 'affenpinscher.n.01', 'name': 'affenpinscher'}, {'id': 3487, 'synset': 'basenji.n.01', 'name': 'basenji'}, {'id': 3488, 'synset': 'leonberg.n.01', 'name': 'Leonberg'}, {'id': 3489, 'synset': 'newfoundland.n.01', 'name': 'Newfoundland'}, {'id': 3490, 'synset': 'great_pyrenees.n.01', 'name': 'Great_Pyrenees'}, {'id': 3491, 'synset': 'spitz.n.01', 'name': 'spitz'}, {'id': 3492, 'synset': 'samoyed.n.03', 'name': 'Samoyed'}, {'id': 3493, 'synset': 'pomeranian.n.01', 'name': 'Pomeranian'}, {'id': 3494, 'synset': 'chow.n.03', 'name': 'chow'}, {'id': 3495, 'synset': 'keeshond.n.01', 'name': 'keeshond'}, {'id': 3496, 'synset': 'griffon.n.02', 'name': 'griffon'}, {'id': 3497, 'synset': 'brabancon_griffon.n.01', 'name': 'Brabancon_griffon'}, {'id': 3498, 'synset': 'corgi.n.01', 'name': 'corgi'}, {'id': 3499, 'synset': 'pembroke.n.01', 'name': 'Pembroke'}, {'id': 3500, 'synset': 'cardigan.n.02', 'name': 'Cardigan'}, {'id': 3501, 'synset': 'poodle.n.01', 'name': 'poodle'}, {'id': 3502, 'synset': 'toy_poodle.n.01', 'name': 'toy_poodle'}, {'id': 3503, 'synset': 'miniature_poodle.n.01', 'name': 'miniature_poodle'}, {'id': 3504, 'synset': 'standard_poodle.n.01', 'name': 'standard_poodle'}, {'id': 3505, 'synset': 'large_poodle.n.01', 'name': 'large_poodle'}, {'id': 3506, 'synset': 'mexican_hairless.n.01', 'name': 'Mexican_hairless'}, {'id': 3507, 'synset': 'timber_wolf.n.01', 'name': 'timber_wolf'}, {'id': 3508, 'synset': 'white_wolf.n.01', 'name': 'white_wolf'}, {'id': 3509, 'synset': 'red_wolf.n.01', 'name': 'red_wolf'}, {'id': 3510, 'synset': 'coyote.n.01', 'name': 'coyote'}, {'id': 3511, 'synset': 'coydog.n.01', 'name': 'coydog'}, {'id': 3512, 'synset': 'jackal.n.01', 'name': 'jackal'}, {'id': 3513, 'synset': 'wild_dog.n.01', 'name': 'wild_dog'}, {'id': 3514, 'synset': 'dingo.n.01', 'name': 'dingo'}, {'id': 3515, 'synset': 'dhole.n.01', 'name': 'dhole'}, {'id': 3516, 'synset': 'crab-eating_dog.n.01', 'name': 'crab-eating_dog'}, {'id': 3517, 'synset': 'raccoon_dog.n.01', 'name': 'raccoon_dog'}, {'id': 3518, 'synset': 'african_hunting_dog.n.01', 'name': 'African_hunting_dog'}, {'id': 3519, 'synset': 'hyena.n.01', 'name': 'hyena'}, {'id': 3520, 'synset': 'striped_hyena.n.01', 'name': 'striped_hyena'}, {'id': 3521, 'synset': 'brown_hyena.n.01', 'name': 'brown_hyena'}, {'id': 3522, 'synset': 'spotted_hyena.n.01', 'name': 'spotted_hyena'}, {'id': 3523, 'synset': 'aardwolf.n.01', 'name': 'aardwolf'}, {'id': 3524, 'synset': 'fox.n.01', 'name': 'fox'}, {'id': 3525, 'synset': 'vixen.n.02', 'name': 'vixen'}, {'id': 3526, 'synset': 'reynard.n.01', 'name': 'Reynard'}, {'id': 3527, 'synset': 'red_fox.n.03', 'name': 'red_fox'}, {'id': 3528, 'synset': 'black_fox.n.01', 'name': 'black_fox'}, {'id': 3529, 'synset': 'silver_fox.n.01', 'name': 'silver_fox'}, {'id': 3530, 'synset': 'red_fox.n.02', 'name': 'red_fox'}, {'id': 3531, 'synset': 'kit_fox.n.02', 'name': 'kit_fox'}, {'id': 3532, 'synset': 'kit_fox.n.01', 'name': 'kit_fox'}, {'id': 3533, 'synset': 'arctic_fox.n.01', 'name': 'Arctic_fox'}, {'id': 3534, 'synset': 'blue_fox.n.01', 'name': 'blue_fox'}, {'id': 3535, 'synset': 'grey_fox.n.01', 'name': 'grey_fox'}, {'id': 3536, 'synset': 'feline.n.01', 'name': 'feline'}, {'id': 3537, 'synset': 'domestic_cat.n.01', 'name': 'domestic_cat'}, {'id': 3538, 'synset': 'kitty.n.04', 'name': 'kitty'}, {'id': 3539, 'synset': 'mouser.n.01', 'name': 'mouser'}, {'id': 3540, 'synset': 'alley_cat.n.01', 'name': 'alley_cat'}, {'id': 3541, 'synset': 'stray.n.01', 'name': 'stray'}, {'id': 3542, 'synset': 'tom.n.02', 'name': 'tom'}, {'id': 3543, 'synset': 'gib.n.02', 'name': 'gib'}, {'id': 3544, 'synset': 'tabby.n.02', 'name': 'tabby'}, {'id': 3545, 'synset': 'tabby.n.01', 'name': 'tabby'}, {'id': 3546, 'synset': 'tiger_cat.n.02', 'name': 'tiger_cat'}, {'id': 3547, 'synset': 'tortoiseshell.n.03', 'name': 'tortoiseshell'}, {'id': 3548, 'synset': 'persian_cat.n.01', 'name': 'Persian_cat'}, {'id': 3549, 'synset': 'angora.n.04', 'name': 'Angora'}, {'id': 3550, 'synset': 'siamese_cat.n.01', 'name': 'Siamese_cat'}, {'id': 3551, 'synset': 'blue_point_siamese.n.01', 'name': 'blue_point_Siamese'}, {'id': 3552, 'synset': 'burmese_cat.n.01', 'name': 'Burmese_cat'}, {'id': 3553, 'synset': 'egyptian_cat.n.01', 'name': 'Egyptian_cat'}, {'id': 3554, 'synset': 'maltese.n.03', 'name': 'Maltese'}, {'id': 3555, 'synset': 'abyssinian.n.01', 'name': 'Abyssinian'}, {'id': 3556, 'synset': 'manx.n.02', 'name': 'Manx'}, {'id': 3557, 'synset': 'wildcat.n.03', 'name': 'wildcat'}, {'id': 3558, 'synset': 'sand_cat.n.01', 'name': 'sand_cat'}, {'id': 3559, 'synset': 'european_wildcat.n.01', 'name': 'European_wildcat'}, {'id': 3560, 'synset': 'ocelot.n.01', 'name': 'ocelot'}, {'id': 3561, 'synset': 'jaguarundi.n.01', 'name': 'jaguarundi'}, {'id': 3562, 'synset': 'kaffir_cat.n.01', 'name': 'kaffir_cat'}, {'id': 3563, 'synset': 'jungle_cat.n.01', 'name': 'jungle_cat'}, {'id': 3564, 'synset': 'serval.n.01', 'name': 'serval'}, {'id': 3565, 'synset': 'leopard_cat.n.01', 'name': 'leopard_cat'}, {'id': 3566, 'synset': 'margay.n.01', 'name': 'margay'}, {'id': 3567, 'synset': 'manul.n.01', 'name': 'manul'}, {'id': 3568, 'synset': 'lynx.n.02', 'name': 'lynx'}, {'id': 3569, 'synset': 'common_lynx.n.01', 'name': 'common_lynx'}, {'id': 3570, 'synset': 'canada_lynx.n.01', 'name': 'Canada_lynx'}, {'id': 3571, 'synset': 'bobcat.n.01', 'name': 'bobcat'}, {'id': 3572, 'synset': 'spotted_lynx.n.01', 'name': 'spotted_lynx'}, {'id': 3573, 'synset': 'caracal.n.01', 'name': 'caracal'}, {'id': 3574, 'synset': 'big_cat.n.01', 'name': 'big_cat'}, {'id': 3575, 'synset': 'leopard.n.02', 'name': 'leopard'}, {'id': 3576, 'synset': 'leopardess.n.01', 'name': 'leopardess'}, {'id': 3577, 'synset': 'panther.n.02', 'name': 'panther'}, {'id': 3578, 'synset': 'snow_leopard.n.01', 'name': 'snow_leopard'}, {'id': 3579, 'synset': 'jaguar.n.01', 'name': 'jaguar'}, {'id': 3580, 'synset': 'lioness.n.01', 'name': 'lioness'}, {'id': 3581, 'synset': 'lionet.n.01', 'name': 'lionet'}, {'id': 3582, 'synset': 'bengal_tiger.n.01', 'name': 'Bengal_tiger'}, {'id': 3583, 'synset': 'tigress.n.01', 'name': 'tigress'}, {'id': 3584, 'synset': 'liger.n.01', 'name': 'liger'}, {'id': 3585, 'synset': 'tiglon.n.01', 'name': 'tiglon'}, {'id': 3586, 'synset': 'cheetah.n.01', 'name': 'cheetah'}, {'id': 3587, 'synset': 'saber-toothed_tiger.n.01', 'name': 'saber-toothed_tiger'}, {'id': 3588, 'synset': 'smiledon_californicus.n.01', 'name': 'Smiledon_californicus'}, {'id': 3589, 'synset': 'brown_bear.n.01', 'name': 'brown_bear'}, {'id': 3590, 'synset': 'bruin.n.01', 'name': 'bruin'}, {'id': 3591, 'synset': 'syrian_bear.n.01', 'name': 'Syrian_bear'}, {'id': 3592, 'synset': 'alaskan_brown_bear.n.01', 'name': 'Alaskan_brown_bear'}, {'id': 3593, 'synset': 'american_black_bear.n.01', 'name': 'American_black_bear'}, {'id': 3594, 'synset': 'cinnamon_bear.n.01', 'name': 'cinnamon_bear'}, {'id': 3595, 'synset': 'asiatic_black_bear.n.01', 'name': 'Asiatic_black_bear'}, {'id': 3596, 'synset': 'sloth_bear.n.01', 'name': 'sloth_bear'}, {'id': 3597, 'synset': 'viverrine.n.01', 'name': 'viverrine'}, {'id': 3598, 'synset': 'civet.n.01', 'name': 'civet'}, {'id': 3599, 'synset': 'large_civet.n.01', 'name': 'large_civet'}, {'id': 3600, 'synset': 'small_civet.n.01', 'name': 'small_civet'}, {'id': 3601, 'synset': 'binturong.n.01', 'name': 'binturong'}, {'id': 3602, 'synset': 'cryptoprocta.n.01', 'name': 'Cryptoprocta'}, {'id': 3603, 'synset': 'fossa.n.03', 'name': 'fossa'}, {'id': 3604, 'synset': 'fanaloka.n.01', 'name': 'fanaloka'}, {'id': 3605, 'synset': 'genet.n.03', 'name': 'genet'}, {'id': 3606, 'synset': 'banded_palm_civet.n.01', 'name': 'banded_palm_civet'}, {'id': 3607, 'synset': 'mongoose.n.01', 'name': 'mongoose'}, {'id': 3608, 'synset': 'indian_mongoose.n.01', 'name': 'Indian_mongoose'}, {'id': 3609, 'synset': 'ichneumon.n.01', 'name': 'ichneumon'}, {'id': 3610, 'synset': 'palm_cat.n.01', 'name': 'palm_cat'}, {'id': 3611, 'synset': 'meerkat.n.01', 'name': 'meerkat'}, {'id': 3612, 'synset': 'slender-tailed_meerkat.n.01', 'name': 'slender-tailed_meerkat'}, {'id': 3613, 'synset': 'suricate.n.01', 'name': 'suricate'}, {'id': 3614, 'synset': 'fruit_bat.n.01', 'name': 'fruit_bat'}, {'id': 3615, 'synset': 'flying_fox.n.01', 'name': 'flying_fox'}, {'id': 3616, 'synset': 'pteropus_capestratus.n.01', 'name': 'Pteropus_capestratus'}, {'id': 3617, 'synset': 'pteropus_hypomelanus.n.01', 'name': 'Pteropus_hypomelanus'}, {'id': 3618, 'synset': 'harpy.n.03', 'name': 'harpy'}, {'id': 3619, 'synset': 'cynopterus_sphinx.n.01', 'name': 'Cynopterus_sphinx'}, {'id': 3620, 'synset': 'carnivorous_bat.n.01', 'name': 'carnivorous_bat'}, {'id': 3621, 'synset': 'mouse-eared_bat.n.01', 'name': 'mouse-eared_bat'}, {'id': 3622, 'synset': 'leafnose_bat.n.01', 'name': 'leafnose_bat'}, {'id': 3623, 'synset': 'macrotus.n.01', 'name': 'macrotus'}, {'id': 3624, 'synset': 'spearnose_bat.n.01', 'name': 'spearnose_bat'}, {'id': 3625, 'synset': 'phyllostomus_hastatus.n.01', 'name': 'Phyllostomus_hastatus'}, {'id': 3626, 'synset': 'hognose_bat.n.01', 'name': 'hognose_bat'}, {'id': 3627, 'synset': 'horseshoe_bat.n.02', 'name': 'horseshoe_bat'}, {'id': 3628, 'synset': 'horseshoe_bat.n.01', 'name': 'horseshoe_bat'}, {'id': 3629, 'synset': 'orange_bat.n.01', 'name': 'orange_bat'}, {'id': 3630, 'synset': 'false_vampire.n.01', 'name': 'false_vampire'}, {'id': 3631, 'synset': 'big-eared_bat.n.01', 'name': 'big-eared_bat'}, {'id': 3632, 'synset': 'vespertilian_bat.n.01', 'name': 'vespertilian_bat'}, {'id': 3633, 'synset': 'frosted_bat.n.01', 'name': 'frosted_bat'}, {'id': 3634, 'synset': 'red_bat.n.01', 'name': 'red_bat'}, {'id': 3635, 'synset': 'brown_bat.n.01', 'name': 'brown_bat'}, {'id': 3636, 'synset': 'little_brown_bat.n.01', 'name': 'little_brown_bat'}, {'id': 3637, 'synset': 'cave_myotis.n.01', 'name': 'cave_myotis'}, {'id': 3638, 'synset': 'big_brown_bat.n.01', 'name': 'big_brown_bat'}, {'id': 3639, 'synset': 'serotine.n.01', 'name': 'serotine'}, {'id': 3640, 'synset': 'pallid_bat.n.01', 'name': 'pallid_bat'}, {'id': 3641, 'synset': 'pipistrelle.n.01', 'name': 'pipistrelle'}, {'id': 3642, 'synset': 'eastern_pipistrel.n.01', 'name': 'eastern_pipistrel'}, {'id': 3643, 'synset': 'jackass_bat.n.01', 'name': 'jackass_bat'}, {'id': 3644, 'synset': 'long-eared_bat.n.01', 'name': 'long-eared_bat'}, {'id': 3645, 'synset': 'western_big-eared_bat.n.01', 'name': 'western_big-eared_bat'}, {'id': 3646, 'synset': 'freetail.n.01', 'name': 'freetail'}, {'id': 3647, 'synset': 'guano_bat.n.01', 'name': 'guano_bat'}, {'id': 3648, 'synset': 'pocketed_bat.n.01', 'name': 'pocketed_bat'}, {'id': 3649, 'synset': 'mastiff_bat.n.01', 'name': 'mastiff_bat'}, {'id': 3650, 'synset': 'vampire_bat.n.01', 'name': 'vampire_bat'}, {'id': 3651, 'synset': 'desmodus_rotundus.n.01', 'name': 'Desmodus_rotundus'}, {'id': 3652, 'synset': 'hairy-legged_vampire_bat.n.01', 'name': 'hairy-legged_vampire_bat'}, {'id': 3653, 'synset': 'predator.n.02', 'name': 'predator'}, {'id': 3654, 'synset': 'prey.n.02', 'name': 'prey'}, {'id': 3655, 'synset': 'game.n.04', 'name': 'game'}, {'id': 3656, 'synset': 'big_game.n.01', 'name': 'big_game'}, {'id': 3657, 'synset': 'game_bird.n.01', 'name': 'game_bird'}, {'id': 3658, 'synset': 'fossorial_mammal.n.01', 'name': 'fossorial_mammal'}, {'id': 3659, 'synset': 'tetrapod.n.01', 'name': 'tetrapod'}, {'id': 3660, 'synset': 'quadruped.n.01', 'name': 'quadruped'}, {'id': 3661, 'synset': 'hexapod.n.01', 'name': 'hexapod'}, {'id': 3662, 'synset': 'biped.n.01', 'name': 'biped'}, {'id': 3663, 'synset': 'insect.n.01', 'name': 'insect'}, {'id': 3664, 'synset': 'social_insect.n.01', 'name': 'social_insect'}, {'id': 3665, 'synset': 'holometabola.n.01', 'name': 'holometabola'}, {'id': 3666, 'synset': 'defoliator.n.01', 'name': 'defoliator'}, {'id': 3667, 'synset': 'pollinator.n.01', 'name': 'pollinator'}, {'id': 3668, 'synset': 'gallfly.n.03', 'name': 'gallfly'}, {'id': 3669, 'synset': 'scorpion_fly.n.01', 'name': 'scorpion_fly'}, {'id': 3670, 'synset': 'hanging_fly.n.01', 'name': 'hanging_fly'}, {'id': 3671, 'synset': 'collembolan.n.01', 'name': 'collembolan'}, {'id': 3672, 'synset': 'tiger_beetle.n.01', 'name': 'tiger_beetle'}, {'id': 3673, 'synset': 'two-spotted_ladybug.n.01', 'name': 'two-spotted_ladybug'}, {'id': 3674, 'synset': 'mexican_bean_beetle.n.01', 'name': 'Mexican_bean_beetle'}, {'id': 3675, 'synset': 'hippodamia_convergens.n.01', 'name': 'Hippodamia_convergens'}, {'id': 3676, 'synset': 'vedalia.n.01', 'name': 'vedalia'}, {'id': 3677, 'synset': 'ground_beetle.n.01', 'name': 'ground_beetle'}, {'id': 3678, 'synset': 'bombardier_beetle.n.01', 'name': 'bombardier_beetle'}, {'id': 3679, 'synset': 'calosoma.n.01', 'name': 'calosoma'}, {'id': 3680, 'synset': 'searcher.n.03', 'name': 'searcher'}, {'id': 3681, 'synset': 'firefly.n.02', 'name': 'firefly'}, {'id': 3682, 'synset': 'glowworm.n.01', 'name': 'glowworm'}, {'id': 3683, 'synset': 'long-horned_beetle.n.01', 'name': 'long-horned_beetle'}, {'id': 3684, 'synset': 'sawyer.n.02', 'name': 'sawyer'}, {'id': 3685, 'synset': 'pine_sawyer.n.01', 'name': 'pine_sawyer'}, {'id': 3686, 'synset': 'leaf_beetle.n.01', 'name': 'leaf_beetle'}, {'id': 3687, 'synset': 'flea_beetle.n.01', 'name': 'flea_beetle'}, {'id': 3688, 'synset': 'colorado_potato_beetle.n.01', 'name': 'Colorado_potato_beetle'}, {'id': 3689, 'synset': 'carpet_beetle.n.01', 'name': 'carpet_beetle'}, {'id': 3690, 'synset': 'buffalo_carpet_beetle.n.01', 'name': 'buffalo_carpet_beetle'}, {'id': 3691, 'synset': 'black_carpet_beetle.n.01', 'name': 'black_carpet_beetle'}, {'id': 3692, 'synset': 'clerid_beetle.n.01', 'name': 'clerid_beetle'}, {'id': 3693, 'synset': 'bee_beetle.n.01', 'name': 'bee_beetle'}, {'id': 3694, 'synset': 'lamellicorn_beetle.n.01', 'name': 'lamellicorn_beetle'}, {'id': 3695, 'synset': 'scarabaeid_beetle.n.01', 'name': 'scarabaeid_beetle'}, {'id': 3696, 'synset': 'dung_beetle.n.01', 'name': 'dung_beetle'}, {'id': 3697, 'synset': 'scarab.n.01', 'name': 'scarab'}, {'id': 3698, 'synset': 'tumblebug.n.01', 'name': 'tumblebug'}, {'id': 3699, 'synset': 'dorbeetle.n.01', 'name': 'dorbeetle'}, {'id': 3700, 'synset': 'june_beetle.n.01', 'name': 'June_beetle'}, {'id': 3701, 'synset': 'green_june_beetle.n.01', 'name': 'green_June_beetle'}, {'id': 3702, 'synset': 'japanese_beetle.n.01', 'name': 'Japanese_beetle'}, {'id': 3703, 'synset': 'oriental_beetle.n.01', 'name': 'Oriental_beetle'}, {'id': 3704, 'synset': 'rhinoceros_beetle.n.01', 'name': 'rhinoceros_beetle'}, {'id': 3705, 'synset': 'melolonthid_beetle.n.01', 'name': 'melolonthid_beetle'}, {'id': 3706, 'synset': 'cockchafer.n.01', 'name': 'cockchafer'}, {'id': 3707, 'synset': 'rose_chafer.n.02', 'name': 'rose_chafer'}, {'id': 3708, 'synset': 'rose_chafer.n.01', 'name': 'rose_chafer'}, {'id': 3709, 'synset': 'stag_beetle.n.01', 'name': 'stag_beetle'}, {'id': 3710, 'synset': 'elaterid_beetle.n.01', 'name': 'elaterid_beetle'}, {'id': 3711, 'synset': 'click_beetle.n.01', 'name': 'click_beetle'}, {'id': 3712, 'synset': 'firefly.n.01', 'name': 'firefly'}, {'id': 3713, 'synset': 'wireworm.n.01', 'name': 'wireworm'}, {'id': 3714, 'synset': 'water_beetle.n.01', 'name': 'water_beetle'}, {'id': 3715, 'synset': 'whirligig_beetle.n.01', 'name': 'whirligig_beetle'}, {'id': 3716, 'synset': 'deathwatch_beetle.n.01', 'name': 'deathwatch_beetle'}, {'id': 3717, 'synset': 'weevil.n.01', 'name': 'weevil'}, {'id': 3718, 'synset': 'snout_beetle.n.01', 'name': 'snout_beetle'}, {'id': 3719, 'synset': 'boll_weevil.n.01', 'name': 'boll_weevil'}, {'id': 3720, 'synset': 'blister_beetle.n.01', 'name': 'blister_beetle'}, {'id': 3721, 'synset': 'oil_beetle.n.01', 'name': 'oil_beetle'}, {'id': 3722, 'synset': 'spanish_fly.n.01', 'name': 'Spanish_fly'}, {'id': 3723, 'synset': 'dutch-elm_beetle.n.01', 'name': 'Dutch-elm_beetle'}, {'id': 3724, 'synset': 'bark_beetle.n.01', 'name': 'bark_beetle'}, {'id': 3725, 'synset': 'spruce_bark_beetle.n.01', 'name': 'spruce_bark_beetle'}, {'id': 3726, 'synset': 'rove_beetle.n.01', 'name': 'rove_beetle'}, {'id': 3727, 'synset': 'darkling_beetle.n.01', 'name': 'darkling_beetle'}, {'id': 3728, 'synset': 'mealworm.n.01', 'name': 'mealworm'}, {'id': 3729, 'synset': 'flour_beetle.n.01', 'name': 'flour_beetle'}, {'id': 3730, 'synset': 'seed_beetle.n.01', 'name': 'seed_beetle'}, {'id': 3731, 'synset': 'pea_weevil.n.01', 'name': 'pea_weevil'}, {'id': 3732, 'synset': 'bean_weevil.n.01', 'name': 'bean_weevil'}, {'id': 3733, 'synset': 'rice_weevil.n.01', 'name': 'rice_weevil'}, {'id': 3734, 'synset': 'asian_longhorned_beetle.n.01', 'name': 'Asian_longhorned_beetle'}, {'id': 3735, 'synset': 'web_spinner.n.01', 'name': 'web_spinner'}, {'id': 3736, 'synset': 'louse.n.01', 'name': 'louse'}, {'id': 3737, 'synset': 'common_louse.n.01', 'name': 'common_louse'}, {'id': 3738, 'synset': 'head_louse.n.01', 'name': 'head_louse'}, {'id': 3739, 'synset': 'body_louse.n.01', 'name': 'body_louse'}, {'id': 3740, 'synset': 'crab_louse.n.01', 'name': 'crab_louse'}, {'id': 3741, 'synset': 'bird_louse.n.01', 'name': 'bird_louse'}, {'id': 3742, 'synset': 'flea.n.01', 'name': 'flea'}, {'id': 3743, 'synset': 'pulex_irritans.n.01', 'name': 'Pulex_irritans'}, {'id': 3744, 'synset': 'dog_flea.n.01', 'name': 'dog_flea'}, {'id': 3745, 'synset': 'cat_flea.n.01', 'name': 'cat_flea'}, {'id': 3746, 'synset': 'chigoe.n.01', 'name': 'chigoe'}, {'id': 3747, 'synset': 'sticktight.n.02', 'name': 'sticktight'}, {'id': 3748, 'synset': 'dipterous_insect.n.01', 'name': 'dipterous_insect'}, {'id': 3749, 'synset': 'gall_midge.n.01', 'name': 'gall_midge'}, {'id': 3750, 'synset': 'hessian_fly.n.01', 'name': 'Hessian_fly'}, {'id': 3751, 'synset': 'fly.n.01', 'name': 'fly'}, {'id': 3752, 'synset': 'housefly.n.01', 'name': 'housefly'}, {'id': 3753, 'synset': 'tsetse_fly.n.01', 'name': 'tsetse_fly'}, {'id': 3754, 'synset': 'blowfly.n.01', 'name': 'blowfly'}, {'id': 3755, 'synset': 'bluebottle.n.02', 'name': 'bluebottle'}, {'id': 3756, 'synset': 'greenbottle.n.01', 'name': 'greenbottle'}, {'id': 3757, 'synset': 'flesh_fly.n.01', 'name': 'flesh_fly'}, {'id': 3758, 'synset': 'tachina_fly.n.01', 'name': 'tachina_fly'}, {'id': 3759, 'synset': 'gadfly.n.02', 'name': 'gadfly'}, {'id': 3760, 'synset': 'botfly.n.01', 'name': 'botfly'}, {'id': 3761, 'synset': 'human_botfly.n.01', 'name': 'human_botfly'}, {'id': 3762, 'synset': 'sheep_botfly.n.01', 'name': 'sheep_botfly'}, {'id': 3763, 'synset': 'warble_fly.n.01', 'name': 'warble_fly'}, {'id': 3764, 'synset': 'horsefly.n.02', 'name': 'horsefly'}, {'id': 3765, 'synset': 'bee_fly.n.01', 'name': 'bee_fly'}, {'id': 3766, 'synset': 'robber_fly.n.01', 'name': 'robber_fly'}, {'id': 3767, 'synset': 'fruit_fly.n.01', 'name': 'fruit_fly'}, {'id': 3768, 'synset': 'apple_maggot.n.01', 'name': 'apple_maggot'}, {'id': 3769, 'synset': 'mediterranean_fruit_fly.n.01', 'name': 'Mediterranean_fruit_fly'}, {'id': 3770, 'synset': 'drosophila.n.01', 'name': 'drosophila'}, {'id': 3771, 'synset': 'vinegar_fly.n.01', 'name': 'vinegar_fly'}, {'id': 3772, 'synset': 'leaf_miner.n.01', 'name': 'leaf_miner'}, {'id': 3773, 'synset': 'louse_fly.n.01', 'name': 'louse_fly'}, {'id': 3774, 'synset': 'horse_tick.n.01', 'name': 'horse_tick'}, {'id': 3775, 'synset': 'sheep_ked.n.01', 'name': 'sheep_ked'}, {'id': 3776, 'synset': 'horn_fly.n.01', 'name': 'horn_fly'}, {'id': 3777, 'synset': 'mosquito.n.01', 'name': 'mosquito'}, {'id': 3778, 'synset': 'wiggler.n.02', 'name': 'wiggler'}, {'id': 3779, 'synset': 'gnat.n.02', 'name': 'gnat'}, {'id': 3780, 'synset': 'yellow-fever_mosquito.n.01', 'name': 'yellow-fever_mosquito'}, {'id': 3781, 'synset': 'asian_tiger_mosquito.n.01', 'name': 'Asian_tiger_mosquito'}, {'id': 3782, 'synset': 'anopheline.n.01', 'name': 'anopheline'}, {'id': 3783, 'synset': 'malarial_mosquito.n.01', 'name': 'malarial_mosquito'}, {'id': 3784, 'synset': 'common_mosquito.n.01', 'name': 'common_mosquito'}, {'id': 3785, 'synset': 'culex_quinquefasciatus.n.01', 'name': 'Culex_quinquefasciatus'}, {'id': 3786, 'synset': 'gnat.n.01', 'name': 'gnat'}, {'id': 3787, 'synset': 'punkie.n.01', 'name': 'punkie'}, {'id': 3788, 'synset': 'midge.n.01', 'name': 'midge'}, {'id': 3789, 'synset': 'fungus_gnat.n.02', 'name': 'fungus_gnat'}, {'id': 3790, 'synset': 'psychodid.n.01', 'name': 'psychodid'}, {'id': 3791, 'synset': 'sand_fly.n.01', 'name': 'sand_fly'}, {'id': 3792, 'synset': 'fungus_gnat.n.01', 'name': 'fungus_gnat'}, {'id': 3793, 'synset': 'armyworm.n.03', 'name': 'armyworm'}, {'id': 3794, 'synset': 'crane_fly.n.01', 'name': 'crane_fly'}, {'id': 3795, 'synset': 'blackfly.n.02', 'name': 'blackfly'}, {'id': 3796, 'synset': 'hymenopterous_insect.n.01', 'name': 'hymenopterous_insect'}, {'id': 3797, 'synset': 'bee.n.01', 'name': 'bee'}, {'id': 3798, 'synset': 'drone.n.01', 'name': 'drone'}, {'id': 3799, 'synset': 'queen_bee.n.01', 'name': 'queen_bee'}, {'id': 3800, 'synset': 'worker.n.03', 'name': 'worker'}, {'id': 3801, 'synset': 'soldier.n.02', 'name': 'soldier'}, {'id': 3802, 'synset': 'worker_bee.n.01', 'name': 'worker_bee'}, {'id': 3803, 'synset': 'honeybee.n.01', 'name': 'honeybee'}, {'id': 3804, 'synset': 'africanized_bee.n.01', 'name': 'Africanized_bee'}, {'id': 3805, 'synset': 'black_bee.n.01', 'name': 'black_bee'}, {'id': 3806, 'synset': 'carniolan_bee.n.01', 'name': 'Carniolan_bee'}, {'id': 3807, 'synset': 'italian_bee.n.01', 'name': 'Italian_bee'}, {'id': 3808, 'synset': 'carpenter_bee.n.01', 'name': 'carpenter_bee'}, {'id': 3809, 'synset': 'bumblebee.n.01', 'name': 'bumblebee'}, {'id': 3810, 'synset': 'cuckoo-bumblebee.n.01', 'name': 'cuckoo-bumblebee'}, {'id': 3811, 'synset': 'andrena.n.01', 'name': 'andrena'}, {'id': 3812, 'synset': 'nomia_melanderi.n.01', 'name': 'Nomia_melanderi'}, {'id': 3813, 'synset': 'leaf-cutting_bee.n.01', 'name': 'leaf-cutting_bee'}, {'id': 3814, 'synset': 'mason_bee.n.01', 'name': 'mason_bee'}, {'id': 3815, 'synset': 'potter_bee.n.01', 'name': 'potter_bee'}, {'id': 3816, 'synset': 'wasp.n.02', 'name': 'wasp'}, {'id': 3817, 'synset': 'vespid.n.01', 'name': 'vespid'}, {'id': 3818, 'synset': 'paper_wasp.n.01', 'name': 'paper_wasp'}, {'id': 3819, 'synset': 'giant_hornet.n.01', 'name': 'giant_hornet'}, {'id': 3820, 'synset': 'common_wasp.n.01', 'name': 'common_wasp'}, {'id': 3821, 'synset': 'bald-faced_hornet.n.01', 'name': 'bald-faced_hornet'}, {'id': 3822, 'synset': 'yellow_jacket.n.02', 'name': 'yellow_jacket'}, {'id': 3823, 'synset': 'polistes_annularis.n.01', 'name': 'Polistes_annularis'}, {'id': 3824, 'synset': 'mason_wasp.n.02', 'name': 'mason_wasp'}, {'id': 3825, 'synset': 'potter_wasp.n.01', 'name': 'potter_wasp'}, {'id': 3826, 'synset': 'mutillidae.n.01', 'name': 'Mutillidae'}, {'id': 3827, 'synset': 'velvet_ant.n.01', 'name': 'velvet_ant'}, {'id': 3828, 'synset': 'sphecoid_wasp.n.01', 'name': 'sphecoid_wasp'}, {'id': 3829, 'synset': 'mason_wasp.n.01', 'name': 'mason_wasp'}, {'id': 3830, 'synset': 'digger_wasp.n.01', 'name': 'digger_wasp'}, {'id': 3831, 'synset': 'cicada_killer.n.01', 'name': 'cicada_killer'}, {'id': 3832, 'synset': 'mud_dauber.n.01', 'name': 'mud_dauber'}, {'id': 3833, 'synset': 'gall_wasp.n.01', 'name': 'gall_wasp'}, {'id': 3834, 'synset': 'chalcid_fly.n.01', 'name': 'chalcid_fly'}, {'id': 3835, 'synset': 'strawworm.n.02', 'name': 'strawworm'}, {'id': 3836, 'synset': 'chalcis_fly.n.01', 'name': 'chalcis_fly'}, {'id': 3837, 'synset': 'ichneumon_fly.n.01', 'name': 'ichneumon_fly'}, {'id': 3838, 'synset': 'sawfly.n.01', 'name': 'sawfly'}, {'id': 3839, 'synset': 'birch_leaf_miner.n.01', 'name': 'birch_leaf_miner'}, {'id': 3840, 'synset': 'ant.n.01', 'name': 'ant'}, {'id': 3841, 'synset': 'pharaoh_ant.n.01', 'name': 'pharaoh_ant'}, {'id': 3842, 'synset': 'little_black_ant.n.01', 'name': 'little_black_ant'}, {'id': 3843, 'synset': 'army_ant.n.01', 'name': 'army_ant'}, {'id': 3844, 'synset': 'carpenter_ant.n.01', 'name': 'carpenter_ant'}, {'id': 3845, 'synset': 'fire_ant.n.01', 'name': 'fire_ant'}, {'id': 3846, 'synset': 'wood_ant.n.01', 'name': 'wood_ant'}, {'id': 3847, 'synset': 'slave_ant.n.01', 'name': 'slave_ant'}, {'id': 3848, 'synset': 'formica_fusca.n.01', 'name': 'Formica_fusca'}, {'id': 3849, 'synset': 'slave-making_ant.n.01', 'name': 'slave-making_ant'}, {'id': 3850, 'synset': 'sanguinary_ant.n.01', 'name': 'sanguinary_ant'}, {'id': 3851, 'synset': 'bulldog_ant.n.01', 'name': 'bulldog_ant'}, {'id': 3852, 'synset': 'amazon_ant.n.01', 'name': 'Amazon_ant'}, {'id': 3853, 'synset': 'termite.n.01', 'name': 'termite'}, {'id': 3854, 'synset': 'dry-wood_termite.n.01', 'name': 'dry-wood_termite'}, {'id': 3855, 'synset': 'reticulitermes_lucifugus.n.01', 'name': 'Reticulitermes_lucifugus'}, {'id': 3856, 'synset': 'mastotermes_darwiniensis.n.01', 'name': 'Mastotermes_darwiniensis'}, {'id': 3857, 'synset': 'mastotermes_electrodominicus.n.01', 'name': 'Mastotermes_electrodominicus'}, {'id': 3858, 'synset': 'powder-post_termite.n.01', 'name': 'powder-post_termite'}, {'id': 3859, 'synset': 'orthopterous_insect.n.01', 'name': 'orthopterous_insect'}, {'id': 3860, 'synset': 'grasshopper.n.01', 'name': 'grasshopper'}, {'id': 3861, 'synset': 'short-horned_grasshopper.n.01', 'name': 'short-horned_grasshopper'}, {'id': 3862, 'synset': 'locust.n.01', 'name': 'locust'}, {'id': 3863, 'synset': 'migratory_locust.n.01', 'name': 'migratory_locust'}, {'id': 3864, 'synset': 'migratory_grasshopper.n.01', 'name': 'migratory_grasshopper'}, {'id': 3865, 'synset': 'long-horned_grasshopper.n.01', 'name': 'long-horned_grasshopper'}, {'id': 3866, 'synset': 'katydid.n.01', 'name': 'katydid'}, {'id': 3867, 'synset': 'mormon_cricket.n.01', 'name': 'mormon_cricket'}, {'id': 3868, 'synset': 'sand_cricket.n.01', 'name': 'sand_cricket'}, {'id': 3869, 'synset': 'cricket.n.01', 'name': 'cricket'}, {'id': 3870, 'synset': 'mole_cricket.n.01', 'name': 'mole_cricket'}, {'id': 3871, 'synset': 'european_house_cricket.n.01', 'name': 'European_house_cricket'}, {'id': 3872, 'synset': 'field_cricket.n.01', 'name': 'field_cricket'}, {'id': 3873, 'synset': 'tree_cricket.n.01', 'name': 'tree_cricket'}, {'id': 3874, 'synset': 'snowy_tree_cricket.n.01', 'name': 'snowy_tree_cricket'}, {'id': 3875, 'synset': 'phasmid.n.01', 'name': 'phasmid'}, {'id': 3876, 'synset': 'walking_stick.n.02', 'name': 'walking_stick'}, {'id': 3877, 'synset': 'diapheromera.n.01', 'name': 'diapheromera'}, {'id': 3878, 'synset': 'walking_leaf.n.02', 'name': 'walking_leaf'}, {'id': 3879, 'synset': 'oriental_cockroach.n.01', 'name': 'oriental_cockroach'}, {'id': 3880, 'synset': 'american_cockroach.n.01', 'name': 'American_cockroach'}, {'id': 3881, 'synset': 'australian_cockroach.n.01', 'name': 'Australian_cockroach'}, {'id': 3882, 'synset': 'german_cockroach.n.01', 'name': 'German_cockroach'}, {'id': 3883, 'synset': 'giant_cockroach.n.01', 'name': 'giant_cockroach'}, {'id': 3884, 'synset': 'mantis.n.01', 'name': 'mantis'}, {'id': 3885, 'synset': 'praying_mantis.n.01', 'name': 'praying_mantis'}, {'id': 3886, 'synset': 'bug.n.01', 'name': 'bug'}, {'id': 3887, 'synset': 'hemipterous_insect.n.01', 'name': 'hemipterous_insect'}, {'id': 3888, 'synset': 'leaf_bug.n.01', 'name': 'leaf_bug'}, {'id': 3889, 'synset': 'mirid_bug.n.01', 'name': 'mirid_bug'}, {'id': 3890, 'synset': 'four-lined_plant_bug.n.01', 'name': 'four-lined_plant_bug'}, {'id': 3891, 'synset': 'lygus_bug.n.01', 'name': 'lygus_bug'}, {'id': 3892, 'synset': 'tarnished_plant_bug.n.01', 'name': 'tarnished_plant_bug'}, {'id': 3893, 'synset': 'lace_bug.n.01', 'name': 'lace_bug'}, {'id': 3894, 'synset': 'lygaeid.n.01', 'name': 'lygaeid'}, {'id': 3895, 'synset': 'chinch_bug.n.01', 'name': 'chinch_bug'}, {'id': 3896, 'synset': 'coreid_bug.n.01', 'name': 'coreid_bug'}, {'id': 3897, 'synset': 'squash_bug.n.01', 'name': 'squash_bug'}, {'id': 3898, 'synset': 'leaf-footed_bug.n.01', 'name': 'leaf-footed_bug'}, {'id': 3899, 'synset': 'bedbug.n.01', 'name': 'bedbug'}, {'id': 3900, 'synset': 'backswimmer.n.01', 'name': 'backswimmer'}, {'id': 3901, 'synset': 'true_bug.n.01', 'name': 'true_bug'}, {'id': 3902, 'synset': 'heteropterous_insect.n.01', 'name': 'heteropterous_insect'}, {'id': 3903, 'synset': 'water_bug.n.01', 'name': 'water_bug'}, {'id': 3904, 'synset': 'giant_water_bug.n.01', 'name': 'giant_water_bug'}, {'id': 3905, 'synset': 'water_scorpion.n.01', 'name': 'water_scorpion'}, {'id': 3906, 'synset': 'water_boatman.n.01', 'name': 'water_boatman'}, {'id': 3907, 'synset': 'water_strider.n.01', 'name': 'water_strider'}, {'id': 3908, 'synset': 'common_pond-skater.n.01', 'name': 'common_pond-skater'}, {'id': 3909, 'synset': 'assassin_bug.n.01', 'name': 'assassin_bug'}, {'id': 3910, 'synset': 'conenose.n.01', 'name': 'conenose'}, {'id': 3911, 'synset': 'wheel_bug.n.01', 'name': 'wheel_bug'}, {'id': 3912, 'synset': 'firebug.n.02', 'name': 'firebug'}, {'id': 3913, 'synset': 'cotton_stainer.n.01', 'name': 'cotton_stainer'}, {'id': 3914, 'synset': 'homopterous_insect.n.01', 'name': 'homopterous_insect'}, {'id': 3915, 'synset': 'whitefly.n.01', 'name': 'whitefly'}, {'id': 3916, 'synset': 'citrus_whitefly.n.01', 'name': 'citrus_whitefly'}, {'id': 3917, 'synset': 'greenhouse_whitefly.n.01', 'name': 'greenhouse_whitefly'}, {'id': 3918, 'synset': 'sweet-potato_whitefly.n.01', 'name': 'sweet-potato_whitefly'}, {'id': 3919, 'synset': 'superbug.n.02', 'name': 'superbug'}, {'id': 3920, 'synset': 'cotton_strain.n.01', 'name': 'cotton_strain'}, {'id': 3921, 'synset': 'coccid_insect.n.01', 'name': 'coccid_insect'}, {'id': 3922, 'synset': 'scale_insect.n.01', 'name': 'scale_insect'}, {'id': 3923, 'synset': 'soft_scale.n.01', 'name': 'soft_scale'}, {'id': 3924, 'synset': 'brown_soft_scale.n.01', 'name': 'brown_soft_scale'}, {'id': 3925, 'synset': 'armored_scale.n.01', 'name': 'armored_scale'}, {'id': 3926, 'synset': 'san_jose_scale.n.01', 'name': 'San_Jose_scale'}, {'id': 3927, 'synset': 'cochineal_insect.n.01', 'name': 'cochineal_insect'}, {'id': 3928, 'synset': 'mealybug.n.01', 'name': 'mealybug'}, {'id': 3929, 'synset': 'citrophilous_mealybug.n.01', 'name': 'citrophilous_mealybug'}, {'id': 3930, 'synset': 'comstock_mealybug.n.01', 'name': 'Comstock_mealybug'}, {'id': 3931, 'synset': 'citrus_mealybug.n.01', 'name': 'citrus_mealybug'}, {'id': 3932, 'synset': 'plant_louse.n.01', 'name': 'plant_louse'}, {'id': 3933, 'synset': 'aphid.n.01', 'name': 'aphid'}, {'id': 3934, 'synset': 'apple_aphid.n.01', 'name': 'apple_aphid'}, {'id': 3935, 'synset': 'blackfly.n.01', 'name': 'blackfly'}, {'id': 3936, 'synset': 'greenfly.n.01', 'name': 'greenfly'}, {'id': 3937, 'synset': 'green_peach_aphid.n.01', 'name': 'green_peach_aphid'}, {'id': 3938, 'synset': 'ant_cow.n.01', 'name': 'ant_cow'}, {'id': 3939, 'synset': 'woolly_aphid.n.01', 'name': 'woolly_aphid'}, {'id': 3940, 'synset': 'woolly_apple_aphid.n.01', 'name': 'woolly_apple_aphid'}, {'id': 3941, 'synset': 'woolly_alder_aphid.n.01', 'name': 'woolly_alder_aphid'}, {'id': 3942, 'synset': 'adelgid.n.01', 'name': 'adelgid'}, {'id': 3943, 'synset': 'balsam_woolly_aphid.n.01', 'name': 'balsam_woolly_aphid'}, {'id': 3944, 'synset': 'spruce_gall_aphid.n.01', 'name': 'spruce_gall_aphid'}, {'id': 3945, 'synset': 'woolly_adelgid.n.01', 'name': 'woolly_adelgid'}, {'id': 3946, 'synset': 'jumping_plant_louse.n.01', 'name': 'jumping_plant_louse'}, {'id': 3947, 'synset': 'cicada.n.01', 'name': 'cicada'}, {'id': 3948, 'synset': 'dog-day_cicada.n.01', 'name': 'dog-day_cicada'}, {'id': 3949, 'synset': 'seventeen-year_locust.n.01', 'name': 'seventeen-year_locust'}, {'id': 3950, 'synset': 'spittle_insect.n.01', 'name': 'spittle_insect'}, {'id': 3951, 'synset': 'froghopper.n.01', 'name': 'froghopper'}, {'id': 3952, 'synset': 'meadow_spittlebug.n.01', 'name': 'meadow_spittlebug'}, {'id': 3953, 'synset': 'pine_spittlebug.n.01', 'name': 'pine_spittlebug'}, {'id': 3954, 'synset': 'saratoga_spittlebug.n.01', 'name': 'Saratoga_spittlebug'}, {'id': 3955, 'synset': 'leafhopper.n.01', 'name': 'leafhopper'}, {'id': 3956, 'synset': 'plant_hopper.n.01', 'name': 'plant_hopper'}, {'id': 3957, 'synset': 'treehopper.n.01', 'name': 'treehopper'}, {'id': 3958, 'synset': 'lantern_fly.n.01', 'name': 'lantern_fly'}, {'id': 3959, 'synset': 'psocopterous_insect.n.01', 'name': 'psocopterous_insect'}, {'id': 3960, 'synset': 'psocid.n.01', 'name': 'psocid'}, {'id': 3961, 'synset': 'bark-louse.n.01', 'name': 'bark-louse'}, {'id': 3962, 'synset': 'booklouse.n.01', 'name': 'booklouse'}, {'id': 3963, 'synset': 'common_booklouse.n.01', 'name': 'common_booklouse'}, {'id': 3964, 'synset': 'ephemerid.n.01', 'name': 'ephemerid'}, {'id': 3965, 'synset': 'mayfly.n.01', 'name': 'mayfly'}, {'id': 3966, 'synset': 'stonefly.n.01', 'name': 'stonefly'}, {'id': 3967, 'synset': 'neuropteron.n.01', 'name': 'neuropteron'}, {'id': 3968, 'synset': 'ant_lion.n.02', 'name': 'ant_lion'}, {'id': 3969, 'synset': 'doodlebug.n.03', 'name': 'doodlebug'}, {'id': 3970, 'synset': 'lacewing.n.01', 'name': 'lacewing'}, {'id': 3971, 'synset': 'aphid_lion.n.01', 'name': 'aphid_lion'}, {'id': 3972, 'synset': 'green_lacewing.n.01', 'name': 'green_lacewing'}, {'id': 3973, 'synset': 'brown_lacewing.n.01', 'name': 'brown_lacewing'}, {'id': 3974, 'synset': 'dobson.n.02', 'name': 'dobson'}, {'id': 3975, 'synset': 'hellgrammiate.n.01', 'name': 'hellgrammiate'}, {'id': 3976, 'synset': 'fish_fly.n.01', 'name': 'fish_fly'}, {'id': 3977, 'synset': 'alderfly.n.01', 'name': 'alderfly'}, {'id': 3978, 'synset': 'snakefly.n.01', 'name': 'snakefly'}, {'id': 3979, 'synset': 'mantispid.n.01', 'name': 'mantispid'}, {'id': 3980, 'synset': 'odonate.n.01', 'name': 'odonate'}, {'id': 3981, 'synset': 'damselfly.n.01', 'name': 'damselfly'}, {'id': 3982, 'synset': 'trichopterous_insect.n.01', 'name': 'trichopterous_insect'}, {'id': 3983, 'synset': 'caddis_fly.n.01', 'name': 'caddis_fly'}, {'id': 3984, 'synset': 'caseworm.n.01', 'name': 'caseworm'}, {'id': 3985, 'synset': 'caddisworm.n.01', 'name': 'caddisworm'}, {'id': 3986, 'synset': 'thysanuran_insect.n.01', 'name': 'thysanuran_insect'}, {'id': 3987, 'synset': 'bristletail.n.01', 'name': 'bristletail'}, {'id': 3988, 'synset': 'silverfish.n.01', 'name': 'silverfish'}, {'id': 3989, 'synset': 'firebrat.n.01', 'name': 'firebrat'}, {'id': 3990, 'synset': 'jumping_bristletail.n.01', 'name': 'jumping_bristletail'}, {'id': 3991, 'synset': 'thysanopter.n.01', 'name': 'thysanopter'}, {'id': 3992, 'synset': 'thrips.n.01', 'name': 'thrips'}, {'id': 3993, 'synset': 'tobacco_thrips.n.01', 'name': 'tobacco_thrips'}, {'id': 3994, 'synset': 'onion_thrips.n.01', 'name': 'onion_thrips'}, {'id': 3995, 'synset': 'earwig.n.01', 'name': 'earwig'}, {'id': 3996, 'synset': 'common_european_earwig.n.01', 'name': 'common_European_earwig'}, {'id': 3997, 'synset': 'lepidopterous_insect.n.01', 'name': 'lepidopterous_insect'}, {'id': 3998, 'synset': 'nymphalid.n.01', 'name': 'nymphalid'}, {'id': 3999, 'synset': 'mourning_cloak.n.01', 'name': 'mourning_cloak'}, {'id': 4000, 'synset': 'tortoiseshell.n.02', 'name': 'tortoiseshell'}, {'id': 4001, 'synset': 'painted_beauty.n.01', 'name': 'painted_beauty'}, {'id': 4002, 'synset': 'admiral.n.02', 'name': 'admiral'}, {'id': 4003, 'synset': 'red_admiral.n.01', 'name': 'red_admiral'}, {'id': 4004, 'synset': 'white_admiral.n.02', 'name': 'white_admiral'}, {'id': 4005, 'synset': 'banded_purple.n.01', 'name': 'banded_purple'}, {'id': 4006, 'synset': 'red-spotted_purple.n.01', 'name': 'red-spotted_purple'}, {'id': 4007, 'synset': 'viceroy.n.02', 'name': 'viceroy'}, {'id': 4008, 'synset': 'anglewing.n.01', 'name': 'anglewing'}, {'id': 4009, 'synset': 'ringlet.n.04', 'name': 'ringlet'}, {'id': 4010, 'synset': 'comma.n.02', 'name': 'comma'}, {'id': 4011, 'synset': 'fritillary.n.02', 'name': 'fritillary'}, {'id': 4012, 'synset': 'silverspot.n.01', 'name': 'silverspot'}, {'id': 4013, 'synset': 'emperor_butterfly.n.01', 'name': 'emperor_butterfly'}, {'id': 4014, 'synset': 'purple_emperor.n.01', 'name': 'purple_emperor'}, {'id': 4015, 'synset': 'peacock.n.01', 'name': 'peacock'}, {'id': 4016, 'synset': 'danaid.n.01', 'name': 'danaid'}, {'id': 4017, 'synset': 'monarch.n.02', 'name': 'monarch'}, {'id': 4018, 'synset': 'pierid.n.01', 'name': 'pierid'}, {'id': 4019, 'synset': 'cabbage_butterfly.n.01', 'name': 'cabbage_butterfly'}, {'id': 4020, 'synset': 'small_white.n.01', 'name': 'small_white'}, {'id': 4021, 'synset': 'large_white.n.01', 'name': 'large_white'}, {'id': 4022, 'synset': 'southern_cabbage_butterfly.n.01', 'name': 'southern_cabbage_butterfly'}, {'id': 4023, 'synset': 'sulphur_butterfly.n.01', 'name': 'sulphur_butterfly'}, {'id': 4024, 'synset': 'lycaenid.n.01', 'name': 'lycaenid'}, {'id': 4025, 'synset': 'blue.n.07', 'name': 'blue'}, {'id': 4026, 'synset': 'copper.n.05', 'name': 'copper'}, {'id': 4027, 'synset': 'american_copper.n.01', 'name': 'American_copper'}, {'id': 4028, 'synset': 'hairstreak.n.01', 'name': 'hairstreak'}, {'id': 4029, 'synset': 'strymon_melinus.n.01', 'name': 'Strymon_melinus'}, {'id': 4030, 'synset': 'moth.n.01', 'name': 'moth'}, {'id': 4031, 'synset': 'moth_miller.n.01', 'name': 'moth_miller'}, {'id': 4032, 'synset': 'tortricid.n.01', 'name': 'tortricid'}, {'id': 4033, 'synset': 'leaf_roller.n.01', 'name': 'leaf_roller'}, {'id': 4034, 'synset': 'tea_tortrix.n.01', 'name': 'tea_tortrix'}, {'id': 4035, 'synset': 'orange_tortrix.n.01', 'name': 'orange_tortrix'}, {'id': 4036, 'synset': 'codling_moth.n.01', 'name': 'codling_moth'}, {'id': 4037, 'synset': 'lymantriid.n.01', 'name': 'lymantriid'}, {'id': 4038, 'synset': 'tussock_caterpillar.n.01', 'name': 'tussock_caterpillar'}, {'id': 4039, 'synset': 'gypsy_moth.n.01', 'name': 'gypsy_moth'}, {'id': 4040, 'synset': 'browntail.n.01', 'name': 'browntail'}, {'id': 4041, 'synset': 'gold-tail_moth.n.01', 'name': 'gold-tail_moth'}, {'id': 4042, 'synset': 'geometrid.n.01', 'name': 'geometrid'}, {'id': 4043, 'synset': 'paleacrita_vernata.n.01', 'name': 'Paleacrita_vernata'}, {'id': 4044, 'synset': 'alsophila_pometaria.n.01', 'name': 'Alsophila_pometaria'}, {'id': 4045, 'synset': 'cankerworm.n.01', 'name': 'cankerworm'}, {'id': 4046, 'synset': 'spring_cankerworm.n.01', 'name': 'spring_cankerworm'}, {'id': 4047, 'synset': 'fall_cankerworm.n.01', 'name': 'fall_cankerworm'}, {'id': 4048, 'synset': 'measuring_worm.n.01', 'name': 'measuring_worm'}, {'id': 4049, 'synset': 'pyralid.n.01', 'name': 'pyralid'}, {'id': 4050, 'synset': 'bee_moth.n.01', 'name': 'bee_moth'}, {'id': 4051, 'synset': 'corn_borer.n.02', 'name': 'corn_borer'}, {'id': 4052, 'synset': 'mediterranean_flour_moth.n.01', 'name': 'Mediterranean_flour_moth'}, {'id': 4053, 'synset': 'tobacco_moth.n.01', 'name': 'tobacco_moth'}, {'id': 4054, 'synset': 'almond_moth.n.01', 'name': 'almond_moth'}, {'id': 4055, 'synset': 'raisin_moth.n.01', 'name': 'raisin_moth'}, {'id': 4056, 'synset': 'tineoid.n.01', 'name': 'tineoid'}, {'id': 4057, 'synset': 'tineid.n.01', 'name': 'tineid'}, {'id': 4058, 'synset': 'clothes_moth.n.01', 'name': 'clothes_moth'}, {'id': 4059, 'synset': 'casemaking_clothes_moth.n.01', 'name': 'casemaking_clothes_moth'}, {'id': 4060, 'synset': 'webbing_clothes_moth.n.01', 'name': 'webbing_clothes_moth'}, {'id': 4061, 'synset': 'carpet_moth.n.01', 'name': 'carpet_moth'}, {'id': 4062, 'synset': 'gelechiid.n.01', 'name': 'gelechiid'}, {'id': 4063, 'synset': 'grain_moth.n.01', 'name': 'grain_moth'}, {'id': 4064, 'synset': 'angoumois_moth.n.01', 'name': 'angoumois_moth'}, {'id': 4065, 'synset': 'potato_moth.n.01', 'name': 'potato_moth'}, {'id': 4066, 'synset': 'potato_tuberworm.n.01', 'name': 'potato_tuberworm'}, {'id': 4067, 'synset': 'noctuid_moth.n.01', 'name': 'noctuid_moth'}, {'id': 4068, 'synset': 'cutworm.n.01', 'name': 'cutworm'}, {'id': 4069, 'synset': 'underwing.n.01', 'name': 'underwing'}, {'id': 4070, 'synset': 'red_underwing.n.01', 'name': 'red_underwing'}, {'id': 4071, 'synset': 'antler_moth.n.01', 'name': 'antler_moth'}, {'id': 4072, 'synset': 'heliothis_moth.n.01', 'name': 'heliothis_moth'}, {'id': 4073, 'synset': 'army_cutworm.n.01', 'name': 'army_cutworm'}, {'id': 4074, 'synset': 'armyworm.n.02', 'name': 'armyworm'}, {'id': 4075, 'synset': 'armyworm.n.01', 'name': 'armyworm'}, {'id': 4076, 'synset': 'spodoptera_exigua.n.02', 'name': 'Spodoptera_exigua'}, {'id': 4077, 'synset': 'beet_armyworm.n.01', 'name': 'beet_armyworm'}, {'id': 4078, 'synset': 'spodoptera_frugiperda.n.02', 'name': 'Spodoptera_frugiperda'}, {'id': 4079, 'synset': 'fall_armyworm.n.01', 'name': 'fall_armyworm'}, {'id': 4080, 'synset': 'hawkmoth.n.01', 'name': 'hawkmoth'}, {'id': 4081, 'synset': 'manduca_sexta.n.02', 'name': 'Manduca_sexta'}, {'id': 4082, 'synset': 'tobacco_hornworm.n.01', 'name': 'tobacco_hornworm'}, {'id': 4083, 'synset': 'manduca_quinquemaculata.n.02', 'name': 'Manduca_quinquemaculata'}, {'id': 4084, 'synset': 'tomato_hornworm.n.01', 'name': 'tomato_hornworm'}, {'id': 4085, 'synset': "death's-head_moth.n.01", 'name': "death's-head_moth"}, {'id': 4086, 'synset': 'bombycid.n.01', 'name': 'bombycid'}, {'id': 4087, 'synset': 'domestic_silkworm_moth.n.01', 'name': 'domestic_silkworm_moth'}, {'id': 4088, 'synset': 'silkworm.n.01', 'name': 'silkworm'}, {'id': 4089, 'synset': 'saturniid.n.01', 'name': 'saturniid'}, {'id': 4090, 'synset': 'emperor.n.03', 'name': 'emperor'}, {'id': 4091, 'synset': 'imperial_moth.n.01', 'name': 'imperial_moth'}, {'id': 4092, 'synset': 'giant_silkworm_moth.n.01', 'name': 'giant_silkworm_moth'}, {'id': 4093, 'synset': 'silkworm.n.02', 'name': 'silkworm'}, {'id': 4094, 'synset': 'luna_moth.n.01', 'name': 'luna_moth'}, {'id': 4095, 'synset': 'cecropia.n.02', 'name': 'cecropia'}, {'id': 4096, 'synset': 'cynthia_moth.n.01', 'name': 'cynthia_moth'}, {'id': 4097, 'synset': 'ailanthus_silkworm.n.01', 'name': 'ailanthus_silkworm'}, {'id': 4098, 'synset': 'io_moth.n.01', 'name': 'io_moth'}, {'id': 4099, 'synset': 'polyphemus_moth.n.01', 'name': 'polyphemus_moth'}, {'id': 4100, 'synset': 'pernyi_moth.n.01', 'name': 'pernyi_moth'}, {'id': 4101, 'synset': 'tussah.n.01', 'name': 'tussah'}, {'id': 4102, 'synset': 'atlas_moth.n.01', 'name': 'atlas_moth'}, {'id': 4103, 'synset': 'arctiid.n.01', 'name': 'arctiid'}, {'id': 4104, 'synset': 'tiger_moth.n.01', 'name': 'tiger_moth'}, {'id': 4105, 'synset': 'cinnabar.n.02', 'name': 'cinnabar'}, {'id': 4106, 'synset': 'lasiocampid.n.01', 'name': 'lasiocampid'}, {'id': 4107, 'synset': 'eggar.n.01', 'name': 'eggar'}, {'id': 4108, 'synset': 'tent-caterpillar_moth.n.02', 'name': 'tent-caterpillar_moth'}, {'id': 4109, 'synset': 'tent_caterpillar.n.01', 'name': 'tent_caterpillar'}, {'id': 4110, 'synset': 'tent-caterpillar_moth.n.01', 'name': 'tent-caterpillar_moth'}, {'id': 4111, 'synset': 'forest_tent_caterpillar.n.01', 'name': 'forest_tent_caterpillar'}, {'id': 4112, 'synset': 'lappet.n.03', 'name': 'lappet'}, {'id': 4113, 'synset': 'lappet_caterpillar.n.01', 'name': 'lappet_caterpillar'}, {'id': 4114, 'synset': 'webworm.n.01', 'name': 'webworm'}, {'id': 4115, 'synset': 'webworm_moth.n.01', 'name': 'webworm_moth'}, {'id': 4116, 'synset': 'hyphantria_cunea.n.02', 'name': 'Hyphantria_cunea'}, {'id': 4117, 'synset': 'fall_webworm.n.01', 'name': 'fall_webworm'}, {'id': 4118, 'synset': 'garden_webworm.n.01', 'name': 'garden_webworm'}, {'id': 4119, 'synset': 'instar.n.01', 'name': 'instar'}, {'id': 4120, 'synset': 'caterpillar.n.01', 'name': 'caterpillar'}, {'id': 4121, 'synset': 'corn_borer.n.01', 'name': 'corn_borer'}, {'id': 4122, 'synset': 'bollworm.n.01', 'name': 'bollworm'}, {'id': 4123, 'synset': 'pink_bollworm.n.01', 'name': 'pink_bollworm'}, {'id': 4124, 'synset': 'corn_earworm.n.01', 'name': 'corn_earworm'}, {'id': 4125, 'synset': 'cabbageworm.n.01', 'name': 'cabbageworm'}, {'id': 4126, 'synset': 'woolly_bear.n.01', 'name': 'woolly_bear'}, {'id': 4127, 'synset': 'woolly_bear_moth.n.01', 'name': 'woolly_bear_moth'}, {'id': 4128, 'synset': 'larva.n.01', 'name': 'larva'}, {'id': 4129, 'synset': 'nymph.n.02', 'name': 'nymph'}, {'id': 4130, 'synset': 'leptocephalus.n.01', 'name': 'leptocephalus'}, {'id': 4131, 'synset': 'grub.n.02', 'name': 'grub'}, {'id': 4132, 'synset': 'maggot.n.01', 'name': 'maggot'}, {'id': 4133, 'synset': 'leatherjacket.n.03', 'name': 'leatherjacket'}, {'id': 4134, 'synset': 'pupa.n.01', 'name': 'pupa'}, {'id': 4135, 'synset': 'chrysalis.n.01', 'name': 'chrysalis'}, {'id': 4136, 'synset': 'imago.n.02', 'name': 'imago'}, {'id': 4137, 'synset': 'queen.n.01', 'name': 'queen'}, {'id': 4138, 'synset': 'phoronid.n.01', 'name': 'phoronid'}, {'id': 4139, 'synset': 'bryozoan.n.01', 'name': 'bryozoan'}, {'id': 4140, 'synset': 'brachiopod.n.01', 'name': 'brachiopod'}, {'id': 4141, 'synset': 'peanut_worm.n.01', 'name': 'peanut_worm'}, {'id': 4142, 'synset': 'echinoderm.n.01', 'name': 'echinoderm'}, {'id': 4143, 'synset': 'brittle_star.n.01', 'name': 'brittle_star'}, {'id': 4144, 'synset': 'basket_star.n.01', 'name': 'basket_star'}, {'id': 4145, 'synset': 'astrophyton_muricatum.n.01', 'name': 'Astrophyton_muricatum'}, {'id': 4146, 'synset': 'sea_urchin.n.01', 'name': 'sea_urchin'}, {'id': 4147, 'synset': 'edible_sea_urchin.n.01', 'name': 'edible_sea_urchin'}, {'id': 4148, 'synset': 'sand_dollar.n.01', 'name': 'sand_dollar'}, {'id': 4149, 'synset': 'heart_urchin.n.01', 'name': 'heart_urchin'}, {'id': 4150, 'synset': 'crinoid.n.01', 'name': 'crinoid'}, {'id': 4151, 'synset': 'sea_lily.n.01', 'name': 'sea_lily'}, {'id': 4152, 'synset': 'feather_star.n.01', 'name': 'feather_star'}, {'id': 4153, 'synset': 'sea_cucumber.n.01', 'name': 'sea_cucumber'}, {'id': 4154, 'synset': 'trepang.n.01', 'name': 'trepang'}, {'id': 4155, 'synset': 'duplicidentata.n.01', 'name': 'Duplicidentata'}, {'id': 4156, 'synset': 'lagomorph.n.01', 'name': 'lagomorph'}, {'id': 4157, 'synset': 'leporid.n.01', 'name': 'leporid'}, {'id': 4158, 'synset': 'rabbit_ears.n.02', 'name': 'rabbit_ears'}, {'id': 4159, 'synset': 'lapin.n.02', 'name': 'lapin'}, {'id': 4160, 'synset': 'bunny.n.02', 'name': 'bunny'}, {'id': 4161, 'synset': 'european_rabbit.n.01', 'name': 'European_rabbit'}, {'id': 4162, 'synset': 'wood_rabbit.n.01', 'name': 'wood_rabbit'}, {'id': 4163, 'synset': 'eastern_cottontail.n.01', 'name': 'eastern_cottontail'}, {'id': 4164, 'synset': 'swamp_rabbit.n.02', 'name': 'swamp_rabbit'}, {'id': 4165, 'synset': 'marsh_hare.n.01', 'name': 'marsh_hare'}, {'id': 4166, 'synset': 'hare.n.01', 'name': 'hare'}, {'id': 4167, 'synset': 'leveret.n.01', 'name': 'leveret'}, {'id': 4168, 'synset': 'european_hare.n.01', 'name': 'European_hare'}, {'id': 4169, 'synset': 'jackrabbit.n.01', 'name': 'jackrabbit'}, {'id': 4170, 'synset': 'white-tailed_jackrabbit.n.01', 'name': 'white-tailed_jackrabbit'}, {'id': 4171, 'synset': 'blacktail_jackrabbit.n.01', 'name': 'blacktail_jackrabbit'}, {'id': 4172, 'synset': 'polar_hare.n.01', 'name': 'polar_hare'}, {'id': 4173, 'synset': 'snowshoe_hare.n.01', 'name': 'snowshoe_hare'}, {'id': 4174, 'synset': 'belgian_hare.n.01', 'name': 'Belgian_hare'}, {'id': 4175, 'synset': 'angora.n.03', 'name': 'Angora'}, {'id': 4176, 'synset': 'pika.n.01', 'name': 'pika'}, {'id': 4177, 'synset': 'little_chief_hare.n.01', 'name': 'little_chief_hare'}, {'id': 4178, 'synset': 'collared_pika.n.01', 'name': 'collared_pika'}, {'id': 4179, 'synset': 'mouse.n.01', 'name': 'mouse'}, {'id': 4180, 'synset': 'pocket_rat.n.01', 'name': 'pocket_rat'}, {'id': 4181, 'synset': 'murine.n.01', 'name': 'murine'}, {'id': 4182, 'synset': 'house_mouse.n.01', 'name': 'house_mouse'}, {'id': 4183, 'synset': 'harvest_mouse.n.02', 'name': 'harvest_mouse'}, {'id': 4184, 'synset': 'field_mouse.n.02', 'name': 'field_mouse'}, {'id': 4185, 'synset': 'nude_mouse.n.01', 'name': 'nude_mouse'}, {'id': 4186, 'synset': 'european_wood_mouse.n.01', 'name': 'European_wood_mouse'}, {'id': 4187, 'synset': 'brown_rat.n.01', 'name': 'brown_rat'}, {'id': 4188, 'synset': 'wharf_rat.n.02', 'name': 'wharf_rat'}, {'id': 4189, 'synset': 'sewer_rat.n.01', 'name': 'sewer_rat'}, {'id': 4190, 'synset': 'black_rat.n.01', 'name': 'black_rat'}, {'id': 4191, 'synset': 'bandicoot_rat.n.01', 'name': 'bandicoot_rat'}, {'id': 4192, 'synset': 'jerboa_rat.n.01', 'name': 'jerboa_rat'}, {'id': 4193, 'synset': 'kangaroo_mouse.n.02', 'name': 'kangaroo_mouse'}, {'id': 4194, 'synset': 'water_rat.n.03', 'name': 'water_rat'}, {'id': 4195, 'synset': 'beaver_rat.n.01', 'name': 'beaver_rat'}, {'id': 4196, 'synset': 'new_world_mouse.n.01', 'name': 'New_World_mouse'}, {'id': 4197, 'synset': 'american_harvest_mouse.n.01', 'name': 'American_harvest_mouse'}, {'id': 4198, 'synset': 'wood_mouse.n.01', 'name': 'wood_mouse'}, {'id': 4199, 'synset': 'white-footed_mouse.n.01', 'name': 'white-footed_mouse'}, {'id': 4200, 'synset': 'deer_mouse.n.01', 'name': 'deer_mouse'}, {'id': 4201, 'synset': 'cactus_mouse.n.01', 'name': 'cactus_mouse'}, {'id': 4202, 'synset': 'cotton_mouse.n.01', 'name': 'cotton_mouse'}, {'id': 4203, 'synset': 'pygmy_mouse.n.01', 'name': 'pygmy_mouse'}, {'id': 4204, 'synset': 'grasshopper_mouse.n.01', 'name': 'grasshopper_mouse'}, {'id': 4205, 'synset': 'muskrat.n.02', 'name': 'muskrat'}, {'id': 4206, 'synset': 'round-tailed_muskrat.n.01', 'name': 'round-tailed_muskrat'}, {'id': 4207, 'synset': 'cotton_rat.n.01', 'name': 'cotton_rat'}, {'id': 4208, 'synset': 'wood_rat.n.01', 'name': 'wood_rat'}, {'id': 4209, 'synset': 'dusky-footed_wood_rat.n.01', 'name': 'dusky-footed_wood_rat'}, {'id': 4210, 'synset': 'vole.n.01', 'name': 'vole'}, {'id': 4211, 'synset': 'packrat.n.02', 'name': 'packrat'}, {'id': 4212, 'synset': 'dusky-footed_woodrat.n.01', 'name': 'dusky-footed_woodrat'}, {'id': 4213, 'synset': 'eastern_woodrat.n.01', 'name': 'eastern_woodrat'}, {'id': 4214, 'synset': 'rice_rat.n.01', 'name': 'rice_rat'}, {'id': 4215, 'synset': 'pine_vole.n.01', 'name': 'pine_vole'}, {'id': 4216, 'synset': 'meadow_vole.n.01', 'name': 'meadow_vole'}, {'id': 4217, 'synset': 'water_vole.n.02', 'name': 'water_vole'}, {'id': 4218, 'synset': 'prairie_vole.n.01', 'name': 'prairie_vole'}, {'id': 4219, 'synset': 'water_vole.n.01', 'name': 'water_vole'}, {'id': 4220, 'synset': 'red-backed_mouse.n.01', 'name': 'red-backed_mouse'}, {'id': 4221, 'synset': 'phenacomys.n.01', 'name': 'phenacomys'}, {'id': 4222, 'synset': 'eurasian_hamster.n.01', 'name': 'Eurasian_hamster'}, {'id': 4223, 'synset': 'golden_hamster.n.01', 'name': 'golden_hamster'}, {'id': 4224, 'synset': 'gerbil.n.01', 'name': 'gerbil'}, {'id': 4225, 'synset': 'jird.n.01', 'name': 'jird'}, {'id': 4226, 'synset': 'tamarisk_gerbil.n.01', 'name': 'tamarisk_gerbil'}, {'id': 4227, 'synset': 'sand_rat.n.02', 'name': 'sand_rat'}, {'id': 4228, 'synset': 'lemming.n.01', 'name': 'lemming'}, {'id': 4229, 'synset': 'european_lemming.n.01', 'name': 'European_lemming'}, {'id': 4230, 'synset': 'brown_lemming.n.01', 'name': 'brown_lemming'}, {'id': 4231, 'synset': 'grey_lemming.n.01', 'name': 'grey_lemming'}, {'id': 4232, 'synset': 'pied_lemming.n.01', 'name': 'pied_lemming'}, {'id': 4233, 'synset': 'hudson_bay_collared_lemming.n.01', 'name': 'Hudson_bay_collared_lemming'}, {'id': 4234, 'synset': 'southern_bog_lemming.n.01', 'name': 'southern_bog_lemming'}, {'id': 4235, 'synset': 'northern_bog_lemming.n.01', 'name': 'northern_bog_lemming'}, {'id': 4236, 'synset': 'porcupine.n.01', 'name': 'porcupine'}, {'id': 4237, 'synset': 'old_world_porcupine.n.01', 'name': 'Old_World_porcupine'}, {'id': 4238, 'synset': 'brush-tailed_porcupine.n.01', 'name': 'brush-tailed_porcupine'}, {'id': 4239, 'synset': 'long-tailed_porcupine.n.01', 'name': 'long-tailed_porcupine'}, {'id': 4240, 'synset': 'new_world_porcupine.n.01', 'name': 'New_World_porcupine'}, {'id': 4241, 'synset': 'canada_porcupine.n.01', 'name': 'Canada_porcupine'}, {'id': 4242, 'synset': 'pocket_mouse.n.01', 'name': 'pocket_mouse'}, {'id': 4243, 'synset': 'silky_pocket_mouse.n.01', 'name': 'silky_pocket_mouse'}, {'id': 4244, 'synset': 'plains_pocket_mouse.n.01', 'name': 'plains_pocket_mouse'}, {'id': 4245, 'synset': 'hispid_pocket_mouse.n.01', 'name': 'hispid_pocket_mouse'}, {'id': 4246, 'synset': 'mexican_pocket_mouse.n.01', 'name': 'Mexican_pocket_mouse'}, {'id': 4247, 'synset': 'kangaroo_rat.n.01', 'name': 'kangaroo_rat'}, {'id': 4248, 'synset': 'ord_kangaroo_rat.n.01', 'name': 'Ord_kangaroo_rat'}, {'id': 4249, 'synset': 'kangaroo_mouse.n.01', 'name': 'kangaroo_mouse'}, {'id': 4250, 'synset': 'jumping_mouse.n.01', 'name': 'jumping_mouse'}, {'id': 4251, 'synset': 'meadow_jumping_mouse.n.01', 'name': 'meadow_jumping_mouse'}, {'id': 4252, 'synset': 'jerboa.n.01', 'name': 'jerboa'}, {'id': 4253, 'synset': 'typical_jerboa.n.01', 'name': 'typical_jerboa'}, {'id': 4254, 'synset': 'jaculus_jaculus.n.01', 'name': 'Jaculus_jaculus'}, {'id': 4255, 'synset': 'dormouse.n.01', 'name': 'dormouse'}, {'id': 4256, 'synset': 'loir.n.01', 'name': 'loir'}, {'id': 4257, 'synset': 'hazel_mouse.n.01', 'name': 'hazel_mouse'}, {'id': 4258, 'synset': 'lerot.n.01', 'name': 'lerot'}, {'id': 4259, 'synset': 'gopher.n.04', 'name': 'gopher'}, {'id': 4260, 'synset': 'plains_pocket_gopher.n.01', 'name': 'plains_pocket_gopher'}, {'id': 4261, 'synset': 'southeastern_pocket_gopher.n.01', 'name': 'southeastern_pocket_gopher'}, {'id': 4262, 'synset': 'valley_pocket_gopher.n.01', 'name': 'valley_pocket_gopher'}, {'id': 4263, 'synset': 'northern_pocket_gopher.n.01', 'name': 'northern_pocket_gopher'}, {'id': 4264, 'synset': 'tree_squirrel.n.01', 'name': 'tree_squirrel'}, {'id': 4265, 'synset': 'eastern_grey_squirrel.n.01', 'name': 'eastern_grey_squirrel'}, {'id': 4266, 'synset': 'western_grey_squirrel.n.01', 'name': 'western_grey_squirrel'}, {'id': 4267, 'synset': 'fox_squirrel.n.01', 'name': 'fox_squirrel'}, {'id': 4268, 'synset': 'black_squirrel.n.01', 'name': 'black_squirrel'}, {'id': 4269, 'synset': 'red_squirrel.n.02', 'name': 'red_squirrel'}, {'id': 4270, 'synset': 'american_red_squirrel.n.01', 'name': 'American_red_squirrel'}, {'id': 4271, 'synset': 'chickeree.n.01', 'name': 'chickeree'}, {'id': 4272, 'synset': 'antelope_squirrel.n.01', 'name': 'antelope_squirrel'}, {'id': 4273, 'synset': 'ground_squirrel.n.02', 'name': 'ground_squirrel'}, {'id': 4274, 'synset': 'mantled_ground_squirrel.n.01', 'name': 'mantled_ground_squirrel'}, {'id': 4275, 'synset': 'suslik.n.01', 'name': 'suslik'}, {'id': 4276, 'synset': 'flickertail.n.01', 'name': 'flickertail'}, {'id': 4277, 'synset': 'rock_squirrel.n.01', 'name': 'rock_squirrel'}, {'id': 4278, 'synset': 'arctic_ground_squirrel.n.01', 'name': 'Arctic_ground_squirrel'}, {'id': 4279, 'synset': 'prairie_dog.n.01', 'name': 'prairie_dog'}, {'id': 4280, 'synset': 'blacktail_prairie_dog.n.01', 'name': 'blacktail_prairie_dog'}, {'id': 4281, 'synset': 'whitetail_prairie_dog.n.01', 'name': 'whitetail_prairie_dog'}, {'id': 4282, 'synset': 'eastern_chipmunk.n.01', 'name': 'eastern_chipmunk'}, {'id': 4283, 'synset': 'chipmunk.n.01', 'name': 'chipmunk'}, {'id': 4284, 'synset': 'baronduki.n.01', 'name': 'baronduki'}, {'id': 4285, 'synset': 'american_flying_squirrel.n.01', 'name': 'American_flying_squirrel'}, {'id': 4286, 'synset': 'southern_flying_squirrel.n.01', 'name': 'southern_flying_squirrel'}, {'id': 4287, 'synset': 'northern_flying_squirrel.n.01', 'name': 'northern_flying_squirrel'}, {'id': 4288, 'synset': 'marmot.n.01', 'name': 'marmot'}, {'id': 4289, 'synset': 'groundhog.n.01', 'name': 'groundhog'}, {'id': 4290, 'synset': 'hoary_marmot.n.01', 'name': 'hoary_marmot'}, {'id': 4291, 'synset': 'yellowbelly_marmot.n.01', 'name': 'yellowbelly_marmot'}, {'id': 4292, 'synset': 'asiatic_flying_squirrel.n.01', 'name': 'Asiatic_flying_squirrel'}, {'id': 4293, 'synset': 'beaver.n.07', 'name': 'beaver'}, {'id': 4294, 'synset': 'old_world_beaver.n.01', 'name': 'Old_World_beaver'}, {'id': 4295, 'synset': 'new_world_beaver.n.01', 'name': 'New_World_beaver'}, {'id': 4296, 'synset': 'mountain_beaver.n.01', 'name': 'mountain_beaver'}, {'id': 4297, 'synset': 'cavy.n.01', 'name': 'cavy'}, {'id': 4298, 'synset': 'guinea_pig.n.02', 'name': 'guinea_pig'}, {'id': 4299, 'synset': 'aperea.n.01', 'name': 'aperea'}, {'id': 4300, 'synset': 'mara.n.02', 'name': 'mara'}, {'id': 4301, 'synset': 'capybara.n.01', 'name': 'capybara'}, {'id': 4302, 'synset': 'agouti.n.01', 'name': 'agouti'}, {'id': 4303, 'synset': 'paca.n.01', 'name': 'paca'}, {'id': 4304, 'synset': 'mountain_paca.n.01', 'name': 'mountain_paca'}, {'id': 4305, 'synset': 'coypu.n.01', 'name': 'coypu'}, {'id': 4306, 'synset': 'chinchilla.n.03', 'name': 'chinchilla'}, {'id': 4307, 'synset': 'mountain_chinchilla.n.01', 'name': 'mountain_chinchilla'}, {'id': 4308, 'synset': 'viscacha.n.01', 'name': 'viscacha'}, {'id': 4309, 'synset': 'abrocome.n.01', 'name': 'abrocome'}, {'id': 4310, 'synset': 'mole_rat.n.02', 'name': 'mole_rat'}, {'id': 4311, 'synset': 'mole_rat.n.01', 'name': 'mole_rat'}, {'id': 4312, 'synset': 'sand_rat.n.01', 'name': 'sand_rat'}, {'id': 4313, 'synset': 'naked_mole_rat.n.01', 'name': 'naked_mole_rat'}, {'id': 4314, 'synset': 'queen.n.09', 'name': 'queen'}, {'id': 4315, 'synset': 'damaraland_mole_rat.n.01', 'name': 'Damaraland_mole_rat'}, {'id': 4316, 'synset': 'ungulata.n.01', 'name': 'Ungulata'}, {'id': 4317, 'synset': 'ungulate.n.01', 'name': 'ungulate'}, {'id': 4318, 'synset': 'unguiculate.n.01', 'name': 'unguiculate'}, {'id': 4319, 'synset': 'dinoceras.n.01', 'name': 'dinoceras'}, {'id': 4320, 'synset': 'hyrax.n.01', 'name': 'hyrax'}, {'id': 4321, 'synset': 'rock_hyrax.n.01', 'name': 'rock_hyrax'}, {'id': 4322, 'synset': 'odd-toed_ungulate.n.01', 'name': 'odd-toed_ungulate'}, {'id': 4323, 'synset': 'equine.n.01', 'name': 'equine'}, {'id': 4324, 'synset': 'roan.n.02', 'name': 'roan'}, {'id': 4325, 'synset': 'stablemate.n.01', 'name': 'stablemate'}, {'id': 4326, 'synset': 'gee-gee.n.01', 'name': 'gee-gee'}, {'id': 4327, 'synset': 'eohippus.n.01', 'name': 'eohippus'}, {'id': 4328, 'synset': 'filly.n.01', 'name': 'filly'}, {'id': 4329, 'synset': 'colt.n.01', 'name': 'colt'}, {'id': 4330, 'synset': 'male_horse.n.01', 'name': 'male_horse'}, {'id': 4331, 'synset': 'ridgeling.n.01', 'name': 'ridgeling'}, {'id': 4332, 'synset': 'stallion.n.01', 'name': 'stallion'}, {'id': 4333, 'synset': 'stud.n.04', 'name': 'stud'}, {'id': 4334, 'synset': 'gelding.n.01', 'name': 'gelding'}, {'id': 4335, 'synset': 'mare.n.01', 'name': 'mare'}, {'id': 4336, 'synset': 'broodmare.n.01', 'name': 'broodmare'}, {'id': 4337, 'synset': 'saddle_horse.n.01', 'name': 'saddle_horse'}, {'id': 4338, 'synset': 'remount.n.01', 'name': 'remount'}, {'id': 4339, 'synset': 'palfrey.n.01', 'name': 'palfrey'}, {'id': 4340, 'synset': 'warhorse.n.03', 'name': 'warhorse'}, {'id': 4341, 'synset': 'cavalry_horse.n.01', 'name': 'cavalry_horse'}, {'id': 4342, 'synset': 'charger.n.01', 'name': 'charger'}, {'id': 4343, 'synset': 'steed.n.01', 'name': 'steed'}, {'id': 4344, 'synset': 'prancer.n.01', 'name': 'prancer'}, {'id': 4345, 'synset': 'hack.n.08', 'name': 'hack'}, {'id': 4346, 'synset': 'cow_pony.n.01', 'name': 'cow_pony'}, {'id': 4347, 'synset': 'quarter_horse.n.01', 'name': 'quarter_horse'}, {'id': 4348, 'synset': 'morgan.n.06', 'name': 'Morgan'}, {'id': 4349, 'synset': 'tennessee_walker.n.01', 'name': 'Tennessee_walker'}, {'id': 4350, 'synset': 'american_saddle_horse.n.01', 'name': 'American_saddle_horse'}, {'id': 4351, 'synset': 'appaloosa.n.01', 'name': 'Appaloosa'}, {'id': 4352, 'synset': 'arabian.n.02', 'name': 'Arabian'}, {'id': 4353, 'synset': 'lippizan.n.01', 'name': 'Lippizan'}, {'id': 4354, 'synset': 'pony.n.01', 'name': 'pony'}, {'id': 4355, 'synset': 'polo_pony.n.01', 'name': 'polo_pony'}, {'id': 4356, 'synset': 'mustang.n.01', 'name': 'mustang'}, {'id': 4357, 'synset': 'bronco.n.01', 'name': 'bronco'}, {'id': 4358, 'synset': 'bucking_bronco.n.01', 'name': 'bucking_bronco'}, {'id': 4359, 'synset': 'buckskin.n.01', 'name': 'buckskin'}, {'id': 4360, 'synset': 'crowbait.n.01', 'name': 'crowbait'}, {'id': 4361, 'synset': 'dun.n.01', 'name': 'dun'}, {'id': 4362, 'synset': 'grey.n.07', 'name': 'grey'}, {'id': 4363, 'synset': 'wild_horse.n.01', 'name': 'wild_horse'}, {'id': 4364, 'synset': 'tarpan.n.01', 'name': 'tarpan'}, {'id': 4365, 'synset': "przewalski's_horse.n.01", 'name': "Przewalski's_horse"}, {'id': 4366, 'synset': 'cayuse.n.01', 'name': 'cayuse'}, {'id': 4367, 'synset': 'hack.n.07', 'name': 'hack'}, {'id': 4368, 'synset': 'hack.n.06', 'name': 'hack'}, {'id': 4369, 'synset': 'plow_horse.n.01', 'name': 'plow_horse'}, {'id': 4370, 'synset': 'shetland_pony.n.01', 'name': 'Shetland_pony'}, {'id': 4371, 'synset': 'welsh_pony.n.01', 'name': 'Welsh_pony'}, {'id': 4372, 'synset': 'exmoor.n.02', 'name': 'Exmoor'}, {'id': 4373, 'synset': 'racehorse.n.01', 'name': 'racehorse'}, {'id': 4374, 'synset': 'thoroughbred.n.02', 'name': 'thoroughbred'}, {'id': 4375, 'synset': 'steeplechaser.n.01', 'name': 'steeplechaser'}, {'id': 4376, 'synset': 'racer.n.03', 'name': 'racer'}, {'id': 4377, 'synset': 'finisher.n.06', 'name': 'finisher'}, {'id': 4378, 'synset': 'pony.n.02', 'name': 'pony'}, {'id': 4379, 'synset': 'yearling.n.02', 'name': 'yearling'}, {'id': 4380, 'synset': 'dark_horse.n.02', 'name': 'dark_horse'}, {'id': 4381, 'synset': 'mudder.n.01', 'name': 'mudder'}, {'id': 4382, 'synset': 'nonstarter.n.02', 'name': 'nonstarter'}, {'id': 4383, 'synset': 'stalking-horse.n.04', 'name': 'stalking-horse'}, {'id': 4384, 'synset': 'harness_horse.n.01', 'name': 'harness_horse'}, {'id': 4385, 'synset': 'cob.n.02', 'name': 'cob'}, {'id': 4386, 'synset': 'hackney.n.02', 'name': 'hackney'}, {'id': 4387, 'synset': 'workhorse.n.02', 'name': 'workhorse'}, {'id': 4388, 'synset': 'draft_horse.n.01', 'name': 'draft_horse'}, {'id': 4389, 'synset': 'packhorse.n.01', 'name': 'packhorse'}, {'id': 4390, 'synset': 'carthorse.n.01', 'name': 'carthorse'}, {'id': 4391, 'synset': 'clydesdale.n.01', 'name': 'Clydesdale'}, {'id': 4392, 'synset': 'percheron.n.01', 'name': 'Percheron'}, {'id': 4393, 'synset': 'farm_horse.n.01', 'name': 'farm_horse'}, {'id': 4394, 'synset': 'shire.n.02', 'name': 'shire'}, {'id': 4395, 'synset': 'pole_horse.n.02', 'name': 'pole_horse'}, {'id': 4396, 'synset': 'post_horse.n.01', 'name': 'post_horse'}, {'id': 4397, 'synset': 'coach_horse.n.01', 'name': 'coach_horse'}, {'id': 4398, 'synset': 'pacer.n.02', 'name': 'pacer'}, {'id': 4399, 'synset': 'pacer.n.01', 'name': 'pacer'}, {'id': 4400, 'synset': 'trotting_horse.n.01', 'name': 'trotting_horse'}, {'id': 4401, 'synset': 'pole_horse.n.01', 'name': 'pole_horse'}, {'id': 4402, 'synset': 'stepper.n.03', 'name': 'stepper'}, {'id': 4403, 'synset': 'chestnut.n.06', 'name': 'chestnut'}, {'id': 4404, 'synset': 'liver_chestnut.n.01', 'name': 'liver_chestnut'}, {'id': 4405, 'synset': 'bay.n.07', 'name': 'bay'}, {'id': 4406, 'synset': 'sorrel.n.05', 'name': 'sorrel'}, {'id': 4407, 'synset': 'palomino.n.01', 'name': 'palomino'}, {'id': 4408, 'synset': 'pinto.n.01', 'name': 'pinto'}, {'id': 4409, 'synset': 'ass.n.03', 'name': 'ass'}, {'id': 4410, 'synset': 'burro.n.01', 'name': 'burro'}, {'id': 4411, 'synset': 'moke.n.01', 'name': 'moke'}, {'id': 4412, 'synset': 'jack.n.12', 'name': 'jack'}, {'id': 4413, 'synset': 'jennet.n.01', 'name': 'jennet'}, {'id': 4414, 'synset': 'mule.n.01', 'name': 'mule'}, {'id': 4415, 'synset': 'hinny.n.01', 'name': 'hinny'}, {'id': 4416, 'synset': 'wild_ass.n.01', 'name': 'wild_ass'}, {'id': 4417, 'synset': 'african_wild_ass.n.01', 'name': 'African_wild_ass'}, {'id': 4418, 'synset': 'kiang.n.01', 'name': 'kiang'}, {'id': 4419, 'synset': 'onager.n.02', 'name': 'onager'}, {'id': 4420, 'synset': 'chigetai.n.01', 'name': 'chigetai'}, {'id': 4421, 'synset': 'common_zebra.n.01', 'name': 'common_zebra'}, {'id': 4422, 'synset': 'mountain_zebra.n.01', 'name': 'mountain_zebra'}, {'id': 4423, 'synset': "grevy's_zebra.n.01", 'name': "grevy's_zebra"}, {'id': 4424, 'synset': 'quagga.n.01', 'name': 'quagga'}, {'id': 4425, 'synset': 'indian_rhinoceros.n.01', 'name': 'Indian_rhinoceros'}, {'id': 4426, 'synset': 'woolly_rhinoceros.n.01', 'name': 'woolly_rhinoceros'}, {'id': 4427, 'synset': 'white_rhinoceros.n.01', 'name': 'white_rhinoceros'}, {'id': 4428, 'synset': 'black_rhinoceros.n.01', 'name': 'black_rhinoceros'}, {'id': 4429, 'synset': 'tapir.n.01', 'name': 'tapir'}, {'id': 4430, 'synset': 'new_world_tapir.n.01', 'name': 'New_World_tapir'}, {'id': 4431, 'synset': 'malayan_tapir.n.01', 'name': 'Malayan_tapir'}, {'id': 4432, 'synset': 'even-toed_ungulate.n.01', 'name': 'even-toed_ungulate'}, {'id': 4433, 'synset': 'swine.n.01', 'name': 'swine'}, {'id': 4434, 'synset': 'piglet.n.01', 'name': 'piglet'}, {'id': 4435, 'synset': 'sucking_pig.n.01', 'name': 'sucking_pig'}, {'id': 4436, 'synset': 'porker.n.01', 'name': 'porker'}, {'id': 4437, 'synset': 'boar.n.02', 'name': 'boar'}, {'id': 4438, 'synset': 'sow.n.01', 'name': 'sow'}, {'id': 4439, 'synset': 'razorback.n.01', 'name': 'razorback'}, {'id': 4440, 'synset': 'wild_boar.n.01', 'name': 'wild_boar'}, {'id': 4441, 'synset': 'babirusa.n.01', 'name': 'babirusa'}, {'id': 4442, 'synset': 'warthog.n.01', 'name': 'warthog'}, {'id': 4443, 'synset': 'peccary.n.01', 'name': 'peccary'}, {'id': 4444, 'synset': 'collared_peccary.n.01', 'name': 'collared_peccary'}, {'id': 4445, 'synset': 'white-lipped_peccary.n.01', 'name': 'white-lipped_peccary'}, {'id': 4446, 'synset': 'ruminant.n.01', 'name': 'ruminant'}, {'id': 4447, 'synset': 'bovid.n.01', 'name': 'bovid'}, {'id': 4448, 'synset': 'bovine.n.01', 'name': 'bovine'}, {'id': 4449, 'synset': 'ox.n.02', 'name': 'ox'}, {'id': 4450, 'synset': 'cattle.n.01', 'name': 'cattle'}, {'id': 4451, 'synset': 'ox.n.01', 'name': 'ox'}, {'id': 4452, 'synset': 'stirk.n.01', 'name': 'stirk'}, {'id': 4453, 'synset': 'bullock.n.02', 'name': 'bullock'}, {'id': 4454, 'synset': 'bull.n.01', 'name': 'bull'}, {'id': 4455, 'synset': 'cow.n.01', 'name': 'cow'}, {'id': 4456, 'synset': 'heifer.n.01', 'name': 'heifer'}, {'id': 4457, 'synset': 'bullock.n.01', 'name': 'bullock'}, {'id': 4458, 'synset': 'dogie.n.01', 'name': 'dogie'}, {'id': 4459, 'synset': 'maverick.n.02', 'name': 'maverick'}, {'id': 4460, 'synset': 'longhorn.n.01', 'name': 'longhorn'}, {'id': 4461, 'synset': 'brahman.n.04', 'name': 'Brahman'}, {'id': 4462, 'synset': 'zebu.n.01', 'name': 'zebu'}, {'id': 4463, 'synset': 'aurochs.n.02', 'name': 'aurochs'}, {'id': 4464, 'synset': 'yak.n.02', 'name': 'yak'}, {'id': 4465, 'synset': 'banteng.n.01', 'name': 'banteng'}, {'id': 4466, 'synset': 'welsh.n.03', 'name': 'Welsh'}, {'id': 4467, 'synset': 'red_poll.n.01', 'name': 'red_poll'}, {'id': 4468, 'synset': 'santa_gertrudis.n.01', 'name': 'Santa_Gertrudis'}, {'id': 4469, 'synset': 'aberdeen_angus.n.01', 'name': 'Aberdeen_Angus'}, {'id': 4470, 'synset': 'africander.n.01', 'name': 'Africander'}, {'id': 4471, 'synset': 'dairy_cattle.n.01', 'name': 'dairy_cattle'}, {'id': 4472, 'synset': 'ayrshire.n.01', 'name': 'Ayrshire'}, {'id': 4473, 'synset': 'brown_swiss.n.01', 'name': 'Brown_Swiss'}, {'id': 4474, 'synset': 'charolais.n.01', 'name': 'Charolais'}, {'id': 4475, 'synset': 'jersey.n.05', 'name': 'Jersey'}, {'id': 4476, 'synset': 'devon.n.02', 'name': 'Devon'}, {'id': 4477, 'synset': 'grade.n.09', 'name': 'grade'}, {'id': 4478, 'synset': 'durham.n.02', 'name': 'Durham'}, {'id': 4479, 'synset': 'milking_shorthorn.n.01', 'name': 'milking_shorthorn'}, {'id': 4480, 'synset': 'galloway.n.02', 'name': 'Galloway'}, {'id': 4481, 'synset': 'friesian.n.01', 'name': 'Friesian'}, {'id': 4482, 'synset': 'guernsey.n.02', 'name': 'Guernsey'}, {'id': 4483, 'synset': 'hereford.n.01', 'name': 'Hereford'}, {'id': 4484, 'synset': 'cattalo.n.01', 'name': 'cattalo'}, {'id': 4485, 'synset': 'old_world_buffalo.n.01', 'name': 'Old_World_buffalo'}, {'id': 4486, 'synset': 'water_buffalo.n.01', 'name': 'water_buffalo'}, {'id': 4487, 'synset': 'indian_buffalo.n.01', 'name': 'Indian_buffalo'}, {'id': 4488, 'synset': 'carabao.n.01', 'name': 'carabao'}, {'id': 4489, 'synset': 'anoa.n.01', 'name': 'anoa'}, {'id': 4490, 'synset': 'tamarau.n.01', 'name': 'tamarau'}, {'id': 4491, 'synset': 'cape_buffalo.n.01', 'name': 'Cape_buffalo'}, {'id': 4492, 'synset': 'asian_wild_ox.n.01', 'name': 'Asian_wild_ox'}, {'id': 4493, 'synset': 'gaur.n.01', 'name': 'gaur'}, {'id': 4494, 'synset': 'gayal.n.01', 'name': 'gayal'}, {'id': 4495, 'synset': 'bison.n.01', 'name': 'bison'}, {'id': 4496, 'synset': 'american_bison.n.01', 'name': 'American_bison'}, {'id': 4497, 'synset': 'wisent.n.01', 'name': 'wisent'}, {'id': 4498, 'synset': 'musk_ox.n.01', 'name': 'musk_ox'}, {'id': 4499, 'synset': 'ewe.n.03', 'name': 'ewe'}, {'id': 4500, 'synset': 'wether.n.01', 'name': 'wether'}, {'id': 4501, 'synset': 'lambkin.n.01', 'name': 'lambkin'}, {'id': 4502, 'synset': 'baa-lamb.n.01', 'name': 'baa-lamb'}, {'id': 4503, 'synset': 'hog.n.02', 'name': 'hog'}, {'id': 4504, 'synset': 'teg.n.01', 'name': 'teg'}, {'id': 4505, 'synset': 'persian_lamb.n.02', 'name': 'Persian_lamb'}, {'id': 4506, 'synset': 'domestic_sheep.n.01', 'name': 'domestic_sheep'}, {'id': 4507, 'synset': 'cotswold.n.01', 'name': 'Cotswold'}, {'id': 4508, 'synset': 'hampshire.n.02', 'name': 'Hampshire'}, {'id': 4509, 'synset': 'lincoln.n.03', 'name': 'Lincoln'}, {'id': 4510, 'synset': 'exmoor.n.01', 'name': 'Exmoor'}, {'id': 4511, 'synset': 'cheviot.n.01', 'name': 'Cheviot'}, {'id': 4512, 'synset': 'broadtail.n.02', 'name': 'broadtail'}, {'id': 4513, 'synset': 'longwool.n.01', 'name': 'longwool'}, {'id': 4514, 'synset': 'merino.n.01', 'name': 'merino'}, {'id': 4515, 'synset': 'rambouillet.n.01', 'name': 'Rambouillet'}, {'id': 4516, 'synset': 'wild_sheep.n.01', 'name': 'wild_sheep'}, {'id': 4517, 'synset': 'argali.n.01', 'name': 'argali'}, {'id': 4518, 'synset': 'marco_polo_sheep.n.01', 'name': 'Marco_Polo_sheep'}, {'id': 4519, 'synset': 'urial.n.01', 'name': 'urial'}, {'id': 4520, 'synset': 'dall_sheep.n.01', 'name': 'Dall_sheep'}, {'id': 4521, 'synset': 'mountain_sheep.n.01', 'name': 'mountain_sheep'}, {'id': 4522, 'synset': 'bighorn.n.02', 'name': 'bighorn'}, {'id': 4523, 'synset': 'mouflon.n.01', 'name': 'mouflon'}, {'id': 4524, 'synset': 'aoudad.n.01', 'name': 'aoudad'}, {'id': 4525, 'synset': 'kid.n.05', 'name': 'kid'}, {'id': 4526, 'synset': 'billy.n.02', 'name': 'billy'}, {'id': 4527, 'synset': 'nanny.n.02', 'name': 'nanny'}, {'id': 4528, 'synset': 'domestic_goat.n.01', 'name': 'domestic_goat'}, {'id': 4529, 'synset': 'cashmere_goat.n.01', 'name': 'Cashmere_goat'}, {'id': 4530, 'synset': 'angora.n.02', 'name': 'Angora'}, {'id': 4531, 'synset': 'wild_goat.n.01', 'name': 'wild_goat'}, {'id': 4532, 'synset': 'bezoar_goat.n.01', 'name': 'bezoar_goat'}, {'id': 4533, 'synset': 'markhor.n.01', 'name': 'markhor'}, {'id': 4534, 'synset': 'ibex.n.01', 'name': 'ibex'}, {'id': 4535, 'synset': 'goat_antelope.n.01', 'name': 'goat_antelope'}, {'id': 4536, 'synset': 'mountain_goat.n.01', 'name': 'mountain_goat'}, {'id': 4537, 'synset': 'goral.n.01', 'name': 'goral'}, {'id': 4538, 'synset': 'serow.n.01', 'name': 'serow'}, {'id': 4539, 'synset': 'chamois.n.02', 'name': 'chamois'}, {'id': 4540, 'synset': 'takin.n.01', 'name': 'takin'}, {'id': 4541, 'synset': 'antelope.n.01', 'name': 'antelope'}, {'id': 4542, 'synset': 'blackbuck.n.01', 'name': 'blackbuck'}, {'id': 4543, 'synset': 'gerenuk.n.01', 'name': 'gerenuk'}, {'id': 4544, 'synset': 'addax.n.01', 'name': 'addax'}, {'id': 4545, 'synset': 'gnu.n.01', 'name': 'gnu'}, {'id': 4546, 'synset': 'dik-dik.n.01', 'name': 'dik-dik'}, {'id': 4547, 'synset': 'hartebeest.n.01', 'name': 'hartebeest'}, {'id': 4548, 'synset': 'sassaby.n.01', 'name': 'sassaby'}, {'id': 4549, 'synset': 'impala.n.01', 'name': 'impala'}, {'id': 4550, 'synset': "thomson's_gazelle.n.01", 'name': "Thomson's_gazelle"}, {'id': 4551, 'synset': 'gazella_subgutturosa.n.01', 'name': 'Gazella_subgutturosa'}, {'id': 4552, 'synset': 'springbok.n.01', 'name': 'springbok'}, {'id': 4553, 'synset': 'bongo.n.02', 'name': 'bongo'}, {'id': 4554, 'synset': 'kudu.n.01', 'name': 'kudu'}, {'id': 4555, 'synset': 'greater_kudu.n.01', 'name': 'greater_kudu'}, {'id': 4556, 'synset': 'lesser_kudu.n.01', 'name': 'lesser_kudu'}, {'id': 4557, 'synset': 'harnessed_antelope.n.01', 'name': 'harnessed_antelope'}, {'id': 4558, 'synset': 'nyala.n.02', 'name': 'nyala'}, {'id': 4559, 'synset': 'mountain_nyala.n.01', 'name': 'mountain_nyala'}, {'id': 4560, 'synset': 'bushbuck.n.01', 'name': 'bushbuck'}, {'id': 4561, 'synset': 'nilgai.n.01', 'name': 'nilgai'}, {'id': 4562, 'synset': 'sable_antelope.n.01', 'name': 'sable_antelope'}, {'id': 4563, 'synset': 'saiga.n.01', 'name': 'saiga'}, {'id': 4564, 'synset': 'steenbok.n.01', 'name': 'steenbok'}, {'id': 4565, 'synset': 'eland.n.01', 'name': 'eland'}, {'id': 4566, 'synset': 'common_eland.n.01', 'name': 'common_eland'}, {'id': 4567, 'synset': 'giant_eland.n.01', 'name': 'giant_eland'}, {'id': 4568, 'synset': 'kob.n.01', 'name': 'kob'}, {'id': 4569, 'synset': 'lechwe.n.01', 'name': 'lechwe'}, {'id': 4570, 'synset': 'waterbuck.n.01', 'name': 'waterbuck'}, {'id': 4571, 'synset': 'puku.n.01', 'name': 'puku'}, {'id': 4572, 'synset': 'oryx.n.01', 'name': 'oryx'}, {'id': 4573, 'synset': 'gemsbok.n.01', 'name': 'gemsbok'}, {'id': 4574, 'synset': 'forest_goat.n.01', 'name': 'forest_goat'}, {'id': 4575, 'synset': 'pronghorn.n.01', 'name': 'pronghorn'}, {'id': 4576, 'synset': 'stag.n.02', 'name': 'stag'}, {'id': 4577, 'synset': 'royal.n.02', 'name': 'royal'}, {'id': 4578, 'synset': 'pricket.n.02', 'name': 'pricket'}, {'id': 4579, 'synset': 'fawn.n.02', 'name': 'fawn'}, {'id': 4580, 'synset': 'red_deer.n.01', 'name': 'red_deer'}, {'id': 4581, 'synset': 'hart.n.03', 'name': 'hart'}, {'id': 4582, 'synset': 'hind.n.02', 'name': 'hind'}, {'id': 4583, 'synset': 'brocket.n.02', 'name': 'brocket'}, {'id': 4584, 'synset': 'sambar.n.01', 'name': 'sambar'}, {'id': 4585, 'synset': 'wapiti.n.01', 'name': 'wapiti'}, {'id': 4586, 'synset': 'japanese_deer.n.01', 'name': 'Japanese_deer'}, {'id': 4587, 'synset': 'virginia_deer.n.01', 'name': 'Virginia_deer'}, {'id': 4588, 'synset': 'mule_deer.n.01', 'name': 'mule_deer'}, {'id': 4589, 'synset': 'black-tailed_deer.n.01', 'name': 'black-tailed_deer'}, {'id': 4590, 'synset': 'fallow_deer.n.01', 'name': 'fallow_deer'}, {'id': 4591, 'synset': 'roe_deer.n.01', 'name': 'roe_deer'}, {'id': 4592, 'synset': 'roebuck.n.01', 'name': 'roebuck'}, {'id': 4593, 'synset': 'caribou.n.01', 'name': 'caribou'}, {'id': 4594, 'synset': 'woodland_caribou.n.01', 'name': 'woodland_caribou'}, {'id': 4595, 'synset': 'barren_ground_caribou.n.01', 'name': 'barren_ground_caribou'}, {'id': 4596, 'synset': 'brocket.n.01', 'name': 'brocket'}, {'id': 4597, 'synset': 'muntjac.n.01', 'name': 'muntjac'}, {'id': 4598, 'synset': 'musk_deer.n.01', 'name': 'musk_deer'}, {'id': 4599, 'synset': "pere_david's_deer.n.01", 'name': "pere_david's_deer"}, {'id': 4600, 'synset': 'chevrotain.n.01', 'name': 'chevrotain'}, {'id': 4601, 'synset': 'kanchil.n.01', 'name': 'kanchil'}, {'id': 4602, 'synset': 'napu.n.01', 'name': 'napu'}, {'id': 4603, 'synset': 'water_chevrotain.n.01', 'name': 'water_chevrotain'}, {'id': 4604, 'synset': 'arabian_camel.n.01', 'name': 'Arabian_camel'}, {'id': 4605, 'synset': 'bactrian_camel.n.01', 'name': 'Bactrian_camel'}, {'id': 4606, 'synset': 'llama.n.01', 'name': 'llama'}, {'id': 4607, 'synset': 'domestic_llama.n.01', 'name': 'domestic_llama'}, {'id': 4608, 'synset': 'guanaco.n.01', 'name': 'guanaco'}, {'id': 4609, 'synset': 'alpaca.n.03', 'name': 'alpaca'}, {'id': 4610, 'synset': 'vicuna.n.03', 'name': 'vicuna'}, {'id': 4611, 'synset': 'okapi.n.01', 'name': 'okapi'}, {'id': 4612, 'synset': 'musteline_mammal.n.01', 'name': 'musteline_mammal'}, {'id': 4613, 'synset': 'weasel.n.02', 'name': 'weasel'}, {'id': 4614, 'synset': 'ermine.n.02', 'name': 'ermine'}, {'id': 4615, 'synset': 'stoat.n.01', 'name': 'stoat'}, {'id': 4616, 'synset': 'new_world_least_weasel.n.01', 'name': 'New_World_least_weasel'}, {'id': 4617, 'synset': 'old_world_least_weasel.n.01', 'name': 'Old_World_least_weasel'}, {'id': 4618, 'synset': 'longtail_weasel.n.01', 'name': 'longtail_weasel'}, {'id': 4619, 'synset': 'mink.n.03', 'name': 'mink'}, {'id': 4620, 'synset': 'american_mink.n.01', 'name': 'American_mink'}, {'id': 4621, 'synset': 'polecat.n.02', 'name': 'polecat'}, {'id': 4622, 'synset': 'black-footed_ferret.n.01', 'name': 'black-footed_ferret'}, {'id': 4623, 'synset': 'muishond.n.01', 'name': 'muishond'}, {'id': 4624, 'synset': 'snake_muishond.n.01', 'name': 'snake_muishond'}, {'id': 4625, 'synset': 'striped_muishond.n.01', 'name': 'striped_muishond'}, {'id': 4626, 'synset': 'otter.n.02', 'name': 'otter'}, {'id': 4627, 'synset': 'river_otter.n.01', 'name': 'river_otter'}, {'id': 4628, 'synset': 'eurasian_otter.n.01', 'name': 'Eurasian_otter'}, {'id': 4629, 'synset': 'sea_otter.n.01', 'name': 'sea_otter'}, {'id': 4630, 'synset': 'skunk.n.04', 'name': 'skunk'}, {'id': 4631, 'synset': 'striped_skunk.n.01', 'name': 'striped_skunk'}, {'id': 4632, 'synset': 'hooded_skunk.n.01', 'name': 'hooded_skunk'}, {'id': 4633, 'synset': 'hog-nosed_skunk.n.01', 'name': 'hog-nosed_skunk'}, {'id': 4634, 'synset': 'spotted_skunk.n.01', 'name': 'spotted_skunk'}, {'id': 4635, 'synset': 'badger.n.02', 'name': 'badger'}, {'id': 4636, 'synset': 'american_badger.n.01', 'name': 'American_badger'}, {'id': 4637, 'synset': 'eurasian_badger.n.01', 'name': 'Eurasian_badger'}, {'id': 4638, 'synset': 'ratel.n.01', 'name': 'ratel'}, {'id': 4639, 'synset': 'ferret_badger.n.01', 'name': 'ferret_badger'}, {'id': 4640, 'synset': 'hog_badger.n.01', 'name': 'hog_badger'}, {'id': 4641, 'synset': 'wolverine.n.03', 'name': 'wolverine'}, {'id': 4642, 'synset': 'glutton.n.02', 'name': 'glutton'}, {'id': 4643, 'synset': 'grison.n.01', 'name': 'grison'}, {'id': 4644, 'synset': 'marten.n.01', 'name': 'marten'}, {'id': 4645, 'synset': 'pine_marten.n.01', 'name': 'pine_marten'}, {'id': 4646, 'synset': 'sable.n.05', 'name': 'sable'}, {'id': 4647, 'synset': 'american_marten.n.01', 'name': 'American_marten'}, {'id': 4648, 'synset': 'stone_marten.n.01', 'name': 'stone_marten'}, {'id': 4649, 'synset': 'fisher.n.02', 'name': 'fisher'}, {'id': 4650, 'synset': 'yellow-throated_marten.n.01', 'name': 'yellow-throated_marten'}, {'id': 4651, 'synset': 'tayra.n.01', 'name': 'tayra'}, {'id': 4652, 'synset': 'fictional_animal.n.01', 'name': 'fictional_animal'}, {'id': 4653, 'synset': 'pachyderm.n.01', 'name': 'pachyderm'}, {'id': 4654, 'synset': 'edentate.n.01', 'name': 'edentate'}, {'id': 4655, 'synset': 'armadillo.n.01', 'name': 'armadillo'}, {'id': 4656, 'synset': 'peba.n.01', 'name': 'peba'}, {'id': 4657, 'synset': 'apar.n.01', 'name': 'apar'}, {'id': 4658, 'synset': 'tatouay.n.01', 'name': 'tatouay'}, {'id': 4659, 'synset': 'peludo.n.01', 'name': 'peludo'}, {'id': 4660, 'synset': 'giant_armadillo.n.01', 'name': 'giant_armadillo'}, {'id': 4661, 'synset': 'pichiciago.n.01', 'name': 'pichiciago'}, {'id': 4662, 'synset': 'sloth.n.02', 'name': 'sloth'}, {'id': 4663, 'synset': 'three-toed_sloth.n.01', 'name': 'three-toed_sloth'}, {'id': 4664, 'synset': 'two-toed_sloth.n.02', 'name': 'two-toed_sloth'}, {'id': 4665, 'synset': 'two-toed_sloth.n.01', 'name': 'two-toed_sloth'}, {'id': 4666, 'synset': 'megatherian.n.01', 'name': 'megatherian'}, {'id': 4667, 'synset': 'mylodontid.n.01', 'name': 'mylodontid'}, {'id': 4668, 'synset': 'anteater.n.02', 'name': 'anteater'}, {'id': 4669, 'synset': 'ant_bear.n.01', 'name': 'ant_bear'}, {'id': 4670, 'synset': 'silky_anteater.n.01', 'name': 'silky_anteater'}, {'id': 4671, 'synset': 'tamandua.n.01', 'name': 'tamandua'}, {'id': 4672, 'synset': 'pangolin.n.01', 'name': 'pangolin'}, {'id': 4673, 'synset': 'coronet.n.02', 'name': 'coronet'}, {'id': 4674, 'synset': 'scapular.n.01', 'name': 'scapular'}, {'id': 4675, 'synset': 'tadpole.n.01', 'name': 'tadpole'}, {'id': 4676, 'synset': 'primate.n.02', 'name': 'primate'}, {'id': 4677, 'synset': 'simian.n.01', 'name': 'simian'}, {'id': 4678, 'synset': 'ape.n.01', 'name': 'ape'}, {'id': 4679, 'synset': 'anthropoid.n.02', 'name': 'anthropoid'}, {'id': 4680, 'synset': 'anthropoid_ape.n.01', 'name': 'anthropoid_ape'}, {'id': 4681, 'synset': 'hominoid.n.01', 'name': 'hominoid'}, {'id': 4682, 'synset': 'hominid.n.01', 'name': 'hominid'}, {'id': 4683, 'synset': 'homo.n.02', 'name': 'homo'}, {'id': 4684, 'synset': 'world.n.08', 'name': 'world'}, {'id': 4685, 'synset': 'homo_erectus.n.01', 'name': 'Homo_erectus'}, {'id': 4686, 'synset': 'pithecanthropus.n.01', 'name': 'Pithecanthropus'}, {'id': 4687, 'synset': 'java_man.n.01', 'name': 'Java_man'}, {'id': 4688, 'synset': 'peking_man.n.01', 'name': 'Peking_man'}, {'id': 4689, 'synset': 'sinanthropus.n.01', 'name': 'Sinanthropus'}, {'id': 4690, 'synset': 'homo_soloensis.n.01', 'name': 'Homo_soloensis'}, {'id': 4691, 'synset': 'javanthropus.n.01', 'name': 'Javanthropus'}, {'id': 4692, 'synset': 'homo_habilis.n.01', 'name': 'Homo_habilis'}, {'id': 4693, 'synset': 'homo_sapiens.n.01', 'name': 'Homo_sapiens'}, {'id': 4694, 'synset': 'neandertal_man.n.01', 'name': 'Neandertal_man'}, {'id': 4695, 'synset': 'cro-magnon.n.01', 'name': 'Cro-magnon'}, {'id': 4696, 'synset': 'homo_sapiens_sapiens.n.01', 'name': 'Homo_sapiens_sapiens'}, {'id': 4697, 'synset': 'australopithecine.n.01', 'name': 'australopithecine'}, {'id': 4698, 'synset': 'australopithecus_afarensis.n.01', 'name': 'Australopithecus_afarensis'}, {'id': 4699, 'synset': 'australopithecus_africanus.n.01', 'name': 'Australopithecus_africanus'}, {'id': 4700, 'synset': 'australopithecus_boisei.n.01', 'name': 'Australopithecus_boisei'}, {'id': 4701, 'synset': 'zinjanthropus.n.01', 'name': 'Zinjanthropus'}, {'id': 4702, 'synset': 'australopithecus_robustus.n.01', 'name': 'Australopithecus_robustus'}, {'id': 4703, 'synset': 'paranthropus.n.01', 'name': 'Paranthropus'}, {'id': 4704, 'synset': 'sivapithecus.n.01', 'name': 'Sivapithecus'}, {'id': 4705, 'synset': 'rudapithecus.n.01', 'name': 'rudapithecus'}, {'id': 4706, 'synset': 'proconsul.n.03', 'name': 'proconsul'}, {'id': 4707, 'synset': 'aegyptopithecus.n.01', 'name': 'Aegyptopithecus'}, {'id': 4708, 'synset': 'great_ape.n.01', 'name': 'great_ape'}, {'id': 4709, 'synset': 'orangutan.n.01', 'name': 'orangutan'}, {'id': 4710, 'synset': 'western_lowland_gorilla.n.01', 'name': 'western_lowland_gorilla'}, {'id': 4711, 'synset': 'eastern_lowland_gorilla.n.01', 'name': 'eastern_lowland_gorilla'}, {'id': 4712, 'synset': 'mountain_gorilla.n.01', 'name': 'mountain_gorilla'}, {'id': 4713, 'synset': 'silverback.n.01', 'name': 'silverback'}, {'id': 4714, 'synset': 'chimpanzee.n.01', 'name': 'chimpanzee'}, {'id': 4715, 'synset': 'western_chimpanzee.n.01', 'name': 'western_chimpanzee'}, {'id': 4716, 'synset': 'eastern_chimpanzee.n.01', 'name': 'eastern_chimpanzee'}, {'id': 4717, 'synset': 'central_chimpanzee.n.01', 'name': 'central_chimpanzee'}, {'id': 4718, 'synset': 'pygmy_chimpanzee.n.01', 'name': 'pygmy_chimpanzee'}, {'id': 4719, 'synset': 'lesser_ape.n.01', 'name': 'lesser_ape'}, {'id': 4720, 'synset': 'gibbon.n.02', 'name': 'gibbon'}, {'id': 4721, 'synset': 'siamang.n.01', 'name': 'siamang'}, {'id': 4722, 'synset': 'old_world_monkey.n.01', 'name': 'Old_World_monkey'}, {'id': 4723, 'synset': 'guenon.n.01', 'name': 'guenon'}, {'id': 4724, 'synset': 'talapoin.n.01', 'name': 'talapoin'}, {'id': 4725, 'synset': 'grivet.n.01', 'name': 'grivet'}, {'id': 4726, 'synset': 'vervet.n.01', 'name': 'vervet'}, {'id': 4727, 'synset': 'green_monkey.n.01', 'name': 'green_monkey'}, {'id': 4728, 'synset': 'mangabey.n.01', 'name': 'mangabey'}, {'id': 4729, 'synset': 'patas.n.01', 'name': 'patas'}, {'id': 4730, 'synset': 'chacma.n.01', 'name': 'chacma'}, {'id': 4731, 'synset': 'mandrill.n.01', 'name': 'mandrill'}, {'id': 4732, 'synset': 'drill.n.02', 'name': 'drill'}, {'id': 4733, 'synset': 'macaque.n.01', 'name': 'macaque'}, {'id': 4734, 'synset': 'rhesus.n.01', 'name': 'rhesus'}, {'id': 4735, 'synset': 'bonnet_macaque.n.01', 'name': 'bonnet_macaque'}, {'id': 4736, 'synset': 'barbary_ape.n.01', 'name': 'Barbary_ape'}, {'id': 4737, 'synset': 'crab-eating_macaque.n.01', 'name': 'crab-eating_macaque'}, {'id': 4738, 'synset': 'langur.n.01', 'name': 'langur'}, {'id': 4739, 'synset': 'entellus.n.01', 'name': 'entellus'}, {'id': 4740, 'synset': 'colobus.n.01', 'name': 'colobus'}, {'id': 4741, 'synset': 'guereza.n.01', 'name': 'guereza'}, {'id': 4742, 'synset': 'proboscis_monkey.n.01', 'name': 'proboscis_monkey'}, {'id': 4743, 'synset': 'new_world_monkey.n.01', 'name': 'New_World_monkey'}, {'id': 4744, 'synset': 'marmoset.n.01', 'name': 'marmoset'}, {'id': 4745, 'synset': 'true_marmoset.n.01', 'name': 'true_marmoset'}, {'id': 4746, 'synset': 'pygmy_marmoset.n.01', 'name': 'pygmy_marmoset'}, {'id': 4747, 'synset': 'tamarin.n.01', 'name': 'tamarin'}, {'id': 4748, 'synset': 'silky_tamarin.n.01', 'name': 'silky_tamarin'}, {'id': 4749, 'synset': 'pinche.n.01', 'name': 'pinche'}, {'id': 4750, 'synset': 'capuchin.n.02', 'name': 'capuchin'}, {'id': 4751, 'synset': 'douroucouli.n.01', 'name': 'douroucouli'}, {'id': 4752, 'synset': 'howler_monkey.n.01', 'name': 'howler_monkey'}, {'id': 4753, 'synset': 'saki.n.03', 'name': 'saki'}, {'id': 4754, 'synset': 'uakari.n.01', 'name': 'uakari'}, {'id': 4755, 'synset': 'titi.n.03', 'name': 'titi'}, {'id': 4756, 'synset': 'spider_monkey.n.01', 'name': 'spider_monkey'}, {'id': 4757, 'synset': 'squirrel_monkey.n.01', 'name': 'squirrel_monkey'}, {'id': 4758, 'synset': 'woolly_monkey.n.01', 'name': 'woolly_monkey'}, {'id': 4759, 'synset': 'tree_shrew.n.01', 'name': 'tree_shrew'}, {'id': 4760, 'synset': 'prosimian.n.01', 'name': 'prosimian'}, {'id': 4761, 'synset': 'lemur.n.01', 'name': 'lemur'}, {'id': 4762, 'synset': 'madagascar_cat.n.01', 'name': 'Madagascar_cat'}, {'id': 4763, 'synset': 'aye-aye.n.01', 'name': 'aye-aye'}, {'id': 4764, 'synset': 'slender_loris.n.01', 'name': 'slender_loris'}, {'id': 4765, 'synset': 'slow_loris.n.01', 'name': 'slow_loris'}, {'id': 4766, 'synset': 'potto.n.02', 'name': 'potto'}, {'id': 4767, 'synset': 'angwantibo.n.01', 'name': 'angwantibo'}, {'id': 4768, 'synset': 'galago.n.01', 'name': 'galago'}, {'id': 4769, 'synset': 'indri.n.01', 'name': 'indri'}, {'id': 4770, 'synset': 'woolly_indris.n.01', 'name': 'woolly_indris'}, {'id': 4771, 'synset': 'tarsier.n.01', 'name': 'tarsier'}, {'id': 4772, 'synset': 'tarsius_syrichta.n.01', 'name': 'Tarsius_syrichta'}, {'id': 4773, 'synset': 'tarsius_glis.n.01', 'name': 'Tarsius_glis'}, {'id': 4774, 'synset': 'flying_lemur.n.01', 'name': 'flying_lemur'}, {'id': 4775, 'synset': 'cynocephalus_variegatus.n.01', 'name': 'Cynocephalus_variegatus'}, {'id': 4776, 'synset': 'proboscidean.n.01', 'name': 'proboscidean'}, {'id': 4777, 'synset': 'rogue_elephant.n.01', 'name': 'rogue_elephant'}, {'id': 4778, 'synset': 'indian_elephant.n.01', 'name': 'Indian_elephant'}, {'id': 4779, 'synset': 'african_elephant.n.01', 'name': 'African_elephant'}, {'id': 4780, 'synset': 'woolly_mammoth.n.01', 'name': 'woolly_mammoth'}, {'id': 4781, 'synset': 'columbian_mammoth.n.01', 'name': 'columbian_mammoth'}, {'id': 4782, 'synset': 'imperial_mammoth.n.01', 'name': 'imperial_mammoth'}, {'id': 4783, 'synset': 'mastodon.n.01', 'name': 'mastodon'}, {'id': 4784, 'synset': 'plantigrade_mammal.n.01', 'name': 'plantigrade_mammal'}, {'id': 4785, 'synset': 'digitigrade_mammal.n.01', 'name': 'digitigrade_mammal'}, {'id': 4786, 'synset': 'procyonid.n.01', 'name': 'procyonid'}, {'id': 4787, 'synset': 'raccoon.n.02', 'name': 'raccoon'}, {'id': 4788, 'synset': 'common_raccoon.n.01', 'name': 'common_raccoon'}, {'id': 4789, 'synset': 'crab-eating_raccoon.n.01', 'name': 'crab-eating_raccoon'}, {'id': 4790, 'synset': 'bassarisk.n.01', 'name': 'bassarisk'}, {'id': 4791, 'synset': 'kinkajou.n.01', 'name': 'kinkajou'}, {'id': 4792, 'synset': 'coati.n.01', 'name': 'coati'}, {'id': 4793, 'synset': 'lesser_panda.n.01', 'name': 'lesser_panda'}, {'id': 4794, 'synset': 'twitterer.n.01', 'name': 'twitterer'}, {'id': 4795, 'synset': 'fingerling.n.01', 'name': 'fingerling'}, {'id': 4796, 'synset': 'game_fish.n.01', 'name': 'game_fish'}, {'id': 4797, 'synset': 'food_fish.n.01', 'name': 'food_fish'}, {'id': 4798, 'synset': 'rough_fish.n.01', 'name': 'rough_fish'}, {'id': 4799, 'synset': 'groundfish.n.01', 'name': 'groundfish'}, {'id': 4800, 'synset': 'young_fish.n.01', 'name': 'young_fish'}, {'id': 4801, 'synset': 'parr.n.03', 'name': 'parr'}, {'id': 4802, 'synset': 'mouthbreeder.n.01', 'name': 'mouthbreeder'}, {'id': 4803, 'synset': 'spawner.n.01', 'name': 'spawner'}, {'id': 4804, 'synset': 'barracouta.n.01', 'name': 'barracouta'}, {'id': 4805, 'synset': 'crossopterygian.n.01', 'name': 'crossopterygian'}, {'id': 4806, 'synset': 'coelacanth.n.01', 'name': 'coelacanth'}, {'id': 4807, 'synset': 'lungfish.n.01', 'name': 'lungfish'}, {'id': 4808, 'synset': 'ceratodus.n.01', 'name': 'ceratodus'}, {'id': 4809, 'synset': 'catfish.n.03', 'name': 'catfish'}, {'id': 4810, 'synset': 'silurid.n.01', 'name': 'silurid'}, {'id': 4811, 'synset': 'european_catfish.n.01', 'name': 'European_catfish'}, {'id': 4812, 'synset': 'electric_catfish.n.01', 'name': 'electric_catfish'}, {'id': 4813, 'synset': 'bullhead.n.02', 'name': 'bullhead'}, {'id': 4814, 'synset': 'horned_pout.n.01', 'name': 'horned_pout'}, {'id': 4815, 'synset': 'brown_bullhead.n.01', 'name': 'brown_bullhead'}, {'id': 4816, 'synset': 'channel_catfish.n.01', 'name': 'channel_catfish'}, {'id': 4817, 'synset': 'blue_catfish.n.01', 'name': 'blue_catfish'}, {'id': 4818, 'synset': 'flathead_catfish.n.01', 'name': 'flathead_catfish'}, {'id': 4819, 'synset': 'armored_catfish.n.01', 'name': 'armored_catfish'}, {'id': 4820, 'synset': 'sea_catfish.n.01', 'name': 'sea_catfish'}, {'id': 4821, 'synset': 'gadoid.n.01', 'name': 'gadoid'}, {'id': 4822, 'synset': 'cod.n.03', 'name': 'cod'}, {'id': 4823, 'synset': 'codling.n.01', 'name': 'codling'}, {'id': 4824, 'synset': 'atlantic_cod.n.01', 'name': 'Atlantic_cod'}, {'id': 4825, 'synset': 'pacific_cod.n.01', 'name': 'Pacific_cod'}, {'id': 4826, 'synset': 'whiting.n.06', 'name': 'whiting'}, {'id': 4827, 'synset': 'burbot.n.01', 'name': 'burbot'}, {'id': 4828, 'synset': 'haddock.n.02', 'name': 'haddock'}, {'id': 4829, 'synset': 'pollack.n.03', 'name': 'pollack'}, {'id': 4830, 'synset': 'hake.n.02', 'name': 'hake'}, {'id': 4831, 'synset': 'silver_hake.n.01', 'name': 'silver_hake'}, {'id': 4832, 'synset': 'ling.n.04', 'name': 'ling'}, {'id': 4833, 'synset': 'cusk.n.02', 'name': 'cusk'}, {'id': 4834, 'synset': 'grenadier.n.02', 'name': 'grenadier'}, {'id': 4835, 'synset': 'eel.n.02', 'name': 'eel'}, {'id': 4836, 'synset': 'elver.n.02', 'name': 'elver'}, {'id': 4837, 'synset': 'common_eel.n.01', 'name': 'common_eel'}, {'id': 4838, 'synset': 'tuna.n.04', 'name': 'tuna'}, {'id': 4839, 'synset': 'moray.n.01', 'name': 'moray'}, {'id': 4840, 'synset': 'conger.n.01', 'name': 'conger'}, {'id': 4841, 'synset': 'teleost_fish.n.01', 'name': 'teleost_fish'}, {'id': 4842, 'synset': 'beaked_salmon.n.01', 'name': 'beaked_salmon'}, {'id': 4843, 'synset': 'clupeid_fish.n.01', 'name': 'clupeid_fish'}, {'id': 4844, 'synset': 'whitebait.n.02', 'name': 'whitebait'}, {'id': 4845, 'synset': 'brit.n.02', 'name': 'brit'}, {'id': 4846, 'synset': 'shad.n.02', 'name': 'shad'}, {'id': 4847, 'synset': 'common_american_shad.n.01', 'name': 'common_American_shad'}, {'id': 4848, 'synset': 'river_shad.n.01', 'name': 'river_shad'}, {'id': 4849, 'synset': 'allice_shad.n.01', 'name': 'allice_shad'}, {'id': 4850, 'synset': 'alewife.n.02', 'name': 'alewife'}, {'id': 4851, 'synset': 'menhaden.n.01', 'name': 'menhaden'}, {'id': 4852, 'synset': 'herring.n.02', 'name': 'herring'}, {'id': 4853, 'synset': 'atlantic_herring.n.01', 'name': 'Atlantic_herring'}, {'id': 4854, 'synset': 'pacific_herring.n.01', 'name': 'Pacific_herring'}, {'id': 4855, 'synset': 'sardine.n.02', 'name': 'sardine'}, {'id': 4856, 'synset': 'sild.n.01', 'name': 'sild'}, {'id': 4857, 'synset': 'brisling.n.02', 'name': 'brisling'}, {'id': 4858, 'synset': 'pilchard.n.02', 'name': 'pilchard'}, {'id': 4859, 'synset': 'pacific_sardine.n.01', 'name': 'Pacific_sardine'}, {'id': 4860, 'synset': 'anchovy.n.02', 'name': 'anchovy'}, {'id': 4861, 'synset': 'mediterranean_anchovy.n.01', 'name': 'mediterranean_anchovy'}, {'id': 4862, 'synset': 'salmonid.n.01', 'name': 'salmonid'}, {'id': 4863, 'synset': 'parr.n.02', 'name': 'parr'}, {'id': 4864, 'synset': 'blackfish.n.02', 'name': 'blackfish'}, {'id': 4865, 'synset': 'redfish.n.03', 'name': 'redfish'}, {'id': 4866, 'synset': 'atlantic_salmon.n.02', 'name': 'Atlantic_salmon'}, {'id': 4867, 'synset': 'landlocked_salmon.n.01', 'name': 'landlocked_salmon'}, {'id': 4868, 'synset': 'sockeye.n.02', 'name': 'sockeye'}, {'id': 4869, 'synset': 'chinook.n.05', 'name': 'chinook'}, {'id': 4870, 'synset': 'coho.n.02', 'name': 'coho'}, {'id': 4871, 'synset': 'trout.n.02', 'name': 'trout'}, {'id': 4872, 'synset': 'brown_trout.n.01', 'name': 'brown_trout'}, {'id': 4873, 'synset': 'rainbow_trout.n.02', 'name': 'rainbow_trout'}, {'id': 4874, 'synset': 'sea_trout.n.03', 'name': 'sea_trout'}, {'id': 4875, 'synset': 'lake_trout.n.02', 'name': 'lake_trout'}, {'id': 4876, 'synset': 'brook_trout.n.02', 'name': 'brook_trout'}, {'id': 4877, 'synset': 'char.n.03', 'name': 'char'}, {'id': 4878, 'synset': 'arctic_char.n.01', 'name': 'Arctic_char'}, {'id': 4879, 'synset': 'whitefish.n.03', 'name': 'whitefish'}, {'id': 4880, 'synset': 'lake_whitefish.n.01', 'name': 'lake_whitefish'}, {'id': 4881, 'synset': 'cisco.n.02', 'name': 'cisco'}, {'id': 4882, 'synset': 'round_whitefish.n.01', 'name': 'round_whitefish'}, {'id': 4883, 'synset': 'smelt.n.02', 'name': 'smelt'}, {'id': 4884, 'synset': 'sparling.n.02', 'name': 'sparling'}, {'id': 4885, 'synset': 'capelin.n.01', 'name': 'capelin'}, {'id': 4886, 'synset': 'tarpon.n.01', 'name': 'tarpon'}, {'id': 4887, 'synset': 'ladyfish.n.01', 'name': 'ladyfish'}, {'id': 4888, 'synset': 'bonefish.n.01', 'name': 'bonefish'}, {'id': 4889, 'synset': 'argentine.n.01', 'name': 'argentine'}, {'id': 4890, 'synset': 'lanternfish.n.01', 'name': 'lanternfish'}, {'id': 4891, 'synset': 'lizardfish.n.01', 'name': 'lizardfish'}, {'id': 4892, 'synset': 'lancetfish.n.01', 'name': 'lancetfish'}, {'id': 4893, 'synset': 'opah.n.01', 'name': 'opah'}, {'id': 4894, 'synset': 'new_world_opah.n.01', 'name': 'New_World_opah'}, {'id': 4895, 'synset': 'ribbonfish.n.02', 'name': 'ribbonfish'}, {'id': 4896, 'synset': 'dealfish.n.01', 'name': 'dealfish'}, {'id': 4897, 'synset': 'oarfish.n.01', 'name': 'oarfish'}, {'id': 4898, 'synset': 'batfish.n.01', 'name': 'batfish'}, {'id': 4899, 'synset': 'goosefish.n.01', 'name': 'goosefish'}, {'id': 4900, 'synset': 'toadfish.n.01', 'name': 'toadfish'}, {'id': 4901, 'synset': 'oyster_fish.n.01', 'name': 'oyster_fish'}, {'id': 4902, 'synset': 'frogfish.n.01', 'name': 'frogfish'}, {'id': 4903, 'synset': 'sargassum_fish.n.01', 'name': 'sargassum_fish'}, {'id': 4904, 'synset': 'needlefish.n.01', 'name': 'needlefish'}, {'id': 4905, 'synset': 'timucu.n.01', 'name': 'timucu'}, {'id': 4906, 'synset': 'flying_fish.n.01', 'name': 'flying_fish'}, {'id': 4907, 'synset': 'monoplane_flying_fish.n.01', 'name': 'monoplane_flying_fish'}, {'id': 4908, 'synset': 'halfbeak.n.01', 'name': 'halfbeak'}, {'id': 4909, 'synset': 'saury.n.01', 'name': 'saury'}, {'id': 4910, 'synset': 'spiny-finned_fish.n.01', 'name': 'spiny-finned_fish'}, {'id': 4911, 'synset': 'lingcod.n.02', 'name': 'lingcod'}, {'id': 4912, 'synset': 'percoid_fish.n.01', 'name': 'percoid_fish'}, {'id': 4913, 'synset': 'perch.n.07', 'name': 'perch'}, {'id': 4914, 'synset': 'climbing_perch.n.01', 'name': 'climbing_perch'}, {'id': 4915, 'synset': 'perch.n.06', 'name': 'perch'}, {'id': 4916, 'synset': 'yellow_perch.n.01', 'name': 'yellow_perch'}, {'id': 4917, 'synset': 'european_perch.n.01', 'name': 'European_perch'}, {'id': 4918, 'synset': 'pike-perch.n.01', 'name': 'pike-perch'}, {'id': 4919, 'synset': 'walleye.n.02', 'name': 'walleye'}, {'id': 4920, 'synset': 'blue_pike.n.01', 'name': 'blue_pike'}, {'id': 4921, 'synset': 'snail_darter.n.01', 'name': 'snail_darter'}, {'id': 4922, 'synset': 'cusk-eel.n.01', 'name': 'cusk-eel'}, {'id': 4923, 'synset': 'brotula.n.01', 'name': 'brotula'}, {'id': 4924, 'synset': 'pearlfish.n.01', 'name': 'pearlfish'}, {'id': 4925, 'synset': 'robalo.n.01', 'name': 'robalo'}, {'id': 4926, 'synset': 'snook.n.01', 'name': 'snook'}, {'id': 4927, 'synset': 'pike.n.05', 'name': 'pike'}, {'id': 4928, 'synset': 'northern_pike.n.01', 'name': 'northern_pike'}, {'id': 4929, 'synset': 'muskellunge.n.02', 'name': 'muskellunge'}, {'id': 4930, 'synset': 'pickerel.n.02', 'name': 'pickerel'}, {'id': 4931, 'synset': 'chain_pickerel.n.01', 'name': 'chain_pickerel'}, {'id': 4932, 'synset': 'redfin_pickerel.n.01', 'name': 'redfin_pickerel'}, {'id': 4933, 'synset': 'sunfish.n.03', 'name': 'sunfish'}, {'id': 4934, 'synset': 'crappie.n.02', 'name': 'crappie'}, {'id': 4935, 'synset': 'black_crappie.n.01', 'name': 'black_crappie'}, {'id': 4936, 'synset': 'white_crappie.n.01', 'name': 'white_crappie'}, {'id': 4937, 'synset': 'freshwater_bream.n.02', 'name': 'freshwater_bream'}, {'id': 4938, 'synset': 'pumpkinseed.n.01', 'name': 'pumpkinseed'}, {'id': 4939, 'synset': 'bluegill.n.01', 'name': 'bluegill'}, {'id': 4940, 'synset': 'spotted_sunfish.n.01', 'name': 'spotted_sunfish'}, {'id': 4941, 'synset': 'freshwater_bass.n.02', 'name': 'freshwater_bass'}, {'id': 4942, 'synset': 'rock_bass.n.02', 'name': 'rock_bass'}, {'id': 4943, 'synset': 'black_bass.n.02', 'name': 'black_bass'}, {'id': 4944, 'synset': 'kentucky_black_bass.n.01', 'name': 'Kentucky_black_bass'}, {'id': 4945, 'synset': 'smallmouth.n.01', 'name': 'smallmouth'}, {'id': 4946, 'synset': 'largemouth.n.01', 'name': 'largemouth'}, {'id': 4947, 'synset': 'bass.n.08', 'name': 'bass'}, {'id': 4948, 'synset': 'serranid_fish.n.01', 'name': 'serranid_fish'}, {'id': 4949, 'synset': 'white_perch.n.01', 'name': 'white_perch'}, {'id': 4950, 'synset': 'yellow_bass.n.01', 'name': 'yellow_bass'}, {'id': 4951, 'synset': 'blackmouth_bass.n.01', 'name': 'blackmouth_bass'}, {'id': 4952, 'synset': 'rock_sea_bass.n.01', 'name': 'rock_sea_bass'}, {'id': 4953, 'synset': 'striped_bass.n.02', 'name': 'striped_bass'}, {'id': 4954, 'synset': 'stone_bass.n.01', 'name': 'stone_bass'}, {'id': 4955, 'synset': 'grouper.n.02', 'name': 'grouper'}, {'id': 4956, 'synset': 'hind.n.01', 'name': 'hind'}, {'id': 4957, 'synset': 'rock_hind.n.01', 'name': 'rock_hind'}, {'id': 4958, 'synset': 'creole-fish.n.01', 'name': 'creole-fish'}, {'id': 4959, 'synset': 'jewfish.n.02', 'name': 'jewfish'}, {'id': 4960, 'synset': 'soapfish.n.01', 'name': 'soapfish'}, {'id': 4961, 'synset': 'surfperch.n.01', 'name': 'surfperch'}, {'id': 4962, 'synset': 'rainbow_seaperch.n.01', 'name': 'rainbow_seaperch'}, {'id': 4963, 'synset': 'bigeye.n.01', 'name': 'bigeye'}, {'id': 4964, 'synset': 'catalufa.n.01', 'name': 'catalufa'}, {'id': 4965, 'synset': 'cardinalfish.n.01', 'name': 'cardinalfish'}, {'id': 4966, 'synset': 'flame_fish.n.01', 'name': 'flame_fish'}, {'id': 4967, 'synset': 'tilefish.n.02', 'name': 'tilefish'}, {'id': 4968, 'synset': 'bluefish.n.01', 'name': 'bluefish'}, {'id': 4969, 'synset': 'cobia.n.01', 'name': 'cobia'}, {'id': 4970, 'synset': 'remora.n.01', 'name': 'remora'}, {'id': 4971, 'synset': 'sharksucker.n.01', 'name': 'sharksucker'}, {'id': 4972, 'synset': 'whale_sucker.n.01', 'name': 'whale_sucker'}, {'id': 4973, 'synset': 'carangid_fish.n.01', 'name': 'carangid_fish'}, {'id': 4974, 'synset': 'jack.n.11', 'name': 'jack'}, {'id': 4975, 'synset': 'crevalle_jack.n.01', 'name': 'crevalle_jack'}, {'id': 4976, 'synset': 'yellow_jack.n.03', 'name': 'yellow_jack'}, {'id': 4977, 'synset': 'runner.n.10', 'name': 'runner'}, {'id': 4978, 'synset': 'rainbow_runner.n.01', 'name': 'rainbow_runner'}, {'id': 4979, 'synset': 'leatherjacket.n.02', 'name': 'leatherjacket'}, {'id': 4980, 'synset': 'threadfish.n.01', 'name': 'threadfish'}, {'id': 4981, 'synset': 'moonfish.n.01', 'name': 'moonfish'}, {'id': 4982, 'synset': 'lookdown.n.01', 'name': 'lookdown'}, {'id': 4983, 'synset': 'amberjack.n.01', 'name': 'amberjack'}, {'id': 4984, 'synset': 'yellowtail.n.02', 'name': 'yellowtail'}, {'id': 4985, 'synset': 'kingfish.n.05', 'name': 'kingfish'}, {'id': 4986, 'synset': 'pompano.n.02', 'name': 'pompano'}, {'id': 4987, 'synset': 'florida_pompano.n.01', 'name': 'Florida_pompano'}, {'id': 4988, 'synset': 'permit.n.03', 'name': 'permit'}, {'id': 4989, 'synset': 'scad.n.01', 'name': 'scad'}, {'id': 4990, 'synset': 'horse_mackerel.n.03', 'name': 'horse_mackerel'}, {'id': 4991, 'synset': 'horse_mackerel.n.02', 'name': 'horse_mackerel'}, {'id': 4992, 'synset': 'bigeye_scad.n.01', 'name': 'bigeye_scad'}, {'id': 4993, 'synset': 'mackerel_scad.n.01', 'name': 'mackerel_scad'}, {'id': 4994, 'synset': 'round_scad.n.01', 'name': 'round_scad'}, {'id': 4995, 'synset': 'dolphinfish.n.02', 'name': 'dolphinfish'}, {'id': 4996, 'synset': 'coryphaena_hippurus.n.01', 'name': 'Coryphaena_hippurus'}, {'id': 4997, 'synset': 'coryphaena_equisetis.n.01', 'name': 'Coryphaena_equisetis'}, {'id': 4998, 'synset': 'pomfret.n.01', 'name': 'pomfret'}, {'id': 4999, 'synset': 'characin.n.01', 'name': 'characin'}, {'id': 5000, 'synset': 'tetra.n.01', 'name': 'tetra'}, {'id': 5001, 'synset': 'cardinal_tetra.n.01', 'name': 'cardinal_tetra'}, {'id': 5002, 'synset': 'piranha.n.02', 'name': 'piranha'}, {'id': 5003, 'synset': 'cichlid.n.01', 'name': 'cichlid'}, {'id': 5004, 'synset': 'bolti.n.01', 'name': 'bolti'}, {'id': 5005, 'synset': 'snapper.n.05', 'name': 'snapper'}, {'id': 5006, 'synset': 'red_snapper.n.02', 'name': 'red_snapper'}, {'id': 5007, 'synset': 'grey_snapper.n.01', 'name': 'grey_snapper'}, {'id': 5008, 'synset': 'mutton_snapper.n.01', 'name': 'mutton_snapper'}, {'id': 5009, 'synset': 'schoolmaster.n.03', 'name': 'schoolmaster'}, {'id': 5010, 'synset': 'yellowtail.n.01', 'name': 'yellowtail'}, {'id': 5011, 'synset': 'grunt.n.03', 'name': 'grunt'}, {'id': 5012, 'synset': 'margate.n.01', 'name': 'margate'}, {'id': 5013, 'synset': 'spanish_grunt.n.01', 'name': 'Spanish_grunt'}, {'id': 5014, 'synset': 'tomtate.n.01', 'name': 'tomtate'}, {'id': 5015, 'synset': 'cottonwick.n.01', 'name': 'cottonwick'}, {'id': 5016, 'synset': "sailor's-choice.n.02", 'name': "sailor's-choice"}, {'id': 5017, 'synset': 'porkfish.n.01', 'name': 'porkfish'}, {'id': 5018, 'synset': 'pompon.n.02', 'name': 'pompon'}, {'id': 5019, 'synset': 'pigfish.n.02', 'name': 'pigfish'}, {'id': 5020, 'synset': 'sparid.n.01', 'name': 'sparid'}, {'id': 5021, 'synset': 'sea_bream.n.02', 'name': 'sea_bream'}, {'id': 5022, 'synset': 'porgy.n.02', 'name': 'porgy'}, {'id': 5023, 'synset': 'red_porgy.n.01', 'name': 'red_porgy'}, {'id': 5024, 'synset': 'european_sea_bream.n.01', 'name': 'European_sea_bream'}, {'id': 5025, 'synset': 'atlantic_sea_bream.n.01', 'name': 'Atlantic_sea_bream'}, {'id': 5026, 'synset': 'sheepshead.n.01', 'name': 'sheepshead'}, {'id': 5027, 'synset': 'pinfish.n.01', 'name': 'pinfish'}, {'id': 5028, 'synset': 'sheepshead_porgy.n.01', 'name': 'sheepshead_porgy'}, {'id': 5029, 'synset': 'snapper.n.04', 'name': 'snapper'}, {'id': 5030, 'synset': 'black_bream.n.01', 'name': 'black_bream'}, {'id': 5031, 'synset': 'scup.n.04', 'name': 'scup'}, {'id': 5032, 'synset': 'scup.n.03', 'name': 'scup'}, {'id': 5033, 'synset': 'sciaenid_fish.n.01', 'name': 'sciaenid_fish'}, {'id': 5034, 'synset': 'striped_drum.n.01', 'name': 'striped_drum'}, {'id': 5035, 'synset': 'jackknife-fish.n.01', 'name': 'jackknife-fish'}, {'id': 5036, 'synset': 'silver_perch.n.01', 'name': 'silver_perch'}, {'id': 5037, 'synset': 'red_drum.n.01', 'name': 'red_drum'}, {'id': 5038, 'synset': 'mulloway.n.01', 'name': 'mulloway'}, {'id': 5039, 'synset': 'maigre.n.01', 'name': 'maigre'}, {'id': 5040, 'synset': 'croaker.n.02', 'name': 'croaker'}, {'id': 5041, 'synset': 'atlantic_croaker.n.01', 'name': 'Atlantic_croaker'}, {'id': 5042, 'synset': 'yellowfin_croaker.n.01', 'name': 'yellowfin_croaker'}, {'id': 5043, 'synset': 'whiting.n.04', 'name': 'whiting'}, {'id': 5044, 'synset': 'kingfish.n.04', 'name': 'kingfish'}, {'id': 5045, 'synset': 'king_whiting.n.01', 'name': 'king_whiting'}, {'id': 5046, 'synset': 'northern_whiting.n.01', 'name': 'northern_whiting'}, {'id': 5047, 'synset': 'corbina.n.01', 'name': 'corbina'}, {'id': 5048, 'synset': 'white_croaker.n.02', 'name': 'white_croaker'}, {'id': 5049, 'synset': 'white_croaker.n.01', 'name': 'white_croaker'}, {'id': 5050, 'synset': 'sea_trout.n.02', 'name': 'sea_trout'}, {'id': 5051, 'synset': 'weakfish.n.02', 'name': 'weakfish'}, {'id': 5052, 'synset': 'spotted_weakfish.n.01', 'name': 'spotted_weakfish'}, {'id': 5053, 'synset': 'mullet.n.03', 'name': 'mullet'}, {'id': 5054, 'synset': 'goatfish.n.01', 'name': 'goatfish'}, {'id': 5055, 'synset': 'red_goatfish.n.01', 'name': 'red_goatfish'}, {'id': 5056, 'synset': 'yellow_goatfish.n.01', 'name': 'yellow_goatfish'}, {'id': 5057, 'synset': 'mullet.n.02', 'name': 'mullet'}, {'id': 5058, 'synset': 'striped_mullet.n.01', 'name': 'striped_mullet'}, {'id': 5059, 'synset': 'white_mullet.n.01', 'name': 'white_mullet'}, {'id': 5060, 'synset': 'liza.n.01', 'name': 'liza'}, {'id': 5061, 'synset': 'silversides.n.01', 'name': 'silversides'}, {'id': 5062, 'synset': 'jacksmelt.n.01', 'name': 'jacksmelt'}, {'id': 5063, 'synset': 'barracuda.n.01', 'name': 'barracuda'}, {'id': 5064, 'synset': 'great_barracuda.n.01', 'name': 'great_barracuda'}, {'id': 5065, 'synset': 'sweeper.n.03', 'name': 'sweeper'}, {'id': 5066, 'synset': 'sea_chub.n.01', 'name': 'sea_chub'}, {'id': 5067, 'synset': 'bermuda_chub.n.01', 'name': 'Bermuda_chub'}, {'id': 5068, 'synset': 'spadefish.n.01', 'name': 'spadefish'}, {'id': 5069, 'synset': 'butterfly_fish.n.01', 'name': 'butterfly_fish'}, {'id': 5070, 'synset': 'chaetodon.n.01', 'name': 'chaetodon'}, {'id': 5071, 'synset': 'angelfish.n.01', 'name': 'angelfish'}, {'id': 5072, 'synset': 'rock_beauty.n.01', 'name': 'rock_beauty'}, {'id': 5073, 'synset': 'damselfish.n.01', 'name': 'damselfish'}, {'id': 5074, 'synset': 'beaugregory.n.01', 'name': 'beaugregory'}, {'id': 5075, 'synset': 'anemone_fish.n.01', 'name': 'anemone_fish'}, {'id': 5076, 'synset': 'clown_anemone_fish.n.01', 'name': 'clown_anemone_fish'}, {'id': 5077, 'synset': 'sergeant_major.n.02', 'name': 'sergeant_major'}, {'id': 5078, 'synset': 'wrasse.n.01', 'name': 'wrasse'}, {'id': 5079, 'synset': 'pigfish.n.01', 'name': 'pigfish'}, {'id': 5080, 'synset': 'hogfish.n.01', 'name': 'hogfish'}, {'id': 5081, 'synset': 'slippery_dick.n.01', 'name': 'slippery_dick'}, {'id': 5082, 'synset': 'puddingwife.n.01', 'name': 'puddingwife'}, {'id': 5083, 'synset': 'bluehead.n.01', 'name': 'bluehead'}, {'id': 5084, 'synset': 'pearly_razorfish.n.01', 'name': 'pearly_razorfish'}, {'id': 5085, 'synset': 'tautog.n.01', 'name': 'tautog'}, {'id': 5086, 'synset': 'cunner.n.01', 'name': 'cunner'}, {'id': 5087, 'synset': 'parrotfish.n.01', 'name': 'parrotfish'}, {'id': 5088, 'synset': 'threadfin.n.01', 'name': 'threadfin'}, {'id': 5089, 'synset': 'jawfish.n.01', 'name': 'jawfish'}, {'id': 5090, 'synset': 'stargazer.n.03', 'name': 'stargazer'}, {'id': 5091, 'synset': 'sand_stargazer.n.01', 'name': 'sand_stargazer'}, {'id': 5092, 'synset': 'blenny.n.01', 'name': 'blenny'}, {'id': 5093, 'synset': 'shanny.n.01', 'name': 'shanny'}, {'id': 5094, 'synset': 'molly_miller.n.01', 'name': 'Molly_Miller'}, {'id': 5095, 'synset': 'clinid.n.01', 'name': 'clinid'}, {'id': 5096, 'synset': 'pikeblenny.n.01', 'name': 'pikeblenny'}, {'id': 5097, 'synset': 'bluethroat_pikeblenny.n.01', 'name': 'bluethroat_pikeblenny'}, {'id': 5098, 'synset': 'gunnel.n.02', 'name': 'gunnel'}, {'id': 5099, 'synset': 'rock_gunnel.n.01', 'name': 'rock_gunnel'}, {'id': 5100, 'synset': 'eelblenny.n.01', 'name': 'eelblenny'}, {'id': 5101, 'synset': 'wrymouth.n.01', 'name': 'wrymouth'}, {'id': 5102, 'synset': 'wolffish.n.01', 'name': 'wolffish'}, {'id': 5103, 'synset': 'viviparous_eelpout.n.01', 'name': 'viviparous_eelpout'}, {'id': 5104, 'synset': 'ocean_pout.n.01', 'name': 'ocean_pout'}, {'id': 5105, 'synset': 'sand_lance.n.01', 'name': 'sand_lance'}, {'id': 5106, 'synset': 'dragonet.n.01', 'name': 'dragonet'}, {'id': 5107, 'synset': 'goby.n.01', 'name': 'goby'}, {'id': 5108, 'synset': 'mudskipper.n.01', 'name': 'mudskipper'}, {'id': 5109, 'synset': 'sleeper.n.08', 'name': 'sleeper'}, {'id': 5110, 'synset': 'flathead.n.02', 'name': 'flathead'}, {'id': 5111, 'synset': 'archerfish.n.01', 'name': 'archerfish'}, {'id': 5112, 'synset': 'surgeonfish.n.01', 'name': 'surgeonfish'}, {'id': 5113, 'synset': 'gempylid.n.01', 'name': 'gempylid'}, {'id': 5114, 'synset': 'snake_mackerel.n.01', 'name': 'snake_mackerel'}, {'id': 5115, 'synset': 'escolar.n.01', 'name': 'escolar'}, {'id': 5116, 'synset': 'oilfish.n.01', 'name': 'oilfish'}, {'id': 5117, 'synset': 'cutlassfish.n.01', 'name': 'cutlassfish'}, {'id': 5118, 'synset': 'scombroid.n.01', 'name': 'scombroid'}, {'id': 5119, 'synset': 'mackerel.n.02', 'name': 'mackerel'}, {'id': 5120, 'synset': 'common_mackerel.n.01', 'name': 'common_mackerel'}, {'id': 5121, 'synset': 'spanish_mackerel.n.03', 'name': 'Spanish_mackerel'}, {'id': 5122, 'synset': 'chub_mackerel.n.01', 'name': 'chub_mackerel'}, {'id': 5123, 'synset': 'wahoo.n.03', 'name': 'wahoo'}, {'id': 5124, 'synset': 'spanish_mackerel.n.02', 'name': 'Spanish_mackerel'}, {'id': 5125, 'synset': 'king_mackerel.n.01', 'name': 'king_mackerel'}, {'id': 5126, 'synset': 'scomberomorus_maculatus.n.01', 'name': 'Scomberomorus_maculatus'}, {'id': 5127, 'synset': 'cero.n.01', 'name': 'cero'}, {'id': 5128, 'synset': 'sierra.n.02', 'name': 'sierra'}, {'id': 5129, 'synset': 'tuna.n.03', 'name': 'tuna'}, {'id': 5130, 'synset': 'albacore.n.02', 'name': 'albacore'}, {'id': 5131, 'synset': 'bluefin.n.02', 'name': 'bluefin'}, {'id': 5132, 'synset': 'yellowfin.n.01', 'name': 'yellowfin'}, {'id': 5133, 'synset': 'bonito.n.03', 'name': 'bonito'}, {'id': 5134, 'synset': 'skipjack.n.02', 'name': 'skipjack'}, {'id': 5135, 'synset': 'chile_bonito.n.01', 'name': 'Chile_bonito'}, {'id': 5136, 'synset': 'skipjack.n.01', 'name': 'skipjack'}, {'id': 5137, 'synset': 'bonito.n.02', 'name': 'bonito'}, {'id': 5138, 'synset': 'swordfish.n.02', 'name': 'swordfish'}, {'id': 5139, 'synset': 'sailfish.n.02', 'name': 'sailfish'}, {'id': 5140, 'synset': 'atlantic_sailfish.n.01', 'name': 'Atlantic_sailfish'}, {'id': 5141, 'synset': 'billfish.n.02', 'name': 'billfish'}, {'id': 5142, 'synset': 'marlin.n.01', 'name': 'marlin'}, {'id': 5143, 'synset': 'blue_marlin.n.01', 'name': 'blue_marlin'}, {'id': 5144, 'synset': 'black_marlin.n.01', 'name': 'black_marlin'}, {'id': 5145, 'synset': 'striped_marlin.n.01', 'name': 'striped_marlin'}, {'id': 5146, 'synset': 'white_marlin.n.01', 'name': 'white_marlin'}, {'id': 5147, 'synset': 'spearfish.n.01', 'name': 'spearfish'}, {'id': 5148, 'synset': 'louvar.n.01', 'name': 'louvar'}, {'id': 5149, 'synset': 'dollarfish.n.01', 'name': 'dollarfish'}, {'id': 5150, 'synset': 'palometa.n.01', 'name': 'palometa'}, {'id': 5151, 'synset': 'harvestfish.n.01', 'name': 'harvestfish'}, {'id': 5152, 'synset': 'driftfish.n.01', 'name': 'driftfish'}, {'id': 5153, 'synset': 'barrelfish.n.01', 'name': 'barrelfish'}, {'id': 5154, 'synset': 'clingfish.n.01', 'name': 'clingfish'}, {'id': 5155, 'synset': 'tripletail.n.01', 'name': 'tripletail'}, {'id': 5156, 'synset': 'atlantic_tripletail.n.01', 'name': 'Atlantic_tripletail'}, {'id': 5157, 'synset': 'pacific_tripletail.n.01', 'name': 'Pacific_tripletail'}, {'id': 5158, 'synset': 'mojarra.n.01', 'name': 'mojarra'}, {'id': 5159, 'synset': 'yellowfin_mojarra.n.01', 'name': 'yellowfin_mojarra'}, {'id': 5160, 'synset': 'silver_jenny.n.01', 'name': 'silver_jenny'}, {'id': 5161, 'synset': 'whiting.n.03', 'name': 'whiting'}, {'id': 5162, 'synset': 'ganoid.n.01', 'name': 'ganoid'}, {'id': 5163, 'synset': 'bowfin.n.01', 'name': 'bowfin'}, {'id': 5164, 'synset': 'paddlefish.n.01', 'name': 'paddlefish'}, {'id': 5165, 'synset': 'chinese_paddlefish.n.01', 'name': 'Chinese_paddlefish'}, {'id': 5166, 'synset': 'sturgeon.n.01', 'name': 'sturgeon'}, {'id': 5167, 'synset': 'pacific_sturgeon.n.01', 'name': 'Pacific_sturgeon'}, {'id': 5168, 'synset': 'beluga.n.01', 'name': 'beluga'}, {'id': 5169, 'synset': 'gar.n.01', 'name': 'gar'}, {'id': 5170, 'synset': 'scorpaenoid.n.01', 'name': 'scorpaenoid'}, {'id': 5171, 'synset': 'scorpaenid.n.01', 'name': 'scorpaenid'}, {'id': 5172, 'synset': 'scorpionfish.n.01', 'name': 'scorpionfish'}, {'id': 5173, 'synset': 'plumed_scorpionfish.n.01', 'name': 'plumed_scorpionfish'}, {'id': 5174, 'synset': 'lionfish.n.01', 'name': 'lionfish'}, {'id': 5175, 'synset': 'stonefish.n.01', 'name': 'stonefish'}, {'id': 5176, 'synset': 'rockfish.n.02', 'name': 'rockfish'}, {'id': 5177, 'synset': 'copper_rockfish.n.01', 'name': 'copper_rockfish'}, {'id': 5178, 'synset': 'vermillion_rockfish.n.01', 'name': 'vermillion_rockfish'}, {'id': 5179, 'synset': 'red_rockfish.n.02', 'name': 'red_rockfish'}, {'id': 5180, 'synset': 'rosefish.n.02', 'name': 'rosefish'}, {'id': 5181, 'synset': 'bullhead.n.01', 'name': 'bullhead'}, {'id': 5182, 'synset': "miller's-thumb.n.01", 'name': "miller's-thumb"}, {'id': 5183, 'synset': 'sea_raven.n.01', 'name': 'sea_raven'}, {'id': 5184, 'synset': 'lumpfish.n.01', 'name': 'lumpfish'}, {'id': 5185, 'synset': 'lumpsucker.n.01', 'name': 'lumpsucker'}, {'id': 5186, 'synset': 'pogge.n.01', 'name': 'pogge'}, {'id': 5187, 'synset': 'greenling.n.01', 'name': 'greenling'}, {'id': 5188, 'synset': 'kelp_greenling.n.01', 'name': 'kelp_greenling'}, {'id': 5189, 'synset': 'painted_greenling.n.01', 'name': 'painted_greenling'}, {'id': 5190, 'synset': 'flathead.n.01', 'name': 'flathead'}, {'id': 5191, 'synset': 'gurnard.n.01', 'name': 'gurnard'}, {'id': 5192, 'synset': 'tub_gurnard.n.01', 'name': 'tub_gurnard'}, {'id': 5193, 'synset': 'sea_robin.n.01', 'name': 'sea_robin'}, {'id': 5194, 'synset': 'northern_sea_robin.n.01', 'name': 'northern_sea_robin'}, {'id': 5195, 'synset': 'flying_gurnard.n.01', 'name': 'flying_gurnard'}, {'id': 5196, 'synset': 'plectognath.n.01', 'name': 'plectognath'}, {'id': 5197, 'synset': 'triggerfish.n.01', 'name': 'triggerfish'}, {'id': 5198, 'synset': 'queen_triggerfish.n.01', 'name': 'queen_triggerfish'}, {'id': 5199, 'synset': 'filefish.n.01', 'name': 'filefish'}, {'id': 5200, 'synset': 'leatherjacket.n.01', 'name': 'leatherjacket'}, {'id': 5201, 'synset': 'boxfish.n.01', 'name': 'boxfish'}, {'id': 5202, 'synset': 'cowfish.n.01', 'name': 'cowfish'}, {'id': 5203, 'synset': 'spiny_puffer.n.01', 'name': 'spiny_puffer'}, {'id': 5204, 'synset': 'porcupinefish.n.01', 'name': 'porcupinefish'}, {'id': 5205, 'synset': 'balloonfish.n.01', 'name': 'balloonfish'}, {'id': 5206, 'synset': 'burrfish.n.01', 'name': 'burrfish'}, {'id': 5207, 'synset': 'ocean_sunfish.n.01', 'name': 'ocean_sunfish'}, {'id': 5208, 'synset': 'sharptail_mola.n.01', 'name': 'sharptail_mola'}, {'id': 5209, 'synset': 'flatfish.n.02', 'name': 'flatfish'}, {'id': 5210, 'synset': 'flounder.n.02', 'name': 'flounder'}, {'id': 5211, 'synset': 'righteye_flounder.n.01', 'name': 'righteye_flounder'}, {'id': 5212, 'synset': 'plaice.n.02', 'name': 'plaice'}, {'id': 5213, 'synset': 'european_flatfish.n.01', 'name': 'European_flatfish'}, {'id': 5214, 'synset': 'yellowtail_flounder.n.02', 'name': 'yellowtail_flounder'}, {'id': 5215, 'synset': 'winter_flounder.n.02', 'name': 'winter_flounder'}, {'id': 5216, 'synset': 'lemon_sole.n.05', 'name': 'lemon_sole'}, {'id': 5217, 'synset': 'american_plaice.n.01', 'name': 'American_plaice'}, {'id': 5218, 'synset': 'halibut.n.02', 'name': 'halibut'}, {'id': 5219, 'synset': 'atlantic_halibut.n.01', 'name': 'Atlantic_halibut'}, {'id': 5220, 'synset': 'pacific_halibut.n.01', 'name': 'Pacific_halibut'}, {'id': 5221, 'synset': 'lefteye_flounder.n.01', 'name': 'lefteye_flounder'}, {'id': 5222, 'synset': 'southern_flounder.n.01', 'name': 'southern_flounder'}, {'id': 5223, 'synset': 'summer_flounder.n.01', 'name': 'summer_flounder'}, {'id': 5224, 'synset': 'whiff.n.02', 'name': 'whiff'}, {'id': 5225, 'synset': 'horned_whiff.n.01', 'name': 'horned_whiff'}, {'id': 5226, 'synset': 'sand_dab.n.02', 'name': 'sand_dab'}, {'id': 5227, 'synset': 'windowpane.n.02', 'name': 'windowpane'}, {'id': 5228, 'synset': 'brill.n.01', 'name': 'brill'}, {'id': 5229, 'synset': 'turbot.n.02', 'name': 'turbot'}, {'id': 5230, 'synset': 'tonguefish.n.01', 'name': 'tonguefish'}, {'id': 5231, 'synset': 'sole.n.04', 'name': 'sole'}, {'id': 5232, 'synset': 'european_sole.n.01', 'name': 'European_sole'}, {'id': 5233, 'synset': 'english_sole.n.02', 'name': 'English_sole'}, {'id': 5234, 'synset': 'hogchoker.n.01', 'name': 'hogchoker'}, {'id': 5235, 'synset': 'aba.n.02', 'name': 'aba'}, {'id': 5236, 'synset': 'abacus.n.02', 'name': 'abacus'}, {'id': 5237, 'synset': 'abandoned_ship.n.01', 'name': 'abandoned_ship'}, {'id': 5238, 'synset': 'a_battery.n.01', 'name': 'A_battery'}, {'id': 5239, 'synset': 'abattoir.n.01', 'name': 'abattoir'}, {'id': 5240, 'synset': 'abaya.n.01', 'name': 'abaya'}, {'id': 5241, 'synset': 'abbe_condenser.n.01', 'name': 'Abbe_condenser'}, {'id': 5242, 'synset': 'abbey.n.03', 'name': 'abbey'}, {'id': 5243, 'synset': 'abbey.n.02', 'name': 'abbey'}, {'id': 5244, 'synset': 'abbey.n.01', 'name': 'abbey'}, {'id': 5245, 'synset': 'abney_level.n.01', 'name': 'Abney_level'}, {'id': 5246, 'synset': 'abrader.n.01', 'name': 'abrader'}, {'id': 5247, 'synset': 'abrading_stone.n.01', 'name': 'abrading_stone'}, {'id': 5248, 'synset': 'abutment.n.02', 'name': 'abutment'}, {'id': 5249, 'synset': 'abutment_arch.n.01', 'name': 'abutment_arch'}, {'id': 5250, 'synset': 'academic_costume.n.01', 'name': 'academic_costume'}, {'id': 5251, 'synset': 'academic_gown.n.01', 'name': 'academic_gown'}, {'id': 5252, 'synset': 'accelerator.n.02', 'name': 'accelerator'}, {'id': 5253, 'synset': 'accelerator.n.04', 'name': 'accelerator'}, {'id': 5254, 'synset': 'accelerator.n.01', 'name': 'accelerator'}, {'id': 5255, 'synset': 'accelerometer.n.01', 'name': 'accelerometer'}, {'id': 5256, 'synset': 'accessory.n.01', 'name': 'accessory'}, {'id': 5257, 'synset': 'accommodating_lens_implant.n.01', 'name': 'accommodating_lens_implant'}, {'id': 5258, 'synset': 'accommodation.n.04', 'name': 'accommodation'}, {'id': 5259, 'synset': 'accordion.n.01', 'name': 'accordion'}, {'id': 5260, 'synset': 'acetate_disk.n.01', 'name': 'acetate_disk'}, {'id': 5261, 'synset': 'acetate_rayon.n.01', 'name': 'acetate_rayon'}, {'id': 5262, 'synset': 'achromatic_lens.n.01', 'name': 'achromatic_lens'}, {'id': 5263, 'synset': 'acoustic_delay_line.n.01', 'name': 'acoustic_delay_line'}, {'id': 5264, 'synset': 'acoustic_device.n.01', 'name': 'acoustic_device'}, {'id': 5265, 'synset': 'acoustic_guitar.n.01', 'name': 'acoustic_guitar'}, {'id': 5266, 'synset': 'acoustic_modem.n.01', 'name': 'acoustic_modem'}, {'id': 5267, 'synset': 'acropolis.n.01', 'name': 'acropolis'}, {'id': 5268, 'synset': 'acrylic.n.04', 'name': 'acrylic'}, {'id': 5269, 'synset': 'acrylic.n.03', 'name': 'acrylic'}, {'id': 5270, 'synset': 'actinometer.n.01', 'name': 'actinometer'}, {'id': 5271, 'synset': 'action.n.07', 'name': 'action'}, {'id': 5272, 'synset': 'active_matrix_screen.n.01', 'name': 'active_matrix_screen'}, {'id': 5273, 'synset': 'actuator.n.01', 'name': 'actuator'}, {'id': 5274, 'synset': 'adapter.n.02', 'name': 'adapter'}, {'id': 5275, 'synset': 'adder.n.02', 'name': 'adder'}, {'id': 5276, 'synset': 'adding_machine.n.01', 'name': 'adding_machine'}, {'id': 5277, 'synset': 'addressing_machine.n.01', 'name': 'addressing_machine'}, {'id': 5278, 'synset': 'adhesive_bandage.n.01', 'name': 'adhesive_bandage'}, {'id': 5279, 'synset': 'adit.n.01', 'name': 'adit'}, {'id': 5280, 'synset': 'adjoining_room.n.01', 'name': 'adjoining_room'}, {'id': 5281, 'synset': 'adjustable_wrench.n.01', 'name': 'adjustable_wrench'}, {'id': 5282, 'synset': 'adobe.n.02', 'name': 'adobe'}, {'id': 5283, 'synset': 'adz.n.01', 'name': 'adz'}, {'id': 5284, 'synset': 'aeolian_harp.n.01', 'name': 'aeolian_harp'}, {'id': 5285, 'synset': 'aerator.n.01', 'name': 'aerator'}, {'id': 5286, 'synset': 'aerial_torpedo.n.01', 'name': 'aerial_torpedo'}, {'id': 5287, 'synset': 'aertex.n.01', 'name': 'Aertex'}, {'id': 5288, 'synset': 'afghan.n.01', 'name': 'afghan'}, {'id': 5289, 'synset': 'afro-wig.n.01', 'name': 'Afro-wig'}, {'id': 5290, 'synset': 'afterburner.n.01', 'name': 'afterburner'}, {'id': 5291, 'synset': 'after-shave.n.01', 'name': 'after-shave'}, {'id': 5292, 'synset': 'agateware.n.01', 'name': 'agateware'}, {'id': 5293, 'synset': 'agglomerator.n.01', 'name': 'agglomerator'}, {'id': 5294, 'synset': 'aglet.n.02', 'name': 'aglet'}, {'id': 5295, 'synset': 'aglet.n.01', 'name': 'aglet'}, {'id': 5296, 'synset': 'agora.n.03', 'name': 'agora'}, {'id': 5297, 'synset': 'aigrette.n.01', 'name': 'aigrette'}, {'id': 5298, 'synset': 'aileron.n.01', 'name': 'aileron'}, {'id': 5299, 'synset': 'air_bag.n.01', 'name': 'air_bag'}, {'id': 5300, 'synset': 'airbrake.n.02', 'name': 'airbrake'}, {'id': 5301, 'synset': 'airbrush.n.01', 'name': 'airbrush'}, {'id': 5302, 'synset': 'airbus.n.01', 'name': 'airbus'}, {'id': 5303, 'synset': 'air_compressor.n.01', 'name': 'air_compressor'}, {'id': 5304, 'synset': 'aircraft.n.01', 'name': 'aircraft'}, {'id': 5305, 'synset': 'aircraft_carrier.n.01', 'name': 'aircraft_carrier'}, {'id': 5306, 'synset': 'aircraft_engine.n.01', 'name': 'aircraft_engine'}, {'id': 5307, 'synset': 'air_cushion.n.02', 'name': 'air_cushion'}, {'id': 5308, 'synset': 'airdock.n.01', 'name': 'airdock'}, {'id': 5309, 'synset': 'airfield.n.01', 'name': 'airfield'}, {'id': 5310, 'synset': 'air_filter.n.01', 'name': 'air_filter'}, {'id': 5311, 'synset': 'airfoil.n.01', 'name': 'airfoil'}, {'id': 5312, 'synset': 'airframe.n.01', 'name': 'airframe'}, {'id': 5313, 'synset': 'air_gun.n.01', 'name': 'air_gun'}, {'id': 5314, 'synset': 'air_hammer.n.01', 'name': 'air_hammer'}, {'id': 5315, 'synset': 'air_horn.n.01', 'name': 'air_horn'}, {'id': 5316, 'synset': 'airing_cupboard.n.01', 'name': 'airing_cupboard'}, {'id': 5317, 'synset': 'airliner.n.01', 'name': 'airliner'}, {'id': 5318, 'synset': 'airmailer.n.01', 'name': 'airmailer'}, {'id': 5319, 'synset': 'airplane_propeller.n.01', 'name': 'airplane_propeller'}, {'id': 5320, 'synset': 'airport.n.01', 'name': 'airport'}, {'id': 5321, 'synset': 'air_pump.n.01', 'name': 'air_pump'}, {'id': 5322, 'synset': 'air_search_radar.n.01', 'name': 'air_search_radar'}, {'id': 5323, 'synset': 'airship.n.01', 'name': 'airship'}, {'id': 5324, 'synset': 'air_terminal.n.01', 'name': 'air_terminal'}, {'id': 5325, 'synset': 'air-to-air_missile.n.01', 'name': 'air-to-air_missile'}, {'id': 5326, 'synset': 'air-to-ground_missile.n.01', 'name': 'air-to-ground_missile'}, {'id': 5327, 'synset': 'aisle.n.03', 'name': 'aisle'}, {'id': 5328, 'synset': "aladdin's_lamp.n.01", 'name': "Aladdin's_lamp"}, {'id': 5329, 'synset': 'alarm.n.02', 'name': 'alarm'}, {'id': 5330, 'synset': 'alb.n.01', 'name': 'alb'}, {'id': 5331, 'synset': 'alcazar.n.01', 'name': 'alcazar'}, {'id': 5332, 'synset': 'alcohol_thermometer.n.01', 'name': 'alcohol_thermometer'}, {'id': 5333, 'synset': 'alehouse.n.01', 'name': 'alehouse'}, {'id': 5334, 'synset': 'alembic.n.01', 'name': 'alembic'}, {'id': 5335, 'synset': 'algometer.n.01', 'name': 'algometer'}, {'id': 5336, 'synset': 'alidade.n.02', 'name': 'alidade'}, {'id': 5337, 'synset': 'alidade.n.01', 'name': 'alidade'}, {'id': 5338, 'synset': 'a-line.n.01', 'name': 'A-line'}, {'id': 5339, 'synset': 'allen_screw.n.01', 'name': 'Allen_screw'}, {'id': 5340, 'synset': 'allen_wrench.n.01', 'name': 'Allen_wrench'}, {'id': 5341, 'synset': 'alligator_wrench.n.01', 'name': 'alligator_wrench'}, {'id': 5342, 'synset': 'alms_dish.n.01', 'name': 'alms_dish'}, {'id': 5343, 'synset': 'alpaca.n.02', 'name': 'alpaca'}, {'id': 5344, 'synset': 'alpenstock.n.01', 'name': 'alpenstock'}, {'id': 5345, 'synset': 'altar.n.02', 'name': 'altar'}, {'id': 5346, 'synset': 'altar.n.01', 'name': 'altar'}, {'id': 5347, 'synset': 'altarpiece.n.01', 'name': 'altarpiece'}, {'id': 5348, 'synset': 'altazimuth.n.01', 'name': 'altazimuth'}, {'id': 5349, 'synset': 'alternator.n.01', 'name': 'alternator'}, {'id': 5350, 'synset': 'altimeter.n.01', 'name': 'altimeter'}, {'id': 5351, 'synset': 'amati.n.02', 'name': 'Amati'}, {'id': 5352, 'synset': 'amen_corner.n.01', 'name': 'amen_corner'}, {'id': 5353, 'synset': 'american_organ.n.01', 'name': 'American_organ'}, {'id': 5354, 'synset': 'ammeter.n.01', 'name': 'ammeter'}, {'id': 5355, 'synset': 'ammonia_clock.n.01', 'name': 'ammonia_clock'}, {'id': 5356, 'synset': 'ammunition.n.01', 'name': 'ammunition'}, {'id': 5357, 'synset': 'amphibian.n.02', 'name': 'amphibian'}, {'id': 5358, 'synset': 'amphibian.n.01', 'name': 'amphibian'}, {'id': 5359, 'synset': 'amphitheater.n.02', 'name': 'amphitheater'}, {'id': 5360, 'synset': 'amphitheater.n.01', 'name': 'amphitheater'}, {'id': 5361, 'synset': 'amphora.n.01', 'name': 'amphora'}, {'id': 5362, 'synset': 'ampulla.n.02', 'name': 'ampulla'}, {'id': 5363, 'synset': 'amusement_arcade.n.01', 'name': 'amusement_arcade'}, {'id': 5364, 'synset': 'analog_clock.n.01', 'name': 'analog_clock'}, {'id': 5365, 'synset': 'analog_computer.n.01', 'name': 'analog_computer'}, {'id': 5366, 'synset': 'analog_watch.n.01', 'name': 'analog_watch'}, {'id': 5367, 'synset': 'analytical_balance.n.01', 'name': 'analytical_balance'}, {'id': 5368, 'synset': 'analyzer.n.01', 'name': 'analyzer'}, {'id': 5369, 'synset': 'anamorphosis.n.02', 'name': 'anamorphosis'}, {'id': 5370, 'synset': 'anastigmat.n.01', 'name': 'anastigmat'}, {'id': 5371, 'synset': 'anchor.n.01', 'name': 'anchor'}, {'id': 5372, 'synset': 'anchor_chain.n.01', 'name': 'anchor_chain'}, {'id': 5373, 'synset': 'anchor_light.n.01', 'name': 'anchor_light'}, {'id': 5374, 'synset': 'and_circuit.n.01', 'name': 'AND_circuit'}, {'id': 5375, 'synset': 'andiron.n.01', 'name': 'andiron'}, {'id': 5376, 'synset': 'android.n.01', 'name': 'android'}, {'id': 5377, 'synset': 'anechoic_chamber.n.01', 'name': 'anechoic_chamber'}, {'id': 5378, 'synset': 'anemometer.n.01', 'name': 'anemometer'}, {'id': 5379, 'synset': 'aneroid_barometer.n.01', 'name': 'aneroid_barometer'}, {'id': 5380, 'synset': 'angiocardiogram.n.01', 'name': 'angiocardiogram'}, {'id': 5381, 'synset': 'angioscope.n.01', 'name': 'angioscope'}, {'id': 5382, 'synset': 'angle_bracket.n.02', 'name': 'angle_bracket'}, {'id': 5383, 'synset': 'angledozer.n.01', 'name': 'angledozer'}, {'id': 5384, 'synset': 'ankle_brace.n.01', 'name': 'ankle_brace'}, {'id': 5385, 'synset': 'anklet.n.02', 'name': 'anklet'}, {'id': 5386, 'synset': 'anklet.n.01', 'name': 'anklet'}, {'id': 5387, 'synset': 'ankus.n.01', 'name': 'ankus'}, {'id': 5388, 'synset': 'anode.n.01', 'name': 'anode'}, {'id': 5389, 'synset': 'anode.n.02', 'name': 'anode'}, {'id': 5390, 'synset': 'answering_machine.n.01', 'name': 'answering_machine'}, {'id': 5391, 'synset': 'anteroom.n.01', 'name': 'anteroom'}, {'id': 5392, 'synset': 'antiaircraft.n.01', 'name': 'antiaircraft'}, {'id': 5393, 'synset': 'antiballistic_missile.n.01', 'name': 'antiballistic_missile'}, {'id': 5394, 'synset': 'antifouling_paint.n.01', 'name': 'antifouling_paint'}, {'id': 5395, 'synset': 'anti-g_suit.n.01', 'name': 'anti-G_suit'}, {'id': 5396, 'synset': 'antimacassar.n.01', 'name': 'antimacassar'}, {'id': 5397, 'synset': 'antiperspirant.n.01', 'name': 'antiperspirant'}, {'id': 5398, 'synset': 'anti-submarine_rocket.n.01', 'name': 'anti-submarine_rocket'}, {'id': 5399, 'synset': 'anvil.n.01', 'name': 'anvil'}, {'id': 5400, 'synset': 'ao_dai.n.01', 'name': 'ao_dai'}, {'id': 5401, 'synset': 'apadana.n.01', 'name': 'apadana'}, {'id': 5402, 'synset': 'apartment.n.01', 'name': 'apartment'}, {'id': 5403, 'synset': 'apartment_building.n.01', 'name': 'apartment_building'}, {'id': 5404, 'synset': 'aperture.n.03', 'name': 'aperture'}, {'id': 5405, 'synset': 'aperture.n.01', 'name': 'aperture'}, {'id': 5406, 'synset': 'apiary.n.01', 'name': 'apiary'}, {'id': 5407, 'synset': 'apparatus.n.01', 'name': 'apparatus'}, {'id': 5408, 'synset': 'apparel.n.01', 'name': 'apparel'}, {'id': 5409, 'synset': 'applecart.n.02', 'name': 'applecart'}, {'id': 5410, 'synset': 'appliance.n.02', 'name': 'appliance'}, {'id': 5411, 'synset': 'appliance.n.01', 'name': 'appliance'}, {'id': 5412, 'synset': 'applicator.n.01', 'name': 'applicator'}, {'id': 5413, 'synset': 'appointment.n.03', 'name': 'appointment'}, {'id': 5414, 'synset': 'apron_string.n.01', 'name': 'apron_string'}, {'id': 5415, 'synset': 'apse.n.01', 'name': 'apse'}, {'id': 5416, 'synset': 'aqualung.n.01', 'name': 'aqualung'}, {'id': 5417, 'synset': 'aquaplane.n.01', 'name': 'aquaplane'}, {'id': 5418, 'synset': 'arabesque.n.02', 'name': 'arabesque'}, {'id': 5419, 'synset': 'arbor.n.03', 'name': 'arbor'}, {'id': 5420, 'synset': 'arcade.n.02', 'name': 'arcade'}, {'id': 5421, 'synset': 'arch.n.04', 'name': 'arch'}, {'id': 5422, 'synset': 'architecture.n.01', 'name': 'architecture'}, {'id': 5423, 'synset': 'architrave.n.02', 'name': 'architrave'}, {'id': 5424, 'synset': 'arch_support.n.01', 'name': 'arch_support'}, {'id': 5425, 'synset': 'arc_lamp.n.01', 'name': 'arc_lamp'}, {'id': 5426, 'synset': 'area.n.05', 'name': 'area'}, {'id': 5427, 'synset': 'areaway.n.01', 'name': 'areaway'}, {'id': 5428, 'synset': 'argyle.n.03', 'name': 'argyle'}, {'id': 5429, 'synset': 'ark.n.02', 'name': 'ark'}, {'id': 5430, 'synset': 'arm.n.04', 'name': 'arm'}, {'id': 5431, 'synset': 'armament.n.01', 'name': 'armament'}, {'id': 5432, 'synset': 'armature.n.01', 'name': 'armature'}, {'id': 5433, 'synset': 'armet.n.01', 'name': 'armet'}, {'id': 5434, 'synset': 'arm_guard.n.01', 'name': 'arm_guard'}, {'id': 5435, 'synset': 'armhole.n.01', 'name': 'armhole'}, {'id': 5436, 'synset': 'armilla.n.02', 'name': 'armilla'}, {'id': 5437, 'synset': 'armlet.n.01', 'name': 'armlet'}, {'id': 5438, 'synset': 'armored_car.n.02', 'name': 'armored_car'}, {'id': 5439, 'synset': 'armored_car.n.01', 'name': 'armored_car'}, {'id': 5440, 'synset': 'armored_personnel_carrier.n.01', 'name': 'armored_personnel_carrier'}, {'id': 5441, 'synset': 'armored_vehicle.n.01', 'name': 'armored_vehicle'}, {'id': 5442, 'synset': 'armor_plate.n.01', 'name': 'armor_plate'}, {'id': 5443, 'synset': 'armory.n.04', 'name': 'armory'}, {'id': 5444, 'synset': 'armrest.n.01', 'name': 'armrest'}, {'id': 5445, 'synset': 'arquebus.n.01', 'name': 'arquebus'}, {'id': 5446, 'synset': 'array.n.04', 'name': 'array'}, {'id': 5447, 'synset': 'array.n.03', 'name': 'array'}, {'id': 5448, 'synset': 'arrester.n.01', 'name': 'arrester'}, {'id': 5449, 'synset': 'arrow.n.02', 'name': 'arrow'}, {'id': 5450, 'synset': 'arsenal.n.01', 'name': 'arsenal'}, {'id': 5451, 'synset': 'arterial_road.n.01', 'name': 'arterial_road'}, {'id': 5452, 'synset': 'arthrogram.n.01', 'name': 'arthrogram'}, {'id': 5453, 'synset': 'arthroscope.n.01', 'name': 'arthroscope'}, {'id': 5454, 'synset': 'artificial_heart.n.01', 'name': 'artificial_heart'}, {'id': 5455, 'synset': 'artificial_horizon.n.01', 'name': 'artificial_horizon'}, {'id': 5456, 'synset': 'artificial_joint.n.01', 'name': 'artificial_joint'}, {'id': 5457, 'synset': 'artificial_kidney.n.01', 'name': 'artificial_kidney'}, {'id': 5458, 'synset': 'artificial_skin.n.01', 'name': 'artificial_skin'}, {'id': 5459, 'synset': 'artillery.n.01', 'name': 'artillery'}, {'id': 5460, 'synset': 'artillery_shell.n.01', 'name': 'artillery_shell'}, {'id': 5461, 'synset': "artist's_loft.n.01", 'name': "artist's_loft"}, {'id': 5462, 'synset': 'art_school.n.01', 'name': 'art_school'}, {'id': 5463, 'synset': 'ascot.n.01', 'name': 'ascot'}, {'id': 5464, 'synset': 'ash-pan.n.01', 'name': 'ash-pan'}, {'id': 5465, 'synset': 'aspergill.n.01', 'name': 'aspergill'}, {'id': 5466, 'synset': 'aspersorium.n.01', 'name': 'aspersorium'}, {'id': 5467, 'synset': 'aspirator.n.01', 'name': 'aspirator'}, {'id': 5468, 'synset': 'aspirin_powder.n.01', 'name': 'aspirin_powder'}, {'id': 5469, 'synset': 'assault_gun.n.02', 'name': 'assault_gun'}, {'id': 5470, 'synset': 'assault_rifle.n.01', 'name': 'assault_rifle'}, {'id': 5471, 'synset': 'assegai.n.01', 'name': 'assegai'}, {'id': 5472, 'synset': 'assembly.n.01', 'name': 'assembly'}, {'id': 5473, 'synset': 'assembly.n.05', 'name': 'assembly'}, {'id': 5474, 'synset': 'assembly_hall.n.01', 'name': 'assembly_hall'}, {'id': 5475, 'synset': 'assembly_plant.n.01', 'name': 'assembly_plant'}, {'id': 5476, 'synset': 'astatic_coils.n.01', 'name': 'astatic_coils'}, {'id': 5477, 'synset': 'astatic_galvanometer.n.01', 'name': 'astatic_galvanometer'}, {'id': 5478, 'synset': 'astrodome.n.01', 'name': 'astrodome'}, {'id': 5479, 'synset': 'astrolabe.n.01', 'name': 'astrolabe'}, {'id': 5480, 'synset': 'astronomical_telescope.n.01', 'name': 'astronomical_telescope'}, {'id': 5481, 'synset': 'astronomy_satellite.n.01', 'name': 'astronomy_satellite'}, {'id': 5482, 'synset': 'athenaeum.n.02', 'name': 'athenaeum'}, {'id': 5483, 'synset': 'athletic_sock.n.01', 'name': 'athletic_sock'}, {'id': 5484, 'synset': 'athletic_supporter.n.01', 'name': 'athletic_supporter'}, {'id': 5485, 'synset': 'atlas.n.04', 'name': 'atlas'}, {'id': 5486, 'synset': 'atmometer.n.01', 'name': 'atmometer'}, {'id': 5487, 'synset': 'atom_bomb.n.01', 'name': 'atom_bomb'}, {'id': 5488, 'synset': 'atomic_clock.n.01', 'name': 'atomic_clock'}, {'id': 5489, 'synset': 'atomic_pile.n.01', 'name': 'atomic_pile'}, {'id': 5490, 'synset': 'atrium.n.02', 'name': 'atrium'}, {'id': 5491, 'synset': 'attache_case.n.01', 'name': 'attache_case'}, {'id': 5492, 'synset': 'attachment.n.04', 'name': 'attachment'}, {'id': 5493, 'synset': 'attack_submarine.n.01', 'name': 'attack_submarine'}, {'id': 5494, 'synset': 'attenuator.n.01', 'name': 'attenuator'}, {'id': 5495, 'synset': 'attic.n.04', 'name': 'attic'}, {'id': 5496, 'synset': 'attic_fan.n.01', 'name': 'attic_fan'}, {'id': 5497, 'synset': 'attire.n.01', 'name': 'attire'}, {'id': 5498, 'synset': 'audio_amplifier.n.01', 'name': 'audio_amplifier'}, {'id': 5499, 'synset': 'audiocassette.n.01', 'name': 'audiocassette'}, {'id': 5500, 'synset': 'audio_cd.n.01', 'name': 'audio_CD'}, {'id': 5501, 'synset': 'audiometer.n.01', 'name': 'audiometer'}, {'id': 5502, 'synset': 'audio_system.n.01', 'name': 'audio_system'}, {'id': 5503, 'synset': 'audiotape.n.02', 'name': 'audiotape'}, {'id': 5504, 'synset': 'audiotape.n.01', 'name': 'audiotape'}, {'id': 5505, 'synset': 'audiovisual.n.01', 'name': 'audiovisual'}, {'id': 5506, 'synset': 'auditorium.n.01', 'name': 'auditorium'}, {'id': 5507, 'synset': 'auger.n.02', 'name': 'auger'}, {'id': 5508, 'synset': 'autobahn.n.01', 'name': 'autobahn'}, {'id': 5509, 'synset': 'autoclave.n.01', 'name': 'autoclave'}, {'id': 5510, 'synset': 'autofocus.n.01', 'name': 'autofocus'}, {'id': 5511, 'synset': 'autogiro.n.01', 'name': 'autogiro'}, {'id': 5512, 'synset': 'autoinjector.n.01', 'name': 'autoinjector'}, {'id': 5513, 'synset': 'autoloader.n.01', 'name': 'autoloader'}, {'id': 5514, 'synset': 'automat.n.02', 'name': 'automat'}, {'id': 5515, 'synset': 'automat.n.01', 'name': 'automat'}, {'id': 5516, 'synset': 'automatic_choke.n.01', 'name': 'automatic_choke'}, {'id': 5517, 'synset': 'automatic_firearm.n.01', 'name': 'automatic_firearm'}, {'id': 5518, 'synset': 'automatic_pistol.n.01', 'name': 'automatic_pistol'}, {'id': 5519, 'synset': 'automatic_rifle.n.01', 'name': 'automatic_rifle'}, {'id': 5520, 'synset': 'automatic_transmission.n.01', 'name': 'automatic_transmission'}, {'id': 5521, 'synset': 'automation.n.03', 'name': 'automation'}, {'id': 5522, 'synset': 'automaton.n.02', 'name': 'automaton'}, {'id': 5523, 'synset': 'automobile_engine.n.01', 'name': 'automobile_engine'}, {'id': 5524, 'synset': 'automobile_factory.n.01', 'name': 'automobile_factory'}, {'id': 5525, 'synset': 'automobile_horn.n.01', 'name': 'automobile_horn'}, {'id': 5526, 'synset': 'autopilot.n.02', 'name': 'autopilot'}, {'id': 5527, 'synset': 'autoradiograph.n.01', 'name': 'autoradiograph'}, {'id': 5528, 'synset': 'autostrada.n.01', 'name': 'autostrada'}, {'id': 5529, 'synset': 'auxiliary_boiler.n.01', 'name': 'auxiliary_boiler'}, {'id': 5530, 'synset': 'auxiliary_engine.n.01', 'name': 'auxiliary_engine'}, {'id': 5531, 'synset': 'auxiliary_pump.n.01', 'name': 'auxiliary_pump'}, {'id': 5532, 'synset': 'auxiliary_research_submarine.n.01', 'name': 'auxiliary_research_submarine'}, {'id': 5533, 'synset': 'auxiliary_storage.n.01', 'name': 'auxiliary_storage'}, {'id': 5534, 'synset': 'aviary.n.01', 'name': 'aviary'}, {'id': 5535, 'synset': 'awl.n.01', 'name': 'awl'}, {'id': 5536, 'synset': 'ax_handle.n.01', 'name': 'ax_handle'}, {'id': 5537, 'synset': 'ax_head.n.01', 'name': 'ax_head'}, {'id': 5538, 'synset': 'axis.n.06', 'name': 'axis'}, {'id': 5539, 'synset': 'axle.n.01', 'name': 'axle'}, {'id': 5540, 'synset': 'axle_bar.n.01', 'name': 'axle_bar'}, {'id': 5541, 'synset': 'axletree.n.01', 'name': 'axletree'}, {'id': 5542, 'synset': 'babushka.n.01', 'name': 'babushka'}, {'id': 5543, 'synset': 'baby_bed.n.01', 'name': 'baby_bed'}, {'id': 5544, 'synset': 'baby_grand.n.01', 'name': 'baby_grand'}, {'id': 5545, 'synset': 'baby_powder.n.01', 'name': 'baby_powder'}, {'id': 5546, 'synset': 'baby_shoe.n.01', 'name': 'baby_shoe'}, {'id': 5547, 'synset': 'back.n.08', 'name': 'back'}, {'id': 5548, 'synset': 'back.n.07', 'name': 'back'}, {'id': 5549, 'synset': 'backbench.n.01', 'name': 'backbench'}, {'id': 5550, 'synset': 'backboard.n.02', 'name': 'backboard'}, {'id': 5551, 'synset': 'backbone.n.05', 'name': 'backbone'}, {'id': 5552, 'synset': 'back_brace.n.01', 'name': 'back_brace'}, {'id': 5553, 'synset': 'backgammon_board.n.01', 'name': 'backgammon_board'}, {'id': 5554, 'synset': 'background.n.07', 'name': 'background'}, {'id': 5555, 'synset': 'backhoe.n.01', 'name': 'backhoe'}, {'id': 5556, 'synset': 'backlighting.n.01', 'name': 'backlighting'}, {'id': 5557, 'synset': 'backpacking_tent.n.01', 'name': 'backpacking_tent'}, {'id': 5558, 'synset': 'backplate.n.01', 'name': 'backplate'}, {'id': 5559, 'synset': 'back_porch.n.01', 'name': 'back_porch'}, {'id': 5560, 'synset': 'backsaw.n.01', 'name': 'backsaw'}, {'id': 5561, 'synset': 'backscratcher.n.02', 'name': 'backscratcher'}, {'id': 5562, 'synset': 'backseat.n.02', 'name': 'backseat'}, {'id': 5563, 'synset': 'backspace_key.n.01', 'name': 'backspace_key'}, {'id': 5564, 'synset': 'backstairs.n.01', 'name': 'backstairs'}, {'id': 5565, 'synset': 'backstay.n.01', 'name': 'backstay'}, {'id': 5566, 'synset': 'backstop.n.02', 'name': 'backstop'}, {'id': 5567, 'synset': 'backsword.n.02', 'name': 'backsword'}, {'id': 5568, 'synset': 'backup_system.n.01', 'name': 'backup_system'}, {'id': 5569, 'synset': 'badminton_court.n.01', 'name': 'badminton_court'}, {'id': 5570, 'synset': 'badminton_equipment.n.01', 'name': 'badminton_equipment'}, {'id': 5571, 'synset': 'badminton_racket.n.01', 'name': 'badminton_racket'}, {'id': 5572, 'synset': 'bag.n.01', 'name': 'bag'}, {'id': 5573, 'synset': 'baggage.n.01', 'name': 'baggage'}, {'id': 5574, 'synset': 'baggage.n.03', 'name': 'baggage'}, {'id': 5575, 'synset': 'baggage_car.n.01', 'name': 'baggage_car'}, {'id': 5576, 'synset': 'baggage_claim.n.01', 'name': 'baggage_claim'}, {'id': 5577, 'synset': 'bailey.n.04', 'name': 'bailey'}, {'id': 5578, 'synset': 'bailey.n.03', 'name': 'bailey'}, {'id': 5579, 'synset': 'bailey_bridge.n.01', 'name': 'Bailey_bridge'}, {'id': 5580, 'synset': 'bain-marie.n.01', 'name': 'bain-marie'}, {'id': 5581, 'synset': 'baize.n.01', 'name': 'baize'}, {'id': 5582, 'synset': 'bakery.n.01', 'name': 'bakery'}, {'id': 5583, 'synset': 'balaclava.n.01', 'name': 'balaclava'}, {'id': 5584, 'synset': 'balalaika.n.01', 'name': 'balalaika'}, {'id': 5585, 'synset': 'balance.n.12', 'name': 'balance'}, {'id': 5586, 'synset': 'balance_beam.n.01', 'name': 'balance_beam'}, {'id': 5587, 'synset': 'balance_wheel.n.01', 'name': 'balance_wheel'}, {'id': 5588, 'synset': 'balbriggan.n.01', 'name': 'balbriggan'}, {'id': 5589, 'synset': 'balcony.n.02', 'name': 'balcony'}, {'id': 5590, 'synset': 'balcony.n.01', 'name': 'balcony'}, {'id': 5591, 'synset': 'baldachin.n.01', 'name': 'baldachin'}, {'id': 5592, 'synset': 'baldric.n.01', 'name': 'baldric'}, {'id': 5593, 'synset': 'bale.n.01', 'name': 'bale'}, {'id': 5594, 'synset': 'baling_wire.n.01', 'name': 'baling_wire'}, {'id': 5595, 'synset': 'ball.n.01', 'name': 'ball'}, {'id': 5596, 'synset': 'ball_and_chain.n.01', 'name': 'ball_and_chain'}, {'id': 5597, 'synset': 'ball-and-socket_joint.n.02', 'name': 'ball-and-socket_joint'}, {'id': 5598, 'synset': 'ballast.n.05', 'name': 'ballast'}, {'id': 5599, 'synset': 'ball_bearing.n.01', 'name': 'ball_bearing'}, {'id': 5600, 'synset': 'ball_cartridge.n.01', 'name': 'ball_cartridge'}, {'id': 5601, 'synset': 'ballcock.n.01', 'name': 'ballcock'}, {'id': 5602, 'synset': 'balldress.n.01', 'name': 'balldress'}, {'id': 5603, 'synset': 'ball_gown.n.01', 'name': 'ball_gown'}, {'id': 5604, 'synset': 'ballistic_galvanometer.n.01', 'name': 'ballistic_galvanometer'}, {'id': 5605, 'synset': 'ballistic_missile.n.01', 'name': 'ballistic_missile'}, {'id': 5606, 'synset': 'ballistic_pendulum.n.01', 'name': 'ballistic_pendulum'}, {'id': 5607, 'synset': 'ballistocardiograph.n.01', 'name': 'ballistocardiograph'}, {'id': 5608, 'synset': 'balloon_bomb.n.01', 'name': 'balloon_bomb'}, {'id': 5609, 'synset': 'balloon_sail.n.01', 'name': 'balloon_sail'}, {'id': 5610, 'synset': 'ballot_box.n.01', 'name': 'ballot_box'}, {'id': 5611, 'synset': 'ballpark.n.01', 'name': 'ballpark'}, {'id': 5612, 'synset': 'ball-peen_hammer.n.01', 'name': 'ball-peen_hammer'}, {'id': 5613, 'synset': 'ballpoint.n.01', 'name': 'ballpoint'}, {'id': 5614, 'synset': 'ballroom.n.01', 'name': 'ballroom'}, {'id': 5615, 'synset': 'ball_valve.n.01', 'name': 'ball_valve'}, {'id': 5616, 'synset': 'balsa_raft.n.01', 'name': 'balsa_raft'}, {'id': 5617, 'synset': 'baluster.n.01', 'name': 'baluster'}, {'id': 5618, 'synset': 'banana_boat.n.01', 'name': 'banana_boat'}, {'id': 5619, 'synset': 'band.n.13', 'name': 'band'}, {'id': 5620, 'synset': 'bandbox.n.01', 'name': 'bandbox'}, {'id': 5621, 'synset': 'banderilla.n.01', 'name': 'banderilla'}, {'id': 5622, 'synset': 'bandoleer.n.01', 'name': 'bandoleer'}, {'id': 5623, 'synset': 'bandoneon.n.01', 'name': 'bandoneon'}, {'id': 5624, 'synset': 'bandsaw.n.01', 'name': 'bandsaw'}, {'id': 5625, 'synset': 'bandwagon.n.02', 'name': 'bandwagon'}, {'id': 5626, 'synset': 'bangalore_torpedo.n.01', 'name': 'bangalore_torpedo'}, {'id': 5627, 'synset': 'bangle.n.02', 'name': 'bangle'}, {'id': 5628, 'synset': 'bannister.n.02', 'name': 'bannister'}, {'id': 5629, 'synset': 'banquette.n.01', 'name': 'banquette'}, {'id': 5630, 'synset': 'banyan.n.02', 'name': 'banyan'}, {'id': 5631, 'synset': 'baptismal_font.n.01', 'name': 'baptismal_font'}, {'id': 5632, 'synset': 'bar.n.03', 'name': 'bar'}, {'id': 5633, 'synset': 'bar.n.02', 'name': 'bar'}, {'id': 5634, 'synset': 'barbecue.n.03', 'name': 'barbecue'}, {'id': 5635, 'synset': 'barbed_wire.n.01', 'name': 'barbed_wire'}, {'id': 5636, 'synset': 'barber_chair.n.01', 'name': 'barber_chair'}, {'id': 5637, 'synset': 'barbershop.n.01', 'name': 'barbershop'}, {'id': 5638, 'synset': 'barbette_carriage.n.01', 'name': 'barbette_carriage'}, {'id': 5639, 'synset': 'barbican.n.01', 'name': 'barbican'}, {'id': 5640, 'synset': 'bar_bit.n.01', 'name': 'bar_bit'}, {'id': 5641, 'synset': 'bareboat.n.01', 'name': 'bareboat'}, {'id': 5642, 'synset': 'barge_pole.n.01', 'name': 'barge_pole'}, {'id': 5643, 'synset': 'baritone.n.03', 'name': 'baritone'}, {'id': 5644, 'synset': 'bark.n.03', 'name': 'bark'}, {'id': 5645, 'synset': 'bar_magnet.n.01', 'name': 'bar_magnet'}, {'id': 5646, 'synset': 'bar_mask.n.01', 'name': 'bar_mask'}, {'id': 5647, 'synset': 'barn.n.01', 'name': 'barn'}, {'id': 5648, 'synset': 'barndoor.n.01', 'name': 'barndoor'}, {'id': 5649, 'synset': 'barn_door.n.01', 'name': 'barn_door'}, {'id': 5650, 'synset': 'barnyard.n.01', 'name': 'barnyard'}, {'id': 5651, 'synset': 'barograph.n.01', 'name': 'barograph'}, {'id': 5652, 'synset': 'barometer.n.01', 'name': 'barometer'}, {'id': 5653, 'synset': 'barong.n.01', 'name': 'barong'}, {'id': 5654, 'synset': 'barouche.n.01', 'name': 'barouche'}, {'id': 5655, 'synset': 'bar_printer.n.01', 'name': 'bar_printer'}, {'id': 5656, 'synset': 'barrack.n.01', 'name': 'barrack'}, {'id': 5657, 'synset': 'barrage_balloon.n.01', 'name': 'barrage_balloon'}, {'id': 5658, 'synset': 'barrel.n.01', 'name': 'barrel'}, {'id': 5659, 'synset': 'barrelhouse.n.01', 'name': 'barrelhouse'}, {'id': 5660, 'synset': 'barrel_knot.n.01', 'name': 'barrel_knot'}, {'id': 5661, 'synset': 'barrel_organ.n.01', 'name': 'barrel_organ'}, {'id': 5662, 'synset': 'barrel_vault.n.01', 'name': 'barrel_vault'}, {'id': 5663, 'synset': 'barricade.n.02', 'name': 'barricade'}, {'id': 5664, 'synset': 'barrier.n.01', 'name': 'barrier'}, {'id': 5665, 'synset': 'barroom.n.01', 'name': 'barroom'}, {'id': 5666, 'synset': 'bascule.n.01', 'name': 'bascule'}, {'id': 5667, 'synset': 'base.n.08', 'name': 'base'}, {'id': 5668, 'synset': 'baseball_equipment.n.01', 'name': 'baseball_equipment'}, {'id': 5669, 'synset': 'basement.n.01', 'name': 'basement'}, {'id': 5670, 'synset': 'basement.n.02', 'name': 'basement'}, {'id': 5671, 'synset': 'basic_point_defense_missile_system.n.01', 'name': 'basic_point_defense_missile_system'}, {'id': 5672, 'synset': 'basilica.n.02', 'name': 'basilica'}, {'id': 5673, 'synset': 'basilica.n.01', 'name': 'basilica'}, {'id': 5674, 'synset': 'basilisk.n.02', 'name': 'basilisk'}, {'id': 5675, 'synset': 'basin.n.01', 'name': 'basin'}, {'id': 5676, 'synset': 'basinet.n.01', 'name': 'basinet'}, {'id': 5677, 'synset': 'basket.n.03', 'name': 'basket'}, {'id': 5678, 'synset': 'basketball_court.n.01', 'name': 'basketball_court'}, {'id': 5679, 'synset': 'basketball_equipment.n.01', 'name': 'basketball_equipment'}, {'id': 5680, 'synset': 'basket_weave.n.01', 'name': 'basket_weave'}, {'id': 5681, 'synset': 'bass.n.07', 'name': 'bass'}, {'id': 5682, 'synset': 'bass_clarinet.n.01', 'name': 'bass_clarinet'}, {'id': 5683, 'synset': 'bass_drum.n.01', 'name': 'bass_drum'}, {'id': 5684, 'synset': 'basset_horn.n.01', 'name': 'basset_horn'}, {'id': 5685, 'synset': 'bass_fiddle.n.01', 'name': 'bass_fiddle'}, {'id': 5686, 'synset': 'bass_guitar.n.01', 'name': 'bass_guitar'}, {'id': 5687, 'synset': 'bassinet.n.01', 'name': 'bassinet'}, {'id': 5688, 'synset': 'bassinet.n.02', 'name': 'bassinet'}, {'id': 5689, 'synset': 'bassoon.n.01', 'name': 'bassoon'}, {'id': 5690, 'synset': 'baster.n.03', 'name': 'baster'}, {'id': 5691, 'synset': 'bastinado.n.01', 'name': 'bastinado'}, {'id': 5692, 'synset': 'bastion.n.03', 'name': 'bastion'}, {'id': 5693, 'synset': 'bastion.n.02', 'name': 'bastion'}, {'id': 5694, 'synset': 'bat.n.05', 'name': 'bat'}, {'id': 5695, 'synset': 'bath.n.01', 'name': 'bath'}, {'id': 5696, 'synset': 'bath_chair.n.01', 'name': 'bath_chair'}, {'id': 5697, 'synset': 'bathhouse.n.02', 'name': 'bathhouse'}, {'id': 5698, 'synset': 'bathhouse.n.01', 'name': 'bathhouse'}, {'id': 5699, 'synset': 'bathing_cap.n.01', 'name': 'bathing_cap'}, {'id': 5700, 'synset': 'bath_oil.n.01', 'name': 'bath_oil'}, {'id': 5701, 'synset': 'bathroom.n.01', 'name': 'bathroom'}, {'id': 5702, 'synset': 'bath_salts.n.01', 'name': 'bath_salts'}, {'id': 5703, 'synset': 'bathyscaphe.n.01', 'name': 'bathyscaphe'}, {'id': 5704, 'synset': 'bathysphere.n.01', 'name': 'bathysphere'}, {'id': 5705, 'synset': 'batik.n.01', 'name': 'batik'}, {'id': 5706, 'synset': 'batiste.n.01', 'name': 'batiste'}, {'id': 5707, 'synset': 'baton.n.01', 'name': 'baton'}, {'id': 5708, 'synset': 'baton.n.05', 'name': 'baton'}, {'id': 5709, 'synset': 'baton.n.04', 'name': 'baton'}, {'id': 5710, 'synset': 'baton.n.03', 'name': 'baton'}, {'id': 5711, 'synset': 'battering_ram.n.01', 'name': 'battering_ram'}, {'id': 5712, 'synset': "batter's_box.n.01", 'name': "batter's_box"}, {'id': 5713, 'synset': 'battery.n.05', 'name': 'battery'}, {'id': 5714, 'synset': 'batting_cage.n.01', 'name': 'batting_cage'}, {'id': 5715, 'synset': 'batting_glove.n.01', 'name': 'batting_glove'}, {'id': 5716, 'synset': 'batting_helmet.n.01', 'name': 'batting_helmet'}, {'id': 5717, 'synset': 'battle-ax.n.01', 'name': 'battle-ax'}, {'id': 5718, 'synset': 'battle_cruiser.n.01', 'name': 'battle_cruiser'}, {'id': 5719, 'synset': 'battle_dress.n.01', 'name': 'battle_dress'}, {'id': 5720, 'synset': 'battlement.n.01', 'name': 'battlement'}, {'id': 5721, 'synset': 'battleship.n.01', 'name': 'battleship'}, {'id': 5722, 'synset': 'battle_sight.n.01', 'name': 'battle_sight'}, {'id': 5723, 'synset': 'bay.n.05', 'name': 'bay'}, {'id': 5724, 'synset': 'bay.n.04', 'name': 'bay'}, {'id': 5725, 'synset': 'bayonet.n.01', 'name': 'bayonet'}, {'id': 5726, 'synset': 'bay_rum.n.01', 'name': 'bay_rum'}, {'id': 5727, 'synset': 'bay_window.n.02', 'name': 'bay_window'}, {'id': 5728, 'synset': 'bazaar.n.01', 'name': 'bazaar'}, {'id': 5729, 'synset': 'bazaar.n.02', 'name': 'bazaar'}, {'id': 5730, 'synset': 'bazooka.n.01', 'name': 'bazooka'}, {'id': 5731, 'synset': 'b_battery.n.01', 'name': 'B_battery'}, {'id': 5732, 'synset': 'bb_gun.n.01', 'name': 'BB_gun'}, {'id': 5733, 'synset': 'beach_house.n.01', 'name': 'beach_house'}, {'id': 5734, 'synset': 'beach_towel.n.01', 'name': 'beach_towel'}, {'id': 5735, 'synset': 'beach_wagon.n.01', 'name': 'beach_wagon'}, {'id': 5736, 'synset': 'beachwear.n.01', 'name': 'beachwear'}, {'id': 5737, 'synset': 'beacon.n.03', 'name': 'beacon'}, {'id': 5738, 'synset': 'beading_plane.n.01', 'name': 'beading_plane'}, {'id': 5739, 'synset': 'beaker.n.02', 'name': 'beaker'}, {'id': 5740, 'synset': 'beaker.n.01', 'name': 'beaker'}, {'id': 5741, 'synset': 'beam.n.02', 'name': 'beam'}, {'id': 5742, 'synset': 'beam_balance.n.01', 'name': 'beam_balance'}, {'id': 5743, 'synset': 'bearing.n.06', 'name': 'bearing'}, {'id': 5744, 'synset': 'bearing_rein.n.01', 'name': 'bearing_rein'}, {'id': 5745, 'synset': 'bearing_wall.n.01', 'name': 'bearing_wall'}, {'id': 5746, 'synset': 'bearskin.n.02', 'name': 'bearskin'}, {'id': 5747, 'synset': 'beater.n.02', 'name': 'beater'}, {'id': 5748, 'synset': 'beating-reed_instrument.n.01', 'name': 'beating-reed_instrument'}, {'id': 5749, 'synset': 'beaver.n.06', 'name': 'beaver'}, {'id': 5750, 'synset': 'beaver.n.05', 'name': 'beaver'}, {'id': 5751, 'synset': 'beckman_thermometer.n.01', 'name': 'Beckman_thermometer'}, {'id': 5752, 'synset': 'bed.n.08', 'name': 'bed'}, {'id': 5753, 'synset': 'bed_and_breakfast.n.01', 'name': 'bed_and_breakfast'}, {'id': 5754, 'synset': 'bedclothes.n.01', 'name': 'bedclothes'}, {'id': 5755, 'synset': 'bedford_cord.n.01', 'name': 'Bedford_cord'}, {'id': 5756, 'synset': 'bed_jacket.n.01', 'name': 'bed_jacket'}, {'id': 5757, 'synset': 'bedpost.n.01', 'name': 'bedpost'}, {'id': 5758, 'synset': 'bedroll.n.01', 'name': 'bedroll'}, {'id': 5759, 'synset': 'bedroom.n.01', 'name': 'bedroom'}, {'id': 5760, 'synset': 'bedroom_furniture.n.01', 'name': 'bedroom_furniture'}, {'id': 5761, 'synset': 'bedsitting_room.n.01', 'name': 'bedsitting_room'}, {'id': 5762, 'synset': 'bedspring.n.01', 'name': 'bedspring'}, {'id': 5763, 'synset': 'bedstead.n.01', 'name': 'bedstead'}, {'id': 5764, 'synset': 'beefcake.n.01', 'name': 'beefcake'}, {'id': 5765, 'synset': 'beehive.n.04', 'name': 'beehive'}, {'id': 5766, 'synset': 'beer_barrel.n.01', 'name': 'beer_barrel'}, {'id': 5767, 'synset': 'beer_garden.n.01', 'name': 'beer_garden'}, {'id': 5768, 'synset': 'beer_glass.n.01', 'name': 'beer_glass'}, {'id': 5769, 'synset': 'beer_hall.n.01', 'name': 'beer_hall'}, {'id': 5770, 'synset': 'beer_mat.n.01', 'name': 'beer_mat'}, {'id': 5771, 'synset': 'beer_mug.n.01', 'name': 'beer_mug'}, {'id': 5772, 'synset': 'belaying_pin.n.01', 'name': 'belaying_pin'}, {'id': 5773, 'synset': 'belfry.n.02', 'name': 'belfry'}, {'id': 5774, 'synset': 'bell_arch.n.01', 'name': 'bell_arch'}, {'id': 5775, 'synset': 'bellarmine.n.02', 'name': 'bellarmine'}, {'id': 5776, 'synset': 'bellbottom_trousers.n.01', 'name': 'bellbottom_trousers'}, {'id': 5777, 'synset': 'bell_cote.n.01', 'name': 'bell_cote'}, {'id': 5778, 'synset': 'bell_foundry.n.01', 'name': 'bell_foundry'}, {'id': 5779, 'synset': 'bell_gable.n.01', 'name': 'bell_gable'}, {'id': 5780, 'synset': 'bell_jar.n.01', 'name': 'bell_jar'}, {'id': 5781, 'synset': 'bellows.n.01', 'name': 'bellows'}, {'id': 5782, 'synset': 'bellpull.n.01', 'name': 'bellpull'}, {'id': 5783, 'synset': 'bell_push.n.01', 'name': 'bell_push'}, {'id': 5784, 'synset': 'bell_seat.n.01', 'name': 'bell_seat'}, {'id': 5785, 'synset': 'bell_tent.n.01', 'name': 'bell_tent'}, {'id': 5786, 'synset': 'bell_tower.n.01', 'name': 'bell_tower'}, {'id': 5787, 'synset': 'bellyband.n.01', 'name': 'bellyband'}, {'id': 5788, 'synset': 'belt.n.06', 'name': 'belt'}, {'id': 5789, 'synset': 'belting.n.01', 'name': 'belting'}, {'id': 5790, 'synset': 'bench_clamp.n.01', 'name': 'bench_clamp'}, {'id': 5791, 'synset': 'bench_hook.n.01', 'name': 'bench_hook'}, {'id': 5792, 'synset': 'bench_lathe.n.01', 'name': 'bench_lathe'}, {'id': 5793, 'synset': 'bench_press.n.02', 'name': 'bench_press'}, {'id': 5794, 'synset': 'bender.n.01', 'name': 'bender'}, {'id': 5795, 'synset': 'berlin.n.03', 'name': 'berlin'}, {'id': 5796, 'synset': 'bermuda_shorts.n.01', 'name': 'Bermuda_shorts'}, {'id': 5797, 'synset': 'berth.n.03', 'name': 'berth'}, {'id': 5798, 'synset': 'besom.n.01', 'name': 'besom'}, {'id': 5799, 'synset': 'bessemer_converter.n.01', 'name': 'Bessemer_converter'}, {'id': 5800, 'synset': 'bethel.n.01', 'name': 'bethel'}, {'id': 5801, 'synset': 'betting_shop.n.01', 'name': 'betting_shop'}, {'id': 5802, 'synset': 'bevatron.n.01', 'name': 'bevatron'}, {'id': 5803, 'synset': 'bevel.n.02', 'name': 'bevel'}, {'id': 5804, 'synset': 'bevel_gear.n.01', 'name': 'bevel_gear'}, {'id': 5805, 'synset': 'b-flat_clarinet.n.01', 'name': 'B-flat_clarinet'}, {'id': 5806, 'synset': 'bib.n.01', 'name': 'bib'}, {'id': 5807, 'synset': 'bib-and-tucker.n.01', 'name': 'bib-and-tucker'}, {'id': 5808, 'synset': 'bicorn.n.01', 'name': 'bicorn'}, {'id': 5809, 'synset': 'bicycle-built-for-two.n.01', 'name': 'bicycle-built-for-two'}, {'id': 5810, 'synset': 'bicycle_chain.n.01', 'name': 'bicycle_chain'}, {'id': 5811, 'synset': 'bicycle_clip.n.01', 'name': 'bicycle_clip'}, {'id': 5812, 'synset': 'bicycle_pump.n.01', 'name': 'bicycle_pump'}, {'id': 5813, 'synset': 'bicycle_rack.n.01', 'name': 'bicycle_rack'}, {'id': 5814, 'synset': 'bicycle_seat.n.01', 'name': 'bicycle_seat'}, {'id': 5815, 'synset': 'bicycle_wheel.n.01', 'name': 'bicycle_wheel'}, {'id': 5816, 'synset': 'bidet.n.01', 'name': 'bidet'}, {'id': 5817, 'synset': 'bier.n.02', 'name': 'bier'}, {'id': 5818, 'synset': 'bier.n.01', 'name': 'bier'}, {'id': 5819, 'synset': 'bi-fold_door.n.01', 'name': 'bi-fold_door'}, {'id': 5820, 'synset': 'bifocals.n.01', 'name': 'bifocals'}, {'id': 5821, 'synset': 'big_blue.n.01', 'name': 'Big_Blue'}, {'id': 5822, 'synset': 'big_board.n.02', 'name': 'big_board'}, {'id': 5823, 'synset': 'bight.n.04', 'name': 'bight'}, {'id': 5824, 'synset': 'bikini.n.02', 'name': 'bikini'}, {'id': 5825, 'synset': 'bikini_pants.n.01', 'name': 'bikini_pants'}, {'id': 5826, 'synset': 'bilge.n.02', 'name': 'bilge'}, {'id': 5827, 'synset': 'bilge_keel.n.01', 'name': 'bilge_keel'}, {'id': 5828, 'synset': 'bilge_pump.n.01', 'name': 'bilge_pump'}, {'id': 5829, 'synset': 'bilge_well.n.01', 'name': 'bilge_well'}, {'id': 5830, 'synset': 'bill.n.08', 'name': 'bill'}, {'id': 5831, 'synset': 'billiard_ball.n.01', 'name': 'billiard_ball'}, {'id': 5832, 'synset': 'billiard_room.n.01', 'name': 'billiard_room'}, {'id': 5833, 'synset': 'bin.n.01', 'name': 'bin'}, {'id': 5834, 'synset': 'binder.n.04', 'name': 'binder'}, {'id': 5835, 'synset': 'bindery.n.01', 'name': 'bindery'}, {'id': 5836, 'synset': 'binding.n.05', 'name': 'binding'}, {'id': 5837, 'synset': 'bin_liner.n.01', 'name': 'bin_liner'}, {'id': 5838, 'synset': 'binnacle.n.01', 'name': 'binnacle'}, {'id': 5839, 'synset': 'binocular_microscope.n.01', 'name': 'binocular_microscope'}, {'id': 5840, 'synset': 'biochip.n.01', 'name': 'biochip'}, {'id': 5841, 'synset': 'biohazard_suit.n.01', 'name': 'biohazard_suit'}, {'id': 5842, 'synset': 'bioscope.n.02', 'name': 'bioscope'}, {'id': 5843, 'synset': 'biplane.n.01', 'name': 'biplane'}, {'id': 5844, 'synset': 'birch.n.03', 'name': 'birch'}, {'id': 5845, 'synset': 'birchbark_canoe.n.01', 'name': 'birchbark_canoe'}, {'id': 5846, 'synset': 'birdcall.n.02', 'name': 'birdcall'}, {'id': 5847, 'synset': 'bird_shot.n.01', 'name': 'bird_shot'}, {'id': 5848, 'synset': 'biretta.n.01', 'name': 'biretta'}, {'id': 5849, 'synset': 'bishop.n.03', 'name': 'bishop'}, {'id': 5850, 'synset': 'bistro.n.01', 'name': 'bistro'}, {'id': 5851, 'synset': 'bit.n.11', 'name': 'bit'}, {'id': 5852, 'synset': 'bit.n.05', 'name': 'bit'}, {'id': 5853, 'synset': 'bite_plate.n.01', 'name': 'bite_plate'}, {'id': 5854, 'synset': 'bitewing.n.01', 'name': 'bitewing'}, {'id': 5855, 'synset': 'bitumastic.n.01', 'name': 'bitumastic'}, {'id': 5856, 'synset': 'black.n.07', 'name': 'black'}, {'id': 5857, 'synset': 'black.n.06', 'name': 'black'}, {'id': 5858, 'synset': 'blackboard_eraser.n.01', 'name': 'blackboard_eraser'}, {'id': 5859, 'synset': 'black_box.n.01', 'name': 'black_box'}, {'id': 5860, 'synset': 'blackface.n.01', 'name': 'blackface'}, {'id': 5861, 'synset': 'blackjack.n.02', 'name': 'blackjack'}, {'id': 5862, 'synset': 'black_tie.n.02', 'name': 'black_tie'}, {'id': 5863, 'synset': 'blackwash.n.03', 'name': 'blackwash'}, {'id': 5864, 'synset': 'bladder.n.02', 'name': 'bladder'}, {'id': 5865, 'synset': 'blade.n.09', 'name': 'blade'}, {'id': 5866, 'synset': 'blade.n.08', 'name': 'blade'}, {'id': 5867, 'synset': 'blade.n.07', 'name': 'blade'}, {'id': 5868, 'synset': 'blank.n.04', 'name': 'blank'}, {'id': 5869, 'synset': 'blast_furnace.n.01', 'name': 'blast_furnace'}, {'id': 5870, 'synset': 'blasting_cap.n.01', 'name': 'blasting_cap'}, {'id': 5871, 'synset': 'blind.n.03', 'name': 'blind'}, {'id': 5872, 'synset': 'blind_curve.n.01', 'name': 'blind_curve'}, {'id': 5873, 'synset': 'blindfold.n.01', 'name': 'blindfold'}, {'id': 5874, 'synset': 'bling.n.01', 'name': 'bling'}, {'id': 5875, 'synset': 'blister_pack.n.01', 'name': 'blister_pack'}, {'id': 5876, 'synset': 'block.n.05', 'name': 'block'}, {'id': 5877, 'synset': 'blockade.n.02', 'name': 'blockade'}, {'id': 5878, 'synset': 'blockade-runner.n.01', 'name': 'blockade-runner'}, {'id': 5879, 'synset': 'block_and_tackle.n.01', 'name': 'block_and_tackle'}, {'id': 5880, 'synset': 'blockbuster.n.01', 'name': 'blockbuster'}, {'id': 5881, 'synset': 'blockhouse.n.01', 'name': 'blockhouse'}, {'id': 5882, 'synset': 'block_plane.n.01', 'name': 'block_plane'}, {'id': 5883, 'synset': 'bloodmobile.n.01', 'name': 'bloodmobile'}, {'id': 5884, 'synset': 'bloomers.n.01', 'name': 'bloomers'}, {'id': 5885, 'synset': 'blower.n.01', 'name': 'blower'}, {'id': 5886, 'synset': 'blowtorch.n.01', 'name': 'blowtorch'}, {'id': 5887, 'synset': 'blucher.n.02', 'name': 'blucher'}, {'id': 5888, 'synset': 'bludgeon.n.01', 'name': 'bludgeon'}, {'id': 5889, 'synset': 'blue.n.02', 'name': 'blue'}, {'id': 5890, 'synset': 'blue_chip.n.02', 'name': 'blue_chip'}, {'id': 5891, 'synset': 'blunderbuss.n.01', 'name': 'blunderbuss'}, {'id': 5892, 'synset': 'blunt_file.n.01', 'name': 'blunt_file'}, {'id': 5893, 'synset': 'boarding.n.02', 'name': 'boarding'}, {'id': 5894, 'synset': 'boarding_house.n.01', 'name': 'boarding_house'}, {'id': 5895, 'synset': 'boardroom.n.01', 'name': 'boardroom'}, {'id': 5896, 'synset': 'boards.n.02', 'name': 'boards'}, {'id': 5897, 'synset': 'boater.n.01', 'name': 'boater'}, {'id': 5898, 'synset': 'boat_hook.n.01', 'name': 'boat_hook'}, {'id': 5899, 'synset': 'boathouse.n.01', 'name': 'boathouse'}, {'id': 5900, 'synset': "boatswain's_chair.n.01", 'name': "boatswain's_chair"}, {'id': 5901, 'synset': 'boat_train.n.01', 'name': 'boat_train'}, {'id': 5902, 'synset': 'boatyard.n.01', 'name': 'boatyard'}, {'id': 5903, 'synset': 'bobsled.n.02', 'name': 'bobsled'}, {'id': 5904, 'synset': 'bobsled.n.01', 'name': 'bobsled'}, {'id': 5905, 'synset': 'bocce_ball.n.01', 'name': 'bocce_ball'}, {'id': 5906, 'synset': 'bodega.n.01', 'name': 'bodega'}, {'id': 5907, 'synset': 'bodice.n.01', 'name': 'bodice'}, {'id': 5908, 'synset': 'bodkin.n.04', 'name': 'bodkin'}, {'id': 5909, 'synset': 'bodkin.n.03', 'name': 'bodkin'}, {'id': 5910, 'synset': 'bodkin.n.02', 'name': 'bodkin'}, {'id': 5911, 'synset': 'body.n.11', 'name': 'body'}, {'id': 5912, 'synset': 'body_armor.n.01', 'name': 'body_armor'}, {'id': 5913, 'synset': 'body_lotion.n.01', 'name': 'body_lotion'}, {'id': 5914, 'synset': 'body_stocking.n.01', 'name': 'body_stocking'}, {'id': 5915, 'synset': 'body_plethysmograph.n.01', 'name': 'body_plethysmograph'}, {'id': 5916, 'synset': 'body_pad.n.01', 'name': 'body_pad'}, {'id': 5917, 'synset': 'bodywork.n.01', 'name': 'bodywork'}, {'id': 5918, 'synset': 'bofors_gun.n.01', 'name': 'Bofors_gun'}, {'id': 5919, 'synset': 'bogy.n.01', 'name': 'bogy'}, {'id': 5920, 'synset': 'boiler.n.01', 'name': 'boiler'}, {'id': 5921, 'synset': 'boiling_water_reactor.n.01', 'name': 'boiling_water_reactor'}, {'id': 5922, 'synset': 'bolero.n.02', 'name': 'bolero'}, {'id': 5923, 'synset': 'bollard.n.01', 'name': 'bollard'}, {'id': 5924, 'synset': 'bolo.n.02', 'name': 'bolo'}, {'id': 5925, 'synset': 'bolt.n.02', 'name': 'bolt'}, {'id': 5926, 'synset': 'bolt_cutter.n.01', 'name': 'bolt_cutter'}, {'id': 5927, 'synset': 'bomb.n.01', 'name': 'bomb'}, {'id': 5928, 'synset': 'bombazine.n.01', 'name': 'bombazine'}, {'id': 5929, 'synset': 'bomb_calorimeter.n.01', 'name': 'bomb_calorimeter'}, {'id': 5930, 'synset': 'bomber.n.01', 'name': 'bomber'}, {'id': 5931, 'synset': 'bomber_jacket.n.01', 'name': 'bomber_jacket'}, {'id': 5932, 'synset': 'bomblet.n.01', 'name': 'bomblet'}, {'id': 5933, 'synset': 'bomb_rack.n.01', 'name': 'bomb_rack'}, {'id': 5934, 'synset': 'bombshell.n.03', 'name': 'bombshell'}, {'id': 5935, 'synset': 'bomb_shelter.n.01', 'name': 'bomb_shelter'}, {'id': 5936, 'synset': 'bone-ash_cup.n.01', 'name': 'bone-ash_cup'}, {'id': 5937, 'synset': 'bone_china.n.01', 'name': 'bone_china'}, {'id': 5938, 'synset': 'bones.n.01', 'name': 'bones'}, {'id': 5939, 'synset': 'boneshaker.n.01', 'name': 'boneshaker'}, {'id': 5940, 'synset': 'bongo.n.01', 'name': 'bongo'}, {'id': 5941, 'synset': 'book.n.11', 'name': 'book'}, {'id': 5942, 'synset': 'book_bag.n.01', 'name': 'book_bag'}, {'id': 5943, 'synset': 'bookbindery.n.01', 'name': 'bookbindery'}, {'id': 5944, 'synset': 'bookend.n.01', 'name': 'bookend'}, {'id': 5945, 'synset': 'bookmobile.n.01', 'name': 'bookmobile'}, {'id': 5946, 'synset': 'bookshelf.n.01', 'name': 'bookshelf'}, {'id': 5947, 'synset': 'bookshop.n.01', 'name': 'bookshop'}, {'id': 5948, 'synset': 'boom.n.05', 'name': 'boom'}, {'id': 5949, 'synset': 'boomerang.n.01', 'name': 'boomerang'}, {'id': 5950, 'synset': 'booster.n.05', 'name': 'booster'}, {'id': 5951, 'synset': 'booster.n.04', 'name': 'booster'}, {'id': 5952, 'synset': 'boot.n.04', 'name': 'boot'}, {'id': 5953, 'synset': 'boot_camp.n.01', 'name': 'boot_camp'}, {'id': 5954, 'synset': 'bootee.n.01', 'name': 'bootee'}, {'id': 5955, 'synset': 'booth.n.02', 'name': 'booth'}, {'id': 5956, 'synset': 'booth.n.04', 'name': 'booth'}, {'id': 5957, 'synset': 'booth.n.01', 'name': 'booth'}, {'id': 5958, 'synset': 'boothose.n.01', 'name': 'boothose'}, {'id': 5959, 'synset': 'bootjack.n.01', 'name': 'bootjack'}, {'id': 5960, 'synset': 'bootlace.n.01', 'name': 'bootlace'}, {'id': 5961, 'synset': 'bootleg.n.02', 'name': 'bootleg'}, {'id': 5962, 'synset': 'bootstrap.n.01', 'name': 'bootstrap'}, {'id': 5963, 'synset': 'bore_bit.n.01', 'name': 'bore_bit'}, {'id': 5964, 'synset': 'boron_chamber.n.01', 'name': 'boron_chamber'}, {'id': 5965, 'synset': 'borstal.n.01', 'name': 'borstal'}, {'id': 5966, 'synset': 'bosom.n.03', 'name': 'bosom'}, {'id': 5967, 'synset': 'boston_rocker.n.01', 'name': 'Boston_rocker'}, {'id': 5968, 'synset': 'bota.n.01', 'name': 'bota'}, {'id': 5969, 'synset': 'bottle.n.03', 'name': 'bottle'}, {'id': 5970, 'synset': 'bottle_bank.n.01', 'name': 'bottle_bank'}, {'id': 5971, 'synset': 'bottlebrush.n.01', 'name': 'bottlebrush'}, {'id': 5972, 'synset': 'bottlecap.n.01', 'name': 'bottlecap'}, {'id': 5973, 'synset': 'bottling_plant.n.01', 'name': 'bottling_plant'}, {'id': 5974, 'synset': 'bottom.n.07', 'name': 'bottom'}, {'id': 5975, 'synset': 'boucle.n.01', 'name': 'boucle'}, {'id': 5976, 'synset': 'boudoir.n.01', 'name': 'boudoir'}, {'id': 5977, 'synset': 'boulle.n.01', 'name': 'boulle'}, {'id': 5978, 'synset': 'bouncing_betty.n.01', 'name': 'bouncing_betty'}, {'id': 5979, 'synset': 'boutique.n.01', 'name': 'boutique'}, {'id': 5980, 'synset': 'boutonniere.n.01', 'name': 'boutonniere'}, {'id': 5981, 'synset': 'bow.n.02', 'name': 'bow'}, {'id': 5982, 'synset': 'bow.n.01', 'name': 'bow'}, {'id': 5983, 'synset': 'bow_and_arrow.n.01', 'name': 'bow_and_arrow'}, {'id': 5984, 'synset': 'bowed_stringed_instrument.n.01', 'name': 'bowed_stringed_instrument'}, {'id': 5985, 'synset': 'bowie_knife.n.01', 'name': 'Bowie_knife'}, {'id': 5986, 'synset': 'bowl.n.01', 'name': 'bowl'}, {'id': 5987, 'synset': 'bowl.n.07', 'name': 'bowl'}, {'id': 5988, 'synset': 'bowline.n.01', 'name': 'bowline'}, {'id': 5989, 'synset': 'bowling_alley.n.01', 'name': 'bowling_alley'}, {'id': 5990, 'synset': 'bowling_equipment.n.01', 'name': 'bowling_equipment'}, {'id': 5991, 'synset': 'bowling_pin.n.01', 'name': 'bowling_pin'}, {'id': 5992, 'synset': 'bowling_shoe.n.01', 'name': 'bowling_shoe'}, {'id': 5993, 'synset': 'bowsprit.n.01', 'name': 'bowsprit'}, {'id': 5994, 'synset': 'bowstring.n.01', 'name': 'bowstring'}, {'id': 5995, 'synset': 'box.n.02', 'name': 'box'}, {'id': 5996, 'synset': 'box.n.08', 'name': 'box'}, {'id': 5997, 'synset': 'box_beam.n.01', 'name': 'box_beam'}, {'id': 5998, 'synset': 'box_camera.n.01', 'name': 'box_camera'}, {'id': 5999, 'synset': 'boxcar.n.01', 'name': 'boxcar'}, {'id': 6000, 'synset': 'box_coat.n.01', 'name': 'box_coat'}, {'id': 6001, 'synset': 'boxing_equipment.n.01', 'name': 'boxing_equipment'}, {'id': 6002, 'synset': 'box_office.n.02', 'name': 'box_office'}, {'id': 6003, 'synset': 'box_spring.n.01', 'name': 'box_spring'}, {'id': 6004, 'synset': 'box_wrench.n.01', 'name': 'box_wrench'}, {'id': 6005, 'synset': 'brace.n.09', 'name': 'brace'}, {'id': 6006, 'synset': 'brace.n.07', 'name': 'brace'}, {'id': 6007, 'synset': 'brace.n.01', 'name': 'brace'}, {'id': 6008, 'synset': 'brace_and_bit.n.01', 'name': 'brace_and_bit'}, {'id': 6009, 'synset': 'bracer.n.01', 'name': 'bracer'}, {'id': 6010, 'synset': 'brace_wrench.n.01', 'name': 'brace_wrench'}, {'id': 6011, 'synset': 'bracket.n.04', 'name': 'bracket'}, {'id': 6012, 'synset': 'bradawl.n.01', 'name': 'bradawl'}, {'id': 6013, 'synset': 'brake.n.01', 'name': 'brake'}, {'id': 6014, 'synset': 'brake.n.05', 'name': 'brake'}, {'id': 6015, 'synset': 'brake_band.n.01', 'name': 'brake_band'}, {'id': 6016, 'synset': 'brake_cylinder.n.01', 'name': 'brake_cylinder'}, {'id': 6017, 'synset': 'brake_disk.n.01', 'name': 'brake_disk'}, {'id': 6018, 'synset': 'brake_drum.n.01', 'name': 'brake_drum'}, {'id': 6019, 'synset': 'brake_lining.n.01', 'name': 'brake_lining'}, {'id': 6020, 'synset': 'brake_pad.n.01', 'name': 'brake_pad'}, {'id': 6021, 'synset': 'brake_pedal.n.01', 'name': 'brake_pedal'}, {'id': 6022, 'synset': 'brake_shoe.n.01', 'name': 'brake_shoe'}, {'id': 6023, 'synset': 'brake_system.n.01', 'name': 'brake_system'}, {'id': 6024, 'synset': 'brass.n.02', 'name': 'brass'}, {'id': 6025, 'synset': 'brass.n.05', 'name': 'brass'}, {'id': 6026, 'synset': 'brassard.n.01', 'name': 'brassard'}, {'id': 6027, 'synset': 'brasserie.n.01', 'name': 'brasserie'}, {'id': 6028, 'synset': 'brassie.n.01', 'name': 'brassie'}, {'id': 6029, 'synset': 'brass_knucks.n.01', 'name': 'brass_knucks'}, {'id': 6030, 'synset': 'brattice.n.01', 'name': 'brattice'}, {'id': 6031, 'synset': 'brazier.n.01', 'name': 'brazier'}, {'id': 6032, 'synset': 'breadbasket.n.03', 'name': 'breadbasket'}, {'id': 6033, 'synset': 'bread_knife.n.01', 'name': 'bread_knife'}, {'id': 6034, 'synset': 'breakable.n.01', 'name': 'breakable'}, {'id': 6035, 'synset': 'breakfast_area.n.01', 'name': 'breakfast_area'}, {'id': 6036, 'synset': 'breakfast_table.n.01', 'name': 'breakfast_table'}, {'id': 6037, 'synset': 'breakwater.n.01', 'name': 'breakwater'}, {'id': 6038, 'synset': 'breast_drill.n.01', 'name': 'breast_drill'}, {'id': 6039, 'synset': 'breast_implant.n.01', 'name': 'breast_implant'}, {'id': 6040, 'synset': 'breastplate.n.01', 'name': 'breastplate'}, {'id': 6041, 'synset': 'breast_pocket.n.01', 'name': 'breast_pocket'}, {'id': 6042, 'synset': 'breathalyzer.n.01', 'name': 'breathalyzer'}, {'id': 6043, 'synset': 'breechblock.n.01', 'name': 'breechblock'}, {'id': 6044, 'synset': 'breeches.n.01', 'name': 'breeches'}, {'id': 6045, 'synset': 'breeches_buoy.n.01', 'name': 'breeches_buoy'}, {'id': 6046, 'synset': 'breechloader.n.01', 'name': 'breechloader'}, {'id': 6047, 'synset': 'breeder_reactor.n.01', 'name': 'breeder_reactor'}, {'id': 6048, 'synset': 'bren.n.01', 'name': 'Bren'}, {'id': 6049, 'synset': 'brewpub.n.01', 'name': 'brewpub'}, {'id': 6050, 'synset': 'brick.n.01', 'name': 'brick'}, {'id': 6051, 'synset': 'brickkiln.n.01', 'name': 'brickkiln'}, {'id': 6052, 'synset': "bricklayer's_hammer.n.01", 'name': "bricklayer's_hammer"}, {'id': 6053, 'synset': 'brick_trowel.n.01', 'name': 'brick_trowel'}, {'id': 6054, 'synset': 'brickwork.n.01', 'name': 'brickwork'}, {'id': 6055, 'synset': 'bridge.n.01', 'name': 'bridge'}, {'id': 6056, 'synset': 'bridge.n.08', 'name': 'bridge'}, {'id': 6057, 'synset': 'bridle.n.01', 'name': 'bridle'}, {'id': 6058, 'synset': 'bridle_path.n.01', 'name': 'bridle_path'}, {'id': 6059, 'synset': 'bridoon.n.01', 'name': 'bridoon'}, {'id': 6060, 'synset': 'briefcase_bomb.n.01', 'name': 'briefcase_bomb'}, {'id': 6061, 'synset': 'briefcase_computer.n.01', 'name': 'briefcase_computer'}, {'id': 6062, 'synset': 'briefs.n.01', 'name': 'briefs'}, {'id': 6063, 'synset': 'brig.n.02', 'name': 'brig'}, {'id': 6064, 'synset': 'brig.n.01', 'name': 'brig'}, {'id': 6065, 'synset': 'brigandine.n.01', 'name': 'brigandine'}, {'id': 6066, 'synset': 'brigantine.n.01', 'name': 'brigantine'}, {'id': 6067, 'synset': 'brilliantine.n.01', 'name': 'brilliantine'}, {'id': 6068, 'synset': 'brilliant_pebble.n.01', 'name': 'brilliant_pebble'}, {'id': 6069, 'synset': 'brim.n.02', 'name': 'brim'}, {'id': 6070, 'synset': 'bristle_brush.n.01', 'name': 'bristle_brush'}, {'id': 6071, 'synset': 'britches.n.01', 'name': 'britches'}, {'id': 6072, 'synset': 'broad_arrow.n.03', 'name': 'broad_arrow'}, {'id': 6073, 'synset': 'broadax.n.01', 'name': 'broadax'}, {'id': 6074, 'synset': 'brochette.n.01', 'name': 'brochette'}, {'id': 6075, 'synset': 'broadcaster.n.02', 'name': 'broadcaster'}, {'id': 6076, 'synset': 'broadcloth.n.02', 'name': 'broadcloth'}, {'id': 6077, 'synset': 'broadcloth.n.01', 'name': 'broadcloth'}, {'id': 6078, 'synset': 'broad_hatchet.n.01', 'name': 'broad_hatchet'}, {'id': 6079, 'synset': 'broadloom.n.01', 'name': 'broadloom'}, {'id': 6080, 'synset': 'broadside.n.03', 'name': 'broadside'}, {'id': 6081, 'synset': 'broadsword.n.01', 'name': 'broadsword'}, {'id': 6082, 'synset': 'brocade.n.01', 'name': 'brocade'}, {'id': 6083, 'synset': 'brogan.n.01', 'name': 'brogan'}, {'id': 6084, 'synset': 'broiler.n.01', 'name': 'broiler'}, {'id': 6085, 'synset': 'broken_arch.n.01', 'name': 'broken_arch'}, {'id': 6086, 'synset': 'bronchoscope.n.01', 'name': 'bronchoscope'}, {'id': 6087, 'synset': 'broom_closet.n.01', 'name': 'broom_closet'}, {'id': 6088, 'synset': 'broomstick.n.01', 'name': 'broomstick'}, {'id': 6089, 'synset': 'brougham.n.01', 'name': 'brougham'}, {'id': 6090, 'synset': 'browning_automatic_rifle.n.01', 'name': 'Browning_automatic_rifle'}, {'id': 6091, 'synset': 'browning_machine_gun.n.01', 'name': 'Browning_machine_gun'}, {'id': 6092, 'synset': 'brownstone.n.02', 'name': 'brownstone'}, {'id': 6093, 'synset': 'brunch_coat.n.01', 'name': 'brunch_coat'}, {'id': 6094, 'synset': 'brush.n.02', 'name': 'brush'}, {'id': 6095, 'synset': 'brussels_carpet.n.01', 'name': 'Brussels_carpet'}, {'id': 6096, 'synset': 'brussels_lace.n.01', 'name': 'Brussels_lace'}, {'id': 6097, 'synset': 'bubble.n.04', 'name': 'bubble'}, {'id': 6098, 'synset': 'bubble_chamber.n.01', 'name': 'bubble_chamber'}, {'id': 6099, 'synset': 'bubble_jet_printer.n.01', 'name': 'bubble_jet_printer'}, {'id': 6100, 'synset': 'buckboard.n.01', 'name': 'buckboard'}, {'id': 6101, 'synset': 'bucket_seat.n.01', 'name': 'bucket_seat'}, {'id': 6102, 'synset': 'bucket_shop.n.02', 'name': 'bucket_shop'}, {'id': 6103, 'synset': 'buckle.n.01', 'name': 'buckle'}, {'id': 6104, 'synset': 'buckram.n.01', 'name': 'buckram'}, {'id': 6105, 'synset': 'bucksaw.n.01', 'name': 'bucksaw'}, {'id': 6106, 'synset': 'buckskins.n.01', 'name': 'buckskins'}, {'id': 6107, 'synset': 'buff.n.05', 'name': 'buff'}, {'id': 6108, 'synset': 'buffer.n.05', 'name': 'buffer'}, {'id': 6109, 'synset': 'buffer.n.04', 'name': 'buffer'}, {'id': 6110, 'synset': 'buffet.n.01', 'name': 'buffet'}, {'id': 6111, 'synset': 'buffing_wheel.n.01', 'name': 'buffing_wheel'}, {'id': 6112, 'synset': 'bugle.n.01', 'name': 'bugle'}, {'id': 6113, 'synset': 'building.n.01', 'name': 'building'}, {'id': 6114, 'synset': 'building_complex.n.01', 'name': 'building_complex'}, {'id': 6115, 'synset': 'bulldog_clip.n.01', 'name': 'bulldog_clip'}, {'id': 6116, 'synset': 'bulldog_wrench.n.01', 'name': 'bulldog_wrench'}, {'id': 6117, 'synset': 'bullet.n.01', 'name': 'bullet'}, {'id': 6118, 'synset': 'bullion.n.02', 'name': 'bullion'}, {'id': 6119, 'synset': 'bullnose.n.01', 'name': 'bullnose'}, {'id': 6120, 'synset': 'bullpen.n.02', 'name': 'bullpen'}, {'id': 6121, 'synset': 'bullpen.n.01', 'name': 'bullpen'}, {'id': 6122, 'synset': 'bullring.n.01', 'name': 'bullring'}, {'id': 6123, 'synset': 'bulwark.n.02', 'name': 'bulwark'}, {'id': 6124, 'synset': 'bumboat.n.01', 'name': 'bumboat'}, {'id': 6125, 'synset': 'bumper.n.02', 'name': 'bumper'}, {'id': 6126, 'synset': 'bumper.n.01', 'name': 'bumper'}, {'id': 6127, 'synset': 'bumper_car.n.01', 'name': 'bumper_car'}, {'id': 6128, 'synset': 'bumper_guard.n.01', 'name': 'bumper_guard'}, {'id': 6129, 'synset': 'bumper_jack.n.01', 'name': 'bumper_jack'}, {'id': 6130, 'synset': 'bundle.n.02', 'name': 'bundle'}, {'id': 6131, 'synset': 'bung.n.01', 'name': 'bung'}, {'id': 6132, 'synset': 'bungalow.n.01', 'name': 'bungalow'}, {'id': 6133, 'synset': 'bungee.n.01', 'name': 'bungee'}, {'id': 6134, 'synset': 'bunghole.n.02', 'name': 'bunghole'}, {'id': 6135, 'synset': 'bunk.n.03', 'name': 'bunk'}, {'id': 6136, 'synset': 'bunk.n.01', 'name': 'bunk'}, {'id': 6137, 'synset': 'bunker.n.01', 'name': 'bunker'}, {'id': 6138, 'synset': 'bunker.n.03', 'name': 'bunker'}, {'id': 6139, 'synset': 'bunker.n.02', 'name': 'bunker'}, {'id': 6140, 'synset': 'bunsen_burner.n.01', 'name': 'bunsen_burner'}, {'id': 6141, 'synset': 'bunting.n.01', 'name': 'bunting'}, {'id': 6142, 'synset': 'bur.n.02', 'name': 'bur'}, {'id': 6143, 'synset': 'burberry.n.01', 'name': 'Burberry'}, {'id': 6144, 'synset': 'burette.n.01', 'name': 'burette'}, {'id': 6145, 'synset': 'burglar_alarm.n.02', 'name': 'burglar_alarm'}, {'id': 6146, 'synset': 'burial_chamber.n.01', 'name': 'burial_chamber'}, {'id': 6147, 'synset': 'burial_garment.n.01', 'name': 'burial_garment'}, {'id': 6148, 'synset': 'burial_mound.n.01', 'name': 'burial_mound'}, {'id': 6149, 'synset': 'burin.n.01', 'name': 'burin'}, {'id': 6150, 'synset': 'burqa.n.01', 'name': 'burqa'}, {'id': 6151, 'synset': 'burlap.n.01', 'name': 'burlap'}, {'id': 6152, 'synset': 'burn_bag.n.01', 'name': 'burn_bag'}, {'id': 6153, 'synset': 'burner.n.01', 'name': 'burner'}, {'id': 6154, 'synset': 'burnous.n.01', 'name': 'burnous'}, {'id': 6155, 'synset': 'burp_gun.n.01', 'name': 'burp_gun'}, {'id': 6156, 'synset': 'burr.n.04', 'name': 'burr'}, {'id': 6157, 'synset': 'bushel_basket.n.01', 'name': 'bushel_basket'}, {'id': 6158, 'synset': 'bushing.n.02', 'name': 'bushing'}, {'id': 6159, 'synset': 'bush_jacket.n.01', 'name': 'bush_jacket'}, {'id': 6160, 'synset': 'business_suit.n.01', 'name': 'business_suit'}, {'id': 6161, 'synset': 'buskin.n.01', 'name': 'buskin'}, {'id': 6162, 'synset': 'bustier.n.01', 'name': 'bustier'}, {'id': 6163, 'synset': 'bustle.n.02', 'name': 'bustle'}, {'id': 6164, 'synset': 'butcher_knife.n.01', 'name': 'butcher_knife'}, {'id': 6165, 'synset': 'butcher_shop.n.01', 'name': 'butcher_shop'}, {'id': 6166, 'synset': 'butter_dish.n.01', 'name': 'butter_dish'}, {'id': 6167, 'synset': 'butterfly_valve.n.01', 'name': 'butterfly_valve'}, {'id': 6168, 'synset': 'butter_knife.n.01', 'name': 'butter_knife'}, {'id': 6169, 'synset': 'butt_hinge.n.01', 'name': 'butt_hinge'}, {'id': 6170, 'synset': 'butt_joint.n.01', 'name': 'butt_joint'}, {'id': 6171, 'synset': 'buttonhook.n.01', 'name': 'buttonhook'}, {'id': 6172, 'synset': 'buttress.n.01', 'name': 'buttress'}, {'id': 6173, 'synset': 'butt_shaft.n.01', 'name': 'butt_shaft'}, {'id': 6174, 'synset': 'butt_weld.n.01', 'name': 'butt_weld'}, {'id': 6175, 'synset': 'buzz_bomb.n.01', 'name': 'buzz_bomb'}, {'id': 6176, 'synset': 'buzzer.n.02', 'name': 'buzzer'}, {'id': 6177, 'synset': 'bvd.n.01', 'name': 'BVD'}, {'id': 6178, 'synset': 'bypass_condenser.n.01', 'name': 'bypass_condenser'}, {'id': 6179, 'synset': 'byway.n.01', 'name': 'byway'}, {'id': 6180, 'synset': 'cab.n.02', 'name': 'cab'}, {'id': 6181, 'synset': 'cab.n.01', 'name': 'cab'}, {'id': 6182, 'synset': 'cabaret.n.01', 'name': 'cabaret'}, {'id': 6183, 'synset': 'caber.n.01', 'name': 'caber'}, {'id': 6184, 'synset': 'cabin.n.03', 'name': 'cabin'}, {'id': 6185, 'synset': 'cabin.n.02', 'name': 'cabin'}, {'id': 6186, 'synset': 'cabin_class.n.01', 'name': 'cabin_class'}, {'id': 6187, 'synset': 'cabin_cruiser.n.01', 'name': 'cabin_cruiser'}, {'id': 6188, 'synset': 'cabinet.n.04', 'name': 'cabinet'}, {'id': 6189, 'synset': 'cabinetwork.n.01', 'name': 'cabinetwork'}, {'id': 6190, 'synset': 'cabin_liner.n.01', 'name': 'cabin_liner'}, {'id': 6191, 'synset': 'cable.n.06', 'name': 'cable'}, {'id': 6192, 'synset': 'cable.n.02', 'name': 'cable'}, {'id': 6193, 'synset': 'cable_car.n.01', 'name': 'cable_car'}, {'id': 6194, 'synset': 'cache.n.03', 'name': 'cache'}, {'id': 6195, 'synset': 'caddy.n.01', 'name': 'caddy'}, {'id': 6196, 'synset': 'caesium_clock.n.01', 'name': 'caesium_clock'}, {'id': 6197, 'synset': 'cafe.n.01', 'name': 'cafe'}, {'id': 6198, 'synset': 'cafeteria.n.01', 'name': 'cafeteria'}, {'id': 6199, 'synset': 'cafeteria_tray.n.01', 'name': 'cafeteria_tray'}, {'id': 6200, 'synset': 'caff.n.01', 'name': 'caff'}, {'id': 6201, 'synset': 'caftan.n.02', 'name': 'caftan'}, {'id': 6202, 'synset': 'caftan.n.01', 'name': 'caftan'}, {'id': 6203, 'synset': 'cage.n.01', 'name': 'cage'}, {'id': 6204, 'synset': 'cage.n.04', 'name': 'cage'}, {'id': 6205, 'synset': 'cagoule.n.01', 'name': 'cagoule'}, {'id': 6206, 'synset': 'caisson.n.02', 'name': 'caisson'}, {'id': 6207, 'synset': 'calash.n.02', 'name': 'calash'}, {'id': 6208, 'synset': 'calceus.n.01', 'name': 'calceus'}, {'id': 6209, 'synset': 'calcimine.n.01', 'name': 'calcimine'}, {'id': 6210, 'synset': 'caldron.n.01', 'name': 'caldron'}, {'id': 6211, 'synset': 'calico.n.01', 'name': 'calico'}, {'id': 6212, 'synset': 'caliper.n.01', 'name': 'caliper'}, {'id': 6213, 'synset': 'call-board.n.01', 'name': 'call-board'}, {'id': 6214, 'synset': 'call_center.n.01', 'name': 'call_center'}, {'id': 6215, 'synset': 'caller_id.n.01', 'name': 'caller_ID'}, {'id': 6216, 'synset': 'calliope.n.02', 'name': 'calliope'}, {'id': 6217, 'synset': 'calorimeter.n.01', 'name': 'calorimeter'}, {'id': 6218, 'synset': 'calpac.n.01', 'name': 'calpac'}, {'id': 6219, 'synset': 'camail.n.01', 'name': 'camail'}, {'id': 6220, 'synset': 'camber_arch.n.01', 'name': 'camber_arch'}, {'id': 6221, 'synset': 'cambric.n.01', 'name': 'cambric'}, {'id': 6222, 'synset': "camel's_hair.n.01", 'name': "camel's_hair"}, {'id': 6223, 'synset': 'camera_lucida.n.01', 'name': 'camera_lucida'}, {'id': 6224, 'synset': 'camera_obscura.n.01', 'name': 'camera_obscura'}, {'id': 6225, 'synset': 'camera_tripod.n.01', 'name': 'camera_tripod'}, {'id': 6226, 'synset': 'camise.n.01', 'name': 'camise'}, {'id': 6227, 'synset': 'camisole.n.02', 'name': 'camisole'}, {'id': 6228, 'synset': 'camisole.n.01', 'name': 'camisole'}, {'id': 6229, 'synset': 'camlet.n.02', 'name': 'camlet'}, {'id': 6230, 'synset': 'camouflage.n.03', 'name': 'camouflage'}, {'id': 6231, 'synset': 'camouflage.n.02', 'name': 'camouflage'}, {'id': 6232, 'synset': 'camp.n.01', 'name': 'camp'}, {'id': 6233, 'synset': 'camp.n.03', 'name': 'camp'}, {'id': 6234, 'synset': 'camp.n.07', 'name': 'camp'}, {'id': 6235, 'synset': 'campaign_hat.n.01', 'name': 'campaign_hat'}, {'id': 6236, 'synset': 'campanile.n.01', 'name': 'campanile'}, {'id': 6237, 'synset': 'camp_chair.n.01', 'name': 'camp_chair'}, {'id': 6238, 'synset': 'camper_trailer.n.01', 'name': 'camper_trailer'}, {'id': 6239, 'synset': 'campstool.n.01', 'name': 'campstool'}, {'id': 6240, 'synset': 'camshaft.n.01', 'name': 'camshaft'}, {'id': 6241, 'synset': 'canal.n.03', 'name': 'canal'}, {'id': 6242, 'synset': 'canal_boat.n.01', 'name': 'canal_boat'}, {'id': 6243, 'synset': 'candelabrum.n.01', 'name': 'candelabrum'}, {'id': 6244, 'synset': 'candid_camera.n.01', 'name': 'candid_camera'}, {'id': 6245, 'synset': 'candlepin.n.01', 'name': 'candlepin'}, {'id': 6246, 'synset': 'candlesnuffer.n.01', 'name': 'candlesnuffer'}, {'id': 6247, 'synset': 'candlewick.n.02', 'name': 'candlewick'}, {'id': 6248, 'synset': 'candy_thermometer.n.01', 'name': 'candy_thermometer'}, {'id': 6249, 'synset': 'cane.n.03', 'name': 'cane'}, {'id': 6250, 'synset': 'cangue.n.01', 'name': 'cangue'}, {'id': 6251, 'synset': 'cannery.n.01', 'name': 'cannery'}, {'id': 6252, 'synset': 'cannikin.n.02', 'name': 'cannikin'}, {'id': 6253, 'synset': 'cannikin.n.01', 'name': 'cannikin'}, {'id': 6254, 'synset': 'cannon.n.01', 'name': 'cannon'}, {'id': 6255, 'synset': 'cannon.n.04', 'name': 'cannon'}, {'id': 6256, 'synset': 'cannon.n.03', 'name': 'cannon'}, {'id': 6257, 'synset': 'cannon.n.02', 'name': 'cannon'}, {'id': 6258, 'synset': 'cannonball.n.01', 'name': 'cannonball'}, {'id': 6259, 'synset': 'canopic_jar.n.01', 'name': 'canopic_jar'}, {'id': 6260, 'synset': 'canopy.n.03', 'name': 'canopy'}, {'id': 6261, 'synset': 'canopy.n.02', 'name': 'canopy'}, {'id': 6262, 'synset': 'canopy.n.01', 'name': 'canopy'}, {'id': 6263, 'synset': 'canteen.n.05', 'name': 'canteen'}, {'id': 6264, 'synset': 'canteen.n.04', 'name': 'canteen'}, {'id': 6265, 'synset': 'canteen.n.03', 'name': 'canteen'}, {'id': 6266, 'synset': 'canteen.n.02', 'name': 'canteen'}, {'id': 6267, 'synset': 'cant_hook.n.01', 'name': 'cant_hook'}, {'id': 6268, 'synset': 'cantilever.n.01', 'name': 'cantilever'}, {'id': 6269, 'synset': 'cantilever_bridge.n.01', 'name': 'cantilever_bridge'}, {'id': 6270, 'synset': 'cantle.n.01', 'name': 'cantle'}, {'id': 6271, 'synset': 'canton_crepe.n.01', 'name': 'Canton_crepe'}, {'id': 6272, 'synset': 'canvas.n.01', 'name': 'canvas'}, {'id': 6273, 'synset': 'canvas.n.06', 'name': 'canvas'}, {'id': 6274, 'synset': 'canvas_tent.n.01', 'name': 'canvas_tent'}, {'id': 6275, 'synset': 'cap.n.04', 'name': 'cap'}, {'id': 6276, 'synset': 'capacitor.n.01', 'name': 'capacitor'}, {'id': 6277, 'synset': 'caparison.n.01', 'name': 'caparison'}, {'id': 6278, 'synset': 'capital_ship.n.01', 'name': 'capital_ship'}, {'id': 6279, 'synset': 'capitol.n.01', 'name': 'capitol'}, {'id': 6280, 'synset': 'cap_opener.n.01', 'name': 'cap_opener'}, {'id': 6281, 'synset': 'capote.n.02', 'name': 'capote'}, {'id': 6282, 'synset': 'capote.n.01', 'name': 'capote'}, {'id': 6283, 'synset': 'cap_screw.n.01', 'name': 'cap_screw'}, {'id': 6284, 'synset': 'capstan.n.01', 'name': 'capstan'}, {'id': 6285, 'synset': 'capstone.n.02', 'name': 'capstone'}, {'id': 6286, 'synset': 'capsule.n.01', 'name': 'capsule'}, {'id': 6287, 'synset': "captain's_chair.n.01", 'name': "captain's_chair"}, {'id': 6288, 'synset': 'carabiner.n.01', 'name': 'carabiner'}, {'id': 6289, 'synset': 'carafe.n.01', 'name': 'carafe'}, {'id': 6290, 'synset': 'caravansary.n.01', 'name': 'caravansary'}, {'id': 6291, 'synset': 'carbine.n.01', 'name': 'carbine'}, {'id': 6292, 'synset': 'car_bomb.n.01', 'name': 'car_bomb'}, {'id': 6293, 'synset': 'carbon_arc_lamp.n.01', 'name': 'carbon_arc_lamp'}, {'id': 6294, 'synset': 'carboy.n.01', 'name': 'carboy'}, {'id': 6295, 'synset': 'carburetor.n.01', 'name': 'carburetor'}, {'id': 6296, 'synset': 'car_carrier.n.01', 'name': 'car_carrier'}, {'id': 6297, 'synset': 'cardcase.n.01', 'name': 'cardcase'}, {'id': 6298, 'synset': 'cardiac_monitor.n.01', 'name': 'cardiac_monitor'}, {'id': 6299, 'synset': 'card_index.n.01', 'name': 'card_index'}, {'id': 6300, 'synset': 'cardiograph.n.01', 'name': 'cardiograph'}, {'id': 6301, 'synset': 'cardioid_microphone.n.01', 'name': 'cardioid_microphone'}, {'id': 6302, 'synset': 'car_door.n.01', 'name': 'car_door'}, {'id': 6303, 'synset': 'cardroom.n.01', 'name': 'cardroom'}, {'id': 6304, 'synset': 'card_table.n.02', 'name': 'card_table'}, {'id': 6305, 'synset': 'card_table.n.01', 'name': 'card_table'}, {'id': 6306, 'synset': 'car-ferry.n.01', 'name': 'car-ferry'}, {'id': 6307, 'synset': 'cargo_area.n.01', 'name': 'cargo_area'}, {'id': 6308, 'synset': 'cargo_container.n.01', 'name': 'cargo_container'}, {'id': 6309, 'synset': 'cargo_door.n.01', 'name': 'cargo_door'}, {'id': 6310, 'synset': 'cargo_hatch.n.01', 'name': 'cargo_hatch'}, {'id': 6311, 'synset': 'cargo_helicopter.n.01', 'name': 'cargo_helicopter'}, {'id': 6312, 'synset': 'cargo_liner.n.01', 'name': 'cargo_liner'}, {'id': 6313, 'synset': 'carillon.n.01', 'name': 'carillon'}, {'id': 6314, 'synset': 'car_mirror.n.01', 'name': 'car_mirror'}, {'id': 6315, 'synset': 'caroche.n.01', 'name': 'caroche'}, {'id': 6316, 'synset': 'carousel.n.02', 'name': 'carousel'}, {'id': 6317, 'synset': "carpenter's_hammer.n.01", 'name': "carpenter's_hammer"}, {'id': 6318, 'synset': "carpenter's_kit.n.01", 'name': "carpenter's_kit"}, {'id': 6319, 'synset': "carpenter's_level.n.01", 'name': "carpenter's_level"}, {'id': 6320, 'synset': "carpenter's_mallet.n.01", 'name': "carpenter's_mallet"}, {'id': 6321, 'synset': "carpenter's_rule.n.01", 'name': "carpenter's_rule"}, {'id': 6322, 'synset': "carpenter's_square.n.01", 'name': "carpenter's_square"}, {'id': 6323, 'synset': 'carpetbag.n.01', 'name': 'carpetbag'}, {'id': 6324, 'synset': 'carpet_beater.n.01', 'name': 'carpet_beater'}, {'id': 6325, 'synset': 'carpet_loom.n.01', 'name': 'carpet_loom'}, {'id': 6326, 'synset': 'carpet_pad.n.01', 'name': 'carpet_pad'}, {'id': 6327, 'synset': 'carpet_sweeper.n.01', 'name': 'carpet_sweeper'}, {'id': 6328, 'synset': 'carpet_tack.n.01', 'name': 'carpet_tack'}, {'id': 6329, 'synset': 'carport.n.01', 'name': 'carport'}, {'id': 6330, 'synset': 'carrack.n.01', 'name': 'carrack'}, {'id': 6331, 'synset': 'carrel.n.02', 'name': 'carrel'}, {'id': 6332, 'synset': 'carriage.n.04', 'name': 'carriage'}, {'id': 6333, 'synset': 'carriage_bolt.n.01', 'name': 'carriage_bolt'}, {'id': 6334, 'synset': 'carriageway.n.01', 'name': 'carriageway'}, {'id': 6335, 'synset': 'carriage_wrench.n.01', 'name': 'carriage_wrench'}, {'id': 6336, 'synset': 'carrick_bend.n.01', 'name': 'carrick_bend'}, {'id': 6337, 'synset': 'carrier.n.10', 'name': 'carrier'}, {'id': 6338, 'synset': 'carrycot.n.01', 'name': 'carrycot'}, {'id': 6339, 'synset': 'car_seat.n.01', 'name': 'car_seat'}, {'id': 6340, 'synset': 'car_tire.n.01', 'name': 'car_tire'}, {'id': 6341, 'synset': 'cartouche.n.01', 'name': 'cartouche'}, {'id': 6342, 'synset': 'car_train.n.01', 'name': 'car_train'}, {'id': 6343, 'synset': 'cartridge.n.01', 'name': 'cartridge'}, {'id': 6344, 'synset': 'cartridge.n.04', 'name': 'cartridge'}, {'id': 6345, 'synset': 'cartridge_belt.n.01', 'name': 'cartridge_belt'}, {'id': 6346, 'synset': 'cartridge_extractor.n.01', 'name': 'cartridge_extractor'}, {'id': 6347, 'synset': 'cartridge_fuse.n.01', 'name': 'cartridge_fuse'}, {'id': 6348, 'synset': 'cartridge_holder.n.01', 'name': 'cartridge_holder'}, {'id': 6349, 'synset': 'cartwheel.n.01', 'name': 'cartwheel'}, {'id': 6350, 'synset': 'carving_fork.n.01', 'name': 'carving_fork'}, {'id': 6351, 'synset': 'carving_knife.n.01', 'name': 'carving_knife'}, {'id': 6352, 'synset': 'car_wheel.n.01', 'name': 'car_wheel'}, {'id': 6353, 'synset': 'caryatid.n.01', 'name': 'caryatid'}, {'id': 6354, 'synset': 'cascade_liquefier.n.01', 'name': 'cascade_liquefier'}, {'id': 6355, 'synset': 'cascade_transformer.n.01', 'name': 'cascade_transformer'}, {'id': 6356, 'synset': 'case.n.05', 'name': 'case'}, {'id': 6357, 'synset': 'case.n.20', 'name': 'case'}, {'id': 6358, 'synset': 'case.n.18', 'name': 'case'}, {'id': 6359, 'synset': 'casein_paint.n.01', 'name': 'casein_paint'}, {'id': 6360, 'synset': 'case_knife.n.02', 'name': 'case_knife'}, {'id': 6361, 'synset': 'case_knife.n.01', 'name': 'case_knife'}, {'id': 6362, 'synset': 'casement.n.01', 'name': 'casement'}, {'id': 6363, 'synset': 'casement_window.n.01', 'name': 'casement_window'}, {'id': 6364, 'synset': 'casern.n.01', 'name': 'casern'}, {'id': 6365, 'synset': 'case_shot.n.01', 'name': 'case_shot'}, {'id': 6366, 'synset': 'cash_bar.n.01', 'name': 'cash_bar'}, {'id': 6367, 'synset': 'cashbox.n.01', 'name': 'cashbox'}, {'id': 6368, 'synset': 'cash_machine.n.01', 'name': 'cash_machine'}, {'id': 6369, 'synset': 'cashmere.n.01', 'name': 'cashmere'}, {'id': 6370, 'synset': 'casing.n.03', 'name': 'casing'}, {'id': 6371, 'synset': 'casino.n.01', 'name': 'casino'}, {'id': 6372, 'synset': 'casket.n.02', 'name': 'casket'}, {'id': 6373, 'synset': 'casque.n.01', 'name': 'casque'}, {'id': 6374, 'synset': 'casquet.n.01', 'name': 'casquet'}, {'id': 6375, 'synset': 'cassegrainian_telescope.n.01', 'name': 'Cassegrainian_telescope'}, {'id': 6376, 'synset': 'casserole.n.02', 'name': 'casserole'}, {'id': 6377, 'synset': 'cassette_deck.n.01', 'name': 'cassette_deck'}, {'id': 6378, 'synset': 'cassette_player.n.01', 'name': 'cassette_player'}, {'id': 6379, 'synset': 'cassette_recorder.n.01', 'name': 'cassette_recorder'}, {'id': 6380, 'synset': 'cassette_tape.n.01', 'name': 'cassette_tape'}, {'id': 6381, 'synset': 'cassock.n.01', 'name': 'cassock'}, {'id': 6382, 'synset': 'caster.n.03', 'name': 'caster'}, {'id': 6383, 'synset': 'caster.n.02', 'name': 'caster'}, {'id': 6384, 'synset': 'castle.n.02', 'name': 'castle'}, {'id': 6385, 'synset': 'castle.n.03', 'name': 'castle'}, {'id': 6386, 'synset': 'catacomb.n.01', 'name': 'catacomb'}, {'id': 6387, 'synset': 'catafalque.n.01', 'name': 'catafalque'}, {'id': 6388, 'synset': 'catalytic_converter.n.01', 'name': 'catalytic_converter'}, {'id': 6389, 'synset': 'catalytic_cracker.n.01', 'name': 'catalytic_cracker'}, {'id': 6390, 'synset': 'catamaran.n.01', 'name': 'catamaran'}, {'id': 6391, 'synset': 'catapult.n.03', 'name': 'catapult'}, {'id': 6392, 'synset': 'catapult.n.02', 'name': 'catapult'}, {'id': 6393, 'synset': 'catboat.n.01', 'name': 'catboat'}, {'id': 6394, 'synset': 'cat_box.n.01', 'name': 'cat_box'}, {'id': 6395, 'synset': 'catch.n.07', 'name': 'catch'}, {'id': 6396, 'synset': 'catchall.n.01', 'name': 'catchall'}, {'id': 6397, 'synset': "catcher's_mask.n.01", 'name': "catcher's_mask"}, {'id': 6398, 'synset': 'catchment.n.01', 'name': 'catchment'}, {'id': 6399, 'synset': 'caterpillar.n.02', 'name': 'Caterpillar'}, {'id': 6400, 'synset': 'cathedra.n.01', 'name': 'cathedra'}, {'id': 6401, 'synset': 'cathedral.n.01', 'name': 'cathedral'}, {'id': 6402, 'synset': 'cathedral.n.02', 'name': 'cathedral'}, {'id': 6403, 'synset': 'catheter.n.01', 'name': 'catheter'}, {'id': 6404, 'synset': 'cathode.n.01', 'name': 'cathode'}, {'id': 6405, 'synset': 'cathode-ray_tube.n.01', 'name': 'cathode-ray_tube'}, {'id': 6406, 'synset': "cat-o'-nine-tails.n.01", 'name': "cat-o'-nine-tails"}, {'id': 6407, 'synset': "cat's-paw.n.02", 'name': "cat's-paw"}, {'id': 6408, 'synset': 'catsup_bottle.n.01', 'name': 'catsup_bottle'}, {'id': 6409, 'synset': 'cattle_car.n.01', 'name': 'cattle_car'}, {'id': 6410, 'synset': 'cattle_guard.n.01', 'name': 'cattle_guard'}, {'id': 6411, 'synset': 'cattleship.n.01', 'name': 'cattleship'}, {'id': 6412, 'synset': 'cautery.n.01', 'name': 'cautery'}, {'id': 6413, 'synset': 'cavalier_hat.n.01', 'name': 'cavalier_hat'}, {'id': 6414, 'synset': 'cavalry_sword.n.01', 'name': 'cavalry_sword'}, {'id': 6415, 'synset': 'cavetto.n.01', 'name': 'cavetto'}, {'id': 6416, 'synset': 'cavity_wall.n.01', 'name': 'cavity_wall'}, {'id': 6417, 'synset': 'c_battery.n.01', 'name': 'C_battery'}, {'id': 6418, 'synset': 'c-clamp.n.01', 'name': 'C-clamp'}, {'id': 6419, 'synset': 'cd_drive.n.01', 'name': 'CD_drive'}, {'id': 6420, 'synset': 'cd-r.n.01', 'name': 'CD-R'}, {'id': 6421, 'synset': 'cd-rom.n.01', 'name': 'CD-ROM'}, {'id': 6422, 'synset': 'cd-rom_drive.n.01', 'name': 'CD-ROM_drive'}, {'id': 6423, 'synset': 'cedar_chest.n.01', 'name': 'cedar_chest'}, {'id': 6424, 'synset': 'ceiling.n.01', 'name': 'ceiling'}, {'id': 6425, 'synset': 'celesta.n.01', 'name': 'celesta'}, {'id': 6426, 'synset': 'cell.n.03', 'name': 'cell'}, {'id': 6427, 'synset': 'cell.n.07', 'name': 'cell'}, {'id': 6428, 'synset': 'cellar.n.03', 'name': 'cellar'}, {'id': 6429, 'synset': 'cellblock.n.01', 'name': 'cellblock'}, {'id': 6430, 'synset': 'cello.n.01', 'name': 'cello'}, {'id': 6431, 'synset': 'cellophane.n.01', 'name': 'cellophane'}, {'id': 6432, 'synset': 'cellulose_tape.n.01', 'name': 'cellulose_tape'}, {'id': 6433, 'synset': 'cenotaph.n.01', 'name': 'cenotaph'}, {'id': 6434, 'synset': 'censer.n.01', 'name': 'censer'}, {'id': 6435, 'synset': 'center.n.03', 'name': 'center'}, {'id': 6436, 'synset': 'center_punch.n.01', 'name': 'center_punch'}, {'id': 6437, 'synset': 'centigrade_thermometer.n.01', 'name': 'Centigrade_thermometer'}, {'id': 6438, 'synset': 'central_processing_unit.n.01', 'name': 'central_processing_unit'}, {'id': 6439, 'synset': 'centrifugal_pump.n.01', 'name': 'centrifugal_pump'}, {'id': 6440, 'synset': 'centrifuge.n.01', 'name': 'centrifuge'}, {'id': 6441, 'synset': 'ceramic.n.01', 'name': 'ceramic'}, {'id': 6442, 'synset': 'ceramic_ware.n.01', 'name': 'ceramic_ware'}, {'id': 6443, 'synset': 'cereal_bowl.n.01', 'name': 'cereal_bowl'}, {'id': 6444, 'synset': 'cereal_box.n.01', 'name': 'cereal_box'}, {'id': 6445, 'synset': 'cerecloth.n.01', 'name': 'cerecloth'}, {'id': 6446, 'synset': 'cesspool.n.01', 'name': 'cesspool'}, {'id': 6447, 'synset': 'chachka.n.02', 'name': 'chachka'}, {'id': 6448, 'synset': 'chador.n.01', 'name': 'chador'}, {'id': 6449, 'synset': 'chafing_dish.n.01', 'name': 'chafing_dish'}, {'id': 6450, 'synset': 'chain.n.03', 'name': 'chain'}, {'id': 6451, 'synset': 'chain.n.05', 'name': 'chain'}, {'id': 6452, 'synset': 'chainlink_fence.n.01', 'name': 'chainlink_fence'}, {'id': 6453, 'synset': 'chain_printer.n.01', 'name': 'chain_printer'}, {'id': 6454, 'synset': 'chain_saw.n.01', 'name': 'chain_saw'}, {'id': 6455, 'synset': 'chain_store.n.01', 'name': 'chain_store'}, {'id': 6456, 'synset': 'chain_tongs.n.01', 'name': 'chain_tongs'}, {'id': 6457, 'synset': 'chain_wrench.n.01', 'name': 'chain_wrench'}, {'id': 6458, 'synset': 'chair.n.05', 'name': 'chair'}, {'id': 6459, 'synset': 'chair_of_state.n.01', 'name': 'chair_of_state'}, {'id': 6460, 'synset': 'chairlift.n.01', 'name': 'chairlift'}, {'id': 6461, 'synset': 'chaise.n.02', 'name': 'chaise'}, {'id': 6462, 'synset': 'chalet.n.01', 'name': 'chalet'}, {'id': 6463, 'synset': 'chalk.n.04', 'name': 'chalk'}, {'id': 6464, 'synset': 'challis.n.01', 'name': 'challis'}, {'id': 6465, 'synset': 'chamberpot.n.01', 'name': 'chamberpot'}, {'id': 6466, 'synset': 'chambray.n.01', 'name': 'chambray'}, {'id': 6467, 'synset': 'chamfer_bit.n.01', 'name': 'chamfer_bit'}, {'id': 6468, 'synset': 'chamfer_plane.n.01', 'name': 'chamfer_plane'}, {'id': 6469, 'synset': 'chamois_cloth.n.01', 'name': 'chamois_cloth'}, {'id': 6470, 'synset': 'chancel.n.01', 'name': 'chancel'}, {'id': 6471, 'synset': 'chancellery.n.01', 'name': 'chancellery'}, {'id': 6472, 'synset': 'chancery.n.02', 'name': 'chancery'}, {'id': 6473, 'synset': 'chandlery.n.01', 'name': 'chandlery'}, {'id': 6474, 'synset': 'chanfron.n.01', 'name': 'chanfron'}, {'id': 6475, 'synset': 'chanter.n.01', 'name': 'chanter'}, {'id': 6476, 'synset': 'chantry.n.02', 'name': 'chantry'}, {'id': 6477, 'synset': 'chapel.n.01', 'name': 'chapel'}, {'id': 6478, 'synset': 'chapterhouse.n.02', 'name': 'chapterhouse'}, {'id': 6479, 'synset': 'chapterhouse.n.01', 'name': 'chapterhouse'}, {'id': 6480, 'synset': 'character_printer.n.01', 'name': 'character_printer'}, {'id': 6481, 'synset': 'charcuterie.n.01', 'name': 'charcuterie'}, {'id': 6482, 'synset': 'charge-exchange_accelerator.n.01', 'name': 'charge-exchange_accelerator'}, {'id': 6483, 'synset': 'charger.n.02', 'name': 'charger'}, {'id': 6484, 'synset': 'chariot.n.01', 'name': 'chariot'}, {'id': 6485, 'synset': 'chariot.n.02', 'name': 'chariot'}, {'id': 6486, 'synset': 'charnel_house.n.01', 'name': 'charnel_house'}, {'id': 6487, 'synset': 'chassis.n.03', 'name': 'chassis'}, {'id': 6488, 'synset': 'chassis.n.02', 'name': 'chassis'}, {'id': 6489, 'synset': 'chasuble.n.01', 'name': 'chasuble'}, {'id': 6490, 'synset': 'chateau.n.01', 'name': 'chateau'}, {'id': 6491, 'synset': 'chatelaine.n.02', 'name': 'chatelaine'}, {'id': 6492, 'synset': 'checker.n.03', 'name': 'checker'}, {'id': 6493, 'synset': 'checkout.n.03', 'name': 'checkout'}, {'id': 6494, 'synset': 'cheekpiece.n.01', 'name': 'cheekpiece'}, {'id': 6495, 'synset': 'cheeseboard.n.01', 'name': 'cheeseboard'}, {'id': 6496, 'synset': 'cheesecloth.n.01', 'name': 'cheesecloth'}, {'id': 6497, 'synset': 'cheese_cutter.n.01', 'name': 'cheese_cutter'}, {'id': 6498, 'synset': 'cheese_press.n.01', 'name': 'cheese_press'}, {'id': 6499, 'synset': 'chemical_bomb.n.01', 'name': 'chemical_bomb'}, {'id': 6500, 'synset': 'chemical_plant.n.01', 'name': 'chemical_plant'}, {'id': 6501, 'synset': 'chemical_reactor.n.01', 'name': 'chemical_reactor'}, {'id': 6502, 'synset': 'chemise.n.02', 'name': 'chemise'}, {'id': 6503, 'synset': 'chemise.n.01', 'name': 'chemise'}, {'id': 6504, 'synset': 'chenille.n.02', 'name': 'chenille'}, {'id': 6505, 'synset': 'chessman.n.01', 'name': 'chessman'}, {'id': 6506, 'synset': 'chest.n.02', 'name': 'chest'}, {'id': 6507, 'synset': 'chesterfield.n.02', 'name': 'chesterfield'}, {'id': 6508, 'synset': 'chest_of_drawers.n.01', 'name': 'chest_of_drawers'}, {'id': 6509, 'synset': 'chest_protector.n.01', 'name': 'chest_protector'}, {'id': 6510, 'synset': 'cheval-de-frise.n.01', 'name': 'cheval-de-frise'}, {'id': 6511, 'synset': 'cheval_glass.n.01', 'name': 'cheval_glass'}, {'id': 6512, 'synset': 'chicane.n.02', 'name': 'chicane'}, {'id': 6513, 'synset': 'chicken_coop.n.01', 'name': 'chicken_coop'}, {'id': 6514, 'synset': 'chicken_wire.n.01', 'name': 'chicken_wire'}, {'id': 6515, 'synset': 'chicken_yard.n.01', 'name': 'chicken_yard'}, {'id': 6516, 'synset': 'chiffon.n.01', 'name': 'chiffon'}, {'id': 6517, 'synset': 'chiffonier.n.01', 'name': 'chiffonier'}, {'id': 6518, 'synset': "child's_room.n.01", 'name': "child's_room"}, {'id': 6519, 'synset': 'chimney_breast.n.01', 'name': 'chimney_breast'}, {'id': 6520, 'synset': 'chimney_corner.n.01', 'name': 'chimney_corner'}, {'id': 6521, 'synset': 'china.n.02', 'name': 'china'}, {'id': 6522, 'synset': 'china_cabinet.n.01', 'name': 'china_cabinet'}, {'id': 6523, 'synset': 'chinchilla.n.02', 'name': 'chinchilla'}, {'id': 6524, 'synset': 'chinese_lantern.n.01', 'name': 'Chinese_lantern'}, {'id': 6525, 'synset': 'chinese_puzzle.n.01', 'name': 'Chinese_puzzle'}, {'id': 6526, 'synset': 'chinning_bar.n.01', 'name': 'chinning_bar'}, {'id': 6527, 'synset': 'chino.n.02', 'name': 'chino'}, {'id': 6528, 'synset': 'chino.n.01', 'name': 'chino'}, {'id': 6529, 'synset': 'chin_rest.n.01', 'name': 'chin_rest'}, {'id': 6530, 'synset': 'chin_strap.n.01', 'name': 'chin_strap'}, {'id': 6531, 'synset': 'chintz.n.01', 'name': 'chintz'}, {'id': 6532, 'synset': 'chip.n.07', 'name': 'chip'}, {'id': 6533, 'synset': 'chisel.n.01', 'name': 'chisel'}, {'id': 6534, 'synset': 'chlamys.n.02', 'name': 'chlamys'}, {'id': 6535, 'synset': 'choir.n.03', 'name': 'choir'}, {'id': 6536, 'synset': 'choir_loft.n.01', 'name': 'choir_loft'}, {'id': 6537, 'synset': 'choke.n.02', 'name': 'choke'}, {'id': 6538, 'synset': 'choke.n.01', 'name': 'choke'}, {'id': 6539, 'synset': 'chokey.n.01', 'name': 'chokey'}, {'id': 6540, 'synset': 'choo-choo.n.01', 'name': 'choo-choo'}, {'id': 6541, 'synset': 'chopine.n.01', 'name': 'chopine'}, {'id': 6542, 'synset': 'chordophone.n.01', 'name': 'chordophone'}, {'id': 6543, 'synset': 'christmas_stocking.n.01', 'name': 'Christmas_stocking'}, {'id': 6544, 'synset': 'chronograph.n.01', 'name': 'chronograph'}, {'id': 6545, 'synset': 'chronometer.n.01', 'name': 'chronometer'}, {'id': 6546, 'synset': 'chronoscope.n.01', 'name': 'chronoscope'}, {'id': 6547, 'synset': 'chuck.n.03', 'name': 'chuck'}, {'id': 6548, 'synset': 'chuck_wagon.n.01', 'name': 'chuck_wagon'}, {'id': 6549, 'synset': 'chukka.n.02', 'name': 'chukka'}, {'id': 6550, 'synset': 'church.n.02', 'name': 'church'}, {'id': 6551, 'synset': 'church_bell.n.01', 'name': 'church_bell'}, {'id': 6552, 'synset': 'church_hat.n.01', 'name': 'church_hat'}, {'id': 6553, 'synset': 'church_key.n.01', 'name': 'church_key'}, {'id': 6554, 'synset': 'church_tower.n.01', 'name': 'church_tower'}, {'id': 6555, 'synset': 'churidars.n.01', 'name': 'churidars'}, {'id': 6556, 'synset': 'churn.n.01', 'name': 'churn'}, {'id': 6557, 'synset': 'ciderpress.n.01', 'name': 'ciderpress'}, {'id': 6558, 'synset': 'cigar_band.n.01', 'name': 'cigar_band'}, {'id': 6559, 'synset': 'cigar_cutter.n.01', 'name': 'cigar_cutter'}, {'id': 6560, 'synset': 'cigarette_butt.n.01', 'name': 'cigarette_butt'}, {'id': 6561, 'synset': 'cigarette_holder.n.01', 'name': 'cigarette_holder'}, {'id': 6562, 'synset': 'cigar_lighter.n.01', 'name': 'cigar_lighter'}, {'id': 6563, 'synset': 'cinch.n.02', 'name': 'cinch'}, {'id': 6564, 'synset': 'cinema.n.02', 'name': 'cinema'}, {'id': 6565, 'synset': 'cinquefoil.n.02', 'name': 'cinquefoil'}, {'id': 6566, 'synset': 'circle.n.08', 'name': 'circle'}, {'id': 6567, 'synset': 'circlet.n.02', 'name': 'circlet'}, {'id': 6568, 'synset': 'circuit.n.01', 'name': 'circuit'}, {'id': 6569, 'synset': 'circuit_board.n.01', 'name': 'circuit_board'}, {'id': 6570, 'synset': 'circuit_breaker.n.01', 'name': 'circuit_breaker'}, {'id': 6571, 'synset': 'circuitry.n.01', 'name': 'circuitry'}, {'id': 6572, 'synset': 'circular_plane.n.01', 'name': 'circular_plane'}, {'id': 6573, 'synset': 'circular_saw.n.01', 'name': 'circular_saw'}, {'id': 6574, 'synset': 'circus_tent.n.01', 'name': 'circus_tent'}, {'id': 6575, 'synset': 'cistern.n.03', 'name': 'cistern'}, {'id': 6576, 'synset': 'cittern.n.01', 'name': 'cittern'}, {'id': 6577, 'synset': 'city_hall.n.01', 'name': 'city_hall'}, {'id': 6578, 'synset': 'cityscape.n.02', 'name': 'cityscape'}, {'id': 6579, 'synset': 'city_university.n.01', 'name': 'city_university'}, {'id': 6580, 'synset': 'civies.n.01', 'name': 'civies'}, {'id': 6581, 'synset': 'civilian_clothing.n.01', 'name': 'civilian_clothing'}, {'id': 6582, 'synset': 'clack_valve.n.01', 'name': 'clack_valve'}, {'id': 6583, 'synset': 'clamp.n.01', 'name': 'clamp'}, {'id': 6584, 'synset': 'clamshell.n.02', 'name': 'clamshell'}, {'id': 6585, 'synset': 'clapper.n.03', 'name': 'clapper'}, {'id': 6586, 'synset': 'clapperboard.n.01', 'name': 'clapperboard'}, {'id': 6587, 'synset': 'clarence.n.01', 'name': 'clarence'}, {'id': 6588, 'synset': 'clark_cell.n.01', 'name': 'Clark_cell'}, {'id': 6589, 'synset': 'clasp_knife.n.01', 'name': 'clasp_knife'}, {'id': 6590, 'synset': 'classroom.n.01', 'name': 'classroom'}, {'id': 6591, 'synset': 'clavichord.n.01', 'name': 'clavichord'}, {'id': 6592, 'synset': 'clavier.n.02', 'name': 'clavier'}, {'id': 6593, 'synset': 'clay_pigeon.n.01', 'name': 'clay_pigeon'}, {'id': 6594, 'synset': 'claymore_mine.n.01', 'name': 'claymore_mine'}, {'id': 6595, 'synset': 'claymore.n.01', 'name': 'claymore'}, {'id': 6596, 'synset': 'cleaners.n.01', 'name': 'cleaners'}, {'id': 6597, 'synset': 'cleaning_implement.n.01', 'name': 'cleaning_implement'}, {'id': 6598, 'synset': 'cleaning_pad.n.01', 'name': 'cleaning_pad'}, {'id': 6599, 'synset': 'clean_room.n.01', 'name': 'clean_room'}, {'id': 6600, 'synset': 'clearway.n.01', 'name': 'clearway'}, {'id': 6601, 'synset': 'cleat.n.01', 'name': 'cleat'}, {'id': 6602, 'synset': 'cleats.n.01', 'name': 'cleats'}, {'id': 6603, 'synset': 'cleaver.n.01', 'name': 'cleaver'}, {'id': 6604, 'synset': 'clerestory.n.01', 'name': 'clerestory'}, {'id': 6605, 'synset': 'clevis.n.01', 'name': 'clevis'}, {'id': 6606, 'synset': 'clews.n.01', 'name': 'clews'}, {'id': 6607, 'synset': 'cliff_dwelling.n.01', 'name': 'cliff_dwelling'}, {'id': 6608, 'synset': 'climbing_frame.n.01', 'name': 'climbing_frame'}, {'id': 6609, 'synset': 'clinch.n.03', 'name': 'clinch'}, {'id': 6610, 'synset': 'clinch.n.02', 'name': 'clinch'}, {'id': 6611, 'synset': 'clincher.n.03', 'name': 'clincher'}, {'id': 6612, 'synset': 'clinic.n.03', 'name': 'clinic'}, {'id': 6613, 'synset': 'clinical_thermometer.n.01', 'name': 'clinical_thermometer'}, {'id': 6614, 'synset': 'clinker.n.02', 'name': 'clinker'}, {'id': 6615, 'synset': 'clinometer.n.01', 'name': 'clinometer'}, {'id': 6616, 'synset': 'clip_lead.n.01', 'name': 'clip_lead'}, {'id': 6617, 'synset': 'clip-on.n.01', 'name': 'clip-on'}, {'id': 6618, 'synset': 'clipper.n.04', 'name': 'clipper'}, {'id': 6619, 'synset': 'clipper.n.02', 'name': 'clipper'}, {'id': 6620, 'synset': 'cloak.n.01', 'name': 'cloak'}, {'id': 6621, 'synset': 'cloakroom.n.02', 'name': 'cloakroom'}, {'id': 6622, 'synset': 'cloche.n.02', 'name': 'cloche'}, {'id': 6623, 'synset': 'cloche.n.01', 'name': 'cloche'}, {'id': 6624, 'synset': 'clock_pendulum.n.01', 'name': 'clock_pendulum'}, {'id': 6625, 'synset': 'clock_radio.n.01', 'name': 'clock_radio'}, {'id': 6626, 'synset': 'clockwork.n.01', 'name': 'clockwork'}, {'id': 6627, 'synset': 'clog.n.01', 'name': 'clog'}, {'id': 6628, 'synset': 'cloisonne.n.01', 'name': 'cloisonne'}, {'id': 6629, 'synset': 'cloister.n.02', 'name': 'cloister'}, {'id': 6630, 'synset': 'closed_circuit.n.01', 'name': 'closed_circuit'}, {'id': 6631, 'synset': 'closed-circuit_television.n.01', 'name': 'closed-circuit_television'}, {'id': 6632, 'synset': 'closed_loop.n.01', 'name': 'closed_loop'}, {'id': 6633, 'synset': 'closet.n.04', 'name': 'closet'}, {'id': 6634, 'synset': 'closeup_lens.n.01', 'name': 'closeup_lens'}, {'id': 6635, 'synset': 'cloth_cap.n.01', 'name': 'cloth_cap'}, {'id': 6636, 'synset': 'cloth_covering.n.01', 'name': 'cloth_covering'}, {'id': 6637, 'synset': 'clothesbrush.n.01', 'name': 'clothesbrush'}, {'id': 6638, 'synset': 'clothes_closet.n.01', 'name': 'clothes_closet'}, {'id': 6639, 'synset': 'clothes_dryer.n.01', 'name': 'clothes_dryer'}, {'id': 6640, 'synset': 'clotheshorse.n.01', 'name': 'clotheshorse'}, {'id': 6641, 'synset': 'clothes_tree.n.01', 'name': 'clothes_tree'}, {'id': 6642, 'synset': 'clothing.n.01', 'name': 'clothing'}, {'id': 6643, 'synset': 'clothing_store.n.01', 'name': 'clothing_store'}, {'id': 6644, 'synset': 'clout_nail.n.01', 'name': 'clout_nail'}, {'id': 6645, 'synset': 'clove_hitch.n.01', 'name': 'clove_hitch'}, {'id': 6646, 'synset': 'club_car.n.01', 'name': 'club_car'}, {'id': 6647, 'synset': 'clubroom.n.01', 'name': 'clubroom'}, {'id': 6648, 'synset': 'cluster_bomb.n.01', 'name': 'cluster_bomb'}, {'id': 6649, 'synset': 'clutch.n.07', 'name': 'clutch'}, {'id': 6650, 'synset': 'clutch.n.06', 'name': 'clutch'}, {'id': 6651, 'synset': 'coach.n.04', 'name': 'coach'}, {'id': 6652, 'synset': 'coach_house.n.01', 'name': 'coach_house'}, {'id': 6653, 'synset': 'coal_car.n.01', 'name': 'coal_car'}, {'id': 6654, 'synset': 'coal_chute.n.01', 'name': 'coal_chute'}, {'id': 6655, 'synset': 'coal_house.n.01', 'name': 'coal_house'}, {'id': 6656, 'synset': 'coal_shovel.n.01', 'name': 'coal_shovel'}, {'id': 6657, 'synset': 'coaming.n.01', 'name': 'coaming'}, {'id': 6658, 'synset': 'coaster_brake.n.01', 'name': 'coaster_brake'}, {'id': 6659, 'synset': 'coat_button.n.01', 'name': 'coat_button'}, {'id': 6660, 'synset': 'coat_closet.n.01', 'name': 'coat_closet'}, {'id': 6661, 'synset': 'coatdress.n.01', 'name': 'coatdress'}, {'id': 6662, 'synset': 'coatee.n.01', 'name': 'coatee'}, {'id': 6663, 'synset': 'coating.n.01', 'name': 'coating'}, {'id': 6664, 'synset': 'coating.n.03', 'name': 'coating'}, {'id': 6665, 'synset': 'coat_of_paint.n.01', 'name': 'coat_of_paint'}, {'id': 6666, 'synset': 'coattail.n.01', 'name': 'coattail'}, {'id': 6667, 'synset': 'coaxial_cable.n.01', 'name': 'coaxial_cable'}, {'id': 6668, 'synset': 'cobweb.n.03', 'name': 'cobweb'}, {'id': 6669, 'synset': 'cobweb.n.01', 'name': 'cobweb'}, {'id': 6670, 'synset': 'cockcroft_and_walton_accelerator.n.01', 'name': 'Cockcroft_and_Walton_accelerator'}, {'id': 6671, 'synset': 'cocked_hat.n.01', 'name': 'cocked_hat'}, {'id': 6672, 'synset': 'cockhorse.n.01', 'name': 'cockhorse'}, {'id': 6673, 'synset': 'cockleshell.n.01', 'name': 'cockleshell'}, {'id': 6674, 'synset': 'cockpit.n.01', 'name': 'cockpit'}, {'id': 6675, 'synset': 'cockpit.n.03', 'name': 'cockpit'}, {'id': 6676, 'synset': 'cockpit.n.02', 'name': 'cockpit'}, {'id': 6677, 'synset': 'cockscomb.n.03', 'name': 'cockscomb'}, {'id': 6678, 'synset': 'cocktail_dress.n.01', 'name': 'cocktail_dress'}, {'id': 6679, 'synset': 'cocktail_lounge.n.01', 'name': 'cocktail_lounge'}, {'id': 6680, 'synset': 'cocktail_shaker.n.01', 'name': 'cocktail_shaker'}, {'id': 6681, 'synset': 'cocotte.n.02', 'name': 'cocotte'}, {'id': 6682, 'synset': 'codpiece.n.01', 'name': 'codpiece'}, {'id': 6683, 'synset': 'coelostat.n.01', 'name': 'coelostat'}, {'id': 6684, 'synset': 'coffee_can.n.01', 'name': 'coffee_can'}, {'id': 6685, 'synset': 'coffee_cup.n.01', 'name': 'coffee_cup'}, {'id': 6686, 'synset': 'coffee_filter.n.01', 'name': 'coffee_filter'}, {'id': 6687, 'synset': 'coffee_mill.n.01', 'name': 'coffee_mill'}, {'id': 6688, 'synset': 'coffee_mug.n.01', 'name': 'coffee_mug'}, {'id': 6689, 'synset': 'coffee_stall.n.01', 'name': 'coffee_stall'}, {'id': 6690, 'synset': 'coffee_urn.n.01', 'name': 'coffee_urn'}, {'id': 6691, 'synset': 'coffer.n.02', 'name': 'coffer'}, {'id': 6692, 'synset': 'coffey_still.n.01', 'name': 'Coffey_still'}, {'id': 6693, 'synset': 'coffin.n.01', 'name': 'coffin'}, {'id': 6694, 'synset': 'cog.n.02', 'name': 'cog'}, {'id': 6695, 'synset': 'coif.n.02', 'name': 'coif'}, {'id': 6696, 'synset': 'coil.n.01', 'name': 'coil'}, {'id': 6697, 'synset': 'coil.n.06', 'name': 'coil'}, {'id': 6698, 'synset': 'coil.n.03', 'name': 'coil'}, {'id': 6699, 'synset': 'coil_spring.n.01', 'name': 'coil_spring'}, {'id': 6700, 'synset': 'coin_box.n.01', 'name': 'coin_box'}, {'id': 6701, 'synset': 'cold_cathode.n.01', 'name': 'cold_cathode'}, {'id': 6702, 'synset': 'cold_chisel.n.01', 'name': 'cold_chisel'}, {'id': 6703, 'synset': 'cold_cream.n.01', 'name': 'cold_cream'}, {'id': 6704, 'synset': 'cold_frame.n.01', 'name': 'cold_frame'}, {'id': 6705, 'synset': 'collar.n.01', 'name': 'collar'}, {'id': 6706, 'synset': 'collar.n.03', 'name': 'collar'}, {'id': 6707, 'synset': 'college.n.03', 'name': 'college'}, {'id': 6708, 'synset': 'collet.n.02', 'name': 'collet'}, {'id': 6709, 'synset': 'collider.n.01', 'name': 'collider'}, {'id': 6710, 'synset': 'colliery.n.01', 'name': 'colliery'}, {'id': 6711, 'synset': 'collimator.n.02', 'name': 'collimator'}, {'id': 6712, 'synset': 'collimator.n.01', 'name': 'collimator'}, {'id': 6713, 'synset': 'cologne.n.02', 'name': 'cologne'}, {'id': 6714, 'synset': 'colonnade.n.01', 'name': 'colonnade'}, {'id': 6715, 'synset': 'colonoscope.n.01', 'name': 'colonoscope'}, {'id': 6716, 'synset': 'colorimeter.n.01', 'name': 'colorimeter'}, {'id': 6717, 'synset': 'colors.n.02', 'name': 'colors'}, {'id': 6718, 'synset': 'color_television.n.01', 'name': 'color_television'}, {'id': 6719, 'synset': 'color_tube.n.01', 'name': 'color_tube'}, {'id': 6720, 'synset': 'color_wash.n.01', 'name': 'color_wash'}, {'id': 6721, 'synset': 'colt.n.02', 'name': 'Colt'}, {'id': 6722, 'synset': 'colter.n.01', 'name': 'colter'}, {'id': 6723, 'synset': 'columbarium.n.03', 'name': 'columbarium'}, {'id': 6724, 'synset': 'columbarium.n.02', 'name': 'columbarium'}, {'id': 6725, 'synset': 'column.n.07', 'name': 'column'}, {'id': 6726, 'synset': 'column.n.06', 'name': 'column'}, {'id': 6727, 'synset': 'comb.n.01', 'name': 'comb'}, {'id': 6728, 'synset': 'comb.n.03', 'name': 'comb'}, {'id': 6729, 'synset': 'comber.n.03', 'name': 'comber'}, {'id': 6730, 'synset': 'combination_plane.n.01', 'name': 'combination_plane'}, {'id': 6731, 'synset': 'combine.n.01', 'name': 'combine'}, {'id': 6732, 'synset': 'command_module.n.01', 'name': 'command_module'}, {'id': 6733, 'synset': 'commissary.n.01', 'name': 'commissary'}, {'id': 6734, 'synset': 'commissary.n.02', 'name': 'commissary'}, {'id': 6735, 'synset': 'commodity.n.01', 'name': 'commodity'}, {'id': 6736, 'synset': 'common_ax.n.01', 'name': 'common_ax'}, {'id': 6737, 'synset': 'common_room.n.01', 'name': 'common_room'}, {'id': 6738, 'synset': 'communications_satellite.n.01', 'name': 'communications_satellite'}, {'id': 6739, 'synset': 'communication_system.n.01', 'name': 'communication_system'}, {'id': 6740, 'synset': 'community_center.n.01', 'name': 'community_center'}, {'id': 6741, 'synset': 'commutator.n.01', 'name': 'commutator'}, {'id': 6742, 'synset': 'commuter.n.01', 'name': 'commuter'}, {'id': 6743, 'synset': 'compact.n.01', 'name': 'compact'}, {'id': 6744, 'synset': 'compact.n.03', 'name': 'compact'}, {'id': 6745, 'synset': 'compact_disk.n.01', 'name': 'compact_disk'}, {'id': 6746, 'synset': 'compact-disk_burner.n.01', 'name': 'compact-disk_burner'}, {'id': 6747, 'synset': 'companionway.n.01', 'name': 'companionway'}, {'id': 6748, 'synset': 'compartment.n.02', 'name': 'compartment'}, {'id': 6749, 'synset': 'compartment.n.01', 'name': 'compartment'}, {'id': 6750, 'synset': 'compass.n.04', 'name': 'compass'}, {'id': 6751, 'synset': 'compass_card.n.01', 'name': 'compass_card'}, {'id': 6752, 'synset': 'compass_saw.n.01', 'name': 'compass_saw'}, {'id': 6753, 'synset': 'compound.n.03', 'name': 'compound'}, {'id': 6754, 'synset': 'compound_lens.n.01', 'name': 'compound_lens'}, {'id': 6755, 'synset': 'compound_lever.n.01', 'name': 'compound_lever'}, {'id': 6756, 'synset': 'compound_microscope.n.01', 'name': 'compound_microscope'}, {'id': 6757, 'synset': 'compress.n.01', 'name': 'compress'}, {'id': 6758, 'synset': 'compression_bandage.n.01', 'name': 'compression_bandage'}, {'id': 6759, 'synset': 'compressor.n.01', 'name': 'compressor'}, {'id': 6760, 'synset': 'computer.n.01', 'name': 'computer'}, {'id': 6761, 'synset': 'computer_circuit.n.01', 'name': 'computer_circuit'}, {'id': 6762, 'synset': 'computerized_axial_tomography_scanner.n.01', 'name': 'computerized_axial_tomography_scanner'}, {'id': 6763, 'synset': 'computer_monitor.n.01', 'name': 'computer_monitor'}, {'id': 6764, 'synset': 'computer_network.n.01', 'name': 'computer_network'}, {'id': 6765, 'synset': 'computer_screen.n.01', 'name': 'computer_screen'}, {'id': 6766, 'synset': 'computer_store.n.01', 'name': 'computer_store'}, {'id': 6767, 'synset': 'computer_system.n.01', 'name': 'computer_system'}, {'id': 6768, 'synset': 'concentration_camp.n.01', 'name': 'concentration_camp'}, {'id': 6769, 'synset': 'concert_grand.n.01', 'name': 'concert_grand'}, {'id': 6770, 'synset': 'concert_hall.n.01', 'name': 'concert_hall'}, {'id': 6771, 'synset': 'concertina.n.02', 'name': 'concertina'}, {'id': 6772, 'synset': 'concertina.n.01', 'name': 'concertina'}, {'id': 6773, 'synset': 'concrete_mixer.n.01', 'name': 'concrete_mixer'}, {'id': 6774, 'synset': 'condensation_pump.n.01', 'name': 'condensation_pump'}, {'id': 6775, 'synset': 'condenser.n.04', 'name': 'condenser'}, {'id': 6776, 'synset': 'condenser.n.03', 'name': 'condenser'}, {'id': 6777, 'synset': 'condenser.n.02', 'name': 'condenser'}, {'id': 6778, 'synset': 'condenser_microphone.n.01', 'name': 'condenser_microphone'}, {'id': 6779, 'synset': 'condominium.n.02', 'name': 'condominium'}, {'id': 6780, 'synset': 'condominium.n.01', 'name': 'condominium'}, {'id': 6781, 'synset': 'conductor.n.04', 'name': 'conductor'}, {'id': 6782, 'synset': 'cone_clutch.n.01', 'name': 'cone_clutch'}, {'id': 6783, 'synset': 'confectionery.n.02', 'name': 'confectionery'}, {'id': 6784, 'synset': 'conference_center.n.01', 'name': 'conference_center'}, {'id': 6785, 'synset': 'conference_room.n.01', 'name': 'conference_room'}, {'id': 6786, 'synset': 'conference_table.n.01', 'name': 'conference_table'}, {'id': 6787, 'synset': 'confessional.n.01', 'name': 'confessional'}, {'id': 6788, 'synset': 'conformal_projection.n.01', 'name': 'conformal_projection'}, {'id': 6789, 'synset': 'congress_boot.n.01', 'name': 'congress_boot'}, {'id': 6790, 'synset': 'conic_projection.n.01', 'name': 'conic_projection'}, {'id': 6791, 'synset': 'connecting_rod.n.01', 'name': 'connecting_rod'}, {'id': 6792, 'synset': 'connecting_room.n.01', 'name': 'connecting_room'}, {'id': 6793, 'synset': 'connection.n.03', 'name': 'connection'}, {'id': 6794, 'synset': 'conning_tower.n.02', 'name': 'conning_tower'}, {'id': 6795, 'synset': 'conning_tower.n.01', 'name': 'conning_tower'}, {'id': 6796, 'synset': 'conservatory.n.03', 'name': 'conservatory'}, {'id': 6797, 'synset': 'conservatory.n.02', 'name': 'conservatory'}, {'id': 6798, 'synset': 'console.n.03', 'name': 'console'}, {'id': 6799, 'synset': 'console.n.02', 'name': 'console'}, {'id': 6800, 'synset': 'console_table.n.01', 'name': 'console_table'}, {'id': 6801, 'synset': 'consulate.n.01', 'name': 'consulate'}, {'id': 6802, 'synset': 'contact.n.07', 'name': 'contact'}, {'id': 6803, 'synset': 'contact.n.09', 'name': 'contact'}, {'id': 6804, 'synset': 'container.n.01', 'name': 'container'}, {'id': 6805, 'synset': 'container_ship.n.01', 'name': 'container_ship'}, {'id': 6806, 'synset': 'containment.n.02', 'name': 'containment'}, {'id': 6807, 'synset': 'contrabassoon.n.01', 'name': 'contrabassoon'}, {'id': 6808, 'synset': 'control_center.n.01', 'name': 'control_center'}, {'id': 6809, 'synset': 'control_circuit.n.01', 'name': 'control_circuit'}, {'id': 6810, 'synset': 'control_key.n.01', 'name': 'control_key'}, {'id': 6811, 'synset': 'control_panel.n.01', 'name': 'control_panel'}, {'id': 6812, 'synset': 'control_rod.n.01', 'name': 'control_rod'}, {'id': 6813, 'synset': 'control_room.n.01', 'name': 'control_room'}, {'id': 6814, 'synset': 'control_system.n.01', 'name': 'control_system'}, {'id': 6815, 'synset': 'control_tower.n.01', 'name': 'control_tower'}, {'id': 6816, 'synset': 'convector.n.01', 'name': 'convector'}, {'id': 6817, 'synset': 'convenience_store.n.01', 'name': 'convenience_store'}, {'id': 6818, 'synset': 'convent.n.01', 'name': 'convent'}, {'id': 6819, 'synset': 'conventicle.n.02', 'name': 'conventicle'}, {'id': 6820, 'synset': 'converging_lens.n.01', 'name': 'converging_lens'}, {'id': 6821, 'synset': 'converter.n.01', 'name': 'converter'}, {'id': 6822, 'synset': 'conveyance.n.03', 'name': 'conveyance'}, {'id': 6823, 'synset': 'conveyer_belt.n.01', 'name': 'conveyer_belt'}, {'id': 6824, 'synset': 'cookfire.n.01', 'name': 'cookfire'}, {'id': 6825, 'synset': 'cookhouse.n.02', 'name': 'cookhouse'}, {'id': 6826, 'synset': 'cookie_cutter.n.01', 'name': 'cookie_cutter'}, {'id': 6827, 'synset': 'cookie_jar.n.01', 'name': 'cookie_jar'}, {'id': 6828, 'synset': 'cookie_sheet.n.01', 'name': 'cookie_sheet'}, {'id': 6829, 'synset': 'cookstove.n.01', 'name': 'cookstove'}, {'id': 6830, 'synset': 'coolant_system.n.01', 'name': 'coolant_system'}, {'id': 6831, 'synset': 'cooling_system.n.02', 'name': 'cooling_system'}, {'id': 6832, 'synset': 'cooling_system.n.01', 'name': 'cooling_system'}, {'id': 6833, 'synset': 'cooling_tower.n.01', 'name': 'cooling_tower'}, {'id': 6834, 'synset': 'coonskin_cap.n.01', 'name': 'coonskin_cap'}, {'id': 6835, 'synset': 'cope.n.02', 'name': 'cope'}, {'id': 6836, 'synset': 'coping_saw.n.01', 'name': 'coping_saw'}, {'id': 6837, 'synset': 'copperware.n.01', 'name': 'copperware'}, {'id': 6838, 'synset': 'copyholder.n.01', 'name': 'copyholder'}, {'id': 6839, 'synset': 'coquille.n.02', 'name': 'coquille'}, {'id': 6840, 'synset': 'coracle.n.01', 'name': 'coracle'}, {'id': 6841, 'synset': 'corbel.n.01', 'name': 'corbel'}, {'id': 6842, 'synset': 'corbel_arch.n.01', 'name': 'corbel_arch'}, {'id': 6843, 'synset': 'corbel_step.n.01', 'name': 'corbel_step'}, {'id': 6844, 'synset': 'corbie_gable.n.01', 'name': 'corbie_gable'}, {'id': 6845, 'synset': 'cord.n.04', 'name': 'cord'}, {'id': 6846, 'synset': 'cord.n.03', 'name': 'cord'}, {'id': 6847, 'synset': 'cordage.n.02', 'name': 'cordage'}, {'id': 6848, 'synset': 'cords.n.01', 'name': 'cords'}, {'id': 6849, 'synset': 'core.n.10', 'name': 'core'}, {'id': 6850, 'synset': 'core_bit.n.01', 'name': 'core_bit'}, {'id': 6851, 'synset': 'core_drill.n.01', 'name': 'core_drill'}, {'id': 6852, 'synset': 'corer.n.01', 'name': 'corer'}, {'id': 6853, 'synset': 'corker.n.02', 'name': 'corker'}, {'id': 6854, 'synset': 'corncrib.n.01', 'name': 'corncrib'}, {'id': 6855, 'synset': 'corner.n.11', 'name': 'corner'}, {'id': 6856, 'synset': 'corner.n.03', 'name': 'corner'}, {'id': 6857, 'synset': 'corner_post.n.01', 'name': 'corner_post'}, {'id': 6858, 'synset': 'cornice.n.03', 'name': 'cornice'}, {'id': 6859, 'synset': 'cornice.n.02', 'name': 'cornice'}, {'id': 6860, 'synset': 'correctional_institution.n.01', 'name': 'correctional_institution'}, {'id': 6861, 'synset': 'corrugated_fastener.n.01', 'name': 'corrugated_fastener'}, {'id': 6862, 'synset': 'corselet.n.01', 'name': 'corselet'}, {'id': 6863, 'synset': 'cosmetic.n.01', 'name': 'cosmetic'}, {'id': 6864, 'synset': 'cosmotron.n.01', 'name': 'cosmotron'}, {'id': 6865, 'synset': 'costume.n.01', 'name': 'costume'}, {'id': 6866, 'synset': 'costume.n.02', 'name': 'costume'}, {'id': 6867, 'synset': 'costume.n.03', 'name': 'costume'}, {'id': 6868, 'synset': 'cosy.n.01', 'name': 'cosy'}, {'id': 6869, 'synset': 'cot.n.03', 'name': 'cot'}, {'id': 6870, 'synset': 'cottage_tent.n.01', 'name': 'cottage_tent'}, {'id': 6871, 'synset': 'cotter.n.03', 'name': 'cotter'}, {'id': 6872, 'synset': 'cotter_pin.n.01', 'name': 'cotter_pin'}, {'id': 6873, 'synset': 'cotton.n.02', 'name': 'cotton'}, {'id': 6874, 'synset': 'cotton_flannel.n.01', 'name': 'cotton_flannel'}, {'id': 6875, 'synset': 'cotton_mill.n.01', 'name': 'cotton_mill'}, {'id': 6876, 'synset': 'couch.n.03', 'name': 'couch'}, {'id': 6877, 'synset': 'couch.n.02', 'name': 'couch'}, {'id': 6878, 'synset': 'couchette.n.01', 'name': 'couchette'}, {'id': 6879, 'synset': 'coude_telescope.n.01', 'name': 'coude_telescope'}, {'id': 6880, 'synset': 'counter.n.01', 'name': 'counter'}, {'id': 6881, 'synset': 'counter.n.03', 'name': 'counter'}, {'id': 6882, 'synset': 'counter.n.02', 'name': 'counter'}, {'id': 6883, 'synset': 'counterbore.n.01', 'name': 'counterbore'}, {'id': 6884, 'synset': 'counter_tube.n.01', 'name': 'counter_tube'}, {'id': 6885, 'synset': 'country_house.n.01', 'name': 'country_house'}, {'id': 6886, 'synset': 'country_store.n.01', 'name': 'country_store'}, {'id': 6887, 'synset': 'coupe.n.01', 'name': 'coupe'}, {'id': 6888, 'synset': 'coupling.n.02', 'name': 'coupling'}, {'id': 6889, 'synset': 'court.n.10', 'name': 'court'}, {'id': 6890, 'synset': 'court.n.04', 'name': 'court'}, {'id': 6891, 'synset': 'court.n.02', 'name': 'court'}, {'id': 6892, 'synset': 'court.n.09', 'name': 'court'}, {'id': 6893, 'synset': 'courtelle.n.01', 'name': 'Courtelle'}, {'id': 6894, 'synset': 'courthouse.n.02', 'name': 'courthouse'}, {'id': 6895, 'synset': 'courthouse.n.01', 'name': 'courthouse'}, {'id': 6896, 'synset': 'covered_bridge.n.01', 'name': 'covered_bridge'}, {'id': 6897, 'synset': 'covered_couch.n.01', 'name': 'covered_couch'}, {'id': 6898, 'synset': 'covered_wagon.n.01', 'name': 'covered_wagon'}, {'id': 6899, 'synset': 'covering.n.02', 'name': 'covering'}, {'id': 6900, 'synset': 'coverlet.n.01', 'name': 'coverlet'}, {'id': 6901, 'synset': 'cover_plate.n.01', 'name': 'cover_plate'}, {'id': 6902, 'synset': 'cowbarn.n.01', 'name': 'cowbarn'}, {'id': 6903, 'synset': 'cowboy_boot.n.01', 'name': 'cowboy_boot'}, {'id': 6904, 'synset': 'cowhide.n.03', 'name': 'cowhide'}, {'id': 6905, 'synset': 'cowl.n.02', 'name': 'cowl'}, {'id': 6906, 'synset': 'cow_pen.n.01', 'name': 'cow_pen'}, {'id': 6907, 'synset': 'cpu_board.n.01', 'name': 'CPU_board'}, {'id': 6908, 'synset': 'crackle.n.02', 'name': 'crackle'}, {'id': 6909, 'synset': 'cradle.n.01', 'name': 'cradle'}, {'id': 6910, 'synset': 'craft.n.02', 'name': 'craft'}, {'id': 6911, 'synset': 'cramp.n.03', 'name': 'cramp'}, {'id': 6912, 'synset': 'crampon.n.02', 'name': 'crampon'}, {'id': 6913, 'synset': 'crampon.n.01', 'name': 'crampon'}, {'id': 6914, 'synset': 'crane.n.04', 'name': 'crane'}, {'id': 6915, 'synset': 'craniometer.n.01', 'name': 'craniometer'}, {'id': 6916, 'synset': 'crank.n.04', 'name': 'crank'}, {'id': 6917, 'synset': 'crankcase.n.01', 'name': 'crankcase'}, {'id': 6918, 'synset': 'crankshaft.n.01', 'name': 'crankshaft'}, {'id': 6919, 'synset': 'crash_barrier.n.01', 'name': 'crash_barrier'}, {'id': 6920, 'synset': 'crash_helmet.n.01', 'name': 'crash_helmet'}, {'id': 6921, 'synset': 'cravat.n.01', 'name': 'cravat'}, {'id': 6922, 'synset': 'crazy_quilt.n.01', 'name': 'crazy_quilt'}, {'id': 6923, 'synset': 'cream.n.03', 'name': 'cream'}, {'id': 6924, 'synset': 'creche.n.01', 'name': 'creche'}, {'id': 6925, 'synset': 'creche.n.02', 'name': 'creche'}, {'id': 6926, 'synset': 'credenza.n.01', 'name': 'credenza'}, {'id': 6927, 'synset': 'creel.n.01', 'name': 'creel'}, {'id': 6928, 'synset': 'crematory.n.02', 'name': 'crematory'}, {'id': 6929, 'synset': 'crematory.n.01', 'name': 'crematory'}, {'id': 6930, 'synset': 'crepe.n.03', 'name': 'crepe'}, {'id': 6931, 'synset': 'crepe_de_chine.n.01', 'name': 'crepe_de_Chine'}, {'id': 6932, 'synset': 'crescent_wrench.n.01', 'name': 'crescent_wrench'}, {'id': 6933, 'synset': 'cretonne.n.01', 'name': 'cretonne'}, {'id': 6934, 'synset': 'crib.n.03', 'name': 'crib'}, {'id': 6935, 'synset': 'cricket_ball.n.01', 'name': 'cricket_ball'}, {'id': 6936, 'synset': 'cricket_bat.n.01', 'name': 'cricket_bat'}, {'id': 6937, 'synset': 'cricket_equipment.n.01', 'name': 'cricket_equipment'}, {'id': 6938, 'synset': 'cringle.n.01', 'name': 'cringle'}, {'id': 6939, 'synset': 'crinoline.n.03', 'name': 'crinoline'}, {'id': 6940, 'synset': 'crinoline.n.02', 'name': 'crinoline'}, {'id': 6941, 'synset': 'crochet_needle.n.01', 'name': 'crochet_needle'}, {'id': 6942, 'synset': 'crock_pot.n.01', 'name': 'Crock_Pot'}, {'id': 6943, 'synset': 'crook.n.03', 'name': 'crook'}, {'id': 6944, 'synset': 'crookes_radiometer.n.01', 'name': 'Crookes_radiometer'}, {'id': 6945, 'synset': 'crookes_tube.n.01', 'name': 'Crookes_tube'}, {'id': 6946, 'synset': 'croquet_ball.n.01', 'name': 'croquet_ball'}, {'id': 6947, 'synset': 'croquet_equipment.n.01', 'name': 'croquet_equipment'}, {'id': 6948, 'synset': 'croquet_mallet.n.01', 'name': 'croquet_mallet'}, {'id': 6949, 'synset': 'cross.n.01', 'name': 'cross'}, {'id': 6950, 'synset': 'crossbar.n.03', 'name': 'crossbar'}, {'id': 6951, 'synset': 'crossbar.n.02', 'name': 'crossbar'}, {'id': 6952, 'synset': 'crossbench.n.01', 'name': 'crossbench'}, {'id': 6953, 'synset': 'cross_bit.n.01', 'name': 'cross_bit'}, {'id': 6954, 'synset': 'crossbow.n.01', 'name': 'crossbow'}, {'id': 6955, 'synset': 'crosscut_saw.n.01', 'name': 'crosscut_saw'}, {'id': 6956, 'synset': 'crossjack.n.01', 'name': 'crossjack'}, {'id': 6957, 'synset': 'crosspiece.n.02', 'name': 'crosspiece'}, {'id': 6958, 'synset': 'crotchet.n.04', 'name': 'crotchet'}, {'id': 6959, 'synset': "croupier's_rake.n.01", 'name': "croupier's_rake"}, {'id': 6960, 'synset': 'crown.n.11', 'name': 'crown'}, {'id': 6961, 'synset': 'crown_jewels.n.01', 'name': 'crown_jewels'}, {'id': 6962, 'synset': 'crown_lens.n.01', 'name': 'crown_lens'}, {'id': 6963, 'synset': "crow's_nest.n.01", 'name': "crow's_nest"}, {'id': 6964, 'synset': 'crucible.n.01', 'name': 'crucible'}, {'id': 6965, 'synset': 'cruet.n.01', 'name': 'cruet'}, {'id': 6966, 'synset': 'cruet-stand.n.01', 'name': 'cruet-stand'}, {'id': 6967, 'synset': 'cruise_control.n.01', 'name': 'cruise_control'}, {'id': 6968, 'synset': 'cruise_missile.n.01', 'name': 'cruise_missile'}, {'id': 6969, 'synset': 'cruiser.n.02', 'name': 'cruiser'}, {'id': 6970, 'synset': 'crupper.n.01', 'name': 'crupper'}, {'id': 6971, 'synset': 'cruse.n.01', 'name': 'cruse'}, {'id': 6972, 'synset': 'crusher.n.01', 'name': 'crusher'}, {'id': 6973, 'synset': 'cryometer.n.01', 'name': 'cryometer'}, {'id': 6974, 'synset': 'cryoscope.n.01', 'name': 'cryoscope'}, {'id': 6975, 'synset': 'cryostat.n.01', 'name': 'cryostat'}, {'id': 6976, 'synset': 'crypt.n.01', 'name': 'crypt'}, {'id': 6977, 'synset': 'crystal.n.06', 'name': 'crystal'}, {'id': 6978, 'synset': 'crystal_detector.n.01', 'name': 'crystal_detector'}, {'id': 6979, 'synset': 'crystal_microphone.n.01', 'name': 'crystal_microphone'}, {'id': 6980, 'synset': 'crystal_oscillator.n.01', 'name': 'crystal_oscillator'}, {'id': 6981, 'synset': 'crystal_set.n.01', 'name': 'crystal_set'}, {'id': 6982, 'synset': 'cubitiere.n.01', 'name': 'cubitiere'}, {'id': 6983, 'synset': 'cucking_stool.n.01', 'name': 'cucking_stool'}, {'id': 6984, 'synset': 'cuckoo_clock.n.01', 'name': 'cuckoo_clock'}, {'id': 6985, 'synset': 'cuddy.n.01', 'name': 'cuddy'}, {'id': 6986, 'synset': 'cudgel.n.01', 'name': 'cudgel'}, {'id': 6987, 'synset': 'cue.n.04', 'name': 'cue'}, {'id': 6988, 'synset': 'cue_ball.n.01', 'name': 'cue_ball'}, {'id': 6989, 'synset': 'cuff.n.01', 'name': 'cuff'}, {'id': 6990, 'synset': 'cuirass.n.01', 'name': 'cuirass'}, {'id': 6991, 'synset': 'cuisse.n.01', 'name': 'cuisse'}, {'id': 6992, 'synset': 'cul.n.01', 'name': 'cul'}, {'id': 6993, 'synset': 'culdoscope.n.01', 'name': 'culdoscope'}, {'id': 6994, 'synset': 'cullis.n.01', 'name': 'cullis'}, {'id': 6995, 'synset': 'culotte.n.01', 'name': 'culotte'}, {'id': 6996, 'synset': 'cultivator.n.02', 'name': 'cultivator'}, {'id': 6997, 'synset': 'culverin.n.02', 'name': 'culverin'}, {'id': 6998, 'synset': 'culverin.n.01', 'name': 'culverin'}, {'id': 6999, 'synset': 'culvert.n.01', 'name': 'culvert'}, {'id': 7000, 'synset': 'cup_hook.n.01', 'name': 'cup_hook'}, {'id': 7001, 'synset': 'cupola.n.02', 'name': 'cupola'}, {'id': 7002, 'synset': 'cupola.n.01', 'name': 'cupola'}, {'id': 7003, 'synset': 'curb.n.02', 'name': 'curb'}, {'id': 7004, 'synset': 'curb_roof.n.01', 'name': 'curb_roof'}, {'id': 7005, 'synset': 'curbstone.n.01', 'name': 'curbstone'}, {'id': 7006, 'synset': 'curette.n.01', 'name': 'curette'}, {'id': 7007, 'synset': 'currycomb.n.01', 'name': 'currycomb'}, {'id': 7008, 'synset': 'cursor.n.01', 'name': 'cursor'}, {'id': 7009, 'synset': 'customhouse.n.01', 'name': 'customhouse'}, {'id': 7010, 'synset': 'cutaway.n.01', 'name': 'cutaway'}, {'id': 7011, 'synset': 'cutlas.n.01', 'name': 'cutlas'}, {'id': 7012, 'synset': 'cutoff.n.03', 'name': 'cutoff'}, {'id': 7013, 'synset': 'cutout.n.01', 'name': 'cutout'}, {'id': 7014, 'synset': 'cutter.n.06', 'name': 'cutter'}, {'id': 7015, 'synset': 'cutter.n.05', 'name': 'cutter'}, {'id': 7016, 'synset': 'cutting_implement.n.01', 'name': 'cutting_implement'}, {'id': 7017, 'synset': 'cutting_room.n.01', 'name': 'cutting_room'}, {'id': 7018, 'synset': 'cutty_stool.n.01', 'name': 'cutty_stool'}, {'id': 7019, 'synset': 'cutwork.n.01', 'name': 'cutwork'}, {'id': 7020, 'synset': 'cybercafe.n.01', 'name': 'cybercafe'}, {'id': 7021, 'synset': 'cyclopean_masonry.n.01', 'name': 'cyclopean_masonry'}, {'id': 7022, 'synset': 'cyclostyle.n.01', 'name': 'cyclostyle'}, {'id': 7023, 'synset': 'cyclotron.n.01', 'name': 'cyclotron'}, {'id': 7024, 'synset': 'cylinder.n.03', 'name': 'cylinder'}, {'id': 7025, 'synset': 'cylinder_lock.n.01', 'name': 'cylinder_lock'}, {'id': 7026, 'synset': 'dacha.n.01', 'name': 'dacha'}, {'id': 7027, 'synset': 'dacron.n.01', 'name': 'Dacron'}, {'id': 7028, 'synset': 'dado.n.02', 'name': 'dado'}, {'id': 7029, 'synset': 'dado_plane.n.01', 'name': 'dado_plane'}, {'id': 7030, 'synset': 'dairy.n.01', 'name': 'dairy'}, {'id': 7031, 'synset': 'dais.n.01', 'name': 'dais'}, {'id': 7032, 'synset': 'daisy_print_wheel.n.01', 'name': 'daisy_print_wheel'}, {'id': 7033, 'synset': 'daisywheel_printer.n.01', 'name': 'daisywheel_printer'}, {'id': 7034, 'synset': 'dam.n.01', 'name': 'dam'}, {'id': 7035, 'synset': 'damask.n.02', 'name': 'damask'}, {'id': 7036, 'synset': 'dampener.n.01', 'name': 'dampener'}, {'id': 7037, 'synset': 'damper.n.02', 'name': 'damper'}, {'id': 7038, 'synset': 'damper_block.n.01', 'name': 'damper_block'}, {'id': 7039, 'synset': 'dark_lantern.n.01', 'name': 'dark_lantern'}, {'id': 7040, 'synset': 'darkroom.n.01', 'name': 'darkroom'}, {'id': 7041, 'synset': 'darning_needle.n.01', 'name': 'darning_needle'}, {'id': 7042, 'synset': 'dart.n.02', 'name': 'dart'}, {'id': 7043, 'synset': 'dart.n.01', 'name': 'dart'}, {'id': 7044, 'synset': 'dashboard.n.02', 'name': 'dashboard'}, {'id': 7045, 'synset': 'dashiki.n.01', 'name': 'dashiki'}, {'id': 7046, 'synset': 'dash-pot.n.01', 'name': 'dash-pot'}, {'id': 7047, 'synset': 'data_converter.n.01', 'name': 'data_converter'}, {'id': 7048, 'synset': 'data_input_device.n.01', 'name': 'data_input_device'}, {'id': 7049, 'synset': 'data_multiplexer.n.01', 'name': 'data_multiplexer'}, {'id': 7050, 'synset': 'data_system.n.01', 'name': 'data_system'}, {'id': 7051, 'synset': 'davenport.n.03', 'name': 'davenport'}, {'id': 7052, 'synset': 'davenport.n.02', 'name': 'davenport'}, {'id': 7053, 'synset': 'davit.n.01', 'name': 'davit'}, {'id': 7054, 'synset': 'daybed.n.01', 'name': 'daybed'}, {'id': 7055, 'synset': 'daybook.n.02', 'name': 'daybook'}, {'id': 7056, 'synset': 'day_nursery.n.01', 'name': 'day_nursery'}, {'id': 7057, 'synset': 'day_school.n.03', 'name': 'day_school'}, {'id': 7058, 'synset': 'dead_axle.n.01', 'name': 'dead_axle'}, {'id': 7059, 'synset': 'deadeye.n.02', 'name': 'deadeye'}, {'id': 7060, 'synset': 'deadhead.n.02', 'name': 'deadhead'}, {'id': 7061, 'synset': 'deanery.n.01', 'name': 'deanery'}, {'id': 7062, 'synset': 'deathbed.n.02', 'name': 'deathbed'}, {'id': 7063, 'synset': 'death_camp.n.01', 'name': 'death_camp'}, {'id': 7064, 'synset': 'death_house.n.01', 'name': 'death_house'}, {'id': 7065, 'synset': 'death_knell.n.02', 'name': 'death_knell'}, {'id': 7066, 'synset': 'death_seat.n.01', 'name': 'death_seat'}, {'id': 7067, 'synset': 'deck.n.02', 'name': 'deck'}, {'id': 7068, 'synset': 'deck.n.04', 'name': 'deck'}, {'id': 7069, 'synset': 'deck-house.n.01', 'name': 'deck-house'}, {'id': 7070, 'synset': 'deckle.n.02', 'name': 'deckle'}, {'id': 7071, 'synset': 'deckle_edge.n.01', 'name': 'deckle_edge'}, {'id': 7072, 'synset': 'declinometer.n.01', 'name': 'declinometer'}, {'id': 7073, 'synset': 'decoder.n.02', 'name': 'decoder'}, {'id': 7074, 'synset': 'decolletage.n.01', 'name': 'decolletage'}, {'id': 7075, 'synset': 'decoupage.n.01', 'name': 'decoupage'}, {'id': 7076, 'synset': 'dedicated_file_server.n.01', 'name': 'dedicated_file_server'}, {'id': 7077, 'synset': 'deep-freeze.n.01', 'name': 'deep-freeze'}, {'id': 7078, 'synset': 'deerstalker.n.01', 'name': 'deerstalker'}, {'id': 7079, 'synset': 'defense_system.n.01', 'name': 'defense_system'}, {'id': 7080, 'synset': 'defensive_structure.n.01', 'name': 'defensive_structure'}, {'id': 7081, 'synset': 'defibrillator.n.01', 'name': 'defibrillator'}, {'id': 7082, 'synset': 'defilade.n.01', 'name': 'defilade'}, {'id': 7083, 'synset': 'deflector.n.01', 'name': 'deflector'}, {'id': 7084, 'synset': 'delayed_action.n.01', 'name': 'delayed_action'}, {'id': 7085, 'synset': 'delay_line.n.01', 'name': 'delay_line'}, {'id': 7086, 'synset': 'delft.n.01', 'name': 'delft'}, {'id': 7087, 'synset': 'delicatessen.n.02', 'name': 'delicatessen'}, {'id': 7088, 'synset': 'delivery_truck.n.01', 'name': 'delivery_truck'}, {'id': 7089, 'synset': 'delta_wing.n.01', 'name': 'delta_wing'}, {'id': 7090, 'synset': 'demijohn.n.01', 'name': 'demijohn'}, {'id': 7091, 'synset': 'demitasse.n.02', 'name': 'demitasse'}, {'id': 7092, 'synset': 'den.n.04', 'name': 'den'}, {'id': 7093, 'synset': 'denim.n.02', 'name': 'denim'}, {'id': 7094, 'synset': 'densimeter.n.01', 'name': 'densimeter'}, {'id': 7095, 'synset': 'densitometer.n.01', 'name': 'densitometer'}, {'id': 7096, 'synset': 'dental_appliance.n.01', 'name': 'dental_appliance'}, {'id': 7097, 'synset': 'dental_implant.n.01', 'name': 'dental_implant'}, {'id': 7098, 'synset': "dentist's_drill.n.01", 'name': "dentist's_drill"}, {'id': 7099, 'synset': 'denture.n.01', 'name': 'denture'}, {'id': 7100, 'synset': 'deodorant.n.01', 'name': 'deodorant'}, {'id': 7101, 'synset': 'department_store.n.01', 'name': 'department_store'}, {'id': 7102, 'synset': 'departure_lounge.n.01', 'name': 'departure_lounge'}, {'id': 7103, 'synset': 'depilatory.n.02', 'name': 'depilatory'}, {'id': 7104, 'synset': 'depressor.n.03', 'name': 'depressor'}, {'id': 7105, 'synset': 'depth_finder.n.01', 'name': 'depth_finder'}, {'id': 7106, 'synset': 'depth_gauge.n.01', 'name': 'depth_gauge'}, {'id': 7107, 'synset': 'derrick.n.02', 'name': 'derrick'}, {'id': 7108, 'synset': 'derrick.n.01', 'name': 'derrick'}, {'id': 7109, 'synset': 'derringer.n.01', 'name': 'derringer'}, {'id': 7110, 'synset': 'desk_phone.n.01', 'name': 'desk_phone'}, {'id': 7111, 'synset': 'desktop_computer.n.01', 'name': 'desktop_computer'}, {'id': 7112, 'synset': 'dessert_spoon.n.01', 'name': 'dessert_spoon'}, {'id': 7113, 'synset': 'destroyer.n.01', 'name': 'destroyer'}, {'id': 7114, 'synset': 'destroyer_escort.n.01', 'name': 'destroyer_escort'}, {'id': 7115, 'synset': 'detached_house.n.01', 'name': 'detached_house'}, {'id': 7116, 'synset': 'detector.n.01', 'name': 'detector'}, {'id': 7117, 'synset': 'detector.n.03', 'name': 'detector'}, {'id': 7118, 'synset': 'detention_home.n.01', 'name': 'detention_home'}, {'id': 7119, 'synset': 'detonating_fuse.n.01', 'name': 'detonating_fuse'}, {'id': 7120, 'synset': 'detonator.n.01', 'name': 'detonator'}, {'id': 7121, 'synset': 'developer.n.02', 'name': 'developer'}, {'id': 7122, 'synset': 'device.n.01', 'name': 'device'}, {'id': 7123, 'synset': 'dewar_flask.n.01', 'name': 'Dewar_flask'}, {'id': 7124, 'synset': 'dhoti.n.01', 'name': 'dhoti'}, {'id': 7125, 'synset': 'dhow.n.01', 'name': 'dhow'}, {'id': 7126, 'synset': 'dial.n.04', 'name': 'dial'}, {'id': 7127, 'synset': 'dial.n.03', 'name': 'dial'}, {'id': 7128, 'synset': 'dial.n.02', 'name': 'dial'}, {'id': 7129, 'synset': 'dialog_box.n.01', 'name': 'dialog_box'}, {'id': 7130, 'synset': 'dial_telephone.n.01', 'name': 'dial_telephone'}, {'id': 7131, 'synset': 'dialyzer.n.01', 'name': 'dialyzer'}, {'id': 7132, 'synset': 'diamante.n.02', 'name': 'diamante'}, {'id': 7133, 'synset': 'diaper.n.02', 'name': 'diaper'}, {'id': 7134, 'synset': 'diaphone.n.01', 'name': 'diaphone'}, {'id': 7135, 'synset': 'diaphragm.n.01', 'name': 'diaphragm'}, {'id': 7136, 'synset': 'diaphragm.n.04', 'name': 'diaphragm'}, {'id': 7137, 'synset': 'diathermy_machine.n.01', 'name': 'diathermy_machine'}, {'id': 7138, 'synset': 'dibble.n.01', 'name': 'dibble'}, {'id': 7139, 'synset': 'dice_cup.n.01', 'name': 'dice_cup'}, {'id': 7140, 'synset': 'dicer.n.01', 'name': 'dicer'}, {'id': 7141, 'synset': 'dickey.n.02', 'name': 'dickey'}, {'id': 7142, 'synset': 'dickey.n.01', 'name': 'dickey'}, {'id': 7143, 'synset': 'dictaphone.n.01', 'name': 'Dictaphone'}, {'id': 7144, 'synset': 'die.n.03', 'name': 'die'}, {'id': 7145, 'synset': 'diesel.n.02', 'name': 'diesel'}, {'id': 7146, 'synset': 'diesel-electric_locomotive.n.01', 'name': 'diesel-electric_locomotive'}, {'id': 7147, 'synset': 'diesel-hydraulic_locomotive.n.01', 'name': 'diesel-hydraulic_locomotive'}, {'id': 7148, 'synset': 'diesel_locomotive.n.01', 'name': 'diesel_locomotive'}, {'id': 7149, 'synset': 'diestock.n.01', 'name': 'diestock'}, {'id': 7150, 'synset': 'differential_analyzer.n.01', 'name': 'differential_analyzer'}, {'id': 7151, 'synset': 'differential_gear.n.01', 'name': 'differential_gear'}, {'id': 7152, 'synset': 'diffuser.n.02', 'name': 'diffuser'}, {'id': 7153, 'synset': 'diffuser.n.01', 'name': 'diffuser'}, {'id': 7154, 'synset': 'digester.n.01', 'name': 'digester'}, {'id': 7155, 'synset': 'diggings.n.02', 'name': 'diggings'}, {'id': 7156, 'synset': 'digital-analog_converter.n.01', 'name': 'digital-analog_converter'}, {'id': 7157, 'synset': 'digital_audiotape.n.01', 'name': 'digital_audiotape'}, {'id': 7158, 'synset': 'digital_camera.n.01', 'name': 'digital_camera'}, {'id': 7159, 'synset': 'digital_clock.n.01', 'name': 'digital_clock'}, {'id': 7160, 'synset': 'digital_computer.n.01', 'name': 'digital_computer'}, {'id': 7161, 'synset': 'digital_display.n.01', 'name': 'digital_display'}, {'id': 7162, 'synset': 'digital_subscriber_line.n.01', 'name': 'digital_subscriber_line'}, {'id': 7163, 'synset': 'digital_voltmeter.n.01', 'name': 'digital_voltmeter'}, {'id': 7164, 'synset': 'digital_watch.n.01', 'name': 'digital_watch'}, {'id': 7165, 'synset': 'digitizer.n.01', 'name': 'digitizer'}, {'id': 7166, 'synset': 'dilator.n.03', 'name': 'dilator'}, {'id': 7167, 'synset': 'dildo.n.01', 'name': 'dildo'}, {'id': 7168, 'synset': 'dimity.n.01', 'name': 'dimity'}, {'id': 7169, 'synset': 'dimmer.n.01', 'name': 'dimmer'}, {'id': 7170, 'synset': 'diner.n.03', 'name': 'diner'}, {'id': 7171, 'synset': 'dinette.n.01', 'name': 'dinette'}, {'id': 7172, 'synset': 'dining_area.n.01', 'name': 'dining_area'}, {'id': 7173, 'synset': 'dining_car.n.01', 'name': 'dining_car'}, {'id': 7174, 'synset': 'dining-hall.n.01', 'name': 'dining-hall'}, {'id': 7175, 'synset': 'dining_room.n.01', 'name': 'dining_room'}, {'id': 7176, 'synset': 'dining-room_furniture.n.01', 'name': 'dining-room_furniture'}, {'id': 7177, 'synset': 'dining-room_table.n.01', 'name': 'dining-room_table'}, {'id': 7178, 'synset': 'dinner_bell.n.01', 'name': 'dinner_bell'}, {'id': 7179, 'synset': 'dinner_dress.n.01', 'name': 'dinner_dress'}, {'id': 7180, 'synset': 'dinner_napkin.n.01', 'name': 'dinner_napkin'}, {'id': 7181, 'synset': 'dinner_pail.n.01', 'name': 'dinner_pail'}, {'id': 7182, 'synset': 'dinner_table.n.01', 'name': 'dinner_table'}, {'id': 7183, 'synset': 'dinner_theater.n.01', 'name': 'dinner_theater'}, {'id': 7184, 'synset': 'diode.n.02', 'name': 'diode'}, {'id': 7185, 'synset': 'diode.n.01', 'name': 'diode'}, {'id': 7186, 'synset': 'dip.n.07', 'name': 'dip'}, {'id': 7187, 'synset': 'diplomatic_building.n.01', 'name': 'diplomatic_building'}, {'id': 7188, 'synset': 'dipole.n.02', 'name': 'dipole'}, {'id': 7189, 'synset': 'dipper.n.01', 'name': 'dipper'}, {'id': 7190, 'synset': 'dipstick.n.01', 'name': 'dipstick'}, {'id': 7191, 'synset': 'dip_switch.n.01', 'name': 'DIP_switch'}, {'id': 7192, 'synset': 'directional_antenna.n.01', 'name': 'directional_antenna'}, {'id': 7193, 'synset': 'directional_microphone.n.01', 'name': 'directional_microphone'}, {'id': 7194, 'synset': 'direction_finder.n.01', 'name': 'direction_finder'}, {'id': 7195, 'synset': 'dirk.n.01', 'name': 'dirk'}, {'id': 7196, 'synset': 'dirndl.n.02', 'name': 'dirndl'}, {'id': 7197, 'synset': 'dirndl.n.01', 'name': 'dirndl'}, {'id': 7198, 'synset': 'dirty_bomb.n.01', 'name': 'dirty_bomb'}, {'id': 7199, 'synset': 'discharge_lamp.n.01', 'name': 'discharge_lamp'}, {'id': 7200, 'synset': 'discharge_pipe.n.01', 'name': 'discharge_pipe'}, {'id': 7201, 'synset': 'disco.n.02', 'name': 'disco'}, {'id': 7202, 'synset': 'discount_house.n.01', 'name': 'discount_house'}, {'id': 7203, 'synset': 'discus.n.02', 'name': 'discus'}, {'id': 7204, 'synset': 'disguise.n.02', 'name': 'disguise'}, {'id': 7205, 'synset': 'dishpan.n.01', 'name': 'dishpan'}, {'id': 7206, 'synset': 'dish_rack.n.01', 'name': 'dish_rack'}, {'id': 7207, 'synset': 'disk.n.02', 'name': 'disk'}, {'id': 7208, 'synset': 'disk_brake.n.01', 'name': 'disk_brake'}, {'id': 7209, 'synset': 'disk_clutch.n.01', 'name': 'disk_clutch'}, {'id': 7210, 'synset': 'disk_controller.n.01', 'name': 'disk_controller'}, {'id': 7211, 'synset': 'disk_drive.n.01', 'name': 'disk_drive'}, {'id': 7212, 'synset': 'diskette.n.01', 'name': 'diskette'}, {'id': 7213, 'synset': 'disk_harrow.n.01', 'name': 'disk_harrow'}, {'id': 7214, 'synset': 'dispatch_case.n.01', 'name': 'dispatch_case'}, {'id': 7215, 'synset': 'dispensary.n.01', 'name': 'dispensary'}, {'id': 7216, 'synset': 'display.n.06', 'name': 'display'}, {'id': 7217, 'synset': 'display_adapter.n.01', 'name': 'display_adapter'}, {'id': 7218, 'synset': 'display_panel.n.01', 'name': 'display_panel'}, {'id': 7219, 'synset': 'display_window.n.01', 'name': 'display_window'}, {'id': 7220, 'synset': 'disposal.n.04', 'name': 'disposal'}, {'id': 7221, 'synset': 'disrupting_explosive.n.01', 'name': 'disrupting_explosive'}, {'id': 7222, 'synset': 'distaff.n.02', 'name': 'distaff'}, {'id': 7223, 'synset': 'distillery.n.01', 'name': 'distillery'}, {'id': 7224, 'synset': 'distributor.n.04', 'name': 'distributor'}, {'id': 7225, 'synset': 'distributor_cam.n.01', 'name': 'distributor_cam'}, {'id': 7226, 'synset': 'distributor_cap.n.01', 'name': 'distributor_cap'}, {'id': 7227, 'synset': 'distributor_housing.n.01', 'name': 'distributor_housing'}, {'id': 7228, 'synset': 'distributor_point.n.01', 'name': 'distributor_point'}, {'id': 7229, 'synset': 'ditch.n.01', 'name': 'ditch'}, {'id': 7230, 'synset': 'ditch_spade.n.01', 'name': 'ditch_spade'}, {'id': 7231, 'synset': 'ditty_bag.n.01', 'name': 'ditty_bag'}, {'id': 7232, 'synset': 'divan.n.01', 'name': 'divan'}, {'id': 7233, 'synset': 'divan.n.04', 'name': 'divan'}, {'id': 7234, 'synset': 'dive_bomber.n.01', 'name': 'dive_bomber'}, {'id': 7235, 'synset': 'diverging_lens.n.01', 'name': 'diverging_lens'}, {'id': 7236, 'synset': 'divided_highway.n.01', 'name': 'divided_highway'}, {'id': 7237, 'synset': 'divider.n.04', 'name': 'divider'}, {'id': 7238, 'synset': 'diving_bell.n.01', 'name': 'diving_bell'}, {'id': 7239, 'synset': 'divining_rod.n.01', 'name': 'divining_rod'}, {'id': 7240, 'synset': 'diving_suit.n.01', 'name': 'diving_suit'}, {'id': 7241, 'synset': 'dixie.n.02', 'name': 'dixie'}, {'id': 7242, 'synset': 'dock.n.05', 'name': 'dock'}, {'id': 7243, 'synset': 'doeskin.n.02', 'name': 'doeskin'}, {'id': 7244, 'synset': 'dogcart.n.01', 'name': 'dogcart'}, {'id': 7245, 'synset': 'doggie_bag.n.01', 'name': 'doggie_bag'}, {'id': 7246, 'synset': 'dogsled.n.01', 'name': 'dogsled'}, {'id': 7247, 'synset': 'dog_wrench.n.01', 'name': 'dog_wrench'}, {'id': 7248, 'synset': 'doily.n.01', 'name': 'doily'}, {'id': 7249, 'synset': 'dolly.n.02', 'name': 'dolly'}, {'id': 7250, 'synset': 'dolman.n.02', 'name': 'dolman'}, {'id': 7251, 'synset': 'dolman.n.01', 'name': 'dolman'}, {'id': 7252, 'synset': 'dolman_sleeve.n.01', 'name': 'dolman_sleeve'}, {'id': 7253, 'synset': 'dolmen.n.01', 'name': 'dolmen'}, {'id': 7254, 'synset': 'dome.n.04', 'name': 'dome'}, {'id': 7255, 'synset': 'dome.n.03', 'name': 'dome'}, {'id': 7256, 'synset': 'domino.n.03', 'name': 'domino'}, {'id': 7257, 'synset': 'dongle.n.01', 'name': 'dongle'}, {'id': 7258, 'synset': 'donkey_jacket.n.01', 'name': 'donkey_jacket'}, {'id': 7259, 'synset': 'door.n.01', 'name': 'door'}, {'id': 7260, 'synset': 'door.n.05', 'name': 'door'}, {'id': 7261, 'synset': 'door.n.04', 'name': 'door'}, {'id': 7262, 'synset': 'doorbell.n.01', 'name': 'doorbell'}, {'id': 7263, 'synset': 'doorframe.n.01', 'name': 'doorframe'}, {'id': 7264, 'synset': 'doorjamb.n.01', 'name': 'doorjamb'}, {'id': 7265, 'synset': 'doorlock.n.01', 'name': 'doorlock'}, {'id': 7266, 'synset': 'doornail.n.01', 'name': 'doornail'}, {'id': 7267, 'synset': 'doorplate.n.01', 'name': 'doorplate'}, {'id': 7268, 'synset': 'doorsill.n.01', 'name': 'doorsill'}, {'id': 7269, 'synset': 'doorstop.n.01', 'name': 'doorstop'}, {'id': 7270, 'synset': 'doppler_radar.n.01', 'name': 'Doppler_radar'}, {'id': 7271, 'synset': 'dormer.n.01', 'name': 'dormer'}, {'id': 7272, 'synset': 'dormer_window.n.01', 'name': 'dormer_window'}, {'id': 7273, 'synset': 'dormitory.n.01', 'name': 'dormitory'}, {'id': 7274, 'synset': 'dormitory.n.02', 'name': 'dormitory'}, {'id': 7275, 'synset': 'dosemeter.n.01', 'name': 'dosemeter'}, {'id': 7276, 'synset': 'dossal.n.01', 'name': 'dossal'}, {'id': 7277, 'synset': 'dot_matrix_printer.n.01', 'name': 'dot_matrix_printer'}, {'id': 7278, 'synset': 'double_bed.n.01', 'name': 'double_bed'}, {'id': 7279, 'synset': 'double-bitted_ax.n.01', 'name': 'double-bitted_ax'}, {'id': 7280, 'synset': 'double_boiler.n.01', 'name': 'double_boiler'}, {'id': 7281, 'synset': 'double-breasted_jacket.n.01', 'name': 'double-breasted_jacket'}, {'id': 7282, 'synset': 'double-breasted_suit.n.01', 'name': 'double-breasted_suit'}, {'id': 7283, 'synset': 'double_door.n.01', 'name': 'double_door'}, {'id': 7284, 'synset': 'double_glazing.n.01', 'name': 'double_glazing'}, {'id': 7285, 'synset': 'double-hung_window.n.01', 'name': 'double-hung_window'}, {'id': 7286, 'synset': 'double_knit.n.01', 'name': 'double_knit'}, {'id': 7287, 'synset': 'doubler.n.01', 'name': 'doubler'}, {'id': 7288, 'synset': 'double_reed.n.02', 'name': 'double_reed'}, {'id': 7289, 'synset': 'double-reed_instrument.n.01', 'name': 'double-reed_instrument'}, {'id': 7290, 'synset': 'doublet.n.01', 'name': 'doublet'}, {'id': 7291, 'synset': 'doubletree.n.01', 'name': 'doubletree'}, {'id': 7292, 'synset': 'douche.n.01', 'name': 'douche'}, {'id': 7293, 'synset': 'dovecote.n.01', 'name': 'dovecote'}, {'id': 7294, 'synset': "dover's_powder.n.01", 'name': "Dover's_powder"}, {'id': 7295, 'synset': 'dovetail.n.01', 'name': 'dovetail'}, {'id': 7296, 'synset': 'dovetail_plane.n.01', 'name': 'dovetail_plane'}, {'id': 7297, 'synset': 'dowel.n.01', 'name': 'dowel'}, {'id': 7298, 'synset': 'downstage.n.01', 'name': 'downstage'}, {'id': 7299, 'synset': 'drafting_instrument.n.01', 'name': 'drafting_instrument'}, {'id': 7300, 'synset': 'drafting_table.n.01', 'name': 'drafting_table'}, {'id': 7301, 'synset': 'dragunov.n.01', 'name': 'Dragunov'}, {'id': 7302, 'synset': 'drainage_ditch.n.01', 'name': 'drainage_ditch'}, {'id': 7303, 'synset': 'drainage_system.n.01', 'name': 'drainage_system'}, {'id': 7304, 'synset': 'drain_basket.n.01', 'name': 'drain_basket'}, {'id': 7305, 'synset': 'drainplug.n.01', 'name': 'drainplug'}, {'id': 7306, 'synset': 'drape.n.03', 'name': 'drape'}, {'id': 7307, 'synset': 'drapery.n.02', 'name': 'drapery'}, {'id': 7308, 'synset': 'drawbar.n.01', 'name': 'drawbar'}, {'id': 7309, 'synset': 'drawbridge.n.01', 'name': 'drawbridge'}, {'id': 7310, 'synset': 'drawing_chalk.n.01', 'name': 'drawing_chalk'}, {'id': 7311, 'synset': 'drawing_room.n.01', 'name': 'drawing_room'}, {'id': 7312, 'synset': 'drawing_room.n.02', 'name': 'drawing_room'}, {'id': 7313, 'synset': 'drawknife.n.01', 'name': 'drawknife'}, {'id': 7314, 'synset': 'drawstring_bag.n.01', 'name': 'drawstring_bag'}, {'id': 7315, 'synset': 'dray.n.01', 'name': 'dray'}, {'id': 7316, 'synset': 'dreadnought.n.01', 'name': 'dreadnought'}, {'id': 7317, 'synset': 'dredge.n.01', 'name': 'dredge'}, {'id': 7318, 'synset': 'dredger.n.01', 'name': 'dredger'}, {'id': 7319, 'synset': 'dredging_bucket.n.01', 'name': 'dredging_bucket'}, {'id': 7320, 'synset': 'dress_blues.n.01', 'name': 'dress_blues'}, {'id': 7321, 'synset': 'dressing.n.04', 'name': 'dressing'}, {'id': 7322, 'synset': 'dressing_case.n.01', 'name': 'dressing_case'}, {'id': 7323, 'synset': 'dressing_gown.n.01', 'name': 'dressing_gown'}, {'id': 7324, 'synset': 'dressing_room.n.01', 'name': 'dressing_room'}, {'id': 7325, 'synset': 'dressing_sack.n.01', 'name': 'dressing_sack'}, {'id': 7326, 'synset': 'dressing_table.n.01', 'name': 'dressing_table'}, {'id': 7327, 'synset': 'dress_rack.n.01', 'name': 'dress_rack'}, {'id': 7328, 'synset': 'dress_shirt.n.01', 'name': 'dress_shirt'}, {'id': 7329, 'synset': 'dress_uniform.n.01', 'name': 'dress_uniform'}, {'id': 7330, 'synset': 'drift_net.n.01', 'name': 'drift_net'}, {'id': 7331, 'synset': 'electric_drill.n.01', 'name': 'electric_drill'}, {'id': 7332, 'synset': 'drilling_platform.n.01', 'name': 'drilling_platform'}, {'id': 7333, 'synset': 'drill_press.n.01', 'name': 'drill_press'}, {'id': 7334, 'synset': 'drill_rig.n.01', 'name': 'drill_rig'}, {'id': 7335, 'synset': 'drinking_fountain.n.01', 'name': 'drinking_fountain'}, {'id': 7336, 'synset': 'drinking_vessel.n.01', 'name': 'drinking_vessel'}, {'id': 7337, 'synset': 'drip_loop.n.01', 'name': 'drip_loop'}, {'id': 7338, 'synset': 'drip_mat.n.01', 'name': 'drip_mat'}, {'id': 7339, 'synset': 'drip_pan.n.02', 'name': 'drip_pan'}, {'id': 7340, 'synset': 'dripping_pan.n.01', 'name': 'dripping_pan'}, {'id': 7341, 'synset': 'drip_pot.n.01', 'name': 'drip_pot'}, {'id': 7342, 'synset': 'drive.n.02', 'name': 'drive'}, {'id': 7343, 'synset': 'drive.n.10', 'name': 'drive'}, {'id': 7344, 'synset': 'drive_line.n.01', 'name': 'drive_line'}, {'id': 7345, 'synset': 'driver.n.05', 'name': 'driver'}, {'id': 7346, 'synset': 'driveshaft.n.01', 'name': 'driveshaft'}, {'id': 7347, 'synset': 'driveway.n.01', 'name': 'driveway'}, {'id': 7348, 'synset': 'driving_iron.n.01', 'name': 'driving_iron'}, {'id': 7349, 'synset': 'driving_wheel.n.01', 'name': 'driving_wheel'}, {'id': 7350, 'synset': 'drogue.n.04', 'name': 'drogue'}, {'id': 7351, 'synset': 'drogue_parachute.n.01', 'name': 'drogue_parachute'}, {'id': 7352, 'synset': 'drone.n.05', 'name': 'drone'}, {'id': 7353, 'synset': 'drop_arch.n.01', 'name': 'drop_arch'}, {'id': 7354, 'synset': 'drop_cloth.n.02', 'name': 'drop_cloth'}, {'id': 7355, 'synset': 'drop_curtain.n.01', 'name': 'drop_curtain'}, {'id': 7356, 'synset': 'drop_forge.n.01', 'name': 'drop_forge'}, {'id': 7357, 'synset': 'drop-leaf_table.n.01', 'name': 'drop-leaf_table'}, {'id': 7358, 'synset': 'droshky.n.01', 'name': 'droshky'}, {'id': 7359, 'synset': 'drove.n.03', 'name': 'drove'}, {'id': 7360, 'synset': 'drugget.n.01', 'name': 'drugget'}, {'id': 7361, 'synset': 'drugstore.n.01', 'name': 'drugstore'}, {'id': 7362, 'synset': 'drum.n.04', 'name': 'drum'}, {'id': 7363, 'synset': 'drum_brake.n.01', 'name': 'drum_brake'}, {'id': 7364, 'synset': 'drumhead.n.01', 'name': 'drumhead'}, {'id': 7365, 'synset': 'drum_printer.n.01', 'name': 'drum_printer'}, {'id': 7366, 'synset': 'drum_sander.n.01', 'name': 'drum_sander'}, {'id': 7367, 'synset': 'dry_battery.n.01', 'name': 'dry_battery'}, {'id': 7368, 'synset': 'dry-bulb_thermometer.n.01', 'name': 'dry-bulb_thermometer'}, {'id': 7369, 'synset': 'dry_cell.n.01', 'name': 'dry_cell'}, {'id': 7370, 'synset': 'dry_dock.n.01', 'name': 'dry_dock'}, {'id': 7371, 'synset': 'dryer.n.01', 'name': 'dryer'}, {'id': 7372, 'synset': 'dry_fly.n.01', 'name': 'dry_fly'}, {'id': 7373, 'synset': 'dry_kiln.n.01', 'name': 'dry_kiln'}, {'id': 7374, 'synset': 'dry_masonry.n.01', 'name': 'dry_masonry'}, {'id': 7375, 'synset': 'dry_point.n.02', 'name': 'dry_point'}, {'id': 7376, 'synset': 'dry_wall.n.02', 'name': 'dry_wall'}, {'id': 7377, 'synset': 'dual_scan_display.n.01', 'name': 'dual_scan_display'}, {'id': 7378, 'synset': 'duck.n.04', 'name': 'duck'}, {'id': 7379, 'synset': 'duckboard.n.01', 'name': 'duckboard'}, {'id': 7380, 'synset': 'duckpin.n.01', 'name': 'duckpin'}, {'id': 7381, 'synset': 'dudeen.n.01', 'name': 'dudeen'}, {'id': 7382, 'synset': 'duffel.n.02', 'name': 'duffel'}, {'id': 7383, 'synset': 'duffel_coat.n.01', 'name': 'duffel_coat'}, {'id': 7384, 'synset': 'dugout.n.01', 'name': 'dugout'}, {'id': 7385, 'synset': 'dugout_canoe.n.01', 'name': 'dugout_canoe'}, {'id': 7386, 'synset': 'dulciana.n.01', 'name': 'dulciana'}, {'id': 7387, 'synset': 'dulcimer.n.02', 'name': 'dulcimer'}, {'id': 7388, 'synset': 'dulcimer.n.01', 'name': 'dulcimer'}, {'id': 7389, 'synset': 'dumb_bomb.n.01', 'name': 'dumb_bomb'}, {'id': 7390, 'synset': 'dumbwaiter.n.01', 'name': 'dumbwaiter'}, {'id': 7391, 'synset': 'dumdum.n.01', 'name': 'dumdum'}, {'id': 7392, 'synset': 'dumpcart.n.01', 'name': 'dumpcart'}, {'id': 7393, 'synset': 'dump_truck.n.01', 'name': 'dump_truck'}, {'id': 7394, 'synset': 'dumpy_level.n.01', 'name': 'Dumpy_level'}, {'id': 7395, 'synset': 'dunce_cap.n.01', 'name': 'dunce_cap'}, {'id': 7396, 'synset': 'dune_buggy.n.01', 'name': 'dune_buggy'}, {'id': 7397, 'synset': 'dungeon.n.02', 'name': 'dungeon'}, {'id': 7398, 'synset': 'duplex_apartment.n.01', 'name': 'duplex_apartment'}, {'id': 7399, 'synset': 'duplex_house.n.01', 'name': 'duplex_house'}, {'id': 7400, 'synset': 'duplicator.n.01', 'name': 'duplicator'}, {'id': 7401, 'synset': 'dust_bag.n.01', 'name': 'dust_bag'}, {'id': 7402, 'synset': 'dustcloth.n.01', 'name': 'dustcloth'}, {'id': 7403, 'synset': 'dust_cover.n.03', 'name': 'dust_cover'}, {'id': 7404, 'synset': 'dust_cover.n.02', 'name': 'dust_cover'}, {'id': 7405, 'synset': 'dustmop.n.01', 'name': 'dustmop'}, {'id': 7406, 'synset': 'dutch_oven.n.01', 'name': 'Dutch_oven'}, {'id': 7407, 'synset': 'dutch_oven.n.02', 'name': 'Dutch_oven'}, {'id': 7408, 'synset': 'dwelling.n.01', 'name': 'dwelling'}, {'id': 7409, 'synset': 'dye-works.n.01', 'name': 'dye-works'}, {'id': 7410, 'synset': 'dynamo.n.01', 'name': 'dynamo'}, {'id': 7411, 'synset': 'dynamometer.n.01', 'name': 'dynamometer'}, {'id': 7412, 'synset': 'eames_chair.n.01', 'name': 'Eames_chair'}, {'id': 7413, 'synset': 'earflap.n.01', 'name': 'earflap'}, {'id': 7414, 'synset': 'early_warning_radar.n.01', 'name': 'early_warning_radar'}, {'id': 7415, 'synset': 'early_warning_system.n.01', 'name': 'early_warning_system'}, {'id': 7416, 'synset': 'earmuff.n.01', 'name': 'earmuff'}, {'id': 7417, 'synset': 'earplug.n.02', 'name': 'earplug'}, {'id': 7418, 'synset': 'earthenware.n.01', 'name': 'earthenware'}, {'id': 7419, 'synset': 'earthwork.n.01', 'name': 'earthwork'}, {'id': 7420, 'synset': 'easy_chair.n.01', 'name': 'easy_chair'}, {'id': 7421, 'synset': 'eaves.n.01', 'name': 'eaves'}, {'id': 7422, 'synset': 'ecclesiastical_attire.n.01', 'name': 'ecclesiastical_attire'}, {'id': 7423, 'synset': 'echinus.n.01', 'name': 'echinus'}, {'id': 7424, 'synset': 'echocardiograph.n.01', 'name': 'echocardiograph'}, {'id': 7425, 'synset': 'edger.n.02', 'name': 'edger'}, {'id': 7426, 'synset': 'edge_tool.n.01', 'name': 'edge_tool'}, {'id': 7427, 'synset': 'efficiency_apartment.n.01', 'name': 'efficiency_apartment'}, {'id': 7428, 'synset': 'egg-and-dart.n.01', 'name': 'egg-and-dart'}, {'id': 7429, 'synset': 'egg_timer.n.01', 'name': 'egg_timer'}, {'id': 7430, 'synset': 'eiderdown.n.01', 'name': 'eiderdown'}, {'id': 7431, 'synset': 'eight_ball.n.01', 'name': 'eight_ball'}, {'id': 7432, 'synset': 'ejection_seat.n.01', 'name': 'ejection_seat'}, {'id': 7433, 'synset': 'elastic.n.02', 'name': 'elastic'}, {'id': 7434, 'synset': 'elastic_bandage.n.01', 'name': 'elastic_bandage'}, {'id': 7435, 'synset': 'elastoplast.n.01', 'name': 'Elastoplast'}, {'id': 7436, 'synset': 'elbow.n.04', 'name': 'elbow'}, {'id': 7437, 'synset': 'elbow_pad.n.01', 'name': 'elbow_pad'}, {'id': 7438, 'synset': 'electric.n.01', 'name': 'electric'}, {'id': 7439, 'synset': 'electrical_cable.n.01', 'name': 'electrical_cable'}, {'id': 7440, 'synset': 'electrical_contact.n.01', 'name': 'electrical_contact'}, {'id': 7441, 'synset': 'electrical_converter.n.01', 'name': 'electrical_converter'}, {'id': 7442, 'synset': 'electrical_device.n.01', 'name': 'electrical_device'}, {'id': 7443, 'synset': 'electrical_system.n.02', 'name': 'electrical_system'}, {'id': 7444, 'synset': 'electric_bell.n.01', 'name': 'electric_bell'}, {'id': 7445, 'synset': 'electric_blanket.n.01', 'name': 'electric_blanket'}, {'id': 7446, 'synset': 'electric_clock.n.01', 'name': 'electric_clock'}, {'id': 7447, 'synset': 'electric-discharge_lamp.n.01', 'name': 'electric-discharge_lamp'}, {'id': 7448, 'synset': 'electric_fan.n.01', 'name': 'electric_fan'}, {'id': 7449, 'synset': 'electric_frying_pan.n.01', 'name': 'electric_frying_pan'}, {'id': 7450, 'synset': 'electric_furnace.n.01', 'name': 'electric_furnace'}, {'id': 7451, 'synset': 'electric_guitar.n.01', 'name': 'electric_guitar'}, {'id': 7452, 'synset': 'electric_hammer.n.01', 'name': 'electric_hammer'}, {'id': 7453, 'synset': 'electric_heater.n.01', 'name': 'electric_heater'}, {'id': 7454, 'synset': 'electric_lamp.n.01', 'name': 'electric_lamp'}, {'id': 7455, 'synset': 'electric_locomotive.n.01', 'name': 'electric_locomotive'}, {'id': 7456, 'synset': 'electric_meter.n.01', 'name': 'electric_meter'}, {'id': 7457, 'synset': 'electric_mixer.n.01', 'name': 'electric_mixer'}, {'id': 7458, 'synset': 'electric_motor.n.01', 'name': 'electric_motor'}, {'id': 7459, 'synset': 'electric_organ.n.01', 'name': 'electric_organ'}, {'id': 7460, 'synset': 'electric_range.n.01', 'name': 'electric_range'}, {'id': 7461, 'synset': 'electric_toothbrush.n.01', 'name': 'electric_toothbrush'}, {'id': 7462, 'synset': 'electric_typewriter.n.01', 'name': 'electric_typewriter'}, {'id': 7463, 'synset': 'electro-acoustic_transducer.n.01', 'name': 'electro-acoustic_transducer'}, {'id': 7464, 'synset': 'electrode.n.01', 'name': 'electrode'}, {'id': 7465, 'synset': 'electrodynamometer.n.01', 'name': 'electrodynamometer'}, {'id': 7466, 'synset': 'electroencephalograph.n.01', 'name': 'electroencephalograph'}, {'id': 7467, 'synset': 'electrograph.n.01', 'name': 'electrograph'}, {'id': 7468, 'synset': 'electrolytic.n.01', 'name': 'electrolytic'}, {'id': 7469, 'synset': 'electrolytic_cell.n.01', 'name': 'electrolytic_cell'}, {'id': 7470, 'synset': 'electromagnet.n.01', 'name': 'electromagnet'}, {'id': 7471, 'synset': 'electrometer.n.01', 'name': 'electrometer'}, {'id': 7472, 'synset': 'electromyograph.n.01', 'name': 'electromyograph'}, {'id': 7473, 'synset': 'electron_accelerator.n.01', 'name': 'electron_accelerator'}, {'id': 7474, 'synset': 'electron_gun.n.01', 'name': 'electron_gun'}, {'id': 7475, 'synset': 'electronic_balance.n.01', 'name': 'electronic_balance'}, {'id': 7476, 'synset': 'electronic_converter.n.01', 'name': 'electronic_converter'}, {'id': 7477, 'synset': 'electronic_device.n.01', 'name': 'electronic_device'}, {'id': 7478, 'synset': 'electronic_equipment.n.01', 'name': 'electronic_equipment'}, {'id': 7479, 'synset': 'electronic_fetal_monitor.n.01', 'name': 'electronic_fetal_monitor'}, {'id': 7480, 'synset': 'electronic_instrument.n.01', 'name': 'electronic_instrument'}, {'id': 7481, 'synset': 'electronic_voltmeter.n.01', 'name': 'electronic_voltmeter'}, {'id': 7482, 'synset': 'electron_microscope.n.01', 'name': 'electron_microscope'}, {'id': 7483, 'synset': 'electron_multiplier.n.01', 'name': 'electron_multiplier'}, {'id': 7484, 'synset': 'electrophorus.n.01', 'name': 'electrophorus'}, {'id': 7485, 'synset': 'electroscope.n.01', 'name': 'electroscope'}, {'id': 7486, 'synset': 'electrostatic_generator.n.01', 'name': 'electrostatic_generator'}, {'id': 7487, 'synset': 'electrostatic_printer.n.01', 'name': 'electrostatic_printer'}, {'id': 7488, 'synset': 'elevator.n.01', 'name': 'elevator'}, {'id': 7489, 'synset': 'elevator.n.02', 'name': 'elevator'}, {'id': 7490, 'synset': 'elevator_shaft.n.01', 'name': 'elevator_shaft'}, {'id': 7491, 'synset': 'embankment.n.01', 'name': 'embankment'}, {'id': 7492, 'synset': 'embassy.n.01', 'name': 'embassy'}, {'id': 7493, 'synset': 'embellishment.n.02', 'name': 'embellishment'}, {'id': 7494, 'synset': 'emergency_room.n.01', 'name': 'emergency_room'}, {'id': 7495, 'synset': 'emesis_basin.n.01', 'name': 'emesis_basin'}, {'id': 7496, 'synset': 'emitter.n.01', 'name': 'emitter'}, {'id': 7497, 'synset': 'empty.n.01', 'name': 'empty'}, {'id': 7498, 'synset': 'emulsion.n.02', 'name': 'emulsion'}, {'id': 7499, 'synset': 'enamel.n.04', 'name': 'enamel'}, {'id': 7500, 'synset': 'enamel.n.03', 'name': 'enamel'}, {'id': 7501, 'synset': 'enamelware.n.01', 'name': 'enamelware'}, {'id': 7502, 'synset': 'encaustic.n.01', 'name': 'encaustic'}, {'id': 7503, 'synset': 'encephalogram.n.02', 'name': 'encephalogram'}, {'id': 7504, 'synset': 'enclosure.n.01', 'name': 'enclosure'}, {'id': 7505, 'synset': 'endoscope.n.01', 'name': 'endoscope'}, {'id': 7506, 'synset': 'energizer.n.02', 'name': 'energizer'}, {'id': 7507, 'synset': 'engine.n.01', 'name': 'engine'}, {'id': 7508, 'synset': 'engine.n.04', 'name': 'engine'}, {'id': 7509, 'synset': 'engineering.n.03', 'name': 'engineering'}, {'id': 7510, 'synset': 'enginery.n.01', 'name': 'enginery'}, {'id': 7511, 'synset': 'english_horn.n.01', 'name': 'English_horn'}, {'id': 7512, 'synset': 'english_saddle.n.01', 'name': 'English_saddle'}, {'id': 7513, 'synset': 'enlarger.n.01', 'name': 'enlarger'}, {'id': 7514, 'synset': 'ensemble.n.05', 'name': 'ensemble'}, {'id': 7515, 'synset': 'ensign.n.03', 'name': 'ensign'}, {'id': 7516, 'synset': 'entablature.n.01', 'name': 'entablature'}, {'id': 7517, 'synset': 'entertainment_center.n.01', 'name': 'entertainment_center'}, {'id': 7518, 'synset': 'entrenching_tool.n.01', 'name': 'entrenching_tool'}, {'id': 7519, 'synset': 'entrenchment.n.01', 'name': 'entrenchment'}, {'id': 7520, 'synset': 'envelope.n.02', 'name': 'envelope'}, {'id': 7521, 'synset': 'envelope.n.06', 'name': 'envelope'}, {'id': 7522, 'synset': 'eolith.n.01', 'name': 'eolith'}, {'id': 7523, 'synset': 'epauliere.n.01', 'name': 'epauliere'}, {'id': 7524, 'synset': 'epee.n.01', 'name': 'epee'}, {'id': 7525, 'synset': 'epergne.n.01', 'name': 'epergne'}, {'id': 7526, 'synset': 'epicyclic_train.n.01', 'name': 'epicyclic_train'}, {'id': 7527, 'synset': 'epidiascope.n.01', 'name': 'epidiascope'}, {'id': 7528, 'synset': 'epilating_wax.n.01', 'name': 'epilating_wax'}, {'id': 7529, 'synset': 'equalizer.n.01', 'name': 'equalizer'}, {'id': 7530, 'synset': 'equatorial.n.01', 'name': 'equatorial'}, {'id': 7531, 'synset': 'equipment.n.01', 'name': 'equipment'}, {'id': 7532, 'synset': 'erasable_programmable_read-only_memory.n.01', 'name': 'erasable_programmable_read-only_memory'}, {'id': 7533, 'synset': 'erecting_prism.n.01', 'name': 'erecting_prism'}, {'id': 7534, 'synset': 'erection.n.02', 'name': 'erection'}, {'id': 7535, 'synset': 'erlenmeyer_flask.n.01', 'name': 'Erlenmeyer_flask'}, {'id': 7536, 'synset': 'escape_hatch.n.01', 'name': 'escape_hatch'}, {'id': 7537, 'synset': 'escapement.n.01', 'name': 'escapement'}, {'id': 7538, 'synset': 'escape_wheel.n.01', 'name': 'escape_wheel'}, {'id': 7539, 'synset': 'escarpment.n.02', 'name': 'escarpment'}, {'id': 7540, 'synset': 'escutcheon.n.03', 'name': 'escutcheon'}, {'id': 7541, 'synset': 'esophagoscope.n.01', 'name': 'esophagoscope'}, {'id': 7542, 'synset': 'espadrille.n.01', 'name': 'espadrille'}, {'id': 7543, 'synset': 'espalier.n.01', 'name': 'espalier'}, {'id': 7544, 'synset': 'espresso_maker.n.01', 'name': 'espresso_maker'}, {'id': 7545, 'synset': 'espresso_shop.n.01', 'name': 'espresso_shop'}, {'id': 7546, 'synset': 'establishment.n.04', 'name': 'establishment'}, {'id': 7547, 'synset': 'estaminet.n.01', 'name': 'estaminet'}, {'id': 7548, 'synset': 'estradiol_patch.n.01', 'name': 'estradiol_patch'}, {'id': 7549, 'synset': 'etagere.n.01', 'name': 'etagere'}, {'id': 7550, 'synset': 'etamine.n.01', 'name': 'etamine'}, {'id': 7551, 'synset': 'etching.n.02', 'name': 'etching'}, {'id': 7552, 'synset': 'ethernet.n.01', 'name': 'ethernet'}, {'id': 7553, 'synset': 'ethernet_cable.n.01', 'name': 'ethernet_cable'}, {'id': 7554, 'synset': 'eton_jacket.n.01', 'name': 'Eton_jacket'}, {'id': 7555, 'synset': 'etui.n.01', 'name': 'etui'}, {'id': 7556, 'synset': 'eudiometer.n.01', 'name': 'eudiometer'}, {'id': 7557, 'synset': 'euphonium.n.01', 'name': 'euphonium'}, {'id': 7558, 'synset': 'evaporative_cooler.n.01', 'name': 'evaporative_cooler'}, {'id': 7559, 'synset': 'evening_bag.n.01', 'name': 'evening_bag'}, {'id': 7560, 'synset': 'exercise_bike.n.01', 'name': 'exercise_bike'}, {'id': 7561, 'synset': 'exercise_device.n.01', 'name': 'exercise_device'}, {'id': 7562, 'synset': 'exhaust.n.02', 'name': 'exhaust'}, {'id': 7563, 'synset': 'exhaust_fan.n.01', 'name': 'exhaust_fan'}, {'id': 7564, 'synset': 'exhaust_valve.n.01', 'name': 'exhaust_valve'}, {'id': 7565, 'synset': 'exhibition_hall.n.01', 'name': 'exhibition_hall'}, {'id': 7566, 'synset': 'exocet.n.01', 'name': 'Exocet'}, {'id': 7567, 'synset': 'expansion_bit.n.01', 'name': 'expansion_bit'}, {'id': 7568, 'synset': 'expansion_bolt.n.01', 'name': 'expansion_bolt'}, {'id': 7569, 'synset': 'explosive_detection_system.n.01', 'name': 'explosive_detection_system'}, {'id': 7570, 'synset': 'explosive_device.n.01', 'name': 'explosive_device'}, {'id': 7571, 'synset': 'explosive_trace_detection.n.01', 'name': 'explosive_trace_detection'}, {'id': 7572, 'synset': 'express.n.02', 'name': 'express'}, {'id': 7573, 'synset': 'extension.n.10', 'name': 'extension'}, {'id': 7574, 'synset': 'extension_cord.n.01', 'name': 'extension_cord'}, {'id': 7575, 'synset': 'external-combustion_engine.n.01', 'name': 'external-combustion_engine'}, {'id': 7576, 'synset': 'external_drive.n.01', 'name': 'external_drive'}, {'id': 7577, 'synset': 'extractor.n.01', 'name': 'extractor'}, {'id': 7578, 'synset': 'eyebrow_pencil.n.01', 'name': 'eyebrow_pencil'}, {'id': 7579, 'synset': 'eyecup.n.01', 'name': 'eyecup'}, {'id': 7580, 'synset': 'eyeliner.n.01', 'name': 'eyeliner'}, {'id': 7581, 'synset': 'eyepiece.n.01', 'name': 'eyepiece'}, {'id': 7582, 'synset': 'eyeshadow.n.01', 'name': 'eyeshadow'}, {'id': 7583, 'synset': 'fabric.n.01', 'name': 'fabric'}, {'id': 7584, 'synset': 'facade.n.01', 'name': 'facade'}, {'id': 7585, 'synset': 'face_guard.n.01', 'name': 'face_guard'}, {'id': 7586, 'synset': 'face_mask.n.01', 'name': 'face_mask'}, {'id': 7587, 'synset': 'faceplate.n.01', 'name': 'faceplate'}, {'id': 7588, 'synset': 'face_powder.n.01', 'name': 'face_powder'}, {'id': 7589, 'synset': 'face_veil.n.01', 'name': 'face_veil'}, {'id': 7590, 'synset': 'facing.n.03', 'name': 'facing'}, {'id': 7591, 'synset': 'facing.n.01', 'name': 'facing'}, {'id': 7592, 'synset': 'facing.n.02', 'name': 'facing'}, {'id': 7593, 'synset': 'facsimile.n.02', 'name': 'facsimile'}, {'id': 7594, 'synset': 'factory.n.01', 'name': 'factory'}, {'id': 7595, 'synset': 'factory_ship.n.01', 'name': 'factory_ship'}, {'id': 7596, 'synset': 'fagot.n.02', 'name': 'fagot'}, {'id': 7597, 'synset': 'fagot_stitch.n.01', 'name': 'fagot_stitch'}, {'id': 7598, 'synset': 'fahrenheit_thermometer.n.01', 'name': 'Fahrenheit_thermometer'}, {'id': 7599, 'synset': 'faience.n.01', 'name': 'faience'}, {'id': 7600, 'synset': 'faille.n.01', 'name': 'faille'}, {'id': 7601, 'synset': 'fairlead.n.01', 'name': 'fairlead'}, {'id': 7602, 'synset': 'fairy_light.n.01', 'name': 'fairy_light'}, {'id': 7603, 'synset': 'falchion.n.01', 'name': 'falchion'}, {'id': 7604, 'synset': 'fallboard.n.01', 'name': 'fallboard'}, {'id': 7605, 'synset': 'fallout_shelter.n.01', 'name': 'fallout_shelter'}, {'id': 7606, 'synset': 'false_face.n.01', 'name': 'false_face'}, {'id': 7607, 'synset': 'false_teeth.n.01', 'name': 'false_teeth'}, {'id': 7608, 'synset': 'family_room.n.01', 'name': 'family_room'}, {'id': 7609, 'synset': 'fan_belt.n.01', 'name': 'fan_belt'}, {'id': 7610, 'synset': 'fan_blade.n.01', 'name': 'fan_blade'}, {'id': 7611, 'synset': 'fancy_dress.n.01', 'name': 'fancy_dress'}, {'id': 7612, 'synset': 'fanion.n.01', 'name': 'fanion'}, {'id': 7613, 'synset': 'fanlight.n.03', 'name': 'fanlight'}, {'id': 7614, 'synset': 'fanjet.n.02', 'name': 'fanjet'}, {'id': 7615, 'synset': 'fanjet.n.01', 'name': 'fanjet'}, {'id': 7616, 'synset': 'fanny_pack.n.01', 'name': 'fanny_pack'}, {'id': 7617, 'synset': 'fan_tracery.n.01', 'name': 'fan_tracery'}, {'id': 7618, 'synset': 'fan_vaulting.n.01', 'name': 'fan_vaulting'}, {'id': 7619, 'synset': 'farm_building.n.01', 'name': 'farm_building'}, {'id': 7620, 'synset': "farmer's_market.n.01", 'name': "farmer's_market"}, {'id': 7621, 'synset': 'farmhouse.n.01', 'name': 'farmhouse'}, {'id': 7622, 'synset': 'farm_machine.n.01', 'name': 'farm_machine'}, {'id': 7623, 'synset': 'farmplace.n.01', 'name': 'farmplace'}, {'id': 7624, 'synset': 'farmyard.n.01', 'name': 'farmyard'}, {'id': 7625, 'synset': 'farthingale.n.01', 'name': 'farthingale'}, {'id': 7626, 'synset': 'fastener.n.02', 'name': 'fastener'}, {'id': 7627, 'synset': 'fast_reactor.n.01', 'name': 'fast_reactor'}, {'id': 7628, 'synset': 'fat_farm.n.01', 'name': 'fat_farm'}, {'id': 7629, 'synset': 'fatigues.n.01', 'name': 'fatigues'}, {'id': 7630, 'synset': 'fauld.n.01', 'name': 'fauld'}, {'id': 7631, 'synset': 'fauteuil.n.01', 'name': 'fauteuil'}, {'id': 7632, 'synset': 'feather_boa.n.01', 'name': 'feather_boa'}, {'id': 7633, 'synset': 'featheredge.n.01', 'name': 'featheredge'}, {'id': 7634, 'synset': 'feedback_circuit.n.01', 'name': 'feedback_circuit'}, {'id': 7635, 'synset': 'feedlot.n.01', 'name': 'feedlot'}, {'id': 7636, 'synset': 'fell.n.02', 'name': 'fell'}, {'id': 7637, 'synset': 'felloe.n.01', 'name': 'felloe'}, {'id': 7638, 'synset': 'felt.n.01', 'name': 'felt'}, {'id': 7639, 'synset': 'felt-tip_pen.n.01', 'name': 'felt-tip_pen'}, {'id': 7640, 'synset': 'felucca.n.01', 'name': 'felucca'}, {'id': 7641, 'synset': 'fence.n.01', 'name': 'fence'}, {'id': 7642, 'synset': 'fencing_mask.n.01', 'name': 'fencing_mask'}, {'id': 7643, 'synset': 'fencing_sword.n.01', 'name': 'fencing_sword'}, {'id': 7644, 'synset': 'fender.n.01', 'name': 'fender'}, {'id': 7645, 'synset': 'fender.n.02', 'name': 'fender'}, {'id': 7646, 'synset': 'ferrule.n.01', 'name': 'ferrule'}, {'id': 7647, 'synset': 'ferule.n.01', 'name': 'ferule'}, {'id': 7648, 'synset': 'festoon.n.01', 'name': 'festoon'}, {'id': 7649, 'synset': 'fetoscope.n.01', 'name': 'fetoscope'}, {'id': 7650, 'synset': 'fetter.n.01', 'name': 'fetter'}, {'id': 7651, 'synset': 'fez.n.02', 'name': 'fez'}, {'id': 7652, 'synset': 'fiber.n.05', 'name': 'fiber'}, {'id': 7653, 'synset': 'fiber_optic_cable.n.01', 'name': 'fiber_optic_cable'}, {'id': 7654, 'synset': 'fiberscope.n.01', 'name': 'fiberscope'}, {'id': 7655, 'synset': 'fichu.n.01', 'name': 'fichu'}, {'id': 7656, 'synset': 'fiddlestick.n.01', 'name': 'fiddlestick'}, {'id': 7657, 'synset': 'field_artillery.n.01', 'name': 'field_artillery'}, {'id': 7658, 'synset': 'field_coil.n.01', 'name': 'field_coil'}, {'id': 7659, 'synset': 'field-effect_transistor.n.01', 'name': 'field-effect_transistor'}, {'id': 7660, 'synset': 'field-emission_microscope.n.01', 'name': 'field-emission_microscope'}, {'id': 7661, 'synset': 'field_glass.n.01', 'name': 'field_glass'}, {'id': 7662, 'synset': 'field_hockey_ball.n.01', 'name': 'field_hockey_ball'}, {'id': 7663, 'synset': 'field_hospital.n.01', 'name': 'field_hospital'}, {'id': 7664, 'synset': 'field_house.n.01', 'name': 'field_house'}, {'id': 7665, 'synset': 'field_lens.n.01', 'name': 'field_lens'}, {'id': 7666, 'synset': 'field_magnet.n.01', 'name': 'field_magnet'}, {'id': 7667, 'synset': 'field-sequential_color_television.n.01', 'name': 'field-sequential_color_television'}, {'id': 7668, 'synset': 'field_tent.n.01', 'name': 'field_tent'}, {'id': 7669, 'synset': 'fieldwork.n.01', 'name': 'fieldwork'}, {'id': 7670, 'synset': 'fife.n.01', 'name': 'fife'}, {'id': 7671, 'synset': 'fifth_wheel.n.02', 'name': 'fifth_wheel'}, {'id': 7672, 'synset': 'fighting_chair.n.01', 'name': 'fighting_chair'}, {'id': 7673, 'synset': 'fig_leaf.n.02', 'name': 'fig_leaf'}, {'id': 7674, 'synset': 'figure_eight.n.01', 'name': 'figure_eight'}, {'id': 7675, 'synset': 'figure_loom.n.01', 'name': 'figure_loom'}, {'id': 7676, 'synset': 'figure_skate.n.01', 'name': 'figure_skate'}, {'id': 7677, 'synset': 'filament.n.04', 'name': 'filament'}, {'id': 7678, 'synset': 'filature.n.01', 'name': 'filature'}, {'id': 7679, 'synset': 'file_folder.n.01', 'name': 'file_folder'}, {'id': 7680, 'synset': 'file_server.n.01', 'name': 'file_server'}, {'id': 7681, 'synset': 'filigree.n.01', 'name': 'filigree'}, {'id': 7682, 'synset': 'filling.n.05', 'name': 'filling'}, {'id': 7683, 'synset': 'film.n.03', 'name': 'film'}, {'id': 7684, 'synset': 'film.n.05', 'name': 'film'}, {'id': 7685, 'synset': 'film_advance.n.01', 'name': 'film_advance'}, {'id': 7686, 'synset': 'filter.n.01', 'name': 'filter'}, {'id': 7687, 'synset': 'filter.n.02', 'name': 'filter'}, {'id': 7688, 'synset': 'finder.n.03', 'name': 'finder'}, {'id': 7689, 'synset': 'finery.n.01', 'name': 'finery'}, {'id': 7690, 'synset': 'fine-tooth_comb.n.01', 'name': 'fine-tooth_comb'}, {'id': 7691, 'synset': 'finger.n.03', 'name': 'finger'}, {'id': 7692, 'synset': 'fingerboard.n.03', 'name': 'fingerboard'}, {'id': 7693, 'synset': 'finger_bowl.n.01', 'name': 'finger_bowl'}, {'id': 7694, 'synset': 'finger_paint.n.01', 'name': 'finger_paint'}, {'id': 7695, 'synset': 'finger-painting.n.01', 'name': 'finger-painting'}, {'id': 7696, 'synset': 'finger_plate.n.01', 'name': 'finger_plate'}, {'id': 7697, 'synset': 'fingerstall.n.01', 'name': 'fingerstall'}, {'id': 7698, 'synset': 'finish_coat.n.02', 'name': 'finish_coat'}, {'id': 7699, 'synset': 'finish_coat.n.01', 'name': 'finish_coat'}, {'id': 7700, 'synset': 'finisher.n.05', 'name': 'finisher'}, {'id': 7701, 'synset': 'fin_keel.n.01', 'name': 'fin_keel'}, {'id': 7702, 'synset': 'fipple.n.01', 'name': 'fipple'}, {'id': 7703, 'synset': 'fipple_flute.n.01', 'name': 'fipple_flute'}, {'id': 7704, 'synset': 'fire.n.04', 'name': 'fire'}, {'id': 7705, 'synset': 'firearm.n.01', 'name': 'firearm'}, {'id': 7706, 'synset': 'fire_bell.n.01', 'name': 'fire_bell'}, {'id': 7707, 'synset': 'fireboat.n.01', 'name': 'fireboat'}, {'id': 7708, 'synset': 'firebox.n.01', 'name': 'firebox'}, {'id': 7709, 'synset': 'firebrick.n.01', 'name': 'firebrick'}, {'id': 7710, 'synset': 'fire_control_radar.n.01', 'name': 'fire_control_radar'}, {'id': 7711, 'synset': 'fire_control_system.n.01', 'name': 'fire_control_system'}, {'id': 7712, 'synset': 'fire_iron.n.01', 'name': 'fire_iron'}, {'id': 7713, 'synset': "fireman's_ax.n.01", 'name': "fireman's_ax"}, {'id': 7714, 'synset': 'fire_screen.n.01', 'name': 'fire_screen'}, {'id': 7715, 'synset': 'fire_tongs.n.01', 'name': 'fire_tongs'}, {'id': 7716, 'synset': 'fire_tower.n.01', 'name': 'fire_tower'}, {'id': 7717, 'synset': 'firewall.n.02', 'name': 'firewall'}, {'id': 7718, 'synset': 'firing_chamber.n.01', 'name': 'firing_chamber'}, {'id': 7719, 'synset': 'firing_pin.n.01', 'name': 'firing_pin'}, {'id': 7720, 'synset': 'firkin.n.02', 'name': 'firkin'}, {'id': 7721, 'synset': 'firmer_chisel.n.01', 'name': 'firmer_chisel'}, {'id': 7722, 'synset': 'first-aid_station.n.01', 'name': 'first-aid_station'}, {'id': 7723, 'synset': 'first_base.n.01', 'name': 'first_base'}, {'id': 7724, 'synset': 'first_class.n.03', 'name': 'first_class'}, {'id': 7725, 'synset': "fisherman's_bend.n.01", 'name': "fisherman's_bend"}, {'id': 7726, 'synset': "fisherman's_knot.n.01", 'name': "fisherman's_knot"}, {'id': 7727, 'synset': "fisherman's_lure.n.01", 'name': "fisherman's_lure"}, {'id': 7728, 'synset': 'fishhook.n.01', 'name': 'fishhook'}, {'id': 7729, 'synset': 'fishing_boat.n.01', 'name': 'fishing_boat'}, {'id': 7730, 'synset': 'fishing_gear.n.01', 'name': 'fishing_gear'}, {'id': 7731, 'synset': 'fish_joint.n.01', 'name': 'fish_joint'}, {'id': 7732, 'synset': 'fish_knife.n.01', 'name': 'fish_knife'}, {'id': 7733, 'synset': 'fishnet.n.01', 'name': 'fishnet'}, {'id': 7734, 'synset': 'fish_slice.n.01', 'name': 'fish_slice'}, {'id': 7735, 'synset': 'fitment.n.01', 'name': 'fitment'}, {'id': 7736, 'synset': 'fixative.n.02', 'name': 'fixative'}, {'id': 7737, 'synset': 'fixer-upper.n.01', 'name': 'fixer-upper'}, {'id': 7738, 'synset': 'flageolet.n.02', 'name': 'flageolet'}, {'id': 7739, 'synset': 'flagon.n.01', 'name': 'flagon'}, {'id': 7740, 'synset': 'flagship.n.02', 'name': 'flagship'}, {'id': 7741, 'synset': 'flail.n.01', 'name': 'flail'}, {'id': 7742, 'synset': 'flambeau.n.01', 'name': 'flambeau'}, {'id': 7743, 'synset': 'flamethrower.n.01', 'name': 'flamethrower'}, {'id': 7744, 'synset': 'flange.n.01', 'name': 'flange'}, {'id': 7745, 'synset': 'flannel.n.03', 'name': 'flannel'}, {'id': 7746, 'synset': 'flannelette.n.01', 'name': 'flannelette'}, {'id': 7747, 'synset': 'flap.n.05', 'name': 'flap'}, {'id': 7748, 'synset': 'flash.n.09', 'name': 'flash'}, {'id': 7749, 'synset': 'flash_camera.n.01', 'name': 'flash_camera'}, {'id': 7750, 'synset': 'flasher.n.02', 'name': 'flasher'}, {'id': 7751, 'synset': 'flashlight_battery.n.01', 'name': 'flashlight_battery'}, {'id': 7752, 'synset': 'flash_memory.n.01', 'name': 'flash_memory'}, {'id': 7753, 'synset': 'flask.n.01', 'name': 'flask'}, {'id': 7754, 'synset': 'flat_arch.n.01', 'name': 'flat_arch'}, {'id': 7755, 'synset': 'flatbed.n.02', 'name': 'flatbed'}, {'id': 7756, 'synset': 'flatbed_press.n.01', 'name': 'flatbed_press'}, {'id': 7757, 'synset': 'flat_bench.n.01', 'name': 'flat_bench'}, {'id': 7758, 'synset': 'flatcar.n.01', 'name': 'flatcar'}, {'id': 7759, 'synset': 'flat_file.n.01', 'name': 'flat_file'}, {'id': 7760, 'synset': 'flatlet.n.01', 'name': 'flatlet'}, {'id': 7761, 'synset': 'flat_panel_display.n.01', 'name': 'flat_panel_display'}, {'id': 7762, 'synset': 'flats.n.01', 'name': 'flats'}, {'id': 7763, 'synset': 'flat_tip_screwdriver.n.01', 'name': 'flat_tip_screwdriver'}, {'id': 7764, 'synset': 'fleet_ballistic_missile_submarine.n.01', 'name': 'fleet_ballistic_missile_submarine'}, {'id': 7765, 'synset': 'fleur-de-lis.n.02', 'name': 'fleur-de-lis'}, {'id': 7766, 'synset': 'flight_simulator.n.01', 'name': 'flight_simulator'}, {'id': 7767, 'synset': 'flintlock.n.02', 'name': 'flintlock'}, {'id': 7768, 'synset': 'flintlock.n.01', 'name': 'flintlock'}, {'id': 7769, 'synset': 'float.n.05', 'name': 'float'}, {'id': 7770, 'synset': 'floating_dock.n.01', 'name': 'floating_dock'}, {'id': 7771, 'synset': 'floatplane.n.01', 'name': 'floatplane'}, {'id': 7772, 'synset': 'flood.n.03', 'name': 'flood'}, {'id': 7773, 'synset': 'floor.n.01', 'name': 'floor'}, {'id': 7774, 'synset': 'floor.n.02', 'name': 'floor'}, {'id': 7775, 'synset': 'floor.n.09', 'name': 'floor'}, {'id': 7776, 'synset': 'floorboard.n.02', 'name': 'floorboard'}, {'id': 7777, 'synset': 'floor_cover.n.01', 'name': 'floor_cover'}, {'id': 7778, 'synset': 'floor_joist.n.01', 'name': 'floor_joist'}, {'id': 7779, 'synset': 'floor_lamp.n.01', 'name': 'floor_lamp'}, {'id': 7780, 'synset': 'flophouse.n.01', 'name': 'flophouse'}, {'id': 7781, 'synset': 'florist.n.02', 'name': 'florist'}, {'id': 7782, 'synset': 'floss.n.01', 'name': 'floss'}, {'id': 7783, 'synset': 'flotsam.n.01', 'name': 'flotsam'}, {'id': 7784, 'synset': 'flour_bin.n.01', 'name': 'flour_bin'}, {'id': 7785, 'synset': 'flour_mill.n.01', 'name': 'flour_mill'}, {'id': 7786, 'synset': 'flowerbed.n.01', 'name': 'flowerbed'}, {'id': 7787, 'synset': 'flugelhorn.n.01', 'name': 'flugelhorn'}, {'id': 7788, 'synset': 'fluid_drive.n.01', 'name': 'fluid_drive'}, {'id': 7789, 'synset': 'fluid_flywheel.n.01', 'name': 'fluid_flywheel'}, {'id': 7790, 'synset': 'flume.n.02', 'name': 'flume'}, {'id': 7791, 'synset': 'fluorescent_lamp.n.01', 'name': 'fluorescent_lamp'}, {'id': 7792, 'synset': 'fluoroscope.n.01', 'name': 'fluoroscope'}, {'id': 7793, 'synset': 'flush_toilet.n.01', 'name': 'flush_toilet'}, {'id': 7794, 'synset': 'flute.n.01', 'name': 'flute'}, {'id': 7795, 'synset': 'flux_applicator.n.01', 'name': 'flux_applicator'}, {'id': 7796, 'synset': 'fluxmeter.n.01', 'name': 'fluxmeter'}, {'id': 7797, 'synset': 'fly.n.05', 'name': 'fly'}, {'id': 7798, 'synset': 'flying_boat.n.01', 'name': 'flying_boat'}, {'id': 7799, 'synset': 'flying_buttress.n.01', 'name': 'flying_buttress'}, {'id': 7800, 'synset': 'flying_carpet.n.01', 'name': 'flying_carpet'}, {'id': 7801, 'synset': 'flying_jib.n.01', 'name': 'flying_jib'}, {'id': 7802, 'synset': 'fly_rod.n.01', 'name': 'fly_rod'}, {'id': 7803, 'synset': 'fly_tent.n.01', 'name': 'fly_tent'}, {'id': 7804, 'synset': 'flytrap.n.01', 'name': 'flytrap'}, {'id': 7805, 'synset': 'flywheel.n.01', 'name': 'flywheel'}, {'id': 7806, 'synset': 'fob.n.03', 'name': 'fob'}, {'id': 7807, 'synset': 'foghorn.n.02', 'name': 'foghorn'}, {'id': 7808, 'synset': 'foglamp.n.01', 'name': 'foglamp'}, {'id': 7809, 'synset': 'foil.n.05', 'name': 'foil'}, {'id': 7810, 'synset': 'fold.n.06', 'name': 'fold'}, {'id': 7811, 'synset': 'folder.n.02', 'name': 'folder'}, {'id': 7812, 'synset': 'folding_door.n.01', 'name': 'folding_door'}, {'id': 7813, 'synset': 'folding_saw.n.01', 'name': 'folding_saw'}, {'id': 7814, 'synset': 'food_court.n.01', 'name': 'food_court'}, {'id': 7815, 'synset': 'food_hamper.n.01', 'name': 'food_hamper'}, {'id': 7816, 'synset': 'foot.n.11', 'name': 'foot'}, {'id': 7817, 'synset': 'footage.n.01', 'name': 'footage'}, {'id': 7818, 'synset': 'football_stadium.n.01', 'name': 'football_stadium'}, {'id': 7819, 'synset': 'footbath.n.01', 'name': 'footbath'}, {'id': 7820, 'synset': 'foot_brake.n.01', 'name': 'foot_brake'}, {'id': 7821, 'synset': 'footbridge.n.01', 'name': 'footbridge'}, {'id': 7822, 'synset': 'foothold.n.02', 'name': 'foothold'}, {'id': 7823, 'synset': 'footlocker.n.01', 'name': 'footlocker'}, {'id': 7824, 'synset': 'foot_rule.n.01', 'name': 'foot_rule'}, {'id': 7825, 'synset': 'footwear.n.02', 'name': 'footwear'}, {'id': 7826, 'synset': 'footwear.n.01', 'name': 'footwear'}, {'id': 7827, 'synset': 'forceps.n.01', 'name': 'forceps'}, {'id': 7828, 'synset': 'force_pump.n.01', 'name': 'force_pump'}, {'id': 7829, 'synset': 'fore-and-after.n.01', 'name': 'fore-and-after'}, {'id': 7830, 'synset': 'fore-and-aft_sail.n.01', 'name': 'fore-and-aft_sail'}, {'id': 7831, 'synset': 'forecastle.n.01', 'name': 'forecastle'}, {'id': 7832, 'synset': 'forecourt.n.01', 'name': 'forecourt'}, {'id': 7833, 'synset': 'foredeck.n.01', 'name': 'foredeck'}, {'id': 7834, 'synset': 'fore_edge.n.01', 'name': 'fore_edge'}, {'id': 7835, 'synset': 'foreground.n.02', 'name': 'foreground'}, {'id': 7836, 'synset': 'foremast.n.01', 'name': 'foremast'}, {'id': 7837, 'synset': 'fore_plane.n.01', 'name': 'fore_plane'}, {'id': 7838, 'synset': 'foresail.n.01', 'name': 'foresail'}, {'id': 7839, 'synset': 'forestay.n.01', 'name': 'forestay'}, {'id': 7840, 'synset': 'foretop.n.01', 'name': 'foretop'}, {'id': 7841, 'synset': 'fore-topmast.n.01', 'name': 'fore-topmast'}, {'id': 7842, 'synset': 'fore-topsail.n.01', 'name': 'fore-topsail'}, {'id': 7843, 'synset': 'forge.n.01', 'name': 'forge'}, {'id': 7844, 'synset': 'fork.n.04', 'name': 'fork'}, {'id': 7845, 'synset': 'formalwear.n.01', 'name': 'formalwear'}, {'id': 7846, 'synset': 'formica.n.01', 'name': 'Formica'}, {'id': 7847, 'synset': 'fortification.n.01', 'name': 'fortification'}, {'id': 7848, 'synset': 'fortress.n.01', 'name': 'fortress'}, {'id': 7849, 'synset': 'forty-five.n.01', 'name': 'forty-five'}, {'id': 7850, 'synset': 'foucault_pendulum.n.01', 'name': 'Foucault_pendulum'}, {'id': 7851, 'synset': 'foulard.n.01', 'name': 'foulard'}, {'id': 7852, 'synset': 'foul-weather_gear.n.01', 'name': 'foul-weather_gear'}, {'id': 7853, 'synset': 'foundation_garment.n.01', 'name': 'foundation_garment'}, {'id': 7854, 'synset': 'foundry.n.01', 'name': 'foundry'}, {'id': 7855, 'synset': 'fountain.n.01', 'name': 'fountain'}, {'id': 7856, 'synset': 'fountain_pen.n.01', 'name': 'fountain_pen'}, {'id': 7857, 'synset': 'four-in-hand.n.01', 'name': 'four-in-hand'}, {'id': 7858, 'synset': 'four-poster.n.01', 'name': 'four-poster'}, {'id': 7859, 'synset': 'four-pounder.n.01', 'name': 'four-pounder'}, {'id': 7860, 'synset': 'four-stroke_engine.n.01', 'name': 'four-stroke_engine'}, {'id': 7861, 'synset': 'four-wheel_drive.n.02', 'name': 'four-wheel_drive'}, {'id': 7862, 'synset': 'four-wheel_drive.n.01', 'name': 'four-wheel_drive'}, {'id': 7863, 'synset': 'four-wheeler.n.01', 'name': 'four-wheeler'}, {'id': 7864, 'synset': 'fowling_piece.n.01', 'name': 'fowling_piece'}, {'id': 7865, 'synset': 'foxhole.n.01', 'name': 'foxhole'}, {'id': 7866, 'synset': 'fragmentation_bomb.n.01', 'name': 'fragmentation_bomb'}, {'id': 7867, 'synset': 'frail.n.02', 'name': 'frail'}, {'id': 7868, 'synset': 'fraise.n.02', 'name': 'fraise'}, {'id': 7869, 'synset': 'frame.n.10', 'name': 'frame'}, {'id': 7870, 'synset': 'frame.n.01', 'name': 'frame'}, {'id': 7871, 'synset': 'frame_buffer.n.01', 'name': 'frame_buffer'}, {'id': 7872, 'synset': 'framework.n.03', 'name': 'framework'}, {'id': 7873, 'synset': 'francis_turbine.n.01', 'name': 'Francis_turbine'}, {'id': 7874, 'synset': 'franking_machine.n.01', 'name': 'franking_machine'}, {'id': 7875, 'synset': 'free_house.n.01', 'name': 'free_house'}, {'id': 7876, 'synset': 'free-reed.n.01', 'name': 'free-reed'}, {'id': 7877, 'synset': 'free-reed_instrument.n.01', 'name': 'free-reed_instrument'}, {'id': 7878, 'synset': 'freewheel.n.01', 'name': 'freewheel'}, {'id': 7879, 'synset': 'freight_elevator.n.01', 'name': 'freight_elevator'}, {'id': 7880, 'synset': 'freight_liner.n.01', 'name': 'freight_liner'}, {'id': 7881, 'synset': 'freight_train.n.01', 'name': 'freight_train'}, {'id': 7882, 'synset': 'french_door.n.01', 'name': 'French_door'}, {'id': 7883, 'synset': 'french_horn.n.01', 'name': 'French_horn'}, {'id': 7884, 'synset': 'french_polish.n.02', 'name': 'French_polish'}, {'id': 7885, 'synset': 'french_roof.n.01', 'name': 'French_roof'}, {'id': 7886, 'synset': 'french_window.n.01', 'name': 'French_window'}, {'id': 7887, 'synset': 'fresnel_lens.n.01', 'name': 'Fresnel_lens'}, {'id': 7888, 'synset': 'fret.n.04', 'name': 'fret'}, {'id': 7889, 'synset': 'friary.n.01', 'name': 'friary'}, {'id': 7890, 'synset': 'friction_clutch.n.01', 'name': 'friction_clutch'}, {'id': 7891, 'synset': 'frieze.n.02', 'name': 'frieze'}, {'id': 7892, 'synset': 'frieze.n.01', 'name': 'frieze'}, {'id': 7893, 'synset': 'frigate.n.02', 'name': 'frigate'}, {'id': 7894, 'synset': 'frigate.n.01', 'name': 'frigate'}, {'id': 7895, 'synset': 'frill.n.03', 'name': 'frill'}, {'id': 7896, 'synset': 'frock.n.01', 'name': 'frock'}, {'id': 7897, 'synset': 'frock_coat.n.01', 'name': 'frock_coat'}, {'id': 7898, 'synset': 'frontlet.n.01', 'name': 'frontlet'}, {'id': 7899, 'synset': 'front_porch.n.01', 'name': 'front_porch'}, {'id': 7900, 'synset': 'front_projector.n.01', 'name': 'front_projector'}, {'id': 7901, 'synset': 'fruit_machine.n.01', 'name': 'fruit_machine'}, {'id': 7902, 'synset': 'fuel_filter.n.01', 'name': 'fuel_filter'}, {'id': 7903, 'synset': 'fuel_gauge.n.01', 'name': 'fuel_gauge'}, {'id': 7904, 'synset': 'fuel_injection.n.01', 'name': 'fuel_injection'}, {'id': 7905, 'synset': 'fuel_system.n.01', 'name': 'fuel_system'}, {'id': 7906, 'synset': 'full-dress_uniform.n.01', 'name': 'full-dress_uniform'}, {'id': 7907, 'synset': 'full_metal_jacket.n.01', 'name': 'full_metal_jacket'}, {'id': 7908, 'synset': 'full_skirt.n.01', 'name': 'full_skirt'}, {'id': 7909, 'synset': 'fumigator.n.02', 'name': 'fumigator'}, {'id': 7910, 'synset': 'funeral_home.n.01', 'name': 'funeral_home'}, {'id': 7911, 'synset': 'funny_wagon.n.01', 'name': 'funny_wagon'}, {'id': 7912, 'synset': 'fur.n.03', 'name': 'fur'}, {'id': 7913, 'synset': 'fur_coat.n.01', 'name': 'fur_coat'}, {'id': 7914, 'synset': 'fur_hat.n.01', 'name': 'fur_hat'}, {'id': 7915, 'synset': 'furnace.n.01', 'name': 'furnace'}, {'id': 7916, 'synset': 'furnace_lining.n.01', 'name': 'furnace_lining'}, {'id': 7917, 'synset': 'furnace_room.n.01', 'name': 'furnace_room'}, {'id': 7918, 'synset': 'furnishing.n.02', 'name': 'furnishing'}, {'id': 7919, 'synset': 'furnishing.n.01', 'name': 'furnishing'}, {'id': 7920, 'synset': 'furniture.n.01', 'name': 'furniture'}, {'id': 7921, 'synset': 'fur-piece.n.01', 'name': 'fur-piece'}, {'id': 7922, 'synset': 'furrow.n.01', 'name': 'furrow'}, {'id': 7923, 'synset': 'fuse.n.01', 'name': 'fuse'}, {'id': 7924, 'synset': 'fusee_drive.n.01', 'name': 'fusee_drive'}, {'id': 7925, 'synset': 'fuselage.n.01', 'name': 'fuselage'}, {'id': 7926, 'synset': 'fusil.n.01', 'name': 'fusil'}, {'id': 7927, 'synset': 'fustian.n.02', 'name': 'fustian'}, {'id': 7928, 'synset': 'gabardine.n.01', 'name': 'gabardine'}, {'id': 7929, 'synset': 'gable.n.01', 'name': 'gable'}, {'id': 7930, 'synset': 'gable_roof.n.01', 'name': 'gable_roof'}, {'id': 7931, 'synset': 'gadgetry.n.01', 'name': 'gadgetry'}, {'id': 7932, 'synset': 'gaff.n.03', 'name': 'gaff'}, {'id': 7933, 'synset': 'gaff.n.02', 'name': 'gaff'}, {'id': 7934, 'synset': 'gaff.n.01', 'name': 'gaff'}, {'id': 7935, 'synset': 'gaffsail.n.01', 'name': 'gaffsail'}, {'id': 7936, 'synset': 'gaff_topsail.n.01', 'name': 'gaff_topsail'}, {'id': 7937, 'synset': 'gaiter.n.03', 'name': 'gaiter'}, {'id': 7938, 'synset': 'gaiter.n.02', 'name': 'gaiter'}, {'id': 7939, 'synset': 'galilean_telescope.n.01', 'name': 'Galilean_telescope'}, {'id': 7940, 'synset': 'galleon.n.01', 'name': 'galleon'}, {'id': 7941, 'synset': 'gallery.n.04', 'name': 'gallery'}, {'id': 7942, 'synset': 'gallery.n.03', 'name': 'gallery'}, {'id': 7943, 'synset': 'galley.n.04', 'name': 'galley'}, {'id': 7944, 'synset': 'galley.n.03', 'name': 'galley'}, {'id': 7945, 'synset': 'galley.n.02', 'name': 'galley'}, {'id': 7946, 'synset': 'gallows.n.01', 'name': 'gallows'}, {'id': 7947, 'synset': 'gallows_tree.n.01', 'name': 'gallows_tree'}, {'id': 7948, 'synset': 'galvanometer.n.01', 'name': 'galvanometer'}, {'id': 7949, 'synset': 'gambling_house.n.01', 'name': 'gambling_house'}, {'id': 7950, 'synset': 'gambrel.n.01', 'name': 'gambrel'}, {'id': 7951, 'synset': 'game.n.09', 'name': 'game'}, {'id': 7952, 'synset': 'gamebag.n.01', 'name': 'gamebag'}, {'id': 7953, 'synset': 'game_equipment.n.01', 'name': 'game_equipment'}, {'id': 7954, 'synset': 'gaming_table.n.01', 'name': 'gaming_table'}, {'id': 7955, 'synset': 'gamp.n.01', 'name': 'gamp'}, {'id': 7956, 'synset': 'gangplank.n.01', 'name': 'gangplank'}, {'id': 7957, 'synset': 'gangsaw.n.01', 'name': 'gangsaw'}, {'id': 7958, 'synset': 'gangway.n.01', 'name': 'gangway'}, {'id': 7959, 'synset': 'gantlet.n.04', 'name': 'gantlet'}, {'id': 7960, 'synset': 'gantry.n.01', 'name': 'gantry'}, {'id': 7961, 'synset': 'garage.n.01', 'name': 'garage'}, {'id': 7962, 'synset': 'garage.n.02', 'name': 'garage'}, {'id': 7963, 'synset': 'garand_rifle.n.01', 'name': 'Garand_rifle'}, {'id': 7964, 'synset': 'garboard.n.01', 'name': 'garboard'}, {'id': 7965, 'synset': 'garden.n.01', 'name': 'garden'}, {'id': 7966, 'synset': 'garden.n.03', 'name': 'garden'}, {'id': 7967, 'synset': 'garden_rake.n.01', 'name': 'garden_rake'}, {'id': 7968, 'synset': 'garden_spade.n.01', 'name': 'garden_spade'}, {'id': 7969, 'synset': 'garden_tool.n.01', 'name': 'garden_tool'}, {'id': 7970, 'synset': 'garden_trowel.n.01', 'name': 'garden_trowel'}, {'id': 7971, 'synset': 'gargoyle.n.01', 'name': 'gargoyle'}, {'id': 7972, 'synset': 'garibaldi.n.02', 'name': 'garibaldi'}, {'id': 7973, 'synset': 'garlic_press.n.01', 'name': 'garlic_press'}, {'id': 7974, 'synset': 'garment.n.01', 'name': 'garment'}, {'id': 7975, 'synset': 'garment_bag.n.01', 'name': 'garment_bag'}, {'id': 7976, 'synset': 'garrison_cap.n.01', 'name': 'garrison_cap'}, {'id': 7977, 'synset': 'garrote.n.01', 'name': 'garrote'}, {'id': 7978, 'synset': 'garter.n.01', 'name': 'garter'}, {'id': 7979, 'synset': 'garter_belt.n.01', 'name': 'garter_belt'}, {'id': 7980, 'synset': 'garter_stitch.n.01', 'name': 'garter_stitch'}, {'id': 7981, 'synset': 'gas_guzzler.n.01', 'name': 'gas_guzzler'}, {'id': 7982, 'synset': 'gas_shell.n.01', 'name': 'gas_shell'}, {'id': 7983, 'synset': 'gas_bracket.n.01', 'name': 'gas_bracket'}, {'id': 7984, 'synset': 'gas_burner.n.01', 'name': 'gas_burner'}, {'id': 7985, 'synset': 'gas-cooled_reactor.n.01', 'name': 'gas-cooled_reactor'}, {'id': 7986, 'synset': 'gas-discharge_tube.n.01', 'name': 'gas-discharge_tube'}, {'id': 7987, 'synset': 'gas_engine.n.01', 'name': 'gas_engine'}, {'id': 7988, 'synset': 'gas_fixture.n.01', 'name': 'gas_fixture'}, {'id': 7989, 'synset': 'gas_furnace.n.01', 'name': 'gas_furnace'}, {'id': 7990, 'synset': 'gas_gun.n.01', 'name': 'gas_gun'}, {'id': 7991, 'synset': 'gas_heater.n.01', 'name': 'gas_heater'}, {'id': 7992, 'synset': 'gas_holder.n.01', 'name': 'gas_holder'}, {'id': 7993, 'synset': 'gasket.n.01', 'name': 'gasket'}, {'id': 7994, 'synset': 'gas_lamp.n.01', 'name': 'gas_lamp'}, {'id': 7995, 'synset': 'gas_maser.n.01', 'name': 'gas_maser'}, {'id': 7996, 'synset': 'gas_meter.n.01', 'name': 'gas_meter'}, {'id': 7997, 'synset': 'gasoline_engine.n.01', 'name': 'gasoline_engine'}, {'id': 7998, 'synset': 'gasoline_gauge.n.01', 'name': 'gasoline_gauge'}, {'id': 7999, 'synset': 'gas_oven.n.02', 'name': 'gas_oven'}, {'id': 8000, 'synset': 'gas_oven.n.01', 'name': 'gas_oven'}, {'id': 8001, 'synset': 'gas_pump.n.01', 'name': 'gas_pump'}, {'id': 8002, 'synset': 'gas_range.n.01', 'name': 'gas_range'}, {'id': 8003, 'synset': 'gas_ring.n.01', 'name': 'gas_ring'}, {'id': 8004, 'synset': 'gas_tank.n.01', 'name': 'gas_tank'}, {'id': 8005, 'synset': 'gas_thermometer.n.01', 'name': 'gas_thermometer'}, {'id': 8006, 'synset': 'gastroscope.n.01', 'name': 'gastroscope'}, {'id': 8007, 'synset': 'gas_turbine.n.01', 'name': 'gas_turbine'}, {'id': 8008, 'synset': 'gas-turbine_ship.n.01', 'name': 'gas-turbine_ship'}, {'id': 8009, 'synset': 'gat.n.01', 'name': 'gat'}, {'id': 8010, 'synset': 'gate.n.01', 'name': 'gate'}, {'id': 8011, 'synset': 'gatehouse.n.01', 'name': 'gatehouse'}, {'id': 8012, 'synset': 'gateleg_table.n.01', 'name': 'gateleg_table'}, {'id': 8013, 'synset': 'gatepost.n.01', 'name': 'gatepost'}, {'id': 8014, 'synset': 'gathered_skirt.n.01', 'name': 'gathered_skirt'}, {'id': 8015, 'synset': 'gatling_gun.n.01', 'name': 'Gatling_gun'}, {'id': 8016, 'synset': 'gauge.n.01', 'name': 'gauge'}, {'id': 8017, 'synset': 'gauntlet.n.03', 'name': 'gauntlet'}, {'id': 8018, 'synset': 'gauntlet.n.02', 'name': 'gauntlet'}, {'id': 8019, 'synset': 'gauze.n.02', 'name': 'gauze'}, {'id': 8020, 'synset': 'gauze.n.01', 'name': 'gauze'}, {'id': 8021, 'synset': 'gavel.n.01', 'name': 'gavel'}, {'id': 8022, 'synset': 'gazebo.n.01', 'name': 'gazebo'}, {'id': 8023, 'synset': 'gear.n.01', 'name': 'gear'}, {'id': 8024, 'synset': 'gear.n.04', 'name': 'gear'}, {'id': 8025, 'synset': 'gear.n.03', 'name': 'gear'}, {'id': 8026, 'synset': 'gearbox.n.01', 'name': 'gearbox'}, {'id': 8027, 'synset': 'gearing.n.01', 'name': 'gearing'}, {'id': 8028, 'synset': 'gearset.n.01', 'name': 'gearset'}, {'id': 8029, 'synset': 'gearshift.n.01', 'name': 'gearshift'}, {'id': 8030, 'synset': 'geiger_counter.n.01', 'name': 'Geiger_counter'}, {'id': 8031, 'synset': 'geiger_tube.n.01', 'name': 'Geiger_tube'}, {'id': 8032, 'synset': 'gene_chip.n.01', 'name': 'gene_chip'}, {'id': 8033, 'synset': 'general-purpose_bomb.n.01', 'name': 'general-purpose_bomb'}, {'id': 8034, 'synset': 'generator.n.01', 'name': 'generator'}, {'id': 8035, 'synset': 'generator.n.04', 'name': 'generator'}, {'id': 8036, 'synset': 'geneva_gown.n.01', 'name': 'Geneva_gown'}, {'id': 8037, 'synset': 'geodesic_dome.n.01', 'name': 'geodesic_dome'}, {'id': 8038, 'synset': 'georgette.n.01', 'name': 'georgette'}, {'id': 8039, 'synset': 'gharry.n.01', 'name': 'gharry'}, {'id': 8040, 'synset': 'ghat.n.01', 'name': 'ghat'}, {'id': 8041, 'synset': 'ghetto_blaster.n.01', 'name': 'ghetto_blaster'}, {'id': 8042, 'synset': 'gift_shop.n.01', 'name': 'gift_shop'}, {'id': 8043, 'synset': 'gift_wrapping.n.01', 'name': 'gift_wrapping'}, {'id': 8044, 'synset': 'gig.n.05', 'name': 'gig'}, {'id': 8045, 'synset': 'gig.n.04', 'name': 'gig'}, {'id': 8046, 'synset': 'gig.n.01', 'name': 'gig'}, {'id': 8047, 'synset': 'gig.n.03', 'name': 'gig'}, {'id': 8048, 'synset': 'gildhall.n.01', 'name': 'gildhall'}, {'id': 8049, 'synset': 'gill_net.n.01', 'name': 'gill_net'}, {'id': 8050, 'synset': 'gilt.n.01', 'name': 'gilt'}, {'id': 8051, 'synset': 'gimbal.n.01', 'name': 'gimbal'}, {'id': 8052, 'synset': 'gingham.n.01', 'name': 'gingham'}, {'id': 8053, 'synset': 'girandole.n.01', 'name': 'girandole'}, {'id': 8054, 'synset': 'girder.n.01', 'name': 'girder'}, {'id': 8055, 'synset': 'glass.n.07', 'name': 'glass'}, {'id': 8056, 'synset': 'glass_cutter.n.03', 'name': 'glass_cutter'}, {'id': 8057, 'synset': 'glasses_case.n.01', 'name': 'glasses_case'}, {'id': 8058, 'synset': 'glebe_house.n.01', 'name': 'glebe_house'}, {'id': 8059, 'synset': 'glengarry.n.01', 'name': 'Glengarry'}, {'id': 8060, 'synset': 'glider.n.01', 'name': 'glider'}, {'id': 8061, 'synset': 'global_positioning_system.n.01', 'name': 'Global_Positioning_System'}, {'id': 8062, 'synset': 'glockenspiel.n.01', 'name': 'glockenspiel'}, {'id': 8063, 'synset': 'glory_hole.n.01', 'name': 'glory_hole'}, {'id': 8064, 'synset': 'glove_compartment.n.01', 'name': 'glove_compartment'}, {'id': 8065, 'synset': 'glow_lamp.n.01', 'name': 'glow_lamp'}, {'id': 8066, 'synset': 'glow_tube.n.01', 'name': 'glow_tube'}, {'id': 8067, 'synset': 'glyptic_art.n.01', 'name': 'glyptic_art'}, {'id': 8068, 'synset': 'glyptics.n.01', 'name': 'glyptics'}, {'id': 8069, 'synset': 'gnomon.n.01', 'name': 'gnomon'}, {'id': 8070, 'synset': 'goal.n.03', 'name': 'goal'}, {'id': 8071, 'synset': 'goalmouth.n.01', 'name': 'goalmouth'}, {'id': 8072, 'synset': 'goalpost.n.01', 'name': 'goalpost'}, {'id': 8073, 'synset': 'goblet.n.01', 'name': 'goblet'}, {'id': 8074, 'synset': 'godown.n.01', 'name': 'godown'}, {'id': 8075, 'synset': 'go-kart.n.01', 'name': 'go-kart'}, {'id': 8076, 'synset': 'gold_plate.n.02', 'name': 'gold_plate'}, {'id': 8077, 'synset': 'golf_bag.n.01', 'name': 'golf_bag'}, {'id': 8078, 'synset': 'golf_ball.n.01', 'name': 'golf_ball'}, {'id': 8079, 'synset': 'golf-club_head.n.01', 'name': 'golf-club_head'}, {'id': 8080, 'synset': 'golf_equipment.n.01', 'name': 'golf_equipment'}, {'id': 8081, 'synset': 'golf_glove.n.01', 'name': 'golf_glove'}, {'id': 8082, 'synset': 'golliwog.n.01', 'name': 'golliwog'}, {'id': 8083, 'synset': 'gong.n.01', 'name': 'gong'}, {'id': 8084, 'synset': 'goniometer.n.01', 'name': 'goniometer'}, {'id': 8085, 'synset': 'gordian_knot.n.02', 'name': 'Gordian_knot'}, {'id': 8086, 'synset': 'gorget.n.01', 'name': 'gorget'}, {'id': 8087, 'synset': 'gossamer.n.01', 'name': 'gossamer'}, {'id': 8088, 'synset': 'gothic_arch.n.01', 'name': 'Gothic_arch'}, {'id': 8089, 'synset': 'gouache.n.01', 'name': 'gouache'}, {'id': 8090, 'synset': 'gouge.n.02', 'name': 'gouge'}, {'id': 8091, 'synset': 'gourd.n.01', 'name': 'gourd'}, {'id': 8092, 'synset': 'government_building.n.01', 'name': 'government_building'}, {'id': 8093, 'synset': 'government_office.n.01', 'name': 'government_office'}, {'id': 8094, 'synset': 'gown.n.01', 'name': 'gown'}, {'id': 8095, 'synset': 'gown.n.05', 'name': 'gown'}, {'id': 8096, 'synset': 'gown.n.04', 'name': 'gown'}, {'id': 8097, 'synset': 'grab.n.01', 'name': 'grab'}, {'id': 8098, 'synset': 'grab_bag.n.02', 'name': 'grab_bag'}, {'id': 8099, 'synset': 'grab_bar.n.01', 'name': 'grab_bar'}, {'id': 8100, 'synset': 'grace_cup.n.01', 'name': 'grace_cup'}, {'id': 8101, 'synset': 'grade_separation.n.01', 'name': 'grade_separation'}, {'id': 8102, 'synset': 'graduated_cylinder.n.01', 'name': 'graduated_cylinder'}, {'id': 8103, 'synset': 'graffito.n.01', 'name': 'graffito'}, {'id': 8104, 'synset': 'gramophone.n.01', 'name': 'gramophone'}, {'id': 8105, 'synset': 'granary.n.01', 'name': 'granary'}, {'id': 8106, 'synset': 'grandfather_clock.n.01', 'name': 'grandfather_clock'}, {'id': 8107, 'synset': 'grand_piano.n.01', 'name': 'grand_piano'}, {'id': 8108, 'synset': 'graniteware.n.01', 'name': 'graniteware'}, {'id': 8109, 'synset': 'granny_knot.n.01', 'name': 'granny_knot'}, {'id': 8110, 'synset': 'grape_arbor.n.01', 'name': 'grape_arbor'}, {'id': 8111, 'synset': 'grapnel.n.02', 'name': 'grapnel'}, {'id': 8112, 'synset': 'grapnel.n.01', 'name': 'grapnel'}, {'id': 8113, 'synset': 'grass_skirt.n.01', 'name': 'grass_skirt'}, {'id': 8114, 'synset': 'grate.n.01', 'name': 'grate'}, {'id': 8115, 'synset': 'grate.n.03', 'name': 'grate'}, {'id': 8116, 'synset': 'graver.n.01', 'name': 'graver'}, {'id': 8117, 'synset': 'gravimeter.n.02', 'name': 'gravimeter'}, {'id': 8118, 'synset': 'gravure.n.03', 'name': 'gravure'}, {'id': 8119, 'synset': 'grey.n.06', 'name': 'grey'}, {'id': 8120, 'synset': 'grease-gun.n.01', 'name': 'grease-gun'}, {'id': 8121, 'synset': 'greasepaint.n.01', 'name': 'greasepaint'}, {'id': 8122, 'synset': 'greasy_spoon.n.01', 'name': 'greasy_spoon'}, {'id': 8123, 'synset': 'greatcoat.n.01', 'name': 'greatcoat'}, {'id': 8124, 'synset': 'great_hall.n.01', 'name': 'great_hall'}, {'id': 8125, 'synset': 'greave.n.01', 'name': 'greave'}, {'id': 8126, 'synset': 'greengrocery.n.02', 'name': 'greengrocery'}, {'id': 8127, 'synset': 'greenhouse.n.01', 'name': 'greenhouse'}, {'id': 8128, 'synset': 'grenade.n.01', 'name': 'grenade'}, {'id': 8129, 'synset': 'grid.n.05', 'name': 'grid'}, {'id': 8130, 'synset': 'grille.n.02', 'name': 'grille'}, {'id': 8131, 'synset': 'grillroom.n.01', 'name': 'grillroom'}, {'id': 8132, 'synset': 'grinder.n.04', 'name': 'grinder'}, {'id': 8133, 'synset': 'grinding_wheel.n.01', 'name': 'grinding_wheel'}, {'id': 8134, 'synset': 'grindstone.n.01', 'name': 'grindstone'}, {'id': 8135, 'synset': 'gripsack.n.01', 'name': 'gripsack'}, {'id': 8136, 'synset': 'gristmill.n.01', 'name': 'gristmill'}, {'id': 8137, 'synset': 'grocery_store.n.01', 'name': 'grocery_store'}, {'id': 8138, 'synset': 'grogram.n.01', 'name': 'grogram'}, {'id': 8139, 'synset': 'groined_vault.n.01', 'name': 'groined_vault'}, {'id': 8140, 'synset': 'groover.n.01', 'name': 'groover'}, {'id': 8141, 'synset': 'grosgrain.n.01', 'name': 'grosgrain'}, {'id': 8142, 'synset': 'gros_point.n.01', 'name': 'gros_point'}, {'id': 8143, 'synset': 'ground.n.09', 'name': 'ground'}, {'id': 8144, 'synset': 'ground_bait.n.01', 'name': 'ground_bait'}, {'id': 8145, 'synset': 'ground_control.n.01', 'name': 'ground_control'}, {'id': 8146, 'synset': 'ground_floor.n.01', 'name': 'ground_floor'}, {'id': 8147, 'synset': 'groundsheet.n.01', 'name': 'groundsheet'}, {'id': 8148, 'synset': 'g-string.n.01', 'name': 'G-string'}, {'id': 8149, 'synset': 'guard.n.03', 'name': 'guard'}, {'id': 8150, 'synset': 'guard_boat.n.01', 'name': 'guard_boat'}, {'id': 8151, 'synset': 'guardroom.n.02', 'name': 'guardroom'}, {'id': 8152, 'synset': 'guardroom.n.01', 'name': 'guardroom'}, {'id': 8153, 'synset': 'guard_ship.n.01', 'name': 'guard_ship'}, {'id': 8154, 'synset': "guard's_van.n.01", 'name': "guard's_van"}, {'id': 8155, 'synset': 'gueridon.n.01', 'name': 'gueridon'}, {'id': 8156, 'synset': 'guarnerius.n.03', 'name': 'Guarnerius'}, {'id': 8157, 'synset': 'guesthouse.n.01', 'name': 'guesthouse'}, {'id': 8158, 'synset': 'guestroom.n.01', 'name': 'guestroom'}, {'id': 8159, 'synset': 'guidance_system.n.01', 'name': 'guidance_system'}, {'id': 8160, 'synset': 'guided_missile.n.01', 'name': 'guided_missile'}, {'id': 8161, 'synset': 'guided_missile_cruiser.n.01', 'name': 'guided_missile_cruiser'}, {'id': 8162, 'synset': 'guided_missile_frigate.n.01', 'name': 'guided_missile_frigate'}, {'id': 8163, 'synset': 'guildhall.n.01', 'name': 'guildhall'}, {'id': 8164, 'synset': 'guilloche.n.01', 'name': 'guilloche'}, {'id': 8165, 'synset': 'guillotine.n.02', 'name': 'guillotine'}, {'id': 8166, 'synset': 'guimpe.n.02', 'name': 'guimpe'}, {'id': 8167, 'synset': 'guimpe.n.01', 'name': 'guimpe'}, {'id': 8168, 'synset': 'guitar_pick.n.01', 'name': 'guitar_pick'}, {'id': 8169, 'synset': 'gulag.n.01', 'name': 'gulag'}, {'id': 8170, 'synset': 'gunboat.n.01', 'name': 'gunboat'}, {'id': 8171, 'synset': 'gun_carriage.n.01', 'name': 'gun_carriage'}, {'id': 8172, 'synset': 'gun_case.n.01', 'name': 'gun_case'}, {'id': 8173, 'synset': 'gun_emplacement.n.01', 'name': 'gun_emplacement'}, {'id': 8174, 'synset': 'gun_enclosure.n.01', 'name': 'gun_enclosure'}, {'id': 8175, 'synset': 'gunlock.n.01', 'name': 'gunlock'}, {'id': 8176, 'synset': 'gunnery.n.01', 'name': 'gunnery'}, {'id': 8177, 'synset': 'gunnysack.n.01', 'name': 'gunnysack'}, {'id': 8178, 'synset': 'gun_pendulum.n.01', 'name': 'gun_pendulum'}, {'id': 8179, 'synset': 'gun_room.n.01', 'name': 'gun_room'}, {'id': 8180, 'synset': 'gunsight.n.01', 'name': 'gunsight'}, {'id': 8181, 'synset': 'gun_trigger.n.01', 'name': 'gun_trigger'}, {'id': 8182, 'synset': 'gurney.n.01', 'name': 'gurney'}, {'id': 8183, 'synset': 'gusher.n.01', 'name': 'gusher'}, {'id': 8184, 'synset': 'gusset.n.03', 'name': 'gusset'}, {'id': 8185, 'synset': 'gusset.n.02', 'name': 'gusset'}, {'id': 8186, 'synset': 'guy.n.03', 'name': 'guy'}, {'id': 8187, 'synset': 'gymnastic_apparatus.n.01', 'name': 'gymnastic_apparatus'}, {'id': 8188, 'synset': 'gym_shoe.n.01', 'name': 'gym_shoe'}, {'id': 8189, 'synset': 'gym_suit.n.01', 'name': 'gym_suit'}, {'id': 8190, 'synset': 'gymslip.n.01', 'name': 'gymslip'}, {'id': 8191, 'synset': 'gypsy_cab.n.01', 'name': 'gypsy_cab'}, {'id': 8192, 'synset': 'gyrocompass.n.01', 'name': 'gyrocompass'}, {'id': 8193, 'synset': 'gyroscope.n.01', 'name': 'gyroscope'}, {'id': 8194, 'synset': 'gyrostabilizer.n.01', 'name': 'gyrostabilizer'}, {'id': 8195, 'synset': 'habergeon.n.01', 'name': 'habergeon'}, {'id': 8196, 'synset': 'habit.n.03', 'name': 'habit'}, {'id': 8197, 'synset': 'habit.n.05', 'name': 'habit'}, {'id': 8198, 'synset': 'hacienda.n.02', 'name': 'hacienda'}, {'id': 8199, 'synset': 'hacksaw.n.01', 'name': 'hacksaw'}, {'id': 8200, 'synset': 'haft.n.01', 'name': 'haft'}, {'id': 8201, 'synset': 'haircloth.n.01', 'name': 'haircloth'}, {'id': 8202, 'synset': 'hairdressing.n.01', 'name': 'hairdressing'}, {'id': 8203, 'synset': 'hairpiece.n.01', 'name': 'hairpiece'}, {'id': 8204, 'synset': 'hair_shirt.n.01', 'name': 'hair_shirt'}, {'id': 8205, 'synset': 'hair_slide.n.01', 'name': 'hair_slide'}, {'id': 8206, 'synset': 'hair_spray.n.01', 'name': 'hair_spray'}, {'id': 8207, 'synset': 'hairspring.n.01', 'name': 'hairspring'}, {'id': 8208, 'synset': 'hair_trigger.n.01', 'name': 'hair_trigger'}, {'id': 8209, 'synset': 'halberd.n.01', 'name': 'halberd'}, {'id': 8210, 'synset': 'half_binding.n.01', 'name': 'half_binding'}, {'id': 8211, 'synset': 'half_hatchet.n.01', 'name': 'half_hatchet'}, {'id': 8212, 'synset': 'half_hitch.n.01', 'name': 'half_hitch'}, {'id': 8213, 'synset': 'half_track.n.01', 'name': 'half_track'}, {'id': 8214, 'synset': 'hall.n.13', 'name': 'hall'}, {'id': 8215, 'synset': 'hall.n.03', 'name': 'hall'}, {'id': 8216, 'synset': 'hall.n.12', 'name': 'hall'}, {'id': 8217, 'synset': 'hall_of_fame.n.01', 'name': 'Hall_of_Fame'}, {'id': 8218, 'synset': 'hall_of_residence.n.01', 'name': 'hall_of_residence'}, {'id': 8219, 'synset': 'hallstand.n.01', 'name': 'hallstand'}, {'id': 8220, 'synset': 'halter.n.01', 'name': 'halter'}, {'id': 8221, 'synset': 'hame.n.01', 'name': 'hame'}, {'id': 8222, 'synset': 'hammer.n.07', 'name': 'hammer'}, {'id': 8223, 'synset': 'hammer.n.05', 'name': 'hammer'}, {'id': 8224, 'synset': 'hammerhead.n.02', 'name': 'hammerhead'}, {'id': 8225, 'synset': 'hand.n.08', 'name': 'hand'}, {'id': 8226, 'synset': 'handball.n.01', 'name': 'handball'}, {'id': 8227, 'synset': 'handbarrow.n.01', 'name': 'handbarrow'}, {'id': 8228, 'synset': 'handbell.n.01', 'name': 'handbell'}, {'id': 8229, 'synset': 'handbow.n.01', 'name': 'handbow'}, {'id': 8230, 'synset': 'hand_brake.n.01', 'name': 'hand_brake'}, {'id': 8231, 'synset': 'hand_calculator.n.01', 'name': 'hand_calculator'}, {'id': 8232, 'synset': 'handcar.n.01', 'name': 'handcar'}, {'id': 8233, 'synset': 'hand_cream.n.01', 'name': 'hand_cream'}, {'id': 8234, 'synset': 'hand_drill.n.01', 'name': 'hand_drill'}, {'id': 8235, 'synset': 'hand_glass.n.02', 'name': 'hand_glass'}, {'id': 8236, 'synset': 'hand_grenade.n.01', 'name': 'hand_grenade'}, {'id': 8237, 'synset': 'hand-held_computer.n.01', 'name': 'hand-held_computer'}, {'id': 8238, 'synset': 'handhold.n.01', 'name': 'handhold'}, {'id': 8239, 'synset': 'handlebar.n.01', 'name': 'handlebar'}, {'id': 8240, 'synset': 'handloom.n.01', 'name': 'handloom'}, {'id': 8241, 'synset': 'hand_lotion.n.01', 'name': 'hand_lotion'}, {'id': 8242, 'synset': 'hand_luggage.n.01', 'name': 'hand_luggage'}, {'id': 8243, 'synset': 'hand-me-down.n.01', 'name': 'hand-me-down'}, {'id': 8244, 'synset': 'hand_mower.n.01', 'name': 'hand_mower'}, {'id': 8245, 'synset': 'hand_pump.n.01', 'name': 'hand_pump'}, {'id': 8246, 'synset': 'handrest.n.01', 'name': 'handrest'}, {'id': 8247, 'synset': 'handset.n.01', 'name': 'handset'}, {'id': 8248, 'synset': 'hand_shovel.n.01', 'name': 'hand_shovel'}, {'id': 8249, 'synset': 'handspike.n.01', 'name': 'handspike'}, {'id': 8250, 'synset': 'handstamp.n.01', 'name': 'handstamp'}, {'id': 8251, 'synset': 'hand_throttle.n.01', 'name': 'hand_throttle'}, {'id': 8252, 'synset': 'hand_tool.n.01', 'name': 'hand_tool'}, {'id': 8253, 'synset': 'hand_truck.n.01', 'name': 'hand_truck'}, {'id': 8254, 'synset': 'handwear.n.01', 'name': 'handwear'}, {'id': 8255, 'synset': 'handwheel.n.02', 'name': 'handwheel'}, {'id': 8256, 'synset': 'handwheel.n.01', 'name': 'handwheel'}, {'id': 8257, 'synset': 'hangar_queen.n.01', 'name': 'hangar_queen'}, {'id': 8258, 'synset': 'hanger.n.02', 'name': 'hanger'}, {'id': 8259, 'synset': 'hang_glider.n.02', 'name': 'hang_glider'}, {'id': 8260, 'synset': "hangman's_rope.n.01", 'name': "hangman's_rope"}, {'id': 8261, 'synset': 'hank.n.01', 'name': 'hank'}, {'id': 8262, 'synset': 'hansom.n.01', 'name': 'hansom'}, {'id': 8263, 'synset': 'harbor.n.02', 'name': 'harbor'}, {'id': 8264, 'synset': 'hard_disc.n.01', 'name': 'hard_disc'}, {'id': 8265, 'synset': 'hard_hat.n.02', 'name': 'hard_hat'}, {'id': 8266, 'synset': 'hardtop.n.01', 'name': 'hardtop'}, {'id': 8267, 'synset': 'hardware.n.02', 'name': 'hardware'}, {'id': 8268, 'synset': 'hardware_store.n.01', 'name': 'hardware_store'}, {'id': 8269, 'synset': 'harmonica.n.01', 'name': 'harmonica'}, {'id': 8270, 'synset': 'harness.n.02', 'name': 'harness'}, {'id': 8271, 'synset': 'harness.n.01', 'name': 'harness'}, {'id': 8272, 'synset': 'harp.n.01', 'name': 'harp'}, {'id': 8273, 'synset': 'harp.n.02', 'name': 'harp'}, {'id': 8274, 'synset': 'harpoon.n.01', 'name': 'harpoon'}, {'id': 8275, 'synset': 'harpoon_gun.n.01', 'name': 'harpoon_gun'}, {'id': 8276, 'synset': 'harpoon_log.n.01', 'name': 'harpoon_log'}, {'id': 8277, 'synset': 'harpsichord.n.01', 'name': 'harpsichord'}, {'id': 8278, 'synset': 'harris_tweed.n.01', 'name': 'Harris_Tweed'}, {'id': 8279, 'synset': 'harrow.n.01', 'name': 'harrow'}, {'id': 8280, 'synset': 'harvester.n.02', 'name': 'harvester'}, {'id': 8281, 'synset': 'hash_house.n.01', 'name': 'hash_house'}, {'id': 8282, 'synset': 'hasp.n.01', 'name': 'hasp'}, {'id': 8283, 'synset': 'hatch.n.03', 'name': 'hatch'}, {'id': 8284, 'synset': 'hatchback.n.02', 'name': 'hatchback'}, {'id': 8285, 'synset': 'hatchback.n.01', 'name': 'hatchback'}, {'id': 8286, 'synset': 'hatchel.n.01', 'name': 'hatchel'}, {'id': 8287, 'synset': 'hatchet.n.02', 'name': 'hatchet'}, {'id': 8288, 'synset': 'hatpin.n.01', 'name': 'hatpin'}, {'id': 8289, 'synset': 'hauberk.n.01', 'name': 'hauberk'}, {'id': 8290, 'synset': 'hawaiian_guitar.n.01', 'name': 'Hawaiian_guitar'}, {'id': 8291, 'synset': 'hawse.n.01', 'name': 'hawse'}, {'id': 8292, 'synset': 'hawser.n.01', 'name': 'hawser'}, {'id': 8293, 'synset': 'hawser_bend.n.01', 'name': 'hawser_bend'}, {'id': 8294, 'synset': 'hay_bale.n.01', 'name': 'hay_bale'}, {'id': 8295, 'synset': 'hayfork.n.01', 'name': 'hayfork'}, {'id': 8296, 'synset': 'hayloft.n.01', 'name': 'hayloft'}, {'id': 8297, 'synset': 'haymaker.n.01', 'name': 'haymaker'}, {'id': 8298, 'synset': 'hayrack.n.02', 'name': 'hayrack'}, {'id': 8299, 'synset': 'hayrack.n.01', 'name': 'hayrack'}, {'id': 8300, 'synset': 'hazard.n.03', 'name': 'hazard'}, {'id': 8301, 'synset': 'head.n.31', 'name': 'head'}, {'id': 8302, 'synset': 'head.n.30', 'name': 'head'}, {'id': 8303, 'synset': 'head.n.29', 'name': 'head'}, {'id': 8304, 'synset': 'headdress.n.01', 'name': 'headdress'}, {'id': 8305, 'synset': 'header.n.05', 'name': 'header'}, {'id': 8306, 'synset': 'header.n.04', 'name': 'header'}, {'id': 8307, 'synset': 'header.n.03', 'name': 'header'}, {'id': 8308, 'synset': 'header.n.02', 'name': 'header'}, {'id': 8309, 'synset': 'headfast.n.01', 'name': 'headfast'}, {'id': 8310, 'synset': 'head_gasket.n.01', 'name': 'head_gasket'}, {'id': 8311, 'synset': 'head_gate.n.02', 'name': 'head_gate'}, {'id': 8312, 'synset': 'headgear.n.03', 'name': 'headgear'}, {'id': 8313, 'synset': 'headpiece.n.02', 'name': 'headpiece'}, {'id': 8314, 'synset': 'headpin.n.01', 'name': 'headpin'}, {'id': 8315, 'synset': 'headquarters.n.01', 'name': 'headquarters'}, {'id': 8316, 'synset': 'headrace.n.01', 'name': 'headrace'}, {'id': 8317, 'synset': 'headrest.n.02', 'name': 'headrest'}, {'id': 8318, 'synset': 'headsail.n.01', 'name': 'headsail'}, {'id': 8319, 'synset': 'head_shop.n.01', 'name': 'head_shop'}, {'id': 8320, 'synset': 'headstock.n.01', 'name': 'headstock'}, {'id': 8321, 'synset': 'health_spa.n.01', 'name': 'health_spa'}, {'id': 8322, 'synset': 'hearing_aid.n.02', 'name': 'hearing_aid'}, {'id': 8323, 'synset': 'hearing_aid.n.01', 'name': 'hearing_aid'}, {'id': 8324, 'synset': 'hearse.n.01', 'name': 'hearse'}, {'id': 8325, 'synset': 'hearth.n.02', 'name': 'hearth'}, {'id': 8326, 'synset': 'hearthrug.n.01', 'name': 'hearthrug'}, {'id': 8327, 'synset': 'heart-lung_machine.n.01', 'name': 'heart-lung_machine'}, {'id': 8328, 'synset': 'heat_engine.n.01', 'name': 'heat_engine'}, {'id': 8329, 'synset': 'heat_exchanger.n.01', 'name': 'heat_exchanger'}, {'id': 8330, 'synset': 'heating_pad.n.01', 'name': 'heating_pad'}, {'id': 8331, 'synset': 'heat_lamp.n.01', 'name': 'heat_lamp'}, {'id': 8332, 'synset': 'heat_pump.n.01', 'name': 'heat_pump'}, {'id': 8333, 'synset': 'heat-seeking_missile.n.01', 'name': 'heat-seeking_missile'}, {'id': 8334, 'synset': 'heat_shield.n.01', 'name': 'heat_shield'}, {'id': 8335, 'synset': 'heat_sink.n.01', 'name': 'heat_sink'}, {'id': 8336, 'synset': 'heaume.n.01', 'name': 'heaume'}, {'id': 8337, 'synset': 'heaver.n.01', 'name': 'heaver'}, {'id': 8338, 'synset': 'heavier-than-air_craft.n.01', 'name': 'heavier-than-air_craft'}, {'id': 8339, 'synset': 'heckelphone.n.01', 'name': 'heckelphone'}, {'id': 8340, 'synset': 'hectograph.n.01', 'name': 'hectograph'}, {'id': 8341, 'synset': 'hedge.n.01', 'name': 'hedge'}, {'id': 8342, 'synset': 'hedge_trimmer.n.01', 'name': 'hedge_trimmer'}, {'id': 8343, 'synset': 'helicon.n.01', 'name': 'helicon'}, {'id': 8344, 'synset': 'heliograph.n.01', 'name': 'heliograph'}, {'id': 8345, 'synset': 'heliometer.n.01', 'name': 'heliometer'}, {'id': 8346, 'synset': 'helm.n.01', 'name': 'helm'}, {'id': 8347, 'synset': 'helmet.n.01', 'name': 'helmet'}, {'id': 8348, 'synset': 'hematocrit.n.02', 'name': 'hematocrit'}, {'id': 8349, 'synset': 'hemming-stitch.n.01', 'name': 'hemming-stitch'}, {'id': 8350, 'synset': 'hemostat.n.01', 'name': 'hemostat'}, {'id': 8351, 'synset': 'hemstitch.n.01', 'name': 'hemstitch'}, {'id': 8352, 'synset': 'henroost.n.01', 'name': 'henroost'}, {'id': 8353, 'synset': 'heraldry.n.02', 'name': 'heraldry'}, {'id': 8354, 'synset': 'hermitage.n.01', 'name': 'hermitage'}, {'id': 8355, 'synset': 'herringbone.n.01', 'name': 'herringbone'}, {'id': 8356, 'synset': 'herringbone.n.02', 'name': 'herringbone'}, {'id': 8357, 'synset': 'herschelian_telescope.n.01', 'name': 'Herschelian_telescope'}, {'id': 8358, 'synset': 'hessian_boot.n.01', 'name': 'Hessian_boot'}, {'id': 8359, 'synset': 'heterodyne_receiver.n.01', 'name': 'heterodyne_receiver'}, {'id': 8360, 'synset': 'hibachi.n.01', 'name': 'hibachi'}, {'id': 8361, 'synset': 'hideaway.n.02', 'name': 'hideaway'}, {'id': 8362, 'synset': 'hi-fi.n.01', 'name': 'hi-fi'}, {'id': 8363, 'synset': 'high_altar.n.01', 'name': 'high_altar'}, {'id': 8364, 'synset': 'high-angle_gun.n.01', 'name': 'high-angle_gun'}, {'id': 8365, 'synset': 'highball_glass.n.01', 'name': 'highball_glass'}, {'id': 8366, 'synset': 'highboard.n.01', 'name': 'highboard'}, {'id': 8367, 'synset': 'highboy.n.01', 'name': 'highboy'}, {'id': 8368, 'synset': 'high_gear.n.01', 'name': 'high_gear'}, {'id': 8369, 'synset': 'high-hat_cymbal.n.01', 'name': 'high-hat_cymbal'}, {'id': 8370, 'synset': 'highlighter.n.02', 'name': 'highlighter'}, {'id': 8371, 'synset': 'highlighter.n.01', 'name': 'highlighter'}, {'id': 8372, 'synset': 'high-pass_filter.n.01', 'name': 'high-pass_filter'}, {'id': 8373, 'synset': 'high-rise.n.01', 'name': 'high-rise'}, {'id': 8374, 'synset': 'high_table.n.01', 'name': 'high_table'}, {'id': 8375, 'synset': 'high-warp_loom.n.01', 'name': 'high-warp_loom'}, {'id': 8376, 'synset': 'hijab.n.01', 'name': 'hijab'}, {'id': 8377, 'synset': 'hinging_post.n.01', 'name': 'hinging_post'}, {'id': 8378, 'synset': 'hip_boot.n.01', 'name': 'hip_boot'}, {'id': 8379, 'synset': 'hipflask.n.01', 'name': 'hipflask'}, {'id': 8380, 'synset': 'hip_pad.n.01', 'name': 'hip_pad'}, {'id': 8381, 'synset': 'hip_pocket.n.01', 'name': 'hip_pocket'}, {'id': 8382, 'synset': 'hippodrome.n.01', 'name': 'hippodrome'}, {'id': 8383, 'synset': 'hip_roof.n.01', 'name': 'hip_roof'}, {'id': 8384, 'synset': 'hitch.n.05', 'name': 'hitch'}, {'id': 8385, 'synset': 'hitch.n.04', 'name': 'hitch'}, {'id': 8386, 'synset': 'hitching_post.n.01', 'name': 'hitching_post'}, {'id': 8387, 'synset': 'hitchrack.n.01', 'name': 'hitchrack'}, {'id': 8388, 'synset': 'hob.n.03', 'name': 'hob'}, {'id': 8389, 'synset': 'hobble_skirt.n.01', 'name': 'hobble_skirt'}, {'id': 8390, 'synset': 'hockey_skate.n.01', 'name': 'hockey_skate'}, {'id': 8391, 'synset': 'hod.n.01', 'name': 'hod'}, {'id': 8392, 'synset': 'hodoscope.n.01', 'name': 'hodoscope'}, {'id': 8393, 'synset': 'hoe.n.01', 'name': 'hoe'}, {'id': 8394, 'synset': 'hoe_handle.n.01', 'name': 'hoe_handle'}, {'id': 8395, 'synset': 'hogshead.n.02', 'name': 'hogshead'}, {'id': 8396, 'synset': 'hoist.n.01', 'name': 'hoist'}, {'id': 8397, 'synset': 'hold.n.07', 'name': 'hold'}, {'id': 8398, 'synset': 'holder.n.01', 'name': 'holder'}, {'id': 8399, 'synset': 'holding_cell.n.01', 'name': 'holding_cell'}, {'id': 8400, 'synset': 'holding_device.n.01', 'name': 'holding_device'}, {'id': 8401, 'synset': 'holding_pen.n.01', 'name': 'holding_pen'}, {'id': 8402, 'synset': 'hollowware.n.01', 'name': 'hollowware'}, {'id': 8403, 'synset': 'holster.n.01', 'name': 'holster'}, {'id': 8404, 'synset': 'holster.n.02', 'name': 'holster'}, {'id': 8405, 'synset': 'holy_of_holies.n.02', 'name': 'holy_of_holies'}, {'id': 8406, 'synset': 'home.n.09', 'name': 'home'}, {'id': 8407, 'synset': 'home_appliance.n.01', 'name': 'home_appliance'}, {'id': 8408, 'synset': 'home_computer.n.01', 'name': 'home_computer'}, {'id': 8409, 'synset': 'home_room.n.01', 'name': 'home_room'}, {'id': 8410, 'synset': 'homespun.n.01', 'name': 'homespun'}, {'id': 8411, 'synset': 'homestead.n.03', 'name': 'homestead'}, {'id': 8412, 'synset': 'home_theater.n.01', 'name': 'home_theater'}, {'id': 8413, 'synset': 'homing_torpedo.n.01', 'name': 'homing_torpedo'}, {'id': 8414, 'synset': 'hone.n.01', 'name': 'hone'}, {'id': 8415, 'synset': 'honeycomb.n.02', 'name': 'honeycomb'}, {'id': 8416, 'synset': 'hood.n.09', 'name': 'hood'}, {'id': 8417, 'synset': 'hood.n.08', 'name': 'hood'}, {'id': 8418, 'synset': 'hood.n.07', 'name': 'hood'}, {'id': 8419, 'synset': 'hood.n.05', 'name': 'hood'}, {'id': 8420, 'synset': 'hood_latch.n.01', 'name': 'hood_latch'}, {'id': 8421, 'synset': 'hook.n.04', 'name': 'hook'}, {'id': 8422, 'synset': 'hook.n.01', 'name': 'hook'}, {'id': 8423, 'synset': 'hook_and_eye.n.01', 'name': 'hook_and_eye'}, {'id': 8424, 'synset': 'hookup.n.02', 'name': 'hookup'}, {'id': 8425, 'synset': 'hookup.n.01', 'name': 'hookup'}, {'id': 8426, 'synset': 'hook_wrench.n.01', 'name': 'hook_wrench'}, {'id': 8427, 'synset': 'hoopskirt.n.01', 'name': 'hoopskirt'}, {'id': 8428, 'synset': 'hoosegow.n.01', 'name': 'hoosegow'}, {'id': 8429, 'synset': 'hoover.n.04', 'name': 'Hoover'}, {'id': 8430, 'synset': 'hope_chest.n.01', 'name': 'hope_chest'}, {'id': 8431, 'synset': 'hopper.n.01', 'name': 'hopper'}, {'id': 8432, 'synset': 'hopsacking.n.01', 'name': 'hopsacking'}, {'id': 8433, 'synset': 'horizontal_bar.n.01', 'name': 'horizontal_bar'}, {'id': 8434, 'synset': 'horizontal_stabilizer.n.01', 'name': 'horizontal_stabilizer'}, {'id': 8435, 'synset': 'horizontal_tail.n.01', 'name': 'horizontal_tail'}, {'id': 8436, 'synset': 'horn.n.09', 'name': 'horn'}, {'id': 8437, 'synset': 'horn.n.01', 'name': 'horn'}, {'id': 8438, 'synset': 'horn.n.08', 'name': 'horn'}, {'id': 8439, 'synset': 'horn_button.n.01', 'name': 'horn_button'}, {'id': 8440, 'synset': 'hornpipe.n.03', 'name': 'hornpipe'}, {'id': 8441, 'synset': 'horse.n.02', 'name': 'horse'}, {'id': 8442, 'synset': 'horsebox.n.01', 'name': 'horsebox'}, {'id': 8443, 'synset': 'horsecar.n.01', 'name': 'horsecar'}, {'id': 8444, 'synset': 'horse_cart.n.01', 'name': 'horse_cart'}, {'id': 8445, 'synset': 'horsecloth.n.01', 'name': 'horsecloth'}, {'id': 8446, 'synset': 'horse-drawn_vehicle.n.01', 'name': 'horse-drawn_vehicle'}, {'id': 8447, 'synset': 'horsehair.n.02', 'name': 'horsehair'}, {'id': 8448, 'synset': 'horsehair_wig.n.01', 'name': 'horsehair_wig'}, {'id': 8449, 'synset': 'horseless_carriage.n.01', 'name': 'horseless_carriage'}, {'id': 8450, 'synset': 'horse_pistol.n.01', 'name': 'horse_pistol'}, {'id': 8451, 'synset': 'horseshoe.n.02', 'name': 'horseshoe'}, {'id': 8452, 'synset': 'horseshoe.n.01', 'name': 'horseshoe'}, {'id': 8453, 'synset': 'horse-trail.n.01', 'name': 'horse-trail'}, {'id': 8454, 'synset': 'horsewhip.n.01', 'name': 'horsewhip'}, {'id': 8455, 'synset': 'hose.n.02', 'name': 'hose'}, {'id': 8456, 'synset': 'hosiery.n.01', 'name': 'hosiery'}, {'id': 8457, 'synset': 'hospice.n.01', 'name': 'hospice'}, {'id': 8458, 'synset': 'hospital.n.01', 'name': 'hospital'}, {'id': 8459, 'synset': 'hospital_bed.n.01', 'name': 'hospital_bed'}, {'id': 8460, 'synset': 'hospital_room.n.01', 'name': 'hospital_room'}, {'id': 8461, 'synset': 'hospital_ship.n.01', 'name': 'hospital_ship'}, {'id': 8462, 'synset': 'hospital_train.n.01', 'name': 'hospital_train'}, {'id': 8463, 'synset': 'hostel.n.02', 'name': 'hostel'}, {'id': 8464, 'synset': 'hostel.n.01', 'name': 'hostel'}, {'id': 8465, 'synset': 'hotel.n.01', 'name': 'hotel'}, {'id': 8466, 'synset': 'hotel-casino.n.02', 'name': 'hotel-casino'}, {'id': 8467, 'synset': 'hotel-casino.n.01', 'name': 'hotel-casino'}, {'id': 8468, 'synset': 'hotel_room.n.01', 'name': 'hotel_room'}, {'id': 8469, 'synset': 'hot_line.n.01', 'name': 'hot_line'}, {'id': 8470, 'synset': 'hot_pants.n.02', 'name': 'hot_pants'}, {'id': 8471, 'synset': 'hot_rod.n.01', 'name': 'hot_rod'}, {'id': 8472, 'synset': 'hot_spot.n.03', 'name': 'hot_spot'}, {'id': 8473, 'synset': 'hot_tub.n.01', 'name': 'hot_tub'}, {'id': 8474, 'synset': 'hot-water_bottle.n.01', 'name': 'hot-water_bottle'}, {'id': 8475, 'synset': 'houndstooth_check.n.01', 'name': 'houndstooth_check'}, {'id': 8476, 'synset': 'hour_hand.n.01', 'name': 'hour_hand'}, {'id': 8477, 'synset': 'house.n.01', 'name': 'house'}, {'id': 8478, 'synset': 'house.n.12', 'name': 'house'}, {'id': 8479, 'synset': 'houselights.n.01', 'name': 'houselights'}, {'id': 8480, 'synset': 'house_of_cards.n.02', 'name': 'house_of_cards'}, {'id': 8481, 'synset': 'house_of_correction.n.01', 'name': 'house_of_correction'}, {'id': 8482, 'synset': 'house_paint.n.01', 'name': 'house_paint'}, {'id': 8483, 'synset': 'housetop.n.01', 'name': 'housetop'}, {'id': 8484, 'synset': 'housing.n.01', 'name': 'housing'}, {'id': 8485, 'synset': 'hovel.n.01', 'name': 'hovel'}, {'id': 8486, 'synset': 'hovercraft.n.01', 'name': 'hovercraft'}, {'id': 8487, 'synset': 'howdah.n.01', 'name': 'howdah'}, {'id': 8488, 'synset': 'huarache.n.01', 'name': 'huarache'}, {'id': 8489, 'synset': 'hub-and-spoke.n.01', 'name': 'hub-and-spoke'}, {'id': 8490, 'synset': 'hubcap.n.01', 'name': 'hubcap'}, {'id': 8491, 'synset': 'huck.n.01', 'name': 'huck'}, {'id': 8492, 'synset': 'hug-me-tight.n.01', 'name': 'hug-me-tight'}, {'id': 8493, 'synset': 'hula-hoop.n.01', 'name': 'hula-hoop'}, {'id': 8494, 'synset': 'hulk.n.02', 'name': 'hulk'}, {'id': 8495, 'synset': 'hull.n.06', 'name': 'hull'}, {'id': 8496, 'synset': 'humeral_veil.n.01', 'name': 'humeral_veil'}, {'id': 8497, 'synset': 'humvee.n.01', 'name': 'Humvee'}, {'id': 8498, 'synset': 'hunter.n.04', 'name': 'hunter'}, {'id': 8499, 'synset': 'hunting_knife.n.01', 'name': 'hunting_knife'}, {'id': 8500, 'synset': 'hurdle.n.01', 'name': 'hurdle'}, {'id': 8501, 'synset': 'hurricane_deck.n.01', 'name': 'hurricane_deck'}, {'id': 8502, 'synset': 'hurricane_lamp.n.01', 'name': 'hurricane_lamp'}, {'id': 8503, 'synset': 'hut.n.01', 'name': 'hut'}, {'id': 8504, 'synset': 'hutch.n.01', 'name': 'hutch'}, {'id': 8505, 'synset': 'hutment.n.01', 'name': 'hutment'}, {'id': 8506, 'synset': 'hydraulic_brake.n.01', 'name': 'hydraulic_brake'}, {'id': 8507, 'synset': 'hydraulic_press.n.01', 'name': 'hydraulic_press'}, {'id': 8508, 'synset': 'hydraulic_pump.n.01', 'name': 'hydraulic_pump'}, {'id': 8509, 'synset': 'hydraulic_system.n.01', 'name': 'hydraulic_system'}, {'id': 8510, 'synset': 'hydraulic_transmission.n.01', 'name': 'hydraulic_transmission'}, {'id': 8511, 'synset': 'hydroelectric_turbine.n.01', 'name': 'hydroelectric_turbine'}, {'id': 8512, 'synset': 'hydrofoil.n.02', 'name': 'hydrofoil'}, {'id': 8513, 'synset': 'hydrofoil.n.01', 'name': 'hydrofoil'}, {'id': 8514, 'synset': 'hydrogen_bomb.n.01', 'name': 'hydrogen_bomb'}, {'id': 8515, 'synset': 'hydrometer.n.01', 'name': 'hydrometer'}, {'id': 8516, 'synset': 'hygrodeik.n.01', 'name': 'hygrodeik'}, {'id': 8517, 'synset': 'hygrometer.n.01', 'name': 'hygrometer'}, {'id': 8518, 'synset': 'hygroscope.n.01', 'name': 'hygroscope'}, {'id': 8519, 'synset': 'hyperbaric_chamber.n.01', 'name': 'hyperbaric_chamber'}, {'id': 8520, 'synset': 'hypercoaster.n.01', 'name': 'hypercoaster'}, {'id': 8521, 'synset': 'hypermarket.n.01', 'name': 'hypermarket'}, {'id': 8522, 'synset': 'hypodermic_needle.n.01', 'name': 'hypodermic_needle'}, {'id': 8523, 'synset': 'hypodermic_syringe.n.01', 'name': 'hypodermic_syringe'}, {'id': 8524, 'synset': 'hypsometer.n.01', 'name': 'hypsometer'}, {'id': 8525, 'synset': 'hysterosalpingogram.n.01', 'name': 'hysterosalpingogram'}, {'id': 8526, 'synset': 'i-beam.n.01', 'name': 'I-beam'}, {'id': 8527, 'synset': 'ice_ax.n.01', 'name': 'ice_ax'}, {'id': 8528, 'synset': 'iceboat.n.02', 'name': 'iceboat'}, {'id': 8529, 'synset': 'icebreaker.n.01', 'name': 'icebreaker'}, {'id': 8530, 'synset': 'iced-tea_spoon.n.01', 'name': 'iced-tea_spoon'}, {'id': 8531, 'synset': 'ice_hockey_rink.n.01', 'name': 'ice_hockey_rink'}, {'id': 8532, 'synset': 'ice_machine.n.01', 'name': 'ice_machine'}, {'id': 8533, 'synset': 'icepick.n.01', 'name': 'icepick'}, {'id': 8534, 'synset': 'ice_rink.n.01', 'name': 'ice_rink'}, {'id': 8535, 'synset': 'ice_tongs.n.01', 'name': 'ice_tongs'}, {'id': 8536, 'synset': 'icetray.n.01', 'name': 'icetray'}, {'id': 8537, 'synset': 'iconoscope.n.01', 'name': 'iconoscope'}, {'id': 8538, 'synset': 'identikit.n.01', 'name': 'Identikit'}, {'id': 8539, 'synset': 'idle_pulley.n.01', 'name': 'idle_pulley'}, {'id': 8540, 'synset': 'igloo.n.01', 'name': 'igloo'}, {'id': 8541, 'synset': 'ignition_coil.n.01', 'name': 'ignition_coil'}, {'id': 8542, 'synset': 'ignition_key.n.01', 'name': 'ignition_key'}, {'id': 8543, 'synset': 'ignition_switch.n.01', 'name': 'ignition_switch'}, {'id': 8544, 'synset': 'imaret.n.01', 'name': 'imaret'}, {'id': 8545, 'synset': 'immovable_bandage.n.01', 'name': 'immovable_bandage'}, {'id': 8546, 'synset': 'impact_printer.n.01', 'name': 'impact_printer'}, {'id': 8547, 'synset': 'impeller.n.01', 'name': 'impeller'}, {'id': 8548, 'synset': 'implant.n.01', 'name': 'implant'}, {'id': 8549, 'synset': 'implement.n.01', 'name': 'implement'}, {'id': 8550, 'synset': 'impression.n.07', 'name': 'impression'}, {'id': 8551, 'synset': 'imprint.n.05', 'name': 'imprint'}, {'id': 8552, 'synset': 'improvised_explosive_device.n.01', 'name': 'improvised_explosive_device'}, {'id': 8553, 'synset': 'impulse_turbine.n.01', 'name': 'impulse_turbine'}, {'id': 8554, 'synset': 'in-basket.n.01', 'name': 'in-basket'}, {'id': 8555, 'synset': 'incendiary_bomb.n.01', 'name': 'incendiary_bomb'}, {'id': 8556, 'synset': 'incinerator.n.01', 'name': 'incinerator'}, {'id': 8557, 'synset': 'inclined_plane.n.01', 'name': 'inclined_plane'}, {'id': 8558, 'synset': 'inclinometer.n.02', 'name': 'inclinometer'}, {'id': 8559, 'synset': 'inclinometer.n.01', 'name': 'inclinometer'}, {'id': 8560, 'synset': 'incrustation.n.03', 'name': 'incrustation'}, {'id': 8561, 'synset': 'incubator.n.01', 'name': 'incubator'}, {'id': 8562, 'synset': 'index_register.n.01', 'name': 'index_register'}, {'id': 8563, 'synset': 'indiaman.n.01', 'name': 'Indiaman'}, {'id': 8564, 'synset': 'indian_club.n.01', 'name': 'Indian_club'}, {'id': 8565, 'synset': 'indicator.n.03', 'name': 'indicator'}, {'id': 8566, 'synset': 'induction_coil.n.01', 'name': 'induction_coil'}, {'id': 8567, 'synset': 'inductor.n.01', 'name': 'inductor'}, {'id': 8568, 'synset': 'industrial_watercourse.n.01', 'name': 'industrial_watercourse'}, {'id': 8569, 'synset': 'inertial_guidance_system.n.01', 'name': 'inertial_guidance_system'}, {'id': 8570, 'synset': 'inflater.n.01', 'name': 'inflater'}, {'id': 8571, 'synset': 'injector.n.01', 'name': 'injector'}, {'id': 8572, 'synset': 'ink_bottle.n.01', 'name': 'ink_bottle'}, {'id': 8573, 'synset': 'ink_eraser.n.01', 'name': 'ink_eraser'}, {'id': 8574, 'synset': 'ink-jet_printer.n.01', 'name': 'ink-jet_printer'}, {'id': 8575, 'synset': 'inkle.n.01', 'name': 'inkle'}, {'id': 8576, 'synset': 'inkstand.n.02', 'name': 'inkstand'}, {'id': 8577, 'synset': 'inkwell.n.01', 'name': 'inkwell'}, {'id': 8578, 'synset': 'inlay.n.01', 'name': 'inlay'}, {'id': 8579, 'synset': 'inside_caliper.n.01', 'name': 'inside_caliper'}, {'id': 8580, 'synset': 'insole.n.01', 'name': 'insole'}, {'id': 8581, 'synset': 'instep.n.02', 'name': 'instep'}, {'id': 8582, 'synset': 'instillator.n.01', 'name': 'instillator'}, {'id': 8583, 'synset': 'institution.n.02', 'name': 'institution'}, {'id': 8584, 'synset': 'instrument.n.01', 'name': 'instrument'}, {'id': 8585, 'synset': 'instrument_of_punishment.n.01', 'name': 'instrument_of_punishment'}, {'id': 8586, 'synset': 'instrument_of_torture.n.01', 'name': 'instrument_of_torture'}, {'id': 8587, 'synset': 'intaglio.n.02', 'name': 'intaglio'}, {'id': 8588, 'synset': 'intake_valve.n.01', 'name': 'intake_valve'}, {'id': 8589, 'synset': 'integrated_circuit.n.01', 'name': 'integrated_circuit'}, {'id': 8590, 'synset': 'integrator.n.01', 'name': 'integrator'}, {'id': 8591, 'synset': 'intelnet.n.01', 'name': 'Intelnet'}, {'id': 8592, 'synset': 'interceptor.n.01', 'name': 'interceptor'}, {'id': 8593, 'synset': 'interchange.n.01', 'name': 'interchange'}, {'id': 8594, 'synset': 'intercommunication_system.n.01', 'name': 'intercommunication_system'}, {'id': 8595, 'synset': 'intercontinental_ballistic_missile.n.01', 'name': 'intercontinental_ballistic_missile'}, {'id': 8596, 'synset': 'interface.n.04', 'name': 'interface'}, {'id': 8597, 'synset': 'interferometer.n.01', 'name': 'interferometer'}, {'id': 8598, 'synset': 'interior_door.n.01', 'name': 'interior_door'}, {'id': 8599, 'synset': 'internal-combustion_engine.n.01', 'name': 'internal-combustion_engine'}, {'id': 8600, 'synset': 'internal_drive.n.01', 'name': 'internal_drive'}, {'id': 8601, 'synset': 'internet.n.01', 'name': 'internet'}, {'id': 8602, 'synset': 'interphone.n.01', 'name': 'interphone'}, {'id': 8603, 'synset': 'interrupter.n.01', 'name': 'interrupter'}, {'id': 8604, 'synset': 'intersection.n.02', 'name': 'intersection'}, {'id': 8605, 'synset': 'interstice.n.02', 'name': 'interstice'}, {'id': 8606, 'synset': 'intraocular_lens.n.01', 'name': 'intraocular_lens'}, {'id': 8607, 'synset': 'intravenous_pyelogram.n.01', 'name': 'intravenous_pyelogram'}, {'id': 8608, 'synset': 'inverter.n.01', 'name': 'inverter'}, {'id': 8609, 'synset': 'ion_engine.n.01', 'name': 'ion_engine'}, {'id': 8610, 'synset': 'ionization_chamber.n.01', 'name': 'ionization_chamber'}, {'id': 8611, 'synset': 'video_ipod.n.01', 'name': 'video_iPod'}, {'id': 8612, 'synset': 'iron.n.02', 'name': 'iron'}, {'id': 8613, 'synset': 'iron.n.03', 'name': 'iron'}, {'id': 8614, 'synset': 'irons.n.01', 'name': 'irons'}, {'id': 8615, 'synset': 'ironclad.n.01', 'name': 'ironclad'}, {'id': 8616, 'synset': 'iron_foundry.n.01', 'name': 'iron_foundry'}, {'id': 8617, 'synset': 'iron_horse.n.01', 'name': 'iron_horse'}, {'id': 8618, 'synset': 'ironing.n.01', 'name': 'ironing'}, {'id': 8619, 'synset': 'iron_lung.n.01', 'name': 'iron_lung'}, {'id': 8620, 'synset': 'ironmongery.n.01', 'name': 'ironmongery'}, {'id': 8621, 'synset': 'ironworks.n.01', 'name': 'ironworks'}, {'id': 8622, 'synset': 'irrigation_ditch.n.01', 'name': 'irrigation_ditch'}, {'id': 8623, 'synset': 'izar.n.01', 'name': 'izar'}, {'id': 8624, 'synset': 'jabot.n.01', 'name': 'jabot'}, {'id': 8625, 'synset': 'jack.n.10', 'name': 'jack'}, {'id': 8626, 'synset': 'jack.n.07', 'name': 'jack'}, {'id': 8627, 'synset': 'jack.n.06', 'name': 'jack'}, {'id': 8628, 'synset': 'jack.n.05', 'name': 'jack'}, {'id': 8629, 'synset': 'jacket.n.02', 'name': 'jacket'}, {'id': 8630, 'synset': 'jacket.n.05', 'name': 'jacket'}, {'id': 8631, 'synset': 'jack-in-the-box.n.01', 'name': 'jack-in-the-box'}, {'id': 8632, 'synset': "jack-o'-lantern.n.02", 'name': "jack-o'-lantern"}, {'id': 8633, 'synset': 'jack_plane.n.01', 'name': 'jack_plane'}, {'id': 8634, 'synset': "jacob's_ladder.n.02", 'name': "Jacob's_ladder"}, {'id': 8635, 'synset': 'jaconet.n.01', 'name': 'jaconet'}, {'id': 8636, 'synset': 'jacquard_loom.n.01', 'name': 'Jacquard_loom'}, {'id': 8637, 'synset': 'jacquard.n.02', 'name': 'jacquard'}, {'id': 8638, 'synset': 'jag.n.03', 'name': 'jag'}, {'id': 8639, 'synset': 'jail.n.01', 'name': 'jail'}, {'id': 8640, 'synset': 'jalousie.n.02', 'name': 'jalousie'}, {'id': 8641, 'synset': 'jamb.n.01', 'name': 'jamb'}, {'id': 8642, 'synset': 'jammer.n.01', 'name': 'jammer'}, {'id': 8643, 'synset': 'jampot.n.01', 'name': 'jampot'}, {'id': 8644, 'synset': 'japan.n.04', 'name': 'japan'}, {'id': 8645, 'synset': 'jarvik_heart.n.01', 'name': 'Jarvik_heart'}, {'id': 8646, 'synset': 'jaunting_car.n.01', 'name': 'jaunting_car'}, {'id': 8647, 'synset': 'javelin.n.02', 'name': 'javelin'}, {'id': 8648, 'synset': 'jaw.n.03', 'name': 'jaw'}, {'id': 8649, 'synset': 'jaws_of_life.n.01', 'name': 'Jaws_of_Life'}, {'id': 8650, 'synset': 'jellaba.n.01', 'name': 'jellaba'}, {'id': 8651, 'synset': 'jerkin.n.01', 'name': 'jerkin'}, {'id': 8652, 'synset': 'jeroboam.n.02', 'name': 'jeroboam'}, {'id': 8653, 'synset': 'jersey.n.04', 'name': 'jersey'}, {'id': 8654, 'synset': 'jet_bridge.n.01', 'name': 'jet_bridge'}, {'id': 8655, 'synset': 'jet_engine.n.01', 'name': 'jet_engine'}, {'id': 8656, 'synset': 'jetliner.n.01', 'name': 'jetliner'}, {'id': 8657, 'synset': "jeweler's_glass.n.01", 'name': "jeweler's_glass"}, {'id': 8658, 'synset': 'jewelled_headdress.n.01', 'name': 'jewelled_headdress'}, {'id': 8659, 'synset': "jew's_harp.n.01", 'name': "jew's_harp"}, {'id': 8660, 'synset': 'jib.n.01', 'name': 'jib'}, {'id': 8661, 'synset': 'jibboom.n.01', 'name': 'jibboom'}, {'id': 8662, 'synset': 'jig.n.03', 'name': 'jig'}, {'id': 8663, 'synset': 'jig.n.02', 'name': 'jig'}, {'id': 8664, 'synset': 'jiggermast.n.01', 'name': 'jiggermast'}, {'id': 8665, 'synset': 'jigsaw.n.02', 'name': 'jigsaw'}, {'id': 8666, 'synset': 'jigsaw_puzzle.n.01', 'name': 'jigsaw_puzzle'}, {'id': 8667, 'synset': 'jinrikisha.n.01', 'name': 'jinrikisha'}, {'id': 8668, 'synset': 'jobcentre.n.01', 'name': 'jobcentre'}, {'id': 8669, 'synset': 'jodhpurs.n.01', 'name': 'jodhpurs'}, {'id': 8670, 'synset': 'jodhpur.n.01', 'name': 'jodhpur'}, {'id': 8671, 'synset': 'joinery.n.01', 'name': 'joinery'}, {'id': 8672, 'synset': 'joint.n.05', 'name': 'joint'}, {'id': 8673, 'synset': 'joint_direct_attack_munition.n.01', 'name': 'Joint_Direct_Attack_Munition'}, {'id': 8674, 'synset': 'jointer.n.01', 'name': 'jointer'}, {'id': 8675, 'synset': 'joist.n.01', 'name': 'joist'}, {'id': 8676, 'synset': 'jolly_boat.n.01', 'name': 'jolly_boat'}, {'id': 8677, 'synset': 'jorum.n.01', 'name': 'jorum'}, {'id': 8678, 'synset': 'joss_house.n.01', 'name': 'joss_house'}, {'id': 8679, 'synset': 'journal_bearing.n.01', 'name': 'journal_bearing'}, {'id': 8680, 'synset': 'journal_box.n.01', 'name': 'journal_box'}, {'id': 8681, 'synset': 'jungle_gym.n.01', 'name': 'jungle_gym'}, {'id': 8682, 'synset': 'junk.n.02', 'name': 'junk'}, {'id': 8683, 'synset': 'jug.n.01', 'name': 'jug'}, {'id': 8684, 'synset': 'jukebox.n.01', 'name': 'jukebox'}, {'id': 8685, 'synset': 'jumbojet.n.01', 'name': 'jumbojet'}, {'id': 8686, 'synset': 'jumper.n.07', 'name': 'jumper'}, {'id': 8687, 'synset': 'jumper.n.06', 'name': 'jumper'}, {'id': 8688, 'synset': 'jumper.n.05', 'name': 'jumper'}, {'id': 8689, 'synset': 'jumper.n.04', 'name': 'jumper'}, {'id': 8690, 'synset': 'jumper_cable.n.01', 'name': 'jumper_cable'}, {'id': 8691, 'synset': 'jump_seat.n.01', 'name': 'jump_seat'}, {'id': 8692, 'synset': 'jump_suit.n.02', 'name': 'jump_suit'}, {'id': 8693, 'synset': 'junction.n.01', 'name': 'junction'}, {'id': 8694, 'synset': 'junction.n.04', 'name': 'junction'}, {'id': 8695, 'synset': 'junction_barrier.n.01', 'name': 'junction_barrier'}, {'id': 8696, 'synset': 'junk_shop.n.01', 'name': 'junk_shop'}, {'id': 8697, 'synset': 'jury_box.n.01', 'name': 'jury_box'}, {'id': 8698, 'synset': 'jury_mast.n.01', 'name': 'jury_mast'}, {'id': 8699, 'synset': 'kachina.n.03', 'name': 'kachina'}, {'id': 8700, 'synset': 'kaffiyeh.n.01', 'name': 'kaffiyeh'}, {'id': 8701, 'synset': 'kalansuwa.n.01', 'name': 'kalansuwa'}, {'id': 8702, 'synset': 'kalashnikov.n.01', 'name': 'Kalashnikov'}, {'id': 8703, 'synset': 'kameez.n.01', 'name': 'kameez'}, {'id': 8704, 'synset': 'kanzu.n.01', 'name': 'kanzu'}, {'id': 8705, 'synset': 'katharometer.n.01', 'name': 'katharometer'}, {'id': 8706, 'synset': 'kazoo.n.01', 'name': 'kazoo'}, {'id': 8707, 'synset': 'keel.n.03', 'name': 'keel'}, {'id': 8708, 'synset': 'keelboat.n.01', 'name': 'keelboat'}, {'id': 8709, 'synset': 'keelson.n.01', 'name': 'keelson'}, {'id': 8710, 'synset': 'keep.n.02', 'name': 'keep'}, {'id': 8711, 'synset': 'kepi.n.01', 'name': 'kepi'}, {'id': 8712, 'synset': 'keratoscope.n.01', 'name': 'keratoscope'}, {'id': 8713, 'synset': 'kerchief.n.01', 'name': 'kerchief'}, {'id': 8714, 'synset': 'ketch.n.01', 'name': 'ketch'}, {'id': 8715, 'synset': 'kettle.n.04', 'name': 'kettle'}, {'id': 8716, 'synset': 'key.n.15', 'name': 'key'}, {'id': 8717, 'synset': 'keyboard.n.01', 'name': 'keyboard'}, {'id': 8718, 'synset': 'keyboard_buffer.n.01', 'name': 'keyboard_buffer'}, {'id': 8719, 'synset': 'keyboard_instrument.n.01', 'name': 'keyboard_instrument'}, {'id': 8720, 'synset': 'keyhole.n.01', 'name': 'keyhole'}, {'id': 8721, 'synset': 'keyhole_saw.n.01', 'name': 'keyhole_saw'}, {'id': 8722, 'synset': 'khadi.n.01', 'name': 'khadi'}, {'id': 8723, 'synset': 'khaki.n.01', 'name': 'khaki'}, {'id': 8724, 'synset': 'khakis.n.01', 'name': 'khakis'}, {'id': 8725, 'synset': 'khimar.n.01', 'name': 'khimar'}, {'id': 8726, 'synset': 'khukuri.n.01', 'name': 'khukuri'}, {'id': 8727, 'synset': 'kick_pleat.n.01', 'name': 'kick_pleat'}, {'id': 8728, 'synset': 'kicksorter.n.01', 'name': 'kicksorter'}, {'id': 8729, 'synset': 'kickstand.n.01', 'name': 'kickstand'}, {'id': 8730, 'synset': 'kick_starter.n.01', 'name': 'kick_starter'}, {'id': 8731, 'synset': 'kid_glove.n.01', 'name': 'kid_glove'}, {'id': 8732, 'synset': 'kiln.n.01', 'name': 'kiln'}, {'id': 8733, 'synset': 'kinescope.n.01', 'name': 'kinescope'}, {'id': 8734, 'synset': 'kinetoscope.n.01', 'name': 'Kinetoscope'}, {'id': 8735, 'synset': 'king.n.10', 'name': 'king'}, {'id': 8736, 'synset': 'king.n.08', 'name': 'king'}, {'id': 8737, 'synset': 'kingbolt.n.01', 'name': 'kingbolt'}, {'id': 8738, 'synset': 'king_post.n.01', 'name': 'king_post'}, {'id': 8739, 'synset': "kipp's_apparatus.n.01", 'name': "Kipp's_apparatus"}, {'id': 8740, 'synset': 'kirk.n.01', 'name': 'kirk'}, {'id': 8741, 'synset': 'kirpan.n.01', 'name': 'kirpan'}, {'id': 8742, 'synset': 'kirtle.n.02', 'name': 'kirtle'}, {'id': 8743, 'synset': 'kirtle.n.01', 'name': 'kirtle'}, {'id': 8744, 'synset': 'kit.n.02', 'name': 'kit'}, {'id': 8745, 'synset': 'kit.n.01', 'name': 'kit'}, {'id': 8746, 'synset': 'kitbag.n.01', 'name': 'kitbag'}, {'id': 8747, 'synset': 'kitchen.n.01', 'name': 'kitchen'}, {'id': 8748, 'synset': 'kitchen_appliance.n.01', 'name': 'kitchen_appliance'}, {'id': 8749, 'synset': 'kitchenette.n.01', 'name': 'kitchenette'}, {'id': 8750, 'synset': 'kitchen_utensil.n.01', 'name': 'kitchen_utensil'}, {'id': 8751, 'synset': 'kitchenware.n.01', 'name': 'kitchenware'}, {'id': 8752, 'synset': 'kite_balloon.n.01', 'name': 'kite_balloon'}, {'id': 8753, 'synset': 'klaxon.n.01', 'name': 'klaxon'}, {'id': 8754, 'synset': 'klieg_light.n.01', 'name': 'klieg_light'}, {'id': 8755, 'synset': 'klystron.n.01', 'name': 'klystron'}, {'id': 8756, 'synset': 'knee_brace.n.01', 'name': 'knee_brace'}, {'id': 8757, 'synset': 'knee-high.n.01', 'name': 'knee-high'}, {'id': 8758, 'synset': 'knee_piece.n.01', 'name': 'knee_piece'}, {'id': 8759, 'synset': 'knife.n.02', 'name': 'knife'}, {'id': 8760, 'synset': 'knife_blade.n.01', 'name': 'knife_blade'}, {'id': 8761, 'synset': 'knight.n.02', 'name': 'knight'}, {'id': 8762, 'synset': 'knit.n.01', 'name': 'knit'}, {'id': 8763, 'synset': 'knitting_machine.n.01', 'name': 'knitting_machine'}, {'id': 8764, 'synset': 'knitwear.n.01', 'name': 'knitwear'}, {'id': 8765, 'synset': 'knob.n.01', 'name': 'knob'}, {'id': 8766, 'synset': 'knob.n.04', 'name': 'knob'}, {'id': 8767, 'synset': 'knobble.n.01', 'name': 'knobble'}, {'id': 8768, 'synset': 'knobkerrie.n.01', 'name': 'knobkerrie'}, {'id': 8769, 'synset': 'knot.n.02', 'name': 'knot'}, {'id': 8770, 'synset': 'knuckle_joint.n.02', 'name': 'knuckle_joint'}, {'id': 8771, 'synset': 'kohl.n.01', 'name': 'kohl'}, {'id': 8772, 'synset': 'koto.n.01', 'name': 'koto'}, {'id': 8773, 'synset': 'kraal.n.02', 'name': 'kraal'}, {'id': 8774, 'synset': 'kremlin.n.02', 'name': 'kremlin'}, {'id': 8775, 'synset': 'kris.n.01', 'name': 'kris'}, {'id': 8776, 'synset': 'krummhorn.n.01', 'name': 'krummhorn'}, {'id': 8777, 'synset': "kundt's_tube.n.01", 'name': "Kundt's_tube"}, {'id': 8778, 'synset': 'kurdistan.n.02', 'name': 'Kurdistan'}, {'id': 8779, 'synset': 'kurta.n.01', 'name': 'kurta'}, {'id': 8780, 'synset': 'kylix.n.01', 'name': 'kylix'}, {'id': 8781, 'synset': 'kymograph.n.01', 'name': 'kymograph'}, {'id': 8782, 'synset': 'lab_bench.n.01', 'name': 'lab_bench'}, {'id': 8783, 'synset': 'lace.n.02', 'name': 'lace'}, {'id': 8784, 'synset': 'lacquer.n.02', 'name': 'lacquer'}, {'id': 8785, 'synset': 'lacquerware.n.01', 'name': 'lacquerware'}, {'id': 8786, 'synset': 'lacrosse_ball.n.01', 'name': 'lacrosse_ball'}, {'id': 8787, 'synset': 'ladder-back.n.02', 'name': 'ladder-back'}, {'id': 8788, 'synset': 'ladder-back.n.01', 'name': 'ladder-back'}, {'id': 8789, 'synset': 'ladder_truck.n.01', 'name': 'ladder_truck'}, {'id': 8790, 'synset': "ladies'_room.n.01", 'name': "ladies'_room"}, {'id': 8791, 'synset': 'lady_chapel.n.01', 'name': 'lady_chapel'}, {'id': 8792, 'synset': 'lagerphone.n.01', 'name': 'lagerphone'}, {'id': 8793, 'synset': 'lag_screw.n.01', 'name': 'lag_screw'}, {'id': 8794, 'synset': 'lake_dwelling.n.01', 'name': 'lake_dwelling'}, {'id': 8795, 'synset': 'lally.n.01', 'name': 'lally'}, {'id': 8796, 'synset': 'lamasery.n.01', 'name': 'lamasery'}, {'id': 8797, 'synset': 'lambrequin.n.02', 'name': 'lambrequin'}, {'id': 8798, 'synset': 'lame.n.02', 'name': 'lame'}, {'id': 8799, 'synset': 'laminar_flow_clean_room.n.01', 'name': 'laminar_flow_clean_room'}, {'id': 8800, 'synset': 'laminate.n.01', 'name': 'laminate'}, {'id': 8801, 'synset': 'lamination.n.01', 'name': 'lamination'}, {'id': 8802, 'synset': 'lamp.n.01', 'name': 'lamp'}, {'id': 8803, 'synset': 'lamp_house.n.01', 'name': 'lamp_house'}, {'id': 8804, 'synset': 'lanai.n.02', 'name': 'lanai'}, {'id': 8805, 'synset': 'lancet_arch.n.01', 'name': 'lancet_arch'}, {'id': 8806, 'synset': 'lancet_window.n.01', 'name': 'lancet_window'}, {'id': 8807, 'synset': 'landau.n.02', 'name': 'landau'}, {'id': 8808, 'synset': 'lander.n.02', 'name': 'lander'}, {'id': 8809, 'synset': 'landing_craft.n.01', 'name': 'landing_craft'}, {'id': 8810, 'synset': 'landing_flap.n.01', 'name': 'landing_flap'}, {'id': 8811, 'synset': 'landing_gear.n.01', 'name': 'landing_gear'}, {'id': 8812, 'synset': 'landing_net.n.01', 'name': 'landing_net'}, {'id': 8813, 'synset': 'landing_skid.n.01', 'name': 'landing_skid'}, {'id': 8814, 'synset': 'land_line.n.01', 'name': 'land_line'}, {'id': 8815, 'synset': 'land_mine.n.01', 'name': 'land_mine'}, {'id': 8816, 'synset': 'land_office.n.01', 'name': 'land_office'}, {'id': 8817, 'synset': 'lanolin.n.02', 'name': 'lanolin'}, {'id': 8818, 'synset': 'lanyard.n.01', 'name': 'lanyard'}, {'id': 8819, 'synset': 'lap.n.03', 'name': 'lap'}, {'id': 8820, 'synset': 'laparoscope.n.01', 'name': 'laparoscope'}, {'id': 8821, 'synset': 'lapboard.n.01', 'name': 'lapboard'}, {'id': 8822, 'synset': 'lapel.n.01', 'name': 'lapel'}, {'id': 8823, 'synset': 'lap_joint.n.01', 'name': 'lap_joint'}, {'id': 8824, 'synset': 'laryngoscope.n.01', 'name': 'laryngoscope'}, {'id': 8825, 'synset': 'laser.n.01', 'name': 'laser'}, {'id': 8826, 'synset': 'laser-guided_bomb.n.01', 'name': 'laser-guided_bomb'}, {'id': 8827, 'synset': 'laser_printer.n.01', 'name': 'laser_printer'}, {'id': 8828, 'synset': 'lash.n.02', 'name': 'lash'}, {'id': 8829, 'synset': 'lashing.n.02', 'name': 'lashing'}, {'id': 8830, 'synset': 'lasso.n.02', 'name': 'lasso'}, {'id': 8831, 'synset': 'latch.n.01', 'name': 'latch'}, {'id': 8832, 'synset': 'latchet.n.01', 'name': 'latchet'}, {'id': 8833, 'synset': 'latchkey.n.01', 'name': 'latchkey'}, {'id': 8834, 'synset': 'lateen.n.01', 'name': 'lateen'}, {'id': 8835, 'synset': 'latex_paint.n.01', 'name': 'latex_paint'}, {'id': 8836, 'synset': 'lath.n.01', 'name': 'lath'}, {'id': 8837, 'synset': 'lathe.n.01', 'name': 'lathe'}, {'id': 8838, 'synset': 'latrine.n.01', 'name': 'latrine'}, {'id': 8839, 'synset': 'lattice.n.03', 'name': 'lattice'}, {'id': 8840, 'synset': 'launch.n.01', 'name': 'launch'}, {'id': 8841, 'synset': 'launcher.n.01', 'name': 'launcher'}, {'id': 8842, 'synset': 'laundry.n.01', 'name': 'laundry'}, {'id': 8843, 'synset': 'laundry_cart.n.01', 'name': 'laundry_cart'}, {'id': 8844, 'synset': 'laundry_truck.n.01', 'name': 'laundry_truck'}, {'id': 8845, 'synset': 'lavalava.n.01', 'name': 'lavalava'}, {'id': 8846, 'synset': 'lavaliere.n.01', 'name': 'lavaliere'}, {'id': 8847, 'synset': 'laver.n.02', 'name': 'laver'}, {'id': 8848, 'synset': 'lawn_chair.n.01', 'name': 'lawn_chair'}, {'id': 8849, 'synset': 'lawn_furniture.n.01', 'name': 'lawn_furniture'}, {'id': 8850, 'synset': 'layette.n.01', 'name': 'layette'}, {'id': 8851, 'synset': 'lead-acid_battery.n.01', 'name': 'lead-acid_battery'}, {'id': 8852, 'synset': 'lead-in.n.02', 'name': 'lead-in'}, {'id': 8853, 'synset': 'leading_rein.n.01', 'name': 'leading_rein'}, {'id': 8854, 'synset': 'lead_pencil.n.01', 'name': 'lead_pencil'}, {'id': 8855, 'synset': 'leaf_spring.n.01', 'name': 'leaf_spring'}, {'id': 8856, 'synset': 'lean-to.n.01', 'name': 'lean-to'}, {'id': 8857, 'synset': 'lean-to_tent.n.01', 'name': 'lean-to_tent'}, {'id': 8858, 'synset': 'leash.n.01', 'name': 'leash'}, {'id': 8859, 'synset': 'leatherette.n.01', 'name': 'leatherette'}, {'id': 8860, 'synset': 'leather_strip.n.01', 'name': 'leather_strip'}, {'id': 8861, 'synset': 'leclanche_cell.n.01', 'name': 'Leclanche_cell'}, {'id': 8862, 'synset': 'lectern.n.01', 'name': 'lectern'}, {'id': 8863, 'synset': 'lecture_room.n.01', 'name': 'lecture_room'}, {'id': 8864, 'synset': 'lederhosen.n.01', 'name': 'lederhosen'}, {'id': 8865, 'synset': 'ledger_board.n.01', 'name': 'ledger_board'}, {'id': 8866, 'synset': 'leg.n.07', 'name': 'leg'}, {'id': 8867, 'synset': 'leg.n.03', 'name': 'leg'}, {'id': 8868, 'synset': 'leiden_jar.n.01', 'name': 'Leiden_jar'}, {'id': 8869, 'synset': 'leisure_wear.n.01', 'name': 'leisure_wear'}, {'id': 8870, 'synset': 'lens.n.01', 'name': 'lens'}, {'id': 8871, 'synset': 'lens.n.05', 'name': 'lens'}, {'id': 8872, 'synset': 'lens_cap.n.01', 'name': 'lens_cap'}, {'id': 8873, 'synset': 'lens_implant.n.01', 'name': 'lens_implant'}, {'id': 8874, 'synset': 'leotard.n.01', 'name': 'leotard'}, {'id': 8875, 'synset': 'letter_case.n.01', 'name': 'letter_case'}, {'id': 8876, 'synset': 'letter_opener.n.01', 'name': 'letter_opener'}, {'id': 8877, 'synset': 'levee.n.03', 'name': 'levee'}, {'id': 8878, 'synset': 'level.n.05', 'name': 'level'}, {'id': 8879, 'synset': 'lever.n.01', 'name': 'lever'}, {'id': 8880, 'synset': 'lever.n.03', 'name': 'lever'}, {'id': 8881, 'synset': 'lever.n.02', 'name': 'lever'}, {'id': 8882, 'synset': 'lever_lock.n.01', 'name': 'lever_lock'}, {'id': 8883, 'synset': "levi's.n.01", 'name': "Levi's"}, {'id': 8884, 'synset': 'liberty_ship.n.01', 'name': 'Liberty_ship'}, {'id': 8885, 'synset': 'library.n.01', 'name': 'library'}, {'id': 8886, 'synset': 'library.n.05', 'name': 'library'}, {'id': 8887, 'synset': 'lid.n.02', 'name': 'lid'}, {'id': 8888, 'synset': 'liebig_condenser.n.01', 'name': 'Liebig_condenser'}, {'id': 8889, 'synset': 'lie_detector.n.01', 'name': 'lie_detector'}, {'id': 8890, 'synset': 'lifeboat.n.01', 'name': 'lifeboat'}, {'id': 8891, 'synset': 'life_office.n.01', 'name': 'life_office'}, {'id': 8892, 'synset': 'life_preserver.n.01', 'name': 'life_preserver'}, {'id': 8893, 'synset': 'life-support_system.n.02', 'name': 'life-support_system'}, {'id': 8894, 'synset': 'life-support_system.n.01', 'name': 'life-support_system'}, {'id': 8895, 'synset': 'lifting_device.n.01', 'name': 'lifting_device'}, {'id': 8896, 'synset': 'lift_pump.n.01', 'name': 'lift_pump'}, {'id': 8897, 'synset': 'ligament.n.02', 'name': 'ligament'}, {'id': 8898, 'synset': 'ligature.n.03', 'name': 'ligature'}, {'id': 8899, 'synset': 'light.n.02', 'name': 'light'}, {'id': 8900, 'synset': 'light_arm.n.01', 'name': 'light_arm'}, {'id': 8901, 'synset': 'light_circuit.n.01', 'name': 'light_circuit'}, {'id': 8902, 'synset': 'light-emitting_diode.n.01', 'name': 'light-emitting_diode'}, {'id': 8903, 'synset': 'lighter.n.02', 'name': 'lighter'}, {'id': 8904, 'synset': 'lighter-than-air_craft.n.01', 'name': 'lighter-than-air_craft'}, {'id': 8905, 'synset': 'light_filter.n.01', 'name': 'light_filter'}, {'id': 8906, 'synset': 'lighting.n.02', 'name': 'lighting'}, {'id': 8907, 'synset': 'light_machine_gun.n.01', 'name': 'light_machine_gun'}, {'id': 8908, 'synset': 'light_meter.n.01', 'name': 'light_meter'}, {'id': 8909, 'synset': 'light_microscope.n.01', 'name': 'light_microscope'}, {'id': 8910, 'synset': 'light_pen.n.01', 'name': 'light_pen'}, {'id': 8911, 'synset': 'lightship.n.01', 'name': 'lightship'}, {'id': 8912, 'synset': 'lilo.n.01', 'name': 'Lilo'}, {'id': 8913, 'synset': 'limber.n.01', 'name': 'limber'}, {'id': 8914, 'synset': 'limekiln.n.01', 'name': 'limekiln'}, {'id': 8915, 'synset': 'limiter.n.01', 'name': 'limiter'}, {'id': 8916, 'synset': 'linear_accelerator.n.01', 'name': 'linear_accelerator'}, {'id': 8917, 'synset': 'linen.n.01', 'name': 'linen'}, {'id': 8918, 'synset': 'line_printer.n.01', 'name': 'line_printer'}, {'id': 8919, 'synset': 'liner.n.04', 'name': 'liner'}, {'id': 8920, 'synset': 'liner.n.03', 'name': 'liner'}, {'id': 8921, 'synset': 'lingerie.n.01', 'name': 'lingerie'}, {'id': 8922, 'synset': 'lining.n.01', 'name': 'lining'}, {'id': 8923, 'synset': 'link.n.09', 'name': 'link'}, {'id': 8924, 'synset': 'linkage.n.03', 'name': 'linkage'}, {'id': 8925, 'synset': 'link_trainer.n.01', 'name': 'Link_trainer'}, {'id': 8926, 'synset': 'linocut.n.02', 'name': 'linocut'}, {'id': 8927, 'synset': 'linoleum_knife.n.01', 'name': 'linoleum_knife'}, {'id': 8928, 'synset': 'linotype.n.01', 'name': 'Linotype'}, {'id': 8929, 'synset': 'linsey-woolsey.n.01', 'name': 'linsey-woolsey'}, {'id': 8930, 'synset': 'linstock.n.01', 'name': 'linstock'}, {'id': 8931, 'synset': 'lion-jaw_forceps.n.01', 'name': 'lion-jaw_forceps'}, {'id': 8932, 'synset': 'lip-gloss.n.01', 'name': 'lip-gloss'}, {'id': 8933, 'synset': 'lipstick.n.01', 'name': 'lipstick'}, {'id': 8934, 'synset': 'liqueur_glass.n.01', 'name': 'liqueur_glass'}, {'id': 8935, 'synset': 'liquid_crystal_display.n.01', 'name': 'liquid_crystal_display'}, {'id': 8936, 'synset': 'liquid_metal_reactor.n.01', 'name': 'liquid_metal_reactor'}, {'id': 8937, 'synset': 'lisle.n.01', 'name': 'lisle'}, {'id': 8938, 'synset': 'lister.n.03', 'name': 'lister'}, {'id': 8939, 'synset': 'litterbin.n.01', 'name': 'litterbin'}, {'id': 8940, 'synset': 'little_theater.n.01', 'name': 'little_theater'}, {'id': 8941, 'synset': 'live_axle.n.01', 'name': 'live_axle'}, {'id': 8942, 'synset': 'living_quarters.n.01', 'name': 'living_quarters'}, {'id': 8943, 'synset': 'living_room.n.01', 'name': 'living_room'}, {'id': 8944, 'synset': 'load.n.09', 'name': 'load'}, {'id': 8945, 'synset': 'loafer.n.02', 'name': 'Loafer'}, {'id': 8946, 'synset': 'loaner.n.02', 'name': 'loaner'}, {'id': 8947, 'synset': 'lobe.n.04', 'name': 'lobe'}, {'id': 8948, 'synset': 'lobster_pot.n.01', 'name': 'lobster_pot'}, {'id': 8949, 'synset': 'local.n.01', 'name': 'local'}, {'id': 8950, 'synset': 'local_area_network.n.01', 'name': 'local_area_network'}, {'id': 8951, 'synset': 'local_oscillator.n.01', 'name': 'local_oscillator'}, {'id': 8952, 'synset': 'lochaber_ax.n.01', 'name': 'Lochaber_ax'}, {'id': 8953, 'synset': 'lock.n.01', 'name': 'lock'}, {'id': 8954, 'synset': 'lock.n.05', 'name': 'lock'}, {'id': 8955, 'synset': 'lock.n.04', 'name': 'lock'}, {'id': 8956, 'synset': 'lock.n.03', 'name': 'lock'}, {'id': 8957, 'synset': 'lockage.n.02', 'name': 'lockage'}, {'id': 8958, 'synset': 'locker.n.02', 'name': 'locker'}, {'id': 8959, 'synset': 'locker_room.n.01', 'name': 'locker_room'}, {'id': 8960, 'synset': 'locket.n.01', 'name': 'locket'}, {'id': 8961, 'synset': 'lock-gate.n.01', 'name': 'lock-gate'}, {'id': 8962, 'synset': 'locking_pliers.n.01', 'name': 'locking_pliers'}, {'id': 8963, 'synset': 'lockring.n.01', 'name': 'lockring'}, {'id': 8964, 'synset': 'lockstitch.n.01', 'name': 'lockstitch'}, {'id': 8965, 'synset': 'lockup.n.01', 'name': 'lockup'}, {'id': 8966, 'synset': 'locomotive.n.01', 'name': 'locomotive'}, {'id': 8967, 'synset': 'lodge.n.05', 'name': 'lodge'}, {'id': 8968, 'synset': 'lodge.n.04', 'name': 'lodge'}, {'id': 8969, 'synset': 'lodge.n.03', 'name': 'lodge'}, {'id': 8970, 'synset': 'lodging_house.n.01', 'name': 'lodging_house'}, {'id': 8971, 'synset': 'loft.n.02', 'name': 'loft'}, {'id': 8972, 'synset': 'loft.n.04', 'name': 'loft'}, {'id': 8973, 'synset': 'loft.n.01', 'name': 'loft'}, {'id': 8974, 'synset': 'log_cabin.n.01', 'name': 'log_cabin'}, {'id': 8975, 'synset': 'loggia.n.01', 'name': 'loggia'}, {'id': 8976, 'synset': 'longbow.n.01', 'name': 'longbow'}, {'id': 8977, 'synset': 'long_iron.n.01', 'name': 'long_iron'}, {'id': 8978, 'synset': 'long_johns.n.01', 'name': 'long_johns'}, {'id': 8979, 'synset': 'long_sleeve.n.01', 'name': 'long_sleeve'}, {'id': 8980, 'synset': 'long_tom.n.01', 'name': 'long_tom'}, {'id': 8981, 'synset': 'long_trousers.n.01', 'name': 'long_trousers'}, {'id': 8982, 'synset': 'long_underwear.n.01', 'name': 'long_underwear'}, {'id': 8983, 'synset': 'looking_glass.n.01', 'name': 'looking_glass'}, {'id': 8984, 'synset': 'lookout.n.03', 'name': 'lookout'}, {'id': 8985, 'synset': 'loom.n.01', 'name': 'loom'}, {'id': 8986, 'synset': 'loop_knot.n.01', 'name': 'loop_knot'}, {'id': 8987, 'synset': 'lorgnette.n.01', 'name': 'lorgnette'}, {'id': 8988, 'synset': 'lorraine_cross.n.01', 'name': 'Lorraine_cross'}, {'id': 8989, 'synset': 'lorry.n.02', 'name': 'lorry'}, {'id': 8990, 'synset': 'lota.n.01', 'name': 'lota'}, {'id': 8991, 'synset': 'lotion.n.01', 'name': 'lotion'}, {'id': 8992, 'synset': 'lounge.n.02', 'name': 'lounge'}, {'id': 8993, 'synset': 'lounger.n.03', 'name': 'lounger'}, {'id': 8994, 'synset': 'lounging_jacket.n.01', 'name': 'lounging_jacket'}, {'id': 8995, 'synset': 'lounging_pajama.n.01', 'name': 'lounging_pajama'}, {'id': 8996, 'synset': 'loungewear.n.01', 'name': 'loungewear'}, {'id': 8997, 'synset': 'loupe.n.01', 'name': 'loupe'}, {'id': 8998, 'synset': 'louvered_window.n.01', 'name': 'louvered_window'}, {'id': 8999, 'synset': 'love_knot.n.01', 'name': 'love_knot'}, {'id': 9000, 'synset': 'loving_cup.n.01', 'name': 'loving_cup'}, {'id': 9001, 'synset': 'lowboy.n.01', 'name': 'lowboy'}, {'id': 9002, 'synset': 'low-pass_filter.n.01', 'name': 'low-pass_filter'}, {'id': 9003, 'synset': 'low-warp-loom.n.01', 'name': 'low-warp-loom'}, {'id': 9004, 'synset': 'lp.n.01', 'name': 'LP'}, {'id': 9005, 'synset': 'l-plate.n.01', 'name': 'L-plate'}, {'id': 9006, 'synset': "lubber's_hole.n.01", 'name': "lubber's_hole"}, {'id': 9007, 'synset': 'lubricating_system.n.01', 'name': 'lubricating_system'}, {'id': 9008, 'synset': 'luff.n.01', 'name': 'luff'}, {'id': 9009, 'synset': 'lug.n.03', 'name': 'lug'}, {'id': 9010, 'synset': 'luge.n.01', 'name': 'luge'}, {'id': 9011, 'synset': 'luger.n.01', 'name': 'Luger'}, {'id': 9012, 'synset': 'luggage_carrier.n.01', 'name': 'luggage_carrier'}, {'id': 9013, 'synset': 'luggage_compartment.n.01', 'name': 'luggage_compartment'}, {'id': 9014, 'synset': 'luggage_rack.n.01', 'name': 'luggage_rack'}, {'id': 9015, 'synset': 'lugger.n.01', 'name': 'lugger'}, {'id': 9016, 'synset': 'lugsail.n.01', 'name': 'lugsail'}, {'id': 9017, 'synset': 'lug_wrench.n.01', 'name': 'lug_wrench'}, {'id': 9018, 'synset': 'lumberjack.n.02', 'name': 'lumberjack'}, {'id': 9019, 'synset': 'lumbermill.n.01', 'name': 'lumbermill'}, {'id': 9020, 'synset': 'lunar_excursion_module.n.01', 'name': 'lunar_excursion_module'}, {'id': 9021, 'synset': 'lunchroom.n.01', 'name': 'lunchroom'}, {'id': 9022, 'synset': 'lunette.n.01', 'name': 'lunette'}, {'id': 9023, 'synset': 'lungi.n.01', 'name': 'lungi'}, {'id': 9024, 'synset': 'lunula.n.02', 'name': 'lunula'}, {'id': 9025, 'synset': 'lusterware.n.01', 'name': 'lusterware'}, {'id': 9026, 'synset': 'lute.n.02', 'name': 'lute'}, {'id': 9027, 'synset': 'luxury_liner.n.01', 'name': 'luxury_liner'}, {'id': 9028, 'synset': 'lyceum.n.02', 'name': 'lyceum'}, {'id': 9029, 'synset': 'lychgate.n.01', 'name': 'lychgate'}, {'id': 9030, 'synset': 'lyre.n.01', 'name': 'lyre'}, {'id': 9031, 'synset': 'machete.n.01', 'name': 'machete'}, {'id': 9032, 'synset': 'machicolation.n.01', 'name': 'machicolation'}, {'id': 9033, 'synset': 'machine.n.01', 'name': 'machine'}, {'id': 9034, 'synset': 'machine.n.04', 'name': 'machine'}, {'id': 9035, 'synset': 'machine_bolt.n.01', 'name': 'machine_bolt'}, {'id': 9036, 'synset': 'machinery.n.01', 'name': 'machinery'}, {'id': 9037, 'synset': 'machine_screw.n.01', 'name': 'machine_screw'}, {'id': 9038, 'synset': 'machine_tool.n.01', 'name': 'machine_tool'}, {'id': 9039, 'synset': "machinist's_vise.n.01", 'name': "machinist's_vise"}, {'id': 9040, 'synset': 'machmeter.n.01', 'name': 'machmeter'}, {'id': 9041, 'synset': 'mackinaw.n.04', 'name': 'mackinaw'}, {'id': 9042, 'synset': 'mackinaw.n.03', 'name': 'mackinaw'}, {'id': 9043, 'synset': 'mackinaw.n.01', 'name': 'mackinaw'}, {'id': 9044, 'synset': 'mackintosh.n.01', 'name': 'mackintosh'}, {'id': 9045, 'synset': 'macrame.n.01', 'name': 'macrame'}, {'id': 9046, 'synset': 'madras.n.03', 'name': 'madras'}, {'id': 9047, 'synset': 'mae_west.n.02', 'name': 'Mae_West'}, {'id': 9048, 'synset': 'magazine_rack.n.01', 'name': 'magazine_rack'}, {'id': 9049, 'synset': 'magic_lantern.n.01', 'name': 'magic_lantern'}, {'id': 9050, 'synset': 'magnetic_bottle.n.01', 'name': 'magnetic_bottle'}, {'id': 9051, 'synset': 'magnetic_compass.n.01', 'name': 'magnetic_compass'}, {'id': 9052, 'synset': 'magnetic_core_memory.n.01', 'name': 'magnetic_core_memory'}, {'id': 9053, 'synset': 'magnetic_disk.n.01', 'name': 'magnetic_disk'}, {'id': 9054, 'synset': 'magnetic_head.n.01', 'name': 'magnetic_head'}, {'id': 9055, 'synset': 'magnetic_mine.n.01', 'name': 'magnetic_mine'}, {'id': 9056, 'synset': 'magnetic_needle.n.01', 'name': 'magnetic_needle'}, {'id': 9057, 'synset': 'magnetic_recorder.n.01', 'name': 'magnetic_recorder'}, {'id': 9058, 'synset': 'magnetic_stripe.n.01', 'name': 'magnetic_stripe'}, {'id': 9059, 'synset': 'magnetic_tape.n.01', 'name': 'magnetic_tape'}, {'id': 9060, 'synset': 'magneto.n.01', 'name': 'magneto'}, {'id': 9061, 'synset': 'magnetometer.n.01', 'name': 'magnetometer'}, {'id': 9062, 'synset': 'magnetron.n.01', 'name': 'magnetron'}, {'id': 9063, 'synset': 'magnifier.n.01', 'name': 'magnifier'}, {'id': 9064, 'synset': 'magnum.n.01', 'name': 'magnum'}, {'id': 9065, 'synset': 'magnus_hitch.n.01', 'name': 'magnus_hitch'}, {'id': 9066, 'synset': 'mail.n.03', 'name': 'mail'}, {'id': 9067, 'synset': 'mailbag.n.02', 'name': 'mailbag'}, {'id': 9068, 'synset': 'mailbag.n.01', 'name': 'mailbag'}, {'id': 9069, 'synset': 'mailboat.n.01', 'name': 'mailboat'}, {'id': 9070, 'synset': 'mail_car.n.01', 'name': 'mail_car'}, {'id': 9071, 'synset': 'maildrop.n.01', 'name': 'maildrop'}, {'id': 9072, 'synset': 'mailer.n.04', 'name': 'mailer'}, {'id': 9073, 'synset': 'maillot.n.02', 'name': 'maillot'}, {'id': 9074, 'synset': 'maillot.n.01', 'name': 'maillot'}, {'id': 9075, 'synset': 'mailsorter.n.01', 'name': 'mailsorter'}, {'id': 9076, 'synset': 'mail_train.n.01', 'name': 'mail_train'}, {'id': 9077, 'synset': 'mainframe.n.01', 'name': 'mainframe'}, {'id': 9078, 'synset': 'mainmast.n.01', 'name': 'mainmast'}, {'id': 9079, 'synset': 'main_rotor.n.01', 'name': 'main_rotor'}, {'id': 9080, 'synset': 'mainsail.n.01', 'name': 'mainsail'}, {'id': 9081, 'synset': 'mainspring.n.01', 'name': 'mainspring'}, {'id': 9082, 'synset': 'main-topmast.n.01', 'name': 'main-topmast'}, {'id': 9083, 'synset': 'main-topsail.n.01', 'name': 'main-topsail'}, {'id': 9084, 'synset': 'main_yard.n.01', 'name': 'main_yard'}, {'id': 9085, 'synset': 'maisonette.n.02', 'name': 'maisonette'}, {'id': 9086, 'synset': 'majolica.n.01', 'name': 'majolica'}, {'id': 9087, 'synset': 'makeup.n.01', 'name': 'makeup'}, {'id': 9088, 'synset': 'maksutov_telescope.n.01', 'name': 'Maksutov_telescope'}, {'id': 9089, 'synset': 'malacca.n.02', 'name': 'malacca'}, {'id': 9090, 'synset': 'mallet.n.03', 'name': 'mallet'}, {'id': 9091, 'synset': 'mallet.n.02', 'name': 'mallet'}, {'id': 9092, 'synset': 'mammogram.n.01', 'name': 'mammogram'}, {'id': 9093, 'synset': 'mandola.n.01', 'name': 'mandola'}, {'id': 9094, 'synset': 'mandolin.n.01', 'name': 'mandolin'}, {'id': 9095, 'synset': 'mangle.n.01', 'name': 'mangle'}, {'id': 9096, 'synset': 'manhole_cover.n.01', 'name': 'manhole_cover'}, {'id': 9097, 'synset': 'man-of-war.n.01', 'name': 'man-of-war'}, {'id': 9098, 'synset': 'manometer.n.01', 'name': 'manometer'}, {'id': 9099, 'synset': 'manor.n.01', 'name': 'manor'}, {'id': 9100, 'synset': 'manor_hall.n.01', 'name': 'manor_hall'}, {'id': 9101, 'synset': 'manpad.n.01', 'name': 'MANPAD'}, {'id': 9102, 'synset': 'mansard.n.01', 'name': 'mansard'}, {'id': 9103, 'synset': 'manse.n.02', 'name': 'manse'}, {'id': 9104, 'synset': 'mansion.n.02', 'name': 'mansion'}, {'id': 9105, 'synset': 'mantel.n.01', 'name': 'mantel'}, {'id': 9106, 'synset': 'mantelet.n.02', 'name': 'mantelet'}, {'id': 9107, 'synset': 'mantilla.n.01', 'name': 'mantilla'}, {'id': 9108, 'synset': 'mao_jacket.n.01', 'name': 'Mao_jacket'}, {'id': 9109, 'synset': 'maquiladora.n.01', 'name': 'maquiladora'}, {'id': 9110, 'synset': 'maraca.n.01', 'name': 'maraca'}, {'id': 9111, 'synset': 'marble.n.02', 'name': 'marble'}, {'id': 9112, 'synset': 'marching_order.n.01', 'name': 'marching_order'}, {'id': 9113, 'synset': 'marimba.n.01', 'name': 'marimba'}, {'id': 9114, 'synset': 'marina.n.01', 'name': 'marina'}, {'id': 9115, 'synset': 'marketplace.n.02', 'name': 'marketplace'}, {'id': 9116, 'synset': 'marlinespike.n.01', 'name': 'marlinespike'}, {'id': 9117, 'synset': 'marocain.n.01', 'name': 'marocain'}, {'id': 9118, 'synset': 'marquee.n.02', 'name': 'marquee'}, {'id': 9119, 'synset': 'marquetry.n.01', 'name': 'marquetry'}, {'id': 9120, 'synset': 'marriage_bed.n.01', 'name': 'marriage_bed'}, {'id': 9121, 'synset': 'martello_tower.n.01', 'name': 'martello_tower'}, {'id': 9122, 'synset': 'martingale.n.01', 'name': 'martingale'}, {'id': 9123, 'synset': 'mascara.n.01', 'name': 'mascara'}, {'id': 9124, 'synset': 'maser.n.01', 'name': 'maser'}, {'id': 9125, 'synset': 'mashie.n.01', 'name': 'mashie'}, {'id': 9126, 'synset': 'mashie_niblick.n.01', 'name': 'mashie_niblick'}, {'id': 9127, 'synset': 'masjid.n.01', 'name': 'masjid'}, {'id': 9128, 'synset': 'mask.n.01', 'name': 'mask'}, {'id': 9129, 'synset': 'masonite.n.01', 'name': 'Masonite'}, {'id': 9130, 'synset': 'mason_jar.n.01', 'name': 'Mason_jar'}, {'id': 9131, 'synset': 'masonry.n.01', 'name': 'masonry'}, {'id': 9132, 'synset': "mason's_level.n.01", 'name': "mason's_level"}, {'id': 9133, 'synset': 'massage_parlor.n.02', 'name': 'massage_parlor'}, {'id': 9134, 'synset': 'massage_parlor.n.01', 'name': 'massage_parlor'}, {'id': 9135, 'synset': 'mass_spectrograph.n.01', 'name': 'mass_spectrograph'}, {'id': 9136, 'synset': 'mass_spectrometer.n.01', 'name': 'mass_spectrometer'}, {'id': 9137, 'synset': 'mast.n.04', 'name': 'mast'}, {'id': 9138, 'synset': 'mastaba.n.01', 'name': 'mastaba'}, {'id': 9139, 'synset': 'master_bedroom.n.01', 'name': 'master_bedroom'}, {'id': 9140, 'synset': 'masterpiece.n.01', 'name': 'masterpiece'}, {'id': 9141, 'synset': 'mat.n.01', 'name': 'mat'}, {'id': 9142, 'synset': 'match.n.01', 'name': 'match'}, {'id': 9143, 'synset': 'match.n.03', 'name': 'match'}, {'id': 9144, 'synset': 'matchboard.n.01', 'name': 'matchboard'}, {'id': 9145, 'synset': 'matchbook.n.01', 'name': 'matchbook'}, {'id': 9146, 'synset': 'matchlock.n.01', 'name': 'matchlock'}, {'id': 9147, 'synset': 'match_plane.n.01', 'name': 'match_plane'}, {'id': 9148, 'synset': 'matchstick.n.01', 'name': 'matchstick'}, {'id': 9149, 'synset': 'material.n.04', 'name': 'material'}, {'id': 9150, 'synset': 'materiel.n.01', 'name': 'materiel'}, {'id': 9151, 'synset': 'maternity_hospital.n.01', 'name': 'maternity_hospital'}, {'id': 9152, 'synset': 'maternity_ward.n.01', 'name': 'maternity_ward'}, {'id': 9153, 'synset': 'matrix.n.06', 'name': 'matrix'}, {'id': 9154, 'synset': 'matthew_walker.n.01', 'name': 'Matthew_Walker'}, {'id': 9155, 'synset': 'matting.n.01', 'name': 'matting'}, {'id': 9156, 'synset': 'mattock.n.01', 'name': 'mattock'}, {'id': 9157, 'synset': 'mattress_cover.n.01', 'name': 'mattress_cover'}, {'id': 9158, 'synset': 'maul.n.01', 'name': 'maul'}, {'id': 9159, 'synset': 'maulstick.n.01', 'name': 'maulstick'}, {'id': 9160, 'synset': 'mauser.n.02', 'name': 'Mauser'}, {'id': 9161, 'synset': 'mausoleum.n.01', 'name': 'mausoleum'}, {'id': 9162, 'synset': 'maxi.n.01', 'name': 'maxi'}, {'id': 9163, 'synset': 'maxim_gun.n.01', 'name': 'Maxim_gun'}, {'id': 9164, 'synset': 'maximum_and_minimum_thermometer.n.01', 'name': 'maximum_and_minimum_thermometer'}, {'id': 9165, 'synset': 'maypole.n.01', 'name': 'maypole'}, {'id': 9166, 'synset': 'maze.n.01', 'name': 'maze'}, {'id': 9167, 'synset': 'mazer.n.01', 'name': 'mazer'}, {'id': 9168, 'synset': 'means.n.02', 'name': 'means'}, {'id': 9169, 'synset': 'measure.n.09', 'name': 'measure'}, {'id': 9170, 'synset': 'measuring_instrument.n.01', 'name': 'measuring_instrument'}, {'id': 9171, 'synset': 'meat_counter.n.01', 'name': 'meat_counter'}, {'id': 9172, 'synset': 'meat_grinder.n.01', 'name': 'meat_grinder'}, {'id': 9173, 'synset': 'meat_hook.n.01', 'name': 'meat_hook'}, {'id': 9174, 'synset': 'meat_house.n.02', 'name': 'meat_house'}, {'id': 9175, 'synset': 'meat_safe.n.01', 'name': 'meat_safe'}, {'id': 9176, 'synset': 'meat_thermometer.n.01', 'name': 'meat_thermometer'}, {'id': 9177, 'synset': 'mechanical_device.n.01', 'name': 'mechanical_device'}, {'id': 9178, 'synset': 'mechanical_piano.n.01', 'name': 'mechanical_piano'}, {'id': 9179, 'synset': 'mechanical_system.n.01', 'name': 'mechanical_system'}, {'id': 9180, 'synset': 'mechanism.n.05', 'name': 'mechanism'}, {'id': 9181, 'synset': 'medical_building.n.01', 'name': 'medical_building'}, {'id': 9182, 'synset': 'medical_instrument.n.01', 'name': 'medical_instrument'}, {'id': 9183, 'synset': 'medicine_ball.n.01', 'name': 'medicine_ball'}, {'id': 9184, 'synset': 'medicine_chest.n.01', 'name': 'medicine_chest'}, {'id': 9185, 'synset': 'medline.n.01', 'name': 'MEDLINE'}, {'id': 9186, 'synset': 'megalith.n.01', 'name': 'megalith'}, {'id': 9187, 'synset': 'megaphone.n.01', 'name': 'megaphone'}, {'id': 9188, 'synset': 'memorial.n.03', 'name': 'memorial'}, {'id': 9189, 'synset': 'memory.n.04', 'name': 'memory'}, {'id': 9190, 'synset': 'memory_chip.n.01', 'name': 'memory_chip'}, {'id': 9191, 'synset': 'memory_device.n.01', 'name': 'memory_device'}, {'id': 9192, 'synset': 'menagerie.n.02', 'name': 'menagerie'}, {'id': 9193, 'synset': 'mending.n.01', 'name': 'mending'}, {'id': 9194, 'synset': 'menhir.n.01', 'name': 'menhir'}, {'id': 9195, 'synset': 'menorah.n.02', 'name': 'menorah'}, {'id': 9196, 'synset': 'menorah.n.01', 'name': 'Menorah'}, {'id': 9197, 'synset': "man's_clothing.n.01", 'name': "man's_clothing"}, {'id': 9198, 'synset': "men's_room.n.01", 'name': "men's_room"}, {'id': 9199, 'synset': 'mercantile_establishment.n.01', 'name': 'mercantile_establishment'}, {'id': 9200, 'synset': 'mercury_barometer.n.01', 'name': 'mercury_barometer'}, {'id': 9201, 'synset': 'mercury_cell.n.01', 'name': 'mercury_cell'}, {'id': 9202, 'synset': 'mercury_thermometer.n.01', 'name': 'mercury_thermometer'}, {'id': 9203, 'synset': 'mercury-vapor_lamp.n.01', 'name': 'mercury-vapor_lamp'}, {'id': 9204, 'synset': 'mercy_seat.n.02', 'name': 'mercy_seat'}, {'id': 9205, 'synset': 'merlon.n.01', 'name': 'merlon'}, {'id': 9206, 'synset': 'mess.n.05', 'name': 'mess'}, {'id': 9207, 'synset': 'mess_jacket.n.01', 'name': 'mess_jacket'}, {'id': 9208, 'synset': 'mess_kit.n.01', 'name': 'mess_kit'}, {'id': 9209, 'synset': 'messuage.n.01', 'name': 'messuage'}, {'id': 9210, 'synset': 'metal_detector.n.01', 'name': 'metal_detector'}, {'id': 9211, 'synset': 'metallic.n.01', 'name': 'metallic'}, {'id': 9212, 'synset': 'metal_screw.n.01', 'name': 'metal_screw'}, {'id': 9213, 'synset': 'metal_wood.n.01', 'name': 'metal_wood'}, {'id': 9214, 'synset': 'meteorological_balloon.n.01', 'name': 'meteorological_balloon'}, {'id': 9215, 'synset': 'meter.n.02', 'name': 'meter'}, {'id': 9216, 'synset': 'meterstick.n.01', 'name': 'meterstick'}, {'id': 9217, 'synset': 'metronome.n.01', 'name': 'metronome'}, {'id': 9218, 'synset': 'mezzanine.n.02', 'name': 'mezzanine'}, {'id': 9219, 'synset': 'mezzanine.n.01', 'name': 'mezzanine'}, {'id': 9220, 'synset': 'microbalance.n.01', 'name': 'microbalance'}, {'id': 9221, 'synset': 'microbrewery.n.01', 'name': 'microbrewery'}, {'id': 9222, 'synset': 'microfiche.n.01', 'name': 'microfiche'}, {'id': 9223, 'synset': 'microfilm.n.01', 'name': 'microfilm'}, {'id': 9224, 'synset': 'micrometer.n.02', 'name': 'micrometer'}, {'id': 9225, 'synset': 'microprocessor.n.01', 'name': 'microprocessor'}, {'id': 9226, 'synset': 'microtome.n.01', 'name': 'microtome'}, {'id': 9227, 'synset': 'microwave_diathermy_machine.n.01', 'name': 'microwave_diathermy_machine'}, {'id': 9228, 'synset': 'microwave_linear_accelerator.n.01', 'name': 'microwave_linear_accelerator'}, {'id': 9229, 'synset': 'middy.n.01', 'name': 'middy'}, {'id': 9230, 'synset': 'midiron.n.01', 'name': 'midiron'}, {'id': 9231, 'synset': 'mihrab.n.02', 'name': 'mihrab'}, {'id': 9232, 'synset': 'mihrab.n.01', 'name': 'mihrab'}, {'id': 9233, 'synset': 'military_hospital.n.01', 'name': 'military_hospital'}, {'id': 9234, 'synset': 'military_quarters.n.01', 'name': 'military_quarters'}, {'id': 9235, 'synset': 'military_uniform.n.01', 'name': 'military_uniform'}, {'id': 9236, 'synset': 'military_vehicle.n.01', 'name': 'military_vehicle'}, {'id': 9237, 'synset': 'milk_bar.n.01', 'name': 'milk_bar'}, {'id': 9238, 'synset': 'milk_float.n.01', 'name': 'milk_float'}, {'id': 9239, 'synset': 'milking_machine.n.01', 'name': 'milking_machine'}, {'id': 9240, 'synset': 'milking_stool.n.01', 'name': 'milking_stool'}, {'id': 9241, 'synset': 'milk_wagon.n.01', 'name': 'milk_wagon'}, {'id': 9242, 'synset': 'mill.n.04', 'name': 'mill'}, {'id': 9243, 'synset': 'milldam.n.01', 'name': 'milldam'}, {'id': 9244, 'synset': 'miller.n.05', 'name': 'miller'}, {'id': 9245, 'synset': 'milliammeter.n.01', 'name': 'milliammeter'}, {'id': 9246, 'synset': 'millinery.n.02', 'name': 'millinery'}, {'id': 9247, 'synset': 'millinery.n.01', 'name': 'millinery'}, {'id': 9248, 'synset': 'milling.n.01', 'name': 'milling'}, {'id': 9249, 'synset': 'millivoltmeter.n.01', 'name': 'millivoltmeter'}, {'id': 9250, 'synset': 'millstone.n.03', 'name': 'millstone'}, {'id': 9251, 'synset': 'millstone.n.02', 'name': 'millstone'}, {'id': 9252, 'synset': 'millwheel.n.01', 'name': 'millwheel'}, {'id': 9253, 'synset': 'mimeograph.n.01', 'name': 'mimeograph'}, {'id': 9254, 'synset': 'minaret.n.01', 'name': 'minaret'}, {'id': 9255, 'synset': 'mincer.n.01', 'name': 'mincer'}, {'id': 9256, 'synset': 'mine.n.02', 'name': 'mine'}, {'id': 9257, 'synset': 'mine_detector.n.01', 'name': 'mine_detector'}, {'id': 9258, 'synset': 'minelayer.n.01', 'name': 'minelayer'}, {'id': 9259, 'synset': 'mineshaft.n.01', 'name': 'mineshaft'}, {'id': 9260, 'synset': 'minibar.n.01', 'name': 'minibar'}, {'id': 9261, 'synset': 'minibike.n.01', 'name': 'minibike'}, {'id': 9262, 'synset': 'minibus.n.01', 'name': 'minibus'}, {'id': 9263, 'synset': 'minicar.n.01', 'name': 'minicar'}, {'id': 9264, 'synset': 'minicomputer.n.01', 'name': 'minicomputer'}, {'id': 9265, 'synset': 'ministry.n.02', 'name': 'ministry'}, {'id': 9266, 'synset': 'miniskirt.n.01', 'name': 'miniskirt'}, {'id': 9267, 'synset': 'minisub.n.01', 'name': 'minisub'}, {'id': 9268, 'synset': 'miniver.n.01', 'name': 'miniver'}, {'id': 9269, 'synset': 'mink.n.02', 'name': 'mink'}, {'id': 9270, 'synset': 'minster.n.01', 'name': 'minster'}, {'id': 9271, 'synset': 'mint.n.06', 'name': 'mint'}, {'id': 9272, 'synset': 'minute_hand.n.01', 'name': 'minute_hand'}, {'id': 9273, 'synset': 'minuteman.n.02', 'name': 'Minuteman'}, {'id': 9274, 'synset': 'missile.n.01', 'name': 'missile'}, {'id': 9275, 'synset': 'missile_defense_system.n.01', 'name': 'missile_defense_system'}, {'id': 9276, 'synset': 'miter_box.n.01', 'name': 'miter_box'}, {'id': 9277, 'synset': 'miter_joint.n.01', 'name': 'miter_joint'}, {'id': 9278, 'synset': 'mixer.n.03', 'name': 'mixer'}, {'id': 9279, 'synset': 'mixing_bowl.n.01', 'name': 'mixing_bowl'}, {'id': 9280, 'synset': 'mixing_faucet.n.01', 'name': 'mixing_faucet'}, {'id': 9281, 'synset': 'mizzen.n.02', 'name': 'mizzen'}, {'id': 9282, 'synset': 'mizzenmast.n.01', 'name': 'mizzenmast'}, {'id': 9283, 'synset': 'mobcap.n.01', 'name': 'mobcap'}, {'id': 9284, 'synset': 'mobile_home.n.01', 'name': 'mobile_home'}, {'id': 9285, 'synset': 'moccasin.n.01', 'name': 'moccasin'}, {'id': 9286, 'synset': 'mock-up.n.01', 'name': 'mock-up'}, {'id': 9287, 'synset': 'mod_con.n.01', 'name': 'mod_con'}, {'id': 9288, 'synset': 'model_t.n.01', 'name': 'Model_T'}, {'id': 9289, 'synset': 'modem.n.01', 'name': 'modem'}, {'id': 9290, 'synset': 'modillion.n.01', 'name': 'modillion'}, {'id': 9291, 'synset': 'module.n.03', 'name': 'module'}, {'id': 9292, 'synset': 'module.n.02', 'name': 'module'}, {'id': 9293, 'synset': 'mohair.n.01', 'name': 'mohair'}, {'id': 9294, 'synset': 'moire.n.01', 'name': 'moire'}, {'id': 9295, 'synset': 'mold.n.02', 'name': 'mold'}, {'id': 9296, 'synset': 'moldboard.n.01', 'name': 'moldboard'}, {'id': 9297, 'synset': 'moldboard_plow.n.01', 'name': 'moldboard_plow'}, {'id': 9298, 'synset': 'moleskin.n.01', 'name': 'moleskin'}, {'id': 9299, 'synset': 'molotov_cocktail.n.01', 'name': 'Molotov_cocktail'}, {'id': 9300, 'synset': 'monastery.n.01', 'name': 'monastery'}, {'id': 9301, 'synset': 'monastic_habit.n.01', 'name': 'monastic_habit'}, {'id': 9302, 'synset': 'moneybag.n.01', 'name': 'moneybag'}, {'id': 9303, 'synset': 'money_belt.n.01', 'name': 'money_belt'}, {'id': 9304, 'synset': 'monitor.n.06', 'name': 'monitor'}, {'id': 9305, 'synset': 'monitor.n.05', 'name': 'monitor'}, {'id': 9306, 'synset': 'monkey-wrench.n.01', 'name': 'monkey-wrench'}, {'id': 9307, 'synset': "monk's_cloth.n.01", 'name': "monk's_cloth"}, {'id': 9308, 'synset': 'monochrome.n.01', 'name': 'monochrome'}, {'id': 9309, 'synset': 'monocle.n.01', 'name': 'monocle'}, {'id': 9310, 'synset': 'monofocal_lens_implant.n.01', 'name': 'monofocal_lens_implant'}, {'id': 9311, 'synset': 'monoplane.n.01', 'name': 'monoplane'}, {'id': 9312, 'synset': 'monotype.n.02', 'name': 'monotype'}, {'id': 9313, 'synset': 'monstrance.n.02', 'name': 'monstrance'}, {'id': 9314, 'synset': 'mooring_tower.n.01', 'name': 'mooring_tower'}, {'id': 9315, 'synset': 'moorish_arch.n.01', 'name': 'Moorish_arch'}, {'id': 9316, 'synset': 'moped.n.01', 'name': 'moped'}, {'id': 9317, 'synset': 'mop_handle.n.01', 'name': 'mop_handle'}, {'id': 9318, 'synset': 'moquette.n.01', 'name': 'moquette'}, {'id': 9319, 'synset': 'morgue.n.01', 'name': 'morgue'}, {'id': 9320, 'synset': 'morion.n.01', 'name': 'morion'}, {'id': 9321, 'synset': 'morning_dress.n.02', 'name': 'morning_dress'}, {'id': 9322, 'synset': 'morning_dress.n.01', 'name': 'morning_dress'}, {'id': 9323, 'synset': 'morning_room.n.01', 'name': 'morning_room'}, {'id': 9324, 'synset': 'morris_chair.n.01', 'name': 'Morris_chair'}, {'id': 9325, 'synset': 'mortar.n.01', 'name': 'mortar'}, {'id': 9326, 'synset': 'mortar.n.03', 'name': 'mortar'}, {'id': 9327, 'synset': 'mortarboard.n.02', 'name': 'mortarboard'}, {'id': 9328, 'synset': 'mortise_joint.n.02', 'name': 'mortise_joint'}, {'id': 9329, 'synset': 'mosaic.n.05', 'name': 'mosaic'}, {'id': 9330, 'synset': 'mosque.n.01', 'name': 'mosque'}, {'id': 9331, 'synset': 'mosquito_net.n.01', 'name': 'mosquito_net'}, {'id': 9332, 'synset': 'motel.n.01', 'name': 'motel'}, {'id': 9333, 'synset': 'motel_room.n.01', 'name': 'motel_room'}, {'id': 9334, 'synset': 'mother_hubbard.n.01', 'name': 'Mother_Hubbard'}, {'id': 9335, 'synset': 'motion-picture_camera.n.01', 'name': 'motion-picture_camera'}, {'id': 9336, 'synset': 'motion-picture_film.n.01', 'name': 'motion-picture_film'}, {'id': 9337, 'synset': 'motley.n.03', 'name': 'motley'}, {'id': 9338, 'synset': 'motley.n.02', 'name': 'motley'}, {'id': 9339, 'synset': 'motorboat.n.01', 'name': 'motorboat'}, {'id': 9340, 'synset': 'motor_hotel.n.01', 'name': 'motor_hotel'}, {'id': 9341, 'synset': 'motorized_wheelchair.n.01', 'name': 'motorized_wheelchair'}, {'id': 9342, 'synset': 'mound.n.04', 'name': 'mound'}, {'id': 9343, 'synset': 'mount.n.04', 'name': 'mount'}, {'id': 9344, 'synset': 'mountain_bike.n.01', 'name': 'mountain_bike'}, {'id': 9345, 'synset': 'mountain_tent.n.01', 'name': 'mountain_tent'}, {'id': 9346, 'synset': 'mouse_button.n.01', 'name': 'mouse_button'}, {'id': 9347, 'synset': 'mousetrap.n.01', 'name': 'mousetrap'}, {'id': 9348, 'synset': 'mousse.n.03', 'name': 'mousse'}, {'id': 9349, 'synset': 'mouthpiece.n.06', 'name': 'mouthpiece'}, {'id': 9350, 'synset': 'mouthpiece.n.02', 'name': 'mouthpiece'}, {'id': 9351, 'synset': 'mouthpiece.n.04', 'name': 'mouthpiece'}, {'id': 9352, 'synset': 'movement.n.10', 'name': 'movement'}, {'id': 9353, 'synset': 'movie_projector.n.01', 'name': 'movie_projector'}, {'id': 9354, 'synset': 'moving-coil_galvanometer.n.01', 'name': 'moving-coil_galvanometer'}, {'id': 9355, 'synset': 'moving_van.n.01', 'name': 'moving_van'}, {'id': 9356, 'synset': 'mud_brick.n.01', 'name': 'mud_brick'}, {'id': 9357, 'synset': 'mudguard.n.01', 'name': 'mudguard'}, {'id': 9358, 'synset': 'mudhif.n.01', 'name': 'mudhif'}, {'id': 9359, 'synset': 'muff.n.01', 'name': 'muff'}, {'id': 9360, 'synset': 'muffle.n.01', 'name': 'muffle'}, {'id': 9361, 'synset': 'muffler.n.02', 'name': 'muffler'}, {'id': 9362, 'synset': 'mufti.n.02', 'name': 'mufti'}, {'id': 9363, 'synset': 'mulch.n.01', 'name': 'mulch'}, {'id': 9364, 'synset': 'mule.n.02', 'name': 'mule'}, {'id': 9365, 'synset': 'multichannel_recorder.n.01', 'name': 'multichannel_recorder'}, {'id': 9366, 'synset': 'multiengine_airplane.n.01', 'name': 'multiengine_airplane'}, {'id': 9367, 'synset': 'multiplex.n.02', 'name': 'multiplex'}, {'id': 9368, 'synset': 'multiplexer.n.01', 'name': 'multiplexer'}, {'id': 9369, 'synset': 'multiprocessor.n.01', 'name': 'multiprocessor'}, {'id': 9370, 'synset': 'multistage_rocket.n.01', 'name': 'multistage_rocket'}, {'id': 9371, 'synset': 'munition.n.02', 'name': 'munition'}, {'id': 9372, 'synset': 'murphy_bed.n.01', 'name': 'Murphy_bed'}, {'id': 9373, 'synset': 'musette.n.01', 'name': 'musette'}, {'id': 9374, 'synset': 'musette_pipe.n.01', 'name': 'musette_pipe'}, {'id': 9375, 'synset': 'museum.n.01', 'name': 'museum'}, {'id': 9376, 'synset': 'mushroom_anchor.n.01', 'name': 'mushroom_anchor'}, {'id': 9377, 'synset': 'music_box.n.01', 'name': 'music_box'}, {'id': 9378, 'synset': 'music_hall.n.01', 'name': 'music_hall'}, {'id': 9379, 'synset': 'music_school.n.02', 'name': 'music_school'}, {'id': 9380, 'synset': 'music_stand.n.01', 'name': 'music_stand'}, {'id': 9381, 'synset': 'musket.n.01', 'name': 'musket'}, {'id': 9382, 'synset': 'musket_ball.n.01', 'name': 'musket_ball'}, {'id': 9383, 'synset': 'muslin.n.01', 'name': 'muslin'}, {'id': 9384, 'synset': 'mustache_cup.n.01', 'name': 'mustache_cup'}, {'id': 9385, 'synset': 'mustard_plaster.n.01', 'name': 'mustard_plaster'}, {'id': 9386, 'synset': 'mute.n.02', 'name': 'mute'}, {'id': 9387, 'synset': 'muzzle_loader.n.01', 'name': 'muzzle_loader'}, {'id': 9388, 'synset': 'muzzle.n.03', 'name': 'muzzle'}, {'id': 9389, 'synset': 'myelogram.n.01', 'name': 'myelogram'}, {'id': 9390, 'synset': 'nacelle.n.01', 'name': 'nacelle'}, {'id': 9391, 'synset': 'nail.n.02', 'name': 'nail'}, {'id': 9392, 'synset': 'nailbrush.n.01', 'name': 'nailbrush'}, {'id': 9393, 'synset': 'nailhead.n.02', 'name': 'nailhead'}, {'id': 9394, 'synset': 'nailhead.n.01', 'name': 'nailhead'}, {'id': 9395, 'synset': 'nail_polish.n.01', 'name': 'nail_polish'}, {'id': 9396, 'synset': 'nainsook.n.01', 'name': 'nainsook'}, {'id': 9397, 'synset': "napier's_bones.n.01", 'name': "Napier's_bones"}, {'id': 9398, 'synset': 'nard.n.01', 'name': 'nard'}, {'id': 9399, 'synset': 'narrowbody_aircraft.n.01', 'name': 'narrowbody_aircraft'}, {'id': 9400, 'synset': 'narrow_wale.n.01', 'name': 'narrow_wale'}, {'id': 9401, 'synset': 'narthex.n.02', 'name': 'narthex'}, {'id': 9402, 'synset': 'narthex.n.01', 'name': 'narthex'}, {'id': 9403, 'synset': 'nasotracheal_tube.n.01', 'name': 'nasotracheal_tube'}, {'id': 9404, 'synset': 'national_monument.n.01', 'name': 'national_monument'}, {'id': 9405, 'synset': 'nautilus.n.01', 'name': 'nautilus'}, {'id': 9406, 'synset': 'navigational_system.n.01', 'name': 'navigational_system'}, {'id': 9407, 'synset': 'naval_equipment.n.01', 'name': 'naval_equipment'}, {'id': 9408, 'synset': 'naval_gun.n.01', 'name': 'naval_gun'}, {'id': 9409, 'synset': 'naval_missile.n.01', 'name': 'naval_missile'}, {'id': 9410, 'synset': 'naval_radar.n.01', 'name': 'naval_radar'}, {'id': 9411, 'synset': 'naval_tactical_data_system.n.01', 'name': 'naval_tactical_data_system'}, {'id': 9412, 'synset': 'naval_weaponry.n.01', 'name': 'naval_weaponry'}, {'id': 9413, 'synset': 'nave.n.01', 'name': 'nave'}, {'id': 9414, 'synset': 'navigational_instrument.n.01', 'name': 'navigational_instrument'}, {'id': 9415, 'synset': 'nebuchadnezzar.n.02', 'name': 'nebuchadnezzar'}, {'id': 9416, 'synset': 'neckband.n.01', 'name': 'neckband'}, {'id': 9417, 'synset': 'neck_brace.n.01', 'name': 'neck_brace'}, {'id': 9418, 'synset': 'neckcloth.n.01', 'name': 'neckcloth'}, {'id': 9419, 'synset': 'necklet.n.01', 'name': 'necklet'}, {'id': 9420, 'synset': 'neckline.n.01', 'name': 'neckline'}, {'id': 9421, 'synset': 'neckpiece.n.01', 'name': 'neckpiece'}, {'id': 9422, 'synset': 'neckwear.n.01', 'name': 'neckwear'}, {'id': 9423, 'synset': 'needle.n.02', 'name': 'needle'}, {'id': 9424, 'synset': 'needlenose_pliers.n.01', 'name': 'needlenose_pliers'}, {'id': 9425, 'synset': 'needlework.n.01', 'name': 'needlework'}, {'id': 9426, 'synset': 'negative.n.02', 'name': 'negative'}, {'id': 9427, 'synset': 'negative_magnetic_pole.n.01', 'name': 'negative_magnetic_pole'}, {'id': 9428, 'synset': 'negative_pole.n.01', 'name': 'negative_pole'}, {'id': 9429, 'synset': 'negligee.n.01', 'name': 'negligee'}, {'id': 9430, 'synset': 'neolith.n.01', 'name': 'neolith'}, {'id': 9431, 'synset': 'neon_lamp.n.01', 'name': 'neon_lamp'}, {'id': 9432, 'synset': 'nephoscope.n.01', 'name': 'nephoscope'}, {'id': 9433, 'synset': 'nest.n.05', 'name': 'nest'}, {'id': 9434, 'synset': 'nest_egg.n.02', 'name': 'nest_egg'}, {'id': 9435, 'synset': 'net.n.06', 'name': 'net'}, {'id': 9436, 'synset': 'net.n.02', 'name': 'net'}, {'id': 9437, 'synset': 'net.n.05', 'name': 'net'}, {'id': 9438, 'synset': 'net.n.04', 'name': 'net'}, {'id': 9439, 'synset': 'network.n.05', 'name': 'network'}, {'id': 9440, 'synset': 'network.n.04', 'name': 'network'}, {'id': 9441, 'synset': 'neutron_bomb.n.01', 'name': 'neutron_bomb'}, {'id': 9442, 'synset': 'newel.n.02', 'name': 'newel'}, {'id': 9443, 'synset': 'newel_post.n.01', 'name': 'newel_post'}, {'id': 9444, 'synset': 'newspaper.n.03', 'name': 'newspaper'}, {'id': 9445, 'synset': 'newsroom.n.03', 'name': 'newsroom'}, {'id': 9446, 'synset': 'newsroom.n.02', 'name': 'newsroom'}, {'id': 9447, 'synset': 'newtonian_telescope.n.01', 'name': 'Newtonian_telescope'}, {'id': 9448, 'synset': 'nib.n.01', 'name': 'nib'}, {'id': 9449, 'synset': 'niblick.n.01', 'name': 'niblick'}, {'id': 9450, 'synset': 'nicad.n.01', 'name': 'nicad'}, {'id': 9451, 'synset': 'nickel-iron_battery.n.01', 'name': 'nickel-iron_battery'}, {'id': 9452, 'synset': 'nicol_prism.n.01', 'name': 'Nicol_prism'}, {'id': 9453, 'synset': 'night_bell.n.01', 'name': 'night_bell'}, {'id': 9454, 'synset': 'nightcap.n.02', 'name': 'nightcap'}, {'id': 9455, 'synset': 'nightgown.n.01', 'name': 'nightgown'}, {'id': 9456, 'synset': 'night_latch.n.01', 'name': 'night_latch'}, {'id': 9457, 'synset': 'night-light.n.01', 'name': 'night-light'}, {'id': 9458, 'synset': 'nightshirt.n.01', 'name': 'nightshirt'}, {'id': 9459, 'synset': 'ninepin.n.01', 'name': 'ninepin'}, {'id': 9460, 'synset': 'ninepin_ball.n.01', 'name': 'ninepin_ball'}, {'id': 9461, 'synset': 'ninon.n.01', 'name': 'ninon'}, {'id': 9462, 'synset': 'nipple.n.02', 'name': 'nipple'}, {'id': 9463, 'synset': 'nipple_shield.n.01', 'name': 'nipple_shield'}, {'id': 9464, 'synset': 'niqab.n.01', 'name': 'niqab'}, {'id': 9465, 'synset': 'nissen_hut.n.01', 'name': 'Nissen_hut'}, {'id': 9466, 'synset': 'nogging.n.01', 'name': 'nogging'}, {'id': 9467, 'synset': 'noisemaker.n.01', 'name': 'noisemaker'}, {'id': 9468, 'synset': 'nonsmoker.n.02', 'name': 'nonsmoker'}, {'id': 9469, 'synset': 'non-volatile_storage.n.01', 'name': 'non-volatile_storage'}, {'id': 9470, 'synset': 'norfolk_jacket.n.01', 'name': 'Norfolk_jacket'}, {'id': 9471, 'synset': 'noria.n.01', 'name': 'noria'}, {'id': 9472, 'synset': 'nose_flute.n.01', 'name': 'nose_flute'}, {'id': 9473, 'synset': 'nosewheel.n.01', 'name': 'nosewheel'}, {'id': 9474, 'synset': 'notebook.n.02', 'name': 'notebook'}, {'id': 9475, 'synset': 'nuclear-powered_ship.n.01', 'name': 'nuclear-powered_ship'}, {'id': 9476, 'synset': 'nuclear_reactor.n.01', 'name': 'nuclear_reactor'}, {'id': 9477, 'synset': 'nuclear_rocket.n.01', 'name': 'nuclear_rocket'}, {'id': 9478, 'synset': 'nuclear_weapon.n.01', 'name': 'nuclear_weapon'}, {'id': 9479, 'synset': 'nude.n.01', 'name': 'nude'}, {'id': 9480, 'synset': 'numdah.n.01', 'name': 'numdah'}, {'id': 9481, 'synset': "nun's_habit.n.01", 'name': "nun's_habit"}, {'id': 9482, 'synset': 'nursery.n.01', 'name': 'nursery'}, {'id': 9483, 'synset': 'nut_and_bolt.n.01', 'name': 'nut_and_bolt'}, {'id': 9484, 'synset': 'nylon.n.02', 'name': 'nylon'}, {'id': 9485, 'synset': 'nylons.n.01', 'name': 'nylons'}, {'id': 9486, 'synset': 'oast.n.01', 'name': 'oast'}, {'id': 9487, 'synset': 'oast_house.n.01', 'name': 'oast_house'}, {'id': 9488, 'synset': 'obelisk.n.01', 'name': 'obelisk'}, {'id': 9489, 'synset': 'object_ball.n.01', 'name': 'object_ball'}, {'id': 9490, 'synset': 'objective.n.02', 'name': 'objective'}, {'id': 9491, 'synset': 'oblique_bandage.n.01', 'name': 'oblique_bandage'}, {'id': 9492, 'synset': 'oboe.n.01', 'name': 'oboe'}, {'id': 9493, 'synset': 'oboe_da_caccia.n.01', 'name': 'oboe_da_caccia'}, {'id': 9494, 'synset': "oboe_d'amore.n.01", 'name': "oboe_d'amore"}, {'id': 9495, 'synset': 'observation_dome.n.01', 'name': 'observation_dome'}, {'id': 9496, 'synset': 'observatory.n.01', 'name': 'observatory'}, {'id': 9497, 'synset': 'obstacle.n.02', 'name': 'obstacle'}, {'id': 9498, 'synset': 'obturator.n.01', 'name': 'obturator'}, {'id': 9499, 'synset': 'ocarina.n.01', 'name': 'ocarina'}, {'id': 9500, 'synset': 'octant.n.01', 'name': 'octant'}, {'id': 9501, 'synset': 'odd-leg_caliper.n.01', 'name': 'odd-leg_caliper'}, {'id': 9502, 'synset': 'odometer.n.01', 'name': 'odometer'}, {'id': 9503, 'synset': 'oeil_de_boeuf.n.01', 'name': 'oeil_de_boeuf'}, {'id': 9504, 'synset': 'office.n.01', 'name': 'office'}, {'id': 9505, 'synset': 'office_building.n.01', 'name': 'office_building'}, {'id': 9506, 'synset': 'office_furniture.n.01', 'name': 'office_furniture'}, {'id': 9507, 'synset': "officer's_mess.n.01", 'name': "officer's_mess"}, {'id': 9508, 'synset': 'off-line_equipment.n.01', 'name': 'off-line_equipment'}, {'id': 9509, 'synset': 'ogee.n.01', 'name': 'ogee'}, {'id': 9510, 'synset': 'ogee_arch.n.01', 'name': 'ogee_arch'}, {'id': 9511, 'synset': 'ohmmeter.n.01', 'name': 'ohmmeter'}, {'id': 9512, 'synset': 'oil.n.02', 'name': 'oil'}, {'id': 9513, 'synset': 'oilcan.n.01', 'name': 'oilcan'}, {'id': 9514, 'synset': 'oilcloth.n.01', 'name': 'oilcloth'}, {'id': 9515, 'synset': 'oil_filter.n.01', 'name': 'oil_filter'}, {'id': 9516, 'synset': 'oil_heater.n.01', 'name': 'oil_heater'}, {'id': 9517, 'synset': 'oil_paint.n.01', 'name': 'oil_paint'}, {'id': 9518, 'synset': 'oil_pump.n.01', 'name': 'oil_pump'}, {'id': 9519, 'synset': 'oil_refinery.n.01', 'name': 'oil_refinery'}, {'id': 9520, 'synset': 'oilskin.n.01', 'name': 'oilskin'}, {'id': 9521, 'synset': 'oil_slick.n.01', 'name': 'oil_slick'}, {'id': 9522, 'synset': 'oilstone.n.01', 'name': 'oilstone'}, {'id': 9523, 'synset': 'oil_tanker.n.01', 'name': 'oil_tanker'}, {'id': 9524, 'synset': 'old_school_tie.n.01', 'name': 'old_school_tie'}, {'id': 9525, 'synset': 'olive_drab.n.03', 'name': 'olive_drab'}, {'id': 9526, 'synset': 'olive_drab.n.02', 'name': 'olive_drab'}, {'id': 9527, 'synset': 'olympian_zeus.n.01', 'name': 'Olympian_Zeus'}, {'id': 9528, 'synset': 'omelet_pan.n.01', 'name': 'omelet_pan'}, {'id': 9529, 'synset': 'omnidirectional_antenna.n.01', 'name': 'omnidirectional_antenna'}, {'id': 9530, 'synset': 'omnirange.n.01', 'name': 'omnirange'}, {'id': 9531, 'synset': 'onion_dome.n.01', 'name': 'onion_dome'}, {'id': 9532, 'synset': 'open-air_market.n.01', 'name': 'open-air_market'}, {'id': 9533, 'synset': 'open_circuit.n.01', 'name': 'open_circuit'}, {'id': 9534, 'synset': 'open-end_wrench.n.01', 'name': 'open-end_wrench'}, {'id': 9535, 'synset': 'opener.n.03', 'name': 'opener'}, {'id': 9536, 'synset': 'open-hearth_furnace.n.01', 'name': 'open-hearth_furnace'}, {'id': 9537, 'synset': 'openside_plane.n.01', 'name': 'openside_plane'}, {'id': 9538, 'synset': 'open_sight.n.01', 'name': 'open_sight'}, {'id': 9539, 'synset': 'openwork.n.01', 'name': 'openwork'}, {'id': 9540, 'synset': 'opera.n.03', 'name': 'opera'}, {'id': 9541, 'synset': 'opera_cloak.n.01', 'name': 'opera_cloak'}, {'id': 9542, 'synset': 'operating_microscope.n.01', 'name': 'operating_microscope'}, {'id': 9543, 'synset': 'operating_room.n.01', 'name': 'operating_room'}, {'id': 9544, 'synset': 'operating_table.n.01', 'name': 'operating_table'}, {'id': 9545, 'synset': 'ophthalmoscope.n.01', 'name': 'ophthalmoscope'}, {'id': 9546, 'synset': 'optical_device.n.01', 'name': 'optical_device'}, {'id': 9547, 'synset': 'optical_disk.n.01', 'name': 'optical_disk'}, {'id': 9548, 'synset': 'optical_instrument.n.01', 'name': 'optical_instrument'}, {'id': 9549, 'synset': 'optical_pyrometer.n.01', 'name': 'optical_pyrometer'}, {'id': 9550, 'synset': 'optical_telescope.n.01', 'name': 'optical_telescope'}, {'id': 9551, 'synset': 'orchestra_pit.n.01', 'name': 'orchestra_pit'}, {'id': 9552, 'synset': 'ordinary.n.04', 'name': 'ordinary'}, {'id': 9553, 'synset': 'organ.n.05', 'name': 'organ'}, {'id': 9554, 'synset': 'organdy.n.01', 'name': 'organdy'}, {'id': 9555, 'synset': 'organic_light-emitting_diode.n.01', 'name': 'organic_light-emitting_diode'}, {'id': 9556, 'synset': 'organ_loft.n.01', 'name': 'organ_loft'}, {'id': 9557, 'synset': 'organ_pipe.n.01', 'name': 'organ_pipe'}, {'id': 9558, 'synset': 'organza.n.01', 'name': 'organza'}, {'id': 9559, 'synset': 'oriel.n.01', 'name': 'oriel'}, {'id': 9560, 'synset': 'oriflamme.n.02', 'name': 'oriflamme'}, {'id': 9561, 'synset': 'o_ring.n.01', 'name': 'O_ring'}, {'id': 9562, 'synset': 'orlon.n.01', 'name': 'Orlon'}, {'id': 9563, 'synset': 'orlop_deck.n.01', 'name': 'orlop_deck'}, {'id': 9564, 'synset': 'orphanage.n.02', 'name': 'orphanage'}, {'id': 9565, 'synset': 'orphrey.n.01', 'name': 'orphrey'}, {'id': 9566, 'synset': 'orrery.n.01', 'name': 'orrery'}, {'id': 9567, 'synset': 'orthicon.n.01', 'name': 'orthicon'}, {'id': 9568, 'synset': 'orthochromatic_film.n.01', 'name': 'orthochromatic_film'}, {'id': 9569, 'synset': 'orthopter.n.01', 'name': 'orthopter'}, {'id': 9570, 'synset': 'orthoscope.n.01', 'name': 'orthoscope'}, {'id': 9571, 'synset': 'oscillograph.n.01', 'name': 'oscillograph'}, {'id': 9572, 'synset': 'oscilloscope.n.01', 'name': 'oscilloscope'}, {'id': 9573, 'synset': 'ossuary.n.01', 'name': 'ossuary'}, {'id': 9574, 'synset': 'otoscope.n.01', 'name': 'otoscope'}, {'id': 9575, 'synset': 'oubliette.n.01', 'name': 'oubliette'}, {'id': 9576, 'synset': 'out-basket.n.01', 'name': 'out-basket'}, {'id': 9577, 'synset': 'outboard_motor.n.01', 'name': 'outboard_motor'}, {'id': 9578, 'synset': 'outboard_motorboat.n.01', 'name': 'outboard_motorboat'}, {'id': 9579, 'synset': 'outbuilding.n.01', 'name': 'outbuilding'}, {'id': 9580, 'synset': 'outerwear.n.01', 'name': 'outerwear'}, {'id': 9581, 'synset': 'outfall.n.01', 'name': 'outfall'}, {'id': 9582, 'synset': 'outfit.n.02', 'name': 'outfit'}, {'id': 9583, 'synset': 'outfitter.n.02', 'name': 'outfitter'}, {'id': 9584, 'synset': 'outhouse.n.01', 'name': 'outhouse'}, {'id': 9585, 'synset': 'output_device.n.01', 'name': 'output_device'}, {'id': 9586, 'synset': 'outrigger.n.01', 'name': 'outrigger'}, {'id': 9587, 'synset': 'outrigger_canoe.n.01', 'name': 'outrigger_canoe'}, {'id': 9588, 'synset': 'outside_caliper.n.01', 'name': 'outside_caliper'}, {'id': 9589, 'synset': 'outside_mirror.n.01', 'name': 'outside_mirror'}, {'id': 9590, 'synset': 'outwork.n.01', 'name': 'outwork'}, {'id': 9591, 'synset': 'oven_thermometer.n.01', 'name': 'oven_thermometer'}, {'id': 9592, 'synset': 'overall.n.02', 'name': 'overall'}, {'id': 9593, 'synset': 'overcoat.n.02', 'name': 'overcoat'}, {'id': 9594, 'synset': 'overdrive.n.02', 'name': 'overdrive'}, {'id': 9595, 'synset': 'overgarment.n.01', 'name': 'overgarment'}, {'id': 9596, 'synset': 'overhand_knot.n.01', 'name': 'overhand_knot'}, {'id': 9597, 'synset': 'overhang.n.01', 'name': 'overhang'}, {'id': 9598, 'synset': 'overhead_projector.n.01', 'name': 'overhead_projector'}, {'id': 9599, 'synset': 'overmantel.n.01', 'name': 'overmantel'}, {'id': 9600, 'synset': 'overnighter.n.02', 'name': 'overnighter'}, {'id': 9601, 'synset': 'overpass.n.01', 'name': 'overpass'}, {'id': 9602, 'synset': 'override.n.01', 'name': 'override'}, {'id': 9603, 'synset': 'overshoe.n.01', 'name': 'overshoe'}, {'id': 9604, 'synset': 'overskirt.n.01', 'name': 'overskirt'}, {'id': 9605, 'synset': 'oxbow.n.03', 'name': 'oxbow'}, {'id': 9606, 'synset': 'oxbridge.n.01', 'name': 'Oxbridge'}, {'id': 9607, 'synset': 'oxcart.n.01', 'name': 'oxcart'}, {'id': 9608, 'synset': 'oxeye.n.03', 'name': 'oxeye'}, {'id': 9609, 'synset': 'oxford.n.04', 'name': 'oxford'}, {'id': 9610, 'synset': 'oximeter.n.01', 'name': 'oximeter'}, {'id': 9611, 'synset': 'oxyacetylene_torch.n.01', 'name': 'oxyacetylene_torch'}, {'id': 9612, 'synset': 'oxygen_mask.n.01', 'name': 'oxygen_mask'}, {'id': 9613, 'synset': 'oyster_bar.n.01', 'name': 'oyster_bar'}, {'id': 9614, 'synset': 'oyster_bed.n.01', 'name': 'oyster_bed'}, {'id': 9615, 'synset': 'pace_car.n.01', 'name': 'pace_car'}, {'id': 9616, 'synset': 'pacemaker.n.03', 'name': 'pacemaker'}, {'id': 9617, 'synset': 'pack.n.03', 'name': 'pack'}, {'id': 9618, 'synset': 'pack.n.09', 'name': 'pack'}, {'id': 9619, 'synset': 'pack.n.07', 'name': 'pack'}, {'id': 9620, 'synset': 'package.n.02', 'name': 'package'}, {'id': 9621, 'synset': 'package_store.n.01', 'name': 'package_store'}, {'id': 9622, 'synset': 'packaging.n.03', 'name': 'packaging'}, {'id': 9623, 'synset': 'packing_box.n.02', 'name': 'packing_box'}, {'id': 9624, 'synset': 'packinghouse.n.02', 'name': 'packinghouse'}, {'id': 9625, 'synset': 'packinghouse.n.01', 'name': 'packinghouse'}, {'id': 9626, 'synset': 'packing_needle.n.01', 'name': 'packing_needle'}, {'id': 9627, 'synset': 'packsaddle.n.01', 'name': 'packsaddle'}, {'id': 9628, 'synset': 'paddle.n.02', 'name': 'paddle'}, {'id': 9629, 'synset': 'paddle.n.01', 'name': 'paddle'}, {'id': 9630, 'synset': 'paddle_box.n.01', 'name': 'paddle_box'}, {'id': 9631, 'synset': 'paddle_steamer.n.01', 'name': 'paddle_steamer'}, {'id': 9632, 'synset': 'paddlewheel.n.01', 'name': 'paddlewheel'}, {'id': 9633, 'synset': 'paddock.n.01', 'name': 'paddock'}, {'id': 9634, 'synset': 'page_printer.n.01', 'name': 'page_printer'}, {'id': 9635, 'synset': 'paint.n.01', 'name': 'paint'}, {'id': 9636, 'synset': 'paintball.n.01', 'name': 'paintball'}, {'id': 9637, 'synset': 'paintball_gun.n.01', 'name': 'paintball_gun'}, {'id': 9638, 'synset': 'paintbox.n.01', 'name': 'paintbox'}, {'id': 9639, 'synset': 'paisley.n.01', 'name': 'paisley'}, {'id': 9640, 'synset': 'pajama.n.01', 'name': 'pajama'}, {'id': 9641, 'synset': 'palace.n.04', 'name': 'palace'}, {'id': 9642, 'synset': 'palace.n.01', 'name': 'palace'}, {'id': 9643, 'synset': 'palace.n.03', 'name': 'palace'}, {'id': 9644, 'synset': 'palanquin.n.01', 'name': 'palanquin'}, {'id': 9645, 'synset': 'paleolith.n.01', 'name': 'paleolith'}, {'id': 9646, 'synset': 'palestra.n.01', 'name': 'palestra'}, {'id': 9647, 'synset': 'palette_knife.n.01', 'name': 'palette_knife'}, {'id': 9648, 'synset': 'palisade.n.01', 'name': 'palisade'}, {'id': 9649, 'synset': 'pallet.n.03', 'name': 'pallet'}, {'id': 9650, 'synset': 'pallette.n.01', 'name': 'pallette'}, {'id': 9651, 'synset': 'pallium.n.04', 'name': 'pallium'}, {'id': 9652, 'synset': 'pallium.n.03', 'name': 'pallium'}, {'id': 9653, 'synset': 'pancake_turner.n.01', 'name': 'pancake_turner'}, {'id': 9654, 'synset': 'panchromatic_film.n.01', 'name': 'panchromatic_film'}, {'id': 9655, 'synset': 'panda_car.n.01', 'name': 'panda_car'}, {'id': 9656, 'synset': 'paneling.n.01', 'name': 'paneling'}, {'id': 9657, 'synset': 'panhandle.n.02', 'name': 'panhandle'}, {'id': 9658, 'synset': 'panic_button.n.01', 'name': 'panic_button'}, {'id': 9659, 'synset': 'pannier.n.02', 'name': 'pannier'}, {'id': 9660, 'synset': 'pannier.n.01', 'name': 'pannier'}, {'id': 9661, 'synset': 'pannikin.n.01', 'name': 'pannikin'}, {'id': 9662, 'synset': 'panopticon.n.02', 'name': 'panopticon'}, {'id': 9663, 'synset': 'panopticon.n.01', 'name': 'panopticon'}, {'id': 9664, 'synset': 'panpipe.n.01', 'name': 'panpipe'}, {'id': 9665, 'synset': 'pantaloon.n.03', 'name': 'pantaloon'}, {'id': 9666, 'synset': 'pantechnicon.n.01', 'name': 'pantechnicon'}, {'id': 9667, 'synset': 'pantheon.n.03', 'name': 'pantheon'}, {'id': 9668, 'synset': 'pantheon.n.02', 'name': 'pantheon'}, {'id': 9669, 'synset': 'pantie.n.01', 'name': 'pantie'}, {'id': 9670, 'synset': 'panting.n.02', 'name': 'panting'}, {'id': 9671, 'synset': 'pant_leg.n.01', 'name': 'pant_leg'}, {'id': 9672, 'synset': 'pantograph.n.01', 'name': 'pantograph'}, {'id': 9673, 'synset': 'pantry.n.01', 'name': 'pantry'}, {'id': 9674, 'synset': 'pants_suit.n.01', 'name': 'pants_suit'}, {'id': 9675, 'synset': 'panty_girdle.n.01', 'name': 'panty_girdle'}, {'id': 9676, 'synset': 'panzer.n.01', 'name': 'panzer'}, {'id': 9677, 'synset': 'paper_chain.n.01', 'name': 'paper_chain'}, {'id': 9678, 'synset': 'paper_clip.n.01', 'name': 'paper_clip'}, {'id': 9679, 'synset': 'paper_cutter.n.01', 'name': 'paper_cutter'}, {'id': 9680, 'synset': 'paper_fastener.n.01', 'name': 'paper_fastener'}, {'id': 9681, 'synset': 'paper_feed.n.01', 'name': 'paper_feed'}, {'id': 9682, 'synset': 'paper_mill.n.01', 'name': 'paper_mill'}, {'id': 9683, 'synset': 'parabolic_mirror.n.01', 'name': 'parabolic_mirror'}, {'id': 9684, 'synset': 'parabolic_reflector.n.01', 'name': 'parabolic_reflector'}, {'id': 9685, 'synset': 'parallel_bars.n.01', 'name': 'parallel_bars'}, {'id': 9686, 'synset': 'parallel_circuit.n.01', 'name': 'parallel_circuit'}, {'id': 9687, 'synset': 'parallel_interface.n.01', 'name': 'parallel_interface'}, {'id': 9688, 'synset': 'parang.n.01', 'name': 'parang'}, {'id': 9689, 'synset': 'parapet.n.02', 'name': 'parapet'}, {'id': 9690, 'synset': 'parapet.n.01', 'name': 'parapet'}, {'id': 9691, 'synset': 'parer.n.02', 'name': 'parer'}, {'id': 9692, 'synset': 'parfait_glass.n.01', 'name': 'parfait_glass'}, {'id': 9693, 'synset': 'pargeting.n.02', 'name': 'pargeting'}, {'id': 9694, 'synset': 'pari-mutuel_machine.n.01', 'name': 'pari-mutuel_machine'}, {'id': 9695, 'synset': 'park_bench.n.01', 'name': 'park_bench'}, {'id': 9696, 'synset': 'parlor.n.01', 'name': 'parlor'}, {'id': 9697, 'synset': 'parquet.n.01', 'name': 'parquet'}, {'id': 9698, 'synset': 'parquetry.n.01', 'name': 'parquetry'}, {'id': 9699, 'synset': 'parsonage.n.01', 'name': 'parsonage'}, {'id': 9700, 'synset': 'parsons_table.n.01', 'name': 'Parsons_table'}, {'id': 9701, 'synset': 'partial_denture.n.01', 'name': 'partial_denture'}, {'id': 9702, 'synset': 'particle_detector.n.01', 'name': 'particle_detector'}, {'id': 9703, 'synset': 'partition.n.01', 'name': 'partition'}, {'id': 9704, 'synset': 'parts_bin.n.01', 'name': 'parts_bin'}, {'id': 9705, 'synset': 'party_line.n.02', 'name': 'party_line'}, {'id': 9706, 'synset': 'party_wall.n.01', 'name': 'party_wall'}, {'id': 9707, 'synset': 'parvis.n.01', 'name': 'parvis'}, {'id': 9708, 'synset': 'passenger_train.n.01', 'name': 'passenger_train'}, {'id': 9709, 'synset': 'passenger_van.n.01', 'name': 'passenger_van'}, {'id': 9710, 'synset': 'passe-partout.n.02', 'name': 'passe-partout'}, {'id': 9711, 'synset': 'passive_matrix_display.n.01', 'name': 'passive_matrix_display'}, {'id': 9712, 'synset': 'passkey.n.01', 'name': 'passkey'}, {'id': 9713, 'synset': 'pass-through.n.01', 'name': 'pass-through'}, {'id': 9714, 'synset': 'pastry_cart.n.01', 'name': 'pastry_cart'}, {'id': 9715, 'synset': 'patch.n.03', 'name': 'patch'}, {'id': 9716, 'synset': 'patchcord.n.01', 'name': 'patchcord'}, {'id': 9717, 'synset': 'patchouli.n.02', 'name': 'patchouli'}, {'id': 9718, 'synset': 'patch_pocket.n.01', 'name': 'patch_pocket'}, {'id': 9719, 'synset': 'patchwork.n.02', 'name': 'patchwork'}, {'id': 9720, 'synset': 'patent_log.n.01', 'name': 'patent_log'}, {'id': 9721, 'synset': 'paternoster.n.02', 'name': 'paternoster'}, {'id': 9722, 'synset': 'patina.n.01', 'name': 'patina'}, {'id': 9723, 'synset': 'patio.n.01', 'name': 'patio'}, {'id': 9724, 'synset': 'patisserie.n.01', 'name': 'patisserie'}, {'id': 9725, 'synset': 'patka.n.01', 'name': 'patka'}, {'id': 9726, 'synset': 'patrol_boat.n.01', 'name': 'patrol_boat'}, {'id': 9727, 'synset': 'patty-pan.n.01', 'name': 'patty-pan'}, {'id': 9728, 'synset': 'pave.n.01', 'name': 'pave'}, {'id': 9729, 'synset': 'pavilion.n.01', 'name': 'pavilion'}, {'id': 9730, 'synset': 'pavior.n.01', 'name': 'pavior'}, {'id': 9731, 'synset': 'pavis.n.01', 'name': 'pavis'}, {'id': 9732, 'synset': 'pawn.n.03', 'name': 'pawn'}, {'id': 9733, 'synset': "pawnbroker's_shop.n.01", 'name': "pawnbroker's_shop"}, {'id': 9734, 'synset': 'pay-phone.n.01', 'name': 'pay-phone'}, {'id': 9735, 'synset': 'pc_board.n.01', 'name': 'PC_board'}, {'id': 9736, 'synset': 'peach_orchard.n.01', 'name': 'peach_orchard'}, {'id': 9737, 'synset': 'pea_jacket.n.01', 'name': 'pea_jacket'}, {'id': 9738, 'synset': 'peavey.n.01', 'name': 'peavey'}, {'id': 9739, 'synset': 'pectoral.n.02', 'name': 'pectoral'}, {'id': 9740, 'synset': 'pedal.n.02', 'name': 'pedal'}, {'id': 9741, 'synset': 'pedal_pusher.n.01', 'name': 'pedal_pusher'}, {'id': 9742, 'synset': 'pedestal.n.03', 'name': 'pedestal'}, {'id': 9743, 'synset': 'pedestal_table.n.01', 'name': 'pedestal_table'}, {'id': 9744, 'synset': 'pedestrian_crossing.n.01', 'name': 'pedestrian_crossing'}, {'id': 9745, 'synset': 'pedicab.n.01', 'name': 'pedicab'}, {'id': 9746, 'synset': 'pediment.n.01', 'name': 'pediment'}, {'id': 9747, 'synset': 'pedometer.n.01', 'name': 'pedometer'}, {'id': 9748, 'synset': 'peep_sight.n.01', 'name': 'peep_sight'}, {'id': 9749, 'synset': 'peg.n.01', 'name': 'peg'}, {'id': 9750, 'synset': 'peg.n.06', 'name': 'peg'}, {'id': 9751, 'synset': 'peg.n.05', 'name': 'peg'}, {'id': 9752, 'synset': 'pelham.n.01', 'name': 'Pelham'}, {'id': 9753, 'synset': 'pelican_crossing.n.01', 'name': 'pelican_crossing'}, {'id': 9754, 'synset': 'pelisse.n.01', 'name': 'pelisse'}, {'id': 9755, 'synset': 'pelvimeter.n.01', 'name': 'pelvimeter'}, {'id': 9756, 'synset': 'penal_colony.n.01', 'name': 'penal_colony'}, {'id': 9757, 'synset': 'penal_institution.n.01', 'name': 'penal_institution'}, {'id': 9758, 'synset': 'penalty_box.n.01', 'name': 'penalty_box'}, {'id': 9759, 'synset': 'pen-and-ink.n.01', 'name': 'pen-and-ink'}, {'id': 9760, 'synset': 'pencil.n.04', 'name': 'pencil'}, {'id': 9761, 'synset': 'pendant_earring.n.01', 'name': 'pendant_earring'}, {'id': 9762, 'synset': 'pendulum_clock.n.01', 'name': 'pendulum_clock'}, {'id': 9763, 'synset': 'pendulum_watch.n.01', 'name': 'pendulum_watch'}, {'id': 9764, 'synset': 'penetration_bomb.n.01', 'name': 'penetration_bomb'}, {'id': 9765, 'synset': 'penile_implant.n.01', 'name': 'penile_implant'}, {'id': 9766, 'synset': 'penitentiary.n.01', 'name': 'penitentiary'}, {'id': 9767, 'synset': 'penknife.n.01', 'name': 'penknife'}, {'id': 9768, 'synset': 'penlight.n.01', 'name': 'penlight'}, {'id': 9769, 'synset': 'pennant.n.03', 'name': 'pennant'}, {'id': 9770, 'synset': 'pennywhistle.n.01', 'name': 'pennywhistle'}, {'id': 9771, 'synset': 'penthouse.n.01', 'name': 'penthouse'}, {'id': 9772, 'synset': 'pentode.n.01', 'name': 'pentode'}, {'id': 9773, 'synset': 'peplos.n.01', 'name': 'peplos'}, {'id': 9774, 'synset': 'peplum.n.01', 'name': 'peplum'}, {'id': 9775, 'synset': 'pepper_shaker.n.01', 'name': 'pepper_shaker'}, {'id': 9776, 'synset': 'pepper_spray.n.01', 'name': 'pepper_spray'}, {'id': 9777, 'synset': 'percale.n.01', 'name': 'percale'}, {'id': 9778, 'synset': 'percolator.n.01', 'name': 'percolator'}, {'id': 9779, 'synset': 'percussion_cap.n.01', 'name': 'percussion_cap'}, {'id': 9780, 'synset': 'percussion_instrument.n.01', 'name': 'percussion_instrument'}, {'id': 9781, 'synset': 'perforation.n.01', 'name': 'perforation'}, {'id': 9782, 'synset': 'perfumery.n.03', 'name': 'perfumery'}, {'id': 9783, 'synset': 'perfumery.n.02', 'name': 'perfumery'}, {'id': 9784, 'synset': 'perfumery.n.01', 'name': 'perfumery'}, {'id': 9785, 'synset': 'peripheral.n.01', 'name': 'peripheral'}, {'id': 9786, 'synset': 'periscope.n.01', 'name': 'periscope'}, {'id': 9787, 'synset': 'peristyle.n.01', 'name': 'peristyle'}, {'id': 9788, 'synset': 'periwig.n.01', 'name': 'periwig'}, {'id': 9789, 'synset': 'permanent_press.n.01', 'name': 'permanent_press'}, {'id': 9790, 'synset': 'perpetual_motion_machine.n.01', 'name': 'perpetual_motion_machine'}, {'id': 9791, 'synset': 'personal_computer.n.01', 'name': 'personal_computer'}, {'id': 9792, 'synset': 'personal_digital_assistant.n.01', 'name': 'personal_digital_assistant'}, {'id': 9793, 'synset': 'personnel_carrier.n.01', 'name': 'personnel_carrier'}, {'id': 9794, 'synset': 'pestle.n.03', 'name': 'pestle'}, {'id': 9795, 'synset': 'pestle.n.02', 'name': 'pestle'}, {'id': 9796, 'synset': 'petcock.n.01', 'name': 'petcock'}, {'id': 9797, 'synset': 'petri_dish.n.01', 'name': 'Petri_dish'}, {'id': 9798, 'synset': 'petrolatum_gauze.n.01', 'name': 'petrolatum_gauze'}, {'id': 9799, 'synset': 'pet_shop.n.01', 'name': 'pet_shop'}, {'id': 9800, 'synset': 'petticoat.n.01', 'name': 'petticoat'}, {'id': 9801, 'synset': 'phial.n.01', 'name': 'phial'}, {'id': 9802, 'synset': 'phillips_screw.n.01', 'name': 'Phillips_screw'}, {'id': 9803, 'synset': 'phillips_screwdriver.n.01', 'name': 'Phillips_screwdriver'}, {'id': 9804, 'synset': 'phonograph_needle.n.01', 'name': 'phonograph_needle'}, {'id': 9805, 'synset': 'photocathode.n.01', 'name': 'photocathode'}, {'id': 9806, 'synset': 'photocoagulator.n.01', 'name': 'photocoagulator'}, {'id': 9807, 'synset': 'photocopier.n.01', 'name': 'photocopier'}, {'id': 9808, 'synset': 'photographic_equipment.n.01', 'name': 'photographic_equipment'}, {'id': 9809, 'synset': 'photographic_paper.n.01', 'name': 'photographic_paper'}, {'id': 9810, 'synset': 'photometer.n.01', 'name': 'photometer'}, {'id': 9811, 'synset': 'photomicrograph.n.01', 'name': 'photomicrograph'}, {'id': 9812, 'synset': 'photostat.n.02', 'name': 'Photostat'}, {'id': 9813, 'synset': 'photostat.n.01', 'name': 'photostat'}, {'id': 9814, 'synset': 'physical_pendulum.n.01', 'name': 'physical_pendulum'}, {'id': 9815, 'synset': 'piano_action.n.01', 'name': 'piano_action'}, {'id': 9816, 'synset': 'piano_keyboard.n.01', 'name': 'piano_keyboard'}, {'id': 9817, 'synset': 'piano_wire.n.01', 'name': 'piano_wire'}, {'id': 9818, 'synset': 'piccolo.n.01', 'name': 'piccolo'}, {'id': 9819, 'synset': 'pick.n.07', 'name': 'pick'}, {'id': 9820, 'synset': 'pick.n.06', 'name': 'pick'}, {'id': 9821, 'synset': 'pick.n.05', 'name': 'pick'}, {'id': 9822, 'synset': 'pickelhaube.n.01', 'name': 'pickelhaube'}, {'id': 9823, 'synset': 'picket_boat.n.01', 'name': 'picket_boat'}, {'id': 9824, 'synset': 'picket_fence.n.01', 'name': 'picket_fence'}, {'id': 9825, 'synset': 'picket_ship.n.01', 'name': 'picket_ship'}, {'id': 9826, 'synset': 'pickle_barrel.n.01', 'name': 'pickle_barrel'}, {'id': 9827, 'synset': 'picture_frame.n.01', 'name': 'picture_frame'}, {'id': 9828, 'synset': 'picture_hat.n.01', 'name': 'picture_hat'}, {'id': 9829, 'synset': 'picture_rail.n.01', 'name': 'picture_rail'}, {'id': 9830, 'synset': 'picture_window.n.01', 'name': 'picture_window'}, {'id': 9831, 'synset': 'piece_of_cloth.n.01', 'name': 'piece_of_cloth'}, {'id': 9832, 'synset': 'pied-a-terre.n.01', 'name': 'pied-a-terre'}, {'id': 9833, 'synset': 'pier.n.03', 'name': 'pier'}, {'id': 9834, 'synset': 'pier.n.02', 'name': 'pier'}, {'id': 9835, 'synset': 'pier_arch.n.01', 'name': 'pier_arch'}, {'id': 9836, 'synset': 'pier_glass.n.01', 'name': 'pier_glass'}, {'id': 9837, 'synset': 'pier_table.n.01', 'name': 'pier_table'}, {'id': 9838, 'synset': 'pieta.n.01', 'name': 'pieta'}, {'id': 9839, 'synset': 'piezometer.n.01', 'name': 'piezometer'}, {'id': 9840, 'synset': 'pig_bed.n.01', 'name': 'pig_bed'}, {'id': 9841, 'synset': 'piggery.n.01', 'name': 'piggery'}, {'id': 9842, 'synset': 'pilaster.n.01', 'name': 'pilaster'}, {'id': 9843, 'synset': 'pile.n.06', 'name': 'pile'}, {'id': 9844, 'synset': 'pile_driver.n.01', 'name': 'pile_driver'}, {'id': 9845, 'synset': 'pill_bottle.n.01', 'name': 'pill_bottle'}, {'id': 9846, 'synset': 'pillbox.n.01', 'name': 'pillbox'}, {'id': 9847, 'synset': 'pillion.n.01', 'name': 'pillion'}, {'id': 9848, 'synset': 'pillory.n.01', 'name': 'pillory'}, {'id': 9849, 'synset': 'pillow_block.n.01', 'name': 'pillow_block'}, {'id': 9850, 'synset': 'pillow_lace.n.01', 'name': 'pillow_lace'}, {'id': 9851, 'synset': 'pillow_sham.n.01', 'name': 'pillow_sham'}, {'id': 9852, 'synset': 'pilot_bit.n.01', 'name': 'pilot_bit'}, {'id': 9853, 'synset': 'pilot_boat.n.01', 'name': 'pilot_boat'}, {'id': 9854, 'synset': 'pilot_burner.n.01', 'name': 'pilot_burner'}, {'id': 9855, 'synset': 'pilot_cloth.n.01', 'name': 'pilot_cloth'}, {'id': 9856, 'synset': 'pilot_engine.n.01', 'name': 'pilot_engine'}, {'id': 9857, 'synset': 'pilothouse.n.01', 'name': 'pilothouse'}, {'id': 9858, 'synset': 'pilot_light.n.02', 'name': 'pilot_light'}, {'id': 9859, 'synset': 'pin.n.08', 'name': 'pin'}, {'id': 9860, 'synset': 'pin.n.07', 'name': 'pin'}, {'id': 9861, 'synset': 'pinata.n.01', 'name': 'pinata'}, {'id': 9862, 'synset': 'pinball_machine.n.01', 'name': 'pinball_machine'}, {'id': 9863, 'synset': 'pince-nez.n.01', 'name': 'pince-nez'}, {'id': 9864, 'synset': 'pincer.n.01', 'name': 'pincer'}, {'id': 9865, 'synset': 'pinch_bar.n.01', 'name': 'pinch_bar'}, {'id': 9866, 'synset': 'pincurl_clip.n.01', 'name': 'pincurl_clip'}, {'id': 9867, 'synset': 'pinfold.n.01', 'name': 'pinfold'}, {'id': 9868, 'synset': 'pinhead.n.02', 'name': 'pinhead'}, {'id': 9869, 'synset': 'pinion.n.01', 'name': 'pinion'}, {'id': 9870, 'synset': 'pinnacle.n.01', 'name': 'pinnacle'}, {'id': 9871, 'synset': 'pinprick.n.02', 'name': 'pinprick'}, {'id': 9872, 'synset': 'pinstripe.n.03', 'name': 'pinstripe'}, {'id': 9873, 'synset': 'pinstripe.n.02', 'name': 'pinstripe'}, {'id': 9874, 'synset': 'pinstripe.n.01', 'name': 'pinstripe'}, {'id': 9875, 'synset': 'pintle.n.01', 'name': 'pintle'}, {'id': 9876, 'synset': 'pinwheel.n.02', 'name': 'pinwheel'}, {'id': 9877, 'synset': 'tabor_pipe.n.01', 'name': 'tabor_pipe'}, {'id': 9878, 'synset': 'pipe.n.04', 'name': 'pipe'}, {'id': 9879, 'synset': 'pipe_bomb.n.01', 'name': 'pipe_bomb'}, {'id': 9880, 'synset': 'pipe_cleaner.n.01', 'name': 'pipe_cleaner'}, {'id': 9881, 'synset': 'pipe_cutter.n.01', 'name': 'pipe_cutter'}, {'id': 9882, 'synset': 'pipefitting.n.01', 'name': 'pipefitting'}, {'id': 9883, 'synset': 'pipet.n.01', 'name': 'pipet'}, {'id': 9884, 'synset': 'pipe_vise.n.01', 'name': 'pipe_vise'}, {'id': 9885, 'synset': 'pipe_wrench.n.01', 'name': 'pipe_wrench'}, {'id': 9886, 'synset': 'pique.n.01', 'name': 'pique'}, {'id': 9887, 'synset': 'pirate.n.03', 'name': 'pirate'}, {'id': 9888, 'synset': 'piste.n.02', 'name': 'piste'}, {'id': 9889, 'synset': 'pistol_grip.n.01', 'name': 'pistol_grip'}, {'id': 9890, 'synset': 'piston.n.02', 'name': 'piston'}, {'id': 9891, 'synset': 'piston_ring.n.01', 'name': 'piston_ring'}, {'id': 9892, 'synset': 'piston_rod.n.01', 'name': 'piston_rod'}, {'id': 9893, 'synset': 'pit.n.07', 'name': 'pit'}, {'id': 9894, 'synset': 'pitching_wedge.n.01', 'name': 'pitching_wedge'}, {'id': 9895, 'synset': 'pitch_pipe.n.01', 'name': 'pitch_pipe'}, {'id': 9896, 'synset': 'pith_hat.n.01', 'name': 'pith_hat'}, {'id': 9897, 'synset': 'piton.n.01', 'name': 'piton'}, {'id': 9898, 'synset': 'pitot-static_tube.n.01', 'name': 'Pitot-static_tube'}, {'id': 9899, 'synset': 'pitot_tube.n.01', 'name': 'Pitot_tube'}, {'id': 9900, 'synset': 'pitsaw.n.01', 'name': 'pitsaw'}, {'id': 9901, 'synset': 'pivot.n.02', 'name': 'pivot'}, {'id': 9902, 'synset': 'pivoting_window.n.01', 'name': 'pivoting_window'}, {'id': 9903, 'synset': 'pizzeria.n.01', 'name': 'pizzeria'}, {'id': 9904, 'synset': 'place_of_business.n.01', 'name': 'place_of_business'}, {'id': 9905, 'synset': 'place_of_worship.n.01', 'name': 'place_of_worship'}, {'id': 9906, 'synset': 'placket.n.01', 'name': 'placket'}, {'id': 9907, 'synset': 'planchet.n.01', 'name': 'planchet'}, {'id': 9908, 'synset': 'plane.n.05', 'name': 'plane'}, {'id': 9909, 'synset': 'plane.n.04', 'name': 'plane'}, {'id': 9910, 'synset': 'plane_seat.n.01', 'name': 'plane_seat'}, {'id': 9911, 'synset': 'planetarium.n.03', 'name': 'planetarium'}, {'id': 9912, 'synset': 'planetarium.n.02', 'name': 'planetarium'}, {'id': 9913, 'synset': 'planetarium.n.01', 'name': 'planetarium'}, {'id': 9914, 'synset': 'planetary_gear.n.01', 'name': 'planetary_gear'}, {'id': 9915, 'synset': 'plank-bed.n.01', 'name': 'plank-bed'}, {'id': 9916, 'synset': 'planking.n.02', 'name': 'planking'}, {'id': 9917, 'synset': 'planner.n.02', 'name': 'planner'}, {'id': 9918, 'synset': 'plant.n.01', 'name': 'plant'}, {'id': 9919, 'synset': 'planter.n.03', 'name': 'planter'}, {'id': 9920, 'synset': 'plaster.n.05', 'name': 'plaster'}, {'id': 9921, 'synset': 'plasterboard.n.01', 'name': 'plasterboard'}, {'id': 9922, 'synset': 'plastering_trowel.n.01', 'name': 'plastering_trowel'}, {'id': 9923, 'synset': 'plastic_bag.n.01', 'name': 'plastic_bag'}, {'id': 9924, 'synset': 'plastic_bomb.n.01', 'name': 'plastic_bomb'}, {'id': 9925, 'synset': 'plastic_laminate.n.01', 'name': 'plastic_laminate'}, {'id': 9926, 'synset': 'plastic_wrap.n.01', 'name': 'plastic_wrap'}, {'id': 9927, 'synset': 'plastron.n.03', 'name': 'plastron'}, {'id': 9928, 'synset': 'plastron.n.02', 'name': 'plastron'}, {'id': 9929, 'synset': 'plastron.n.01', 'name': 'plastron'}, {'id': 9930, 'synset': 'plate.n.14', 'name': 'plate'}, {'id': 9931, 'synset': 'plate.n.13', 'name': 'plate'}, {'id': 9932, 'synset': 'plate.n.12', 'name': 'plate'}, {'id': 9933, 'synset': 'platen.n.03', 'name': 'platen'}, {'id': 9934, 'synset': 'platen.n.01', 'name': 'platen'}, {'id': 9935, 'synset': 'plate_rack.n.01', 'name': 'plate_rack'}, {'id': 9936, 'synset': 'plate_rail.n.01', 'name': 'plate_rail'}, {'id': 9937, 'synset': 'platform.n.01', 'name': 'platform'}, {'id': 9938, 'synset': 'platform.n.04', 'name': 'platform'}, {'id': 9939, 'synset': 'platform.n.03', 'name': 'platform'}, {'id': 9940, 'synset': 'platform_bed.n.01', 'name': 'platform_bed'}, {'id': 9941, 'synset': 'platform_rocker.n.01', 'name': 'platform_rocker'}, {'id': 9942, 'synset': 'plating.n.01', 'name': 'plating'}, {'id': 9943, 'synset': 'playback.n.02', 'name': 'playback'}, {'id': 9944, 'synset': 'playbox.n.01', 'name': 'playbox'}, {'id': 9945, 'synset': 'playground.n.02', 'name': 'playground'}, {'id': 9946, 'synset': 'playsuit.n.01', 'name': 'playsuit'}, {'id': 9947, 'synset': 'plaza.n.02', 'name': 'plaza'}, {'id': 9948, 'synset': 'pleat.n.01', 'name': 'pleat'}, {'id': 9949, 'synset': 'plenum.n.02', 'name': 'plenum'}, {'id': 9950, 'synset': 'plethysmograph.n.01', 'name': 'plethysmograph'}, {'id': 9951, 'synset': 'pleximeter.n.01', 'name': 'pleximeter'}, {'id': 9952, 'synset': 'plexor.n.01', 'name': 'plexor'}, {'id': 9953, 'synset': 'plimsoll.n.02', 'name': 'plimsoll'}, {'id': 9954, 'synset': 'plotter.n.04', 'name': 'plotter'}, {'id': 9955, 'synset': 'plug.n.01', 'name': 'plug'}, {'id': 9956, 'synset': 'plug.n.05', 'name': 'plug'}, {'id': 9957, 'synset': 'plug_fuse.n.01', 'name': 'plug_fuse'}, {'id': 9958, 'synset': 'plughole.n.01', 'name': 'plughole'}, {'id': 9959, 'synset': 'plumb_bob.n.01', 'name': 'plumb_bob'}, {'id': 9960, 'synset': 'plumb_level.n.01', 'name': 'plumb_level'}, {'id': 9961, 'synset': 'plunger.n.03', 'name': 'plunger'}, {'id': 9962, 'synset': 'plus_fours.n.01', 'name': 'plus_fours'}, {'id': 9963, 'synset': 'plush.n.01', 'name': 'plush'}, {'id': 9964, 'synset': 'plywood.n.01', 'name': 'plywood'}, {'id': 9965, 'synset': 'pneumatic_drill.n.01', 'name': 'pneumatic_drill'}, {'id': 9966, 'synset': 'p-n_junction.n.01', 'name': 'p-n_junction'}, {'id': 9967, 'synset': 'p-n-p_transistor.n.01', 'name': 'p-n-p_transistor'}, {'id': 9968, 'synset': 'poacher.n.02', 'name': 'poacher'}, {'id': 9969, 'synset': 'pocket.n.01', 'name': 'pocket'}, {'id': 9970, 'synset': 'pocket_battleship.n.01', 'name': 'pocket_battleship'}, {'id': 9971, 'synset': 'pocketcomb.n.01', 'name': 'pocketcomb'}, {'id': 9972, 'synset': 'pocket_flap.n.01', 'name': 'pocket_flap'}, {'id': 9973, 'synset': 'pocket-handkerchief.n.01', 'name': 'pocket-handkerchief'}, {'id': 9974, 'synset': 'pod.n.04', 'name': 'pod'}, {'id': 9975, 'synset': 'pogo_stick.n.01', 'name': 'pogo_stick'}, {'id': 9976, 'synset': 'point-and-shoot_camera.n.01', 'name': 'point-and-shoot_camera'}, {'id': 9977, 'synset': 'pointed_arch.n.01', 'name': 'pointed_arch'}, {'id': 9978, 'synset': 'pointing_trowel.n.01', 'name': 'pointing_trowel'}, {'id': 9979, 'synset': 'point_lace.n.01', 'name': 'point_lace'}, {'id': 9980, 'synset': 'polarimeter.n.01', 'name': 'polarimeter'}, {'id': 9981, 'synset': 'polaroid.n.01', 'name': 'Polaroid'}, {'id': 9982, 'synset': 'polaroid_camera.n.01', 'name': 'Polaroid_camera'}, {'id': 9983, 'synset': 'pole.n.09', 'name': 'pole'}, {'id': 9984, 'synset': 'poleax.n.02', 'name': 'poleax'}, {'id': 9985, 'synset': 'poleax.n.01', 'name': 'poleax'}, {'id': 9986, 'synset': 'police_boat.n.01', 'name': 'police_boat'}, {'id': 9987, 'synset': 'police_van.n.01', 'name': 'police_van'}, {'id': 9988, 'synset': 'polling_booth.n.01', 'name': 'polling_booth'}, {'id': 9989, 'synset': 'polo_ball.n.01', 'name': 'polo_ball'}, {'id': 9990, 'synset': 'polo_mallet.n.01', 'name': 'polo_mallet'}, {'id': 9991, 'synset': 'polonaise.n.01', 'name': 'polonaise'}, {'id': 9992, 'synset': 'polyester.n.03', 'name': 'polyester'}, {'id': 9993, 'synset': 'polygraph.n.01', 'name': 'polygraph'}, {'id': 9994, 'synset': 'pomade.n.01', 'name': 'pomade'}, {'id': 9995, 'synset': 'pommel_horse.n.01', 'name': 'pommel_horse'}, {'id': 9996, 'synset': 'pongee.n.01', 'name': 'pongee'}, {'id': 9997, 'synset': 'poniard.n.01', 'name': 'poniard'}, {'id': 9998, 'synset': 'pontifical.n.01', 'name': 'pontifical'}, {'id': 9999, 'synset': 'pontoon.n.01', 'name': 'pontoon'}, {'id': 10000, 'synset': 'pontoon_bridge.n.01', 'name': 'pontoon_bridge'}, {'id': 10001, 'synset': 'pony_cart.n.01', 'name': 'pony_cart'}, {'id': 10002, 'synset': 'pool_ball.n.01', 'name': 'pool_ball'}, {'id': 10003, 'synset': 'poolroom.n.01', 'name': 'poolroom'}, {'id': 10004, 'synset': 'poop_deck.n.01', 'name': 'poop_deck'}, {'id': 10005, 'synset': 'poor_box.n.01', 'name': 'poor_box'}, {'id': 10006, 'synset': 'poorhouse.n.01', 'name': 'poorhouse'}, {'id': 10007, 'synset': 'pop_bottle.n.01', 'name': 'pop_bottle'}, {'id': 10008, 'synset': 'popgun.n.01', 'name': 'popgun'}, {'id': 10009, 'synset': 'poplin.n.01', 'name': 'poplin'}, {'id': 10010, 'synset': 'popper.n.03', 'name': 'popper'}, {'id': 10011, 'synset': 'poppet.n.01', 'name': 'poppet'}, {'id': 10012, 'synset': 'pop_tent.n.01', 'name': 'pop_tent'}, {'id': 10013, 'synset': 'porcelain.n.01', 'name': 'porcelain'}, {'id': 10014, 'synset': 'porch.n.01', 'name': 'porch'}, {'id': 10015, 'synset': 'porkpie.n.01', 'name': 'porkpie'}, {'id': 10016, 'synset': 'porringer.n.01', 'name': 'porringer'}, {'id': 10017, 'synset': 'portable.n.01', 'name': 'portable'}, {'id': 10018, 'synset': 'portable_computer.n.01', 'name': 'portable_computer'}, {'id': 10019, 'synset': 'portable_circular_saw.n.01', 'name': 'portable_circular_saw'}, {'id': 10020, 'synset': 'portcullis.n.01', 'name': 'portcullis'}, {'id': 10021, 'synset': 'porte-cochere.n.02', 'name': 'porte-cochere'}, {'id': 10022, 'synset': 'porte-cochere.n.01', 'name': 'porte-cochere'}, {'id': 10023, 'synset': 'portfolio.n.01', 'name': 'portfolio'}, {'id': 10024, 'synset': 'porthole.n.01', 'name': 'porthole'}, {'id': 10025, 'synset': 'portico.n.01', 'name': 'portico'}, {'id': 10026, 'synset': 'portiere.n.01', 'name': 'portiere'}, {'id': 10027, 'synset': 'portmanteau.n.02', 'name': 'portmanteau'}, {'id': 10028, 'synset': 'portrait_camera.n.01', 'name': 'portrait_camera'}, {'id': 10029, 'synset': 'portrait_lens.n.01', 'name': 'portrait_lens'}, {'id': 10030, 'synset': 'positive_pole.n.02', 'name': 'positive_pole'}, {'id': 10031, 'synset': 'positive_pole.n.01', 'name': 'positive_pole'}, {'id': 10032, 'synset': 'positron_emission_tomography_scanner.n.01', 'name': 'positron_emission_tomography_scanner'}, {'id': 10033, 'synset': 'post.n.04', 'name': 'post'}, {'id': 10034, 'synset': 'postage_meter.n.01', 'name': 'postage_meter'}, {'id': 10035, 'synset': 'post_and_lintel.n.01', 'name': 'post_and_lintel'}, {'id': 10036, 'synset': 'post_chaise.n.01', 'name': 'post_chaise'}, {'id': 10037, 'synset': 'postern.n.01', 'name': 'postern'}, {'id': 10038, 'synset': 'post_exchange.n.01', 'name': 'post_exchange'}, {'id': 10039, 'synset': 'posthole_digger.n.01', 'name': 'posthole_digger'}, {'id': 10040, 'synset': 'post_horn.n.01', 'name': 'post_horn'}, {'id': 10041, 'synset': 'posthouse.n.01', 'name': 'posthouse'}, {'id': 10042, 'synset': 'potbelly.n.02', 'name': 'potbelly'}, {'id': 10043, 'synset': 'potemkin_village.n.01', 'name': 'Potemkin_village'}, {'id': 10044, 'synset': 'potential_divider.n.01', 'name': 'potential_divider'}, {'id': 10045, 'synset': 'potentiometer.n.02', 'name': 'potentiometer'}, {'id': 10046, 'synset': 'potentiometer.n.01', 'name': 'potentiometer'}, {'id': 10047, 'synset': 'potpourri.n.03', 'name': 'potpourri'}, {'id': 10048, 'synset': 'potsherd.n.01', 'name': 'potsherd'}, {'id': 10049, 'synset': "potter's_wheel.n.01", 'name': "potter's_wheel"}, {'id': 10050, 'synset': 'pottle.n.01', 'name': 'pottle'}, {'id': 10051, 'synset': 'potty_seat.n.01', 'name': 'potty_seat'}, {'id': 10052, 'synset': 'poultice.n.01', 'name': 'poultice'}, {'id': 10053, 'synset': 'pound.n.13', 'name': 'pound'}, {'id': 10054, 'synset': 'pound_net.n.01', 'name': 'pound_net'}, {'id': 10055, 'synset': 'powder.n.03', 'name': 'powder'}, {'id': 10056, 'synset': 'powder_and_shot.n.01', 'name': 'powder_and_shot'}, {'id': 10057, 'synset': 'powdered_mustard.n.01', 'name': 'powdered_mustard'}, {'id': 10058, 'synset': 'powder_horn.n.01', 'name': 'powder_horn'}, {'id': 10059, 'synset': 'powder_keg.n.02', 'name': 'powder_keg'}, {'id': 10060, 'synset': 'power_brake.n.01', 'name': 'power_brake'}, {'id': 10061, 'synset': 'power_cord.n.01', 'name': 'power_cord'}, {'id': 10062, 'synset': 'power_drill.n.01', 'name': 'power_drill'}, {'id': 10063, 'synset': 'power_line.n.01', 'name': 'power_line'}, {'id': 10064, 'synset': 'power_loom.n.01', 'name': 'power_loom'}, {'id': 10065, 'synset': 'power_mower.n.01', 'name': 'power_mower'}, {'id': 10066, 'synset': 'power_pack.n.01', 'name': 'power_pack'}, {'id': 10067, 'synset': 'power_saw.n.01', 'name': 'power_saw'}, {'id': 10068, 'synset': 'power_steering.n.01', 'name': 'power_steering'}, {'id': 10069, 'synset': 'power_takeoff.n.01', 'name': 'power_takeoff'}, {'id': 10070, 'synset': 'power_tool.n.01', 'name': 'power_tool'}, {'id': 10071, 'synset': 'praetorium.n.01', 'name': 'praetorium'}, {'id': 10072, 'synset': 'prayer_rug.n.01', 'name': 'prayer_rug'}, {'id': 10073, 'synset': 'prayer_shawl.n.01', 'name': 'prayer_shawl'}, {'id': 10074, 'synset': 'precipitator.n.01', 'name': 'precipitator'}, {'id': 10075, 'synset': 'prefab.n.01', 'name': 'prefab'}, {'id': 10076, 'synset': 'presbytery.n.01', 'name': 'presbytery'}, {'id': 10077, 'synset': 'presence_chamber.n.01', 'name': 'presence_chamber'}, {'id': 10078, 'synset': 'press.n.07', 'name': 'press'}, {'id': 10079, 'synset': 'press.n.03', 'name': 'press'}, {'id': 10080, 'synset': 'press.n.06', 'name': 'press'}, {'id': 10081, 'synset': 'press_box.n.01', 'name': 'press_box'}, {'id': 10082, 'synset': 'press_gallery.n.01', 'name': 'press_gallery'}, {'id': 10083, 'synset': 'press_of_sail.n.01', 'name': 'press_of_sail'}, {'id': 10084, 'synset': 'pressure_cabin.n.01', 'name': 'pressure_cabin'}, {'id': 10085, 'synset': 'pressure_cooker.n.01', 'name': 'pressure_cooker'}, {'id': 10086, 'synset': 'pressure_dome.n.01', 'name': 'pressure_dome'}, {'id': 10087, 'synset': 'pressure_gauge.n.01', 'name': 'pressure_gauge'}, {'id': 10088, 'synset': 'pressurized_water_reactor.n.01', 'name': 'pressurized_water_reactor'}, {'id': 10089, 'synset': 'pressure_suit.n.01', 'name': 'pressure_suit'}, {'id': 10090, 'synset': 'pricket.n.01', 'name': 'pricket'}, {'id': 10091, 'synset': 'prie-dieu.n.01', 'name': 'prie-dieu'}, {'id': 10092, 'synset': 'primary_coil.n.01', 'name': 'primary_coil'}, {'id': 10093, 'synset': 'primus_stove.n.01', 'name': 'Primus_stove'}, {'id': 10094, 'synset': 'prince_albert.n.02', 'name': 'Prince_Albert'}, {'id': 10095, 'synset': 'print.n.06', 'name': 'print'}, {'id': 10096, 'synset': 'print_buffer.n.01', 'name': 'print_buffer'}, {'id': 10097, 'synset': 'printed_circuit.n.01', 'name': 'printed_circuit'}, {'id': 10098, 'synset': 'printer.n.02', 'name': 'printer'}, {'id': 10099, 'synset': 'printer_cable.n.01', 'name': 'printer_cable'}, {'id': 10100, 'synset': 'priory.n.01', 'name': 'priory'}, {'id': 10101, 'synset': 'prison.n.01', 'name': 'prison'}, {'id': 10102, 'synset': 'prison_camp.n.01', 'name': 'prison_camp'}, {'id': 10103, 'synset': 'privateer.n.02', 'name': 'privateer'}, {'id': 10104, 'synset': 'private_line.n.01', 'name': 'private_line'}, {'id': 10105, 'synset': 'privet_hedge.n.01', 'name': 'privet_hedge'}, {'id': 10106, 'synset': 'probe.n.02', 'name': 'probe'}, {'id': 10107, 'synset': 'proctoscope.n.01', 'name': 'proctoscope'}, {'id': 10108, 'synset': 'prod.n.02', 'name': 'prod'}, {'id': 10109, 'synset': 'production_line.n.01', 'name': 'production_line'}, {'id': 10110, 'synset': 'projector.n.01', 'name': 'projector'}, {'id': 10111, 'synset': 'prolonge.n.01', 'name': 'prolonge'}, {'id': 10112, 'synset': 'prolonge_knot.n.01', 'name': 'prolonge_knot'}, {'id': 10113, 'synset': 'prompter.n.02', 'name': 'prompter'}, {'id': 10114, 'synset': 'prong.n.01', 'name': 'prong'}, {'id': 10115, 'synset': 'propeller_plane.n.01', 'name': 'propeller_plane'}, {'id': 10116, 'synset': 'propjet.n.01', 'name': 'propjet'}, {'id': 10117, 'synset': 'proportional_counter_tube.n.01', 'name': 'proportional_counter_tube'}, {'id': 10118, 'synset': 'propulsion_system.n.01', 'name': 'propulsion_system'}, {'id': 10119, 'synset': 'proscenium.n.02', 'name': 'proscenium'}, {'id': 10120, 'synset': 'proscenium_arch.n.01', 'name': 'proscenium_arch'}, {'id': 10121, 'synset': 'prosthesis.n.01', 'name': 'prosthesis'}, {'id': 10122, 'synset': 'protective_covering.n.01', 'name': 'protective_covering'}, {'id': 10123, 'synset': 'protective_garment.n.01', 'name': 'protective_garment'}, {'id': 10124, 'synset': 'proton_accelerator.n.01', 'name': 'proton_accelerator'}, {'id': 10125, 'synset': 'protractor.n.01', 'name': 'protractor'}, {'id': 10126, 'synset': 'pruner.n.02', 'name': 'pruner'}, {'id': 10127, 'synset': 'pruning_knife.n.01', 'name': 'pruning_knife'}, {'id': 10128, 'synset': 'pruning_saw.n.01', 'name': 'pruning_saw'}, {'id': 10129, 'synset': 'pruning_shears.n.01', 'name': 'pruning_shears'}, {'id': 10130, 'synset': 'psaltery.n.01', 'name': 'psaltery'}, {'id': 10131, 'synset': 'psychrometer.n.01', 'name': 'psychrometer'}, {'id': 10132, 'synset': 'pt_boat.n.01', 'name': 'PT_boat'}, {'id': 10133, 'synset': 'public_address_system.n.01', 'name': 'public_address_system'}, {'id': 10134, 'synset': 'public_house.n.01', 'name': 'public_house'}, {'id': 10135, 'synset': 'public_toilet.n.01', 'name': 'public_toilet'}, {'id': 10136, 'synset': 'public_transport.n.01', 'name': 'public_transport'}, {'id': 10137, 'synset': 'public_works.n.01', 'name': 'public_works'}, {'id': 10138, 'synset': 'puck.n.02', 'name': 'puck'}, {'id': 10139, 'synset': 'pull.n.04', 'name': 'pull'}, {'id': 10140, 'synset': 'pullback.n.01', 'name': 'pullback'}, {'id': 10141, 'synset': 'pull_chain.n.01', 'name': 'pull_chain'}, {'id': 10142, 'synset': 'pulley.n.01', 'name': 'pulley'}, {'id': 10143, 'synset': 'pull-off.n.01', 'name': 'pull-off'}, {'id': 10144, 'synset': 'pullman.n.01', 'name': 'Pullman'}, {'id': 10145, 'synset': 'pullover.n.01', 'name': 'pullover'}, {'id': 10146, 'synset': 'pull-through.n.01', 'name': 'pull-through'}, {'id': 10147, 'synset': 'pulse_counter.n.01', 'name': 'pulse_counter'}, {'id': 10148, 'synset': 'pulse_generator.n.01', 'name': 'pulse_generator'}, {'id': 10149, 'synset': 'pulse_timing_circuit.n.01', 'name': 'pulse_timing_circuit'}, {'id': 10150, 'synset': 'pump.n.01', 'name': 'pump'}, {'id': 10151, 'synset': 'pump.n.03', 'name': 'pump'}, {'id': 10152, 'synset': 'pump_action.n.01', 'name': 'pump_action'}, {'id': 10153, 'synset': 'pump_house.n.01', 'name': 'pump_house'}, {'id': 10154, 'synset': 'pump_room.n.01', 'name': 'pump_room'}, {'id': 10155, 'synset': 'pump-type_pliers.n.01', 'name': 'pump-type_pliers'}, {'id': 10156, 'synset': 'pump_well.n.01', 'name': 'pump_well'}, {'id': 10157, 'synset': 'punchboard.n.01', 'name': 'punchboard'}, {'id': 10158, 'synset': 'punch_bowl.n.01', 'name': 'punch_bowl'}, {'id': 10159, 'synset': 'punching_bag.n.02', 'name': 'punching_bag'}, {'id': 10160, 'synset': 'punch_pliers.n.01', 'name': 'punch_pliers'}, {'id': 10161, 'synset': 'punch_press.n.01', 'name': 'punch_press'}, {'id': 10162, 'synset': 'punnet.n.01', 'name': 'punnet'}, {'id': 10163, 'synset': 'punt.n.02', 'name': 'punt'}, {'id': 10164, 'synset': 'pup_tent.n.01', 'name': 'pup_tent'}, {'id': 10165, 'synset': 'purdah.n.03', 'name': 'purdah'}, {'id': 10166, 'synset': 'purifier.n.01', 'name': 'purifier'}, {'id': 10167, 'synset': 'purl.n.02', 'name': 'purl'}, {'id': 10168, 'synset': 'purse.n.03', 'name': 'purse'}, {'id': 10169, 'synset': 'push-bike.n.01', 'name': 'push-bike'}, {'id': 10170, 'synset': 'push_broom.n.01', 'name': 'push_broom'}, {'id': 10171, 'synset': 'push_button.n.01', 'name': 'push_button'}, {'id': 10172, 'synset': 'push-button_radio.n.01', 'name': 'push-button_radio'}, {'id': 10173, 'synset': 'pusher.n.04', 'name': 'pusher'}, {'id': 10174, 'synset': 'put-put.n.01', 'name': 'put-put'}, {'id': 10175, 'synset': 'puttee.n.01', 'name': 'puttee'}, {'id': 10176, 'synset': 'putter.n.02', 'name': 'putter'}, {'id': 10177, 'synset': 'putty_knife.n.01', 'name': 'putty_knife'}, {'id': 10178, 'synset': 'puzzle.n.02', 'name': 'puzzle'}, {'id': 10179, 'synset': 'pylon.n.02', 'name': 'pylon'}, {'id': 10180, 'synset': 'pylon.n.01', 'name': 'pylon'}, {'id': 10181, 'synset': 'pyramidal_tent.n.01', 'name': 'pyramidal_tent'}, {'id': 10182, 'synset': 'pyrograph.n.01', 'name': 'pyrograph'}, {'id': 10183, 'synset': 'pyrometer.n.01', 'name': 'pyrometer'}, {'id': 10184, 'synset': 'pyrometric_cone.n.01', 'name': 'pyrometric_cone'}, {'id': 10185, 'synset': 'pyrostat.n.01', 'name': 'pyrostat'}, {'id': 10186, 'synset': 'pyx.n.02', 'name': 'pyx'}, {'id': 10187, 'synset': 'pyx.n.01', 'name': 'pyx'}, {'id': 10188, 'synset': 'pyxis.n.03', 'name': 'pyxis'}, {'id': 10189, 'synset': 'quad.n.04', 'name': 'quad'}, {'id': 10190, 'synset': 'quadrant.n.04', 'name': 'quadrant'}, {'id': 10191, 'synset': 'quadraphony.n.01', 'name': 'quadraphony'}, {'id': 10192, 'synset': 'quartering.n.02', 'name': 'quartering'}, {'id': 10193, 'synset': 'quarterstaff.n.01', 'name': 'quarterstaff'}, {'id': 10194, 'synset': 'quartz_battery.n.01', 'name': 'quartz_battery'}, {'id': 10195, 'synset': 'quartz_lamp.n.01', 'name': 'quartz_lamp'}, {'id': 10196, 'synset': 'queen.n.08', 'name': 'queen'}, {'id': 10197, 'synset': 'queen.n.07', 'name': 'queen'}, {'id': 10198, 'synset': 'queen_post.n.01', 'name': 'queen_post'}, {'id': 10199, 'synset': 'quern.n.01', 'name': 'quern'}, {'id': 10200, 'synset': 'quill.n.01', 'name': 'quill'}, {'id': 10201, 'synset': 'quilted_bedspread.n.01', 'name': 'quilted_bedspread'}, {'id': 10202, 'synset': 'quilting.n.02', 'name': 'quilting'}, {'id': 10203, 'synset': 'quipu.n.01', 'name': 'quipu'}, {'id': 10204, 'synset': 'quirk_molding.n.01', 'name': 'quirk_molding'}, {'id': 10205, 'synset': 'quirt.n.01', 'name': 'quirt'}, {'id': 10206, 'synset': 'quiver.n.03', 'name': 'quiver'}, {'id': 10207, 'synset': 'quoin.n.02', 'name': 'quoin'}, {'id': 10208, 'synset': 'quoit.n.01', 'name': 'quoit'}, {'id': 10209, 'synset': 'qwerty_keyboard.n.01', 'name': 'QWERTY_keyboard'}, {'id': 10210, 'synset': 'rabbet.n.01', 'name': 'rabbet'}, {'id': 10211, 'synset': 'rabbet_joint.n.01', 'name': 'rabbet_joint'}, {'id': 10212, 'synset': 'rabbit_ears.n.01', 'name': 'rabbit_ears'}, {'id': 10213, 'synset': 'rabbit_hutch.n.01', 'name': 'rabbit_hutch'}, {'id': 10214, 'synset': 'raceabout.n.01', 'name': 'raceabout'}, {'id': 10215, 'synset': 'raceway.n.01', 'name': 'raceway'}, {'id': 10216, 'synset': 'racing_boat.n.01', 'name': 'racing_boat'}, {'id': 10217, 'synset': 'racing_gig.n.01', 'name': 'racing_gig'}, {'id': 10218, 'synset': 'racing_skiff.n.01', 'name': 'racing_skiff'}, {'id': 10219, 'synset': 'rack.n.05', 'name': 'rack'}, {'id': 10220, 'synset': 'rack.n.01', 'name': 'rack'}, {'id': 10221, 'synset': 'rack.n.04', 'name': 'rack'}, {'id': 10222, 'synset': 'rack_and_pinion.n.01', 'name': 'rack_and_pinion'}, {'id': 10223, 'synset': 'racquetball.n.01', 'name': 'racquetball'}, {'id': 10224, 'synset': 'radial.n.01', 'name': 'radial'}, {'id': 10225, 'synset': 'radial_engine.n.01', 'name': 'radial_engine'}, {'id': 10226, 'synset': 'radiation_pyrometer.n.01', 'name': 'radiation_pyrometer'}, {'id': 10227, 'synset': 'radiator.n.02', 'name': 'radiator'}, {'id': 10228, 'synset': 'radiator_cap.n.01', 'name': 'radiator_cap'}, {'id': 10229, 'synset': 'radiator_hose.n.01', 'name': 'radiator_hose'}, {'id': 10230, 'synset': 'radio.n.03', 'name': 'radio'}, {'id': 10231, 'synset': 'radio_antenna.n.01', 'name': 'radio_antenna'}, {'id': 10232, 'synset': 'radio_chassis.n.01', 'name': 'radio_chassis'}, {'id': 10233, 'synset': 'radio_compass.n.01', 'name': 'radio_compass'}, {'id': 10234, 'synset': 'radiogram.n.02', 'name': 'radiogram'}, {'id': 10235, 'synset': 'radio_interferometer.n.01', 'name': 'radio_interferometer'}, {'id': 10236, 'synset': 'radio_link.n.01', 'name': 'radio_link'}, {'id': 10237, 'synset': 'radiometer.n.01', 'name': 'radiometer'}, {'id': 10238, 'synset': 'radiomicrometer.n.01', 'name': 'radiomicrometer'}, {'id': 10239, 'synset': 'radio-phonograph.n.01', 'name': 'radio-phonograph'}, {'id': 10240, 'synset': 'radiotelegraph.n.02', 'name': 'radiotelegraph'}, {'id': 10241, 'synset': 'radiotelephone.n.02', 'name': 'radiotelephone'}, {'id': 10242, 'synset': 'radio_telescope.n.01', 'name': 'radio_telescope'}, {'id': 10243, 'synset': 'radiotherapy_equipment.n.01', 'name': 'radiotherapy_equipment'}, {'id': 10244, 'synset': 'radio_transmitter.n.01', 'name': 'radio_transmitter'}, {'id': 10245, 'synset': 'radome.n.01', 'name': 'radome'}, {'id': 10246, 'synset': 'rafter.n.01', 'name': 'rafter'}, {'id': 10247, 'synset': 'raft_foundation.n.01', 'name': 'raft_foundation'}, {'id': 10248, 'synset': 'rag.n.01', 'name': 'rag'}, {'id': 10249, 'synset': 'ragbag.n.02', 'name': 'ragbag'}, {'id': 10250, 'synset': 'raglan.n.01', 'name': 'raglan'}, {'id': 10251, 'synset': 'raglan_sleeve.n.01', 'name': 'raglan_sleeve'}, {'id': 10252, 'synset': 'rail.n.04', 'name': 'rail'}, {'id': 10253, 'synset': 'rail_fence.n.01', 'name': 'rail_fence'}, {'id': 10254, 'synset': 'railhead.n.01', 'name': 'railhead'}, {'id': 10255, 'synset': 'railing.n.01', 'name': 'railing'}, {'id': 10256, 'synset': 'railing.n.02', 'name': 'railing'}, {'id': 10257, 'synset': 'railroad_bed.n.01', 'name': 'railroad_bed'}, {'id': 10258, 'synset': 'railroad_tunnel.n.01', 'name': 'railroad_tunnel'}, {'id': 10259, 'synset': 'rain_barrel.n.01', 'name': 'rain_barrel'}, {'id': 10260, 'synset': 'rain_gauge.n.01', 'name': 'rain_gauge'}, {'id': 10261, 'synset': 'rain_stick.n.01', 'name': 'rain_stick'}, {'id': 10262, 'synset': 'rake.n.03', 'name': 'rake'}, {'id': 10263, 'synset': 'rake_handle.n.01', 'name': 'rake_handle'}, {'id': 10264, 'synset': 'ram_disk.n.01', 'name': 'RAM_disk'}, {'id': 10265, 'synset': 'ramekin.n.02', 'name': 'ramekin'}, {'id': 10266, 'synset': 'ramjet.n.01', 'name': 'ramjet'}, {'id': 10267, 'synset': 'rammer.n.01', 'name': 'rammer'}, {'id': 10268, 'synset': 'ramp.n.01', 'name': 'ramp'}, {'id': 10269, 'synset': 'rampant_arch.n.01', 'name': 'rampant_arch'}, {'id': 10270, 'synset': 'rampart.n.01', 'name': 'rampart'}, {'id': 10271, 'synset': 'ramrod.n.01', 'name': 'ramrod'}, {'id': 10272, 'synset': 'ramrod.n.03', 'name': 'ramrod'}, {'id': 10273, 'synset': 'ranch.n.01', 'name': 'ranch'}, {'id': 10274, 'synset': 'ranch_house.n.01', 'name': 'ranch_house'}, {'id': 10275, 'synset': 'random-access_memory.n.01', 'name': 'random-access_memory'}, {'id': 10276, 'synset': 'rangefinder.n.01', 'name': 'rangefinder'}, {'id': 10277, 'synset': 'range_hood.n.01', 'name': 'range_hood'}, {'id': 10278, 'synset': 'range_pole.n.01', 'name': 'range_pole'}, {'id': 10279, 'synset': 'rapier.n.01', 'name': 'rapier'}, {'id': 10280, 'synset': 'rariora.n.01', 'name': 'rariora'}, {'id': 10281, 'synset': 'rasp.n.02', 'name': 'rasp'}, {'id': 10282, 'synset': 'ratchet.n.01', 'name': 'ratchet'}, {'id': 10283, 'synset': 'ratchet_wheel.n.01', 'name': 'ratchet_wheel'}, {'id': 10284, 'synset': 'rathskeller.n.01', 'name': 'rathskeller'}, {'id': 10285, 'synset': 'ratline.n.01', 'name': 'ratline'}, {'id': 10286, 'synset': 'rat-tail_file.n.01', 'name': 'rat-tail_file'}, {'id': 10287, 'synset': 'rattan.n.03', 'name': 'rattan'}, {'id': 10288, 'synset': 'rattrap.n.03', 'name': 'rattrap'}, {'id': 10289, 'synset': 'rayon.n.01', 'name': 'rayon'}, {'id': 10290, 'synset': 'razor.n.01', 'name': 'razor'}, {'id': 10291, 'synset': 'reaction-propulsion_engine.n.01', 'name': 'reaction-propulsion_engine'}, {'id': 10292, 'synset': 'reaction_turbine.n.01', 'name': 'reaction_turbine'}, {'id': 10293, 'synset': 'reactor.n.01', 'name': 'reactor'}, {'id': 10294, 'synset': 'reading_lamp.n.01', 'name': 'reading_lamp'}, {'id': 10295, 'synset': 'reading_room.n.01', 'name': 'reading_room'}, {'id': 10296, 'synset': 'read-only_memory.n.01', 'name': 'read-only_memory'}, {'id': 10297, 'synset': 'read-only_memory_chip.n.01', 'name': 'read-only_memory_chip'}, {'id': 10298, 'synset': 'readout.n.03', 'name': 'readout'}, {'id': 10299, 'synset': 'read/write_head.n.01', 'name': 'read/write_head'}, {'id': 10300, 'synset': 'ready-to-wear.n.01', 'name': 'ready-to-wear'}, {'id': 10301, 'synset': 'real_storage.n.01', 'name': 'real_storage'}, {'id': 10302, 'synset': 'reamer.n.02', 'name': 'reamer'}, {'id': 10303, 'synset': 'reaumur_thermometer.n.01', 'name': 'Reaumur_thermometer'}, {'id': 10304, 'synset': 'rebozo.n.01', 'name': 'rebozo'}, {'id': 10305, 'synset': 'receiver.n.01', 'name': 'receiver'}, {'id': 10306, 'synset': 'receptacle.n.01', 'name': 'receptacle'}, {'id': 10307, 'synset': 'reception_desk.n.01', 'name': 'reception_desk'}, {'id': 10308, 'synset': 'reception_room.n.01', 'name': 'reception_room'}, {'id': 10309, 'synset': 'recess.n.04', 'name': 'recess'}, {'id': 10310, 'synset': 'reciprocating_engine.n.01', 'name': 'reciprocating_engine'}, {'id': 10311, 'synset': 'reconnaissance_plane.n.01', 'name': 'reconnaissance_plane'}, {'id': 10312, 'synset': 'reconnaissance_vehicle.n.01', 'name': 'reconnaissance_vehicle'}, {'id': 10313, 'synset': 'record_changer.n.01', 'name': 'record_changer'}, {'id': 10314, 'synset': 'recorder.n.01', 'name': 'recorder'}, {'id': 10315, 'synset': 'recording.n.03', 'name': 'recording'}, {'id': 10316, 'synset': 'recording_system.n.01', 'name': 'recording_system'}, {'id': 10317, 'synset': 'record_sleeve.n.01', 'name': 'record_sleeve'}, {'id': 10318, 'synset': 'recovery_room.n.01', 'name': 'recovery_room'}, {'id': 10319, 'synset': 'recreational_vehicle.n.01', 'name': 'recreational_vehicle'}, {'id': 10320, 'synset': 'recreation_room.n.01', 'name': 'recreation_room'}, {'id': 10321, 'synset': 'recycling_bin.n.01', 'name': 'recycling_bin'}, {'id': 10322, 'synset': 'recycling_plant.n.01', 'name': 'recycling_plant'}, {'id': 10323, 'synset': 'redbrick_university.n.01', 'name': 'redbrick_university'}, {'id': 10324, 'synset': 'red_carpet.n.01', 'name': 'red_carpet'}, {'id': 10325, 'synset': 'redoubt.n.02', 'name': 'redoubt'}, {'id': 10326, 'synset': 'redoubt.n.01', 'name': 'redoubt'}, {'id': 10327, 'synset': 'reduction_gear.n.01', 'name': 'reduction_gear'}, {'id': 10328, 'synset': 'reed_pipe.n.01', 'name': 'reed_pipe'}, {'id': 10329, 'synset': 'reed_stop.n.01', 'name': 'reed_stop'}, {'id': 10330, 'synset': 'reef_knot.n.01', 'name': 'reef_knot'}, {'id': 10331, 'synset': 'reel.n.03', 'name': 'reel'}, {'id': 10332, 'synset': 'reel.n.01', 'name': 'reel'}, {'id': 10333, 'synset': 'refectory.n.01', 'name': 'refectory'}, {'id': 10334, 'synset': 'refectory_table.n.01', 'name': 'refectory_table'}, {'id': 10335, 'synset': 'refinery.n.01', 'name': 'refinery'}, {'id': 10336, 'synset': 'reflecting_telescope.n.01', 'name': 'reflecting_telescope'}, {'id': 10337, 'synset': 'reflectometer.n.01', 'name': 'reflectometer'}, {'id': 10338, 'synset': 'reflex_camera.n.01', 'name': 'reflex_camera'}, {'id': 10339, 'synset': 'reflux_condenser.n.01', 'name': 'reflux_condenser'}, {'id': 10340, 'synset': 'reformatory.n.01', 'name': 'reformatory'}, {'id': 10341, 'synset': 'reformer.n.02', 'name': 'reformer'}, {'id': 10342, 'synset': 'refracting_telescope.n.01', 'name': 'refracting_telescope'}, {'id': 10343, 'synset': 'refractometer.n.01', 'name': 'refractometer'}, {'id': 10344, 'synset': 'refrigeration_system.n.01', 'name': 'refrigeration_system'}, {'id': 10345, 'synset': 'refrigerator.n.01', 'name': 'refrigerator'}, {'id': 10346, 'synset': 'refrigerator_car.n.01', 'name': 'refrigerator_car'}, {'id': 10347, 'synset': 'refuge.n.03', 'name': 'refuge'}, {'id': 10348, 'synset': 'regalia.n.01', 'name': 'regalia'}, {'id': 10349, 'synset': 'regimentals.n.01', 'name': 'regimentals'}, {'id': 10350, 'synset': 'regulator.n.01', 'name': 'regulator'}, {'id': 10351, 'synset': 'rein.n.01', 'name': 'rein'}, {'id': 10352, 'synset': 'relay.n.05', 'name': 'relay'}, {'id': 10353, 'synset': 'release.n.08', 'name': 'release'}, {'id': 10354, 'synset': 'religious_residence.n.01', 'name': 'religious_residence'}, {'id': 10355, 'synset': 'reliquary.n.01', 'name': 'reliquary'}, {'id': 10356, 'synset': 'remote_terminal.n.01', 'name': 'remote_terminal'}, {'id': 10357, 'synset': 'removable_disk.n.01', 'name': 'removable_disk'}, {'id': 10358, 'synset': 'rendering.n.05', 'name': 'rendering'}, {'id': 10359, 'synset': 'rep.n.02', 'name': 'rep'}, {'id': 10360, 'synset': 'repair_shop.n.01', 'name': 'repair_shop'}, {'id': 10361, 'synset': 'repeater.n.04', 'name': 'repeater'}, {'id': 10362, 'synset': 'repeating_firearm.n.01', 'name': 'repeating_firearm'}, {'id': 10363, 'synset': 'repository.n.03', 'name': 'repository'}, {'id': 10364, 'synset': 'reproducer.n.01', 'name': 'reproducer'}, {'id': 10365, 'synset': 'rerebrace.n.01', 'name': 'rerebrace'}, {'id': 10366, 'synset': 'rescue_equipment.n.01', 'name': 'rescue_equipment'}, {'id': 10367, 'synset': 'research_center.n.01', 'name': 'research_center'}, {'id': 10368, 'synset': 'reseau.n.02', 'name': 'reseau'}, {'id': 10369, 'synset': 'reservoir.n.03', 'name': 'reservoir'}, {'id': 10370, 'synset': 'reset.n.01', 'name': 'reset'}, {'id': 10371, 'synset': 'reset_button.n.01', 'name': 'reset_button'}, {'id': 10372, 'synset': 'residence.n.02', 'name': 'residence'}, {'id': 10373, 'synset': 'resistance_pyrometer.n.01', 'name': 'resistance_pyrometer'}, {'id': 10374, 'synset': 'resistor.n.01', 'name': 'resistor'}, {'id': 10375, 'synset': 'resonator.n.03', 'name': 'resonator'}, {'id': 10376, 'synset': 'resonator.n.01', 'name': 'resonator'}, {'id': 10377, 'synset': 'resort_hotel.n.02', 'name': 'resort_hotel'}, {'id': 10378, 'synset': 'respirator.n.01', 'name': 'respirator'}, {'id': 10379, 'synset': 'restaurant.n.01', 'name': 'restaurant'}, {'id': 10380, 'synset': 'rest_house.n.01', 'name': 'rest_house'}, {'id': 10381, 'synset': 'restraint.n.06', 'name': 'restraint'}, {'id': 10382, 'synset': 'resuscitator.n.01', 'name': 'resuscitator'}, {'id': 10383, 'synset': 'retainer.n.03', 'name': 'retainer'}, {'id': 10384, 'synset': 'retaining_wall.n.01', 'name': 'retaining_wall'}, {'id': 10385, 'synset': 'reticle.n.01', 'name': 'reticle'}, {'id': 10386, 'synset': 'reticulation.n.02', 'name': 'reticulation'}, {'id': 10387, 'synset': 'reticule.n.01', 'name': 'reticule'}, {'id': 10388, 'synset': 'retort.n.02', 'name': 'retort'}, {'id': 10389, 'synset': 'retractor.n.01', 'name': 'retractor'}, {'id': 10390, 'synset': 'return_key.n.01', 'name': 'return_key'}, {'id': 10391, 'synset': 'reverberatory_furnace.n.01', 'name': 'reverberatory_furnace'}, {'id': 10392, 'synset': 'revers.n.01', 'name': 'revers'}, {'id': 10393, 'synset': 'reverse.n.02', 'name': 'reverse'}, {'id': 10394, 'synset': 'reversible.n.01', 'name': 'reversible'}, {'id': 10395, 'synset': 'revetment.n.02', 'name': 'revetment'}, {'id': 10396, 'synset': 'revetment.n.01', 'name': 'revetment'}, {'id': 10397, 'synset': 'revolver.n.01', 'name': 'revolver'}, {'id': 10398, 'synset': 'revolving_door.n.02', 'name': 'revolving_door'}, {'id': 10399, 'synset': 'rheometer.n.01', 'name': 'rheometer'}, {'id': 10400, 'synset': 'rheostat.n.01', 'name': 'rheostat'}, {'id': 10401, 'synset': 'rhinoscope.n.01', 'name': 'rhinoscope'}, {'id': 10402, 'synset': 'rib.n.01', 'name': 'rib'}, {'id': 10403, 'synset': 'riband.n.01', 'name': 'riband'}, {'id': 10404, 'synset': 'ribbed_vault.n.01', 'name': 'ribbed_vault'}, {'id': 10405, 'synset': 'ribbing.n.01', 'name': 'ribbing'}, {'id': 10406, 'synset': 'ribbon_development.n.01', 'name': 'ribbon_development'}, {'id': 10407, 'synset': 'rib_joint_pliers.n.01', 'name': 'rib_joint_pliers'}, {'id': 10408, 'synset': 'ricer.n.01', 'name': 'ricer'}, {'id': 10409, 'synset': 'riddle.n.02', 'name': 'riddle'}, {'id': 10410, 'synset': 'ride.n.02', 'name': 'ride'}, {'id': 10411, 'synset': 'ridge.n.06', 'name': 'ridge'}, {'id': 10412, 'synset': 'ridge_rope.n.01', 'name': 'ridge_rope'}, {'id': 10413, 'synset': 'riding_boot.n.01', 'name': 'riding_boot'}, {'id': 10414, 'synset': 'riding_crop.n.01', 'name': 'riding_crop'}, {'id': 10415, 'synset': 'riding_mower.n.01', 'name': 'riding_mower'}, {'id': 10416, 'synset': 'rifle_ball.n.01', 'name': 'rifle_ball'}, {'id': 10417, 'synset': 'rifle_grenade.n.01', 'name': 'rifle_grenade'}, {'id': 10418, 'synset': 'rig.n.01', 'name': 'rig'}, {'id': 10419, 'synset': 'rigger.n.02', 'name': 'rigger'}, {'id': 10420, 'synset': 'rigger.n.04', 'name': 'rigger'}, {'id': 10421, 'synset': 'rigging.n.01', 'name': 'rigging'}, {'id': 10422, 'synset': 'rigout.n.01', 'name': 'rigout'}, {'id': 10423, 'synset': 'ringlet.n.03', 'name': 'ringlet'}, {'id': 10424, 'synset': 'rings.n.01', 'name': 'rings'}, {'id': 10425, 'synset': 'rink.n.01', 'name': 'rink'}, {'id': 10426, 'synset': 'riot_gun.n.01', 'name': 'riot_gun'}, {'id': 10427, 'synset': 'ripcord.n.02', 'name': 'ripcord'}, {'id': 10428, 'synset': 'ripcord.n.01', 'name': 'ripcord'}, {'id': 10429, 'synset': 'ripping_bar.n.01', 'name': 'ripping_bar'}, {'id': 10430, 'synset': 'ripping_chisel.n.01', 'name': 'ripping_chisel'}, {'id': 10431, 'synset': 'ripsaw.n.01', 'name': 'ripsaw'}, {'id': 10432, 'synset': 'riser.n.03', 'name': 'riser'}, {'id': 10433, 'synset': 'riser.n.02', 'name': 'riser'}, {'id': 10434, 'synset': 'ritz.n.03', 'name': 'Ritz'}, {'id': 10435, 'synset': 'rivet.n.02', 'name': 'rivet'}, {'id': 10436, 'synset': 'riveting_machine.n.01', 'name': 'riveting_machine'}, {'id': 10437, 'synset': 'roach_clip.n.01', 'name': 'roach_clip'}, {'id': 10438, 'synset': 'road.n.01', 'name': 'road'}, {'id': 10439, 'synset': 'roadbed.n.01', 'name': 'roadbed'}, {'id': 10440, 'synset': 'roadblock.n.02', 'name': 'roadblock'}, {'id': 10441, 'synset': 'roadhouse.n.01', 'name': 'roadhouse'}, {'id': 10442, 'synset': 'roadster.n.01', 'name': 'roadster'}, {'id': 10443, 'synset': 'roadway.n.01', 'name': 'roadway'}, {'id': 10444, 'synset': 'roaster.n.04', 'name': 'roaster'}, {'id': 10445, 'synset': 'robotics_equipment.n.01', 'name': 'robotics_equipment'}, {'id': 10446, 'synset': 'rochon_prism.n.01', 'name': 'Rochon_prism'}, {'id': 10447, 'synset': 'rock_bit.n.01', 'name': 'rock_bit'}, {'id': 10448, 'synset': 'rocker.n.07', 'name': 'rocker'}, {'id': 10449, 'synset': 'rocker.n.05', 'name': 'rocker'}, {'id': 10450, 'synset': 'rocker_arm.n.01', 'name': 'rocker_arm'}, {'id': 10451, 'synset': 'rocket.n.02', 'name': 'rocket'}, {'id': 10452, 'synset': 'rocket.n.01', 'name': 'rocket'}, {'id': 10453, 'synset': 'rod.n.01', 'name': 'rod'}, {'id': 10454, 'synset': 'rodeo.n.02', 'name': 'rodeo'}, {'id': 10455, 'synset': 'roll.n.04', 'name': 'roll'}, {'id': 10456, 'synset': 'roller.n.04', 'name': 'roller'}, {'id': 10457, 'synset': 'roller.n.03', 'name': 'roller'}, {'id': 10458, 'synset': 'roller_bandage.n.01', 'name': 'roller_bandage'}, {'id': 10459, 'synset': 'in-line_skate.n.01', 'name': 'in-line_skate'}, {'id': 10460, 'synset': 'roller_blind.n.01', 'name': 'roller_blind'}, {'id': 10461, 'synset': 'roller_coaster.n.02', 'name': 'roller_coaster'}, {'id': 10462, 'synset': 'roller_towel.n.01', 'name': 'roller_towel'}, {'id': 10463, 'synset': 'roll_film.n.01', 'name': 'roll_film'}, {'id': 10464, 'synset': 'rolling_hitch.n.01', 'name': 'rolling_hitch'}, {'id': 10465, 'synset': 'rolling_mill.n.01', 'name': 'rolling_mill'}, {'id': 10466, 'synset': 'rolling_stock.n.01', 'name': 'rolling_stock'}, {'id': 10467, 'synset': 'roll-on.n.02', 'name': 'roll-on'}, {'id': 10468, 'synset': 'roll-on.n.01', 'name': 'roll-on'}, {'id': 10469, 'synset': 'roll-on_roll-off.n.01', 'name': 'roll-on_roll-off'}, {'id': 10470, 'synset': 'rolodex.n.01', 'name': 'Rolodex'}, {'id': 10471, 'synset': 'roman_arch.n.01', 'name': 'Roman_arch'}, {'id': 10472, 'synset': 'roman_building.n.01', 'name': 'Roman_building'}, {'id': 10473, 'synset': 'romper.n.02', 'name': 'romper'}, {'id': 10474, 'synset': 'rood_screen.n.01', 'name': 'rood_screen'}, {'id': 10475, 'synset': 'roof.n.01', 'name': 'roof'}, {'id': 10476, 'synset': 'roof.n.02', 'name': 'roof'}, {'id': 10477, 'synset': 'roofing.n.01', 'name': 'roofing'}, {'id': 10478, 'synset': 'room.n.01', 'name': 'room'}, {'id': 10479, 'synset': 'roomette.n.01', 'name': 'roomette'}, {'id': 10480, 'synset': 'room_light.n.01', 'name': 'room_light'}, {'id': 10481, 'synset': 'roost.n.01', 'name': 'roost'}, {'id': 10482, 'synset': 'rope.n.01', 'name': 'rope'}, {'id': 10483, 'synset': 'rope_bridge.n.01', 'name': 'rope_bridge'}, {'id': 10484, 'synset': 'rope_tow.n.01', 'name': 'rope_tow'}, {'id': 10485, 'synset': 'rose_water.n.01', 'name': 'rose_water'}, {'id': 10486, 'synset': 'rose_window.n.01', 'name': 'rose_window'}, {'id': 10487, 'synset': 'rosin_bag.n.01', 'name': 'rosin_bag'}, {'id': 10488, 'synset': 'rotary_actuator.n.01', 'name': 'rotary_actuator'}, {'id': 10489, 'synset': 'rotary_engine.n.01', 'name': 'rotary_engine'}, {'id': 10490, 'synset': 'rotary_press.n.01', 'name': 'rotary_press'}, {'id': 10491, 'synset': 'rotating_mechanism.n.01', 'name': 'rotating_mechanism'}, {'id': 10492, 'synset': 'rotating_shaft.n.01', 'name': 'rotating_shaft'}, {'id': 10493, 'synset': 'rotisserie.n.02', 'name': 'rotisserie'}, {'id': 10494, 'synset': 'rotisserie.n.01', 'name': 'rotisserie'}, {'id': 10495, 'synset': 'rotor.n.03', 'name': 'rotor'}, {'id': 10496, 'synset': 'rotor.n.01', 'name': 'rotor'}, {'id': 10497, 'synset': 'rotor.n.02', 'name': 'rotor'}, {'id': 10498, 'synset': 'rotor_blade.n.01', 'name': 'rotor_blade'}, {'id': 10499, 'synset': 'rotor_head.n.01', 'name': 'rotor_head'}, {'id': 10500, 'synset': 'rotunda.n.02', 'name': 'rotunda'}, {'id': 10501, 'synset': 'rotunda.n.01', 'name': 'rotunda'}, {'id': 10502, 'synset': 'rouge.n.01', 'name': 'rouge'}, {'id': 10503, 'synset': 'roughcast.n.02', 'name': 'roughcast'}, {'id': 10504, 'synset': 'rouleau.n.02', 'name': 'rouleau'}, {'id': 10505, 'synset': 'roulette.n.02', 'name': 'roulette'}, {'id': 10506, 'synset': 'roulette_ball.n.01', 'name': 'roulette_ball'}, {'id': 10507, 'synset': 'roulette_wheel.n.01', 'name': 'roulette_wheel'}, {'id': 10508, 'synset': 'round.n.01', 'name': 'round'}, {'id': 10509, 'synset': 'round_arch.n.01', 'name': 'round_arch'}, {'id': 10510, 'synset': 'round-bottom_flask.n.01', 'name': 'round-bottom_flask'}, {'id': 10511, 'synset': 'roundel.n.02', 'name': 'roundel'}, {'id': 10512, 'synset': 'round_file.n.01', 'name': 'round_file'}, {'id': 10513, 'synset': 'roundhouse.n.01', 'name': 'roundhouse'}, {'id': 10514, 'synset': 'router.n.03', 'name': 'router'}, {'id': 10515, 'synset': 'router_plane.n.01', 'name': 'router_plane'}, {'id': 10516, 'synset': 'rowel.n.01', 'name': 'rowel'}, {'id': 10517, 'synset': 'row_house.n.01', 'name': 'row_house'}, {'id': 10518, 'synset': 'rowing_boat.n.01', 'name': 'rowing_boat'}, {'id': 10519, 'synset': 'rowlock_arch.n.01', 'name': 'rowlock_arch'}, {'id': 10520, 'synset': 'royal.n.01', 'name': 'royal'}, {'id': 10521, 'synset': 'royal_mast.n.01', 'name': 'royal_mast'}, {'id': 10522, 'synset': 'rubber_boot.n.01', 'name': 'rubber_boot'}, {'id': 10523, 'synset': 'rubber_bullet.n.01', 'name': 'rubber_bullet'}, {'id': 10524, 'synset': 'rubber_eraser.n.01', 'name': 'rubber_eraser'}, {'id': 10525, 'synset': 'rudder.n.02', 'name': 'rudder'}, {'id': 10526, 'synset': 'rudder.n.01', 'name': 'rudder'}, {'id': 10527, 'synset': 'rudder_blade.n.01', 'name': 'rudder_blade'}, {'id': 10528, 'synset': 'rug.n.01', 'name': 'rug'}, {'id': 10529, 'synset': 'rugby_ball.n.01', 'name': 'rugby_ball'}, {'id': 10530, 'synset': 'ruin.n.02', 'name': 'ruin'}, {'id': 10531, 'synset': 'rule.n.12', 'name': 'rule'}, {'id': 10532, 'synset': 'rumble.n.02', 'name': 'rumble'}, {'id': 10533, 'synset': 'rumble_seat.n.01', 'name': 'rumble_seat'}, {'id': 10534, 'synset': 'rummer.n.01', 'name': 'rummer'}, {'id': 10535, 'synset': 'rumpus_room.n.01', 'name': 'rumpus_room'}, {'id': 10536, 'synset': 'runcible_spoon.n.01', 'name': 'runcible_spoon'}, {'id': 10537, 'synset': 'rundle.n.01', 'name': 'rundle'}, {'id': 10538, 'synset': 'running_shoe.n.01', 'name': 'running_shoe'}, {'id': 10539, 'synset': 'running_suit.n.01', 'name': 'running_suit'}, {'id': 10540, 'synset': 'runway.n.04', 'name': 'runway'}, {'id': 10541, 'synset': 'rushlight.n.01', 'name': 'rushlight'}, {'id': 10542, 'synset': 'russet.n.01', 'name': 'russet'}, {'id': 10543, 'synset': 'rya.n.01', 'name': 'rya'}, {'id': 10544, 'synset': 'saber.n.01', 'name': 'saber'}, {'id': 10545, 'synset': 'saber_saw.n.01', 'name': 'saber_saw'}, {'id': 10546, 'synset': 'sable.n.04', 'name': 'sable'}, {'id': 10547, 'synset': 'sable.n.01', 'name': 'sable'}, {'id': 10548, 'synset': 'sable_coat.n.01', 'name': 'sable_coat'}, {'id': 10549, 'synset': 'sabot.n.01', 'name': 'sabot'}, {'id': 10550, 'synset': 'sachet.n.01', 'name': 'sachet'}, {'id': 10551, 'synset': 'sack.n.05', 'name': 'sack'}, {'id': 10552, 'synset': 'sackbut.n.01', 'name': 'sackbut'}, {'id': 10553, 'synset': 'sackcloth.n.02', 'name': 'sackcloth'}, {'id': 10554, 'synset': 'sackcloth.n.01', 'name': 'sackcloth'}, {'id': 10555, 'synset': 'sack_coat.n.01', 'name': 'sack_coat'}, {'id': 10556, 'synset': 'sacking.n.01', 'name': 'sacking'}, {'id': 10557, 'synset': 'saddle_oxford.n.01', 'name': 'saddle_oxford'}, {'id': 10558, 'synset': 'saddlery.n.02', 'name': 'saddlery'}, {'id': 10559, 'synset': 'saddle_seat.n.01', 'name': 'saddle_seat'}, {'id': 10560, 'synset': 'saddle_stitch.n.01', 'name': 'saddle_stitch'}, {'id': 10561, 'synset': 'safe.n.01', 'name': 'safe'}, {'id': 10562, 'synset': 'safe.n.02', 'name': 'safe'}, {'id': 10563, 'synset': 'safe-deposit.n.01', 'name': 'safe-deposit'}, {'id': 10564, 'synset': 'safe_house.n.01', 'name': 'safe_house'}, {'id': 10565, 'synset': 'safety_arch.n.01', 'name': 'safety_arch'}, {'id': 10566, 'synset': 'safety_belt.n.01', 'name': 'safety_belt'}, {'id': 10567, 'synset': 'safety_bicycle.n.01', 'name': 'safety_bicycle'}, {'id': 10568, 'synset': 'safety_bolt.n.01', 'name': 'safety_bolt'}, {'id': 10569, 'synset': 'safety_curtain.n.01', 'name': 'safety_curtain'}, {'id': 10570, 'synset': 'safety_fuse.n.01', 'name': 'safety_fuse'}, {'id': 10571, 'synset': 'safety_lamp.n.01', 'name': 'safety_lamp'}, {'id': 10572, 'synset': 'safety_match.n.01', 'name': 'safety_match'}, {'id': 10573, 'synset': 'safety_net.n.02', 'name': 'safety_net'}, {'id': 10574, 'synset': 'safety_rail.n.01', 'name': 'safety_rail'}, {'id': 10575, 'synset': 'safety_razor.n.01', 'name': 'safety_razor'}, {'id': 10576, 'synset': 'safety_valve.n.01', 'name': 'safety_valve'}, {'id': 10577, 'synset': 'sail.n.03', 'name': 'sail'}, {'id': 10578, 'synset': 'sailboat.n.01', 'name': 'sailboat'}, {'id': 10579, 'synset': 'sailcloth.n.01', 'name': 'sailcloth'}, {'id': 10580, 'synset': 'sailing_vessel.n.01', 'name': 'sailing_vessel'}, {'id': 10581, 'synset': 'sailing_warship.n.01', 'name': 'sailing_warship'}, {'id': 10582, 'synset': 'sailor_cap.n.01', 'name': 'sailor_cap'}, {'id': 10583, 'synset': 'sailor_suit.n.01', 'name': 'sailor_suit'}, {'id': 10584, 'synset': 'salad_bar.n.01', 'name': 'salad_bar'}, {'id': 10585, 'synset': 'salad_bowl.n.02', 'name': 'salad_bowl'}, {'id': 10586, 'synset': 'salinometer.n.01', 'name': 'salinometer'}, {'id': 10587, 'synset': 'sallet.n.01', 'name': 'sallet'}, {'id': 10588, 'synset': 'salon.n.03', 'name': 'salon'}, {'id': 10589, 'synset': 'salon.n.01', 'name': 'salon'}, {'id': 10590, 'synset': 'salon.n.02', 'name': 'salon'}, {'id': 10591, 'synset': 'saltbox.n.01', 'name': 'saltbox'}, {'id': 10592, 'synset': 'saltcellar.n.01', 'name': 'saltcellar'}, {'id': 10593, 'synset': 'saltworks.n.01', 'name': 'saltworks'}, {'id': 10594, 'synset': 'salver.n.01', 'name': 'salver'}, {'id': 10595, 'synset': 'salwar.n.01', 'name': 'salwar'}, {'id': 10596, 'synset': 'sam_browne_belt.n.01', 'name': 'Sam_Browne_belt'}, {'id': 10597, 'synset': 'samisen.n.01', 'name': 'samisen'}, {'id': 10598, 'synset': 'samite.n.01', 'name': 'samite'}, {'id': 10599, 'synset': 'samovar.n.01', 'name': 'samovar'}, {'id': 10600, 'synset': 'sampan.n.01', 'name': 'sampan'}, {'id': 10601, 'synset': 'sandbag.n.01', 'name': 'sandbag'}, {'id': 10602, 'synset': 'sandblaster.n.01', 'name': 'sandblaster'}, {'id': 10603, 'synset': 'sandbox.n.01', 'name': 'sandbox'}, {'id': 10604, 'synset': 'sandglass.n.01', 'name': 'sandglass'}, {'id': 10605, 'synset': 'sand_wedge.n.01', 'name': 'sand_wedge'}, {'id': 10606, 'synset': 'sandwich_board.n.01', 'name': 'sandwich_board'}, {'id': 10607, 'synset': 'sanitary_napkin.n.01', 'name': 'sanitary_napkin'}, {'id': 10608, 'synset': 'cling_film.n.01', 'name': 'cling_film'}, {'id': 10609, 'synset': 'sarcenet.n.01', 'name': 'sarcenet'}, {'id': 10610, 'synset': 'sarcophagus.n.01', 'name': 'sarcophagus'}, {'id': 10611, 'synset': 'sari.n.01', 'name': 'sari'}, {'id': 10612, 'synset': 'sarong.n.01', 'name': 'sarong'}, {'id': 10613, 'synset': 'sash.n.01', 'name': 'sash'}, {'id': 10614, 'synset': 'sash_fastener.n.01', 'name': 'sash_fastener'}, {'id': 10615, 'synset': 'sash_window.n.01', 'name': 'sash_window'}, {'id': 10616, 'synset': 'sateen.n.01', 'name': 'sateen'}, {'id': 10617, 'synset': 'satellite.n.01', 'name': 'satellite'}, {'id': 10618, 'synset': 'satellite_receiver.n.01', 'name': 'satellite_receiver'}, {'id': 10619, 'synset': 'satellite_television.n.01', 'name': 'satellite_television'}, {'id': 10620, 'synset': 'satellite_transmitter.n.01', 'name': 'satellite_transmitter'}, {'id': 10621, 'synset': 'satin.n.01', 'name': 'satin'}, {'id': 10622, 'synset': 'saturday_night_special.n.01', 'name': 'Saturday_night_special'}, {'id': 10623, 'synset': 'saucepot.n.01', 'name': 'saucepot'}, {'id': 10624, 'synset': 'sauna.n.01', 'name': 'sauna'}, {'id': 10625, 'synset': 'savings_bank.n.02', 'name': 'savings_bank'}, {'id': 10626, 'synset': 'saw.n.02', 'name': 'saw'}, {'id': 10627, 'synset': 'sawed-off_shotgun.n.01', 'name': 'sawed-off_shotgun'}, {'id': 10628, 'synset': 'sawmill.n.01', 'name': 'sawmill'}, {'id': 10629, 'synset': 'saw_set.n.01', 'name': 'saw_set'}, {'id': 10630, 'synset': 'saxhorn.n.01', 'name': 'saxhorn'}, {'id': 10631, 'synset': 'scabbard.n.01', 'name': 'scabbard'}, {'id': 10632, 'synset': 'scaffolding.n.01', 'name': 'scaffolding'}, {'id': 10633, 'synset': 'scale.n.08', 'name': 'scale'}, {'id': 10634, 'synset': 'scaler.n.01', 'name': 'scaler'}, {'id': 10635, 'synset': 'scaling_ladder.n.01', 'name': 'scaling_ladder'}, {'id': 10636, 'synset': 'scalpel.n.01', 'name': 'scalpel'}, {'id': 10637, 'synset': 'scanner.n.04', 'name': 'scanner'}, {'id': 10638, 'synset': 'scanner.n.03', 'name': 'scanner'}, {'id': 10639, 'synset': 'scanner.n.02', 'name': 'scanner'}, {'id': 10640, 'synset': 'scantling.n.01', 'name': 'scantling'}, {'id': 10641, 'synset': 'scarf_joint.n.01', 'name': 'scarf_joint'}, {'id': 10642, 'synset': 'scatter_rug.n.01', 'name': 'scatter_rug'}, {'id': 10643, 'synset': 'scauper.n.01', 'name': 'scauper'}, {'id': 10644, 'synset': 'schmidt_telescope.n.01', 'name': 'Schmidt_telescope'}, {'id': 10645, 'synset': 'school.n.02', 'name': 'school'}, {'id': 10646, 'synset': 'schoolbag.n.01', 'name': 'schoolbag'}, {'id': 10647, 'synset': 'school_bell.n.01', 'name': 'school_bell'}, {'id': 10648, 'synset': 'school_ship.n.01', 'name': 'school_ship'}, {'id': 10649, 'synset': 'school_system.n.01', 'name': 'school_system'}, {'id': 10650, 'synset': 'schooner.n.02', 'name': 'schooner'}, {'id': 10651, 'synset': 'schooner.n.01', 'name': 'schooner'}, {'id': 10652, 'synset': 'scientific_instrument.n.01', 'name': 'scientific_instrument'}, {'id': 10653, 'synset': 'scimitar.n.01', 'name': 'scimitar'}, {'id': 10654, 'synset': 'scintillation_counter.n.01', 'name': 'scintillation_counter'}, {'id': 10655, 'synset': 'sclerometer.n.01', 'name': 'sclerometer'}, {'id': 10656, 'synset': 'scoinson_arch.n.01', 'name': 'scoinson_arch'}, {'id': 10657, 'synset': 'sconce.n.04', 'name': 'sconce'}, {'id': 10658, 'synset': 'sconce.n.03', 'name': 'sconce'}, {'id': 10659, 'synset': 'scoop.n.06', 'name': 'scoop'}, {'id': 10660, 'synset': 'scooter.n.02', 'name': 'scooter'}, {'id': 10661, 'synset': 'scouring_pad.n.01', 'name': 'scouring_pad'}, {'id': 10662, 'synset': 'scow.n.02', 'name': 'scow'}, {'id': 10663, 'synset': 'scow.n.01', 'name': 'scow'}, {'id': 10664, 'synset': 'scratcher.n.03', 'name': 'scratcher'}, {'id': 10665, 'synset': 'screen.n.05', 'name': 'screen'}, {'id': 10666, 'synset': 'screen.n.04', 'name': 'screen'}, {'id': 10667, 'synset': 'screen.n.09', 'name': 'screen'}, {'id': 10668, 'synset': 'screen.n.03', 'name': 'screen'}, {'id': 10669, 'synset': 'screen_door.n.01', 'name': 'screen_door'}, {'id': 10670, 'synset': 'screening.n.02', 'name': 'screening'}, {'id': 10671, 'synset': 'screw.n.04', 'name': 'screw'}, {'id': 10672, 'synset': 'screw.n.03', 'name': 'screw'}, {'id': 10673, 'synset': 'screw.n.02', 'name': 'screw'}, {'id': 10674, 'synset': 'screw_eye.n.01', 'name': 'screw_eye'}, {'id': 10675, 'synset': 'screw_key.n.01', 'name': 'screw_key'}, {'id': 10676, 'synset': 'screw_thread.n.01', 'name': 'screw_thread'}, {'id': 10677, 'synset': 'screwtop.n.01', 'name': 'screwtop'}, {'id': 10678, 'synset': 'screw_wrench.n.01', 'name': 'screw_wrench'}, {'id': 10679, 'synset': 'scriber.n.01', 'name': 'scriber'}, {'id': 10680, 'synset': 'scrim.n.01', 'name': 'scrim'}, {'id': 10681, 'synset': 'scrimshaw.n.01', 'name': 'scrimshaw'}, {'id': 10682, 'synset': 'scriptorium.n.01', 'name': 'scriptorium'}, {'id': 10683, 'synset': 'scrubber.n.03', 'name': 'scrubber'}, {'id': 10684, 'synset': 'scrub_plane.n.01', 'name': 'scrub_plane'}, {'id': 10685, 'synset': 'scuffer.n.01', 'name': 'scuffer'}, {'id': 10686, 'synset': 'scuffle.n.02', 'name': 'scuffle'}, {'id': 10687, 'synset': 'scull.n.02', 'name': 'scull'}, {'id': 10688, 'synset': 'scull.n.01', 'name': 'scull'}, {'id': 10689, 'synset': 'scullery.n.01', 'name': 'scullery'}, {'id': 10690, 'synset': 'scuttle.n.01', 'name': 'scuttle'}, {'id': 10691, 'synset': 'scyphus.n.01', 'name': 'scyphus'}, {'id': 10692, 'synset': 'scythe.n.01', 'name': 'scythe'}, {'id': 10693, 'synset': 'seabag.n.01', 'name': 'seabag'}, {'id': 10694, 'synset': 'sea_boat.n.01', 'name': 'sea_boat'}, {'id': 10695, 'synset': 'sea_chest.n.01', 'name': 'sea_chest'}, {'id': 10696, 'synset': 'sealing_wax.n.01', 'name': 'sealing_wax'}, {'id': 10697, 'synset': 'sealskin.n.02', 'name': 'sealskin'}, {'id': 10698, 'synset': 'seam.n.01', 'name': 'seam'}, {'id': 10699, 'synset': 'searchlight.n.01', 'name': 'searchlight'}, {'id': 10700, 'synset': 'searing_iron.n.01', 'name': 'searing_iron'}, {'id': 10701, 'synset': 'seat.n.04', 'name': 'seat'}, {'id': 10702, 'synset': 'seat.n.03', 'name': 'seat'}, {'id': 10703, 'synset': 'seat.n.09', 'name': 'seat'}, {'id': 10704, 'synset': 'seat_belt.n.01', 'name': 'seat_belt'}, {'id': 10705, 'synset': 'secateurs.n.01', 'name': 'secateurs'}, {'id': 10706, 'synset': 'secondary_coil.n.01', 'name': 'secondary_coil'}, {'id': 10707, 'synset': 'second_balcony.n.01', 'name': 'second_balcony'}, {'id': 10708, 'synset': 'second_base.n.01', 'name': 'second_base'}, {'id': 10709, 'synset': 'second_hand.n.02', 'name': 'second_hand'}, {'id': 10710, 'synset': 'secretary.n.04', 'name': 'secretary'}, {'id': 10711, 'synset': 'sectional.n.01', 'name': 'sectional'}, {'id': 10712, 'synset': 'security_blanket.n.02', 'name': 'security_blanket'}, {'id': 10713, 'synset': 'security_system.n.02', 'name': 'security_system'}, {'id': 10714, 'synset': 'security_system.n.01', 'name': 'security_system'}, {'id': 10715, 'synset': 'sedan.n.01', 'name': 'sedan'}, {'id': 10716, 'synset': 'sedan.n.02', 'name': 'sedan'}, {'id': 10717, 'synset': 'seeder.n.02', 'name': 'seeder'}, {'id': 10718, 'synset': 'seeker.n.02', 'name': 'seeker'}, {'id': 10719, 'synset': 'seersucker.n.01', 'name': 'seersucker'}, {'id': 10720, 'synset': 'segmental_arch.n.01', 'name': 'segmental_arch'}, {'id': 10721, 'synset': 'segway.n.01', 'name': 'Segway'}, {'id': 10722, 'synset': 'seidel.n.01', 'name': 'seidel'}, {'id': 10723, 'synset': 'seine.n.02', 'name': 'seine'}, {'id': 10724, 'synset': 'seismograph.n.01', 'name': 'seismograph'}, {'id': 10725, 'synset': 'selector.n.02', 'name': 'selector'}, {'id': 10726, 'synset': 'selenium_cell.n.01', 'name': 'selenium_cell'}, {'id': 10727, 'synset': 'self-propelled_vehicle.n.01', 'name': 'self-propelled_vehicle'}, {'id': 10728, 'synset': 'self-registering_thermometer.n.01', 'name': 'self-registering_thermometer'}, {'id': 10729, 'synset': 'self-starter.n.02', 'name': 'self-starter'}, {'id': 10730, 'synset': 'selsyn.n.01', 'name': 'selsyn'}, {'id': 10731, 'synset': 'selvage.n.02', 'name': 'selvage'}, {'id': 10732, 'synset': 'semaphore.n.01', 'name': 'semaphore'}, {'id': 10733, 'synset': 'semiautomatic_firearm.n.01', 'name': 'semiautomatic_firearm'}, {'id': 10734, 'synset': 'semiautomatic_pistol.n.01', 'name': 'semiautomatic_pistol'}, {'id': 10735, 'synset': 'semiconductor_device.n.01', 'name': 'semiconductor_device'}, {'id': 10736, 'synset': 'semi-detached_house.n.01', 'name': 'semi-detached_house'}, {'id': 10737, 'synset': 'semigloss.n.01', 'name': 'semigloss'}, {'id': 10738, 'synset': 'semitrailer.n.01', 'name': 'semitrailer'}, {'id': 10739, 'synset': 'sennit.n.01', 'name': 'sennit'}, {'id': 10740, 'synset': 'sensitometer.n.01', 'name': 'sensitometer'}, {'id': 10741, 'synset': 'sentry_box.n.01', 'name': 'sentry_box'}, {'id': 10742, 'synset': 'separate.n.02', 'name': 'separate'}, {'id': 10743, 'synset': 'septic_tank.n.01', 'name': 'septic_tank'}, {'id': 10744, 'synset': 'sequence.n.03', 'name': 'sequence'}, {'id': 10745, 'synset': 'sequencer.n.01', 'name': 'sequencer'}, {'id': 10746, 'synset': 'serape.n.01', 'name': 'serape'}, {'id': 10747, 'synset': 'serge.n.01', 'name': 'serge'}, {'id': 10748, 'synset': 'serger.n.01', 'name': 'serger'}, {'id': 10749, 'synset': 'serial_port.n.01', 'name': 'serial_port'}, {'id': 10750, 'synset': 'serpent.n.03', 'name': 'serpent'}, {'id': 10751, 'synset': 'serration.n.03', 'name': 'serration'}, {'id': 10752, 'synset': 'server.n.04', 'name': 'server'}, {'id': 10753, 'synset': 'server.n.03', 'name': 'server'}, {'id': 10754, 'synset': 'service_club.n.02', 'name': 'service_club'}, {'id': 10755, 'synset': 'serving_cart.n.01', 'name': 'serving_cart'}, {'id': 10756, 'synset': 'serving_dish.n.01', 'name': 'serving_dish'}, {'id': 10757, 'synset': 'servo.n.01', 'name': 'servo'}, {'id': 10758, 'synset': 'set.n.13', 'name': 'set'}, {'id': 10759, 'synset': 'set_gun.n.01', 'name': 'set_gun'}, {'id': 10760, 'synset': 'setscrew.n.02', 'name': 'setscrew'}, {'id': 10761, 'synset': 'setscrew.n.01', 'name': 'setscrew'}, {'id': 10762, 'synset': 'set_square.n.01', 'name': 'set_square'}, {'id': 10763, 'synset': 'settee.n.02', 'name': 'settee'}, {'id': 10764, 'synset': 'settle.n.01', 'name': 'settle'}, {'id': 10765, 'synset': 'settlement_house.n.01', 'name': 'settlement_house'}, {'id': 10766, 'synset': 'seventy-eight.n.02', 'name': 'seventy-eight'}, {'id': 10767, 'synset': 'seven_wonders_of_the_ancient_world.n.01', 'name': 'Seven_Wonders_of_the_Ancient_World'}, {'id': 10768, 'synset': 'sewage_disposal_plant.n.01', 'name': 'sewage_disposal_plant'}, {'id': 10769, 'synset': 'sewer.n.01', 'name': 'sewer'}, {'id': 10770, 'synset': 'sewing_basket.n.01', 'name': 'sewing_basket'}, {'id': 10771, 'synset': 'sewing_kit.n.01', 'name': 'sewing_kit'}, {'id': 10772, 'synset': 'sewing_needle.n.01', 'name': 'sewing_needle'}, {'id': 10773, 'synset': 'sewing_room.n.01', 'name': 'sewing_room'}, {'id': 10774, 'synset': 'sextant.n.02', 'name': 'sextant'}, {'id': 10775, 'synset': 'sgraffito.n.01', 'name': 'sgraffito'}, {'id': 10776, 'synset': 'shackle.n.01', 'name': 'shackle'}, {'id': 10777, 'synset': 'shackle.n.02', 'name': 'shackle'}, {'id': 10778, 'synset': 'shade.n.03', 'name': 'shade'}, {'id': 10779, 'synset': 'shadow_box.n.01', 'name': 'shadow_box'}, {'id': 10780, 'synset': 'shaft.n.03', 'name': 'shaft'}, {'id': 10781, 'synset': 'shag_rug.n.01', 'name': 'shag_rug'}, {'id': 10782, 'synset': 'shank.n.04', 'name': 'shank'}, {'id': 10783, 'synset': 'shank.n.03', 'name': 'shank'}, {'id': 10784, 'synset': 'shantung.n.01', 'name': 'shantung'}, {'id': 10785, 'synset': 'shaper.n.02', 'name': 'shaper'}, {'id': 10786, 'synset': 'shaping_tool.n.01', 'name': 'shaping_tool'}, {'id': 10787, 'synset': 'sharkskin.n.01', 'name': 'sharkskin'}, {'id': 10788, 'synset': 'shaving_brush.n.01', 'name': 'shaving_brush'}, {'id': 10789, 'synset': 'shaving_foam.n.01', 'name': 'shaving_foam'}, {'id': 10790, 'synset': 'shawm.n.01', 'name': 'shawm'}, {'id': 10791, 'synset': 'sheath.n.01', 'name': 'sheath'}, {'id': 10792, 'synset': 'sheathing.n.01', 'name': 'sheathing'}, {'id': 10793, 'synset': 'shed.n.01', 'name': 'shed'}, {'id': 10794, 'synset': 'sheep_bell.n.01', 'name': 'sheep_bell'}, {'id': 10795, 'synset': 'sheepshank.n.01', 'name': 'sheepshank'}, {'id': 10796, 'synset': 'sheepskin_coat.n.01', 'name': 'sheepskin_coat'}, {'id': 10797, 'synset': 'sheepwalk.n.01', 'name': 'sheepwalk'}, {'id': 10798, 'synset': 'sheet.n.03', 'name': 'sheet'}, {'id': 10799, 'synset': 'sheet_bend.n.01', 'name': 'sheet_bend'}, {'id': 10800, 'synset': 'sheeting.n.01', 'name': 'sheeting'}, {'id': 10801, 'synset': 'sheet_pile.n.01', 'name': 'sheet_pile'}, {'id': 10802, 'synset': 'sheetrock.n.01', 'name': 'Sheetrock'}, {'id': 10803, 'synset': 'shelf.n.01', 'name': 'shelf'}, {'id': 10804, 'synset': 'shelf_bracket.n.01', 'name': 'shelf_bracket'}, {'id': 10805, 'synset': 'shell.n.01', 'name': 'shell'}, {'id': 10806, 'synset': 'shell.n.08', 'name': 'shell'}, {'id': 10807, 'synset': 'shell.n.07', 'name': 'shell'}, {'id': 10808, 'synset': 'shellac.n.02', 'name': 'shellac'}, {'id': 10809, 'synset': 'shelter.n.01', 'name': 'shelter'}, {'id': 10810, 'synset': 'shelter.n.02', 'name': 'shelter'}, {'id': 10811, 'synset': 'shelter.n.05', 'name': 'shelter'}, {'id': 10812, 'synset': 'sheltered_workshop.n.01', 'name': 'sheltered_workshop'}, {'id': 10813, 'synset': 'sheraton.n.01', 'name': 'Sheraton'}, {'id': 10814, 'synset': 'shield.n.01', 'name': 'shield'}, {'id': 10815, 'synset': 'shielding.n.03', 'name': 'shielding'}, {'id': 10816, 'synset': 'shift_key.n.01', 'name': 'shift_key'}, {'id': 10817, 'synset': 'shillelagh.n.01', 'name': 'shillelagh'}, {'id': 10818, 'synset': 'shim.n.01', 'name': 'shim'}, {'id': 10819, 'synset': 'shingle.n.03', 'name': 'shingle'}, {'id': 10820, 'synset': 'shin_guard.n.01', 'name': 'shin_guard'}, {'id': 10821, 'synset': 'ship.n.01', 'name': 'ship'}, {'id': 10822, 'synset': 'shipboard_system.n.01', 'name': 'shipboard_system'}, {'id': 10823, 'synset': 'shipping.n.02', 'name': 'shipping'}, {'id': 10824, 'synset': 'shipping_room.n.01', 'name': 'shipping_room'}, {'id': 10825, 'synset': 'ship-towed_long-range_acoustic_detection_system.n.01', 'name': 'ship-towed_long-range_acoustic_detection_system'}, {'id': 10826, 'synset': 'shipwreck.n.01', 'name': 'shipwreck'}, {'id': 10827, 'synset': 'shirt_button.n.01', 'name': 'shirt_button'}, {'id': 10828, 'synset': 'shirtdress.n.01', 'name': 'shirtdress'}, {'id': 10829, 'synset': 'shirtfront.n.01', 'name': 'shirtfront'}, {'id': 10830, 'synset': 'shirting.n.01', 'name': 'shirting'}, {'id': 10831, 'synset': 'shirtsleeve.n.01', 'name': 'shirtsleeve'}, {'id': 10832, 'synset': 'shirttail.n.02', 'name': 'shirttail'}, {'id': 10833, 'synset': 'shirtwaist.n.01', 'name': 'shirtwaist'}, {'id': 10834, 'synset': 'shiv.n.01', 'name': 'shiv'}, {'id': 10835, 'synset': 'shock_absorber.n.01', 'name': 'shock_absorber'}, {'id': 10836, 'synset': 'shoe.n.02', 'name': 'shoe'}, {'id': 10837, 'synset': 'shoebox.n.02', 'name': 'shoebox'}, {'id': 10838, 'synset': 'shoehorn.n.01', 'name': 'shoehorn'}, {'id': 10839, 'synset': 'shoe_shop.n.01', 'name': 'shoe_shop'}, {'id': 10840, 'synset': 'shoetree.n.01', 'name': 'shoetree'}, {'id': 10841, 'synset': 'shofar.n.01', 'name': 'shofar'}, {'id': 10842, 'synset': 'shoji.n.01', 'name': 'shoji'}, {'id': 10843, 'synset': 'shooting_brake.n.01', 'name': 'shooting_brake'}, {'id': 10844, 'synset': 'shooting_lodge.n.01', 'name': 'shooting_lodge'}, {'id': 10845, 'synset': 'shooting_stick.n.01', 'name': 'shooting_stick'}, {'id': 10846, 'synset': 'shop.n.01', 'name': 'shop'}, {'id': 10847, 'synset': 'shop_bell.n.01', 'name': 'shop_bell'}, {'id': 10848, 'synset': 'shopping_basket.n.01', 'name': 'shopping_basket'}, {'id': 10849, 'synset': 'short_circuit.n.01', 'name': 'short_circuit'}, {'id': 10850, 'synset': 'short_iron.n.01', 'name': 'short_iron'}, {'id': 10851, 'synset': 'short_sleeve.n.01', 'name': 'short_sleeve'}, {'id': 10852, 'synset': 'shortwave_diathermy_machine.n.01', 'name': 'shortwave_diathermy_machine'}, {'id': 10853, 'synset': 'shot.n.12', 'name': 'shot'}, {'id': 10854, 'synset': 'shotgun.n.01', 'name': 'shotgun'}, {'id': 10855, 'synset': 'shotgun_shell.n.01', 'name': 'shotgun_shell'}, {'id': 10856, 'synset': 'shot_tower.n.01', 'name': 'shot_tower'}, {'id': 10857, 'synset': 'shoulder.n.04', 'name': 'shoulder'}, {'id': 10858, 'synset': 'shouldered_arch.n.01', 'name': 'shouldered_arch'}, {'id': 10859, 'synset': 'shoulder_holster.n.01', 'name': 'shoulder_holster'}, {'id': 10860, 'synset': 'shoulder_pad.n.01', 'name': 'shoulder_pad'}, {'id': 10861, 'synset': 'shoulder_patch.n.01', 'name': 'shoulder_patch'}, {'id': 10862, 'synset': 'shovel.n.03', 'name': 'shovel'}, {'id': 10863, 'synset': 'shovel_hat.n.01', 'name': 'shovel_hat'}, {'id': 10864, 'synset': 'showboat.n.01', 'name': 'showboat'}, {'id': 10865, 'synset': 'shower_room.n.01', 'name': 'shower_room'}, {'id': 10866, 'synset': 'shower_stall.n.01', 'name': 'shower_stall'}, {'id': 10867, 'synset': 'showroom.n.01', 'name': 'showroom'}, {'id': 10868, 'synset': 'shrapnel.n.01', 'name': 'shrapnel'}, {'id': 10869, 'synset': 'shrimper.n.01', 'name': 'shrimper'}, {'id': 10870, 'synset': 'shrine.n.01', 'name': 'shrine'}, {'id': 10871, 'synset': 'shrink-wrap.n.01', 'name': 'shrink-wrap'}, {'id': 10872, 'synset': 'shunt.n.03', 'name': 'shunt'}, {'id': 10873, 'synset': 'shunt.n.02', 'name': 'shunt'}, {'id': 10874, 'synset': 'shunter.n.01', 'name': 'shunter'}, {'id': 10875, 'synset': 'shutter.n.02', 'name': 'shutter'}, {'id': 10876, 'synset': 'shutter.n.01', 'name': 'shutter'}, {'id': 10877, 'synset': 'shuttle.n.03', 'name': 'shuttle'}, {'id': 10878, 'synset': 'shuttle.n.02', 'name': 'shuttle'}, {'id': 10879, 'synset': 'shuttle_bus.n.01', 'name': 'shuttle_bus'}, {'id': 10880, 'synset': 'shuttlecock.n.01', 'name': 'shuttlecock'}, {'id': 10881, 'synset': 'shuttle_helicopter.n.01', 'name': 'shuttle_helicopter'}, {'id': 10882, 'synset': 'sibley_tent.n.01', 'name': 'Sibley_tent'}, {'id': 10883, 'synset': 'sickbay.n.01', 'name': 'sickbay'}, {'id': 10884, 'synset': 'sickbed.n.01', 'name': 'sickbed'}, {'id': 10885, 'synset': 'sickle.n.01', 'name': 'sickle'}, {'id': 10886, 'synset': 'sickroom.n.01', 'name': 'sickroom'}, {'id': 10887, 'synset': 'sideboard.n.02', 'name': 'sideboard'}, {'id': 10888, 'synset': 'sidecar.n.02', 'name': 'sidecar'}, {'id': 10889, 'synset': 'side_chapel.n.01', 'name': 'side_chapel'}, {'id': 10890, 'synset': 'sidelight.n.01', 'name': 'sidelight'}, {'id': 10891, 'synset': 'sidesaddle.n.01', 'name': 'sidesaddle'}, {'id': 10892, 'synset': 'sidewalk.n.01', 'name': 'sidewalk'}, {'id': 10893, 'synset': 'sidewall.n.02', 'name': 'sidewall'}, {'id': 10894, 'synset': 'side-wheeler.n.01', 'name': 'side-wheeler'}, {'id': 10895, 'synset': 'sidewinder.n.02', 'name': 'sidewinder'}, {'id': 10896, 'synset': 'sieve.n.01', 'name': 'sieve'}, {'id': 10897, 'synset': 'sifter.n.01', 'name': 'sifter'}, {'id': 10898, 'synset': 'sights.n.01', 'name': 'sights'}, {'id': 10899, 'synset': 'sigmoidoscope.n.01', 'name': 'sigmoidoscope'}, {'id': 10900, 'synset': 'signal_box.n.01', 'name': 'signal_box'}, {'id': 10901, 'synset': 'signaling_device.n.01', 'name': 'signaling_device'}, {'id': 10902, 'synset': 'silencer.n.02', 'name': 'silencer'}, {'id': 10903, 'synset': 'silent_butler.n.01', 'name': 'silent_butler'}, {'id': 10904, 'synset': 'silex.n.02', 'name': 'Silex'}, {'id': 10905, 'synset': 'silk.n.01', 'name': 'silk'}, {'id': 10906, 'synset': 'silks.n.01', 'name': 'silks'}, {'id': 10907, 'synset': 'silver_plate.n.02', 'name': 'silver_plate'}, {'id': 10908, 'synset': 'silverpoint.n.01', 'name': 'silverpoint'}, {'id': 10909, 'synset': 'simple_pendulum.n.01', 'name': 'simple_pendulum'}, {'id': 10910, 'synset': 'simulator.n.01', 'name': 'simulator'}, {'id': 10911, 'synset': 'single_bed.n.01', 'name': 'single_bed'}, {'id': 10912, 'synset': 'single-breasted_jacket.n.01', 'name': 'single-breasted_jacket'}, {'id': 10913, 'synset': 'single-breasted_suit.n.01', 'name': 'single-breasted_suit'}, {'id': 10914, 'synset': 'single_prop.n.01', 'name': 'single_prop'}, {'id': 10915, 'synset': 'single-reed_instrument.n.01', 'name': 'single-reed_instrument'}, {'id': 10916, 'synset': 'single-rotor_helicopter.n.01', 'name': 'single-rotor_helicopter'}, {'id': 10917, 'synset': 'singlestick.n.01', 'name': 'singlestick'}, {'id': 10918, 'synset': 'singlet.n.01', 'name': 'singlet'}, {'id': 10919, 'synset': 'siren.n.04', 'name': 'siren'}, {'id': 10920, 'synset': 'sister_ship.n.01', 'name': 'sister_ship'}, {'id': 10921, 'synset': 'sitar.n.01', 'name': 'sitar'}, {'id': 10922, 'synset': 'sitz_bath.n.01', 'name': 'sitz_bath'}, {'id': 10923, 'synset': 'six-pack.n.01', 'name': 'six-pack'}, {'id': 10924, 'synset': 'skate.n.01', 'name': 'skate'}, {'id': 10925, 'synset': 'skeg.n.01', 'name': 'skeg'}, {'id': 10926, 'synset': 'skein.n.01', 'name': 'skein'}, {'id': 10927, 'synset': 'skeleton.n.04', 'name': 'skeleton'}, {'id': 10928, 'synset': 'skeleton_key.n.01', 'name': 'skeleton_key'}, {'id': 10929, 'synset': 'skep.n.02', 'name': 'skep'}, {'id': 10930, 'synset': 'skep.n.01', 'name': 'skep'}, {'id': 10931, 'synset': 'sketch.n.01', 'name': 'sketch'}, {'id': 10932, 'synset': 'sketcher.n.02', 'name': 'sketcher'}, {'id': 10933, 'synset': 'skew_arch.n.01', 'name': 'skew_arch'}, {'id': 10934, 'synset': 'ski_binding.n.01', 'name': 'ski_binding'}, {'id': 10935, 'synset': 'skibob.n.01', 'name': 'skibob'}, {'id': 10936, 'synset': 'ski_cap.n.01', 'name': 'ski_cap'}, {'id': 10937, 'synset': 'skidder.n.03', 'name': 'skidder'}, {'id': 10938, 'synset': 'skid_lid.n.01', 'name': 'skid_lid'}, {'id': 10939, 'synset': 'skiff.n.01', 'name': 'skiff'}, {'id': 10940, 'synset': 'ski_jump.n.01', 'name': 'ski_jump'}, {'id': 10941, 'synset': 'ski_lodge.n.01', 'name': 'ski_lodge'}, {'id': 10942, 'synset': 'ski_mask.n.01', 'name': 'ski_mask'}, {'id': 10943, 'synset': 'skimmer.n.02', 'name': 'skimmer'}, {'id': 10944, 'synset': 'ski-plane.n.01', 'name': 'ski-plane'}, {'id': 10945, 'synset': 'ski_rack.n.01', 'name': 'ski_rack'}, {'id': 10946, 'synset': 'skirt.n.01', 'name': 'skirt'}, {'id': 10947, 'synset': 'ski_tow.n.01', 'name': 'ski_tow'}, {'id': 10948, 'synset': 'skivvies.n.01', 'name': 'Skivvies'}, {'id': 10949, 'synset': 'skybox.n.01', 'name': 'skybox'}, {'id': 10950, 'synset': 'skyhook.n.02', 'name': 'skyhook'}, {'id': 10951, 'synset': 'skylight.n.01', 'name': 'skylight'}, {'id': 10952, 'synset': 'skysail.n.01', 'name': 'skysail'}, {'id': 10953, 'synset': 'skyscraper.n.01', 'name': 'skyscraper'}, {'id': 10954, 'synset': 'skywalk.n.01', 'name': 'skywalk'}, {'id': 10955, 'synset': 'slacks.n.01', 'name': 'slacks'}, {'id': 10956, 'synset': 'slack_suit.n.01', 'name': 'slack_suit'}, {'id': 10957, 'synset': 'slasher.n.02', 'name': 'slasher'}, {'id': 10958, 'synset': 'slash_pocket.n.01', 'name': 'slash_pocket'}, {'id': 10959, 'synset': 'slat.n.01', 'name': 'slat'}, {'id': 10960, 'synset': 'slate.n.01', 'name': 'slate'}, {'id': 10961, 'synset': 'slate_pencil.n.01', 'name': 'slate_pencil'}, {'id': 10962, 'synset': 'slate_roof.n.01', 'name': 'slate_roof'}, {'id': 10963, 'synset': 'sleeper.n.07', 'name': 'sleeper'}, {'id': 10964, 'synset': 'sleeper.n.06', 'name': 'sleeper'}, {'id': 10965, 'synset': 'sleeping_car.n.01', 'name': 'sleeping_car'}, {'id': 10966, 'synset': 'sleeve.n.01', 'name': 'sleeve'}, {'id': 10967, 'synset': 'sleeve.n.02', 'name': 'sleeve'}, {'id': 10968, 'synset': 'sleigh_bed.n.01', 'name': 'sleigh_bed'}, {'id': 10969, 'synset': 'sleigh_bell.n.01', 'name': 'sleigh_bell'}, {'id': 10970, 'synset': 'slice_bar.n.01', 'name': 'slice_bar'}, {'id': 10971, 'synset': 'slicer.n.03', 'name': 'slicer'}, {'id': 10972, 'synset': 'slicer.n.02', 'name': 'slicer'}, {'id': 10973, 'synset': 'slide.n.04', 'name': 'slide'}, {'id': 10974, 'synset': 'slide_fastener.n.01', 'name': 'slide_fastener'}, {'id': 10975, 'synset': 'slide_projector.n.01', 'name': 'slide_projector'}, {'id': 10976, 'synset': 'slide_rule.n.01', 'name': 'slide_rule'}, {'id': 10977, 'synset': 'slide_valve.n.01', 'name': 'slide_valve'}, {'id': 10978, 'synset': 'sliding_door.n.01', 'name': 'sliding_door'}, {'id': 10979, 'synset': 'sliding_seat.n.01', 'name': 'sliding_seat'}, {'id': 10980, 'synset': 'sliding_window.n.01', 'name': 'sliding_window'}, {'id': 10981, 'synset': 'sling.n.04', 'name': 'sling'}, {'id': 10982, 'synset': 'slingback.n.01', 'name': 'slingback'}, {'id': 10983, 'synset': 'slinger_ring.n.01', 'name': 'slinger_ring'}, {'id': 10984, 'synset': 'slip_clutch.n.01', 'name': 'slip_clutch'}, {'id': 10985, 'synset': 'slipcover.n.01', 'name': 'slipcover'}, {'id': 10986, 'synset': 'slip-joint_pliers.n.01', 'name': 'slip-joint_pliers'}, {'id': 10987, 'synset': 'slipknot.n.01', 'name': 'slipknot'}, {'id': 10988, 'synset': 'slip-on.n.01', 'name': 'slip-on'}, {'id': 10989, 'synset': 'slip_ring.n.01', 'name': 'slip_ring'}, {'id': 10990, 'synset': 'slit_lamp.n.01', 'name': 'slit_lamp'}, {'id': 10991, 'synset': 'slit_trench.n.01', 'name': 'slit_trench'}, {'id': 10992, 'synset': 'sloop.n.01', 'name': 'sloop'}, {'id': 10993, 'synset': 'sloop_of_war.n.01', 'name': 'sloop_of_war'}, {'id': 10994, 'synset': 'slop_basin.n.01', 'name': 'slop_basin'}, {'id': 10995, 'synset': 'slop_pail.n.01', 'name': 'slop_pail'}, {'id': 10996, 'synset': 'slops.n.02', 'name': 'slops'}, {'id': 10997, 'synset': 'slopshop.n.01', 'name': 'slopshop'}, {'id': 10998, 'synset': 'slot.n.07', 'name': 'slot'}, {'id': 10999, 'synset': 'slot_machine.n.01', 'name': 'slot_machine'}, {'id': 11000, 'synset': 'sluice.n.01', 'name': 'sluice'}, {'id': 11001, 'synset': 'smack.n.03', 'name': 'smack'}, {'id': 11002, 'synset': 'small_boat.n.01', 'name': 'small_boat'}, {'id': 11003, 'synset': 'small_computer_system_interface.n.01', 'name': 'small_computer_system_interface'}, {'id': 11004, 'synset': 'small_ship.n.01', 'name': 'small_ship'}, {'id': 11005, 'synset': 'small_stores.n.01', 'name': 'small_stores'}, {'id': 11006, 'synset': 'smart_bomb.n.01', 'name': 'smart_bomb'}, {'id': 11007, 'synset': 'smelling_bottle.n.01', 'name': 'smelling_bottle'}, {'id': 11008, 'synset': 'smocking.n.01', 'name': 'smocking'}, {'id': 11009, 'synset': 'smoke_bomb.n.01', 'name': 'smoke_bomb'}, {'id': 11010, 'synset': 'smokehouse.n.01', 'name': 'smokehouse'}, {'id': 11011, 'synset': 'smoker.n.03', 'name': 'smoker'}, {'id': 11012, 'synset': 'smoke_screen.n.01', 'name': 'smoke_screen'}, {'id': 11013, 'synset': 'smoking_room.n.01', 'name': 'smoking_room'}, {'id': 11014, 'synset': 'smoothbore.n.01', 'name': 'smoothbore'}, {'id': 11015, 'synset': 'smooth_plane.n.01', 'name': 'smooth_plane'}, {'id': 11016, 'synset': 'snack_bar.n.01', 'name': 'snack_bar'}, {'id': 11017, 'synset': 'snaffle.n.01', 'name': 'snaffle'}, {'id': 11018, 'synset': 'snap.n.10', 'name': 'snap'}, {'id': 11019, 'synset': 'snap_brim.n.01', 'name': 'snap_brim'}, {'id': 11020, 'synset': 'snap-brim_hat.n.01', 'name': 'snap-brim_hat'}, {'id': 11021, 'synset': 'snare.n.05', 'name': 'snare'}, {'id': 11022, 'synset': 'snare_drum.n.01', 'name': 'snare_drum'}, {'id': 11023, 'synset': 'snatch_block.n.01', 'name': 'snatch_block'}, {'id': 11024, 'synset': 'snifter.n.01', 'name': 'snifter'}, {'id': 11025, 'synset': 'sniper_rifle.n.01', 'name': 'sniper_rifle'}, {'id': 11026, 'synset': 'snips.n.01', 'name': 'snips'}, {'id': 11027, 'synset': 'sno-cat.n.01', 'name': 'Sno-cat'}, {'id': 11028, 'synset': 'snood.n.01', 'name': 'snood'}, {'id': 11029, 'synset': 'snorkel.n.02', 'name': 'snorkel'}, {'id': 11030, 'synset': 'snorkel.n.01', 'name': 'snorkel'}, {'id': 11031, 'synset': 'snowbank.n.01', 'name': 'snowbank'}, {'id': 11032, 'synset': 'snowplow.n.01', 'name': 'snowplow'}, {'id': 11033, 'synset': 'snowshoe.n.01', 'name': 'snowshoe'}, {'id': 11034, 'synset': 'snowsuit.n.01', 'name': 'snowsuit'}, {'id': 11035, 'synset': 'snow_thrower.n.01', 'name': 'snow_thrower'}, {'id': 11036, 'synset': 'snuffbox.n.01', 'name': 'snuffbox'}, {'id': 11037, 'synset': 'snuffer.n.01', 'name': 'snuffer'}, {'id': 11038, 'synset': 'snuffers.n.01', 'name': 'snuffers'}, {'id': 11039, 'synset': 'soapbox.n.01', 'name': 'soapbox'}, {'id': 11040, 'synset': 'soap_dish.n.01', 'name': 'soap_dish'}, {'id': 11041, 'synset': 'soap_dispenser.n.01', 'name': 'soap_dispenser'}, {'id': 11042, 'synset': 'soap_pad.n.01', 'name': 'soap_pad'}, {'id': 11043, 'synset': 'socket.n.02', 'name': 'socket'}, {'id': 11044, 'synset': 'socket_wrench.n.01', 'name': 'socket_wrench'}, {'id': 11045, 'synset': 'socle.n.01', 'name': 'socle'}, {'id': 11046, 'synset': 'soda_can.n.01', 'name': 'soda_can'}, {'id': 11047, 'synset': 'soda_fountain.n.02', 'name': 'soda_fountain'}, {'id': 11048, 'synset': 'soda_fountain.n.01', 'name': 'soda_fountain'}, {'id': 11049, 'synset': 'sod_house.n.01', 'name': 'sod_house'}, {'id': 11050, 'synset': 'sodium-vapor_lamp.n.01', 'name': 'sodium-vapor_lamp'}, {'id': 11051, 'synset': 'soffit.n.01', 'name': 'soffit'}, {'id': 11052, 'synset': 'soft_pedal.n.01', 'name': 'soft_pedal'}, {'id': 11053, 'synset': 'soil_pipe.n.01', 'name': 'soil_pipe'}, {'id': 11054, 'synset': 'solar_cell.n.01', 'name': 'solar_cell'}, {'id': 11055, 'synset': 'solar_dish.n.01', 'name': 'solar_dish'}, {'id': 11056, 'synset': 'solar_heater.n.01', 'name': 'solar_heater'}, {'id': 11057, 'synset': 'solar_house.n.01', 'name': 'solar_house'}, {'id': 11058, 'synset': 'solar_telescope.n.01', 'name': 'solar_telescope'}, {'id': 11059, 'synset': 'solar_thermal_system.n.01', 'name': 'solar_thermal_system'}, {'id': 11060, 'synset': 'soldering_iron.n.01', 'name': 'soldering_iron'}, {'id': 11061, 'synset': 'solenoid.n.01', 'name': 'solenoid'}, {'id': 11062, 'synset': 'solleret.n.01', 'name': 'solleret'}, {'id': 11063, 'synset': 'sonic_depth_finder.n.01', 'name': 'sonic_depth_finder'}, {'id': 11064, 'synset': 'sonogram.n.01', 'name': 'sonogram'}, {'id': 11065, 'synset': 'sonograph.n.01', 'name': 'sonograph'}, {'id': 11066, 'synset': 'sorter.n.02', 'name': 'sorter'}, {'id': 11067, 'synset': 'souk.n.01', 'name': 'souk'}, {'id': 11068, 'synset': 'sound_bow.n.01', 'name': 'sound_bow'}, {'id': 11069, 'synset': 'soundbox.n.01', 'name': 'soundbox'}, {'id': 11070, 'synset': 'sound_camera.n.01', 'name': 'sound_camera'}, {'id': 11071, 'synset': 'sounder.n.01', 'name': 'sounder'}, {'id': 11072, 'synset': 'sound_film.n.01', 'name': 'sound_film'}, {'id': 11073, 'synset': 'sounding_board.n.02', 'name': 'sounding_board'}, {'id': 11074, 'synset': 'sounding_rocket.n.01', 'name': 'sounding_rocket'}, {'id': 11075, 'synset': 'sound_recording.n.01', 'name': 'sound_recording'}, {'id': 11076, 'synset': 'sound_spectrograph.n.01', 'name': 'sound_spectrograph'}, {'id': 11077, 'synset': 'soup_ladle.n.01', 'name': 'soup_ladle'}, {'id': 11078, 'synset': 'source_of_illumination.n.01', 'name': 'source_of_illumination'}, {'id': 11079, 'synset': 'sourdine.n.02', 'name': 'sourdine'}, {'id': 11080, 'synset': 'soutache.n.01', 'name': 'soutache'}, {'id': 11081, 'synset': 'soutane.n.01', 'name': 'soutane'}, {'id': 11082, 'synset': "sou'wester.n.02", 'name': "sou'wester"}, {'id': 11083, 'synset': 'soybean_future.n.01', 'name': 'soybean_future'}, {'id': 11084, 'synset': 'space_bar.n.01', 'name': 'space_bar'}, {'id': 11085, 'synset': 'space_capsule.n.01', 'name': 'space_capsule'}, {'id': 11086, 'synset': 'spacecraft.n.01', 'name': 'spacecraft'}, {'id': 11087, 'synset': 'space_heater.n.01', 'name': 'space_heater'}, {'id': 11088, 'synset': 'space_helmet.n.01', 'name': 'space_helmet'}, {'id': 11089, 'synset': 'space_rocket.n.01', 'name': 'space_rocket'}, {'id': 11090, 'synset': 'space_station.n.01', 'name': 'space_station'}, {'id': 11091, 'synset': 'spacesuit.n.01', 'name': 'spacesuit'}, {'id': 11092, 'synset': 'spade.n.02', 'name': 'spade'}, {'id': 11093, 'synset': 'spade_bit.n.01', 'name': 'spade_bit'}, {'id': 11094, 'synset': 'spaghetti_junction.n.01', 'name': 'spaghetti_junction'}, {'id': 11095, 'synset': 'spandau.n.01', 'name': 'Spandau'}, {'id': 11096, 'synset': 'spandex.n.01', 'name': 'spandex'}, {'id': 11097, 'synset': 'spandrel.n.01', 'name': 'spandrel'}, {'id': 11098, 'synset': 'spanker.n.02', 'name': 'spanker'}, {'id': 11099, 'synset': 'spar.n.02', 'name': 'spar'}, {'id': 11100, 'synset': 'sparge_pipe.n.01', 'name': 'sparge_pipe'}, {'id': 11101, 'synset': 'spark_arrester.n.02', 'name': 'spark_arrester'}, {'id': 11102, 'synset': 'spark_arrester.n.01', 'name': 'spark_arrester'}, {'id': 11103, 'synset': 'spark_chamber.n.01', 'name': 'spark_chamber'}, {'id': 11104, 'synset': 'spark_coil.n.01', 'name': 'spark_coil'}, {'id': 11105, 'synset': 'spark_gap.n.01', 'name': 'spark_gap'}, {'id': 11106, 'synset': 'spark_lever.n.01', 'name': 'spark_lever'}, {'id': 11107, 'synset': 'spark_plug.n.01', 'name': 'spark_plug'}, {'id': 11108, 'synset': 'sparkplug_wrench.n.01', 'name': 'sparkplug_wrench'}, {'id': 11109, 'synset': 'spark_transmitter.n.01', 'name': 'spark_transmitter'}, {'id': 11110, 'synset': 'spat.n.02', 'name': 'spat'}, {'id': 11111, 'synset': 'spatula.n.01', 'name': 'spatula'}, {'id': 11112, 'synset': 'speakerphone.n.01', 'name': 'speakerphone'}, {'id': 11113, 'synset': 'speaking_trumpet.n.01', 'name': 'speaking_trumpet'}, {'id': 11114, 'synset': 'spear.n.02', 'name': 'spear'}, {'id': 11115, 'synset': 'specialty_store.n.01', 'name': 'specialty_store'}, {'id': 11116, 'synset': 'specimen_bottle.n.01', 'name': 'specimen_bottle'}, {'id': 11117, 'synset': 'spectacle.n.02', 'name': 'spectacle'}, {'id': 11118, 'synset': 'spectator_pump.n.01', 'name': 'spectator_pump'}, {'id': 11119, 'synset': 'spectrograph.n.01', 'name': 'spectrograph'}, {'id': 11120, 'synset': 'spectrophotometer.n.01', 'name': 'spectrophotometer'}, {'id': 11121, 'synset': 'spectroscope.n.01', 'name': 'spectroscope'}, {'id': 11122, 'synset': 'speculum.n.02', 'name': 'speculum'}, {'id': 11123, 'synset': 'speedboat.n.01', 'name': 'speedboat'}, {'id': 11124, 'synset': 'speed_bump.n.01', 'name': 'speed_bump'}, {'id': 11125, 'synset': 'speedometer.n.01', 'name': 'speedometer'}, {'id': 11126, 'synset': 'speed_skate.n.01', 'name': 'speed_skate'}, {'id': 11127, 'synset': 'spherometer.n.01', 'name': 'spherometer'}, {'id': 11128, 'synset': 'sphygmomanometer.n.01', 'name': 'sphygmomanometer'}, {'id': 11129, 'synset': 'spicemill.n.01', 'name': 'spicemill'}, {'id': 11130, 'synset': 'spider.n.03', 'name': 'spider'}, {'id': 11131, 'synset': 'spider_web.n.01', 'name': 'spider_web'}, {'id': 11132, 'synset': 'spike.n.02', 'name': 'spike'}, {'id': 11133, 'synset': 'spike.n.11', 'name': 'spike'}, {'id': 11134, 'synset': 'spindle.n.04', 'name': 'spindle'}, {'id': 11135, 'synset': 'spindle.n.03', 'name': 'spindle'}, {'id': 11136, 'synset': 'spindle.n.02', 'name': 'spindle'}, {'id': 11137, 'synset': 'spin_dryer.n.01', 'name': 'spin_dryer'}, {'id': 11138, 'synset': 'spinet.n.02', 'name': 'spinet'}, {'id': 11139, 'synset': 'spinet.n.01', 'name': 'spinet'}, {'id': 11140, 'synset': 'spinnaker.n.01', 'name': 'spinnaker'}, {'id': 11141, 'synset': 'spinner.n.03', 'name': 'spinner'}, {'id': 11142, 'synset': 'spinning_frame.n.01', 'name': 'spinning_frame'}, {'id': 11143, 'synset': 'spinning_jenny.n.01', 'name': 'spinning_jenny'}, {'id': 11144, 'synset': 'spinning_machine.n.01', 'name': 'spinning_machine'}, {'id': 11145, 'synset': 'spinning_rod.n.01', 'name': 'spinning_rod'}, {'id': 11146, 'synset': 'spinning_wheel.n.01', 'name': 'spinning_wheel'}, {'id': 11147, 'synset': 'spiral_bandage.n.01', 'name': 'spiral_bandage'}, {'id': 11148, 'synset': 'spiral_ratchet_screwdriver.n.01', 'name': 'spiral_ratchet_screwdriver'}, {'id': 11149, 'synset': 'spiral_spring.n.01', 'name': 'spiral_spring'}, {'id': 11150, 'synset': 'spirit_lamp.n.01', 'name': 'spirit_lamp'}, {'id': 11151, 'synset': 'spirit_stove.n.01', 'name': 'spirit_stove'}, {'id': 11152, 'synset': 'spirometer.n.01', 'name': 'spirometer'}, {'id': 11153, 'synset': 'spit.n.03', 'name': 'spit'}, {'id': 11154, 'synset': 'spittoon.n.01', 'name': 'spittoon'}, {'id': 11155, 'synset': 'splashboard.n.02', 'name': 'splashboard'}, {'id': 11156, 'synset': 'splasher.n.01', 'name': 'splasher'}, {'id': 11157, 'synset': 'splice.n.01', 'name': 'splice'}, {'id': 11158, 'synset': 'splicer.n.03', 'name': 'splicer'}, {'id': 11159, 'synset': 'splint.n.02', 'name': 'splint'}, {'id': 11160, 'synset': 'split_rail.n.01', 'name': 'split_rail'}, {'id': 11161, 'synset': 'spode.n.02', 'name': 'Spode'}, {'id': 11162, 'synset': 'spoiler.n.05', 'name': 'spoiler'}, {'id': 11163, 'synset': 'spoiler.n.04', 'name': 'spoiler'}, {'id': 11164, 'synset': 'spoke.n.01', 'name': 'spoke'}, {'id': 11165, 'synset': 'spokeshave.n.01', 'name': 'spokeshave'}, {'id': 11166, 'synset': 'sponge_cloth.n.01', 'name': 'sponge_cloth'}, {'id': 11167, 'synset': 'sponge_mop.n.01', 'name': 'sponge_mop'}, {'id': 11168, 'synset': 'spoon.n.03', 'name': 'spoon'}, {'id': 11169, 'synset': 'spork.n.01', 'name': 'Spork'}, {'id': 11170, 'synset': 'sporran.n.01', 'name': 'sporran'}, {'id': 11171, 'synset': 'sport_kite.n.01', 'name': 'sport_kite'}, {'id': 11172, 'synset': 'sports_car.n.01', 'name': 'sports_car'}, {'id': 11173, 'synset': 'sports_equipment.n.01', 'name': 'sports_equipment'}, {'id': 11174, 'synset': 'sports_implement.n.01', 'name': 'sports_implement'}, {'id': 11175, 'synset': 'sport_utility.n.01', 'name': 'sport_utility'}, {'id': 11176, 'synset': 'spot.n.07', 'name': 'spot'}, {'id': 11177, 'synset': 'spot_weld.n.01', 'name': 'spot_weld'}, {'id': 11178, 'synset': 'spouter.n.02', 'name': 'spouter'}, {'id': 11179, 'synset': 'sprag.n.01', 'name': 'sprag'}, {'id': 11180, 'synset': 'spray_gun.n.01', 'name': 'spray_gun'}, {'id': 11181, 'synset': 'spray_paint.n.01', 'name': 'spray_paint'}, {'id': 11182, 'synset': 'spreader.n.01', 'name': 'spreader'}, {'id': 11183, 'synset': 'sprig.n.02', 'name': 'sprig'}, {'id': 11184, 'synset': 'spring.n.02', 'name': 'spring'}, {'id': 11185, 'synset': 'spring_balance.n.01', 'name': 'spring_balance'}, {'id': 11186, 'synset': 'springboard.n.01', 'name': 'springboard'}, {'id': 11187, 'synset': 'sprinkler.n.01', 'name': 'sprinkler'}, {'id': 11188, 'synset': 'sprinkler_system.n.01', 'name': 'sprinkler_system'}, {'id': 11189, 'synset': 'sprit.n.01', 'name': 'sprit'}, {'id': 11190, 'synset': 'spritsail.n.01', 'name': 'spritsail'}, {'id': 11191, 'synset': 'sprocket.n.02', 'name': 'sprocket'}, {'id': 11192, 'synset': 'sprocket.n.01', 'name': 'sprocket'}, {'id': 11193, 'synset': 'spun_yarn.n.01', 'name': 'spun_yarn'}, {'id': 11194, 'synset': 'spur.n.04', 'name': 'spur'}, {'id': 11195, 'synset': 'spur_gear.n.01', 'name': 'spur_gear'}, {'id': 11196, 'synset': 'sputnik.n.01', 'name': 'sputnik'}, {'id': 11197, 'synset': 'spy_satellite.n.01', 'name': 'spy_satellite'}, {'id': 11198, 'synset': 'squad_room.n.01', 'name': 'squad_room'}, {'id': 11199, 'synset': 'square.n.08', 'name': 'square'}, {'id': 11200, 'synset': 'square_knot.n.01', 'name': 'square_knot'}, {'id': 11201, 'synset': 'square-rigger.n.01', 'name': 'square-rigger'}, {'id': 11202, 'synset': 'square_sail.n.01', 'name': 'square_sail'}, {'id': 11203, 'synset': 'squash_ball.n.01', 'name': 'squash_ball'}, {'id': 11204, 'synset': 'squash_racket.n.01', 'name': 'squash_racket'}, {'id': 11205, 'synset': 'squawk_box.n.01', 'name': 'squawk_box'}, {'id': 11206, 'synset': 'squeegee.n.01', 'name': 'squeegee'}, {'id': 11207, 'synset': 'squeezer.n.01', 'name': 'squeezer'}, {'id': 11208, 'synset': 'squelch_circuit.n.01', 'name': 'squelch_circuit'}, {'id': 11209, 'synset': 'squinch.n.01', 'name': 'squinch'}, {'id': 11210, 'synset': 'stabilizer.n.03', 'name': 'stabilizer'}, {'id': 11211, 'synset': 'stabilizer.n.02', 'name': 'stabilizer'}, {'id': 11212, 'synset': 'stabilizer_bar.n.01', 'name': 'stabilizer_bar'}, {'id': 11213, 'synset': 'stable.n.01', 'name': 'stable'}, {'id': 11214, 'synset': 'stable_gear.n.01', 'name': 'stable_gear'}, {'id': 11215, 'synset': 'stabling.n.01', 'name': 'stabling'}, {'id': 11216, 'synset': 'stacks.n.02', 'name': 'stacks'}, {'id': 11217, 'synset': 'staddle.n.01', 'name': 'staddle'}, {'id': 11218, 'synset': 'stadium.n.01', 'name': 'stadium'}, {'id': 11219, 'synset': 'stage.n.03', 'name': 'stage'}, {'id': 11220, 'synset': 'stained-glass_window.n.01', 'name': 'stained-glass_window'}, {'id': 11221, 'synset': 'stair-carpet.n.01', 'name': 'stair-carpet'}, {'id': 11222, 'synset': 'stair-rod.n.01', 'name': 'stair-rod'}, {'id': 11223, 'synset': 'stairwell.n.01', 'name': 'stairwell'}, {'id': 11224, 'synset': 'stake.n.05', 'name': 'stake'}, {'id': 11225, 'synset': 'stall.n.03', 'name': 'stall'}, {'id': 11226, 'synset': 'stall.n.01', 'name': 'stall'}, {'id': 11227, 'synset': 'stamp.n.08', 'name': 'stamp'}, {'id': 11228, 'synset': 'stamp_mill.n.01', 'name': 'stamp_mill'}, {'id': 11229, 'synset': 'stamping_machine.n.01', 'name': 'stamping_machine'}, {'id': 11230, 'synset': 'stanchion.n.01', 'name': 'stanchion'}, {'id': 11231, 'synset': 'stand.n.04', 'name': 'stand'}, {'id': 11232, 'synset': 'standard.n.05', 'name': 'standard'}, {'id': 11233, 'synset': 'standard_cell.n.01', 'name': 'standard_cell'}, {'id': 11234, 'synset': 'standard_transmission.n.01', 'name': 'standard_transmission'}, {'id': 11235, 'synset': 'standing_press.n.01', 'name': 'standing_press'}, {'id': 11236, 'synset': 'stanhope.n.01', 'name': 'stanhope'}, {'id': 11237, 'synset': 'stanley_steamer.n.01', 'name': 'Stanley_Steamer'}, {'id': 11238, 'synset': 'staple.n.05', 'name': 'staple'}, {'id': 11239, 'synset': 'staple.n.04', 'name': 'staple'}, {'id': 11240, 'synset': 'staple_gun.n.01', 'name': 'staple_gun'}, {'id': 11241, 'synset': 'starship.n.01', 'name': 'starship'}, {'id': 11242, 'synset': 'starter.n.01', 'name': 'starter'}, {'id': 11243, 'synset': 'starting_gate.n.01', 'name': 'starting_gate'}, {'id': 11244, 'synset': 'stassano_furnace.n.01', 'name': 'Stassano_furnace'}, {'id': 11245, 'synset': 'statehouse.n.01', 'name': 'Statehouse'}, {'id': 11246, 'synset': 'stately_home.n.01', 'name': 'stately_home'}, {'id': 11247, 'synset': 'state_prison.n.01', 'name': 'state_prison'}, {'id': 11248, 'synset': 'stateroom.n.01', 'name': 'stateroom'}, {'id': 11249, 'synset': 'static_tube.n.01', 'name': 'static_tube'}, {'id': 11250, 'synset': 'station.n.01', 'name': 'station'}, {'id': 11251, 'synset': 'stator.n.01', 'name': 'stator'}, {'id': 11252, 'synset': 'stay.n.05', 'name': 'stay'}, {'id': 11253, 'synset': 'staysail.n.01', 'name': 'staysail'}, {'id': 11254, 'synset': 'steakhouse.n.01', 'name': 'steakhouse'}, {'id': 11255, 'synset': 'stealth_aircraft.n.01', 'name': 'stealth_aircraft'}, {'id': 11256, 'synset': 'stealth_bomber.n.01', 'name': 'stealth_bomber'}, {'id': 11257, 'synset': 'stealth_fighter.n.01', 'name': 'stealth_fighter'}, {'id': 11258, 'synset': 'steam_bath.n.01', 'name': 'steam_bath'}, {'id': 11259, 'synset': 'steamboat.n.01', 'name': 'steamboat'}, {'id': 11260, 'synset': 'steam_chest.n.01', 'name': 'steam_chest'}, {'id': 11261, 'synset': 'steam_engine.n.01', 'name': 'steam_engine'}, {'id': 11262, 'synset': 'steamer.n.03', 'name': 'steamer'}, {'id': 11263, 'synset': 'steamer.n.02', 'name': 'steamer'}, {'id': 11264, 'synset': 'steam_iron.n.01', 'name': 'steam_iron'}, {'id': 11265, 'synset': 'steam_locomotive.n.01', 'name': 'steam_locomotive'}, {'id': 11266, 'synset': 'steamroller.n.02', 'name': 'steamroller'}, {'id': 11267, 'synset': 'steam_shovel.n.01', 'name': 'steam_shovel'}, {'id': 11268, 'synset': 'steam_turbine.n.01', 'name': 'steam_turbine'}, {'id': 11269, 'synset': 'steam_whistle.n.01', 'name': 'steam_whistle'}, {'id': 11270, 'synset': 'steel.n.03', 'name': 'steel'}, {'id': 11271, 'synset': 'steel_arch_bridge.n.01', 'name': 'steel_arch_bridge'}, {'id': 11272, 'synset': 'steel_drum.n.01', 'name': 'steel_drum'}, {'id': 11273, 'synset': 'steel_mill.n.01', 'name': 'steel_mill'}, {'id': 11274, 'synset': 'steel-wool_pad.n.01', 'name': 'steel-wool_pad'}, {'id': 11275, 'synset': 'steelyard.n.01', 'name': 'steelyard'}, {'id': 11276, 'synset': 'steeple.n.01', 'name': 'steeple'}, {'id': 11277, 'synset': 'steerage.n.01', 'name': 'steerage'}, {'id': 11278, 'synset': 'steering_gear.n.01', 'name': 'steering_gear'}, {'id': 11279, 'synset': 'steering_linkage.n.01', 'name': 'steering_linkage'}, {'id': 11280, 'synset': 'steering_system.n.01', 'name': 'steering_system'}, {'id': 11281, 'synset': 'stele.n.02', 'name': 'stele'}, {'id': 11282, 'synset': 'stem-winder.n.01', 'name': 'stem-winder'}, {'id': 11283, 'synset': 'stencil.n.01', 'name': 'stencil'}, {'id': 11284, 'synset': 'sten_gun.n.01', 'name': 'Sten_gun'}, {'id': 11285, 'synset': 'stenograph.n.02', 'name': 'stenograph'}, {'id': 11286, 'synset': 'step.n.04', 'name': 'step'}, {'id': 11287, 'synset': 'step-down_transformer.n.01', 'name': 'step-down_transformer'}, {'id': 11288, 'synset': 'step-up_transformer.n.01', 'name': 'step-up_transformer'}, {'id': 11289, 'synset': 'stereoscope.n.01', 'name': 'stereoscope'}, {'id': 11290, 'synset': 'stern_chaser.n.01', 'name': 'stern_chaser'}, {'id': 11291, 'synset': 'sternpost.n.01', 'name': 'sternpost'}, {'id': 11292, 'synset': 'sternwheeler.n.01', 'name': 'sternwheeler'}, {'id': 11293, 'synset': 'stethoscope.n.01', 'name': 'stethoscope'}, {'id': 11294, 'synset': 'stewing_pan.n.01', 'name': 'stewing_pan'}, {'id': 11295, 'synset': 'stick.n.01', 'name': 'stick'}, {'id': 11296, 'synset': 'stick.n.07', 'name': 'stick'}, {'id': 11297, 'synset': 'stick.n.03', 'name': 'stick'}, {'id': 11298, 'synset': 'stick.n.06', 'name': 'stick'}, {'id': 11299, 'synset': 'stile.n.01', 'name': 'stile'}, {'id': 11300, 'synset': 'stiletto.n.01', 'name': 'stiletto'}, {'id': 11301, 'synset': 'still.n.03', 'name': 'still'}, {'id': 11302, 'synset': 'stillroom.n.01', 'name': 'stillroom'}, {'id': 11303, 'synset': 'stillson_wrench.n.01', 'name': 'Stillson_wrench'}, {'id': 11304, 'synset': 'stilt.n.02', 'name': 'stilt'}, {'id': 11305, 'synset': 'stinger.n.03', 'name': 'Stinger'}, {'id': 11306, 'synset': 'stink_bomb.n.01', 'name': 'stink_bomb'}, {'id': 11307, 'synset': 'stirrup_pump.n.01', 'name': 'stirrup_pump'}, {'id': 11308, 'synset': 'stob.n.01', 'name': 'stob'}, {'id': 11309, 'synset': 'stock.n.03', 'name': 'stock'}, {'id': 11310, 'synset': 'stockade.n.01', 'name': 'stockade'}, {'id': 11311, 'synset': 'stockcar.n.01', 'name': 'stockcar'}, {'id': 11312, 'synset': 'stock_car.n.02', 'name': 'stock_car'}, {'id': 11313, 'synset': 'stockinet.n.01', 'name': 'stockinet'}, {'id': 11314, 'synset': 'stocking.n.01', 'name': 'stocking'}, {'id': 11315, 'synset': 'stock-in-trade.n.01', 'name': 'stock-in-trade'}, {'id': 11316, 'synset': 'stockpot.n.01', 'name': 'stockpot'}, {'id': 11317, 'synset': 'stockroom.n.01', 'name': 'stockroom'}, {'id': 11318, 'synset': 'stocks.n.03', 'name': 'stocks'}, {'id': 11319, 'synset': 'stock_saddle.n.01', 'name': 'stock_saddle'}, {'id': 11320, 'synset': 'stockyard.n.01', 'name': 'stockyard'}, {'id': 11321, 'synset': 'stole.n.01', 'name': 'stole'}, {'id': 11322, 'synset': 'stomacher.n.01', 'name': 'stomacher'}, {'id': 11323, 'synset': 'stomach_pump.n.01', 'name': 'stomach_pump'}, {'id': 11324, 'synset': 'stone_wall.n.01', 'name': 'stone_wall'}, {'id': 11325, 'synset': 'stoneware.n.01', 'name': 'stoneware'}, {'id': 11326, 'synset': 'stonework.n.01', 'name': 'stonework'}, {'id': 11327, 'synset': 'stoop.n.03', 'name': 'stoop'}, {'id': 11328, 'synset': 'stop_bath.n.01', 'name': 'stop_bath'}, {'id': 11329, 'synset': 'stopcock.n.01', 'name': 'stopcock'}, {'id': 11330, 'synset': 'stopper_knot.n.01', 'name': 'stopper_knot'}, {'id': 11331, 'synset': 'stopwatch.n.01', 'name': 'stopwatch'}, {'id': 11332, 'synset': 'storage_battery.n.01', 'name': 'storage_battery'}, {'id': 11333, 'synset': 'storage_cell.n.01', 'name': 'storage_cell'}, {'id': 11334, 'synset': 'storage_ring.n.01', 'name': 'storage_ring'}, {'id': 11335, 'synset': 'storage_space.n.01', 'name': 'storage_space'}, {'id': 11336, 'synset': 'storeroom.n.01', 'name': 'storeroom'}, {'id': 11337, 'synset': 'storm_cellar.n.01', 'name': 'storm_cellar'}, {'id': 11338, 'synset': 'storm_door.n.01', 'name': 'storm_door'}, {'id': 11339, 'synset': 'storm_window.n.01', 'name': 'storm_window'}, {'id': 11340, 'synset': 'stoup.n.02', 'name': 'stoup'}, {'id': 11341, 'synset': 'stoup.n.01', 'name': 'stoup'}, {'id': 11342, 'synset': 'stove.n.02', 'name': 'stove'}, {'id': 11343, 'synset': 'stove_bolt.n.01', 'name': 'stove_bolt'}, {'id': 11344, 'synset': 'stovepipe.n.01', 'name': 'stovepipe'}, {'id': 11345, 'synset': 'stovepipe_iron.n.01', 'name': 'stovepipe_iron'}, {'id': 11346, 'synset': 'stradavarius.n.01', 'name': 'Stradavarius'}, {'id': 11347, 'synset': 'straight_chair.n.01', 'name': 'straight_chair'}, {'id': 11348, 'synset': 'straightedge.n.01', 'name': 'straightedge'}, {'id': 11349, 'synset': 'straightener.n.01', 'name': 'straightener'}, {'id': 11350, 'synset': 'straight_flute.n.01', 'name': 'straight_flute'}, {'id': 11351, 'synset': 'straight_pin.n.01', 'name': 'straight_pin'}, {'id': 11352, 'synset': 'straight_razor.n.01', 'name': 'straight_razor'}, {'id': 11353, 'synset': 'straitjacket.n.02', 'name': 'straitjacket'}, {'id': 11354, 'synset': 'strap.n.04', 'name': 'strap'}, {'id': 11355, 'synset': 'strap_hinge.n.01', 'name': 'strap_hinge'}, {'id': 11356, 'synset': 'strapless.n.01', 'name': 'strapless'}, {'id': 11357, 'synset': 'streamer_fly.n.01', 'name': 'streamer_fly'}, {'id': 11358, 'synset': 'streamliner.n.01', 'name': 'streamliner'}, {'id': 11359, 'synset': 'street.n.01', 'name': 'street'}, {'id': 11360, 'synset': 'street.n.02', 'name': 'street'}, {'id': 11361, 'synset': 'streetcar.n.01', 'name': 'streetcar'}, {'id': 11362, 'synset': 'street_clothes.n.01', 'name': 'street_clothes'}, {'id': 11363, 'synset': 'stretcher.n.03', 'name': 'stretcher'}, {'id': 11364, 'synset': 'stretcher.n.01', 'name': 'stretcher'}, {'id': 11365, 'synset': 'stretch_pants.n.01', 'name': 'stretch_pants'}, {'id': 11366, 'synset': 'strickle.n.02', 'name': 'strickle'}, {'id': 11367, 'synset': 'strickle.n.01', 'name': 'strickle'}, {'id': 11368, 'synset': 'stringed_instrument.n.01', 'name': 'stringed_instrument'}, {'id': 11369, 'synset': 'stringer.n.04', 'name': 'stringer'}, {'id': 11370, 'synset': 'stringer.n.03', 'name': 'stringer'}, {'id': 11371, 'synset': 'string_tie.n.01', 'name': 'string_tie'}, {'id': 11372, 'synset': 'strip.n.05', 'name': 'strip'}, {'id': 11373, 'synset': 'strip_lighting.n.01', 'name': 'strip_lighting'}, {'id': 11374, 'synset': 'strip_mall.n.01', 'name': 'strip_mall'}, {'id': 11375, 'synset': 'stroboscope.n.01', 'name': 'stroboscope'}, {'id': 11376, 'synset': 'strongbox.n.01', 'name': 'strongbox'}, {'id': 11377, 'synset': 'stronghold.n.01', 'name': 'stronghold'}, {'id': 11378, 'synset': 'strongroom.n.01', 'name': 'strongroom'}, {'id': 11379, 'synset': 'strop.n.01', 'name': 'strop'}, {'id': 11380, 'synset': 'structural_member.n.01', 'name': 'structural_member'}, {'id': 11381, 'synset': 'structure.n.01', 'name': 'structure'}, {'id': 11382, 'synset': 'student_center.n.01', 'name': 'student_center'}, {'id': 11383, 'synset': 'student_lamp.n.01', 'name': 'student_lamp'}, {'id': 11384, 'synset': 'student_union.n.01', 'name': 'student_union'}, {'id': 11385, 'synset': 'stud_finder.n.01', 'name': 'stud_finder'}, {'id': 11386, 'synset': 'studio_apartment.n.01', 'name': 'studio_apartment'}, {'id': 11387, 'synset': 'studio_couch.n.01', 'name': 'studio_couch'}, {'id': 11388, 'synset': 'study.n.05', 'name': 'study'}, {'id': 11389, 'synset': 'study_hall.n.02', 'name': 'study_hall'}, {'id': 11390, 'synset': 'stuffing_nut.n.01', 'name': 'stuffing_nut'}, {'id': 11391, 'synset': 'stump.n.03', 'name': 'stump'}, {'id': 11392, 'synset': 'stun_gun.n.01', 'name': 'stun_gun'}, {'id': 11393, 'synset': 'stupa.n.01', 'name': 'stupa'}, {'id': 11394, 'synset': 'sty.n.02', 'name': 'sty'}, {'id': 11395, 'synset': 'stylus.n.01', 'name': 'stylus'}, {'id': 11396, 'synset': 'sub-assembly.n.01', 'name': 'sub-assembly'}, {'id': 11397, 'synset': 'subcompact.n.01', 'name': 'subcompact'}, {'id': 11398, 'synset': 'submachine_gun.n.01', 'name': 'submachine_gun'}, {'id': 11399, 'synset': 'submarine.n.01', 'name': 'submarine'}, {'id': 11400, 'synset': 'submarine_torpedo.n.01', 'name': 'submarine_torpedo'}, {'id': 11401, 'synset': 'submersible.n.02', 'name': 'submersible'}, {'id': 11402, 'synset': 'submersible.n.01', 'name': 'submersible'}, {'id': 11403, 'synset': 'subtracter.n.02', 'name': 'subtracter'}, {'id': 11404, 'synset': 'subway_token.n.01', 'name': 'subway_token'}, {'id': 11405, 'synset': 'subway_train.n.01', 'name': 'subway_train'}, {'id': 11406, 'synset': 'suction_cup.n.01', 'name': 'suction_cup'}, {'id': 11407, 'synset': 'suction_pump.n.01', 'name': 'suction_pump'}, {'id': 11408, 'synset': 'sudatorium.n.01', 'name': 'sudatorium'}, {'id': 11409, 'synset': 'suede_cloth.n.01', 'name': 'suede_cloth'}, {'id': 11410, 'synset': 'sugar_refinery.n.01', 'name': 'sugar_refinery'}, {'id': 11411, 'synset': 'sugar_spoon.n.01', 'name': 'sugar_spoon'}, {'id': 11412, 'synset': 'suite.n.02', 'name': 'suite'}, {'id': 11413, 'synset': 'suiting.n.01', 'name': 'suiting'}, {'id': 11414, 'synset': 'sulky.n.01', 'name': 'sulky'}, {'id': 11415, 'synset': 'summer_house.n.01', 'name': 'summer_house'}, {'id': 11416, 'synset': 'sumo_ring.n.01', 'name': 'sumo_ring'}, {'id': 11417, 'synset': 'sump.n.01', 'name': 'sump'}, {'id': 11418, 'synset': 'sump_pump.n.01', 'name': 'sump_pump'}, {'id': 11419, 'synset': 'sunbonnet.n.01', 'name': 'sunbonnet'}, {'id': 11420, 'synset': 'sunday_best.n.01', 'name': 'Sunday_best'}, {'id': 11421, 'synset': 'sun_deck.n.01', 'name': 'sun_deck'}, {'id': 11422, 'synset': 'sundial.n.01', 'name': 'sundial'}, {'id': 11423, 'synset': 'sundress.n.01', 'name': 'sundress'}, {'id': 11424, 'synset': 'sundries.n.01', 'name': 'sundries'}, {'id': 11425, 'synset': 'sun_gear.n.01', 'name': 'sun_gear'}, {'id': 11426, 'synset': 'sunglass.n.01', 'name': 'sunglass'}, {'id': 11427, 'synset': 'sunlamp.n.01', 'name': 'sunlamp'}, {'id': 11428, 'synset': 'sun_parlor.n.01', 'name': 'sun_parlor'}, {'id': 11429, 'synset': 'sunroof.n.01', 'name': 'sunroof'}, {'id': 11430, 'synset': 'sunscreen.n.01', 'name': 'sunscreen'}, {'id': 11431, 'synset': 'sunsuit.n.01', 'name': 'sunsuit'}, {'id': 11432, 'synset': 'supercharger.n.01', 'name': 'supercharger'}, {'id': 11433, 'synset': 'supercomputer.n.01', 'name': 'supercomputer'}, {'id': 11434, 'synset': 'superconducting_supercollider.n.01', 'name': 'superconducting_supercollider'}, {'id': 11435, 'synset': 'superhighway.n.02', 'name': 'superhighway'}, {'id': 11436, 'synset': 'supermarket.n.01', 'name': 'supermarket'}, {'id': 11437, 'synset': 'superstructure.n.01', 'name': 'superstructure'}, {'id': 11438, 'synset': 'supertanker.n.01', 'name': 'supertanker'}, {'id': 11439, 'synset': 'supper_club.n.01', 'name': 'supper_club'}, {'id': 11440, 'synset': 'supplejack.n.01', 'name': 'supplejack'}, {'id': 11441, 'synset': 'supply_chamber.n.01', 'name': 'supply_chamber'}, {'id': 11442, 'synset': 'supply_closet.n.01', 'name': 'supply_closet'}, {'id': 11443, 'synset': 'support.n.10', 'name': 'support'}, {'id': 11444, 'synset': 'support.n.07', 'name': 'support'}, {'id': 11445, 'synset': 'support_column.n.01', 'name': 'support_column'}, {'id': 11446, 'synset': 'support_hose.n.01', 'name': 'support_hose'}, {'id': 11447, 'synset': 'supporting_structure.n.01', 'name': 'supporting_structure'}, {'id': 11448, 'synset': 'supporting_tower.n.01', 'name': 'supporting_tower'}, {'id': 11449, 'synset': 'surcoat.n.02', 'name': 'surcoat'}, {'id': 11450, 'synset': 'surface_gauge.n.01', 'name': 'surface_gauge'}, {'id': 11451, 'synset': 'surface_lift.n.01', 'name': 'surface_lift'}, {'id': 11452, 'synset': 'surface_search_radar.n.01', 'name': 'surface_search_radar'}, {'id': 11453, 'synset': 'surface_ship.n.01', 'name': 'surface_ship'}, {'id': 11454, 'synset': 'surface-to-air_missile.n.01', 'name': 'surface-to-air_missile'}, {'id': 11455, 'synset': 'surface-to-air_missile_system.n.01', 'name': 'surface-to-air_missile_system'}, {'id': 11456, 'synset': 'surfboat.n.01', 'name': 'surfboat'}, {'id': 11457, 'synset': 'surcoat.n.01', 'name': 'surcoat'}, {'id': 11458, 'synset': "surgeon's_knot.n.01", 'name': "surgeon's_knot"}, {'id': 11459, 'synset': 'surgery.n.02', 'name': 'surgery'}, {'id': 11460, 'synset': 'surge_suppressor.n.01', 'name': 'surge_suppressor'}, {'id': 11461, 'synset': 'surgical_dressing.n.01', 'name': 'surgical_dressing'}, {'id': 11462, 'synset': 'surgical_instrument.n.01', 'name': 'surgical_instrument'}, {'id': 11463, 'synset': 'surgical_knife.n.01', 'name': 'surgical_knife'}, {'id': 11464, 'synset': 'surplice.n.01', 'name': 'surplice'}, {'id': 11465, 'synset': 'surrey.n.02', 'name': 'surrey'}, {'id': 11466, 'synset': 'surtout.n.01', 'name': 'surtout'}, {'id': 11467, 'synset': 'surveillance_system.n.01', 'name': 'surveillance_system'}, {'id': 11468, 'synset': 'surveying_instrument.n.01', 'name': 'surveying_instrument'}, {'id': 11469, 'synset': "surveyor's_level.n.01", 'name': "surveyor's_level"}, {'id': 11470, 'synset': 'sushi_bar.n.01', 'name': 'sushi_bar'}, {'id': 11471, 'synset': 'suspension.n.05', 'name': 'suspension'}, {'id': 11472, 'synset': 'suspension_bridge.n.01', 'name': 'suspension_bridge'}, {'id': 11473, 'synset': 'suspensory.n.01', 'name': 'suspensory'}, {'id': 11474, 'synset': 'sustaining_pedal.n.01', 'name': 'sustaining_pedal'}, {'id': 11475, 'synset': 'suture.n.02', 'name': 'suture'}, {'id': 11476, 'synset': 'swab.n.01', 'name': 'swab'}, {'id': 11477, 'synset': 'swaddling_clothes.n.01', 'name': 'swaddling_clothes'}, {'id': 11478, 'synset': 'swag.n.03', 'name': 'swag'}, {'id': 11479, 'synset': 'swage_block.n.01', 'name': 'swage_block'}, {'id': 11480, 'synset': 'swagger_stick.n.01', 'name': 'swagger_stick'}, {'id': 11481, 'synset': 'swallow-tailed_coat.n.01', 'name': 'swallow-tailed_coat'}, {'id': 11482, 'synset': 'swamp_buggy.n.01', 'name': 'swamp_buggy'}, {'id': 11483, 'synset': "swan's_down.n.01", 'name': "swan's_down"}, {'id': 11484, 'synset': 'swathe.n.01', 'name': 'swathe'}, {'id': 11485, 'synset': 'swatter.n.01', 'name': 'swatter'}, {'id': 11486, 'synset': 'sweat_bag.n.01', 'name': 'sweat_bag'}, {'id': 11487, 'synset': 'sweatband.n.01', 'name': 'sweatband'}, {'id': 11488, 'synset': 'sweatshop.n.01', 'name': 'sweatshop'}, {'id': 11489, 'synset': 'sweat_suit.n.01', 'name': 'sweat_suit'}, {'id': 11490, 'synset': 'sweep.n.04', 'name': 'sweep'}, {'id': 11491, 'synset': 'sweep_hand.n.01', 'name': 'sweep_hand'}, {'id': 11492, 'synset': 'swimming_trunks.n.01', 'name': 'swimming_trunks'}, {'id': 11493, 'synset': 'swing.n.02', 'name': 'swing'}, {'id': 11494, 'synset': 'swing_door.n.01', 'name': 'swing_door'}, {'id': 11495, 'synset': 'switch.n.01', 'name': 'switch'}, {'id': 11496, 'synset': 'switchblade.n.01', 'name': 'switchblade'}, {'id': 11497, 'synset': 'switch_engine.n.01', 'name': 'switch_engine'}, {'id': 11498, 'synset': 'swivel.n.01', 'name': 'swivel'}, {'id': 11499, 'synset': 'swivel_chair.n.01', 'name': 'swivel_chair'}, {'id': 11500, 'synset': 'swizzle_stick.n.01', 'name': 'swizzle_stick'}, {'id': 11501, 'synset': 'sword_cane.n.01', 'name': 'sword_cane'}, {'id': 11502, 'synset': 's_wrench.n.01', 'name': 'S_wrench'}, {'id': 11503, 'synset': 'synagogue.n.01', 'name': 'synagogue'}, {'id': 11504, 'synset': 'synchrocyclotron.n.01', 'name': 'synchrocyclotron'}, {'id': 11505, 'synset': 'synchroflash.n.01', 'name': 'synchroflash'}, {'id': 11506, 'synset': 'synchromesh.n.01', 'name': 'synchromesh'}, {'id': 11507, 'synset': 'synchronous_converter.n.01', 'name': 'synchronous_converter'}, {'id': 11508, 'synset': 'synchronous_motor.n.01', 'name': 'synchronous_motor'}, {'id': 11509, 'synset': 'synchrotron.n.01', 'name': 'synchrotron'}, {'id': 11510, 'synset': 'synchroscope.n.01', 'name': 'synchroscope'}, {'id': 11511, 'synset': 'synthesizer.n.02', 'name': 'synthesizer'}, {'id': 11512, 'synset': 'system.n.01', 'name': 'system'}, {'id': 11513, 'synset': 'tabard.n.01', 'name': 'tabard'}, {'id': 11514, 'synset': 'tabernacle.n.02', 'name': 'Tabernacle'}, {'id': 11515, 'synset': 'tabi.n.01', 'name': 'tabi'}, {'id': 11516, 'synset': 'tab_key.n.01', 'name': 'tab_key'}, {'id': 11517, 'synset': 'table.n.03', 'name': 'table'}, {'id': 11518, 'synset': 'tablefork.n.01', 'name': 'tablefork'}, {'id': 11519, 'synset': 'table_knife.n.01', 'name': 'table_knife'}, {'id': 11520, 'synset': 'table_saw.n.01', 'name': 'table_saw'}, {'id': 11521, 'synset': 'tablespoon.n.02', 'name': 'tablespoon'}, {'id': 11522, 'synset': 'tablet-armed_chair.n.01', 'name': 'tablet-armed_chair'}, {'id': 11523, 'synset': 'table-tennis_racquet.n.01', 'name': 'table-tennis_racquet'}, {'id': 11524, 'synset': 'tabletop.n.01', 'name': 'tabletop'}, {'id': 11525, 'synset': 'tableware.n.01', 'name': 'tableware'}, {'id': 11526, 'synset': 'tabor.n.01', 'name': 'tabor'}, {'id': 11527, 'synset': 'taboret.n.01', 'name': 'taboret'}, {'id': 11528, 'synset': 'tachistoscope.n.01', 'name': 'tachistoscope'}, {'id': 11529, 'synset': 'tachograph.n.01', 'name': 'tachograph'}, {'id': 11530, 'synset': 'tachymeter.n.01', 'name': 'tachymeter'}, {'id': 11531, 'synset': 'tack.n.02', 'name': 'tack'}, {'id': 11532, 'synset': 'tack_hammer.n.01', 'name': 'tack_hammer'}, {'id': 11533, 'synset': 'taffeta.n.01', 'name': 'taffeta'}, {'id': 11534, 'synset': 'taffrail.n.01', 'name': 'taffrail'}, {'id': 11535, 'synset': 'tailgate.n.01', 'name': 'tailgate'}, {'id': 11536, 'synset': 'tailor-made.n.01', 'name': 'tailor-made'}, {'id': 11537, 'synset': "tailor's_chalk.n.01", 'name': "tailor's_chalk"}, {'id': 11538, 'synset': 'tailpipe.n.01', 'name': 'tailpipe'}, {'id': 11539, 'synset': 'tail_rotor.n.01', 'name': 'tail_rotor'}, {'id': 11540, 'synset': 'tailstock.n.01', 'name': 'tailstock'}, {'id': 11541, 'synset': 'take-up.n.01', 'name': 'take-up'}, {'id': 11542, 'synset': 'talaria.n.01', 'name': 'talaria'}, {'id': 11543, 'synset': 'talcum.n.02', 'name': 'talcum'}, {'id': 11544, 'synset': 'tam.n.01', 'name': 'tam'}, {'id': 11545, 'synset': 'tambour.n.02', 'name': 'tambour'}, {'id': 11546, 'synset': 'tambour.n.01', 'name': 'tambour'}, {'id': 11547, 'synset': 'tammy.n.01', 'name': 'tammy'}, {'id': 11548, 'synset': 'tamp.n.01', 'name': 'tamp'}, {'id': 11549, 'synset': 'tampax.n.01', 'name': 'Tampax'}, {'id': 11550, 'synset': 'tampion.n.01', 'name': 'tampion'}, {'id': 11551, 'synset': 'tampon.n.01', 'name': 'tampon'}, {'id': 11552, 'synset': 'tandoor.n.01', 'name': 'tandoor'}, {'id': 11553, 'synset': 'tangram.n.01', 'name': 'tangram'}, {'id': 11554, 'synset': 'tankard.n.01', 'name': 'tankard'}, {'id': 11555, 'synset': 'tank_car.n.01', 'name': 'tank_car'}, {'id': 11556, 'synset': 'tank_destroyer.n.01', 'name': 'tank_destroyer'}, {'id': 11557, 'synset': 'tank_engine.n.01', 'name': 'tank_engine'}, {'id': 11558, 'synset': 'tanker_plane.n.01', 'name': 'tanker_plane'}, {'id': 11559, 'synset': 'tank_shell.n.01', 'name': 'tank_shell'}, {'id': 11560, 'synset': 'tannoy.n.01', 'name': 'tannoy'}, {'id': 11561, 'synset': 'tap.n.06', 'name': 'tap'}, {'id': 11562, 'synset': 'tapa.n.02', 'name': 'tapa'}, {'id': 11563, 'synset': 'tape.n.02', 'name': 'tape'}, {'id': 11564, 'synset': 'tape_deck.n.01', 'name': 'tape_deck'}, {'id': 11565, 'synset': 'tape_drive.n.01', 'name': 'tape_drive'}, {'id': 11566, 'synset': 'tape_player.n.01', 'name': 'tape_player'}, {'id': 11567, 'synset': 'tape_recorder.n.01', 'name': 'tape_recorder'}, {'id': 11568, 'synset': 'taper_file.n.01', 'name': 'taper_file'}, {'id': 11569, 'synset': 'tappet.n.01', 'name': 'tappet'}, {'id': 11570, 'synset': 'tap_wrench.n.01', 'name': 'tap_wrench'}, {'id': 11571, 'synset': 'tare.n.05', 'name': 'tare'}, {'id': 11572, 'synset': 'target.n.04', 'name': 'target'}, {'id': 11573, 'synset': 'target_acquisition_system.n.01', 'name': 'target_acquisition_system'}, {'id': 11574, 'synset': 'tarmacadam.n.02', 'name': 'tarmacadam'}, {'id': 11575, 'synset': 'tasset.n.01', 'name': 'tasset'}, {'id': 11576, 'synset': 'tattoo.n.02', 'name': 'tattoo'}, {'id': 11577, 'synset': 'tavern.n.01', 'name': 'tavern'}, {'id': 11578, 'synset': 'tawse.n.01', 'name': 'tawse'}, {'id': 11579, 'synset': 'taximeter.n.01', 'name': 'taximeter'}, {'id': 11580, 'synset': 't-bar_lift.n.01', 'name': 'T-bar_lift'}, {'id': 11581, 'synset': 'tea_bag.n.02', 'name': 'tea_bag'}, {'id': 11582, 'synset': 'tea_ball.n.01', 'name': 'tea_ball'}, {'id': 11583, 'synset': 'tea_cart.n.01', 'name': 'tea_cart'}, {'id': 11584, 'synset': 'tea_chest.n.01', 'name': 'tea_chest'}, {'id': 11585, 'synset': 'teaching_aid.n.01', 'name': 'teaching_aid'}, {'id': 11586, 'synset': 'tea_gown.n.01', 'name': 'tea_gown'}, {'id': 11587, 'synset': 'tea_maker.n.01', 'name': 'tea_maker'}, {'id': 11588, 'synset': 'teashop.n.01', 'name': 'teashop'}, {'id': 11589, 'synset': 'teaspoon.n.02', 'name': 'teaspoon'}, {'id': 11590, 'synset': 'tea-strainer.n.01', 'name': 'tea-strainer'}, {'id': 11591, 'synset': 'tea_table.n.01', 'name': 'tea_table'}, {'id': 11592, 'synset': 'tea_tray.n.01', 'name': 'tea_tray'}, {'id': 11593, 'synset': 'tea_urn.n.01', 'name': 'tea_urn'}, {'id': 11594, 'synset': 'tee.n.03', 'name': 'tee'}, {'id': 11595, 'synset': 'tee_hinge.n.01', 'name': 'tee_hinge'}, {'id': 11596, 'synset': 'telecom_hotel.n.01', 'name': 'telecom_hotel'}, {'id': 11597, 'synset': 'telecommunication_system.n.01', 'name': 'telecommunication_system'}, {'id': 11598, 'synset': 'telegraph.n.01', 'name': 'telegraph'}, {'id': 11599, 'synset': 'telegraph_key.n.01', 'name': 'telegraph_key'}, {'id': 11600, 'synset': 'telemeter.n.01', 'name': 'telemeter'}, {'id': 11601, 'synset': 'telephone_bell.n.01', 'name': 'telephone_bell'}, {'id': 11602, 'synset': 'telephone_cord.n.01', 'name': 'telephone_cord'}, {'id': 11603, 'synset': 'telephone_jack.n.01', 'name': 'telephone_jack'}, {'id': 11604, 'synset': 'telephone_line.n.02', 'name': 'telephone_line'}, {'id': 11605, 'synset': 'telephone_plug.n.01', 'name': 'telephone_plug'}, {'id': 11606, 'synset': 'telephone_receiver.n.01', 'name': 'telephone_receiver'}, {'id': 11607, 'synset': 'telephone_system.n.01', 'name': 'telephone_system'}, {'id': 11608, 'synset': 'telephone_wire.n.01', 'name': 'telephone_wire'}, {'id': 11609, 'synset': 'teleprompter.n.01', 'name': 'Teleprompter'}, {'id': 11610, 'synset': 'telescope.n.01', 'name': 'telescope'}, {'id': 11611, 'synset': 'telescopic_sight.n.01', 'name': 'telescopic_sight'}, {'id': 11612, 'synset': 'telethermometer.n.01', 'name': 'telethermometer'}, {'id': 11613, 'synset': 'teletypewriter.n.01', 'name': 'teletypewriter'}, {'id': 11614, 'synset': 'television.n.02', 'name': 'television'}, {'id': 11615, 'synset': 'television_antenna.n.01', 'name': 'television_antenna'}, {'id': 11616, 'synset': 'television_equipment.n.01', 'name': 'television_equipment'}, {'id': 11617, 'synset': 'television_monitor.n.01', 'name': 'television_monitor'}, {'id': 11618, 'synset': 'television_room.n.01', 'name': 'television_room'}, {'id': 11619, 'synset': 'television_transmitter.n.01', 'name': 'television_transmitter'}, {'id': 11620, 'synset': 'telpher.n.01', 'name': 'telpher'}, {'id': 11621, 'synset': 'telpherage.n.01', 'name': 'telpherage'}, {'id': 11622, 'synset': 'tempera.n.01', 'name': 'tempera'}, {'id': 11623, 'synset': 'temple.n.01', 'name': 'temple'}, {'id': 11624, 'synset': 'temple.n.03', 'name': 'temple'}, {'id': 11625, 'synset': 'temporary_hookup.n.01', 'name': 'temporary_hookup'}, {'id': 11626, 'synset': 'tender.n.06', 'name': 'tender'}, {'id': 11627, 'synset': 'tender.n.05', 'name': 'tender'}, {'id': 11628, 'synset': 'tender.n.04', 'name': 'tender'}, {'id': 11629, 'synset': 'tenement.n.01', 'name': 'tenement'}, {'id': 11630, 'synset': 'tennis_camp.n.01', 'name': 'tennis_camp'}, {'id': 11631, 'synset': 'tenon.n.01', 'name': 'tenon'}, {'id': 11632, 'synset': 'tenor_drum.n.01', 'name': 'tenor_drum'}, {'id': 11633, 'synset': 'tenoroon.n.01', 'name': 'tenoroon'}, {'id': 11634, 'synset': 'tenpenny_nail.n.01', 'name': 'tenpenny_nail'}, {'id': 11635, 'synset': 'tenpin.n.01', 'name': 'tenpin'}, {'id': 11636, 'synset': 'tensimeter.n.01', 'name': 'tensimeter'}, {'id': 11637, 'synset': 'tensiometer.n.03', 'name': 'tensiometer'}, {'id': 11638, 'synset': 'tensiometer.n.02', 'name': 'tensiometer'}, {'id': 11639, 'synset': 'tensiometer.n.01', 'name': 'tensiometer'}, {'id': 11640, 'synset': 'tent.n.01', 'name': 'tent'}, {'id': 11641, 'synset': 'tenter.n.01', 'name': 'tenter'}, {'id': 11642, 'synset': 'tenterhook.n.01', 'name': 'tenterhook'}, {'id': 11643, 'synset': 'tent-fly.n.01', 'name': 'tent-fly'}, {'id': 11644, 'synset': 'tent_peg.n.01', 'name': 'tent_peg'}, {'id': 11645, 'synset': 'tepee.n.01', 'name': 'tepee'}, {'id': 11646, 'synset': 'terminal.n.02', 'name': 'terminal'}, {'id': 11647, 'synset': 'terminal.n.04', 'name': 'terminal'}, {'id': 11648, 'synset': 'terraced_house.n.01', 'name': 'terraced_house'}, {'id': 11649, 'synset': 'terra_cotta.n.01', 'name': 'terra_cotta'}, {'id': 11650, 'synset': 'terrarium.n.01', 'name': 'terrarium'}, {'id': 11651, 'synset': 'terra_sigillata.n.01', 'name': 'terra_sigillata'}, {'id': 11652, 'synset': 'terry.n.02', 'name': 'terry'}, {'id': 11653, 'synset': 'tesla_coil.n.01', 'name': 'Tesla_coil'}, {'id': 11654, 'synset': 'tessera.n.01', 'name': 'tessera'}, {'id': 11655, 'synset': 'test_equipment.n.01', 'name': 'test_equipment'}, {'id': 11656, 'synset': 'test_rocket.n.01', 'name': 'test_rocket'}, {'id': 11657, 'synset': 'test_room.n.01', 'name': 'test_room'}, {'id': 11658, 'synset': 'testudo.n.01', 'name': 'testudo'}, {'id': 11659, 'synset': 'tetraskelion.n.01', 'name': 'tetraskelion'}, {'id': 11660, 'synset': 'tetrode.n.01', 'name': 'tetrode'}, {'id': 11661, 'synset': 'textile_machine.n.01', 'name': 'textile_machine'}, {'id': 11662, 'synset': 'textile_mill.n.01', 'name': 'textile_mill'}, {'id': 11663, 'synset': 'thatch.n.04', 'name': 'thatch'}, {'id': 11664, 'synset': 'theater.n.01', 'name': 'theater'}, {'id': 11665, 'synset': 'theater_curtain.n.01', 'name': 'theater_curtain'}, {'id': 11666, 'synset': 'theater_light.n.01', 'name': 'theater_light'}, {'id': 11667, 'synset': 'theodolite.n.01', 'name': 'theodolite'}, {'id': 11668, 'synset': 'theremin.n.01', 'name': 'theremin'}, {'id': 11669, 'synset': 'thermal_printer.n.01', 'name': 'thermal_printer'}, {'id': 11670, 'synset': 'thermal_reactor.n.01', 'name': 'thermal_reactor'}, {'id': 11671, 'synset': 'thermocouple.n.01', 'name': 'thermocouple'}, {'id': 11672, 'synset': 'thermoelectric_thermometer.n.01', 'name': 'thermoelectric_thermometer'}, {'id': 11673, 'synset': 'thermograph.n.02', 'name': 'thermograph'}, {'id': 11674, 'synset': 'thermograph.n.01', 'name': 'thermograph'}, {'id': 11675, 'synset': 'thermohydrometer.n.01', 'name': 'thermohydrometer'}, {'id': 11676, 'synset': 'thermojunction.n.01', 'name': 'thermojunction'}, {'id': 11677, 'synset': 'thermonuclear_reactor.n.01', 'name': 'thermonuclear_reactor'}, {'id': 11678, 'synset': 'thermopile.n.01', 'name': 'thermopile'}, {'id': 11679, 'synset': 'thigh_pad.n.01', 'name': 'thigh_pad'}, {'id': 11680, 'synset': 'thill.n.01', 'name': 'thill'}, {'id': 11681, 'synset': 'thinning_shears.n.01', 'name': 'thinning_shears'}, {'id': 11682, 'synset': 'third_base.n.01', 'name': 'third_base'}, {'id': 11683, 'synset': 'third_gear.n.01', 'name': 'third_gear'}, {'id': 11684, 'synset': 'third_rail.n.01', 'name': 'third_rail'}, {'id': 11685, 'synset': 'thong.n.03', 'name': 'thong'}, {'id': 11686, 'synset': 'thong.n.02', 'name': 'thong'}, {'id': 11687, 'synset': 'three-centered_arch.n.01', 'name': 'three-centered_arch'}, {'id': 11688, 'synset': 'three-decker.n.02', 'name': 'three-decker'}, {'id': 11689, 'synset': 'three-dimensional_radar.n.01', 'name': 'three-dimensional_radar'}, {'id': 11690, 'synset': 'three-piece_suit.n.01', 'name': 'three-piece_suit'}, {'id': 11691, 'synset': 'three-quarter_binding.n.01', 'name': 'three-quarter_binding'}, {'id': 11692, 'synset': 'three-way_switch.n.01', 'name': 'three-way_switch'}, {'id': 11693, 'synset': 'thresher.n.01', 'name': 'thresher'}, {'id': 11694, 'synset': 'threshing_floor.n.01', 'name': 'threshing_floor'}, {'id': 11695, 'synset': 'thriftshop.n.01', 'name': 'thriftshop'}, {'id': 11696, 'synset': 'throat_protector.n.01', 'name': 'throat_protector'}, {'id': 11697, 'synset': 'throne.n.01', 'name': 'throne'}, {'id': 11698, 'synset': 'thrust_bearing.n.01', 'name': 'thrust_bearing'}, {'id': 11699, 'synset': 'thruster.n.02', 'name': 'thruster'}, {'id': 11700, 'synset': 'thumb.n.02', 'name': 'thumb'}, {'id': 11701, 'synset': 'thumbhole.n.02', 'name': 'thumbhole'}, {'id': 11702, 'synset': 'thumbscrew.n.02', 'name': 'thumbscrew'}, {'id': 11703, 'synset': 'thumbstall.n.01', 'name': 'thumbstall'}, {'id': 11704, 'synset': 'thunderer.n.02', 'name': 'thunderer'}, {'id': 11705, 'synset': 'thwart.n.01', 'name': 'thwart'}, {'id': 11706, 'synset': 'ticking.n.02', 'name': 'ticking'}, {'id': 11707, 'synset': 'tickler_coil.n.01', 'name': 'tickler_coil'}, {'id': 11708, 'synset': 'tie.n.04', 'name': 'tie'}, {'id': 11709, 'synset': 'tie.n.08', 'name': 'tie'}, {'id': 11710, 'synset': 'tie_rack.n.01', 'name': 'tie_rack'}, {'id': 11711, 'synset': 'tie_rod.n.01', 'name': 'tie_rod'}, {'id': 11712, 'synset': 'tile.n.01', 'name': 'tile'}, {'id': 11713, 'synset': 'tile_cutter.n.01', 'name': 'tile_cutter'}, {'id': 11714, 'synset': 'tile_roof.n.01', 'name': 'tile_roof'}, {'id': 11715, 'synset': 'tiller.n.03', 'name': 'tiller'}, {'id': 11716, 'synset': 'tilter.n.02', 'name': 'tilter'}, {'id': 11717, 'synset': 'tilt-top_table.n.01', 'name': 'tilt-top_table'}, {'id': 11718, 'synset': 'timber.n.02', 'name': 'timber'}, {'id': 11719, 'synset': 'timber.n.03', 'name': 'timber'}, {'id': 11720, 'synset': 'timber_hitch.n.01', 'name': 'timber_hitch'}, {'id': 11721, 'synset': 'timbrel.n.01', 'name': 'timbrel'}, {'id': 11722, 'synset': 'time_bomb.n.02', 'name': 'time_bomb'}, {'id': 11723, 'synset': 'time_capsule.n.01', 'name': 'time_capsule'}, {'id': 11724, 'synset': 'time_clock.n.01', 'name': 'time_clock'}, {'id': 11725, 'synset': 'time-delay_measuring_instrument.n.01', 'name': 'time-delay_measuring_instrument'}, {'id': 11726, 'synset': 'time-fuse.n.01', 'name': 'time-fuse'}, {'id': 11727, 'synset': 'timepiece.n.01', 'name': 'timepiece'}, {'id': 11728, 'synset': 'timer.n.03', 'name': 'timer'}, {'id': 11729, 'synset': 'time-switch.n.01', 'name': 'time-switch'}, {'id': 11730, 'synset': 'tin.n.02', 'name': 'tin'}, {'id': 11731, 'synset': 'tinderbox.n.02', 'name': 'tinderbox'}, {'id': 11732, 'synset': 'tine.n.01', 'name': 'tine'}, {'id': 11733, 'synset': 'tippet.n.01', 'name': 'tippet'}, {'id': 11734, 'synset': 'tire_chain.n.01', 'name': 'tire_chain'}, {'id': 11735, 'synset': 'tire_iron.n.01', 'name': 'tire_iron'}, {'id': 11736, 'synset': 'titfer.n.01', 'name': 'titfer'}, {'id': 11737, 'synset': 'tithe_barn.n.01', 'name': 'tithe_barn'}, {'id': 11738, 'synset': 'titrator.n.01', 'name': 'titrator'}, {'id': 11739, 'synset': 'toasting_fork.n.01', 'name': 'toasting_fork'}, {'id': 11740, 'synset': 'toastrack.n.01', 'name': 'toastrack'}, {'id': 11741, 'synset': 'tobacco_pouch.n.01', 'name': 'tobacco_pouch'}, {'id': 11742, 'synset': 'tobacco_shop.n.01', 'name': 'tobacco_shop'}, {'id': 11743, 'synset': 'toboggan.n.01', 'name': 'toboggan'}, {'id': 11744, 'synset': 'toby.n.01', 'name': 'toby'}, {'id': 11745, 'synset': 'tocsin.n.02', 'name': 'tocsin'}, {'id': 11746, 'synset': 'toe.n.02', 'name': 'toe'}, {'id': 11747, 'synset': 'toecap.n.01', 'name': 'toecap'}, {'id': 11748, 'synset': 'toehold.n.02', 'name': 'toehold'}, {'id': 11749, 'synset': 'toga.n.01', 'name': 'toga'}, {'id': 11750, 'synset': 'toga_virilis.n.01', 'name': 'toga_virilis'}, {'id': 11751, 'synset': 'toggle.n.03', 'name': 'toggle'}, {'id': 11752, 'synset': 'toggle_bolt.n.01', 'name': 'toggle_bolt'}, {'id': 11753, 'synset': 'toggle_joint.n.01', 'name': 'toggle_joint'}, {'id': 11754, 'synset': 'toggle_switch.n.01', 'name': 'toggle_switch'}, {'id': 11755, 'synset': 'togs.n.01', 'name': 'togs'}, {'id': 11756, 'synset': 'toilet.n.01', 'name': 'toilet'}, {'id': 11757, 'synset': 'toilet_bag.n.01', 'name': 'toilet_bag'}, {'id': 11758, 'synset': 'toilet_bowl.n.01', 'name': 'toilet_bowl'}, {'id': 11759, 'synset': 'toilet_kit.n.01', 'name': 'toilet_kit'}, {'id': 11760, 'synset': 'toilet_powder.n.01', 'name': 'toilet_powder'}, {'id': 11761, 'synset': 'toiletry.n.01', 'name': 'toiletry'}, {'id': 11762, 'synset': 'toilet_seat.n.01', 'name': 'toilet_seat'}, {'id': 11763, 'synset': 'toilet_water.n.01', 'name': 'toilet_water'}, {'id': 11764, 'synset': 'tokamak.n.01', 'name': 'tokamak'}, {'id': 11765, 'synset': 'token.n.03', 'name': 'token'}, {'id': 11766, 'synset': 'tollbooth.n.01', 'name': 'tollbooth'}, {'id': 11767, 'synset': 'toll_bridge.n.01', 'name': 'toll_bridge'}, {'id': 11768, 'synset': 'tollgate.n.01', 'name': 'tollgate'}, {'id': 11769, 'synset': 'toll_line.n.01', 'name': 'toll_line'}, {'id': 11770, 'synset': 'tomahawk.n.01', 'name': 'tomahawk'}, {'id': 11771, 'synset': 'tommy_gun.n.01', 'name': 'Tommy_gun'}, {'id': 11772, 'synset': 'tomograph.n.01', 'name': 'tomograph'}, {'id': 11773, 'synset': 'tone_arm.n.01', 'name': 'tone_arm'}, {'id': 11774, 'synset': 'toner.n.03', 'name': 'toner'}, {'id': 11775, 'synset': 'tongue.n.07', 'name': 'tongue'}, {'id': 11776, 'synset': 'tongue_and_groove_joint.n.01', 'name': 'tongue_and_groove_joint'}, {'id': 11777, 'synset': 'tongue_depressor.n.01', 'name': 'tongue_depressor'}, {'id': 11778, 'synset': 'tonometer.n.01', 'name': 'tonometer'}, {'id': 11779, 'synset': 'tool.n.01', 'name': 'tool'}, {'id': 11780, 'synset': 'tool_bag.n.01', 'name': 'tool_bag'}, {'id': 11781, 'synset': 'toolshed.n.01', 'name': 'toolshed'}, {'id': 11782, 'synset': 'tooth.n.02', 'name': 'tooth'}, {'id': 11783, 'synset': 'tooth.n.05', 'name': 'tooth'}, {'id': 11784, 'synset': 'top.n.10', 'name': 'top'}, {'id': 11785, 'synset': 'topgallant.n.02', 'name': 'topgallant'}, {'id': 11786, 'synset': 'topgallant.n.01', 'name': 'topgallant'}, {'id': 11787, 'synset': 'topiary.n.01', 'name': 'topiary'}, {'id': 11788, 'synset': 'topknot.n.01', 'name': 'topknot'}, {'id': 11789, 'synset': 'topmast.n.01', 'name': 'topmast'}, {'id': 11790, 'synset': 'topper.n.05', 'name': 'topper'}, {'id': 11791, 'synset': 'topsail.n.01', 'name': 'topsail'}, {'id': 11792, 'synset': 'toque.n.01', 'name': 'toque'}, {'id': 11793, 'synset': 'torch.n.01', 'name': 'torch'}, {'id': 11794, 'synset': 'torpedo.n.06', 'name': 'torpedo'}, {'id': 11795, 'synset': 'torpedo.n.05', 'name': 'torpedo'}, {'id': 11796, 'synset': 'torpedo.n.03', 'name': 'torpedo'}, {'id': 11797, 'synset': 'torpedo_boat.n.01', 'name': 'torpedo_boat'}, {'id': 11798, 'synset': 'torpedo-boat_destroyer.n.01', 'name': 'torpedo-boat_destroyer'}, {'id': 11799, 'synset': 'torpedo_tube.n.01', 'name': 'torpedo_tube'}, {'id': 11800, 'synset': 'torque_converter.n.01', 'name': 'torque_converter'}, {'id': 11801, 'synset': 'torque_wrench.n.01', 'name': 'torque_wrench'}, {'id': 11802, 'synset': 'torture_chamber.n.01', 'name': 'torture_chamber'}, {'id': 11803, 'synset': 'totem_pole.n.01', 'name': 'totem_pole'}, {'id': 11804, 'synset': 'touch_screen.n.01', 'name': 'touch_screen'}, {'id': 11805, 'synset': 'toupee.n.01', 'name': 'toupee'}, {'id': 11806, 'synset': 'touring_car.n.01', 'name': 'touring_car'}, {'id': 11807, 'synset': 'tourist_class.n.01', 'name': 'tourist_class'}, {'id': 11808, 'synset': 'toweling.n.01', 'name': 'toweling'}, {'id': 11809, 'synset': 'towel_rail.n.01', 'name': 'towel_rail'}, {'id': 11810, 'synset': 'tower.n.01', 'name': 'tower'}, {'id': 11811, 'synset': 'town_hall.n.01', 'name': 'town_hall'}, {'id': 11812, 'synset': 'towpath.n.01', 'name': 'towpath'}, {'id': 11813, 'synset': 'toy_box.n.01', 'name': 'toy_box'}, {'id': 11814, 'synset': 'toyshop.n.01', 'name': 'toyshop'}, {'id': 11815, 'synset': 'trace_detector.n.01', 'name': 'trace_detector'}, {'id': 11816, 'synset': 'track.n.09', 'name': 'track'}, {'id': 11817, 'synset': 'track.n.08', 'name': 'track'}, {'id': 11818, 'synset': 'trackball.n.01', 'name': 'trackball'}, {'id': 11819, 'synset': 'tracked_vehicle.n.01', 'name': 'tracked_vehicle'}, {'id': 11820, 'synset': 'tract_house.n.01', 'name': 'tract_house'}, {'id': 11821, 'synset': 'tract_housing.n.01', 'name': 'tract_housing'}, {'id': 11822, 'synset': 'traction_engine.n.01', 'name': 'traction_engine'}, {'id': 11823, 'synset': 'tractor.n.02', 'name': 'tractor'}, {'id': 11824, 'synset': 'trailer.n.04', 'name': 'trailer'}, {'id': 11825, 'synset': 'trailer.n.03', 'name': 'trailer'}, {'id': 11826, 'synset': 'trailer_camp.n.01', 'name': 'trailer_camp'}, {'id': 11827, 'synset': 'trailing_edge.n.01', 'name': 'trailing_edge'}, {'id': 11828, 'synset': 'tramline.n.01', 'name': 'tramline'}, {'id': 11829, 'synset': 'trammel.n.02', 'name': 'trammel'}, {'id': 11830, 'synset': 'tramp_steamer.n.01', 'name': 'tramp_steamer'}, {'id': 11831, 'synset': 'tramway.n.01', 'name': 'tramway'}, {'id': 11832, 'synset': 'transdermal_patch.n.01', 'name': 'transdermal_patch'}, {'id': 11833, 'synset': 'transept.n.01', 'name': 'transept'}, {'id': 11834, 'synset': 'transformer.n.01', 'name': 'transformer'}, {'id': 11835, 'synset': 'transistor.n.01', 'name': 'transistor'}, {'id': 11836, 'synset': 'transit_instrument.n.01', 'name': 'transit_instrument'}, {'id': 11837, 'synset': 'transmission.n.05', 'name': 'transmission'}, {'id': 11838, 'synset': 'transmission_shaft.n.01', 'name': 'transmission_shaft'}, {'id': 11839, 'synset': 'transmitter.n.03', 'name': 'transmitter'}, {'id': 11840, 'synset': 'transom.n.02', 'name': 'transom'}, {'id': 11841, 'synset': 'transom.n.01', 'name': 'transom'}, {'id': 11842, 'synset': 'transponder.n.01', 'name': 'transponder'}, {'id': 11843, 'synset': 'transporter.n.02', 'name': 'transporter'}, {'id': 11844, 'synset': 'transporter.n.01', 'name': 'transporter'}, {'id': 11845, 'synset': 'transport_ship.n.01', 'name': 'transport_ship'}, {'id': 11846, 'synset': 'trap.n.01', 'name': 'trap'}, {'id': 11847, 'synset': 'trap_door.n.01', 'name': 'trap_door'}, {'id': 11848, 'synset': 'trapeze.n.01', 'name': 'trapeze'}, {'id': 11849, 'synset': 'trave.n.01', 'name': 'trave'}, {'id': 11850, 'synset': 'travel_iron.n.01', 'name': 'travel_iron'}, {'id': 11851, 'synset': 'trawl.n.02', 'name': 'trawl'}, {'id': 11852, 'synset': 'trawl.n.01', 'name': 'trawl'}, {'id': 11853, 'synset': 'trawler.n.02', 'name': 'trawler'}, {'id': 11854, 'synset': 'tray_cloth.n.01', 'name': 'tray_cloth'}, {'id': 11855, 'synset': 'tread.n.04', 'name': 'tread'}, {'id': 11856, 'synset': 'tread.n.03', 'name': 'tread'}, {'id': 11857, 'synset': 'treadmill.n.02', 'name': 'treadmill'}, {'id': 11858, 'synset': 'treadmill.n.01', 'name': 'treadmill'}, {'id': 11859, 'synset': 'treasure_chest.n.01', 'name': 'treasure_chest'}, {'id': 11860, 'synset': 'treasure_ship.n.01', 'name': 'treasure_ship'}, {'id': 11861, 'synset': 'treenail.n.01', 'name': 'treenail'}, {'id': 11862, 'synset': 'trefoil_arch.n.01', 'name': 'trefoil_arch'}, {'id': 11863, 'synset': 'trellis.n.01', 'name': 'trellis'}, {'id': 11864, 'synset': 'trench.n.01', 'name': 'trench'}, {'id': 11865, 'synset': 'trench_knife.n.01', 'name': 'trench_knife'}, {'id': 11866, 'synset': 'trepan.n.02', 'name': 'trepan'}, {'id': 11867, 'synset': 'trepan.n.01', 'name': 'trepan'}, {'id': 11868, 'synset': 'trestle.n.02', 'name': 'trestle'}, {'id': 11869, 'synset': 'trestle.n.01', 'name': 'trestle'}, {'id': 11870, 'synset': 'trestle_bridge.n.01', 'name': 'trestle_bridge'}, {'id': 11871, 'synset': 'trestle_table.n.01', 'name': 'trestle_table'}, {'id': 11872, 'synset': 'trestlework.n.01', 'name': 'trestlework'}, {'id': 11873, 'synset': 'trews.n.01', 'name': 'trews'}, {'id': 11874, 'synset': 'trial_balloon.n.02', 'name': 'trial_balloon'}, {'id': 11875, 'synset': 'triangle.n.04', 'name': 'triangle'}, {'id': 11876, 'synset': 'triclinium.n.02', 'name': 'triclinium'}, {'id': 11877, 'synset': 'triclinium.n.01', 'name': 'triclinium'}, {'id': 11878, 'synset': 'tricorn.n.01', 'name': 'tricorn'}, {'id': 11879, 'synset': 'tricot.n.01', 'name': 'tricot'}, {'id': 11880, 'synset': 'trident.n.01', 'name': 'trident'}, {'id': 11881, 'synset': 'trigger.n.02', 'name': 'trigger'}, {'id': 11882, 'synset': 'trimaran.n.01', 'name': 'trimaran'}, {'id': 11883, 'synset': 'trimmer.n.02', 'name': 'trimmer'}, {'id': 11884, 'synset': 'trimmer_arch.n.01', 'name': 'trimmer_arch'}, {'id': 11885, 'synset': 'triode.n.01', 'name': 'triode'}, {'id': 11886, 'synset': 'triptych.n.01', 'name': 'triptych'}, {'id': 11887, 'synset': 'trip_wire.n.02', 'name': 'trip_wire'}, {'id': 11888, 'synset': 'trireme.n.01', 'name': 'trireme'}, {'id': 11889, 'synset': 'triskelion.n.01', 'name': 'triskelion'}, {'id': 11890, 'synset': 'triumphal_arch.n.01', 'name': 'triumphal_arch'}, {'id': 11891, 'synset': 'trivet.n.02', 'name': 'trivet'}, {'id': 11892, 'synset': 'trivet.n.01', 'name': 'trivet'}, {'id': 11893, 'synset': 'troika.n.01', 'name': 'troika'}, {'id': 11894, 'synset': 'troll.n.03', 'name': 'troll'}, {'id': 11895, 'synset': 'trolleybus.n.01', 'name': 'trolleybus'}, {'id': 11896, 'synset': 'trombone.n.01', 'name': 'trombone'}, {'id': 11897, 'synset': 'troop_carrier.n.01', 'name': 'troop_carrier'}, {'id': 11898, 'synset': 'troopship.n.01', 'name': 'troopship'}, {'id': 11899, 'synset': 'trophy_case.n.01', 'name': 'trophy_case'}, {'id': 11900, 'synset': 'trough.n.05', 'name': 'trough'}, {'id': 11901, 'synset': 'trouser.n.02', 'name': 'trouser'}, {'id': 11902, 'synset': 'trouser_cuff.n.01', 'name': 'trouser_cuff'}, {'id': 11903, 'synset': 'trouser_press.n.01', 'name': 'trouser_press'}, {'id': 11904, 'synset': 'trousseau.n.01', 'name': 'trousseau'}, {'id': 11905, 'synset': 'trowel.n.01', 'name': 'trowel'}, {'id': 11906, 'synset': 'trumpet_arch.n.01', 'name': 'trumpet_arch'}, {'id': 11907, 'synset': 'truncheon.n.01', 'name': 'truncheon'}, {'id': 11908, 'synset': 'trundle_bed.n.01', 'name': 'trundle_bed'}, {'id': 11909, 'synset': 'trunk_hose.n.01', 'name': 'trunk_hose'}, {'id': 11910, 'synset': 'trunk_lid.n.01', 'name': 'trunk_lid'}, {'id': 11911, 'synset': 'trunk_line.n.02', 'name': 'trunk_line'}, {'id': 11912, 'synset': 'truss.n.02', 'name': 'truss'}, {'id': 11913, 'synset': 'truss_bridge.n.01', 'name': 'truss_bridge'}, {'id': 11914, 'synset': 'try_square.n.01', 'name': 'try_square'}, {'id': 11915, 'synset': 't-square.n.01', 'name': 'T-square'}, {'id': 11916, 'synset': 'tube.n.02', 'name': 'tube'}, {'id': 11917, 'synset': 'tuck_box.n.01', 'name': 'tuck_box'}, {'id': 11918, 'synset': 'tucker.n.04', 'name': 'tucker'}, {'id': 11919, 'synset': 'tucker-bag.n.01', 'name': 'tucker-bag'}, {'id': 11920, 'synset': 'tuck_shop.n.01', 'name': 'tuck_shop'}, {'id': 11921, 'synset': 'tudor_arch.n.01', 'name': 'Tudor_arch'}, {'id': 11922, 'synset': 'tudung.n.01', 'name': 'tudung'}, {'id': 11923, 'synset': 'tugboat.n.01', 'name': 'tugboat'}, {'id': 11924, 'synset': 'tulle.n.01', 'name': 'tulle'}, {'id': 11925, 'synset': 'tumble-dryer.n.01', 'name': 'tumble-dryer'}, {'id': 11926, 'synset': 'tumbler.n.02', 'name': 'tumbler'}, {'id': 11927, 'synset': 'tumbrel.n.01', 'name': 'tumbrel'}, {'id': 11928, 'synset': 'tun.n.01', 'name': 'tun'}, {'id': 11929, 'synset': 'tunic.n.02', 'name': 'tunic'}, {'id': 11930, 'synset': 'tuning_fork.n.01', 'name': 'tuning_fork'}, {'id': 11931, 'synset': 'tupik.n.01', 'name': 'tupik'}, {'id': 11932, 'synset': 'turbine.n.01', 'name': 'turbine'}, {'id': 11933, 'synset': 'turbogenerator.n.01', 'name': 'turbogenerator'}, {'id': 11934, 'synset': 'tureen.n.01', 'name': 'tureen'}, {'id': 11935, 'synset': 'turkish_bath.n.01', 'name': 'Turkish_bath'}, {'id': 11936, 'synset': 'turkish_towel.n.01', 'name': 'Turkish_towel'}, {'id': 11937, 'synset': "turk's_head.n.01", 'name': "Turk's_head"}, {'id': 11938, 'synset': 'turnbuckle.n.01', 'name': 'turnbuckle'}, {'id': 11939, 'synset': 'turner.n.08', 'name': 'turner'}, {'id': 11940, 'synset': 'turnery.n.01', 'name': 'turnery'}, {'id': 11941, 'synset': 'turnpike.n.01', 'name': 'turnpike'}, {'id': 11942, 'synset': 'turnspit.n.01', 'name': 'turnspit'}, {'id': 11943, 'synset': 'turnstile.n.01', 'name': 'turnstile'}, {'id': 11944, 'synset': 'turntable.n.01', 'name': 'turntable'}, {'id': 11945, 'synset': 'turntable.n.02', 'name': 'turntable'}, {'id': 11946, 'synset': 'turret.n.01', 'name': 'turret'}, {'id': 11947, 'synset': 'turret_clock.n.01', 'name': 'turret_clock'}, {'id': 11948, 'synset': 'tweed.n.01', 'name': 'tweed'}, {'id': 11949, 'synset': 'tweeter.n.01', 'name': 'tweeter'}, {'id': 11950, 'synset': 'twenty-two.n.02', 'name': 'twenty-two'}, {'id': 11951, 'synset': 'twenty-two_pistol.n.01', 'name': 'twenty-two_pistol'}, {'id': 11952, 'synset': 'twenty-two_rifle.n.01', 'name': 'twenty-two_rifle'}, {'id': 11953, 'synset': 'twill.n.02', 'name': 'twill'}, {'id': 11954, 'synset': 'twill.n.01', 'name': 'twill'}, {'id': 11955, 'synset': 'twin_bed.n.01', 'name': 'twin_bed'}, {'id': 11956, 'synset': 'twinjet.n.01', 'name': 'twinjet'}, {'id': 11957, 'synset': 'twist_bit.n.01', 'name': 'twist_bit'}, {'id': 11958, 'synset': 'two-by-four.n.01', 'name': 'two-by-four'}, {'id': 11959, 'synset': 'two-man_tent.n.01', 'name': 'two-man_tent'}, {'id': 11960, 'synset': 'two-piece.n.01', 'name': 'two-piece'}, {'id': 11961, 'synset': 'typesetting_machine.n.01', 'name': 'typesetting_machine'}, {'id': 11962, 'synset': 'typewriter_carriage.n.01', 'name': 'typewriter_carriage'}, {'id': 11963, 'synset': 'typewriter_keyboard.n.01', 'name': 'typewriter_keyboard'}, {'id': 11964, 'synset': 'tyrolean.n.02', 'name': 'tyrolean'}, {'id': 11965, 'synset': 'uke.n.01', 'name': 'uke'}, {'id': 11966, 'synset': 'ulster.n.02', 'name': 'ulster'}, {'id': 11967, 'synset': 'ultracentrifuge.n.01', 'name': 'ultracentrifuge'}, {'id': 11968, 'synset': 'ultramicroscope.n.01', 'name': 'ultramicroscope'}, {'id': 11969, 'synset': 'ultrasuede.n.01', 'name': 'Ultrasuede'}, {'id': 11970, 'synset': 'ultraviolet_lamp.n.01', 'name': 'ultraviolet_lamp'}, {'id': 11971, 'synset': 'umbrella_tent.n.01', 'name': 'umbrella_tent'}, {'id': 11972, 'synset': 'undercarriage.n.01', 'name': 'undercarriage'}, {'id': 11973, 'synset': 'undercoat.n.01', 'name': 'undercoat'}, {'id': 11974, 'synset': 'undergarment.n.01', 'name': 'undergarment'}, {'id': 11975, 'synset': 'underpants.n.01', 'name': 'underpants'}, {'id': 11976, 'synset': 'undies.n.01', 'name': 'undies'}, {'id': 11977, 'synset': 'uneven_parallel_bars.n.01', 'name': 'uneven_parallel_bars'}, {'id': 11978, 'synset': 'uniform.n.01', 'name': 'uniform'}, {'id': 11979, 'synset': 'universal_joint.n.01', 'name': 'universal_joint'}, {'id': 11980, 'synset': 'university.n.02', 'name': 'university'}, {'id': 11981, 'synset': 'upholstery.n.01', 'name': 'upholstery'}, {'id': 11982, 'synset': 'upholstery_material.n.01', 'name': 'upholstery_material'}, {'id': 11983, 'synset': 'upholstery_needle.n.01', 'name': 'upholstery_needle'}, {'id': 11984, 'synset': 'uplift.n.02', 'name': 'uplift'}, {'id': 11985, 'synset': 'upper_berth.n.01', 'name': 'upper_berth'}, {'id': 11986, 'synset': 'upright.n.02', 'name': 'upright'}, {'id': 11987, 'synset': 'upset.n.04', 'name': 'upset'}, {'id': 11988, 'synset': 'upstairs.n.01', 'name': 'upstairs'}, {'id': 11989, 'synset': 'urceole.n.01', 'name': 'urceole'}, {'id': 11990, 'synset': 'urn.n.02', 'name': 'urn'}, {'id': 11991, 'synset': 'used-car.n.01', 'name': 'used-car'}, {'id': 11992, 'synset': 'utensil.n.01', 'name': 'utensil'}, {'id': 11993, 'synset': 'uzi.n.01', 'name': 'Uzi'}, {'id': 11994, 'synset': 'vacation_home.n.01', 'name': 'vacation_home'}, {'id': 11995, 'synset': 'vacuum_chamber.n.01', 'name': 'vacuum_chamber'}, {'id': 11996, 'synset': 'vacuum_flask.n.01', 'name': 'vacuum_flask'}, {'id': 11997, 'synset': 'vacuum_gauge.n.01', 'name': 'vacuum_gauge'}, {'id': 11998, 'synset': 'valenciennes.n.02', 'name': 'Valenciennes'}, {'id': 11999, 'synset': 'valise.n.01', 'name': 'valise'}, {'id': 12000, 'synset': 'valve.n.03', 'name': 'valve'}, {'id': 12001, 'synset': 'valve.n.02', 'name': 'valve'}, {'id': 12002, 'synset': 'valve-in-head_engine.n.01', 'name': 'valve-in-head_engine'}, {'id': 12003, 'synset': 'vambrace.n.01', 'name': 'vambrace'}, {'id': 12004, 'synset': 'van.n.05', 'name': 'van'}, {'id': 12005, 'synset': 'van.n.04', 'name': 'van'}, {'id': 12006, 'synset': 'vane.n.02', 'name': 'vane'}, {'id': 12007, 'synset': 'vaporizer.n.01', 'name': 'vaporizer'}, {'id': 12008, 'synset': 'variable-pitch_propeller.n.01', 'name': 'variable-pitch_propeller'}, {'id': 12009, 'synset': 'variometer.n.01', 'name': 'variometer'}, {'id': 12010, 'synset': 'varnish.n.01', 'name': 'varnish'}, {'id': 12011, 'synset': 'vault.n.03', 'name': 'vault'}, {'id': 12012, 'synset': 'vault.n.02', 'name': 'vault'}, {'id': 12013, 'synset': 'vaulting_horse.n.01', 'name': 'vaulting_horse'}, {'id': 12014, 'synset': 'vehicle.n.01', 'name': 'vehicle'}, {'id': 12015, 'synset': 'velcro.n.01', 'name': 'Velcro'}, {'id': 12016, 'synset': 'velocipede.n.01', 'name': 'velocipede'}, {'id': 12017, 'synset': 'velour.n.01', 'name': 'velour'}, {'id': 12018, 'synset': 'velvet.n.01', 'name': 'velvet'}, {'id': 12019, 'synset': 'velveteen.n.01', 'name': 'velveteen'}, {'id': 12020, 'synset': 'veneer.n.01', 'name': 'veneer'}, {'id': 12021, 'synset': 'venetian_blind.n.01', 'name': 'Venetian_blind'}, {'id': 12022, 'synset': 'venn_diagram.n.01', 'name': 'Venn_diagram'}, {'id': 12023, 'synset': 'ventilation.n.02', 'name': 'ventilation'}, {'id': 12024, 'synset': 'ventilation_shaft.n.01', 'name': 'ventilation_shaft'}, {'id': 12025, 'synset': 'ventilator.n.01', 'name': 'ventilator'}, {'id': 12026, 'synset': 'veranda.n.01', 'name': 'veranda'}, {'id': 12027, 'synset': 'verdigris.n.02', 'name': 'verdigris'}, {'id': 12028, 'synset': 'vernier_caliper.n.01', 'name': 'vernier_caliper'}, {'id': 12029, 'synset': 'vernier_scale.n.01', 'name': 'vernier_scale'}, {'id': 12030, 'synset': 'vertical_file.n.01', 'name': 'vertical_file'}, {'id': 12031, 'synset': 'vertical_stabilizer.n.01', 'name': 'vertical_stabilizer'}, {'id': 12032, 'synset': 'vertical_tail.n.01', 'name': 'vertical_tail'}, {'id': 12033, 'synset': 'very_pistol.n.01', 'name': 'Very_pistol'}, {'id': 12034, 'synset': 'vessel.n.02', 'name': 'vessel'}, {'id': 12035, 'synset': 'vessel.n.03', 'name': 'vessel'}, {'id': 12036, 'synset': 'vestiture.n.01', 'name': 'vestiture'}, {'id': 12037, 'synset': 'vestment.n.01', 'name': 'vestment'}, {'id': 12038, 'synset': 'vest_pocket.n.01', 'name': 'vest_pocket'}, {'id': 12039, 'synset': 'vestry.n.02', 'name': 'vestry'}, {'id': 12040, 'synset': 'viaduct.n.01', 'name': 'viaduct'}, {'id': 12041, 'synset': 'vibraphone.n.01', 'name': 'vibraphone'}, {'id': 12042, 'synset': 'vibrator.n.02', 'name': 'vibrator'}, {'id': 12043, 'synset': 'vibrator.n.01', 'name': 'vibrator'}, {'id': 12044, 'synset': 'victrola.n.01', 'name': 'Victrola'}, {'id': 12045, 'synset': 'vicuna.n.02', 'name': 'vicuna'}, {'id': 12046, 'synset': 'videocassette.n.01', 'name': 'videocassette'}, {'id': 12047, 'synset': 'videocassette_recorder.n.01', 'name': 'videocassette_recorder'}, {'id': 12048, 'synset': 'videodisk.n.01', 'name': 'videodisk'}, {'id': 12049, 'synset': 'video_recording.n.01', 'name': 'video_recording'}, {'id': 12050, 'synset': 'videotape.n.02', 'name': 'videotape'}, {'id': 12051, 'synset': 'vigil_light.n.01', 'name': 'vigil_light'}, {'id': 12052, 'synset': 'villa.n.04', 'name': 'villa'}, {'id': 12053, 'synset': 'villa.n.03', 'name': 'villa'}, {'id': 12054, 'synset': 'villa.n.02', 'name': 'villa'}, {'id': 12055, 'synset': 'viol.n.01', 'name': 'viol'}, {'id': 12056, 'synset': 'viola.n.03', 'name': 'viola'}, {'id': 12057, 'synset': 'viola_da_braccio.n.01', 'name': 'viola_da_braccio'}, {'id': 12058, 'synset': 'viola_da_gamba.n.01', 'name': 'viola_da_gamba'}, {'id': 12059, 'synset': "viola_d'amore.n.01", 'name': "viola_d'amore"}, {'id': 12060, 'synset': 'virginal.n.01', 'name': 'virginal'}, {'id': 12061, 'synset': 'viscometer.n.01', 'name': 'viscometer'}, {'id': 12062, 'synset': 'viscose_rayon.n.01', 'name': 'viscose_rayon'}, {'id': 12063, 'synset': 'vise.n.01', 'name': 'vise'}, {'id': 12064, 'synset': 'visor.n.01', 'name': 'visor'}, {'id': 12065, 'synset': 'visual_display_unit.n.01', 'name': 'visual_display_unit'}, {'id': 12066, 'synset': 'vivarium.n.01', 'name': 'vivarium'}, {'id': 12067, 'synset': 'viyella.n.01', 'name': 'Viyella'}, {'id': 12068, 'synset': 'voile.n.01', 'name': 'voile'}, {'id': 12069, 'synset': 'volleyball_net.n.01', 'name': 'volleyball_net'}, {'id': 12070, 'synset': 'voltage_regulator.n.01', 'name': 'voltage_regulator'}, {'id': 12071, 'synset': 'voltaic_cell.n.01', 'name': 'voltaic_cell'}, {'id': 12072, 'synset': 'voltaic_pile.n.01', 'name': 'voltaic_pile'}, {'id': 12073, 'synset': 'voltmeter.n.01', 'name': 'voltmeter'}, {'id': 12074, 'synset': 'vomitory.n.01', 'name': 'vomitory'}, {'id': 12075, 'synset': 'von_neumann_machine.n.01', 'name': 'von_Neumann_machine'}, {'id': 12076, 'synset': 'voting_booth.n.01', 'name': 'voting_booth'}, {'id': 12077, 'synset': 'voting_machine.n.01', 'name': 'voting_machine'}, {'id': 12078, 'synset': 'voussoir.n.01', 'name': 'voussoir'}, {'id': 12079, 'synset': 'vox_angelica.n.01', 'name': 'vox_angelica'}, {'id': 12080, 'synset': 'vox_humana.n.01', 'name': 'vox_humana'}, {'id': 12081, 'synset': 'waders.n.01', 'name': 'waders'}, {'id': 12082, 'synset': 'wading_pool.n.01', 'name': 'wading_pool'}, {'id': 12083, 'synset': 'wagon.n.04', 'name': 'wagon'}, {'id': 12084, 'synset': 'wagon_tire.n.01', 'name': 'wagon_tire'}, {'id': 12085, 'synset': 'wain.n.03', 'name': 'wain'}, {'id': 12086, 'synset': 'wainscot.n.02', 'name': 'wainscot'}, {'id': 12087, 'synset': 'wainscoting.n.01', 'name': 'wainscoting'}, {'id': 12088, 'synset': 'waist_pack.n.01', 'name': 'waist_pack'}, {'id': 12089, 'synset': 'walker.n.06', 'name': 'walker'}, {'id': 12090, 'synset': 'walker.n.05', 'name': 'walker'}, {'id': 12091, 'synset': 'walker.n.04', 'name': 'walker'}, {'id': 12092, 'synset': 'walkie-talkie.n.01', 'name': 'walkie-talkie'}, {'id': 12093, 'synset': 'walk-in.n.04', 'name': 'walk-in'}, {'id': 12094, 'synset': 'walking_shoe.n.01', 'name': 'walking_shoe'}, {'id': 12095, 'synset': 'walkman.n.01', 'name': 'Walkman'}, {'id': 12096, 'synset': 'walk-up_apartment.n.01', 'name': 'walk-up_apartment'}, {'id': 12097, 'synset': 'wall.n.01', 'name': 'wall'}, {'id': 12098, 'synset': 'wall.n.07', 'name': 'wall'}, {'id': 12099, 'synset': 'wall_tent.n.01', 'name': 'wall_tent'}, {'id': 12100, 'synset': 'wall_unit.n.01', 'name': 'wall_unit'}, {'id': 12101, 'synset': 'wand.n.01', 'name': 'wand'}, {'id': 12102, 'synset': 'wankel_engine.n.01', 'name': 'Wankel_engine'}, {'id': 12103, 'synset': 'ward.n.03', 'name': 'ward'}, {'id': 12104, 'synset': 'wardroom.n.01', 'name': 'wardroom'}, {'id': 12105, 'synset': 'warehouse.n.01', 'name': 'warehouse'}, {'id': 12106, 'synset': 'warming_pan.n.01', 'name': 'warming_pan'}, {'id': 12107, 'synset': 'war_paint.n.02', 'name': 'war_paint'}, {'id': 12108, 'synset': 'warplane.n.01', 'name': 'warplane'}, {'id': 12109, 'synset': 'war_room.n.01', 'name': 'war_room'}, {'id': 12110, 'synset': 'warship.n.01', 'name': 'warship'}, {'id': 12111, 'synset': 'wash.n.01', 'name': 'wash'}, {'id': 12112, 'synset': 'wash-and-wear.n.01', 'name': 'wash-and-wear'}, {'id': 12113, 'synset': 'washbasin.n.02', 'name': 'washbasin'}, {'id': 12114, 'synset': 'washboard.n.02', 'name': 'washboard'}, {'id': 12115, 'synset': 'washboard.n.01', 'name': 'washboard'}, {'id': 12116, 'synset': 'washer.n.02', 'name': 'washer'}, {'id': 12117, 'synset': 'washhouse.n.01', 'name': 'washhouse'}, {'id': 12118, 'synset': 'washroom.n.01', 'name': 'washroom'}, {'id': 12119, 'synset': 'washstand.n.01', 'name': 'washstand'}, {'id': 12120, 'synset': 'washtub.n.01', 'name': 'washtub'}, {'id': 12121, 'synset': 'wastepaper_basket.n.01', 'name': 'wastepaper_basket'}, {'id': 12122, 'synset': 'watch_cap.n.01', 'name': 'watch_cap'}, {'id': 12123, 'synset': 'watch_case.n.01', 'name': 'watch_case'}, {'id': 12124, 'synset': 'watch_glass.n.01', 'name': 'watch_glass'}, {'id': 12125, 'synset': 'watchtower.n.01', 'name': 'watchtower'}, {'id': 12126, 'synset': 'water-base_paint.n.01', 'name': 'water-base_paint'}, {'id': 12127, 'synset': 'water_bed.n.01', 'name': 'water_bed'}, {'id': 12128, 'synset': 'water_butt.n.01', 'name': 'water_butt'}, {'id': 12129, 'synset': 'water_cart.n.01', 'name': 'water_cart'}, {'id': 12130, 'synset': 'water_chute.n.01', 'name': 'water_chute'}, {'id': 12131, 'synset': 'water_closet.n.01', 'name': 'water_closet'}, {'id': 12132, 'synset': 'watercolor.n.02', 'name': 'watercolor'}, {'id': 12133, 'synset': 'water-cooled_reactor.n.01', 'name': 'water-cooled_reactor'}, {'id': 12134, 'synset': 'water_filter.n.01', 'name': 'water_filter'}, {'id': 12135, 'synset': 'water_gauge.n.01', 'name': 'water_gauge'}, {'id': 12136, 'synset': 'water_glass.n.02', 'name': 'water_glass'}, {'id': 12137, 'synset': 'water_hazard.n.01', 'name': 'water_hazard'}, {'id': 12138, 'synset': 'watering_cart.n.01', 'name': 'watering_cart'}, {'id': 12139, 'synset': 'water_jacket.n.01', 'name': 'water_jacket'}, {'id': 12140, 'synset': 'water_jump.n.01', 'name': 'water_jump'}, {'id': 12141, 'synset': 'water_level.n.04', 'name': 'water_level'}, {'id': 12142, 'synset': 'water_meter.n.01', 'name': 'water_meter'}, {'id': 12143, 'synset': 'water_mill.n.01', 'name': 'water_mill'}, {'id': 12144, 'synset': 'waterproof.n.01', 'name': 'waterproof'}, {'id': 12145, 'synset': 'waterproofing.n.02', 'name': 'waterproofing'}, {'id': 12146, 'synset': 'water_pump.n.01', 'name': 'water_pump'}, {'id': 12147, 'synset': 'waterspout.n.03', 'name': 'waterspout'}, {'id': 12148, 'synset': 'water_wagon.n.01', 'name': 'water_wagon'}, {'id': 12149, 'synset': 'waterwheel.n.02', 'name': 'waterwheel'}, {'id': 12150, 'synset': 'waterwheel.n.01', 'name': 'waterwheel'}, {'id': 12151, 'synset': 'water_wings.n.01', 'name': 'water_wings'}, {'id': 12152, 'synset': 'waterworks.n.02', 'name': 'waterworks'}, {'id': 12153, 'synset': 'wattmeter.n.01', 'name': 'wattmeter'}, {'id': 12154, 'synset': 'waxwork.n.02', 'name': 'waxwork'}, {'id': 12155, 'synset': 'ways.n.01', 'name': 'ways'}, {'id': 12156, 'synset': 'weapon.n.01', 'name': 'weapon'}, {'id': 12157, 'synset': 'weaponry.n.01', 'name': 'weaponry'}, {'id': 12158, 'synset': 'weapons_carrier.n.01', 'name': 'weapons_carrier'}, {'id': 12159, 'synset': 'weathercock.n.01', 'name': 'weathercock'}, {'id': 12160, 'synset': 'weatherglass.n.01', 'name': 'weatherglass'}, {'id': 12161, 'synset': 'weather_satellite.n.01', 'name': 'weather_satellite'}, {'id': 12162, 'synset': 'weather_ship.n.01', 'name': 'weather_ship'}, {'id': 12163, 'synset': 'web.n.02', 'name': 'web'}, {'id': 12164, 'synset': 'web.n.06', 'name': 'web'}, {'id': 12165, 'synset': 'webbing.n.03', 'name': 'webbing'}, {'id': 12166, 'synset': 'wedge.n.06', 'name': 'wedge'}, {'id': 12167, 'synset': 'wedge.n.05', 'name': 'wedge'}, {'id': 12168, 'synset': 'wedgie.n.01', 'name': 'wedgie'}, {'id': 12169, 'synset': 'wedgwood.n.02', 'name': 'Wedgwood'}, {'id': 12170, 'synset': 'weeder.n.02', 'name': 'weeder'}, {'id': 12171, 'synset': 'weeds.n.01', 'name': 'weeds'}, {'id': 12172, 'synset': 'weekender.n.02', 'name': 'weekender'}, {'id': 12173, 'synset': 'weighbridge.n.01', 'name': 'weighbridge'}, {'id': 12174, 'synset': 'weight.n.02', 'name': 'weight'}, {'id': 12175, 'synset': 'weir.n.01', 'name': 'weir'}, {'id': 12176, 'synset': 'weir.n.02', 'name': 'weir'}, {'id': 12177, 'synset': 'welcome_wagon.n.01', 'name': 'welcome_wagon'}, {'id': 12178, 'synset': 'weld.n.03', 'name': 'weld'}, {'id': 12179, 'synset': "welder's_mask.n.01", 'name': "welder's_mask"}, {'id': 12180, 'synset': 'weldment.n.01', 'name': 'weldment'}, {'id': 12181, 'synset': 'well.n.02', 'name': 'well'}, {'id': 12182, 'synset': 'wellhead.n.02', 'name': 'wellhead'}, {'id': 12183, 'synset': 'welt.n.02', 'name': 'welt'}, {'id': 12184, 'synset': 'weston_cell.n.01', 'name': 'Weston_cell'}, {'id': 12185, 'synset': 'wet_bar.n.01', 'name': 'wet_bar'}, {'id': 12186, 'synset': 'wet-bulb_thermometer.n.01', 'name': 'wet-bulb_thermometer'}, {'id': 12187, 'synset': 'wet_cell.n.01', 'name': 'wet_cell'}, {'id': 12188, 'synset': 'wet_fly.n.01', 'name': 'wet_fly'}, {'id': 12189, 'synset': 'whaleboat.n.01', 'name': 'whaleboat'}, {'id': 12190, 'synset': 'whaler.n.02', 'name': 'whaler'}, {'id': 12191, 'synset': 'whaling_gun.n.01', 'name': 'whaling_gun'}, {'id': 12192, 'synset': 'wheel.n.04', 'name': 'wheel'}, {'id': 12193, 'synset': 'wheel_and_axle.n.01', 'name': 'wheel_and_axle'}, {'id': 12194, 'synset': 'wheeled_vehicle.n.01', 'name': 'wheeled_vehicle'}, {'id': 12195, 'synset': 'wheelwork.n.01', 'name': 'wheelwork'}, {'id': 12196, 'synset': 'wherry.n.02', 'name': 'wherry'}, {'id': 12197, 'synset': 'wherry.n.01', 'name': 'wherry'}, {'id': 12198, 'synset': 'whetstone.n.01', 'name': 'whetstone'}, {'id': 12199, 'synset': 'whiffletree.n.01', 'name': 'whiffletree'}, {'id': 12200, 'synset': 'whip.n.01', 'name': 'whip'}, {'id': 12201, 'synset': 'whipcord.n.02', 'name': 'whipcord'}, {'id': 12202, 'synset': 'whipping_post.n.01', 'name': 'whipping_post'}, {'id': 12203, 'synset': 'whipstitch.n.01', 'name': 'whipstitch'}, {'id': 12204, 'synset': 'whirler.n.02', 'name': 'whirler'}, {'id': 12205, 'synset': 'whisk.n.02', 'name': 'whisk'}, {'id': 12206, 'synset': 'whisk.n.01', 'name': 'whisk'}, {'id': 12207, 'synset': 'whiskey_bottle.n.01', 'name': 'whiskey_bottle'}, {'id': 12208, 'synset': 'whiskey_jug.n.01', 'name': 'whiskey_jug'}, {'id': 12209, 'synset': 'whispering_gallery.n.01', 'name': 'whispering_gallery'}, {'id': 12210, 'synset': 'whistle.n.04', 'name': 'whistle'}, {'id': 12211, 'synset': 'white.n.11', 'name': 'white'}, {'id': 12212, 'synset': 'white_goods.n.01', 'name': 'white_goods'}, {'id': 12213, 'synset': 'whitewash.n.02', 'name': 'whitewash'}, {'id': 12214, 'synset': 'whorehouse.n.01', 'name': 'whorehouse'}, {'id': 12215, 'synset': 'wick.n.02', 'name': 'wick'}, {'id': 12216, 'synset': 'wicker.n.02', 'name': 'wicker'}, {'id': 12217, 'synset': 'wicker_basket.n.01', 'name': 'wicker_basket'}, {'id': 12218, 'synset': 'wicket.n.02', 'name': 'wicket'}, {'id': 12219, 'synset': 'wicket.n.01', 'name': 'wicket'}, {'id': 12220, 'synset': 'wickiup.n.01', 'name': 'wickiup'}, {'id': 12221, 'synset': 'wide-angle_lens.n.01', 'name': 'wide-angle_lens'}, {'id': 12222, 'synset': 'widebody_aircraft.n.01', 'name': 'widebody_aircraft'}, {'id': 12223, 'synset': 'wide_wale.n.01', 'name': 'wide_wale'}, {'id': 12224, 'synset': "widow's_walk.n.01", 'name': "widow's_walk"}, {'id': 12225, 'synset': 'wiffle.n.01', 'name': 'Wiffle'}, {'id': 12226, 'synset': 'wigwam.n.01', 'name': 'wigwam'}, {'id': 12227, 'synset': 'wilton.n.01', 'name': 'Wilton'}, {'id': 12228, 'synset': 'wimple.n.01', 'name': 'wimple'}, {'id': 12229, 'synset': 'wincey.n.01', 'name': 'wincey'}, {'id': 12230, 'synset': 'winceyette.n.01', 'name': 'winceyette'}, {'id': 12231, 'synset': 'winch.n.01', 'name': 'winch'}, {'id': 12232, 'synset': 'winchester.n.02', 'name': 'Winchester'}, {'id': 12233, 'synset': 'windbreak.n.01', 'name': 'windbreak'}, {'id': 12234, 'synset': 'winder.n.02', 'name': 'winder'}, {'id': 12235, 'synset': 'wind_instrument.n.01', 'name': 'wind_instrument'}, {'id': 12236, 'synset': 'windjammer.n.01', 'name': 'windjammer'}, {'id': 12237, 'synset': 'windmill.n.02', 'name': 'windmill'}, {'id': 12238, 'synset': 'window.n.01', 'name': 'window'}, {'id': 12239, 'synset': 'window.n.08', 'name': 'window'}, {'id': 12240, 'synset': 'window_blind.n.01', 'name': 'window_blind'}, {'id': 12241, 'synset': 'window_envelope.n.01', 'name': 'window_envelope'}, {'id': 12242, 'synset': 'window_frame.n.01', 'name': 'window_frame'}, {'id': 12243, 'synset': 'window_screen.n.01', 'name': 'window_screen'}, {'id': 12244, 'synset': 'window_seat.n.01', 'name': 'window_seat'}, {'id': 12245, 'synset': 'window_shade.n.01', 'name': 'window_shade'}, {'id': 12246, 'synset': 'windowsill.n.01', 'name': 'windowsill'}, {'id': 12247, 'synset': 'windshield.n.01', 'name': 'windshield'}, {'id': 12248, 'synset': 'windsor_chair.n.01', 'name': 'Windsor_chair'}, {'id': 12249, 'synset': 'windsor_knot.n.01', 'name': 'Windsor_knot'}, {'id': 12250, 'synset': 'windsor_tie.n.01', 'name': 'Windsor_tie'}, {'id': 12251, 'synset': 'wind_tee.n.01', 'name': 'wind_tee'}, {'id': 12252, 'synset': 'wind_tunnel.n.01', 'name': 'wind_tunnel'}, {'id': 12253, 'synset': 'wind_turbine.n.01', 'name': 'wind_turbine'}, {'id': 12254, 'synset': 'wine_bar.n.01', 'name': 'wine_bar'}, {'id': 12255, 'synset': 'wine_cask.n.01', 'name': 'wine_cask'}, {'id': 12256, 'synset': 'winepress.n.01', 'name': 'winepress'}, {'id': 12257, 'synset': 'winery.n.01', 'name': 'winery'}, {'id': 12258, 'synset': 'wineskin.n.01', 'name': 'wineskin'}, {'id': 12259, 'synset': 'wing.n.02', 'name': 'wing'}, {'id': 12260, 'synset': 'wing_chair.n.01', 'name': 'wing_chair'}, {'id': 12261, 'synset': 'wing_nut.n.02', 'name': 'wing_nut'}, {'id': 12262, 'synset': 'wing_tip.n.02', 'name': 'wing_tip'}, {'id': 12263, 'synset': 'wing_tip.n.01', 'name': 'wing_tip'}, {'id': 12264, 'synset': 'wiper.n.02', 'name': 'wiper'}, {'id': 12265, 'synset': 'wiper_motor.n.01', 'name': 'wiper_motor'}, {'id': 12266, 'synset': 'wire.n.01', 'name': 'wire'}, {'id': 12267, 'synset': 'wire.n.02', 'name': 'wire'}, {'id': 12268, 'synset': 'wire_cloth.n.01', 'name': 'wire_cloth'}, {'id': 12269, 'synset': 'wire_cutter.n.01', 'name': 'wire_cutter'}, {'id': 12270, 'synset': 'wire_gauge.n.01', 'name': 'wire_gauge'}, {'id': 12271, 'synset': 'wireless_local_area_network.n.01', 'name': 'wireless_local_area_network'}, {'id': 12272, 'synset': 'wire_matrix_printer.n.01', 'name': 'wire_matrix_printer'}, {'id': 12273, 'synset': 'wire_recorder.n.01', 'name': 'wire_recorder'}, {'id': 12274, 'synset': 'wire_stripper.n.01', 'name': 'wire_stripper'}, {'id': 12275, 'synset': 'wirework.n.01', 'name': 'wirework'}, {'id': 12276, 'synset': 'wiring.n.01', 'name': 'wiring'}, {'id': 12277, 'synset': 'wishing_cap.n.01', 'name': 'wishing_cap'}, {'id': 12278, 'synset': 'witness_box.n.01', 'name': 'witness_box'}, {'id': 12279, 'synset': "woman's_clothing.n.01", 'name': "woman's_clothing"}, {'id': 12280, 'synset': 'wood.n.08', 'name': 'wood'}, {'id': 12281, 'synset': 'woodcarving.n.01', 'name': 'woodcarving'}, {'id': 12282, 'synset': 'wood_chisel.n.01', 'name': 'wood_chisel'}, {'id': 12283, 'synset': 'woodenware.n.01', 'name': 'woodenware'}, {'id': 12284, 'synset': 'woodscrew.n.01', 'name': 'woodscrew'}, {'id': 12285, 'synset': 'woodshed.n.01', 'name': 'woodshed'}, {'id': 12286, 'synset': 'wood_vise.n.01', 'name': 'wood_vise'}, {'id': 12287, 'synset': 'woodwind.n.01', 'name': 'woodwind'}, {'id': 12288, 'synset': 'woof.n.01', 'name': 'woof'}, {'id': 12289, 'synset': 'woofer.n.01', 'name': 'woofer'}, {'id': 12290, 'synset': 'wool.n.01', 'name': 'wool'}, {'id': 12291, 'synset': 'workbasket.n.01', 'name': 'workbasket'}, {'id': 12292, 'synset': 'workbench.n.01', 'name': 'workbench'}, {'id': 12293, 'synset': 'work-clothing.n.01', 'name': 'work-clothing'}, {'id': 12294, 'synset': 'workhouse.n.02', 'name': 'workhouse'}, {'id': 12295, 'synset': 'workhouse.n.01', 'name': 'workhouse'}, {'id': 12296, 'synset': 'workpiece.n.01', 'name': 'workpiece'}, {'id': 12297, 'synset': 'workroom.n.01', 'name': 'workroom'}, {'id': 12298, 'synset': 'works.n.04', 'name': 'works'}, {'id': 12299, 'synset': 'work-shirt.n.01', 'name': 'work-shirt'}, {'id': 12300, 'synset': 'workstation.n.01', 'name': 'workstation'}, {'id': 12301, 'synset': 'worktable.n.01', 'name': 'worktable'}, {'id': 12302, 'synset': 'workwear.n.01', 'name': 'workwear'}, {'id': 12303, 'synset': 'world_wide_web.n.01', 'name': 'World_Wide_Web'}, {'id': 12304, 'synset': 'worm_fence.n.01', 'name': 'worm_fence'}, {'id': 12305, 'synset': 'worm_gear.n.01', 'name': 'worm_gear'}, {'id': 12306, 'synset': 'worm_wheel.n.01', 'name': 'worm_wheel'}, {'id': 12307, 'synset': 'worsted.n.01', 'name': 'worsted'}, {'id': 12308, 'synset': 'worsted.n.02', 'name': 'worsted'}, {'id': 12309, 'synset': 'wrap.n.01', 'name': 'wrap'}, {'id': 12310, 'synset': 'wraparound.n.01', 'name': 'wraparound'}, {'id': 12311, 'synset': 'wrapping.n.01', 'name': 'wrapping'}, {'id': 12312, 'synset': 'wreck.n.04', 'name': 'wreck'}, {'id': 12313, 'synset': 'wrestling_mat.n.01', 'name': 'wrestling_mat'}, {'id': 12314, 'synset': 'wringer.n.01', 'name': 'wringer'}, {'id': 12315, 'synset': 'wrist_pad.n.01', 'name': 'wrist_pad'}, {'id': 12316, 'synset': 'wrist_pin.n.01', 'name': 'wrist_pin'}, {'id': 12317, 'synset': 'wristwatch.n.01', 'name': 'wristwatch'}, {'id': 12318, 'synset': 'writing_arm.n.01', 'name': 'writing_arm'}, {'id': 12319, 'synset': 'writing_desk.n.02', 'name': 'writing_desk'}, {'id': 12320, 'synset': 'writing_desk.n.01', 'name': 'writing_desk'}, {'id': 12321, 'synset': 'writing_implement.n.01', 'name': 'writing_implement'}, {'id': 12322, 'synset': 'xerographic_printer.n.01', 'name': 'xerographic_printer'}, {'id': 12323, 'synset': 'xerox.n.02', 'name': 'Xerox'}, {'id': 12324, 'synset': 'x-ray_film.n.01', 'name': 'X-ray_film'}, {'id': 12325, 'synset': 'x-ray_machine.n.01', 'name': 'X-ray_machine'}, {'id': 12326, 'synset': 'x-ray_tube.n.01', 'name': 'X-ray_tube'}, {'id': 12327, 'synset': 'yacht_chair.n.01', 'name': 'yacht_chair'}, {'id': 12328, 'synset': 'yagi.n.01', 'name': 'yagi'}, {'id': 12329, 'synset': 'yard.n.09', 'name': 'yard'}, {'id': 12330, 'synset': 'yard.n.08', 'name': 'yard'}, {'id': 12331, 'synset': 'yardarm.n.01', 'name': 'yardarm'}, {'id': 12332, 'synset': 'yard_marker.n.01', 'name': 'yard_marker'}, {'id': 12333, 'synset': 'yardstick.n.02', 'name': 'yardstick'}, {'id': 12334, 'synset': 'yarmulke.n.01', 'name': 'yarmulke'}, {'id': 12335, 'synset': 'yashmak.n.01', 'name': 'yashmak'}, {'id': 12336, 'synset': 'yataghan.n.01', 'name': 'yataghan'}, {'id': 12337, 'synset': 'yawl.n.02', 'name': 'yawl'}, {'id': 12338, 'synset': 'yawl.n.01', 'name': 'yawl'}, {'id': 12339, 'synset': 'yoke.n.01', 'name': 'yoke'}, {'id': 12340, 'synset': 'yoke.n.06', 'name': 'yoke'}, {'id': 12341, 'synset': 'yurt.n.01', 'name': 'yurt'}, {'id': 12342, 'synset': 'zamboni.n.01', 'name': 'Zamboni'}, {'id': 12343, 'synset': 'zero.n.04', 'name': 'zero'}, {'id': 12344, 'synset': 'ziggurat.n.01', 'name': 'ziggurat'}, {'id': 12345, 'synset': 'zill.n.01', 'name': 'zill'}, {'id': 12346, 'synset': 'zip_gun.n.01', 'name': 'zip_gun'}, {'id': 12347, 'synset': 'zither.n.01', 'name': 'zither'}, {'id': 12348, 'synset': 'zoot_suit.n.01', 'name': 'zoot_suit'}, {'id': 12349, 'synset': 'shading.n.01', 'name': 'shading'}, {'id': 12350, 'synset': 'grain.n.10', 'name': 'grain'}, {'id': 12351, 'synset': 'wood_grain.n.01', 'name': 'wood_grain'}, {'id': 12352, 'synset': 'graining.n.01', 'name': 'graining'}, {'id': 12353, 'synset': 'marbleization.n.01', 'name': 'marbleization'}, {'id': 12354, 'synset': 'light.n.07', 'name': 'light'}, {'id': 12355, 'synset': 'aura.n.02', 'name': 'aura'}, {'id': 12356, 'synset': 'sunniness.n.01', 'name': 'sunniness'}, {'id': 12357, 'synset': 'glint.n.02', 'name': 'glint'}, {'id': 12358, 'synset': 'opalescence.n.01', 'name': 'opalescence'}, {'id': 12359, 'synset': 'polish.n.01', 'name': 'polish'}, {'id': 12360, 'synset': 'primary_color_for_pigments.n.01', 'name': 'primary_color_for_pigments'}, {'id': 12361, 'synset': 'primary_color_for_light.n.01', 'name': 'primary_color_for_light'}, {'id': 12362, 'synset': 'colorlessness.n.01', 'name': 'colorlessness'}, {'id': 12363, 'synset': 'mottle.n.01', 'name': 'mottle'}, {'id': 12364, 'synset': 'achromia.n.01', 'name': 'achromia'}, {'id': 12365, 'synset': 'shade.n.02', 'name': 'shade'}, {'id': 12366, 'synset': 'chromatic_color.n.01', 'name': 'chromatic_color'}, {'id': 12367, 'synset': 'black.n.01', 'name': 'black'}, {'id': 12368, 'synset': 'coal_black.n.01', 'name': 'coal_black'}, {'id': 12369, 'synset': 'alabaster.n.03', 'name': 'alabaster'}, {'id': 12370, 'synset': 'bone.n.03', 'name': 'bone'}, {'id': 12371, 'synset': 'gray.n.01', 'name': 'gray'}, {'id': 12372, 'synset': 'ash_grey.n.01', 'name': 'ash_grey'}, {'id': 12373, 'synset': 'charcoal.n.03', 'name': 'charcoal'}, {'id': 12374, 'synset': 'sanguine.n.01', 'name': 'sanguine'}, {'id': 12375, 'synset': 'turkey_red.n.01', 'name': 'Turkey_red'}, {'id': 12376, 'synset': 'crimson.n.01', 'name': 'crimson'}, {'id': 12377, 'synset': 'dark_red.n.01', 'name': 'dark_red'}, {'id': 12378, 'synset': 'claret.n.01', 'name': 'claret'}, {'id': 12379, 'synset': 'fuschia.n.01', 'name': 'fuschia'}, {'id': 12380, 'synset': 'maroon.n.02', 'name': 'maroon'}, {'id': 12381, 'synset': 'orange.n.02', 'name': 'orange'}, {'id': 12382, 'synset': 'reddish_orange.n.01', 'name': 'reddish_orange'}, {'id': 12383, 'synset': 'yellow.n.01', 'name': 'yellow'}, {'id': 12384, 'synset': 'gamboge.n.02', 'name': 'gamboge'}, {'id': 12385, 'synset': 'pale_yellow.n.01', 'name': 'pale_yellow'}, {'id': 12386, 'synset': 'green.n.01', 'name': 'green'}, {'id': 12387, 'synset': 'greenishness.n.01', 'name': 'greenishness'}, {'id': 12388, 'synset': 'sea_green.n.01', 'name': 'sea_green'}, {'id': 12389, 'synset': 'sage_green.n.01', 'name': 'sage_green'}, {'id': 12390, 'synset': 'bottle_green.n.01', 'name': 'bottle_green'}, {'id': 12391, 'synset': 'emerald.n.03', 'name': 'emerald'}, {'id': 12392, 'synset': 'olive_green.n.01', 'name': 'olive_green'}, {'id': 12393, 'synset': 'jade_green.n.01', 'name': 'jade_green'}, {'id': 12394, 'synset': 'blue.n.01', 'name': 'blue'}, {'id': 12395, 'synset': 'azure.n.01', 'name': 'azure'}, {'id': 12396, 'synset': 'steel_blue.n.01', 'name': 'steel_blue'}, {'id': 12397, 'synset': 'greenish_blue.n.01', 'name': 'greenish_blue'}, {'id': 12398, 'synset': 'purplish_blue.n.01', 'name': 'purplish_blue'}, {'id': 12399, 'synset': 'purple.n.01', 'name': 'purple'}, {'id': 12400, 'synset': 'tyrian_purple.n.02', 'name': 'Tyrian_purple'}, {'id': 12401, 'synset': 'indigo.n.03', 'name': 'indigo'}, {'id': 12402, 'synset': 'lavender.n.02', 'name': 'lavender'}, {'id': 12403, 'synset': 'reddish_purple.n.01', 'name': 'reddish_purple'}, {'id': 12404, 'synset': 'pink.n.01', 'name': 'pink'}, {'id': 12405, 'synset': 'carnation.n.02', 'name': 'carnation'}, {'id': 12406, 'synset': 'rose.n.03', 'name': 'rose'}, {'id': 12407, 'synset': 'chestnut.n.04', 'name': 'chestnut'}, {'id': 12408, 'synset': 'chocolate.n.03', 'name': 'chocolate'}, {'id': 12409, 'synset': 'light_brown.n.01', 'name': 'light_brown'}, {'id': 12410, 'synset': 'tan.n.02', 'name': 'tan'}, {'id': 12411, 'synset': 'beige.n.01', 'name': 'beige'}, {'id': 12412, 'synset': 'reddish_brown.n.01', 'name': 'reddish_brown'}, {'id': 12413, 'synset': 'brick_red.n.01', 'name': 'brick_red'}, {'id': 12414, 'synset': 'copper.n.04', 'name': 'copper'}, {'id': 12415, 'synset': 'indian_red.n.03', 'name': 'Indian_red'}, {'id': 12416, 'synset': 'puce.n.01', 'name': 'puce'}, {'id': 12417, 'synset': 'olive.n.05', 'name': 'olive'}, {'id': 12418, 'synset': 'ultramarine.n.02', 'name': 'ultramarine'}, {'id': 12419, 'synset': 'complementary_color.n.01', 'name': 'complementary_color'}, {'id': 12420, 'synset': 'pigmentation.n.02', 'name': 'pigmentation'}, {'id': 12421, 'synset': 'complexion.n.01', 'name': 'complexion'}, {'id': 12422, 'synset': 'ruddiness.n.01', 'name': 'ruddiness'}, {'id': 12423, 'synset': 'nonsolid_color.n.01', 'name': 'nonsolid_color'}, {'id': 12424, 'synset': 'aposematic_coloration.n.01', 'name': 'aposematic_coloration'}, {'id': 12425, 'synset': 'cryptic_coloration.n.01', 'name': 'cryptic_coloration'}, {'id': 12426, 'synset': 'ring.n.01', 'name': 'ring'}, {'id': 12427, 'synset': 'center_of_curvature.n.01', 'name': 'center_of_curvature'}, {'id': 12428, 'synset': 'cadaver.n.01', 'name': 'cadaver'}, {'id': 12429, 'synset': 'mandibular_notch.n.01', 'name': 'mandibular_notch'}, {'id': 12430, 'synset': 'rib.n.05', 'name': 'rib'}, {'id': 12431, 'synset': 'skin.n.01', 'name': 'skin'}, {'id': 12432, 'synset': 'skin_graft.n.01', 'name': 'skin_graft'}, {'id': 12433, 'synset': 'epidermal_cell.n.01', 'name': 'epidermal_cell'}, {'id': 12434, 'synset': 'melanocyte.n.01', 'name': 'melanocyte'}, {'id': 12435, 'synset': 'prickle_cell.n.01', 'name': 'prickle_cell'}, {'id': 12436, 'synset': 'columnar_cell.n.01', 'name': 'columnar_cell'}, {'id': 12437, 'synset': 'spongioblast.n.01', 'name': 'spongioblast'}, {'id': 12438, 'synset': 'squamous_cell.n.01', 'name': 'squamous_cell'}, {'id': 12439, 'synset': 'amyloid_plaque.n.01', 'name': 'amyloid_plaque'}, {'id': 12440, 'synset': 'dental_plaque.n.01', 'name': 'dental_plaque'}, {'id': 12441, 'synset': 'macule.n.01', 'name': 'macule'}, {'id': 12442, 'synset': 'freckle.n.01', 'name': 'freckle'}, {'id': 12443, 'synset': 'bouffant.n.01', 'name': 'bouffant'}, {'id': 12444, 'synset': 'sausage_curl.n.01', 'name': 'sausage_curl'}, {'id': 12445, 'synset': 'forelock.n.01', 'name': 'forelock'}, {'id': 12446, 'synset': 'spit_curl.n.01', 'name': 'spit_curl'}, {'id': 12447, 'synset': 'pigtail.n.01', 'name': 'pigtail'}, {'id': 12448, 'synset': 'pageboy.n.02', 'name': 'pageboy'}, {'id': 12449, 'synset': 'pompadour.n.02', 'name': 'pompadour'}, {'id': 12450, 'synset': 'thatch.n.01', 'name': 'thatch'}, {'id': 12451, 'synset': 'soup-strainer.n.01', 'name': 'soup-strainer'}, {'id': 12452, 'synset': 'mustachio.n.01', 'name': 'mustachio'}, {'id': 12453, 'synset': 'walrus_mustache.n.01', 'name': 'walrus_mustache'}, {'id': 12454, 'synset': 'stubble.n.02', 'name': 'stubble'}, {'id': 12455, 'synset': 'vandyke_beard.n.01', 'name': 'vandyke_beard'}, {'id': 12456, 'synset': 'soul_patch.n.01', 'name': 'soul_patch'}, {'id': 12457, 'synset': 'esophageal_smear.n.01', 'name': 'esophageal_smear'}, {'id': 12458, 'synset': 'paraduodenal_smear.n.01', 'name': 'paraduodenal_smear'}, {'id': 12459, 'synset': 'specimen.n.02', 'name': 'specimen'}, {'id': 12460, 'synset': 'punctum.n.01', 'name': 'punctum'}, {'id': 12461, 'synset': 'glenoid_fossa.n.02', 'name': 'glenoid_fossa'}, {'id': 12462, 'synset': 'diastema.n.01', 'name': 'diastema'}, {'id': 12463, 'synset': 'marrow.n.01', 'name': 'marrow'}, {'id': 12464, 'synset': 'mouth.n.01', 'name': 'mouth'}, {'id': 12465, 'synset': 'canthus.n.01', 'name': 'canthus'}, {'id': 12466, 'synset': 'milk.n.02', 'name': 'milk'}, {'id': 12467, 'synset': "mother's_milk.n.01", 'name': "mother's_milk"}, {'id': 12468, 'synset': 'colostrum.n.01', 'name': 'colostrum'}, {'id': 12469, 'synset': 'vein.n.01', 'name': 'vein'}, {'id': 12470, 'synset': 'ganglion_cell.n.01', 'name': 'ganglion_cell'}, {'id': 12471, 'synset': 'x_chromosome.n.01', 'name': 'X_chromosome'}, {'id': 12472, 'synset': 'embryonic_cell.n.01', 'name': 'embryonic_cell'}, {'id': 12473, 'synset': 'myeloblast.n.01', 'name': 'myeloblast'}, {'id': 12474, 'synset': 'sideroblast.n.01', 'name': 'sideroblast'}, {'id': 12475, 'synset': 'osteocyte.n.01', 'name': 'osteocyte'}, {'id': 12476, 'synset': 'megalocyte.n.01', 'name': 'megalocyte'}, {'id': 12477, 'synset': 'leukocyte.n.01', 'name': 'leukocyte'}, {'id': 12478, 'synset': 'histiocyte.n.01', 'name': 'histiocyte'}, {'id': 12479, 'synset': 'fixed_phagocyte.n.01', 'name': 'fixed_phagocyte'}, {'id': 12480, 'synset': 'lymphocyte.n.01', 'name': 'lymphocyte'}, {'id': 12481, 'synset': 'monoblast.n.01', 'name': 'monoblast'}, {'id': 12482, 'synset': 'neutrophil.n.01', 'name': 'neutrophil'}, {'id': 12483, 'synset': 'microphage.n.01', 'name': 'microphage'}, {'id': 12484, 'synset': 'sickle_cell.n.01', 'name': 'sickle_cell'}, {'id': 12485, 'synset': 'siderocyte.n.01', 'name': 'siderocyte'}, {'id': 12486, 'synset': 'spherocyte.n.01', 'name': 'spherocyte'}, {'id': 12487, 'synset': 'ootid.n.01', 'name': 'ootid'}, {'id': 12488, 'synset': 'oocyte.n.01', 'name': 'oocyte'}, {'id': 12489, 'synset': 'spermatid.n.01', 'name': 'spermatid'}, {'id': 12490, 'synset': 'leydig_cell.n.01', 'name': 'Leydig_cell'}, {'id': 12491, 'synset': 'striated_muscle_cell.n.01', 'name': 'striated_muscle_cell'}, {'id': 12492, 'synset': 'smooth_muscle_cell.n.01', 'name': 'smooth_muscle_cell'}, {'id': 12493, 'synset': "ranvier's_nodes.n.01", 'name': "Ranvier's_nodes"}, {'id': 12494, 'synset': 'neuroglia.n.01', 'name': 'neuroglia'}, {'id': 12495, 'synset': 'astrocyte.n.01', 'name': 'astrocyte'}, {'id': 12496, 'synset': 'protoplasmic_astrocyte.n.01', 'name': 'protoplasmic_astrocyte'}, {'id': 12497, 'synset': 'oligodendrocyte.n.01', 'name': 'oligodendrocyte'}, {'id': 12498, 'synset': 'proprioceptor.n.01', 'name': 'proprioceptor'}, {'id': 12499, 'synset': 'dendrite.n.01', 'name': 'dendrite'}, {'id': 12500, 'synset': 'sensory_fiber.n.01', 'name': 'sensory_fiber'}, {'id': 12501, 'synset': 'subarachnoid_space.n.01', 'name': 'subarachnoid_space'}, {'id': 12502, 'synset': 'cerebral_cortex.n.01', 'name': 'cerebral_cortex'}, {'id': 12503, 'synset': 'renal_cortex.n.01', 'name': 'renal_cortex'}, {'id': 12504, 'synset': 'prepuce.n.02', 'name': 'prepuce'}, {'id': 12505, 'synset': 'head.n.01', 'name': 'head'}, {'id': 12506, 'synset': 'scalp.n.01', 'name': 'scalp'}, {'id': 12507, 'synset': 'frontal_eminence.n.01', 'name': 'frontal_eminence'}, {'id': 12508, 'synset': 'suture.n.01', 'name': 'suture'}, {'id': 12509, 'synset': 'foramen_magnum.n.01', 'name': 'foramen_magnum'}, {'id': 12510, 'synset': 'esophagogastric_junction.n.01', 'name': 'esophagogastric_junction'}, {'id': 12511, 'synset': 'heel.n.02', 'name': 'heel'}, {'id': 12512, 'synset': 'cuticle.n.01', 'name': 'cuticle'}, {'id': 12513, 'synset': 'hangnail.n.01', 'name': 'hangnail'}, {'id': 12514, 'synset': 'exoskeleton.n.01', 'name': 'exoskeleton'}, {'id': 12515, 'synset': 'abdominal_wall.n.01', 'name': 'abdominal_wall'}, {'id': 12516, 'synset': 'lemon.n.04', 'name': 'lemon'}, {'id': 12517, 'synset': 'coordinate_axis.n.01', 'name': 'coordinate_axis'}, {'id': 12518, 'synset': 'landscape.n.04', 'name': 'landscape'}, {'id': 12519, 'synset': 'medium.n.01', 'name': 'medium'}, {'id': 12520, 'synset': 'vehicle.n.02', 'name': 'vehicle'}, {'id': 12521, 'synset': 'paper.n.04', 'name': 'paper'}, {'id': 12522, 'synset': 'channel.n.01', 'name': 'channel'}, {'id': 12523, 'synset': 'film.n.02', 'name': 'film'}, {'id': 12524, 'synset': 'silver_screen.n.01', 'name': 'silver_screen'}, {'id': 12525, 'synset': 'free_press.n.01', 'name': 'free_press'}, {'id': 12526, 'synset': 'press.n.02', 'name': 'press'}, {'id': 12527, 'synset': 'print_media.n.01', 'name': 'print_media'}, {'id': 12528, 'synset': 'storage_medium.n.01', 'name': 'storage_medium'}, {'id': 12529, 'synset': 'magnetic_storage_medium.n.01', 'name': 'magnetic_storage_medium'}, {'id': 12530, 'synset': 'journalism.n.01', 'name': 'journalism'}, {'id': 12531, 'synset': 'fleet_street.n.02', 'name': 'Fleet_Street'}, {'id': 12532, 'synset': 'photojournalism.n.01', 'name': 'photojournalism'}, {'id': 12533, 'synset': 'news_photography.n.01', 'name': 'news_photography'}, {'id': 12534, 'synset': 'rotogravure.n.02', 'name': 'rotogravure'}, {'id': 12535, 'synset': 'daily.n.01', 'name': 'daily'}, {'id': 12536, 'synset': 'gazette.n.01', 'name': 'gazette'}, {'id': 12537, 'synset': 'school_newspaper.n.01', 'name': 'school_newspaper'}, {'id': 12538, 'synset': 'tabloid.n.02', 'name': 'tabloid'}, {'id': 12539, 'synset': 'yellow_journalism.n.01', 'name': 'yellow_journalism'}, {'id': 12540, 'synset': 'telecommunication.n.01', 'name': 'telecommunication'}, {'id': 12541, 'synset': 'telephone.n.02', 'name': 'telephone'}, {'id': 12542, 'synset': 'voice_mail.n.01', 'name': 'voice_mail'}, {'id': 12543, 'synset': 'call.n.01', 'name': 'call'}, {'id': 12544, 'synset': 'call-back.n.01', 'name': 'call-back'}, {'id': 12545, 'synset': 'collect_call.n.01', 'name': 'collect_call'}, {'id': 12546, 'synset': 'call_forwarding.n.01', 'name': 'call_forwarding'}, {'id': 12547, 'synset': 'call-in.n.01', 'name': 'call-in'}, {'id': 12548, 'synset': 'call_waiting.n.01', 'name': 'call_waiting'}, {'id': 12549, 'synset': 'crank_call.n.01', 'name': 'crank_call'}, {'id': 12550, 'synset': 'local_call.n.01', 'name': 'local_call'}, {'id': 12551, 'synset': 'long_distance.n.01', 'name': 'long_distance'}, {'id': 12552, 'synset': 'toll_call.n.01', 'name': 'toll_call'}, {'id': 12553, 'synset': 'wake-up_call.n.02', 'name': 'wake-up_call'}, {'id': 12554, 'synset': 'three-way_calling.n.01', 'name': 'three-way_calling'}, {'id': 12555, 'synset': 'telegraphy.n.01', 'name': 'telegraphy'}, {'id': 12556, 'synset': 'cable.n.01', 'name': 'cable'}, {'id': 12557, 'synset': 'wireless.n.02', 'name': 'wireless'}, {'id': 12558, 'synset': 'radiotelegraph.n.01', 'name': 'radiotelegraph'}, {'id': 12559, 'synset': 'radiotelephone.n.01', 'name': 'radiotelephone'}, {'id': 12560, 'synset': 'broadcasting.n.02', 'name': 'broadcasting'}, {'id': 12561, 'synset': 'rediffusion.n.01', 'name': 'Rediffusion'}, {'id': 12562, 'synset': 'multiplex.n.01', 'name': 'multiplex'}, {'id': 12563, 'synset': 'radio.n.01', 'name': 'radio'}, {'id': 12564, 'synset': 'television.n.01', 'name': 'television'}, {'id': 12565, 'synset': 'cable_television.n.01', 'name': 'cable_television'}, {'id': 12566, 'synset': 'high-definition_television.n.01', 'name': 'high-definition_television'}, {'id': 12567, 'synset': 'reception.n.03', 'name': 'reception'}, {'id': 12568, 'synset': 'signal_detection.n.01', 'name': 'signal_detection'}, {'id': 12569, 'synset': 'hakham.n.01', 'name': 'Hakham'}, {'id': 12570, 'synset': 'web_site.n.01', 'name': 'web_site'}, {'id': 12571, 'synset': 'chat_room.n.01', 'name': 'chat_room'}, {'id': 12572, 'synset': 'portal_site.n.01', 'name': 'portal_site'}, {'id': 12573, 'synset': 'jotter.n.01', 'name': 'jotter'}, {'id': 12574, 'synset': 'breviary.n.01', 'name': 'breviary'}, {'id': 12575, 'synset': 'wordbook.n.01', 'name': 'wordbook'}, {'id': 12576, 'synset': 'desk_dictionary.n.01', 'name': 'desk_dictionary'}, {'id': 12577, 'synset': 'reckoner.n.02', 'name': 'reckoner'}, {'id': 12578, 'synset': 'document.n.01', 'name': 'document'}, {'id': 12579, 'synset': 'album.n.01', 'name': 'album'}, {'id': 12580, 'synset': 'concept_album.n.01', 'name': 'concept_album'}, {'id': 12581, 'synset': 'rock_opera.n.01', 'name': 'rock_opera'}, {'id': 12582, 'synset': 'tribute_album.n.01', 'name': 'tribute_album'}, {'id': 12583, 'synset': 'magazine.n.01', 'name': 'magazine'}, {'id': 12584, 'synset': 'colour_supplement.n.01', 'name': 'colour_supplement'}, {'id': 12585, 'synset': 'news_magazine.n.01', 'name': 'news_magazine'}, {'id': 12586, 'synset': 'pulp.n.04', 'name': 'pulp'}, {'id': 12587, 'synset': 'slick.n.02', 'name': 'slick'}, {'id': 12588, 'synset': 'trade_magazine.n.01', 'name': 'trade_magazine'}, {'id': 12589, 'synset': 'movie.n.01', 'name': 'movie'}, {'id': 12590, 'synset': 'outtake.n.01', 'name': 'outtake'}, {'id': 12591, 'synset': "shoot-'em-up.n.01", 'name': "shoot-'em-up"}, {'id': 12592, 'synset': 'spaghetti_western.n.01', 'name': 'spaghetti_Western'}, {'id': 12593, 'synset': 'encyclical.n.01', 'name': 'encyclical'}, {'id': 12594, 'synset': 'crossword_puzzle.n.01', 'name': 'crossword_puzzle'}, {'id': 12595, 'synset': 'sign.n.02', 'name': 'sign'}, {'id': 12596, 'synset': 'swastika.n.01', 'name': 'swastika'}, {'id': 12597, 'synset': 'concert.n.01', 'name': 'concert'}, {'id': 12598, 'synset': 'artwork.n.01', 'name': 'artwork'}, {'id': 12599, 'synset': 'lobe.n.03', 'name': 'lobe'}, {'id': 12600, 'synset': 'book_jacket.n.01', 'name': 'book_jacket'}, {'id': 12601, 'synset': 'cairn.n.01', 'name': 'cairn'}, {'id': 12602, 'synset': 'three-day_event.n.01', 'name': 'three-day_event'}, {'id': 12603, 'synset': 'comfort_food.n.01', 'name': 'comfort_food'}, {'id': 12604, 'synset': 'comestible.n.01', 'name': 'comestible'}, {'id': 12605, 'synset': 'tuck.n.01', 'name': 'tuck'}, {'id': 12606, 'synset': 'course.n.07', 'name': 'course'}, {'id': 12607, 'synset': 'dainty.n.01', 'name': 'dainty'}, {'id': 12608, 'synset': 'dish.n.02', 'name': 'dish'}, {'id': 12609, 'synset': 'fast_food.n.01', 'name': 'fast_food'}, {'id': 12610, 'synset': 'finger_food.n.01', 'name': 'finger_food'}, {'id': 12611, 'synset': 'ingesta.n.01', 'name': 'ingesta'}, {'id': 12612, 'synset': 'kosher.n.01', 'name': 'kosher'}, {'id': 12613, 'synset': 'fare.n.04', 'name': 'fare'}, {'id': 12614, 'synset': 'diet.n.03', 'name': 'diet'}, {'id': 12615, 'synset': 'diet.n.01', 'name': 'diet'}, {'id': 12616, 'synset': 'dietary.n.01', 'name': 'dietary'}, {'id': 12617, 'synset': 'balanced_diet.n.01', 'name': 'balanced_diet'}, {'id': 12618, 'synset': 'bland_diet.n.01', 'name': 'bland_diet'}, {'id': 12619, 'synset': 'clear_liquid_diet.n.01', 'name': 'clear_liquid_diet'}, {'id': 12620, 'synset': 'diabetic_diet.n.01', 'name': 'diabetic_diet'}, {'id': 12621, 'synset': 'dietary_supplement.n.01', 'name': 'dietary_supplement'}, {'id': 12622, 'synset': 'carbohydrate_loading.n.01', 'name': 'carbohydrate_loading'}, {'id': 12623, 'synset': 'fad_diet.n.01', 'name': 'fad_diet'}, {'id': 12624, 'synset': 'gluten-free_diet.n.01', 'name': 'gluten-free_diet'}, {'id': 12625, 'synset': 'high-protein_diet.n.01', 'name': 'high-protein_diet'}, {'id': 12626, 'synset': 'high-vitamin_diet.n.01', 'name': 'high-vitamin_diet'}, {'id': 12627, 'synset': 'light_diet.n.01', 'name': 'light_diet'}, {'id': 12628, 'synset': 'liquid_diet.n.01', 'name': 'liquid_diet'}, {'id': 12629, 'synset': 'low-calorie_diet.n.01', 'name': 'low-calorie_diet'}, {'id': 12630, 'synset': 'low-fat_diet.n.01', 'name': 'low-fat_diet'}, {'id': 12631, 'synset': 'low-sodium_diet.n.01', 'name': 'low-sodium_diet'}, {'id': 12632, 'synset': 'macrobiotic_diet.n.01', 'name': 'macrobiotic_diet'}, {'id': 12633, 'synset': 'reducing_diet.n.01', 'name': 'reducing_diet'}, {'id': 12634, 'synset': 'soft_diet.n.01', 'name': 'soft_diet'}, {'id': 12635, 'synset': 'vegetarianism.n.01', 'name': 'vegetarianism'}, {'id': 12636, 'synset': 'menu.n.02', 'name': 'menu'}, {'id': 12637, 'synset': 'chow.n.02', 'name': 'chow'}, {'id': 12638, 'synset': 'board.n.04', 'name': 'board'}, {'id': 12639, 'synset': 'mess.n.04', 'name': 'mess'}, {'id': 12640, 'synset': 'ration.n.01', 'name': 'ration'}, {'id': 12641, 'synset': 'field_ration.n.01', 'name': 'field_ration'}, {'id': 12642, 'synset': 'k_ration.n.01', 'name': 'K_ration'}, {'id': 12643, 'synset': 'c-ration.n.01', 'name': 'C-ration'}, {'id': 12644, 'synset': 'foodstuff.n.02', 'name': 'foodstuff'}, {'id': 12645, 'synset': 'starches.n.01', 'name': 'starches'}, {'id': 12646, 'synset': 'breadstuff.n.02', 'name': 'breadstuff'}, {'id': 12647, 'synset': 'coloring.n.01', 'name': 'coloring'}, {'id': 12648, 'synset': 'concentrate.n.02', 'name': 'concentrate'}, {'id': 12649, 'synset': 'tomato_concentrate.n.01', 'name': 'tomato_concentrate'}, {'id': 12650, 'synset': 'meal.n.03', 'name': 'meal'}, {'id': 12651, 'synset': 'kibble.n.01', 'name': 'kibble'}, {'id': 12652, 'synset': 'farina.n.01', 'name': 'farina'}, {'id': 12653, 'synset': 'matzo_meal.n.01', 'name': 'matzo_meal'}, {'id': 12654, 'synset': 'oatmeal.n.02', 'name': 'oatmeal'}, {'id': 12655, 'synset': 'pea_flour.n.01', 'name': 'pea_flour'}, {'id': 12656, 'synset': 'roughage.n.01', 'name': 'roughage'}, {'id': 12657, 'synset': 'bran.n.02', 'name': 'bran'}, {'id': 12658, 'synset': 'flour.n.01', 'name': 'flour'}, {'id': 12659, 'synset': 'plain_flour.n.01', 'name': 'plain_flour'}, {'id': 12660, 'synset': 'wheat_flour.n.01', 'name': 'wheat_flour'}, {'id': 12661, 'synset': 'whole_wheat_flour.n.01', 'name': 'whole_wheat_flour'}, {'id': 12662, 'synset': 'soybean_meal.n.01', 'name': 'soybean_meal'}, {'id': 12663, 'synset': 'semolina.n.01', 'name': 'semolina'}, {'id': 12664, 'synset': 'corn_gluten_feed.n.01', 'name': 'corn_gluten_feed'}, {'id': 12665, 'synset': 'nutriment.n.01', 'name': 'nutriment'}, {'id': 12666, 'synset': 'commissariat.n.01', 'name': 'commissariat'}, {'id': 12667, 'synset': 'larder.n.01', 'name': 'larder'}, {'id': 12668, 'synset': 'frozen_food.n.01', 'name': 'frozen_food'}, {'id': 12669, 'synset': 'canned_food.n.01', 'name': 'canned_food'}, {'id': 12670, 'synset': 'canned_meat.n.01', 'name': 'canned_meat'}, {'id': 12671, 'synset': 'spam.n.01', 'name': 'Spam'}, {'id': 12672, 'synset': 'dehydrated_food.n.01', 'name': 'dehydrated_food'}, {'id': 12673, 'synset': 'square_meal.n.01', 'name': 'square_meal'}, {'id': 12674, 'synset': 'meal.n.01', 'name': 'meal'}, {'id': 12675, 'synset': 'potluck.n.01', 'name': 'potluck'}, {'id': 12676, 'synset': 'refection.n.01', 'name': 'refection'}, {'id': 12677, 'synset': 'refreshment.n.01', 'name': 'refreshment'}, {'id': 12678, 'synset': 'breakfast.n.01', 'name': 'breakfast'}, {'id': 12679, 'synset': 'continental_breakfast.n.01', 'name': 'continental_breakfast'}, {'id': 12680, 'synset': 'brunch.n.01', 'name': 'brunch'}, {'id': 12681, 'synset': 'lunch.n.01', 'name': 'lunch'}, {'id': 12682, 'synset': 'business_lunch.n.01', 'name': 'business_lunch'}, {'id': 12683, 'synset': 'high_tea.n.01', 'name': 'high_tea'}, {'id': 12684, 'synset': 'tea.n.02', 'name': 'tea'}, {'id': 12685, 'synset': 'dinner.n.01', 'name': 'dinner'}, {'id': 12686, 'synset': 'supper.n.01', 'name': 'supper'}, {'id': 12687, 'synset': 'buffet.n.02', 'name': 'buffet'}, {'id': 12688, 'synset': 'picnic.n.03', 'name': 'picnic'}, {'id': 12689, 'synset': 'cookout.n.01', 'name': 'cookout'}, {'id': 12690, 'synset': 'barbecue.n.02', 'name': 'barbecue'}, {'id': 12691, 'synset': 'clambake.n.01', 'name': 'clambake'}, {'id': 12692, 'synset': 'fish_fry.n.01', 'name': 'fish_fry'}, {'id': 12693, 'synset': 'bite.n.04', 'name': 'bite'}, {'id': 12694, 'synset': 'nosh.n.01', 'name': 'nosh'}, {'id': 12695, 'synset': 'nosh-up.n.01', 'name': 'nosh-up'}, {'id': 12696, 'synset': "ploughman's_lunch.n.01", 'name': "ploughman's_lunch"}, {'id': 12697, 'synset': 'coffee_break.n.01', 'name': 'coffee_break'}, {'id': 12698, 'synset': 'banquet.n.02', 'name': 'banquet'}, {'id': 12699, 'synset': 'entree.n.01', 'name': 'entree'}, {'id': 12700, 'synset': 'piece_de_resistance.n.02', 'name': 'piece_de_resistance'}, {'id': 12701, 'synset': 'plate.n.08', 'name': 'plate'}, {'id': 12702, 'synset': 'adobo.n.01', 'name': 'adobo'}, {'id': 12703, 'synset': 'side_dish.n.01', 'name': 'side_dish'}, {'id': 12704, 'synset': 'special.n.02', 'name': 'special'}, {'id': 12705, 'synset': 'chicken_casserole.n.01', 'name': 'chicken_casserole'}, {'id': 12706, 'synset': 'chicken_cacciatore.n.01', 'name': 'chicken_cacciatore'}, {'id': 12707, 'synset': 'antipasto.n.01', 'name': 'antipasto'}, {'id': 12708, 'synset': 'appetizer.n.01', 'name': 'appetizer'}, {'id': 12709, 'synset': 'canape.n.01', 'name': 'canape'}, {'id': 12710, 'synset': 'cocktail.n.02', 'name': 'cocktail'}, {'id': 12711, 'synset': 'fruit_cocktail.n.01', 'name': 'fruit_cocktail'}, {'id': 12712, 'synset': 'crab_cocktail.n.01', 'name': 'crab_cocktail'}, {'id': 12713, 'synset': 'shrimp_cocktail.n.01', 'name': 'shrimp_cocktail'}, {'id': 12714, 'synset': "hors_d'oeuvre.n.01", 'name': "hors_d'oeuvre"}, {'id': 12715, 'synset': 'relish.n.02', 'name': 'relish'}, {'id': 12716, 'synset': 'dip.n.04', 'name': 'dip'}, {'id': 12717, 'synset': 'bean_dip.n.01', 'name': 'bean_dip'}, {'id': 12718, 'synset': 'cheese_dip.n.01', 'name': 'cheese_dip'}, {'id': 12719, 'synset': 'clam_dip.n.01', 'name': 'clam_dip'}, {'id': 12720, 'synset': 'guacamole.n.01', 'name': 'guacamole'}, {'id': 12721, 'synset': 'soup_du_jour.n.01', 'name': 'soup_du_jour'}, {'id': 12722, 'synset': 'alphabet_soup.n.02', 'name': 'alphabet_soup'}, {'id': 12723, 'synset': 'consomme.n.01', 'name': 'consomme'}, {'id': 12724, 'synset': 'madrilene.n.01', 'name': 'madrilene'}, {'id': 12725, 'synset': 'bisque.n.01', 'name': 'bisque'}, {'id': 12726, 'synset': 'borsch.n.01', 'name': 'borsch'}, {'id': 12727, 'synset': 'broth.n.02', 'name': 'broth'}, {'id': 12728, 'synset': 'barley_water.n.01', 'name': 'barley_water'}, {'id': 12729, 'synset': 'bouillon.n.01', 'name': 'bouillon'}, {'id': 12730, 'synset': 'beef_broth.n.01', 'name': 'beef_broth'}, {'id': 12731, 'synset': 'chicken_broth.n.01', 'name': 'chicken_broth'}, {'id': 12732, 'synset': 'broth.n.01', 'name': 'broth'}, {'id': 12733, 'synset': 'stock_cube.n.01', 'name': 'stock_cube'}, {'id': 12734, 'synset': 'chicken_soup.n.01', 'name': 'chicken_soup'}, {'id': 12735, 'synset': 'cock-a-leekie.n.01', 'name': 'cock-a-leekie'}, {'id': 12736, 'synset': 'gazpacho.n.01', 'name': 'gazpacho'}, {'id': 12737, 'synset': 'gumbo.n.04', 'name': 'gumbo'}, {'id': 12738, 'synset': 'julienne.n.02', 'name': 'julienne'}, {'id': 12739, 'synset': 'marmite.n.01', 'name': 'marmite'}, {'id': 12740, 'synset': 'mock_turtle_soup.n.01', 'name': 'mock_turtle_soup'}, {'id': 12741, 'synset': 'mulligatawny.n.01', 'name': 'mulligatawny'}, {'id': 12742, 'synset': 'oxtail_soup.n.01', 'name': 'oxtail_soup'}, {'id': 12743, 'synset': 'pea_soup.n.01', 'name': 'pea_soup'}, {'id': 12744, 'synset': 'pepper_pot.n.01', 'name': 'pepper_pot'}, {'id': 12745, 'synset': 'petite_marmite.n.01', 'name': 'petite_marmite'}, {'id': 12746, 'synset': 'potage.n.01', 'name': 'potage'}, {'id': 12747, 'synset': 'pottage.n.01', 'name': 'pottage'}, {'id': 12748, 'synset': 'turtle_soup.n.01', 'name': 'turtle_soup'}, {'id': 12749, 'synset': 'eggdrop_soup.n.01', 'name': 'eggdrop_soup'}, {'id': 12750, 'synset': 'chowder.n.01', 'name': 'chowder'}, {'id': 12751, 'synset': 'corn_chowder.n.01', 'name': 'corn_chowder'}, {'id': 12752, 'synset': 'clam_chowder.n.01', 'name': 'clam_chowder'}, {'id': 12753, 'synset': 'manhattan_clam_chowder.n.01', 'name': 'Manhattan_clam_chowder'}, {'id': 12754, 'synset': 'new_england_clam_chowder.n.01', 'name': 'New_England_clam_chowder'}, {'id': 12755, 'synset': 'fish_chowder.n.01', 'name': 'fish_chowder'}, {'id': 12756, 'synset': 'won_ton.n.02', 'name': 'won_ton'}, {'id': 12757, 'synset': 'split-pea_soup.n.01', 'name': 'split-pea_soup'}, {'id': 12758, 'synset': 'green_pea_soup.n.01', 'name': 'green_pea_soup'}, {'id': 12759, 'synset': 'lentil_soup.n.01', 'name': 'lentil_soup'}, {'id': 12760, 'synset': 'scotch_broth.n.01', 'name': 'Scotch_broth'}, {'id': 12761, 'synset': 'vichyssoise.n.01', 'name': 'vichyssoise'}, {'id': 12762, 'synset': 'bigos.n.01', 'name': 'bigos'}, {'id': 12763, 'synset': 'brunswick_stew.n.01', 'name': 'Brunswick_stew'}, {'id': 12764, 'synset': 'burgoo.n.03', 'name': 'burgoo'}, {'id': 12765, 'synset': 'burgoo.n.02', 'name': 'burgoo'}, {'id': 12766, 'synset': 'olla_podrida.n.01', 'name': 'olla_podrida'}, {'id': 12767, 'synset': 'mulligan_stew.n.01', 'name': 'mulligan_stew'}, {'id': 12768, 'synset': 'purloo.n.01', 'name': 'purloo'}, {'id': 12769, 'synset': 'goulash.n.01', 'name': 'goulash'}, {'id': 12770, 'synset': 'hotchpotch.n.02', 'name': 'hotchpotch'}, {'id': 12771, 'synset': 'hot_pot.n.01', 'name': 'hot_pot'}, {'id': 12772, 'synset': 'beef_goulash.n.01', 'name': 'beef_goulash'}, {'id': 12773, 'synset': 'pork-and-veal_goulash.n.01', 'name': 'pork-and-veal_goulash'}, {'id': 12774, 'synset': 'porkholt.n.01', 'name': 'porkholt'}, {'id': 12775, 'synset': 'irish_stew.n.01', 'name': 'Irish_stew'}, {'id': 12776, 'synset': 'oyster_stew.n.01', 'name': 'oyster_stew'}, {'id': 12777, 'synset': 'lobster_stew.n.01', 'name': 'lobster_stew'}, {'id': 12778, 'synset': 'lobscouse.n.01', 'name': 'lobscouse'}, {'id': 12779, 'synset': 'fish_stew.n.01', 'name': 'fish_stew'}, {'id': 12780, 'synset': 'bouillabaisse.n.01', 'name': 'bouillabaisse'}, {'id': 12781, 'synset': 'matelote.n.01', 'name': 'matelote'}, {'id': 12782, 'synset': 'paella.n.01', 'name': 'paella'}, {'id': 12783, 'synset': 'fricassee.n.01', 'name': 'fricassee'}, {'id': 12784, 'synset': 'chicken_stew.n.01', 'name': 'chicken_stew'}, {'id': 12785, 'synset': 'turkey_stew.n.01', 'name': 'turkey_stew'}, {'id': 12786, 'synset': 'beef_stew.n.01', 'name': 'beef_stew'}, {'id': 12787, 'synset': 'ragout.n.01', 'name': 'ragout'}, {'id': 12788, 'synset': 'ratatouille.n.01', 'name': 'ratatouille'}, {'id': 12789, 'synset': 'salmi.n.01', 'name': 'salmi'}, {'id': 12790, 'synset': 'pot-au-feu.n.01', 'name': 'pot-au-feu'}, {'id': 12791, 'synset': 'slumgullion.n.01', 'name': 'slumgullion'}, {'id': 12792, 'synset': 'smorgasbord.n.02', 'name': 'smorgasbord'}, {'id': 12793, 'synset': 'viand.n.01', 'name': 'viand'}, {'id': 12794, 'synset': 'ready-mix.n.01', 'name': 'ready-mix'}, {'id': 12795, 'synset': 'brownie_mix.n.01', 'name': 'brownie_mix'}, {'id': 12796, 'synset': 'cake_mix.n.01', 'name': 'cake_mix'}, {'id': 12797, 'synset': 'lemonade_mix.n.01', 'name': 'lemonade_mix'}, {'id': 12798, 'synset': 'self-rising_flour.n.01', 'name': 'self-rising_flour'}, {'id': 12799, 'synset': 'choice_morsel.n.01', 'name': 'choice_morsel'}, {'id': 12800, 'synset': 'savory.n.04', 'name': 'savory'}, {'id': 12801, 'synset': "calf's-foot_jelly.n.01", 'name': "calf's-foot_jelly"}, {'id': 12802, 'synset': 'caramel.n.02', 'name': 'caramel'}, {'id': 12803, 'synset': 'lump_sugar.n.01', 'name': 'lump_sugar'}, {'id': 12804, 'synset': 'cane_sugar.n.02', 'name': 'cane_sugar'}, {'id': 12805, 'synset': 'castor_sugar.n.01', 'name': 'castor_sugar'}, {'id': 12806, 'synset': 'powdered_sugar.n.01', 'name': 'powdered_sugar'}, {'id': 12807, 'synset': 'granulated_sugar.n.01', 'name': 'granulated_sugar'}, {'id': 12808, 'synset': 'icing_sugar.n.01', 'name': 'icing_sugar'}, {'id': 12809, 'synset': 'corn_sugar.n.02', 'name': 'corn_sugar'}, {'id': 12810, 'synset': 'brown_sugar.n.01', 'name': 'brown_sugar'}, {'id': 12811, 'synset': 'demerara.n.05', 'name': 'demerara'}, {'id': 12812, 'synset': 'sweet.n.03', 'name': 'sweet'}, {'id': 12813, 'synset': 'confectionery.n.01', 'name': 'confectionery'}, {'id': 12814, 'synset': 'confiture.n.01', 'name': 'confiture'}, {'id': 12815, 'synset': 'sweetmeat.n.01', 'name': 'sweetmeat'}, {'id': 12816, 'synset': 'candy.n.01', 'name': 'candy'}, {'id': 12817, 'synset': 'carob_bar.n.01', 'name': 'carob_bar'}, {'id': 12818, 'synset': 'hardbake.n.01', 'name': 'hardbake'}, {'id': 12819, 'synset': 'hard_candy.n.01', 'name': 'hard_candy'}, {'id': 12820, 'synset': 'barley-sugar.n.01', 'name': 'barley-sugar'}, {'id': 12821, 'synset': 'brandyball.n.01', 'name': 'brandyball'}, {'id': 12822, 'synset': 'jawbreaker.n.01', 'name': 'jawbreaker'}, {'id': 12823, 'synset': 'lemon_drop.n.01', 'name': 'lemon_drop'}, {'id': 12824, 'synset': 'sourball.n.01', 'name': 'sourball'}, {'id': 12825, 'synset': 'patty.n.03', 'name': 'patty'}, {'id': 12826, 'synset': 'peppermint_patty.n.01', 'name': 'peppermint_patty'}, {'id': 12827, 'synset': 'bonbon.n.01', 'name': 'bonbon'}, {'id': 12828, 'synset': 'brittle.n.01', 'name': 'brittle'}, {'id': 12829, 'synset': 'peanut_brittle.n.01', 'name': 'peanut_brittle'}, {'id': 12830, 'synset': 'chewing_gum.n.01', 'name': 'chewing_gum'}, {'id': 12831, 'synset': 'gum_ball.n.01', 'name': 'gum_ball'}, {'id': 12832, 'synset': 'butterscotch.n.01', 'name': 'butterscotch'}, {'id': 12833, 'synset': 'candied_fruit.n.01', 'name': 'candied_fruit'}, {'id': 12834, 'synset': 'candied_apple.n.01', 'name': 'candied_apple'}, {'id': 12835, 'synset': 'crystallized_ginger.n.01', 'name': 'crystallized_ginger'}, {'id': 12836, 'synset': 'grapefruit_peel.n.01', 'name': 'grapefruit_peel'}, {'id': 12837, 'synset': 'lemon_peel.n.02', 'name': 'lemon_peel'}, {'id': 12838, 'synset': 'orange_peel.n.02', 'name': 'orange_peel'}, {'id': 12839, 'synset': 'candied_citrus_peel.n.01', 'name': 'candied_citrus_peel'}, {'id': 12840, 'synset': 'candy_corn.n.01', 'name': 'candy_corn'}, {'id': 12841, 'synset': 'caramel.n.01', 'name': 'caramel'}, {'id': 12842, 'synset': 'center.n.14', 'name': 'center'}, {'id': 12843, 'synset': 'comfit.n.01', 'name': 'comfit'}, {'id': 12844, 'synset': 'cotton_candy.n.01', 'name': 'cotton_candy'}, {'id': 12845, 'synset': 'dragee.n.02', 'name': 'dragee'}, {'id': 12846, 'synset': 'dragee.n.01', 'name': 'dragee'}, {'id': 12847, 'synset': 'fondant.n.01', 'name': 'fondant'}, {'id': 12848, 'synset': 'chocolate_fudge.n.01', 'name': 'chocolate_fudge'}, {'id': 12849, 'synset': 'divinity.n.03', 'name': 'divinity'}, {'id': 12850, 'synset': 'penuche.n.01', 'name': 'penuche'}, {'id': 12851, 'synset': 'gumdrop.n.01', 'name': 'gumdrop'}, {'id': 12852, 'synset': 'jujube.n.03', 'name': 'jujube'}, {'id': 12853, 'synset': 'honey_crisp.n.01', 'name': 'honey_crisp'}, {'id': 12854, 'synset': 'horehound.n.02', 'name': 'horehound'}, {'id': 12855, 'synset': 'peppermint.n.03', 'name': 'peppermint'}, {'id': 12856, 'synset': 'kiss.n.03', 'name': 'kiss'}, {'id': 12857, 'synset': 'molasses_kiss.n.01', 'name': 'molasses_kiss'}, {'id': 12858, 'synset': 'meringue_kiss.n.01', 'name': 'meringue_kiss'}, {'id': 12859, 'synset': 'chocolate_kiss.n.01', 'name': 'chocolate_kiss'}, {'id': 12860, 'synset': 'licorice.n.02', 'name': 'licorice'}, {'id': 12861, 'synset': 'life_saver.n.01', 'name': 'Life_Saver'}, {'id': 12862, 'synset': 'lozenge.n.01', 'name': 'lozenge'}, {'id': 12863, 'synset': 'cachou.n.01', 'name': 'cachou'}, {'id': 12864, 'synset': 'cough_drop.n.01', 'name': 'cough_drop'}, {'id': 12865, 'synset': 'marshmallow.n.01', 'name': 'marshmallow'}, {'id': 12866, 'synset': 'marzipan.n.01', 'name': 'marzipan'}, {'id': 12867, 'synset': 'nougat.n.01', 'name': 'nougat'}, {'id': 12868, 'synset': 'nougat_bar.n.01', 'name': 'nougat_bar'}, {'id': 12869, 'synset': 'nut_bar.n.01', 'name': 'nut_bar'}, {'id': 12870, 'synset': 'peanut_bar.n.01', 'name': 'peanut_bar'}, {'id': 12871, 'synset': 'popcorn_ball.n.01', 'name': 'popcorn_ball'}, {'id': 12872, 'synset': 'praline.n.01', 'name': 'praline'}, {'id': 12873, 'synset': 'rock_candy.n.02', 'name': 'rock_candy'}, {'id': 12874, 'synset': 'rock_candy.n.01', 'name': 'rock_candy'}, {'id': 12875, 'synset': 'sugar_candy.n.01', 'name': 'sugar_candy'}, {'id': 12876, 'synset': 'sugarplum.n.01', 'name': 'sugarplum'}, {'id': 12877, 'synset': 'taffy.n.01', 'name': 'taffy'}, {'id': 12878, 'synset': 'molasses_taffy.n.01', 'name': 'molasses_taffy'}, {'id': 12879, 'synset': 'turkish_delight.n.01', 'name': 'Turkish_Delight'}, {'id': 12880, 'synset': 'dessert.n.01', 'name': 'dessert'}, {'id': 12881, 'synset': 'ambrosia.n.04', 'name': 'ambrosia'}, {'id': 12882, 'synset': 'ambrosia.n.03', 'name': 'ambrosia'}, {'id': 12883, 'synset': 'baked_alaska.n.01', 'name': 'baked_Alaska'}, {'id': 12884, 'synset': 'blancmange.n.01', 'name': 'blancmange'}, {'id': 12885, 'synset': 'charlotte.n.02', 'name': 'charlotte'}, {'id': 12886, 'synset': 'compote.n.01', 'name': 'compote'}, {'id': 12887, 'synset': 'dumpling.n.02', 'name': 'dumpling'}, {'id': 12888, 'synset': 'flan.n.01', 'name': 'flan'}, {'id': 12889, 'synset': 'frozen_dessert.n.01', 'name': 'frozen_dessert'}, {'id': 12890, 'synset': 'junket.n.01', 'name': 'junket'}, {'id': 12891, 'synset': 'mousse.n.02', 'name': 'mousse'}, {'id': 12892, 'synset': 'mousse.n.01', 'name': 'mousse'}, {'id': 12893, 'synset': 'pavlova.n.02', 'name': 'pavlova'}, {'id': 12894, 'synset': 'peach_melba.n.01', 'name': 'peach_melba'}, {'id': 12895, 'synset': 'whip.n.03', 'name': 'whip'}, {'id': 12896, 'synset': 'prune_whip.n.01', 'name': 'prune_whip'}, {'id': 12897, 'synset': 'pudding.n.03', 'name': 'pudding'}, {'id': 12898, 'synset': 'pudding.n.02', 'name': 'pudding'}, {'id': 12899, 'synset': 'syllabub.n.02', 'name': 'syllabub'}, {'id': 12900, 'synset': 'tiramisu.n.01', 'name': 'tiramisu'}, {'id': 12901, 'synset': 'trifle.n.01', 'name': 'trifle'}, {'id': 12902, 'synset': 'tipsy_cake.n.01', 'name': 'tipsy_cake'}, {'id': 12903, 'synset': 'jello.n.01', 'name': 'jello'}, {'id': 12904, 'synset': 'apple_dumpling.n.01', 'name': 'apple_dumpling'}, {'id': 12905, 'synset': 'ice.n.05', 'name': 'ice'}, {'id': 12906, 'synset': 'water_ice.n.02', 'name': 'water_ice'}, {'id': 12907, 'synset': 'ice-cream_cone.n.01', 'name': 'ice-cream_cone'}, {'id': 12908, 'synset': 'chocolate_ice_cream.n.01', 'name': 'chocolate_ice_cream'}, {'id': 12909, 'synset': 'neapolitan_ice_cream.n.01', 'name': 'Neapolitan_ice_cream'}, {'id': 12910, 'synset': 'peach_ice_cream.n.01', 'name': 'peach_ice_cream'}, {'id': 12911, 'synset': 'strawberry_ice_cream.n.01', 'name': 'strawberry_ice_cream'}, {'id': 12912, 'synset': 'tutti-frutti.n.01', 'name': 'tutti-frutti'}, {'id': 12913, 'synset': 'vanilla_ice_cream.n.01', 'name': 'vanilla_ice_cream'}, {'id': 12914, 'synset': 'ice_milk.n.01', 'name': 'ice_milk'}, {'id': 12915, 'synset': 'frozen_yogurt.n.01', 'name': 'frozen_yogurt'}, {'id': 12916, 'synset': 'snowball.n.03', 'name': 'snowball'}, {'id': 12917, 'synset': 'snowball.n.02', 'name': 'snowball'}, {'id': 12918, 'synset': 'parfait.n.01', 'name': 'parfait'}, {'id': 12919, 'synset': 'ice-cream_sundae.n.01', 'name': 'ice-cream_sundae'}, {'id': 12920, 'synset': 'split.n.07', 'name': 'split'}, {'id': 12921, 'synset': 'banana_split.n.01', 'name': 'banana_split'}, {'id': 12922, 'synset': 'frozen_pudding.n.01', 'name': 'frozen_pudding'}, {'id': 12923, 'synset': 'frozen_custard.n.01', 'name': 'frozen_custard'}, {'id': 12924, 'synset': 'flummery.n.01', 'name': 'flummery'}, {'id': 12925, 'synset': 'fish_mousse.n.01', 'name': 'fish_mousse'}, {'id': 12926, 'synset': 'chicken_mousse.n.01', 'name': 'chicken_mousse'}, {'id': 12927, 'synset': 'plum_pudding.n.01', 'name': 'plum_pudding'}, {'id': 12928, 'synset': 'carrot_pudding.n.01', 'name': 'carrot_pudding'}, {'id': 12929, 'synset': 'corn_pudding.n.01', 'name': 'corn_pudding'}, {'id': 12930, 'synset': 'steamed_pudding.n.01', 'name': 'steamed_pudding'}, {'id': 12931, 'synset': 'duff.n.01', 'name': 'duff'}, {'id': 12932, 'synset': 'vanilla_pudding.n.01', 'name': 'vanilla_pudding'}, {'id': 12933, 'synset': 'chocolate_pudding.n.01', 'name': 'chocolate_pudding'}, {'id': 12934, 'synset': 'brown_betty.n.01', 'name': 'brown_Betty'}, {'id': 12935, 'synset': 'nesselrode.n.01', 'name': 'Nesselrode'}, {'id': 12936, 'synset': 'pease_pudding.n.01', 'name': 'pease_pudding'}, {'id': 12937, 'synset': 'custard.n.01', 'name': 'custard'}, {'id': 12938, 'synset': 'creme_caramel.n.01', 'name': 'creme_caramel'}, {'id': 12939, 'synset': 'creme_anglais.n.01', 'name': 'creme_anglais'}, {'id': 12940, 'synset': 'creme_brulee.n.01', 'name': 'creme_brulee'}, {'id': 12941, 'synset': 'fruit_custard.n.01', 'name': 'fruit_custard'}, {'id': 12942, 'synset': 'tapioca.n.01', 'name': 'tapioca'}, {'id': 12943, 'synset': 'tapioca_pudding.n.01', 'name': 'tapioca_pudding'}, {'id': 12944, 'synset': 'roly-poly.n.02', 'name': 'roly-poly'}, {'id': 12945, 'synset': 'suet_pudding.n.01', 'name': 'suet_pudding'}, {'id': 12946, 'synset': 'bavarian_cream.n.01', 'name': 'Bavarian_cream'}, {'id': 12947, 'synset': 'maraschino.n.02', 'name': 'maraschino'}, {'id': 12948, 'synset': 'nonpareil.n.02', 'name': 'nonpareil'}, {'id': 12949, 'synset': 'zabaglione.n.01', 'name': 'zabaglione'}, {'id': 12950, 'synset': 'garnish.n.01', 'name': 'garnish'}, {'id': 12951, 'synset': 'pastry.n.01', 'name': 'pastry'}, {'id': 12952, 'synset': 'turnover.n.02', 'name': 'turnover'}, {'id': 12953, 'synset': 'apple_turnover.n.01', 'name': 'apple_turnover'}, {'id': 12954, 'synset': 'knish.n.01', 'name': 'knish'}, {'id': 12955, 'synset': 'pirogi.n.01', 'name': 'pirogi'}, {'id': 12956, 'synset': 'samosa.n.01', 'name': 'samosa'}, {'id': 12957, 'synset': 'timbale.n.01', 'name': 'timbale'}, {'id': 12958, 'synset': 'puff_paste.n.01', 'name': 'puff_paste'}, {'id': 12959, 'synset': 'phyllo.n.01', 'name': 'phyllo'}, {'id': 12960, 'synset': 'puff_batter.n.01', 'name': 'puff_batter'}, {'id': 12961, 'synset': 'ice-cream_cake.n.01', 'name': 'ice-cream_cake'}, {'id': 12962, 'synset': 'fish_cake.n.01', 'name': 'fish_cake'}, {'id': 12963, 'synset': 'fish_stick.n.01', 'name': 'fish_stick'}, {'id': 12964, 'synset': 'conserve.n.01', 'name': 'conserve'}, {'id': 12965, 'synset': 'apple_butter.n.01', 'name': 'apple_butter'}, {'id': 12966, 'synset': 'chowchow.n.02', 'name': 'chowchow'}, {'id': 12967, 'synset': 'lemon_curd.n.01', 'name': 'lemon_curd'}, {'id': 12968, 'synset': 'strawberry_jam.n.01', 'name': 'strawberry_jam'}, {'id': 12969, 'synset': 'jelly.n.02', 'name': 'jelly'}, {'id': 12970, 'synset': 'apple_jelly.n.01', 'name': 'apple_jelly'}, {'id': 12971, 'synset': 'crabapple_jelly.n.01', 'name': 'crabapple_jelly'}, {'id': 12972, 'synset': 'grape_jelly.n.01', 'name': 'grape_jelly'}, {'id': 12973, 'synset': 'marmalade.n.01', 'name': 'marmalade'}, {'id': 12974, 'synset': 'orange_marmalade.n.01', 'name': 'orange_marmalade'}, {'id': 12975, 'synset': 'gelatin_dessert.n.01', 'name': 'gelatin_dessert'}, {'id': 12976, 'synset': 'buffalo_wing.n.01', 'name': 'buffalo_wing'}, {'id': 12977, 'synset': 'barbecued_wing.n.01', 'name': 'barbecued_wing'}, {'id': 12978, 'synset': 'mess.n.03', 'name': 'mess'}, {'id': 12979, 'synset': 'mince.n.01', 'name': 'mince'}, {'id': 12980, 'synset': 'puree.n.01', 'name': 'puree'}, {'id': 12981, 'synset': 'barbecue.n.01', 'name': 'barbecue'}, {'id': 12982, 'synset': 'biryani.n.01', 'name': 'biryani'}, {'id': 12983, 'synset': 'escalope_de_veau_orloff.n.01', 'name': 'escalope_de_veau_Orloff'}, {'id': 12984, 'synset': 'saute.n.01', 'name': 'saute'}, {'id': 12985, 'synset': 'veal_parmesan.n.01', 'name': 'veal_parmesan'}, {'id': 12986, 'synset': 'veal_cordon_bleu.n.01', 'name': 'veal_cordon_bleu'}, {'id': 12987, 'synset': 'margarine.n.01', 'name': 'margarine'}, {'id': 12988, 'synset': 'mincemeat.n.01', 'name': 'mincemeat'}, {'id': 12989, 'synset': 'stuffing.n.01', 'name': 'stuffing'}, {'id': 12990, 'synset': 'turkey_stuffing.n.01', 'name': 'turkey_stuffing'}, {'id': 12991, 'synset': 'oyster_stuffing.n.01', 'name': 'oyster_stuffing'}, {'id': 12992, 'synset': 'forcemeat.n.01', 'name': 'forcemeat'}, {'id': 12993, 'synset': 'anadama_bread.n.01', 'name': 'anadama_bread'}, {'id': 12994, 'synset': 'bap.n.01', 'name': 'bap'}, {'id': 12995, 'synset': 'barmbrack.n.01', 'name': 'barmbrack'}, {'id': 12996, 'synset': 'breadstick.n.01', 'name': 'breadstick'}, {'id': 12997, 'synset': 'grissino.n.01', 'name': 'grissino'}, {'id': 12998, 'synset': 'brown_bread.n.02', 'name': 'brown_bread'}, {'id': 12999, 'synset': 'tea_bread.n.01', 'name': 'tea_bread'}, {'id': 13000, 'synset': 'caraway_seed_bread.n.01', 'name': 'caraway_seed_bread'}, {'id': 13001, 'synset': 'challah.n.01', 'name': 'challah'}, {'id': 13002, 'synset': 'cinnamon_bread.n.01', 'name': 'cinnamon_bread'}, {'id': 13003, 'synset': 'cracked-wheat_bread.n.01', 'name': 'cracked-wheat_bread'}, {'id': 13004, 'synset': 'dark_bread.n.01', 'name': 'dark_bread'}, {'id': 13005, 'synset': 'english_muffin.n.01', 'name': 'English_muffin'}, {'id': 13006, 'synset': 'flatbread.n.01', 'name': 'flatbread'}, {'id': 13007, 'synset': 'garlic_bread.n.01', 'name': 'garlic_bread'}, {'id': 13008, 'synset': 'gluten_bread.n.01', 'name': 'gluten_bread'}, {'id': 13009, 'synset': 'graham_bread.n.01', 'name': 'graham_bread'}, {'id': 13010, 'synset': 'host.n.09', 'name': 'Host'}, {'id': 13011, 'synset': 'flatbrod.n.01', 'name': 'flatbrod'}, {'id': 13012, 'synset': 'bannock.n.01', 'name': 'bannock'}, {'id': 13013, 'synset': 'chapatti.n.01', 'name': 'chapatti'}, {'id': 13014, 'synset': 'loaf_of_bread.n.01', 'name': 'loaf_of_bread'}, {'id': 13015, 'synset': 'french_loaf.n.01', 'name': 'French_loaf'}, {'id': 13016, 'synset': 'matzo.n.01', 'name': 'matzo'}, {'id': 13017, 'synset': 'nan.n.04', 'name': 'nan'}, {'id': 13018, 'synset': 'onion_bread.n.01', 'name': 'onion_bread'}, {'id': 13019, 'synset': 'raisin_bread.n.01', 'name': 'raisin_bread'}, {'id': 13020, 'synset': 'quick_bread.n.01', 'name': 'quick_bread'}, {'id': 13021, 'synset': 'banana_bread.n.01', 'name': 'banana_bread'}, {'id': 13022, 'synset': 'date_bread.n.01', 'name': 'date_bread'}, {'id': 13023, 'synset': 'date-nut_bread.n.01', 'name': 'date-nut_bread'}, {'id': 13024, 'synset': 'nut_bread.n.01', 'name': 'nut_bread'}, {'id': 13025, 'synset': 'oatcake.n.01', 'name': 'oatcake'}, {'id': 13026, 'synset': 'irish_soda_bread.n.01', 'name': 'Irish_soda_bread'}, {'id': 13027, 'synset': 'skillet_bread.n.01', 'name': 'skillet_bread'}, {'id': 13028, 'synset': 'rye_bread.n.01', 'name': 'rye_bread'}, {'id': 13029, 'synset': 'black_bread.n.01', 'name': 'black_bread'}, {'id': 13030, 'synset': 'jewish_rye_bread.n.01', 'name': 'Jewish_rye_bread'}, {'id': 13031, 'synset': 'limpa.n.01', 'name': 'limpa'}, {'id': 13032, 'synset': 'swedish_rye_bread.n.01', 'name': 'Swedish_rye_bread'}, {'id': 13033, 'synset': 'salt-rising_bread.n.01', 'name': 'salt-rising_bread'}, {'id': 13034, 'synset': 'simnel.n.01', 'name': 'simnel'}, {'id': 13035, 'synset': 'sour_bread.n.01', 'name': 'sour_bread'}, {'id': 13036, 'synset': 'wafer.n.03', 'name': 'wafer'}, {'id': 13037, 'synset': 'white_bread.n.01', 'name': 'white_bread'}, {'id': 13038, 'synset': 'french_bread.n.01', 'name': 'French_bread'}, {'id': 13039, 'synset': 'italian_bread.n.01', 'name': 'Italian_bread'}, {'id': 13040, 'synset': 'corn_cake.n.01', 'name': 'corn_cake'}, {'id': 13041, 'synset': 'skillet_corn_bread.n.01', 'name': 'skillet_corn_bread'}, {'id': 13042, 'synset': 'ashcake.n.01', 'name': 'ashcake'}, {'id': 13043, 'synset': 'hoecake.n.01', 'name': 'hoecake'}, {'id': 13044, 'synset': 'cornpone.n.01', 'name': 'cornpone'}, {'id': 13045, 'synset': 'corn_dab.n.01', 'name': 'corn_dab'}, {'id': 13046, 'synset': 'hush_puppy.n.01', 'name': 'hush_puppy'}, {'id': 13047, 'synset': 'johnnycake.n.01', 'name': 'johnnycake'}, {'id': 13048, 'synset': 'shawnee_cake.n.01', 'name': 'Shawnee_cake'}, {'id': 13049, 'synset': 'spoon_bread.n.01', 'name': 'spoon_bread'}, {'id': 13050, 'synset': 'cinnamon_toast.n.01', 'name': 'cinnamon_toast'}, {'id': 13051, 'synset': 'orange_toast.n.01', 'name': 'orange_toast'}, {'id': 13052, 'synset': 'melba_toast.n.01', 'name': 'Melba_toast'}, {'id': 13053, 'synset': 'zwieback.n.01', 'name': 'zwieback'}, {'id': 13054, 'synset': 'frankfurter_bun.n.01', 'name': 'frankfurter_bun'}, {'id': 13055, 'synset': 'hamburger_bun.n.01', 'name': 'hamburger_bun'}, {'id': 13056, 'synset': 'bran_muffin.n.01', 'name': 'bran_muffin'}, {'id': 13057, 'synset': 'corn_muffin.n.01', 'name': 'corn_muffin'}, {'id': 13058, 'synset': 'yorkshire_pudding.n.01', 'name': 'Yorkshire_pudding'}, {'id': 13059, 'synset': 'popover.n.01', 'name': 'popover'}, {'id': 13060, 'synset': 'scone.n.01', 'name': 'scone'}, {'id': 13061, 'synset': 'drop_scone.n.01', 'name': 'drop_scone'}, {'id': 13062, 'synset': 'cross_bun.n.01', 'name': 'cross_bun'}, {'id': 13063, 'synset': 'brioche.n.01', 'name': 'brioche'}, {'id': 13064, 'synset': 'hard_roll.n.01', 'name': 'hard_roll'}, {'id': 13065, 'synset': 'soft_roll.n.01', 'name': 'soft_roll'}, {'id': 13066, 'synset': 'kaiser_roll.n.01', 'name': 'kaiser_roll'}, {'id': 13067, 'synset': 'parker_house_roll.n.01', 'name': 'Parker_House_roll'}, {'id': 13068, 'synset': 'clover-leaf_roll.n.01', 'name': 'clover-leaf_roll'}, {'id': 13069, 'synset': 'onion_roll.n.01', 'name': 'onion_roll'}, {'id': 13070, 'synset': 'bialy.n.01', 'name': 'bialy'}, {'id': 13071, 'synset': 'sweet_roll.n.01', 'name': 'sweet_roll'}, {'id': 13072, 'synset': 'bear_claw.n.01', 'name': 'bear_claw'}, {'id': 13073, 'synset': 'cinnamon_roll.n.01', 'name': 'cinnamon_roll'}, {'id': 13074, 'synset': 'honey_bun.n.01', 'name': 'honey_bun'}, {'id': 13075, 'synset': 'pinwheel_roll.n.01', 'name': 'pinwheel_roll'}, {'id': 13076, 'synset': 'danish.n.02', 'name': 'danish'}, {'id': 13077, 'synset': 'onion_bagel.n.01', 'name': 'onion_bagel'}, {'id': 13078, 'synset': 'biscuit.n.01', 'name': 'biscuit'}, {'id': 13079, 'synset': 'rolled_biscuit.n.01', 'name': 'rolled_biscuit'}, {'id': 13080, 'synset': 'baking-powder_biscuit.n.01', 'name': 'baking-powder_biscuit'}, {'id': 13081, 'synset': 'buttermilk_biscuit.n.01', 'name': 'buttermilk_biscuit'}, {'id': 13082, 'synset': 'shortcake.n.01', 'name': 'shortcake'}, {'id': 13083, 'synset': 'hardtack.n.01', 'name': 'hardtack'}, {'id': 13084, 'synset': 'saltine.n.01', 'name': 'saltine'}, {'id': 13085, 'synset': 'soda_cracker.n.01', 'name': 'soda_cracker'}, {'id': 13086, 'synset': 'oyster_cracker.n.01', 'name': 'oyster_cracker'}, {'id': 13087, 'synset': 'water_biscuit.n.01', 'name': 'water_biscuit'}, {'id': 13088, 'synset': 'graham_cracker.n.01', 'name': 'graham_cracker'}, {'id': 13089, 'synset': 'soft_pretzel.n.01', 'name': 'soft_pretzel'}, {'id': 13090, 'synset': 'sandwich_plate.n.01', 'name': 'sandwich_plate'}, {'id': 13091, 'synset': 'butty.n.01', 'name': 'butty'}, {'id': 13092, 'synset': 'ham_sandwich.n.01', 'name': 'ham_sandwich'}, {'id': 13093, 'synset': 'chicken_sandwich.n.01', 'name': 'chicken_sandwich'}, {'id': 13094, 'synset': 'club_sandwich.n.01', 'name': 'club_sandwich'}, {'id': 13095, 'synset': 'open-face_sandwich.n.01', 'name': 'open-face_sandwich'}, {'id': 13096, 'synset': 'cheeseburger.n.01', 'name': 'cheeseburger'}, {'id': 13097, 'synset': 'tunaburger.n.01', 'name': 'tunaburger'}, {'id': 13098, 'synset': 'hotdog.n.02', 'name': 'hotdog'}, {'id': 13099, 'synset': 'sloppy_joe.n.01', 'name': 'Sloppy_Joe'}, {'id': 13100, 'synset': 'bomber.n.03', 'name': 'bomber'}, {'id': 13101, 'synset': 'gyro.n.01', 'name': 'gyro'}, {'id': 13102, 'synset': 'bacon-lettuce-tomato_sandwich.n.01', 'name': 'bacon-lettuce-tomato_sandwich'}, {'id': 13103, 'synset': 'reuben.n.02', 'name': 'Reuben'}, {'id': 13104, 'synset': 'western.n.02', 'name': 'western'}, {'id': 13105, 'synset': 'wrap.n.02', 'name': 'wrap'}, {'id': 13106, 'synset': 'spaghetti.n.01', 'name': 'spaghetti'}, {'id': 13107, 'synset': 'hasty_pudding.n.01', 'name': 'hasty_pudding'}, {'id': 13108, 'synset': 'gruel.n.01', 'name': 'gruel'}, {'id': 13109, 'synset': 'congee.n.01', 'name': 'congee'}, {'id': 13110, 'synset': 'skilly.n.01', 'name': 'skilly'}, {'id': 13111, 'synset': 'edible_fruit.n.01', 'name': 'edible_fruit'}, {'id': 13112, 'synset': 'vegetable.n.01', 'name': 'vegetable'}, {'id': 13113, 'synset': 'julienne.n.01', 'name': 'julienne'}, {'id': 13114, 'synset': 'raw_vegetable.n.01', 'name': 'raw_vegetable'}, {'id': 13115, 'synset': 'crudites.n.01', 'name': 'crudites'}, {'id': 13116, 'synset': 'celery_stick.n.01', 'name': 'celery_stick'}, {'id': 13117, 'synset': 'legume.n.03', 'name': 'legume'}, {'id': 13118, 'synset': 'pulse.n.04', 'name': 'pulse'}, {'id': 13119, 'synset': 'potherb.n.01', 'name': 'potherb'}, {'id': 13120, 'synset': 'greens.n.01', 'name': 'greens'}, {'id': 13121, 'synset': 'chop-suey_greens.n.02', 'name': 'chop-suey_greens'}, {'id': 13122, 'synset': 'solanaceous_vegetable.n.01', 'name': 'solanaceous_vegetable'}, {'id': 13123, 'synset': 'root_vegetable.n.01', 'name': 'root_vegetable'}, {'id': 13124, 'synset': 'baked_potato.n.01', 'name': 'baked_potato'}, {'id': 13125, 'synset': 'french_fries.n.01', 'name': 'french_fries'}, {'id': 13126, 'synset': 'home_fries.n.01', 'name': 'home_fries'}, {'id': 13127, 'synset': 'jacket_potato.n.01', 'name': 'jacket_potato'}, {'id': 13128, 'synset': 'potato_skin.n.01', 'name': 'potato_skin'}, {'id': 13129, 'synset': 'uruguay_potato.n.02', 'name': 'Uruguay_potato'}, {'id': 13130, 'synset': 'yam.n.04', 'name': 'yam'}, {'id': 13131, 'synset': 'yam.n.03', 'name': 'yam'}, {'id': 13132, 'synset': 'snack_food.n.01', 'name': 'snack_food'}, {'id': 13133, 'synset': 'corn_chip.n.01', 'name': 'corn_chip'}, {'id': 13134, 'synset': 'tortilla_chip.n.01', 'name': 'tortilla_chip'}, {'id': 13135, 'synset': 'nacho.n.01', 'name': 'nacho'}, {'id': 13136, 'synset': 'pieplant.n.01', 'name': 'pieplant'}, {'id': 13137, 'synset': 'cruciferous_vegetable.n.01', 'name': 'cruciferous_vegetable'}, {'id': 13138, 'synset': 'mustard.n.03', 'name': 'mustard'}, {'id': 13139, 'synset': 'cabbage.n.01', 'name': 'cabbage'}, {'id': 13140, 'synset': 'kale.n.03', 'name': 'kale'}, {'id': 13141, 'synset': 'collards.n.01', 'name': 'collards'}, {'id': 13142, 'synset': 'chinese_cabbage.n.02', 'name': 'Chinese_cabbage'}, {'id': 13143, 'synset': 'bok_choy.n.02', 'name': 'bok_choy'}, {'id': 13144, 'synset': 'head_cabbage.n.02', 'name': 'head_cabbage'}, {'id': 13145, 'synset': 'red_cabbage.n.02', 'name': 'red_cabbage'}, {'id': 13146, 'synset': 'savoy_cabbage.n.02', 'name': 'savoy_cabbage'}, {'id': 13147, 'synset': 'broccoli.n.02', 'name': 'broccoli'}, {'id': 13148, 'synset': 'broccoli_rabe.n.02', 'name': 'broccoli_rabe'}, {'id': 13149, 'synset': 'squash.n.02', 'name': 'squash'}, {'id': 13150, 'synset': 'summer_squash.n.02', 'name': 'summer_squash'}, {'id': 13151, 'synset': 'yellow_squash.n.02', 'name': 'yellow_squash'}, {'id': 13152, 'synset': 'crookneck.n.01', 'name': 'crookneck'}, {'id': 13153, 'synset': 'marrow.n.04', 'name': 'marrow'}, {'id': 13154, 'synset': 'cocozelle.n.02', 'name': 'cocozelle'}, {'id': 13155, 'synset': 'pattypan_squash.n.02', 'name': 'pattypan_squash'}, {'id': 13156, 'synset': 'spaghetti_squash.n.02', 'name': 'spaghetti_squash'}, {'id': 13157, 'synset': 'winter_squash.n.02', 'name': 'winter_squash'}, {'id': 13158, 'synset': 'acorn_squash.n.02', 'name': 'acorn_squash'}, {'id': 13159, 'synset': 'butternut_squash.n.02', 'name': 'butternut_squash'}, {'id': 13160, 'synset': 'hubbard_squash.n.02', 'name': 'hubbard_squash'}, {'id': 13161, 'synset': 'turban_squash.n.02', 'name': 'turban_squash'}, {'id': 13162, 'synset': 'buttercup_squash.n.02', 'name': 'buttercup_squash'}, {'id': 13163, 'synset': 'cushaw.n.02', 'name': 'cushaw'}, {'id': 13164, 'synset': 'winter_crookneck_squash.n.02', 'name': 'winter_crookneck_squash'}, {'id': 13165, 'synset': 'gherkin.n.02', 'name': 'gherkin'}, {'id': 13166, 'synset': 'artichoke_heart.n.01', 'name': 'artichoke_heart'}, {'id': 13167, 'synset': 'jerusalem_artichoke.n.03', 'name': 'Jerusalem_artichoke'}, {'id': 13168, 'synset': 'bamboo_shoot.n.01', 'name': 'bamboo_shoot'}, {'id': 13169, 'synset': 'sprout.n.02', 'name': 'sprout'}, {'id': 13170, 'synset': 'bean_sprout.n.01', 'name': 'bean_sprout'}, {'id': 13171, 'synset': 'alfalfa_sprout.n.01', 'name': 'alfalfa_sprout'}, {'id': 13172, 'synset': 'beet.n.02', 'name': 'beet'}, {'id': 13173, 'synset': 'beet_green.n.01', 'name': 'beet_green'}, {'id': 13174, 'synset': 'sugar_beet.n.02', 'name': 'sugar_beet'}, {'id': 13175, 'synset': 'mangel-wurzel.n.02', 'name': 'mangel-wurzel'}, {'id': 13176, 'synset': 'chard.n.02', 'name': 'chard'}, {'id': 13177, 'synset': 'pepper.n.04', 'name': 'pepper'}, {'id': 13178, 'synset': 'sweet_pepper.n.02', 'name': 'sweet_pepper'}, {'id': 13179, 'synset': 'green_pepper.n.01', 'name': 'green_pepper'}, {'id': 13180, 'synset': 'globe_pepper.n.01', 'name': 'globe_pepper'}, {'id': 13181, 'synset': 'pimento.n.02', 'name': 'pimento'}, {'id': 13182, 'synset': 'hot_pepper.n.02', 'name': 'hot_pepper'}, {'id': 13183, 'synset': 'jalapeno.n.02', 'name': 'jalapeno'}, {'id': 13184, 'synset': 'chipotle.n.01', 'name': 'chipotle'}, {'id': 13185, 'synset': 'cayenne.n.03', 'name': 'cayenne'}, {'id': 13186, 'synset': 'tabasco.n.03', 'name': 'tabasco'}, {'id': 13187, 'synset': 'onion.n.03', 'name': 'onion'}, {'id': 13188, 'synset': 'bermuda_onion.n.01', 'name': 'Bermuda_onion'}, {'id': 13189, 'synset': 'vidalia_onion.n.01', 'name': 'Vidalia_onion'}, {'id': 13190, 'synset': 'spanish_onion.n.01', 'name': 'Spanish_onion'}, {'id': 13191, 'synset': 'purple_onion.n.01', 'name': 'purple_onion'}, {'id': 13192, 'synset': 'leek.n.02', 'name': 'leek'}, {'id': 13193, 'synset': 'shallot.n.03', 'name': 'shallot'}, {'id': 13194, 'synset': 'salad_green.n.01', 'name': 'salad_green'}, {'id': 13195, 'synset': 'lettuce.n.03', 'name': 'lettuce'}, {'id': 13196, 'synset': 'butterhead_lettuce.n.01', 'name': 'butterhead_lettuce'}, {'id': 13197, 'synset': 'buttercrunch.n.01', 'name': 'buttercrunch'}, {'id': 13198, 'synset': 'bibb_lettuce.n.01', 'name': 'Bibb_lettuce'}, {'id': 13199, 'synset': 'boston_lettuce.n.01', 'name': 'Boston_lettuce'}, {'id': 13200, 'synset': 'crisphead_lettuce.n.01', 'name': 'crisphead_lettuce'}, {'id': 13201, 'synset': 'cos.n.02', 'name': 'cos'}, {'id': 13202, 'synset': 'leaf_lettuce.n.02', 'name': 'leaf_lettuce'}, {'id': 13203, 'synset': 'celtuce.n.02', 'name': 'celtuce'}, {'id': 13204, 'synset': 'bean.n.01', 'name': 'bean'}, {'id': 13205, 'synset': 'goa_bean.n.02', 'name': 'goa_bean'}, {'id': 13206, 'synset': 'lentil.n.01', 'name': 'lentil'}, {'id': 13207, 'synset': 'green_pea.n.01', 'name': 'green_pea'}, {'id': 13208, 'synset': 'marrowfat_pea.n.01', 'name': 'marrowfat_pea'}, {'id': 13209, 'synset': 'snow_pea.n.02', 'name': 'snow_pea'}, {'id': 13210, 'synset': 'sugar_snap_pea.n.02', 'name': 'sugar_snap_pea'}, {'id': 13211, 'synset': 'split-pea.n.01', 'name': 'split-pea'}, {'id': 13212, 'synset': 'chickpea.n.03', 'name': 'chickpea'}, {'id': 13213, 'synset': 'cajan_pea.n.02', 'name': 'cajan_pea'}, {'id': 13214, 'synset': 'field_pea.n.03', 'name': 'field_pea'}, {'id': 13215, 'synset': 'mushy_peas.n.01', 'name': 'mushy_peas'}, {'id': 13216, 'synset': 'black-eyed_pea.n.03', 'name': 'black-eyed_pea'}, {'id': 13217, 'synset': 'common_bean.n.02', 'name': 'common_bean'}, {'id': 13218, 'synset': 'kidney_bean.n.02', 'name': 'kidney_bean'}, {'id': 13219, 'synset': 'navy_bean.n.01', 'name': 'navy_bean'}, {'id': 13220, 'synset': 'pinto_bean.n.01', 'name': 'pinto_bean'}, {'id': 13221, 'synset': 'frijole.n.02', 'name': 'frijole'}, {'id': 13222, 'synset': 'black_bean.n.01', 'name': 'black_bean'}, {'id': 13223, 'synset': 'fresh_bean.n.01', 'name': 'fresh_bean'}, {'id': 13224, 'synset': 'flageolet.n.01', 'name': 'flageolet'}, {'id': 13225, 'synset': 'green_bean.n.01', 'name': 'green_bean'}, {'id': 13226, 'synset': 'snap_bean.n.01', 'name': 'snap_bean'}, {'id': 13227, 'synset': 'string_bean.n.01', 'name': 'string_bean'}, {'id': 13228, 'synset': 'kentucky_wonder.n.01', 'name': 'Kentucky_wonder'}, {'id': 13229, 'synset': 'scarlet_runner.n.03', 'name': 'scarlet_runner'}, {'id': 13230, 'synset': 'haricot_vert.n.01', 'name': 'haricot_vert'}, {'id': 13231, 'synset': 'wax_bean.n.02', 'name': 'wax_bean'}, {'id': 13232, 'synset': 'shell_bean.n.02', 'name': 'shell_bean'}, {'id': 13233, 'synset': 'lima_bean.n.03', 'name': 'lima_bean'}, {'id': 13234, 'synset': 'fordhooks.n.01', 'name': 'Fordhooks'}, {'id': 13235, 'synset': 'sieva_bean.n.02', 'name': 'sieva_bean'}, {'id': 13236, 'synset': 'fava_bean.n.02', 'name': 'fava_bean'}, {'id': 13237, 'synset': 'soy.n.04', 'name': 'soy'}, {'id': 13238, 'synset': 'green_soybean.n.01', 'name': 'green_soybean'}, {'id': 13239, 'synset': 'field_soybean.n.01', 'name': 'field_soybean'}, {'id': 13240, 'synset': 'cardoon.n.02', 'name': 'cardoon'}, {'id': 13241, 'synset': 'carrot.n.03', 'name': 'carrot'}, {'id': 13242, 'synset': 'carrot_stick.n.01', 'name': 'carrot_stick'}, {'id': 13243, 'synset': 'celery.n.02', 'name': 'celery'}, {'id': 13244, 'synset': 'pascal_celery.n.01', 'name': 'pascal_celery'}, {'id': 13245, 'synset': 'celeriac.n.02', 'name': 'celeriac'}, {'id': 13246, 'synset': 'chicory.n.04', 'name': 'chicory'}, {'id': 13247, 'synset': 'radicchio.n.01', 'name': 'radicchio'}, {'id': 13248, 'synset': 'coffee_substitute.n.01', 'name': 'coffee_substitute'}, {'id': 13249, 'synset': 'chicory.n.03', 'name': 'chicory'}, {'id': 13250, 'synset': 'postum.n.01', 'name': 'Postum'}, {'id': 13251, 'synset': 'chicory_escarole.n.01', 'name': 'chicory_escarole'}, {'id': 13252, 'synset': 'belgian_endive.n.01', 'name': 'Belgian_endive'}, {'id': 13253, 'synset': 'sweet_corn.n.02', 'name': 'sweet_corn'}, {'id': 13254, 'synset': 'hominy.n.01', 'name': 'hominy'}, {'id': 13255, 'synset': 'lye_hominy.n.01', 'name': 'lye_hominy'}, {'id': 13256, 'synset': 'pearl_hominy.n.01', 'name': 'pearl_hominy'}, {'id': 13257, 'synset': 'popcorn.n.02', 'name': 'popcorn'}, {'id': 13258, 'synset': 'cress.n.02', 'name': 'cress'}, {'id': 13259, 'synset': 'watercress.n.02', 'name': 'watercress'}, {'id': 13260, 'synset': 'garden_cress.n.01', 'name': 'garden_cress'}, {'id': 13261, 'synset': 'winter_cress.n.02', 'name': 'winter_cress'}, {'id': 13262, 'synset': 'dandelion_green.n.02', 'name': 'dandelion_green'}, {'id': 13263, 'synset': 'gumbo.n.03', 'name': 'gumbo'}, {'id': 13264, 'synset': 'kohlrabi.n.02', 'name': 'kohlrabi'}, {'id': 13265, 'synset': "lamb's-quarter.n.01", 'name': "lamb's-quarter"}, {'id': 13266, 'synset': 'wild_spinach.n.03', 'name': 'wild_spinach'}, {'id': 13267, 'synset': 'beefsteak_tomato.n.01', 'name': 'beefsteak_tomato'}, {'id': 13268, 'synset': 'cherry_tomato.n.02', 'name': 'cherry_tomato'}, {'id': 13269, 'synset': 'plum_tomato.n.02', 'name': 'plum_tomato'}, {'id': 13270, 'synset': 'tomatillo.n.03', 'name': 'tomatillo'}, {'id': 13271, 'synset': 'mushroom.n.05', 'name': 'mushroom'}, {'id': 13272, 'synset': 'stuffed_mushroom.n.01', 'name': 'stuffed_mushroom'}, {'id': 13273, 'synset': 'salsify.n.03', 'name': 'salsify'}, {'id': 13274, 'synset': 'oyster_plant.n.03', 'name': 'oyster_plant'}, {'id': 13275, 'synset': 'scorzonera.n.02', 'name': 'scorzonera'}, {'id': 13276, 'synset': 'parsnip.n.03', 'name': 'parsnip'}, {'id': 13277, 'synset': 'radish.n.01', 'name': 'radish'}, {'id': 13278, 'synset': 'turnip.n.02', 'name': 'turnip'}, {'id': 13279, 'synset': 'white_turnip.n.02', 'name': 'white_turnip'}, {'id': 13280, 'synset': 'rutabaga.n.01', 'name': 'rutabaga'}, {'id': 13281, 'synset': 'turnip_greens.n.01', 'name': 'turnip_greens'}, {'id': 13282, 'synset': 'sorrel.n.04', 'name': 'sorrel'}, {'id': 13283, 'synset': 'french_sorrel.n.02', 'name': 'French_sorrel'}, {'id': 13284, 'synset': 'spinach.n.02', 'name': 'spinach'}, {'id': 13285, 'synset': 'taro.n.03', 'name': 'taro'}, {'id': 13286, 'synset': 'truffle.n.02', 'name': 'truffle'}, {'id': 13287, 'synset': 'edible_nut.n.01', 'name': 'edible_nut'}, {'id': 13288, 'synset': 'bunya_bunya.n.02', 'name': 'bunya_bunya'}, {'id': 13289, 'synset': 'peanut.n.04', 'name': 'peanut'}, {'id': 13290, 'synset': 'freestone.n.01', 'name': 'freestone'}, {'id': 13291, 'synset': 'cling.n.01', 'name': 'cling'}, {'id': 13292, 'synset': 'windfall.n.01', 'name': 'windfall'}, {'id': 13293, 'synset': 'crab_apple.n.03', 'name': 'crab_apple'}, {'id': 13294, 'synset': 'eating_apple.n.01', 'name': 'eating_apple'}, {'id': 13295, 'synset': 'baldwin.n.03', 'name': 'Baldwin'}, {'id': 13296, 'synset': 'cortland.n.01', 'name': 'Cortland'}, {'id': 13297, 'synset': "cox's_orange_pippin.n.01", 'name': "Cox's_Orange_Pippin"}, {'id': 13298, 'synset': 'delicious.n.01', 'name': 'Delicious'}, {'id': 13299, 'synset': 'golden_delicious.n.01', 'name': 'Golden_Delicious'}, {'id': 13300, 'synset': 'red_delicious.n.01', 'name': 'Red_Delicious'}, {'id': 13301, 'synset': 'empire.n.05', 'name': 'Empire'}, {'id': 13302, 'synset': "grimes'_golden.n.01", 'name': "Grimes'_golden"}, {'id': 13303, 'synset': 'jonathan.n.01', 'name': 'Jonathan'}, {'id': 13304, 'synset': 'mcintosh.n.01', 'name': 'McIntosh'}, {'id': 13305, 'synset': 'macoun.n.01', 'name': 'Macoun'}, {'id': 13306, 'synset': 'northern_spy.n.01', 'name': 'Northern_Spy'}, {'id': 13307, 'synset': 'pearmain.n.01', 'name': 'Pearmain'}, {'id': 13308, 'synset': 'pippin.n.01', 'name': 'Pippin'}, {'id': 13309, 'synset': 'prima.n.01', 'name': 'Prima'}, {'id': 13310, 'synset': 'stayman.n.01', 'name': 'Stayman'}, {'id': 13311, 'synset': 'winesap.n.01', 'name': 'Winesap'}, {'id': 13312, 'synset': 'stayman_winesap.n.01', 'name': 'Stayman_Winesap'}, {'id': 13313, 'synset': 'cooking_apple.n.01', 'name': 'cooking_apple'}, {'id': 13314, 'synset': "bramley's_seedling.n.01", 'name': "Bramley's_Seedling"}, {'id': 13315, 'synset': 'granny_smith.n.01', 'name': 'Granny_Smith'}, {'id': 13316, 'synset': "lane's_prince_albert.n.01", 'name': "Lane's_Prince_Albert"}, {'id': 13317, 'synset': 'newtown_wonder.n.01', 'name': 'Newtown_Wonder'}, {'id': 13318, 'synset': 'rome_beauty.n.01', 'name': 'Rome_Beauty'}, {'id': 13319, 'synset': 'berry.n.01', 'name': 'berry'}, {'id': 13320, 'synset': 'bilberry.n.03', 'name': 'bilberry'}, {'id': 13321, 'synset': 'huckleberry.n.03', 'name': 'huckleberry'}, {'id': 13322, 'synset': 'wintergreen.n.03', 'name': 'wintergreen'}, {'id': 13323, 'synset': 'cranberry.n.02', 'name': 'cranberry'}, {'id': 13324, 'synset': 'lingonberry.n.02', 'name': 'lingonberry'}, {'id': 13325, 'synset': 'currant.n.01', 'name': 'currant'}, {'id': 13326, 'synset': 'gooseberry.n.02', 'name': 'gooseberry'}, {'id': 13327, 'synset': 'black_currant.n.02', 'name': 'black_currant'}, {'id': 13328, 'synset': 'red_currant.n.02', 'name': 'red_currant'}, {'id': 13329, 'synset': 'boysenberry.n.02', 'name': 'boysenberry'}, {'id': 13330, 'synset': 'dewberry.n.02', 'name': 'dewberry'}, {'id': 13331, 'synset': 'loganberry.n.02', 'name': 'loganberry'}, {'id': 13332, 'synset': 'saskatoon.n.02', 'name': 'saskatoon'}, {'id': 13333, 'synset': 'sugarberry.n.02', 'name': 'sugarberry'}, {'id': 13334, 'synset': 'acerola.n.02', 'name': 'acerola'}, {'id': 13335, 'synset': 'carambola.n.02', 'name': 'carambola'}, {'id': 13336, 'synset': 'ceriman.n.02', 'name': 'ceriman'}, {'id': 13337, 'synset': 'carissa_plum.n.01', 'name': 'carissa_plum'}, {'id': 13338, 'synset': 'citrus.n.01', 'name': 'citrus'}, {'id': 13339, 'synset': 'temple_orange.n.02', 'name': 'temple_orange'}, {'id': 13340, 'synset': 'clementine.n.02', 'name': 'clementine'}, {'id': 13341, 'synset': 'satsuma.n.02', 'name': 'satsuma'}, {'id': 13342, 'synset': 'tangerine.n.02', 'name': 'tangerine'}, {'id': 13343, 'synset': 'tangelo.n.02', 'name': 'tangelo'}, {'id': 13344, 'synset': 'bitter_orange.n.02', 'name': 'bitter_orange'}, {'id': 13345, 'synset': 'sweet_orange.n.01', 'name': 'sweet_orange'}, {'id': 13346, 'synset': 'jaffa_orange.n.01', 'name': 'Jaffa_orange'}, {'id': 13347, 'synset': 'navel_orange.n.01', 'name': 'navel_orange'}, {'id': 13348, 'synset': 'valencia_orange.n.01', 'name': 'Valencia_orange'}, {'id': 13349, 'synset': 'kumquat.n.02', 'name': 'kumquat'}, {'id': 13350, 'synset': 'key_lime.n.01', 'name': 'key_lime'}, {'id': 13351, 'synset': 'grapefruit.n.02', 'name': 'grapefruit'}, {'id': 13352, 'synset': 'pomelo.n.02', 'name': 'pomelo'}, {'id': 13353, 'synset': 'citrange.n.02', 'name': 'citrange'}, {'id': 13354, 'synset': 'citron.n.01', 'name': 'citron'}, {'id': 13355, 'synset': 'jordan_almond.n.02', 'name': 'Jordan_almond'}, {'id': 13356, 'synset': 'nectarine.n.02', 'name': 'nectarine'}, {'id': 13357, 'synset': 'pitahaya.n.02', 'name': 'pitahaya'}, {'id': 13358, 'synset': 'plum.n.02', 'name': 'plum'}, {'id': 13359, 'synset': 'damson.n.01', 'name': 'damson'}, {'id': 13360, 'synset': 'greengage.n.01', 'name': 'greengage'}, {'id': 13361, 'synset': 'beach_plum.n.02', 'name': 'beach_plum'}, {'id': 13362, 'synset': 'sloe.n.03', 'name': 'sloe'}, {'id': 13363, 'synset': 'victoria_plum.n.01', 'name': 'Victoria_plum'}, {'id': 13364, 'synset': 'dried_fruit.n.01', 'name': 'dried_fruit'}, {'id': 13365, 'synset': 'dried_apricot.n.01', 'name': 'dried_apricot'}, {'id': 13366, 'synset': 'raisin.n.01', 'name': 'raisin'}, {'id': 13367, 'synset': 'seedless_raisin.n.01', 'name': 'seedless_raisin'}, {'id': 13368, 'synset': 'seeded_raisin.n.01', 'name': 'seeded_raisin'}, {'id': 13369, 'synset': 'currant.n.03', 'name': 'currant'}, {'id': 13370, 'synset': 'anchovy_pear.n.02', 'name': 'anchovy_pear'}, {'id': 13371, 'synset': 'passion_fruit.n.01', 'name': 'passion_fruit'}, {'id': 13372, 'synset': 'granadilla.n.04', 'name': 'granadilla'}, {'id': 13373, 'synset': 'sweet_calabash.n.02', 'name': 'sweet_calabash'}, {'id': 13374, 'synset': 'bell_apple.n.01', 'name': 'bell_apple'}, {'id': 13375, 'synset': 'breadfruit.n.02', 'name': 'breadfruit'}, {'id': 13376, 'synset': 'jackfruit.n.02', 'name': 'jackfruit'}, {'id': 13377, 'synset': 'cacao_bean.n.01', 'name': 'cacao_bean'}, {'id': 13378, 'synset': 'cocoa.n.02', 'name': 'cocoa'}, {'id': 13379, 'synset': 'canistel.n.02', 'name': 'canistel'}, {'id': 13380, 'synset': 'melon_ball.n.01', 'name': 'melon_ball'}, {'id': 13381, 'synset': 'muskmelon.n.02', 'name': 'muskmelon'}, {'id': 13382, 'synset': 'winter_melon.n.02', 'name': 'winter_melon'}, {'id': 13383, 'synset': 'honeydew.n.01', 'name': 'honeydew'}, {'id': 13384, 'synset': 'persian_melon.n.02', 'name': 'Persian_melon'}, {'id': 13385, 'synset': 'net_melon.n.02', 'name': 'net_melon'}, {'id': 13386, 'synset': 'casaba.n.01', 'name': 'casaba'}, {'id': 13387, 'synset': 'sweet_cherry.n.02', 'name': 'sweet_cherry'}, {'id': 13388, 'synset': 'bing_cherry.n.01', 'name': 'bing_cherry'}, {'id': 13389, 'synset': 'heart_cherry.n.02', 'name': 'heart_cherry'}, {'id': 13390, 'synset': 'blackheart.n.02', 'name': 'blackheart'}, {'id': 13391, 'synset': 'capulin.n.02', 'name': 'capulin'}, {'id': 13392, 'synset': 'sour_cherry.n.03', 'name': 'sour_cherry'}, {'id': 13393, 'synset': 'amarelle.n.02', 'name': 'amarelle'}, {'id': 13394, 'synset': 'morello.n.02', 'name': 'morello'}, {'id': 13395, 'synset': 'cocoa_plum.n.02', 'name': 'cocoa_plum'}, {'id': 13396, 'synset': 'gherkin.n.01', 'name': 'gherkin'}, {'id': 13397, 'synset': 'fox_grape.n.02', 'name': 'fox_grape'}, {'id': 13398, 'synset': 'concord_grape.n.01', 'name': 'Concord_grape'}, {'id': 13399, 'synset': 'catawba.n.02', 'name': 'Catawba'}, {'id': 13400, 'synset': 'muscadine.n.02', 'name': 'muscadine'}, {'id': 13401, 'synset': 'scuppernong.n.01', 'name': 'scuppernong'}, {'id': 13402, 'synset': 'slipskin_grape.n.01', 'name': 'slipskin_grape'}, {'id': 13403, 'synset': 'vinifera_grape.n.02', 'name': 'vinifera_grape'}, {'id': 13404, 'synset': 'emperor.n.02', 'name': 'emperor'}, {'id': 13405, 'synset': 'muscat.n.04', 'name': 'muscat'}, {'id': 13406, 'synset': 'ribier.n.01', 'name': 'ribier'}, {'id': 13407, 'synset': 'sultana.n.01', 'name': 'sultana'}, {'id': 13408, 'synset': 'tokay.n.02', 'name': 'Tokay'}, {'id': 13409, 'synset': 'flame_tokay.n.01', 'name': 'flame_tokay'}, {'id': 13410, 'synset': 'thompson_seedless.n.01', 'name': 'Thompson_Seedless'}, {'id': 13411, 'synset': 'custard_apple.n.02', 'name': 'custard_apple'}, {'id': 13412, 'synset': 'cherimoya.n.02', 'name': 'cherimoya'}, {'id': 13413, 'synset': 'soursop.n.02', 'name': 'soursop'}, {'id': 13414, 'synset': 'sweetsop.n.02', 'name': 'sweetsop'}, {'id': 13415, 'synset': 'ilama.n.02', 'name': 'ilama'}, {'id': 13416, 'synset': 'pond_apple.n.02', 'name': 'pond_apple'}, {'id': 13417, 'synset': 'papaw.n.02', 'name': 'papaw'}, {'id': 13418, 'synset': 'kai_apple.n.01', 'name': 'kai_apple'}, {'id': 13419, 'synset': 'ketembilla.n.02', 'name': 'ketembilla'}, {'id': 13420, 'synset': 'ackee.n.01', 'name': 'ackee'}, {'id': 13421, 'synset': 'durian.n.02', 'name': 'durian'}, {'id': 13422, 'synset': 'feijoa.n.02', 'name': 'feijoa'}, {'id': 13423, 'synset': 'genip.n.02', 'name': 'genip'}, {'id': 13424, 'synset': 'genipap.n.01', 'name': 'genipap'}, {'id': 13425, 'synset': 'loquat.n.02', 'name': 'loquat'}, {'id': 13426, 'synset': 'mangosteen.n.02', 'name': 'mangosteen'}, {'id': 13427, 'synset': 'mango.n.02', 'name': 'mango'}, {'id': 13428, 'synset': 'sapodilla.n.02', 'name': 'sapodilla'}, {'id': 13429, 'synset': 'sapote.n.02', 'name': 'sapote'}, {'id': 13430, 'synset': 'tamarind.n.02', 'name': 'tamarind'}, {'id': 13431, 'synset': 'elderberry.n.02', 'name': 'elderberry'}, {'id': 13432, 'synset': 'guava.n.03', 'name': 'guava'}, {'id': 13433, 'synset': 'mombin.n.02', 'name': 'mombin'}, {'id': 13434, 'synset': 'hog_plum.n.04', 'name': 'hog_plum'}, {'id': 13435, 'synset': 'hog_plum.n.03', 'name': 'hog_plum'}, {'id': 13436, 'synset': 'jaboticaba.n.02', 'name': 'jaboticaba'}, {'id': 13437, 'synset': 'jujube.n.02', 'name': 'jujube'}, {'id': 13438, 'synset': 'litchi.n.02', 'name': 'litchi'}, {'id': 13439, 'synset': 'longanberry.n.02', 'name': 'longanberry'}, {'id': 13440, 'synset': 'mamey.n.02', 'name': 'mamey'}, {'id': 13441, 'synset': 'marang.n.02', 'name': 'marang'}, {'id': 13442, 'synset': 'medlar.n.04', 'name': 'medlar'}, {'id': 13443, 'synset': 'medlar.n.03', 'name': 'medlar'}, {'id': 13444, 'synset': 'mulberry.n.02', 'name': 'mulberry'}, {'id': 13445, 'synset': 'olive.n.04', 'name': 'olive'}, {'id': 13446, 'synset': 'black_olive.n.01', 'name': 'black_olive'}, {'id': 13447, 'synset': 'green_olive.n.01', 'name': 'green_olive'}, {'id': 13448, 'synset': 'bosc.n.01', 'name': 'bosc'}, {'id': 13449, 'synset': 'anjou.n.02', 'name': 'anjou'}, {'id': 13450, 'synset': 'bartlett.n.03', 'name': 'bartlett'}, {'id': 13451, 'synset': 'seckel.n.01', 'name': 'seckel'}, {'id': 13452, 'synset': 'plantain.n.03', 'name': 'plantain'}, {'id': 13453, 'synset': 'plumcot.n.02', 'name': 'plumcot'}, {'id': 13454, 'synset': 'pomegranate.n.02', 'name': 'pomegranate'}, {'id': 13455, 'synset': 'prickly_pear.n.02', 'name': 'prickly_pear'}, {'id': 13456, 'synset': 'barbados_gooseberry.n.02', 'name': 'Barbados_gooseberry'}, {'id': 13457, 'synset': 'quandong.n.04', 'name': 'quandong'}, {'id': 13458, 'synset': 'quandong_nut.n.01', 'name': 'quandong_nut'}, {'id': 13459, 'synset': 'quince.n.02', 'name': 'quince'}, {'id': 13460, 'synset': 'rambutan.n.02', 'name': 'rambutan'}, {'id': 13461, 'synset': 'pulasan.n.02', 'name': 'pulasan'}, {'id': 13462, 'synset': 'rose_apple.n.02', 'name': 'rose_apple'}, {'id': 13463, 'synset': 'sorb.n.01', 'name': 'sorb'}, {'id': 13464, 'synset': 'sour_gourd.n.02', 'name': 'sour_gourd'}, {'id': 13465, 'synset': 'edible_seed.n.01', 'name': 'edible_seed'}, {'id': 13466, 'synset': 'pumpkin_seed.n.01', 'name': 'pumpkin_seed'}, {'id': 13467, 'synset': 'betel_nut.n.01', 'name': 'betel_nut'}, {'id': 13468, 'synset': 'beechnut.n.01', 'name': 'beechnut'}, {'id': 13469, 'synset': 'walnut.n.01', 'name': 'walnut'}, {'id': 13470, 'synset': 'black_walnut.n.02', 'name': 'black_walnut'}, {'id': 13471, 'synset': 'english_walnut.n.02', 'name': 'English_walnut'}, {'id': 13472, 'synset': 'brazil_nut.n.02', 'name': 'brazil_nut'}, {'id': 13473, 'synset': 'butternut.n.02', 'name': 'butternut'}, {'id': 13474, 'synset': 'souari_nut.n.02', 'name': 'souari_nut'}, {'id': 13475, 'synset': 'cashew.n.02', 'name': 'cashew'}, {'id': 13476, 'synset': 'chestnut.n.03', 'name': 'chestnut'}, {'id': 13477, 'synset': 'chincapin.n.01', 'name': 'chincapin'}, {'id': 13478, 'synset': 'hazelnut.n.02', 'name': 'hazelnut'}, {'id': 13479, 'synset': 'coconut_milk.n.02', 'name': 'coconut_milk'}, {'id': 13480, 'synset': 'grugru_nut.n.01', 'name': 'grugru_nut'}, {'id': 13481, 'synset': 'hickory_nut.n.01', 'name': 'hickory_nut'}, {'id': 13482, 'synset': 'cola_extract.n.01', 'name': 'cola_extract'}, {'id': 13483, 'synset': 'macadamia_nut.n.02', 'name': 'macadamia_nut'}, {'id': 13484, 'synset': 'pecan.n.03', 'name': 'pecan'}, {'id': 13485, 'synset': 'pine_nut.n.01', 'name': 'pine_nut'}, {'id': 13486, 'synset': 'pistachio.n.02', 'name': 'pistachio'}, {'id': 13487, 'synset': 'sunflower_seed.n.01', 'name': 'sunflower_seed'}, {'id': 13488, 'synset': 'anchovy_paste.n.01', 'name': 'anchovy_paste'}, {'id': 13489, 'synset': 'rollmops.n.01', 'name': 'rollmops'}, {'id': 13490, 'synset': 'feed.n.01', 'name': 'feed'}, {'id': 13491, 'synset': 'cattle_cake.n.01', 'name': 'cattle_cake'}, {'id': 13492, 'synset': 'creep_feed.n.01', 'name': 'creep_feed'}, {'id': 13493, 'synset': 'fodder.n.02', 'name': 'fodder'}, {'id': 13494, 'synset': 'feed_grain.n.01', 'name': 'feed_grain'}, {'id': 13495, 'synset': 'eatage.n.01', 'name': 'eatage'}, {'id': 13496, 'synset': 'silage.n.01', 'name': 'silage'}, {'id': 13497, 'synset': 'oil_cake.n.01', 'name': 'oil_cake'}, {'id': 13498, 'synset': 'oil_meal.n.01', 'name': 'oil_meal'}, {'id': 13499, 'synset': 'alfalfa.n.02', 'name': 'alfalfa'}, {'id': 13500, 'synset': 'broad_bean.n.03', 'name': 'broad_bean'}, {'id': 13501, 'synset': 'hay.n.01', 'name': 'hay'}, {'id': 13502, 'synset': 'timothy.n.03', 'name': 'timothy'}, {'id': 13503, 'synset': 'stover.n.01', 'name': 'stover'}, {'id': 13504, 'synset': 'grain.n.02', 'name': 'grain'}, {'id': 13505, 'synset': 'grist.n.01', 'name': 'grist'}, {'id': 13506, 'synset': 'groats.n.01', 'name': 'groats'}, {'id': 13507, 'synset': 'millet.n.03', 'name': 'millet'}, {'id': 13508, 'synset': 'barley.n.01', 'name': 'barley'}, {'id': 13509, 'synset': 'pearl_barley.n.01', 'name': 'pearl_barley'}, {'id': 13510, 'synset': 'buckwheat.n.02', 'name': 'buckwheat'}, {'id': 13511, 'synset': 'bulgur.n.01', 'name': 'bulgur'}, {'id': 13512, 'synset': 'wheat.n.02', 'name': 'wheat'}, {'id': 13513, 'synset': 'cracked_wheat.n.01', 'name': 'cracked_wheat'}, {'id': 13514, 'synset': 'stodge.n.01', 'name': 'stodge'}, {'id': 13515, 'synset': 'wheat_germ.n.01', 'name': 'wheat_germ'}, {'id': 13516, 'synset': 'oat.n.02', 'name': 'oat'}, {'id': 13517, 'synset': 'rice.n.01', 'name': 'rice'}, {'id': 13518, 'synset': 'brown_rice.n.01', 'name': 'brown_rice'}, {'id': 13519, 'synset': 'white_rice.n.01', 'name': 'white_rice'}, {'id': 13520, 'synset': 'wild_rice.n.02', 'name': 'wild_rice'}, {'id': 13521, 'synset': 'paddy.n.03', 'name': 'paddy'}, {'id': 13522, 'synset': 'slop.n.01', 'name': 'slop'}, {'id': 13523, 'synset': 'mash.n.02', 'name': 'mash'}, {'id': 13524, 'synset': 'chicken_feed.n.01', 'name': 'chicken_feed'}, {'id': 13525, 'synset': 'cud.n.01', 'name': 'cud'}, {'id': 13526, 'synset': 'bird_feed.n.01', 'name': 'bird_feed'}, {'id': 13527, 'synset': 'petfood.n.01', 'name': 'petfood'}, {'id': 13528, 'synset': 'dog_food.n.01', 'name': 'dog_food'}, {'id': 13529, 'synset': 'cat_food.n.01', 'name': 'cat_food'}, {'id': 13530, 'synset': 'canary_seed.n.01', 'name': 'canary_seed'}, {'id': 13531, 'synset': 'tossed_salad.n.01', 'name': 'tossed_salad'}, {'id': 13532, 'synset': 'green_salad.n.01', 'name': 'green_salad'}, {'id': 13533, 'synset': 'caesar_salad.n.01', 'name': 'Caesar_salad'}, {'id': 13534, 'synset': 'salmagundi.n.02', 'name': 'salmagundi'}, {'id': 13535, 'synset': 'salad_nicoise.n.01', 'name': 'salad_nicoise'}, {'id': 13536, 'synset': 'combination_salad.n.01', 'name': 'combination_salad'}, {'id': 13537, 'synset': "chef's_salad.n.01", 'name': "chef's_salad"}, {'id': 13538, 'synset': 'potato_salad.n.01', 'name': 'potato_salad'}, {'id': 13539, 'synset': 'pasta_salad.n.01', 'name': 'pasta_salad'}, {'id': 13540, 'synset': 'macaroni_salad.n.01', 'name': 'macaroni_salad'}, {'id': 13541, 'synset': 'fruit_salad.n.01', 'name': 'fruit_salad'}, {'id': 13542, 'synset': 'waldorf_salad.n.01', 'name': 'Waldorf_salad'}, {'id': 13543, 'synset': 'crab_louis.n.01', 'name': 'crab_Louis'}, {'id': 13544, 'synset': 'herring_salad.n.01', 'name': 'herring_salad'}, {'id': 13545, 'synset': 'tuna_fish_salad.n.01', 'name': 'tuna_fish_salad'}, {'id': 13546, 'synset': 'chicken_salad.n.01', 'name': 'chicken_salad'}, {'id': 13547, 'synset': 'aspic.n.01', 'name': 'aspic'}, {'id': 13548, 'synset': 'molded_salad.n.01', 'name': 'molded_salad'}, {'id': 13549, 'synset': 'tabbouleh.n.01', 'name': 'tabbouleh'}, {'id': 13550, 'synset': 'ingredient.n.03', 'name': 'ingredient'}, {'id': 13551, 'synset': 'flavorer.n.01', 'name': 'flavorer'}, {'id': 13552, 'synset': 'bouillon_cube.n.01', 'name': 'bouillon_cube'}, {'id': 13553, 'synset': 'herb.n.02', 'name': 'herb'}, {'id': 13554, 'synset': 'fines_herbes.n.01', 'name': 'fines_herbes'}, {'id': 13555, 'synset': 'spice.n.02', 'name': 'spice'}, {'id': 13556, 'synset': 'spearmint_oil.n.01', 'name': 'spearmint_oil'}, {'id': 13557, 'synset': 'lemon_oil.n.01', 'name': 'lemon_oil'}, {'id': 13558, 'synset': 'wintergreen_oil.n.01', 'name': 'wintergreen_oil'}, {'id': 13559, 'synset': 'salt.n.02', 'name': 'salt'}, {'id': 13560, 'synset': 'celery_salt.n.01', 'name': 'celery_salt'}, {'id': 13561, 'synset': 'onion_salt.n.01', 'name': 'onion_salt'}, {'id': 13562, 'synset': 'seasoned_salt.n.01', 'name': 'seasoned_salt'}, {'id': 13563, 'synset': 'sour_salt.n.01', 'name': 'sour_salt'}, {'id': 13564, 'synset': 'five_spice_powder.n.01', 'name': 'five_spice_powder'}, {'id': 13565, 'synset': 'allspice.n.03', 'name': 'allspice'}, {'id': 13566, 'synset': 'cinnamon.n.03', 'name': 'cinnamon'}, {'id': 13567, 'synset': 'stick_cinnamon.n.01', 'name': 'stick_cinnamon'}, {'id': 13568, 'synset': 'clove.n.04', 'name': 'clove'}, {'id': 13569, 'synset': 'cumin.n.02', 'name': 'cumin'}, {'id': 13570, 'synset': 'fennel.n.04', 'name': 'fennel'}, {'id': 13571, 'synset': 'ginger.n.02', 'name': 'ginger'}, {'id': 13572, 'synset': 'mace.n.03', 'name': 'mace'}, {'id': 13573, 'synset': 'nutmeg.n.02', 'name': 'nutmeg'}, {'id': 13574, 'synset': 'black_pepper.n.02', 'name': 'black_pepper'}, {'id': 13575, 'synset': 'white_pepper.n.02', 'name': 'white_pepper'}, {'id': 13576, 'synset': 'sassafras.n.02', 'name': 'sassafras'}, {'id': 13577, 'synset': 'basil.n.03', 'name': 'basil'}, {'id': 13578, 'synset': 'bay_leaf.n.01', 'name': 'bay_leaf'}, {'id': 13579, 'synset': 'borage.n.02', 'name': 'borage'}, {'id': 13580, 'synset': 'hyssop.n.02', 'name': 'hyssop'}, {'id': 13581, 'synset': 'caraway.n.02', 'name': 'caraway'}, {'id': 13582, 'synset': 'chervil.n.02', 'name': 'chervil'}, {'id': 13583, 'synset': 'chives.n.02', 'name': 'chives'}, {'id': 13584, 'synset': 'comfrey.n.02', 'name': 'comfrey'}, {'id': 13585, 'synset': 'coriander.n.03', 'name': 'coriander'}, {'id': 13586, 'synset': 'coriander.n.02', 'name': 'coriander'}, {'id': 13587, 'synset': 'costmary.n.02', 'name': 'costmary'}, {'id': 13588, 'synset': 'fennel.n.03', 'name': 'fennel'}, {'id': 13589, 'synset': 'fennel.n.02', 'name': 'fennel'}, {'id': 13590, 'synset': 'fennel_seed.n.01', 'name': 'fennel_seed'}, {'id': 13591, 'synset': 'fenugreek.n.02', 'name': 'fenugreek'}, {'id': 13592, 'synset': 'clove.n.03', 'name': 'clove'}, {'id': 13593, 'synset': 'garlic_chive.n.02', 'name': 'garlic_chive'}, {'id': 13594, 'synset': 'lemon_balm.n.02', 'name': 'lemon_balm'}, {'id': 13595, 'synset': 'lovage.n.02', 'name': 'lovage'}, {'id': 13596, 'synset': 'marjoram.n.02', 'name': 'marjoram'}, {'id': 13597, 'synset': 'mint.n.04', 'name': 'mint'}, {'id': 13598, 'synset': 'mustard_seed.n.01', 'name': 'mustard_seed'}, {'id': 13599, 'synset': 'mustard.n.02', 'name': 'mustard'}, {'id': 13600, 'synset': 'chinese_mustard.n.02', 'name': 'Chinese_mustard'}, {'id': 13601, 'synset': 'nasturtium.n.03', 'name': 'nasturtium'}, {'id': 13602, 'synset': 'parsley.n.02', 'name': 'parsley'}, {'id': 13603, 'synset': 'salad_burnet.n.02', 'name': 'salad_burnet'}, {'id': 13604, 'synset': 'rosemary.n.02', 'name': 'rosemary'}, {'id': 13605, 'synset': 'rue.n.02', 'name': 'rue'}, {'id': 13606, 'synset': 'sage.n.02', 'name': 'sage'}, {'id': 13607, 'synset': 'clary_sage.n.02', 'name': 'clary_sage'}, {'id': 13608, 'synset': 'savory.n.03', 'name': 'savory'}, {'id': 13609, 'synset': 'summer_savory.n.02', 'name': 'summer_savory'}, {'id': 13610, 'synset': 'winter_savory.n.02', 'name': 'winter_savory'}, {'id': 13611, 'synset': 'sweet_woodruff.n.02', 'name': 'sweet_woodruff'}, {'id': 13612, 'synset': 'sweet_cicely.n.03', 'name': 'sweet_cicely'}, {'id': 13613, 'synset': 'tarragon.n.02', 'name': 'tarragon'}, {'id': 13614, 'synset': 'thyme.n.02', 'name': 'thyme'}, {'id': 13615, 'synset': 'turmeric.n.02', 'name': 'turmeric'}, {'id': 13616, 'synset': 'caper.n.02', 'name': 'caper'}, {'id': 13617, 'synset': 'catsup.n.01', 'name': 'catsup'}, {'id': 13618, 'synset': 'cardamom.n.02', 'name': 'cardamom'}, {'id': 13619, 'synset': 'chili_powder.n.01', 'name': 'chili_powder'}, {'id': 13620, 'synset': 'chili_sauce.n.01', 'name': 'chili_sauce'}, {'id': 13621, 'synset': 'chutney.n.01', 'name': 'chutney'}, {'id': 13622, 'synset': 'steak_sauce.n.01', 'name': 'steak_sauce'}, {'id': 13623, 'synset': 'taco_sauce.n.01', 'name': 'taco_sauce'}, {'id': 13624, 'synset': 'mint_sauce.n.01', 'name': 'mint_sauce'}, {'id': 13625, 'synset': 'cranberry_sauce.n.01', 'name': 'cranberry_sauce'}, {'id': 13626, 'synset': 'curry_powder.n.01', 'name': 'curry_powder'}, {'id': 13627, 'synset': 'curry.n.01', 'name': 'curry'}, {'id': 13628, 'synset': 'lamb_curry.n.01', 'name': 'lamb_curry'}, {'id': 13629, 'synset': 'duck_sauce.n.01', 'name': 'duck_sauce'}, {'id': 13630, 'synset': 'horseradish.n.03', 'name': 'horseradish'}, {'id': 13631, 'synset': 'marinade.n.01', 'name': 'marinade'}, {'id': 13632, 'synset': 'paprika.n.02', 'name': 'paprika'}, {'id': 13633, 'synset': 'spanish_paprika.n.01', 'name': 'Spanish_paprika'}, {'id': 13634, 'synset': 'dill_pickle.n.01', 'name': 'dill_pickle'}, {'id': 13635, 'synset': 'bread_and_butter_pickle.n.01', 'name': 'bread_and_butter_pickle'}, {'id': 13636, 'synset': 'pickle_relish.n.01', 'name': 'pickle_relish'}, {'id': 13637, 'synset': 'piccalilli.n.01', 'name': 'piccalilli'}, {'id': 13638, 'synset': 'sweet_pickle.n.01', 'name': 'sweet_pickle'}, {'id': 13639, 'synset': 'soy_sauce.n.01', 'name': 'soy_sauce'}, {'id': 13640, 'synset': 'tomato_paste.n.01', 'name': 'tomato_paste'}, {'id': 13641, 'synset': 'angelica.n.03', 'name': 'angelica'}, {'id': 13642, 'synset': 'angelica.n.02', 'name': 'angelica'}, {'id': 13643, 'synset': 'almond_extract.n.01', 'name': 'almond_extract'}, {'id': 13644, 'synset': 'anise.n.02', 'name': 'anise'}, {'id': 13645, 'synset': 'chinese_anise.n.02', 'name': 'Chinese_anise'}, {'id': 13646, 'synset': 'juniper_berries.n.01', 'name': 'juniper_berries'}, {'id': 13647, 'synset': 'saffron.n.02', 'name': 'saffron'}, {'id': 13648, 'synset': 'sesame_seed.n.01', 'name': 'sesame_seed'}, {'id': 13649, 'synset': 'caraway_seed.n.01', 'name': 'caraway_seed'}, {'id': 13650, 'synset': 'poppy_seed.n.01', 'name': 'poppy_seed'}, {'id': 13651, 'synset': 'dill.n.02', 'name': 'dill'}, {'id': 13652, 'synset': 'dill_seed.n.01', 'name': 'dill_seed'}, {'id': 13653, 'synset': 'celery_seed.n.01', 'name': 'celery_seed'}, {'id': 13654, 'synset': 'lemon_extract.n.01', 'name': 'lemon_extract'}, {'id': 13655, 'synset': 'monosodium_glutamate.n.01', 'name': 'monosodium_glutamate'}, {'id': 13656, 'synset': 'vanilla_bean.n.01', 'name': 'vanilla_bean'}, {'id': 13657, 'synset': 'cider_vinegar.n.01', 'name': 'cider_vinegar'}, {'id': 13658, 'synset': 'wine_vinegar.n.01', 'name': 'wine_vinegar'}, {'id': 13659, 'synset': 'sauce.n.01', 'name': 'sauce'}, {'id': 13660, 'synset': 'anchovy_sauce.n.01', 'name': 'anchovy_sauce'}, {'id': 13661, 'synset': 'hard_sauce.n.01', 'name': 'hard_sauce'}, {'id': 13662, 'synset': 'horseradish_sauce.n.01', 'name': 'horseradish_sauce'}, {'id': 13663, 'synset': 'bolognese_pasta_sauce.n.01', 'name': 'bolognese_pasta_sauce'}, {'id': 13664, 'synset': 'carbonara.n.01', 'name': 'carbonara'}, {'id': 13665, 'synset': 'tomato_sauce.n.01', 'name': 'tomato_sauce'}, {'id': 13666, 'synset': 'tartare_sauce.n.01', 'name': 'tartare_sauce'}, {'id': 13667, 'synset': 'wine_sauce.n.01', 'name': 'wine_sauce'}, {'id': 13668, 'synset': 'marchand_de_vin.n.01', 'name': 'marchand_de_vin'}, {'id': 13669, 'synset': 'bread_sauce.n.01', 'name': 'bread_sauce'}, {'id': 13670, 'synset': 'plum_sauce.n.01', 'name': 'plum_sauce'}, {'id': 13671, 'synset': 'peach_sauce.n.01', 'name': 'peach_sauce'}, {'id': 13672, 'synset': 'apricot_sauce.n.01', 'name': 'apricot_sauce'}, {'id': 13673, 'synset': 'pesto.n.01', 'name': 'pesto'}, {'id': 13674, 'synset': 'ravigote.n.01', 'name': 'ravigote'}, {'id': 13675, 'synset': 'remoulade_sauce.n.01', 'name': 'remoulade_sauce'}, {'id': 13676, 'synset': 'dressing.n.01', 'name': 'dressing'}, {'id': 13677, 'synset': 'sauce_louis.n.01', 'name': 'sauce_Louis'}, {'id': 13678, 'synset': 'bleu_cheese_dressing.n.01', 'name': 'bleu_cheese_dressing'}, {'id': 13679, 'synset': 'blue_cheese_dressing.n.01', 'name': 'blue_cheese_dressing'}, {'id': 13680, 'synset': 'french_dressing.n.01', 'name': 'French_dressing'}, {'id': 13681, 'synset': 'lorenzo_dressing.n.01', 'name': 'Lorenzo_dressing'}, {'id': 13682, 'synset': 'anchovy_dressing.n.01', 'name': 'anchovy_dressing'}, {'id': 13683, 'synset': 'italian_dressing.n.01', 'name': 'Italian_dressing'}, {'id': 13684, 'synset': 'half-and-half_dressing.n.01', 'name': 'half-and-half_dressing'}, {'id': 13685, 'synset': 'mayonnaise.n.01', 'name': 'mayonnaise'}, {'id': 13686, 'synset': 'green_mayonnaise.n.01', 'name': 'green_mayonnaise'}, {'id': 13687, 'synset': 'aioli.n.01', 'name': 'aioli'}, {'id': 13688, 'synset': 'russian_dressing.n.01', 'name': 'Russian_dressing'}, {'id': 13689, 'synset': 'salad_cream.n.01', 'name': 'salad_cream'}, {'id': 13690, 'synset': 'thousand_island_dressing.n.01', 'name': 'Thousand_Island_dressing'}, {'id': 13691, 'synset': 'barbecue_sauce.n.01', 'name': 'barbecue_sauce'}, {'id': 13692, 'synset': 'hollandaise.n.01', 'name': 'hollandaise'}, {'id': 13693, 'synset': 'bearnaise.n.01', 'name': 'bearnaise'}, {'id': 13694, 'synset': 'bercy.n.01', 'name': 'Bercy'}, {'id': 13695, 'synset': 'bordelaise.n.01', 'name': 'bordelaise'}, {'id': 13696, 'synset': 'bourguignon.n.01', 'name': 'bourguignon'}, {'id': 13697, 'synset': 'brown_sauce.n.02', 'name': 'brown_sauce'}, {'id': 13698, 'synset': 'espagnole.n.01', 'name': 'Espagnole'}, {'id': 13699, 'synset': 'chinese_brown_sauce.n.01', 'name': 'Chinese_brown_sauce'}, {'id': 13700, 'synset': 'blanc.n.01', 'name': 'blanc'}, {'id': 13701, 'synset': 'cheese_sauce.n.01', 'name': 'cheese_sauce'}, {'id': 13702, 'synset': 'chocolate_sauce.n.01', 'name': 'chocolate_sauce'}, {'id': 13703, 'synset': 'hot-fudge_sauce.n.01', 'name': 'hot-fudge_sauce'}, {'id': 13704, 'synset': 'cocktail_sauce.n.01', 'name': 'cocktail_sauce'}, {'id': 13705, 'synset': 'colbert.n.01', 'name': 'Colbert'}, {'id': 13706, 'synset': 'white_sauce.n.01', 'name': 'white_sauce'}, {'id': 13707, 'synset': 'cream_sauce.n.01', 'name': 'cream_sauce'}, {'id': 13708, 'synset': 'mornay_sauce.n.01', 'name': 'Mornay_sauce'}, {'id': 13709, 'synset': 'demiglace.n.01', 'name': 'demiglace'}, {'id': 13710, 'synset': 'gravy.n.02', 'name': 'gravy'}, {'id': 13711, 'synset': 'gravy.n.01', 'name': 'gravy'}, {'id': 13712, 'synset': 'spaghetti_sauce.n.01', 'name': 'spaghetti_sauce'}, {'id': 13713, 'synset': 'marinara.n.01', 'name': 'marinara'}, {'id': 13714, 'synset': 'mole.n.03', 'name': 'mole'}, {'id': 13715, 'synset': "hunter's_sauce.n.01", 'name': "hunter's_sauce"}, {'id': 13716, 'synset': 'mushroom_sauce.n.01', 'name': 'mushroom_sauce'}, {'id': 13717, 'synset': 'mustard_sauce.n.01', 'name': 'mustard_sauce'}, {'id': 13718, 'synset': 'nantua.n.01', 'name': 'Nantua'}, {'id': 13719, 'synset': 'hungarian_sauce.n.01', 'name': 'Hungarian_sauce'}, {'id': 13720, 'synset': 'pepper_sauce.n.01', 'name': 'pepper_sauce'}, {'id': 13721, 'synset': 'roux.n.01', 'name': 'roux'}, {'id': 13722, 'synset': 'smitane.n.01', 'name': 'Smitane'}, {'id': 13723, 'synset': 'soubise.n.01', 'name': 'Soubise'}, {'id': 13724, 'synset': 'lyonnaise_sauce.n.01', 'name': 'Lyonnaise_sauce'}, {'id': 13725, 'synset': 'veloute.n.01', 'name': 'veloute'}, {'id': 13726, 'synset': 'allemande.n.01', 'name': 'allemande'}, {'id': 13727, 'synset': 'caper_sauce.n.01', 'name': 'caper_sauce'}, {'id': 13728, 'synset': 'poulette.n.01', 'name': 'poulette'}, {'id': 13729, 'synset': 'curry_sauce.n.01', 'name': 'curry_sauce'}, {'id': 13730, 'synset': 'worcester_sauce.n.01', 'name': 'Worcester_sauce'}, {'id': 13731, 'synset': 'coconut_milk.n.01', 'name': 'coconut_milk'}, {'id': 13732, 'synset': 'egg_white.n.01', 'name': 'egg_white'}, {'id': 13733, 'synset': 'hard-boiled_egg.n.01', 'name': 'hard-boiled_egg'}, {'id': 13734, 'synset': 'easter_egg.n.02', 'name': 'Easter_egg'}, {'id': 13735, 'synset': 'easter_egg.n.01', 'name': 'Easter_egg'}, {'id': 13736, 'synset': 'chocolate_egg.n.01', 'name': 'chocolate_egg'}, {'id': 13737, 'synset': 'candy_egg.n.01', 'name': 'candy_egg'}, {'id': 13738, 'synset': 'poached_egg.n.01', 'name': 'poached_egg'}, {'id': 13739, 'synset': 'scrambled_eggs.n.01', 'name': 'scrambled_eggs'}, {'id': 13740, 'synset': 'deviled_egg.n.01', 'name': 'deviled_egg'}, {'id': 13741, 'synset': 'shirred_egg.n.01', 'name': 'shirred_egg'}, {'id': 13742, 'synset': 'firm_omelet.n.01', 'name': 'firm_omelet'}, {'id': 13743, 'synset': 'french_omelet.n.01', 'name': 'French_omelet'}, {'id': 13744, 'synset': 'fluffy_omelet.n.01', 'name': 'fluffy_omelet'}, {'id': 13745, 'synset': 'western_omelet.n.01', 'name': 'western_omelet'}, {'id': 13746, 'synset': 'souffle.n.01', 'name': 'souffle'}, {'id': 13747, 'synset': 'fried_egg.n.01', 'name': 'fried_egg'}, {'id': 13748, 'synset': 'dairy_product.n.01', 'name': 'dairy_product'}, {'id': 13749, 'synset': 'milk.n.04', 'name': 'milk'}, {'id': 13750, 'synset': 'sour_milk.n.01', 'name': 'sour_milk'}, {'id': 13751, 'synset': 'formula.n.06', 'name': 'formula'}, {'id': 13752, 'synset': 'pasteurized_milk.n.01', 'name': 'pasteurized_milk'}, {'id': 13753, 'synset': "cows'_milk.n.01", 'name': "cows'_milk"}, {'id': 13754, 'synset': "yak's_milk.n.01", 'name': "yak's_milk"}, {'id': 13755, 'synset': "goats'_milk.n.01", 'name': "goats'_milk"}, {'id': 13756, 'synset': 'acidophilus_milk.n.01', 'name': 'acidophilus_milk'}, {'id': 13757, 'synset': 'raw_milk.n.01', 'name': 'raw_milk'}, {'id': 13758, 'synset': 'scalded_milk.n.01', 'name': 'scalded_milk'}, {'id': 13759, 'synset': 'homogenized_milk.n.01', 'name': 'homogenized_milk'}, {'id': 13760, 'synset': 'certified_milk.n.01', 'name': 'certified_milk'}, {'id': 13761, 'synset': 'powdered_milk.n.01', 'name': 'powdered_milk'}, {'id': 13762, 'synset': 'nonfat_dry_milk.n.01', 'name': 'nonfat_dry_milk'}, {'id': 13763, 'synset': 'evaporated_milk.n.01', 'name': 'evaporated_milk'}, {'id': 13764, 'synset': 'condensed_milk.n.01', 'name': 'condensed_milk'}, {'id': 13765, 'synset': 'skim_milk.n.01', 'name': 'skim_milk'}, {'id': 13766, 'synset': 'semi-skimmed_milk.n.01', 'name': 'semi-skimmed_milk'}, {'id': 13767, 'synset': 'whole_milk.n.01', 'name': 'whole_milk'}, {'id': 13768, 'synset': 'low-fat_milk.n.01', 'name': 'low-fat_milk'}, {'id': 13769, 'synset': 'buttermilk.n.01', 'name': 'buttermilk'}, {'id': 13770, 'synset': 'cream.n.02', 'name': 'cream'}, {'id': 13771, 'synset': 'clotted_cream.n.01', 'name': 'clotted_cream'}, {'id': 13772, 'synset': 'double_creme.n.01', 'name': 'double_creme'}, {'id': 13773, 'synset': 'half-and-half.n.01', 'name': 'half-and-half'}, {'id': 13774, 'synset': 'heavy_cream.n.01', 'name': 'heavy_cream'}, {'id': 13775, 'synset': 'light_cream.n.01', 'name': 'light_cream'}, {'id': 13776, 'synset': 'whipping_cream.n.01', 'name': 'whipping_cream'}, {'id': 13777, 'synset': 'clarified_butter.n.01', 'name': 'clarified_butter'}, {'id': 13778, 'synset': 'ghee.n.01', 'name': 'ghee'}, {'id': 13779, 'synset': 'brown_butter.n.01', 'name': 'brown_butter'}, {'id': 13780, 'synset': 'meuniere_butter.n.01', 'name': 'Meuniere_butter'}, {'id': 13781, 'synset': 'blueberry_yogurt.n.01', 'name': 'blueberry_yogurt'}, {'id': 13782, 'synset': 'raita.n.01', 'name': 'raita'}, {'id': 13783, 'synset': 'whey.n.02', 'name': 'whey'}, {'id': 13784, 'synset': 'curd.n.02', 'name': 'curd'}, {'id': 13785, 'synset': 'curd.n.01', 'name': 'curd'}, {'id': 13786, 'synset': 'clabber.n.01', 'name': 'clabber'}, {'id': 13787, 'synset': 'cheese.n.01', 'name': 'cheese'}, {'id': 13788, 'synset': 'paring.n.02', 'name': 'paring'}, {'id': 13789, 'synset': 'cream_cheese.n.01', 'name': 'cream_cheese'}, {'id': 13790, 'synset': 'double_cream.n.01', 'name': 'double_cream'}, {'id': 13791, 'synset': 'mascarpone.n.01', 'name': 'mascarpone'}, {'id': 13792, 'synset': 'triple_cream.n.01', 'name': 'triple_cream'}, {'id': 13793, 'synset': 'cottage_cheese.n.01', 'name': 'cottage_cheese'}, {'id': 13794, 'synset': 'process_cheese.n.01', 'name': 'process_cheese'}, {'id': 13795, 'synset': 'bleu.n.01', 'name': 'bleu'}, {'id': 13796, 'synset': 'stilton.n.01', 'name': 'Stilton'}, {'id': 13797, 'synset': 'roquefort.n.01', 'name': 'Roquefort'}, {'id': 13798, 'synset': 'gorgonzola.n.01', 'name': 'gorgonzola'}, {'id': 13799, 'synset': 'danish_blue.n.01', 'name': 'Danish_blue'}, {'id': 13800, 'synset': 'bavarian_blue.n.01', 'name': 'Bavarian_blue'}, {'id': 13801, 'synset': 'brie.n.01', 'name': 'Brie'}, {'id': 13802, 'synset': 'brick_cheese.n.01', 'name': 'brick_cheese'}, {'id': 13803, 'synset': 'camembert.n.01', 'name': 'Camembert'}, {'id': 13804, 'synset': 'cheddar.n.02', 'name': 'cheddar'}, {'id': 13805, 'synset': 'rat_cheese.n.01', 'name': 'rat_cheese'}, {'id': 13806, 'synset': 'cheshire_cheese.n.01', 'name': 'Cheshire_cheese'}, {'id': 13807, 'synset': 'double_gloucester.n.01', 'name': 'double_Gloucester'}, {'id': 13808, 'synset': 'edam.n.01', 'name': 'Edam'}, {'id': 13809, 'synset': 'goat_cheese.n.01', 'name': 'goat_cheese'}, {'id': 13810, 'synset': 'gouda.n.01', 'name': 'Gouda'}, {'id': 13811, 'synset': 'grated_cheese.n.01', 'name': 'grated_cheese'}, {'id': 13812, 'synset': 'hand_cheese.n.01', 'name': 'hand_cheese'}, {'id': 13813, 'synset': 'liederkranz.n.01', 'name': 'Liederkranz'}, {'id': 13814, 'synset': 'limburger.n.01', 'name': 'Limburger'}, {'id': 13815, 'synset': 'mozzarella.n.01', 'name': 'mozzarella'}, {'id': 13816, 'synset': 'muenster.n.01', 'name': 'Muenster'}, {'id': 13817, 'synset': 'parmesan.n.01', 'name': 'Parmesan'}, {'id': 13818, 'synset': 'quark_cheese.n.01', 'name': 'quark_cheese'}, {'id': 13819, 'synset': 'ricotta.n.01', 'name': 'ricotta'}, {'id': 13820, 'synset': 'swiss_cheese.n.01', 'name': 'Swiss_cheese'}, {'id': 13821, 'synset': 'emmenthal.n.01', 'name': 'Emmenthal'}, {'id': 13822, 'synset': 'gruyere.n.01', 'name': 'Gruyere'}, {'id': 13823, 'synset': 'sapsago.n.01', 'name': 'sapsago'}, {'id': 13824, 'synset': 'velveeta.n.01', 'name': 'Velveeta'}, {'id': 13825, 'synset': 'nut_butter.n.01', 'name': 'nut_butter'}, {'id': 13826, 'synset': 'marshmallow_fluff.n.01', 'name': 'marshmallow_fluff'}, {'id': 13827, 'synset': 'onion_butter.n.01', 'name': 'onion_butter'}, {'id': 13828, 'synset': 'pimento_butter.n.01', 'name': 'pimento_butter'}, {'id': 13829, 'synset': 'shrimp_butter.n.01', 'name': 'shrimp_butter'}, {'id': 13830, 'synset': 'lobster_butter.n.01', 'name': 'lobster_butter'}, {'id': 13831, 'synset': 'yak_butter.n.01', 'name': 'yak_butter'}, {'id': 13832, 'synset': 'spread.n.05', 'name': 'spread'}, {'id': 13833, 'synset': 'cheese_spread.n.01', 'name': 'cheese_spread'}, {'id': 13834, 'synset': 'anchovy_butter.n.01', 'name': 'anchovy_butter'}, {'id': 13835, 'synset': 'fishpaste.n.01', 'name': 'fishpaste'}, {'id': 13836, 'synset': 'garlic_butter.n.01', 'name': 'garlic_butter'}, {'id': 13837, 'synset': 'miso.n.01', 'name': 'miso'}, {'id': 13838, 'synset': 'wasabi.n.02', 'name': 'wasabi'}, {'id': 13839, 'synset': 'snail_butter.n.01', 'name': 'snail_butter'}, {'id': 13840, 'synset': 'pate.n.01', 'name': 'pate'}, {'id': 13841, 'synset': 'duck_pate.n.01', 'name': 'duck_pate'}, {'id': 13842, 'synset': 'foie_gras.n.01', 'name': 'foie_gras'}, {'id': 13843, 'synset': 'tapenade.n.01', 'name': 'tapenade'}, {'id': 13844, 'synset': 'tahini.n.01', 'name': 'tahini'}, {'id': 13845, 'synset': 'sweetening.n.01', 'name': 'sweetening'}, {'id': 13846, 'synset': 'aspartame.n.01', 'name': 'aspartame'}, {'id': 13847, 'synset': 'saccharin.n.01', 'name': 'saccharin'}, {'id': 13848, 'synset': 'sugar.n.01', 'name': 'sugar'}, {'id': 13849, 'synset': 'syrup.n.01', 'name': 'syrup'}, {'id': 13850, 'synset': 'sugar_syrup.n.01', 'name': 'sugar_syrup'}, {'id': 13851, 'synset': 'molasses.n.01', 'name': 'molasses'}, {'id': 13852, 'synset': 'sorghum.n.03', 'name': 'sorghum'}, {'id': 13853, 'synset': 'treacle.n.01', 'name': 'treacle'}, {'id': 13854, 'synset': 'grenadine.n.01', 'name': 'grenadine'}, {'id': 13855, 'synset': 'maple_syrup.n.01', 'name': 'maple_syrup'}, {'id': 13856, 'synset': 'corn_syrup.n.01', 'name': 'corn_syrup'}, {'id': 13857, 'synset': 'miraculous_food.n.01', 'name': 'miraculous_food'}, {'id': 13858, 'synset': 'dough.n.01', 'name': 'dough'}, {'id': 13859, 'synset': 'bread_dough.n.01', 'name': 'bread_dough'}, {'id': 13860, 'synset': 'pancake_batter.n.01', 'name': 'pancake_batter'}, {'id': 13861, 'synset': 'fritter_batter.n.01', 'name': 'fritter_batter'}, {'id': 13862, 'synset': 'coq_au_vin.n.01', 'name': 'coq_au_vin'}, {'id': 13863, 'synset': 'chicken_provencale.n.01', 'name': 'chicken_provencale'}, {'id': 13864, 'synset': 'chicken_and_rice.n.01', 'name': 'chicken_and_rice'}, {'id': 13865, 'synset': 'moo_goo_gai_pan.n.01', 'name': 'moo_goo_gai_pan'}, {'id': 13866, 'synset': 'arroz_con_pollo.n.01', 'name': 'arroz_con_pollo'}, {'id': 13867, 'synset': 'bacon_and_eggs.n.02', 'name': 'bacon_and_eggs'}, {'id': 13868, 'synset': 'barbecued_spareribs.n.01', 'name': 'barbecued_spareribs'}, {'id': 13869, 'synset': 'beef_bourguignonne.n.01', 'name': 'beef_Bourguignonne'}, {'id': 13870, 'synset': 'beef_wellington.n.01', 'name': 'beef_Wellington'}, {'id': 13871, 'synset': 'bitok.n.01', 'name': 'bitok'}, {'id': 13872, 'synset': 'boiled_dinner.n.01', 'name': 'boiled_dinner'}, {'id': 13873, 'synset': 'boston_baked_beans.n.01', 'name': 'Boston_baked_beans'}, {'id': 13874, 'synset': 'bubble_and_squeak.n.01', 'name': 'bubble_and_squeak'}, {'id': 13875, 'synset': 'pasta.n.01', 'name': 'pasta'}, {'id': 13876, 'synset': 'cannelloni.n.01', 'name': 'cannelloni'}, {'id': 13877, 'synset': 'carbonnade_flamande.n.01', 'name': 'carbonnade_flamande'}, {'id': 13878, 'synset': 'cheese_souffle.n.01', 'name': 'cheese_souffle'}, {'id': 13879, 'synset': 'chicken_marengo.n.01', 'name': 'chicken_Marengo'}, {'id': 13880, 'synset': 'chicken_cordon_bleu.n.01', 'name': 'chicken_cordon_bleu'}, {'id': 13881, 'synset': 'maryland_chicken.n.01', 'name': 'Maryland_chicken'}, {'id': 13882, 'synset': 'chicken_paprika.n.01', 'name': 'chicken_paprika'}, {'id': 13883, 'synset': 'chicken_tetrazzini.n.01', 'name': 'chicken_Tetrazzini'}, {'id': 13884, 'synset': 'tetrazzini.n.01', 'name': 'Tetrazzini'}, {'id': 13885, 'synset': 'chicken_kiev.n.01', 'name': 'chicken_Kiev'}, {'id': 13886, 'synset': 'chili.n.01', 'name': 'chili'}, {'id': 13887, 'synset': 'chili_dog.n.01', 'name': 'chili_dog'}, {'id': 13888, 'synset': 'chop_suey.n.01', 'name': 'chop_suey'}, {'id': 13889, 'synset': 'chow_mein.n.01', 'name': 'chow_mein'}, {'id': 13890, 'synset': 'codfish_ball.n.01', 'name': 'codfish_ball'}, {'id': 13891, 'synset': 'coquille.n.01', 'name': 'coquille'}, {'id': 13892, 'synset': 'coquilles_saint-jacques.n.01', 'name': 'coquilles_Saint-Jacques'}, {'id': 13893, 'synset': 'croquette.n.01', 'name': 'croquette'}, {'id': 13894, 'synset': 'cottage_pie.n.01', 'name': 'cottage_pie'}, {'id': 13895, 'synset': 'rissole.n.01', 'name': 'rissole'}, {'id': 13896, 'synset': 'dolmas.n.01', 'name': 'dolmas'}, {'id': 13897, 'synset': 'egg_foo_yong.n.01', 'name': 'egg_foo_yong'}, {'id': 13898, 'synset': 'eggs_benedict.n.01', 'name': 'eggs_Benedict'}, {'id': 13899, 'synset': 'enchilada.n.01', 'name': 'enchilada'}, {'id': 13900, 'synset': 'falafel.n.01', 'name': 'falafel'}, {'id': 13901, 'synset': 'fish_and_chips.n.01', 'name': 'fish_and_chips'}, {'id': 13902, 'synset': 'fondue.n.02', 'name': 'fondue'}, {'id': 13903, 'synset': 'cheese_fondue.n.01', 'name': 'cheese_fondue'}, {'id': 13904, 'synset': 'chocolate_fondue.n.01', 'name': 'chocolate_fondue'}, {'id': 13905, 'synset': 'fondue.n.01', 'name': 'fondue'}, {'id': 13906, 'synset': 'beef_fondue.n.01', 'name': 'beef_fondue'}, {'id': 13907, 'synset': 'fried_rice.n.01', 'name': 'fried_rice'}, {'id': 13908, 'synset': 'frittata.n.01', 'name': 'frittata'}, {'id': 13909, 'synset': 'frog_legs.n.01', 'name': 'frog_legs'}, {'id': 13910, 'synset': 'galantine.n.01', 'name': 'galantine'}, {'id': 13911, 'synset': 'gefilte_fish.n.01', 'name': 'gefilte_fish'}, {'id': 13912, 'synset': 'haggis.n.01', 'name': 'haggis'}, {'id': 13913, 'synset': 'ham_and_eggs.n.01', 'name': 'ham_and_eggs'}, {'id': 13914, 'synset': 'hash.n.01', 'name': 'hash'}, {'id': 13915, 'synset': 'corned_beef_hash.n.01', 'name': 'corned_beef_hash'}, {'id': 13916, 'synset': 'jambalaya.n.01', 'name': 'jambalaya'}, {'id': 13917, 'synset': 'kabob.n.01', 'name': 'kabob'}, {'id': 13918, 'synset': 'kedgeree.n.01', 'name': 'kedgeree'}, {'id': 13919, 'synset': 'souvlaki.n.01', 'name': 'souvlaki'}, {'id': 13920, 'synset': 'seafood_newburg.n.01', 'name': 'seafood_Newburg'}, {'id': 13921, 'synset': 'lobster_newburg.n.01', 'name': 'lobster_Newburg'}, {'id': 13922, 'synset': 'shrimp_newburg.n.01', 'name': 'shrimp_Newburg'}, {'id': 13923, 'synset': 'newburg_sauce.n.01', 'name': 'Newburg_sauce'}, {'id': 13924, 'synset': 'lobster_thermidor.n.01', 'name': 'lobster_thermidor'}, {'id': 13925, 'synset': 'lutefisk.n.01', 'name': 'lutefisk'}, {'id': 13926, 'synset': 'macaroni_and_cheese.n.01', 'name': 'macaroni_and_cheese'}, {'id': 13927, 'synset': 'macedoine.n.01', 'name': 'macedoine'}, {'id': 13928, 'synset': 'porcupine_ball.n.01', 'name': 'porcupine_ball'}, {'id': 13929, 'synset': 'swedish_meatball.n.01', 'name': 'Swedish_meatball'}, {'id': 13930, 'synset': 'meat_loaf.n.01', 'name': 'meat_loaf'}, {'id': 13931, 'synset': 'moussaka.n.01', 'name': 'moussaka'}, {'id': 13932, 'synset': 'osso_buco.n.01', 'name': 'osso_buco'}, {'id': 13933, 'synset': 'marrow.n.03', 'name': 'marrow'}, {'id': 13934, 'synset': 'pheasant_under_glass.n.01', 'name': 'pheasant_under_glass'}, {'id': 13935, 'synset': 'pigs_in_blankets.n.01', 'name': 'pigs_in_blankets'}, {'id': 13936, 'synset': 'pilaf.n.01', 'name': 'pilaf'}, {'id': 13937, 'synset': 'bulgur_pilaf.n.01', 'name': 'bulgur_pilaf'}, {'id': 13938, 'synset': 'sausage_pizza.n.01', 'name': 'sausage_pizza'}, {'id': 13939, 'synset': 'pepperoni_pizza.n.01', 'name': 'pepperoni_pizza'}, {'id': 13940, 'synset': 'cheese_pizza.n.01', 'name': 'cheese_pizza'}, {'id': 13941, 'synset': 'anchovy_pizza.n.01', 'name': 'anchovy_pizza'}, {'id': 13942, 'synset': 'sicilian_pizza.n.01', 'name': 'Sicilian_pizza'}, {'id': 13943, 'synset': 'poi.n.01', 'name': 'poi'}, {'id': 13944, 'synset': 'pork_and_beans.n.01', 'name': 'pork_and_beans'}, {'id': 13945, 'synset': 'porridge.n.01', 'name': 'porridge'}, {'id': 13946, 'synset': 'oatmeal.n.01', 'name': 'oatmeal'}, {'id': 13947, 'synset': 'loblolly.n.01', 'name': 'loblolly'}, {'id': 13948, 'synset': 'potpie.n.01', 'name': 'potpie'}, {'id': 13949, 'synset': 'rijsttaffel.n.01', 'name': 'rijsttaffel'}, {'id': 13950, 'synset': 'risotto.n.01', 'name': 'risotto'}, {'id': 13951, 'synset': 'roulade.n.01', 'name': 'roulade'}, {'id': 13952, 'synset': 'fish_loaf.n.01', 'name': 'fish_loaf'}, {'id': 13953, 'synset': 'salmon_loaf.n.01', 'name': 'salmon_loaf'}, {'id': 13954, 'synset': 'salisbury_steak.n.01', 'name': 'Salisbury_steak'}, {'id': 13955, 'synset': 'sauerbraten.n.01', 'name': 'sauerbraten'}, {'id': 13956, 'synset': 'sauerkraut.n.01', 'name': 'sauerkraut'}, {'id': 13957, 'synset': 'scallopine.n.01', 'name': 'scallopine'}, {'id': 13958, 'synset': 'veal_scallopini.n.01', 'name': 'veal_scallopini'}, {'id': 13959, 'synset': 'scampi.n.01', 'name': 'scampi'}, {'id': 13960, 'synset': 'scotch_egg.n.01', 'name': 'Scotch_egg'}, {'id': 13961, 'synset': 'scotch_woodcock.n.01', 'name': 'Scotch_woodcock'}, {'id': 13962, 'synset': 'scrapple.n.01', 'name': 'scrapple'}, {'id': 13963, 'synset': 'spaghetti_and_meatballs.n.01', 'name': 'spaghetti_and_meatballs'}, {'id': 13964, 'synset': 'spanish_rice.n.01', 'name': 'Spanish_rice'}, {'id': 13965, 'synset': 'steak_tartare.n.01', 'name': 'steak_tartare'}, {'id': 13966, 'synset': 'pepper_steak.n.02', 'name': 'pepper_steak'}, {'id': 13967, 'synset': 'steak_au_poivre.n.01', 'name': 'steak_au_poivre'}, {'id': 13968, 'synset': 'beef_stroganoff.n.01', 'name': 'beef_Stroganoff'}, {'id': 13969, 'synset': 'stuffed_cabbage.n.01', 'name': 'stuffed_cabbage'}, {'id': 13970, 'synset': 'kishke.n.01', 'name': 'kishke'}, {'id': 13971, 'synset': 'stuffed_peppers.n.01', 'name': 'stuffed_peppers'}, {'id': 13972, 'synset': 'stuffed_tomato.n.02', 'name': 'stuffed_tomato'}, {'id': 13973, 'synset': 'stuffed_tomato.n.01', 'name': 'stuffed_tomato'}, {'id': 13974, 'synset': 'succotash.n.01', 'name': 'succotash'}, {'id': 13975, 'synset': 'sukiyaki.n.01', 'name': 'sukiyaki'}, {'id': 13976, 'synset': 'sashimi.n.01', 'name': 'sashimi'}, {'id': 13977, 'synset': 'swiss_steak.n.01', 'name': 'Swiss_steak'}, {'id': 13978, 'synset': 'tamale.n.02', 'name': 'tamale'}, {'id': 13979, 'synset': 'tamale_pie.n.01', 'name': 'tamale_pie'}, {'id': 13980, 'synset': 'tempura.n.01', 'name': 'tempura'}, {'id': 13981, 'synset': 'teriyaki.n.01', 'name': 'teriyaki'}, {'id': 13982, 'synset': 'terrine.n.01', 'name': 'terrine'}, {'id': 13983, 'synset': 'welsh_rarebit.n.01', 'name': 'Welsh_rarebit'}, {'id': 13984, 'synset': 'schnitzel.n.01', 'name': 'schnitzel'}, {'id': 13985, 'synset': 'chicken_taco.n.01', 'name': 'chicken_taco'}, {'id': 13986, 'synset': 'beef_burrito.n.01', 'name': 'beef_burrito'}, {'id': 13987, 'synset': 'tostada.n.01', 'name': 'tostada'}, {'id': 13988, 'synset': 'bean_tostada.n.01', 'name': 'bean_tostada'}, {'id': 13989, 'synset': 'refried_beans.n.01', 'name': 'refried_beans'}, {'id': 13990, 'synset': 'beverage.n.01', 'name': 'beverage'}, {'id': 13991, 'synset': 'wish-wash.n.01', 'name': 'wish-wash'}, {'id': 13992, 'synset': 'concoction.n.01', 'name': 'concoction'}, {'id': 13993, 'synset': 'mix.n.01', 'name': 'mix'}, {'id': 13994, 'synset': 'filling.n.03', 'name': 'filling'}, {'id': 13995, 'synset': 'lekvar.n.01', 'name': 'lekvar'}, {'id': 13996, 'synset': 'potion.n.01', 'name': 'potion'}, {'id': 13997, 'synset': 'elixir.n.03', 'name': 'elixir'}, {'id': 13998, 'synset': 'elixir_of_life.n.01', 'name': 'elixir_of_life'}, {'id': 13999, 'synset': 'philter.n.01', 'name': 'philter'}, {'id': 14000, 'synset': 'proof_spirit.n.01', 'name': 'proof_spirit'}, {'id': 14001, 'synset': 'home_brew.n.01', 'name': 'home_brew'}, {'id': 14002, 'synset': 'hooch.n.01', 'name': 'hooch'}, {'id': 14003, 'synset': 'kava.n.01', 'name': 'kava'}, {'id': 14004, 'synset': 'aperitif.n.01', 'name': 'aperitif'}, {'id': 14005, 'synset': 'brew.n.01', 'name': 'brew'}, {'id': 14006, 'synset': 'beer.n.01', 'name': 'beer'}, {'id': 14007, 'synset': 'draft_beer.n.01', 'name': 'draft_beer'}, {'id': 14008, 'synset': 'suds.n.02', 'name': 'suds'}, {'id': 14009, 'synset': 'munich_beer.n.01', 'name': 'Munich_beer'}, {'id': 14010, 'synset': 'bock.n.01', 'name': 'bock'}, {'id': 14011, 'synset': 'lager.n.02', 'name': 'lager'}, {'id': 14012, 'synset': 'light_beer.n.01', 'name': 'light_beer'}, {'id': 14013, 'synset': 'oktoberfest.n.01', 'name': 'Oktoberfest'}, {'id': 14014, 'synset': 'pilsner.n.01', 'name': 'Pilsner'}, {'id': 14015, 'synset': 'shebeen.n.01', 'name': 'shebeen'}, {'id': 14016, 'synset': 'weissbier.n.01', 'name': 'Weissbier'}, {'id': 14017, 'synset': 'weizenbock.n.01', 'name': 'Weizenbock'}, {'id': 14018, 'synset': 'malt.n.03', 'name': 'malt'}, {'id': 14019, 'synset': 'wort.n.02', 'name': 'wort'}, {'id': 14020, 'synset': 'malt.n.02', 'name': 'malt'}, {'id': 14021, 'synset': 'ale.n.01', 'name': 'ale'}, {'id': 14022, 'synset': 'bitter.n.01', 'name': 'bitter'}, {'id': 14023, 'synset': 'burton.n.03', 'name': 'Burton'}, {'id': 14024, 'synset': 'pale_ale.n.01', 'name': 'pale_ale'}, {'id': 14025, 'synset': 'porter.n.07', 'name': 'porter'}, {'id': 14026, 'synset': 'stout.n.01', 'name': 'stout'}, {'id': 14027, 'synset': 'guinness.n.02', 'name': 'Guinness'}, {'id': 14028, 'synset': 'kvass.n.01', 'name': 'kvass'}, {'id': 14029, 'synset': 'mead.n.03', 'name': 'mead'}, {'id': 14030, 'synset': 'metheglin.n.01', 'name': 'metheglin'}, {'id': 14031, 'synset': 'hydromel.n.01', 'name': 'hydromel'}, {'id': 14032, 'synset': 'oenomel.n.01', 'name': 'oenomel'}, {'id': 14033, 'synset': 'near_beer.n.01', 'name': 'near_beer'}, {'id': 14034, 'synset': 'ginger_beer.n.01', 'name': 'ginger_beer'}, {'id': 14035, 'synset': 'sake.n.02', 'name': 'sake'}, {'id': 14036, 'synset': 'wine.n.01', 'name': 'wine'}, {'id': 14037, 'synset': 'vintage.n.01', 'name': 'vintage'}, {'id': 14038, 'synset': 'red_wine.n.01', 'name': 'red_wine'}, {'id': 14039, 'synset': 'white_wine.n.01', 'name': 'white_wine'}, {'id': 14040, 'synset': 'blush_wine.n.01', 'name': 'blush_wine'}, {'id': 14041, 'synset': 'altar_wine.n.01', 'name': 'altar_wine'}, {'id': 14042, 'synset': 'sparkling_wine.n.01', 'name': 'sparkling_wine'}, {'id': 14043, 'synset': 'champagne.n.01', 'name': 'champagne'}, {'id': 14044, 'synset': 'cold_duck.n.01', 'name': 'cold_duck'}, {'id': 14045, 'synset': 'burgundy.n.02', 'name': 'Burgundy'}, {'id': 14046, 'synset': 'beaujolais.n.01', 'name': 'Beaujolais'}, {'id': 14047, 'synset': 'medoc.n.01', 'name': 'Medoc'}, {'id': 14048, 'synset': 'canary_wine.n.01', 'name': 'Canary_wine'}, {'id': 14049, 'synset': 'chablis.n.02', 'name': 'Chablis'}, {'id': 14050, 'synset': 'montrachet.n.01', 'name': 'Montrachet'}, {'id': 14051, 'synset': 'chardonnay.n.02', 'name': 'Chardonnay'}, {'id': 14052, 'synset': 'pinot_noir.n.02', 'name': 'Pinot_noir'}, {'id': 14053, 'synset': 'pinot_blanc.n.02', 'name': 'Pinot_blanc'}, {'id': 14054, 'synset': 'bordeaux.n.02', 'name': 'Bordeaux'}, {'id': 14055, 'synset': 'claret.n.02', 'name': 'claret'}, {'id': 14056, 'synset': 'chianti.n.01', 'name': 'Chianti'}, {'id': 14057, 'synset': 'cabernet.n.01', 'name': 'Cabernet'}, {'id': 14058, 'synset': 'merlot.n.02', 'name': 'Merlot'}, {'id': 14059, 'synset': 'sauvignon_blanc.n.02', 'name': 'Sauvignon_blanc'}, {'id': 14060, 'synset': 'california_wine.n.01', 'name': 'California_wine'}, {'id': 14061, 'synset': 'cotes_de_provence.n.01', 'name': 'Cotes_de_Provence'}, {'id': 14062, 'synset': 'dessert_wine.n.01', 'name': 'dessert_wine'}, {'id': 14063, 'synset': 'dubonnet.n.01', 'name': 'Dubonnet'}, {'id': 14064, 'synset': 'jug_wine.n.01', 'name': 'jug_wine'}, {'id': 14065, 'synset': 'macon.n.02', 'name': 'macon'}, {'id': 14066, 'synset': 'moselle.n.01', 'name': 'Moselle'}, {'id': 14067, 'synset': 'muscadet.n.02', 'name': 'Muscadet'}, {'id': 14068, 'synset': 'plonk.n.01', 'name': 'plonk'}, {'id': 14069, 'synset': 'retsina.n.01', 'name': 'retsina'}, {'id': 14070, 'synset': 'rhine_wine.n.01', 'name': 'Rhine_wine'}, {'id': 14071, 'synset': 'riesling.n.02', 'name': 'Riesling'}, {'id': 14072, 'synset': 'liebfraumilch.n.01', 'name': 'liebfraumilch'}, {'id': 14073, 'synset': 'rhone_wine.n.01', 'name': 'Rhone_wine'}, {'id': 14074, 'synset': 'rioja.n.01', 'name': 'Rioja'}, {'id': 14075, 'synset': 'sack.n.04', 'name': 'sack'}, {'id': 14076, 'synset': 'saint_emilion.n.01', 'name': 'Saint_Emilion'}, {'id': 14077, 'synset': 'soave.n.01', 'name': 'Soave'}, {'id': 14078, 'synset': 'zinfandel.n.02', 'name': 'zinfandel'}, {'id': 14079, 'synset': 'sauterne.n.01', 'name': 'Sauterne'}, {'id': 14080, 'synset': 'straw_wine.n.01', 'name': 'straw_wine'}, {'id': 14081, 'synset': 'table_wine.n.01', 'name': 'table_wine'}, {'id': 14082, 'synset': 'tokay.n.01', 'name': 'Tokay'}, {'id': 14083, 'synset': 'vin_ordinaire.n.01', 'name': 'vin_ordinaire'}, {'id': 14084, 'synset': 'vermouth.n.01', 'name': 'vermouth'}, {'id': 14085, 'synset': 'sweet_vermouth.n.01', 'name': 'sweet_vermouth'}, {'id': 14086, 'synset': 'dry_vermouth.n.01', 'name': 'dry_vermouth'}, {'id': 14087, 'synset': 'chenin_blanc.n.02', 'name': 'Chenin_blanc'}, {'id': 14088, 'synset': 'verdicchio.n.02', 'name': 'Verdicchio'}, {'id': 14089, 'synset': 'vouvray.n.01', 'name': 'Vouvray'}, {'id': 14090, 'synset': 'yquem.n.01', 'name': 'Yquem'}, {'id': 14091, 'synset': 'generic.n.01', 'name': 'generic'}, {'id': 14092, 'synset': 'varietal.n.01', 'name': 'varietal'}, {'id': 14093, 'synset': 'fortified_wine.n.01', 'name': 'fortified_wine'}, {'id': 14094, 'synset': 'madeira.n.03', 'name': 'Madeira'}, {'id': 14095, 'synset': 'malmsey.n.01', 'name': 'malmsey'}, {'id': 14096, 'synset': 'port.n.02', 'name': 'port'}, {'id': 14097, 'synset': 'sherry.n.01', 'name': 'sherry'}, {'id': 14098, 'synset': 'marsala.n.01', 'name': 'Marsala'}, {'id': 14099, 'synset': 'muscat.n.03', 'name': 'muscat'}, {'id': 14100, 'synset': 'neutral_spirits.n.01', 'name': 'neutral_spirits'}, {'id': 14101, 'synset': 'aqua_vitae.n.01', 'name': 'aqua_vitae'}, {'id': 14102, 'synset': 'eau_de_vie.n.01', 'name': 'eau_de_vie'}, {'id': 14103, 'synset': 'moonshine.n.02', 'name': 'moonshine'}, {'id': 14104, 'synset': 'bathtub_gin.n.01', 'name': 'bathtub_gin'}, {'id': 14105, 'synset': 'aquavit.n.01', 'name': 'aquavit'}, {'id': 14106, 'synset': 'arrack.n.01', 'name': 'arrack'}, {'id': 14107, 'synset': 'bitters.n.01', 'name': 'bitters'}, {'id': 14108, 'synset': 'brandy.n.01', 'name': 'brandy'}, {'id': 14109, 'synset': 'applejack.n.01', 'name': 'applejack'}, {'id': 14110, 'synset': 'calvados.n.01', 'name': 'Calvados'}, {'id': 14111, 'synset': 'armagnac.n.01', 'name': 'Armagnac'}, {'id': 14112, 'synset': 'cognac.n.01', 'name': 'Cognac'}, {'id': 14113, 'synset': 'grappa.n.01', 'name': 'grappa'}, {'id': 14114, 'synset': 'kirsch.n.01', 'name': 'kirsch'}, {'id': 14115, 'synset': 'slivovitz.n.01', 'name': 'slivovitz'}, {'id': 14116, 'synset': 'gin.n.01', 'name': 'gin'}, {'id': 14117, 'synset': 'sloe_gin.n.01', 'name': 'sloe_gin'}, {'id': 14118, 'synset': 'geneva.n.02', 'name': 'geneva'}, {'id': 14119, 'synset': 'grog.n.01', 'name': 'grog'}, {'id': 14120, 'synset': 'ouzo.n.01', 'name': 'ouzo'}, {'id': 14121, 'synset': 'rum.n.01', 'name': 'rum'}, {'id': 14122, 'synset': 'demerara.n.04', 'name': 'demerara'}, {'id': 14123, 'synset': 'jamaica_rum.n.01', 'name': 'Jamaica_rum'}, {'id': 14124, 'synset': 'schnapps.n.01', 'name': 'schnapps'}, {'id': 14125, 'synset': 'pulque.n.01', 'name': 'pulque'}, {'id': 14126, 'synset': 'mescal.n.02', 'name': 'mescal'}, {'id': 14127, 'synset': 'whiskey.n.01', 'name': 'whiskey'}, {'id': 14128, 'synset': 'blended_whiskey.n.01', 'name': 'blended_whiskey'}, {'id': 14129, 'synset': 'bourbon.n.02', 'name': 'bourbon'}, {'id': 14130, 'synset': 'corn_whiskey.n.01', 'name': 'corn_whiskey'}, {'id': 14131, 'synset': 'firewater.n.01', 'name': 'firewater'}, {'id': 14132, 'synset': 'irish.n.02', 'name': 'Irish'}, {'id': 14133, 'synset': 'poteen.n.01', 'name': 'poteen'}, {'id': 14134, 'synset': 'rye.n.03', 'name': 'rye'}, {'id': 14135, 'synset': 'scotch.n.02', 'name': 'Scotch'}, {'id': 14136, 'synset': 'sour_mash.n.02', 'name': 'sour_mash'}, {'id': 14137, 'synset': 'liqueur.n.01', 'name': 'liqueur'}, {'id': 14138, 'synset': 'absinth.n.01', 'name': 'absinth'}, {'id': 14139, 'synset': 'amaretto.n.01', 'name': 'amaretto'}, {'id': 14140, 'synset': 'anisette.n.01', 'name': 'anisette'}, {'id': 14141, 'synset': 'benedictine.n.02', 'name': 'benedictine'}, {'id': 14142, 'synset': 'chartreuse.n.01', 'name': 'Chartreuse'}, {'id': 14143, 'synset': 'coffee_liqueur.n.01', 'name': 'coffee_liqueur'}, {'id': 14144, 'synset': 'creme_de_cacao.n.01', 'name': 'creme_de_cacao'}, {'id': 14145, 'synset': 'creme_de_menthe.n.01', 'name': 'creme_de_menthe'}, {'id': 14146, 'synset': 'creme_de_fraise.n.01', 'name': 'creme_de_fraise'}, {'id': 14147, 'synset': 'drambuie.n.01', 'name': 'Drambuie'}, {'id': 14148, 'synset': 'galliano.n.01', 'name': 'Galliano'}, {'id': 14149, 'synset': 'orange_liqueur.n.01', 'name': 'orange_liqueur'}, {'id': 14150, 'synset': 'curacao.n.02', 'name': 'curacao'}, {'id': 14151, 'synset': 'triple_sec.n.01', 'name': 'triple_sec'}, {'id': 14152, 'synset': 'grand_marnier.n.01', 'name': 'Grand_Marnier'}, {'id': 14153, 'synset': 'kummel.n.01', 'name': 'kummel'}, {'id': 14154, 'synset': 'maraschino.n.01', 'name': 'maraschino'}, {'id': 14155, 'synset': 'pastis.n.01', 'name': 'pastis'}, {'id': 14156, 'synset': 'pernod.n.01', 'name': 'Pernod'}, {'id': 14157, 'synset': 'pousse-cafe.n.01', 'name': 'pousse-cafe'}, {'id': 14158, 'synset': 'kahlua.n.01', 'name': 'Kahlua'}, {'id': 14159, 'synset': 'ratafia.n.01', 'name': 'ratafia'}, {'id': 14160, 'synset': 'sambuca.n.01', 'name': 'sambuca'}, {'id': 14161, 'synset': 'mixed_drink.n.01', 'name': 'mixed_drink'}, {'id': 14162, 'synset': 'cocktail.n.01', 'name': 'cocktail'}, {'id': 14163, 'synset': 'dom_pedro.n.01', 'name': 'Dom_Pedro'}, {'id': 14164, 'synset': 'highball.n.01', 'name': 'highball'}, {'id': 14165, 'synset': 'mixer.n.02', 'name': 'mixer'}, {'id': 14166, 'synset': 'bishop.n.02', 'name': 'bishop'}, {'id': 14167, 'synset': 'bloody_mary.n.02', 'name': 'Bloody_Mary'}, {'id': 14168, 'synset': 'virgin_mary.n.02', 'name': 'Virgin_Mary'}, {'id': 14169, 'synset': 'bullshot.n.01', 'name': 'bullshot'}, {'id': 14170, 'synset': 'cobbler.n.02', 'name': 'cobbler'}, {'id': 14171, 'synset': 'collins.n.02', 'name': 'collins'}, {'id': 14172, 'synset': 'cooler.n.02', 'name': 'cooler'}, {'id': 14173, 'synset': 'refresher.n.02', 'name': 'refresher'}, {'id': 14174, 'synset': 'daiquiri.n.01', 'name': 'daiquiri'}, {'id': 14175, 'synset': 'strawberry_daiquiri.n.01', 'name': 'strawberry_daiquiri'}, {'id': 14176, 'synset': 'nada_daiquiri.n.01', 'name': 'NADA_daiquiri'}, {'id': 14177, 'synset': 'spritzer.n.01', 'name': 'spritzer'}, {'id': 14178, 'synset': 'flip.n.02', 'name': 'flip'}, {'id': 14179, 'synset': 'gimlet.n.01', 'name': 'gimlet'}, {'id': 14180, 'synset': 'gin_and_tonic.n.01', 'name': 'gin_and_tonic'}, {'id': 14181, 'synset': 'grasshopper.n.02', 'name': 'grasshopper'}, {'id': 14182, 'synset': 'harvey_wallbanger.n.01', 'name': 'Harvey_Wallbanger'}, {'id': 14183, 'synset': 'julep.n.01', 'name': 'julep'}, {'id': 14184, 'synset': 'manhattan.n.02', 'name': 'manhattan'}, {'id': 14185, 'synset': 'rob_roy.n.02', 'name': 'Rob_Roy'}, {'id': 14186, 'synset': 'margarita.n.01', 'name': 'margarita'}, {'id': 14187, 'synset': 'gin_and_it.n.01', 'name': 'gin_and_it'}, {'id': 14188, 'synset': 'vodka_martini.n.01', 'name': 'vodka_martini'}, {'id': 14189, 'synset': 'old_fashioned.n.01', 'name': 'old_fashioned'}, {'id': 14190, 'synset': 'pink_lady.n.01', 'name': 'pink_lady'}, {'id': 14191, 'synset': 'sazerac.n.01', 'name': 'Sazerac'}, {'id': 14192, 'synset': 'screwdriver.n.02', 'name': 'screwdriver'}, {'id': 14193, 'synset': 'sidecar.n.01', 'name': 'sidecar'}, {'id': 14194, 'synset': 'scotch_and_soda.n.01', 'name': 'Scotch_and_soda'}, {'id': 14195, 'synset': 'sling.n.01', 'name': 'sling'}, {'id': 14196, 'synset': 'brandy_sling.n.01', 'name': 'brandy_sling'}, {'id': 14197, 'synset': 'gin_sling.n.01', 'name': 'gin_sling'}, {'id': 14198, 'synset': 'rum_sling.n.01', 'name': 'rum_sling'}, {'id': 14199, 'synset': 'sour.n.01', 'name': 'sour'}, {'id': 14200, 'synset': 'whiskey_sour.n.01', 'name': 'whiskey_sour'}, {'id': 14201, 'synset': 'stinger.n.01', 'name': 'stinger'}, {'id': 14202, 'synset': 'swizzle.n.01', 'name': 'swizzle'}, {'id': 14203, 'synset': 'hot_toddy.n.01', 'name': 'hot_toddy'}, {'id': 14204, 'synset': 'zombie.n.05', 'name': 'zombie'}, {'id': 14205, 'synset': 'fizz.n.01', 'name': 'fizz'}, {'id': 14206, 'synset': 'irish_coffee.n.01', 'name': 'Irish_coffee'}, {'id': 14207, 'synset': 'cafe_au_lait.n.01', 'name': 'cafe_au_lait'}, {'id': 14208, 'synset': 'cafe_noir.n.01', 'name': 'cafe_noir'}, {'id': 14209, 'synset': 'decaffeinated_coffee.n.01', 'name': 'decaffeinated_coffee'}, {'id': 14210, 'synset': 'drip_coffee.n.01', 'name': 'drip_coffee'}, {'id': 14211, 'synset': 'espresso.n.01', 'name': 'espresso'}, {'id': 14212, 'synset': 'caffe_latte.n.01', 'name': 'caffe_latte'}, {'id': 14213, 'synset': 'iced_coffee.n.01', 'name': 'iced_coffee'}, {'id': 14214, 'synset': 'instant_coffee.n.01', 'name': 'instant_coffee'}, {'id': 14215, 'synset': 'mocha.n.03', 'name': 'mocha'}, {'id': 14216, 'synset': 'mocha.n.02', 'name': 'mocha'}, {'id': 14217, 'synset': 'cassareep.n.01', 'name': 'cassareep'}, {'id': 14218, 'synset': 'turkish_coffee.n.01', 'name': 'Turkish_coffee'}, {'id': 14219, 'synset': 'hard_cider.n.01', 'name': 'hard_cider'}, {'id': 14220, 'synset': 'scrumpy.n.01', 'name': 'scrumpy'}, {'id': 14221, 'synset': 'sweet_cider.n.01', 'name': 'sweet_cider'}, {'id': 14222, 'synset': 'mulled_cider.n.01', 'name': 'mulled_cider'}, {'id': 14223, 'synset': 'perry.n.04', 'name': 'perry'}, {'id': 14224, 'synset': 'rotgut.n.01', 'name': 'rotgut'}, {'id': 14225, 'synset': 'slug.n.05', 'name': 'slug'}, {'id': 14226, 'synset': 'criollo.n.02', 'name': 'criollo'}, {'id': 14227, 'synset': 'juice.n.01', 'name': 'juice'}, {'id': 14228, 'synset': 'nectar.n.02', 'name': 'nectar'}, {'id': 14229, 'synset': 'apple_juice.n.01', 'name': 'apple_juice'}, {'id': 14230, 'synset': 'cranberry_juice.n.01', 'name': 'cranberry_juice'}, {'id': 14231, 'synset': 'grape_juice.n.01', 'name': 'grape_juice'}, {'id': 14232, 'synset': 'must.n.02', 'name': 'must'}, {'id': 14233, 'synset': 'grapefruit_juice.n.01', 'name': 'grapefruit_juice'}, {'id': 14234, 'synset': 'frozen_orange_juice.n.01', 'name': 'frozen_orange_juice'}, {'id': 14235, 'synset': 'pineapple_juice.n.01', 'name': 'pineapple_juice'}, {'id': 14236, 'synset': 'lemon_juice.n.01', 'name': 'lemon_juice'}, {'id': 14237, 'synset': 'lime_juice.n.01', 'name': 'lime_juice'}, {'id': 14238, 'synset': 'papaya_juice.n.01', 'name': 'papaya_juice'}, {'id': 14239, 'synset': 'tomato_juice.n.01', 'name': 'tomato_juice'}, {'id': 14240, 'synset': 'carrot_juice.n.01', 'name': 'carrot_juice'}, {'id': 14241, 'synset': 'v-8_juice.n.01', 'name': 'V-8_juice'}, {'id': 14242, 'synset': 'koumiss.n.01', 'name': 'koumiss'}, {'id': 14243, 'synset': 'fruit_drink.n.01', 'name': 'fruit_drink'}, {'id': 14244, 'synset': 'limeade.n.01', 'name': 'limeade'}, {'id': 14245, 'synset': 'orangeade.n.01', 'name': 'orangeade'}, {'id': 14246, 'synset': 'malted_milk.n.02', 'name': 'malted_milk'}, {'id': 14247, 'synset': 'mate.n.09', 'name': 'mate'}, {'id': 14248, 'synset': 'mulled_wine.n.01', 'name': 'mulled_wine'}, {'id': 14249, 'synset': 'negus.n.01', 'name': 'negus'}, {'id': 14250, 'synset': 'soft_drink.n.01', 'name': 'soft_drink'}, {'id': 14251, 'synset': 'birch_beer.n.01', 'name': 'birch_beer'}, {'id': 14252, 'synset': 'bitter_lemon.n.01', 'name': 'bitter_lemon'}, {'id': 14253, 'synset': 'cola.n.02', 'name': 'cola'}, {'id': 14254, 'synset': 'cream_soda.n.01', 'name': 'cream_soda'}, {'id': 14255, 'synset': 'egg_cream.n.01', 'name': 'egg_cream'}, {'id': 14256, 'synset': 'ginger_ale.n.01', 'name': 'ginger_ale'}, {'id': 14257, 'synset': 'orange_soda.n.01', 'name': 'orange_soda'}, {'id': 14258, 'synset': 'phosphate.n.02', 'name': 'phosphate'}, {'id': 14259, 'synset': 'coca_cola.n.01', 'name': 'Coca_Cola'}, {'id': 14260, 'synset': 'pepsi.n.01', 'name': 'Pepsi'}, {'id': 14261, 'synset': 'sarsaparilla.n.02', 'name': 'sarsaparilla'}, {'id': 14262, 'synset': 'tonic.n.01', 'name': 'tonic'}, {'id': 14263, 'synset': 'coffee_bean.n.01', 'name': 'coffee_bean'}, {'id': 14264, 'synset': 'coffee.n.01', 'name': 'coffee'}, {'id': 14265, 'synset': 'cafe_royale.n.01', 'name': 'cafe_royale'}, {'id': 14266, 'synset': 'fruit_punch.n.01', 'name': 'fruit_punch'}, {'id': 14267, 'synset': 'milk_punch.n.01', 'name': 'milk_punch'}, {'id': 14268, 'synset': 'mimosa.n.03', 'name': 'mimosa'}, {'id': 14269, 'synset': 'pina_colada.n.01', 'name': 'pina_colada'}, {'id': 14270, 'synset': 'punch.n.02', 'name': 'punch'}, {'id': 14271, 'synset': 'cup.n.06', 'name': 'cup'}, {'id': 14272, 'synset': 'champagne_cup.n.01', 'name': 'champagne_cup'}, {'id': 14273, 'synset': 'claret_cup.n.01', 'name': 'claret_cup'}, {'id': 14274, 'synset': 'wassail.n.01', 'name': 'wassail'}, {'id': 14275, 'synset': "planter's_punch.n.01", 'name': "planter's_punch"}, {'id': 14276, 'synset': 'white_russian.n.02', 'name': 'White_Russian'}, {'id': 14277, 'synset': 'fish_house_punch.n.01', 'name': 'fish_house_punch'}, {'id': 14278, 'synset': 'may_wine.n.01', 'name': 'May_wine'}, {'id': 14279, 'synset': 'eggnog.n.01', 'name': 'eggnog'}, {'id': 14280, 'synset': 'cassiri.n.01', 'name': 'cassiri'}, {'id': 14281, 'synset': 'spruce_beer.n.01', 'name': 'spruce_beer'}, {'id': 14282, 'synset': 'rickey.n.01', 'name': 'rickey'}, {'id': 14283, 'synset': 'gin_rickey.n.01', 'name': 'gin_rickey'}, {'id': 14284, 'synset': 'tea.n.05', 'name': 'tea'}, {'id': 14285, 'synset': 'tea.n.01', 'name': 'tea'}, {'id': 14286, 'synset': 'tea-like_drink.n.01', 'name': 'tea-like_drink'}, {'id': 14287, 'synset': 'cambric_tea.n.01', 'name': 'cambric_tea'}, {'id': 14288, 'synset': 'cuppa.n.01', 'name': 'cuppa'}, {'id': 14289, 'synset': 'herb_tea.n.01', 'name': 'herb_tea'}, {'id': 14290, 'synset': 'tisane.n.01', 'name': 'tisane'}, {'id': 14291, 'synset': 'camomile_tea.n.01', 'name': 'camomile_tea'}, {'id': 14292, 'synset': 'ice_tea.n.01', 'name': 'ice_tea'}, {'id': 14293, 'synset': 'sun_tea.n.01', 'name': 'sun_tea'}, {'id': 14294, 'synset': 'black_tea.n.01', 'name': 'black_tea'}, {'id': 14295, 'synset': 'congou.n.01', 'name': 'congou'}, {'id': 14296, 'synset': 'darjeeling.n.01', 'name': 'Darjeeling'}, {'id': 14297, 'synset': 'orange_pekoe.n.01', 'name': 'orange_pekoe'}, {'id': 14298, 'synset': 'souchong.n.01', 'name': 'souchong'}, {'id': 14299, 'synset': 'green_tea.n.01', 'name': 'green_tea'}, {'id': 14300, 'synset': 'hyson.n.01', 'name': 'hyson'}, {'id': 14301, 'synset': 'oolong.n.01', 'name': 'oolong'}, {'id': 14302, 'synset': 'water.n.06', 'name': 'water'}, {'id': 14303, 'synset': 'bottled_water.n.01', 'name': 'bottled_water'}, {'id': 14304, 'synset': 'branch_water.n.01', 'name': 'branch_water'}, {'id': 14305, 'synset': 'spring_water.n.02', 'name': 'spring_water'}, {'id': 14306, 'synset': 'sugar_water.n.01', 'name': 'sugar_water'}, {'id': 14307, 'synset': 'drinking_water.n.01', 'name': 'drinking_water'}, {'id': 14308, 'synset': 'ice_water.n.01', 'name': 'ice_water'}, {'id': 14309, 'synset': 'soda_water.n.01', 'name': 'soda_water'}, {'id': 14310, 'synset': 'mineral_water.n.01', 'name': 'mineral_water'}, {'id': 14311, 'synset': 'seltzer.n.01', 'name': 'seltzer'}, {'id': 14312, 'synset': 'vichy_water.n.01', 'name': 'Vichy_water'}, {'id': 14313, 'synset': 'perishable.n.01', 'name': 'perishable'}, {'id': 14314, 'synset': 'couscous.n.01', 'name': 'couscous'}, {'id': 14315, 'synset': 'ramekin.n.01', 'name': 'ramekin'}, {'id': 14316, 'synset': 'multivitamin.n.01', 'name': 'multivitamin'}, {'id': 14317, 'synset': 'vitamin_pill.n.01', 'name': 'vitamin_pill'}, {'id': 14318, 'synset': 'soul_food.n.01', 'name': 'soul_food'}, {'id': 14319, 'synset': 'mold.n.06', 'name': 'mold'}, {'id': 14320, 'synset': 'people.n.01', 'name': 'people'}, {'id': 14321, 'synset': 'collection.n.01', 'name': 'collection'}, {'id': 14322, 'synset': 'book.n.07', 'name': 'book'}, {'id': 14323, 'synset': 'library.n.02', 'name': 'library'}, {'id': 14324, 'synset': 'baseball_club.n.01', 'name': 'baseball_club'}, {'id': 14325, 'synset': 'crowd.n.01', 'name': 'crowd'}, {'id': 14326, 'synset': 'class.n.02', 'name': 'class'}, {'id': 14327, 'synset': 'core.n.01', 'name': 'core'}, {'id': 14328, 'synset': 'concert_band.n.01', 'name': 'concert_band'}, {'id': 14329, 'synset': 'dance.n.02', 'name': 'dance'}, {'id': 14330, 'synset': 'wedding.n.03', 'name': 'wedding'}, {'id': 14331, 'synset': 'chain.n.01', 'name': 'chain'}, {'id': 14332, 'synset': 'power_breakfast.n.01', 'name': 'power_breakfast'}, {'id': 14333, 'synset': 'aerie.n.02', 'name': 'aerie'}, {'id': 14334, 'synset': 'agora.n.02', 'name': 'agora'}, {'id': 14335, 'synset': 'amusement_park.n.01', 'name': 'amusement_park'}, {'id': 14336, 'synset': 'aphelion.n.01', 'name': 'aphelion'}, {'id': 14337, 'synset': 'apron.n.02', 'name': 'apron'}, {'id': 14338, 'synset': 'interplanetary_space.n.01', 'name': 'interplanetary_space'}, {'id': 14339, 'synset': 'interstellar_space.n.01', 'name': 'interstellar_space'}, {'id': 14340, 'synset': 'intergalactic_space.n.01', 'name': 'intergalactic_space'}, {'id': 14341, 'synset': 'bush.n.02', 'name': 'bush'}, {'id': 14342, 'synset': 'semidesert.n.01', 'name': 'semidesert'}, {'id': 14343, 'synset': 'beam-ends.n.01', 'name': 'beam-ends'}, {'id': 14344, 'synset': 'bridgehead.n.02', 'name': 'bridgehead'}, {'id': 14345, 'synset': 'bus_stop.n.01', 'name': 'bus_stop'}, {'id': 14346, 'synset': 'campsite.n.01', 'name': 'campsite'}, {'id': 14347, 'synset': 'detention_basin.n.01', 'name': 'detention_basin'}, {'id': 14348, 'synset': 'cemetery.n.01', 'name': 'cemetery'}, {'id': 14349, 'synset': 'trichion.n.01', 'name': 'trichion'}, {'id': 14350, 'synset': 'city.n.01', 'name': 'city'}, {'id': 14351, 'synset': 'business_district.n.01', 'name': 'business_district'}, {'id': 14352, 'synset': 'outskirts.n.01', 'name': 'outskirts'}, {'id': 14353, 'synset': 'borough.n.01', 'name': 'borough'}, {'id': 14354, 'synset': 'cow_pasture.n.01', 'name': 'cow_pasture'}, {'id': 14355, 'synset': 'crest.n.01', 'name': 'crest'}, {'id': 14356, 'synset': 'eparchy.n.02', 'name': 'eparchy'}, {'id': 14357, 'synset': 'suburb.n.01', 'name': 'suburb'}, {'id': 14358, 'synset': 'stockbroker_belt.n.01', 'name': 'stockbroker_belt'}, {'id': 14359, 'synset': 'crawlspace.n.01', 'name': 'crawlspace'}, {'id': 14360, 'synset': 'sheikdom.n.01', 'name': 'sheikdom'}, {'id': 14361, 'synset': 'residence.n.01', 'name': 'residence'}, {'id': 14362, 'synset': 'domicile.n.01', 'name': 'domicile'}, {'id': 14363, 'synset': 'dude_ranch.n.01', 'name': 'dude_ranch'}, {'id': 14364, 'synset': 'farmland.n.01', 'name': 'farmland'}, {'id': 14365, 'synset': 'midfield.n.01', 'name': 'midfield'}, {'id': 14366, 'synset': 'firebreak.n.01', 'name': 'firebreak'}, {'id': 14367, 'synset': 'flea_market.n.01', 'name': 'flea_market'}, {'id': 14368, 'synset': 'battlefront.n.01', 'name': 'battlefront'}, {'id': 14369, 'synset': 'garbage_heap.n.01', 'name': 'garbage_heap'}, {'id': 14370, 'synset': 'benthos.n.01', 'name': 'benthos'}, {'id': 14371, 'synset': 'goldfield.n.01', 'name': 'goldfield'}, {'id': 14372, 'synset': 'grainfield.n.01', 'name': 'grainfield'}, {'id': 14373, 'synset': 'half-mast.n.01', 'name': 'half-mast'}, {'id': 14374, 'synset': 'hemline.n.01', 'name': 'hemline'}, {'id': 14375, 'synset': 'heronry.n.01', 'name': 'heronry'}, {'id': 14376, 'synset': 'hipline.n.02', 'name': 'hipline'}, {'id': 14377, 'synset': 'hipline.n.01', 'name': 'hipline'}, {'id': 14378, 'synset': 'hole-in-the-wall.n.01', 'name': 'hole-in-the-wall'}, {'id': 14379, 'synset': 'junkyard.n.01', 'name': 'junkyard'}, {'id': 14380, 'synset': 'isoclinic_line.n.01', 'name': 'isoclinic_line'}, {'id': 14381, 'synset': 'littoral.n.01', 'name': 'littoral'}, {'id': 14382, 'synset': 'magnetic_pole.n.01', 'name': 'magnetic_pole'}, {'id': 14383, 'synset': 'grassland.n.01', 'name': 'grassland'}, {'id': 14384, 'synset': 'mecca.n.02', 'name': 'mecca'}, {'id': 14385, 'synset': "observer's_meridian.n.01", 'name': "observer's_meridian"}, {'id': 14386, 'synset': 'prime_meridian.n.01', 'name': 'prime_meridian'}, {'id': 14387, 'synset': 'nombril.n.01', 'name': 'nombril'}, {'id': 14388, 'synset': 'no-parking_zone.n.01', 'name': 'no-parking_zone'}, {'id': 14389, 'synset': 'outdoors.n.01', 'name': 'outdoors'}, {'id': 14390, 'synset': 'fairground.n.01', 'name': 'fairground'}, {'id': 14391, 'synset': 'pasture.n.01', 'name': 'pasture'}, {'id': 14392, 'synset': 'perihelion.n.01', 'name': 'perihelion'}, {'id': 14393, 'synset': 'periselene.n.01', 'name': 'periselene'}, {'id': 14394, 'synset': 'locus_of_infection.n.01', 'name': 'locus_of_infection'}, {'id': 14395, 'synset': 'kasbah.n.01', 'name': 'kasbah'}, {'id': 14396, 'synset': 'waterfront.n.01', 'name': 'waterfront'}, {'id': 14397, 'synset': 'resort.n.01', 'name': 'resort'}, {'id': 14398, 'synset': 'resort_area.n.01', 'name': 'resort_area'}, {'id': 14399, 'synset': 'rough.n.01', 'name': 'rough'}, {'id': 14400, 'synset': 'ashram.n.02', 'name': 'ashram'}, {'id': 14401, 'synset': 'harborage.n.01', 'name': 'harborage'}, {'id': 14402, 'synset': 'scrubland.n.01', 'name': 'scrubland'}, {'id': 14403, 'synset': 'weald.n.01', 'name': 'weald'}, {'id': 14404, 'synset': 'wold.n.01', 'name': 'wold'}, {'id': 14405, 'synset': 'schoolyard.n.01', 'name': 'schoolyard'}, {'id': 14406, 'synset': 'showplace.n.01', 'name': 'showplace'}, {'id': 14407, 'synset': 'bedside.n.01', 'name': 'bedside'}, {'id': 14408, 'synset': 'sideline.n.01', 'name': 'sideline'}, {'id': 14409, 'synset': 'ski_resort.n.01', 'name': 'ski_resort'}, {'id': 14410, 'synset': 'soil_horizon.n.01', 'name': 'soil_horizon'}, {'id': 14411, 'synset': 'geological_horizon.n.01', 'name': 'geological_horizon'}, {'id': 14412, 'synset': 'coal_seam.n.01', 'name': 'coal_seam'}, {'id': 14413, 'synset': 'coalface.n.01', 'name': 'coalface'}, {'id': 14414, 'synset': 'field.n.14', 'name': 'field'}, {'id': 14415, 'synset': 'oilfield.n.01', 'name': 'oilfield'}, {'id': 14416, 'synset': 'temperate_zone.n.01', 'name': 'Temperate_Zone'}, {'id': 14417, 'synset': 'terreplein.n.01', 'name': 'terreplein'}, {'id': 14418, 'synset': 'three-mile_limit.n.01', 'name': 'three-mile_limit'}, {'id': 14419, 'synset': 'desktop.n.01', 'name': 'desktop'}, {'id': 14420, 'synset': 'top.n.01', 'name': 'top'}, {'id': 14421, 'synset': 'kampong.n.01', 'name': 'kampong'}, {'id': 14422, 'synset': 'subtropics.n.01', 'name': 'subtropics'}, {'id': 14423, 'synset': 'barrio.n.02', 'name': 'barrio'}, {'id': 14424, 'synset': 'veld.n.01', 'name': 'veld'}, {'id': 14425, 'synset': 'vertex.n.02', 'name': 'vertex'}, {'id': 14426, 'synset': 'waterline.n.01', 'name': 'waterline'}, {'id': 14427, 'synset': 'high-water_mark.n.01', 'name': 'high-water_mark'}, {'id': 14428, 'synset': 'low-water_mark.n.02', 'name': 'low-water_mark'}, {'id': 14429, 'synset': 'continental_divide.n.01', 'name': 'continental_divide'}, {'id': 14430, 'synset': 'zodiac.n.01', 'name': 'zodiac'}, {'id': 14431, 'synset': 'aegean_island.n.01', 'name': 'Aegean_island'}, {'id': 14432, 'synset': 'sultanate.n.01', 'name': 'sultanate'}, {'id': 14433, 'synset': 'swiss_canton.n.01', 'name': 'Swiss_canton'}, {'id': 14434, 'synset': 'abyssal_zone.n.01', 'name': 'abyssal_zone'}, {'id': 14435, 'synset': 'aerie.n.01', 'name': 'aerie'}, {'id': 14436, 'synset': 'air_bubble.n.01', 'name': 'air_bubble'}, {'id': 14437, 'synset': 'alluvial_flat.n.01', 'name': 'alluvial_flat'}, {'id': 14438, 'synset': 'alp.n.01', 'name': 'alp'}, {'id': 14439, 'synset': 'alpine_glacier.n.01', 'name': 'Alpine_glacier'}, {'id': 14440, 'synset': 'anthill.n.01', 'name': 'anthill'}, {'id': 14441, 'synset': 'aquifer.n.01', 'name': 'aquifer'}, {'id': 14442, 'synset': 'archipelago.n.01', 'name': 'archipelago'}, {'id': 14443, 'synset': 'arete.n.01', 'name': 'arete'}, {'id': 14444, 'synset': 'arroyo.n.01', 'name': 'arroyo'}, {'id': 14445, 'synset': 'ascent.n.01', 'name': 'ascent'}, {'id': 14446, 'synset': 'asterism.n.02', 'name': 'asterism'}, {'id': 14447, 'synset': 'asthenosphere.n.01', 'name': 'asthenosphere'}, {'id': 14448, 'synset': 'atoll.n.01', 'name': 'atoll'}, {'id': 14449, 'synset': 'bank.n.03', 'name': 'bank'}, {'id': 14450, 'synset': 'bank.n.01', 'name': 'bank'}, {'id': 14451, 'synset': 'bar.n.08', 'name': 'bar'}, {'id': 14452, 'synset': 'barbecue_pit.n.01', 'name': 'barbecue_pit'}, {'id': 14453, 'synset': 'barrier_reef.n.01', 'name': 'barrier_reef'}, {'id': 14454, 'synset': 'baryon.n.01', 'name': 'baryon'}, {'id': 14455, 'synset': 'basin.n.03', 'name': 'basin'}, {'id': 14456, 'synset': 'beach.n.01', 'name': 'beach'}, {'id': 14457, 'synset': 'honeycomb.n.01', 'name': 'honeycomb'}, {'id': 14458, 'synset': 'belay.n.01', 'name': 'belay'}, {'id': 14459, 'synset': 'ben.n.01', 'name': 'ben'}, {'id': 14460, 'synset': 'berm.n.01', 'name': 'berm'}, {'id': 14461, 'synset': 'bladder_stone.n.01', 'name': 'bladder_stone'}, {'id': 14462, 'synset': 'bluff.n.01', 'name': 'bluff'}, {'id': 14463, 'synset': 'borrow_pit.n.01', 'name': 'borrow_pit'}, {'id': 14464, 'synset': 'brae.n.01', 'name': 'brae'}, {'id': 14465, 'synset': 'bubble.n.01', 'name': 'bubble'}, {'id': 14466, 'synset': 'burrow.n.01', 'name': 'burrow'}, {'id': 14467, 'synset': 'butte.n.01', 'name': 'butte'}, {'id': 14468, 'synset': 'caldera.n.01', 'name': 'caldera'}, {'id': 14469, 'synset': 'canyon.n.01', 'name': 'canyon'}, {'id': 14470, 'synset': 'canyonside.n.01', 'name': 'canyonside'}, {'id': 14471, 'synset': 'cave.n.01', 'name': 'cave'}, {'id': 14472, 'synset': 'cavern.n.02', 'name': 'cavern'}, {'id': 14473, 'synset': 'chasm.n.01', 'name': 'chasm'}, {'id': 14474, 'synset': 'cirque.n.01', 'name': 'cirque'}, {'id': 14475, 'synset': 'cliff.n.01', 'name': 'cliff'}, {'id': 14476, 'synset': 'cloud.n.02', 'name': 'cloud'}, {'id': 14477, 'synset': 'coast.n.02', 'name': 'coast'}, {'id': 14478, 'synset': 'coastland.n.01', 'name': 'coastland'}, {'id': 14479, 'synset': 'col.n.01', 'name': 'col'}, {'id': 14480, 'synset': 'collector.n.03', 'name': 'collector'}, {'id': 14481, 'synset': 'comet.n.01', 'name': 'comet'}, {'id': 14482, 'synset': 'continental_glacier.n.01', 'name': 'continental_glacier'}, {'id': 14483, 'synset': 'coral_reef.n.01', 'name': 'coral_reef'}, {'id': 14484, 'synset': 'cove.n.02', 'name': 'cove'}, {'id': 14485, 'synset': 'crag.n.01', 'name': 'crag'}, {'id': 14486, 'synset': 'crater.n.03', 'name': 'crater'}, {'id': 14487, 'synset': 'cultivated_land.n.01', 'name': 'cultivated_land'}, {'id': 14488, 'synset': 'dale.n.01', 'name': 'dale'}, {'id': 14489, 'synset': 'defile.n.01', 'name': 'defile'}, {'id': 14490, 'synset': 'delta.n.01', 'name': 'delta'}, {'id': 14491, 'synset': 'descent.n.05', 'name': 'descent'}, {'id': 14492, 'synset': 'diapir.n.01', 'name': 'diapir'}, {'id': 14493, 'synset': 'divot.n.02', 'name': 'divot'}, {'id': 14494, 'synset': 'divot.n.01', 'name': 'divot'}, {'id': 14495, 'synset': 'down.n.04', 'name': 'down'}, {'id': 14496, 'synset': 'downhill.n.01', 'name': 'downhill'}, {'id': 14497, 'synset': 'draw.n.01', 'name': 'draw'}, {'id': 14498, 'synset': 'drey.n.01', 'name': 'drey'}, {'id': 14499, 'synset': 'drumlin.n.01', 'name': 'drumlin'}, {'id': 14500, 'synset': 'dune.n.01', 'name': 'dune'}, {'id': 14501, 'synset': 'escarpment.n.01', 'name': 'escarpment'}, {'id': 14502, 'synset': 'esker.n.01', 'name': 'esker'}, {'id': 14503, 'synset': 'fireball.n.03', 'name': 'fireball'}, {'id': 14504, 'synset': 'flare_star.n.01', 'name': 'flare_star'}, {'id': 14505, 'synset': 'floor.n.04', 'name': 'floor'}, {'id': 14506, 'synset': 'fomite.n.01', 'name': 'fomite'}, {'id': 14507, 'synset': 'foothill.n.01', 'name': 'foothill'}, {'id': 14508, 'synset': 'footwall.n.01', 'name': 'footwall'}, {'id': 14509, 'synset': 'foreland.n.02', 'name': 'foreland'}, {'id': 14510, 'synset': 'foreshore.n.01', 'name': 'foreshore'}, {'id': 14511, 'synset': 'gauge_boson.n.01', 'name': 'gauge_boson'}, {'id': 14512, 'synset': 'geological_formation.n.01', 'name': 'geological_formation'}, {'id': 14513, 'synset': 'geyser.n.01', 'name': 'geyser'}, {'id': 14514, 'synset': 'glacier.n.01', 'name': 'glacier'}, {'id': 14515, 'synset': 'glen.n.01', 'name': 'glen'}, {'id': 14516, 'synset': 'gopher_hole.n.01', 'name': 'gopher_hole'}, {'id': 14517, 'synset': 'gorge.n.01', 'name': 'gorge'}, {'id': 14518, 'synset': 'grotto.n.01', 'name': 'grotto'}, {'id': 14519, 'synset': 'growler.n.02', 'name': 'growler'}, {'id': 14520, 'synset': 'gulch.n.01', 'name': 'gulch'}, {'id': 14521, 'synset': 'gully.n.01', 'name': 'gully'}, {'id': 14522, 'synset': 'hail.n.02', 'name': 'hail'}, {'id': 14523, 'synset': 'highland.n.01', 'name': 'highland'}, {'id': 14524, 'synset': 'hill.n.01', 'name': 'hill'}, {'id': 14525, 'synset': 'hillside.n.01', 'name': 'hillside'}, {'id': 14526, 'synset': 'hole.n.05', 'name': 'hole'}, {'id': 14527, 'synset': 'hollow.n.02', 'name': 'hollow'}, {'id': 14528, 'synset': 'hot_spring.n.01', 'name': 'hot_spring'}, {'id': 14529, 'synset': 'iceberg.n.01', 'name': 'iceberg'}, {'id': 14530, 'synset': 'icecap.n.01', 'name': 'icecap'}, {'id': 14531, 'synset': 'ice_field.n.01', 'name': 'ice_field'}, {'id': 14532, 'synset': 'ice_floe.n.01', 'name': 'ice_floe'}, {'id': 14533, 'synset': 'ice_mass.n.01', 'name': 'ice_mass'}, {'id': 14534, 'synset': 'inclined_fault.n.01', 'name': 'inclined_fault'}, {'id': 14535, 'synset': 'ion.n.01', 'name': 'ion'}, {'id': 14536, 'synset': 'isthmus.n.01', 'name': 'isthmus'}, {'id': 14537, 'synset': 'kidney_stone.n.01', 'name': 'kidney_stone'}, {'id': 14538, 'synset': 'knoll.n.01', 'name': 'knoll'}, {'id': 14539, 'synset': 'kopje.n.01', 'name': 'kopje'}, {'id': 14540, 'synset': 'kuiper_belt.n.01', 'name': 'Kuiper_belt'}, {'id': 14541, 'synset': 'lake_bed.n.01', 'name': 'lake_bed'}, {'id': 14542, 'synset': 'lakefront.n.01', 'name': 'lakefront'}, {'id': 14543, 'synset': 'lakeside.n.01', 'name': 'lakeside'}, {'id': 14544, 'synset': 'landfall.n.01', 'name': 'landfall'}, {'id': 14545, 'synset': 'landfill.n.01', 'name': 'landfill'}, {'id': 14546, 'synset': 'lather.n.04', 'name': 'lather'}, {'id': 14547, 'synset': 'leak.n.01', 'name': 'leak'}, {'id': 14548, 'synset': 'ledge.n.01', 'name': 'ledge'}, {'id': 14549, 'synset': 'lepton.n.02', 'name': 'lepton'}, {'id': 14550, 'synset': 'lithosphere.n.01', 'name': 'lithosphere'}, {'id': 14551, 'synset': 'lowland.n.01', 'name': 'lowland'}, {'id': 14552, 'synset': 'lunar_crater.n.01', 'name': 'lunar_crater'}, {'id': 14553, 'synset': 'maar.n.01', 'name': 'maar'}, {'id': 14554, 'synset': 'massif.n.01', 'name': 'massif'}, {'id': 14555, 'synset': 'meander.n.01', 'name': 'meander'}, {'id': 14556, 'synset': 'mesa.n.01', 'name': 'mesa'}, {'id': 14557, 'synset': 'meteorite.n.01', 'name': 'meteorite'}, {'id': 14558, 'synset': 'microfossil.n.01', 'name': 'microfossil'}, {'id': 14559, 'synset': 'midstream.n.01', 'name': 'midstream'}, {'id': 14560, 'synset': 'molehill.n.01', 'name': 'molehill'}, {'id': 14561, 'synset': 'monocline.n.01', 'name': 'monocline'}, {'id': 14562, 'synset': 'mountain.n.01', 'name': 'mountain'}, {'id': 14563, 'synset': 'mountainside.n.01', 'name': 'mountainside'}, {'id': 14564, 'synset': 'mouth.n.04', 'name': 'mouth'}, {'id': 14565, 'synset': 'mull.n.01', 'name': 'mull'}, {'id': 14566, 'synset': 'natural_depression.n.01', 'name': 'natural_depression'}, {'id': 14567, 'synset': 'natural_elevation.n.01', 'name': 'natural_elevation'}, {'id': 14568, 'synset': 'nullah.n.01', 'name': 'nullah'}, {'id': 14569, 'synset': 'ocean.n.01', 'name': 'ocean'}, {'id': 14570, 'synset': 'ocean_floor.n.01', 'name': 'ocean_floor'}, {'id': 14571, 'synset': 'oceanfront.n.01', 'name': 'oceanfront'}, {'id': 14572, 'synset': 'outcrop.n.01', 'name': 'outcrop'}, {'id': 14573, 'synset': 'oxbow.n.01', 'name': 'oxbow'}, {'id': 14574, 'synset': 'pallasite.n.01', 'name': 'pallasite'}, {'id': 14575, 'synset': 'perforation.n.02', 'name': 'perforation'}, {'id': 14576, 'synset': 'photosphere.n.01', 'name': 'photosphere'}, {'id': 14577, 'synset': 'piedmont.n.02', 'name': 'piedmont'}, {'id': 14578, 'synset': 'piedmont_glacier.n.01', 'name': 'Piedmont_glacier'}, {'id': 14579, 'synset': 'pinetum.n.01', 'name': 'pinetum'}, {'id': 14580, 'synset': 'plage.n.01', 'name': 'plage'}, {'id': 14581, 'synset': 'plain.n.01', 'name': 'plain'}, {'id': 14582, 'synset': 'point.n.11', 'name': 'point'}, {'id': 14583, 'synset': 'polar_glacier.n.01', 'name': 'polar_glacier'}, {'id': 14584, 'synset': 'pothole.n.01', 'name': 'pothole'}, {'id': 14585, 'synset': 'precipice.n.01', 'name': 'precipice'}, {'id': 14586, 'synset': 'promontory.n.01', 'name': 'promontory'}, {'id': 14587, 'synset': 'ptyalith.n.01', 'name': 'ptyalith'}, {'id': 14588, 'synset': 'pulsar.n.01', 'name': 'pulsar'}, {'id': 14589, 'synset': 'quicksand.n.02', 'name': 'quicksand'}, {'id': 14590, 'synset': 'rabbit_burrow.n.01', 'name': 'rabbit_burrow'}, {'id': 14591, 'synset': 'radiator.n.01', 'name': 'radiator'}, {'id': 14592, 'synset': 'rainbow.n.01', 'name': 'rainbow'}, {'id': 14593, 'synset': 'range.n.04', 'name': 'range'}, {'id': 14594, 'synset': 'rangeland.n.01', 'name': 'rangeland'}, {'id': 14595, 'synset': 'ravine.n.01', 'name': 'ravine'}, {'id': 14596, 'synset': 'reef.n.01', 'name': 'reef'}, {'id': 14597, 'synset': 'ridge.n.01', 'name': 'ridge'}, {'id': 14598, 'synset': 'ridge.n.04', 'name': 'ridge'}, {'id': 14599, 'synset': 'rift_valley.n.01', 'name': 'rift_valley'}, {'id': 14600, 'synset': 'riparian_forest.n.01', 'name': 'riparian_forest'}, {'id': 14601, 'synset': 'ripple_mark.n.01', 'name': 'ripple_mark'}, {'id': 14602, 'synset': 'riverbank.n.01', 'name': 'riverbank'}, {'id': 14603, 'synset': 'riverbed.n.01', 'name': 'riverbed'}, {'id': 14604, 'synset': 'rock.n.01', 'name': 'rock'}, {'id': 14605, 'synset': 'roof.n.03', 'name': 'roof'}, {'id': 14606, 'synset': 'saltpan.n.01', 'name': 'saltpan'}, {'id': 14607, 'synset': 'sandbank.n.01', 'name': 'sandbank'}, {'id': 14608, 'synset': 'sandbar.n.01', 'name': 'sandbar'}, {'id': 14609, 'synset': 'sandpit.n.01', 'name': 'sandpit'}, {'id': 14610, 'synset': 'sanitary_landfill.n.01', 'name': 'sanitary_landfill'}, {'id': 14611, 'synset': 'sawpit.n.01', 'name': 'sawpit'}, {'id': 14612, 'synset': 'scablands.n.01', 'name': 'scablands'}, {'id': 14613, 'synset': 'seashore.n.01', 'name': 'seashore'}, {'id': 14614, 'synset': 'seaside.n.01', 'name': 'seaside'}, {'id': 14615, 'synset': 'seif_dune.n.01', 'name': 'seif_dune'}, {'id': 14616, 'synset': 'shell.n.06', 'name': 'shell'}, {'id': 14617, 'synset': 'shiner.n.02', 'name': 'shiner'}, {'id': 14618, 'synset': 'shoal.n.01', 'name': 'shoal'}, {'id': 14619, 'synset': 'shore.n.01', 'name': 'shore'}, {'id': 14620, 'synset': 'shoreline.n.01', 'name': 'shoreline'}, {'id': 14621, 'synset': 'sinkhole.n.01', 'name': 'sinkhole'}, {'id': 14622, 'synset': 'ski_slope.n.01', 'name': 'ski_slope'}, {'id': 14623, 'synset': 'sky.n.01', 'name': 'sky'}, {'id': 14624, 'synset': 'slope.n.01', 'name': 'slope'}, {'id': 14625, 'synset': 'snowcap.n.01', 'name': 'snowcap'}, {'id': 14626, 'synset': 'snowdrift.n.01', 'name': 'snowdrift'}, {'id': 14627, 'synset': 'snowfield.n.01', 'name': 'snowfield'}, {'id': 14628, 'synset': 'soapsuds.n.01', 'name': 'soapsuds'}, {'id': 14629, 'synset': 'spit.n.01', 'name': 'spit'}, {'id': 14630, 'synset': 'spoor.n.01', 'name': 'spoor'}, {'id': 14631, 'synset': 'spume.n.01', 'name': 'spume'}, {'id': 14632, 'synset': 'star.n.03', 'name': 'star'}, {'id': 14633, 'synset': 'steep.n.01', 'name': 'steep'}, {'id': 14634, 'synset': 'steppe.n.01', 'name': 'steppe'}, {'id': 14635, 'synset': 'strand.n.05', 'name': 'strand'}, {'id': 14636, 'synset': 'streambed.n.01', 'name': 'streambed'}, {'id': 14637, 'synset': 'sun.n.01', 'name': 'sun'}, {'id': 14638, 'synset': 'supernova.n.01', 'name': 'supernova'}, {'id': 14639, 'synset': 'swale.n.01', 'name': 'swale'}, {'id': 14640, 'synset': 'swamp.n.01', 'name': 'swamp'}, {'id': 14641, 'synset': 'swell.n.02', 'name': 'swell'}, {'id': 14642, 'synset': 'tableland.n.01', 'name': 'tableland'}, {'id': 14643, 'synset': 'talus.n.01', 'name': 'talus'}, {'id': 14644, 'synset': 'tangle.n.01', 'name': 'tangle'}, {'id': 14645, 'synset': 'tar_pit.n.01', 'name': 'tar_pit'}, {'id': 14646, 'synset': 'terrace.n.02', 'name': 'terrace'}, {'id': 14647, 'synset': 'tidal_basin.n.01', 'name': 'tidal_basin'}, {'id': 14648, 'synset': 'tideland.n.01', 'name': 'tideland'}, {'id': 14649, 'synset': 'tor.n.02', 'name': 'tor'}, {'id': 14650, 'synset': 'tor.n.01', 'name': 'tor'}, {'id': 14651, 'synset': 'trapezium.n.02', 'name': 'Trapezium'}, {'id': 14652, 'synset': 'troposphere.n.01', 'name': 'troposphere'}, {'id': 14653, 'synset': 'tundra.n.01', 'name': 'tundra'}, {'id': 14654, 'synset': 'twinkler.n.01', 'name': 'twinkler'}, {'id': 14655, 'synset': 'uphill.n.01', 'name': 'uphill'}, {'id': 14656, 'synset': 'urolith.n.01', 'name': 'urolith'}, {'id': 14657, 'synset': 'valley.n.01', 'name': 'valley'}, {'id': 14658, 'synset': 'vehicle-borne_transmission.n.01', 'name': 'vehicle-borne_transmission'}, {'id': 14659, 'synset': 'vein.n.04', 'name': 'vein'}, {'id': 14660, 'synset': 'volcanic_crater.n.01', 'name': 'volcanic_crater'}, {'id': 14661, 'synset': 'volcano.n.02', 'name': 'volcano'}, {'id': 14662, 'synset': 'wadi.n.01', 'name': 'wadi'}, {'id': 14663, 'synset': 'wall.n.05', 'name': 'wall'}, {'id': 14664, 'synset': 'warren.n.03', 'name': 'warren'}, {'id': 14665, 'synset': "wasp's_nest.n.01", 'name': "wasp's_nest"}, {'id': 14666, 'synset': 'watercourse.n.01', 'name': 'watercourse'}, {'id': 14667, 'synset': 'waterside.n.01', 'name': 'waterside'}, {'id': 14668, 'synset': 'water_table.n.01', 'name': 'water_table'}, {'id': 14669, 'synset': 'whinstone.n.01', 'name': 'whinstone'}, {'id': 14670, 'synset': 'wormcast.n.02', 'name': 'wormcast'}, {'id': 14671, 'synset': 'xenolith.n.01', 'name': 'xenolith'}, {'id': 14672, 'synset': 'circe.n.01', 'name': 'Circe'}, {'id': 14673, 'synset': 'gryphon.n.01', 'name': 'gryphon'}, {'id': 14674, 'synset': 'spiritual_leader.n.01', 'name': 'spiritual_leader'}, {'id': 14675, 'synset': 'messiah.n.01', 'name': 'messiah'}, {'id': 14676, 'synset': 'rhea_silvia.n.01', 'name': 'Rhea_Silvia'}, {'id': 14677, 'synset': 'number_one.n.01', 'name': 'number_one'}, {'id': 14678, 'synset': 'adventurer.n.01', 'name': 'adventurer'}, {'id': 14679, 'synset': 'anomaly.n.02', 'name': 'anomaly'}, {'id': 14680, 'synset': 'appointee.n.02', 'name': 'appointee'}, {'id': 14681, 'synset': 'argonaut.n.01', 'name': 'argonaut'}, {'id': 14682, 'synset': 'ashkenazi.n.01', 'name': 'Ashkenazi'}, {'id': 14683, 'synset': 'benefactor.n.01', 'name': 'benefactor'}, {'id': 14684, 'synset': 'color-blind_person.n.01', 'name': 'color-blind_person'}, {'id': 14685, 'synset': 'commoner.n.01', 'name': 'commoner'}, {'id': 14686, 'synset': 'conservator.n.02', 'name': 'conservator'}, {'id': 14687, 'synset': 'contrarian.n.01', 'name': 'contrarian'}, {'id': 14688, 'synset': 'contadino.n.01', 'name': 'contadino'}, {'id': 14689, 'synset': 'contestant.n.01', 'name': 'contestant'}, {'id': 14690, 'synset': 'cosigner.n.01', 'name': 'cosigner'}, {'id': 14691, 'synset': 'discussant.n.01', 'name': 'discussant'}, {'id': 14692, 'synset': 'enologist.n.01', 'name': 'enologist'}, {'id': 14693, 'synset': 'entertainer.n.01', 'name': 'entertainer'}, {'id': 14694, 'synset': 'eulogist.n.01', 'name': 'eulogist'}, {'id': 14695, 'synset': 'ex-gambler.n.01', 'name': 'ex-gambler'}, {'id': 14696, 'synset': 'experimenter.n.01', 'name': 'experimenter'}, {'id': 14697, 'synset': 'experimenter.n.02', 'name': 'experimenter'}, {'id': 14698, 'synset': 'exponent.n.02', 'name': 'exponent'}, {'id': 14699, 'synset': 'ex-president.n.01', 'name': 'ex-president'}, {'id': 14700, 'synset': 'face.n.05', 'name': 'face'}, {'id': 14701, 'synset': 'female.n.02', 'name': 'female'}, {'id': 14702, 'synset': 'finisher.n.04', 'name': 'finisher'}, {'id': 14703, 'synset': 'inhabitant.n.01', 'name': 'inhabitant'}, {'id': 14704, 'synset': 'native.n.01', 'name': 'native'}, {'id': 14705, 'synset': 'native.n.02', 'name': 'native'}, {'id': 14706, 'synset': 'juvenile.n.01', 'name': 'juvenile'}, {'id': 14707, 'synset': 'lover.n.01', 'name': 'lover'}, {'id': 14708, 'synset': 'male.n.02', 'name': 'male'}, {'id': 14709, 'synset': 'mediator.n.01', 'name': 'mediator'}, {'id': 14710, 'synset': 'mediatrix.n.01', 'name': 'mediatrix'}, {'id': 14711, 'synset': 'national.n.01', 'name': 'national'}, {'id': 14712, 'synset': 'peer.n.01', 'name': 'peer'}, {'id': 14713, 'synset': 'prize_winner.n.01', 'name': 'prize_winner'}, {'id': 14714, 'synset': 'recipient.n.01', 'name': 'recipient'}, {'id': 14715, 'synset': 'religionist.n.01', 'name': 'religionist'}, {'id': 14716, 'synset': 'sensualist.n.01', 'name': 'sensualist'}, {'id': 14717, 'synset': 'traveler.n.01', 'name': 'traveler'}, {'id': 14718, 'synset': 'unwelcome_person.n.01', 'name': 'unwelcome_person'}, {'id': 14719, 'synset': 'unskilled_person.n.01', 'name': 'unskilled_person'}, {'id': 14720, 'synset': 'worker.n.01', 'name': 'worker'}, {'id': 14721, 'synset': 'wrongdoer.n.01', 'name': 'wrongdoer'}, {'id': 14722, 'synset': 'black_african.n.01', 'name': 'Black_African'}, {'id': 14723, 'synset': 'afrikaner.n.01', 'name': 'Afrikaner'}, {'id': 14724, 'synset': 'aryan.n.01', 'name': 'Aryan'}, {'id': 14725, 'synset': 'black.n.05', 'name': 'Black'}, {'id': 14726, 'synset': 'black_woman.n.01', 'name': 'Black_woman'}, {'id': 14727, 'synset': 'mulatto.n.01', 'name': 'mulatto'}, {'id': 14728, 'synset': 'white.n.01', 'name': 'White'}, {'id': 14729, 'synset': 'circassian.n.01', 'name': 'Circassian'}, {'id': 14730, 'synset': 'semite.n.01', 'name': 'Semite'}, {'id': 14731, 'synset': 'chaldean.n.02', 'name': 'Chaldean'}, {'id': 14732, 'synset': 'elamite.n.01', 'name': 'Elamite'}, {'id': 14733, 'synset': 'white_man.n.01', 'name': 'white_man'}, {'id': 14734, 'synset': 'wasp.n.01', 'name': 'WASP'}, {'id': 14735, 'synset': 'gook.n.02', 'name': 'gook'}, {'id': 14736, 'synset': 'mongol.n.01', 'name': 'Mongol'}, {'id': 14737, 'synset': 'tatar.n.01', 'name': 'Tatar'}, {'id': 14738, 'synset': 'nahuatl.n.01', 'name': 'Nahuatl'}, {'id': 14739, 'synset': 'aztec.n.01', 'name': 'Aztec'}, {'id': 14740, 'synset': 'olmec.n.01', 'name': 'Olmec'}, {'id': 14741, 'synset': 'biloxi.n.01', 'name': 'Biloxi'}, {'id': 14742, 'synset': 'blackfoot.n.01', 'name': 'Blackfoot'}, {'id': 14743, 'synset': 'brule.n.01', 'name': 'Brule'}, {'id': 14744, 'synset': 'caddo.n.01', 'name': 'Caddo'}, {'id': 14745, 'synset': 'cheyenne.n.03', 'name': 'Cheyenne'}, {'id': 14746, 'synset': 'chickasaw.n.01', 'name': 'Chickasaw'}, {'id': 14747, 'synset': 'cocopa.n.01', 'name': 'Cocopa'}, {'id': 14748, 'synset': 'comanche.n.01', 'name': 'Comanche'}, {'id': 14749, 'synset': 'creek.n.02', 'name': 'Creek'}, {'id': 14750, 'synset': 'delaware.n.02', 'name': 'Delaware'}, {'id': 14751, 'synset': 'diegueno.n.01', 'name': 'Diegueno'}, {'id': 14752, 'synset': 'esselen.n.01', 'name': 'Esselen'}, {'id': 14753, 'synset': 'eyeish.n.01', 'name': 'Eyeish'}, {'id': 14754, 'synset': 'havasupai.n.01', 'name': 'Havasupai'}, {'id': 14755, 'synset': 'hunkpapa.n.01', 'name': 'Hunkpapa'}, {'id': 14756, 'synset': 'iowa.n.01', 'name': 'Iowa'}, {'id': 14757, 'synset': 'kalapooia.n.01', 'name': 'Kalapooia'}, {'id': 14758, 'synset': 'kamia.n.01', 'name': 'Kamia'}, {'id': 14759, 'synset': 'kekchi.n.01', 'name': 'Kekchi'}, {'id': 14760, 'synset': 'kichai.n.01', 'name': 'Kichai'}, {'id': 14761, 'synset': 'kickapoo.n.01', 'name': 'Kickapoo'}, {'id': 14762, 'synset': 'kiliwa.n.01', 'name': 'Kiliwa'}, {'id': 14763, 'synset': 'malecite.n.01', 'name': 'Malecite'}, {'id': 14764, 'synset': 'maricopa.n.01', 'name': 'Maricopa'}, {'id': 14765, 'synset': 'mohican.n.01', 'name': 'Mohican'}, {'id': 14766, 'synset': 'muskhogean.n.01', 'name': 'Muskhogean'}, {'id': 14767, 'synset': 'navaho.n.01', 'name': 'Navaho'}, {'id': 14768, 'synset': 'nootka.n.01', 'name': 'Nootka'}, {'id': 14769, 'synset': 'oglala.n.01', 'name': 'Oglala'}, {'id': 14770, 'synset': 'osage.n.01', 'name': 'Osage'}, {'id': 14771, 'synset': 'oneida.n.01', 'name': 'Oneida'}, {'id': 14772, 'synset': 'paiute.n.01', 'name': 'Paiute'}, {'id': 14773, 'synset': 'passamaquody.n.01', 'name': 'Passamaquody'}, {'id': 14774, 'synset': 'penobscot.n.01', 'name': 'Penobscot'}, {'id': 14775, 'synset': 'penutian.n.02', 'name': 'Penutian'}, {'id': 14776, 'synset': 'potawatomi.n.01', 'name': 'Potawatomi'}, {'id': 14777, 'synset': 'powhatan.n.02', 'name': 'Powhatan'}, {'id': 14778, 'synset': 'kachina.n.02', 'name': 'kachina'}, {'id': 14779, 'synset': 'salish.n.02', 'name': 'Salish'}, {'id': 14780, 'synset': 'shahaptian.n.01', 'name': 'Shahaptian'}, {'id': 14781, 'synset': 'shasta.n.01', 'name': 'Shasta'}, {'id': 14782, 'synset': 'shawnee.n.01', 'name': 'Shawnee'}, {'id': 14783, 'synset': 'sihasapa.n.01', 'name': 'Sihasapa'}, {'id': 14784, 'synset': 'teton.n.01', 'name': 'Teton'}, {'id': 14785, 'synset': 'taracahitian.n.01', 'name': 'Taracahitian'}, {'id': 14786, 'synset': 'tarahumara.n.01', 'name': 'Tarahumara'}, {'id': 14787, 'synset': 'tuscarora.n.01', 'name': 'Tuscarora'}, {'id': 14788, 'synset': 'tutelo.n.01', 'name': 'Tutelo'}, {'id': 14789, 'synset': 'yana.n.01', 'name': 'Yana'}, {'id': 14790, 'synset': 'yavapai.n.01', 'name': 'Yavapai'}, {'id': 14791, 'synset': 'yokuts.n.02', 'name': 'Yokuts'}, {'id': 14792, 'synset': 'yuma.n.01', 'name': 'Yuma'}, {'id': 14793, 'synset': 'gadaba.n.01', 'name': 'Gadaba'}, {'id': 14794, 'synset': 'kolam.n.01', 'name': 'Kolam'}, {'id': 14795, 'synset': 'kui.n.01', 'name': 'Kui'}, {'id': 14796, 'synset': 'toda.n.01', 'name': 'Toda'}, {'id': 14797, 'synset': 'tulu.n.01', 'name': 'Tulu'}, {'id': 14798, 'synset': 'gujarati.n.01', 'name': 'Gujarati'}, {'id': 14799, 'synset': 'kashmiri.n.01', 'name': 'Kashmiri'}, {'id': 14800, 'synset': 'punjabi.n.01', 'name': 'Punjabi'}, {'id': 14801, 'synset': 'slav.n.01', 'name': 'Slav'}, {'id': 14802, 'synset': 'anabaptist.n.01', 'name': 'Anabaptist'}, {'id': 14803, 'synset': 'adventist.n.01', 'name': 'Adventist'}, {'id': 14804, 'synset': 'gentile.n.03', 'name': 'gentile'}, {'id': 14805, 'synset': 'gentile.n.02', 'name': 'gentile'}, {'id': 14806, 'synset': 'catholic.n.01', 'name': 'Catholic'}, {'id': 14807, 'synset': 'old_catholic.n.01', 'name': 'Old_Catholic'}, {'id': 14808, 'synset': 'uniat.n.01', 'name': 'Uniat'}, {'id': 14809, 'synset': 'copt.n.02', 'name': 'Copt'}, {'id': 14810, 'synset': 'jewess.n.01', 'name': 'Jewess'}, {'id': 14811, 'synset': 'jihadist.n.01', 'name': 'Jihadist'}, {'id': 14812, 'synset': 'buddhist.n.01', 'name': 'Buddhist'}, {'id': 14813, 'synset': 'zen_buddhist.n.01', 'name': 'Zen_Buddhist'}, {'id': 14814, 'synset': 'mahayanist.n.01', 'name': 'Mahayanist'}, {'id': 14815, 'synset': 'swami.n.01', 'name': 'swami'}, {'id': 14816, 'synset': 'hare_krishna.n.01', 'name': 'Hare_Krishna'}, {'id': 14817, 'synset': 'shintoist.n.01', 'name': 'Shintoist'}, {'id': 14818, 'synset': 'eurafrican.n.01', 'name': 'Eurafrican'}, {'id': 14819, 'synset': 'eurasian.n.01', 'name': 'Eurasian'}, {'id': 14820, 'synset': 'gael.n.01', 'name': 'Gael'}, {'id': 14821, 'synset': 'frank.n.01', 'name': 'Frank'}, {'id': 14822, 'synset': 'afghan.n.02', 'name': 'Afghan'}, {'id': 14823, 'synset': 'albanian.n.01', 'name': 'Albanian'}, {'id': 14824, 'synset': 'algerian.n.01', 'name': 'Algerian'}, {'id': 14825, 'synset': 'altaic.n.01', 'name': 'Altaic'}, {'id': 14826, 'synset': 'andorran.n.01', 'name': 'Andorran'}, {'id': 14827, 'synset': 'angolan.n.01', 'name': 'Angolan'}, {'id': 14828, 'synset': 'anguillan.n.01', 'name': 'Anguillan'}, {'id': 14829, 'synset': 'austrian.n.01', 'name': 'Austrian'}, {'id': 14830, 'synset': 'bahamian.n.01', 'name': 'Bahamian'}, {'id': 14831, 'synset': 'bahraini.n.01', 'name': 'Bahraini'}, {'id': 14832, 'synset': 'basotho.n.01', 'name': 'Basotho'}, {'id': 14833, 'synset': 'herero.n.01', 'name': 'Herero'}, {'id': 14834, 'synset': 'luba.n.01', 'name': 'Luba'}, {'id': 14835, 'synset': 'barbadian.n.01', 'name': 'Barbadian'}, {'id': 14836, 'synset': 'bolivian.n.01', 'name': 'Bolivian'}, {'id': 14837, 'synset': 'bornean.n.01', 'name': 'Bornean'}, {'id': 14838, 'synset': 'carioca.n.01', 'name': 'Carioca'}, {'id': 14839, 'synset': 'tupi.n.01', 'name': 'Tupi'}, {'id': 14840, 'synset': 'bruneian.n.01', 'name': 'Bruneian'}, {'id': 14841, 'synset': 'bulgarian.n.01', 'name': 'Bulgarian'}, {'id': 14842, 'synset': 'byelorussian.n.01', 'name': 'Byelorussian'}, {'id': 14843, 'synset': 'cameroonian.n.01', 'name': 'Cameroonian'}, {'id': 14844, 'synset': 'canadian.n.01', 'name': 'Canadian'}, {'id': 14845, 'synset': 'french_canadian.n.01', 'name': 'French_Canadian'}, {'id': 14846, 'synset': 'central_american.n.01', 'name': 'Central_American'}, {'id': 14847, 'synset': 'chilean.n.01', 'name': 'Chilean'}, {'id': 14848, 'synset': 'congolese.n.01', 'name': 'Congolese'}, {'id': 14849, 'synset': 'cypriot.n.01', 'name': 'Cypriot'}, {'id': 14850, 'synset': 'dane.n.01', 'name': 'Dane'}, {'id': 14851, 'synset': 'djiboutian.n.01', 'name': 'Djiboutian'}, {'id': 14852, 'synset': 'britisher.n.01', 'name': 'Britisher'}, {'id': 14853, 'synset': 'english_person.n.01', 'name': 'English_person'}, {'id': 14854, 'synset': 'englishwoman.n.01', 'name': 'Englishwoman'}, {'id': 14855, 'synset': 'anglo-saxon.n.02', 'name': 'Anglo-Saxon'}, {'id': 14856, 'synset': 'angle.n.03', 'name': 'Angle'}, {'id': 14857, 'synset': 'west_saxon.n.01', 'name': 'West_Saxon'}, {'id': 14858, 'synset': 'lombard.n.01', 'name': 'Lombard'}, {'id': 14859, 'synset': 'limey.n.01', 'name': 'limey'}, {'id': 14860, 'synset': 'cantabrigian.n.01', 'name': 'Cantabrigian'}, {'id': 14861, 'synset': 'cornishman.n.01', 'name': 'Cornishman'}, {'id': 14862, 'synset': 'cornishwoman.n.01', 'name': 'Cornishwoman'}, {'id': 14863, 'synset': 'lancastrian.n.02', 'name': 'Lancastrian'}, {'id': 14864, 'synset': 'lancastrian.n.01', 'name': 'Lancastrian'}, {'id': 14865, 'synset': 'geordie.n.01', 'name': 'Geordie'}, {'id': 14866, 'synset': 'oxonian.n.01', 'name': 'Oxonian'}, {'id': 14867, 'synset': 'ethiopian.n.01', 'name': 'Ethiopian'}, {'id': 14868, 'synset': 'amhara.n.01', 'name': 'Amhara'}, {'id': 14869, 'synset': 'eritrean.n.01', 'name': 'Eritrean'}, {'id': 14870, 'synset': 'finn.n.01', 'name': 'Finn'}, {'id': 14871, 'synset': 'komi.n.01', 'name': 'Komi'}, {'id': 14872, 'synset': 'livonian.n.01', 'name': 'Livonian'}, {'id': 14873, 'synset': 'lithuanian.n.01', 'name': 'Lithuanian'}, {'id': 14874, 'synset': 'selkup.n.01', 'name': 'Selkup'}, {'id': 14875, 'synset': 'parisian.n.01', 'name': 'Parisian'}, {'id': 14876, 'synset': 'parisienne.n.01', 'name': 'Parisienne'}, {'id': 14877, 'synset': 'creole.n.02', 'name': 'Creole'}, {'id': 14878, 'synset': 'creole.n.01', 'name': 'Creole'}, {'id': 14879, 'synset': 'gabonese.n.01', 'name': 'Gabonese'}, {'id': 14880, 'synset': 'greek.n.02', 'name': 'Greek'}, {'id': 14881, 'synset': 'dorian.n.01', 'name': 'Dorian'}, {'id': 14882, 'synset': 'athenian.n.01', 'name': 'Athenian'}, {'id': 14883, 'synset': 'laconian.n.01', 'name': 'Laconian'}, {'id': 14884, 'synset': 'guyanese.n.01', 'name': 'Guyanese'}, {'id': 14885, 'synset': 'haitian.n.01', 'name': 'Haitian'}, {'id': 14886, 'synset': 'malay.n.01', 'name': 'Malay'}, {'id': 14887, 'synset': 'moro.n.01', 'name': 'Moro'}, {'id': 14888, 'synset': 'netherlander.n.01', 'name': 'Netherlander'}, {'id': 14889, 'synset': 'icelander.n.01', 'name': 'Icelander'}, {'id': 14890, 'synset': 'iraqi.n.01', 'name': 'Iraqi'}, {'id': 14891, 'synset': 'irishman.n.01', 'name': 'Irishman'}, {'id': 14892, 'synset': 'irishwoman.n.01', 'name': 'Irishwoman'}, {'id': 14893, 'synset': 'dubliner.n.01', 'name': 'Dubliner'}, {'id': 14894, 'synset': 'italian.n.01', 'name': 'Italian'}, {'id': 14895, 'synset': 'roman.n.01', 'name': 'Roman'}, {'id': 14896, 'synset': 'sabine.n.02', 'name': 'Sabine'}, {'id': 14897, 'synset': 'japanese.n.01', 'name': 'Japanese'}, {'id': 14898, 'synset': 'jordanian.n.01', 'name': 'Jordanian'}, {'id': 14899, 'synset': 'korean.n.01', 'name': 'Korean'}, {'id': 14900, 'synset': 'kenyan.n.01', 'name': 'Kenyan'}, {'id': 14901, 'synset': 'lao.n.01', 'name': 'Lao'}, {'id': 14902, 'synset': 'lapp.n.01', 'name': 'Lapp'}, {'id': 14903, 'synset': 'latin_american.n.01', 'name': 'Latin_American'}, {'id': 14904, 'synset': 'lebanese.n.01', 'name': 'Lebanese'}, {'id': 14905, 'synset': 'levantine.n.01', 'name': 'Levantine'}, {'id': 14906, 'synset': 'liberian.n.01', 'name': 'Liberian'}, {'id': 14907, 'synset': 'luxemburger.n.01', 'name': 'Luxemburger'}, {'id': 14908, 'synset': 'macedonian.n.01', 'name': 'Macedonian'}, {'id': 14909, 'synset': 'sabahan.n.01', 'name': 'Sabahan'}, {'id': 14910, 'synset': 'mexican.n.01', 'name': 'Mexican'}, {'id': 14911, 'synset': 'chicano.n.01', 'name': 'Chicano'}, {'id': 14912, 'synset': 'mexican-american.n.01', 'name': 'Mexican-American'}, {'id': 14913, 'synset': 'namibian.n.01', 'name': 'Namibian'}, {'id': 14914, 'synset': 'nauruan.n.01', 'name': 'Nauruan'}, {'id': 14915, 'synset': 'gurkha.n.02', 'name': 'Gurkha'}, {'id': 14916, 'synset': 'new_zealander.n.01', 'name': 'New_Zealander'}, {'id': 14917, 'synset': 'nicaraguan.n.01', 'name': 'Nicaraguan'}, {'id': 14918, 'synset': 'nigerian.n.01', 'name': 'Nigerian'}, {'id': 14919, 'synset': 'hausa.n.01', 'name': 'Hausa'}, {'id': 14920, 'synset': 'north_american.n.01', 'name': 'North_American'}, {'id': 14921, 'synset': 'nova_scotian.n.01', 'name': 'Nova_Scotian'}, {'id': 14922, 'synset': 'omani.n.01', 'name': 'Omani'}, {'id': 14923, 'synset': 'pakistani.n.01', 'name': 'Pakistani'}, {'id': 14924, 'synset': 'brahui.n.01', 'name': 'Brahui'}, {'id': 14925, 'synset': 'south_american_indian.n.01', 'name': 'South_American_Indian'}, {'id': 14926, 'synset': 'carib.n.01', 'name': 'Carib'}, {'id': 14927, 'synset': 'filipino.n.01', 'name': 'Filipino'}, {'id': 14928, 'synset': 'polynesian.n.01', 'name': 'Polynesian'}, {'id': 14929, 'synset': 'qatari.n.01', 'name': 'Qatari'}, {'id': 14930, 'synset': 'romanian.n.01', 'name': 'Romanian'}, {'id': 14931, 'synset': 'muscovite.n.02', 'name': 'Muscovite'}, {'id': 14932, 'synset': 'georgian.n.02', 'name': 'Georgian'}, {'id': 14933, 'synset': 'sarawakian.n.01', 'name': 'Sarawakian'}, {'id': 14934, 'synset': 'scandinavian.n.01', 'name': 'Scandinavian'}, {'id': 14935, 'synset': 'senegalese.n.01', 'name': 'Senegalese'}, {'id': 14936, 'synset': 'slovene.n.01', 'name': 'Slovene'}, {'id': 14937, 'synset': 'south_african.n.01', 'name': 'South_African'}, {'id': 14938, 'synset': 'south_american.n.01', 'name': 'South_American'}, {'id': 14939, 'synset': 'sudanese.n.01', 'name': 'Sudanese'}, {'id': 14940, 'synset': 'syrian.n.01', 'name': 'Syrian'}, {'id': 14941, 'synset': 'tahitian.n.01', 'name': 'Tahitian'}, {'id': 14942, 'synset': 'tanzanian.n.01', 'name': 'Tanzanian'}, {'id': 14943, 'synset': 'tibetan.n.02', 'name': 'Tibetan'}, {'id': 14944, 'synset': 'togolese.n.01', 'name': 'Togolese'}, {'id': 14945, 'synset': 'tuareg.n.01', 'name': 'Tuareg'}, {'id': 14946, 'synset': 'turki.n.01', 'name': 'Turki'}, {'id': 14947, 'synset': 'chuvash.n.01', 'name': 'Chuvash'}, {'id': 14948, 'synset': 'turkoman.n.01', 'name': 'Turkoman'}, {'id': 14949, 'synset': 'uzbek.n.01', 'name': 'Uzbek'}, {'id': 14950, 'synset': 'ugandan.n.01', 'name': 'Ugandan'}, {'id': 14951, 'synset': 'ukranian.n.01', 'name': 'Ukranian'}, {'id': 14952, 'synset': 'yakut.n.01', 'name': 'Yakut'}, {'id': 14953, 'synset': 'tungus.n.01', 'name': 'Tungus'}, {'id': 14954, 'synset': 'igbo.n.01', 'name': 'Igbo'}, {'id': 14955, 'synset': 'american.n.03', 'name': 'American'}, {'id': 14956, 'synset': 'anglo-american.n.01', 'name': 'Anglo-American'}, {'id': 14957, 'synset': 'alaska_native.n.01', 'name': 'Alaska_Native'}, {'id': 14958, 'synset': 'arkansan.n.01', 'name': 'Arkansan'}, {'id': 14959, 'synset': 'carolinian.n.01', 'name': 'Carolinian'}, {'id': 14960, 'synset': 'coloradan.n.01', 'name': 'Coloradan'}, {'id': 14961, 'synset': 'connecticuter.n.01', 'name': 'Connecticuter'}, {'id': 14962, 'synset': 'delawarean.n.01', 'name': 'Delawarean'}, {'id': 14963, 'synset': 'floridian.n.01', 'name': 'Floridian'}, {'id': 14964, 'synset': 'german_american.n.01', 'name': 'German_American'}, {'id': 14965, 'synset': 'illinoisan.n.01', 'name': 'Illinoisan'}, {'id': 14966, 'synset': 'mainer.n.01', 'name': 'Mainer'}, {'id': 14967, 'synset': 'marylander.n.01', 'name': 'Marylander'}, {'id': 14968, 'synset': 'minnesotan.n.01', 'name': 'Minnesotan'}, {'id': 14969, 'synset': 'nebraskan.n.01', 'name': 'Nebraskan'}, {'id': 14970, 'synset': 'new_hampshirite.n.01', 'name': 'New_Hampshirite'}, {'id': 14971, 'synset': 'new_jerseyan.n.01', 'name': 'New_Jerseyan'}, {'id': 14972, 'synset': 'new_yorker.n.01', 'name': 'New_Yorker'}, {'id': 14973, 'synset': 'north_carolinian.n.01', 'name': 'North_Carolinian'}, {'id': 14974, 'synset': 'oregonian.n.01', 'name': 'Oregonian'}, {'id': 14975, 'synset': 'pennsylvanian.n.02', 'name': 'Pennsylvanian'}, {'id': 14976, 'synset': 'texan.n.01', 'name': 'Texan'}, {'id': 14977, 'synset': 'utahan.n.01', 'name': 'Utahan'}, {'id': 14978, 'synset': 'uruguayan.n.01', 'name': 'Uruguayan'}, {'id': 14979, 'synset': 'vietnamese.n.01', 'name': 'Vietnamese'}, {'id': 14980, 'synset': 'gambian.n.01', 'name': 'Gambian'}, {'id': 14981, 'synset': 'east_german.n.01', 'name': 'East_German'}, {'id': 14982, 'synset': 'berliner.n.01', 'name': 'Berliner'}, {'id': 14983, 'synset': 'prussian.n.01', 'name': 'Prussian'}, {'id': 14984, 'synset': 'ghanian.n.01', 'name': 'Ghanian'}, {'id': 14985, 'synset': 'guinean.n.01', 'name': 'Guinean'}, {'id': 14986, 'synset': 'papuan.n.01', 'name': 'Papuan'}, {'id': 14987, 'synset': 'walloon.n.01', 'name': 'Walloon'}, {'id': 14988, 'synset': 'yemeni.n.01', 'name': 'Yemeni'}, {'id': 14989, 'synset': 'yugoslav.n.01', 'name': 'Yugoslav'}, {'id': 14990, 'synset': 'serbian.n.01', 'name': 'Serbian'}, {'id': 14991, 'synset': 'xhosa.n.01', 'name': 'Xhosa'}, {'id': 14992, 'synset': 'zairese.n.01', 'name': 'Zairese'}, {'id': 14993, 'synset': 'zimbabwean.n.01', 'name': 'Zimbabwean'}, {'id': 14994, 'synset': 'zulu.n.01', 'name': 'Zulu'}, {'id': 14995, 'synset': 'gemini.n.01', 'name': 'Gemini'}, {'id': 14996, 'synset': 'sagittarius.n.01', 'name': 'Sagittarius'}, {'id': 14997, 'synset': 'pisces.n.02', 'name': 'Pisces'}, {'id': 14998, 'synset': 'abbe.n.01', 'name': 'abbe'}, {'id': 14999, 'synset': 'abbess.n.01', 'name': 'abbess'}, {'id': 15000, 'synset': 'abnegator.n.01', 'name': 'abnegator'}, {'id': 15001, 'synset': 'abridger.n.01', 'name': 'abridger'}, {'id': 15002, 'synset': 'abstractor.n.01', 'name': 'abstractor'}, {'id': 15003, 'synset': 'absconder.n.01', 'name': 'absconder'}, {'id': 15004, 'synset': 'absolver.n.01', 'name': 'absolver'}, {'id': 15005, 'synset': 'abecedarian.n.01', 'name': 'abecedarian'}, {'id': 15006, 'synset': 'aberrant.n.01', 'name': 'aberrant'}, {'id': 15007, 'synset': 'abettor.n.01', 'name': 'abettor'}, {'id': 15008, 'synset': 'abhorrer.n.01', 'name': 'abhorrer'}, {'id': 15009, 'synset': 'abomination.n.01', 'name': 'abomination'}, {'id': 15010, 'synset': 'abseiler.n.01', 'name': 'abseiler'}, {'id': 15011, 'synset': 'abstainer.n.01', 'name': 'abstainer'}, {'id': 15012, 'synset': 'academic_administrator.n.01', 'name': 'academic_administrator'}, {'id': 15013, 'synset': 'academician.n.01', 'name': 'academician'}, {'id': 15014, 'synset': 'accessory_before_the_fact.n.01', 'name': 'accessory_before_the_fact'}, {'id': 15015, 'synset': 'companion.n.03', 'name': 'companion'}, {'id': 15016, 'synset': 'accompanist.n.01', 'name': 'accompanist'}, {'id': 15017, 'synset': 'accomplice.n.01', 'name': 'accomplice'}, {'id': 15018, 'synset': 'account_executive.n.01', 'name': 'account_executive'}, {'id': 15019, 'synset': 'accused.n.01', 'name': 'accused'}, {'id': 15020, 'synset': 'accuser.n.01', 'name': 'accuser'}, {'id': 15021, 'synset': 'acid_head.n.01', 'name': 'acid_head'}, {'id': 15022, 'synset': 'acquaintance.n.03', 'name': 'acquaintance'}, {'id': 15023, 'synset': 'acquirer.n.01', 'name': 'acquirer'}, {'id': 15024, 'synset': 'aerialist.n.01', 'name': 'aerialist'}, {'id': 15025, 'synset': 'action_officer.n.01', 'name': 'action_officer'}, {'id': 15026, 'synset': 'active.n.03', 'name': 'active'}, {'id': 15027, 'synset': 'active_citizen.n.01', 'name': 'active_citizen'}, {'id': 15028, 'synset': 'actor.n.01', 'name': 'actor'}, {'id': 15029, 'synset': 'actor.n.02', 'name': 'actor'}, {'id': 15030, 'synset': 'addict.n.01', 'name': 'addict'}, {'id': 15031, 'synset': 'adducer.n.01', 'name': 'adducer'}, {'id': 15032, 'synset': 'adjuster.n.01', 'name': 'adjuster'}, {'id': 15033, 'synset': 'adjutant.n.01', 'name': 'adjutant'}, {'id': 15034, 'synset': 'adjutant_general.n.01', 'name': 'adjutant_general'}, {'id': 15035, 'synset': 'admirer.n.03', 'name': 'admirer'}, {'id': 15036, 'synset': 'adoptee.n.01', 'name': 'adoptee'}, {'id': 15037, 'synset': 'adulterer.n.01', 'name': 'adulterer'}, {'id': 15038, 'synset': 'adulteress.n.01', 'name': 'adulteress'}, {'id': 15039, 'synset': 'advertiser.n.01', 'name': 'advertiser'}, {'id': 15040, 'synset': 'advisee.n.01', 'name': 'advisee'}, {'id': 15041, 'synset': 'advocate.n.01', 'name': 'advocate'}, {'id': 15042, 'synset': 'aeronautical_engineer.n.01', 'name': 'aeronautical_engineer'}, {'id': 15043, 'synset': 'affiliate.n.01', 'name': 'affiliate'}, {'id': 15044, 'synset': 'affluent.n.01', 'name': 'affluent'}, {'id': 15045, 'synset': 'aficionado.n.02', 'name': 'aficionado'}, {'id': 15046, 'synset': 'buck_sergeant.n.01', 'name': 'buck_sergeant'}, {'id': 15047, 'synset': 'agent-in-place.n.01', 'name': 'agent-in-place'}, {'id': 15048, 'synset': 'aggravator.n.01', 'name': 'aggravator'}, {'id': 15049, 'synset': 'agitator.n.01', 'name': 'agitator'}, {'id': 15050, 'synset': 'agnostic.n.02', 'name': 'agnostic'}, {'id': 15051, 'synset': 'agnostic.n.01', 'name': 'agnostic'}, {'id': 15052, 'synset': 'agonist.n.02', 'name': 'agonist'}, {'id': 15053, 'synset': 'agony_aunt.n.01', 'name': 'agony_aunt'}, {'id': 15054, 'synset': 'agriculturist.n.01', 'name': 'agriculturist'}, {'id': 15055, 'synset': 'air_attache.n.01', 'name': 'air_attache'}, {'id': 15056, 'synset': 'air_force_officer.n.01', 'name': 'air_force_officer'}, {'id': 15057, 'synset': 'airhead.n.01', 'name': 'airhead'}, {'id': 15058, 'synset': 'air_traveler.n.01', 'name': 'air_traveler'}, {'id': 15059, 'synset': 'alarmist.n.01', 'name': 'alarmist'}, {'id': 15060, 'synset': 'albino.n.01', 'name': 'albino'}, {'id': 15061, 'synset': 'alcoholic.n.01', 'name': 'alcoholic'}, {'id': 15062, 'synset': 'alderman.n.01', 'name': 'alderman'}, {'id': 15063, 'synset': 'alexic.n.01', 'name': 'alexic'}, {'id': 15064, 'synset': 'alienee.n.01', 'name': 'alienee'}, {'id': 15065, 'synset': 'alienor.n.01', 'name': 'alienor'}, {'id': 15066, 'synset': 'aliterate.n.01', 'name': 'aliterate'}, {'id': 15067, 'synset': 'algebraist.n.01', 'name': 'algebraist'}, {'id': 15068, 'synset': 'allegorizer.n.01', 'name': 'allegorizer'}, {'id': 15069, 'synset': 'alliterator.n.01', 'name': 'alliterator'}, {'id': 15070, 'synset': 'almoner.n.01', 'name': 'almoner'}, {'id': 15071, 'synset': 'alpinist.n.01', 'name': 'alpinist'}, {'id': 15072, 'synset': 'altar_boy.n.01', 'name': 'altar_boy'}, {'id': 15073, 'synset': 'alto.n.01', 'name': 'alto'}, {'id': 15074, 'synset': 'ambassador.n.01', 'name': 'ambassador'}, {'id': 15075, 'synset': 'ambassador.n.02', 'name': 'ambassador'}, {'id': 15076, 'synset': 'ambusher.n.01', 'name': 'ambusher'}, {'id': 15077, 'synset': 'amicus_curiae.n.01', 'name': 'amicus_curiae'}, {'id': 15078, 'synset': 'amoralist.n.01', 'name': 'amoralist'}, {'id': 15079, 'synset': 'amputee.n.01', 'name': 'amputee'}, {'id': 15080, 'synset': 'analogist.n.01', 'name': 'analogist'}, {'id': 15081, 'synset': 'analphabet.n.01', 'name': 'analphabet'}, {'id': 15082, 'synset': 'analyst.n.01', 'name': 'analyst'}, {'id': 15083, 'synset': 'industry_analyst.n.01', 'name': 'industry_analyst'}, {'id': 15084, 'synset': 'market_strategist.n.01', 'name': 'market_strategist'}, {'id': 15085, 'synset': 'anarchist.n.01', 'name': 'anarchist'}, {'id': 15086, 'synset': 'anathema.n.01', 'name': 'anathema'}, {'id': 15087, 'synset': 'ancestor.n.01', 'name': 'ancestor'}, {'id': 15088, 'synset': 'anchor.n.03', 'name': 'anchor'}, {'id': 15089, 'synset': 'ancient.n.02', 'name': 'ancient'}, {'id': 15090, 'synset': 'anecdotist.n.01', 'name': 'anecdotist'}, {'id': 15091, 'synset': 'angler.n.02', 'name': 'angler'}, {'id': 15092, 'synset': 'animator.n.02', 'name': 'animator'}, {'id': 15093, 'synset': 'animist.n.01', 'name': 'animist'}, {'id': 15094, 'synset': 'annotator.n.01', 'name': 'annotator'}, {'id': 15095, 'synset': 'announcer.n.02', 'name': 'announcer'}, {'id': 15096, 'synset': 'announcer.n.01', 'name': 'announcer'}, {'id': 15097, 'synset': 'anti.n.01', 'name': 'anti'}, {'id': 15098, 'synset': 'anti-american.n.01', 'name': 'anti-American'}, {'id': 15099, 'synset': 'anti-semite.n.01', 'name': 'anti-Semite'}, {'id': 15100, 'synset': 'anzac.n.01', 'name': 'Anzac'}, {'id': 15101, 'synset': 'ape-man.n.02', 'name': 'ape-man'}, {'id': 15102, 'synset': 'aphakic.n.01', 'name': 'aphakic'}, {'id': 15103, 'synset': 'appellant.n.01', 'name': 'appellant'}, {'id': 15104, 'synset': 'appointee.n.01', 'name': 'appointee'}, {'id': 15105, 'synset': 'apprehender.n.02', 'name': 'apprehender'}, {'id': 15106, 'synset': 'april_fool.n.01', 'name': 'April_fool'}, {'id': 15107, 'synset': 'aspirant.n.01', 'name': 'aspirant'}, {'id': 15108, 'synset': 'appreciator.n.01', 'name': 'appreciator'}, {'id': 15109, 'synset': 'appropriator.n.01', 'name': 'appropriator'}, {'id': 15110, 'synset': 'arabist.n.01', 'name': 'Arabist'}, {'id': 15111, 'synset': 'archaist.n.01', 'name': 'archaist'}, {'id': 15112, 'synset': 'archbishop.n.01', 'name': 'archbishop'}, {'id': 15113, 'synset': 'archer.n.01', 'name': 'archer'}, {'id': 15114, 'synset': 'architect.n.01', 'name': 'architect'}, {'id': 15115, 'synset': 'archivist.n.01', 'name': 'archivist'}, {'id': 15116, 'synset': 'archpriest.n.01', 'name': 'archpriest'}, {'id': 15117, 'synset': 'aristotelian.n.01', 'name': 'Aristotelian'}, {'id': 15118, 'synset': 'armiger.n.02', 'name': 'armiger'}, {'id': 15119, 'synset': 'army_attache.n.01', 'name': 'army_attache'}, {'id': 15120, 'synset': 'army_engineer.n.01', 'name': 'army_engineer'}, {'id': 15121, 'synset': 'army_officer.n.01', 'name': 'army_officer'}, {'id': 15122, 'synset': 'arranger.n.02', 'name': 'arranger'}, {'id': 15123, 'synset': 'arrival.n.03', 'name': 'arrival'}, {'id': 15124, 'synset': 'arthritic.n.01', 'name': 'arthritic'}, {'id': 15125, 'synset': 'articulator.n.01', 'name': 'articulator'}, {'id': 15126, 'synset': 'artilleryman.n.01', 'name': 'artilleryman'}, {'id': 15127, 'synset': "artist's_model.n.01", 'name': "artist's_model"}, {'id': 15128, 'synset': 'assayer.n.01', 'name': 'assayer'}, {'id': 15129, 'synset': 'assemblyman.n.01', 'name': 'assemblyman'}, {'id': 15130, 'synset': 'assemblywoman.n.01', 'name': 'assemblywoman'}, {'id': 15131, 'synset': 'assenter.n.01', 'name': 'assenter'}, {'id': 15132, 'synset': 'asserter.n.01', 'name': 'asserter'}, {'id': 15133, 'synset': 'assignee.n.01', 'name': 'assignee'}, {'id': 15134, 'synset': 'assistant.n.01', 'name': 'assistant'}, {'id': 15135, 'synset': 'assistant_professor.n.01', 'name': 'assistant_professor'}, {'id': 15136, 'synset': 'associate.n.01', 'name': 'associate'}, {'id': 15137, 'synset': 'associate.n.03', 'name': 'associate'}, {'id': 15138, 'synset': 'associate_professor.n.01', 'name': 'associate_professor'}, {'id': 15139, 'synset': 'astronaut.n.01', 'name': 'astronaut'}, {'id': 15140, 'synset': 'cosmographer.n.01', 'name': 'cosmographer'}, {'id': 15141, 'synset': 'atheist.n.01', 'name': 'atheist'}, {'id': 15142, 'synset': 'athlete.n.01', 'name': 'athlete'}, {'id': 15143, 'synset': 'attendant.n.01', 'name': 'attendant'}, {'id': 15144, 'synset': 'attorney_general.n.01', 'name': 'attorney_general'}, {'id': 15145, 'synset': 'auditor.n.02', 'name': 'auditor'}, {'id': 15146, 'synset': 'augur.n.01', 'name': 'augur'}, {'id': 15147, 'synset': 'aunt.n.01', 'name': 'aunt'}, {'id': 15148, 'synset': 'au_pair_girl.n.01', 'name': 'au_pair_girl'}, {'id': 15149, 'synset': 'authoritarian.n.01', 'name': 'authoritarian'}, {'id': 15150, 'synset': 'authority.n.02', 'name': 'authority'}, {'id': 15151, 'synset': 'authorizer.n.01', 'name': 'authorizer'}, {'id': 15152, 'synset': 'automobile_mechanic.n.01', 'name': 'automobile_mechanic'}, {'id': 15153, 'synset': 'aviator.n.01', 'name': 'aviator'}, {'id': 15154, 'synset': 'aviatrix.n.01', 'name': 'aviatrix'}, {'id': 15155, 'synset': 'ayah.n.01', 'name': 'ayah'}, {'id': 15156, 'synset': 'babu.n.01', 'name': 'babu'}, {'id': 15157, 'synset': 'baby.n.05', 'name': 'baby'}, {'id': 15158, 'synset': 'baby.n.04', 'name': 'baby'}, {'id': 15159, 'synset': 'baby_boomer.n.01', 'name': 'baby_boomer'}, {'id': 15160, 'synset': 'baby_farmer.n.01', 'name': 'baby_farmer'}, {'id': 15161, 'synset': 'back.n.04', 'name': 'back'}, {'id': 15162, 'synset': 'backbencher.n.01', 'name': 'backbencher'}, {'id': 15163, 'synset': 'backpacker.n.01', 'name': 'backpacker'}, {'id': 15164, 'synset': 'backroom_boy.n.01', 'name': 'backroom_boy'}, {'id': 15165, 'synset': 'backscratcher.n.01', 'name': 'backscratcher'}, {'id': 15166, 'synset': 'bad_person.n.01', 'name': 'bad_person'}, {'id': 15167, 'synset': 'baggage.n.02', 'name': 'baggage'}, {'id': 15168, 'synset': 'bag_lady.n.01', 'name': 'bag_lady'}, {'id': 15169, 'synset': 'bailee.n.01', 'name': 'bailee'}, {'id': 15170, 'synset': 'bailiff.n.01', 'name': 'bailiff'}, {'id': 15171, 'synset': 'bailor.n.01', 'name': 'bailor'}, {'id': 15172, 'synset': 'bairn.n.01', 'name': 'bairn'}, {'id': 15173, 'synset': 'baker.n.02', 'name': 'baker'}, {'id': 15174, 'synset': 'balancer.n.01', 'name': 'balancer'}, {'id': 15175, 'synset': 'balker.n.01', 'name': 'balker'}, {'id': 15176, 'synset': 'ball-buster.n.01', 'name': 'ball-buster'}, {'id': 15177, 'synset': 'ball_carrier.n.01', 'name': 'ball_carrier'}, {'id': 15178, 'synset': 'ballet_dancer.n.01', 'name': 'ballet_dancer'}, {'id': 15179, 'synset': 'ballet_master.n.01', 'name': 'ballet_master'}, {'id': 15180, 'synset': 'ballet_mistress.n.01', 'name': 'ballet_mistress'}, {'id': 15181, 'synset': 'balletomane.n.01', 'name': 'balletomane'}, {'id': 15182, 'synset': 'ball_hawk.n.01', 'name': 'ball_hawk'}, {'id': 15183, 'synset': 'balloonist.n.01', 'name': 'balloonist'}, {'id': 15184, 'synset': 'ballplayer.n.01', 'name': 'ballplayer'}, {'id': 15185, 'synset': 'bullfighter.n.01', 'name': 'bullfighter'}, {'id': 15186, 'synset': 'banderillero.n.01', 'name': 'banderillero'}, {'id': 15187, 'synset': 'matador.n.01', 'name': 'matador'}, {'id': 15188, 'synset': 'picador.n.01', 'name': 'picador'}, {'id': 15189, 'synset': 'bandsman.n.01', 'name': 'bandsman'}, {'id': 15190, 'synset': 'banker.n.02', 'name': 'banker'}, {'id': 15191, 'synset': 'bank_robber.n.01', 'name': 'bank_robber'}, {'id': 15192, 'synset': 'bankrupt.n.01', 'name': 'bankrupt'}, {'id': 15193, 'synset': 'bantamweight.n.01', 'name': 'bantamweight'}, {'id': 15194, 'synset': 'barmaid.n.01', 'name': 'barmaid'}, {'id': 15195, 'synset': 'baron.n.03', 'name': 'baron'}, {'id': 15196, 'synset': 'baron.n.02', 'name': 'baron'}, {'id': 15197, 'synset': 'baron.n.01', 'name': 'baron'}, {'id': 15198, 'synset': 'bartender.n.01', 'name': 'bartender'}, {'id': 15199, 'synset': 'baseball_coach.n.01', 'name': 'baseball_coach'}, {'id': 15200, 'synset': 'base_runner.n.01', 'name': 'base_runner'}, {'id': 15201, 'synset': 'basketball_player.n.01', 'name': 'basketball_player'}, {'id': 15202, 'synset': 'basketweaver.n.01', 'name': 'basketweaver'}, {'id': 15203, 'synset': 'basket_maker.n.01', 'name': 'Basket_Maker'}, {'id': 15204, 'synset': 'bass.n.03', 'name': 'bass'}, {'id': 15205, 'synset': 'bastard.n.02', 'name': 'bastard'}, {'id': 15206, 'synset': 'bat_boy.n.01', 'name': 'bat_boy'}, {'id': 15207, 'synset': 'bather.n.02', 'name': 'bather'}, {'id': 15208, 'synset': 'batman.n.01', 'name': 'batman'}, {'id': 15209, 'synset': 'baton_twirler.n.01', 'name': 'baton_twirler'}, {'id': 15210, 'synset': 'bavarian.n.01', 'name': 'Bavarian'}, {'id': 15211, 'synset': 'beadsman.n.01', 'name': 'beadsman'}, {'id': 15212, 'synset': 'beard.n.03', 'name': 'beard'}, {'id': 15213, 'synset': 'beatnik.n.01', 'name': 'beatnik'}, {'id': 15214, 'synset': 'beauty_consultant.n.01', 'name': 'beauty_consultant'}, {'id': 15215, 'synset': 'bedouin.n.01', 'name': 'Bedouin'}, {'id': 15216, 'synset': 'bedwetter.n.01', 'name': 'bedwetter'}, {'id': 15217, 'synset': 'beekeeper.n.01', 'name': 'beekeeper'}, {'id': 15218, 'synset': 'beer_drinker.n.01', 'name': 'beer_drinker'}, {'id': 15219, 'synset': 'beggarman.n.01', 'name': 'beggarman'}, {'id': 15220, 'synset': 'beggarwoman.n.01', 'name': 'beggarwoman'}, {'id': 15221, 'synset': 'beldam.n.02', 'name': 'beldam'}, {'id': 15222, 'synset': 'theist.n.01', 'name': 'theist'}, {'id': 15223, 'synset': 'believer.n.01', 'name': 'believer'}, {'id': 15224, 'synset': 'bell_founder.n.01', 'name': 'bell_founder'}, {'id': 15225, 'synset': 'benedick.n.01', 'name': 'benedick'}, {'id': 15226, 'synset': 'berserker.n.01', 'name': 'berserker'}, {'id': 15227, 'synset': 'besieger.n.01', 'name': 'besieger'}, {'id': 15228, 'synset': 'best.n.02', 'name': 'best'}, {'id': 15229, 'synset': 'betrothed.n.01', 'name': 'betrothed'}, {'id': 15230, 'synset': 'big_brother.n.01', 'name': 'Big_Brother'}, {'id': 15231, 'synset': 'bigot.n.01', 'name': 'bigot'}, {'id': 15232, 'synset': 'big_shot.n.01', 'name': 'big_shot'}, {'id': 15233, 'synset': 'big_sister.n.01', 'name': 'big_sister'}, {'id': 15234, 'synset': 'billiard_player.n.01', 'name': 'billiard_player'}, {'id': 15235, 'synset': 'biochemist.n.01', 'name': 'biochemist'}, {'id': 15236, 'synset': 'biographer.n.01', 'name': 'biographer'}, {'id': 15237, 'synset': 'bird_fancier.n.01', 'name': 'bird_fancier'}, {'id': 15238, 'synset': 'birth.n.05', 'name': 'birth'}, {'id': 15239, 'synset': 'birth-control_campaigner.n.01', 'name': 'birth-control_campaigner'}, {'id': 15240, 'synset': 'bisexual.n.01', 'name': 'bisexual'}, {'id': 15241, 'synset': 'black_belt.n.01', 'name': 'black_belt'}, {'id': 15242, 'synset': 'blackmailer.n.01', 'name': 'blackmailer'}, {'id': 15243, 'synset': 'black_muslim.n.01', 'name': 'Black_Muslim'}, {'id': 15244, 'synset': 'blacksmith.n.01', 'name': 'blacksmith'}, {'id': 15245, 'synset': 'blade.n.02', 'name': 'blade'}, {'id': 15246, 'synset': 'blind_date.n.01', 'name': 'blind_date'}, {'id': 15247, 'synset': 'bluecoat.n.01', 'name': 'bluecoat'}, {'id': 15248, 'synset': 'bluestocking.n.01', 'name': 'bluestocking'}, {'id': 15249, 'synset': 'boatbuilder.n.01', 'name': 'boatbuilder'}, {'id': 15250, 'synset': 'boatman.n.01', 'name': 'boatman'}, {'id': 15251, 'synset': 'boatswain.n.01', 'name': 'boatswain'}, {'id': 15252, 'synset': 'bobby.n.01', 'name': 'bobby'}, {'id': 15253, 'synset': 'bodyguard.n.01', 'name': 'bodyguard'}, {'id': 15254, 'synset': 'boffin.n.01', 'name': 'boffin'}, {'id': 15255, 'synset': 'bolshevik.n.01', 'name': 'Bolshevik'}, {'id': 15256, 'synset': 'bolshevik.n.02', 'name': 'Bolshevik'}, {'id': 15257, 'synset': 'bombshell.n.01', 'name': 'bombshell'}, {'id': 15258, 'synset': 'bondman.n.01', 'name': 'bondman'}, {'id': 15259, 'synset': 'bondwoman.n.02', 'name': 'bondwoman'}, {'id': 15260, 'synset': 'bondwoman.n.01', 'name': 'bondwoman'}, {'id': 15261, 'synset': 'bond_servant.n.01', 'name': 'bond_servant'}, {'id': 15262, 'synset': 'book_agent.n.01', 'name': 'book_agent'}, {'id': 15263, 'synset': 'bookbinder.n.01', 'name': 'bookbinder'}, {'id': 15264, 'synset': 'bookkeeper.n.01', 'name': 'bookkeeper'}, {'id': 15265, 'synset': 'bookmaker.n.01', 'name': 'bookmaker'}, {'id': 15266, 'synset': 'bookworm.n.02', 'name': 'bookworm'}, {'id': 15267, 'synset': 'booster.n.03', 'name': 'booster'}, {'id': 15268, 'synset': 'bootblack.n.01', 'name': 'bootblack'}, {'id': 15269, 'synset': 'bootlegger.n.01', 'name': 'bootlegger'}, {'id': 15270, 'synset': 'bootmaker.n.01', 'name': 'bootmaker'}, {'id': 15271, 'synset': 'borderer.n.01', 'name': 'borderer'}, {'id': 15272, 'synset': 'border_patrolman.n.01', 'name': 'border_patrolman'}, {'id': 15273, 'synset': 'botanist.n.01', 'name': 'botanist'}, {'id': 15274, 'synset': 'bottom_feeder.n.01', 'name': 'bottom_feeder'}, {'id': 15275, 'synset': 'boulevardier.n.01', 'name': 'boulevardier'}, {'id': 15276, 'synset': 'bounty_hunter.n.02', 'name': 'bounty_hunter'}, {'id': 15277, 'synset': 'bounty_hunter.n.01', 'name': 'bounty_hunter'}, {'id': 15278, 'synset': 'bourbon.n.03', 'name': 'Bourbon'}, {'id': 15279, 'synset': 'bowler.n.01', 'name': 'bowler'}, {'id': 15280, 'synset': 'slugger.n.02', 'name': 'slugger'}, {'id': 15281, 'synset': 'cub.n.02', 'name': 'cub'}, {'id': 15282, 'synset': 'boy_scout.n.01', 'name': 'Boy_Scout'}, {'id': 15283, 'synset': 'boy_scout.n.02', 'name': 'boy_scout'}, {'id': 15284, 'synset': 'boy_wonder.n.01', 'name': 'boy_wonder'}, {'id': 15285, 'synset': 'bragger.n.01', 'name': 'bragger'}, {'id': 15286, 'synset': 'brahman.n.02', 'name': 'brahman'}, {'id': 15287, 'synset': 'brawler.n.01', 'name': 'brawler'}, {'id': 15288, 'synset': 'breadwinner.n.01', 'name': 'breadwinner'}, {'id': 15289, 'synset': 'breaststroker.n.01', 'name': 'breaststroker'}, {'id': 15290, 'synset': 'breeder.n.01', 'name': 'breeder'}, {'id': 15291, 'synset': 'brick.n.02', 'name': 'brick'}, {'id': 15292, 'synset': 'bride.n.03', 'name': 'bride'}, {'id': 15293, 'synset': 'bridesmaid.n.01', 'name': 'bridesmaid'}, {'id': 15294, 'synset': 'bridge_agent.n.01', 'name': 'bridge_agent'}, {'id': 15295, 'synset': 'broadcast_journalist.n.01', 'name': 'broadcast_journalist'}, {'id': 15296, 'synset': 'brother.n.05', 'name': 'Brother'}, {'id': 15297, 'synset': 'brother-in-law.n.01', 'name': 'brother-in-law'}, {'id': 15298, 'synset': 'browser.n.01', 'name': 'browser'}, {'id': 15299, 'synset': 'brummie.n.01', 'name': 'Brummie'}, {'id': 15300, 'synset': 'buddy.n.01', 'name': 'buddy'}, {'id': 15301, 'synset': 'bull.n.06', 'name': 'bull'}, {'id': 15302, 'synset': 'bully.n.02', 'name': 'bully'}, {'id': 15303, 'synset': 'bunny.n.01', 'name': 'bunny'}, {'id': 15304, 'synset': 'burglar.n.01', 'name': 'burglar'}, {'id': 15305, 'synset': 'bursar.n.01', 'name': 'bursar'}, {'id': 15306, 'synset': 'busboy.n.01', 'name': 'busboy'}, {'id': 15307, 'synset': 'business_editor.n.01', 'name': 'business_editor'}, {'id': 15308, 'synset': 'business_traveler.n.01', 'name': 'business_traveler'}, {'id': 15309, 'synset': 'buster.n.04', 'name': 'buster'}, {'id': 15310, 'synset': 'busybody.n.01', 'name': 'busybody'}, {'id': 15311, 'synset': 'buttinsky.n.01', 'name': 'buttinsky'}, {'id': 15312, 'synset': 'cabinetmaker.n.01', 'name': 'cabinetmaker'}, {'id': 15313, 'synset': 'caddie.n.01', 'name': 'caddie'}, {'id': 15314, 'synset': 'cadet.n.01', 'name': 'cadet'}, {'id': 15315, 'synset': 'caller.n.04', 'name': 'caller'}, {'id': 15316, 'synset': 'call_girl.n.01', 'name': 'call_girl'}, {'id': 15317, 'synset': 'calligrapher.n.01', 'name': 'calligrapher'}, {'id': 15318, 'synset': 'campaigner.n.01', 'name': 'campaigner'}, {'id': 15319, 'synset': 'camper.n.01', 'name': 'camper'}, {'id': 15320, 'synset': 'camp_follower.n.02', 'name': 'camp_follower'}, {'id': 15321, 'synset': 'candidate.n.02', 'name': 'candidate'}, {'id': 15322, 'synset': 'canonist.n.01', 'name': 'canonist'}, {'id': 15323, 'synset': 'capitalist.n.01', 'name': 'capitalist'}, {'id': 15324, 'synset': 'captain.n.07', 'name': 'captain'}, {'id': 15325, 'synset': 'captain.n.06', 'name': 'captain'}, {'id': 15326, 'synset': 'captain.n.01', 'name': 'captain'}, {'id': 15327, 'synset': 'captain.n.05', 'name': 'captain'}, {'id': 15328, 'synset': 'captive.n.02', 'name': 'captive'}, {'id': 15329, 'synset': 'captive.n.03', 'name': 'captive'}, {'id': 15330, 'synset': 'cardinal.n.01', 'name': 'cardinal'}, {'id': 15331, 'synset': 'cardiologist.n.01', 'name': 'cardiologist'}, {'id': 15332, 'synset': 'card_player.n.01', 'name': 'card_player'}, {'id': 15333, 'synset': 'cardsharp.n.01', 'name': 'cardsharp'}, {'id': 15334, 'synset': 'careerist.n.01', 'name': 'careerist'}, {'id': 15335, 'synset': 'career_man.n.01', 'name': 'career_man'}, {'id': 15336, 'synset': 'caregiver.n.02', 'name': 'caregiver'}, {'id': 15337, 'synset': 'caretaker.n.01', 'name': 'caretaker'}, {'id': 15338, 'synset': 'caretaker.n.02', 'name': 'caretaker'}, {'id': 15339, 'synset': 'caricaturist.n.01', 'name': 'caricaturist'}, {'id': 15340, 'synset': 'carillonneur.n.01', 'name': 'carillonneur'}, {'id': 15341, 'synset': 'caroler.n.01', 'name': 'caroler'}, {'id': 15342, 'synset': 'carpenter.n.01', 'name': 'carpenter'}, {'id': 15343, 'synset': 'carper.n.01', 'name': 'carper'}, {'id': 15344, 'synset': 'cartesian.n.01', 'name': 'Cartesian'}, {'id': 15345, 'synset': 'cashier.n.02', 'name': 'cashier'}, {'id': 15346, 'synset': 'casualty.n.02', 'name': 'casualty'}, {'id': 15347, 'synset': 'casualty.n.01', 'name': 'casualty'}, {'id': 15348, 'synset': 'casuist.n.01', 'name': 'casuist'}, {'id': 15349, 'synset': 'catechist.n.01', 'name': 'catechist'}, {'id': 15350, 'synset': 'catechumen.n.01', 'name': 'catechumen'}, {'id': 15351, 'synset': 'caterer.n.01', 'name': 'caterer'}, {'id': 15352, 'synset': 'catholicos.n.01', 'name': 'Catholicos'}, {'id': 15353, 'synset': 'cat_fancier.n.01', 'name': 'cat_fancier'}, {'id': 15354, 'synset': 'cavalier.n.02', 'name': 'Cavalier'}, {'id': 15355, 'synset': 'cavalryman.n.02', 'name': 'cavalryman'}, {'id': 15356, 'synset': 'caveman.n.01', 'name': 'caveman'}, {'id': 15357, 'synset': 'celebrant.n.02', 'name': 'celebrant'}, {'id': 15358, 'synset': 'celebrant.n.01', 'name': 'celebrant'}, {'id': 15359, 'synset': 'celebrity.n.01', 'name': 'celebrity'}, {'id': 15360, 'synset': 'cellist.n.01', 'name': 'cellist'}, {'id': 15361, 'synset': 'censor.n.02', 'name': 'censor'}, {'id': 15362, 'synset': 'censor.n.01', 'name': 'censor'}, {'id': 15363, 'synset': 'centenarian.n.01', 'name': 'centenarian'}, {'id': 15364, 'synset': 'centrist.n.01', 'name': 'centrist'}, {'id': 15365, 'synset': 'centurion.n.01', 'name': 'centurion'}, {'id': 15366, 'synset': 'certified_public_accountant.n.01', 'name': 'certified_public_accountant'}, {'id': 15367, 'synset': 'chachka.n.01', 'name': 'chachka'}, {'id': 15368, 'synset': 'chambermaid.n.01', 'name': 'chambermaid'}, {'id': 15369, 'synset': 'chameleon.n.01', 'name': 'chameleon'}, {'id': 15370, 'synset': 'champion.n.01', 'name': 'champion'}, {'id': 15371, 'synset': 'chandler.n.02', 'name': 'chandler'}, {'id': 15372, 'synset': 'prison_chaplain.n.01', 'name': 'prison_chaplain'}, {'id': 15373, 'synset': 'charcoal_burner.n.01', 'name': 'charcoal_burner'}, {'id': 15374, 'synset': "charge_d'affaires.n.01", 'name': "charge_d'affaires"}, {'id': 15375, 'synset': 'charioteer.n.01', 'name': 'charioteer'}, {'id': 15376, 'synset': 'charmer.n.02', 'name': 'charmer'}, {'id': 15377, 'synset': 'chartered_accountant.n.01', 'name': 'chartered_accountant'}, {'id': 15378, 'synset': 'chartist.n.02', 'name': 'chartist'}, {'id': 15379, 'synset': 'charwoman.n.01', 'name': 'charwoman'}, {'id': 15380, 'synset': 'male_chauvinist.n.01', 'name': 'male_chauvinist'}, {'id': 15381, 'synset': 'cheapskate.n.01', 'name': 'cheapskate'}, {'id': 15382, 'synset': 'chechen.n.01', 'name': 'Chechen'}, {'id': 15383, 'synset': 'checker.n.02', 'name': 'checker'}, {'id': 15384, 'synset': 'cheerer.n.01', 'name': 'cheerer'}, {'id': 15385, 'synset': 'cheerleader.n.02', 'name': 'cheerleader'}, {'id': 15386, 'synset': 'cheerleader.n.01', 'name': 'cheerleader'}, {'id': 15387, 'synset': 'cheops.n.01', 'name': 'Cheops'}, {'id': 15388, 'synset': 'chess_master.n.01', 'name': 'chess_master'}, {'id': 15389, 'synset': 'chief_executive_officer.n.01', 'name': 'chief_executive_officer'}, {'id': 15390, 'synset': 'chief_of_staff.n.01', 'name': 'chief_of_staff'}, {'id': 15391, 'synset': 'chief_petty_officer.n.01', 'name': 'chief_petty_officer'}, {'id': 15392, 'synset': 'chief_secretary.n.01', 'name': 'Chief_Secretary'}, {'id': 15393, 'synset': 'child.n.01', 'name': 'child'}, {'id': 15394, 'synset': 'child.n.02', 'name': 'child'}, {'id': 15395, 'synset': 'child.n.03', 'name': 'child'}, {'id': 15396, 'synset': 'child_prodigy.n.01', 'name': 'child_prodigy'}, {'id': 15397, 'synset': 'chimneysweeper.n.01', 'name': 'chimneysweeper'}, {'id': 15398, 'synset': 'chiropractor.n.01', 'name': 'chiropractor'}, {'id': 15399, 'synset': 'chit.n.01', 'name': 'chit'}, {'id': 15400, 'synset': 'choker.n.02', 'name': 'choker'}, {'id': 15401, 'synset': 'choragus.n.01', 'name': 'choragus'}, {'id': 15402, 'synset': 'choreographer.n.01', 'name': 'choreographer'}, {'id': 15403, 'synset': 'chorus_girl.n.01', 'name': 'chorus_girl'}, {'id': 15404, 'synset': 'chosen.n.01', 'name': 'chosen'}, {'id': 15405, 'synset': 'cicerone.n.01', 'name': 'cicerone'}, {'id': 15406, 'synset': 'cigar_smoker.n.01', 'name': 'cigar_smoker'}, {'id': 15407, 'synset': 'cipher.n.04', 'name': 'cipher'}, {'id': 15408, 'synset': 'circus_acrobat.n.01', 'name': 'circus_acrobat'}, {'id': 15409, 'synset': 'citizen.n.01', 'name': 'citizen'}, {'id': 15410, 'synset': 'city_editor.n.01', 'name': 'city_editor'}, {'id': 15411, 'synset': 'city_father.n.01', 'name': 'city_father'}, {'id': 15412, 'synset': 'city_man.n.01', 'name': 'city_man'}, {'id': 15413, 'synset': 'city_slicker.n.01', 'name': 'city_slicker'}, {'id': 15414, 'synset': 'civic_leader.n.01', 'name': 'civic_leader'}, {'id': 15415, 'synset': 'civil_rights_leader.n.01', 'name': 'civil_rights_leader'}, {'id': 15416, 'synset': 'cleaner.n.03', 'name': 'cleaner'}, {'id': 15417, 'synset': 'clergyman.n.01', 'name': 'clergyman'}, {'id': 15418, 'synset': 'cleric.n.01', 'name': 'cleric'}, {'id': 15419, 'synset': 'clerk.n.01', 'name': 'clerk'}, {'id': 15420, 'synset': 'clever_dick.n.01', 'name': 'clever_Dick'}, {'id': 15421, 'synset': 'climatologist.n.01', 'name': 'climatologist'}, {'id': 15422, 'synset': 'climber.n.04', 'name': 'climber'}, {'id': 15423, 'synset': 'clinician.n.01', 'name': 'clinician'}, {'id': 15424, 'synset': 'closer.n.02', 'name': 'closer'}, {'id': 15425, 'synset': 'closet_queen.n.01', 'name': 'closet_queen'}, {'id': 15426, 'synset': 'clown.n.02', 'name': 'clown'}, {'id': 15427, 'synset': 'clown.n.01', 'name': 'clown'}, {'id': 15428, 'synset': 'coach.n.02', 'name': 'coach'}, {'id': 15429, 'synset': 'coach.n.01', 'name': 'coach'}, {'id': 15430, 'synset': 'pitching_coach.n.01', 'name': 'pitching_coach'}, {'id': 15431, 'synset': 'coachman.n.01', 'name': 'coachman'}, {'id': 15432, 'synset': 'coal_miner.n.01', 'name': 'coal_miner'}, {'id': 15433, 'synset': 'coastguardsman.n.01', 'name': 'coastguardsman'}, {'id': 15434, 'synset': 'cobber.n.01', 'name': 'cobber'}, {'id': 15435, 'synset': 'cobbler.n.01', 'name': 'cobbler'}, {'id': 15436, 'synset': 'codger.n.01', 'name': 'codger'}, {'id': 15437, 'synset': 'co-beneficiary.n.01', 'name': 'co-beneficiary'}, {'id': 15438, 'synset': 'cog.n.01', 'name': 'cog'}, {'id': 15439, 'synset': 'cognitive_neuroscientist.n.01', 'name': 'cognitive_neuroscientist'}, {'id': 15440, 'synset': 'coiffeur.n.01', 'name': 'coiffeur'}, {'id': 15441, 'synset': 'coiner.n.02', 'name': 'coiner'}, {'id': 15442, 'synset': 'collaborator.n.03', 'name': 'collaborator'}, {'id': 15443, 'synset': 'colleen.n.01', 'name': 'colleen'}, {'id': 15444, 'synset': 'college_student.n.01', 'name': 'college_student'}, {'id': 15445, 'synset': 'collegian.n.01', 'name': 'collegian'}, {'id': 15446, 'synset': 'colonial.n.01', 'name': 'colonial'}, {'id': 15447, 'synset': 'colonialist.n.01', 'name': 'colonialist'}, {'id': 15448, 'synset': 'colonizer.n.01', 'name': 'colonizer'}, {'id': 15449, 'synset': 'coloratura.n.01', 'name': 'coloratura'}, {'id': 15450, 'synset': 'color_guard.n.01', 'name': 'color_guard'}, {'id': 15451, 'synset': 'colossus.n.02', 'name': 'colossus'}, {'id': 15452, 'synset': 'comedian.n.02', 'name': 'comedian'}, {'id': 15453, 'synset': 'comedienne.n.02', 'name': 'comedienne'}, {'id': 15454, 'synset': 'comer.n.01', 'name': 'comer'}, {'id': 15455, 'synset': 'commander.n.03', 'name': 'commander'}, {'id': 15456, 'synset': 'commander_in_chief.n.01', 'name': 'commander_in_chief'}, {'id': 15457, 'synset': 'commanding_officer.n.01', 'name': 'commanding_officer'}, {'id': 15458, 'synset': 'commissar.n.01', 'name': 'commissar'}, {'id': 15459, 'synset': 'commissioned_officer.n.01', 'name': 'commissioned_officer'}, {'id': 15460, 'synset': 'commissioned_military_officer.n.01', 'name': 'commissioned_military_officer'}, {'id': 15461, 'synset': 'commissioner.n.01', 'name': 'commissioner'}, {'id': 15462, 'synset': 'commissioner.n.02', 'name': 'commissioner'}, {'id': 15463, 'synset': 'committee_member.n.01', 'name': 'committee_member'}, {'id': 15464, 'synset': 'committeewoman.n.01', 'name': 'committeewoman'}, {'id': 15465, 'synset': 'commodore.n.01', 'name': 'commodore'}, {'id': 15466, 'synset': 'communicant.n.01', 'name': 'communicant'}, {'id': 15467, 'synset': 'communist.n.02', 'name': 'communist'}, {'id': 15468, 'synset': 'communist.n.01', 'name': 'Communist'}, {'id': 15469, 'synset': 'commuter.n.02', 'name': 'commuter'}, {'id': 15470, 'synset': 'compere.n.01', 'name': 'compere'}, {'id': 15471, 'synset': 'complexifier.n.01', 'name': 'complexifier'}, {'id': 15472, 'synset': 'compulsive.n.01', 'name': 'compulsive'}, {'id': 15473, 'synset': 'computational_linguist.n.01', 'name': 'computational_linguist'}, {'id': 15474, 'synset': 'computer_scientist.n.01', 'name': 'computer_scientist'}, {'id': 15475, 'synset': 'computer_user.n.01', 'name': 'computer_user'}, {'id': 15476, 'synset': 'comrade.n.02', 'name': 'Comrade'}, {'id': 15477, 'synset': 'concert-goer.n.01', 'name': 'concert-goer'}, {'id': 15478, 'synset': 'conciliator.n.01', 'name': 'conciliator'}, {'id': 15479, 'synset': 'conductor.n.03', 'name': 'conductor'}, {'id': 15480, 'synset': 'confectioner.n.01', 'name': 'confectioner'}, {'id': 15481, 'synset': 'confederate.n.01', 'name': 'Confederate'}, {'id': 15482, 'synset': 'confessor.n.01', 'name': 'confessor'}, {'id': 15483, 'synset': 'confidant.n.01', 'name': 'confidant'}, {'id': 15484, 'synset': 'confucian.n.01', 'name': 'Confucian'}, {'id': 15485, 'synset': 'rep.n.01', 'name': 'rep'}, {'id': 15486, 'synset': 'conqueror.n.01', 'name': 'conqueror'}, {'id': 15487, 'synset': 'conservative.n.02', 'name': 'Conservative'}, {'id': 15488, 'synset': 'nonconformist.n.01', 'name': 'Nonconformist'}, {'id': 15489, 'synset': 'anglican.n.01', 'name': 'Anglican'}, {'id': 15490, 'synset': 'consignee.n.01', 'name': 'consignee'}, {'id': 15491, 'synset': 'consigner.n.01', 'name': 'consigner'}, {'id': 15492, 'synset': 'constable.n.01', 'name': 'constable'}, {'id': 15493, 'synset': 'constructivist.n.01', 'name': 'constructivist'}, {'id': 15494, 'synset': 'contractor.n.01', 'name': 'contractor'}, {'id': 15495, 'synset': 'contralto.n.01', 'name': 'contralto'}, {'id': 15496, 'synset': 'contributor.n.02', 'name': 'contributor'}, {'id': 15497, 'synset': 'control_freak.n.01', 'name': 'control_freak'}, {'id': 15498, 'synset': 'convalescent.n.01', 'name': 'convalescent'}, {'id': 15499, 'synset': 'convener.n.01', 'name': 'convener'}, {'id': 15500, 'synset': 'convict.n.01', 'name': 'convict'}, {'id': 15501, 'synset': 'copilot.n.01', 'name': 'copilot'}, {'id': 15502, 'synset': 'copycat.n.01', 'name': 'copycat'}, {'id': 15503, 'synset': 'coreligionist.n.01', 'name': 'coreligionist'}, {'id': 15504, 'synset': 'cornerback.n.01', 'name': 'cornerback'}, {'id': 15505, 'synset': 'corporatist.n.01', 'name': 'corporatist'}, {'id': 15506, 'synset': 'correspondent.n.01', 'name': 'correspondent'}, {'id': 15507, 'synset': 'cosmetician.n.01', 'name': 'cosmetician'}, {'id': 15508, 'synset': 'cosmopolitan.n.01', 'name': 'cosmopolitan'}, {'id': 15509, 'synset': 'cossack.n.01', 'name': 'Cossack'}, {'id': 15510, 'synset': 'cost_accountant.n.01', 'name': 'cost_accountant'}, {'id': 15511, 'synset': 'co-star.n.01', 'name': 'co-star'}, {'id': 15512, 'synset': 'costumier.n.01', 'name': 'costumier'}, {'id': 15513, 'synset': 'cotter.n.02', 'name': 'cotter'}, {'id': 15514, 'synset': 'cotter.n.01', 'name': 'cotter'}, {'id': 15515, 'synset': 'counselor.n.01', 'name': 'counselor'}, {'id': 15516, 'synset': 'counterterrorist.n.01', 'name': 'counterterrorist'}, {'id': 15517, 'synset': 'counterspy.n.01', 'name': 'counterspy'}, {'id': 15518, 'synset': 'countess.n.01', 'name': 'countess'}, {'id': 15519, 'synset': 'compromiser.n.01', 'name': 'compromiser'}, {'id': 15520, 'synset': 'countrywoman.n.01', 'name': 'countrywoman'}, {'id': 15521, 'synset': 'county_agent.n.01', 'name': 'county_agent'}, {'id': 15522, 'synset': 'courtier.n.01', 'name': 'courtier'}, {'id': 15523, 'synset': 'cousin.n.01', 'name': 'cousin'}, {'id': 15524, 'synset': 'cover_girl.n.01', 'name': 'cover_girl'}, {'id': 15525, 'synset': 'cow.n.03', 'name': 'cow'}, {'id': 15526, 'synset': 'craftsman.n.03', 'name': 'craftsman'}, {'id': 15527, 'synset': 'craftsman.n.02', 'name': 'craftsman'}, {'id': 15528, 'synset': 'crapshooter.n.01', 'name': 'crapshooter'}, {'id': 15529, 'synset': 'crazy.n.01', 'name': 'crazy'}, {'id': 15530, 'synset': 'creature.n.02', 'name': 'creature'}, {'id': 15531, 'synset': 'creditor.n.01', 'name': 'creditor'}, {'id': 15532, 'synset': 'creep.n.01', 'name': 'creep'}, {'id': 15533, 'synset': 'criminologist.n.01', 'name': 'criminologist'}, {'id': 15534, 'synset': 'critic.n.02', 'name': 'critic'}, {'id': 15535, 'synset': 'croesus.n.02', 'name': 'Croesus'}, {'id': 15536, 'synset': 'cross-examiner.n.01', 'name': 'cross-examiner'}, {'id': 15537, 'synset': 'crossover_voter.n.01', 'name': 'crossover_voter'}, {'id': 15538, 'synset': 'croupier.n.01', 'name': 'croupier'}, {'id': 15539, 'synset': 'crown_prince.n.01', 'name': 'crown_prince'}, {'id': 15540, 'synset': 'crown_princess.n.01', 'name': 'crown_princess'}, {'id': 15541, 'synset': 'cryptanalyst.n.01', 'name': 'cryptanalyst'}, {'id': 15542, 'synset': 'cub_scout.n.01', 'name': 'Cub_Scout'}, {'id': 15543, 'synset': 'cuckold.n.01', 'name': 'cuckold'}, {'id': 15544, 'synset': 'cultist.n.02', 'name': 'cultist'}, {'id': 15545, 'synset': 'curandera.n.01', 'name': 'curandera'}, {'id': 15546, 'synset': 'curate.n.01', 'name': 'curate'}, {'id': 15547, 'synset': 'curator.n.01', 'name': 'curator'}, {'id': 15548, 'synset': 'customer_agent.n.01', 'name': 'customer_agent'}, {'id': 15549, 'synset': 'cutter.n.02', 'name': 'cutter'}, {'id': 15550, 'synset': 'cyberpunk.n.02', 'name': 'cyberpunk'}, {'id': 15551, 'synset': 'cyborg.n.01', 'name': 'cyborg'}, {'id': 15552, 'synset': 'cymbalist.n.01', 'name': 'cymbalist'}, {'id': 15553, 'synset': 'cynic.n.02', 'name': 'Cynic'}, {'id': 15554, 'synset': 'cytogeneticist.n.01', 'name': 'cytogeneticist'}, {'id': 15555, 'synset': 'cytologist.n.01', 'name': 'cytologist'}, {'id': 15556, 'synset': 'czar.n.02', 'name': 'czar'}, {'id': 15557, 'synset': 'czar.n.01', 'name': 'czar'}, {'id': 15558, 'synset': 'dad.n.01', 'name': 'dad'}, {'id': 15559, 'synset': 'dairyman.n.02', 'name': 'dairyman'}, {'id': 15560, 'synset': 'dalai_lama.n.01', 'name': 'Dalai_Lama'}, {'id': 15561, 'synset': 'dallier.n.01', 'name': 'dallier'}, {'id': 15562, 'synset': 'dancer.n.01', 'name': 'dancer'}, {'id': 15563, 'synset': 'dancer.n.02', 'name': 'dancer'}, {'id': 15564, 'synset': 'clog_dancer.n.01', 'name': 'clog_dancer'}, {'id': 15565, 'synset': 'dancing-master.n.01', 'name': 'dancing-master'}, {'id': 15566, 'synset': 'dark_horse.n.01', 'name': 'dark_horse'}, {'id': 15567, 'synset': 'darling.n.01', 'name': 'darling'}, {'id': 15568, 'synset': 'date.n.02', 'name': 'date'}, {'id': 15569, 'synset': 'daughter.n.01', 'name': 'daughter'}, {'id': 15570, 'synset': 'dawdler.n.01', 'name': 'dawdler'}, {'id': 15571, 'synset': 'day_boarder.n.01', 'name': 'day_boarder'}, {'id': 15572, 'synset': 'day_laborer.n.01', 'name': 'day_laborer'}, {'id': 15573, 'synset': 'deacon.n.01', 'name': 'deacon'}, {'id': 15574, 'synset': 'deaconess.n.01', 'name': 'deaconess'}, {'id': 15575, 'synset': 'deadeye.n.01', 'name': 'deadeye'}, {'id': 15576, 'synset': 'deipnosophist.n.01', 'name': 'deipnosophist'}, {'id': 15577, 'synset': 'dropout.n.02', 'name': 'dropout'}, {'id': 15578, 'synset': 'deadhead.n.01', 'name': 'deadhead'}, {'id': 15579, 'synset': 'deaf_person.n.01', 'name': 'deaf_person'}, {'id': 15580, 'synset': 'debtor.n.01', 'name': 'debtor'}, {'id': 15581, 'synset': 'deckhand.n.01', 'name': 'deckhand'}, {'id': 15582, 'synset': 'defamer.n.01', 'name': 'defamer'}, {'id': 15583, 'synset': 'defense_contractor.n.01', 'name': 'defense_contractor'}, {'id': 15584, 'synset': 'deist.n.01', 'name': 'deist'}, {'id': 15585, 'synset': 'delegate.n.01', 'name': 'delegate'}, {'id': 15586, 'synset': 'deliveryman.n.01', 'name': 'deliveryman'}, {'id': 15587, 'synset': 'demagogue.n.01', 'name': 'demagogue'}, {'id': 15588, 'synset': 'demigod.n.01', 'name': 'demigod'}, {'id': 15589, 'synset': 'demographer.n.01', 'name': 'demographer'}, {'id': 15590, 'synset': 'demonstrator.n.03', 'name': 'demonstrator'}, {'id': 15591, 'synset': 'den_mother.n.02', 'name': 'den_mother'}, {'id': 15592, 'synset': 'department_head.n.01', 'name': 'department_head'}, {'id': 15593, 'synset': 'depositor.n.01', 'name': 'depositor'}, {'id': 15594, 'synset': 'deputy.n.03', 'name': 'deputy'}, {'id': 15595, 'synset': 'dermatologist.n.01', 'name': 'dermatologist'}, {'id': 15596, 'synset': 'descender.n.01', 'name': 'descender'}, {'id': 15597, 'synset': 'designated_hitter.n.01', 'name': 'designated_hitter'}, {'id': 15598, 'synset': 'designer.n.04', 'name': 'designer'}, {'id': 15599, 'synset': 'desk_clerk.n.01', 'name': 'desk_clerk'}, {'id': 15600, 'synset': 'desk_officer.n.01', 'name': 'desk_officer'}, {'id': 15601, 'synset': 'desk_sergeant.n.01', 'name': 'desk_sergeant'}, {'id': 15602, 'synset': 'detainee.n.01', 'name': 'detainee'}, {'id': 15603, 'synset': 'detective.n.01', 'name': 'detective'}, {'id': 15604, 'synset': 'detective.n.02', 'name': 'detective'}, {'id': 15605, 'synset': 'detractor.n.01', 'name': 'detractor'}, {'id': 15606, 'synset': 'developer.n.01', 'name': 'developer'}, {'id': 15607, 'synset': 'deviationist.n.01', 'name': 'deviationist'}, {'id': 15608, 'synset': 'devisee.n.01', 'name': 'devisee'}, {'id': 15609, 'synset': 'devisor.n.01', 'name': 'devisor'}, {'id': 15610, 'synset': 'devourer.n.01', 'name': 'devourer'}, {'id': 15611, 'synset': 'dialectician.n.01', 'name': 'dialectician'}, {'id': 15612, 'synset': 'diarist.n.01', 'name': 'diarist'}, {'id': 15613, 'synset': 'dietician.n.01', 'name': 'dietician'}, {'id': 15614, 'synset': 'diocesan.n.01', 'name': 'diocesan'}, {'id': 15615, 'synset': 'director.n.03', 'name': 'director'}, {'id': 15616, 'synset': 'director.n.02', 'name': 'director'}, {'id': 15617, 'synset': 'dirty_old_man.n.01', 'name': 'dirty_old_man'}, {'id': 15618, 'synset': 'disbeliever.n.01', 'name': 'disbeliever'}, {'id': 15619, 'synset': 'disk_jockey.n.01', 'name': 'disk_jockey'}, {'id': 15620, 'synset': 'dispatcher.n.02', 'name': 'dispatcher'}, {'id': 15621, 'synset': 'distortionist.n.01', 'name': 'distortionist'}, {'id': 15622, 'synset': 'distributor.n.01', 'name': 'distributor'}, {'id': 15623, 'synset': 'district_attorney.n.01', 'name': 'district_attorney'}, {'id': 15624, 'synset': 'district_manager.n.01', 'name': 'district_manager'}, {'id': 15625, 'synset': 'diver.n.02', 'name': 'diver'}, {'id': 15626, 'synset': 'divorcee.n.01', 'name': 'divorcee'}, {'id': 15627, 'synset': 'ex-wife.n.01', 'name': 'ex-wife'}, {'id': 15628, 'synset': 'divorce_lawyer.n.01', 'name': 'divorce_lawyer'}, {'id': 15629, 'synset': 'docent.n.01', 'name': 'docent'}, {'id': 15630, 'synset': 'doctor.n.01', 'name': 'doctor'}, {'id': 15631, 'synset': 'dodo.n.01', 'name': 'dodo'}, {'id': 15632, 'synset': 'doge.n.01', 'name': 'doge'}, {'id': 15633, 'synset': 'dog_in_the_manger.n.01', 'name': 'dog_in_the_manger'}, {'id': 15634, 'synset': 'dogmatist.n.01', 'name': 'dogmatist'}, {'id': 15635, 'synset': 'dolichocephalic.n.01', 'name': 'dolichocephalic'}, {'id': 15636, 'synset': 'domestic_partner.n.01', 'name': 'domestic_partner'}, {'id': 15637, 'synset': 'dominican.n.02', 'name': 'Dominican'}, {'id': 15638, 'synset': 'dominus.n.01', 'name': 'dominus'}, {'id': 15639, 'synset': 'don.n.03', 'name': 'don'}, {'id': 15640, 'synset': 'donatist.n.01', 'name': 'Donatist'}, {'id': 15641, 'synset': 'donna.n.01', 'name': 'donna'}, {'id': 15642, 'synset': 'dosser.n.01', 'name': 'dosser'}, {'id': 15643, 'synset': 'double.n.03', 'name': 'double'}, {'id': 15644, 'synset': 'double-crosser.n.01', 'name': 'double-crosser'}, {'id': 15645, 'synset': 'down-and-out.n.01', 'name': 'down-and-out'}, {'id': 15646, 'synset': 'doyenne.n.01', 'name': 'doyenne'}, {'id': 15647, 'synset': 'draftsman.n.02', 'name': 'draftsman'}, {'id': 15648, 'synset': 'dramatist.n.01', 'name': 'dramatist'}, {'id': 15649, 'synset': 'dreamer.n.01', 'name': 'dreamer'}, {'id': 15650, 'synset': 'dressmaker.n.01', 'name': 'dressmaker'}, {'id': 15651, 'synset': "dressmaker's_model.n.01", 'name': "dressmaker's_model"}, {'id': 15652, 'synset': 'dribbler.n.02', 'name': 'dribbler'}, {'id': 15653, 'synset': 'dribbler.n.01', 'name': 'dribbler'}, {'id': 15654, 'synset': 'drinker.n.02', 'name': 'drinker'}, {'id': 15655, 'synset': 'drinker.n.01', 'name': 'drinker'}, {'id': 15656, 'synset': 'drug_addict.n.01', 'name': 'drug_addict'}, {'id': 15657, 'synset': 'drug_user.n.01', 'name': 'drug_user'}, {'id': 15658, 'synset': 'druid.n.01', 'name': 'Druid'}, {'id': 15659, 'synset': 'drum_majorette.n.02', 'name': 'drum_majorette'}, {'id': 15660, 'synset': 'drummer.n.01', 'name': 'drummer'}, {'id': 15661, 'synset': 'drunk.n.02', 'name': 'drunk'}, {'id': 15662, 'synset': 'drunkard.n.01', 'name': 'drunkard'}, {'id': 15663, 'synset': 'druze.n.01', 'name': 'Druze'}, {'id': 15664, 'synset': 'dry.n.01', 'name': 'dry'}, {'id': 15665, 'synset': 'dry_nurse.n.01', 'name': 'dry_nurse'}, {'id': 15666, 'synset': 'duchess.n.01', 'name': 'duchess'}, {'id': 15667, 'synset': 'duke.n.01', 'name': 'duke'}, {'id': 15668, 'synset': 'duffer.n.01', 'name': 'duffer'}, {'id': 15669, 'synset': 'dunker.n.02', 'name': 'dunker'}, {'id': 15670, 'synset': 'dutch_uncle.n.01', 'name': 'Dutch_uncle'}, {'id': 15671, 'synset': 'dyspeptic.n.01', 'name': 'dyspeptic'}, {'id': 15672, 'synset': 'eager_beaver.n.01', 'name': 'eager_beaver'}, {'id': 15673, 'synset': 'earl.n.01', 'name': 'earl'}, {'id': 15674, 'synset': 'earner.n.01', 'name': 'earner'}, {'id': 15675, 'synset': 'eavesdropper.n.01', 'name': 'eavesdropper'}, {'id': 15676, 'synset': 'eccentric.n.01', 'name': 'eccentric'}, {'id': 15677, 'synset': 'eclectic.n.01', 'name': 'eclectic'}, {'id': 15678, 'synset': 'econometrician.n.01', 'name': 'econometrician'}, {'id': 15679, 'synset': 'economist.n.01', 'name': 'economist'}, {'id': 15680, 'synset': 'ectomorph.n.01', 'name': 'ectomorph'}, {'id': 15681, 'synset': 'editor.n.01', 'name': 'editor'}, {'id': 15682, 'synset': 'egocentric.n.01', 'name': 'egocentric'}, {'id': 15683, 'synset': 'egotist.n.01', 'name': 'egotist'}, {'id': 15684, 'synset': 'ejaculator.n.01', 'name': 'ejaculator'}, {'id': 15685, 'synset': 'elder.n.03', 'name': 'elder'}, {'id': 15686, 'synset': 'elder_statesman.n.01', 'name': 'elder_statesman'}, {'id': 15687, 'synset': 'elected_official.n.01', 'name': 'elected_official'}, {'id': 15688, 'synset': 'electrician.n.01', 'name': 'electrician'}, {'id': 15689, 'synset': 'elegist.n.01', 'name': 'elegist'}, {'id': 15690, 'synset': 'elocutionist.n.01', 'name': 'elocutionist'}, {'id': 15691, 'synset': 'emancipator.n.01', 'name': 'emancipator'}, {'id': 15692, 'synset': 'embryologist.n.01', 'name': 'embryologist'}, {'id': 15693, 'synset': 'emeritus.n.01', 'name': 'emeritus'}, {'id': 15694, 'synset': 'emigrant.n.01', 'name': 'emigrant'}, {'id': 15695, 'synset': 'emissary.n.01', 'name': 'emissary'}, {'id': 15696, 'synset': 'empress.n.01', 'name': 'empress'}, {'id': 15697, 'synset': 'employee.n.01', 'name': 'employee'}, {'id': 15698, 'synset': 'employer.n.01', 'name': 'employer'}, {'id': 15699, 'synset': 'enchantress.n.02', 'name': 'enchantress'}, {'id': 15700, 'synset': 'enchantress.n.01', 'name': 'enchantress'}, {'id': 15701, 'synset': 'encyclopedist.n.01', 'name': 'encyclopedist'}, {'id': 15702, 'synset': 'endomorph.n.01', 'name': 'endomorph'}, {'id': 15703, 'synset': 'enemy.n.02', 'name': 'enemy'}, {'id': 15704, 'synset': 'energizer.n.01', 'name': 'energizer'}, {'id': 15705, 'synset': 'end_man.n.02', 'name': 'end_man'}, {'id': 15706, 'synset': 'end_man.n.01', 'name': 'end_man'}, {'id': 15707, 'synset': 'endorser.n.02', 'name': 'endorser'}, {'id': 15708, 'synset': 'enjoyer.n.01', 'name': 'enjoyer'}, {'id': 15709, 'synset': 'enlisted_woman.n.01', 'name': 'enlisted_woman'}, {'id': 15710, 'synset': 'enophile.n.01', 'name': 'enophile'}, {'id': 15711, 'synset': 'entrant.n.04', 'name': 'entrant'}, {'id': 15712, 'synset': 'entrant.n.03', 'name': 'entrant'}, {'id': 15713, 'synset': 'entrepreneur.n.01', 'name': 'entrepreneur'}, {'id': 15714, 'synset': 'envoy.n.01', 'name': 'envoy'}, {'id': 15715, 'synset': 'enzymologist.n.01', 'name': 'enzymologist'}, {'id': 15716, 'synset': 'eparch.n.01', 'name': 'eparch'}, {'id': 15717, 'synset': 'epidemiologist.n.01', 'name': 'epidemiologist'}, {'id': 15718, 'synset': 'epigone.n.01', 'name': 'epigone'}, {'id': 15719, 'synset': 'epileptic.n.01', 'name': 'epileptic'}, {'id': 15720, 'synset': 'episcopalian.n.01', 'name': 'Episcopalian'}, {'id': 15721, 'synset': 'equerry.n.02', 'name': 'equerry'}, {'id': 15722, 'synset': 'equerry.n.01', 'name': 'equerry'}, {'id': 15723, 'synset': 'erotic.n.01', 'name': 'erotic'}, {'id': 15724, 'synset': 'escapee.n.01', 'name': 'escapee'}, {'id': 15725, 'synset': 'escapist.n.01', 'name': 'escapist'}, {'id': 15726, 'synset': 'eskimo.n.01', 'name': 'Eskimo'}, {'id': 15727, 'synset': 'espionage_agent.n.01', 'name': 'espionage_agent'}, {'id': 15728, 'synset': 'esthetician.n.01', 'name': 'esthetician'}, {'id': 15729, 'synset': 'etcher.n.01', 'name': 'etcher'}, {'id': 15730, 'synset': 'ethnologist.n.01', 'name': 'ethnologist'}, {'id': 15731, 'synset': 'etonian.n.01', 'name': 'Etonian'}, {'id': 15732, 'synset': 'etymologist.n.01', 'name': 'etymologist'}, {'id': 15733, 'synset': 'evangelist.n.01', 'name': 'evangelist'}, {'id': 15734, 'synset': 'evangelist.n.02', 'name': 'Evangelist'}, {'id': 15735, 'synset': 'event_planner.n.01', 'name': 'event_planner'}, {'id': 15736, 'synset': 'examiner.n.02', 'name': 'examiner'}, {'id': 15737, 'synset': 'examiner.n.01', 'name': 'examiner'}, {'id': 15738, 'synset': 'exarch.n.03', 'name': 'exarch'}, {'id': 15739, 'synset': 'executant.n.01', 'name': 'executant'}, {'id': 15740, 'synset': 'executive_secretary.n.01', 'name': 'executive_secretary'}, {'id': 15741, 'synset': 'executive_vice_president.n.01', 'name': 'executive_vice_president'}, {'id': 15742, 'synset': 'executrix.n.01', 'name': 'executrix'}, {'id': 15743, 'synset': 'exegete.n.01', 'name': 'exegete'}, {'id': 15744, 'synset': 'exhibitor.n.01', 'name': 'exhibitor'}, {'id': 15745, 'synset': 'exhibitionist.n.02', 'name': 'exhibitionist'}, {'id': 15746, 'synset': 'exile.n.01', 'name': 'exile'}, {'id': 15747, 'synset': 'existentialist.n.01', 'name': 'existentialist'}, {'id': 15748, 'synset': 'exorcist.n.02', 'name': 'exorcist'}, {'id': 15749, 'synset': 'ex-spouse.n.01', 'name': 'ex-spouse'}, {'id': 15750, 'synset': 'extern.n.01', 'name': 'extern'}, {'id': 15751, 'synset': 'extremist.n.01', 'name': 'extremist'}, {'id': 15752, 'synset': 'extrovert.n.01', 'name': 'extrovert'}, {'id': 15753, 'synset': 'eyewitness.n.01', 'name': 'eyewitness'}, {'id': 15754, 'synset': 'facilitator.n.01', 'name': 'facilitator'}, {'id': 15755, 'synset': 'fairy_godmother.n.01', 'name': 'fairy_godmother'}, {'id': 15756, 'synset': 'falangist.n.01', 'name': 'falangist'}, {'id': 15757, 'synset': 'falconer.n.01', 'name': 'falconer'}, {'id': 15758, 'synset': 'falsifier.n.01', 'name': 'falsifier'}, {'id': 15759, 'synset': 'familiar.n.01', 'name': 'familiar'}, {'id': 15760, 'synset': 'fan.n.03', 'name': 'fan'}, {'id': 15761, 'synset': 'fanatic.n.01', 'name': 'fanatic'}, {'id': 15762, 'synset': 'fancier.n.01', 'name': 'fancier'}, {'id': 15763, 'synset': 'farm_boy.n.01', 'name': 'farm_boy'}, {'id': 15764, 'synset': 'farmer.n.01', 'name': 'farmer'}, {'id': 15765, 'synset': 'farmhand.n.01', 'name': 'farmhand'}, {'id': 15766, 'synset': 'fascist.n.01', 'name': 'fascist'}, {'id': 15767, 'synset': 'fascista.n.01', 'name': 'fascista'}, {'id': 15768, 'synset': 'fatalist.n.01', 'name': 'fatalist'}, {'id': 15769, 'synset': 'father.n.01', 'name': 'father'}, {'id': 15770, 'synset': 'father.n.03', 'name': 'Father'}, {'id': 15771, 'synset': 'father-figure.n.01', 'name': 'father-figure'}, {'id': 15772, 'synset': 'father-in-law.n.01', 'name': 'father-in-law'}, {'id': 15773, 'synset': 'fauntleroy.n.01', 'name': 'Fauntleroy'}, {'id': 15774, 'synset': 'fauve.n.01', 'name': 'Fauve'}, {'id': 15775, 'synset': 'favorite_son.n.01', 'name': 'favorite_son'}, {'id': 15776, 'synset': 'featherweight.n.03', 'name': 'featherweight'}, {'id': 15777, 'synset': 'federalist.n.02', 'name': 'federalist'}, {'id': 15778, 'synset': 'fellow_traveler.n.01', 'name': 'fellow_traveler'}, {'id': 15779, 'synset': 'female_aristocrat.n.01', 'name': 'female_aristocrat'}, {'id': 15780, 'synset': 'female_offspring.n.01', 'name': 'female_offspring'}, {'id': 15781, 'synset': 'female_child.n.01', 'name': 'female_child'}, {'id': 15782, 'synset': 'fence.n.02', 'name': 'fence'}, {'id': 15783, 'synset': 'fiance.n.01', 'name': 'fiance'}, {'id': 15784, 'synset': 'fielder.n.02', 'name': 'fielder'}, {'id': 15785, 'synset': 'field_judge.n.01', 'name': 'field_judge'}, {'id': 15786, 'synset': 'fighter_pilot.n.01', 'name': 'fighter_pilot'}, {'id': 15787, 'synset': 'filer.n.01', 'name': 'filer'}, {'id': 15788, 'synset': 'film_director.n.01', 'name': 'film_director'}, {'id': 15789, 'synset': 'finder.n.01', 'name': 'finder'}, {'id': 15790, 'synset': 'fire_chief.n.01', 'name': 'fire_chief'}, {'id': 15791, 'synset': 'fire-eater.n.03', 'name': 'fire-eater'}, {'id': 15792, 'synset': 'fire-eater.n.02', 'name': 'fire-eater'}, {'id': 15793, 'synset': 'fireman.n.04', 'name': 'fireman'}, {'id': 15794, 'synset': 'fire_marshall.n.01', 'name': 'fire_marshall'}, {'id': 15795, 'synset': 'fire_walker.n.01', 'name': 'fire_walker'}, {'id': 15796, 'synset': 'first_baseman.n.01', 'name': 'first_baseman'}, {'id': 15797, 'synset': 'firstborn.n.01', 'name': 'firstborn'}, {'id': 15798, 'synset': 'first_lady.n.02', 'name': 'first_lady'}, {'id': 15799, 'synset': 'first_lieutenant.n.01', 'name': 'first_lieutenant'}, {'id': 15800, 'synset': 'first_offender.n.01', 'name': 'first_offender'}, {'id': 15801, 'synset': 'first_sergeant.n.01', 'name': 'first_sergeant'}, {'id': 15802, 'synset': 'fishmonger.n.01', 'name': 'fishmonger'}, {'id': 15803, 'synset': 'flagellant.n.02', 'name': 'flagellant'}, {'id': 15804, 'synset': 'flag_officer.n.01', 'name': 'flag_officer'}, {'id': 15805, 'synset': 'flak_catcher.n.01', 'name': 'flak_catcher'}, {'id': 15806, 'synset': 'flanker_back.n.01', 'name': 'flanker_back'}, {'id': 15807, 'synset': 'flapper.n.01', 'name': 'flapper'}, {'id': 15808, 'synset': 'flatmate.n.01', 'name': 'flatmate'}, {'id': 15809, 'synset': 'flatterer.n.01', 'name': 'flatterer'}, {'id': 15810, 'synset': 'flibbertigibbet.n.01', 'name': 'flibbertigibbet'}, {'id': 15811, 'synset': 'flight_surgeon.n.01', 'name': 'flight_surgeon'}, {'id': 15812, 'synset': 'floorwalker.n.01', 'name': 'floorwalker'}, {'id': 15813, 'synset': 'flop.n.02', 'name': 'flop'}, {'id': 15814, 'synset': 'florentine.n.01', 'name': 'Florentine'}, {'id': 15815, 'synset': 'flower_girl.n.02', 'name': 'flower_girl'}, {'id': 15816, 'synset': 'flower_girl.n.01', 'name': 'flower_girl'}, {'id': 15817, 'synset': 'flutist.n.01', 'name': 'flutist'}, {'id': 15818, 'synset': 'fly-by-night.n.01', 'name': 'fly-by-night'}, {'id': 15819, 'synset': 'flyweight.n.02', 'name': 'flyweight'}, {'id': 15820, 'synset': 'flyweight.n.01', 'name': 'flyweight'}, {'id': 15821, 'synset': 'foe.n.02', 'name': 'foe'}, {'id': 15822, 'synset': 'folk_dancer.n.01', 'name': 'folk_dancer'}, {'id': 15823, 'synset': 'folk_poet.n.01', 'name': 'folk_poet'}, {'id': 15824, 'synset': 'follower.n.01', 'name': 'follower'}, {'id': 15825, 'synset': 'football_hero.n.01', 'name': 'football_hero'}, {'id': 15826, 'synset': 'football_player.n.01', 'name': 'football_player'}, {'id': 15827, 'synset': 'footman.n.01', 'name': 'footman'}, {'id': 15828, 'synset': 'forefather.n.01', 'name': 'forefather'}, {'id': 15829, 'synset': 'foremother.n.01', 'name': 'foremother'}, {'id': 15830, 'synset': 'foreign_agent.n.01', 'name': 'foreign_agent'}, {'id': 15831, 'synset': 'foreigner.n.02', 'name': 'foreigner'}, {'id': 15832, 'synset': 'boss.n.03', 'name': 'boss'}, {'id': 15833, 'synset': 'foreman.n.02', 'name': 'foreman'}, {'id': 15834, 'synset': 'forester.n.02', 'name': 'forester'}, {'id': 15835, 'synset': 'forewoman.n.02', 'name': 'forewoman'}, {'id': 15836, 'synset': 'forger.n.02', 'name': 'forger'}, {'id': 15837, 'synset': 'forward.n.01', 'name': 'forward'}, {'id': 15838, 'synset': 'foster-brother.n.01', 'name': 'foster-brother'}, {'id': 15839, 'synset': 'foster-father.n.01', 'name': 'foster-father'}, {'id': 15840, 'synset': 'foster-mother.n.01', 'name': 'foster-mother'}, {'id': 15841, 'synset': 'foster-sister.n.01', 'name': 'foster-sister'}, {'id': 15842, 'synset': 'foster-son.n.01', 'name': 'foster-son'}, {'id': 15843, 'synset': 'founder.n.02', 'name': 'founder'}, {'id': 15844, 'synset': 'foundress.n.01', 'name': 'foundress'}, {'id': 15845, 'synset': 'four-minute_man.n.01', 'name': 'four-minute_man'}, {'id': 15846, 'synset': 'framer.n.02', 'name': 'framer'}, {'id': 15847, 'synset': 'francophobe.n.01', 'name': 'Francophobe'}, {'id': 15848, 'synset': 'freak.n.01', 'name': 'freak'}, {'id': 15849, 'synset': 'free_agent.n.02', 'name': 'free_agent'}, {'id': 15850, 'synset': 'free_agent.n.01', 'name': 'free_agent'}, {'id': 15851, 'synset': 'freedom_rider.n.01', 'name': 'freedom_rider'}, {'id': 15852, 'synset': 'free-liver.n.01', 'name': 'free-liver'}, {'id': 15853, 'synset': 'freeloader.n.01', 'name': 'freeloader'}, {'id': 15854, 'synset': 'free_trader.n.01', 'name': 'free_trader'}, {'id': 15855, 'synset': 'freudian.n.01', 'name': 'Freudian'}, {'id': 15856, 'synset': 'friar.n.01', 'name': 'friar'}, {'id': 15857, 'synset': 'monk.n.01', 'name': 'monk'}, {'id': 15858, 'synset': 'frontierswoman.n.01', 'name': 'frontierswoman'}, {'id': 15859, 'synset': 'front_man.n.01', 'name': 'front_man'}, {'id': 15860, 'synset': 'frotteur.n.01', 'name': 'frotteur'}, {'id': 15861, 'synset': 'fucker.n.02', 'name': 'fucker'}, {'id': 15862, 'synset': 'fucker.n.01', 'name': 'fucker'}, {'id': 15863, 'synset': 'fuddy-duddy.n.01', 'name': 'fuddy-duddy'}, {'id': 15864, 'synset': 'fullback.n.01', 'name': 'fullback'}, {'id': 15865, 'synset': 'funambulist.n.01', 'name': 'funambulist'}, {'id': 15866, 'synset': 'fundamentalist.n.01', 'name': 'fundamentalist'}, {'id': 15867, 'synset': 'fundraiser.n.01', 'name': 'fundraiser'}, {'id': 15868, 'synset': 'futurist.n.01', 'name': 'futurist'}, {'id': 15869, 'synset': 'gadgeteer.n.01', 'name': 'gadgeteer'}, {'id': 15870, 'synset': 'gagman.n.02', 'name': 'gagman'}, {'id': 15871, 'synset': 'gagman.n.01', 'name': 'gagman'}, {'id': 15872, 'synset': 'gainer.n.01', 'name': 'gainer'}, {'id': 15873, 'synset': 'gal.n.03', 'name': 'gal'}, {'id': 15874, 'synset': 'galoot.n.01', 'name': 'galoot'}, {'id': 15875, 'synset': 'gambist.n.01', 'name': 'gambist'}, {'id': 15876, 'synset': 'gambler.n.01', 'name': 'gambler'}, {'id': 15877, 'synset': 'gamine.n.02', 'name': 'gamine'}, {'id': 15878, 'synset': 'garbage_man.n.01', 'name': 'garbage_man'}, {'id': 15879, 'synset': 'gardener.n.02', 'name': 'gardener'}, {'id': 15880, 'synset': 'garment_cutter.n.01', 'name': 'garment_cutter'}, {'id': 15881, 'synset': 'garroter.n.01', 'name': 'garroter'}, {'id': 15882, 'synset': 'gasman.n.01', 'name': 'gasman'}, {'id': 15883, 'synset': 'gastroenterologist.n.01', 'name': 'gastroenterologist'}, {'id': 15884, 'synset': 'gatherer.n.01', 'name': 'gatherer'}, {'id': 15885, 'synset': 'gawker.n.01', 'name': 'gawker'}, {'id': 15886, 'synset': 'gendarme.n.01', 'name': 'gendarme'}, {'id': 15887, 'synset': 'general.n.01', 'name': 'general'}, {'id': 15888, 'synset': 'generator.n.03', 'name': 'generator'}, {'id': 15889, 'synset': 'geneticist.n.01', 'name': 'geneticist'}, {'id': 15890, 'synset': 'genitor.n.01', 'name': 'genitor'}, {'id': 15891, 'synset': 'gent.n.01', 'name': 'gent'}, {'id': 15892, 'synset': 'geologist.n.01', 'name': 'geologist'}, {'id': 15893, 'synset': 'geophysicist.n.01', 'name': 'geophysicist'}, {'id': 15894, 'synset': 'ghostwriter.n.01', 'name': 'ghostwriter'}, {'id': 15895, 'synset': 'gibson_girl.n.01', 'name': 'Gibson_girl'}, {'id': 15896, 'synset': 'girl.n.01', 'name': 'girl'}, {'id': 15897, 'synset': 'girlfriend.n.02', 'name': 'girlfriend'}, {'id': 15898, 'synset': 'girlfriend.n.01', 'name': 'girlfriend'}, {'id': 15899, 'synset': 'girl_wonder.n.01', 'name': 'girl_wonder'}, {'id': 15900, 'synset': 'girondist.n.01', 'name': 'Girondist'}, {'id': 15901, 'synset': 'gitano.n.01', 'name': 'gitano'}, {'id': 15902, 'synset': 'gladiator.n.01', 'name': 'gladiator'}, {'id': 15903, 'synset': 'glassblower.n.01', 'name': 'glassblower'}, {'id': 15904, 'synset': 'gleaner.n.02', 'name': 'gleaner'}, {'id': 15905, 'synset': 'goat_herder.n.01', 'name': 'goat_herder'}, {'id': 15906, 'synset': 'godchild.n.01', 'name': 'godchild'}, {'id': 15907, 'synset': 'godfather.n.01', 'name': 'godfather'}, {'id': 15908, 'synset': 'godparent.n.01', 'name': 'godparent'}, {'id': 15909, 'synset': 'godson.n.01', 'name': 'godson'}, {'id': 15910, 'synset': 'gofer.n.01', 'name': 'gofer'}, {'id': 15911, 'synset': 'goffer.n.01', 'name': 'goffer'}, {'id': 15912, 'synset': 'goldsmith.n.01', 'name': 'goldsmith'}, {'id': 15913, 'synset': 'golfer.n.01', 'name': 'golfer'}, {'id': 15914, 'synset': 'gondolier.n.01', 'name': 'gondolier'}, {'id': 15915, 'synset': 'good_guy.n.01', 'name': 'good_guy'}, {'id': 15916, 'synset': 'good_old_boy.n.01', 'name': 'good_old_boy'}, {'id': 15917, 'synset': 'good_samaritan.n.01', 'name': 'good_Samaritan'}, {'id': 15918, 'synset': 'gossip_columnist.n.01', 'name': 'gossip_columnist'}, {'id': 15919, 'synset': 'gouger.n.01', 'name': 'gouger'}, {'id': 15920, 'synset': 'governor_general.n.01', 'name': 'governor_general'}, {'id': 15921, 'synset': 'grabber.n.01', 'name': 'grabber'}, {'id': 15922, 'synset': 'grader.n.01', 'name': 'grader'}, {'id': 15923, 'synset': 'graduate_nurse.n.01', 'name': 'graduate_nurse'}, {'id': 15924, 'synset': 'grammarian.n.01', 'name': 'grammarian'}, {'id': 15925, 'synset': 'granddaughter.n.01', 'name': 'granddaughter'}, {'id': 15926, 'synset': 'grande_dame.n.01', 'name': 'grande_dame'}, {'id': 15927, 'synset': 'grandfather.n.01', 'name': 'grandfather'}, {'id': 15928, 'synset': 'grand_inquisitor.n.01', 'name': 'Grand_Inquisitor'}, {'id': 15929, 'synset': 'grandma.n.01', 'name': 'grandma'}, {'id': 15930, 'synset': 'grandmaster.n.01', 'name': 'grandmaster'}, {'id': 15931, 'synset': 'grandparent.n.01', 'name': 'grandparent'}, {'id': 15932, 'synset': 'grantee.n.01', 'name': 'grantee'}, {'id': 15933, 'synset': 'granter.n.01', 'name': 'granter'}, {'id': 15934, 'synset': 'grass_widower.n.01', 'name': 'grass_widower'}, {'id': 15935, 'synset': 'great-aunt.n.01', 'name': 'great-aunt'}, {'id': 15936, 'synset': 'great_grandchild.n.01', 'name': 'great_grandchild'}, {'id': 15937, 'synset': 'great_granddaughter.n.01', 'name': 'great_granddaughter'}, {'id': 15938, 'synset': 'great_grandmother.n.01', 'name': 'great_grandmother'}, {'id': 15939, 'synset': 'great_grandparent.n.01', 'name': 'great_grandparent'}, {'id': 15940, 'synset': 'great_grandson.n.01', 'name': 'great_grandson'}, {'id': 15941, 'synset': 'great-nephew.n.01', 'name': 'great-nephew'}, {'id': 15942, 'synset': 'great-niece.n.01', 'name': 'great-niece'}, {'id': 15943, 'synset': 'green_beret.n.01', 'name': 'Green_Beret'}, {'id': 15944, 'synset': 'grenadier.n.01', 'name': 'grenadier'}, {'id': 15945, 'synset': 'greeter.n.01', 'name': 'greeter'}, {'id': 15946, 'synset': 'gringo.n.01', 'name': 'gringo'}, {'id': 15947, 'synset': 'grinner.n.01', 'name': 'grinner'}, {'id': 15948, 'synset': 'grocer.n.01', 'name': 'grocer'}, {'id': 15949, 'synset': 'groom.n.03', 'name': 'groom'}, {'id': 15950, 'synset': 'groom.n.01', 'name': 'groom'}, {'id': 15951, 'synset': 'grouch.n.01', 'name': 'grouch'}, {'id': 15952, 'synset': 'group_captain.n.01', 'name': 'group_captain'}, {'id': 15953, 'synset': 'grunter.n.01', 'name': 'grunter'}, {'id': 15954, 'synset': 'prison_guard.n.01', 'name': 'prison_guard'}, {'id': 15955, 'synset': 'guard.n.01', 'name': 'guard'}, {'id': 15956, 'synset': 'guesser.n.01', 'name': 'guesser'}, {'id': 15957, 'synset': 'guest.n.01', 'name': 'guest'}, {'id': 15958, 'synset': 'guest.n.03', 'name': 'guest'}, {'id': 15959, 'synset': 'guest_of_honor.n.01', 'name': 'guest_of_honor'}, {'id': 15960, 'synset': 'guest_worker.n.01', 'name': 'guest_worker'}, {'id': 15961, 'synset': 'guide.n.02', 'name': 'guide'}, {'id': 15962, 'synset': 'guitarist.n.01', 'name': 'guitarist'}, {'id': 15963, 'synset': 'gunnery_sergeant.n.01', 'name': 'gunnery_sergeant'}, {'id': 15964, 'synset': 'guru.n.01', 'name': 'guru'}, {'id': 15965, 'synset': 'guru.n.03', 'name': 'guru'}, {'id': 15966, 'synset': 'guvnor.n.01', 'name': 'guvnor'}, {'id': 15967, 'synset': 'guy.n.01', 'name': 'guy'}, {'id': 15968, 'synset': 'gymnast.n.01', 'name': 'gymnast'}, {'id': 15969, 'synset': 'gym_rat.n.01', 'name': 'gym_rat'}, {'id': 15970, 'synset': 'gynecologist.n.01', 'name': 'gynecologist'}, {'id': 15971, 'synset': 'gypsy.n.02', 'name': 'Gypsy'}, {'id': 15972, 'synset': 'hack.n.01', 'name': 'hack'}, {'id': 15973, 'synset': 'hacker.n.02', 'name': 'hacker'}, {'id': 15974, 'synset': 'haggler.n.01', 'name': 'haggler'}, {'id': 15975, 'synset': 'hairdresser.n.01', 'name': 'hairdresser'}, {'id': 15976, 'synset': 'hakim.n.02', 'name': 'hakim'}, {'id': 15977, 'synset': 'hakka.n.01', 'name': 'Hakka'}, {'id': 15978, 'synset': 'halberdier.n.01', 'name': 'halberdier'}, {'id': 15979, 'synset': 'halfback.n.01', 'name': 'halfback'}, {'id': 15980, 'synset': 'half_blood.n.01', 'name': 'half_blood'}, {'id': 15981, 'synset': 'hand.n.10', 'name': 'hand'}, {'id': 15982, 'synset': 'animal_trainer.n.01', 'name': 'animal_trainer'}, {'id': 15983, 'synset': 'handyman.n.01', 'name': 'handyman'}, {'id': 15984, 'synset': 'hang_glider.n.01', 'name': 'hang_glider'}, {'id': 15985, 'synset': 'hardliner.n.01', 'name': 'hardliner'}, {'id': 15986, 'synset': 'harlequin.n.01', 'name': 'harlequin'}, {'id': 15987, 'synset': 'harmonizer.n.02', 'name': 'harmonizer'}, {'id': 15988, 'synset': 'hash_head.n.01', 'name': 'hash_head'}, {'id': 15989, 'synset': 'hatchet_man.n.01', 'name': 'hatchet_man'}, {'id': 15990, 'synset': 'hater.n.01', 'name': 'hater'}, {'id': 15991, 'synset': 'hatmaker.n.01', 'name': 'hatmaker'}, {'id': 15992, 'synset': 'headman.n.02', 'name': 'headman'}, {'id': 15993, 'synset': 'headmaster.n.01', 'name': 'headmaster'}, {'id': 15994, 'synset': 'head_nurse.n.01', 'name': 'head_nurse'}, {'id': 15995, 'synset': 'hearer.n.01', 'name': 'hearer'}, {'id': 15996, 'synset': 'heartbreaker.n.01', 'name': 'heartbreaker'}, {'id': 15997, 'synset': 'heathen.n.01', 'name': 'heathen'}, {'id': 15998, 'synset': 'heavyweight.n.02', 'name': 'heavyweight'}, {'id': 15999, 'synset': 'heavy.n.01', 'name': 'heavy'}, {'id': 16000, 'synset': 'heckler.n.01', 'name': 'heckler'}, {'id': 16001, 'synset': 'hedger.n.02', 'name': 'hedger'}, {'id': 16002, 'synset': 'hedger.n.01', 'name': 'hedger'}, {'id': 16003, 'synset': 'hedonist.n.01', 'name': 'hedonist'}, {'id': 16004, 'synset': 'heir.n.01', 'name': 'heir'}, {'id': 16005, 'synset': 'heir_apparent.n.01', 'name': 'heir_apparent'}, {'id': 16006, 'synset': 'heiress.n.01', 'name': 'heiress'}, {'id': 16007, 'synset': 'heir_presumptive.n.01', 'name': 'heir_presumptive'}, {'id': 16008, 'synset': 'hellion.n.01', 'name': 'hellion'}, {'id': 16009, 'synset': 'helmsman.n.01', 'name': 'helmsman'}, {'id': 16010, 'synset': 'hire.n.01', 'name': 'hire'}, {'id': 16011, 'synset': 'hematologist.n.01', 'name': 'hematologist'}, {'id': 16012, 'synset': 'hemiplegic.n.01', 'name': 'hemiplegic'}, {'id': 16013, 'synset': 'herald.n.01', 'name': 'herald'}, {'id': 16014, 'synset': 'herbalist.n.01', 'name': 'herbalist'}, {'id': 16015, 'synset': 'herder.n.02', 'name': 'herder'}, {'id': 16016, 'synset': 'hermaphrodite.n.01', 'name': 'hermaphrodite'}, {'id': 16017, 'synset': 'heroine.n.02', 'name': 'heroine'}, {'id': 16018, 'synset': 'heroin_addict.n.01', 'name': 'heroin_addict'}, {'id': 16019, 'synset': 'hero_worshiper.n.01', 'name': 'hero_worshiper'}, {'id': 16020, 'synset': 'herr.n.01', 'name': 'Herr'}, {'id': 16021, 'synset': 'highbinder.n.01', 'name': 'highbinder'}, {'id': 16022, 'synset': 'highbrow.n.01', 'name': 'highbrow'}, {'id': 16023, 'synset': 'high_commissioner.n.01', 'name': 'high_commissioner'}, {'id': 16024, 'synset': 'highflier.n.01', 'name': 'highflier'}, {'id': 16025, 'synset': 'highlander.n.02', 'name': 'Highlander'}, {'id': 16026, 'synset': 'high-muck-a-muck.n.01', 'name': 'high-muck-a-muck'}, {'id': 16027, 'synset': 'high_priest.n.01', 'name': 'high_priest'}, {'id': 16028, 'synset': 'highjacker.n.01', 'name': 'highjacker'}, {'id': 16029, 'synset': 'hireling.n.01', 'name': 'hireling'}, {'id': 16030, 'synset': 'historian.n.01', 'name': 'historian'}, {'id': 16031, 'synset': 'hitchhiker.n.01', 'name': 'hitchhiker'}, {'id': 16032, 'synset': 'hitter.n.02', 'name': 'hitter'}, {'id': 16033, 'synset': 'hobbyist.n.01', 'name': 'hobbyist'}, {'id': 16034, 'synset': 'holdout.n.01', 'name': 'holdout'}, {'id': 16035, 'synset': 'holdover.n.01', 'name': 'holdover'}, {'id': 16036, 'synset': 'holdup_man.n.01', 'name': 'holdup_man'}, {'id': 16037, 'synset': 'homeboy.n.02', 'name': 'homeboy'}, {'id': 16038, 'synset': 'homeboy.n.01', 'name': 'homeboy'}, {'id': 16039, 'synset': 'home_buyer.n.01', 'name': 'home_buyer'}, {'id': 16040, 'synset': 'homegirl.n.01', 'name': 'homegirl'}, {'id': 16041, 'synset': 'homeless.n.01', 'name': 'homeless'}, {'id': 16042, 'synset': 'homeopath.n.01', 'name': 'homeopath'}, {'id': 16043, 'synset': 'honest_woman.n.01', 'name': 'honest_woman'}, {'id': 16044, 'synset': 'honor_guard.n.01', 'name': 'honor_guard'}, {'id': 16045, 'synset': 'hooker.n.05', 'name': 'hooker'}, {'id': 16046, 'synset': 'hoper.n.01', 'name': 'hoper'}, {'id': 16047, 'synset': 'hornist.n.01', 'name': 'hornist'}, {'id': 16048, 'synset': 'horseman.n.01', 'name': 'horseman'}, {'id': 16049, 'synset': 'horse_trader.n.01', 'name': 'horse_trader'}, {'id': 16050, 'synset': 'horsewoman.n.01', 'name': 'horsewoman'}, {'id': 16051, 'synset': 'horse_wrangler.n.01', 'name': 'horse_wrangler'}, {'id': 16052, 'synset': 'horticulturist.n.01', 'name': 'horticulturist'}, {'id': 16053, 'synset': 'hospital_chaplain.n.01', 'name': 'hospital_chaplain'}, {'id': 16054, 'synset': 'host.n.08', 'name': 'host'}, {'id': 16055, 'synset': 'host.n.01', 'name': 'host'}, {'id': 16056, 'synset': 'hostess.n.01', 'name': 'hostess'}, {'id': 16057, 'synset': 'hotelier.n.01', 'name': 'hotelier'}, {'id': 16058, 'synset': 'housekeeper.n.01', 'name': 'housekeeper'}, {'id': 16059, 'synset': 'housemaster.n.01', 'name': 'housemaster'}, {'id': 16060, 'synset': 'housemate.n.01', 'name': 'housemate'}, {'id': 16061, 'synset': 'house_physician.n.01', 'name': 'house_physician'}, {'id': 16062, 'synset': 'house_sitter.n.01', 'name': 'house_sitter'}, {'id': 16063, 'synset': 'housing_commissioner.n.01', 'name': 'housing_commissioner'}, {'id': 16064, 'synset': 'huckster.n.01', 'name': 'huckster'}, {'id': 16065, 'synset': 'hugger.n.01', 'name': 'hugger'}, {'id': 16066, 'synset': 'humanist.n.02', 'name': 'humanist'}, {'id': 16067, 'synset': 'humanitarian.n.01', 'name': 'humanitarian'}, {'id': 16068, 'synset': 'hunk.n.01', 'name': 'hunk'}, {'id': 16069, 'synset': 'huntress.n.01', 'name': 'huntress'}, {'id': 16070, 'synset': 'ex-husband.n.01', 'name': 'ex-husband'}, {'id': 16071, 'synset': 'hydrologist.n.01', 'name': 'hydrologist'}, {'id': 16072, 'synset': 'hyperope.n.01', 'name': 'hyperope'}, {'id': 16073, 'synset': 'hypertensive.n.01', 'name': 'hypertensive'}, {'id': 16074, 'synset': 'hypnotist.n.01', 'name': 'hypnotist'}, {'id': 16075, 'synset': 'hypocrite.n.01', 'name': 'hypocrite'}, {'id': 16076, 'synset': 'iceman.n.01', 'name': 'iceman'}, {'id': 16077, 'synset': 'iconoclast.n.02', 'name': 'iconoclast'}, {'id': 16078, 'synset': 'ideologist.n.01', 'name': 'ideologist'}, {'id': 16079, 'synset': 'idol.n.02', 'name': 'idol'}, {'id': 16080, 'synset': 'idolizer.n.01', 'name': 'idolizer'}, {'id': 16081, 'synset': 'imam.n.01', 'name': 'imam'}, {'id': 16082, 'synset': 'imperialist.n.01', 'name': 'imperialist'}, {'id': 16083, 'synset': 'important_person.n.01', 'name': 'important_person'}, {'id': 16084, 'synset': 'inamorato.n.01', 'name': 'inamorato'}, {'id': 16085, 'synset': 'incumbent.n.01', 'name': 'incumbent'}, {'id': 16086, 'synset': 'incurable.n.01', 'name': 'incurable'}, {'id': 16087, 'synset': 'inductee.n.01', 'name': 'inductee'}, {'id': 16088, 'synset': 'industrialist.n.01', 'name': 'industrialist'}, {'id': 16089, 'synset': 'infanticide.n.01', 'name': 'infanticide'}, {'id': 16090, 'synset': 'inferior.n.01', 'name': 'inferior'}, {'id': 16091, 'synset': 'infernal.n.01', 'name': 'infernal'}, {'id': 16092, 'synset': 'infielder.n.01', 'name': 'infielder'}, {'id': 16093, 'synset': 'infiltrator.n.02', 'name': 'infiltrator'}, {'id': 16094, 'synset': 'informer.n.01', 'name': 'informer'}, {'id': 16095, 'synset': 'ingenue.n.02', 'name': 'ingenue'}, {'id': 16096, 'synset': 'ingenue.n.01', 'name': 'ingenue'}, {'id': 16097, 'synset': 'polymath.n.01', 'name': 'polymath'}, {'id': 16098, 'synset': 'in-law.n.01', 'name': 'in-law'}, {'id': 16099, 'synset': 'inquiry_agent.n.01', 'name': 'inquiry_agent'}, {'id': 16100, 'synset': 'inspector.n.01', 'name': 'inspector'}, {'id': 16101, 'synset': 'inspector_general.n.01', 'name': 'inspector_general'}, {'id': 16102, 'synset': 'instigator.n.02', 'name': 'instigator'}, {'id': 16103, 'synset': 'insurance_broker.n.01', 'name': 'insurance_broker'}, {'id': 16104, 'synset': 'insurgent.n.01', 'name': 'insurgent'}, {'id': 16105, 'synset': 'intelligence_analyst.n.01', 'name': 'intelligence_analyst'}, {'id': 16106, 'synset': 'interior_designer.n.01', 'name': 'interior_designer'}, {'id': 16107, 'synset': 'interlocutor.n.02', 'name': 'interlocutor'}, {'id': 16108, 'synset': 'interlocutor.n.01', 'name': 'interlocutor'}, {'id': 16109, 'synset': 'international_grandmaster.n.01', 'name': 'International_Grandmaster'}, {'id': 16110, 'synset': 'internationalist.n.02', 'name': 'internationalist'}, {'id': 16111, 'synset': 'internist.n.01', 'name': 'internist'}, {'id': 16112, 'synset': 'interpreter.n.01', 'name': 'interpreter'}, {'id': 16113, 'synset': 'interpreter.n.02', 'name': 'interpreter'}, {'id': 16114, 'synset': 'intervenor.n.01', 'name': 'intervenor'}, {'id': 16115, 'synset': 'introvert.n.01', 'name': 'introvert'}, {'id': 16116, 'synset': 'invader.n.01', 'name': 'invader'}, {'id': 16117, 'synset': 'invalidator.n.01', 'name': 'invalidator'}, {'id': 16118, 'synset': 'investigator.n.02', 'name': 'investigator'}, {'id': 16119, 'synset': 'investor.n.01', 'name': 'investor'}, {'id': 16120, 'synset': 'invigilator.n.01', 'name': 'invigilator'}, {'id': 16121, 'synset': 'irreligionist.n.01', 'name': 'irreligionist'}, {'id': 16122, 'synset': 'ivy_leaguer.n.01', 'name': 'Ivy_Leaguer'}, {'id': 16123, 'synset': 'jack_of_all_trades.n.01', 'name': 'Jack_of_all_trades'}, {'id': 16124, 'synset': 'jacksonian.n.01', 'name': 'Jacksonian'}, {'id': 16125, 'synset': 'jane_doe.n.01', 'name': 'Jane_Doe'}, {'id': 16126, 'synset': 'janissary.n.01', 'name': 'janissary'}, {'id': 16127, 'synset': 'jat.n.01', 'name': 'Jat'}, {'id': 16128, 'synset': 'javanese.n.01', 'name': 'Javanese'}, {'id': 16129, 'synset': 'jekyll_and_hyde.n.01', 'name': 'Jekyll_and_Hyde'}, {'id': 16130, 'synset': 'jester.n.01', 'name': 'jester'}, {'id': 16131, 'synset': 'jesuit.n.01', 'name': 'Jesuit'}, {'id': 16132, 'synset': 'jezebel.n.02', 'name': 'jezebel'}, {'id': 16133, 'synset': 'jilt.n.01', 'name': 'jilt'}, {'id': 16134, 'synset': 'jobber.n.01', 'name': 'jobber'}, {'id': 16135, 'synset': 'job_candidate.n.01', 'name': 'job_candidate'}, {'id': 16136, 'synset': "job's_comforter.n.01", 'name': "Job's_comforter"}, {'id': 16137, 'synset': 'jockey.n.01', 'name': 'jockey'}, {'id': 16138, 'synset': 'john_doe.n.02', 'name': 'John_Doe'}, {'id': 16139, 'synset': 'journalist.n.01', 'name': 'journalist'}, {'id': 16140, 'synset': 'judge.n.01', 'name': 'judge'}, {'id': 16141, 'synset': 'judge_advocate.n.01', 'name': 'judge_advocate'}, {'id': 16142, 'synset': 'juggler.n.01', 'name': 'juggler'}, {'id': 16143, 'synset': 'jungian.n.01', 'name': 'Jungian'}, {'id': 16144, 'synset': 'junior.n.03', 'name': 'junior'}, {'id': 16145, 'synset': 'junior.n.02', 'name': 'junior'}, {'id': 16146, 'synset': 'junior.n.04', 'name': 'Junior'}, {'id': 16147, 'synset': 'junior_lightweight.n.01', 'name': 'junior_lightweight'}, {'id': 16148, 'synset': 'junior_middleweight.n.01', 'name': 'junior_middleweight'}, {'id': 16149, 'synset': 'jurist.n.01', 'name': 'jurist'}, {'id': 16150, 'synset': 'juror.n.01', 'name': 'juror'}, {'id': 16151, 'synset': 'justice_of_the_peace.n.01', 'name': 'justice_of_the_peace'}, {'id': 16152, 'synset': 'justiciar.n.01', 'name': 'justiciar'}, {'id': 16153, 'synset': 'kachina.n.01', 'name': 'kachina'}, {'id': 16154, 'synset': 'keyboardist.n.01', 'name': 'keyboardist'}, {'id': 16155, 'synset': 'khedive.n.01', 'name': 'Khedive'}, {'id': 16156, 'synset': 'kingmaker.n.02', 'name': 'kingmaker'}, {'id': 16157, 'synset': 'king.n.02', 'name': 'king'}, {'id': 16158, 'synset': "king's_counsel.n.01", 'name': "King's_Counsel"}, {'id': 16159, 'synset': 'counsel_to_the_crown.n.01', 'name': 'Counsel_to_the_Crown'}, {'id': 16160, 'synset': 'kin.n.01', 'name': 'kin'}, {'id': 16161, 'synset': 'enate.n.01', 'name': 'enate'}, {'id': 16162, 'synset': 'kink.n.03', 'name': 'kink'}, {'id': 16163, 'synset': 'kinswoman.n.01', 'name': 'kinswoman'}, {'id': 16164, 'synset': 'kisser.n.01', 'name': 'kisser'}, {'id': 16165, 'synset': 'kitchen_help.n.01', 'name': 'kitchen_help'}, {'id': 16166, 'synset': 'kitchen_police.n.01', 'name': 'kitchen_police'}, {'id': 16167, 'synset': 'klansman.n.01', 'name': 'Klansman'}, {'id': 16168, 'synset': 'kleptomaniac.n.01', 'name': 'kleptomaniac'}, {'id': 16169, 'synset': 'kneeler.n.01', 'name': 'kneeler'}, {'id': 16170, 'synset': 'knight.n.01', 'name': 'knight'}, {'id': 16171, 'synset': 'knocker.n.01', 'name': 'knocker'}, {'id': 16172, 'synset': 'knower.n.01', 'name': 'knower'}, {'id': 16173, 'synset': 'know-it-all.n.01', 'name': 'know-it-all'}, {'id': 16174, 'synset': 'kolkhoznik.n.01', 'name': 'kolkhoznik'}, {'id': 16175, 'synset': 'kshatriya.n.01', 'name': 'Kshatriya'}, {'id': 16176, 'synset': 'labor_coach.n.01', 'name': 'labor_coach'}, {'id': 16177, 'synset': 'laborer.n.01', 'name': 'laborer'}, {'id': 16178, 'synset': 'labourite.n.01', 'name': 'Labourite'}, {'id': 16179, 'synset': 'lady.n.01', 'name': 'lady'}, {'id': 16180, 'synset': 'lady-in-waiting.n.01', 'name': 'lady-in-waiting'}, {'id': 16181, 'synset': "lady's_maid.n.01", 'name': "lady's_maid"}, {'id': 16182, 'synset': 'lama.n.01', 'name': 'lama'}, {'id': 16183, 'synset': 'lamb.n.04', 'name': 'lamb'}, {'id': 16184, 'synset': 'lame_duck.n.01', 'name': 'lame_duck'}, {'id': 16185, 'synset': 'lamplighter.n.01', 'name': 'lamplighter'}, {'id': 16186, 'synset': 'land_agent.n.02', 'name': 'land_agent'}, {'id': 16187, 'synset': 'landgrave.n.01', 'name': 'landgrave'}, {'id': 16188, 'synset': 'landlubber.n.02', 'name': 'landlubber'}, {'id': 16189, 'synset': 'landlubber.n.01', 'name': 'landlubber'}, {'id': 16190, 'synset': 'landowner.n.01', 'name': 'landowner'}, {'id': 16191, 'synset': 'landscape_architect.n.01', 'name': 'landscape_architect'}, {'id': 16192, 'synset': 'langlaufer.n.01', 'name': 'langlaufer'}, {'id': 16193, 'synset': 'languisher.n.01', 'name': 'languisher'}, {'id': 16194, 'synset': 'lapidary.n.01', 'name': 'lapidary'}, {'id': 16195, 'synset': 'lass.n.01', 'name': 'lass'}, {'id': 16196, 'synset': 'latin.n.03', 'name': 'Latin'}, {'id': 16197, 'synset': 'latin.n.02', 'name': 'Latin'}, {'id': 16198, 'synset': 'latitudinarian.n.01', 'name': 'latitudinarian'}, {'id': 16199, 'synset': "jehovah's_witness.n.01", 'name': "Jehovah's_Witness"}, {'id': 16200, 'synset': 'law_agent.n.01', 'name': 'law_agent'}, {'id': 16201, 'synset': 'lawgiver.n.01', 'name': 'lawgiver'}, {'id': 16202, 'synset': 'lawman.n.01', 'name': 'lawman'}, {'id': 16203, 'synset': 'law_student.n.01', 'name': 'law_student'}, {'id': 16204, 'synset': 'lawyer.n.01', 'name': 'lawyer'}, {'id': 16205, 'synset': 'lay_reader.n.01', 'name': 'lay_reader'}, {'id': 16206, 'synset': 'lazybones.n.01', 'name': 'lazybones'}, {'id': 16207, 'synset': 'leaker.n.01', 'name': 'leaker'}, {'id': 16208, 'synset': 'leaseholder.n.01', 'name': 'leaseholder'}, {'id': 16209, 'synset': 'lector.n.02', 'name': 'lector'}, {'id': 16210, 'synset': 'lector.n.01', 'name': 'lector'}, {'id': 16211, 'synset': 'lecturer.n.02', 'name': 'lecturer'}, {'id': 16212, 'synset': 'left-hander.n.02', 'name': 'left-hander'}, {'id': 16213, 'synset': 'legal_representative.n.01', 'name': 'legal_representative'}, {'id': 16214, 'synset': 'legate.n.01', 'name': 'legate'}, {'id': 16215, 'synset': 'legatee.n.01', 'name': 'legatee'}, {'id': 16216, 'synset': 'legionnaire.n.02', 'name': 'legionnaire'}, {'id': 16217, 'synset': 'letterman.n.01', 'name': 'letterman'}, {'id': 16218, 'synset': 'liberator.n.01', 'name': 'liberator'}, {'id': 16219, 'synset': 'licenser.n.01', 'name': 'licenser'}, {'id': 16220, 'synset': 'licentiate.n.01', 'name': 'licentiate'}, {'id': 16221, 'synset': 'lieutenant.n.01', 'name': 'lieutenant'}, {'id': 16222, 'synset': 'lieutenant_colonel.n.01', 'name': 'lieutenant_colonel'}, {'id': 16223, 'synset': 'lieutenant_commander.n.01', 'name': 'lieutenant_commander'}, {'id': 16224, 'synset': 'lieutenant_junior_grade.n.01', 'name': 'lieutenant_junior_grade'}, {'id': 16225, 'synset': 'life.n.08', 'name': 'life'}, {'id': 16226, 'synset': 'lifeguard.n.01', 'name': 'lifeguard'}, {'id': 16227, 'synset': 'life_tenant.n.01', 'name': 'life_tenant'}, {'id': 16228, 'synset': 'light_flyweight.n.01', 'name': 'light_flyweight'}, {'id': 16229, 'synset': 'light_heavyweight.n.03', 'name': 'light_heavyweight'}, {'id': 16230, 'synset': 'light_heavyweight.n.01', 'name': 'light_heavyweight'}, {'id': 16231, 'synset': "light-o'-love.n.01", 'name': "light-o'-love"}, {'id': 16232, 'synset': 'lightweight.n.01', 'name': 'lightweight'}, {'id': 16233, 'synset': 'lightweight.n.04', 'name': 'lightweight'}, {'id': 16234, 'synset': 'lightweight.n.03', 'name': 'lightweight'}, {'id': 16235, 'synset': 'lilliputian.n.01', 'name': 'lilliputian'}, {'id': 16236, 'synset': 'limnologist.n.01', 'name': 'limnologist'}, {'id': 16237, 'synset': 'lineman.n.01', 'name': 'lineman'}, {'id': 16238, 'synset': 'line_officer.n.01', 'name': 'line_officer'}, {'id': 16239, 'synset': 'lion-hunter.n.01', 'name': 'lion-hunter'}, {'id': 16240, 'synset': 'lisper.n.01', 'name': 'lisper'}, {'id': 16241, 'synset': 'lister.n.02', 'name': 'lister'}, {'id': 16242, 'synset': 'literary_critic.n.01', 'name': 'literary_critic'}, {'id': 16243, 'synset': 'literate.n.01', 'name': 'literate'}, {'id': 16244, 'synset': 'litigant.n.01', 'name': 'litigant'}, {'id': 16245, 'synset': 'litterer.n.01', 'name': 'litterer'}, {'id': 16246, 'synset': 'little_brother.n.01', 'name': 'little_brother'}, {'id': 16247, 'synset': 'little_sister.n.01', 'name': 'little_sister'}, {'id': 16248, 'synset': 'lobbyist.n.01', 'name': 'lobbyist'}, {'id': 16249, 'synset': 'locksmith.n.01', 'name': 'locksmith'}, {'id': 16250, 'synset': 'locum_tenens.n.01', 'name': 'locum_tenens'}, {'id': 16251, 'synset': 'lord.n.03', 'name': 'Lord'}, {'id': 16252, 'synset': 'loser.n.03', 'name': 'loser'}, {'id': 16253, 'synset': 'loser.n.01', 'name': 'loser'}, {'id': 16254, 'synset': 'failure.n.04', 'name': 'failure'}, {'id': 16255, 'synset': 'lothario.n.01', 'name': 'Lothario'}, {'id': 16256, 'synset': 'loudmouth.n.01', 'name': 'loudmouth'}, {'id': 16257, 'synset': 'lowerclassman.n.01', 'name': 'lowerclassman'}, {'id': 16258, 'synset': 'lowlander.n.01', 'name': 'Lowlander'}, {'id': 16259, 'synset': 'loyalist.n.01', 'name': 'loyalist'}, {'id': 16260, 'synset': 'luddite.n.01', 'name': 'Luddite'}, {'id': 16261, 'synset': 'lumberman.n.01', 'name': 'lumberman'}, {'id': 16262, 'synset': 'lumper.n.02', 'name': 'lumper'}, {'id': 16263, 'synset': 'bedlamite.n.01', 'name': 'bedlamite'}, {'id': 16264, 'synset': 'pyromaniac.n.01', 'name': 'pyromaniac'}, {'id': 16265, 'synset': 'lutist.n.01', 'name': 'lutist'}, {'id': 16266, 'synset': 'lutheran.n.01', 'name': 'Lutheran'}, {'id': 16267, 'synset': 'lyricist.n.01', 'name': 'lyricist'}, {'id': 16268, 'synset': 'macebearer.n.01', 'name': 'macebearer'}, {'id': 16269, 'synset': 'machinist.n.01', 'name': 'machinist'}, {'id': 16270, 'synset': 'madame.n.01', 'name': 'madame'}, {'id': 16271, 'synset': 'maenad.n.01', 'name': 'maenad'}, {'id': 16272, 'synset': 'maestro.n.01', 'name': 'maestro'}, {'id': 16273, 'synset': 'magdalen.n.01', 'name': 'magdalen'}, {'id': 16274, 'synset': 'magician.n.01', 'name': 'magician'}, {'id': 16275, 'synset': 'magus.n.01', 'name': 'magus'}, {'id': 16276, 'synset': 'maharani.n.01', 'name': 'maharani'}, {'id': 16277, 'synset': 'mahatma.n.01', 'name': 'mahatma'}, {'id': 16278, 'synset': 'maid.n.02', 'name': 'maid'}, {'id': 16279, 'synset': 'maid.n.01', 'name': 'maid'}, {'id': 16280, 'synset': 'major.n.01', 'name': 'major'}, {'id': 16281, 'synset': 'major.n.03', 'name': 'major'}, {'id': 16282, 'synset': 'major-domo.n.01', 'name': 'major-domo'}, {'id': 16283, 'synset': 'maker.n.01', 'name': 'maker'}, {'id': 16284, 'synset': 'malahini.n.01', 'name': 'malahini'}, {'id': 16285, 'synset': 'malcontent.n.01', 'name': 'malcontent'}, {'id': 16286, 'synset': 'malik.n.01', 'name': 'malik'}, {'id': 16287, 'synset': 'malingerer.n.01', 'name': 'malingerer'}, {'id': 16288, 'synset': 'malthusian.n.01', 'name': 'Malthusian'}, {'id': 16289, 'synset': 'adonis.n.01', 'name': 'adonis'}, {'id': 16290, 'synset': 'man.n.03', 'name': 'man'}, {'id': 16291, 'synset': 'man.n.05', 'name': 'man'}, {'id': 16292, 'synset': 'manageress.n.01', 'name': 'manageress'}, {'id': 16293, 'synset': 'mandarin.n.03', 'name': 'mandarin'}, {'id': 16294, 'synset': 'maneuverer.n.01', 'name': 'maneuverer'}, {'id': 16295, 'synset': 'maniac.n.02', 'name': 'maniac'}, {'id': 16296, 'synset': 'manichaean.n.01', 'name': 'Manichaean'}, {'id': 16297, 'synset': 'manicurist.n.01', 'name': 'manicurist'}, {'id': 16298, 'synset': 'manipulator.n.02', 'name': 'manipulator'}, {'id': 16299, 'synset': 'man-at-arms.n.01', 'name': 'man-at-arms'}, {'id': 16300, 'synset': 'man_of_action.n.01', 'name': 'man_of_action'}, {'id': 16301, 'synset': 'man_of_letters.n.01', 'name': 'man_of_letters'}, {'id': 16302, 'synset': 'manufacturer.n.02', 'name': 'manufacturer'}, {'id': 16303, 'synset': 'marcher.n.02', 'name': 'marcher'}, {'id': 16304, 'synset': 'marchioness.n.02', 'name': 'marchioness'}, {'id': 16305, 'synset': 'margrave.n.02', 'name': 'margrave'}, {'id': 16306, 'synset': 'margrave.n.01', 'name': 'margrave'}, {'id': 16307, 'synset': 'marine.n.01', 'name': 'Marine'}, {'id': 16308, 'synset': 'marquess.n.02', 'name': 'marquess'}, {'id': 16309, 'synset': 'marquis.n.02', 'name': 'marquis'}, {'id': 16310, 'synset': 'marshal.n.02', 'name': 'marshal'}, {'id': 16311, 'synset': 'martinet.n.01', 'name': 'martinet'}, {'id': 16312, 'synset': 'masochist.n.01', 'name': 'masochist'}, {'id': 16313, 'synset': 'mason.n.04', 'name': 'mason'}, {'id': 16314, 'synset': 'masquerader.n.01', 'name': 'masquerader'}, {'id': 16315, 'synset': 'masseur.n.01', 'name': 'masseur'}, {'id': 16316, 'synset': 'masseuse.n.01', 'name': 'masseuse'}, {'id': 16317, 'synset': 'master.n.04', 'name': 'master'}, {'id': 16318, 'synset': 'master.n.07', 'name': 'master'}, {'id': 16319, 'synset': 'master-at-arms.n.01', 'name': 'master-at-arms'}, {'id': 16320, 'synset': 'master_of_ceremonies.n.01', 'name': 'master_of_ceremonies'}, {'id': 16321, 'synset': 'masturbator.n.01', 'name': 'masturbator'}, {'id': 16322, 'synset': 'matchmaker.n.01', 'name': 'matchmaker'}, {'id': 16323, 'synset': 'mate.n.01', 'name': 'mate'}, {'id': 16324, 'synset': 'mate.n.08', 'name': 'mate'}, {'id': 16325, 'synset': 'mate.n.03', 'name': 'mate'}, {'id': 16326, 'synset': 'mater.n.01', 'name': 'mater'}, {'id': 16327, 'synset': 'material.n.05', 'name': 'material'}, {'id': 16328, 'synset': 'materialist.n.02', 'name': 'materialist'}, {'id': 16329, 'synset': 'matriarch.n.01', 'name': 'matriarch'}, {'id': 16330, 'synset': 'matriarch.n.02', 'name': 'matriarch'}, {'id': 16331, 'synset': 'matriculate.n.01', 'name': 'matriculate'}, {'id': 16332, 'synset': 'matron.n.01', 'name': 'matron'}, {'id': 16333, 'synset': 'mayor.n.01', 'name': 'mayor'}, {'id': 16334, 'synset': 'mayoress.n.01', 'name': 'mayoress'}, {'id': 16335, 'synset': 'mechanical_engineer.n.01', 'name': 'mechanical_engineer'}, {'id': 16336, 'synset': 'medalist.n.02', 'name': 'medalist'}, {'id': 16337, 'synset': 'medical_officer.n.01', 'name': 'medical_officer'}, {'id': 16338, 'synset': 'medical_practitioner.n.01', 'name': 'medical_practitioner'}, {'id': 16339, 'synset': 'medical_scientist.n.01', 'name': 'medical_scientist'}, {'id': 16340, 'synset': 'medium.n.09', 'name': 'medium'}, {'id': 16341, 'synset': 'megalomaniac.n.01', 'name': 'megalomaniac'}, {'id': 16342, 'synset': 'melancholic.n.01', 'name': 'melancholic'}, {'id': 16343, 'synset': 'melkite.n.01', 'name': 'Melkite'}, {'id': 16344, 'synset': 'melter.n.01', 'name': 'melter'}, {'id': 16345, 'synset': 'nonmember.n.01', 'name': 'nonmember'}, {'id': 16346, 'synset': 'board_member.n.01', 'name': 'board_member'}, {'id': 16347, 'synset': 'clansman.n.01', 'name': 'clansman'}, {'id': 16348, 'synset': 'memorizer.n.01', 'name': 'memorizer'}, {'id': 16349, 'synset': 'mendelian.n.01', 'name': 'Mendelian'}, {'id': 16350, 'synset': 'mender.n.01', 'name': 'mender'}, {'id': 16351, 'synset': 'mesoamerican.n.01', 'name': 'Mesoamerican'}, {'id': 16352, 'synset': 'messmate.n.01', 'name': 'messmate'}, {'id': 16353, 'synset': 'mestiza.n.01', 'name': 'mestiza'}, {'id': 16354, 'synset': 'meteorologist.n.01', 'name': 'meteorologist'}, {'id': 16355, 'synset': 'meter_maid.n.01', 'name': 'meter_maid'}, {'id': 16356, 'synset': 'methodist.n.01', 'name': 'Methodist'}, {'id': 16357, 'synset': 'metis.n.01', 'name': 'Metis'}, {'id': 16358, 'synset': 'metropolitan.n.01', 'name': 'metropolitan'}, {'id': 16359, 'synset': 'mezzo-soprano.n.01', 'name': 'mezzo-soprano'}, {'id': 16360, 'synset': 'microeconomist.n.01', 'name': 'microeconomist'}, {'id': 16361, 'synset': 'middle-aged_man.n.01', 'name': 'middle-aged_man'}, {'id': 16362, 'synset': 'middlebrow.n.01', 'name': 'middlebrow'}, {'id': 16363, 'synset': 'middleweight.n.01', 'name': 'middleweight'}, {'id': 16364, 'synset': 'midwife.n.01', 'name': 'midwife'}, {'id': 16365, 'synset': 'mikado.n.01', 'name': 'mikado'}, {'id': 16366, 'synset': 'milanese.n.01', 'name': 'Milanese'}, {'id': 16367, 'synset': 'miler.n.02', 'name': 'miler'}, {'id': 16368, 'synset': 'miles_gloriosus.n.01', 'name': 'miles_gloriosus'}, {'id': 16369, 'synset': 'military_attache.n.01', 'name': 'military_attache'}, {'id': 16370, 'synset': 'military_chaplain.n.01', 'name': 'military_chaplain'}, {'id': 16371, 'synset': 'military_leader.n.01', 'name': 'military_leader'}, {'id': 16372, 'synset': 'military_officer.n.01', 'name': 'military_officer'}, {'id': 16373, 'synset': 'military_policeman.n.01', 'name': 'military_policeman'}, {'id': 16374, 'synset': 'mill_agent.n.01', 'name': 'mill_agent'}, {'id': 16375, 'synset': 'mill-hand.n.01', 'name': 'mill-hand'}, {'id': 16376, 'synset': 'millionairess.n.01', 'name': 'millionairess'}, {'id': 16377, 'synset': 'millwright.n.01', 'name': 'millwright'}, {'id': 16378, 'synset': 'minder.n.01', 'name': 'minder'}, {'id': 16379, 'synset': 'mining_engineer.n.01', 'name': 'mining_engineer'}, {'id': 16380, 'synset': 'minister.n.02', 'name': 'minister'}, {'id': 16381, 'synset': 'ministrant.n.01', 'name': 'ministrant'}, {'id': 16382, 'synset': 'minor_leaguer.n.01', 'name': 'minor_leaguer'}, {'id': 16383, 'synset': 'minuteman.n.01', 'name': 'Minuteman'}, {'id': 16384, 'synset': 'misanthrope.n.01', 'name': 'misanthrope'}, {'id': 16385, 'synset': 'misfit.n.01', 'name': 'misfit'}, {'id': 16386, 'synset': 'mistress.n.03', 'name': 'mistress'}, {'id': 16387, 'synset': 'mistress.n.01', 'name': 'mistress'}, {'id': 16388, 'synset': 'mixed-blood.n.01', 'name': 'mixed-blood'}, {'id': 16389, 'synset': 'model.n.03', 'name': 'model'}, {'id': 16390, 'synset': 'class_act.n.01', 'name': 'class_act'}, {'id': 16391, 'synset': 'modeler.n.01', 'name': 'modeler'}, {'id': 16392, 'synset': 'modifier.n.02', 'name': 'modifier'}, {'id': 16393, 'synset': 'molecular_biologist.n.01', 'name': 'molecular_biologist'}, {'id': 16394, 'synset': 'monegasque.n.01', 'name': 'Monegasque'}, {'id': 16395, 'synset': 'monetarist.n.01', 'name': 'monetarist'}, {'id': 16396, 'synset': 'moneygrubber.n.01', 'name': 'moneygrubber'}, {'id': 16397, 'synset': 'moneymaker.n.01', 'name': 'moneymaker'}, {'id': 16398, 'synset': 'mongoloid.n.01', 'name': 'Mongoloid'}, {'id': 16399, 'synset': 'monolingual.n.01', 'name': 'monolingual'}, {'id': 16400, 'synset': 'monologist.n.01', 'name': 'monologist'}, {'id': 16401, 'synset': 'moonlighter.n.01', 'name': 'moonlighter'}, {'id': 16402, 'synset': 'moralist.n.01', 'name': 'moralist'}, {'id': 16403, 'synset': 'morosoph.n.01', 'name': 'morosoph'}, {'id': 16404, 'synset': 'morris_dancer.n.01', 'name': 'morris_dancer'}, {'id': 16405, 'synset': 'mortal_enemy.n.01', 'name': 'mortal_enemy'}, {'id': 16406, 'synset': 'mortgagee.n.01', 'name': 'mortgagee'}, {'id': 16407, 'synset': 'mortician.n.01', 'name': 'mortician'}, {'id': 16408, 'synset': 'moss-trooper.n.01', 'name': 'moss-trooper'}, {'id': 16409, 'synset': 'mother.n.01', 'name': 'mother'}, {'id': 16410, 'synset': 'mother.n.04', 'name': 'mother'}, {'id': 16411, 'synset': 'mother.n.03', 'name': 'mother'}, {'id': 16412, 'synset': 'mother_figure.n.01', 'name': 'mother_figure'}, {'id': 16413, 'synset': 'mother_hen.n.01', 'name': 'mother_hen'}, {'id': 16414, 'synset': 'mother-in-law.n.01', 'name': 'mother-in-law'}, {'id': 16415, 'synset': "mother's_boy.n.01", 'name': "mother's_boy"}, {'id': 16416, 'synset': "mother's_daughter.n.01", 'name': "mother's_daughter"}, {'id': 16417, 'synset': 'motorcycle_cop.n.01', 'name': 'motorcycle_cop'}, {'id': 16418, 'synset': 'motorcyclist.n.01', 'name': 'motorcyclist'}, {'id': 16419, 'synset': 'mound_builder.n.01', 'name': 'Mound_Builder'}, {'id': 16420, 'synset': 'mountebank.n.01', 'name': 'mountebank'}, {'id': 16421, 'synset': 'mourner.n.01', 'name': 'mourner'}, {'id': 16422, 'synset': 'mouthpiece.n.03', 'name': 'mouthpiece'}, {'id': 16423, 'synset': 'mover.n.03', 'name': 'mover'}, {'id': 16424, 'synset': 'moviegoer.n.01', 'name': 'moviegoer'}, {'id': 16425, 'synset': 'muffin_man.n.01', 'name': 'muffin_man'}, {'id': 16426, 'synset': 'mugwump.n.02', 'name': 'mugwump'}, {'id': 16427, 'synset': 'mullah.n.01', 'name': 'Mullah'}, {'id': 16428, 'synset': 'muncher.n.01', 'name': 'muncher'}, {'id': 16429, 'synset': 'murderess.n.01', 'name': 'murderess'}, {'id': 16430, 'synset': 'murder_suspect.n.01', 'name': 'murder_suspect'}, {'id': 16431, 'synset': 'musher.n.01', 'name': 'musher'}, {'id': 16432, 'synset': 'musician.n.01', 'name': 'musician'}, {'id': 16433, 'synset': 'musicologist.n.01', 'name': 'musicologist'}, {'id': 16434, 'synset': 'music_teacher.n.01', 'name': 'music_teacher'}, {'id': 16435, 'synset': 'musketeer.n.01', 'name': 'musketeer'}, {'id': 16436, 'synset': 'muslimah.n.01', 'name': 'Muslimah'}, {'id': 16437, 'synset': 'mutilator.n.01', 'name': 'mutilator'}, {'id': 16438, 'synset': 'mutineer.n.01', 'name': 'mutineer'}, {'id': 16439, 'synset': 'mute.n.01', 'name': 'mute'}, {'id': 16440, 'synset': 'mutterer.n.01', 'name': 'mutterer'}, {'id': 16441, 'synset': 'muzzler.n.01', 'name': 'muzzler'}, {'id': 16442, 'synset': 'mycenaen.n.01', 'name': 'Mycenaen'}, {'id': 16443, 'synset': 'mycologist.n.01', 'name': 'mycologist'}, {'id': 16444, 'synset': 'myope.n.01', 'name': 'myope'}, {'id': 16445, 'synset': 'myrmidon.n.01', 'name': 'myrmidon'}, {'id': 16446, 'synset': 'mystic.n.01', 'name': 'mystic'}, {'id': 16447, 'synset': 'mythologist.n.01', 'name': 'mythologist'}, {'id': 16448, 'synset': 'naif.n.01', 'name': 'naif'}, {'id': 16449, 'synset': 'nailer.n.01', 'name': 'nailer'}, {'id': 16450, 'synset': 'namby-pamby.n.01', 'name': 'namby-pamby'}, {'id': 16451, 'synset': 'name_dropper.n.01', 'name': 'name_dropper'}, {'id': 16452, 'synset': 'namer.n.01', 'name': 'namer'}, {'id': 16453, 'synset': 'nan.n.01', 'name': 'nan'}, {'id': 16454, 'synset': 'nanny.n.01', 'name': 'nanny'}, {'id': 16455, 'synset': 'narc.n.01', 'name': 'narc'}, {'id': 16456, 'synset': 'narcissist.n.01', 'name': 'narcissist'}, {'id': 16457, 'synset': 'nark.n.01', 'name': 'nark'}, {'id': 16458, 'synset': 'nationalist.n.02', 'name': 'nationalist'}, {'id': 16459, 'synset': 'nautch_girl.n.01', 'name': 'nautch_girl'}, {'id': 16460, 'synset': 'naval_commander.n.01', 'name': 'naval_commander'}, {'id': 16461, 'synset': 'navy_seal.n.01', 'name': 'Navy_SEAL'}, {'id': 16462, 'synset': 'obstructionist.n.01', 'name': 'obstructionist'}, {'id': 16463, 'synset': 'nazarene.n.02', 'name': 'Nazarene'}, {'id': 16464, 'synset': 'nazarene.n.01', 'name': 'Nazarene'}, {'id': 16465, 'synset': 'nazi.n.01', 'name': 'Nazi'}, {'id': 16466, 'synset': 'nebbish.n.01', 'name': 'nebbish'}, {'id': 16467, 'synset': 'necker.n.01', 'name': 'necker'}, {'id': 16468, 'synset': 'neonate.n.01', 'name': 'neonate'}, {'id': 16469, 'synset': 'nephew.n.01', 'name': 'nephew'}, {'id': 16470, 'synset': 'neurobiologist.n.01', 'name': 'neurobiologist'}, {'id': 16471, 'synset': 'neurologist.n.01', 'name': 'neurologist'}, {'id': 16472, 'synset': 'neurosurgeon.n.01', 'name': 'neurosurgeon'}, {'id': 16473, 'synset': 'neutral.n.01', 'name': 'neutral'}, {'id': 16474, 'synset': 'neutralist.n.01', 'name': 'neutralist'}, {'id': 16475, 'synset': 'newcomer.n.01', 'name': 'newcomer'}, {'id': 16476, 'synset': 'newcomer.n.02', 'name': 'newcomer'}, {'id': 16477, 'synset': 'new_dealer.n.01', 'name': 'New_Dealer'}, {'id': 16478, 'synset': 'newspaper_editor.n.01', 'name': 'newspaper_editor'}, {'id': 16479, 'synset': 'newsreader.n.01', 'name': 'newsreader'}, {'id': 16480, 'synset': 'newtonian.n.01', 'name': 'Newtonian'}, {'id': 16481, 'synset': 'niece.n.01', 'name': 'niece'}, {'id': 16482, 'synset': 'niggard.n.01', 'name': 'niggard'}, {'id': 16483, 'synset': 'night_porter.n.01', 'name': 'night_porter'}, {'id': 16484, 'synset': 'night_rider.n.01', 'name': 'night_rider'}, {'id': 16485, 'synset': 'nimby.n.01', 'name': 'NIMBY'}, {'id': 16486, 'synset': 'niqaabi.n.01', 'name': 'niqaabi'}, {'id': 16487, 'synset': 'nitpicker.n.01', 'name': 'nitpicker'}, {'id': 16488, 'synset': 'nobelist.n.01', 'name': 'Nobelist'}, {'id': 16489, 'synset': 'noc.n.01', 'name': 'NOC'}, {'id': 16490, 'synset': 'noncandidate.n.01', 'name': 'noncandidate'}, {'id': 16491, 'synset': 'noncommissioned_officer.n.01', 'name': 'noncommissioned_officer'}, {'id': 16492, 'synset': 'nondescript.n.01', 'name': 'nondescript'}, {'id': 16493, 'synset': 'nondriver.n.01', 'name': 'nondriver'}, {'id': 16494, 'synset': 'nonparticipant.n.01', 'name': 'nonparticipant'}, {'id': 16495, 'synset': 'nonperson.n.01', 'name': 'nonperson'}, {'id': 16496, 'synset': 'nonresident.n.01', 'name': 'nonresident'}, {'id': 16497, 'synset': 'nonsmoker.n.01', 'name': 'nonsmoker'}, {'id': 16498, 'synset': 'northern_baptist.n.01', 'name': 'Northern_Baptist'}, {'id': 16499, 'synset': 'noticer.n.01', 'name': 'noticer'}, {'id': 16500, 'synset': 'novelist.n.01', 'name': 'novelist'}, {'id': 16501, 'synset': 'novitiate.n.02', 'name': 'novitiate'}, {'id': 16502, 'synset': 'nuclear_chemist.n.01', 'name': 'nuclear_chemist'}, {'id': 16503, 'synset': 'nudger.n.01', 'name': 'nudger'}, {'id': 16504, 'synset': 'nullipara.n.01', 'name': 'nullipara'}, {'id': 16505, 'synset': 'number_theorist.n.01', 'name': 'number_theorist'}, {'id': 16506, 'synset': 'nurse.n.01', 'name': 'nurse'}, {'id': 16507, 'synset': 'nursling.n.01', 'name': 'nursling'}, {'id': 16508, 'synset': 'nymph.n.03', 'name': 'nymph'}, {'id': 16509, 'synset': 'nymphet.n.01', 'name': 'nymphet'}, {'id': 16510, 'synset': 'nympholept.n.01', 'name': 'nympholept'}, {'id': 16511, 'synset': 'nymphomaniac.n.01', 'name': 'nymphomaniac'}, {'id': 16512, 'synset': 'oarswoman.n.01', 'name': 'oarswoman'}, {'id': 16513, 'synset': 'oboist.n.01', 'name': 'oboist'}, {'id': 16514, 'synset': 'obscurantist.n.01', 'name': 'obscurantist'}, {'id': 16515, 'synset': 'observer.n.02', 'name': 'observer'}, {'id': 16516, 'synset': 'obstetrician.n.01', 'name': 'obstetrician'}, {'id': 16517, 'synset': 'occupier.n.02', 'name': 'occupier'}, {'id': 16518, 'synset': 'occultist.n.01', 'name': 'occultist'}, {'id': 16519, 'synset': 'wine_lover.n.01', 'name': 'wine_lover'}, {'id': 16520, 'synset': 'offerer.n.01', 'name': 'offerer'}, {'id': 16521, 'synset': 'office-bearer.n.01', 'name': 'office-bearer'}, {'id': 16522, 'synset': 'office_boy.n.01', 'name': 'office_boy'}, {'id': 16523, 'synset': 'officeholder.n.01', 'name': 'officeholder'}, {'id': 16524, 'synset': 'officiant.n.01', 'name': 'officiant'}, {'id': 16525, 'synset': 'federal.n.02', 'name': 'Federal'}, {'id': 16526, 'synset': 'oilman.n.02', 'name': 'oilman'}, {'id': 16527, 'synset': 'oil_tycoon.n.01', 'name': 'oil_tycoon'}, {'id': 16528, 'synset': 'old-age_pensioner.n.01', 'name': 'old-age_pensioner'}, {'id': 16529, 'synset': 'old_boy.n.02', 'name': 'old_boy'}, {'id': 16530, 'synset': 'old_lady.n.01', 'name': 'old_lady'}, {'id': 16531, 'synset': 'old_man.n.03', 'name': 'old_man'}, {'id': 16532, 'synset': 'oldster.n.01', 'name': 'oldster'}, {'id': 16533, 'synset': 'old-timer.n.02', 'name': 'old-timer'}, {'id': 16534, 'synset': 'old_woman.n.01', 'name': 'old_woman'}, {'id': 16535, 'synset': 'oligarch.n.01', 'name': 'oligarch'}, {'id': 16536, 'synset': 'olympian.n.01', 'name': 'Olympian'}, {'id': 16537, 'synset': 'omnivore.n.01', 'name': 'omnivore'}, {'id': 16538, 'synset': 'oncologist.n.01', 'name': 'oncologist'}, {'id': 16539, 'synset': 'onlooker.n.01', 'name': 'onlooker'}, {'id': 16540, 'synset': 'onomancer.n.01', 'name': 'onomancer'}, {'id': 16541, 'synset': 'operator.n.03', 'name': 'operator'}, {'id': 16542, 'synset': 'opportunist.n.01', 'name': 'opportunist'}, {'id': 16543, 'synset': 'optimist.n.01', 'name': 'optimist'}, {'id': 16544, 'synset': 'orangeman.n.01', 'name': 'Orangeman'}, {'id': 16545, 'synset': 'orator.n.01', 'name': 'orator'}, {'id': 16546, 'synset': 'orderly.n.02', 'name': 'orderly'}, {'id': 16547, 'synset': 'orderly.n.01', 'name': 'orderly'}, {'id': 16548, 'synset': 'orderly_sergeant.n.01', 'name': 'orderly_sergeant'}, {'id': 16549, 'synset': 'ordinand.n.01', 'name': 'ordinand'}, {'id': 16550, 'synset': 'ordinary.n.03', 'name': 'ordinary'}, {'id': 16551, 'synset': 'organ-grinder.n.01', 'name': 'organ-grinder'}, {'id': 16552, 'synset': 'organist.n.01', 'name': 'organist'}, {'id': 16553, 'synset': 'organization_man.n.01', 'name': 'organization_man'}, {'id': 16554, 'synset': 'organizer.n.01', 'name': 'organizer'}, {'id': 16555, 'synset': 'organizer.n.02', 'name': 'organizer'}, {'id': 16556, 'synset': 'originator.n.01', 'name': 'originator'}, {'id': 16557, 'synset': 'ornithologist.n.01', 'name': 'ornithologist'}, {'id': 16558, 'synset': 'orphan.n.01', 'name': 'orphan'}, {'id': 16559, 'synset': 'orphan.n.02', 'name': 'orphan'}, {'id': 16560, 'synset': 'osteopath.n.01', 'name': 'osteopath'}, {'id': 16561, 'synset': 'out-and-outer.n.01', 'name': 'out-and-outer'}, {'id': 16562, 'synset': 'outdoorswoman.n.01', 'name': 'outdoorswoman'}, {'id': 16563, 'synset': 'outfielder.n.02', 'name': 'outfielder'}, {'id': 16564, 'synset': 'outfielder.n.01', 'name': 'outfielder'}, {'id': 16565, 'synset': 'right_fielder.n.01', 'name': 'right_fielder'}, {'id': 16566, 'synset': 'right-handed_pitcher.n.01', 'name': 'right-handed_pitcher'}, {'id': 16567, 'synset': 'outlier.n.01', 'name': 'outlier'}, {'id': 16568, 'synset': 'owner-occupier.n.01', 'name': 'owner-occupier'}, {'id': 16569, 'synset': 'oyabun.n.01', 'name': 'oyabun'}, {'id': 16570, 'synset': 'packrat.n.01', 'name': 'packrat'}, {'id': 16571, 'synset': 'padrone.n.02', 'name': 'padrone'}, {'id': 16572, 'synset': 'padrone.n.01', 'name': 'padrone'}, {'id': 16573, 'synset': 'page.n.04', 'name': 'page'}, {'id': 16574, 'synset': 'painter.n.02', 'name': 'painter'}, {'id': 16575, 'synset': 'paleo-american.n.01', 'name': 'Paleo-American'}, {'id': 16576, 'synset': 'paleontologist.n.01', 'name': 'paleontologist'}, {'id': 16577, 'synset': 'pallbearer.n.01', 'name': 'pallbearer'}, {'id': 16578, 'synset': 'palmist.n.01', 'name': 'palmist'}, {'id': 16579, 'synset': 'pamperer.n.01', 'name': 'pamperer'}, {'id': 16580, 'synset': 'panchen_lama.n.01', 'name': 'Panchen_Lama'}, {'id': 16581, 'synset': 'panelist.n.01', 'name': 'panelist'}, {'id': 16582, 'synset': 'panhandler.n.01', 'name': 'panhandler'}, {'id': 16583, 'synset': 'paparazzo.n.01', 'name': 'paparazzo'}, {'id': 16584, 'synset': 'paperboy.n.01', 'name': 'paperboy'}, {'id': 16585, 'synset': 'paperhanger.n.02', 'name': 'paperhanger'}, {'id': 16586, 'synset': 'paperhanger.n.01', 'name': 'paperhanger'}, {'id': 16587, 'synset': 'papoose.n.01', 'name': 'papoose'}, {'id': 16588, 'synset': 'pardoner.n.02', 'name': 'pardoner'}, {'id': 16589, 'synset': 'paretic.n.01', 'name': 'paretic'}, {'id': 16590, 'synset': 'parishioner.n.01', 'name': 'parishioner'}, {'id': 16591, 'synset': 'park_commissioner.n.01', 'name': 'park_commissioner'}, {'id': 16592, 'synset': 'parliamentarian.n.01', 'name': 'Parliamentarian'}, {'id': 16593, 'synset': 'parliamentary_agent.n.01', 'name': 'parliamentary_agent'}, {'id': 16594, 'synset': 'parodist.n.01', 'name': 'parodist'}, {'id': 16595, 'synset': 'parricide.n.01', 'name': 'parricide'}, {'id': 16596, 'synset': 'parrot.n.02', 'name': 'parrot'}, {'id': 16597, 'synset': 'partaker.n.01', 'name': 'partaker'}, {'id': 16598, 'synset': 'part-timer.n.01', 'name': 'part-timer'}, {'id': 16599, 'synset': 'party.n.05', 'name': 'party'}, {'id': 16600, 'synset': 'party_man.n.01', 'name': 'party_man'}, {'id': 16601, 'synset': 'passenger.n.01', 'name': 'passenger'}, {'id': 16602, 'synset': 'passer.n.03', 'name': 'passer'}, {'id': 16603, 'synset': 'paster.n.01', 'name': 'paster'}, {'id': 16604, 'synset': 'pater.n.01', 'name': 'pater'}, {'id': 16605, 'synset': 'patient.n.01', 'name': 'patient'}, {'id': 16606, 'synset': 'patriarch.n.04', 'name': 'patriarch'}, {'id': 16607, 'synset': 'patriarch.n.03', 'name': 'patriarch'}, {'id': 16608, 'synset': 'patriarch.n.02', 'name': 'patriarch'}, {'id': 16609, 'synset': 'patriot.n.01', 'name': 'patriot'}, {'id': 16610, 'synset': 'patron.n.03', 'name': 'patron'}, {'id': 16611, 'synset': 'patternmaker.n.01', 'name': 'patternmaker'}, {'id': 16612, 'synset': 'pawnbroker.n.01', 'name': 'pawnbroker'}, {'id': 16613, 'synset': 'payer.n.01', 'name': 'payer'}, {'id': 16614, 'synset': 'peacekeeper.n.01', 'name': 'peacekeeper'}, {'id': 16615, 'synset': 'peasant.n.02', 'name': 'peasant'}, {'id': 16616, 'synset': 'pedant.n.01', 'name': 'pedant'}, {'id': 16617, 'synset': 'peddler.n.01', 'name': 'peddler'}, {'id': 16618, 'synset': 'pederast.n.01', 'name': 'pederast'}, {'id': 16619, 'synset': 'penologist.n.01', 'name': 'penologist'}, {'id': 16620, 'synset': 'pentathlete.n.01', 'name': 'pentathlete'}, {'id': 16621, 'synset': 'pentecostal.n.01', 'name': 'Pentecostal'}, {'id': 16622, 'synset': 'percussionist.n.01', 'name': 'percussionist'}, {'id': 16623, 'synset': 'periodontist.n.01', 'name': 'periodontist'}, {'id': 16624, 'synset': 'peshmerga.n.01', 'name': 'peshmerga'}, {'id': 16625, 'synset': 'personality.n.02', 'name': 'personality'}, {'id': 16626, 'synset': 'personal_representative.n.01', 'name': 'personal_representative'}, {'id': 16627, 'synset': 'personage.n.01', 'name': 'personage'}, {'id': 16628, 'synset': 'persona_grata.n.01', 'name': 'persona_grata'}, {'id': 16629, 'synset': 'persona_non_grata.n.01', 'name': 'persona_non_grata'}, {'id': 16630, 'synset': 'personification.n.01', 'name': 'personification'}, {'id': 16631, 'synset': 'perspirer.n.01', 'name': 'perspirer'}, {'id': 16632, 'synset': 'pervert.n.01', 'name': 'pervert'}, {'id': 16633, 'synset': 'pessimist.n.01', 'name': 'pessimist'}, {'id': 16634, 'synset': 'pest.n.03', 'name': 'pest'}, {'id': 16635, 'synset': 'peter_pan.n.01', 'name': 'Peter_Pan'}, {'id': 16636, 'synset': 'petitioner.n.01', 'name': 'petitioner'}, {'id': 16637, 'synset': 'petit_juror.n.01', 'name': 'petit_juror'}, {'id': 16638, 'synset': 'pet_sitter.n.01', 'name': 'pet_sitter'}, {'id': 16639, 'synset': 'petter.n.01', 'name': 'petter'}, {'id': 16640, 'synset': 'pharaoh.n.01', 'name': 'Pharaoh'}, {'id': 16641, 'synset': 'pharmacist.n.01', 'name': 'pharmacist'}, {'id': 16642, 'synset': 'philanthropist.n.01', 'name': 'philanthropist'}, {'id': 16643, 'synset': 'philatelist.n.01', 'name': 'philatelist'}, {'id': 16644, 'synset': 'philosopher.n.02', 'name': 'philosopher'}, {'id': 16645, 'synset': 'phonetician.n.01', 'name': 'phonetician'}, {'id': 16646, 'synset': 'phonologist.n.01', 'name': 'phonologist'}, {'id': 16647, 'synset': 'photojournalist.n.01', 'name': 'photojournalist'}, {'id': 16648, 'synset': 'photometrist.n.01', 'name': 'photometrist'}, {'id': 16649, 'synset': 'physical_therapist.n.01', 'name': 'physical_therapist'}, {'id': 16650, 'synset': 'physicist.n.01', 'name': 'physicist'}, {'id': 16651, 'synset': 'piano_maker.n.01', 'name': 'piano_maker'}, {'id': 16652, 'synset': 'picker.n.01', 'name': 'picker'}, {'id': 16653, 'synset': 'picnicker.n.01', 'name': 'picnicker'}, {'id': 16654, 'synset': 'pilgrim.n.01', 'name': 'pilgrim'}, {'id': 16655, 'synset': 'pill.n.03', 'name': 'pill'}, {'id': 16656, 'synset': 'pillar.n.03', 'name': 'pillar'}, {'id': 16657, 'synset': 'pill_head.n.01', 'name': 'pill_head'}, {'id': 16658, 'synset': 'pilot.n.02', 'name': 'pilot'}, {'id': 16659, 'synset': 'piltdown_man.n.01', 'name': 'Piltdown_man'}, {'id': 16660, 'synset': 'pimp.n.01', 'name': 'pimp'}, {'id': 16661, 'synset': 'pipe_smoker.n.01', 'name': 'pipe_smoker'}, {'id': 16662, 'synset': 'pip-squeak.n.01', 'name': 'pip-squeak'}, {'id': 16663, 'synset': 'pisser.n.01', 'name': 'pisser'}, {'id': 16664, 'synset': 'pitcher.n.01', 'name': 'pitcher'}, {'id': 16665, 'synset': 'pitchman.n.01', 'name': 'pitchman'}, {'id': 16666, 'synset': 'placeman.n.01', 'name': 'placeman'}, {'id': 16667, 'synset': 'placer_miner.n.01', 'name': 'placer_miner'}, {'id': 16668, 'synset': 'plagiarist.n.01', 'name': 'plagiarist'}, {'id': 16669, 'synset': 'plainsman.n.01', 'name': 'plainsman'}, {'id': 16670, 'synset': 'planner.n.01', 'name': 'planner'}, {'id': 16671, 'synset': 'planter.n.01', 'name': 'planter'}, {'id': 16672, 'synset': 'plasterer.n.01', 'name': 'plasterer'}, {'id': 16673, 'synset': 'platinum_blond.n.01', 'name': 'platinum_blond'}, {'id': 16674, 'synset': 'platitudinarian.n.01', 'name': 'platitudinarian'}, {'id': 16675, 'synset': 'playboy.n.01', 'name': 'playboy'}, {'id': 16676, 'synset': 'player.n.01', 'name': 'player'}, {'id': 16677, 'synset': 'playmate.n.01', 'name': 'playmate'}, {'id': 16678, 'synset': 'pleaser.n.01', 'name': 'pleaser'}, {'id': 16679, 'synset': 'pledger.n.01', 'name': 'pledger'}, {'id': 16680, 'synset': 'plenipotentiary.n.01', 'name': 'plenipotentiary'}, {'id': 16681, 'synset': 'plier.n.01', 'name': 'plier'}, {'id': 16682, 'synset': 'plodder.n.03', 'name': 'plodder'}, {'id': 16683, 'synset': 'plodder.n.02', 'name': 'plodder'}, {'id': 16684, 'synset': 'plotter.n.02', 'name': 'plotter'}, {'id': 16685, 'synset': 'plumber.n.01', 'name': 'plumber'}, {'id': 16686, 'synset': 'pluralist.n.02', 'name': 'pluralist'}, {'id': 16687, 'synset': 'pluralist.n.01', 'name': 'pluralist'}, {'id': 16688, 'synset': 'poet.n.01', 'name': 'poet'}, {'id': 16689, 'synset': 'pointsman.n.01', 'name': 'pointsman'}, {'id': 16690, 'synset': 'point_woman.n.01', 'name': 'point_woman'}, {'id': 16691, 'synset': 'policyholder.n.01', 'name': 'policyholder'}, {'id': 16692, 'synset': 'political_prisoner.n.01', 'name': 'political_prisoner'}, {'id': 16693, 'synset': 'political_scientist.n.01', 'name': 'political_scientist'}, {'id': 16694, 'synset': 'politician.n.02', 'name': 'politician'}, {'id': 16695, 'synset': 'politician.n.03', 'name': 'politician'}, {'id': 16696, 'synset': 'pollster.n.01', 'name': 'pollster'}, {'id': 16697, 'synset': 'polluter.n.01', 'name': 'polluter'}, {'id': 16698, 'synset': 'pool_player.n.01', 'name': 'pool_player'}, {'id': 16699, 'synset': 'portraitist.n.01', 'name': 'portraitist'}, {'id': 16700, 'synset': 'poseuse.n.01', 'name': 'poseuse'}, {'id': 16701, 'synset': 'positivist.n.01', 'name': 'positivist'}, {'id': 16702, 'synset': 'postdoc.n.02', 'name': 'postdoc'}, {'id': 16703, 'synset': 'poster_girl.n.01', 'name': 'poster_girl'}, {'id': 16704, 'synset': 'postulator.n.02', 'name': 'postulator'}, {'id': 16705, 'synset': 'private_citizen.n.01', 'name': 'private_citizen'}, {'id': 16706, 'synset': 'problem_solver.n.01', 'name': 'problem_solver'}, {'id': 16707, 'synset': 'pro-lifer.n.01', 'name': 'pro-lifer'}, {'id': 16708, 'synset': 'prosthetist.n.01', 'name': 'prosthetist'}, {'id': 16709, 'synset': 'postulant.n.01', 'name': 'postulant'}, {'id': 16710, 'synset': 'potboy.n.01', 'name': 'potboy'}, {'id': 16711, 'synset': 'poultryman.n.01', 'name': 'poultryman'}, {'id': 16712, 'synset': 'power_user.n.01', 'name': 'power_user'}, {'id': 16713, 'synset': 'power_worker.n.01', 'name': 'power_worker'}, {'id': 16714, 'synset': 'practitioner.n.01', 'name': 'practitioner'}, {'id': 16715, 'synset': 'prayer.n.05', 'name': 'prayer'}, {'id': 16716, 'synset': 'preceptor.n.01', 'name': 'preceptor'}, {'id': 16717, 'synset': 'predecessor.n.01', 'name': 'predecessor'}, {'id': 16718, 'synset': 'preemptor.n.02', 'name': 'preemptor'}, {'id': 16719, 'synset': 'preemptor.n.01', 'name': 'preemptor'}, {'id': 16720, 'synset': 'premature_baby.n.01', 'name': 'premature_baby'}, {'id': 16721, 'synset': 'presbyter.n.01', 'name': 'presbyter'}, {'id': 16722, 'synset': 'presenter.n.02', 'name': 'presenter'}, {'id': 16723, 'synset': 'presentist.n.01', 'name': 'presentist'}, {'id': 16724, 'synset': 'preserver.n.03', 'name': 'preserver'}, {'id': 16725, 'synset': 'president.n.03', 'name': 'president'}, {'id': 16726, 'synset': 'president_of_the_united_states.n.01', 'name': 'President_of_the_United_States'}, {'id': 16727, 'synset': 'president.n.05', 'name': 'president'}, {'id': 16728, 'synset': 'press_agent.n.01', 'name': 'press_agent'}, {'id': 16729, 'synset': 'press_photographer.n.01', 'name': 'press_photographer'}, {'id': 16730, 'synset': 'priest.n.01', 'name': 'priest'}, {'id': 16731, 'synset': 'prima_ballerina.n.01', 'name': 'prima_ballerina'}, {'id': 16732, 'synset': 'prima_donna.n.02', 'name': 'prima_donna'}, {'id': 16733, 'synset': 'prima_donna.n.01', 'name': 'prima_donna'}, {'id': 16734, 'synset': 'primigravida.n.01', 'name': 'primigravida'}, {'id': 16735, 'synset': 'primordial_dwarf.n.01', 'name': 'primordial_dwarf'}, {'id': 16736, 'synset': 'prince_charming.n.01', 'name': 'prince_charming'}, {'id': 16737, 'synset': 'prince_consort.n.01', 'name': 'prince_consort'}, {'id': 16738, 'synset': 'princeling.n.01', 'name': 'princeling'}, {'id': 16739, 'synset': 'prince_of_wales.n.01', 'name': 'Prince_of_Wales'}, {'id': 16740, 'synset': 'princess.n.01', 'name': 'princess'}, {'id': 16741, 'synset': 'princess_royal.n.01', 'name': 'princess_royal'}, {'id': 16742, 'synset': 'principal.n.06', 'name': 'principal'}, {'id': 16743, 'synset': 'principal.n.02', 'name': 'principal'}, {'id': 16744, 'synset': 'print_seller.n.01', 'name': 'print_seller'}, {'id': 16745, 'synset': 'prior.n.01', 'name': 'prior'}, {'id': 16746, 'synset': 'private.n.01', 'name': 'private'}, {'id': 16747, 'synset': 'probationer.n.01', 'name': 'probationer'}, {'id': 16748, 'synset': 'processor.n.02', 'name': 'processor'}, {'id': 16749, 'synset': 'process-server.n.01', 'name': 'process-server'}, {'id': 16750, 'synset': 'proconsul.n.02', 'name': 'proconsul'}, {'id': 16751, 'synset': 'proconsul.n.01', 'name': 'proconsul'}, {'id': 16752, 'synset': 'proctologist.n.01', 'name': 'proctologist'}, {'id': 16753, 'synset': 'proctor.n.01', 'name': 'proctor'}, {'id': 16754, 'synset': 'procurator.n.02', 'name': 'procurator'}, {'id': 16755, 'synset': 'procurer.n.02', 'name': 'procurer'}, {'id': 16756, 'synset': 'profit_taker.n.01', 'name': 'profit_taker'}, {'id': 16757, 'synset': 'programmer.n.01', 'name': 'programmer'}, {'id': 16758, 'synset': 'promiser.n.01', 'name': 'promiser'}, {'id': 16759, 'synset': 'promoter.n.01', 'name': 'promoter'}, {'id': 16760, 'synset': 'promulgator.n.01', 'name': 'promulgator'}, {'id': 16761, 'synset': 'propagandist.n.01', 'name': 'propagandist'}, {'id': 16762, 'synset': 'propagator.n.02', 'name': 'propagator'}, {'id': 16763, 'synset': 'property_man.n.01', 'name': 'property_man'}, {'id': 16764, 'synset': 'prophetess.n.01', 'name': 'prophetess'}, {'id': 16765, 'synset': 'prophet.n.02', 'name': 'prophet'}, {'id': 16766, 'synset': 'prosecutor.n.01', 'name': 'prosecutor'}, {'id': 16767, 'synset': 'prospector.n.01', 'name': 'prospector'}, {'id': 16768, 'synset': 'protectionist.n.01', 'name': 'protectionist'}, {'id': 16769, 'synset': 'protegee.n.01', 'name': 'protegee'}, {'id': 16770, 'synset': 'protozoologist.n.01', 'name': 'protozoologist'}, {'id': 16771, 'synset': 'provost_marshal.n.01', 'name': 'provost_marshal'}, {'id': 16772, 'synset': 'pruner.n.01', 'name': 'pruner'}, {'id': 16773, 'synset': 'psalmist.n.01', 'name': 'psalmist'}, {'id': 16774, 'synset': 'psephologist.n.01', 'name': 'psephologist'}, {'id': 16775, 'synset': 'psychiatrist.n.01', 'name': 'psychiatrist'}, {'id': 16776, 'synset': 'psychic.n.01', 'name': 'psychic'}, {'id': 16777, 'synset': 'psycholinguist.n.01', 'name': 'psycholinguist'}, {'id': 16778, 'synset': 'psychophysicist.n.01', 'name': 'psychophysicist'}, {'id': 16779, 'synset': 'publican.n.01', 'name': 'publican'}, {'id': 16780, 'synset': 'pudge.n.01', 'name': 'pudge'}, {'id': 16781, 'synset': 'puerpera.n.01', 'name': 'puerpera'}, {'id': 16782, 'synset': 'punching_bag.n.01', 'name': 'punching_bag'}, {'id': 16783, 'synset': 'punter.n.02', 'name': 'punter'}, {'id': 16784, 'synset': 'punter.n.01', 'name': 'punter'}, {'id': 16785, 'synset': 'puppeteer.n.01', 'name': 'puppeteer'}, {'id': 16786, 'synset': 'puppy.n.02', 'name': 'puppy'}, {'id': 16787, 'synset': 'purchasing_agent.n.01', 'name': 'purchasing_agent'}, {'id': 16788, 'synset': 'puritan.n.02', 'name': 'puritan'}, {'id': 16789, 'synset': 'puritan.n.01', 'name': 'Puritan'}, {'id': 16790, 'synset': 'pursuer.n.02', 'name': 'pursuer'}, {'id': 16791, 'synset': 'pusher.n.03', 'name': 'pusher'}, {'id': 16792, 'synset': 'pusher.n.02', 'name': 'pusher'}, {'id': 16793, 'synset': 'pusher.n.01', 'name': 'pusher'}, {'id': 16794, 'synset': 'putz.n.01', 'name': 'putz'}, {'id': 16795, 'synset': 'pygmy.n.02', 'name': 'Pygmy'}, {'id': 16796, 'synset': 'qadi.n.01', 'name': 'qadi'}, {'id': 16797, 'synset': 'quadriplegic.n.01', 'name': 'quadriplegic'}, {'id': 16798, 'synset': 'quadruplet.n.02', 'name': 'quadruplet'}, {'id': 16799, 'synset': 'quaker.n.02', 'name': 'quaker'}, {'id': 16800, 'synset': 'quarter.n.11', 'name': 'quarter'}, {'id': 16801, 'synset': 'quarterback.n.01', 'name': 'quarterback'}, {'id': 16802, 'synset': 'quartermaster.n.01', 'name': 'quartermaster'}, {'id': 16803, 'synset': 'quartermaster_general.n.01', 'name': 'quartermaster_general'}, {'id': 16804, 'synset': 'quebecois.n.01', 'name': 'Quebecois'}, {'id': 16805, 'synset': 'queen.n.02', 'name': 'queen'}, {'id': 16806, 'synset': 'queen_of_england.n.01', 'name': 'Queen_of_England'}, {'id': 16807, 'synset': 'queen.n.03', 'name': 'queen'}, {'id': 16808, 'synset': 'queen.n.04', 'name': 'queen'}, {'id': 16809, 'synset': 'queen_consort.n.01', 'name': 'queen_consort'}, {'id': 16810, 'synset': 'queen_mother.n.01', 'name': 'queen_mother'}, {'id': 16811, 'synset': "queen's_counsel.n.01", 'name': "Queen's_Counsel"}, {'id': 16812, 'synset': 'question_master.n.01', 'name': 'question_master'}, {'id': 16813, 'synset': 'quick_study.n.01', 'name': 'quick_study'}, {'id': 16814, 'synset': 'quietist.n.01', 'name': 'quietist'}, {'id': 16815, 'synset': 'quitter.n.01', 'name': 'quitter'}, {'id': 16816, 'synset': 'rabbi.n.01', 'name': 'rabbi'}, {'id': 16817, 'synset': 'racist.n.01', 'name': 'racist'}, {'id': 16818, 'synset': 'radiobiologist.n.01', 'name': 'radiobiologist'}, {'id': 16819, 'synset': 'radiologic_technologist.n.01', 'name': 'radiologic_technologist'}, {'id': 16820, 'synset': 'radiologist.n.01', 'name': 'radiologist'}, {'id': 16821, 'synset': 'rainmaker.n.02', 'name': 'rainmaker'}, {'id': 16822, 'synset': 'raiser.n.01', 'name': 'raiser'}, {'id': 16823, 'synset': 'raja.n.01', 'name': 'raja'}, {'id': 16824, 'synset': 'rake.n.01', 'name': 'rake'}, {'id': 16825, 'synset': 'ramrod.n.02', 'name': 'ramrod'}, {'id': 16826, 'synset': 'ranch_hand.n.01', 'name': 'ranch_hand'}, {'id': 16827, 'synset': 'ranker.n.01', 'name': 'ranker'}, {'id': 16828, 'synset': 'ranter.n.01', 'name': 'ranter'}, {'id': 16829, 'synset': 'rape_suspect.n.01', 'name': 'rape_suspect'}, {'id': 16830, 'synset': 'rapper.n.01', 'name': 'rapper'}, {'id': 16831, 'synset': 'rapporteur.n.01', 'name': 'rapporteur'}, {'id': 16832, 'synset': 'rare_bird.n.01', 'name': 'rare_bird'}, {'id': 16833, 'synset': 'ratepayer.n.01', 'name': 'ratepayer'}, {'id': 16834, 'synset': 'raw_recruit.n.01', 'name': 'raw_recruit'}, {'id': 16835, 'synset': 'reader.n.01', 'name': 'reader'}, {'id': 16836, 'synset': 'reading_teacher.n.01', 'name': 'reading_teacher'}, {'id': 16837, 'synset': 'realist.n.01', 'name': 'realist'}, {'id': 16838, 'synset': 'real_estate_broker.n.01', 'name': 'real_estate_broker'}, {'id': 16839, 'synset': 'rear_admiral.n.01', 'name': 'rear_admiral'}, {'id': 16840, 'synset': 'receiver.n.05', 'name': 'receiver'}, {'id': 16841, 'synset': 'reciter.n.01', 'name': 'reciter'}, {'id': 16842, 'synset': 'recruit.n.02', 'name': 'recruit'}, {'id': 16843, 'synset': 'recruit.n.01', 'name': 'recruit'}, {'id': 16844, 'synset': 'recruiter.n.01', 'name': 'recruiter'}, {'id': 16845, 'synset': 'recruiting-sergeant.n.01', 'name': 'recruiting-sergeant'}, {'id': 16846, 'synset': 'redcap.n.01', 'name': 'redcap'}, {'id': 16847, 'synset': 'redhead.n.01', 'name': 'redhead'}, {'id': 16848, 'synset': 'redneck.n.01', 'name': 'redneck'}, {'id': 16849, 'synset': 'reeler.n.02', 'name': 'reeler'}, {'id': 16850, 'synset': 'reenactor.n.01', 'name': 'reenactor'}, {'id': 16851, 'synset': 'referral.n.01', 'name': 'referral'}, {'id': 16852, 'synset': 'referee.n.01', 'name': 'referee'}, {'id': 16853, 'synset': 'refiner.n.01', 'name': 'refiner'}, {'id': 16854, 'synset': 'reform_jew.n.01', 'name': 'Reform_Jew'}, {'id': 16855, 'synset': 'registered_nurse.n.01', 'name': 'registered_nurse'}, {'id': 16856, 'synset': 'registrar.n.01', 'name': 'registrar'}, {'id': 16857, 'synset': 'regius_professor.n.01', 'name': 'Regius_professor'}, {'id': 16858, 'synset': 'reliever.n.02', 'name': 'reliever'}, {'id': 16859, 'synset': 'anchorite.n.01', 'name': 'anchorite'}, {'id': 16860, 'synset': 'religious_leader.n.01', 'name': 'religious_leader'}, {'id': 16861, 'synset': 'remover.n.02', 'name': 'remover'}, {'id': 16862, 'synset': 'renaissance_man.n.01', 'name': 'Renaissance_man'}, {'id': 16863, 'synset': 'renegade.n.01', 'name': 'renegade'}, {'id': 16864, 'synset': 'rentier.n.01', 'name': 'rentier'}, {'id': 16865, 'synset': 'repairman.n.01', 'name': 'repairman'}, {'id': 16866, 'synset': 'reporter.n.01', 'name': 'reporter'}, {'id': 16867, 'synset': 'newswoman.n.01', 'name': 'newswoman'}, {'id': 16868, 'synset': 'representative.n.01', 'name': 'representative'}, {'id': 16869, 'synset': 'reprobate.n.01', 'name': 'reprobate'}, {'id': 16870, 'synset': 'rescuer.n.02', 'name': 'rescuer'}, {'id': 16871, 'synset': 'reservist.n.01', 'name': 'reservist'}, {'id': 16872, 'synset': 'resident_commissioner.n.01', 'name': 'resident_commissioner'}, {'id': 16873, 'synset': 'respecter.n.01', 'name': 'respecter'}, {'id': 16874, 'synset': 'restaurateur.n.01', 'name': 'restaurateur'}, {'id': 16875, 'synset': 'restrainer.n.02', 'name': 'restrainer'}, {'id': 16876, 'synset': 'retailer.n.01', 'name': 'retailer'}, {'id': 16877, 'synset': 'retiree.n.01', 'name': 'retiree'}, {'id': 16878, 'synset': 'returning_officer.n.01', 'name': 'returning_officer'}, {'id': 16879, 'synset': 'revenant.n.01', 'name': 'revenant'}, {'id': 16880, 'synset': 'revisionist.n.01', 'name': 'revisionist'}, {'id': 16881, 'synset': 'revolutionist.n.01', 'name': 'revolutionist'}, {'id': 16882, 'synset': 'rheumatologist.n.01', 'name': 'rheumatologist'}, {'id': 16883, 'synset': 'rhodesian_man.n.01', 'name': 'Rhodesian_man'}, {'id': 16884, 'synset': 'rhymer.n.01', 'name': 'rhymer'}, {'id': 16885, 'synset': 'rich_person.n.01', 'name': 'rich_person'}, {'id': 16886, 'synset': 'rider.n.03', 'name': 'rider'}, {'id': 16887, 'synset': 'riding_master.n.01', 'name': 'riding_master'}, {'id': 16888, 'synset': 'rifleman.n.02', 'name': 'rifleman'}, {'id': 16889, 'synset': 'right-hander.n.02', 'name': 'right-hander'}, {'id': 16890, 'synset': 'right-hand_man.n.01', 'name': 'right-hand_man'}, {'id': 16891, 'synset': 'ringer.n.03', 'name': 'ringer'}, {'id': 16892, 'synset': 'ringleader.n.01', 'name': 'ringleader'}, {'id': 16893, 'synset': 'roadman.n.02', 'name': 'roadman'}, {'id': 16894, 'synset': 'roarer.n.01', 'name': 'roarer'}, {'id': 16895, 'synset': 'rocket_engineer.n.01', 'name': 'rocket_engineer'}, {'id': 16896, 'synset': 'rocket_scientist.n.01', 'name': 'rocket_scientist'}, {'id': 16897, 'synset': 'rock_star.n.01', 'name': 'rock_star'}, {'id': 16898, 'synset': 'romanov.n.01', 'name': 'Romanov'}, {'id': 16899, 'synset': 'romanticist.n.02', 'name': 'romanticist'}, {'id': 16900, 'synset': 'ropemaker.n.01', 'name': 'ropemaker'}, {'id': 16901, 'synset': 'roper.n.02', 'name': 'roper'}, {'id': 16902, 'synset': 'roper.n.01', 'name': 'roper'}, {'id': 16903, 'synset': 'ropewalker.n.01', 'name': 'ropewalker'}, {'id': 16904, 'synset': 'rosebud.n.02', 'name': 'rosebud'}, {'id': 16905, 'synset': 'rosicrucian.n.02', 'name': 'Rosicrucian'}, {'id': 16906, 'synset': 'mountie.n.01', 'name': 'Mountie'}, {'id': 16907, 'synset': 'rough_rider.n.01', 'name': 'Rough_Rider'}, {'id': 16908, 'synset': 'roundhead.n.01', 'name': 'roundhead'}, {'id': 16909, 'synset': 'civil_authority.n.01', 'name': 'civil_authority'}, {'id': 16910, 'synset': 'runner.n.03', 'name': 'runner'}, {'id': 16911, 'synset': 'runner.n.02', 'name': 'runner'}, {'id': 16912, 'synset': 'runner.n.06', 'name': 'runner'}, {'id': 16913, 'synset': 'running_back.n.01', 'name': 'running_back'}, {'id': 16914, 'synset': 'rusher.n.02', 'name': 'rusher'}, {'id': 16915, 'synset': 'rustic.n.01', 'name': 'rustic'}, {'id': 16916, 'synset': 'saboteur.n.01', 'name': 'saboteur'}, {'id': 16917, 'synset': 'sadist.n.01', 'name': 'sadist'}, {'id': 16918, 'synset': 'sailing_master.n.01', 'name': 'sailing_master'}, {'id': 16919, 'synset': 'sailor.n.01', 'name': 'sailor'}, {'id': 16920, 'synset': 'salesgirl.n.01', 'name': 'salesgirl'}, {'id': 16921, 'synset': 'salesman.n.01', 'name': 'salesman'}, {'id': 16922, 'synset': 'salesperson.n.01', 'name': 'salesperson'}, {'id': 16923, 'synset': 'salvager.n.01', 'name': 'salvager'}, {'id': 16924, 'synset': 'sandwichman.n.01', 'name': 'sandwichman'}, {'id': 16925, 'synset': 'sangoma.n.01', 'name': 'sangoma'}, {'id': 16926, 'synset': 'sannup.n.01', 'name': 'sannup'}, {'id': 16927, 'synset': 'sapper.n.02', 'name': 'sapper'}, {'id': 16928, 'synset': 'sassenach.n.01', 'name': 'Sassenach'}, {'id': 16929, 'synset': 'satrap.n.01', 'name': 'satrap'}, {'id': 16930, 'synset': 'saunterer.n.01', 'name': 'saunterer'}, {'id': 16931, 'synset': 'savoyard.n.01', 'name': 'Savoyard'}, {'id': 16932, 'synset': 'sawyer.n.01', 'name': 'sawyer'}, {'id': 16933, 'synset': 'scalper.n.01', 'name': 'scalper'}, {'id': 16934, 'synset': 'scandalmonger.n.01', 'name': 'scandalmonger'}, {'id': 16935, 'synset': 'scapegrace.n.01', 'name': 'scapegrace'}, {'id': 16936, 'synset': 'scene_painter.n.02', 'name': 'scene_painter'}, {'id': 16937, 'synset': 'schemer.n.01', 'name': 'schemer'}, {'id': 16938, 'synset': 'schizophrenic.n.01', 'name': 'schizophrenic'}, {'id': 16939, 'synset': 'schlemiel.n.01', 'name': 'schlemiel'}, {'id': 16940, 'synset': 'schlockmeister.n.01', 'name': 'schlockmeister'}, {'id': 16941, 'synset': 'scholar.n.01', 'name': 'scholar'}, {'id': 16942, 'synset': 'scholiast.n.01', 'name': 'scholiast'}, {'id': 16943, 'synset': 'schoolchild.n.01', 'name': 'schoolchild'}, {'id': 16944, 'synset': 'schoolfriend.n.01', 'name': 'schoolfriend'}, {'id': 16945, 'synset': 'schoolman.n.01', 'name': 'Schoolman'}, {'id': 16946, 'synset': 'schoolmaster.n.02', 'name': 'schoolmaster'}, {'id': 16947, 'synset': 'schoolmate.n.01', 'name': 'schoolmate'}, {'id': 16948, 'synset': 'scientist.n.01', 'name': 'scientist'}, {'id': 16949, 'synset': 'scion.n.01', 'name': 'scion'}, {'id': 16950, 'synset': 'scoffer.n.02', 'name': 'scoffer'}, {'id': 16951, 'synset': 'scofflaw.n.01', 'name': 'scofflaw'}, {'id': 16952, 'synset': 'scorekeeper.n.01', 'name': 'scorekeeper'}, {'id': 16953, 'synset': 'scorer.n.02', 'name': 'scorer'}, {'id': 16954, 'synset': 'scourer.n.02', 'name': 'scourer'}, {'id': 16955, 'synset': 'scout.n.03', 'name': 'scout'}, {'id': 16956, 'synset': 'scoutmaster.n.01', 'name': 'scoutmaster'}, {'id': 16957, 'synset': 'scrambler.n.01', 'name': 'scrambler'}, {'id': 16958, 'synset': 'scratcher.n.02', 'name': 'scratcher'}, {'id': 16959, 'synset': 'screen_actor.n.01', 'name': 'screen_actor'}, {'id': 16960, 'synset': 'scrutineer.n.01', 'name': 'scrutineer'}, {'id': 16961, 'synset': 'scuba_diver.n.01', 'name': 'scuba_diver'}, {'id': 16962, 'synset': 'sculptor.n.01', 'name': 'sculptor'}, {'id': 16963, 'synset': 'sea_scout.n.01', 'name': 'Sea_Scout'}, {'id': 16964, 'synset': 'seasonal_worker.n.01', 'name': 'seasonal_worker'}, {'id': 16965, 'synset': 'seasoner.n.01', 'name': 'seasoner'}, {'id': 16966, 'synset': 'second_baseman.n.01', 'name': 'second_baseman'}, {'id': 16967, 'synset': 'second_cousin.n.01', 'name': 'second_cousin'}, {'id': 16968, 'synset': 'seconder.n.01', 'name': 'seconder'}, {'id': 16969, 'synset': 'second_fiddle.n.01', 'name': 'second_fiddle'}, {'id': 16970, 'synset': 'second-in-command.n.01', 'name': 'second-in-command'}, {'id': 16971, 'synset': 'second_lieutenant.n.01', 'name': 'second_lieutenant'}, {'id': 16972, 'synset': 'second-rater.n.01', 'name': 'second-rater'}, {'id': 16973, 'synset': 'secretary.n.01', 'name': 'secretary'}, {'id': 16974, 'synset': 'secretary_of_agriculture.n.01', 'name': 'Secretary_of_Agriculture'}, {'id': 16975, 'synset': 'secretary_of_health_and_human_services.n.01', 'name': 'Secretary_of_Health_and_Human_Services'}, {'id': 16976, 'synset': 'secretary_of_state.n.01', 'name': 'Secretary_of_State'}, {'id': 16977, 'synset': 'secretary_of_the_interior.n.02', 'name': 'Secretary_of_the_Interior'}, {'id': 16978, 'synset': 'sectarian.n.01', 'name': 'sectarian'}, {'id': 16979, 'synset': 'section_hand.n.01', 'name': 'section_hand'}, {'id': 16980, 'synset': 'secularist.n.01', 'name': 'secularist'}, {'id': 16981, 'synset': 'security_consultant.n.01', 'name': 'security_consultant'}, {'id': 16982, 'synset': 'seeded_player.n.01', 'name': 'seeded_player'}, {'id': 16983, 'synset': 'seeder.n.01', 'name': 'seeder'}, {'id': 16984, 'synset': 'seeker.n.01', 'name': 'seeker'}, {'id': 16985, 'synset': 'segregate.n.01', 'name': 'segregate'}, {'id': 16986, 'synset': 'segregator.n.01', 'name': 'segregator'}, {'id': 16987, 'synset': 'selectman.n.01', 'name': 'selectman'}, {'id': 16988, 'synset': 'selectwoman.n.01', 'name': 'selectwoman'}, {'id': 16989, 'synset': 'selfish_person.n.01', 'name': 'selfish_person'}, {'id': 16990, 'synset': 'self-starter.n.01', 'name': 'self-starter'}, {'id': 16991, 'synset': 'seller.n.01', 'name': 'seller'}, {'id': 16992, 'synset': 'selling_agent.n.01', 'name': 'selling_agent'}, {'id': 16993, 'synset': 'semanticist.n.01', 'name': 'semanticist'}, {'id': 16994, 'synset': 'semifinalist.n.01', 'name': 'semifinalist'}, {'id': 16995, 'synset': 'seminarian.n.01', 'name': 'seminarian'}, {'id': 16996, 'synset': 'senator.n.01', 'name': 'senator'}, {'id': 16997, 'synset': 'sendee.n.01', 'name': 'sendee'}, {'id': 16998, 'synset': 'senior.n.01', 'name': 'senior'}, {'id': 16999, 'synset': 'senior_vice_president.n.01', 'name': 'senior_vice_president'}, {'id': 17000, 'synset': 'separatist.n.01', 'name': 'separatist'}, {'id': 17001, 'synset': 'septuagenarian.n.01', 'name': 'septuagenarian'}, {'id': 17002, 'synset': 'serf.n.01', 'name': 'serf'}, {'id': 17003, 'synset': 'spree_killer.n.01', 'name': 'spree_killer'}, {'id': 17004, 'synset': 'serjeant-at-law.n.01', 'name': 'serjeant-at-law'}, {'id': 17005, 'synset': 'server.n.02', 'name': 'server'}, {'id': 17006, 'synset': 'serviceman.n.01', 'name': 'serviceman'}, {'id': 17007, 'synset': 'settler.n.01', 'name': 'settler'}, {'id': 17008, 'synset': 'settler.n.03', 'name': 'settler'}, {'id': 17009, 'synset': 'sex_symbol.n.01', 'name': 'sex_symbol'}, {'id': 17010, 'synset': 'sexton.n.02', 'name': 'sexton'}, {'id': 17011, 'synset': 'shaheed.n.01', 'name': 'shaheed'}, {'id': 17012, 'synset': 'shakespearian.n.01', 'name': 'Shakespearian'}, {'id': 17013, 'synset': 'shanghaier.n.01', 'name': 'shanghaier'}, {'id': 17014, 'synset': 'sharecropper.n.01', 'name': 'sharecropper'}, {'id': 17015, 'synset': 'shaver.n.01', 'name': 'shaver'}, {'id': 17016, 'synset': 'shavian.n.01', 'name': 'Shavian'}, {'id': 17017, 'synset': 'sheep.n.02', 'name': 'sheep'}, {'id': 17018, 'synset': 'sheik.n.01', 'name': 'sheik'}, {'id': 17019, 'synset': 'shelver.n.01', 'name': 'shelver'}, {'id': 17020, 'synset': 'shepherd.n.01', 'name': 'shepherd'}, {'id': 17021, 'synset': 'ship-breaker.n.01', 'name': 'ship-breaker'}, {'id': 17022, 'synset': 'shipmate.n.01', 'name': 'shipmate'}, {'id': 17023, 'synset': 'shipowner.n.01', 'name': 'shipowner'}, {'id': 17024, 'synset': 'shipping_agent.n.01', 'name': 'shipping_agent'}, {'id': 17025, 'synset': 'shirtmaker.n.01', 'name': 'shirtmaker'}, {'id': 17026, 'synset': 'shogun.n.01', 'name': 'shogun'}, {'id': 17027, 'synset': 'shopaholic.n.01', 'name': 'shopaholic'}, {'id': 17028, 'synset': 'shop_girl.n.01', 'name': 'shop_girl'}, {'id': 17029, 'synset': 'shop_steward.n.01', 'name': 'shop_steward'}, {'id': 17030, 'synset': 'shot_putter.n.01', 'name': 'shot_putter'}, {'id': 17031, 'synset': 'shrew.n.01', 'name': 'shrew'}, {'id': 17032, 'synset': 'shuffler.n.01', 'name': 'shuffler'}, {'id': 17033, 'synset': 'shyster.n.01', 'name': 'shyster'}, {'id': 17034, 'synset': 'sibling.n.01', 'name': 'sibling'}, {'id': 17035, 'synset': 'sick_person.n.01', 'name': 'sick_person'}, {'id': 17036, 'synset': 'sightreader.n.01', 'name': 'sightreader'}, {'id': 17037, 'synset': 'signaler.n.01', 'name': 'signaler'}, {'id': 17038, 'synset': 'signer.n.01', 'name': 'signer'}, {'id': 17039, 'synset': 'signor.n.01', 'name': 'signor'}, {'id': 17040, 'synset': 'signora.n.01', 'name': 'signora'}, {'id': 17041, 'synset': 'signore.n.01', 'name': 'signore'}, {'id': 17042, 'synset': 'signorina.n.01', 'name': 'signorina'}, {'id': 17043, 'synset': 'silent_partner.n.01', 'name': 'silent_partner'}, {'id': 17044, 'synset': 'addle-head.n.01', 'name': 'addle-head'}, {'id': 17045, 'synset': 'simperer.n.01', 'name': 'simperer'}, {'id': 17046, 'synset': 'singer.n.01', 'name': 'singer'}, {'id': 17047, 'synset': 'sinologist.n.01', 'name': 'Sinologist'}, {'id': 17048, 'synset': 'sipper.n.01', 'name': 'sipper'}, {'id': 17049, 'synset': 'sirrah.n.01', 'name': 'sirrah'}, {'id': 17050, 'synset': 'sister.n.02', 'name': 'Sister'}, {'id': 17051, 'synset': 'sister.n.01', 'name': 'sister'}, {'id': 17052, 'synset': 'waverer.n.01', 'name': 'waverer'}, {'id': 17053, 'synset': 'sitar_player.n.01', 'name': 'sitar_player'}, {'id': 17054, 'synset': 'sixth-former.n.01', 'name': 'sixth-former'}, {'id': 17055, 'synset': 'skateboarder.n.01', 'name': 'skateboarder'}, {'id': 17056, 'synset': 'skeptic.n.01', 'name': 'skeptic'}, {'id': 17057, 'synset': 'sketcher.n.01', 'name': 'sketcher'}, {'id': 17058, 'synset': 'skidder.n.02', 'name': 'skidder'}, {'id': 17059, 'synset': 'skier.n.01', 'name': 'skier'}, {'id': 17060, 'synset': 'skinny-dipper.n.01', 'name': 'skinny-dipper'}, {'id': 17061, 'synset': 'skin-diver.n.01', 'name': 'skin-diver'}, {'id': 17062, 'synset': 'skinhead.n.01', 'name': 'skinhead'}, {'id': 17063, 'synset': 'slasher.n.01', 'name': 'slasher'}, {'id': 17064, 'synset': 'slattern.n.02', 'name': 'slattern'}, {'id': 17065, 'synset': 'sleeper.n.01', 'name': 'sleeper'}, {'id': 17066, 'synset': 'sleeper.n.02', 'name': 'sleeper'}, {'id': 17067, 'synset': 'sleeping_beauty.n.02', 'name': 'sleeping_beauty'}, {'id': 17068, 'synset': 'sleuth.n.01', 'name': 'sleuth'}, {'id': 17069, 'synset': 'slob.n.01', 'name': 'slob'}, {'id': 17070, 'synset': 'sloganeer.n.01', 'name': 'sloganeer'}, {'id': 17071, 'synset': 'slopseller.n.01', 'name': 'slopseller'}, {'id': 17072, 'synset': 'smasher.n.02', 'name': 'smasher'}, {'id': 17073, 'synset': 'smirker.n.01', 'name': 'smirker'}, {'id': 17074, 'synset': 'smith.n.10', 'name': 'smith'}, {'id': 17075, 'synset': 'smoothie.n.01', 'name': 'smoothie'}, {'id': 17076, 'synset': 'smuggler.n.01', 'name': 'smuggler'}, {'id': 17077, 'synset': 'sneezer.n.01', 'name': 'sneezer'}, {'id': 17078, 'synset': 'snob.n.01', 'name': 'snob'}, {'id': 17079, 'synset': 'snoop.n.01', 'name': 'snoop'}, {'id': 17080, 'synset': 'snorer.n.01', 'name': 'snorer'}, {'id': 17081, 'synset': 'sob_sister.n.01', 'name': 'sob_sister'}, {'id': 17082, 'synset': 'soccer_player.n.01', 'name': 'soccer_player'}, {'id': 17083, 'synset': 'social_anthropologist.n.01', 'name': 'social_anthropologist'}, {'id': 17084, 'synset': 'social_climber.n.01', 'name': 'social_climber'}, {'id': 17085, 'synset': 'socialist.n.01', 'name': 'socialist'}, {'id': 17086, 'synset': 'socializer.n.01', 'name': 'socializer'}, {'id': 17087, 'synset': 'social_scientist.n.01', 'name': 'social_scientist'}, {'id': 17088, 'synset': 'social_secretary.n.01', 'name': 'social_secretary'}, {'id': 17089, 'synset': 'socinian.n.01', 'name': 'Socinian'}, {'id': 17090, 'synset': 'sociolinguist.n.01', 'name': 'sociolinguist'}, {'id': 17091, 'synset': 'sociologist.n.01', 'name': 'sociologist'}, {'id': 17092, 'synset': 'soda_jerk.n.01', 'name': 'soda_jerk'}, {'id': 17093, 'synset': 'sodalist.n.01', 'name': 'sodalist'}, {'id': 17094, 'synset': 'sodomite.n.01', 'name': 'sodomite'}, {'id': 17095, 'synset': 'soldier.n.01', 'name': 'soldier'}, {'id': 17096, 'synset': 'son.n.01', 'name': 'son'}, {'id': 17097, 'synset': 'songster.n.02', 'name': 'songster'}, {'id': 17098, 'synset': 'songstress.n.01', 'name': 'songstress'}, {'id': 17099, 'synset': 'songwriter.n.01', 'name': 'songwriter'}, {'id': 17100, 'synset': 'sorcerer.n.01', 'name': 'sorcerer'}, {'id': 17101, 'synset': 'sorehead.n.01', 'name': 'sorehead'}, {'id': 17102, 'synset': 'soul_mate.n.01', 'name': 'soul_mate'}, {'id': 17103, 'synset': 'southern_baptist.n.01', 'name': 'Southern_Baptist'}, {'id': 17104, 'synset': 'sovereign.n.01', 'name': 'sovereign'}, {'id': 17105, 'synset': 'spacewalker.n.01', 'name': 'spacewalker'}, {'id': 17106, 'synset': 'spanish_american.n.01', 'name': 'Spanish_American'}, {'id': 17107, 'synset': 'sparring_partner.n.01', 'name': 'sparring_partner'}, {'id': 17108, 'synset': 'spastic.n.01', 'name': 'spastic'}, {'id': 17109, 'synset': 'speaker.n.01', 'name': 'speaker'}, {'id': 17110, 'synset': 'native_speaker.n.01', 'name': 'native_speaker'}, {'id': 17111, 'synset': 'speaker.n.03', 'name': 'Speaker'}, {'id': 17112, 'synset': 'speechwriter.n.01', 'name': 'speechwriter'}, {'id': 17113, 'synset': 'specialist.n.02', 'name': 'specialist'}, {'id': 17114, 'synset': 'specifier.n.01', 'name': 'specifier'}, {'id': 17115, 'synset': 'spectator.n.01', 'name': 'spectator'}, {'id': 17116, 'synset': 'speech_therapist.n.01', 'name': 'speech_therapist'}, {'id': 17117, 'synset': 'speedskater.n.01', 'name': 'speedskater'}, {'id': 17118, 'synset': 'spellbinder.n.01', 'name': 'spellbinder'}, {'id': 17119, 'synset': 'sphinx.n.01', 'name': 'sphinx'}, {'id': 17120, 'synset': 'spinster.n.01', 'name': 'spinster'}, {'id': 17121, 'synset': 'split_end.n.01', 'name': 'split_end'}, {'id': 17122, 'synset': 'sport.n.05', 'name': 'sport'}, {'id': 17123, 'synset': 'sport.n.03', 'name': 'sport'}, {'id': 17124, 'synset': 'sporting_man.n.02', 'name': 'sporting_man'}, {'id': 17125, 'synset': 'sports_announcer.n.01', 'name': 'sports_announcer'}, {'id': 17126, 'synset': 'sports_editor.n.01', 'name': 'sports_editor'}, {'id': 17127, 'synset': 'sprog.n.02', 'name': 'sprog'}, {'id': 17128, 'synset': 'square_dancer.n.01', 'name': 'square_dancer'}, {'id': 17129, 'synset': 'square_shooter.n.01', 'name': 'square_shooter'}, {'id': 17130, 'synset': 'squatter.n.02', 'name': 'squatter'}, {'id': 17131, 'synset': 'squire.n.02', 'name': 'squire'}, {'id': 17132, 'synset': 'squire.n.01', 'name': 'squire'}, {'id': 17133, 'synset': 'staff_member.n.01', 'name': 'staff_member'}, {'id': 17134, 'synset': 'staff_sergeant.n.01', 'name': 'staff_sergeant'}, {'id': 17135, 'synset': 'stage_director.n.01', 'name': 'stage_director'}, {'id': 17136, 'synset': 'stainer.n.01', 'name': 'stainer'}, {'id': 17137, 'synset': 'stakeholder.n.01', 'name': 'stakeholder'}, {'id': 17138, 'synset': 'stalker.n.02', 'name': 'stalker'}, {'id': 17139, 'synset': 'stalking-horse.n.01', 'name': 'stalking-horse'}, {'id': 17140, 'synset': 'stammerer.n.01', 'name': 'stammerer'}, {'id': 17141, 'synset': 'stamper.n.02', 'name': 'stamper'}, {'id': 17142, 'synset': 'standee.n.01', 'name': 'standee'}, {'id': 17143, 'synset': 'stand-in.n.01', 'name': 'stand-in'}, {'id': 17144, 'synset': 'star.n.04', 'name': 'star'}, {'id': 17145, 'synset': 'starlet.n.01', 'name': 'starlet'}, {'id': 17146, 'synset': 'starter.n.03', 'name': 'starter'}, {'id': 17147, 'synset': 'statesman.n.01', 'name': 'statesman'}, {'id': 17148, 'synset': 'state_treasurer.n.01', 'name': 'state_treasurer'}, {'id': 17149, 'synset': 'stationer.n.01', 'name': 'stationer'}, {'id': 17150, 'synset': 'stenographer.n.01', 'name': 'stenographer'}, {'id': 17151, 'synset': 'stentor.n.01', 'name': 'stentor'}, {'id': 17152, 'synset': 'stepbrother.n.01', 'name': 'stepbrother'}, {'id': 17153, 'synset': 'stepmother.n.01', 'name': 'stepmother'}, {'id': 17154, 'synset': 'stepparent.n.01', 'name': 'stepparent'}, {'id': 17155, 'synset': 'stevedore.n.01', 'name': 'stevedore'}, {'id': 17156, 'synset': 'steward.n.01', 'name': 'steward'}, {'id': 17157, 'synset': 'steward.n.03', 'name': 'steward'}, {'id': 17158, 'synset': 'steward.n.02', 'name': 'steward'}, {'id': 17159, 'synset': 'stickler.n.01', 'name': 'stickler'}, {'id': 17160, 'synset': 'stiff.n.01', 'name': 'stiff'}, {'id': 17161, 'synset': 'stifler.n.01', 'name': 'stifler'}, {'id': 17162, 'synset': 'stipendiary.n.01', 'name': 'stipendiary'}, {'id': 17163, 'synset': 'stitcher.n.01', 'name': 'stitcher'}, {'id': 17164, 'synset': 'stockjobber.n.01', 'name': 'stockjobber'}, {'id': 17165, 'synset': 'stock_trader.n.01', 'name': 'stock_trader'}, {'id': 17166, 'synset': 'stockist.n.01', 'name': 'stockist'}, {'id': 17167, 'synset': 'stoker.n.02', 'name': 'stoker'}, {'id': 17168, 'synset': 'stooper.n.02', 'name': 'stooper'}, {'id': 17169, 'synset': 'store_detective.n.01', 'name': 'store_detective'}, {'id': 17170, 'synset': 'strafer.n.01', 'name': 'strafer'}, {'id': 17171, 'synset': 'straight_man.n.01', 'name': 'straight_man'}, {'id': 17172, 'synset': 'stranger.n.01', 'name': 'stranger'}, {'id': 17173, 'synset': 'stranger.n.02', 'name': 'stranger'}, {'id': 17174, 'synset': 'strategist.n.01', 'name': 'strategist'}, {'id': 17175, 'synset': 'straw_boss.n.01', 'name': 'straw_boss'}, {'id': 17176, 'synset': 'streetwalker.n.01', 'name': 'streetwalker'}, {'id': 17177, 'synset': 'stretcher-bearer.n.01', 'name': 'stretcher-bearer'}, {'id': 17178, 'synset': 'struggler.n.01', 'name': 'struggler'}, {'id': 17179, 'synset': 'stud.n.01', 'name': 'stud'}, {'id': 17180, 'synset': 'student.n.01', 'name': 'student'}, {'id': 17181, 'synset': 'stumblebum.n.01', 'name': 'stumblebum'}, {'id': 17182, 'synset': 'stylist.n.01', 'name': 'stylist'}, {'id': 17183, 'synset': 'subaltern.n.01', 'name': 'subaltern'}, {'id': 17184, 'synset': 'subcontractor.n.01', 'name': 'subcontractor'}, {'id': 17185, 'synset': 'subduer.n.01', 'name': 'subduer'}, {'id': 17186, 'synset': 'subject.n.06', 'name': 'subject'}, {'id': 17187, 'synset': 'subordinate.n.01', 'name': 'subordinate'}, {'id': 17188, 'synset': 'substitute.n.02', 'name': 'substitute'}, {'id': 17189, 'synset': 'successor.n.03', 'name': 'successor'}, {'id': 17190, 'synset': 'successor.n.01', 'name': 'successor'}, {'id': 17191, 'synset': 'succorer.n.01', 'name': 'succorer'}, {'id': 17192, 'synset': 'sufi.n.01', 'name': 'Sufi'}, {'id': 17193, 'synset': 'suffragan.n.01', 'name': 'suffragan'}, {'id': 17194, 'synset': 'suffragette.n.01', 'name': 'suffragette'}, {'id': 17195, 'synset': 'sugar_daddy.n.01', 'name': 'sugar_daddy'}, {'id': 17196, 'synset': 'suicide_bomber.n.01', 'name': 'suicide_bomber'}, {'id': 17197, 'synset': 'suitor.n.01', 'name': 'suitor'}, {'id': 17198, 'synset': 'sumo_wrestler.n.01', 'name': 'sumo_wrestler'}, {'id': 17199, 'synset': 'sunbather.n.01', 'name': 'sunbather'}, {'id': 17200, 'synset': 'sundowner.n.01', 'name': 'sundowner'}, {'id': 17201, 'synset': 'super_heavyweight.n.01', 'name': 'super_heavyweight'}, {'id': 17202, 'synset': 'superior.n.01', 'name': 'superior'}, {'id': 17203, 'synset': 'supermom.n.01', 'name': 'supermom'}, {'id': 17204, 'synset': 'supernumerary.n.02', 'name': 'supernumerary'}, {'id': 17205, 'synset': 'supremo.n.01', 'name': 'supremo'}, {'id': 17206, 'synset': 'surgeon.n.01', 'name': 'surgeon'}, {'id': 17207, 'synset': 'surgeon_general.n.02', 'name': 'Surgeon_General'}, {'id': 17208, 'synset': 'surgeon_general.n.01', 'name': 'Surgeon_General'}, {'id': 17209, 'synset': 'surpriser.n.01', 'name': 'surpriser'}, {'id': 17210, 'synset': 'surveyor.n.01', 'name': 'surveyor'}, {'id': 17211, 'synset': 'surveyor.n.02', 'name': 'surveyor'}, {'id': 17212, 'synset': 'survivor.n.01', 'name': 'survivor'}, {'id': 17213, 'synset': 'sutler.n.01', 'name': 'sutler'}, {'id': 17214, 'synset': 'sweeper.n.01', 'name': 'sweeper'}, {'id': 17215, 'synset': 'sweetheart.n.01', 'name': 'sweetheart'}, {'id': 17216, 'synset': 'swinger.n.02', 'name': 'swinger'}, {'id': 17217, 'synset': 'switcher.n.01', 'name': 'switcher'}, {'id': 17218, 'synset': 'swot.n.01', 'name': 'swot'}, {'id': 17219, 'synset': 'sycophant.n.01', 'name': 'sycophant'}, {'id': 17220, 'synset': 'sylph.n.01', 'name': 'sylph'}, {'id': 17221, 'synset': 'sympathizer.n.02', 'name': 'sympathizer'}, {'id': 17222, 'synset': 'symphonist.n.01', 'name': 'symphonist'}, {'id': 17223, 'synset': 'syncopator.n.01', 'name': 'syncopator'}, {'id': 17224, 'synset': 'syndic.n.01', 'name': 'syndic'}, {'id': 17225, 'synset': 'tactician.n.01', 'name': 'tactician'}, {'id': 17226, 'synset': 'tagger.n.02', 'name': 'tagger'}, {'id': 17227, 'synset': 'tailback.n.01', 'name': 'tailback'}, {'id': 17228, 'synset': 'tallyman.n.02', 'name': 'tallyman'}, {'id': 17229, 'synset': 'tallyman.n.01', 'name': 'tallyman'}, {'id': 17230, 'synset': 'tanker.n.02', 'name': 'tanker'}, {'id': 17231, 'synset': 'tapper.n.04', 'name': 'tapper'}, {'id': 17232, 'synset': 'tartuffe.n.01', 'name': 'Tartuffe'}, {'id': 17233, 'synset': 'tarzan.n.01', 'name': 'Tarzan'}, {'id': 17234, 'synset': 'taster.n.01', 'name': 'taster'}, {'id': 17235, 'synset': 'tax_assessor.n.01', 'name': 'tax_assessor'}, {'id': 17236, 'synset': 'taxer.n.01', 'name': 'taxer'}, {'id': 17237, 'synset': 'taxi_dancer.n.01', 'name': 'taxi_dancer'}, {'id': 17238, 'synset': 'taxonomist.n.01', 'name': 'taxonomist'}, {'id': 17239, 'synset': 'teacher.n.01', 'name': 'teacher'}, {'id': 17240, 'synset': 'teaching_fellow.n.01', 'name': 'teaching_fellow'}, {'id': 17241, 'synset': 'tearaway.n.01', 'name': 'tearaway'}, {'id': 17242, 'synset': 'technical_sergeant.n.01', 'name': 'technical_sergeant'}, {'id': 17243, 'synset': 'technician.n.02', 'name': 'technician'}, {'id': 17244, 'synset': 'ted.n.01', 'name': 'Ted'}, {'id': 17245, 'synset': 'teetotaler.n.01', 'name': 'teetotaler'}, {'id': 17246, 'synset': 'television_reporter.n.01', 'name': 'television_reporter'}, {'id': 17247, 'synset': 'temporizer.n.01', 'name': 'temporizer'}, {'id': 17248, 'synset': 'tempter.n.01', 'name': 'tempter'}, {'id': 17249, 'synset': 'term_infant.n.01', 'name': 'term_infant'}, {'id': 17250, 'synset': 'toiler.n.01', 'name': 'toiler'}, {'id': 17251, 'synset': 'tenant.n.01', 'name': 'tenant'}, {'id': 17252, 'synset': 'tenant.n.02', 'name': 'tenant'}, {'id': 17253, 'synset': 'tenderfoot.n.01', 'name': 'tenderfoot'}, {'id': 17254, 'synset': 'tennis_player.n.01', 'name': 'tennis_player'}, {'id': 17255, 'synset': 'tennis_pro.n.01', 'name': 'tennis_pro'}, {'id': 17256, 'synset': 'tenor_saxophonist.n.01', 'name': 'tenor_saxophonist'}, {'id': 17257, 'synset': 'termer.n.01', 'name': 'termer'}, {'id': 17258, 'synset': 'terror.n.02', 'name': 'terror'}, {'id': 17259, 'synset': 'tertigravida.n.01', 'name': 'tertigravida'}, {'id': 17260, 'synset': 'testator.n.01', 'name': 'testator'}, {'id': 17261, 'synset': 'testatrix.n.01', 'name': 'testatrix'}, {'id': 17262, 'synset': 'testee.n.01', 'name': 'testee'}, {'id': 17263, 'synset': 'test-tube_baby.n.01', 'name': 'test-tube_baby'}, {'id': 17264, 'synset': 'texas_ranger.n.01', 'name': 'Texas_Ranger'}, {'id': 17265, 'synset': 'thane.n.02', 'name': 'thane'}, {'id': 17266, 'synset': 'theatrical_producer.n.01', 'name': 'theatrical_producer'}, {'id': 17267, 'synset': 'theologian.n.01', 'name': 'theologian'}, {'id': 17268, 'synset': 'theorist.n.01', 'name': 'theorist'}, {'id': 17269, 'synset': 'theosophist.n.01', 'name': 'theosophist'}, {'id': 17270, 'synset': 'therapist.n.01', 'name': 'therapist'}, {'id': 17271, 'synset': 'thessalonian.n.01', 'name': 'Thessalonian'}, {'id': 17272, 'synset': 'thinker.n.01', 'name': 'thinker'}, {'id': 17273, 'synset': 'thinker.n.02', 'name': 'thinker'}, {'id': 17274, 'synset': 'thrower.n.02', 'name': 'thrower'}, {'id': 17275, 'synset': 'thurifer.n.01', 'name': 'thurifer'}, {'id': 17276, 'synset': 'ticket_collector.n.01', 'name': 'ticket_collector'}, {'id': 17277, 'synset': 'tight_end.n.01', 'name': 'tight_end'}, {'id': 17278, 'synset': 'tiler.n.01', 'name': 'tiler'}, {'id': 17279, 'synset': 'timekeeper.n.01', 'name': 'timekeeper'}, {'id': 17280, 'synset': 'timorese.n.01', 'name': 'Timorese'}, {'id': 17281, 'synset': 'tinkerer.n.01', 'name': 'tinkerer'}, {'id': 17282, 'synset': 'tinsmith.n.01', 'name': 'tinsmith'}, {'id': 17283, 'synset': 'tinter.n.01', 'name': 'tinter'}, {'id': 17284, 'synset': 'tippler.n.01', 'name': 'tippler'}, {'id': 17285, 'synset': 'tipster.n.01', 'name': 'tipster'}, {'id': 17286, 'synset': 't-man.n.01', 'name': 'T-man'}, {'id': 17287, 'synset': 'toastmaster.n.01', 'name': 'toastmaster'}, {'id': 17288, 'synset': 'toast_mistress.n.01', 'name': 'toast_mistress'}, {'id': 17289, 'synset': 'tobogganist.n.01', 'name': 'tobogganist'}, {'id': 17290, 'synset': 'tomboy.n.01', 'name': 'tomboy'}, {'id': 17291, 'synset': 'toolmaker.n.01', 'name': 'toolmaker'}, {'id': 17292, 'synset': 'torchbearer.n.01', 'name': 'torchbearer'}, {'id': 17293, 'synset': 'tory.n.01', 'name': 'Tory'}, {'id': 17294, 'synset': 'tory.n.02', 'name': 'Tory'}, {'id': 17295, 'synset': 'tosser.n.02', 'name': 'tosser'}, {'id': 17296, 'synset': 'tosser.n.01', 'name': 'tosser'}, {'id': 17297, 'synset': 'totalitarian.n.01', 'name': 'totalitarian'}, {'id': 17298, 'synset': 'tourist.n.01', 'name': 'tourist'}, {'id': 17299, 'synset': 'tout.n.02', 'name': 'tout'}, {'id': 17300, 'synset': 'tout.n.01', 'name': 'tout'}, {'id': 17301, 'synset': 'tovarich.n.01', 'name': 'tovarich'}, {'id': 17302, 'synset': 'towhead.n.01', 'name': 'towhead'}, {'id': 17303, 'synset': 'town_clerk.n.01', 'name': 'town_clerk'}, {'id': 17304, 'synset': 'town_crier.n.01', 'name': 'town_crier'}, {'id': 17305, 'synset': 'townsman.n.02', 'name': 'townsman'}, {'id': 17306, 'synset': 'toxicologist.n.01', 'name': 'toxicologist'}, {'id': 17307, 'synset': 'track_star.n.01', 'name': 'track_star'}, {'id': 17308, 'synset': 'trader.n.01', 'name': 'trader'}, {'id': 17309, 'synset': 'trade_unionist.n.01', 'name': 'trade_unionist'}, {'id': 17310, 'synset': 'traditionalist.n.01', 'name': 'traditionalist'}, {'id': 17311, 'synset': 'traffic_cop.n.01', 'name': 'traffic_cop'}, {'id': 17312, 'synset': 'tragedian.n.02', 'name': 'tragedian'}, {'id': 17313, 'synset': 'tragedian.n.01', 'name': 'tragedian'}, {'id': 17314, 'synset': 'tragedienne.n.01', 'name': 'tragedienne'}, {'id': 17315, 'synset': 'trail_boss.n.01', 'name': 'trail_boss'}, {'id': 17316, 'synset': 'trainer.n.01', 'name': 'trainer'}, {'id': 17317, 'synset': 'traitor.n.01', 'name': 'traitor'}, {'id': 17318, 'synset': 'traitress.n.01', 'name': 'traitress'}, {'id': 17319, 'synset': 'transactor.n.01', 'name': 'transactor'}, {'id': 17320, 'synset': 'transcriber.n.03', 'name': 'transcriber'}, {'id': 17321, 'synset': 'transfer.n.02', 'name': 'transfer'}, {'id': 17322, 'synset': 'transferee.n.01', 'name': 'transferee'}, {'id': 17323, 'synset': 'translator.n.01', 'name': 'translator'}, {'id': 17324, 'synset': 'transvestite.n.01', 'name': 'transvestite'}, {'id': 17325, 'synset': 'traveling_salesman.n.01', 'name': 'traveling_salesman'}, {'id': 17326, 'synset': 'traverser.n.01', 'name': 'traverser'}, {'id': 17327, 'synset': 'trawler.n.01', 'name': 'trawler'}, {'id': 17328, 'synset': 'treasury.n.04', 'name': 'Treasury'}, {'id': 17329, 'synset': 'trencher.n.01', 'name': 'trencher'}, {'id': 17330, 'synset': 'trend-setter.n.01', 'name': 'trend-setter'}, {'id': 17331, 'synset': 'tribesman.n.01', 'name': 'tribesman'}, {'id': 17332, 'synset': 'trier.n.02', 'name': 'trier'}, {'id': 17333, 'synset': 'trifler.n.01', 'name': 'trifler'}, {'id': 17334, 'synset': 'trooper.n.02', 'name': 'trooper'}, {'id': 17335, 'synset': 'trooper.n.03', 'name': 'trooper'}, {'id': 17336, 'synset': 'trotskyite.n.01', 'name': 'Trotskyite'}, {'id': 17337, 'synset': 'truant.n.01', 'name': 'truant'}, {'id': 17338, 'synset': 'trumpeter.n.01', 'name': 'trumpeter'}, {'id': 17339, 'synset': 'trusty.n.01', 'name': 'trusty'}, {'id': 17340, 'synset': 'tudor.n.03', 'name': 'Tudor'}, {'id': 17341, 'synset': 'tumbler.n.01', 'name': 'tumbler'}, {'id': 17342, 'synset': 'tutee.n.01', 'name': 'tutee'}, {'id': 17343, 'synset': 'twin.n.01', 'name': 'twin'}, {'id': 17344, 'synset': 'two-timer.n.01', 'name': 'two-timer'}, {'id': 17345, 'synset': 'tyke.n.01', 'name': 'Tyke'}, {'id': 17346, 'synset': 'tympanist.n.01', 'name': 'tympanist'}, {'id': 17347, 'synset': 'typist.n.01', 'name': 'typist'}, {'id': 17348, 'synset': 'tyrant.n.01', 'name': 'tyrant'}, {'id': 17349, 'synset': 'umpire.n.01', 'name': 'umpire'}, {'id': 17350, 'synset': 'understudy.n.01', 'name': 'understudy'}, {'id': 17351, 'synset': 'undesirable.n.01', 'name': 'undesirable'}, {'id': 17352, 'synset': 'unicyclist.n.01', 'name': 'unicyclist'}, {'id': 17353, 'synset': 'unilateralist.n.01', 'name': 'unilateralist'}, {'id': 17354, 'synset': 'unitarian.n.01', 'name': 'Unitarian'}, {'id': 17355, 'synset': 'arminian.n.01', 'name': 'Arminian'}, {'id': 17356, 'synset': 'universal_donor.n.01', 'name': 'universal_donor'}, {'id': 17357, 'synset': 'unix_guru.n.01', 'name': 'UNIX_guru'}, {'id': 17358, 'synset': 'unknown_soldier.n.01', 'name': 'Unknown_Soldier'}, {'id': 17359, 'synset': 'upsetter.n.01', 'name': 'upsetter'}, {'id': 17360, 'synset': 'upstager.n.01', 'name': 'upstager'}, {'id': 17361, 'synset': 'upstart.n.02', 'name': 'upstart'}, {'id': 17362, 'synset': 'upstart.n.01', 'name': 'upstart'}, {'id': 17363, 'synset': 'urchin.n.01', 'name': 'urchin'}, {'id': 17364, 'synset': 'urologist.n.01', 'name': 'urologist'}, {'id': 17365, 'synset': 'usherette.n.01', 'name': 'usherette'}, {'id': 17366, 'synset': 'usher.n.02', 'name': 'usher'}, {'id': 17367, 'synset': 'usurper.n.01', 'name': 'usurper'}, {'id': 17368, 'synset': 'utility_man.n.01', 'name': 'utility_man'}, {'id': 17369, 'synset': 'utilizer.n.01', 'name': 'utilizer'}, {'id': 17370, 'synset': 'utopian.n.01', 'name': 'Utopian'}, {'id': 17371, 'synset': 'uxoricide.n.01', 'name': 'uxoricide'}, {'id': 17372, 'synset': 'vacationer.n.01', 'name': 'vacationer'}, {'id': 17373, 'synset': 'valedictorian.n.01', 'name': 'valedictorian'}, {'id': 17374, 'synset': 'valley_girl.n.01', 'name': 'valley_girl'}, {'id': 17375, 'synset': 'vaulter.n.01', 'name': 'vaulter'}, {'id': 17376, 'synset': 'vegetarian.n.01', 'name': 'vegetarian'}, {'id': 17377, 'synset': 'vegan.n.01', 'name': 'vegan'}, {'id': 17378, 'synset': 'venerator.n.01', 'name': 'venerator'}, {'id': 17379, 'synset': 'venture_capitalist.n.01', 'name': 'venture_capitalist'}, {'id': 17380, 'synset': 'venturer.n.01', 'name': 'venturer'}, {'id': 17381, 'synset': 'vermin.n.01', 'name': 'vermin'}, {'id': 17382, 'synset': 'very_important_person.n.01', 'name': 'very_important_person'}, {'id': 17383, 'synset': 'vibist.n.01', 'name': 'vibist'}, {'id': 17384, 'synset': 'vicar.n.01', 'name': 'vicar'}, {'id': 17385, 'synset': 'vicar.n.03', 'name': 'vicar'}, {'id': 17386, 'synset': 'vicar-general.n.01', 'name': 'vicar-general'}, {'id': 17387, 'synset': 'vice_chancellor.n.01', 'name': 'vice_chancellor'}, {'id': 17388, 'synset': 'vicegerent.n.01', 'name': 'vicegerent'}, {'id': 17389, 'synset': 'vice_president.n.01', 'name': 'vice_president'}, {'id': 17390, 'synset': 'vice-regent.n.01', 'name': 'vice-regent'}, {'id': 17391, 'synset': 'victim.n.02', 'name': 'victim'}, {'id': 17392, 'synset': 'victorian.n.01', 'name': 'Victorian'}, {'id': 17393, 'synset': 'victualer.n.01', 'name': 'victualer'}, {'id': 17394, 'synset': 'vigilante.n.01', 'name': 'vigilante'}, {'id': 17395, 'synset': 'villager.n.01', 'name': 'villager'}, {'id': 17396, 'synset': 'vintager.n.01', 'name': 'vintager'}, {'id': 17397, 'synset': 'vintner.n.01', 'name': 'vintner'}, {'id': 17398, 'synset': 'violator.n.02', 'name': 'violator'}, {'id': 17399, 'synset': 'violator.n.01', 'name': 'violator'}, {'id': 17400, 'synset': 'violist.n.01', 'name': 'violist'}, {'id': 17401, 'synset': 'virago.n.01', 'name': 'virago'}, {'id': 17402, 'synset': 'virologist.n.01', 'name': 'virologist'}, {'id': 17403, 'synset': 'visayan.n.01', 'name': 'Visayan'}, {'id': 17404, 'synset': 'viscountess.n.01', 'name': 'viscountess'}, {'id': 17405, 'synset': 'viscount.n.01', 'name': 'viscount'}, {'id': 17406, 'synset': 'visigoth.n.01', 'name': 'Visigoth'}, {'id': 17407, 'synset': 'visionary.n.01', 'name': 'visionary'}, {'id': 17408, 'synset': 'visiting_fireman.n.01', 'name': 'visiting_fireman'}, {'id': 17409, 'synset': 'visiting_professor.n.01', 'name': 'visiting_professor'}, {'id': 17410, 'synset': 'visualizer.n.01', 'name': 'visualizer'}, {'id': 17411, 'synset': 'vixen.n.01', 'name': 'vixen'}, {'id': 17412, 'synset': 'vizier.n.01', 'name': 'vizier'}, {'id': 17413, 'synset': 'voicer.n.01', 'name': 'voicer'}, {'id': 17414, 'synset': 'volunteer.n.02', 'name': 'volunteer'}, {'id': 17415, 'synset': 'volunteer.n.01', 'name': 'volunteer'}, {'id': 17416, 'synset': 'votary.n.02', 'name': 'votary'}, {'id': 17417, 'synset': 'votary.n.01', 'name': 'votary'}, {'id': 17418, 'synset': 'vouchee.n.01', 'name': 'vouchee'}, {'id': 17419, 'synset': 'vower.n.01', 'name': 'vower'}, {'id': 17420, 'synset': 'voyager.n.01', 'name': 'voyager'}, {'id': 17421, 'synset': 'voyeur.n.01', 'name': 'voyeur'}, {'id': 17422, 'synset': 'vulcanizer.n.01', 'name': 'vulcanizer'}, {'id': 17423, 'synset': 'waffler.n.01', 'name': 'waffler'}, {'id': 17424, 'synset': 'wagnerian.n.01', 'name': 'Wagnerian'}, {'id': 17425, 'synset': 'waif.n.01', 'name': 'waif'}, {'id': 17426, 'synset': 'wailer.n.01', 'name': 'wailer'}, {'id': 17427, 'synset': 'waiter.n.01', 'name': 'waiter'}, {'id': 17428, 'synset': 'waitress.n.01', 'name': 'waitress'}, {'id': 17429, 'synset': 'walking_delegate.n.01', 'name': 'walking_delegate'}, {'id': 17430, 'synset': 'walk-on.n.01', 'name': 'walk-on'}, {'id': 17431, 'synset': 'wallah.n.01', 'name': 'wallah'}, {'id': 17432, 'synset': 'wally.n.01', 'name': 'wally'}, {'id': 17433, 'synset': 'waltzer.n.01', 'name': 'waltzer'}, {'id': 17434, 'synset': 'wanderer.n.01', 'name': 'wanderer'}, {'id': 17435, 'synset': 'wandering_jew.n.01', 'name': 'Wandering_Jew'}, {'id': 17436, 'synset': 'wanton.n.01', 'name': 'wanton'}, {'id': 17437, 'synset': 'warrantee.n.02', 'name': 'warrantee'}, {'id': 17438, 'synset': 'warrantee.n.01', 'name': 'warrantee'}, {'id': 17439, 'synset': 'washer.n.01', 'name': 'washer'}, {'id': 17440, 'synset': 'washerman.n.01', 'name': 'washerman'}, {'id': 17441, 'synset': 'washwoman.n.01', 'name': 'washwoman'}, {'id': 17442, 'synset': 'wassailer.n.01', 'name': 'wassailer'}, {'id': 17443, 'synset': 'wastrel.n.01', 'name': 'wastrel'}, {'id': 17444, 'synset': 'wave.n.09', 'name': 'Wave'}, {'id': 17445, 'synset': 'weatherman.n.01', 'name': 'weatherman'}, {'id': 17446, 'synset': 'weekend_warrior.n.02', 'name': 'weekend_warrior'}, {'id': 17447, 'synset': 'weeder.n.01', 'name': 'weeder'}, {'id': 17448, 'synset': 'welder.n.01', 'name': 'welder'}, {'id': 17449, 'synset': 'welfare_case.n.01', 'name': 'welfare_case'}, {'id': 17450, 'synset': 'westerner.n.01', 'name': 'westerner'}, {'id': 17451, 'synset': 'west-sider.n.01', 'name': 'West-sider'}, {'id': 17452, 'synset': 'wetter.n.02', 'name': 'wetter'}, {'id': 17453, 'synset': 'whaler.n.01', 'name': 'whaler'}, {'id': 17454, 'synset': 'whig.n.02', 'name': 'Whig'}, {'id': 17455, 'synset': 'whiner.n.01', 'name': 'whiner'}, {'id': 17456, 'synset': 'whipper-in.n.01', 'name': 'whipper-in'}, {'id': 17457, 'synset': 'whisperer.n.01', 'name': 'whisperer'}, {'id': 17458, 'synset': 'whiteface.n.02', 'name': 'whiteface'}, {'id': 17459, 'synset': 'carmelite.n.01', 'name': 'Carmelite'}, {'id': 17460, 'synset': 'augustinian.n.01', 'name': 'Augustinian'}, {'id': 17461, 'synset': 'white_hope.n.01', 'name': 'white_hope'}, {'id': 17462, 'synset': 'white_supremacist.n.01', 'name': 'white_supremacist'}, {'id': 17463, 'synset': 'whoremaster.n.02', 'name': 'whoremaster'}, {'id': 17464, 'synset': 'whoremaster.n.01', 'name': 'whoremaster'}, {'id': 17465, 'synset': 'widow.n.01', 'name': 'widow'}, {'id': 17466, 'synset': 'wife.n.01', 'name': 'wife'}, {'id': 17467, 'synset': 'wiggler.n.01', 'name': 'wiggler'}, {'id': 17468, 'synset': 'wimp.n.01', 'name': 'wimp'}, {'id': 17469, 'synset': 'wing_commander.n.01', 'name': 'wing_commander'}, {'id': 17470, 'synset': 'winger.n.01', 'name': 'winger'}, {'id': 17471, 'synset': 'winner.n.02', 'name': 'winner'}, {'id': 17472, 'synset': 'winner.n.01', 'name': 'winner'}, {'id': 17473, 'synset': 'window_dresser.n.01', 'name': 'window_dresser'}, {'id': 17474, 'synset': 'winker.n.01', 'name': 'winker'}, {'id': 17475, 'synset': 'wiper.n.01', 'name': 'wiper'}, {'id': 17476, 'synset': 'wireman.n.01', 'name': 'wireman'}, {'id': 17477, 'synset': 'wise_guy.n.01', 'name': 'wise_guy'}, {'id': 17478, 'synset': 'witch_doctor.n.01', 'name': 'witch_doctor'}, {'id': 17479, 'synset': 'withdrawer.n.05', 'name': 'withdrawer'}, {'id': 17480, 'synset': 'withdrawer.n.01', 'name': 'withdrawer'}, {'id': 17481, 'synset': 'woman.n.01', 'name': 'woman'}, {'id': 17482, 'synset': 'woman.n.02', 'name': 'woman'}, {'id': 17483, 'synset': 'wonder_boy.n.01', 'name': 'wonder_boy'}, {'id': 17484, 'synset': 'wonderer.n.01', 'name': 'wonderer'}, {'id': 17485, 'synset': 'working_girl.n.01', 'name': 'working_girl'}, {'id': 17486, 'synset': 'workman.n.01', 'name': 'workman'}, {'id': 17487, 'synset': 'workmate.n.01', 'name': 'workmate'}, {'id': 17488, 'synset': 'worldling.n.01', 'name': 'worldling'}, {'id': 17489, 'synset': 'worshiper.n.01', 'name': 'worshiper'}, {'id': 17490, 'synset': 'worthy.n.01', 'name': 'worthy'}, {'id': 17491, 'synset': 'wrecker.n.01', 'name': 'wrecker'}, {'id': 17492, 'synset': 'wright.n.07', 'name': 'wright'}, {'id': 17493, 'synset': 'write-in_candidate.n.01', 'name': 'write-in_candidate'}, {'id': 17494, 'synset': 'writer.n.01', 'name': 'writer'}, {'id': 17495, 'synset': 'wykehamist.n.01', 'name': 'Wykehamist'}, {'id': 17496, 'synset': 'yakuza.n.01', 'name': 'yakuza'}, {'id': 17497, 'synset': 'yard_bird.n.01', 'name': 'yard_bird'}, {'id': 17498, 'synset': 'yardie.n.01', 'name': 'yardie'}, {'id': 17499, 'synset': 'yardman.n.01', 'name': 'yardman'}, {'id': 17500, 'synset': 'yardmaster.n.01', 'name': 'yardmaster'}, {'id': 17501, 'synset': 'yenta.n.02', 'name': 'yenta'}, {'id': 17502, 'synset': 'yogi.n.02', 'name': 'yogi'}, {'id': 17503, 'synset': 'young_buck.n.01', 'name': 'young_buck'}, {'id': 17504, 'synset': 'young_turk.n.02', 'name': 'young_Turk'}, {'id': 17505, 'synset': 'young_turk.n.01', 'name': 'Young_Turk'}, {'id': 17506, 'synset': 'zionist.n.01', 'name': 'Zionist'}, {'id': 17507, 'synset': 'zoo_keeper.n.01', 'name': 'zoo_keeper'}, {'id': 17508, 'synset': 'genet.n.01', 'name': 'Genet'}, {'id': 17509, 'synset': 'kennan.n.01', 'name': 'Kennan'}, {'id': 17510, 'synset': 'munro.n.01', 'name': 'Munro'}, {'id': 17511, 'synset': 'popper.n.01', 'name': 'Popper'}, {'id': 17512, 'synset': 'stoker.n.01', 'name': 'Stoker'}, {'id': 17513, 'synset': 'townes.n.01', 'name': 'Townes'}, {'id': 17514, 'synset': 'dust_storm.n.01', 'name': 'dust_storm'}, {'id': 17515, 'synset': 'parhelion.n.01', 'name': 'parhelion'}, {'id': 17516, 'synset': 'snow.n.01', 'name': 'snow'}, {'id': 17517, 'synset': 'facula.n.01', 'name': 'facula'}, {'id': 17518, 'synset': 'wave.n.08', 'name': 'wave'}, {'id': 17519, 'synset': 'microflora.n.01', 'name': 'microflora'}, {'id': 17520, 'synset': 'wilding.n.01', 'name': 'wilding'}, {'id': 17521, 'synset': 'semi-climber.n.01', 'name': 'semi-climber'}, {'id': 17522, 'synset': 'volva.n.01', 'name': 'volva'}, {'id': 17523, 'synset': 'basidiocarp.n.01', 'name': 'basidiocarp'}, {'id': 17524, 'synset': 'domatium.n.01', 'name': 'domatium'}, {'id': 17525, 'synset': 'apomict.n.01', 'name': 'apomict'}, {'id': 17526, 'synset': 'aquatic.n.01', 'name': 'aquatic'}, {'id': 17527, 'synset': 'bryophyte.n.01', 'name': 'bryophyte'}, {'id': 17528, 'synset': 'acrocarp.n.01', 'name': 'acrocarp'}, {'id': 17529, 'synset': 'sphagnum.n.01', 'name': 'sphagnum'}, {'id': 17530, 'synset': 'liverwort.n.01', 'name': 'liverwort'}, {'id': 17531, 'synset': 'hepatica.n.02', 'name': 'hepatica'}, {'id': 17532, 'synset': 'pecopteris.n.01', 'name': 'pecopteris'}, {'id': 17533, 'synset': 'pteridophyte.n.01', 'name': 'pteridophyte'}, {'id': 17534, 'synset': 'fern.n.01', 'name': 'fern'}, {'id': 17535, 'synset': 'fern_ally.n.01', 'name': 'fern_ally'}, {'id': 17536, 'synset': 'spore.n.01', 'name': 'spore'}, {'id': 17537, 'synset': 'carpospore.n.01', 'name': 'carpospore'}, {'id': 17538, 'synset': 'chlamydospore.n.01', 'name': 'chlamydospore'}, {'id': 17539, 'synset': 'conidium.n.01', 'name': 'conidium'}, {'id': 17540, 'synset': 'oospore.n.01', 'name': 'oospore'}, {'id': 17541, 'synset': 'tetraspore.n.01', 'name': 'tetraspore'}, {'id': 17542, 'synset': 'zoospore.n.01', 'name': 'zoospore'}, {'id': 17543, 'synset': 'cryptogam.n.01', 'name': 'cryptogam'}, {'id': 17544, 'synset': 'spermatophyte.n.01', 'name': 'spermatophyte'}, {'id': 17545, 'synset': 'seedling.n.01', 'name': 'seedling'}, {'id': 17546, 'synset': 'annual.n.01', 'name': 'annual'}, {'id': 17547, 'synset': 'biennial.n.01', 'name': 'biennial'}, {'id': 17548, 'synset': 'perennial.n.01', 'name': 'perennial'}, {'id': 17549, 'synset': 'hygrophyte.n.01', 'name': 'hygrophyte'}, {'id': 17550, 'synset': 'gymnosperm.n.01', 'name': 'gymnosperm'}, {'id': 17551, 'synset': 'gnetum.n.01', 'name': 'gnetum'}, {'id': 17552, 'synset': 'catha_edulis.n.01', 'name': 'Catha_edulis'}, {'id': 17553, 'synset': 'ephedra.n.01', 'name': 'ephedra'}, {'id': 17554, 'synset': 'mahuang.n.01', 'name': 'mahuang'}, {'id': 17555, 'synset': 'welwitschia.n.01', 'name': 'welwitschia'}, {'id': 17556, 'synset': 'cycad.n.01', 'name': 'cycad'}, {'id': 17557, 'synset': 'sago_palm.n.02', 'name': 'sago_palm'}, {'id': 17558, 'synset': 'false_sago.n.01', 'name': 'false_sago'}, {'id': 17559, 'synset': 'zamia.n.01', 'name': 'zamia'}, {'id': 17560, 'synset': 'coontie.n.01', 'name': 'coontie'}, {'id': 17561, 'synset': 'ceratozamia.n.01', 'name': 'ceratozamia'}, {'id': 17562, 'synset': 'dioon.n.01', 'name': 'dioon'}, {'id': 17563, 'synset': 'encephalartos.n.01', 'name': 'encephalartos'}, {'id': 17564, 'synset': 'kaffir_bread.n.01', 'name': 'kaffir_bread'}, {'id': 17565, 'synset': 'macrozamia.n.01', 'name': 'macrozamia'}, {'id': 17566, 'synset': 'burrawong.n.01', 'name': 'burrawong'}, {'id': 17567, 'synset': 'pine.n.01', 'name': 'pine'}, {'id': 17568, 'synset': 'pinon.n.01', 'name': 'pinon'}, {'id': 17569, 'synset': 'nut_pine.n.01', 'name': 'nut_pine'}, {'id': 17570, 'synset': 'pinon_pine.n.01', 'name': 'pinon_pine'}, {'id': 17571, 'synset': 'rocky_mountain_pinon.n.01', 'name': 'Rocky_mountain_pinon'}, {'id': 17572, 'synset': 'single-leaf.n.01', 'name': 'single-leaf'}, {'id': 17573, 'synset': 'bishop_pine.n.01', 'name': 'bishop_pine'}, {'id': 17574, 'synset': 'california_single-leaf_pinyon.n.01', 'name': 'California_single-leaf_pinyon'}, {'id': 17575, 'synset': "parry's_pinyon.n.01", 'name': "Parry's_pinyon"}, {'id': 17576, 'synset': 'spruce_pine.n.04', 'name': 'spruce_pine'}, {'id': 17577, 'synset': 'black_pine.n.05', 'name': 'black_pine'}, {'id': 17578, 'synset': 'pitch_pine.n.02', 'name': 'pitch_pine'}, {'id': 17579, 'synset': 'pond_pine.n.01', 'name': 'pond_pine'}, {'id': 17580, 'synset': 'stone_pine.n.01', 'name': 'stone_pine'}, {'id': 17581, 'synset': 'swiss_pine.n.01', 'name': 'Swiss_pine'}, {'id': 17582, 'synset': 'cembra_nut.n.01', 'name': 'cembra_nut'}, {'id': 17583, 'synset': 'swiss_mountain_pine.n.01', 'name': 'Swiss_mountain_pine'}, {'id': 17584, 'synset': 'ancient_pine.n.01', 'name': 'ancient_pine'}, {'id': 17585, 'synset': 'white_pine.n.01', 'name': 'white_pine'}, {'id': 17586, 'synset': 'american_white_pine.n.01', 'name': 'American_white_pine'}, {'id': 17587, 'synset': 'western_white_pine.n.01', 'name': 'western_white_pine'}, {'id': 17588, 'synset': 'southwestern_white_pine.n.01', 'name': 'southwestern_white_pine'}, {'id': 17589, 'synset': 'limber_pine.n.01', 'name': 'limber_pine'}, {'id': 17590, 'synset': 'whitebark_pine.n.01', 'name': 'whitebark_pine'}, {'id': 17591, 'synset': 'yellow_pine.n.01', 'name': 'yellow_pine'}, {'id': 17592, 'synset': 'ponderosa.n.01', 'name': 'ponderosa'}, {'id': 17593, 'synset': 'jeffrey_pine.n.01', 'name': 'Jeffrey_pine'}, {'id': 17594, 'synset': 'shore_pine.n.01', 'name': 'shore_pine'}, {'id': 17595, 'synset': 'sierra_lodgepole_pine.n.01', 'name': 'Sierra_lodgepole_pine'}, {'id': 17596, 'synset': 'loblolly_pine.n.01', 'name': 'loblolly_pine'}, {'id': 17597, 'synset': 'jack_pine.n.01', 'name': 'jack_pine'}, {'id': 17598, 'synset': 'swamp_pine.n.01', 'name': 'swamp_pine'}, {'id': 17599, 'synset': 'longleaf_pine.n.01', 'name': 'longleaf_pine'}, {'id': 17600, 'synset': 'shortleaf_pine.n.01', 'name': 'shortleaf_pine'}, {'id': 17601, 'synset': 'red_pine.n.02', 'name': 'red_pine'}, {'id': 17602, 'synset': 'scotch_pine.n.01', 'name': 'Scotch_pine'}, {'id': 17603, 'synset': 'scrub_pine.n.01', 'name': 'scrub_pine'}, {'id': 17604, 'synset': 'monterey_pine.n.01', 'name': 'Monterey_pine'}, {'id': 17605, 'synset': 'bristlecone_pine.n.01', 'name': 'bristlecone_pine'}, {'id': 17606, 'synset': 'table-mountain_pine.n.01', 'name': 'table-mountain_pine'}, {'id': 17607, 'synset': 'knobcone_pine.n.01', 'name': 'knobcone_pine'}, {'id': 17608, 'synset': 'japanese_red_pine.n.01', 'name': 'Japanese_red_pine'}, {'id': 17609, 'synset': 'japanese_black_pine.n.01', 'name': 'Japanese_black_pine'}, {'id': 17610, 'synset': 'torrey_pine.n.01', 'name': 'Torrey_pine'}, {'id': 17611, 'synset': 'larch.n.02', 'name': 'larch'}, {'id': 17612, 'synset': 'american_larch.n.01', 'name': 'American_larch'}, {'id': 17613, 'synset': 'western_larch.n.01', 'name': 'western_larch'}, {'id': 17614, 'synset': 'subalpine_larch.n.01', 'name': 'subalpine_larch'}, {'id': 17615, 'synset': 'european_larch.n.01', 'name': 'European_larch'}, {'id': 17616, 'synset': 'siberian_larch.n.01', 'name': 'Siberian_larch'}, {'id': 17617, 'synset': 'golden_larch.n.01', 'name': 'golden_larch'}, {'id': 17618, 'synset': 'fir.n.02', 'name': 'fir'}, {'id': 17619, 'synset': 'silver_fir.n.01', 'name': 'silver_fir'}, {'id': 17620, 'synset': 'amabilis_fir.n.01', 'name': 'amabilis_fir'}, {'id': 17621, 'synset': 'european_silver_fir.n.01', 'name': 'European_silver_fir'}, {'id': 17622, 'synset': 'white_fir.n.01', 'name': 'white_fir'}, {'id': 17623, 'synset': 'balsam_fir.n.01', 'name': 'balsam_fir'}, {'id': 17624, 'synset': 'fraser_fir.n.01', 'name': 'Fraser_fir'}, {'id': 17625, 'synset': 'lowland_fir.n.01', 'name': 'lowland_fir'}, {'id': 17626, 'synset': 'alpine_fir.n.01', 'name': 'Alpine_fir'}, {'id': 17627, 'synset': 'santa_lucia_fir.n.01', 'name': 'Santa_Lucia_fir'}, {'id': 17628, 'synset': 'cedar.n.03', 'name': 'cedar'}, {'id': 17629, 'synset': 'cedar_of_lebanon.n.01', 'name': 'cedar_of_Lebanon'}, {'id': 17630, 'synset': 'deodar.n.01', 'name': 'deodar'}, {'id': 17631, 'synset': 'atlas_cedar.n.01', 'name': 'Atlas_cedar'}, {'id': 17632, 'synset': 'spruce.n.02', 'name': 'spruce'}, {'id': 17633, 'synset': 'norway_spruce.n.01', 'name': 'Norway_spruce'}, {'id': 17634, 'synset': 'weeping_spruce.n.01', 'name': 'weeping_spruce'}, {'id': 17635, 'synset': 'engelmann_spruce.n.01', 'name': 'Engelmann_spruce'}, {'id': 17636, 'synset': 'white_spruce.n.01', 'name': 'white_spruce'}, {'id': 17637, 'synset': 'black_spruce.n.01', 'name': 'black_spruce'}, {'id': 17638, 'synset': 'siberian_spruce.n.01', 'name': 'Siberian_spruce'}, {'id': 17639, 'synset': 'sitka_spruce.n.01', 'name': 'Sitka_spruce'}, {'id': 17640, 'synset': 'oriental_spruce.n.01', 'name': 'oriental_spruce'}, {'id': 17641, 'synset': 'colorado_spruce.n.01', 'name': 'Colorado_spruce'}, {'id': 17642, 'synset': 'red_spruce.n.01', 'name': 'red_spruce'}, {'id': 17643, 'synset': 'hemlock.n.04', 'name': 'hemlock'}, {'id': 17644, 'synset': 'eastern_hemlock.n.01', 'name': 'eastern_hemlock'}, {'id': 17645, 'synset': 'carolina_hemlock.n.01', 'name': 'Carolina_hemlock'}, {'id': 17646, 'synset': 'mountain_hemlock.n.01', 'name': 'mountain_hemlock'}, {'id': 17647, 'synset': 'western_hemlock.n.01', 'name': 'western_hemlock'}, {'id': 17648, 'synset': 'douglas_fir.n.02', 'name': 'douglas_fir'}, {'id': 17649, 'synset': 'green_douglas_fir.n.01', 'name': 'green_douglas_fir'}, {'id': 17650, 'synset': 'big-cone_spruce.n.01', 'name': 'big-cone_spruce'}, {'id': 17651, 'synset': 'cathaya.n.01', 'name': 'Cathaya'}, {'id': 17652, 'synset': 'cedar.n.01', 'name': 'cedar'}, {'id': 17653, 'synset': 'cypress.n.02', 'name': 'cypress'}, {'id': 17654, 'synset': 'gowen_cypress.n.01', 'name': 'gowen_cypress'}, {'id': 17655, 'synset': 'pygmy_cypress.n.01', 'name': 'pygmy_cypress'}, {'id': 17656, 'synset': 'santa_cruz_cypress.n.01', 'name': 'Santa_Cruz_cypress'}, {'id': 17657, 'synset': 'arizona_cypress.n.01', 'name': 'Arizona_cypress'}, {'id': 17658, 'synset': 'guadalupe_cypress.n.01', 'name': 'Guadalupe_cypress'}, {'id': 17659, 'synset': 'monterey_cypress.n.01', 'name': 'Monterey_cypress'}, {'id': 17660, 'synset': 'mexican_cypress.n.01', 'name': 'Mexican_cypress'}, {'id': 17661, 'synset': 'italian_cypress.n.01', 'name': 'Italian_cypress'}, {'id': 17662, 'synset': 'king_william_pine.n.01', 'name': 'King_William_pine'}, {'id': 17663, 'synset': 'chilean_cedar.n.01', 'name': 'Chilean_cedar'}, {'id': 17664, 'synset': 'incense_cedar.n.02', 'name': 'incense_cedar'}, {'id': 17665, 'synset': 'southern_white_cedar.n.01', 'name': 'southern_white_cedar'}, {'id': 17666, 'synset': 'oregon_cedar.n.01', 'name': 'Oregon_cedar'}, {'id': 17667, 'synset': 'yellow_cypress.n.01', 'name': 'yellow_cypress'}, {'id': 17668, 'synset': 'japanese_cedar.n.01', 'name': 'Japanese_cedar'}, {'id': 17669, 'synset': 'juniper_berry.n.01', 'name': 'juniper_berry'}, {'id': 17670, 'synset': 'incense_cedar.n.01', 'name': 'incense_cedar'}, {'id': 17671, 'synset': 'kawaka.n.01', 'name': 'kawaka'}, {'id': 17672, 'synset': 'pahautea.n.01', 'name': 'pahautea'}, {'id': 17673, 'synset': 'metasequoia.n.01', 'name': 'metasequoia'}, {'id': 17674, 'synset': 'arborvitae.n.01', 'name': 'arborvitae'}, {'id': 17675, 'synset': 'western_red_cedar.n.01', 'name': 'western_red_cedar'}, {'id': 17676, 'synset': 'american_arborvitae.n.01', 'name': 'American_arborvitae'}, {'id': 17677, 'synset': 'oriental_arborvitae.n.01', 'name': 'Oriental_arborvitae'}, {'id': 17678, 'synset': 'hiba_arborvitae.n.01', 'name': 'hiba_arborvitae'}, {'id': 17679, 'synset': 'keteleeria.n.01', 'name': 'keteleeria'}, {'id': 17680, 'synset': 'wollemi_pine.n.01', 'name': 'Wollemi_pine'}, {'id': 17681, 'synset': 'araucaria.n.01', 'name': 'araucaria'}, {'id': 17682, 'synset': 'monkey_puzzle.n.01', 'name': 'monkey_puzzle'}, {'id': 17683, 'synset': 'norfolk_island_pine.n.01', 'name': 'norfolk_island_pine'}, {'id': 17684, 'synset': 'new_caledonian_pine.n.01', 'name': 'new_caledonian_pine'}, {'id': 17685, 'synset': 'bunya_bunya.n.01', 'name': 'bunya_bunya'}, {'id': 17686, 'synset': 'hoop_pine.n.01', 'name': 'hoop_pine'}, {'id': 17687, 'synset': 'kauri_pine.n.01', 'name': 'kauri_pine'}, {'id': 17688, 'synset': 'kauri.n.02', 'name': 'kauri'}, {'id': 17689, 'synset': 'amboina_pine.n.01', 'name': 'amboina_pine'}, {'id': 17690, 'synset': 'dundathu_pine.n.01', 'name': 'dundathu_pine'}, {'id': 17691, 'synset': 'red_kauri.n.01', 'name': 'red_kauri'}, {'id': 17692, 'synset': 'plum-yew.n.01', 'name': 'plum-yew'}, {'id': 17693, 'synset': 'california_nutmeg.n.01', 'name': 'California_nutmeg'}, {'id': 17694, 'synset': 'stinking_cedar.n.01', 'name': 'stinking_cedar'}, {'id': 17695, 'synset': 'celery_pine.n.01', 'name': 'celery_pine'}, {'id': 17696, 'synset': 'celery_top_pine.n.01', 'name': 'celery_top_pine'}, {'id': 17697, 'synset': 'tanekaha.n.01', 'name': 'tanekaha'}, {'id': 17698, 'synset': 'alpine_celery_pine.n.01', 'name': 'Alpine_celery_pine'}, {'id': 17699, 'synset': 'yellowwood.n.02', 'name': 'yellowwood'}, {'id': 17700, 'synset': 'gymnospermous_yellowwood.n.01', 'name': 'gymnospermous_yellowwood'}, {'id': 17701, 'synset': 'podocarp.n.01', 'name': 'podocarp'}, {'id': 17702, 'synset': 'yacca.n.01', 'name': 'yacca'}, {'id': 17703, 'synset': 'brown_pine.n.01', 'name': 'brown_pine'}, {'id': 17704, 'synset': 'cape_yellowwood.n.01', 'name': 'cape_yellowwood'}, {'id': 17705, 'synset': 'south-african_yellowwood.n.01', 'name': 'South-African_yellowwood'}, {'id': 17706, 'synset': 'alpine_totara.n.01', 'name': 'alpine_totara'}, {'id': 17707, 'synset': 'totara.n.01', 'name': 'totara'}, {'id': 17708, 'synset': 'common_yellowwood.n.01', 'name': 'common_yellowwood'}, {'id': 17709, 'synset': 'kahikatea.n.01', 'name': 'kahikatea'}, {'id': 17710, 'synset': 'rimu.n.01', 'name': 'rimu'}, {'id': 17711, 'synset': 'tarwood.n.02', 'name': 'tarwood'}, {'id': 17712, 'synset': 'common_sickle_pine.n.01', 'name': 'common_sickle_pine'}, {'id': 17713, 'synset': 'yellow-leaf_sickle_pine.n.01', 'name': 'yellow-leaf_sickle_pine'}, {'id': 17714, 'synset': 'tarwood.n.01', 'name': 'tarwood'}, {'id': 17715, 'synset': 'westland_pine.n.01', 'name': 'westland_pine'}, {'id': 17716, 'synset': 'huon_pine.n.01', 'name': 'huon_pine'}, {'id': 17717, 'synset': 'chilean_rimu.n.01', 'name': 'Chilean_rimu'}, {'id': 17718, 'synset': 'mountain_rimu.n.01', 'name': 'mountain_rimu'}, {'id': 17719, 'synset': 'nagi.n.01', 'name': 'nagi'}, {'id': 17720, 'synset': 'miro.n.01', 'name': 'miro'}, {'id': 17721, 'synset': 'matai.n.01', 'name': 'matai'}, {'id': 17722, 'synset': 'plum-fruited_yew.n.01', 'name': 'plum-fruited_yew'}, {'id': 17723, 'synset': 'prince_albert_yew.n.01', 'name': 'Prince_Albert_yew'}, {'id': 17724, 'synset': 'sundacarpus_amara.n.01', 'name': 'Sundacarpus_amara'}, {'id': 17725, 'synset': 'japanese_umbrella_pine.n.01', 'name': 'Japanese_umbrella_pine'}, {'id': 17726, 'synset': 'yew.n.02', 'name': 'yew'}, {'id': 17727, 'synset': 'old_world_yew.n.01', 'name': 'Old_World_yew'}, {'id': 17728, 'synset': 'pacific_yew.n.01', 'name': 'Pacific_yew'}, {'id': 17729, 'synset': 'japanese_yew.n.01', 'name': 'Japanese_yew'}, {'id': 17730, 'synset': 'florida_yew.n.01', 'name': 'Florida_yew'}, {'id': 17731, 'synset': 'new_caledonian_yew.n.01', 'name': 'New_Caledonian_yew'}, {'id': 17732, 'synset': 'white-berry_yew.n.01', 'name': 'white-berry_yew'}, {'id': 17733, 'synset': 'ginkgo.n.01', 'name': 'ginkgo'}, {'id': 17734, 'synset': 'angiosperm.n.01', 'name': 'angiosperm'}, {'id': 17735, 'synset': 'dicot.n.01', 'name': 'dicot'}, {'id': 17736, 'synset': 'monocot.n.01', 'name': 'monocot'}, {'id': 17737, 'synset': 'floret.n.01', 'name': 'floret'}, {'id': 17738, 'synset': 'flower.n.01', 'name': 'flower'}, {'id': 17739, 'synset': 'bloomer.n.01', 'name': 'bloomer'}, {'id': 17740, 'synset': 'wildflower.n.01', 'name': 'wildflower'}, {'id': 17741, 'synset': 'apetalous_flower.n.01', 'name': 'apetalous_flower'}, {'id': 17742, 'synset': 'inflorescence.n.02', 'name': 'inflorescence'}, {'id': 17743, 'synset': 'rosebud.n.01', 'name': 'rosebud'}, {'id': 17744, 'synset': 'gynostegium.n.01', 'name': 'gynostegium'}, {'id': 17745, 'synset': 'pollinium.n.01', 'name': 'pollinium'}, {'id': 17746, 'synset': 'pistil.n.01', 'name': 'pistil'}, {'id': 17747, 'synset': 'gynobase.n.01', 'name': 'gynobase'}, {'id': 17748, 'synset': 'gynophore.n.01', 'name': 'gynophore'}, {'id': 17749, 'synset': 'stylopodium.n.01', 'name': 'stylopodium'}, {'id': 17750, 'synset': 'carpophore.n.01', 'name': 'carpophore'}, {'id': 17751, 'synset': 'cornstalk.n.01', 'name': 'cornstalk'}, {'id': 17752, 'synset': 'petiolule.n.01', 'name': 'petiolule'}, {'id': 17753, 'synset': 'mericarp.n.01', 'name': 'mericarp'}, {'id': 17754, 'synset': 'micropyle.n.01', 'name': 'micropyle'}, {'id': 17755, 'synset': 'germ_tube.n.01', 'name': 'germ_tube'}, {'id': 17756, 'synset': 'pollen_tube.n.01', 'name': 'pollen_tube'}, {'id': 17757, 'synset': 'gemma.n.01', 'name': 'gemma'}, {'id': 17758, 'synset': 'galbulus.n.01', 'name': 'galbulus'}, {'id': 17759, 'synset': 'nectary.n.01', 'name': 'nectary'}, {'id': 17760, 'synset': 'pericarp.n.01', 'name': 'pericarp'}, {'id': 17761, 'synset': 'epicarp.n.01', 'name': 'epicarp'}, {'id': 17762, 'synset': 'mesocarp.n.01', 'name': 'mesocarp'}, {'id': 17763, 'synset': 'pip.n.03', 'name': 'pip'}, {'id': 17764, 'synset': 'silique.n.01', 'name': 'silique'}, {'id': 17765, 'synset': 'cataphyll.n.01', 'name': 'cataphyll'}, {'id': 17766, 'synset': 'perisperm.n.01', 'name': 'perisperm'}, {'id': 17767, 'synset': 'monocarp.n.01', 'name': 'monocarp'}, {'id': 17768, 'synset': 'sporophyte.n.01', 'name': 'sporophyte'}, {'id': 17769, 'synset': 'gametophyte.n.01', 'name': 'gametophyte'}, {'id': 17770, 'synset': 'megasporangium.n.01', 'name': 'megasporangium'}, {'id': 17771, 'synset': 'microspore.n.01', 'name': 'microspore'}, {'id': 17772, 'synset': 'microsporangium.n.01', 'name': 'microsporangium'}, {'id': 17773, 'synset': 'microsporophyll.n.01', 'name': 'microsporophyll'}, {'id': 17774, 'synset': 'archespore.n.01', 'name': 'archespore'}, {'id': 17775, 'synset': 'bonduc_nut.n.01', 'name': 'bonduc_nut'}, {'id': 17776, 'synset': "job's_tears.n.01", 'name': "Job's_tears"}, {'id': 17777, 'synset': 'oilseed.n.01', 'name': 'oilseed'}, {'id': 17778, 'synset': 'castor_bean.n.01', 'name': 'castor_bean'}, {'id': 17779, 'synset': 'cottonseed.n.01', 'name': 'cottonseed'}, {'id': 17780, 'synset': 'candlenut.n.02', 'name': 'candlenut'}, {'id': 17781, 'synset': 'peach_pit.n.01', 'name': 'peach_pit'}, {'id': 17782, 'synset': 'hypanthium.n.01', 'name': 'hypanthium'}, {'id': 17783, 'synset': 'petal.n.01', 'name': 'petal'}, {'id': 17784, 'synset': 'corolla.n.01', 'name': 'corolla'}, {'id': 17785, 'synset': 'lip.n.02', 'name': 'lip'}, {'id': 17786, 'synset': 'perianth.n.01', 'name': 'perianth'}, {'id': 17787, 'synset': 'thistledown.n.01', 'name': 'thistledown'}, {'id': 17788, 'synset': 'custard_apple.n.01', 'name': 'custard_apple'}, {'id': 17789, 'synset': 'cherimoya.n.01', 'name': 'cherimoya'}, {'id': 17790, 'synset': 'ilama.n.01', 'name': 'ilama'}, {'id': 17791, 'synset': 'soursop.n.01', 'name': 'soursop'}, {'id': 17792, 'synset': "bullock's_heart.n.01", 'name': "bullock's_heart"}, {'id': 17793, 'synset': 'sweetsop.n.01', 'name': 'sweetsop'}, {'id': 17794, 'synset': 'pond_apple.n.01', 'name': 'pond_apple'}, {'id': 17795, 'synset': 'pawpaw.n.02', 'name': 'pawpaw'}, {'id': 17796, 'synset': 'ilang-ilang.n.02', 'name': 'ilang-ilang'}, {'id': 17797, 'synset': 'lancewood.n.02', 'name': 'lancewood'}, {'id': 17798, 'synset': 'guinea_pepper.n.02', 'name': 'Guinea_pepper'}, {'id': 17799, 'synset': 'barberry.n.01', 'name': 'barberry'}, {'id': 17800, 'synset': 'american_barberry.n.01', 'name': 'American_barberry'}, {'id': 17801, 'synset': 'common_barberry.n.01', 'name': 'common_barberry'}, {'id': 17802, 'synset': 'japanese_barberry.n.01', 'name': 'Japanese_barberry'}, {'id': 17803, 'synset': 'oregon_grape.n.02', 'name': 'Oregon_grape'}, {'id': 17804, 'synset': 'oregon_grape.n.01', 'name': 'Oregon_grape'}, {'id': 17805, 'synset': 'mayapple.n.01', 'name': 'mayapple'}, {'id': 17806, 'synset': 'may_apple.n.01', 'name': 'May_apple'}, {'id': 17807, 'synset': 'allspice.n.02', 'name': 'allspice'}, {'id': 17808, 'synset': 'carolina_allspice.n.01', 'name': 'Carolina_allspice'}, {'id': 17809, 'synset': 'spicebush.n.02', 'name': 'spicebush'}, {'id': 17810, 'synset': 'katsura_tree.n.01', 'name': 'katsura_tree'}, {'id': 17811, 'synset': 'laurel.n.01', 'name': 'laurel'}, {'id': 17812, 'synset': 'true_laurel.n.01', 'name': 'true_laurel'}, {'id': 17813, 'synset': 'camphor_tree.n.01', 'name': 'camphor_tree'}, {'id': 17814, 'synset': 'cinnamon.n.02', 'name': 'cinnamon'}, {'id': 17815, 'synset': 'cassia.n.03', 'name': 'cassia'}, {'id': 17816, 'synset': 'cassia_bark.n.01', 'name': 'cassia_bark'}, {'id': 17817, 'synset': 'saigon_cinnamon.n.01', 'name': 'Saigon_cinnamon'}, {'id': 17818, 'synset': 'cinnamon_bark.n.01', 'name': 'cinnamon_bark'}, {'id': 17819, 'synset': 'spicebush.n.01', 'name': 'spicebush'}, {'id': 17820, 'synset': 'avocado.n.02', 'name': 'avocado'}, {'id': 17821, 'synset': 'laurel-tree.n.01', 'name': 'laurel-tree'}, {'id': 17822, 'synset': 'sassafras.n.01', 'name': 'sassafras'}, {'id': 17823, 'synset': 'california_laurel.n.01', 'name': 'California_laurel'}, {'id': 17824, 'synset': 'anise_tree.n.01', 'name': 'anise_tree'}, {'id': 17825, 'synset': 'purple_anise.n.01', 'name': 'purple_anise'}, {'id': 17826, 'synset': 'star_anise.n.02', 'name': 'star_anise'}, {'id': 17827, 'synset': 'star_anise.n.01', 'name': 'star_anise'}, {'id': 17828, 'synset': 'magnolia.n.02', 'name': 'magnolia'}, {'id': 17829, 'synset': 'southern_magnolia.n.01', 'name': 'southern_magnolia'}, {'id': 17830, 'synset': 'umbrella_tree.n.02', 'name': 'umbrella_tree'}, {'id': 17831, 'synset': 'earleaved_umbrella_tree.n.01', 'name': 'earleaved_umbrella_tree'}, {'id': 17832, 'synset': 'cucumber_tree.n.01', 'name': 'cucumber_tree'}, {'id': 17833, 'synset': 'large-leaved_magnolia.n.01', 'name': 'large-leaved_magnolia'}, {'id': 17834, 'synset': 'saucer_magnolia.n.01', 'name': 'saucer_magnolia'}, {'id': 17835, 'synset': 'star_magnolia.n.01', 'name': 'star_magnolia'}, {'id': 17836, 'synset': 'sweet_bay.n.01', 'name': 'sweet_bay'}, {'id': 17837, 'synset': 'manglietia.n.01', 'name': 'manglietia'}, {'id': 17838, 'synset': 'tulip_tree.n.01', 'name': 'tulip_tree'}, {'id': 17839, 'synset': 'moonseed.n.01', 'name': 'moonseed'}, {'id': 17840, 'synset': 'common_moonseed.n.01', 'name': 'common_moonseed'}, {'id': 17841, 'synset': 'carolina_moonseed.n.01', 'name': 'Carolina_moonseed'}, {'id': 17842, 'synset': 'nutmeg.n.01', 'name': 'nutmeg'}, {'id': 17843, 'synset': 'water_nymph.n.02', 'name': 'water_nymph'}, {'id': 17844, 'synset': 'european_white_lily.n.01', 'name': 'European_white_lily'}, {'id': 17845, 'synset': 'southern_spatterdock.n.01', 'name': 'southern_spatterdock'}, {'id': 17846, 'synset': 'lotus.n.01', 'name': 'lotus'}, {'id': 17847, 'synset': 'water_chinquapin.n.01', 'name': 'water_chinquapin'}, {'id': 17848, 'synset': 'water-shield.n.02', 'name': 'water-shield'}, {'id': 17849, 'synset': 'water-shield.n.01', 'name': 'water-shield'}, {'id': 17850, 'synset': 'peony.n.01', 'name': 'peony'}, {'id': 17851, 'synset': 'buttercup.n.01', 'name': 'buttercup'}, {'id': 17852, 'synset': 'meadow_buttercup.n.01', 'name': 'meadow_buttercup'}, {'id': 17853, 'synset': 'water_crowfoot.n.01', 'name': 'water_crowfoot'}, {'id': 17854, 'synset': 'lesser_celandine.n.01', 'name': 'lesser_celandine'}, {'id': 17855, 'synset': 'lesser_spearwort.n.01', 'name': 'lesser_spearwort'}, {'id': 17856, 'synset': 'greater_spearwort.n.01', 'name': 'greater_spearwort'}, {'id': 17857, 'synset': 'western_buttercup.n.01', 'name': 'western_buttercup'}, {'id': 17858, 'synset': 'creeping_buttercup.n.01', 'name': 'creeping_buttercup'}, {'id': 17859, 'synset': 'cursed_crowfoot.n.01', 'name': 'cursed_crowfoot'}, {'id': 17860, 'synset': 'aconite.n.01', 'name': 'aconite'}, {'id': 17861, 'synset': 'monkshood.n.01', 'name': 'monkshood'}, {'id': 17862, 'synset': 'wolfsbane.n.01', 'name': 'wolfsbane'}, {'id': 17863, 'synset': 'baneberry.n.02', 'name': 'baneberry'}, {'id': 17864, 'synset': 'baneberry.n.01', 'name': 'baneberry'}, {'id': 17865, 'synset': 'red_baneberry.n.01', 'name': 'red_baneberry'}, {'id': 17866, 'synset': "pheasant's-eye.n.01", 'name': "pheasant's-eye"}, {'id': 17867, 'synset': 'anemone.n.01', 'name': 'anemone'}, {'id': 17868, 'synset': 'alpine_anemone.n.01', 'name': 'Alpine_anemone'}, {'id': 17869, 'synset': 'canada_anemone.n.01', 'name': 'Canada_anemone'}, {'id': 17870, 'synset': 'thimbleweed.n.01', 'name': 'thimbleweed'}, {'id': 17871, 'synset': 'wood_anemone.n.02', 'name': 'wood_anemone'}, {'id': 17872, 'synset': 'wood_anemone.n.01', 'name': 'wood_anemone'}, {'id': 17873, 'synset': 'longheaded_thimbleweed.n.01', 'name': 'longheaded_thimbleweed'}, {'id': 17874, 'synset': 'snowdrop_anemone.n.01', 'name': 'snowdrop_anemone'}, {'id': 17875, 'synset': 'virginia_thimbleweed.n.01', 'name': 'Virginia_thimbleweed'}, {'id': 17876, 'synset': 'rue_anemone.n.01', 'name': 'rue_anemone'}, {'id': 17877, 'synset': 'columbine.n.01', 'name': 'columbine'}, {'id': 17878, 'synset': 'meeting_house.n.01', 'name': 'meeting_house'}, {'id': 17879, 'synset': 'blue_columbine.n.01', 'name': 'blue_columbine'}, {'id': 17880, 'synset': "granny's_bonnets.n.01", 'name': "granny's_bonnets"}, {'id': 17881, 'synset': 'marsh_marigold.n.01', 'name': 'marsh_marigold'}, {'id': 17882, 'synset': 'american_bugbane.n.01', 'name': 'American_bugbane'}, {'id': 17883, 'synset': 'black_cohosh.n.01', 'name': 'black_cohosh'}, {'id': 17884, 'synset': 'fetid_bugbane.n.01', 'name': 'fetid_bugbane'}, {'id': 17885, 'synset': 'clematis.n.01', 'name': 'clematis'}, {'id': 17886, 'synset': 'pine_hyacinth.n.01', 'name': 'pine_hyacinth'}, {'id': 17887, 'synset': 'blue_jasmine.n.01', 'name': 'blue_jasmine'}, {'id': 17888, 'synset': 'golden_clematis.n.01', 'name': 'golden_clematis'}, {'id': 17889, 'synset': 'scarlet_clematis.n.01', 'name': 'scarlet_clematis'}, {'id': 17890, 'synset': 'leather_flower.n.02', 'name': 'leather_flower'}, {'id': 17891, 'synset': 'leather_flower.n.01', 'name': 'leather_flower'}, {'id': 17892, 'synset': "virgin's_bower.n.01", 'name': "virgin's_bower"}, {'id': 17893, 'synset': 'purple_clematis.n.01', 'name': 'purple_clematis'}, {'id': 17894, 'synset': 'goldthread.n.01', 'name': 'goldthread'}, {'id': 17895, 'synset': 'rocket_larkspur.n.01', 'name': 'rocket_larkspur'}, {'id': 17896, 'synset': 'delphinium.n.01', 'name': 'delphinium'}, {'id': 17897, 'synset': 'larkspur.n.01', 'name': 'larkspur'}, {'id': 17898, 'synset': 'winter_aconite.n.01', 'name': 'winter_aconite'}, {'id': 17899, 'synset': 'lenten_rose.n.01', 'name': 'lenten_rose'}, {'id': 17900, 'synset': 'green_hellebore.n.01', 'name': 'green_hellebore'}, {'id': 17901, 'synset': 'hepatica.n.01', 'name': 'hepatica'}, {'id': 17902, 'synset': 'goldenseal.n.01', 'name': 'goldenseal'}, {'id': 17903, 'synset': 'false_rue_anemone.n.01', 'name': 'false_rue_anemone'}, {'id': 17904, 'synset': 'giant_buttercup.n.01', 'name': 'giant_buttercup'}, {'id': 17905, 'synset': 'nigella.n.01', 'name': 'nigella'}, {'id': 17906, 'synset': 'love-in-a-mist.n.03', 'name': 'love-in-a-mist'}, {'id': 17907, 'synset': 'fennel_flower.n.01', 'name': 'fennel_flower'}, {'id': 17908, 'synset': 'black_caraway.n.01', 'name': 'black_caraway'}, {'id': 17909, 'synset': 'pasqueflower.n.01', 'name': 'pasqueflower'}, {'id': 17910, 'synset': 'meadow_rue.n.01', 'name': 'meadow_rue'}, {'id': 17911, 'synset': 'false_bugbane.n.01', 'name': 'false_bugbane'}, {'id': 17912, 'synset': 'globeflower.n.01', 'name': 'globeflower'}, {'id': 17913, 'synset': "winter's_bark.n.02", 'name': "winter's_bark"}, {'id': 17914, 'synset': 'pepper_shrub.n.01', 'name': 'pepper_shrub'}, {'id': 17915, 'synset': 'sweet_gale.n.01', 'name': 'sweet_gale'}, {'id': 17916, 'synset': 'wax_myrtle.n.01', 'name': 'wax_myrtle'}, {'id': 17917, 'synset': 'bay_myrtle.n.01', 'name': 'bay_myrtle'}, {'id': 17918, 'synset': 'bayberry.n.02', 'name': 'bayberry'}, {'id': 17919, 'synset': 'sweet_fern.n.02', 'name': 'sweet_fern'}, {'id': 17920, 'synset': 'corkwood.n.01', 'name': 'corkwood'}, {'id': 17921, 'synset': 'jointed_rush.n.01', 'name': 'jointed_rush'}, {'id': 17922, 'synset': 'toad_rush.n.01', 'name': 'toad_rush'}, {'id': 17923, 'synset': 'slender_rush.n.01', 'name': 'slender_rush'}, {'id': 17924, 'synset': 'zebrawood.n.02', 'name': 'zebrawood'}, {'id': 17925, 'synset': 'connarus_guianensis.n.01', 'name': 'Connarus_guianensis'}, {'id': 17926, 'synset': 'legume.n.01', 'name': 'legume'}, {'id': 17927, 'synset': 'peanut.n.01', 'name': 'peanut'}, {'id': 17928, 'synset': 'granadilla_tree.n.01', 'name': 'granadilla_tree'}, {'id': 17929, 'synset': 'arariba.n.01', 'name': 'arariba'}, {'id': 17930, 'synset': 'tonka_bean.n.01', 'name': 'tonka_bean'}, {'id': 17931, 'synset': 'courbaril.n.01', 'name': 'courbaril'}, {'id': 17932, 'synset': 'melilotus.n.01', 'name': 'melilotus'}, {'id': 17933, 'synset': 'darling_pea.n.01', 'name': 'darling_pea'}, {'id': 17934, 'synset': 'smooth_darling_pea.n.01', 'name': 'smooth_darling_pea'}, {'id': 17935, 'synset': 'clover.n.01', 'name': 'clover'}, {'id': 17936, 'synset': 'alpine_clover.n.01', 'name': 'alpine_clover'}, {'id': 17937, 'synset': 'hop_clover.n.02', 'name': 'hop_clover'}, {'id': 17938, 'synset': 'crimson_clover.n.01', 'name': 'crimson_clover'}, {'id': 17939, 'synset': 'red_clover.n.01', 'name': 'red_clover'}, {'id': 17940, 'synset': 'buffalo_clover.n.02', 'name': 'buffalo_clover'}, {'id': 17941, 'synset': 'white_clover.n.01', 'name': 'white_clover'}, {'id': 17942, 'synset': 'mimosa.n.02', 'name': 'mimosa'}, {'id': 17943, 'synset': 'acacia.n.01', 'name': 'acacia'}, {'id': 17944, 'synset': 'shittah.n.01', 'name': 'shittah'}, {'id': 17945, 'synset': 'wattle.n.03', 'name': 'wattle'}, {'id': 17946, 'synset': 'black_wattle.n.01', 'name': 'black_wattle'}, {'id': 17947, 'synset': 'gidgee.n.01', 'name': 'gidgee'}, {'id': 17948, 'synset': 'catechu.n.02', 'name': 'catechu'}, {'id': 17949, 'synset': 'silver_wattle.n.01', 'name': 'silver_wattle'}, {'id': 17950, 'synset': 'huisache.n.01', 'name': 'huisache'}, {'id': 17951, 'synset': 'lightwood.n.01', 'name': 'lightwood'}, {'id': 17952, 'synset': 'golden_wattle.n.01', 'name': 'golden_wattle'}, {'id': 17953, 'synset': 'fever_tree.n.04', 'name': 'fever_tree'}, {'id': 17954, 'synset': 'coralwood.n.01', 'name': 'coralwood'}, {'id': 17955, 'synset': 'albizzia.n.01', 'name': 'albizzia'}, {'id': 17956, 'synset': 'silk_tree.n.01', 'name': 'silk_tree'}, {'id': 17957, 'synset': 'siris.n.01', 'name': 'siris'}, {'id': 17958, 'synset': 'rain_tree.n.01', 'name': 'rain_tree'}, {'id': 17959, 'synset': 'calliandra.n.01', 'name': 'calliandra'}, {'id': 17960, 'synset': 'conacaste.n.01', 'name': 'conacaste'}, {'id': 17961, 'synset': 'inga.n.01', 'name': 'inga'}, {'id': 17962, 'synset': 'ice-cream_bean.n.01', 'name': 'ice-cream_bean'}, {'id': 17963, 'synset': 'guama.n.01', 'name': 'guama'}, {'id': 17964, 'synset': 'lead_tree.n.01', 'name': 'lead_tree'}, {'id': 17965, 'synset': 'wild_tamarind.n.02', 'name': 'wild_tamarind'}, {'id': 17966, 'synset': 'sabicu.n.02', 'name': 'sabicu'}, {'id': 17967, 'synset': 'nitta_tree.n.01', 'name': 'nitta_tree'}, {'id': 17968, 'synset': 'parkia_javanica.n.01', 'name': 'Parkia_javanica'}, {'id': 17969, 'synset': 'manila_tamarind.n.01', 'name': 'manila_tamarind'}, {'id': 17970, 'synset': "cat's-claw.n.01", 'name': "cat's-claw"}, {'id': 17971, 'synset': 'honey_mesquite.n.01', 'name': 'honey_mesquite'}, {'id': 17972, 'synset': 'algarroba.n.03', 'name': 'algarroba'}, {'id': 17973, 'synset': 'screw_bean.n.02', 'name': 'screw_bean'}, {'id': 17974, 'synset': 'screw_bean.n.01', 'name': 'screw_bean'}, {'id': 17975, 'synset': 'dogbane.n.01', 'name': 'dogbane'}, {'id': 17976, 'synset': 'indian_hemp.n.03', 'name': 'Indian_hemp'}, {'id': 17977, 'synset': "bushman's_poison.n.01", 'name': "bushman's_poison"}, {'id': 17978, 'synset': 'impala_lily.n.01', 'name': 'impala_lily'}, {'id': 17979, 'synset': 'allamanda.n.01', 'name': 'allamanda'}, {'id': 17980, 'synset': 'common_allamanda.n.01', 'name': 'common_allamanda'}, {'id': 17981, 'synset': 'dita.n.01', 'name': 'dita'}, {'id': 17982, 'synset': 'nepal_trumpet_flower.n.01', 'name': 'Nepal_trumpet_flower'}, {'id': 17983, 'synset': 'carissa.n.01', 'name': 'carissa'}, {'id': 17984, 'synset': 'hedge_thorn.n.01', 'name': 'hedge_thorn'}, {'id': 17985, 'synset': 'natal_plum.n.01', 'name': 'natal_plum'}, {'id': 17986, 'synset': 'periwinkle.n.02', 'name': 'periwinkle'}, {'id': 17987, 'synset': 'ivory_tree.n.01', 'name': 'ivory_tree'}, {'id': 17988, 'synset': 'white_dipladenia.n.01', 'name': 'white_dipladenia'}, {'id': 17989, 'synset': 'chilean_jasmine.n.01', 'name': 'Chilean_jasmine'}, {'id': 17990, 'synset': 'oleander.n.01', 'name': 'oleander'}, {'id': 17991, 'synset': 'frangipani.n.01', 'name': 'frangipani'}, {'id': 17992, 'synset': 'west_indian_jasmine.n.01', 'name': 'West_Indian_jasmine'}, {'id': 17993, 'synset': 'rauwolfia.n.02', 'name': 'rauwolfia'}, {'id': 17994, 'synset': 'snakewood.n.01', 'name': 'snakewood'}, {'id': 17995, 'synset': 'strophanthus_kombe.n.01', 'name': 'Strophanthus_kombe'}, {'id': 17996, 'synset': 'yellow_oleander.n.01', 'name': 'yellow_oleander'}, {'id': 17997, 'synset': 'myrtle.n.01', 'name': 'myrtle'}, {'id': 17998, 'synset': 'large_periwinkle.n.01', 'name': 'large_periwinkle'}, {'id': 17999, 'synset': 'arum.n.02', 'name': 'arum'}, {'id': 18000, 'synset': 'cuckoopint.n.01', 'name': 'cuckoopint'}, {'id': 18001, 'synset': 'black_calla.n.01', 'name': 'black_calla'}, {'id': 18002, 'synset': 'calamus.n.02', 'name': 'calamus'}, {'id': 18003, 'synset': 'alocasia.n.01', 'name': 'alocasia'}, {'id': 18004, 'synset': 'giant_taro.n.01', 'name': 'giant_taro'}, {'id': 18005, 'synset': 'amorphophallus.n.01', 'name': 'amorphophallus'}, {'id': 18006, 'synset': 'pungapung.n.01', 'name': 'pungapung'}, {'id': 18007, 'synset': "devil's_tongue.n.01", 'name': "devil's_tongue"}, {'id': 18008, 'synset': 'anthurium.n.01', 'name': 'anthurium'}, {'id': 18009, 'synset': 'flamingo_flower.n.01', 'name': 'flamingo_flower'}, {'id': 18010, 'synset': 'jack-in-the-pulpit.n.01', 'name': 'jack-in-the-pulpit'}, {'id': 18011, 'synset': "friar's-cowl.n.01", 'name': "friar's-cowl"}, {'id': 18012, 'synset': 'caladium.n.01', 'name': 'caladium'}, {'id': 18013, 'synset': 'caladium_bicolor.n.01', 'name': 'Caladium_bicolor'}, {'id': 18014, 'synset': 'wild_calla.n.01', 'name': 'wild_calla'}, {'id': 18015, 'synset': 'taro.n.02', 'name': 'taro'}, {'id': 18016, 'synset': 'taro.n.01', 'name': 'taro'}, {'id': 18017, 'synset': 'cryptocoryne.n.01', 'name': 'cryptocoryne'}, {'id': 18018, 'synset': 'dracontium.n.01', 'name': 'dracontium'}, {'id': 18019, 'synset': 'golden_pothos.n.01', 'name': 'golden_pothos'}, {'id': 18020, 'synset': 'skunk_cabbage.n.02', 'name': 'skunk_cabbage'}, {'id': 18021, 'synset': 'monstera.n.01', 'name': 'monstera'}, {'id': 18022, 'synset': 'ceriman.n.01', 'name': 'ceriman'}, {'id': 18023, 'synset': 'nephthytis.n.01', 'name': 'nephthytis'}, {'id': 18024, 'synset': 'nephthytis_afzelii.n.01', 'name': 'Nephthytis_afzelii'}, {'id': 18025, 'synset': 'arrow_arum.n.01', 'name': 'arrow_arum'}, {'id': 18026, 'synset': 'green_arrow_arum.n.01', 'name': 'green_arrow_arum'}, {'id': 18027, 'synset': 'philodendron.n.01', 'name': 'philodendron'}, {'id': 18028, 'synset': 'pistia.n.01', 'name': 'pistia'}, {'id': 18029, 'synset': 'pothos.n.01', 'name': 'pothos'}, {'id': 18030, 'synset': 'spathiphyllum.n.01', 'name': 'spathiphyllum'}, {'id': 18031, 'synset': 'skunk_cabbage.n.01', 'name': 'skunk_cabbage'}, {'id': 18032, 'synset': 'yautia.n.01', 'name': 'yautia'}, {'id': 18033, 'synset': 'calla_lily.n.01', 'name': 'calla_lily'}, {'id': 18034, 'synset': 'pink_calla.n.01', 'name': 'pink_calla'}, {'id': 18035, 'synset': 'golden_calla.n.01', 'name': 'golden_calla'}, {'id': 18036, 'synset': 'duckweed.n.01', 'name': 'duckweed'}, {'id': 18037, 'synset': 'common_duckweed.n.01', 'name': 'common_duckweed'}, {'id': 18038, 'synset': 'star-duckweed.n.01', 'name': 'star-duckweed'}, {'id': 18039, 'synset': 'great_duckweed.n.01', 'name': 'great_duckweed'}, {'id': 18040, 'synset': 'watermeal.n.01', 'name': 'watermeal'}, {'id': 18041, 'synset': 'common_wolffia.n.01', 'name': 'common_wolffia'}, {'id': 18042, 'synset': 'aralia.n.01', 'name': 'aralia'}, {'id': 18043, 'synset': 'american_angelica_tree.n.01', 'name': 'American_angelica_tree'}, {'id': 18044, 'synset': 'american_spikenard.n.01', 'name': 'American_spikenard'}, {'id': 18045, 'synset': 'bristly_sarsaparilla.n.01', 'name': 'bristly_sarsaparilla'}, {'id': 18046, 'synset': 'japanese_angelica_tree.n.01', 'name': 'Japanese_angelica_tree'}, {'id': 18047, 'synset': 'chinese_angelica.n.01', 'name': 'Chinese_angelica'}, {'id': 18048, 'synset': 'ivy.n.01', 'name': 'ivy'}, {'id': 18049, 'synset': 'puka.n.02', 'name': 'puka'}, {'id': 18050, 'synset': 'ginseng.n.02', 'name': 'ginseng'}, {'id': 18051, 'synset': 'ginseng.n.01', 'name': 'ginseng'}, {'id': 18052, 'synset': 'umbrella_tree.n.01', 'name': 'umbrella_tree'}, {'id': 18053, 'synset': 'birthwort.n.01', 'name': 'birthwort'}, {'id': 18054, 'synset': "dutchman's-pipe.n.01", 'name': "Dutchman's-pipe"}, {'id': 18055, 'synset': 'virginia_snakeroot.n.01', 'name': 'Virginia_snakeroot'}, {'id': 18056, 'synset': 'canada_ginger.n.01', 'name': 'Canada_ginger'}, {'id': 18057, 'synset': 'heartleaf.n.02', 'name': 'heartleaf'}, {'id': 18058, 'synset': 'heartleaf.n.01', 'name': 'heartleaf'}, {'id': 18059, 'synset': 'asarabacca.n.01', 'name': 'asarabacca'}, {'id': 18060, 'synset': 'caryophyllaceous_plant.n.01', 'name': 'caryophyllaceous_plant'}, {'id': 18061, 'synset': 'corn_cockle.n.01', 'name': 'corn_cockle'}, {'id': 18062, 'synset': 'sandwort.n.03', 'name': 'sandwort'}, {'id': 18063, 'synset': 'mountain_sandwort.n.01', 'name': 'mountain_sandwort'}, {'id': 18064, 'synset': 'pine-barren_sandwort.n.01', 'name': 'pine-barren_sandwort'}, {'id': 18065, 'synset': 'seabeach_sandwort.n.01', 'name': 'seabeach_sandwort'}, {'id': 18066, 'synset': 'rock_sandwort.n.01', 'name': 'rock_sandwort'}, {'id': 18067, 'synset': 'thyme-leaved_sandwort.n.01', 'name': 'thyme-leaved_sandwort'}, {'id': 18068, 'synset': 'mouse-ear_chickweed.n.01', 'name': 'mouse-ear_chickweed'}, {'id': 18069, 'synset': 'snow-in-summer.n.02', 'name': 'snow-in-summer'}, {'id': 18070, 'synset': 'alpine_mouse-ear.n.01', 'name': 'Alpine_mouse-ear'}, {'id': 18071, 'synset': 'pink.n.02', 'name': 'pink'}, {'id': 18072, 'synset': 'sweet_william.n.01', 'name': 'sweet_William'}, {'id': 18073, 'synset': 'china_pink.n.01', 'name': 'china_pink'}, {'id': 18074, 'synset': 'japanese_pink.n.01', 'name': 'Japanese_pink'}, {'id': 18075, 'synset': 'maiden_pink.n.01', 'name': 'maiden_pink'}, {'id': 18076, 'synset': 'cheddar_pink.n.01', 'name': 'cheddar_pink'}, {'id': 18077, 'synset': 'button_pink.n.01', 'name': 'button_pink'}, {'id': 18078, 'synset': 'cottage_pink.n.01', 'name': 'cottage_pink'}, {'id': 18079, 'synset': 'fringed_pink.n.02', 'name': 'fringed_pink'}, {'id': 18080, 'synset': 'drypis.n.01', 'name': 'drypis'}, {'id': 18081, 'synset': "baby's_breath.n.01", 'name': "baby's_breath"}, {'id': 18082, 'synset': 'coral_necklace.n.01', 'name': 'coral_necklace'}, {'id': 18083, 'synset': 'lychnis.n.01', 'name': 'lychnis'}, {'id': 18084, 'synset': 'ragged_robin.n.01', 'name': 'ragged_robin'}, {'id': 18085, 'synset': 'scarlet_lychnis.n.01', 'name': 'scarlet_lychnis'}, {'id': 18086, 'synset': 'mullein_pink.n.01', 'name': 'mullein_pink'}, {'id': 18087, 'synset': 'sandwort.n.02', 'name': 'sandwort'}, {'id': 18088, 'synset': 'sandwort.n.01', 'name': 'sandwort'}, {'id': 18089, 'synset': 'soapwort.n.01', 'name': 'soapwort'}, {'id': 18090, 'synset': 'knawel.n.01', 'name': 'knawel'}, {'id': 18091, 'synset': 'silene.n.01', 'name': 'silene'}, {'id': 18092, 'synset': 'moss_campion.n.01', 'name': 'moss_campion'}, {'id': 18093, 'synset': 'wild_pink.n.02', 'name': 'wild_pink'}, {'id': 18094, 'synset': 'red_campion.n.01', 'name': 'red_campion'}, {'id': 18095, 'synset': 'white_campion.n.01', 'name': 'white_campion'}, {'id': 18096, 'synset': 'fire_pink.n.01', 'name': 'fire_pink'}, {'id': 18097, 'synset': 'bladder_campion.n.01', 'name': 'bladder_campion'}, {'id': 18098, 'synset': 'corn_spurry.n.01', 'name': 'corn_spurry'}, {'id': 18099, 'synset': 'sand_spurry.n.01', 'name': 'sand_spurry'}, {'id': 18100, 'synset': 'chickweed.n.01', 'name': 'chickweed'}, {'id': 18101, 'synset': 'common_chickweed.n.01', 'name': 'common_chickweed'}, {'id': 18102, 'synset': 'cowherb.n.01', 'name': 'cowherb'}, {'id': 18103, 'synset': 'hottentot_fig.n.01', 'name': 'Hottentot_fig'}, {'id': 18104, 'synset': 'livingstone_daisy.n.01', 'name': 'livingstone_daisy'}, {'id': 18105, 'synset': 'fig_marigold.n.01', 'name': 'fig_marigold'}, {'id': 18106, 'synset': 'ice_plant.n.01', 'name': 'ice_plant'}, {'id': 18107, 'synset': 'new_zealand_spinach.n.01', 'name': 'New_Zealand_spinach'}, {'id': 18108, 'synset': 'amaranth.n.02', 'name': 'amaranth'}, {'id': 18109, 'synset': 'amaranth.n.01', 'name': 'amaranth'}, {'id': 18110, 'synset': 'tumbleweed.n.04', 'name': 'tumbleweed'}, {'id': 18111, 'synset': "prince's-feather.n.02", 'name': "prince's-feather"}, {'id': 18112, 'synset': 'pigweed.n.02', 'name': 'pigweed'}, {'id': 18113, 'synset': 'thorny_amaranth.n.01', 'name': 'thorny_amaranth'}, {'id': 18114, 'synset': 'alligator_weed.n.01', 'name': 'alligator_weed'}, {'id': 18115, 'synset': 'cockscomb.n.01', 'name': 'cockscomb'}, {'id': 18116, 'synset': 'cottonweed.n.02', 'name': 'cottonweed'}, {'id': 18117, 'synset': 'globe_amaranth.n.01', 'name': 'globe_amaranth'}, {'id': 18118, 'synset': 'bloodleaf.n.01', 'name': 'bloodleaf'}, {'id': 18119, 'synset': 'saltwort.n.02', 'name': 'saltwort'}, {'id': 18120, 'synset': "lamb's-quarters.n.01", 'name': "lamb's-quarters"}, {'id': 18121, 'synset': 'good-king-henry.n.01', 'name': 'good-king-henry'}, {'id': 18122, 'synset': 'jerusalem_oak.n.01', 'name': 'Jerusalem_oak'}, {'id': 18123, 'synset': 'oak-leaved_goosefoot.n.01', 'name': 'oak-leaved_goosefoot'}, {'id': 18124, 'synset': 'sowbane.n.01', 'name': 'sowbane'}, {'id': 18125, 'synset': 'nettle-leaved_goosefoot.n.01', 'name': 'nettle-leaved_goosefoot'}, {'id': 18126, 'synset': 'red_goosefoot.n.01', 'name': 'red_goosefoot'}, {'id': 18127, 'synset': 'stinking_goosefoot.n.01', 'name': 'stinking_goosefoot'}, {'id': 18128, 'synset': 'orach.n.01', 'name': 'orach'}, {'id': 18129, 'synset': 'saltbush.n.01', 'name': 'saltbush'}, {'id': 18130, 'synset': 'garden_orache.n.01', 'name': 'garden_orache'}, {'id': 18131, 'synset': 'desert_holly.n.01', 'name': 'desert_holly'}, {'id': 18132, 'synset': 'quail_bush.n.01', 'name': 'quail_bush'}, {'id': 18133, 'synset': 'beet.n.01', 'name': 'beet'}, {'id': 18134, 'synset': 'beetroot.n.01', 'name': 'beetroot'}, {'id': 18135, 'synset': 'chard.n.01', 'name': 'chard'}, {'id': 18136, 'synset': 'mangel-wurzel.n.01', 'name': 'mangel-wurzel'}, {'id': 18137, 'synset': 'winged_pigweed.n.01', 'name': 'winged_pigweed'}, {'id': 18138, 'synset': 'halogeton.n.01', 'name': 'halogeton'}, {'id': 18139, 'synset': 'glasswort.n.02', 'name': 'glasswort'}, {'id': 18140, 'synset': 'saltwort.n.01', 'name': 'saltwort'}, {'id': 18141, 'synset': 'russian_thistle.n.01', 'name': 'Russian_thistle'}, {'id': 18142, 'synset': 'greasewood.n.01', 'name': 'greasewood'}, {'id': 18143, 'synset': 'scarlet_musk_flower.n.01', 'name': 'scarlet_musk_flower'}, {'id': 18144, 'synset': 'sand_verbena.n.01', 'name': 'sand_verbena'}, {'id': 18145, 'synset': 'sweet_sand_verbena.n.01', 'name': 'sweet_sand_verbena'}, {'id': 18146, 'synset': 'yellow_sand_verbena.n.01', 'name': 'yellow_sand_verbena'}, {'id': 18147, 'synset': 'beach_pancake.n.01', 'name': 'beach_pancake'}, {'id': 18148, 'synset': 'beach_sand_verbena.n.01', 'name': 'beach_sand_verbena'}, {'id': 18149, 'synset': 'desert_sand_verbena.n.01', 'name': 'desert_sand_verbena'}, {'id': 18150, 'synset': "trailing_four_o'clock.n.01", 'name': "trailing_four_o'clock"}, {'id': 18151, 'synset': 'bougainvillea.n.01', 'name': 'bougainvillea'}, {'id': 18152, 'synset': 'umbrellawort.n.01', 'name': 'umbrellawort'}, {'id': 18153, 'synset': "four_o'clock.n.01", 'name': "four_o'clock"}, {'id': 18154, 'synset': "common_four-o'clock.n.01", 'name': "common_four-o'clock"}, {'id': 18155, 'synset': "california_four_o'clock.n.01", 'name': "California_four_o'clock"}, {'id': 18156, 'synset': "sweet_four_o'clock.n.01", 'name': "sweet_four_o'clock"}, {'id': 18157, 'synset': "desert_four_o'clock.n.01", 'name': "desert_four_o'clock"}, {'id': 18158, 'synset': "mountain_four_o'clock.n.01", 'name': "mountain_four_o'clock"}, {'id': 18159, 'synset': 'cockspur.n.02', 'name': 'cockspur'}, {'id': 18160, 'synset': 'rattail_cactus.n.01', 'name': 'rattail_cactus'}, {'id': 18161, 'synset': 'saguaro.n.01', 'name': 'saguaro'}, {'id': 18162, 'synset': 'night-blooming_cereus.n.03', 'name': 'night-blooming_cereus'}, {'id': 18163, 'synset': 'echinocactus.n.01', 'name': 'echinocactus'}, {'id': 18164, 'synset': 'hedgehog_cactus.n.01', 'name': 'hedgehog_cactus'}, {'id': 18165, 'synset': 'golden_barrel_cactus.n.01', 'name': 'golden_barrel_cactus'}, {'id': 18166, 'synset': 'hedgehog_cereus.n.01', 'name': 'hedgehog_cereus'}, {'id': 18167, 'synset': 'rainbow_cactus.n.01', 'name': 'rainbow_cactus'}, {'id': 18168, 'synset': 'epiphyllum.n.01', 'name': 'epiphyllum'}, {'id': 18169, 'synset': 'barrel_cactus.n.01', 'name': 'barrel_cactus'}, {'id': 18170, 'synset': 'night-blooming_cereus.n.02', 'name': 'night-blooming_cereus'}, {'id': 18171, 'synset': 'chichipe.n.01', 'name': 'chichipe'}, {'id': 18172, 'synset': 'mescal.n.01', 'name': 'mescal'}, {'id': 18173, 'synset': 'mescal_button.n.01', 'name': 'mescal_button'}, {'id': 18174, 'synset': 'mammillaria.n.01', 'name': 'mammillaria'}, {'id': 18175, 'synset': 'feather_ball.n.01', 'name': 'feather_ball'}, {'id': 18176, 'synset': 'garambulla.n.01', 'name': 'garambulla'}, {'id': 18177, 'synset': "knowlton's_cactus.n.01", 'name': "Knowlton's_cactus"}, {'id': 18178, 'synset': 'nopal.n.02', 'name': 'nopal'}, {'id': 18179, 'synset': 'prickly_pear.n.01', 'name': 'prickly_pear'}, {'id': 18180, 'synset': 'cholla.n.01', 'name': 'cholla'}, {'id': 18181, 'synset': 'nopal.n.01', 'name': 'nopal'}, {'id': 18182, 'synset': 'tuna.n.01', 'name': 'tuna'}, {'id': 18183, 'synset': 'barbados_gooseberry.n.01', 'name': 'Barbados_gooseberry'}, {'id': 18184, 'synset': 'mistletoe_cactus.n.01', 'name': 'mistletoe_cactus'}, {'id': 18185, 'synset': 'christmas_cactus.n.01', 'name': 'Christmas_cactus'}, {'id': 18186, 'synset': 'night-blooming_cereus.n.01', 'name': 'night-blooming_cereus'}, {'id': 18187, 'synset': 'crab_cactus.n.01', 'name': 'crab_cactus'}, {'id': 18188, 'synset': 'pokeweed.n.01', 'name': 'pokeweed'}, {'id': 18189, 'synset': 'indian_poke.n.02', 'name': 'Indian_poke'}, {'id': 18190, 'synset': 'poke.n.01', 'name': 'poke'}, {'id': 18191, 'synset': 'ombu.n.01', 'name': 'ombu'}, {'id': 18192, 'synset': 'bloodberry.n.01', 'name': 'bloodberry'}, {'id': 18193, 'synset': 'portulaca.n.01', 'name': 'portulaca'}, {'id': 18194, 'synset': 'rose_moss.n.01', 'name': 'rose_moss'}, {'id': 18195, 'synset': 'common_purslane.n.01', 'name': 'common_purslane'}, {'id': 18196, 'synset': 'rock_purslane.n.01', 'name': 'rock_purslane'}, {'id': 18197, 'synset': 'red_maids.n.01', 'name': 'red_maids'}, {'id': 18198, 'synset': 'carolina_spring_beauty.n.01', 'name': 'Carolina_spring_beauty'}, {'id': 18199, 'synset': 'spring_beauty.n.01', 'name': 'spring_beauty'}, {'id': 18200, 'synset': 'virginia_spring_beauty.n.01', 'name': 'Virginia_spring_beauty'}, {'id': 18201, 'synset': 'siskiyou_lewisia.n.01', 'name': 'siskiyou_lewisia'}, {'id': 18202, 'synset': 'bitterroot.n.01', 'name': 'bitterroot'}, {'id': 18203, 'synset': 'broad-leaved_montia.n.01', 'name': 'broad-leaved_montia'}, {'id': 18204, 'synset': 'blinks.n.01', 'name': 'blinks'}, {'id': 18205, 'synset': 'toad_lily.n.01', 'name': 'toad_lily'}, {'id': 18206, 'synset': 'winter_purslane.n.01', 'name': 'winter_purslane'}, {'id': 18207, 'synset': 'flame_flower.n.02', 'name': 'flame_flower'}, {'id': 18208, 'synset': 'pigmy_talinum.n.01', 'name': 'pigmy_talinum'}, {'id': 18209, 'synset': 'jewels-of-opar.n.01', 'name': 'jewels-of-opar'}, {'id': 18210, 'synset': 'caper.n.01', 'name': 'caper'}, {'id': 18211, 'synset': 'native_pomegranate.n.01', 'name': 'native_pomegranate'}, {'id': 18212, 'synset': 'caper_tree.n.02', 'name': 'caper_tree'}, {'id': 18213, 'synset': 'caper_tree.n.01', 'name': 'caper_tree'}, {'id': 18214, 'synset': 'common_caper.n.01', 'name': 'common_caper'}, {'id': 18215, 'synset': 'spiderflower.n.01', 'name': 'spiderflower'}, {'id': 18216, 'synset': 'rocky_mountain_bee_plant.n.01', 'name': 'Rocky_Mountain_bee_plant'}, {'id': 18217, 'synset': 'clammyweed.n.01', 'name': 'clammyweed'}, {'id': 18218, 'synset': 'crucifer.n.01', 'name': 'crucifer'}, {'id': 18219, 'synset': 'cress.n.01', 'name': 'cress'}, {'id': 18220, 'synset': 'watercress.n.01', 'name': 'watercress'}, {'id': 18221, 'synset': 'stonecress.n.01', 'name': 'stonecress'}, {'id': 18222, 'synset': 'garlic_mustard.n.01', 'name': 'garlic_mustard'}, {'id': 18223, 'synset': 'alyssum.n.01', 'name': 'alyssum'}, {'id': 18224, 'synset': 'rose_of_jericho.n.02', 'name': 'rose_of_Jericho'}, {'id': 18225, 'synset': 'arabidopsis_thaliana.n.01', 'name': 'Arabidopsis_thaliana'}, {'id': 18226, 'synset': 'arabidopsis_lyrata.n.01', 'name': 'Arabidopsis_lyrata'}, {'id': 18227, 'synset': 'rock_cress.n.01', 'name': 'rock_cress'}, {'id': 18228, 'synset': 'sicklepod.n.02', 'name': 'sicklepod'}, {'id': 18229, 'synset': 'tower_mustard.n.01', 'name': 'tower_mustard'}, {'id': 18230, 'synset': 'horseradish.n.01', 'name': 'horseradish'}, {'id': 18231, 'synset': 'winter_cress.n.01', 'name': 'winter_cress'}, {'id': 18232, 'synset': 'yellow_rocket.n.01', 'name': 'yellow_rocket'}, {'id': 18233, 'synset': 'hoary_alison.n.01', 'name': 'hoary_alison'}, {'id': 18234, 'synset': 'buckler_mustard.n.01', 'name': 'buckler_mustard'}, {'id': 18235, 'synset': 'wild_cabbage.n.01', 'name': 'wild_cabbage'}, {'id': 18236, 'synset': 'cabbage.n.03', 'name': 'cabbage'}, {'id': 18237, 'synset': 'head_cabbage.n.01', 'name': 'head_cabbage'}, {'id': 18238, 'synset': 'savoy_cabbage.n.01', 'name': 'savoy_cabbage'}, {'id': 18239, 'synset': 'brussels_sprout.n.01', 'name': 'brussels_sprout'}, {'id': 18240, 'synset': 'cauliflower.n.01', 'name': 'cauliflower'}, {'id': 18241, 'synset': 'collard.n.01', 'name': 'collard'}, {'id': 18242, 'synset': 'kohlrabi.n.01', 'name': 'kohlrabi'}, {'id': 18243, 'synset': 'turnip_plant.n.01', 'name': 'turnip_plant'}, {'id': 18244, 'synset': 'rutabaga.n.02', 'name': 'rutabaga'}, {'id': 18245, 'synset': 'broccoli_raab.n.01', 'name': 'broccoli_raab'}, {'id': 18246, 'synset': 'mustard.n.01', 'name': 'mustard'}, {'id': 18247, 'synset': 'chinese_mustard.n.01', 'name': 'chinese_mustard'}, {'id': 18248, 'synset': 'bok_choy.n.01', 'name': 'bok_choy'}, {'id': 18249, 'synset': 'rape.n.01', 'name': 'rape'}, {'id': 18250, 'synset': 'rapeseed.n.01', 'name': 'rapeseed'}, {'id': 18251, 'synset': "shepherd's_purse.n.01", 'name': "shepherd's_purse"}, {'id': 18252, 'synset': "lady's_smock.n.01", 'name': "lady's_smock"}, {'id': 18253, 'synset': 'coral-root_bittercress.n.01', 'name': 'coral-root_bittercress'}, {'id': 18254, 'synset': 'crinkleroot.n.01', 'name': 'crinkleroot'}, {'id': 18255, 'synset': 'american_watercress.n.01', 'name': 'American_watercress'}, {'id': 18256, 'synset': 'spring_cress.n.01', 'name': 'spring_cress'}, {'id': 18257, 'synset': 'purple_cress.n.01', 'name': 'purple_cress'}, {'id': 18258, 'synset': 'wallflower.n.02', 'name': 'wallflower'}, {'id': 18259, 'synset': 'prairie_rocket.n.02', 'name': 'prairie_rocket'}, {'id': 18260, 'synset': 'scurvy_grass.n.01', 'name': 'scurvy_grass'}, {'id': 18261, 'synset': 'sea_kale.n.01', 'name': 'sea_kale'}, {'id': 18262, 'synset': 'tansy_mustard.n.01', 'name': 'tansy_mustard'}, {'id': 18263, 'synset': 'draba.n.01', 'name': 'draba'}, {'id': 18264, 'synset': 'wallflower.n.01', 'name': 'wallflower'}, {'id': 18265, 'synset': 'prairie_rocket.n.01', 'name': 'prairie_rocket'}, {'id': 18266, 'synset': 'siberian_wall_flower.n.01', 'name': 'Siberian_wall_flower'}, {'id': 18267, 'synset': 'western_wall_flower.n.01', 'name': 'western_wall_flower'}, {'id': 18268, 'synset': 'wormseed_mustard.n.01', 'name': 'wormseed_mustard'}, {'id': 18269, 'synset': 'heliophila.n.01', 'name': 'heliophila'}, {'id': 18270, 'synset': 'damask_violet.n.01', 'name': 'damask_violet'}, {'id': 18271, 'synset': 'tansy-leaved_rocket.n.01', 'name': 'tansy-leaved_rocket'}, {'id': 18272, 'synset': 'candytuft.n.01', 'name': 'candytuft'}, {'id': 18273, 'synset': 'woad.n.02', 'name': 'woad'}, {'id': 18274, 'synset': "dyer's_woad.n.01", 'name': "dyer's_woad"}, {'id': 18275, 'synset': 'bladderpod.n.04', 'name': 'bladderpod'}, {'id': 18276, 'synset': 'sweet_alyssum.n.01', 'name': 'sweet_alyssum'}, {'id': 18277, 'synset': 'malcolm_stock.n.01', 'name': 'Malcolm_stock'}, {'id': 18278, 'synset': 'virginian_stock.n.01', 'name': 'Virginian_stock'}, {'id': 18279, 'synset': 'stock.n.12', 'name': 'stock'}, {'id': 18280, 'synset': 'brompton_stock.n.01', 'name': 'brompton_stock'}, {'id': 18281, 'synset': 'bladderpod.n.03', 'name': 'bladderpod'}, {'id': 18282, 'synset': 'chamois_cress.n.01', 'name': 'chamois_cress'}, {'id': 18283, 'synset': 'radish_plant.n.01', 'name': 'radish_plant'}, {'id': 18284, 'synset': 'jointed_charlock.n.01', 'name': 'jointed_charlock'}, {'id': 18285, 'synset': 'radish.n.04', 'name': 'radish'}, {'id': 18286, 'synset': 'radish.n.02', 'name': 'radish'}, {'id': 18287, 'synset': 'marsh_cress.n.01', 'name': 'marsh_cress'}, {'id': 18288, 'synset': 'great_yellowcress.n.01', 'name': 'great_yellowcress'}, {'id': 18289, 'synset': 'schizopetalon.n.01', 'name': 'schizopetalon'}, {'id': 18290, 'synset': 'field_mustard.n.01', 'name': 'field_mustard'}, {'id': 18291, 'synset': 'hedge_mustard.n.01', 'name': 'hedge_mustard'}, {'id': 18292, 'synset': 'desert_plume.n.01', 'name': 'desert_plume'}, {'id': 18293, 'synset': 'pennycress.n.01', 'name': 'pennycress'}, {'id': 18294, 'synset': 'field_pennycress.n.01', 'name': 'field_pennycress'}, {'id': 18295, 'synset': 'fringepod.n.01', 'name': 'fringepod'}, {'id': 18296, 'synset': 'bladderpod.n.02', 'name': 'bladderpod'}, {'id': 18297, 'synset': 'wasabi.n.01', 'name': 'wasabi'}, {'id': 18298, 'synset': 'poppy.n.01', 'name': 'poppy'}, {'id': 18299, 'synset': 'iceland_poppy.n.02', 'name': 'Iceland_poppy'}, {'id': 18300, 'synset': 'western_poppy.n.01', 'name': 'western_poppy'}, {'id': 18301, 'synset': 'prickly_poppy.n.02', 'name': 'prickly_poppy'}, {'id': 18302, 'synset': 'iceland_poppy.n.01', 'name': 'Iceland_poppy'}, {'id': 18303, 'synset': 'oriental_poppy.n.01', 'name': 'oriental_poppy'}, {'id': 18304, 'synset': 'corn_poppy.n.01', 'name': 'corn_poppy'}, {'id': 18305, 'synset': 'opium_poppy.n.01', 'name': 'opium_poppy'}, {'id': 18306, 'synset': 'prickly_poppy.n.01', 'name': 'prickly_poppy'}, {'id': 18307, 'synset': 'mexican_poppy.n.01', 'name': 'Mexican_poppy'}, {'id': 18308, 'synset': 'bocconia.n.02', 'name': 'bocconia'}, {'id': 18309, 'synset': 'celandine.n.02', 'name': 'celandine'}, {'id': 18310, 'synset': 'corydalis.n.01', 'name': 'corydalis'}, {'id': 18311, 'synset': 'climbing_corydalis.n.01', 'name': 'climbing_corydalis'}, {'id': 18312, 'synset': 'california_poppy.n.01', 'name': 'California_poppy'}, {'id': 18313, 'synset': 'horn_poppy.n.01', 'name': 'horn_poppy'}, {'id': 18314, 'synset': 'golden_cup.n.01', 'name': 'golden_cup'}, {'id': 18315, 'synset': 'plume_poppy.n.01', 'name': 'plume_poppy'}, {'id': 18316, 'synset': 'blue_poppy.n.01', 'name': 'blue_poppy'}, {'id': 18317, 'synset': 'welsh_poppy.n.01', 'name': 'Welsh_poppy'}, {'id': 18318, 'synset': 'creamcups.n.01', 'name': 'creamcups'}, {'id': 18319, 'synset': 'matilija_poppy.n.01', 'name': 'matilija_poppy'}, {'id': 18320, 'synset': 'wind_poppy.n.01', 'name': 'wind_poppy'}, {'id': 18321, 'synset': 'celandine_poppy.n.01', 'name': 'celandine_poppy'}, {'id': 18322, 'synset': 'climbing_fumitory.n.01', 'name': 'climbing_fumitory'}, {'id': 18323, 'synset': 'bleeding_heart.n.01', 'name': 'bleeding_heart'}, {'id': 18324, 'synset': "dutchman's_breeches.n.01", 'name': "Dutchman's_breeches"}, {'id': 18325, 'synset': 'squirrel_corn.n.01', 'name': 'squirrel_corn'}, {'id': 18326, 'synset': 'composite.n.02', 'name': 'composite'}, {'id': 18327, 'synset': 'compass_plant.n.02', 'name': 'compass_plant'}, {'id': 18328, 'synset': 'everlasting.n.01', 'name': 'everlasting'}, {'id': 18329, 'synset': 'achillea.n.01', 'name': 'achillea'}, {'id': 18330, 'synset': 'yarrow.n.01', 'name': 'yarrow'}, {'id': 18331, 'synset': 'pink-and-white_everlasting.n.01', 'name': 'pink-and-white_everlasting'}, {'id': 18332, 'synset': 'white_snakeroot.n.01', 'name': 'white_snakeroot'}, {'id': 18333, 'synset': 'ageratum.n.02', 'name': 'ageratum'}, {'id': 18334, 'synset': 'common_ageratum.n.01', 'name': 'common_ageratum'}, {'id': 18335, 'synset': 'sweet_sultan.n.03', 'name': 'sweet_sultan'}, {'id': 18336, 'synset': 'ragweed.n.02', 'name': 'ragweed'}, {'id': 18337, 'synset': 'common_ragweed.n.01', 'name': 'common_ragweed'}, {'id': 18338, 'synset': 'great_ragweed.n.01', 'name': 'great_ragweed'}, {'id': 18339, 'synset': 'western_ragweed.n.01', 'name': 'western_ragweed'}, {'id': 18340, 'synset': 'ammobium.n.01', 'name': 'ammobium'}, {'id': 18341, 'synset': 'winged_everlasting.n.01', 'name': 'winged_everlasting'}, {'id': 18342, 'synset': 'pellitory.n.02', 'name': 'pellitory'}, {'id': 18343, 'synset': 'pearly_everlasting.n.01', 'name': 'pearly_everlasting'}, {'id': 18344, 'synset': 'andryala.n.01', 'name': 'andryala'}, {'id': 18345, 'synset': 'plantain-leaved_pussytoes.n.01', 'name': 'plantain-leaved_pussytoes'}, {'id': 18346, 'synset': 'field_pussytoes.n.01', 'name': 'field_pussytoes'}, {'id': 18347, 'synset': 'solitary_pussytoes.n.01', 'name': 'solitary_pussytoes'}, {'id': 18348, 'synset': 'mountain_everlasting.n.01', 'name': 'mountain_everlasting'}, {'id': 18349, 'synset': 'mayweed.n.01', 'name': 'mayweed'}, {'id': 18350, 'synset': 'yellow_chamomile.n.01', 'name': 'yellow_chamomile'}, {'id': 18351, 'synset': 'corn_chamomile.n.01', 'name': 'corn_chamomile'}, {'id': 18352, 'synset': 'woolly_daisy.n.01', 'name': 'woolly_daisy'}, {'id': 18353, 'synset': 'burdock.n.01', 'name': 'burdock'}, {'id': 18354, 'synset': 'great_burdock.n.01', 'name': 'great_burdock'}, {'id': 18355, 'synset': 'african_daisy.n.03', 'name': 'African_daisy'}, {'id': 18356, 'synset': 'blue-eyed_african_daisy.n.01', 'name': 'blue-eyed_African_daisy'}, {'id': 18357, 'synset': 'marguerite.n.02', 'name': 'marguerite'}, {'id': 18358, 'synset': 'silversword.n.01', 'name': 'silversword'}, {'id': 18359, 'synset': 'arnica.n.02', 'name': 'arnica'}, {'id': 18360, 'synset': 'heartleaf_arnica.n.01', 'name': 'heartleaf_arnica'}, {'id': 18361, 'synset': 'arnica_montana.n.01', 'name': 'Arnica_montana'}, {'id': 18362, 'synset': 'lamb_succory.n.01', 'name': 'lamb_succory'}, {'id': 18363, 'synset': 'artemisia.n.01', 'name': 'artemisia'}, {'id': 18364, 'synset': 'mugwort.n.01', 'name': 'mugwort'}, {'id': 18365, 'synset': 'sweet_wormwood.n.01', 'name': 'sweet_wormwood'}, {'id': 18366, 'synset': 'field_wormwood.n.01', 'name': 'field_wormwood'}, {'id': 18367, 'synset': 'tarragon.n.01', 'name': 'tarragon'}, {'id': 18368, 'synset': 'sand_sage.n.01', 'name': 'sand_sage'}, {'id': 18369, 'synset': 'wormwood_sage.n.01', 'name': 'wormwood_sage'}, {'id': 18370, 'synset': 'western_mugwort.n.01', 'name': 'western_mugwort'}, {'id': 18371, 'synset': 'roman_wormwood.n.01', 'name': 'Roman_wormwood'}, {'id': 18372, 'synset': 'bud_brush.n.01', 'name': 'bud_brush'}, {'id': 18373, 'synset': 'common_mugwort.n.01', 'name': 'common_mugwort'}, {'id': 18374, 'synset': 'aster.n.01', 'name': 'aster'}, {'id': 18375, 'synset': 'wood_aster.n.01', 'name': 'wood_aster'}, {'id': 18376, 'synset': 'whorled_aster.n.01', 'name': 'whorled_aster'}, {'id': 18377, 'synset': 'heath_aster.n.02', 'name': 'heath_aster'}, {'id': 18378, 'synset': 'heart-leaved_aster.n.01', 'name': 'heart-leaved_aster'}, {'id': 18379, 'synset': 'white_wood_aster.n.01', 'name': 'white_wood_aster'}, {'id': 18380, 'synset': 'bushy_aster.n.01', 'name': 'bushy_aster'}, {'id': 18381, 'synset': 'heath_aster.n.01', 'name': 'heath_aster'}, {'id': 18382, 'synset': 'white_prairie_aster.n.01', 'name': 'white_prairie_aster'}, {'id': 18383, 'synset': 'stiff_aster.n.01', 'name': 'stiff_aster'}, {'id': 18384, 'synset': 'goldilocks.n.01', 'name': 'goldilocks'}, {'id': 18385, 'synset': 'large-leaved_aster.n.01', 'name': 'large-leaved_aster'}, {'id': 18386, 'synset': 'new_england_aster.n.01', 'name': 'New_England_aster'}, {'id': 18387, 'synset': 'michaelmas_daisy.n.01', 'name': 'Michaelmas_daisy'}, {'id': 18388, 'synset': 'upland_white_aster.n.01', 'name': 'upland_white_aster'}, {'id': 18389, 'synset': "short's_aster.n.01", 'name': "Short's_aster"}, {'id': 18390, 'synset': 'sea_aster.n.01', 'name': 'sea_aster'}, {'id': 18391, 'synset': 'prairie_aster.n.01', 'name': 'prairie_aster'}, {'id': 18392, 'synset': 'annual_salt-marsh_aster.n.01', 'name': 'annual_salt-marsh_aster'}, {'id': 18393, 'synset': 'aromatic_aster.n.01', 'name': 'aromatic_aster'}, {'id': 18394, 'synset': 'arrow_leaved_aster.n.01', 'name': 'arrow_leaved_aster'}, {'id': 18395, 'synset': 'azure_aster.n.01', 'name': 'azure_aster'}, {'id': 18396, 'synset': 'bog_aster.n.01', 'name': 'bog_aster'}, {'id': 18397, 'synset': 'crooked-stemmed_aster.n.01', 'name': 'crooked-stemmed_aster'}, {'id': 18398, 'synset': 'eastern_silvery_aster.n.01', 'name': 'Eastern_silvery_aster'}, {'id': 18399, 'synset': 'flat-topped_white_aster.n.01', 'name': 'flat-topped_white_aster'}, {'id': 18400, 'synset': 'late_purple_aster.n.01', 'name': 'late_purple_aster'}, {'id': 18401, 'synset': 'panicled_aster.n.01', 'name': 'panicled_aster'}, {'id': 18402, 'synset': 'perennial_salt_marsh_aster.n.01', 'name': 'perennial_salt_marsh_aster'}, {'id': 18403, 'synset': 'purple-stemmed_aster.n.01', 'name': 'purple-stemmed_aster'}, {'id': 18404, 'synset': 'rough-leaved_aster.n.01', 'name': 'rough-leaved_aster'}, {'id': 18405, 'synset': 'rush_aster.n.01', 'name': 'rush_aster'}, {'id': 18406, 'synset': "schreiber's_aster.n.01", 'name': "Schreiber's_aster"}, {'id': 18407, 'synset': 'small_white_aster.n.01', 'name': 'small_white_aster'}, {'id': 18408, 'synset': 'smooth_aster.n.01', 'name': 'smooth_aster'}, {'id': 18409, 'synset': 'southern_aster.n.01', 'name': 'southern_aster'}, {'id': 18410, 'synset': 'starved_aster.n.01', 'name': 'starved_aster'}, {'id': 18411, 'synset': "tradescant's_aster.n.01", 'name': "tradescant's_aster"}, {'id': 18412, 'synset': 'wavy-leaved_aster.n.01', 'name': 'wavy-leaved_aster'}, {'id': 18413, 'synset': 'western_silvery_aster.n.01', 'name': 'Western_silvery_aster'}, {'id': 18414, 'synset': 'willow_aster.n.01', 'name': 'willow_aster'}, {'id': 18415, 'synset': 'ayapana.n.01', 'name': 'ayapana'}, {'id': 18416, 'synset': 'mule_fat.n.01', 'name': 'mule_fat'}, {'id': 18417, 'synset': 'balsamroot.n.01', 'name': 'balsamroot'}, {'id': 18418, 'synset': 'daisy.n.01', 'name': 'daisy'}, {'id': 18419, 'synset': 'common_daisy.n.01', 'name': 'common_daisy'}, {'id': 18420, 'synset': 'bur_marigold.n.01', 'name': 'bur_marigold'}, {'id': 18421, 'synset': 'spanish_needles.n.02', 'name': 'Spanish_needles'}, {'id': 18422, 'synset': 'tickseed_sunflower.n.01', 'name': 'tickseed_sunflower'}, {'id': 18423, 'synset': 'european_beggar-ticks.n.01', 'name': 'European_beggar-ticks'}, {'id': 18424, 'synset': 'slender_knapweed.n.01', 'name': 'slender_knapweed'}, {'id': 18425, 'synset': 'false_chamomile.n.01', 'name': 'false_chamomile'}, {'id': 18426, 'synset': 'swan_river_daisy.n.01', 'name': 'Swan_River_daisy'}, {'id': 18427, 'synset': 'woodland_oxeye.n.01', 'name': 'woodland_oxeye'}, {'id': 18428, 'synset': 'indian_plantain.n.01', 'name': 'Indian_plantain'}, {'id': 18429, 'synset': 'calendula.n.01', 'name': 'calendula'}, {'id': 18430, 'synset': 'common_marigold.n.01', 'name': 'common_marigold'}, {'id': 18431, 'synset': 'china_aster.n.01', 'name': 'China_aster'}, {'id': 18432, 'synset': 'thistle.n.01', 'name': 'thistle'}, {'id': 18433, 'synset': 'welted_thistle.n.01', 'name': 'welted_thistle'}, {'id': 18434, 'synset': 'musk_thistle.n.01', 'name': 'musk_thistle'}, {'id': 18435, 'synset': 'carline_thistle.n.01', 'name': 'carline_thistle'}, {'id': 18436, 'synset': 'stemless_carline_thistle.n.01', 'name': 'stemless_carline_thistle'}, {'id': 18437, 'synset': 'common_carline_thistle.n.01', 'name': 'common_carline_thistle'}, {'id': 18438, 'synset': 'safflower.n.01', 'name': 'safflower'}, {'id': 18439, 'synset': 'safflower_seed.n.01', 'name': 'safflower_seed'}, {'id': 18440, 'synset': 'catananche.n.01', 'name': 'catananche'}, {'id': 18441, 'synset': 'blue_succory.n.01', 'name': 'blue_succory'}, {'id': 18442, 'synset': 'centaury.n.02', 'name': 'centaury'}, {'id': 18443, 'synset': 'dusty_miller.n.03', 'name': 'dusty_miller'}, {'id': 18444, 'synset': 'cornflower.n.02', 'name': 'cornflower'}, {'id': 18445, 'synset': 'star-thistle.n.01', 'name': 'star-thistle'}, {'id': 18446, 'synset': 'knapweed.n.01', 'name': 'knapweed'}, {'id': 18447, 'synset': 'sweet_sultan.n.02', 'name': 'sweet_sultan'}, {'id': 18448, 'synset': 'great_knapweed.n.01', 'name': 'great_knapweed'}, {'id': 18449, 'synset': "barnaby's_thistle.n.01", 'name': "Barnaby's_thistle"}, {'id': 18450, 'synset': 'chamomile.n.01', 'name': 'chamomile'}, {'id': 18451, 'synset': 'chaenactis.n.01', 'name': 'chaenactis'}, {'id': 18452, 'synset': 'chrysanthemum.n.02', 'name': 'chrysanthemum'}, {'id': 18453, 'synset': 'corn_marigold.n.01', 'name': 'corn_marigold'}, {'id': 18454, 'synset': 'crown_daisy.n.01', 'name': 'crown_daisy'}, {'id': 18455, 'synset': 'chop-suey_greens.n.01', 'name': 'chop-suey_greens'}, {'id': 18456, 'synset': 'golden_aster.n.01', 'name': 'golden_aster'}, {'id': 18457, 'synset': 'maryland_golden_aster.n.01', 'name': 'Maryland_golden_aster'}, {'id': 18458, 'synset': 'goldenbush.n.02', 'name': 'goldenbush'}, {'id': 18459, 'synset': 'rabbit_brush.n.01', 'name': 'rabbit_brush'}, {'id': 18460, 'synset': 'chicory.n.02', 'name': 'chicory'}, {'id': 18461, 'synset': 'endive.n.01', 'name': 'endive'}, {'id': 18462, 'synset': 'chicory.n.01', 'name': 'chicory'}, {'id': 18463, 'synset': 'plume_thistle.n.01', 'name': 'plume_thistle'}, {'id': 18464, 'synset': 'canada_thistle.n.01', 'name': 'Canada_thistle'}, {'id': 18465, 'synset': 'field_thistle.n.01', 'name': 'field_thistle'}, {'id': 18466, 'synset': 'woolly_thistle.n.02', 'name': 'woolly_thistle'}, {'id': 18467, 'synset': 'european_woolly_thistle.n.01', 'name': 'European_woolly_thistle'}, {'id': 18468, 'synset': 'melancholy_thistle.n.01', 'name': 'melancholy_thistle'}, {'id': 18469, 'synset': 'brook_thistle.n.01', 'name': 'brook_thistle'}, {'id': 18470, 'synset': 'bull_thistle.n.01', 'name': 'bull_thistle'}, {'id': 18471, 'synset': 'blessed_thistle.n.02', 'name': 'blessed_thistle'}, {'id': 18472, 'synset': 'mistflower.n.01', 'name': 'mistflower'}, {'id': 18473, 'synset': 'horseweed.n.02', 'name': 'horseweed'}, {'id': 18474, 'synset': 'coreopsis.n.01', 'name': 'coreopsis'}, {'id': 18475, 'synset': 'giant_coreopsis.n.01', 'name': 'giant_coreopsis'}, {'id': 18476, 'synset': 'sea_dahlia.n.01', 'name': 'sea_dahlia'}, {'id': 18477, 'synset': 'calliopsis.n.01', 'name': 'calliopsis'}, {'id': 18478, 'synset': 'cosmos.n.02', 'name': 'cosmos'}, {'id': 18479, 'synset': 'brass_buttons.n.01', 'name': 'brass_buttons'}, {'id': 18480, 'synset': 'billy_buttons.n.01', 'name': 'billy_buttons'}, {'id': 18481, 'synset': "hawk's-beard.n.01", 'name': "hawk's-beard"}, {'id': 18482, 'synset': 'artichoke.n.01', 'name': 'artichoke'}, {'id': 18483, 'synset': 'cardoon.n.01', 'name': 'cardoon'}, {'id': 18484, 'synset': 'dahlia.n.01', 'name': 'dahlia'}, {'id': 18485, 'synset': 'german_ivy.n.01', 'name': 'German_ivy'}, {'id': 18486, 'synset': "florist's_chrysanthemum.n.01", 'name': "florist's_chrysanthemum"}, {'id': 18487, 'synset': 'cape_marigold.n.01', 'name': 'cape_marigold'}, {'id': 18488, 'synset': "leopard's-bane.n.01", 'name': "leopard's-bane"}, {'id': 18489, 'synset': 'coneflower.n.03', 'name': 'coneflower'}, {'id': 18490, 'synset': 'globe_thistle.n.01', 'name': 'globe_thistle'}, {'id': 18491, 'synset': "elephant's-foot.n.02", 'name': "elephant's-foot"}, {'id': 18492, 'synset': 'tassel_flower.n.01', 'name': 'tassel_flower'}, {'id': 18493, 'synset': 'brittlebush.n.01', 'name': 'brittlebush'}, {'id': 18494, 'synset': 'sunray.n.02', 'name': 'sunray'}, {'id': 18495, 'synset': 'engelmannia.n.01', 'name': 'engelmannia'}, {'id': 18496, 'synset': 'fireweed.n.02', 'name': 'fireweed'}, {'id': 18497, 'synset': 'fleabane.n.02', 'name': 'fleabane'}, {'id': 18498, 'synset': 'blue_fleabane.n.01', 'name': 'blue_fleabane'}, {'id': 18499, 'synset': 'daisy_fleabane.n.01', 'name': 'daisy_fleabane'}, {'id': 18500, 'synset': 'orange_daisy.n.01', 'name': 'orange_daisy'}, {'id': 18501, 'synset': 'spreading_fleabane.n.01', 'name': 'spreading_fleabane'}, {'id': 18502, 'synset': 'seaside_daisy.n.01', 'name': 'seaside_daisy'}, {'id': 18503, 'synset': 'philadelphia_fleabane.n.01', 'name': 'Philadelphia_fleabane'}, {'id': 18504, 'synset': "robin's_plantain.n.01", 'name': "robin's_plantain"}, {'id': 18505, 'synset': 'showy_daisy.n.01', 'name': 'showy_daisy'}, {'id': 18506, 'synset': 'woolly_sunflower.n.01', 'name': 'woolly_sunflower'}, {'id': 18507, 'synset': 'golden_yarrow.n.01', 'name': 'golden_yarrow'}, {'id': 18508, 'synset': 'dog_fennel.n.01', 'name': 'dog_fennel'}, {'id': 18509, 'synset': 'joe-pye_weed.n.02', 'name': 'Joe-Pye_weed'}, {'id': 18510, 'synset': 'boneset.n.02', 'name': 'boneset'}, {'id': 18511, 'synset': 'joe-pye_weed.n.01', 'name': 'Joe-Pye_weed'}, {'id': 18512, 'synset': 'blue_daisy.n.01', 'name': 'blue_daisy'}, {'id': 18513, 'synset': 'kingfisher_daisy.n.01', 'name': 'kingfisher_daisy'}, {'id': 18514, 'synset': 'cotton_rose.n.02', 'name': 'cotton_rose'}, {'id': 18515, 'synset': 'herba_impia.n.01', 'name': 'herba_impia'}, {'id': 18516, 'synset': 'gaillardia.n.01', 'name': 'gaillardia'}, {'id': 18517, 'synset': 'gazania.n.01', 'name': 'gazania'}, {'id': 18518, 'synset': 'treasure_flower.n.01', 'name': 'treasure_flower'}, {'id': 18519, 'synset': 'african_daisy.n.02', 'name': 'African_daisy'}, {'id': 18520, 'synset': 'barberton_daisy.n.01', 'name': 'Barberton_daisy'}, {'id': 18521, 'synset': 'desert_sunflower.n.01', 'name': 'desert_sunflower'}, {'id': 18522, 'synset': 'cudweed.n.01', 'name': 'cudweed'}, {'id': 18523, 'synset': 'chafeweed.n.01', 'name': 'chafeweed'}, {'id': 18524, 'synset': 'gumweed.n.01', 'name': 'gumweed'}, {'id': 18525, 'synset': 'grindelia_robusta.n.01', 'name': 'Grindelia_robusta'}, {'id': 18526, 'synset': 'curlycup_gumweed.n.01', 'name': 'curlycup_gumweed'}, {'id': 18527, 'synset': 'little-head_snakeweed.n.01', 'name': 'little-head_snakeweed'}, {'id': 18528, 'synset': 'rabbitweed.n.01', 'name': 'rabbitweed'}, {'id': 18529, 'synset': 'broomweed.n.01', 'name': 'broomweed'}, {'id': 18530, 'synset': 'velvet_plant.n.02', 'name': 'velvet_plant'}, {'id': 18531, 'synset': 'goldenbush.n.01', 'name': 'goldenbush'}, {'id': 18532, 'synset': 'camphor_daisy.n.01', 'name': 'camphor_daisy'}, {'id': 18533, 'synset': 'yellow_spiny_daisy.n.01', 'name': 'yellow_spiny_daisy'}, {'id': 18534, 'synset': 'hoary_golden_bush.n.01', 'name': 'hoary_golden_bush'}, {'id': 18535, 'synset': 'sneezeweed.n.01', 'name': 'sneezeweed'}, {'id': 18536, 'synset': 'orange_sneezeweed.n.01', 'name': 'orange_sneezeweed'}, {'id': 18537, 'synset': 'rosilla.n.01', 'name': 'rosilla'}, {'id': 18538, 'synset': 'swamp_sunflower.n.01', 'name': 'swamp_sunflower'}, {'id': 18539, 'synset': 'common_sunflower.n.01', 'name': 'common_sunflower'}, {'id': 18540, 'synset': 'giant_sunflower.n.01', 'name': 'giant_sunflower'}, {'id': 18541, 'synset': 'showy_sunflower.n.01', 'name': 'showy_sunflower'}, {'id': 18542, 'synset': "maximilian's_sunflower.n.01", 'name': "Maximilian's_sunflower"}, {'id': 18543, 'synset': 'prairie_sunflower.n.01', 'name': 'prairie_sunflower'}, {'id': 18544, 'synset': 'jerusalem_artichoke.n.02', 'name': 'Jerusalem_artichoke'}, {'id': 18545, 'synset': 'jerusalem_artichoke.n.01', 'name': 'Jerusalem_artichoke'}, {'id': 18546, 'synset': 'strawflower.n.03', 'name': 'strawflower'}, {'id': 18547, 'synset': 'heliopsis.n.01', 'name': 'heliopsis'}, {'id': 18548, 'synset': 'strawflower.n.02', 'name': 'strawflower'}, {'id': 18549, 'synset': 'hairy_golden_aster.n.01', 'name': 'hairy_golden_aster'}, {'id': 18550, 'synset': 'hawkweed.n.02', 'name': 'hawkweed'}, {'id': 18551, 'synset': 'rattlesnake_weed.n.01', 'name': 'rattlesnake_weed'}, {'id': 18552, 'synset': 'alpine_coltsfoot.n.01', 'name': 'alpine_coltsfoot'}, {'id': 18553, 'synset': 'alpine_gold.n.01', 'name': 'alpine_gold'}, {'id': 18554, 'synset': 'dwarf_hulsea.n.01', 'name': 'dwarf_hulsea'}, {'id': 18555, 'synset': "cat's-ear.n.02", 'name': "cat's-ear"}, {'id': 18556, 'synset': 'inula.n.01', 'name': 'inula'}, {'id': 18557, 'synset': 'marsh_elder.n.01', 'name': 'marsh_elder'}, {'id': 18558, 'synset': 'burweed_marsh_elder.n.01', 'name': 'burweed_marsh_elder'}, {'id': 18559, 'synset': 'krigia.n.01', 'name': 'krigia'}, {'id': 18560, 'synset': 'dwarf_dandelion.n.01', 'name': 'dwarf_dandelion'}, {'id': 18561, 'synset': 'garden_lettuce.n.01', 'name': 'garden_lettuce'}, {'id': 18562, 'synset': 'cos_lettuce.n.01', 'name': 'cos_lettuce'}, {'id': 18563, 'synset': 'leaf_lettuce.n.01', 'name': 'leaf_lettuce'}, {'id': 18564, 'synset': 'celtuce.n.01', 'name': 'celtuce'}, {'id': 18565, 'synset': 'prickly_lettuce.n.01', 'name': 'prickly_lettuce'}, {'id': 18566, 'synset': 'goldfields.n.01', 'name': 'goldfields'}, {'id': 18567, 'synset': 'tidytips.n.01', 'name': 'tidytips'}, {'id': 18568, 'synset': 'hawkbit.n.01', 'name': 'hawkbit'}, {'id': 18569, 'synset': 'fall_dandelion.n.01', 'name': 'fall_dandelion'}, {'id': 18570, 'synset': 'edelweiss.n.01', 'name': 'edelweiss'}, {'id': 18571, 'synset': 'oxeye_daisy.n.02', 'name': 'oxeye_daisy'}, {'id': 18572, 'synset': 'oxeye_daisy.n.01', 'name': 'oxeye_daisy'}, {'id': 18573, 'synset': 'shasta_daisy.n.01', 'name': 'shasta_daisy'}, {'id': 18574, 'synset': 'pyrenees_daisy.n.01', 'name': 'Pyrenees_daisy'}, {'id': 18575, 'synset': 'north_island_edelweiss.n.01', 'name': 'north_island_edelweiss'}, {'id': 18576, 'synset': 'blazing_star.n.02', 'name': 'blazing_star'}, {'id': 18577, 'synset': 'dotted_gayfeather.n.01', 'name': 'dotted_gayfeather'}, {'id': 18578, 'synset': 'dense_blazing_star.n.01', 'name': 'dense_blazing_star'}, {'id': 18579, 'synset': 'texas_star.n.02', 'name': 'Texas_star'}, {'id': 18580, 'synset': 'african_daisy.n.01', 'name': 'African_daisy'}, {'id': 18581, 'synset': 'tahoka_daisy.n.01', 'name': 'tahoka_daisy'}, {'id': 18582, 'synset': 'sticky_aster.n.01', 'name': 'sticky_aster'}, {'id': 18583, 'synset': 'mojave_aster.n.01', 'name': 'Mojave_aster'}, {'id': 18584, 'synset': 'tarweed.n.01', 'name': 'tarweed'}, {'id': 18585, 'synset': 'sweet_false_chamomile.n.01', 'name': 'sweet_false_chamomile'}, {'id': 18586, 'synset': 'pineapple_weed.n.01', 'name': 'pineapple_weed'}, {'id': 18587, 'synset': 'climbing_hempweed.n.01', 'name': 'climbing_hempweed'}, {'id': 18588, 'synset': 'mutisia.n.01', 'name': 'mutisia'}, {'id': 18589, 'synset': 'rattlesnake_root.n.02', 'name': 'rattlesnake_root'}, {'id': 18590, 'synset': 'white_lettuce.n.01', 'name': 'white_lettuce'}, {'id': 18591, 'synset': 'daisybush.n.01', 'name': 'daisybush'}, {'id': 18592, 'synset': 'new_zealand_daisybush.n.01', 'name': 'New_Zealand_daisybush'}, {'id': 18593, 'synset': 'cotton_thistle.n.01', 'name': 'cotton_thistle'}, {'id': 18594, 'synset': 'othonna.n.01', 'name': 'othonna'}, {'id': 18595, 'synset': 'cascade_everlasting.n.01', 'name': 'cascade_everlasting'}, {'id': 18596, 'synset': 'butterweed.n.02', 'name': 'butterweed'}, {'id': 18597, 'synset': 'american_feverfew.n.01', 'name': 'American_feverfew'}, {'id': 18598, 'synset': 'cineraria.n.01', 'name': 'cineraria'}, {'id': 18599, 'synset': "florest's_cineraria.n.01", 'name': "florest's_cineraria"}, {'id': 18600, 'synset': 'butterbur.n.01', 'name': 'butterbur'}, {'id': 18601, 'synset': 'winter_heliotrope.n.01', 'name': 'winter_heliotrope'}, {'id': 18602, 'synset': 'sweet_coltsfoot.n.01', 'name': 'sweet_coltsfoot'}, {'id': 18603, 'synset': 'oxtongue.n.01', 'name': 'oxtongue'}, {'id': 18604, 'synset': 'hawkweed.n.01', 'name': 'hawkweed'}, {'id': 18605, 'synset': 'mouse-ear_hawkweed.n.01', 'name': 'mouse-ear_hawkweed'}, {'id': 18606, 'synset': 'stevia.n.02', 'name': 'stevia'}, {'id': 18607, 'synset': 'rattlesnake_root.n.01', 'name': 'rattlesnake_root'}, {'id': 18608, 'synset': 'fleabane.n.01', 'name': 'fleabane'}, {'id': 18609, 'synset': 'sheep_plant.n.01', 'name': 'sheep_plant'}, {'id': 18610, 'synset': 'coneflower.n.02', 'name': 'coneflower'}, {'id': 18611, 'synset': 'mexican_hat.n.01', 'name': 'Mexican_hat'}, {'id': 18612, 'synset': 'long-head_coneflower.n.01', 'name': 'long-head_coneflower'}, {'id': 18613, 'synset': 'prairie_coneflower.n.01', 'name': 'prairie_coneflower'}, {'id': 18614, 'synset': 'swan_river_everlasting.n.01', 'name': 'Swan_River_everlasting'}, {'id': 18615, 'synset': 'coneflower.n.01', 'name': 'coneflower'}, {'id': 18616, 'synset': 'black-eyed_susan.n.03', 'name': 'black-eyed_Susan'}, {'id': 18617, 'synset': 'cutleaved_coneflower.n.01', 'name': 'cutleaved_coneflower'}, {'id': 18618, 'synset': 'golden_glow.n.01', 'name': 'golden_glow'}, {'id': 18619, 'synset': 'lavender_cotton.n.01', 'name': 'lavender_cotton'}, {'id': 18620, 'synset': 'creeping_zinnia.n.01', 'name': 'creeping_zinnia'}, {'id': 18621, 'synset': 'golden_thistle.n.01', 'name': 'golden_thistle'}, {'id': 18622, 'synset': 'spanish_oyster_plant.n.01', 'name': 'Spanish_oyster_plant'}, {'id': 18623, 'synset': 'nodding_groundsel.n.01', 'name': 'nodding_groundsel'}, {'id': 18624, 'synset': 'dusty_miller.n.02', 'name': 'dusty_miller'}, {'id': 18625, 'synset': 'butterweed.n.01', 'name': 'butterweed'}, {'id': 18626, 'synset': 'ragwort.n.01', 'name': 'ragwort'}, {'id': 18627, 'synset': 'arrowleaf_groundsel.n.01', 'name': 'arrowleaf_groundsel'}, {'id': 18628, 'synset': 'black_salsify.n.01', 'name': 'black_salsify'}, {'id': 18629, 'synset': 'white-topped_aster.n.01', 'name': 'white-topped_aster'}, {'id': 18630, 'synset': 'narrow-leaved_white-topped_aster.n.01', 'name': 'narrow-leaved_white-topped_aster'}, {'id': 18631, 'synset': 'silver_sage.n.01', 'name': 'silver_sage'}, {'id': 18632, 'synset': 'sea_wormwood.n.01', 'name': 'sea_wormwood'}, {'id': 18633, 'synset': 'sawwort.n.01', 'name': 'sawwort'}, {'id': 18634, 'synset': 'rosinweed.n.01', 'name': 'rosinweed'}, {'id': 18635, 'synset': 'milk_thistle.n.02', 'name': 'milk_thistle'}, {'id': 18636, 'synset': 'goldenrod.n.01', 'name': 'goldenrod'}, {'id': 18637, 'synset': 'silverrod.n.01', 'name': 'silverrod'}, {'id': 18638, 'synset': 'meadow_goldenrod.n.01', 'name': 'meadow_goldenrod'}, {'id': 18639, 'synset': 'missouri_goldenrod.n.01', 'name': 'Missouri_goldenrod'}, {'id': 18640, 'synset': 'alpine_goldenrod.n.01', 'name': 'alpine_goldenrod'}, {'id': 18641, 'synset': 'grey_goldenrod.n.01', 'name': 'grey_goldenrod'}, {'id': 18642, 'synset': 'blue_mountain_tea.n.01', 'name': 'Blue_Mountain_tea'}, {'id': 18643, 'synset': "dyer's_weed.n.01", 'name': "dyer's_weed"}, {'id': 18644, 'synset': 'seaside_goldenrod.n.01', 'name': 'seaside_goldenrod'}, {'id': 18645, 'synset': 'narrow_goldenrod.n.01', 'name': 'narrow_goldenrod'}, {'id': 18646, 'synset': "boott's_goldenrod.n.01", 'name': "Boott's_goldenrod"}, {'id': 18647, 'synset': "elliott's_goldenrod.n.01", 'name': "Elliott's_goldenrod"}, {'id': 18648, 'synset': 'ohio_goldenrod.n.01', 'name': 'Ohio_goldenrod'}, {'id': 18649, 'synset': 'rough-stemmed_goldenrod.n.01', 'name': 'rough-stemmed_goldenrod'}, {'id': 18650, 'synset': 'showy_goldenrod.n.01', 'name': 'showy_goldenrod'}, {'id': 18651, 'synset': 'tall_goldenrod.n.01', 'name': 'tall_goldenrod'}, {'id': 18652, 'synset': 'zigzag_goldenrod.n.01', 'name': 'zigzag_goldenrod'}, {'id': 18653, 'synset': 'sow_thistle.n.01', 'name': 'sow_thistle'}, {'id': 18654, 'synset': 'milkweed.n.02', 'name': 'milkweed'}, {'id': 18655, 'synset': 'stevia.n.01', 'name': 'stevia'}, {'id': 18656, 'synset': "stokes'_aster.n.01", 'name': "stokes'_aster"}, {'id': 18657, 'synset': 'marigold.n.01', 'name': 'marigold'}, {'id': 18658, 'synset': 'african_marigold.n.01', 'name': 'African_marigold'}, {'id': 18659, 'synset': 'french_marigold.n.01', 'name': 'French_marigold'}, {'id': 18660, 'synset': 'painted_daisy.n.01', 'name': 'painted_daisy'}, {'id': 18661, 'synset': 'pyrethrum.n.02', 'name': 'pyrethrum'}, {'id': 18662, 'synset': 'northern_dune_tansy.n.01', 'name': 'northern_dune_tansy'}, {'id': 18663, 'synset': 'feverfew.n.01', 'name': 'feverfew'}, {'id': 18664, 'synset': 'dusty_miller.n.01', 'name': 'dusty_miller'}, {'id': 18665, 'synset': 'tansy.n.01', 'name': 'tansy'}, {'id': 18666, 'synset': 'dandelion.n.01', 'name': 'dandelion'}, {'id': 18667, 'synset': 'common_dandelion.n.01', 'name': 'common_dandelion'}, {'id': 18668, 'synset': 'dandelion_green.n.01', 'name': 'dandelion_green'}, {'id': 18669, 'synset': 'russian_dandelion.n.01', 'name': 'Russian_dandelion'}, {'id': 18670, 'synset': 'stemless_hymenoxys.n.01', 'name': 'stemless_hymenoxys'}, {'id': 18671, 'synset': 'mexican_sunflower.n.01', 'name': 'Mexican_sunflower'}, {'id': 18672, 'synset': 'easter_daisy.n.01', 'name': 'Easter_daisy'}, {'id': 18673, 'synset': 'yellow_salsify.n.01', 'name': 'yellow_salsify'}, {'id': 18674, 'synset': 'salsify.n.02', 'name': 'salsify'}, {'id': 18675, 'synset': 'meadow_salsify.n.01', 'name': 'meadow_salsify'}, {'id': 18676, 'synset': 'scentless_camomile.n.01', 'name': 'scentless_camomile'}, {'id': 18677, 'synset': 'turfing_daisy.n.01', 'name': 'turfing_daisy'}, {'id': 18678, 'synset': 'coltsfoot.n.02', 'name': 'coltsfoot'}, {'id': 18679, 'synset': 'ursinia.n.01', 'name': 'ursinia'}, {'id': 18680, 'synset': 'crownbeard.n.01', 'name': 'crownbeard'}, {'id': 18681, 'synset': 'wingstem.n.01', 'name': 'wingstem'}, {'id': 18682, 'synset': 'cowpen_daisy.n.01', 'name': 'cowpen_daisy'}, {'id': 18683, 'synset': 'gravelweed.n.01', 'name': 'gravelweed'}, {'id': 18684, 'synset': 'virginia_crownbeard.n.01', 'name': 'Virginia_crownbeard'}, {'id': 18685, 'synset': 'ironweed.n.01', 'name': 'ironweed'}, {'id': 18686, 'synset': "mule's_ears.n.01", 'name': "mule's_ears"}, {'id': 18687, 'synset': "white-rayed_mule's_ears.n.01", 'name': "white-rayed_mule's_ears"}, {'id': 18688, 'synset': 'cocklebur.n.01', 'name': 'cocklebur'}, {'id': 18689, 'synset': 'xeranthemum.n.01', 'name': 'xeranthemum'}, {'id': 18690, 'synset': 'immortelle.n.01', 'name': 'immortelle'}, {'id': 18691, 'synset': 'zinnia.n.01', 'name': 'zinnia'}, {'id': 18692, 'synset': 'white_zinnia.n.01', 'name': 'white_zinnia'}, {'id': 18693, 'synset': 'little_golden_zinnia.n.01', 'name': 'little_golden_zinnia'}, {'id': 18694, 'synset': 'blazing_star.n.01', 'name': 'blazing_star'}, {'id': 18695, 'synset': 'bartonia.n.01', 'name': 'bartonia'}, {'id': 18696, 'synset': 'achene.n.01', 'name': 'achene'}, {'id': 18697, 'synset': 'samara.n.01', 'name': 'samara'}, {'id': 18698, 'synset': 'campanula.n.01', 'name': 'campanula'}, {'id': 18699, 'synset': 'creeping_bellflower.n.01', 'name': 'creeping_bellflower'}, {'id': 18700, 'synset': 'canterbury_bell.n.02', 'name': 'Canterbury_bell'}, {'id': 18701, 'synset': 'tall_bellflower.n.01', 'name': 'tall_bellflower'}, {'id': 18702, 'synset': 'marsh_bellflower.n.01', 'name': 'marsh_bellflower'}, {'id': 18703, 'synset': 'clustered_bellflower.n.01', 'name': 'clustered_bellflower'}, {'id': 18704, 'synset': 'peach_bells.n.01', 'name': 'peach_bells'}, {'id': 18705, 'synset': 'chimney_plant.n.01', 'name': 'chimney_plant'}, {'id': 18706, 'synset': 'rampion.n.01', 'name': 'rampion'}, {'id': 18707, 'synset': 'tussock_bellflower.n.01', 'name': 'tussock_bellflower'}, {'id': 18708, 'synset': 'orchid.n.01', 'name': 'orchid'}, {'id': 18709, 'synset': 'orchis.n.01', 'name': 'orchis'}, {'id': 18710, 'synset': 'male_orchis.n.01', 'name': 'male_orchis'}, {'id': 18711, 'synset': 'butterfly_orchid.n.05', 'name': 'butterfly_orchid'}, {'id': 18712, 'synset': 'showy_orchis.n.01', 'name': 'showy_orchis'}, {'id': 18713, 'synset': 'aerides.n.01', 'name': 'aerides'}, {'id': 18714, 'synset': 'angrecum.n.01', 'name': 'angrecum'}, {'id': 18715, 'synset': 'jewel_orchid.n.01', 'name': 'jewel_orchid'}, {'id': 18716, 'synset': 'puttyroot.n.01', 'name': 'puttyroot'}, {'id': 18717, 'synset': 'arethusa.n.01', 'name': 'arethusa'}, {'id': 18718, 'synset': 'bog_rose.n.01', 'name': 'bog_rose'}, {'id': 18719, 'synset': 'bletia.n.01', 'name': 'bletia'}, {'id': 18720, 'synset': 'bletilla_striata.n.01', 'name': 'Bletilla_striata'}, {'id': 18721, 'synset': 'brassavola.n.01', 'name': 'brassavola'}, {'id': 18722, 'synset': 'spider_orchid.n.03', 'name': 'spider_orchid'}, {'id': 18723, 'synset': 'spider_orchid.n.02', 'name': 'spider_orchid'}, {'id': 18724, 'synset': 'caladenia.n.01', 'name': 'caladenia'}, {'id': 18725, 'synset': 'calanthe.n.01', 'name': 'calanthe'}, {'id': 18726, 'synset': 'grass_pink.n.01', 'name': 'grass_pink'}, {'id': 18727, 'synset': 'calypso.n.01', 'name': 'calypso'}, {'id': 18728, 'synset': 'cattleya.n.01', 'name': 'cattleya'}, {'id': 18729, 'synset': 'helleborine.n.03', 'name': 'helleborine'}, {'id': 18730, 'synset': 'red_helleborine.n.01', 'name': 'red_helleborine'}, {'id': 18731, 'synset': 'spreading_pogonia.n.01', 'name': 'spreading_pogonia'}, {'id': 18732, 'synset': 'rosebud_orchid.n.01', 'name': 'rosebud_orchid'}, {'id': 18733, 'synset': 'satyr_orchid.n.01', 'name': 'satyr_orchid'}, {'id': 18734, 'synset': 'frog_orchid.n.02', 'name': 'frog_orchid'}, {'id': 18735, 'synset': 'coelogyne.n.01', 'name': 'coelogyne'}, {'id': 18736, 'synset': 'coral_root.n.01', 'name': 'coral_root'}, {'id': 18737, 'synset': 'spotted_coral_root.n.01', 'name': 'spotted_coral_root'}, {'id': 18738, 'synset': 'striped_coral_root.n.01', 'name': 'striped_coral_root'}, {'id': 18739, 'synset': 'early_coral_root.n.01', 'name': 'early_coral_root'}, {'id': 18740, 'synset': 'swan_orchid.n.01', 'name': 'swan_orchid'}, {'id': 18741, 'synset': 'cymbid.n.01', 'name': 'cymbid'}, {'id': 18742, 'synset': 'cypripedia.n.01', 'name': 'cypripedia'}, {'id': 18743, 'synset': "lady's_slipper.n.01", 'name': "lady's_slipper"}, {'id': 18744, 'synset': 'moccasin_flower.n.01', 'name': 'moccasin_flower'}, {'id': 18745, 'synset': "common_lady's-slipper.n.01", 'name': "common_lady's-slipper"}, {'id': 18746, 'synset': "ram's-head.n.01", 'name': "ram's-head"}, {'id': 18747, 'synset': "yellow_lady's_slipper.n.01", 'name': "yellow_lady's_slipper"}, {'id': 18748, 'synset': "large_yellow_lady's_slipper.n.01", 'name': "large_yellow_lady's_slipper"}, {'id': 18749, 'synset': "california_lady's_slipper.n.01", 'name': "California_lady's_slipper"}, {'id': 18750, 'synset': "clustered_lady's_slipper.n.01", 'name': "clustered_lady's_slipper"}, {'id': 18751, 'synset': "mountain_lady's_slipper.n.01", 'name': "mountain_lady's_slipper"}, {'id': 18752, 'synset': 'marsh_orchid.n.01', 'name': 'marsh_orchid'}, {'id': 18753, 'synset': 'common_spotted_orchid.n.01', 'name': 'common_spotted_orchid'}, {'id': 18754, 'synset': 'dendrobium.n.01', 'name': 'dendrobium'}, {'id': 18755, 'synset': 'disa.n.01', 'name': 'disa'}, {'id': 18756, 'synset': 'phantom_orchid.n.01', 'name': 'phantom_orchid'}, {'id': 18757, 'synset': 'tulip_orchid.n.01', 'name': 'tulip_orchid'}, {'id': 18758, 'synset': 'butterfly_orchid.n.04', 'name': 'butterfly_orchid'}, {'id': 18759, 'synset': 'butterfly_orchid.n.03', 'name': 'butterfly_orchid'}, {'id': 18760, 'synset': 'epidendron.n.01', 'name': 'epidendron'}, {'id': 18761, 'synset': 'helleborine.n.02', 'name': 'helleborine'}, {'id': 18762, 'synset': 'epipactis_helleborine.n.01', 'name': 'Epipactis_helleborine'}, {'id': 18763, 'synset': 'stream_orchid.n.01', 'name': 'stream_orchid'}, {'id': 18764, 'synset': 'tongueflower.n.01', 'name': 'tongueflower'}, {'id': 18765, 'synset': 'rattlesnake_plantain.n.01', 'name': 'rattlesnake_plantain'}, {'id': 18766, 'synset': 'fragrant_orchid.n.01', 'name': 'fragrant_orchid'}, {'id': 18767, 'synset': 'short-spurred_fragrant_orchid.n.01', 'name': 'short-spurred_fragrant_orchid'}, {'id': 18768, 'synset': 'fringed_orchis.n.01', 'name': 'fringed_orchis'}, {'id': 18769, 'synset': 'frog_orchid.n.01', 'name': 'frog_orchid'}, {'id': 18770, 'synset': 'rein_orchid.n.01', 'name': 'rein_orchid'}, {'id': 18771, 'synset': 'bog_rein_orchid.n.01', 'name': 'bog_rein_orchid'}, {'id': 18772, 'synset': 'white_fringed_orchis.n.01', 'name': 'white_fringed_orchis'}, {'id': 18773, 'synset': 'elegant_habenaria.n.01', 'name': 'elegant_Habenaria'}, {'id': 18774, 'synset': 'purple-fringed_orchid.n.02', 'name': 'purple-fringed_orchid'}, {'id': 18775, 'synset': 'coastal_rein_orchid.n.01', 'name': 'coastal_rein_orchid'}, {'id': 18776, 'synset': "hooker's_orchid.n.01", 'name': "Hooker's_orchid"}, {'id': 18777, 'synset': 'ragged_orchid.n.01', 'name': 'ragged_orchid'}, {'id': 18778, 'synset': 'prairie_orchid.n.01', 'name': 'prairie_orchid'}, {'id': 18779, 'synset': 'snowy_orchid.n.01', 'name': 'snowy_orchid'}, {'id': 18780, 'synset': 'round-leaved_rein_orchid.n.01', 'name': 'round-leaved_rein_orchid'}, {'id': 18781, 'synset': 'purple_fringeless_orchid.n.01', 'name': 'purple_fringeless_orchid'}, {'id': 18782, 'synset': 'purple-fringed_orchid.n.01', 'name': 'purple-fringed_orchid'}, {'id': 18783, 'synset': 'alaska_rein_orchid.n.01', 'name': 'Alaska_rein_orchid'}, {'id': 18784, 'synset': 'crested_coral_root.n.01', 'name': 'crested_coral_root'}, {'id': 18785, 'synset': 'texas_purple_spike.n.01', 'name': 'Texas_purple_spike'}, {'id': 18786, 'synset': 'lizard_orchid.n.01', 'name': 'lizard_orchid'}, {'id': 18787, 'synset': 'laelia.n.01', 'name': 'laelia'}, {'id': 18788, 'synset': 'liparis.n.01', 'name': 'liparis'}, {'id': 18789, 'synset': 'twayblade.n.02', 'name': 'twayblade'}, {'id': 18790, 'synset': 'fen_orchid.n.01', 'name': 'fen_orchid'}, {'id': 18791, 'synset': 'broad-leaved_twayblade.n.01', 'name': 'broad-leaved_twayblade'}, {'id': 18792, 'synset': 'lesser_twayblade.n.01', 'name': 'lesser_twayblade'}, {'id': 18793, 'synset': 'twayblade.n.01', 'name': 'twayblade'}, {'id': 18794, 'synset': "green_adder's_mouth.n.01", 'name': "green_adder's_mouth"}, {'id': 18795, 'synset': 'masdevallia.n.01', 'name': 'masdevallia'}, {'id': 18796, 'synset': 'maxillaria.n.01', 'name': 'maxillaria'}, {'id': 18797, 'synset': 'pansy_orchid.n.01', 'name': 'pansy_orchid'}, {'id': 18798, 'synset': 'odontoglossum.n.01', 'name': 'odontoglossum'}, {'id': 18799, 'synset': 'oncidium.n.01', 'name': 'oncidium'}, {'id': 18800, 'synset': 'bee_orchid.n.01', 'name': 'bee_orchid'}, {'id': 18801, 'synset': 'fly_orchid.n.02', 'name': 'fly_orchid'}, {'id': 18802, 'synset': 'spider_orchid.n.01', 'name': 'spider_orchid'}, {'id': 18803, 'synset': 'early_spider_orchid.n.01', 'name': 'early_spider_orchid'}, {'id': 18804, 'synset': "venus'_slipper.n.01", 'name': "Venus'_slipper"}, {'id': 18805, 'synset': 'phaius.n.01', 'name': 'phaius'}, {'id': 18806, 'synset': 'moth_orchid.n.01', 'name': 'moth_orchid'}, {'id': 18807, 'synset': 'butterfly_plant.n.01', 'name': 'butterfly_plant'}, {'id': 18808, 'synset': 'rattlesnake_orchid.n.01', 'name': 'rattlesnake_orchid'}, {'id': 18809, 'synset': 'lesser_butterfly_orchid.n.01', 'name': 'lesser_butterfly_orchid'}, {'id': 18810, 'synset': 'greater_butterfly_orchid.n.01', 'name': 'greater_butterfly_orchid'}, {'id': 18811, 'synset': 'prairie_white-fringed_orchid.n.01', 'name': 'prairie_white-fringed_orchid'}, {'id': 18812, 'synset': 'tangle_orchid.n.01', 'name': 'tangle_orchid'}, {'id': 18813, 'synset': 'indian_crocus.n.01', 'name': 'Indian_crocus'}, {'id': 18814, 'synset': 'pleurothallis.n.01', 'name': 'pleurothallis'}, {'id': 18815, 'synset': 'pogonia.n.01', 'name': 'pogonia'}, {'id': 18816, 'synset': 'butterfly_orchid.n.01', 'name': 'butterfly_orchid'}, {'id': 18817, 'synset': 'psychopsis_krameriana.n.01', 'name': 'Psychopsis_krameriana'}, {'id': 18818, 'synset': 'psychopsis_papilio.n.01', 'name': 'Psychopsis_papilio'}, {'id': 18819, 'synset': 'helmet_orchid.n.01', 'name': 'helmet_orchid'}, {'id': 18820, 'synset': 'foxtail_orchid.n.01', 'name': 'foxtail_orchid'}, {'id': 18821, 'synset': 'orange-blossom_orchid.n.01', 'name': 'orange-blossom_orchid'}, {'id': 18822, 'synset': 'sobralia.n.01', 'name': 'sobralia'}, {'id': 18823, 'synset': "ladies'_tresses.n.01", 'name': "ladies'_tresses"}, {'id': 18824, 'synset': 'screw_augur.n.01', 'name': 'screw_augur'}, {'id': 18825, 'synset': "hooded_ladies'_tresses.n.01", 'name': "hooded_ladies'_tresses"}, {'id': 18826, 'synset': "western_ladies'_tresses.n.01", 'name': "western_ladies'_tresses"}, {'id': 18827, 'synset': "european_ladies'_tresses.n.01", 'name': "European_ladies'_tresses"}, {'id': 18828, 'synset': 'stanhopea.n.01', 'name': 'stanhopea'}, {'id': 18829, 'synset': 'stelis.n.01', 'name': 'stelis'}, {'id': 18830, 'synset': 'fly_orchid.n.01', 'name': 'fly_orchid'}, {'id': 18831, 'synset': 'vanda.n.01', 'name': 'vanda'}, {'id': 18832, 'synset': 'blue_orchid.n.01', 'name': 'blue_orchid'}, {'id': 18833, 'synset': 'vanilla.n.01', 'name': 'vanilla'}, {'id': 18834, 'synset': 'vanilla_orchid.n.01', 'name': 'vanilla_orchid'}, {'id': 18835, 'synset': 'yam.n.02', 'name': 'yam'}, {'id': 18836, 'synset': 'yam.n.01', 'name': 'yam'}, {'id': 18837, 'synset': 'white_yam.n.01', 'name': 'white_yam'}, {'id': 18838, 'synset': 'cinnamon_vine.n.01', 'name': 'cinnamon_vine'}, {'id': 18839, 'synset': "elephant's-foot.n.01", 'name': "elephant's-foot"}, {'id': 18840, 'synset': 'wild_yam.n.01', 'name': 'wild_yam'}, {'id': 18841, 'synset': 'cush-cush.n.01', 'name': 'cush-cush'}, {'id': 18842, 'synset': 'black_bryony.n.01', 'name': 'black_bryony'}, {'id': 18843, 'synset': 'primrose.n.01', 'name': 'primrose'}, {'id': 18844, 'synset': 'english_primrose.n.01', 'name': 'English_primrose'}, {'id': 18845, 'synset': 'cowslip.n.01', 'name': 'cowslip'}, {'id': 18846, 'synset': 'oxlip.n.01', 'name': 'oxlip'}, {'id': 18847, 'synset': 'chinese_primrose.n.01', 'name': 'Chinese_primrose'}, {'id': 18848, 'synset': 'polyanthus.n.01', 'name': 'polyanthus'}, {'id': 18849, 'synset': 'pimpernel.n.02', 'name': 'pimpernel'}, {'id': 18850, 'synset': 'scarlet_pimpernel.n.01', 'name': 'scarlet_pimpernel'}, {'id': 18851, 'synset': 'bog_pimpernel.n.01', 'name': 'bog_pimpernel'}, {'id': 18852, 'synset': 'chaffweed.n.01', 'name': 'chaffweed'}, {'id': 18853, 'synset': 'cyclamen.n.01', 'name': 'cyclamen'}, {'id': 18854, 'synset': 'sowbread.n.01', 'name': 'sowbread'}, {'id': 18855, 'synset': 'sea_milkwort.n.01', 'name': 'sea_milkwort'}, {'id': 18856, 'synset': 'featherfoil.n.01', 'name': 'featherfoil'}, {'id': 18857, 'synset': 'water_gillyflower.n.01', 'name': 'water_gillyflower'}, {'id': 18858, 'synset': 'water_violet.n.01', 'name': 'water_violet'}, {'id': 18859, 'synset': 'loosestrife.n.02', 'name': 'loosestrife'}, {'id': 18860, 'synset': 'gooseneck_loosestrife.n.01', 'name': 'gooseneck_loosestrife'}, {'id': 18861, 'synset': 'yellow_pimpernel.n.01', 'name': 'yellow_pimpernel'}, {'id': 18862, 'synset': 'fringed_loosestrife.n.01', 'name': 'fringed_loosestrife'}, {'id': 18863, 'synset': 'moneywort.n.01', 'name': 'moneywort'}, {'id': 18864, 'synset': 'swamp_candles.n.01', 'name': 'swamp_candles'}, {'id': 18865, 'synset': 'whorled_loosestrife.n.01', 'name': 'whorled_loosestrife'}, {'id': 18866, 'synset': 'water_pimpernel.n.01', 'name': 'water_pimpernel'}, {'id': 18867, 'synset': 'brookweed.n.02', 'name': 'brookweed'}, {'id': 18868, 'synset': 'brookweed.n.01', 'name': 'brookweed'}, {'id': 18869, 'synset': 'coralberry.n.02', 'name': 'coralberry'}, {'id': 18870, 'synset': 'marlberry.n.01', 'name': 'marlberry'}, {'id': 18871, 'synset': 'plumbago.n.02', 'name': 'plumbago'}, {'id': 18872, 'synset': 'leadwort.n.01', 'name': 'leadwort'}, {'id': 18873, 'synset': 'thrift.n.01', 'name': 'thrift'}, {'id': 18874, 'synset': 'sea_lavender.n.01', 'name': 'sea_lavender'}, {'id': 18875, 'synset': 'barbasco.n.01', 'name': 'barbasco'}, {'id': 18876, 'synset': 'gramineous_plant.n.01', 'name': 'gramineous_plant'}, {'id': 18877, 'synset': 'grass.n.01', 'name': 'grass'}, {'id': 18878, 'synset': 'midgrass.n.01', 'name': 'midgrass'}, {'id': 18879, 'synset': 'shortgrass.n.01', 'name': 'shortgrass'}, {'id': 18880, 'synset': 'sword_grass.n.01', 'name': 'sword_grass'}, {'id': 18881, 'synset': 'tallgrass.n.01', 'name': 'tallgrass'}, {'id': 18882, 'synset': 'herbage.n.01', 'name': 'herbage'}, {'id': 18883, 'synset': 'goat_grass.n.01', 'name': 'goat_grass'}, {'id': 18884, 'synset': 'wheatgrass.n.01', 'name': 'wheatgrass'}, {'id': 18885, 'synset': 'crested_wheatgrass.n.01', 'name': 'crested_wheatgrass'}, {'id': 18886, 'synset': 'bearded_wheatgrass.n.01', 'name': 'bearded_wheatgrass'}, {'id': 18887, 'synset': 'western_wheatgrass.n.01', 'name': 'western_wheatgrass'}, {'id': 18888, 'synset': 'intermediate_wheatgrass.n.01', 'name': 'intermediate_wheatgrass'}, {'id': 18889, 'synset': 'slender_wheatgrass.n.01', 'name': 'slender_wheatgrass'}, {'id': 18890, 'synset': 'velvet_bent.n.01', 'name': 'velvet_bent'}, {'id': 18891, 'synset': 'cloud_grass.n.01', 'name': 'cloud_grass'}, {'id': 18892, 'synset': 'meadow_foxtail.n.01', 'name': 'meadow_foxtail'}, {'id': 18893, 'synset': 'foxtail.n.01', 'name': 'foxtail'}, {'id': 18894, 'synset': 'broom_grass.n.01', 'name': 'broom_grass'}, {'id': 18895, 'synset': 'broom_sedge.n.01', 'name': 'broom_sedge'}, {'id': 18896, 'synset': 'tall_oat_grass.n.01', 'name': 'tall_oat_grass'}, {'id': 18897, 'synset': 'toetoe.n.02', 'name': 'toetoe'}, {'id': 18898, 'synset': 'oat.n.01', 'name': 'oat'}, {'id': 18899, 'synset': 'cereal_oat.n.01', 'name': 'cereal_oat'}, {'id': 18900, 'synset': 'wild_oat.n.01', 'name': 'wild_oat'}, {'id': 18901, 'synset': 'slender_wild_oat.n.01', 'name': 'slender_wild_oat'}, {'id': 18902, 'synset': 'wild_red_oat.n.01', 'name': 'wild_red_oat'}, {'id': 18903, 'synset': 'brome.n.01', 'name': 'brome'}, {'id': 18904, 'synset': 'chess.n.01', 'name': 'chess'}, {'id': 18905, 'synset': 'field_brome.n.01', 'name': 'field_brome'}, {'id': 18906, 'synset': 'grama.n.01', 'name': 'grama'}, {'id': 18907, 'synset': 'black_grama.n.01', 'name': 'black_grama'}, {'id': 18908, 'synset': 'buffalo_grass.n.02', 'name': 'buffalo_grass'}, {'id': 18909, 'synset': 'reed_grass.n.01', 'name': 'reed_grass'}, {'id': 18910, 'synset': 'feather_reed_grass.n.01', 'name': 'feather_reed_grass'}, {'id': 18911, 'synset': 'australian_reed_grass.n.01', 'name': 'Australian_reed_grass'}, {'id': 18912, 'synset': 'burgrass.n.01', 'name': 'burgrass'}, {'id': 18913, 'synset': 'buffel_grass.n.01', 'name': 'buffel_grass'}, {'id': 18914, 'synset': 'rhodes_grass.n.01', 'name': 'Rhodes_grass'}, {'id': 18915, 'synset': 'pampas_grass.n.01', 'name': 'pampas_grass'}, {'id': 18916, 'synset': 'giant_star_grass.n.01', 'name': 'giant_star_grass'}, {'id': 18917, 'synset': 'orchard_grass.n.01', 'name': 'orchard_grass'}, {'id': 18918, 'synset': 'egyptian_grass.n.01', 'name': 'Egyptian_grass'}, {'id': 18919, 'synset': 'crabgrass.n.01', 'name': 'crabgrass'}, {'id': 18920, 'synset': 'smooth_crabgrass.n.01', 'name': 'smooth_crabgrass'}, {'id': 18921, 'synset': 'large_crabgrass.n.01', 'name': 'large_crabgrass'}, {'id': 18922, 'synset': 'barnyard_grass.n.01', 'name': 'barnyard_grass'}, {'id': 18923, 'synset': 'japanese_millet.n.01', 'name': 'Japanese_millet'}, {'id': 18924, 'synset': 'yardgrass.n.01', 'name': 'yardgrass'}, {'id': 18925, 'synset': 'finger_millet.n.01', 'name': 'finger_millet'}, {'id': 18926, 'synset': 'lyme_grass.n.01', 'name': 'lyme_grass'}, {'id': 18927, 'synset': 'wild_rye.n.01', 'name': 'wild_rye'}, {'id': 18928, 'synset': 'giant_ryegrass.n.01', 'name': 'giant_ryegrass'}, {'id': 18929, 'synset': 'sea_lyme_grass.n.01', 'name': 'sea_lyme_grass'}, {'id': 18930, 'synset': 'canada_wild_rye.n.01', 'name': 'Canada_wild_rye'}, {'id': 18931, 'synset': 'teff.n.01', 'name': 'teff'}, {'id': 18932, 'synset': 'weeping_love_grass.n.01', 'name': 'weeping_love_grass'}, {'id': 18933, 'synset': 'plume_grass.n.01', 'name': 'plume_grass'}, {'id': 18934, 'synset': 'ravenna_grass.n.01', 'name': 'Ravenna_grass'}, {'id': 18935, 'synset': 'fescue.n.01', 'name': 'fescue'}, {'id': 18936, 'synset': 'reed_meadow_grass.n.01', 'name': 'reed_meadow_grass'}, {'id': 18937, 'synset': 'velvet_grass.n.01', 'name': 'velvet_grass'}, {'id': 18938, 'synset': 'creeping_soft_grass.n.01', 'name': 'creeping_soft_grass'}, {'id': 18939, 'synset': 'barleycorn.n.01', 'name': 'barleycorn'}, {'id': 18940, 'synset': 'barley_grass.n.01', 'name': 'barley_grass'}, {'id': 18941, 'synset': 'little_barley.n.01', 'name': 'little_barley'}, {'id': 18942, 'synset': 'rye_grass.n.01', 'name': 'rye_grass'}, {'id': 18943, 'synset': 'perennial_ryegrass.n.01', 'name': 'perennial_ryegrass'}, {'id': 18944, 'synset': 'italian_ryegrass.n.01', 'name': 'Italian_ryegrass'}, {'id': 18945, 'synset': 'darnel.n.01', 'name': 'darnel'}, {'id': 18946, 'synset': 'nimblewill.n.01', 'name': 'nimblewill'}, {'id': 18947, 'synset': 'cultivated_rice.n.01', 'name': 'cultivated_rice'}, {'id': 18948, 'synset': 'ricegrass.n.01', 'name': 'ricegrass'}, {'id': 18949, 'synset': 'smilo.n.01', 'name': 'smilo'}, {'id': 18950, 'synset': 'switch_grass.n.01', 'name': 'switch_grass'}, {'id': 18951, 'synset': 'broomcorn_millet.n.01', 'name': 'broomcorn_millet'}, {'id': 18952, 'synset': 'goose_grass.n.03', 'name': 'goose_grass'}, {'id': 18953, 'synset': 'dallisgrass.n.01', 'name': 'dallisgrass'}, {'id': 18954, 'synset': 'bahia_grass.n.01', 'name': 'Bahia_grass'}, {'id': 18955, 'synset': 'knotgrass.n.01', 'name': 'knotgrass'}, {'id': 18956, 'synset': 'fountain_grass.n.01', 'name': 'fountain_grass'}, {'id': 18957, 'synset': 'reed_canary_grass.n.01', 'name': 'reed_canary_grass'}, {'id': 18958, 'synset': 'canary_grass.n.01', 'name': 'canary_grass'}, {'id': 18959, 'synset': 'timothy.n.01', 'name': 'timothy'}, {'id': 18960, 'synset': 'bluegrass.n.01', 'name': 'bluegrass'}, {'id': 18961, 'synset': 'meadowgrass.n.01', 'name': 'meadowgrass'}, {'id': 18962, 'synset': 'wood_meadowgrass.n.01', 'name': 'wood_meadowgrass'}, {'id': 18963, 'synset': 'noble_cane.n.01', 'name': 'noble_cane'}, {'id': 18964, 'synset': 'munj.n.01', 'name': 'munj'}, {'id': 18965, 'synset': 'broom_beard_grass.n.01', 'name': 'broom_beard_grass'}, {'id': 18966, 'synset': 'bluestem.n.01', 'name': 'bluestem'}, {'id': 18967, 'synset': 'rye.n.02', 'name': 'rye'}, {'id': 18968, 'synset': 'bristlegrass.n.01', 'name': 'bristlegrass'}, {'id': 18969, 'synset': 'giant_foxtail.n.01', 'name': 'giant_foxtail'}, {'id': 18970, 'synset': 'yellow_bristlegrass.n.01', 'name': 'yellow_bristlegrass'}, {'id': 18971, 'synset': 'green_bristlegrass.n.01', 'name': 'green_bristlegrass'}, {'id': 18972, 'synset': 'siberian_millet.n.01', 'name': 'Siberian_millet'}, {'id': 18973, 'synset': 'german_millet.n.01', 'name': 'German_millet'}, {'id': 18974, 'synset': 'millet.n.01', 'name': 'millet'}, {'id': 18975, 'synset': 'rattan.n.02', 'name': 'rattan'}, {'id': 18976, 'synset': 'malacca.n.01', 'name': 'malacca'}, {'id': 18977, 'synset': 'reed.n.01', 'name': 'reed'}, {'id': 18978, 'synset': 'sorghum.n.01', 'name': 'sorghum'}, {'id': 18979, 'synset': 'grain_sorghum.n.01', 'name': 'grain_sorghum'}, {'id': 18980, 'synset': 'durra.n.01', 'name': 'durra'}, {'id': 18981, 'synset': 'feterita.n.01', 'name': 'feterita'}, {'id': 18982, 'synset': 'hegari.n.01', 'name': 'hegari'}, {'id': 18983, 'synset': 'kaoliang.n.01', 'name': 'kaoliang'}, {'id': 18984, 'synset': 'milo.n.01', 'name': 'milo'}, {'id': 18985, 'synset': 'shallu.n.01', 'name': 'shallu'}, {'id': 18986, 'synset': 'broomcorn.n.01', 'name': 'broomcorn'}, {'id': 18987, 'synset': 'cordgrass.n.01', 'name': 'cordgrass'}, {'id': 18988, 'synset': 'salt_reed_grass.n.01', 'name': 'salt_reed_grass'}, {'id': 18989, 'synset': 'prairie_cordgrass.n.01', 'name': 'prairie_cordgrass'}, {'id': 18990, 'synset': 'smut_grass.n.01', 'name': 'smut_grass'}, {'id': 18991, 'synset': 'sand_dropseed.n.01', 'name': 'sand_dropseed'}, {'id': 18992, 'synset': 'rush_grass.n.01', 'name': 'rush_grass'}, {'id': 18993, 'synset': 'st._augustine_grass.n.01', 'name': 'St._Augustine_grass'}, {'id': 18994, 'synset': 'grain.n.08', 'name': 'grain'}, {'id': 18995, 'synset': 'cereal.n.01', 'name': 'cereal'}, {'id': 18996, 'synset': 'wheat.n.01', 'name': 'wheat'}, {'id': 18997, 'synset': 'wheat_berry.n.01', 'name': 'wheat_berry'}, {'id': 18998, 'synset': 'durum.n.01', 'name': 'durum'}, {'id': 18999, 'synset': 'spelt.n.01', 'name': 'spelt'}, {'id': 19000, 'synset': 'emmer.n.01', 'name': 'emmer'}, {'id': 19001, 'synset': 'wild_wheat.n.01', 'name': 'wild_wheat'}, {'id': 19002, 'synset': 'corn.n.01', 'name': 'corn'}, {'id': 19003, 'synset': 'mealie.n.01', 'name': 'mealie'}, {'id': 19004, 'synset': 'corn.n.02', 'name': 'corn'}, {'id': 19005, 'synset': 'dent_corn.n.01', 'name': 'dent_corn'}, {'id': 19006, 'synset': 'flint_corn.n.01', 'name': 'flint_corn'}, {'id': 19007, 'synset': 'popcorn.n.01', 'name': 'popcorn'}, {'id': 19008, 'synset': 'zoysia.n.01', 'name': 'zoysia'}, {'id': 19009, 'synset': 'manila_grass.n.01', 'name': 'Manila_grass'}, {'id': 19010, 'synset': 'korean_lawn_grass.n.01', 'name': 'Korean_lawn_grass'}, {'id': 19011, 'synset': 'common_bamboo.n.01', 'name': 'common_bamboo'}, {'id': 19012, 'synset': 'giant_bamboo.n.01', 'name': 'giant_bamboo'}, {'id': 19013, 'synset': 'umbrella_plant.n.03', 'name': 'umbrella_plant'}, {'id': 19014, 'synset': 'chufa.n.01', 'name': 'chufa'}, {'id': 19015, 'synset': 'galingale.n.01', 'name': 'galingale'}, {'id': 19016, 'synset': 'nutgrass.n.01', 'name': 'nutgrass'}, {'id': 19017, 'synset': 'sand_sedge.n.01', 'name': 'sand_sedge'}, {'id': 19018, 'synset': 'cypress_sedge.n.01', 'name': 'cypress_sedge'}, {'id': 19019, 'synset': 'cotton_grass.n.01', 'name': 'cotton_grass'}, {'id': 19020, 'synset': 'common_cotton_grass.n.01', 'name': 'common_cotton_grass'}, {'id': 19021, 'synset': 'hardstem_bulrush.n.01', 'name': 'hardstem_bulrush'}, {'id': 19022, 'synset': 'wool_grass.n.01', 'name': 'wool_grass'}, {'id': 19023, 'synset': 'spike_rush.n.01', 'name': 'spike_rush'}, {'id': 19024, 'synset': 'water_chestnut.n.02', 'name': 'water_chestnut'}, {'id': 19025, 'synset': 'needle_spike_rush.n.01', 'name': 'needle_spike_rush'}, {'id': 19026, 'synset': 'creeping_spike_rush.n.01', 'name': 'creeping_spike_rush'}, {'id': 19027, 'synset': 'pandanus.n.02', 'name': 'pandanus'}, {'id': 19028, 'synset': 'textile_screw_pine.n.01', 'name': 'textile_screw_pine'}, {'id': 19029, 'synset': 'cattail.n.01', 'name': 'cattail'}, {'id': 19030, 'synset': "cat's-tail.n.01", 'name': "cat's-tail"}, {'id': 19031, 'synset': 'bur_reed.n.01', 'name': 'bur_reed'}, {'id': 19032, 'synset': 'grain.n.07', 'name': 'grain'}, {'id': 19033, 'synset': 'kernel.n.02', 'name': 'kernel'}, {'id': 19034, 'synset': 'rye.n.01', 'name': 'rye'}, {'id': 19035, 'synset': 'gourd.n.03', 'name': 'gourd'}, {'id': 19036, 'synset': 'pumpkin.n.01', 'name': 'pumpkin'}, {'id': 19037, 'synset': 'squash.n.01', 'name': 'squash'}, {'id': 19038, 'synset': 'summer_squash.n.01', 'name': 'summer_squash'}, {'id': 19039, 'synset': 'yellow_squash.n.01', 'name': 'yellow_squash'}, {'id': 19040, 'synset': 'marrow.n.02', 'name': 'marrow'}, {'id': 19041, 'synset': 'zucchini.n.01', 'name': 'zucchini'}, {'id': 19042, 'synset': 'cocozelle.n.01', 'name': 'cocozelle'}, {'id': 19043, 'synset': 'cymling.n.01', 'name': 'cymling'}, {'id': 19044, 'synset': 'spaghetti_squash.n.01', 'name': 'spaghetti_squash'}, {'id': 19045, 'synset': 'winter_squash.n.01', 'name': 'winter_squash'}, {'id': 19046, 'synset': 'acorn_squash.n.01', 'name': 'acorn_squash'}, {'id': 19047, 'synset': 'hubbard_squash.n.01', 'name': 'hubbard_squash'}, {'id': 19048, 'synset': 'turban_squash.n.01', 'name': 'turban_squash'}, {'id': 19049, 'synset': 'buttercup_squash.n.01', 'name': 'buttercup_squash'}, {'id': 19050, 'synset': 'butternut_squash.n.01', 'name': 'butternut_squash'}, {'id': 19051, 'synset': 'winter_crookneck.n.01', 'name': 'winter_crookneck'}, {'id': 19052, 'synset': 'cushaw.n.01', 'name': 'cushaw'}, {'id': 19053, 'synset': 'prairie_gourd.n.02', 'name': 'prairie_gourd'}, {'id': 19054, 'synset': 'prairie_gourd.n.01', 'name': 'prairie_gourd'}, {'id': 19055, 'synset': 'bryony.n.01', 'name': 'bryony'}, {'id': 19056, 'synset': 'white_bryony.n.01', 'name': 'white_bryony'}, {'id': 19057, 'synset': 'sweet_melon.n.01', 'name': 'sweet_melon'}, {'id': 19058, 'synset': 'cantaloupe.n.01', 'name': 'cantaloupe'}, {'id': 19059, 'synset': 'winter_melon.n.01', 'name': 'winter_melon'}, {'id': 19060, 'synset': 'net_melon.n.01', 'name': 'net_melon'}, {'id': 19061, 'synset': 'cucumber.n.01', 'name': 'cucumber'}, {'id': 19062, 'synset': 'squirting_cucumber.n.01', 'name': 'squirting_cucumber'}, {'id': 19063, 'synset': 'bottle_gourd.n.01', 'name': 'bottle_gourd'}, {'id': 19064, 'synset': 'luffa.n.02', 'name': 'luffa'}, {'id': 19065, 'synset': 'loofah.n.02', 'name': 'loofah'}, {'id': 19066, 'synset': 'angled_loofah.n.01', 'name': 'angled_loofah'}, {'id': 19067, 'synset': 'loofa.n.01', 'name': 'loofa'}, {'id': 19068, 'synset': 'balsam_apple.n.01', 'name': 'balsam_apple'}, {'id': 19069, 'synset': 'balsam_pear.n.01', 'name': 'balsam_pear'}, {'id': 19070, 'synset': 'lobelia.n.01', 'name': 'lobelia'}, {'id': 19071, 'synset': 'water_lobelia.n.01', 'name': 'water_lobelia'}, {'id': 19072, 'synset': 'mallow.n.01', 'name': 'mallow'}, {'id': 19073, 'synset': 'musk_mallow.n.02', 'name': 'musk_mallow'}, {'id': 19074, 'synset': 'common_mallow.n.01', 'name': 'common_mallow'}, {'id': 19075, 'synset': 'okra.n.02', 'name': 'okra'}, {'id': 19076, 'synset': 'okra.n.01', 'name': 'okra'}, {'id': 19077, 'synset': 'abelmosk.n.01', 'name': 'abelmosk'}, {'id': 19078, 'synset': 'flowering_maple.n.01', 'name': 'flowering_maple'}, {'id': 19079, 'synset': 'velvetleaf.n.02', 'name': 'velvetleaf'}, {'id': 19080, 'synset': 'hollyhock.n.02', 'name': 'hollyhock'}, {'id': 19081, 'synset': 'rose_mallow.n.02', 'name': 'rose_mallow'}, {'id': 19082, 'synset': 'althea.n.01', 'name': 'althea'}, {'id': 19083, 'synset': 'marsh_mallow.n.01', 'name': 'marsh_mallow'}, {'id': 19084, 'synset': 'poppy_mallow.n.01', 'name': 'poppy_mallow'}, {'id': 19085, 'synset': 'fringed_poppy_mallow.n.01', 'name': 'fringed_poppy_mallow'}, {'id': 19086, 'synset': 'purple_poppy_mallow.n.01', 'name': 'purple_poppy_mallow'}, {'id': 19087, 'synset': 'clustered_poppy_mallow.n.01', 'name': 'clustered_poppy_mallow'}, {'id': 19088, 'synset': 'sea_island_cotton.n.01', 'name': 'sea_island_cotton'}, {'id': 19089, 'synset': 'levant_cotton.n.01', 'name': 'Levant_cotton'}, {'id': 19090, 'synset': 'upland_cotton.n.01', 'name': 'upland_cotton'}, {'id': 19091, 'synset': 'peruvian_cotton.n.01', 'name': 'Peruvian_cotton'}, {'id': 19092, 'synset': 'wild_cotton.n.01', 'name': 'wild_cotton'}, {'id': 19093, 'synset': 'kenaf.n.02', 'name': 'kenaf'}, {'id': 19094, 'synset': 'sorrel_tree.n.02', 'name': 'sorrel_tree'}, {'id': 19095, 'synset': 'rose_mallow.n.01', 'name': 'rose_mallow'}, {'id': 19096, 'synset': 'cotton_rose.n.01', 'name': 'cotton_rose'}, {'id': 19097, 'synset': 'roselle.n.01', 'name': 'roselle'}, {'id': 19098, 'synset': 'mahoe.n.01', 'name': 'mahoe'}, {'id': 19099, 'synset': 'flower-of-an-hour.n.01', 'name': 'flower-of-an-hour'}, {'id': 19100, 'synset': 'lacebark.n.01', 'name': 'lacebark'}, {'id': 19101, 'synset': 'wild_hollyhock.n.02', 'name': 'wild_hollyhock'}, {'id': 19102, 'synset': 'mountain_hollyhock.n.01', 'name': 'mountain_hollyhock'}, {'id': 19103, 'synset': 'seashore_mallow.n.01', 'name': 'seashore_mallow'}, {'id': 19104, 'synset': 'salt_marsh_mallow.n.01', 'name': 'salt_marsh_mallow'}, {'id': 19105, 'synset': 'chaparral_mallow.n.01', 'name': 'chaparral_mallow'}, {'id': 19106, 'synset': 'malope.n.01', 'name': 'malope'}, {'id': 19107, 'synset': 'false_mallow.n.02', 'name': 'false_mallow'}, {'id': 19108, 'synset': 'waxmallow.n.01', 'name': 'waxmallow'}, {'id': 19109, 'synset': 'glade_mallow.n.01', 'name': 'glade_mallow'}, {'id': 19110, 'synset': 'pavonia.n.01', 'name': 'pavonia'}, {'id': 19111, 'synset': 'ribbon_tree.n.01', 'name': 'ribbon_tree'}, {'id': 19112, 'synset': 'bush_hibiscus.n.01', 'name': 'bush_hibiscus'}, {'id': 19113, 'synset': 'virginia_mallow.n.01', 'name': 'Virginia_mallow'}, {'id': 19114, 'synset': 'queensland_hemp.n.01', 'name': 'Queensland_hemp'}, {'id': 19115, 'synset': 'indian_mallow.n.01', 'name': 'Indian_mallow'}, {'id': 19116, 'synset': 'checkerbloom.n.01', 'name': 'checkerbloom'}, {'id': 19117, 'synset': 'globe_mallow.n.01', 'name': 'globe_mallow'}, {'id': 19118, 'synset': 'prairie_mallow.n.01', 'name': 'prairie_mallow'}, {'id': 19119, 'synset': 'tulipwood_tree.n.01', 'name': 'tulipwood_tree'}, {'id': 19120, 'synset': 'portia_tree.n.01', 'name': 'portia_tree'}, {'id': 19121, 'synset': 'red_silk-cotton_tree.n.01', 'name': 'red_silk-cotton_tree'}, {'id': 19122, 'synset': 'cream-of-tartar_tree.n.01', 'name': 'cream-of-tartar_tree'}, {'id': 19123, 'synset': 'baobab.n.01', 'name': 'baobab'}, {'id': 19124, 'synset': 'kapok.n.02', 'name': 'kapok'}, {'id': 19125, 'synset': 'durian.n.01', 'name': 'durian'}, {'id': 19126, 'synset': 'montezuma.n.01', 'name': 'Montezuma'}, {'id': 19127, 'synset': 'shaving-brush_tree.n.01', 'name': 'shaving-brush_tree'}, {'id': 19128, 'synset': 'quandong.n.03', 'name': 'quandong'}, {'id': 19129, 'synset': 'quandong.n.02', 'name': 'quandong'}, {'id': 19130, 'synset': 'makomako.n.01', 'name': 'makomako'}, {'id': 19131, 'synset': 'jamaican_cherry.n.01', 'name': 'Jamaican_cherry'}, {'id': 19132, 'synset': 'breakax.n.01', 'name': 'breakax'}, {'id': 19133, 'synset': 'sterculia.n.01', 'name': 'sterculia'}, {'id': 19134, 'synset': 'panama_tree.n.01', 'name': 'Panama_tree'}, {'id': 19135, 'synset': 'kalumpang.n.01', 'name': 'kalumpang'}, {'id': 19136, 'synset': 'bottle-tree.n.01', 'name': 'bottle-tree'}, {'id': 19137, 'synset': 'flame_tree.n.04', 'name': 'flame_tree'}, {'id': 19138, 'synset': 'flame_tree.n.03', 'name': 'flame_tree'}, {'id': 19139, 'synset': 'kurrajong.n.01', 'name': 'kurrajong'}, {'id': 19140, 'synset': 'queensland_bottletree.n.01', 'name': 'Queensland_bottletree'}, {'id': 19141, 'synset': 'kola.n.01', 'name': 'kola'}, {'id': 19142, 'synset': 'kola_nut.n.01', 'name': 'kola_nut'}, {'id': 19143, 'synset': 'chinese_parasol_tree.n.01', 'name': 'Chinese_parasol_tree'}, {'id': 19144, 'synset': 'flannelbush.n.01', 'name': 'flannelbush'}, {'id': 19145, 'synset': 'screw_tree.n.01', 'name': 'screw_tree'}, {'id': 19146, 'synset': 'nut-leaved_screw_tree.n.01', 'name': 'nut-leaved_screw_tree'}, {'id': 19147, 'synset': 'red_beech.n.02', 'name': 'red_beech'}, {'id': 19148, 'synset': 'looking_glass_tree.n.01', 'name': 'looking_glass_tree'}, {'id': 19149, 'synset': 'looking-glass_plant.n.01', 'name': 'looking-glass_plant'}, {'id': 19150, 'synset': 'honey_bell.n.01', 'name': 'honey_bell'}, {'id': 19151, 'synset': 'mayeng.n.01', 'name': 'mayeng'}, {'id': 19152, 'synset': 'silver_tree.n.02', 'name': 'silver_tree'}, {'id': 19153, 'synset': 'cacao.n.01', 'name': 'cacao'}, {'id': 19154, 'synset': 'obeche.n.02', 'name': 'obeche'}, {'id': 19155, 'synset': 'linden.n.02', 'name': 'linden'}, {'id': 19156, 'synset': 'american_basswood.n.01', 'name': 'American_basswood'}, {'id': 19157, 'synset': 'small-leaved_linden.n.01', 'name': 'small-leaved_linden'}, {'id': 19158, 'synset': 'white_basswood.n.01', 'name': 'white_basswood'}, {'id': 19159, 'synset': 'japanese_linden.n.01', 'name': 'Japanese_linden'}, {'id': 19160, 'synset': 'silver_lime.n.01', 'name': 'silver_lime'}, {'id': 19161, 'synset': 'corchorus.n.01', 'name': 'corchorus'}, {'id': 19162, 'synset': 'african_hemp.n.02', 'name': 'African_hemp'}, {'id': 19163, 'synset': 'herb.n.01', 'name': 'herb'}, {'id': 19164, 'synset': 'protea.n.01', 'name': 'protea'}, {'id': 19165, 'synset': 'honeypot.n.01', 'name': 'honeypot'}, {'id': 19166, 'synset': 'honeyflower.n.02', 'name': 'honeyflower'}, {'id': 19167, 'synset': 'banksia.n.01', 'name': 'banksia'}, {'id': 19168, 'synset': 'honeysuckle.n.02', 'name': 'honeysuckle'}, {'id': 19169, 'synset': 'smoke_bush.n.02', 'name': 'smoke_bush'}, {'id': 19170, 'synset': 'chilean_firebush.n.01', 'name': 'Chilean_firebush'}, {'id': 19171, 'synset': 'chilean_nut.n.01', 'name': 'Chilean_nut'}, {'id': 19172, 'synset': 'grevillea.n.01', 'name': 'grevillea'}, {'id': 19173, 'synset': 'red-flowered_silky_oak.n.01', 'name': 'red-flowered_silky_oak'}, {'id': 19174, 'synset': 'silky_oak.n.01', 'name': 'silky_oak'}, {'id': 19175, 'synset': 'beefwood.n.05', 'name': 'beefwood'}, {'id': 19176, 'synset': 'cushion_flower.n.01', 'name': 'cushion_flower'}, {'id': 19177, 'synset': 'rewa-rewa.n.01', 'name': 'rewa-rewa'}, {'id': 19178, 'synset': 'honeyflower.n.01', 'name': 'honeyflower'}, {'id': 19179, 'synset': 'silver_tree.n.01', 'name': 'silver_tree'}, {'id': 19180, 'synset': 'lomatia.n.01', 'name': 'lomatia'}, {'id': 19181, 'synset': 'macadamia.n.01', 'name': 'macadamia'}, {'id': 19182, 'synset': 'macadamia_integrifolia.n.01', 'name': 'Macadamia_integrifolia'}, {'id': 19183, 'synset': 'macadamia_nut.n.01', 'name': 'macadamia_nut'}, {'id': 19184, 'synset': 'queensland_nut.n.01', 'name': 'Queensland_nut'}, {'id': 19185, 'synset': 'prickly_ash.n.02', 'name': 'prickly_ash'}, {'id': 19186, 'synset': 'geebung.n.01', 'name': 'geebung'}, {'id': 19187, 'synset': 'wheel_tree.n.01', 'name': 'wheel_tree'}, {'id': 19188, 'synset': 'scrub_beefwood.n.01', 'name': 'scrub_beefwood'}, {'id': 19189, 'synset': 'waratah.n.02', 'name': 'waratah'}, {'id': 19190, 'synset': 'waratah.n.01', 'name': 'waratah'}, {'id': 19191, 'synset': 'casuarina.n.01', 'name': 'casuarina'}, {'id': 19192, 'synset': 'she-oak.n.01', 'name': 'she-oak'}, {'id': 19193, 'synset': 'beefwood.n.03', 'name': 'beefwood'}, {'id': 19194, 'synset': 'australian_pine.n.01', 'name': 'Australian_pine'}, {'id': 19195, 'synset': 'heath.n.01', 'name': 'heath'}, {'id': 19196, 'synset': 'tree_heath.n.02', 'name': 'tree_heath'}, {'id': 19197, 'synset': 'briarroot.n.01', 'name': 'briarroot'}, {'id': 19198, 'synset': 'winter_heath.n.01', 'name': 'winter_heath'}, {'id': 19199, 'synset': 'bell_heather.n.02', 'name': 'bell_heather'}, {'id': 19200, 'synset': 'cornish_heath.n.01', 'name': 'Cornish_heath'}, {'id': 19201, 'synset': 'spanish_heath.n.01', 'name': 'Spanish_heath'}, {'id': 19202, 'synset': "prince-of-wales'-heath.n.01", 'name': "Prince-of-Wales'-heath"}, {'id': 19203, 'synset': 'bog_rosemary.n.01', 'name': 'bog_rosemary'}, {'id': 19204, 'synset': 'marsh_andromeda.n.01', 'name': 'marsh_andromeda'}, {'id': 19205, 'synset': 'madrona.n.01', 'name': 'madrona'}, {'id': 19206, 'synset': 'strawberry_tree.n.01', 'name': 'strawberry_tree'}, {'id': 19207, 'synset': 'bearberry.n.03', 'name': 'bearberry'}, {'id': 19208, 'synset': 'alpine_bearberry.n.01', 'name': 'alpine_bearberry'}, {'id': 19209, 'synset': 'heartleaf_manzanita.n.01', 'name': 'heartleaf_manzanita'}, {'id': 19210, 'synset': 'parry_manzanita.n.01', 'name': 'Parry_manzanita'}, {'id': 19211, 'synset': 'spike_heath.n.01', 'name': 'spike_heath'}, {'id': 19212, 'synset': 'bryanthus.n.01', 'name': 'bryanthus'}, {'id': 19213, 'synset': 'leatherleaf.n.02', 'name': 'leatherleaf'}, {'id': 19214, 'synset': 'connemara_heath.n.01', 'name': 'Connemara_heath'}, {'id': 19215, 'synset': 'trailing_arbutus.n.01', 'name': 'trailing_arbutus'}, {'id': 19216, 'synset': 'creeping_snowberry.n.01', 'name': 'creeping_snowberry'}, {'id': 19217, 'synset': 'salal.n.01', 'name': 'salal'}, {'id': 19218, 'synset': 'huckleberry.n.02', 'name': 'huckleberry'}, {'id': 19219, 'synset': 'black_huckleberry.n.01', 'name': 'black_huckleberry'}, {'id': 19220, 'synset': 'dangleberry.n.01', 'name': 'dangleberry'}, {'id': 19221, 'synset': 'box_huckleberry.n.01', 'name': 'box_huckleberry'}, {'id': 19222, 'synset': 'kalmia.n.01', 'name': 'kalmia'}, {'id': 19223, 'synset': 'mountain_laurel.n.01', 'name': 'mountain_laurel'}, {'id': 19224, 'synset': 'swamp_laurel.n.01', 'name': 'swamp_laurel'}, {'id': 19225, 'synset': "trapper's_tea.n.01", 'name': "trapper's_tea"}, {'id': 19226, 'synset': 'wild_rosemary.n.01', 'name': 'wild_rosemary'}, {'id': 19227, 'synset': 'sand_myrtle.n.01', 'name': 'sand_myrtle'}, {'id': 19228, 'synset': 'leucothoe.n.01', 'name': 'leucothoe'}, {'id': 19229, 'synset': 'dog_laurel.n.01', 'name': 'dog_laurel'}, {'id': 19230, 'synset': 'sweet_bells.n.01', 'name': 'sweet_bells'}, {'id': 19231, 'synset': 'alpine_azalea.n.01', 'name': 'alpine_azalea'}, {'id': 19232, 'synset': 'staggerbush.n.01', 'name': 'staggerbush'}, {'id': 19233, 'synset': 'maleberry.n.01', 'name': 'maleberry'}, {'id': 19234, 'synset': 'fetterbush.n.02', 'name': 'fetterbush'}, {'id': 19235, 'synset': 'false_azalea.n.01', 'name': 'false_azalea'}, {'id': 19236, 'synset': 'minniebush.n.01', 'name': 'minniebush'}, {'id': 19237, 'synset': 'sorrel_tree.n.01', 'name': 'sorrel_tree'}, {'id': 19238, 'synset': 'mountain_heath.n.01', 'name': 'mountain_heath'}, {'id': 19239, 'synset': 'purple_heather.n.01', 'name': 'purple_heather'}, {'id': 19240, 'synset': 'fetterbush.n.01', 'name': 'fetterbush'}, {'id': 19241, 'synset': 'rhododendron.n.01', 'name': 'rhododendron'}, {'id': 19242, 'synset': 'coast_rhododendron.n.01', 'name': 'coast_rhododendron'}, {'id': 19243, 'synset': 'rosebay.n.01', 'name': 'rosebay'}, {'id': 19244, 'synset': 'swamp_azalea.n.01', 'name': 'swamp_azalea'}, {'id': 19245, 'synset': 'azalea.n.01', 'name': 'azalea'}, {'id': 19246, 'synset': 'cranberry.n.01', 'name': 'cranberry'}, {'id': 19247, 'synset': 'american_cranberry.n.01', 'name': 'American_cranberry'}, {'id': 19248, 'synset': 'european_cranberry.n.01', 'name': 'European_cranberry'}, {'id': 19249, 'synset': 'blueberry.n.01', 'name': 'blueberry'}, {'id': 19250, 'synset': 'farkleberry.n.01', 'name': 'farkleberry'}, {'id': 19251, 'synset': 'low-bush_blueberry.n.01', 'name': 'low-bush_blueberry'}, {'id': 19252, 'synset': 'rabbiteye_blueberry.n.01', 'name': 'rabbiteye_blueberry'}, {'id': 19253, 'synset': 'dwarf_bilberry.n.01', 'name': 'dwarf_bilberry'}, {'id': 19254, 'synset': 'evergreen_blueberry.n.01', 'name': 'evergreen_blueberry'}, {'id': 19255, 'synset': 'evergreen_huckleberry.n.01', 'name': 'evergreen_huckleberry'}, {'id': 19256, 'synset': 'bilberry.n.02', 'name': 'bilberry'}, {'id': 19257, 'synset': 'bilberry.n.01', 'name': 'bilberry'}, {'id': 19258, 'synset': 'bog_bilberry.n.01', 'name': 'bog_bilberry'}, {'id': 19259, 'synset': 'dryland_blueberry.n.01', 'name': 'dryland_blueberry'}, {'id': 19260, 'synset': 'grouseberry.n.01', 'name': 'grouseberry'}, {'id': 19261, 'synset': 'deerberry.n.01', 'name': 'deerberry'}, {'id': 19262, 'synset': 'cowberry.n.01', 'name': 'cowberry'}, {'id': 19263, 'synset': 'diapensia.n.01', 'name': 'diapensia'}, {'id': 19264, 'synset': 'galax.n.01', 'name': 'galax'}, {'id': 19265, 'synset': 'pyxie.n.01', 'name': 'pyxie'}, {'id': 19266, 'synset': 'shortia.n.01', 'name': 'shortia'}, {'id': 19267, 'synset': 'oconee_bells.n.01', 'name': 'oconee_bells'}, {'id': 19268, 'synset': 'australian_heath.n.01', 'name': 'Australian_heath'}, {'id': 19269, 'synset': 'epacris.n.01', 'name': 'epacris'}, {'id': 19270, 'synset': 'common_heath.n.02', 'name': 'common_heath'}, {'id': 19271, 'synset': 'common_heath.n.01', 'name': 'common_heath'}, {'id': 19272, 'synset': 'port_jackson_heath.n.01', 'name': 'Port_Jackson_heath'}, {'id': 19273, 'synset': 'native_cranberry.n.01', 'name': 'native_cranberry'}, {'id': 19274, 'synset': 'pink_fivecorner.n.01', 'name': 'pink_fivecorner'}, {'id': 19275, 'synset': 'wintergreen.n.01', 'name': 'wintergreen'}, {'id': 19276, 'synset': 'false_wintergreen.n.01', 'name': 'false_wintergreen'}, {'id': 19277, 'synset': 'lesser_wintergreen.n.01', 'name': 'lesser_wintergreen'}, {'id': 19278, 'synset': 'wild_lily_of_the_valley.n.02', 'name': 'wild_lily_of_the_valley'}, {'id': 19279, 'synset': 'wild_lily_of_the_valley.n.01', 'name': 'wild_lily_of_the_valley'}, {'id': 19280, 'synset': 'pipsissewa.n.01', 'name': 'pipsissewa'}, {'id': 19281, 'synset': 'love-in-winter.n.01', 'name': 'love-in-winter'}, {'id': 19282, 'synset': 'one-flowered_wintergreen.n.01', 'name': 'one-flowered_wintergreen'}, {'id': 19283, 'synset': 'indian_pipe.n.01', 'name': 'Indian_pipe'}, {'id': 19284, 'synset': 'pinesap.n.01', 'name': 'pinesap'}, {'id': 19285, 'synset': 'beech.n.01', 'name': 'beech'}, {'id': 19286, 'synset': 'common_beech.n.01', 'name': 'common_beech'}, {'id': 19287, 'synset': 'copper_beech.n.01', 'name': 'copper_beech'}, {'id': 19288, 'synset': 'american_beech.n.01', 'name': 'American_beech'}, {'id': 19289, 'synset': 'weeping_beech.n.01', 'name': 'weeping_beech'}, {'id': 19290, 'synset': 'japanese_beech.n.01', 'name': 'Japanese_beech'}, {'id': 19291, 'synset': 'chestnut.n.02', 'name': 'chestnut'}, {'id': 19292, 'synset': 'american_chestnut.n.01', 'name': 'American_chestnut'}, {'id': 19293, 'synset': 'european_chestnut.n.01', 'name': 'European_chestnut'}, {'id': 19294, 'synset': 'chinese_chestnut.n.01', 'name': 'Chinese_chestnut'}, {'id': 19295, 'synset': 'japanese_chestnut.n.01', 'name': 'Japanese_chestnut'}, {'id': 19296, 'synset': 'allegheny_chinkapin.n.01', 'name': 'Allegheny_chinkapin'}, {'id': 19297, 'synset': 'ozark_chinkapin.n.01', 'name': 'Ozark_chinkapin'}, {'id': 19298, 'synset': 'oak_chestnut.n.01', 'name': 'oak_chestnut'}, {'id': 19299, 'synset': 'giant_chinkapin.n.01', 'name': 'giant_chinkapin'}, {'id': 19300, 'synset': 'dwarf_golden_chinkapin.n.01', 'name': 'dwarf_golden_chinkapin'}, {'id': 19301, 'synset': 'tanbark_oak.n.01', 'name': 'tanbark_oak'}, {'id': 19302, 'synset': 'japanese_oak.n.02', 'name': 'Japanese_oak'}, {'id': 19303, 'synset': 'southern_beech.n.01', 'name': 'southern_beech'}, {'id': 19304, 'synset': 'myrtle_beech.n.01', 'name': 'myrtle_beech'}, {'id': 19305, 'synset': 'coigue.n.01', 'name': 'Coigue'}, {'id': 19306, 'synset': 'new_zealand_beech.n.01', 'name': 'New_Zealand_beech'}, {'id': 19307, 'synset': 'silver_beech.n.01', 'name': 'silver_beech'}, {'id': 19308, 'synset': 'roble_beech.n.01', 'name': 'roble_beech'}, {'id': 19309, 'synset': 'rauli_beech.n.01', 'name': 'rauli_beech'}, {'id': 19310, 'synset': 'black_beech.n.01', 'name': 'black_beech'}, {'id': 19311, 'synset': 'hard_beech.n.01', 'name': 'hard_beech'}, {'id': 19312, 'synset': 'acorn.n.01', 'name': 'acorn'}, {'id': 19313, 'synset': 'cupule.n.01', 'name': 'cupule'}, {'id': 19314, 'synset': 'oak.n.02', 'name': 'oak'}, {'id': 19315, 'synset': 'live_oak.n.01', 'name': 'live_oak'}, {'id': 19316, 'synset': 'coast_live_oak.n.01', 'name': 'coast_live_oak'}, {'id': 19317, 'synset': 'white_oak.n.01', 'name': 'white_oak'}, {'id': 19318, 'synset': 'american_white_oak.n.01', 'name': 'American_white_oak'}, {'id': 19319, 'synset': 'arizona_white_oak.n.01', 'name': 'Arizona_white_oak'}, {'id': 19320, 'synset': 'swamp_white_oak.n.01', 'name': 'swamp_white_oak'}, {'id': 19321, 'synset': 'european_turkey_oak.n.01', 'name': 'European_turkey_oak'}, {'id': 19322, 'synset': 'canyon_oak.n.01', 'name': 'canyon_oak'}, {'id': 19323, 'synset': 'scarlet_oak.n.01', 'name': 'scarlet_oak'}, {'id': 19324, 'synset': 'jack_oak.n.02', 'name': 'jack_oak'}, {'id': 19325, 'synset': 'red_oak.n.01', 'name': 'red_oak'}, {'id': 19326, 'synset': 'southern_red_oak.n.01', 'name': 'southern_red_oak'}, {'id': 19327, 'synset': 'oregon_white_oak.n.01', 'name': 'Oregon_white_oak'}, {'id': 19328, 'synset': 'holm_oak.n.02', 'name': 'holm_oak'}, {'id': 19329, 'synset': 'bear_oak.n.01', 'name': 'bear_oak'}, {'id': 19330, 'synset': 'shingle_oak.n.01', 'name': 'shingle_oak'}, {'id': 19331, 'synset': 'bluejack_oak.n.01', 'name': 'bluejack_oak'}, {'id': 19332, 'synset': 'california_black_oak.n.01', 'name': 'California_black_oak'}, {'id': 19333, 'synset': 'american_turkey_oak.n.01', 'name': 'American_turkey_oak'}, {'id': 19334, 'synset': 'laurel_oak.n.01', 'name': 'laurel_oak'}, {'id': 19335, 'synset': 'california_white_oak.n.01', 'name': 'California_white_oak'}, {'id': 19336, 'synset': 'overcup_oak.n.01', 'name': 'overcup_oak'}, {'id': 19337, 'synset': 'bur_oak.n.01', 'name': 'bur_oak'}, {'id': 19338, 'synset': 'scrub_oak.n.01', 'name': 'scrub_oak'}, {'id': 19339, 'synset': 'blackjack_oak.n.01', 'name': 'blackjack_oak'}, {'id': 19340, 'synset': 'swamp_chestnut_oak.n.01', 'name': 'swamp_chestnut_oak'}, {'id': 19341, 'synset': 'japanese_oak.n.01', 'name': 'Japanese_oak'}, {'id': 19342, 'synset': 'chestnut_oak.n.01', 'name': 'chestnut_oak'}, {'id': 19343, 'synset': 'chinquapin_oak.n.01', 'name': 'chinquapin_oak'}, {'id': 19344, 'synset': 'myrtle_oak.n.01', 'name': 'myrtle_oak'}, {'id': 19345, 'synset': 'water_oak.n.01', 'name': 'water_oak'}, {'id': 19346, 'synset': 'nuttall_oak.n.01', 'name': 'Nuttall_oak'}, {'id': 19347, 'synset': 'durmast.n.01', 'name': 'durmast'}, {'id': 19348, 'synset': 'basket_oak.n.01', 'name': 'basket_oak'}, {'id': 19349, 'synset': 'pin_oak.n.01', 'name': 'pin_oak'}, {'id': 19350, 'synset': 'willow_oak.n.01', 'name': 'willow_oak'}, {'id': 19351, 'synset': 'dwarf_chinkapin_oak.n.01', 'name': 'dwarf_chinkapin_oak'}, {'id': 19352, 'synset': 'common_oak.n.01', 'name': 'common_oak'}, {'id': 19353, 'synset': 'northern_red_oak.n.01', 'name': 'northern_red_oak'}, {'id': 19354, 'synset': 'shumard_oak.n.01', 'name': 'Shumard_oak'}, {'id': 19355, 'synset': 'post_oak.n.01', 'name': 'post_oak'}, {'id': 19356, 'synset': 'cork_oak.n.01', 'name': 'cork_oak'}, {'id': 19357, 'synset': 'spanish_oak.n.01', 'name': 'Spanish_oak'}, {'id': 19358, 'synset': 'huckleberry_oak.n.01', 'name': 'huckleberry_oak'}, {'id': 19359, 'synset': 'chinese_cork_oak.n.01', 'name': 'Chinese_cork_oak'}, {'id': 19360, 'synset': 'black_oak.n.01', 'name': 'black_oak'}, {'id': 19361, 'synset': 'southern_live_oak.n.01', 'name': 'southern_live_oak'}, {'id': 19362, 'synset': 'interior_live_oak.n.01', 'name': 'interior_live_oak'}, {'id': 19363, 'synset': 'mast.n.02', 'name': 'mast'}, {'id': 19364, 'synset': 'birch.n.02', 'name': 'birch'}, {'id': 19365, 'synset': 'yellow_birch.n.01', 'name': 'yellow_birch'}, {'id': 19366, 'synset': 'american_white_birch.n.01', 'name': 'American_white_birch'}, {'id': 19367, 'synset': 'grey_birch.n.01', 'name': 'grey_birch'}, {'id': 19368, 'synset': 'silver_birch.n.01', 'name': 'silver_birch'}, {'id': 19369, 'synset': 'downy_birch.n.01', 'name': 'downy_birch'}, {'id': 19370, 'synset': 'black_birch.n.02', 'name': 'black_birch'}, {'id': 19371, 'synset': 'sweet_birch.n.01', 'name': 'sweet_birch'}, {'id': 19372, 'synset': 'yukon_white_birch.n.01', 'name': 'Yukon_white_birch'}, {'id': 19373, 'synset': 'swamp_birch.n.01', 'name': 'swamp_birch'}, {'id': 19374, 'synset': 'newfoundland_dwarf_birch.n.01', 'name': 'Newfoundland_dwarf_birch'}, {'id': 19375, 'synset': 'alder.n.02', 'name': 'alder'}, {'id': 19376, 'synset': 'common_alder.n.01', 'name': 'common_alder'}, {'id': 19377, 'synset': 'grey_alder.n.01', 'name': 'grey_alder'}, {'id': 19378, 'synset': 'seaside_alder.n.01', 'name': 'seaside_alder'}, {'id': 19379, 'synset': 'white_alder.n.01', 'name': 'white_alder'}, {'id': 19380, 'synset': 'red_alder.n.01', 'name': 'red_alder'}, {'id': 19381, 'synset': 'speckled_alder.n.01', 'name': 'speckled_alder'}, {'id': 19382, 'synset': 'smooth_alder.n.01', 'name': 'smooth_alder'}, {'id': 19383, 'synset': 'green_alder.n.02', 'name': 'green_alder'}, {'id': 19384, 'synset': 'green_alder.n.01', 'name': 'green_alder'}, {'id': 19385, 'synset': 'hornbeam.n.01', 'name': 'hornbeam'}, {'id': 19386, 'synset': 'european_hornbeam.n.01', 'name': 'European_hornbeam'}, {'id': 19387, 'synset': 'american_hornbeam.n.01', 'name': 'American_hornbeam'}, {'id': 19388, 'synset': 'hop_hornbeam.n.01', 'name': 'hop_hornbeam'}, {'id': 19389, 'synset': 'old_world_hop_hornbeam.n.01', 'name': 'Old_World_hop_hornbeam'}, {'id': 19390, 'synset': 'eastern_hop_hornbeam.n.01', 'name': 'Eastern_hop_hornbeam'}, {'id': 19391, 'synset': 'hazelnut.n.01', 'name': 'hazelnut'}, {'id': 19392, 'synset': 'american_hazel.n.01', 'name': 'American_hazel'}, {'id': 19393, 'synset': 'cobnut.n.01', 'name': 'cobnut'}, {'id': 19394, 'synset': 'beaked_hazelnut.n.01', 'name': 'beaked_hazelnut'}, {'id': 19395, 'synset': 'centaury.n.01', 'name': 'centaury'}, {'id': 19396, 'synset': 'rosita.n.01', 'name': 'rosita'}, {'id': 19397, 'synset': 'lesser_centaury.n.01', 'name': 'lesser_centaury'}, {'id': 19398, 'synset': 'seaside_centaury.n.01', 'name': 'seaside_centaury'}, {'id': 19399, 'synset': 'slender_centaury.n.01', 'name': 'slender_centaury'}, {'id': 19400, 'synset': 'prairie_gentian.n.01', 'name': 'prairie_gentian'}, {'id': 19401, 'synset': 'persian_violet.n.01', 'name': 'Persian_violet'}, {'id': 19402, 'synset': 'columbo.n.01', 'name': 'columbo'}, {'id': 19403, 'synset': 'gentian.n.01', 'name': 'gentian'}, {'id': 19404, 'synset': 'gentianella.n.02', 'name': 'gentianella'}, {'id': 19405, 'synset': 'closed_gentian.n.02', 'name': 'closed_gentian'}, {'id': 19406, 'synset': "explorer's_gentian.n.01", 'name': "explorer's_gentian"}, {'id': 19407, 'synset': 'closed_gentian.n.01', 'name': 'closed_gentian'}, {'id': 19408, 'synset': 'great_yellow_gentian.n.01', 'name': 'great_yellow_gentian'}, {'id': 19409, 'synset': 'marsh_gentian.n.01', 'name': 'marsh_gentian'}, {'id': 19410, 'synset': 'soapwort_gentian.n.01', 'name': 'soapwort_gentian'}, {'id': 19411, 'synset': 'striped_gentian.n.01', 'name': 'striped_gentian'}, {'id': 19412, 'synset': 'agueweed.n.01', 'name': 'agueweed'}, {'id': 19413, 'synset': 'felwort.n.01', 'name': 'felwort'}, {'id': 19414, 'synset': 'fringed_gentian.n.01', 'name': 'fringed_gentian'}, {'id': 19415, 'synset': 'gentianopsis_crinita.n.01', 'name': 'Gentianopsis_crinita'}, {'id': 19416, 'synset': 'gentianopsis_detonsa.n.01', 'name': 'Gentianopsis_detonsa'}, {'id': 19417, 'synset': 'gentianopsid_procera.n.01', 'name': 'Gentianopsid_procera'}, {'id': 19418, 'synset': 'gentianopsis_thermalis.n.01', 'name': 'Gentianopsis_thermalis'}, {'id': 19419, 'synset': 'tufted_gentian.n.01', 'name': 'tufted_gentian'}, {'id': 19420, 'synset': 'spurred_gentian.n.01', 'name': 'spurred_gentian'}, {'id': 19421, 'synset': 'sabbatia.n.01', 'name': 'sabbatia'}, {'id': 19422, 'synset': 'toothbrush_tree.n.01', 'name': 'toothbrush_tree'}, {'id': 19423, 'synset': 'olive_tree.n.01', 'name': 'olive_tree'}, {'id': 19424, 'synset': 'olive.n.02', 'name': 'olive'}, {'id': 19425, 'synset': 'olive.n.01', 'name': 'olive'}, {'id': 19426, 'synset': 'black_maire.n.01', 'name': 'black_maire'}, {'id': 19427, 'synset': 'white_maire.n.01', 'name': 'white_maire'}, {'id': 19428, 'synset': 'fringe_tree.n.01', 'name': 'fringe_tree'}, {'id': 19429, 'synset': 'fringe_bush.n.01', 'name': 'fringe_bush'}, {'id': 19430, 'synset': 'forestiera.n.01', 'name': 'forestiera'}, {'id': 19431, 'synset': 'forsythia.n.01', 'name': 'forsythia'}, {'id': 19432, 'synset': 'ash.n.02', 'name': 'ash'}, {'id': 19433, 'synset': 'white_ash.n.02', 'name': 'white_ash'}, {'id': 19434, 'synset': 'swamp_ash.n.01', 'name': 'swamp_ash'}, {'id': 19435, 'synset': 'flowering_ash.n.03', 'name': 'flowering_ash'}, {'id': 19436, 'synset': 'european_ash.n.01', 'name': 'European_ash'}, {'id': 19437, 'synset': 'oregon_ash.n.01', 'name': 'Oregon_ash'}, {'id': 19438, 'synset': 'black_ash.n.01', 'name': 'black_ash'}, {'id': 19439, 'synset': 'manna_ash.n.01', 'name': 'manna_ash'}, {'id': 19440, 'synset': 'red_ash.n.01', 'name': 'red_ash'}, {'id': 19441, 'synset': 'green_ash.n.01', 'name': 'green_ash'}, {'id': 19442, 'synset': 'blue_ash.n.01', 'name': 'blue_ash'}, {'id': 19443, 'synset': 'mountain_ash.n.03', 'name': 'mountain_ash'}, {'id': 19444, 'synset': 'pumpkin_ash.n.01', 'name': 'pumpkin_ash'}, {'id': 19445, 'synset': 'arizona_ash.n.01', 'name': 'Arizona_ash'}, {'id': 19446, 'synset': 'jasmine.n.01', 'name': 'jasmine'}, {'id': 19447, 'synset': 'primrose_jasmine.n.01', 'name': 'primrose_jasmine'}, {'id': 19448, 'synset': 'winter_jasmine.n.01', 'name': 'winter_jasmine'}, {'id': 19449, 'synset': 'common_jasmine.n.01', 'name': 'common_jasmine'}, {'id': 19450, 'synset': 'privet.n.01', 'name': 'privet'}, {'id': 19451, 'synset': 'amur_privet.n.01', 'name': 'Amur_privet'}, {'id': 19452, 'synset': 'japanese_privet.n.01', 'name': 'Japanese_privet'}, {'id': 19453, 'synset': 'ligustrum_obtusifolium.n.01', 'name': 'Ligustrum_obtusifolium'}, {'id': 19454, 'synset': 'common_privet.n.01', 'name': 'common_privet'}, {'id': 19455, 'synset': 'devilwood.n.01', 'name': 'devilwood'}, {'id': 19456, 'synset': 'mock_privet.n.01', 'name': 'mock_privet'}, {'id': 19457, 'synset': 'lilac.n.01', 'name': 'lilac'}, {'id': 19458, 'synset': 'himalayan_lilac.n.01', 'name': 'Himalayan_lilac'}, {'id': 19459, 'synset': 'persian_lilac.n.02', 'name': 'Persian_lilac'}, {'id': 19460, 'synset': 'japanese_tree_lilac.n.01', 'name': 'Japanese_tree_lilac'}, {'id': 19461, 'synset': 'japanese_lilac.n.01', 'name': 'Japanese_lilac'}, {'id': 19462, 'synset': 'common_lilac.n.01', 'name': 'common_lilac'}, {'id': 19463, 'synset': 'bloodwort.n.01', 'name': 'bloodwort'}, {'id': 19464, 'synset': 'kangaroo_paw.n.01', 'name': 'kangaroo_paw'}, {'id': 19465, 'synset': 'virginian_witch_hazel.n.01', 'name': 'Virginian_witch_hazel'}, {'id': 19466, 'synset': 'vernal_witch_hazel.n.01', 'name': 'vernal_witch_hazel'}, {'id': 19467, 'synset': 'winter_hazel.n.01', 'name': 'winter_hazel'}, {'id': 19468, 'synset': 'fothergilla.n.01', 'name': 'fothergilla'}, {'id': 19469, 'synset': 'liquidambar.n.02', 'name': 'liquidambar'}, {'id': 19470, 'synset': 'sweet_gum.n.03', 'name': 'sweet_gum'}, {'id': 19471, 'synset': 'iron_tree.n.01', 'name': 'iron_tree'}, {'id': 19472, 'synset': 'walnut.n.03', 'name': 'walnut'}, {'id': 19473, 'synset': 'california_black_walnut.n.01', 'name': 'California_black_walnut'}, {'id': 19474, 'synset': 'butternut.n.01', 'name': 'butternut'}, {'id': 19475, 'synset': 'black_walnut.n.01', 'name': 'black_walnut'}, {'id': 19476, 'synset': 'english_walnut.n.01', 'name': 'English_walnut'}, {'id': 19477, 'synset': 'hickory.n.02', 'name': 'hickory'}, {'id': 19478, 'synset': 'water_hickory.n.01', 'name': 'water_hickory'}, {'id': 19479, 'synset': 'pignut.n.01', 'name': 'pignut'}, {'id': 19480, 'synset': 'bitternut.n.01', 'name': 'bitternut'}, {'id': 19481, 'synset': 'pecan.n.02', 'name': 'pecan'}, {'id': 19482, 'synset': 'big_shellbark.n.01', 'name': 'big_shellbark'}, {'id': 19483, 'synset': 'nutmeg_hickory.n.01', 'name': 'nutmeg_hickory'}, {'id': 19484, 'synset': 'shagbark.n.01', 'name': 'shagbark'}, {'id': 19485, 'synset': 'mockernut.n.01', 'name': 'mockernut'}, {'id': 19486, 'synset': 'wing_nut.n.01', 'name': 'wing_nut'}, {'id': 19487, 'synset': 'caucasian_walnut.n.01', 'name': 'Caucasian_walnut'}, {'id': 19488, 'synset': 'dhawa.n.01', 'name': 'dhawa'}, {'id': 19489, 'synset': 'combretum.n.01', 'name': 'combretum'}, {'id': 19490, 'synset': 'hiccup_nut.n.01', 'name': 'hiccup_nut'}, {'id': 19491, 'synset': 'bush_willow.n.02', 'name': 'bush_willow'}, {'id': 19492, 'synset': 'bush_willow.n.01', 'name': 'bush_willow'}, {'id': 19493, 'synset': 'button_tree.n.01', 'name': 'button_tree'}, {'id': 19494, 'synset': 'white_mangrove.n.02', 'name': 'white_mangrove'}, {'id': 19495, 'synset': 'oleaster.n.01', 'name': 'oleaster'}, {'id': 19496, 'synset': 'water_milfoil.n.01', 'name': 'water_milfoil'}, {'id': 19497, 'synset': 'anchovy_pear.n.01', 'name': 'anchovy_pear'}, {'id': 19498, 'synset': 'brazil_nut.n.01', 'name': 'brazil_nut'}, {'id': 19499, 'synset': 'loosestrife.n.01', 'name': 'loosestrife'}, {'id': 19500, 'synset': 'purple_loosestrife.n.01', 'name': 'purple_loosestrife'}, {'id': 19501, 'synset': 'grass_poly.n.01', 'name': 'grass_poly'}, {'id': 19502, 'synset': 'crape_myrtle.n.01', 'name': 'crape_myrtle'}, {'id': 19503, 'synset': "queen's_crape_myrtle.n.01", 'name': "Queen's_crape_myrtle"}, {'id': 19504, 'synset': 'myrtaceous_tree.n.01', 'name': 'myrtaceous_tree'}, {'id': 19505, 'synset': 'myrtle.n.02', 'name': 'myrtle'}, {'id': 19506, 'synset': 'common_myrtle.n.01', 'name': 'common_myrtle'}, {'id': 19507, 'synset': 'bayberry.n.01', 'name': 'bayberry'}, {'id': 19508, 'synset': 'allspice.n.01', 'name': 'allspice'}, {'id': 19509, 'synset': 'allspice_tree.n.01', 'name': 'allspice_tree'}, {'id': 19510, 'synset': 'sour_cherry.n.02', 'name': 'sour_cherry'}, {'id': 19511, 'synset': 'nakedwood.n.02', 'name': 'nakedwood'}, {'id': 19512, 'synset': 'surinam_cherry.n.02', 'name': 'Surinam_cherry'}, {'id': 19513, 'synset': 'rose_apple.n.01', 'name': 'rose_apple'}, {'id': 19514, 'synset': 'feijoa.n.01', 'name': 'feijoa'}, {'id': 19515, 'synset': 'jaboticaba.n.01', 'name': 'jaboticaba'}, {'id': 19516, 'synset': 'guava.n.02', 'name': 'guava'}, {'id': 19517, 'synset': 'guava.n.01', 'name': 'guava'}, {'id': 19518, 'synset': 'cattley_guava.n.01', 'name': 'cattley_guava'}, {'id': 19519, 'synset': 'brazilian_guava.n.01', 'name': 'Brazilian_guava'}, {'id': 19520, 'synset': 'gum_tree.n.01', 'name': 'gum_tree'}, {'id': 19521, 'synset': 'eucalyptus.n.02', 'name': 'eucalyptus'}, {'id': 19522, 'synset': 'flooded_gum.n.01', 'name': 'flooded_gum'}, {'id': 19523, 'synset': 'mallee.n.01', 'name': 'mallee'}, {'id': 19524, 'synset': 'stringybark.n.01', 'name': 'stringybark'}, {'id': 19525, 'synset': 'smoothbark.n.01', 'name': 'smoothbark'}, {'id': 19526, 'synset': 'red_gum.n.03', 'name': 'red_gum'}, {'id': 19527, 'synset': 'red_gum.n.02', 'name': 'red_gum'}, {'id': 19528, 'synset': 'river_red_gum.n.01', 'name': 'river_red_gum'}, {'id': 19529, 'synset': 'mountain_swamp_gum.n.01', 'name': 'mountain_swamp_gum'}, {'id': 19530, 'synset': 'snow_gum.n.01', 'name': 'snow_gum'}, {'id': 19531, 'synset': 'alpine_ash.n.01', 'name': 'alpine_ash'}, {'id': 19532, 'synset': 'white_mallee.n.01', 'name': 'white_mallee'}, {'id': 19533, 'synset': 'white_stringybark.n.01', 'name': 'white_stringybark'}, {'id': 19534, 'synset': 'white_mountain_ash.n.01', 'name': 'white_mountain_ash'}, {'id': 19535, 'synset': 'blue_gum.n.01', 'name': 'blue_gum'}, {'id': 19536, 'synset': 'rose_gum.n.01', 'name': 'rose_gum'}, {'id': 19537, 'synset': 'cider_gum.n.01', 'name': 'cider_gum'}, {'id': 19538, 'synset': 'swamp_gum.n.01', 'name': 'swamp_gum'}, {'id': 19539, 'synset': 'spotted_gum.n.01', 'name': 'spotted_gum'}, {'id': 19540, 'synset': 'lemon-scented_gum.n.01', 'name': 'lemon-scented_gum'}, {'id': 19541, 'synset': 'black_mallee.n.01', 'name': 'black_mallee'}, {'id': 19542, 'synset': 'forest_red_gum.n.01', 'name': 'forest_red_gum'}, {'id': 19543, 'synset': 'mountain_ash.n.02', 'name': 'mountain_ash'}, {'id': 19544, 'synset': 'manna_gum.n.01', 'name': 'manna_gum'}, {'id': 19545, 'synset': 'clove.n.02', 'name': 'clove'}, {'id': 19546, 'synset': 'clove.n.01', 'name': 'clove'}, {'id': 19547, 'synset': 'tupelo.n.02', 'name': 'tupelo'}, {'id': 19548, 'synset': 'water_gum.n.01', 'name': 'water_gum'}, {'id': 19549, 'synset': 'sour_gum.n.01', 'name': 'sour_gum'}, {'id': 19550, 'synset': "enchanter's_nightshade.n.01", 'name': "enchanter's_nightshade"}, {'id': 19551, 'synset': 'circaea_lutetiana.n.01', 'name': 'Circaea_lutetiana'}, {'id': 19552, 'synset': 'willowherb.n.01', 'name': 'willowherb'}, {'id': 19553, 'synset': 'fireweed.n.01', 'name': 'fireweed'}, {'id': 19554, 'synset': 'california_fuchsia.n.01', 'name': 'California_fuchsia'}, {'id': 19555, 'synset': 'fuchsia.n.01', 'name': 'fuchsia'}, {'id': 19556, 'synset': "lady's-eardrop.n.01", 'name': "lady's-eardrop"}, {'id': 19557, 'synset': 'evening_primrose.n.01', 'name': 'evening_primrose'}, {'id': 19558, 'synset': 'common_evening_primrose.n.01', 'name': 'common_evening_primrose'}, {'id': 19559, 'synset': 'sundrops.n.01', 'name': 'sundrops'}, {'id': 19560, 'synset': 'missouri_primrose.n.01', 'name': 'Missouri_primrose'}, {'id': 19561, 'synset': 'pomegranate.n.01', 'name': 'pomegranate'}, {'id': 19562, 'synset': 'mangrove.n.01', 'name': 'mangrove'}, {'id': 19563, 'synset': 'daphne.n.01', 'name': 'daphne'}, {'id': 19564, 'synset': 'garland_flower.n.01', 'name': 'garland_flower'}, {'id': 19565, 'synset': 'spurge_laurel.n.01', 'name': 'spurge_laurel'}, {'id': 19566, 'synset': 'mezereon.n.01', 'name': 'mezereon'}, {'id': 19567, 'synset': 'indian_rhododendron.n.01', 'name': 'Indian_rhododendron'}, {'id': 19568, 'synset': 'medinilla_magnifica.n.01', 'name': 'Medinilla_magnifica'}, {'id': 19569, 'synset': 'deer_grass.n.01', 'name': 'deer_grass'}, {'id': 19570, 'synset': 'canna.n.01', 'name': 'canna'}, {'id': 19571, 'synset': 'achira.n.01', 'name': 'achira'}, {'id': 19572, 'synset': 'arrowroot.n.02', 'name': 'arrowroot'}, {'id': 19573, 'synset': 'banana.n.01', 'name': 'banana'}, {'id': 19574, 'synset': 'dwarf_banana.n.01', 'name': 'dwarf_banana'}, {'id': 19575, 'synset': 'japanese_banana.n.01', 'name': 'Japanese_banana'}, {'id': 19576, 'synset': 'plantain.n.02', 'name': 'plantain'}, {'id': 19577, 'synset': 'edible_banana.n.01', 'name': 'edible_banana'}, {'id': 19578, 'synset': 'abaca.n.02', 'name': 'abaca'}, {'id': 19579, 'synset': 'abyssinian_banana.n.01', 'name': 'Abyssinian_banana'}, {'id': 19580, 'synset': 'ginger.n.01', 'name': 'ginger'}, {'id': 19581, 'synset': 'common_ginger.n.01', 'name': 'common_ginger'}, {'id': 19582, 'synset': 'turmeric.n.01', 'name': 'turmeric'}, {'id': 19583, 'synset': 'galangal.n.01', 'name': 'galangal'}, {'id': 19584, 'synset': 'shellflower.n.02', 'name': 'shellflower'}, {'id': 19585, 'synset': 'grains_of_paradise.n.01', 'name': 'grains_of_paradise'}, {'id': 19586, 'synset': 'cardamom.n.01', 'name': 'cardamom'}, {'id': 19587, 'synset': 'begonia.n.01', 'name': 'begonia'}, {'id': 19588, 'synset': 'fibrous-rooted_begonia.n.01', 'name': 'fibrous-rooted_begonia'}, {'id': 19589, 'synset': 'tuberous_begonia.n.01', 'name': 'tuberous_begonia'}, {'id': 19590, 'synset': 'rhizomatous_begonia.n.01', 'name': 'rhizomatous_begonia'}, {'id': 19591, 'synset': 'christmas_begonia.n.01', 'name': 'Christmas_begonia'}, {'id': 19592, 'synset': 'angel-wing_begonia.n.01', 'name': 'angel-wing_begonia'}, {'id': 19593, 'synset': 'beefsteak_begonia.n.01', 'name': 'beefsteak_begonia'}, {'id': 19594, 'synset': 'star_begonia.n.01', 'name': 'star_begonia'}, {'id': 19595, 'synset': 'rex_begonia.n.01', 'name': 'rex_begonia'}, {'id': 19596, 'synset': 'wax_begonia.n.01', 'name': 'wax_begonia'}, {'id': 19597, 'synset': 'socotra_begonia.n.01', 'name': 'Socotra_begonia'}, {'id': 19598, 'synset': 'hybrid_tuberous_begonia.n.01', 'name': 'hybrid_tuberous_begonia'}, {'id': 19599, 'synset': 'dillenia.n.01', 'name': 'dillenia'}, {'id': 19600, 'synset': 'guinea_gold_vine.n.01', 'name': 'guinea_gold_vine'}, {'id': 19601, 'synset': 'poon.n.02', 'name': 'poon'}, {'id': 19602, 'synset': 'calaba.n.01', 'name': 'calaba'}, {'id': 19603, 'synset': 'maria.n.02', 'name': 'Maria'}, {'id': 19604, 'synset': 'laurelwood.n.01', 'name': 'laurelwood'}, {'id': 19605, 'synset': 'alexandrian_laurel.n.01', 'name': 'Alexandrian_laurel'}, {'id': 19606, 'synset': 'clusia.n.01', 'name': 'clusia'}, {'id': 19607, 'synset': 'wild_fig.n.02', 'name': 'wild_fig'}, {'id': 19608, 'synset': 'waxflower.n.02', 'name': 'waxflower'}, {'id': 19609, 'synset': 'pitch_apple.n.01', 'name': 'pitch_apple'}, {'id': 19610, 'synset': 'mangosteen.n.01', 'name': 'mangosteen'}, {'id': 19611, 'synset': 'gamboge_tree.n.01', 'name': 'gamboge_tree'}, {'id': 19612, 'synset': "st_john's_wort.n.01", 'name': "St_John's_wort"}, {'id': 19613, 'synset': "common_st_john's_wort.n.01", 'name': "common_St_John's_wort"}, {'id': 19614, 'synset': "great_st_john's_wort.n.01", 'name': "great_St_John's_wort"}, {'id': 19615, 'synset': "creeping_st_john's_wort.n.01", 'name': "creeping_St_John's_wort"}, {'id': 19616, 'synset': "low_st_andrew's_cross.n.01", 'name': "low_St_Andrew's_cross"}, {'id': 19617, 'synset': 'klammath_weed.n.01', 'name': 'klammath_weed'}, {'id': 19618, 'synset': "shrubby_st_john's_wort.n.01", 'name': "shrubby_St_John's_wort"}, {'id': 19619, 'synset': "st_peter's_wort.n.01", 'name': "St_Peter's_wort"}, {'id': 19620, 'synset': "marsh_st-john's_wort.n.01", 'name': "marsh_St-John's_wort"}, {'id': 19621, 'synset': 'mammee_apple.n.01', 'name': 'mammee_apple'}, {'id': 19622, 'synset': 'rose_chestnut.n.01', 'name': 'rose_chestnut'}, {'id': 19623, 'synset': 'bower_actinidia.n.01', 'name': 'bower_actinidia'}, {'id': 19624, 'synset': 'chinese_gooseberry.n.01', 'name': 'Chinese_gooseberry'}, {'id': 19625, 'synset': 'silvervine.n.01', 'name': 'silvervine'}, {'id': 19626, 'synset': 'wild_cinnamon.n.01', 'name': 'wild_cinnamon'}, {'id': 19627, 'synset': 'papaya.n.01', 'name': 'papaya'}, {'id': 19628, 'synset': 'souari.n.01', 'name': 'souari'}, {'id': 19629, 'synset': 'rockrose.n.02', 'name': 'rockrose'}, {'id': 19630, 'synset': 'white-leaved_rockrose.n.01', 'name': 'white-leaved_rockrose'}, {'id': 19631, 'synset': 'common_gum_cistus.n.01', 'name': 'common_gum_cistus'}, {'id': 19632, 'synset': 'frostweed.n.01', 'name': 'frostweed'}, {'id': 19633, 'synset': 'dipterocarp.n.01', 'name': 'dipterocarp'}, {'id': 19634, 'synset': 'red_lauan.n.02', 'name': 'red_lauan'}, {'id': 19635, 'synset': "governor's_plum.n.01", 'name': "governor's_plum"}, {'id': 19636, 'synset': 'kei_apple.n.01', 'name': 'kei_apple'}, {'id': 19637, 'synset': 'ketembilla.n.01', 'name': 'ketembilla'}, {'id': 19638, 'synset': 'chaulmoogra.n.01', 'name': 'chaulmoogra'}, {'id': 19639, 'synset': 'wild_peach.n.01', 'name': 'wild_peach'}, {'id': 19640, 'synset': 'candlewood.n.01', 'name': 'candlewood'}, {'id': 19641, 'synset': 'boojum_tree.n.01', 'name': 'boojum_tree'}, {'id': 19642, 'synset': "bird's-eye_bush.n.01", 'name': "bird's-eye_bush"}, {'id': 19643, 'synset': 'granadilla.n.03', 'name': 'granadilla'}, {'id': 19644, 'synset': 'granadilla.n.02', 'name': 'granadilla'}, {'id': 19645, 'synset': 'granadilla.n.01', 'name': 'granadilla'}, {'id': 19646, 'synset': 'maypop.n.01', 'name': 'maypop'}, {'id': 19647, 'synset': 'jamaica_honeysuckle.n.01', 'name': 'Jamaica_honeysuckle'}, {'id': 19648, 'synset': 'banana_passion_fruit.n.01', 'name': 'banana_passion_fruit'}, {'id': 19649, 'synset': 'sweet_calabash.n.01', 'name': 'sweet_calabash'}, {'id': 19650, 'synset': 'love-in-a-mist.n.01', 'name': 'love-in-a-mist'}, {'id': 19651, 'synset': 'reseda.n.01', 'name': 'reseda'}, {'id': 19652, 'synset': 'mignonette.n.01', 'name': 'mignonette'}, {'id': 19653, 'synset': "dyer's_rocket.n.01", 'name': "dyer's_rocket"}, {'id': 19654, 'synset': 'false_tamarisk.n.01', 'name': 'false_tamarisk'}, {'id': 19655, 'synset': 'halophyte.n.01', 'name': 'halophyte'}, {'id': 19656, 'synset': 'viola.n.01', 'name': 'viola'}, {'id': 19657, 'synset': 'violet.n.01', 'name': 'violet'}, {'id': 19658, 'synset': 'field_pansy.n.01', 'name': 'field_pansy'}, {'id': 19659, 'synset': 'american_dog_violet.n.01', 'name': 'American_dog_violet'}, {'id': 19660, 'synset': 'dog_violet.n.01', 'name': 'dog_violet'}, {'id': 19661, 'synset': 'horned_violet.n.01', 'name': 'horned_violet'}, {'id': 19662, 'synset': 'two-eyed_violet.n.01', 'name': 'two-eyed_violet'}, {'id': 19663, 'synset': "bird's-foot_violet.n.01", 'name': "bird's-foot_violet"}, {'id': 19664, 'synset': 'downy_yellow_violet.n.01', 'name': 'downy_yellow_violet'}, {'id': 19665, 'synset': 'long-spurred_violet.n.01', 'name': 'long-spurred_violet'}, {'id': 19666, 'synset': 'pale_violet.n.01', 'name': 'pale_violet'}, {'id': 19667, 'synset': 'hedge_violet.n.01', 'name': 'hedge_violet'}, {'id': 19668, 'synset': 'nettle.n.01', 'name': 'nettle'}, {'id': 19669, 'synset': 'stinging_nettle.n.01', 'name': 'stinging_nettle'}, {'id': 19670, 'synset': 'roman_nettle.n.01', 'name': 'Roman_nettle'}, {'id': 19671, 'synset': 'ramie.n.01', 'name': 'ramie'}, {'id': 19672, 'synset': 'wood_nettle.n.01', 'name': 'wood_nettle'}, {'id': 19673, 'synset': 'australian_nettle.n.01', 'name': 'Australian_nettle'}, {'id': 19674, 'synset': 'pellitory-of-the-wall.n.01', 'name': 'pellitory-of-the-wall'}, {'id': 19675, 'synset': 'richweed.n.02', 'name': 'richweed'}, {'id': 19676, 'synset': 'artillery_plant.n.01', 'name': 'artillery_plant'}, {'id': 19677, 'synset': 'friendship_plant.n.01', 'name': 'friendship_plant'}, {'id': 19678, 'synset': 'queensland_grass-cloth_plant.n.01', 'name': 'Queensland_grass-cloth_plant'}, {'id': 19679, 'synset': 'pipturus_albidus.n.01', 'name': 'Pipturus_albidus'}, {'id': 19680, 'synset': 'cannabis.n.01', 'name': 'cannabis'}, {'id': 19681, 'synset': 'indian_hemp.n.01', 'name': 'Indian_hemp'}, {'id': 19682, 'synset': 'mulberry.n.01', 'name': 'mulberry'}, {'id': 19683, 'synset': 'white_mulberry.n.01', 'name': 'white_mulberry'}, {'id': 19684, 'synset': 'black_mulberry.n.01', 'name': 'black_mulberry'}, {'id': 19685, 'synset': 'red_mulberry.n.01', 'name': 'red_mulberry'}, {'id': 19686, 'synset': 'osage_orange.n.01', 'name': 'osage_orange'}, {'id': 19687, 'synset': 'breadfruit.n.01', 'name': 'breadfruit'}, {'id': 19688, 'synset': 'jackfruit.n.01', 'name': 'jackfruit'}, {'id': 19689, 'synset': 'marang.n.01', 'name': 'marang'}, {'id': 19690, 'synset': 'fig_tree.n.01', 'name': 'fig_tree'}, {'id': 19691, 'synset': 'fig.n.02', 'name': 'fig'}, {'id': 19692, 'synset': 'caprifig.n.01', 'name': 'caprifig'}, {'id': 19693, 'synset': 'golden_fig.n.01', 'name': 'golden_fig'}, {'id': 19694, 'synset': 'banyan.n.01', 'name': 'banyan'}, {'id': 19695, 'synset': 'pipal.n.01', 'name': 'pipal'}, {'id': 19696, 'synset': 'india-rubber_tree.n.01', 'name': 'India-rubber_tree'}, {'id': 19697, 'synset': 'mistletoe_fig.n.01', 'name': 'mistletoe_fig'}, {'id': 19698, 'synset': 'port_jackson_fig.n.01', 'name': 'Port_Jackson_fig'}, {'id': 19699, 'synset': 'sycamore.n.04', 'name': 'sycamore'}, {'id': 19700, 'synset': 'paper_mulberry.n.01', 'name': 'paper_mulberry'}, {'id': 19701, 'synset': 'trumpetwood.n.01', 'name': 'trumpetwood'}, {'id': 19702, 'synset': 'elm.n.01', 'name': 'elm'}, {'id': 19703, 'synset': 'winged_elm.n.01', 'name': 'winged_elm'}, {'id': 19704, 'synset': 'american_elm.n.01', 'name': 'American_elm'}, {'id': 19705, 'synset': 'smooth-leaved_elm.n.01', 'name': 'smooth-leaved_elm'}, {'id': 19706, 'synset': 'cedar_elm.n.01', 'name': 'cedar_elm'}, {'id': 19707, 'synset': 'witch_elm.n.01', 'name': 'witch_elm'}, {'id': 19708, 'synset': 'dutch_elm.n.01', 'name': 'Dutch_elm'}, {'id': 19709, 'synset': 'huntingdon_elm.n.01', 'name': 'Huntingdon_elm'}, {'id': 19710, 'synset': 'water_elm.n.01', 'name': 'water_elm'}, {'id': 19711, 'synset': 'chinese_elm.n.02', 'name': 'Chinese_elm'}, {'id': 19712, 'synset': 'english_elm.n.01', 'name': 'English_elm'}, {'id': 19713, 'synset': 'siberian_elm.n.01', 'name': 'Siberian_elm'}, {'id': 19714, 'synset': 'slippery_elm.n.01', 'name': 'slippery_elm'}, {'id': 19715, 'synset': 'jersey_elm.n.01', 'name': 'Jersey_elm'}, {'id': 19716, 'synset': 'september_elm.n.01', 'name': 'September_elm'}, {'id': 19717, 'synset': 'rock_elm.n.01', 'name': 'rock_elm'}, {'id': 19718, 'synset': 'hackberry.n.01', 'name': 'hackberry'}, {'id': 19719, 'synset': 'european_hackberry.n.01', 'name': 'European_hackberry'}, {'id': 19720, 'synset': 'american_hackberry.n.01', 'name': 'American_hackberry'}, {'id': 19721, 'synset': 'sugarberry.n.01', 'name': 'sugarberry'}, {'id': 19722, 'synset': 'iridaceous_plant.n.01', 'name': 'iridaceous_plant'}, {'id': 19723, 'synset': 'bearded_iris.n.01', 'name': 'bearded_iris'}, {'id': 19724, 'synset': 'beardless_iris.n.01', 'name': 'beardless_iris'}, {'id': 19725, 'synset': 'orrisroot.n.01', 'name': 'orrisroot'}, {'id': 19726, 'synset': 'dwarf_iris.n.02', 'name': 'dwarf_iris'}, {'id': 19727, 'synset': 'dutch_iris.n.02', 'name': 'Dutch_iris'}, {'id': 19728, 'synset': 'florentine_iris.n.01', 'name': 'Florentine_iris'}, {'id': 19729, 'synset': 'stinking_iris.n.01', 'name': 'stinking_iris'}, {'id': 19730, 'synset': 'german_iris.n.02', 'name': 'German_iris'}, {'id': 19731, 'synset': 'japanese_iris.n.01', 'name': 'Japanese_iris'}, {'id': 19732, 'synset': 'german_iris.n.01', 'name': 'German_iris'}, {'id': 19733, 'synset': 'dalmatian_iris.n.01', 'name': 'Dalmatian_iris'}, {'id': 19734, 'synset': 'persian_iris.n.01', 'name': 'Persian_iris'}, {'id': 19735, 'synset': 'dutch_iris.n.01', 'name': 'Dutch_iris'}, {'id': 19736, 'synset': 'dwarf_iris.n.01', 'name': 'dwarf_iris'}, {'id': 19737, 'synset': 'spanish_iris.n.01', 'name': 'Spanish_iris'}, {'id': 19738, 'synset': 'blackberry-lily.n.01', 'name': 'blackberry-lily'}, {'id': 19739, 'synset': 'crocus.n.01', 'name': 'crocus'}, {'id': 19740, 'synset': 'saffron.n.01', 'name': 'saffron'}, {'id': 19741, 'synset': 'corn_lily.n.01', 'name': 'corn_lily'}, {'id': 19742, 'synset': 'blue-eyed_grass.n.01', 'name': 'blue-eyed_grass'}, {'id': 19743, 'synset': 'wandflower.n.01', 'name': 'wandflower'}, {'id': 19744, 'synset': 'amaryllis.n.01', 'name': 'amaryllis'}, {'id': 19745, 'synset': 'salsilla.n.02', 'name': 'salsilla'}, {'id': 19746, 'synset': 'salsilla.n.01', 'name': 'salsilla'}, {'id': 19747, 'synset': 'blood_lily.n.01', 'name': 'blood_lily'}, {'id': 19748, 'synset': 'cape_tulip.n.01', 'name': 'Cape_tulip'}, {'id': 19749, 'synset': 'hippeastrum.n.01', 'name': 'hippeastrum'}, {'id': 19750, 'synset': 'narcissus.n.01', 'name': 'narcissus'}, {'id': 19751, 'synset': 'daffodil.n.01', 'name': 'daffodil'}, {'id': 19752, 'synset': 'jonquil.n.01', 'name': 'jonquil'}, {'id': 19753, 'synset': 'jonquil.n.02', 'name': 'jonquil'}, {'id': 19754, 'synset': 'jacobean_lily.n.01', 'name': 'Jacobean_lily'}, {'id': 19755, 'synset': 'liliaceous_plant.n.01', 'name': 'liliaceous_plant'}, {'id': 19756, 'synset': 'mountain_lily.n.01', 'name': 'mountain_lily'}, {'id': 19757, 'synset': 'canada_lily.n.01', 'name': 'Canada_lily'}, {'id': 19758, 'synset': 'tiger_lily.n.02', 'name': 'tiger_lily'}, {'id': 19759, 'synset': 'columbia_tiger_lily.n.01', 'name': 'Columbia_tiger_lily'}, {'id': 19760, 'synset': 'tiger_lily.n.01', 'name': 'tiger_lily'}, {'id': 19761, 'synset': 'easter_lily.n.01', 'name': 'Easter_lily'}, {'id': 19762, 'synset': 'coast_lily.n.01', 'name': 'coast_lily'}, {'id': 19763, 'synset': "turk's-cap.n.02", 'name': "Turk's-cap"}, {'id': 19764, 'synset': 'michigan_lily.n.01', 'name': 'Michigan_lily'}, {'id': 19765, 'synset': 'leopard_lily.n.01', 'name': 'leopard_lily'}, {'id': 19766, 'synset': "turk's-cap.n.01", 'name': "Turk's-cap"}, {'id': 19767, 'synset': 'african_lily.n.01', 'name': 'African_lily'}, {'id': 19768, 'synset': 'colicroot.n.01', 'name': 'colicroot'}, {'id': 19769, 'synset': 'ague_root.n.01', 'name': 'ague_root'}, {'id': 19770, 'synset': 'yellow_colicroot.n.01', 'name': 'yellow_colicroot'}, {'id': 19771, 'synset': 'alliaceous_plant.n.01', 'name': 'alliaceous_plant'}, {'id': 19772, 'synset': "hooker's_onion.n.01", 'name': "Hooker's_onion"}, {'id': 19773, 'synset': 'wild_leek.n.02', 'name': 'wild_leek'}, {'id': 19774, 'synset': 'canada_garlic.n.01', 'name': 'Canada_garlic'}, {'id': 19775, 'synset': 'keeled_garlic.n.01', 'name': 'keeled_garlic'}, {'id': 19776, 'synset': 'shallot.n.02', 'name': 'shallot'}, {'id': 19777, 'synset': 'nodding_onion.n.01', 'name': 'nodding_onion'}, {'id': 19778, 'synset': 'welsh_onion.n.01', 'name': 'Welsh_onion'}, {'id': 19779, 'synset': 'red-skinned_onion.n.01', 'name': 'red-skinned_onion'}, {'id': 19780, 'synset': 'daffodil_garlic.n.01', 'name': 'daffodil_garlic'}, {'id': 19781, 'synset': 'few-flowered_leek.n.01', 'name': 'few-flowered_leek'}, {'id': 19782, 'synset': 'garlic.n.01', 'name': 'garlic'}, {'id': 19783, 'synset': 'sand_leek.n.01', 'name': 'sand_leek'}, {'id': 19784, 'synset': 'chives.n.01', 'name': 'chives'}, {'id': 19785, 'synset': 'crow_garlic.n.01', 'name': 'crow_garlic'}, {'id': 19786, 'synset': 'wild_garlic.n.01', 'name': 'wild_garlic'}, {'id': 19787, 'synset': 'garlic_chive.n.01', 'name': 'garlic_chive'}, {'id': 19788, 'synset': 'round-headed_leek.n.01', 'name': 'round-headed_leek'}, {'id': 19789, 'synset': 'three-cornered_leek.n.01', 'name': 'three-cornered_leek'}, {'id': 19790, 'synset': 'cape_aloe.n.01', 'name': 'cape_aloe'}, {'id': 19791, 'synset': 'kniphofia.n.01', 'name': 'kniphofia'}, {'id': 19792, 'synset': 'poker_plant.n.01', 'name': 'poker_plant'}, {'id': 19793, 'synset': 'red-hot_poker.n.01', 'name': 'red-hot_poker'}, {'id': 19794, 'synset': 'fly_poison.n.01', 'name': 'fly_poison'}, {'id': 19795, 'synset': 'amber_lily.n.01', 'name': 'amber_lily'}, {'id': 19796, 'synset': 'asparagus.n.01', 'name': 'asparagus'}, {'id': 19797, 'synset': 'asparagus_fern.n.01', 'name': 'asparagus_fern'}, {'id': 19798, 'synset': 'smilax.n.02', 'name': 'smilax'}, {'id': 19799, 'synset': 'asphodel.n.01', 'name': 'asphodel'}, {'id': 19800, 'synset': "jacob's_rod.n.01", 'name': "Jacob's_rod"}, {'id': 19801, 'synset': 'aspidistra.n.01', 'name': 'aspidistra'}, {'id': 19802, 'synset': 'coral_drops.n.01', 'name': 'coral_drops'}, {'id': 19803, 'synset': 'christmas_bells.n.01', 'name': 'Christmas_bells'}, {'id': 19804, 'synset': 'climbing_onion.n.01', 'name': 'climbing_onion'}, {'id': 19805, 'synset': 'mariposa.n.01', 'name': 'mariposa'}, {'id': 19806, 'synset': 'globe_lily.n.01', 'name': 'globe_lily'}, {'id': 19807, 'synset': "cat's-ear.n.01", 'name': "cat's-ear"}, {'id': 19808, 'synset': 'white_globe_lily.n.01', 'name': 'white_globe_lily'}, {'id': 19809, 'synset': 'yellow_globe_lily.n.01', 'name': 'yellow_globe_lily'}, {'id': 19810, 'synset': 'rose_globe_lily.n.01', 'name': 'rose_globe_lily'}, {'id': 19811, 'synset': 'star_tulip.n.01', 'name': 'star_tulip'}, {'id': 19812, 'synset': 'desert_mariposa_tulip.n.01', 'name': 'desert_mariposa_tulip'}, {'id': 19813, 'synset': 'yellow_mariposa_tulip.n.01', 'name': 'yellow_mariposa_tulip'}, {'id': 19814, 'synset': 'sagebrush_mariposa_tulip.n.01', 'name': 'sagebrush_mariposa_tulip'}, {'id': 19815, 'synset': 'sego_lily.n.01', 'name': 'sego_lily'}, {'id': 19816, 'synset': 'camas.n.01', 'name': 'camas'}, {'id': 19817, 'synset': 'common_camas.n.01', 'name': 'common_camas'}, {'id': 19818, 'synset': "leichtlin's_camas.n.01", 'name': "Leichtlin's_camas"}, {'id': 19819, 'synset': 'wild_hyacinth.n.02', 'name': 'wild_hyacinth'}, {'id': 19820, 'synset': 'dogtooth_violet.n.01', 'name': 'dogtooth_violet'}, {'id': 19821, 'synset': 'white_dogtooth_violet.n.01', 'name': 'white_dogtooth_violet'}, {'id': 19822, 'synset': "yellow_adder's_tongue.n.01", 'name': "yellow_adder's_tongue"}, {'id': 19823, 'synset': 'european_dogtooth.n.01', 'name': 'European_dogtooth'}, {'id': 19824, 'synset': 'fawn_lily.n.01', 'name': 'fawn_lily'}, {'id': 19825, 'synset': 'glacier_lily.n.01', 'name': 'glacier_lily'}, {'id': 19826, 'synset': 'avalanche_lily.n.01', 'name': 'avalanche_lily'}, {'id': 19827, 'synset': 'fritillary.n.01', 'name': 'fritillary'}, {'id': 19828, 'synset': 'mission_bells.n.02', 'name': 'mission_bells'}, {'id': 19829, 'synset': 'mission_bells.n.01', 'name': 'mission_bells'}, {'id': 19830, 'synset': 'stink_bell.n.01', 'name': 'stink_bell'}, {'id': 19831, 'synset': 'crown_imperial.n.01', 'name': 'crown_imperial'}, {'id': 19832, 'synset': 'white_fritillary.n.01', 'name': 'white_fritillary'}, {'id': 19833, 'synset': "snake's_head_fritillary.n.01", 'name': "snake's_head_fritillary"}, {'id': 19834, 'synset': 'adobe_lily.n.01', 'name': 'adobe_lily'}, {'id': 19835, 'synset': 'scarlet_fritillary.n.01', 'name': 'scarlet_fritillary'}, {'id': 19836, 'synset': 'tulip.n.01', 'name': 'tulip'}, {'id': 19837, 'synset': 'dwarf_tulip.n.01', 'name': 'dwarf_tulip'}, {'id': 19838, 'synset': 'lady_tulip.n.01', 'name': 'lady_tulip'}, {'id': 19839, 'synset': 'tulipa_gesneriana.n.01', 'name': 'Tulipa_gesneriana'}, {'id': 19840, 'synset': 'cottage_tulip.n.01', 'name': 'cottage_tulip'}, {'id': 19841, 'synset': 'darwin_tulip.n.01', 'name': 'Darwin_tulip'}, {'id': 19842, 'synset': 'gloriosa.n.01', 'name': 'gloriosa'}, {'id': 19843, 'synset': 'lemon_lily.n.01', 'name': 'lemon_lily'}, {'id': 19844, 'synset': 'common_hyacinth.n.01', 'name': 'common_hyacinth'}, {'id': 19845, 'synset': 'roman_hyacinth.n.01', 'name': 'Roman_hyacinth'}, {'id': 19846, 'synset': 'summer_hyacinth.n.01', 'name': 'summer_hyacinth'}, {'id': 19847, 'synset': 'star-of-bethlehem.n.01', 'name': 'star-of-Bethlehem'}, {'id': 19848, 'synset': 'bath_asparagus.n.01', 'name': 'bath_asparagus'}, {'id': 19849, 'synset': 'grape_hyacinth.n.01', 'name': 'grape_hyacinth'}, {'id': 19850, 'synset': 'common_grape_hyacinth.n.01', 'name': 'common_grape_hyacinth'}, {'id': 19851, 'synset': 'tassel_hyacinth.n.01', 'name': 'tassel_hyacinth'}, {'id': 19852, 'synset': 'scilla.n.01', 'name': 'scilla'}, {'id': 19853, 'synset': 'spring_squill.n.01', 'name': 'spring_squill'}, {'id': 19854, 'synset': 'false_asphodel.n.01', 'name': 'false_asphodel'}, {'id': 19855, 'synset': 'scotch_asphodel.n.01', 'name': 'Scotch_asphodel'}, {'id': 19856, 'synset': 'sea_squill.n.01', 'name': 'sea_squill'}, {'id': 19857, 'synset': 'squill.n.01', 'name': 'squill'}, {'id': 19858, 'synset': "butcher's_broom.n.01", 'name': "butcher's_broom"}, {'id': 19859, 'synset': 'bog_asphodel.n.01', 'name': 'bog_asphodel'}, {'id': 19860, 'synset': 'european_bog_asphodel.n.01', 'name': 'European_bog_asphodel'}, {'id': 19861, 'synset': 'american_bog_asphodel.n.01', 'name': 'American_bog_asphodel'}, {'id': 19862, 'synset': 'hellebore.n.01', 'name': 'hellebore'}, {'id': 19863, 'synset': 'white_hellebore.n.01', 'name': 'white_hellebore'}, {'id': 19864, 'synset': 'squaw_grass.n.01', 'name': 'squaw_grass'}, {'id': 19865, 'synset': 'death_camas.n.01', 'name': 'death_camas'}, {'id': 19866, 'synset': 'alkali_grass.n.01', 'name': 'alkali_grass'}, {'id': 19867, 'synset': 'white_camas.n.01', 'name': 'white_camas'}, {'id': 19868, 'synset': 'poison_camas.n.01', 'name': 'poison_camas'}, {'id': 19869, 'synset': 'grassy_death_camas.n.01', 'name': 'grassy_death_camas'}, {'id': 19870, 'synset': 'prairie_wake-robin.n.01', 'name': 'prairie_wake-robin'}, {'id': 19871, 'synset': 'dwarf-white_trillium.n.01', 'name': 'dwarf-white_trillium'}, {'id': 19872, 'synset': 'herb_paris.n.01', 'name': 'herb_Paris'}, {'id': 19873, 'synset': 'sarsaparilla.n.01', 'name': 'sarsaparilla'}, {'id': 19874, 'synset': 'bullbrier.n.01', 'name': 'bullbrier'}, {'id': 19875, 'synset': 'rough_bindweed.n.01', 'name': 'rough_bindweed'}, {'id': 19876, 'synset': 'clintonia.n.01', 'name': 'clintonia'}, {'id': 19877, 'synset': 'false_lily_of_the_valley.n.02', 'name': 'false_lily_of_the_valley'}, {'id': 19878, 'synset': 'false_lily_of_the_valley.n.01', 'name': 'false_lily_of_the_valley'}, {'id': 19879, 'synset': "solomon's-seal.n.01", 'name': "Solomon's-seal"}, {'id': 19880, 'synset': "great_solomon's-seal.n.01", 'name': "great_Solomon's-seal"}, {'id': 19881, 'synset': 'bellwort.n.01', 'name': 'bellwort'}, {'id': 19882, 'synset': 'strawflower.n.01', 'name': 'strawflower'}, {'id': 19883, 'synset': 'pia.n.01', 'name': 'pia'}, {'id': 19884, 'synset': 'agave.n.01', 'name': 'agave'}, {'id': 19885, 'synset': 'american_agave.n.01', 'name': 'American_agave'}, {'id': 19886, 'synset': 'sisal.n.02', 'name': 'sisal'}, {'id': 19887, 'synset': 'maguey.n.02', 'name': 'maguey'}, {'id': 19888, 'synset': 'maguey.n.01', 'name': 'maguey'}, {'id': 19889, 'synset': 'agave_tequilana.n.01', 'name': 'Agave_tequilana'}, {'id': 19890, 'synset': 'cabbage_tree.n.03', 'name': 'cabbage_tree'}, {'id': 19891, 'synset': 'dracaena.n.01', 'name': 'dracaena'}, {'id': 19892, 'synset': 'tuberose.n.01', 'name': 'tuberose'}, {'id': 19893, 'synset': 'sansevieria.n.01', 'name': 'sansevieria'}, {'id': 19894, 'synset': 'african_bowstring_hemp.n.01', 'name': 'African_bowstring_hemp'}, {'id': 19895, 'synset': 'ceylon_bowstring_hemp.n.01', 'name': 'Ceylon_bowstring_hemp'}, {'id': 19896, 'synset': "mother-in-law's_tongue.n.01", 'name': "mother-in-law's_tongue"}, {'id': 19897, 'synset': 'spanish_bayonet.n.02', 'name': 'Spanish_bayonet'}, {'id': 19898, 'synset': 'spanish_bayonet.n.01', 'name': 'Spanish_bayonet'}, {'id': 19899, 'synset': 'joshua_tree.n.01', 'name': 'Joshua_tree'}, {'id': 19900, 'synset': 'soapweed.n.01', 'name': 'soapweed'}, {'id': 19901, 'synset': "adam's_needle.n.01", 'name': "Adam's_needle"}, {'id': 19902, 'synset': 'bear_grass.n.02', 'name': 'bear_grass'}, {'id': 19903, 'synset': 'spanish_dagger.n.01', 'name': 'Spanish_dagger'}, {'id': 19904, 'synset': "our_lord's_candle.n.01", 'name': "Our_Lord's_candle"}, {'id': 19905, 'synset': 'water_shamrock.n.01', 'name': 'water_shamrock'}, {'id': 19906, 'synset': 'butterfly_bush.n.01', 'name': 'butterfly_bush'}, {'id': 19907, 'synset': 'yellow_jasmine.n.01', 'name': 'yellow_jasmine'}, {'id': 19908, 'synset': 'flax.n.02', 'name': 'flax'}, {'id': 19909, 'synset': 'calabar_bean.n.01', 'name': 'calabar_bean'}, {'id': 19910, 'synset': 'bonduc.n.02', 'name': 'bonduc'}, {'id': 19911, 'synset': 'divi-divi.n.02', 'name': 'divi-divi'}, {'id': 19912, 'synset': 'mysore_thorn.n.01', 'name': 'Mysore_thorn'}, {'id': 19913, 'synset': 'brazilian_ironwood.n.01', 'name': 'brazilian_ironwood'}, {'id': 19914, 'synset': 'bird_of_paradise.n.01', 'name': 'bird_of_paradise'}, {'id': 19915, 'synset': 'shingle_tree.n.01', 'name': 'shingle_tree'}, {'id': 19916, 'synset': 'mountain_ebony.n.01', 'name': 'mountain_ebony'}, {'id': 19917, 'synset': 'msasa.n.01', 'name': 'msasa'}, {'id': 19918, 'synset': 'cassia.n.01', 'name': 'cassia'}, {'id': 19919, 'synset': 'golden_shower_tree.n.01', 'name': 'golden_shower_tree'}, {'id': 19920, 'synset': 'pink_shower.n.01', 'name': 'pink_shower'}, {'id': 19921, 'synset': 'rainbow_shower.n.01', 'name': 'rainbow_shower'}, {'id': 19922, 'synset': 'horse_cassia.n.01', 'name': 'horse_cassia'}, {'id': 19923, 'synset': 'carob.n.02', 'name': 'carob'}, {'id': 19924, 'synset': 'carob.n.01', 'name': 'carob'}, {'id': 19925, 'synset': 'paloverde.n.01', 'name': 'paloverde'}, {'id': 19926, 'synset': 'royal_poinciana.n.01', 'name': 'royal_poinciana'}, {'id': 19927, 'synset': 'locust_tree.n.01', 'name': 'locust_tree'}, {'id': 19928, 'synset': 'water_locust.n.01', 'name': 'water_locust'}, {'id': 19929, 'synset': 'honey_locust.n.01', 'name': 'honey_locust'}, {'id': 19930, 'synset': 'kentucky_coffee_tree.n.01', 'name': 'Kentucky_coffee_tree'}, {'id': 19931, 'synset': 'logwood.n.02', 'name': 'logwood'}, {'id': 19932, 'synset': 'jerusalem_thorn.n.03', 'name': 'Jerusalem_thorn'}, {'id': 19933, 'synset': 'palo_verde.n.01', 'name': 'palo_verde'}, {'id': 19934, 'synset': 'dalmatian_laburnum.n.01', 'name': 'Dalmatian_laburnum'}, {'id': 19935, 'synset': 'senna.n.01', 'name': 'senna'}, {'id': 19936, 'synset': 'avaram.n.01', 'name': 'avaram'}, {'id': 19937, 'synset': 'alexandria_senna.n.01', 'name': 'Alexandria_senna'}, {'id': 19938, 'synset': 'wild_senna.n.01', 'name': 'wild_senna'}, {'id': 19939, 'synset': 'sicklepod.n.01', 'name': 'sicklepod'}, {'id': 19940, 'synset': 'coffee_senna.n.01', 'name': 'coffee_senna'}, {'id': 19941, 'synset': 'tamarind.n.01', 'name': 'tamarind'}, {'id': 19942, 'synset': 'false_indigo.n.03', 'name': 'false_indigo'}, {'id': 19943, 'synset': 'false_indigo.n.02', 'name': 'false_indigo'}, {'id': 19944, 'synset': 'hog_peanut.n.01', 'name': 'hog_peanut'}, {'id': 19945, 'synset': 'angelim.n.01', 'name': 'angelim'}, {'id': 19946, 'synset': 'cabbage_bark.n.01', 'name': 'cabbage_bark'}, {'id': 19947, 'synset': 'kidney_vetch.n.01', 'name': 'kidney_vetch'}, {'id': 19948, 'synset': 'groundnut.n.01', 'name': 'groundnut'}, {'id': 19949, 'synset': 'rooibos.n.01', 'name': 'rooibos'}, {'id': 19950, 'synset': 'milk_vetch.n.01', 'name': 'milk_vetch'}, {'id': 19951, 'synset': 'alpine_milk_vetch.n.01', 'name': 'alpine_milk_vetch'}, {'id': 19952, 'synset': 'purple_milk_vetch.n.01', 'name': 'purple_milk_vetch'}, {'id': 19953, 'synset': 'camwood.n.01', 'name': 'camwood'}, {'id': 19954, 'synset': 'wild_indigo.n.01', 'name': 'wild_indigo'}, {'id': 19955, 'synset': 'blue_false_indigo.n.01', 'name': 'blue_false_indigo'}, {'id': 19956, 'synset': 'white_false_indigo.n.01', 'name': 'white_false_indigo'}, {'id': 19957, 'synset': 'indigo_broom.n.01', 'name': 'indigo_broom'}, {'id': 19958, 'synset': 'dhak.n.01', 'name': 'dhak'}, {'id': 19959, 'synset': 'pigeon_pea.n.01', 'name': 'pigeon_pea'}, {'id': 19960, 'synset': 'sword_bean.n.01', 'name': 'sword_bean'}, {'id': 19961, 'synset': 'pea_tree.n.01', 'name': 'pea_tree'}, {'id': 19962, 'synset': 'siberian_pea_tree.n.01', 'name': 'Siberian_pea_tree'}, {'id': 19963, 'synset': 'chinese_pea_tree.n.01', 'name': 'Chinese_pea_tree'}, {'id': 19964, 'synset': 'moreton_bay_chestnut.n.01', 'name': 'Moreton_Bay_chestnut'}, {'id': 19965, 'synset': 'butterfly_pea.n.03', 'name': 'butterfly_pea'}, {'id': 19966, 'synset': 'judas_tree.n.01', 'name': 'Judas_tree'}, {'id': 19967, 'synset': 'redbud.n.01', 'name': 'redbud'}, {'id': 19968, 'synset': 'western_redbud.n.01', 'name': 'western_redbud'}, {'id': 19969, 'synset': 'tagasaste.n.01', 'name': 'tagasaste'}, {'id': 19970, 'synset': 'weeping_tree_broom.n.01', 'name': 'weeping_tree_broom'}, {'id': 19971, 'synset': 'flame_pea.n.01', 'name': 'flame_pea'}, {'id': 19972, 'synset': 'chickpea.n.02', 'name': 'chickpea'}, {'id': 19973, 'synset': 'kentucky_yellowwood.n.01', 'name': 'Kentucky_yellowwood'}, {'id': 19974, 'synset': 'glory_pea.n.01', 'name': 'glory_pea'}, {'id': 19975, 'synset': 'desert_pea.n.01', 'name': 'desert_pea'}, {'id': 19976, 'synset': "parrot's_beak.n.01", 'name': "parrot's_beak"}, {'id': 19977, 'synset': 'butterfly_pea.n.02', 'name': 'butterfly_pea'}, {'id': 19978, 'synset': 'blue_pea.n.01', 'name': 'blue_pea'}, {'id': 19979, 'synset': 'telegraph_plant.n.01', 'name': 'telegraph_plant'}, {'id': 19980, 'synset': 'bladder_senna.n.01', 'name': 'bladder_senna'}, {'id': 19981, 'synset': 'axseed.n.01', 'name': 'axseed'}, {'id': 19982, 'synset': 'crotalaria.n.01', 'name': 'crotalaria'}, {'id': 19983, 'synset': 'guar.n.01', 'name': 'guar'}, {'id': 19984, 'synset': 'white_broom.n.01', 'name': 'white_broom'}, {'id': 19985, 'synset': 'common_broom.n.01', 'name': 'common_broom'}, {'id': 19986, 'synset': 'rosewood.n.02', 'name': 'rosewood'}, {'id': 19987, 'synset': 'indian_blackwood.n.01', 'name': 'Indian_blackwood'}, {'id': 19988, 'synset': 'sissoo.n.01', 'name': 'sissoo'}, {'id': 19989, 'synset': 'kingwood.n.02', 'name': 'kingwood'}, {'id': 19990, 'synset': 'brazilian_rosewood.n.01', 'name': 'Brazilian_rosewood'}, {'id': 19991, 'synset': 'cocobolo.n.01', 'name': 'cocobolo'}, {'id': 19992, 'synset': 'blackwood.n.02', 'name': 'blackwood'}, {'id': 19993, 'synset': 'bitter_pea.n.01', 'name': 'bitter_pea'}, {'id': 19994, 'synset': 'derris.n.01', 'name': 'derris'}, {'id': 19995, 'synset': 'derris_root.n.01', 'name': 'derris_root'}, {'id': 19996, 'synset': 'prairie_mimosa.n.01', 'name': 'prairie_mimosa'}, {'id': 19997, 'synset': 'tick_trefoil.n.01', 'name': 'tick_trefoil'}, {'id': 19998, 'synset': 'beggarweed.n.01', 'name': 'beggarweed'}, {'id': 19999, 'synset': 'australian_pea.n.01', 'name': 'Australian_pea'}, {'id': 20000, 'synset': 'coral_tree.n.01', 'name': 'coral_tree'}, {'id': 20001, 'synset': 'kaffir_boom.n.02', 'name': 'kaffir_boom'}, {'id': 20002, 'synset': 'coral_bean_tree.n.01', 'name': 'coral_bean_tree'}, {'id': 20003, 'synset': 'ceibo.n.01', 'name': 'ceibo'}, {'id': 20004, 'synset': 'kaffir_boom.n.01', 'name': 'kaffir_boom'}, {'id': 20005, 'synset': 'indian_coral_tree.n.01', 'name': 'Indian_coral_tree'}, {'id': 20006, 'synset': 'cork_tree.n.02', 'name': 'cork_tree'}, {'id': 20007, 'synset': "goat's_rue.n.02", 'name': "goat's_rue"}, {'id': 20008, 'synset': 'poison_bush.n.01', 'name': 'poison_bush'}, {'id': 20009, 'synset': 'spanish_broom.n.02', 'name': 'Spanish_broom'}, {'id': 20010, 'synset': 'woodwaxen.n.01', 'name': 'woodwaxen'}, {'id': 20011, 'synset': 'chanar.n.01', 'name': 'chanar'}, {'id': 20012, 'synset': 'gliricidia.n.01', 'name': 'gliricidia'}, {'id': 20013, 'synset': 'soy.n.01', 'name': 'soy'}, {'id': 20014, 'synset': 'licorice.n.01', 'name': 'licorice'}, {'id': 20015, 'synset': 'wild_licorice.n.02', 'name': 'wild_licorice'}, {'id': 20016, 'synset': 'licorice_root.n.01', 'name': 'licorice_root'}, {'id': 20017, 'synset': 'western_australia_coral_pea.n.01', 'name': 'Western_Australia_coral_pea'}, {'id': 20018, 'synset': 'sweet_vetch.n.01', 'name': 'sweet_vetch'}, {'id': 20019, 'synset': 'french_honeysuckle.n.02', 'name': 'French_honeysuckle'}, {'id': 20020, 'synset': 'anil.n.02', 'name': 'anil'}, {'id': 20021, 'synset': 'scarlet_runner.n.02', 'name': 'scarlet_runner'}, {'id': 20022, 'synset': 'hyacinth_bean.n.01', 'name': 'hyacinth_bean'}, {'id': 20023, 'synset': 'scotch_laburnum.n.01', 'name': 'Scotch_laburnum'}, {'id': 20024, 'synset': 'vetchling.n.01', 'name': 'vetchling'}, {'id': 20025, 'synset': 'wild_pea.n.01', 'name': 'wild_pea'}, {'id': 20026, 'synset': 'everlasting_pea.n.01', 'name': 'everlasting_pea'}, {'id': 20027, 'synset': 'beach_pea.n.01', 'name': 'beach_pea'}, {'id': 20028, 'synset': 'grass_vetch.n.01', 'name': 'grass_vetch'}, {'id': 20029, 'synset': 'marsh_pea.n.01', 'name': 'marsh_pea'}, {'id': 20030, 'synset': 'common_vetchling.n.01', 'name': 'common_vetchling'}, {'id': 20031, 'synset': 'grass_pea.n.01', 'name': 'grass_pea'}, {'id': 20032, 'synset': 'tangier_pea.n.01', 'name': 'Tangier_pea'}, {'id': 20033, 'synset': 'heath_pea.n.01', 'name': 'heath_pea'}, {'id': 20034, 'synset': 'bicolor_lespediza.n.01', 'name': 'bicolor_lespediza'}, {'id': 20035, 'synset': 'japanese_clover.n.01', 'name': 'japanese_clover'}, {'id': 20036, 'synset': 'korean_lespedeza.n.01', 'name': 'Korean_lespedeza'}, {'id': 20037, 'synset': 'sericea_lespedeza.n.01', 'name': 'sericea_lespedeza'}, {'id': 20038, 'synset': 'lentil.n.03', 'name': 'lentil'}, {'id': 20039, 'synset': 'lentil.n.02', 'name': 'lentil'}, {'id': 20040, 'synset': "prairie_bird's-foot_trefoil.n.01", 'name': "prairie_bird's-foot_trefoil"}, {'id': 20041, 'synset': "bird's_foot_trefoil.n.02", 'name': "bird's_foot_trefoil"}, {'id': 20042, 'synset': 'winged_pea.n.02', 'name': 'winged_pea'}, {'id': 20043, 'synset': 'lupine.n.01', 'name': 'lupine'}, {'id': 20044, 'synset': 'white_lupine.n.01', 'name': 'white_lupine'}, {'id': 20045, 'synset': 'tree_lupine.n.01', 'name': 'tree_lupine'}, {'id': 20046, 'synset': 'wild_lupine.n.01', 'name': 'wild_lupine'}, {'id': 20047, 'synset': 'bluebonnet.n.01', 'name': 'bluebonnet'}, {'id': 20048, 'synset': 'texas_bluebonnet.n.01', 'name': 'Texas_bluebonnet'}, {'id': 20049, 'synset': 'medic.n.01', 'name': 'medic'}, {'id': 20050, 'synset': 'moon_trefoil.n.01', 'name': 'moon_trefoil'}, {'id': 20051, 'synset': 'sickle_alfalfa.n.01', 'name': 'sickle_alfalfa'}, {'id': 20052, 'synset': 'calvary_clover.n.01', 'name': 'Calvary_clover'}, {'id': 20053, 'synset': 'black_medick.n.01', 'name': 'black_medick'}, {'id': 20054, 'synset': 'alfalfa.n.01', 'name': 'alfalfa'}, {'id': 20055, 'synset': 'millettia.n.01', 'name': 'millettia'}, {'id': 20056, 'synset': 'mucuna.n.01', 'name': 'mucuna'}, {'id': 20057, 'synset': 'cowage.n.02', 'name': 'cowage'}, {'id': 20058, 'synset': 'tolu_tree.n.01', 'name': 'tolu_tree'}, {'id': 20059, 'synset': 'peruvian_balsam.n.01', 'name': 'Peruvian_balsam'}, {'id': 20060, 'synset': 'sainfoin.n.01', 'name': 'sainfoin'}, {'id': 20061, 'synset': 'restharrow.n.02', 'name': 'restharrow'}, {'id': 20062, 'synset': 'bead_tree.n.01', 'name': 'bead_tree'}, {'id': 20063, 'synset': 'jumby_bead.n.01', 'name': 'jumby_bead'}, {'id': 20064, 'synset': 'locoweed.n.01', 'name': 'locoweed'}, {'id': 20065, 'synset': 'purple_locoweed.n.01', 'name': 'purple_locoweed'}, {'id': 20066, 'synset': 'tumbleweed.n.01', 'name': 'tumbleweed'}, {'id': 20067, 'synset': 'yam_bean.n.02', 'name': 'yam_bean'}, {'id': 20068, 'synset': 'shamrock_pea.n.01', 'name': 'shamrock_pea'}, {'id': 20069, 'synset': 'pole_bean.n.01', 'name': 'pole_bean'}, {'id': 20070, 'synset': 'kidney_bean.n.01', 'name': 'kidney_bean'}, {'id': 20071, 'synset': 'haricot.n.01', 'name': 'haricot'}, {'id': 20072, 'synset': 'wax_bean.n.01', 'name': 'wax_bean'}, {'id': 20073, 'synset': 'scarlet_runner.n.01', 'name': 'scarlet_runner'}, {'id': 20074, 'synset': 'lima_bean.n.02', 'name': 'lima_bean'}, {'id': 20075, 'synset': 'sieva_bean.n.01', 'name': 'sieva_bean'}, {'id': 20076, 'synset': 'tepary_bean.n.01', 'name': 'tepary_bean'}, {'id': 20077, 'synset': 'chaparral_pea.n.01', 'name': 'chaparral_pea'}, {'id': 20078, 'synset': 'jamaica_dogwood.n.01', 'name': 'Jamaica_dogwood'}, {'id': 20079, 'synset': 'pea.n.02', 'name': 'pea'}, {'id': 20080, 'synset': 'garden_pea.n.01', 'name': 'garden_pea'}, {'id': 20081, 'synset': 'edible-pod_pea.n.01', 'name': 'edible-pod_pea'}, {'id': 20082, 'synset': 'sugar_snap_pea.n.01', 'name': 'sugar_snap_pea'}, {'id': 20083, 'synset': 'field_pea.n.02', 'name': 'field_pea'}, {'id': 20084, 'synset': 'field_pea.n.01', 'name': 'field_pea'}, {'id': 20085, 'synset': 'common_flat_pea.n.01', 'name': 'common_flat_pea'}, {'id': 20086, 'synset': 'quira.n.02', 'name': 'quira'}, {'id': 20087, 'synset': 'roble.n.01', 'name': 'roble'}, {'id': 20088, 'synset': 'panama_redwood_tree.n.01', 'name': 'Panama_redwood_tree'}, {'id': 20089, 'synset': 'indian_beech.n.01', 'name': 'Indian_beech'}, {'id': 20090, 'synset': 'winged_bean.n.01', 'name': 'winged_bean'}, {'id': 20091, 'synset': 'breadroot.n.01', 'name': 'breadroot'}, {'id': 20092, 'synset': 'bloodwood_tree.n.01', 'name': 'bloodwood_tree'}, {'id': 20093, 'synset': 'kino.n.02', 'name': 'kino'}, {'id': 20094, 'synset': 'red_sandalwood.n.02', 'name': 'red_sandalwood'}, {'id': 20095, 'synset': 'kudzu.n.01', 'name': 'kudzu'}, {'id': 20096, 'synset': 'bristly_locust.n.01', 'name': 'bristly_locust'}, {'id': 20097, 'synset': 'black_locust.n.02', 'name': 'black_locust'}, {'id': 20098, 'synset': 'clammy_locust.n.01', 'name': 'clammy_locust'}, {'id': 20099, 'synset': 'carib_wood.n.01', 'name': 'carib_wood'}, {'id': 20100, 'synset': 'colorado_river_hemp.n.01', 'name': 'Colorado_River_hemp'}, {'id': 20101, 'synset': 'scarlet_wisteria_tree.n.01', 'name': 'scarlet_wisteria_tree'}, {'id': 20102, 'synset': 'japanese_pagoda_tree.n.01', 'name': 'Japanese_pagoda_tree'}, {'id': 20103, 'synset': 'mescal_bean.n.01', 'name': 'mescal_bean'}, {'id': 20104, 'synset': 'kowhai.n.01', 'name': 'kowhai'}, {'id': 20105, 'synset': 'jade_vine.n.01', 'name': 'jade_vine'}, {'id': 20106, 'synset': 'hoary_pea.n.01', 'name': 'hoary_pea'}, {'id': 20107, 'synset': 'bastard_indigo.n.01', 'name': 'bastard_indigo'}, {'id': 20108, 'synset': 'catgut.n.01', 'name': 'catgut'}, {'id': 20109, 'synset': 'bush_pea.n.01', 'name': 'bush_pea'}, {'id': 20110, 'synset': 'false_lupine.n.01', 'name': 'false_lupine'}, {'id': 20111, 'synset': 'carolina_lupine.n.01', 'name': 'Carolina_lupine'}, {'id': 20112, 'synset': 'tipu.n.01', 'name': 'tipu'}, {'id': 20113, 'synset': "bird's_foot_trefoil.n.01", 'name': "bird's_foot_trefoil"}, {'id': 20114, 'synset': 'fenugreek.n.01', 'name': 'fenugreek'}, {'id': 20115, 'synset': 'gorse.n.01', 'name': 'gorse'}, {'id': 20116, 'synset': 'vetch.n.01', 'name': 'vetch'}, {'id': 20117, 'synset': 'tufted_vetch.n.01', 'name': 'tufted_vetch'}, {'id': 20118, 'synset': 'broad_bean.n.01', 'name': 'broad_bean'}, {'id': 20119, 'synset': 'bitter_betch.n.01', 'name': 'bitter_betch'}, {'id': 20120, 'synset': 'bush_vetch.n.01', 'name': 'bush_vetch'}, {'id': 20121, 'synset': 'moth_bean.n.01', 'name': 'moth_bean'}, {'id': 20122, 'synset': 'snailflower.n.01', 'name': 'snailflower'}, {'id': 20123, 'synset': 'mung.n.01', 'name': 'mung'}, {'id': 20124, 'synset': 'cowpea.n.02', 'name': 'cowpea'}, {'id': 20125, 'synset': 'cowpea.n.01', 'name': 'cowpea'}, {'id': 20126, 'synset': 'asparagus_bean.n.01', 'name': 'asparagus_bean'}, {'id': 20127, 'synset': 'swamp_oak.n.01', 'name': 'swamp_oak'}, {'id': 20128, 'synset': 'keurboom.n.02', 'name': 'keurboom'}, {'id': 20129, 'synset': 'keurboom.n.01', 'name': 'keurboom'}, {'id': 20130, 'synset': 'japanese_wistaria.n.01', 'name': 'Japanese_wistaria'}, {'id': 20131, 'synset': 'chinese_wistaria.n.01', 'name': 'Chinese_wistaria'}, {'id': 20132, 'synset': 'american_wistaria.n.01', 'name': 'American_wistaria'}, {'id': 20133, 'synset': 'silky_wisteria.n.01', 'name': 'silky_wisteria'}, {'id': 20134, 'synset': 'palm.n.03', 'name': 'palm'}, {'id': 20135, 'synset': 'sago_palm.n.01', 'name': 'sago_palm'}, {'id': 20136, 'synset': 'feather_palm.n.01', 'name': 'feather_palm'}, {'id': 20137, 'synset': 'fan_palm.n.01', 'name': 'fan_palm'}, {'id': 20138, 'synset': 'palmetto.n.01', 'name': 'palmetto'}, {'id': 20139, 'synset': 'coyol.n.01', 'name': 'coyol'}, {'id': 20140, 'synset': 'grugru.n.01', 'name': 'grugru'}, {'id': 20141, 'synset': 'areca.n.01', 'name': 'areca'}, {'id': 20142, 'synset': 'betel_palm.n.01', 'name': 'betel_palm'}, {'id': 20143, 'synset': 'sugar_palm.n.01', 'name': 'sugar_palm'}, {'id': 20144, 'synset': 'piassava_palm.n.01', 'name': 'piassava_palm'}, {'id': 20145, 'synset': 'coquilla_nut.n.01', 'name': 'coquilla_nut'}, {'id': 20146, 'synset': 'palmyra.n.01', 'name': 'palmyra'}, {'id': 20147, 'synset': 'calamus.n.01', 'name': 'calamus'}, {'id': 20148, 'synset': 'rattan.n.01', 'name': 'rattan'}, {'id': 20149, 'synset': 'lawyer_cane.n.01', 'name': 'lawyer_cane'}, {'id': 20150, 'synset': 'fishtail_palm.n.01', 'name': 'fishtail_palm'}, {'id': 20151, 'synset': 'wine_palm.n.01', 'name': 'wine_palm'}, {'id': 20152, 'synset': 'wax_palm.n.03', 'name': 'wax_palm'}, {'id': 20153, 'synset': 'coconut.n.03', 'name': 'coconut'}, {'id': 20154, 'synset': 'carnauba.n.02', 'name': 'carnauba'}, {'id': 20155, 'synset': 'caranday.n.01', 'name': 'caranday'}, {'id': 20156, 'synset': 'corozo.n.01', 'name': 'corozo'}, {'id': 20157, 'synset': 'gebang_palm.n.01', 'name': 'gebang_palm'}, {'id': 20158, 'synset': 'latanier.n.01', 'name': 'latanier'}, {'id': 20159, 'synset': 'talipot.n.01', 'name': 'talipot'}, {'id': 20160, 'synset': 'oil_palm.n.01', 'name': 'oil_palm'}, {'id': 20161, 'synset': 'african_oil_palm.n.01', 'name': 'African_oil_palm'}, {'id': 20162, 'synset': 'american_oil_palm.n.01', 'name': 'American_oil_palm'}, {'id': 20163, 'synset': 'palm_nut.n.01', 'name': 'palm_nut'}, {'id': 20164, 'synset': 'cabbage_palm.n.04', 'name': 'cabbage_palm'}, {'id': 20165, 'synset': 'cabbage_palm.n.03', 'name': 'cabbage_palm'}, {'id': 20166, 'synset': 'true_sago_palm.n.01', 'name': 'true_sago_palm'}, {'id': 20167, 'synset': 'nipa_palm.n.01', 'name': 'nipa_palm'}, {'id': 20168, 'synset': 'babassu.n.01', 'name': 'babassu'}, {'id': 20169, 'synset': 'babassu_nut.n.01', 'name': 'babassu_nut'}, {'id': 20170, 'synset': 'cohune_palm.n.01', 'name': 'cohune_palm'}, {'id': 20171, 'synset': 'cohune_nut.n.01', 'name': 'cohune_nut'}, {'id': 20172, 'synset': 'date_palm.n.01', 'name': 'date_palm'}, {'id': 20173, 'synset': 'ivory_palm.n.01', 'name': 'ivory_palm'}, {'id': 20174, 'synset': 'raffia_palm.n.01', 'name': 'raffia_palm'}, {'id': 20175, 'synset': 'bamboo_palm.n.02', 'name': 'bamboo_palm'}, {'id': 20176, 'synset': 'lady_palm.n.01', 'name': 'lady_palm'}, {'id': 20177, 'synset': 'miniature_fan_palm.n.01', 'name': 'miniature_fan_palm'}, {'id': 20178, 'synset': 'reed_rhapis.n.01', 'name': 'reed_rhapis'}, {'id': 20179, 'synset': 'royal_palm.n.01', 'name': 'royal_palm'}, {'id': 20180, 'synset': 'cabbage_palm.n.02', 'name': 'cabbage_palm'}, {'id': 20181, 'synset': 'cabbage_palmetto.n.01', 'name': 'cabbage_palmetto'}, {'id': 20182, 'synset': 'saw_palmetto.n.01', 'name': 'saw_palmetto'}, {'id': 20183, 'synset': 'thatch_palm.n.01', 'name': 'thatch_palm'}, {'id': 20184, 'synset': 'key_palm.n.01', 'name': 'key_palm'}, {'id': 20185, 'synset': 'english_plantain.n.01', 'name': 'English_plantain'}, {'id': 20186, 'synset': 'broad-leaved_plantain.n.02', 'name': 'broad-leaved_plantain'}, {'id': 20187, 'synset': 'hoary_plantain.n.02', 'name': 'hoary_plantain'}, {'id': 20188, 'synset': 'fleawort.n.01', 'name': 'fleawort'}, {'id': 20189, 'synset': "rugel's_plantain.n.01", 'name': "rugel's_plantain"}, {'id': 20190, 'synset': 'hoary_plantain.n.01', 'name': 'hoary_plantain'}, {'id': 20191, 'synset': 'buckwheat.n.01', 'name': 'buckwheat'}, {'id': 20192, 'synset': "prince's-feather.n.01", 'name': "prince's-feather"}, {'id': 20193, 'synset': 'eriogonum.n.01', 'name': 'eriogonum'}, {'id': 20194, 'synset': 'umbrella_plant.n.02', 'name': 'umbrella_plant'}, {'id': 20195, 'synset': 'wild_buckwheat.n.01', 'name': 'wild_buckwheat'}, {'id': 20196, 'synset': 'rhubarb.n.02', 'name': 'rhubarb'}, {'id': 20197, 'synset': 'himalayan_rhubarb.n.01', 'name': 'Himalayan_rhubarb'}, {'id': 20198, 'synset': 'pie_plant.n.01', 'name': 'pie_plant'}, {'id': 20199, 'synset': 'chinese_rhubarb.n.01', 'name': 'Chinese_rhubarb'}, {'id': 20200, 'synset': 'sour_dock.n.01', 'name': 'sour_dock'}, {'id': 20201, 'synset': 'sheep_sorrel.n.01', 'name': 'sheep_sorrel'}, {'id': 20202, 'synset': 'bitter_dock.n.01', 'name': 'bitter_dock'}, {'id': 20203, 'synset': 'french_sorrel.n.01', 'name': 'French_sorrel'}, {'id': 20204, 'synset': 'yellow-eyed_grass.n.01', 'name': 'yellow-eyed_grass'}, {'id': 20205, 'synset': 'commelina.n.01', 'name': 'commelina'}, {'id': 20206, 'synset': 'spiderwort.n.01', 'name': 'spiderwort'}, {'id': 20207, 'synset': 'pineapple.n.01', 'name': 'pineapple'}, {'id': 20208, 'synset': 'pipewort.n.01', 'name': 'pipewort'}, {'id': 20209, 'synset': 'water_hyacinth.n.01', 'name': 'water_hyacinth'}, {'id': 20210, 'synset': 'water_star_grass.n.01', 'name': 'water_star_grass'}, {'id': 20211, 'synset': 'naiad.n.01', 'name': 'naiad'}, {'id': 20212, 'synset': 'water_plantain.n.01', 'name': 'water_plantain'}, {'id': 20213, 'synset': 'narrow-leaved_water_plantain.n.01', 'name': 'narrow-leaved_water_plantain'}, {'id': 20214, 'synset': 'hydrilla.n.01', 'name': 'hydrilla'}, {'id': 20215, 'synset': 'american_frogbit.n.01', 'name': 'American_frogbit'}, {'id': 20216, 'synset': 'waterweed.n.01', 'name': 'waterweed'}, {'id': 20217, 'synset': 'canadian_pondweed.n.01', 'name': 'Canadian_pondweed'}, {'id': 20218, 'synset': 'tape_grass.n.01', 'name': 'tape_grass'}, {'id': 20219, 'synset': 'pondweed.n.01', 'name': 'pondweed'}, {'id': 20220, 'synset': 'curled_leaf_pondweed.n.01', 'name': 'curled_leaf_pondweed'}, {'id': 20221, 'synset': 'loddon_pondweed.n.01', 'name': 'loddon_pondweed'}, {'id': 20222, 'synset': "frog's_lettuce.n.01", 'name': "frog's_lettuce"}, {'id': 20223, 'synset': 'arrow_grass.n.01', 'name': 'arrow_grass'}, {'id': 20224, 'synset': 'horned_pondweed.n.01', 'name': 'horned_pondweed'}, {'id': 20225, 'synset': 'eelgrass.n.01', 'name': 'eelgrass'}, {'id': 20226, 'synset': 'rose.n.01', 'name': 'rose'}, {'id': 20227, 'synset': 'hip.n.05', 'name': 'hip'}, {'id': 20228, 'synset': 'banksia_rose.n.01', 'name': 'banksia_rose'}, {'id': 20229, 'synset': 'damask_rose.n.01', 'name': 'damask_rose'}, {'id': 20230, 'synset': 'sweetbrier.n.01', 'name': 'sweetbrier'}, {'id': 20231, 'synset': 'cherokee_rose.n.01', 'name': 'Cherokee_rose'}, {'id': 20232, 'synset': 'musk_rose.n.01', 'name': 'musk_rose'}, {'id': 20233, 'synset': 'agrimonia.n.01', 'name': 'agrimonia'}, {'id': 20234, 'synset': 'harvest-lice.n.01', 'name': 'harvest-lice'}, {'id': 20235, 'synset': 'fragrant_agrimony.n.01', 'name': 'fragrant_agrimony'}, {'id': 20236, 'synset': 'alderleaf_juneberry.n.01', 'name': 'alderleaf_Juneberry'}, {'id': 20237, 'synset': 'flowering_quince.n.01', 'name': 'flowering_quince'}, {'id': 20238, 'synset': 'japonica.n.02', 'name': 'japonica'}, {'id': 20239, 'synset': 'coco_plum.n.01', 'name': 'coco_plum'}, {'id': 20240, 'synset': 'cotoneaster.n.01', 'name': 'cotoneaster'}, {'id': 20241, 'synset': 'cotoneaster_dammeri.n.01', 'name': 'Cotoneaster_dammeri'}, {'id': 20242, 'synset': 'cotoneaster_horizontalis.n.01', 'name': 'Cotoneaster_horizontalis'}, {'id': 20243, 'synset': 'parsley_haw.n.01', 'name': 'parsley_haw'}, {'id': 20244, 'synset': 'scarlet_haw.n.01', 'name': 'scarlet_haw'}, {'id': 20245, 'synset': 'blackthorn.n.02', 'name': 'blackthorn'}, {'id': 20246, 'synset': 'cockspur_thorn.n.01', 'name': 'cockspur_thorn'}, {'id': 20247, 'synset': 'mayhaw.n.01', 'name': 'mayhaw'}, {'id': 20248, 'synset': 'red_haw.n.02', 'name': 'red_haw'}, {'id': 20249, 'synset': 'red_haw.n.01', 'name': 'red_haw'}, {'id': 20250, 'synset': 'quince.n.01', 'name': 'quince'}, {'id': 20251, 'synset': 'mountain_avens.n.01', 'name': 'mountain_avens'}, {'id': 20252, 'synset': 'loquat.n.01', 'name': 'loquat'}, {'id': 20253, 'synset': 'beach_strawberry.n.01', 'name': 'beach_strawberry'}, {'id': 20254, 'synset': 'virginia_strawberry.n.01', 'name': 'Virginia_strawberry'}, {'id': 20255, 'synset': 'avens.n.01', 'name': 'avens'}, {'id': 20256, 'synset': 'yellow_avens.n.02', 'name': 'yellow_avens'}, {'id': 20257, 'synset': 'yellow_avens.n.01', 'name': 'yellow_avens'}, {'id': 20258, 'synset': 'prairie_smoke.n.01', 'name': 'prairie_smoke'}, {'id': 20259, 'synset': 'bennet.n.01', 'name': 'bennet'}, {'id': 20260, 'synset': 'toyon.n.01', 'name': 'toyon'}, {'id': 20261, 'synset': 'apple_tree.n.01', 'name': 'apple_tree'}, {'id': 20262, 'synset': 'apple.n.02', 'name': 'apple'}, {'id': 20263, 'synset': 'wild_apple.n.01', 'name': 'wild_apple'}, {'id': 20264, 'synset': 'crab_apple.n.01', 'name': 'crab_apple'}, {'id': 20265, 'synset': 'siberian_crab.n.01', 'name': 'Siberian_crab'}, {'id': 20266, 'synset': 'wild_crab.n.01', 'name': 'wild_crab'}, {'id': 20267, 'synset': 'american_crab_apple.n.01', 'name': 'American_crab_apple'}, {'id': 20268, 'synset': 'oregon_crab_apple.n.01', 'name': 'Oregon_crab_apple'}, {'id': 20269, 'synset': 'southern_crab_apple.n.01', 'name': 'Southern_crab_apple'}, {'id': 20270, 'synset': 'iowa_crab.n.01', 'name': 'Iowa_crab'}, {'id': 20271, 'synset': 'bechtel_crab.n.01', 'name': 'Bechtel_crab'}, {'id': 20272, 'synset': 'medlar.n.02', 'name': 'medlar'}, {'id': 20273, 'synset': 'cinquefoil.n.01', 'name': 'cinquefoil'}, {'id': 20274, 'synset': 'silverweed.n.02', 'name': 'silverweed'}, {'id': 20275, 'synset': 'salad_burnet.n.01', 'name': 'salad_burnet'}, {'id': 20276, 'synset': 'plum.n.01', 'name': 'plum'}, {'id': 20277, 'synset': 'wild_plum.n.01', 'name': 'wild_plum'}, {'id': 20278, 'synset': 'allegheny_plum.n.01', 'name': 'Allegheny_plum'}, {'id': 20279, 'synset': 'american_red_plum.n.01', 'name': 'American_red_plum'}, {'id': 20280, 'synset': 'chickasaw_plum.n.01', 'name': 'chickasaw_plum'}, {'id': 20281, 'synset': 'beach_plum.n.01', 'name': 'beach_plum'}, {'id': 20282, 'synset': 'common_plum.n.01', 'name': 'common_plum'}, {'id': 20283, 'synset': 'bullace.n.01', 'name': 'bullace'}, {'id': 20284, 'synset': 'damson_plum.n.02', 'name': 'damson_plum'}, {'id': 20285, 'synset': 'big-tree_plum.n.01', 'name': 'big-tree_plum'}, {'id': 20286, 'synset': 'canada_plum.n.01', 'name': 'Canada_plum'}, {'id': 20287, 'synset': 'plumcot.n.01', 'name': 'plumcot'}, {'id': 20288, 'synset': 'apricot.n.01', 'name': 'apricot'}, {'id': 20289, 'synset': 'japanese_apricot.n.01', 'name': 'Japanese_apricot'}, {'id': 20290, 'synset': 'common_apricot.n.01', 'name': 'common_apricot'}, {'id': 20291, 'synset': 'purple_apricot.n.01', 'name': 'purple_apricot'}, {'id': 20292, 'synset': 'cherry.n.02', 'name': 'cherry'}, {'id': 20293, 'synset': 'wild_cherry.n.02', 'name': 'wild_cherry'}, {'id': 20294, 'synset': 'wild_cherry.n.01', 'name': 'wild_cherry'}, {'id': 20295, 'synset': 'sweet_cherry.n.01', 'name': 'sweet_cherry'}, {'id': 20296, 'synset': 'heart_cherry.n.01', 'name': 'heart_cherry'}, {'id': 20297, 'synset': 'gean.n.01', 'name': 'gean'}, {'id': 20298, 'synset': 'capulin.n.01', 'name': 'capulin'}, {'id': 20299, 'synset': 'cherry_laurel.n.02', 'name': 'cherry_laurel'}, {'id': 20300, 'synset': 'cherry_plum.n.01', 'name': 'cherry_plum'}, {'id': 20301, 'synset': 'sour_cherry.n.01', 'name': 'sour_cherry'}, {'id': 20302, 'synset': 'amarelle.n.01', 'name': 'amarelle'}, {'id': 20303, 'synset': 'morello.n.01', 'name': 'morello'}, {'id': 20304, 'synset': 'marasca.n.01', 'name': 'marasca'}, {'id': 20305, 'synset': 'almond_tree.n.01', 'name': 'almond_tree'}, {'id': 20306, 'synset': 'almond.n.01', 'name': 'almond'}, {'id': 20307, 'synset': 'bitter_almond.n.01', 'name': 'bitter_almond'}, {'id': 20308, 'synset': 'jordan_almond.n.01', 'name': 'jordan_almond'}, {'id': 20309, 'synset': 'dwarf_flowering_almond.n.01', 'name': 'dwarf_flowering_almond'}, {'id': 20310, 'synset': 'holly-leaved_cherry.n.01', 'name': 'holly-leaved_cherry'}, {'id': 20311, 'synset': 'fuji.n.01', 'name': 'fuji'}, {'id': 20312, 'synset': 'flowering_almond.n.02', 'name': 'flowering_almond'}, {'id': 20313, 'synset': 'cherry_laurel.n.01', 'name': 'cherry_laurel'}, {'id': 20314, 'synset': 'catalina_cherry.n.01', 'name': 'Catalina_cherry'}, {'id': 20315, 'synset': 'bird_cherry.n.01', 'name': 'bird_cherry'}, {'id': 20316, 'synset': 'hagberry_tree.n.01', 'name': 'hagberry_tree'}, {'id': 20317, 'synset': 'hagberry.n.01', 'name': 'hagberry'}, {'id': 20318, 'synset': 'pin_cherry.n.01', 'name': 'pin_cherry'}, {'id': 20319, 'synset': 'peach.n.01', 'name': 'peach'}, {'id': 20320, 'synset': 'nectarine.n.01', 'name': 'nectarine'}, {'id': 20321, 'synset': 'sand_cherry.n.01', 'name': 'sand_cherry'}, {'id': 20322, 'synset': 'japanese_plum.n.01', 'name': 'Japanese_plum'}, {'id': 20323, 'synset': 'black_cherry.n.01', 'name': 'black_cherry'}, {'id': 20324, 'synset': 'flowering_cherry.n.01', 'name': 'flowering_cherry'}, {'id': 20325, 'synset': 'oriental_cherry.n.01', 'name': 'oriental_cherry'}, {'id': 20326, 'synset': 'japanese_flowering_cherry.n.01', 'name': 'Japanese_flowering_cherry'}, {'id': 20327, 'synset': 'sierra_plum.n.01', 'name': 'Sierra_plum'}, {'id': 20328, 'synset': 'rosebud_cherry.n.01', 'name': 'rosebud_cherry'}, {'id': 20329, 'synset': 'russian_almond.n.01', 'name': 'Russian_almond'}, {'id': 20330, 'synset': 'flowering_almond.n.01', 'name': 'flowering_almond'}, {'id': 20331, 'synset': 'chokecherry.n.02', 'name': 'chokecherry'}, {'id': 20332, 'synset': 'chokecherry.n.01', 'name': 'chokecherry'}, {'id': 20333, 'synset': 'western_chokecherry.n.01', 'name': 'western_chokecherry'}, {'id': 20334, 'synset': 'pyracantha.n.01', 'name': 'Pyracantha'}, {'id': 20335, 'synset': 'pear.n.02', 'name': 'pear'}, {'id': 20336, 'synset': 'fruit_tree.n.01', 'name': 'fruit_tree'}, {'id': 20337, 'synset': 'bramble_bush.n.01', 'name': 'bramble_bush'}, {'id': 20338, 'synset': 'lawyerbush.n.01', 'name': 'lawyerbush'}, {'id': 20339, 'synset': 'stone_bramble.n.01', 'name': 'stone_bramble'}, {'id': 20340, 'synset': 'sand_blackberry.n.01', 'name': 'sand_blackberry'}, {'id': 20341, 'synset': 'boysenberry.n.01', 'name': 'boysenberry'}, {'id': 20342, 'synset': 'loganberry.n.01', 'name': 'loganberry'}, {'id': 20343, 'synset': 'american_dewberry.n.02', 'name': 'American_dewberry'}, {'id': 20344, 'synset': 'northern_dewberry.n.01', 'name': 'Northern_dewberry'}, {'id': 20345, 'synset': 'southern_dewberry.n.01', 'name': 'Southern_dewberry'}, {'id': 20346, 'synset': 'swamp_dewberry.n.01', 'name': 'swamp_dewberry'}, {'id': 20347, 'synset': 'european_dewberry.n.01', 'name': 'European_dewberry'}, {'id': 20348, 'synset': 'raspberry.n.01', 'name': 'raspberry'}, {'id': 20349, 'synset': 'wild_raspberry.n.01', 'name': 'wild_raspberry'}, {'id': 20350, 'synset': 'american_raspberry.n.01', 'name': 'American_raspberry'}, {'id': 20351, 'synset': 'black_raspberry.n.01', 'name': 'black_raspberry'}, {'id': 20352, 'synset': 'salmonberry.n.03', 'name': 'salmonberry'}, {'id': 20353, 'synset': 'salmonberry.n.02', 'name': 'salmonberry'}, {'id': 20354, 'synset': 'wineberry.n.01', 'name': 'wineberry'}, {'id': 20355, 'synset': 'mountain_ash.n.01', 'name': 'mountain_ash'}, {'id': 20356, 'synset': 'rowan.n.01', 'name': 'rowan'}, {'id': 20357, 'synset': 'rowanberry.n.01', 'name': 'rowanberry'}, {'id': 20358, 'synset': 'american_mountain_ash.n.01', 'name': 'American_mountain_ash'}, {'id': 20359, 'synset': 'western_mountain_ash.n.01', 'name': 'Western_mountain_ash'}, {'id': 20360, 'synset': 'service_tree.n.01', 'name': 'service_tree'}, {'id': 20361, 'synset': 'wild_service_tree.n.01', 'name': 'wild_service_tree'}, {'id': 20362, 'synset': 'spirea.n.02', 'name': 'spirea'}, {'id': 20363, 'synset': 'bridal_wreath.n.02', 'name': 'bridal_wreath'}, {'id': 20364, 'synset': 'madderwort.n.01', 'name': 'madderwort'}, {'id': 20365, 'synset': 'indian_madder.n.01', 'name': 'Indian_madder'}, {'id': 20366, 'synset': 'madder.n.01', 'name': 'madder'}, {'id': 20367, 'synset': 'woodruff.n.02', 'name': 'woodruff'}, {'id': 20368, 'synset': 'dagame.n.01', 'name': 'dagame'}, {'id': 20369, 'synset': 'blolly.n.01', 'name': 'blolly'}, {'id': 20370, 'synset': 'coffee.n.02', 'name': 'coffee'}, {'id': 20371, 'synset': 'arabian_coffee.n.01', 'name': 'Arabian_coffee'}, {'id': 20372, 'synset': 'liberian_coffee.n.01', 'name': 'Liberian_coffee'}, {'id': 20373, 'synset': 'robusta_coffee.n.01', 'name': 'robusta_coffee'}, {'id': 20374, 'synset': 'cinchona.n.02', 'name': 'cinchona'}, {'id': 20375, 'synset': 'cartagena_bark.n.01', 'name': 'Cartagena_bark'}, {'id': 20376, 'synset': 'calisaya.n.01', 'name': 'calisaya'}, {'id': 20377, 'synset': 'cinchona_tree.n.01', 'name': 'cinchona_tree'}, {'id': 20378, 'synset': 'cinchona.n.01', 'name': 'cinchona'}, {'id': 20379, 'synset': 'bedstraw.n.01', 'name': 'bedstraw'}, {'id': 20380, 'synset': 'sweet_woodruff.n.01', 'name': 'sweet_woodruff'}, {'id': 20381, 'synset': 'northern_bedstraw.n.01', 'name': 'Northern_bedstraw'}, {'id': 20382, 'synset': 'yellow_bedstraw.n.01', 'name': 'yellow_bedstraw'}, {'id': 20383, 'synset': 'wild_licorice.n.01', 'name': 'wild_licorice'}, {'id': 20384, 'synset': 'cleavers.n.01', 'name': 'cleavers'}, {'id': 20385, 'synset': 'wild_madder.n.01', 'name': 'wild_madder'}, {'id': 20386, 'synset': 'cape_jasmine.n.01', 'name': 'cape_jasmine'}, {'id': 20387, 'synset': 'genipa.n.01', 'name': 'genipa'}, {'id': 20388, 'synset': 'genipap_fruit.n.01', 'name': 'genipap_fruit'}, {'id': 20389, 'synset': 'hamelia.n.01', 'name': 'hamelia'}, {'id': 20390, 'synset': 'scarlet_bush.n.01', 'name': 'scarlet_bush'}, {'id': 20391, 'synset': 'lemonwood.n.02', 'name': 'lemonwood'}, {'id': 20392, 'synset': 'negro_peach.n.01', 'name': 'negro_peach'}, {'id': 20393, 'synset': 'wild_medlar.n.01', 'name': 'wild_medlar'}, {'id': 20394, 'synset': 'spanish_tamarind.n.01', 'name': 'Spanish_tamarind'}, {'id': 20395, 'synset': 'abelia.n.01', 'name': 'abelia'}, {'id': 20396, 'synset': 'bush_honeysuckle.n.02', 'name': 'bush_honeysuckle'}, {'id': 20397, 'synset': 'american_twinflower.n.01', 'name': 'American_twinflower'}, {'id': 20398, 'synset': 'honeysuckle.n.01', 'name': 'honeysuckle'}, {'id': 20399, 'synset': 'american_fly_honeysuckle.n.01', 'name': 'American_fly_honeysuckle'}, {'id': 20400, 'synset': 'italian_honeysuckle.n.01', 'name': 'Italian_honeysuckle'}, {'id': 20401, 'synset': 'yellow_honeysuckle.n.01', 'name': 'yellow_honeysuckle'}, {'id': 20402, 'synset': 'hairy_honeysuckle.n.01', 'name': 'hairy_honeysuckle'}, {'id': 20403, 'synset': 'japanese_honeysuckle.n.01', 'name': 'Japanese_honeysuckle'}, {'id': 20404, 'synset': "hall's_honeysuckle.n.01", 'name': "Hall's_honeysuckle"}, {'id': 20405, 'synset': "morrow's_honeysuckle.n.01", 'name': "Morrow's_honeysuckle"}, {'id': 20406, 'synset': 'woodbine.n.02', 'name': 'woodbine'}, {'id': 20407, 'synset': 'trumpet_honeysuckle.n.01', 'name': 'trumpet_honeysuckle'}, {'id': 20408, 'synset': 'european_fly_honeysuckle.n.01', 'name': 'European_fly_honeysuckle'}, {'id': 20409, 'synset': 'swamp_fly_honeysuckle.n.01', 'name': 'swamp_fly_honeysuckle'}, {'id': 20410, 'synset': 'snowberry.n.01', 'name': 'snowberry'}, {'id': 20411, 'synset': 'coralberry.n.01', 'name': 'coralberry'}, {'id': 20412, 'synset': 'blue_elder.n.01', 'name': 'blue_elder'}, {'id': 20413, 'synset': 'dwarf_elder.n.01', 'name': 'dwarf_elder'}, {'id': 20414, 'synset': 'american_red_elder.n.01', 'name': 'American_red_elder'}, {'id': 20415, 'synset': 'european_red_elder.n.01', 'name': 'European_red_elder'}, {'id': 20416, 'synset': 'feverroot.n.01', 'name': 'feverroot'}, {'id': 20417, 'synset': 'cranberry_bush.n.01', 'name': 'cranberry_bush'}, {'id': 20418, 'synset': 'wayfaring_tree.n.01', 'name': 'wayfaring_tree'}, {'id': 20419, 'synset': 'guelder_rose.n.01', 'name': 'guelder_rose'}, {'id': 20420, 'synset': 'arrow_wood.n.01', 'name': 'arrow_wood'}, {'id': 20421, 'synset': 'black_haw.n.02', 'name': 'black_haw'}, {'id': 20422, 'synset': 'weigela.n.01', 'name': 'weigela'}, {'id': 20423, 'synset': 'teasel.n.01', 'name': 'teasel'}, {'id': 20424, 'synset': 'common_teasel.n.01', 'name': 'common_teasel'}, {'id': 20425, 'synset': "fuller's_teasel.n.01", 'name': "fuller's_teasel"}, {'id': 20426, 'synset': 'wild_teasel.n.01', 'name': 'wild_teasel'}, {'id': 20427, 'synset': 'scabious.n.01', 'name': 'scabious'}, {'id': 20428, 'synset': 'sweet_scabious.n.01', 'name': 'sweet_scabious'}, {'id': 20429, 'synset': 'field_scabious.n.01', 'name': 'field_scabious'}, {'id': 20430, 'synset': 'jewelweed.n.01', 'name': 'jewelweed'}, {'id': 20431, 'synset': 'geranium.n.01', 'name': 'geranium'}, {'id': 20432, 'synset': 'cranesbill.n.01', 'name': 'cranesbill'}, {'id': 20433, 'synset': 'wild_geranium.n.01', 'name': 'wild_geranium'}, {'id': 20434, 'synset': 'meadow_cranesbill.n.01', 'name': 'meadow_cranesbill'}, {'id': 20435, 'synset': "richardson's_geranium.n.01", 'name': "Richardson's_geranium"}, {'id': 20436, 'synset': 'herb_robert.n.01', 'name': 'herb_robert'}, {'id': 20437, 'synset': 'sticky_geranium.n.01', 'name': 'sticky_geranium'}, {'id': 20438, 'synset': "dove's_foot_geranium.n.01", 'name': "dove's_foot_geranium"}, {'id': 20439, 'synset': 'rose_geranium.n.01', 'name': 'rose_geranium'}, {'id': 20440, 'synset': 'fish_geranium.n.01', 'name': 'fish_geranium'}, {'id': 20441, 'synset': 'ivy_geranium.n.01', 'name': 'ivy_geranium'}, {'id': 20442, 'synset': 'apple_geranium.n.01', 'name': 'apple_geranium'}, {'id': 20443, 'synset': 'lemon_geranium.n.01', 'name': 'lemon_geranium'}, {'id': 20444, 'synset': 'storksbill.n.01', 'name': 'storksbill'}, {'id': 20445, 'synset': 'musk_clover.n.01', 'name': 'musk_clover'}, {'id': 20446, 'synset': 'incense_tree.n.01', 'name': 'incense_tree'}, {'id': 20447, 'synset': 'elephant_tree.n.01', 'name': 'elephant_tree'}, {'id': 20448, 'synset': 'gumbo-limbo.n.01', 'name': 'gumbo-limbo'}, {'id': 20449, 'synset': 'boswellia_carteri.n.01', 'name': 'Boswellia_carteri'}, {'id': 20450, 'synset': 'salai.n.01', 'name': 'salai'}, {'id': 20451, 'synset': 'balm_of_gilead.n.03', 'name': 'balm_of_gilead'}, {'id': 20452, 'synset': 'myrrh_tree.n.01', 'name': 'myrrh_tree'}, {'id': 20453, 'synset': 'protium_heptaphyllum.n.01', 'name': 'Protium_heptaphyllum'}, {'id': 20454, 'synset': 'protium_guianense.n.01', 'name': 'Protium_guianense'}, {'id': 20455, 'synset': 'water_starwort.n.01', 'name': 'water_starwort'}, {'id': 20456, 'synset': 'barbados_cherry.n.01', 'name': 'barbados_cherry'}, {'id': 20457, 'synset': 'mahogany.n.02', 'name': 'mahogany'}, {'id': 20458, 'synset': 'chinaberry.n.02', 'name': 'chinaberry'}, {'id': 20459, 'synset': 'neem.n.01', 'name': 'neem'}, {'id': 20460, 'synset': 'neem_seed.n.01', 'name': 'neem_seed'}, {'id': 20461, 'synset': 'spanish_cedar.n.01', 'name': 'Spanish_cedar'}, {'id': 20462, 'synset': 'satinwood.n.03', 'name': 'satinwood'}, {'id': 20463, 'synset': 'african_scented_mahogany.n.01', 'name': 'African_scented_mahogany'}, {'id': 20464, 'synset': 'silver_ash.n.01', 'name': 'silver_ash'}, {'id': 20465, 'synset': 'native_beech.n.01', 'name': 'native_beech'}, {'id': 20466, 'synset': 'bunji-bunji.n.01', 'name': 'bunji-bunji'}, {'id': 20467, 'synset': 'african_mahogany.n.01', 'name': 'African_mahogany'}, {'id': 20468, 'synset': 'lanseh_tree.n.01', 'name': 'lanseh_tree'}, {'id': 20469, 'synset': 'true_mahogany.n.01', 'name': 'true_mahogany'}, {'id': 20470, 'synset': 'honduras_mahogany.n.01', 'name': 'Honduras_mahogany'}, {'id': 20471, 'synset': 'philippine_mahogany.n.02', 'name': 'Philippine_mahogany'}, {'id': 20472, 'synset': 'caracolito.n.01', 'name': 'caracolito'}, {'id': 20473, 'synset': 'common_wood_sorrel.n.01', 'name': 'common_wood_sorrel'}, {'id': 20474, 'synset': 'bermuda_buttercup.n.01', 'name': 'Bermuda_buttercup'}, {'id': 20475, 'synset': 'creeping_oxalis.n.01', 'name': 'creeping_oxalis'}, {'id': 20476, 'synset': 'goatsfoot.n.01', 'name': 'goatsfoot'}, {'id': 20477, 'synset': 'violet_wood_sorrel.n.01', 'name': 'violet_wood_sorrel'}, {'id': 20478, 'synset': 'oca.n.01', 'name': 'oca'}, {'id': 20479, 'synset': 'carambola.n.01', 'name': 'carambola'}, {'id': 20480, 'synset': 'bilimbi.n.01', 'name': 'bilimbi'}, {'id': 20481, 'synset': 'milkwort.n.01', 'name': 'milkwort'}, {'id': 20482, 'synset': 'senega.n.02', 'name': 'senega'}, {'id': 20483, 'synset': 'orange_milkwort.n.01', 'name': 'orange_milkwort'}, {'id': 20484, 'synset': 'flowering_wintergreen.n.01', 'name': 'flowering_wintergreen'}, {'id': 20485, 'synset': 'seneca_snakeroot.n.01', 'name': 'Seneca_snakeroot'}, {'id': 20486, 'synset': 'common_milkwort.n.01', 'name': 'common_milkwort'}, {'id': 20487, 'synset': 'rue.n.01', 'name': 'rue'}, {'id': 20488, 'synset': 'citrus.n.02', 'name': 'citrus'}, {'id': 20489, 'synset': 'orange.n.03', 'name': 'orange'}, {'id': 20490, 'synset': 'sour_orange.n.01', 'name': 'sour_orange'}, {'id': 20491, 'synset': 'bergamot.n.01', 'name': 'bergamot'}, {'id': 20492, 'synset': 'pomelo.n.01', 'name': 'pomelo'}, {'id': 20493, 'synset': 'citron.n.02', 'name': 'citron'}, {'id': 20494, 'synset': 'grapefruit.n.01', 'name': 'grapefruit'}, {'id': 20495, 'synset': 'mandarin.n.01', 'name': 'mandarin'}, {'id': 20496, 'synset': 'tangerine.n.01', 'name': 'tangerine'}, {'id': 20497, 'synset': 'satsuma.n.01', 'name': 'satsuma'}, {'id': 20498, 'synset': 'sweet_orange.n.02', 'name': 'sweet_orange'}, {'id': 20499, 'synset': 'temple_orange.n.01', 'name': 'temple_orange'}, {'id': 20500, 'synset': 'tangelo.n.01', 'name': 'tangelo'}, {'id': 20501, 'synset': 'rangpur.n.01', 'name': 'rangpur'}, {'id': 20502, 'synset': 'lemon.n.03', 'name': 'lemon'}, {'id': 20503, 'synset': 'sweet_lemon.n.01', 'name': 'sweet_lemon'}, {'id': 20504, 'synset': 'lime.n.04', 'name': 'lime'}, {'id': 20505, 'synset': 'citrange.n.01', 'name': 'citrange'}, {'id': 20506, 'synset': 'fraxinella.n.01', 'name': 'fraxinella'}, {'id': 20507, 'synset': 'kumquat.n.01', 'name': 'kumquat'}, {'id': 20508, 'synset': 'marumi.n.01', 'name': 'marumi'}, {'id': 20509, 'synset': 'nagami.n.01', 'name': 'nagami'}, {'id': 20510, 'synset': 'cork_tree.n.01', 'name': 'cork_tree'}, {'id': 20511, 'synset': 'trifoliate_orange.n.01', 'name': 'trifoliate_orange'}, {'id': 20512, 'synset': 'prickly_ash.n.01', 'name': 'prickly_ash'}, {'id': 20513, 'synset': 'toothache_tree.n.01', 'name': 'toothache_tree'}, {'id': 20514, 'synset': "hercules'-club.n.01", 'name': "Hercules'-club"}, {'id': 20515, 'synset': 'bitterwood_tree.n.01', 'name': 'bitterwood_tree'}, {'id': 20516, 'synset': 'marupa.n.01', 'name': 'marupa'}, {'id': 20517, 'synset': 'paradise_tree.n.01', 'name': 'paradise_tree'}, {'id': 20518, 'synset': 'ailanthus.n.01', 'name': 'ailanthus'}, {'id': 20519, 'synset': 'tree_of_heaven.n.01', 'name': 'tree_of_heaven'}, {'id': 20520, 'synset': 'wild_mango.n.01', 'name': 'wild_mango'}, {'id': 20521, 'synset': 'pepper_tree.n.02', 'name': 'pepper_tree'}, {'id': 20522, 'synset': 'jamaica_quassia.n.02', 'name': 'Jamaica_quassia'}, {'id': 20523, 'synset': 'quassia.n.02', 'name': 'quassia'}, {'id': 20524, 'synset': 'nasturtium.n.01', 'name': 'nasturtium'}, {'id': 20525, 'synset': 'garden_nasturtium.n.01', 'name': 'garden_nasturtium'}, {'id': 20526, 'synset': 'bush_nasturtium.n.01', 'name': 'bush_nasturtium'}, {'id': 20527, 'synset': 'canarybird_flower.n.01', 'name': 'canarybird_flower'}, {'id': 20528, 'synset': 'bean_caper.n.01', 'name': 'bean_caper'}, {'id': 20529, 'synset': 'palo_santo.n.01', 'name': 'palo_santo'}, {'id': 20530, 'synset': 'lignum_vitae.n.02', 'name': 'lignum_vitae'}, {'id': 20531, 'synset': 'creosote_bush.n.01', 'name': 'creosote_bush'}, {'id': 20532, 'synset': 'caltrop.n.01', 'name': 'caltrop'}, {'id': 20533, 'synset': 'willow.n.01', 'name': 'willow'}, {'id': 20534, 'synset': 'osier.n.02', 'name': 'osier'}, {'id': 20535, 'synset': 'white_willow.n.01', 'name': 'white_willow'}, {'id': 20536, 'synset': 'silver_willow.n.01', 'name': 'silver_willow'}, {'id': 20537, 'synset': 'golden_willow.n.01', 'name': 'golden_willow'}, {'id': 20538, 'synset': 'cricket-bat_willow.n.01', 'name': 'cricket-bat_willow'}, {'id': 20539, 'synset': 'arctic_willow.n.01', 'name': 'arctic_willow'}, {'id': 20540, 'synset': 'weeping_willow.n.01', 'name': 'weeping_willow'}, {'id': 20541, 'synset': 'wisconsin_weeping_willow.n.01', 'name': 'Wisconsin_weeping_willow'}, {'id': 20542, 'synset': 'pussy_willow.n.01', 'name': 'pussy_willow'}, {'id': 20543, 'synset': 'sallow.n.01', 'name': 'sallow'}, {'id': 20544, 'synset': 'goat_willow.n.01', 'name': 'goat_willow'}, {'id': 20545, 'synset': 'peachleaf_willow.n.01', 'name': 'peachleaf_willow'}, {'id': 20546, 'synset': 'almond_willow.n.01', 'name': 'almond_willow'}, {'id': 20547, 'synset': 'hoary_willow.n.01', 'name': 'hoary_willow'}, {'id': 20548, 'synset': 'crack_willow.n.01', 'name': 'crack_willow'}, {'id': 20549, 'synset': 'prairie_willow.n.01', 'name': 'prairie_willow'}, {'id': 20550, 'synset': 'dwarf_willow.n.01', 'name': 'dwarf_willow'}, {'id': 20551, 'synset': 'grey_willow.n.01', 'name': 'grey_willow'}, {'id': 20552, 'synset': 'arroyo_willow.n.01', 'name': 'arroyo_willow'}, {'id': 20553, 'synset': 'shining_willow.n.01', 'name': 'shining_willow'}, {'id': 20554, 'synset': 'swamp_willow.n.01', 'name': 'swamp_willow'}, {'id': 20555, 'synset': 'bay_willow.n.01', 'name': 'bay_willow'}, {'id': 20556, 'synset': 'purple_willow.n.01', 'name': 'purple_willow'}, {'id': 20557, 'synset': 'balsam_willow.n.01', 'name': 'balsam_willow'}, {'id': 20558, 'synset': 'creeping_willow.n.01', 'name': 'creeping_willow'}, {'id': 20559, 'synset': 'sitka_willow.n.01', 'name': 'Sitka_willow'}, {'id': 20560, 'synset': 'dwarf_grey_willow.n.01', 'name': 'dwarf_grey_willow'}, {'id': 20561, 'synset': 'bearberry_willow.n.01', 'name': 'bearberry_willow'}, {'id': 20562, 'synset': 'common_osier.n.01', 'name': 'common_osier'}, {'id': 20563, 'synset': 'poplar.n.02', 'name': 'poplar'}, {'id': 20564, 'synset': 'balsam_poplar.n.01', 'name': 'balsam_poplar'}, {'id': 20565, 'synset': 'white_poplar.n.01', 'name': 'white_poplar'}, {'id': 20566, 'synset': 'grey_poplar.n.01', 'name': 'grey_poplar'}, {'id': 20567, 'synset': 'black_poplar.n.01', 'name': 'black_poplar'}, {'id': 20568, 'synset': 'lombardy_poplar.n.01', 'name': 'Lombardy_poplar'}, {'id': 20569, 'synset': 'cottonwood.n.01', 'name': 'cottonwood'}, {'id': 20570, 'synset': 'eastern_cottonwood.n.01', 'name': 'Eastern_cottonwood'}, {'id': 20571, 'synset': 'black_cottonwood.n.02', 'name': 'black_cottonwood'}, {'id': 20572, 'synset': 'swamp_cottonwood.n.01', 'name': 'swamp_cottonwood'}, {'id': 20573, 'synset': 'aspen.n.01', 'name': 'aspen'}, {'id': 20574, 'synset': 'quaking_aspen.n.01', 'name': 'quaking_aspen'}, {'id': 20575, 'synset': 'american_quaking_aspen.n.01', 'name': 'American_quaking_aspen'}, {'id': 20576, 'synset': 'canadian_aspen.n.01', 'name': 'Canadian_aspen'}, {'id': 20577, 'synset': 'sandalwood_tree.n.01', 'name': 'sandalwood_tree'}, {'id': 20578, 'synset': 'quandong.n.01', 'name': 'quandong'}, {'id': 20579, 'synset': 'rabbitwood.n.01', 'name': 'rabbitwood'}, {'id': 20580, 'synset': 'loranthaceae.n.01', 'name': 'Loranthaceae'}, {'id': 20581, 'synset': 'mistletoe.n.03', 'name': 'mistletoe'}, {'id': 20582, 'synset': 'american_mistletoe.n.02', 'name': 'American_mistletoe'}, {'id': 20583, 'synset': 'mistletoe.n.02', 'name': 'mistletoe'}, {'id': 20584, 'synset': 'american_mistletoe.n.01', 'name': 'American_mistletoe'}, {'id': 20585, 'synset': 'aalii.n.01', 'name': 'aalii'}, {'id': 20586, 'synset': 'soapberry.n.01', 'name': 'soapberry'}, {'id': 20587, 'synset': 'wild_china_tree.n.01', 'name': 'wild_China_tree'}, {'id': 20588, 'synset': 'china_tree.n.01', 'name': 'China_tree'}, {'id': 20589, 'synset': 'akee.n.01', 'name': 'akee'}, {'id': 20590, 'synset': 'soapberry_vine.n.01', 'name': 'soapberry_vine'}, {'id': 20591, 'synset': 'heartseed.n.01', 'name': 'heartseed'}, {'id': 20592, 'synset': 'balloon_vine.n.01', 'name': 'balloon_vine'}, {'id': 20593, 'synset': 'longan.n.01', 'name': 'longan'}, {'id': 20594, 'synset': 'harpullia.n.01', 'name': 'harpullia'}, {'id': 20595, 'synset': 'harpulla.n.01', 'name': 'harpulla'}, {'id': 20596, 'synset': 'moreton_bay_tulipwood.n.01', 'name': 'Moreton_Bay_tulipwood'}, {'id': 20597, 'synset': 'litchi.n.01', 'name': 'litchi'}, {'id': 20598, 'synset': 'spanish_lime.n.01', 'name': 'Spanish_lime'}, {'id': 20599, 'synset': 'rambutan.n.01', 'name': 'rambutan'}, {'id': 20600, 'synset': 'pulasan.n.01', 'name': 'pulasan'}, {'id': 20601, 'synset': 'pachysandra.n.01', 'name': 'pachysandra'}, {'id': 20602, 'synset': 'allegheny_spurge.n.01', 'name': 'Allegheny_spurge'}, {'id': 20603, 'synset': 'bittersweet.n.02', 'name': 'bittersweet'}, {'id': 20604, 'synset': 'spindle_tree.n.01', 'name': 'spindle_tree'}, {'id': 20605, 'synset': 'winged_spindle_tree.n.01', 'name': 'winged_spindle_tree'}, {'id': 20606, 'synset': 'wahoo.n.02', 'name': 'wahoo'}, {'id': 20607, 'synset': 'strawberry_bush.n.01', 'name': 'strawberry_bush'}, {'id': 20608, 'synset': 'evergreen_bittersweet.n.01', 'name': 'evergreen_bittersweet'}, {'id': 20609, 'synset': 'cyrilla.n.01', 'name': 'cyrilla'}, {'id': 20610, 'synset': 'titi.n.01', 'name': 'titi'}, {'id': 20611, 'synset': 'crowberry.n.01', 'name': 'crowberry'}, {'id': 20612, 'synset': 'maple.n.02', 'name': 'maple'}, {'id': 20613, 'synset': 'silver_maple.n.01', 'name': 'silver_maple'}, {'id': 20614, 'synset': 'sugar_maple.n.01', 'name': 'sugar_maple'}, {'id': 20615, 'synset': 'red_maple.n.01', 'name': 'red_maple'}, {'id': 20616, 'synset': 'moosewood.n.01', 'name': 'moosewood'}, {'id': 20617, 'synset': 'oregon_maple.n.01', 'name': 'Oregon_maple'}, {'id': 20618, 'synset': 'dwarf_maple.n.01', 'name': 'dwarf_maple'}, {'id': 20619, 'synset': 'mountain_maple.n.01', 'name': 'mountain_maple'}, {'id': 20620, 'synset': 'vine_maple.n.01', 'name': 'vine_maple'}, {'id': 20621, 'synset': 'hedge_maple.n.01', 'name': 'hedge_maple'}, {'id': 20622, 'synset': 'norway_maple.n.01', 'name': 'Norway_maple'}, {'id': 20623, 'synset': 'sycamore.n.03', 'name': 'sycamore'}, {'id': 20624, 'synset': 'box_elder.n.01', 'name': 'box_elder'}, {'id': 20625, 'synset': 'california_box_elder.n.01', 'name': 'California_box_elder'}, {'id': 20626, 'synset': 'pointed-leaf_maple.n.01', 'name': 'pointed-leaf_maple'}, {'id': 20627, 'synset': 'japanese_maple.n.02', 'name': 'Japanese_maple'}, {'id': 20628, 'synset': 'japanese_maple.n.01', 'name': 'Japanese_maple'}, {'id': 20629, 'synset': 'holly.n.01', 'name': 'holly'}, {'id': 20630, 'synset': 'chinese_holly.n.01', 'name': 'Chinese_holly'}, {'id': 20631, 'synset': 'bearberry.n.02', 'name': 'bearberry'}, {'id': 20632, 'synset': 'inkberry.n.01', 'name': 'inkberry'}, {'id': 20633, 'synset': 'mate.n.07', 'name': 'mate'}, {'id': 20634, 'synset': 'american_holly.n.01', 'name': 'American_holly'}, {'id': 20635, 'synset': 'low_gallberry_holly.n.01', 'name': 'low_gallberry_holly'}, {'id': 20636, 'synset': 'tall_gallberry_holly.n.01', 'name': 'tall_gallberry_holly'}, {'id': 20637, 'synset': 'yaupon_holly.n.01', 'name': 'yaupon_holly'}, {'id': 20638, 'synset': 'deciduous_holly.n.01', 'name': 'deciduous_holly'}, {'id': 20639, 'synset': 'juneberry_holly.n.01', 'name': 'juneberry_holly'}, {'id': 20640, 'synset': 'largeleaf_holly.n.01', 'name': 'largeleaf_holly'}, {'id': 20641, 'synset': 'geogia_holly.n.01', 'name': 'Geogia_holly'}, {'id': 20642, 'synset': 'common_winterberry_holly.n.01', 'name': 'common_winterberry_holly'}, {'id': 20643, 'synset': 'smooth_winterberry_holly.n.01', 'name': 'smooth_winterberry_holly'}, {'id': 20644, 'synset': 'cashew.n.01', 'name': 'cashew'}, {'id': 20645, 'synset': 'goncalo_alves.n.01', 'name': 'goncalo_alves'}, {'id': 20646, 'synset': 'venetian_sumac.n.01', 'name': 'Venetian_sumac'}, {'id': 20647, 'synset': 'laurel_sumac.n.01', 'name': 'laurel_sumac'}, {'id': 20648, 'synset': 'mango.n.01', 'name': 'mango'}, {'id': 20649, 'synset': 'pistachio.n.01', 'name': 'pistachio'}, {'id': 20650, 'synset': 'terebinth.n.01', 'name': 'terebinth'}, {'id': 20651, 'synset': 'mastic.n.03', 'name': 'mastic'}, {'id': 20652, 'synset': 'australian_sumac.n.01', 'name': 'Australian_sumac'}, {'id': 20653, 'synset': 'sumac.n.02', 'name': 'sumac'}, {'id': 20654, 'synset': 'smooth_sumac.n.01', 'name': 'smooth_sumac'}, {'id': 20655, 'synset': 'sugar-bush.n.01', 'name': 'sugar-bush'}, {'id': 20656, 'synset': 'staghorn_sumac.n.01', 'name': 'staghorn_sumac'}, {'id': 20657, 'synset': 'squawbush.n.01', 'name': 'squawbush'}, {'id': 20658, 'synset': 'aroeira_blanca.n.01', 'name': 'aroeira_blanca'}, {'id': 20659, 'synset': 'pepper_tree.n.01', 'name': 'pepper_tree'}, {'id': 20660, 'synset': 'brazilian_pepper_tree.n.01', 'name': 'Brazilian_pepper_tree'}, {'id': 20661, 'synset': 'hog_plum.n.01', 'name': 'hog_plum'}, {'id': 20662, 'synset': 'mombin.n.01', 'name': 'mombin'}, {'id': 20663, 'synset': 'poison_ash.n.01', 'name': 'poison_ash'}, {'id': 20664, 'synset': 'poison_ivy.n.02', 'name': 'poison_ivy'}, {'id': 20665, 'synset': 'western_poison_oak.n.01', 'name': 'western_poison_oak'}, {'id': 20666, 'synset': 'eastern_poison_oak.n.01', 'name': 'eastern_poison_oak'}, {'id': 20667, 'synset': 'varnish_tree.n.02', 'name': 'varnish_tree'}, {'id': 20668, 'synset': 'horse_chestnut.n.01', 'name': 'horse_chestnut'}, {'id': 20669, 'synset': 'buckeye.n.01', 'name': 'buckeye'}, {'id': 20670, 'synset': 'sweet_buckeye.n.01', 'name': 'sweet_buckeye'}, {'id': 20671, 'synset': 'ohio_buckeye.n.01', 'name': 'Ohio_buckeye'}, {'id': 20672, 'synset': 'dwarf_buckeye.n.01', 'name': 'dwarf_buckeye'}, {'id': 20673, 'synset': 'red_buckeye.n.01', 'name': 'red_buckeye'}, {'id': 20674, 'synset': 'particolored_buckeye.n.01', 'name': 'particolored_buckeye'}, {'id': 20675, 'synset': 'ebony.n.03', 'name': 'ebony'}, {'id': 20676, 'synset': 'marblewood.n.02', 'name': 'marblewood'}, {'id': 20677, 'synset': 'marblewood.n.01', 'name': 'marblewood'}, {'id': 20678, 'synset': 'persimmon.n.01', 'name': 'persimmon'}, {'id': 20679, 'synset': 'japanese_persimmon.n.01', 'name': 'Japanese_persimmon'}, {'id': 20680, 'synset': 'american_persimmon.n.01', 'name': 'American_persimmon'}, {'id': 20681, 'synset': 'date_plum.n.01', 'name': 'date_plum'}, {'id': 20682, 'synset': 'buckthorn.n.02', 'name': 'buckthorn'}, {'id': 20683, 'synset': 'southern_buckthorn.n.01', 'name': 'southern_buckthorn'}, {'id': 20684, 'synset': 'false_buckthorn.n.01', 'name': 'false_buckthorn'}, {'id': 20685, 'synset': 'star_apple.n.01', 'name': 'star_apple'}, {'id': 20686, 'synset': 'satinleaf.n.01', 'name': 'satinleaf'}, {'id': 20687, 'synset': 'balata.n.02', 'name': 'balata'}, {'id': 20688, 'synset': 'sapodilla.n.01', 'name': 'sapodilla'}, {'id': 20689, 'synset': 'gutta-percha_tree.n.02', 'name': 'gutta-percha_tree'}, {'id': 20690, 'synset': 'gutta-percha_tree.n.01', 'name': 'gutta-percha_tree'}, {'id': 20691, 'synset': 'canistel.n.01', 'name': 'canistel'}, {'id': 20692, 'synset': 'marmalade_tree.n.01', 'name': 'marmalade_tree'}, {'id': 20693, 'synset': 'sweetleaf.n.01', 'name': 'sweetleaf'}, {'id': 20694, 'synset': 'asiatic_sweetleaf.n.01', 'name': 'Asiatic_sweetleaf'}, {'id': 20695, 'synset': 'styrax.n.01', 'name': 'styrax'}, {'id': 20696, 'synset': 'snowbell.n.01', 'name': 'snowbell'}, {'id': 20697, 'synset': 'japanese_snowbell.n.01', 'name': 'Japanese_snowbell'}, {'id': 20698, 'synset': 'texas_snowbell.n.01', 'name': 'Texas_snowbell'}, {'id': 20699, 'synset': 'silver-bell_tree.n.01', 'name': 'silver-bell_tree'}, {'id': 20700, 'synset': 'carnivorous_plant.n.01', 'name': 'carnivorous_plant'}, {'id': 20701, 'synset': 'pitcher_plant.n.01', 'name': 'pitcher_plant'}, {'id': 20702, 'synset': 'common_pitcher_plant.n.01', 'name': 'common_pitcher_plant'}, {'id': 20703, 'synset': 'hooded_pitcher_plant.n.01', 'name': 'hooded_pitcher_plant'}, {'id': 20704, 'synset': "huntsman's_horn.n.01", 'name': "huntsman's_horn"}, {'id': 20705, 'synset': 'tropical_pitcher_plant.n.01', 'name': 'tropical_pitcher_plant'}, {'id': 20706, 'synset': 'sundew.n.01', 'name': 'sundew'}, {'id': 20707, 'synset': "venus's_flytrap.n.01", 'name': "Venus's_flytrap"}, {'id': 20708, 'synset': 'waterwheel_plant.n.01', 'name': 'waterwheel_plant'}, {'id': 20709, 'synset': 'drosophyllum_lusitanicum.n.01', 'name': 'Drosophyllum_lusitanicum'}, {'id': 20710, 'synset': 'roridula.n.01', 'name': 'roridula'}, {'id': 20711, 'synset': 'australian_pitcher_plant.n.01', 'name': 'Australian_pitcher_plant'}, {'id': 20712, 'synset': 'sedum.n.01', 'name': 'sedum'}, {'id': 20713, 'synset': 'stonecrop.n.01', 'name': 'stonecrop'}, {'id': 20714, 'synset': 'rose-root.n.01', 'name': 'rose-root'}, {'id': 20715, 'synset': 'orpine.n.01', 'name': 'orpine'}, {'id': 20716, 'synset': 'pinwheel.n.01', 'name': 'pinwheel'}, {'id': 20717, 'synset': 'christmas_bush.n.01', 'name': 'Christmas_bush'}, {'id': 20718, 'synset': 'hortensia.n.01', 'name': 'hortensia'}, {'id': 20719, 'synset': 'fall-blooming_hydrangea.n.01', 'name': 'fall-blooming_hydrangea'}, {'id': 20720, 'synset': 'carpenteria.n.01', 'name': 'carpenteria'}, {'id': 20721, 'synset': 'decumary.n.01', 'name': 'decumary'}, {'id': 20722, 'synset': 'deutzia.n.01', 'name': 'deutzia'}, {'id': 20723, 'synset': 'philadelphus.n.01', 'name': 'philadelphus'}, {'id': 20724, 'synset': 'mock_orange.n.01', 'name': 'mock_orange'}, {'id': 20725, 'synset': 'saxifrage.n.01', 'name': 'saxifrage'}, {'id': 20726, 'synset': 'yellow_mountain_saxifrage.n.01', 'name': 'yellow_mountain_saxifrage'}, {'id': 20727, 'synset': 'meadow_saxifrage.n.01', 'name': 'meadow_saxifrage'}, {'id': 20728, 'synset': 'mossy_saxifrage.n.01', 'name': 'mossy_saxifrage'}, {'id': 20729, 'synset': 'western_saxifrage.n.01', 'name': 'western_saxifrage'}, {'id': 20730, 'synset': 'purple_saxifrage.n.01', 'name': 'purple_saxifrage'}, {'id': 20731, 'synset': 'star_saxifrage.n.01', 'name': 'star_saxifrage'}, {'id': 20732, 'synset': 'strawberry_geranium.n.01', 'name': 'strawberry_geranium'}, {'id': 20733, 'synset': 'astilbe.n.01', 'name': 'astilbe'}, {'id': 20734, 'synset': 'false_goatsbeard.n.01', 'name': 'false_goatsbeard'}, {'id': 20735, 'synset': 'dwarf_astilbe.n.01', 'name': 'dwarf_astilbe'}, {'id': 20736, 'synset': 'spirea.n.01', 'name': 'spirea'}, {'id': 20737, 'synset': 'bergenia.n.01', 'name': 'bergenia'}, {'id': 20738, 'synset': 'coast_boykinia.n.01', 'name': 'coast_boykinia'}, {'id': 20739, 'synset': 'golden_saxifrage.n.01', 'name': 'golden_saxifrage'}, {'id': 20740, 'synset': 'umbrella_plant.n.01', 'name': 'umbrella_plant'}, {'id': 20741, 'synset': 'bridal_wreath.n.01', 'name': 'bridal_wreath'}, {'id': 20742, 'synset': 'alumroot.n.01', 'name': 'alumroot'}, {'id': 20743, 'synset': 'coralbells.n.01', 'name': 'coralbells'}, {'id': 20744, 'synset': 'leatherleaf_saxifrage.n.01', 'name': 'leatherleaf_saxifrage'}, {'id': 20745, 'synset': 'woodland_star.n.01', 'name': 'woodland_star'}, {'id': 20746, 'synset': 'prairie_star.n.01', 'name': 'prairie_star'}, {'id': 20747, 'synset': 'miterwort.n.01', 'name': 'miterwort'}, {'id': 20748, 'synset': "five-point_bishop's_cap.n.01", 'name': "five-point_bishop's_cap"}, {'id': 20749, 'synset': 'parnassia.n.01', 'name': 'parnassia'}, {'id': 20750, 'synset': 'bog_star.n.01', 'name': 'bog_star'}, {'id': 20751, 'synset': 'fringed_grass_of_parnassus.n.01', 'name': 'fringed_grass_of_Parnassus'}, {'id': 20752, 'synset': 'false_alumroot.n.01', 'name': 'false_alumroot'}, {'id': 20753, 'synset': 'foamflower.n.01', 'name': 'foamflower'}, {'id': 20754, 'synset': 'false_miterwort.n.01', 'name': 'false_miterwort'}, {'id': 20755, 'synset': 'pickaback_plant.n.01', 'name': 'pickaback_plant'}, {'id': 20756, 'synset': 'currant.n.02', 'name': 'currant'}, {'id': 20757, 'synset': 'black_currant.n.01', 'name': 'black_currant'}, {'id': 20758, 'synset': 'white_currant.n.01', 'name': 'white_currant'}, {'id': 20759, 'synset': 'gooseberry.n.01', 'name': 'gooseberry'}, {'id': 20760, 'synset': 'plane_tree.n.01', 'name': 'plane_tree'}, {'id': 20761, 'synset': 'london_plane.n.01', 'name': 'London_plane'}, {'id': 20762, 'synset': 'american_sycamore.n.01', 'name': 'American_sycamore'}, {'id': 20763, 'synset': 'oriental_plane.n.01', 'name': 'oriental_plane'}, {'id': 20764, 'synset': 'california_sycamore.n.01', 'name': 'California_sycamore'}, {'id': 20765, 'synset': 'arizona_sycamore.n.01', 'name': 'Arizona_sycamore'}, {'id': 20766, 'synset': 'greek_valerian.n.01', 'name': 'Greek_valerian'}, {'id': 20767, 'synset': "northern_jacob's_ladder.n.01", 'name': "northern_Jacob's_ladder"}, {'id': 20768, 'synset': 'skunkweed.n.01', 'name': 'skunkweed'}, {'id': 20769, 'synset': 'phlox.n.01', 'name': 'phlox'}, {'id': 20770, 'synset': 'moss_pink.n.02', 'name': 'moss_pink'}, {'id': 20771, 'synset': 'evening-snow.n.01', 'name': 'evening-snow'}, {'id': 20772, 'synset': 'acanthus.n.01', 'name': 'acanthus'}, {'id': 20773, 'synset': "bear's_breech.n.01", 'name': "bear's_breech"}, {'id': 20774, 'synset': 'caricature_plant.n.01', 'name': 'caricature_plant'}, {'id': 20775, 'synset': 'black-eyed_susan.n.01', 'name': 'black-eyed_Susan'}, {'id': 20776, 'synset': 'catalpa.n.01', 'name': 'catalpa'}, {'id': 20777, 'synset': 'catalpa_bignioides.n.01', 'name': 'Catalpa_bignioides'}, {'id': 20778, 'synset': 'catalpa_speciosa.n.01', 'name': 'Catalpa_speciosa'}, {'id': 20779, 'synset': 'desert_willow.n.01', 'name': 'desert_willow'}, {'id': 20780, 'synset': 'calabash.n.02', 'name': 'calabash'}, {'id': 20781, 'synset': 'calabash.n.01', 'name': 'calabash'}, {'id': 20782, 'synset': 'borage.n.01', 'name': 'borage'}, {'id': 20783, 'synset': 'common_amsinckia.n.01', 'name': 'common_amsinckia'}, {'id': 20784, 'synset': 'anchusa.n.01', 'name': 'anchusa'}, {'id': 20785, 'synset': 'bugloss.n.01', 'name': 'bugloss'}, {'id': 20786, 'synset': 'cape_forget-me-not.n.02', 'name': 'cape_forget-me-not'}, {'id': 20787, 'synset': 'cape_forget-me-not.n.01', 'name': 'cape_forget-me-not'}, {'id': 20788, 'synset': 'spanish_elm.n.02', 'name': 'Spanish_elm'}, {'id': 20789, 'synset': 'princewood.n.01', 'name': 'princewood'}, {'id': 20790, 'synset': 'chinese_forget-me-not.n.01', 'name': 'Chinese_forget-me-not'}, {'id': 20791, 'synset': "hound's-tongue.n.02", 'name': "hound's-tongue"}, {'id': 20792, 'synset': "hound's-tongue.n.01", 'name': "hound's-tongue"}, {'id': 20793, 'synset': 'blueweed.n.01', 'name': 'blueweed'}, {'id': 20794, 'synset': "beggar's_lice.n.01", 'name': "beggar's_lice"}, {'id': 20795, 'synset': 'gromwell.n.01', 'name': 'gromwell'}, {'id': 20796, 'synset': 'puccoon.n.01', 'name': 'puccoon'}, {'id': 20797, 'synset': 'virginia_bluebell.n.01', 'name': 'Virginia_bluebell'}, {'id': 20798, 'synset': 'garden_forget-me-not.n.01', 'name': 'garden_forget-me-not'}, {'id': 20799, 'synset': 'forget-me-not.n.01', 'name': 'forget-me-not'}, {'id': 20800, 'synset': 'false_gromwell.n.01', 'name': 'false_gromwell'}, {'id': 20801, 'synset': 'comfrey.n.01', 'name': 'comfrey'}, {'id': 20802, 'synset': 'common_comfrey.n.01', 'name': 'common_comfrey'}, {'id': 20803, 'synset': 'convolvulus.n.01', 'name': 'convolvulus'}, {'id': 20804, 'synset': 'bindweed.n.01', 'name': 'bindweed'}, {'id': 20805, 'synset': 'field_bindweed.n.01', 'name': 'field_bindweed'}, {'id': 20806, 'synset': 'scammony.n.03', 'name': 'scammony'}, {'id': 20807, 'synset': 'silverweed.n.01', 'name': 'silverweed'}, {'id': 20808, 'synset': 'dodder.n.01', 'name': 'dodder'}, {'id': 20809, 'synset': 'dichondra.n.01', 'name': 'dichondra'}, {'id': 20810, 'synset': 'cypress_vine.n.01', 'name': 'cypress_vine'}, {'id': 20811, 'synset': 'moonflower.n.01', 'name': 'moonflower'}, {'id': 20812, 'synset': 'wild_potato_vine.n.01', 'name': 'wild_potato_vine'}, {'id': 20813, 'synset': 'red_morning-glory.n.01', 'name': 'red_morning-glory'}, {'id': 20814, 'synset': 'man-of-the-earth.n.01', 'name': 'man-of-the-earth'}, {'id': 20815, 'synset': 'scammony.n.01', 'name': 'scammony'}, {'id': 20816, 'synset': 'japanese_morning_glory.n.01', 'name': 'Japanese_morning_glory'}, {'id': 20817, 'synset': 'imperial_japanese_morning_glory.n.01', 'name': 'imperial_Japanese_morning_glory'}, {'id': 20818, 'synset': 'gesneriad.n.01', 'name': 'gesneriad'}, {'id': 20819, 'synset': 'gesneria.n.01', 'name': 'gesneria'}, {'id': 20820, 'synset': 'achimenes.n.01', 'name': 'achimenes'}, {'id': 20821, 'synset': 'aeschynanthus.n.01', 'name': 'aeschynanthus'}, {'id': 20822, 'synset': 'lace-flower_vine.n.01', 'name': 'lace-flower_vine'}, {'id': 20823, 'synset': 'columnea.n.01', 'name': 'columnea'}, {'id': 20824, 'synset': 'episcia.n.01', 'name': 'episcia'}, {'id': 20825, 'synset': 'gloxinia.n.01', 'name': 'gloxinia'}, {'id': 20826, 'synset': 'canterbury_bell.n.01', 'name': 'Canterbury_bell'}, {'id': 20827, 'synset': 'kohleria.n.01', 'name': 'kohleria'}, {'id': 20828, 'synset': 'african_violet.n.01', 'name': 'African_violet'}, {'id': 20829, 'synset': 'streptocarpus.n.01', 'name': 'streptocarpus'}, {'id': 20830, 'synset': 'cape_primrose.n.01', 'name': 'Cape_primrose'}, {'id': 20831, 'synset': 'waterleaf.n.01', 'name': 'waterleaf'}, {'id': 20832, 'synset': 'virginia_waterleaf.n.01', 'name': 'Virginia_waterleaf'}, {'id': 20833, 'synset': 'yellow_bells.n.01', 'name': 'yellow_bells'}, {'id': 20834, 'synset': 'yerba_santa.n.01', 'name': 'yerba_santa'}, {'id': 20835, 'synset': 'nemophila.n.01', 'name': 'nemophila'}, {'id': 20836, 'synset': 'baby_blue-eyes.n.01', 'name': 'baby_blue-eyes'}, {'id': 20837, 'synset': 'five-spot.n.02', 'name': 'five-spot'}, {'id': 20838, 'synset': 'scorpionweed.n.01', 'name': 'scorpionweed'}, {'id': 20839, 'synset': 'california_bluebell.n.02', 'name': 'California_bluebell'}, {'id': 20840, 'synset': 'california_bluebell.n.01', 'name': 'California_bluebell'}, {'id': 20841, 'synset': 'fiddleneck.n.01', 'name': 'fiddleneck'}, {'id': 20842, 'synset': 'fiesta_flower.n.01', 'name': 'fiesta_flower'}, {'id': 20843, 'synset': 'basil_thyme.n.01', 'name': 'basil_thyme'}, {'id': 20844, 'synset': 'giant_hyssop.n.01', 'name': 'giant_hyssop'}, {'id': 20845, 'synset': 'yellow_giant_hyssop.n.01', 'name': 'yellow_giant_hyssop'}, {'id': 20846, 'synset': 'anise_hyssop.n.01', 'name': 'anise_hyssop'}, {'id': 20847, 'synset': 'mexican_hyssop.n.01', 'name': 'Mexican_hyssop'}, {'id': 20848, 'synset': 'bugle.n.02', 'name': 'bugle'}, {'id': 20849, 'synset': 'creeping_bugle.n.01', 'name': 'creeping_bugle'}, {'id': 20850, 'synset': 'erect_bugle.n.01', 'name': 'erect_bugle'}, {'id': 20851, 'synset': 'pyramid_bugle.n.01', 'name': 'pyramid_bugle'}, {'id': 20852, 'synset': 'wood_mint.n.01', 'name': 'wood_mint'}, {'id': 20853, 'synset': 'hairy_wood_mint.n.01', 'name': 'hairy_wood_mint'}, {'id': 20854, 'synset': 'downy_wood_mint.n.01', 'name': 'downy_wood_mint'}, {'id': 20855, 'synset': 'calamint.n.01', 'name': 'calamint'}, {'id': 20856, 'synset': 'common_calamint.n.01', 'name': 'common_calamint'}, {'id': 20857, 'synset': 'large-flowered_calamint.n.01', 'name': 'large-flowered_calamint'}, {'id': 20858, 'synset': 'lesser_calamint.n.01', 'name': 'lesser_calamint'}, {'id': 20859, 'synset': 'wild_basil.n.01', 'name': 'wild_basil'}, {'id': 20860, 'synset': 'horse_balm.n.01', 'name': 'horse_balm'}, {'id': 20861, 'synset': 'coleus.n.01', 'name': 'coleus'}, {'id': 20862, 'synset': 'country_borage.n.01', 'name': 'country_borage'}, {'id': 20863, 'synset': 'painted_nettle.n.01', 'name': 'painted_nettle'}, {'id': 20864, 'synset': 'apalachicola_rosemary.n.01', 'name': 'Apalachicola_rosemary'}, {'id': 20865, 'synset': 'dragonhead.n.01', 'name': 'dragonhead'}, {'id': 20866, 'synset': 'elsholtzia.n.01', 'name': 'elsholtzia'}, {'id': 20867, 'synset': 'hemp_nettle.n.01', 'name': 'hemp_nettle'}, {'id': 20868, 'synset': 'ground_ivy.n.01', 'name': 'ground_ivy'}, {'id': 20869, 'synset': 'pennyroyal.n.02', 'name': 'pennyroyal'}, {'id': 20870, 'synset': 'hyssop.n.01', 'name': 'hyssop'}, {'id': 20871, 'synset': 'dead_nettle.n.02', 'name': 'dead_nettle'}, {'id': 20872, 'synset': 'white_dead_nettle.n.01', 'name': 'white_dead_nettle'}, {'id': 20873, 'synset': 'henbit.n.01', 'name': 'henbit'}, {'id': 20874, 'synset': 'english_lavender.n.01', 'name': 'English_lavender'}, {'id': 20875, 'synset': 'french_lavender.n.02', 'name': 'French_lavender'}, {'id': 20876, 'synset': 'spike_lavender.n.01', 'name': 'spike_lavender'}, {'id': 20877, 'synset': 'dagga.n.01', 'name': 'dagga'}, {'id': 20878, 'synset': "lion's-ear.n.01", 'name': "lion's-ear"}, {'id': 20879, 'synset': 'motherwort.n.01', 'name': 'motherwort'}, {'id': 20880, 'synset': 'pitcher_sage.n.02', 'name': 'pitcher_sage'}, {'id': 20881, 'synset': 'bugleweed.n.01', 'name': 'bugleweed'}, {'id': 20882, 'synset': 'water_horehound.n.01', 'name': 'water_horehound'}, {'id': 20883, 'synset': 'gipsywort.n.01', 'name': 'gipsywort'}, {'id': 20884, 'synset': 'origanum.n.01', 'name': 'origanum'}, {'id': 20885, 'synset': 'oregano.n.01', 'name': 'oregano'}, {'id': 20886, 'synset': 'sweet_marjoram.n.01', 'name': 'sweet_marjoram'}, {'id': 20887, 'synset': 'horehound.n.01', 'name': 'horehound'}, {'id': 20888, 'synset': 'common_horehound.n.01', 'name': 'common_horehound'}, {'id': 20889, 'synset': 'lemon_balm.n.01', 'name': 'lemon_balm'}, {'id': 20890, 'synset': 'corn_mint.n.01', 'name': 'corn_mint'}, {'id': 20891, 'synset': 'water-mint.n.01', 'name': 'water-mint'}, {'id': 20892, 'synset': 'bergamot_mint.n.02', 'name': 'bergamot_mint'}, {'id': 20893, 'synset': 'horsemint.n.03', 'name': 'horsemint'}, {'id': 20894, 'synset': 'peppermint.n.01', 'name': 'peppermint'}, {'id': 20895, 'synset': 'spearmint.n.01', 'name': 'spearmint'}, {'id': 20896, 'synset': 'apple_mint.n.01', 'name': 'apple_mint'}, {'id': 20897, 'synset': 'pennyroyal.n.01', 'name': 'pennyroyal'}, {'id': 20898, 'synset': 'yerba_buena.n.01', 'name': 'yerba_buena'}, {'id': 20899, 'synset': 'molucca_balm.n.01', 'name': 'molucca_balm'}, {'id': 20900, 'synset': 'monarda.n.01', 'name': 'monarda'}, {'id': 20901, 'synset': 'bee_balm.n.02', 'name': 'bee_balm'}, {'id': 20902, 'synset': 'horsemint.n.02', 'name': 'horsemint'}, {'id': 20903, 'synset': 'bee_balm.n.01', 'name': 'bee_balm'}, {'id': 20904, 'synset': 'lemon_mint.n.01', 'name': 'lemon_mint'}, {'id': 20905, 'synset': 'plains_lemon_monarda.n.01', 'name': 'plains_lemon_monarda'}, {'id': 20906, 'synset': 'basil_balm.n.01', 'name': 'basil_balm'}, {'id': 20907, 'synset': 'mustang_mint.n.01', 'name': 'mustang_mint'}, {'id': 20908, 'synset': 'catmint.n.01', 'name': 'catmint'}, {'id': 20909, 'synset': 'basil.n.01', 'name': 'basil'}, {'id': 20910, 'synset': 'beefsteak_plant.n.01', 'name': 'beefsteak_plant'}, {'id': 20911, 'synset': 'phlomis.n.01', 'name': 'phlomis'}, {'id': 20912, 'synset': 'jerusalem_sage.n.01', 'name': 'Jerusalem_sage'}, {'id': 20913, 'synset': 'physostegia.n.01', 'name': 'physostegia'}, {'id': 20914, 'synset': 'plectranthus.n.01', 'name': 'plectranthus'}, {'id': 20915, 'synset': 'patchouli.n.01', 'name': 'patchouli'}, {'id': 20916, 'synset': 'self-heal.n.01', 'name': 'self-heal'}, {'id': 20917, 'synset': 'mountain_mint.n.01', 'name': 'mountain_mint'}, {'id': 20918, 'synset': 'rosemary.n.01', 'name': 'rosemary'}, {'id': 20919, 'synset': 'clary_sage.n.01', 'name': 'clary_sage'}, {'id': 20920, 'synset': 'purple_sage.n.01', 'name': 'purple_sage'}, {'id': 20921, 'synset': 'cancerweed.n.01', 'name': 'cancerweed'}, {'id': 20922, 'synset': 'common_sage.n.01', 'name': 'common_sage'}, {'id': 20923, 'synset': 'meadow_clary.n.01', 'name': 'meadow_clary'}, {'id': 20924, 'synset': 'clary.n.01', 'name': 'clary'}, {'id': 20925, 'synset': 'pitcher_sage.n.01', 'name': 'pitcher_sage'}, {'id': 20926, 'synset': 'mexican_mint.n.01', 'name': 'Mexican_mint'}, {'id': 20927, 'synset': 'wild_sage.n.01', 'name': 'wild_sage'}, {'id': 20928, 'synset': 'savory.n.01', 'name': 'savory'}, {'id': 20929, 'synset': 'summer_savory.n.01', 'name': 'summer_savory'}, {'id': 20930, 'synset': 'winter_savory.n.01', 'name': 'winter_savory'}, {'id': 20931, 'synset': 'skullcap.n.02', 'name': 'skullcap'}, {'id': 20932, 'synset': 'blue_pimpernel.n.01', 'name': 'blue_pimpernel'}, {'id': 20933, 'synset': 'hedge_nettle.n.02', 'name': 'hedge_nettle'}, {'id': 20934, 'synset': 'hedge_nettle.n.01', 'name': 'hedge_nettle'}, {'id': 20935, 'synset': 'germander.n.01', 'name': 'germander'}, {'id': 20936, 'synset': 'american_germander.n.01', 'name': 'American_germander'}, {'id': 20937, 'synset': 'cat_thyme.n.01', 'name': 'cat_thyme'}, {'id': 20938, 'synset': 'wood_sage.n.01', 'name': 'wood_sage'}, {'id': 20939, 'synset': 'thyme.n.01', 'name': 'thyme'}, {'id': 20940, 'synset': 'common_thyme.n.01', 'name': 'common_thyme'}, {'id': 20941, 'synset': 'wild_thyme.n.01', 'name': 'wild_thyme'}, {'id': 20942, 'synset': 'blue_curls.n.01', 'name': 'blue_curls'}, {'id': 20943, 'synset': 'turpentine_camphor_weed.n.01', 'name': 'turpentine_camphor_weed'}, {'id': 20944, 'synset': 'bastard_pennyroyal.n.01', 'name': 'bastard_pennyroyal'}, {'id': 20945, 'synset': 'bladderwort.n.01', 'name': 'bladderwort'}, {'id': 20946, 'synset': 'butterwort.n.01', 'name': 'butterwort'}, {'id': 20947, 'synset': 'genlisea.n.01', 'name': 'genlisea'}, {'id': 20948, 'synset': 'martynia.n.01', 'name': 'martynia'}, {'id': 20949, 'synset': 'common_unicorn_plant.n.01', 'name': 'common_unicorn_plant'}, {'id': 20950, 'synset': "sand_devil's_claw.n.01", 'name': "sand_devil's_claw"}, {'id': 20951, 'synset': 'sweet_unicorn_plant.n.01', 'name': 'sweet_unicorn_plant'}, {'id': 20952, 'synset': 'figwort.n.01', 'name': 'figwort'}, {'id': 20953, 'synset': 'snapdragon.n.01', 'name': 'snapdragon'}, {'id': 20954, 'synset': 'white_snapdragon.n.01', 'name': 'white_snapdragon'}, {'id': 20955, 'synset': 'yellow_twining_snapdragon.n.01', 'name': 'yellow_twining_snapdragon'}, {'id': 20956, 'synset': 'mediterranean_snapdragon.n.01', 'name': 'Mediterranean_snapdragon'}, {'id': 20957, 'synset': 'kitten-tails.n.01', 'name': 'kitten-tails'}, {'id': 20958, 'synset': 'alpine_besseya.n.01', 'name': 'Alpine_besseya'}, {'id': 20959, 'synset': 'false_foxglove.n.02', 'name': 'false_foxglove'}, {'id': 20960, 'synset': 'false_foxglove.n.01', 'name': 'false_foxglove'}, {'id': 20961, 'synset': 'calceolaria.n.01', 'name': 'calceolaria'}, {'id': 20962, 'synset': 'indian_paintbrush.n.02', 'name': 'Indian_paintbrush'}, {'id': 20963, 'synset': 'desert_paintbrush.n.01', 'name': 'desert_paintbrush'}, {'id': 20964, 'synset': 'giant_red_paintbrush.n.01', 'name': 'giant_red_paintbrush'}, {'id': 20965, 'synset': 'great_plains_paintbrush.n.01', 'name': 'great_plains_paintbrush'}, {'id': 20966, 'synset': 'sulfur_paintbrush.n.01', 'name': 'sulfur_paintbrush'}, {'id': 20967, 'synset': 'shellflower.n.01', 'name': 'shellflower'}, {'id': 20968, 'synset': 'maiden_blue-eyed_mary.n.01', 'name': 'maiden_blue-eyed_Mary'}, {'id': 20969, 'synset': 'blue-eyed_mary.n.01', 'name': 'blue-eyed_Mary'}, {'id': 20970, 'synset': 'foxglove.n.01', 'name': 'foxglove'}, {'id': 20971, 'synset': 'common_foxglove.n.01', 'name': 'common_foxglove'}, {'id': 20972, 'synset': 'yellow_foxglove.n.01', 'name': 'yellow_foxglove'}, {'id': 20973, 'synset': 'gerardia.n.01', 'name': 'gerardia'}, {'id': 20974, 'synset': 'blue_toadflax.n.01', 'name': 'blue_toadflax'}, {'id': 20975, 'synset': 'toadflax.n.01', 'name': 'toadflax'}, {'id': 20976, 'synset': 'golden-beard_penstemon.n.01', 'name': 'golden-beard_penstemon'}, {'id': 20977, 'synset': 'scarlet_bugler.n.01', 'name': 'scarlet_bugler'}, {'id': 20978, 'synset': 'red_shrubby_penstemon.n.01', 'name': 'red_shrubby_penstemon'}, {'id': 20979, 'synset': 'platte_river_penstemon.n.01', 'name': 'Platte_River_penstemon'}, {'id': 20980, 'synset': 'hot-rock_penstemon.n.01', 'name': 'hot-rock_penstemon'}, {'id': 20981, 'synset': "jones'_penstemon.n.01", 'name': "Jones'_penstemon"}, {'id': 20982, 'synset': 'shrubby_penstemon.n.01', 'name': 'shrubby_penstemon'}, {'id': 20983, 'synset': 'narrow-leaf_penstemon.n.01', 'name': 'narrow-leaf_penstemon'}, {'id': 20984, 'synset': 'balloon_flower.n.01', 'name': 'balloon_flower'}, {'id': 20985, 'synset': "parry's_penstemon.n.01", 'name': "Parry's_penstemon"}, {'id': 20986, 'synset': 'rock_penstemon.n.01', 'name': 'rock_penstemon'}, {'id': 20987, 'synset': "rydberg's_penstemon.n.01", 'name': "Rydberg's_penstemon"}, {'id': 20988, 'synset': 'cascade_penstemon.n.01', 'name': 'cascade_penstemon'}, {'id': 20989, 'synset': "whipple's_penstemon.n.01", 'name': "Whipple's_penstemon"}, {'id': 20990, 'synset': 'moth_mullein.n.01', 'name': 'moth_mullein'}, {'id': 20991, 'synset': 'white_mullein.n.01', 'name': 'white_mullein'}, {'id': 20992, 'synset': 'purple_mullein.n.01', 'name': 'purple_mullein'}, {'id': 20993, 'synset': 'common_mullein.n.01', 'name': 'common_mullein'}, {'id': 20994, 'synset': 'veronica.n.01', 'name': 'veronica'}, {'id': 20995, 'synset': 'field_speedwell.n.01', 'name': 'field_speedwell'}, {'id': 20996, 'synset': 'brooklime.n.02', 'name': 'brooklime'}, {'id': 20997, 'synset': 'corn_speedwell.n.01', 'name': 'corn_speedwell'}, {'id': 20998, 'synset': 'brooklime.n.01', 'name': 'brooklime'}, {'id': 20999, 'synset': 'germander_speedwell.n.01', 'name': 'germander_speedwell'}, {'id': 21000, 'synset': 'water_speedwell.n.01', 'name': 'water_speedwell'}, {'id': 21001, 'synset': 'common_speedwell.n.01', 'name': 'common_speedwell'}, {'id': 21002, 'synset': 'purslane_speedwell.n.01', 'name': 'purslane_speedwell'}, {'id': 21003, 'synset': 'thyme-leaved_speedwell.n.01', 'name': 'thyme-leaved_speedwell'}, {'id': 21004, 'synset': 'nightshade.n.01', 'name': 'nightshade'}, {'id': 21005, 'synset': 'horse_nettle.n.01', 'name': 'horse_nettle'}, {'id': 21006, 'synset': 'african_holly.n.01', 'name': 'African_holly'}, {'id': 21007, 'synset': 'potato_vine.n.02', 'name': 'potato_vine'}, {'id': 21008, 'synset': 'garden_huckleberry.n.01', 'name': 'garden_huckleberry'}, {'id': 21009, 'synset': 'naranjilla.n.01', 'name': 'naranjilla'}, {'id': 21010, 'synset': 'potato_vine.n.01', 'name': 'potato_vine'}, {'id': 21011, 'synset': 'potato_tree.n.01', 'name': 'potato_tree'}, {'id': 21012, 'synset': 'belladonna.n.01', 'name': 'belladonna'}, {'id': 21013, 'synset': 'bush_violet.n.01', 'name': 'bush_violet'}, {'id': 21014, 'synset': 'lady-of-the-night.n.01', 'name': 'lady-of-the-night'}, {'id': 21015, 'synset': "angel's_trumpet.n.02", 'name': "angel's_trumpet"}, {'id': 21016, 'synset': "angel's_trumpet.n.01", 'name': "angel's_trumpet"}, {'id': 21017, 'synset': "red_angel's_trumpet.n.01", 'name': "red_angel's_trumpet"}, {'id': 21018, 'synset': 'cone_pepper.n.01', 'name': 'cone_pepper'}, {'id': 21019, 'synset': 'bird_pepper.n.01', 'name': 'bird_pepper'}, {'id': 21020, 'synset': 'day_jessamine.n.01', 'name': 'day_jessamine'}, {'id': 21021, 'synset': 'night_jasmine.n.01', 'name': 'night_jasmine'}, {'id': 21022, 'synset': 'tree_tomato.n.01', 'name': 'tree_tomato'}, {'id': 21023, 'synset': 'thorn_apple.n.01', 'name': 'thorn_apple'}, {'id': 21024, 'synset': 'jimsonweed.n.01', 'name': 'jimsonweed'}, {'id': 21025, 'synset': 'pichi.n.01', 'name': 'pichi'}, {'id': 21026, 'synset': 'henbane.n.01', 'name': 'henbane'}, {'id': 21027, 'synset': 'egyptian_henbane.n.01', 'name': 'Egyptian_henbane'}, {'id': 21028, 'synset': 'matrimony_vine.n.01', 'name': 'matrimony_vine'}, {'id': 21029, 'synset': 'common_matrimony_vine.n.01', 'name': 'common_matrimony_vine'}, {'id': 21030, 'synset': 'christmasberry.n.01', 'name': 'Christmasberry'}, {'id': 21031, 'synset': 'plum_tomato.n.01', 'name': 'plum_tomato'}, {'id': 21032, 'synset': 'mandrake.n.02', 'name': 'mandrake'}, {'id': 21033, 'synset': 'mandrake_root.n.01', 'name': 'mandrake_root'}, {'id': 21034, 'synset': 'apple_of_peru.n.01', 'name': 'apple_of_Peru'}, {'id': 21035, 'synset': 'flowering_tobacco.n.01', 'name': 'flowering_tobacco'}, {'id': 21036, 'synset': 'common_tobacco.n.01', 'name': 'common_tobacco'}, {'id': 21037, 'synset': 'wild_tobacco.n.01', 'name': 'wild_tobacco'}, {'id': 21038, 'synset': 'cupflower.n.02', 'name': 'cupflower'}, {'id': 21039, 'synset': 'whitecup.n.01', 'name': 'whitecup'}, {'id': 21040, 'synset': 'petunia.n.01', 'name': 'petunia'}, {'id': 21041, 'synset': 'large_white_petunia.n.01', 'name': 'large_white_petunia'}, {'id': 21042, 'synset': 'violet-flowered_petunia.n.01', 'name': 'violet-flowered_petunia'}, {'id': 21043, 'synset': 'hybrid_petunia.n.01', 'name': 'hybrid_petunia'}, {'id': 21044, 'synset': 'cape_gooseberry.n.01', 'name': 'cape_gooseberry'}, {'id': 21045, 'synset': 'strawberry_tomato.n.01', 'name': 'strawberry_tomato'}, {'id': 21046, 'synset': 'tomatillo.n.02', 'name': 'tomatillo'}, {'id': 21047, 'synset': 'tomatillo.n.01', 'name': 'tomatillo'}, {'id': 21048, 'synset': 'yellow_henbane.n.01', 'name': 'yellow_henbane'}, {'id': 21049, 'synset': "cock's_eggs.n.01", 'name': "cock's_eggs"}, {'id': 21050, 'synset': 'salpiglossis.n.01', 'name': 'salpiglossis'}, {'id': 21051, 'synset': 'painted_tongue.n.01', 'name': 'painted_tongue'}, {'id': 21052, 'synset': 'butterfly_flower.n.01', 'name': 'butterfly_flower'}, {'id': 21053, 'synset': 'scopolia_carniolica.n.01', 'name': 'Scopolia_carniolica'}, {'id': 21054, 'synset': 'chalice_vine.n.01', 'name': 'chalice_vine'}, {'id': 21055, 'synset': 'verbena.n.01', 'name': 'verbena'}, {'id': 21056, 'synset': 'lantana.n.01', 'name': 'lantana'}, {'id': 21057, 'synset': 'black_mangrove.n.02', 'name': 'black_mangrove'}, {'id': 21058, 'synset': 'white_mangrove.n.01', 'name': 'white_mangrove'}, {'id': 21059, 'synset': 'black_mangrove.n.01', 'name': 'black_mangrove'}, {'id': 21060, 'synset': 'teak.n.02', 'name': 'teak'}, {'id': 21061, 'synset': 'spurge.n.01', 'name': 'spurge'}, {'id': 21062, 'synset': 'sun_spurge.n.01', 'name': 'sun_spurge'}, {'id': 21063, 'synset': 'petty_spurge.n.01', 'name': 'petty_spurge'}, {'id': 21064, 'synset': "medusa's_head.n.01", 'name': "medusa's_head"}, {'id': 21065, 'synset': 'wild_spurge.n.01', 'name': 'wild_spurge'}, {'id': 21066, 'synset': 'snow-on-the-mountain.n.01', 'name': 'snow-on-the-mountain'}, {'id': 21067, 'synset': 'cypress_spurge.n.01', 'name': 'cypress_spurge'}, {'id': 21068, 'synset': 'leafy_spurge.n.01', 'name': 'leafy_spurge'}, {'id': 21069, 'synset': 'hairy_spurge.n.01', 'name': 'hairy_spurge'}, {'id': 21070, 'synset': 'poinsettia.n.01', 'name': 'poinsettia'}, {'id': 21071, 'synset': 'japanese_poinsettia.n.01', 'name': 'Japanese_poinsettia'}, {'id': 21072, 'synset': 'fire-on-the-mountain.n.01', 'name': 'fire-on-the-mountain'}, {'id': 21073, 'synset': 'wood_spurge.n.01', 'name': 'wood_spurge'}, {'id': 21074, 'synset': 'dwarf_spurge.n.01', 'name': 'dwarf_spurge'}, {'id': 21075, 'synset': 'scarlet_plume.n.01', 'name': 'scarlet_plume'}, {'id': 21076, 'synset': 'naboom.n.01', 'name': 'naboom'}, {'id': 21077, 'synset': 'crown_of_thorns.n.02', 'name': 'crown_of_thorns'}, {'id': 21078, 'synset': 'toothed_spurge.n.01', 'name': 'toothed_spurge'}, {'id': 21079, 'synset': 'three-seeded_mercury.n.01', 'name': 'three-seeded_mercury'}, {'id': 21080, 'synset': 'croton.n.02', 'name': 'croton'}, {'id': 21081, 'synset': 'cascarilla.n.01', 'name': 'cascarilla'}, {'id': 21082, 'synset': 'cascarilla_bark.n.01', 'name': 'cascarilla_bark'}, {'id': 21083, 'synset': 'castor-oil_plant.n.01', 'name': 'castor-oil_plant'}, {'id': 21084, 'synset': 'spurge_nettle.n.01', 'name': 'spurge_nettle'}, {'id': 21085, 'synset': 'physic_nut.n.01', 'name': 'physic_nut'}, {'id': 21086, 'synset': 'para_rubber_tree.n.01', 'name': 'Para_rubber_tree'}, {'id': 21087, 'synset': 'cassava.n.03', 'name': 'cassava'}, {'id': 21088, 'synset': 'bitter_cassava.n.01', 'name': 'bitter_cassava'}, {'id': 21089, 'synset': 'cassava.n.02', 'name': 'cassava'}, {'id': 21090, 'synset': 'sweet_cassava.n.01', 'name': 'sweet_cassava'}, {'id': 21091, 'synset': 'candlenut.n.01', 'name': 'candlenut'}, {'id': 21092, 'synset': 'tung_tree.n.01', 'name': 'tung_tree'}, {'id': 21093, 'synset': 'slipper_spurge.n.01', 'name': 'slipper_spurge'}, {'id': 21094, 'synset': 'candelilla.n.01', 'name': 'candelilla'}, {'id': 21095, 'synset': 'jewbush.n.01', 'name': 'Jewbush'}, {'id': 21096, 'synset': 'jumping_bean.n.01', 'name': 'jumping_bean'}, {'id': 21097, 'synset': 'camellia.n.01', 'name': 'camellia'}, {'id': 21098, 'synset': 'japonica.n.01', 'name': 'japonica'}, {'id': 21099, 'synset': 'umbellifer.n.01', 'name': 'umbellifer'}, {'id': 21100, 'synset': 'wild_parsley.n.01', 'name': 'wild_parsley'}, {'id': 21101, 'synset': "fool's_parsley.n.01", 'name': "fool's_parsley"}, {'id': 21102, 'synset': 'dill.n.01', 'name': 'dill'}, {'id': 21103, 'synset': 'angelica.n.01', 'name': 'angelica'}, {'id': 21104, 'synset': 'garden_angelica.n.01', 'name': 'garden_angelica'}, {'id': 21105, 'synset': 'wild_angelica.n.01', 'name': 'wild_angelica'}, {'id': 21106, 'synset': 'chervil.n.01', 'name': 'chervil'}, {'id': 21107, 'synset': 'cow_parsley.n.01', 'name': 'cow_parsley'}, {'id': 21108, 'synset': 'wild_celery.n.01', 'name': 'wild_celery'}, {'id': 21109, 'synset': 'astrantia.n.01', 'name': 'astrantia'}, {'id': 21110, 'synset': 'greater_masterwort.n.01', 'name': 'greater_masterwort'}, {'id': 21111, 'synset': 'caraway.n.01', 'name': 'caraway'}, {'id': 21112, 'synset': 'whorled_caraway.n.01', 'name': 'whorled_caraway'}, {'id': 21113, 'synset': 'water_hemlock.n.01', 'name': 'water_hemlock'}, {'id': 21114, 'synset': 'spotted_cowbane.n.01', 'name': 'spotted_cowbane'}, {'id': 21115, 'synset': 'hemlock.n.02', 'name': 'hemlock'}, {'id': 21116, 'synset': 'earthnut.n.02', 'name': 'earthnut'}, {'id': 21117, 'synset': 'cumin.n.01', 'name': 'cumin'}, {'id': 21118, 'synset': 'wild_carrot.n.01', 'name': 'wild_carrot'}, {'id': 21119, 'synset': 'eryngo.n.01', 'name': 'eryngo'}, {'id': 21120, 'synset': 'sea_holly.n.01', 'name': 'sea_holly'}, {'id': 21121, 'synset': 'button_snakeroot.n.02', 'name': 'button_snakeroot'}, {'id': 21122, 'synset': 'rattlesnake_master.n.01', 'name': 'rattlesnake_master'}, {'id': 21123, 'synset': 'fennel.n.01', 'name': 'fennel'}, {'id': 21124, 'synset': 'common_fennel.n.01', 'name': 'common_fennel'}, {'id': 21125, 'synset': 'florence_fennel.n.01', 'name': 'Florence_fennel'}, {'id': 21126, 'synset': 'cow_parsnip.n.01', 'name': 'cow_parsnip'}, {'id': 21127, 'synset': 'lovage.n.01', 'name': 'lovage'}, {'id': 21128, 'synset': 'sweet_cicely.n.01', 'name': 'sweet_cicely'}, {'id': 21129, 'synset': 'water_fennel.n.01', 'name': 'water_fennel'}, {'id': 21130, 'synset': 'parsnip.n.02', 'name': 'parsnip'}, {'id': 21131, 'synset': 'cultivated_parsnip.n.01', 'name': 'cultivated_parsnip'}, {'id': 21132, 'synset': 'wild_parsnip.n.01', 'name': 'wild_parsnip'}, {'id': 21133, 'synset': 'parsley.n.01', 'name': 'parsley'}, {'id': 21134, 'synset': 'italian_parsley.n.01', 'name': 'Italian_parsley'}, {'id': 21135, 'synset': 'hamburg_parsley.n.01', 'name': 'Hamburg_parsley'}, {'id': 21136, 'synset': 'anise.n.01', 'name': 'anise'}, {'id': 21137, 'synset': 'sanicle.n.01', 'name': 'sanicle'}, {'id': 21138, 'synset': 'purple_sanicle.n.01', 'name': 'purple_sanicle'}, {'id': 21139, 'synset': 'european_sanicle.n.01', 'name': 'European_sanicle'}, {'id': 21140, 'synset': 'water_parsnip.n.01', 'name': 'water_parsnip'}, {'id': 21141, 'synset': 'greater_water_parsnip.n.01', 'name': 'greater_water_parsnip'}, {'id': 21142, 'synset': 'skirret.n.01', 'name': 'skirret'}, {'id': 21143, 'synset': 'dogwood.n.01', 'name': 'dogwood'}, {'id': 21144, 'synset': 'common_white_dogwood.n.01', 'name': 'common_white_dogwood'}, {'id': 21145, 'synset': 'red_osier.n.01', 'name': 'red_osier'}, {'id': 21146, 'synset': 'silky_dogwood.n.02', 'name': 'silky_dogwood'}, {'id': 21147, 'synset': 'silky_cornel.n.01', 'name': 'silky_cornel'}, {'id': 21148, 'synset': 'common_european_dogwood.n.01', 'name': 'common_European_dogwood'}, {'id': 21149, 'synset': 'bunchberry.n.01', 'name': 'bunchberry'}, {'id': 21150, 'synset': 'cornelian_cherry.n.01', 'name': 'cornelian_cherry'}, {'id': 21151, 'synset': 'puka.n.01', 'name': 'puka'}, {'id': 21152, 'synset': 'kapuka.n.01', 'name': 'kapuka'}, {'id': 21153, 'synset': 'valerian.n.01', 'name': 'valerian'}, {'id': 21154, 'synset': 'common_valerian.n.01', 'name': 'common_valerian'}, {'id': 21155, 'synset': 'common_corn_salad.n.01', 'name': 'common_corn_salad'}, {'id': 21156, 'synset': 'red_valerian.n.01', 'name': 'red_valerian'}, {'id': 21157, 'synset': 'filmy_fern.n.02', 'name': 'filmy_fern'}, {'id': 21158, 'synset': 'bristle_fern.n.01', 'name': 'bristle_fern'}, {'id': 21159, 'synset': "hare's-foot_bristle_fern.n.01", 'name': "hare's-foot_bristle_fern"}, {'id': 21160, 'synset': 'killarney_fern.n.01', 'name': 'Killarney_fern'}, {'id': 21161, 'synset': 'kidney_fern.n.01', 'name': 'kidney_fern'}, {'id': 21162, 'synset': 'flowering_fern.n.02', 'name': 'flowering_fern'}, {'id': 21163, 'synset': 'royal_fern.n.01', 'name': 'royal_fern'}, {'id': 21164, 'synset': 'interrupted_fern.n.01', 'name': 'interrupted_fern'}, {'id': 21165, 'synset': 'crape_fern.n.01', 'name': 'crape_fern'}, {'id': 21166, 'synset': 'crepe_fern.n.01', 'name': 'crepe_fern'}, {'id': 21167, 'synset': 'curly_grass.n.01', 'name': 'curly_grass'}, {'id': 21168, 'synset': 'pine_fern.n.01', 'name': 'pine_fern'}, {'id': 21169, 'synset': 'climbing_fern.n.01', 'name': 'climbing_fern'}, {'id': 21170, 'synset': 'creeping_fern.n.01', 'name': 'creeping_fern'}, {'id': 21171, 'synset': 'climbing_maidenhair.n.01', 'name': 'climbing_maidenhair'}, {'id': 21172, 'synset': 'scented_fern.n.02', 'name': 'scented_fern'}, {'id': 21173, 'synset': 'clover_fern.n.01', 'name': 'clover_fern'}, {'id': 21174, 'synset': 'nardoo.n.01', 'name': 'nardoo'}, {'id': 21175, 'synset': 'water_clover.n.01', 'name': 'water_clover'}, {'id': 21176, 'synset': 'pillwort.n.01', 'name': 'pillwort'}, {'id': 21177, 'synset': 'regnellidium.n.01', 'name': 'regnellidium'}, {'id': 21178, 'synset': 'floating-moss.n.01', 'name': 'floating-moss'}, {'id': 21179, 'synset': 'mosquito_fern.n.01', 'name': 'mosquito_fern'}, {'id': 21180, 'synset': "adder's_tongue.n.01", 'name': "adder's_tongue"}, {'id': 21181, 'synset': 'ribbon_fern.n.03', 'name': 'ribbon_fern'}, {'id': 21182, 'synset': 'grape_fern.n.01', 'name': 'grape_fern'}, {'id': 21183, 'synset': 'daisyleaf_grape_fern.n.01', 'name': 'daisyleaf_grape_fern'}, {'id': 21184, 'synset': 'leathery_grape_fern.n.01', 'name': 'leathery_grape_fern'}, {'id': 21185, 'synset': 'rattlesnake_fern.n.01', 'name': 'rattlesnake_fern'}, {'id': 21186, 'synset': 'flowering_fern.n.01', 'name': 'flowering_fern'}, {'id': 21187, 'synset': 'powdery_mildew.n.01', 'name': 'powdery_mildew'}, {'id': 21188, 'synset': 'dutch_elm_fungus.n.01', 'name': 'Dutch_elm_fungus'}, {'id': 21189, 'synset': 'ergot.n.02', 'name': 'ergot'}, {'id': 21190, 'synset': 'rye_ergot.n.01', 'name': 'rye_ergot'}, {'id': 21191, 'synset': 'black_root_rot_fungus.n.01', 'name': 'black_root_rot_fungus'}, {'id': 21192, 'synset': "dead-man's-fingers.n.01", 'name': "dead-man's-fingers"}, {'id': 21193, 'synset': 'sclerotinia.n.01', 'name': 'sclerotinia'}, {'id': 21194, 'synset': 'brown_cup.n.01', 'name': 'brown_cup'}, {'id': 21195, 'synset': 'earthball.n.01', 'name': 'earthball'}, {'id': 21196, 'synset': 'scleroderma_citrinum.n.01', 'name': 'Scleroderma_citrinum'}, {'id': 21197, 'synset': 'scleroderma_flavidium.n.01', 'name': 'Scleroderma_flavidium'}, {'id': 21198, 'synset': 'scleroderma_bovista.n.01', 'name': 'Scleroderma_bovista'}, {'id': 21199, 'synset': 'podaxaceae.n.01', 'name': 'Podaxaceae'}, {'id': 21200, 'synset': 'stalked_puffball.n.02', 'name': 'stalked_puffball'}, {'id': 21201, 'synset': 'stalked_puffball.n.01', 'name': 'stalked_puffball'}, {'id': 21202, 'synset': 'false_truffle.n.01', 'name': 'false_truffle'}, {'id': 21203, 'synset': 'rhizopogon_idahoensis.n.01', 'name': 'Rhizopogon_idahoensis'}, {'id': 21204, 'synset': 'truncocolumella_citrina.n.01', 'name': 'Truncocolumella_citrina'}, {'id': 21205, 'synset': 'mucor.n.01', 'name': 'mucor'}, {'id': 21206, 'synset': 'rhizopus.n.01', 'name': 'rhizopus'}, {'id': 21207, 'synset': 'bread_mold.n.01', 'name': 'bread_mold'}, {'id': 21208, 'synset': 'slime_mold.n.01', 'name': 'slime_mold'}, {'id': 21209, 'synset': 'true_slime_mold.n.01', 'name': 'true_slime_mold'}, {'id': 21210, 'synset': 'cellular_slime_mold.n.01', 'name': 'cellular_slime_mold'}, {'id': 21211, 'synset': 'dictostylium.n.01', 'name': 'dictostylium'}, {'id': 21212, 'synset': 'pond-scum_parasite.n.01', 'name': 'pond-scum_parasite'}, {'id': 21213, 'synset': 'potato_wart_fungus.n.01', 'name': 'potato_wart_fungus'}, {'id': 21214, 'synset': 'white_fungus.n.01', 'name': 'white_fungus'}, {'id': 21215, 'synset': 'water_mold.n.01', 'name': 'water_mold'}, {'id': 21216, 'synset': 'downy_mildew.n.01', 'name': 'downy_mildew'}, {'id': 21217, 'synset': 'blue_mold_fungus.n.01', 'name': 'blue_mold_fungus'}, {'id': 21218, 'synset': 'onion_mildew.n.01', 'name': 'onion_mildew'}, {'id': 21219, 'synset': 'tobacco_mildew.n.01', 'name': 'tobacco_mildew'}, {'id': 21220, 'synset': 'white_rust.n.01', 'name': 'white_rust'}, {'id': 21221, 'synset': 'pythium.n.01', 'name': 'pythium'}, {'id': 21222, 'synset': 'damping_off_fungus.n.01', 'name': 'damping_off_fungus'}, {'id': 21223, 'synset': 'phytophthora_citrophthora.n.01', 'name': 'Phytophthora_citrophthora'}, {'id': 21224, 'synset': 'phytophthora_infestans.n.01', 'name': 'Phytophthora_infestans'}, {'id': 21225, 'synset': 'clubroot_fungus.n.01', 'name': 'clubroot_fungus'}, {'id': 21226, 'synset': 'geglossaceae.n.01', 'name': 'Geglossaceae'}, {'id': 21227, 'synset': 'sarcosomataceae.n.01', 'name': 'Sarcosomataceae'}, {'id': 21228, 'synset': 'rufous_rubber_cup.n.01', 'name': 'Rufous_rubber_cup'}, {'id': 21229, 'synset': "devil's_cigar.n.01", 'name': "devil's_cigar"}, {'id': 21230, 'synset': "devil's_urn.n.01", 'name': "devil's_urn"}, {'id': 21231, 'synset': 'truffle.n.01', 'name': 'truffle'}, {'id': 21232, 'synset': 'club_fungus.n.01', 'name': 'club_fungus'}, {'id': 21233, 'synset': 'coral_fungus.n.01', 'name': 'coral_fungus'}, {'id': 21234, 'synset': 'tooth_fungus.n.01', 'name': 'tooth_fungus'}, {'id': 21235, 'synset': 'lichen.n.02', 'name': 'lichen'}, {'id': 21236, 'synset': 'ascolichen.n.01', 'name': 'ascolichen'}, {'id': 21237, 'synset': 'basidiolichen.n.01', 'name': 'basidiolichen'}, {'id': 21238, 'synset': 'lecanora.n.01', 'name': 'lecanora'}, {'id': 21239, 'synset': 'manna_lichen.n.01', 'name': 'manna_lichen'}, {'id': 21240, 'synset': 'archil.n.02', 'name': 'archil'}, {'id': 21241, 'synset': 'roccella.n.01', 'name': 'roccella'}, {'id': 21242, 'synset': 'beard_lichen.n.01', 'name': 'beard_lichen'}, {'id': 21243, 'synset': 'horsehair_lichen.n.01', 'name': 'horsehair_lichen'}, {'id': 21244, 'synset': 'reindeer_moss.n.01', 'name': 'reindeer_moss'}, {'id': 21245, 'synset': 'crottle.n.01', 'name': 'crottle'}, {'id': 21246, 'synset': 'iceland_moss.n.01', 'name': 'Iceland_moss'}, {'id': 21247, 'synset': 'fungus.n.01', 'name': 'fungus'}, {'id': 21248, 'synset': 'promycelium.n.01', 'name': 'promycelium'}, {'id': 21249, 'synset': 'true_fungus.n.01', 'name': 'true_fungus'}, {'id': 21250, 'synset': 'basidiomycete.n.01', 'name': 'basidiomycete'}, {'id': 21251, 'synset': 'mushroom.n.03', 'name': 'mushroom'}, {'id': 21252, 'synset': 'agaric.n.02', 'name': 'agaric'}, {'id': 21253, 'synset': 'mushroom.n.01', 'name': 'mushroom'}, {'id': 21254, 'synset': 'toadstool.n.01', 'name': 'toadstool'}, {'id': 21255, 'synset': 'horse_mushroom.n.01', 'name': 'horse_mushroom'}, {'id': 21256, 'synset': 'meadow_mushroom.n.01', 'name': 'meadow_mushroom'}, {'id': 21257, 'synset': 'shiitake.n.01', 'name': 'shiitake'}, {'id': 21258, 'synset': 'scaly_lentinus.n.01', 'name': 'scaly_lentinus'}, {'id': 21259, 'synset': 'royal_agaric.n.01', 'name': 'royal_agaric'}, {'id': 21260, 'synset': 'false_deathcap.n.01', 'name': 'false_deathcap'}, {'id': 21261, 'synset': 'fly_agaric.n.01', 'name': 'fly_agaric'}, {'id': 21262, 'synset': 'death_cap.n.01', 'name': 'death_cap'}, {'id': 21263, 'synset': 'blushing_mushroom.n.01', 'name': 'blushing_mushroom'}, {'id': 21264, 'synset': 'destroying_angel.n.01', 'name': 'destroying_angel'}, {'id': 21265, 'synset': 'chanterelle.n.01', 'name': 'chanterelle'}, {'id': 21266, 'synset': 'floccose_chanterelle.n.01', 'name': 'floccose_chanterelle'}, {'id': 21267, 'synset': "pig's_ears.n.01", 'name': "pig's_ears"}, {'id': 21268, 'synset': 'cinnabar_chanterelle.n.01', 'name': 'cinnabar_chanterelle'}, {'id': 21269, 'synset': 'jack-o-lantern_fungus.n.01', 'name': 'jack-o-lantern_fungus'}, {'id': 21270, 'synset': 'inky_cap.n.01', 'name': 'inky_cap'}, {'id': 21271, 'synset': 'shaggymane.n.01', 'name': 'shaggymane'}, {'id': 21272, 'synset': 'milkcap.n.01', 'name': 'milkcap'}, {'id': 21273, 'synset': 'fairy-ring_mushroom.n.01', 'name': 'fairy-ring_mushroom'}, {'id': 21274, 'synset': 'fairy_ring.n.01', 'name': 'fairy_ring'}, {'id': 21275, 'synset': 'oyster_mushroom.n.01', 'name': 'oyster_mushroom'}, {'id': 21276, 'synset': 'olive-tree_agaric.n.01', 'name': 'olive-tree_agaric'}, {'id': 21277, 'synset': 'pholiota_astragalina.n.01', 'name': 'Pholiota_astragalina'}, {'id': 21278, 'synset': 'pholiota_aurea.n.01', 'name': 'Pholiota_aurea'}, {'id': 21279, 'synset': 'pholiota_destruens.n.01', 'name': 'Pholiota_destruens'}, {'id': 21280, 'synset': 'pholiota_flammans.n.01', 'name': 'Pholiota_flammans'}, {'id': 21281, 'synset': 'pholiota_flavida.n.01', 'name': 'Pholiota_flavida'}, {'id': 21282, 'synset': 'nameko.n.01', 'name': 'nameko'}, {'id': 21283, 'synset': 'pholiota_squarrosa-adiposa.n.01', 'name': 'Pholiota_squarrosa-adiposa'}, {'id': 21284, 'synset': 'pholiota_squarrosa.n.01', 'name': 'Pholiota_squarrosa'}, {'id': 21285, 'synset': 'pholiota_squarrosoides.n.01', 'name': 'Pholiota_squarrosoides'}, {'id': 21286, 'synset': 'stropharia_ambigua.n.01', 'name': 'Stropharia_ambigua'}, {'id': 21287, 'synset': 'stropharia_hornemannii.n.01', 'name': 'Stropharia_hornemannii'}, {'id': 21288, 'synset': 'stropharia_rugoso-annulata.n.01', 'name': 'Stropharia_rugoso-annulata'}, {'id': 21289, 'synset': 'gill_fungus.n.01', 'name': 'gill_fungus'}, {'id': 21290, 'synset': 'entoloma_lividum.n.01', 'name': 'Entoloma_lividum'}, {'id': 21291, 'synset': 'entoloma_aprile.n.01', 'name': 'Entoloma_aprile'}, {'id': 21292, 'synset': 'chlorophyllum_molybdites.n.01', 'name': 'Chlorophyllum_molybdites'}, {'id': 21293, 'synset': 'lepiota.n.01', 'name': 'lepiota'}, {'id': 21294, 'synset': 'parasol_mushroom.n.01', 'name': 'parasol_mushroom'}, {'id': 21295, 'synset': 'poisonous_parasol.n.01', 'name': 'poisonous_parasol'}, {'id': 21296, 'synset': 'lepiota_naucina.n.01', 'name': 'Lepiota_naucina'}, {'id': 21297, 'synset': 'lepiota_rhacodes.n.01', 'name': 'Lepiota_rhacodes'}, {'id': 21298, 'synset': 'american_parasol.n.01', 'name': 'American_parasol'}, {'id': 21299, 'synset': 'lepiota_rubrotincta.n.01', 'name': 'Lepiota_rubrotincta'}, {'id': 21300, 'synset': 'lepiota_clypeolaria.n.01', 'name': 'Lepiota_clypeolaria'}, {'id': 21301, 'synset': 'onion_stem.n.01', 'name': 'onion_stem'}, {'id': 21302, 'synset': 'pink_disease_fungus.n.01', 'name': 'pink_disease_fungus'}, {'id': 21303, 'synset': 'bottom_rot_fungus.n.01', 'name': 'bottom_rot_fungus'}, {'id': 21304, 'synset': 'potato_fungus.n.01', 'name': 'potato_fungus'}, {'id': 21305, 'synset': 'coffee_fungus.n.01', 'name': 'coffee_fungus'}, {'id': 21306, 'synset': 'blewits.n.01', 'name': 'blewits'}, {'id': 21307, 'synset': 'sandy_mushroom.n.01', 'name': 'sandy_mushroom'}, {'id': 21308, 'synset': 'tricholoma_pessundatum.n.01', 'name': 'Tricholoma_pessundatum'}, {'id': 21309, 'synset': 'tricholoma_sejunctum.n.01', 'name': 'Tricholoma_sejunctum'}, {'id': 21310, 'synset': 'man-on-a-horse.n.01', 'name': 'man-on-a-horse'}, {'id': 21311, 'synset': 'tricholoma_venenata.n.01', 'name': 'Tricholoma_venenata'}, {'id': 21312, 'synset': 'tricholoma_pardinum.n.01', 'name': 'Tricholoma_pardinum'}, {'id': 21313, 'synset': 'tricholoma_vaccinum.n.01', 'name': 'Tricholoma_vaccinum'}, {'id': 21314, 'synset': 'tricholoma_aurantium.n.01', 'name': 'Tricholoma_aurantium'}, {'id': 21315, 'synset': 'volvaria_bombycina.n.01', 'name': 'Volvaria_bombycina'}, {'id': 21316, 'synset': 'pluteus_aurantiorugosus.n.01', 'name': 'Pluteus_aurantiorugosus'}, {'id': 21317, 'synset': 'pluteus_magnus.n.01', 'name': 'Pluteus_magnus'}, {'id': 21318, 'synset': 'deer_mushroom.n.01', 'name': 'deer_mushroom'}, {'id': 21319, 'synset': 'straw_mushroom.n.01', 'name': 'straw_mushroom'}, {'id': 21320, 'synset': 'volvariella_bombycina.n.01', 'name': 'Volvariella_bombycina'}, {'id': 21321, 'synset': 'clitocybe_clavipes.n.01', 'name': 'Clitocybe_clavipes'}, {'id': 21322, 'synset': 'clitocybe_dealbata.n.01', 'name': 'Clitocybe_dealbata'}, {'id': 21323, 'synset': 'clitocybe_inornata.n.01', 'name': 'Clitocybe_inornata'}, {'id': 21324, 'synset': 'clitocybe_robusta.n.01', 'name': 'Clitocybe_robusta'}, {'id': 21325, 'synset': 'clitocybe_irina.n.01', 'name': 'Clitocybe_irina'}, {'id': 21326, 'synset': 'clitocybe_subconnexa.n.01', 'name': 'Clitocybe_subconnexa'}, {'id': 21327, 'synset': 'winter_mushroom.n.01', 'name': 'winter_mushroom'}, {'id': 21328, 'synset': 'mycelium.n.01', 'name': 'mycelium'}, {'id': 21329, 'synset': 'sclerotium.n.02', 'name': 'sclerotium'}, {'id': 21330, 'synset': 'sac_fungus.n.01', 'name': 'sac_fungus'}, {'id': 21331, 'synset': 'ascomycete.n.01', 'name': 'ascomycete'}, {'id': 21332, 'synset': 'clavicipitaceae.n.01', 'name': 'Clavicipitaceae'}, {'id': 21333, 'synset': 'grainy_club.n.01', 'name': 'grainy_club'}, {'id': 21334, 'synset': 'yeast.n.02', 'name': 'yeast'}, {'id': 21335, 'synset': "baker's_yeast.n.01", 'name': "baker's_yeast"}, {'id': 21336, 'synset': "wine-maker's_yeast.n.01", 'name': "wine-maker's_yeast"}, {'id': 21337, 'synset': 'aspergillus_fumigatus.n.01', 'name': 'Aspergillus_fumigatus'}, {'id': 21338, 'synset': 'brown_root_rot_fungus.n.01', 'name': 'brown_root_rot_fungus'}, {'id': 21339, 'synset': 'discomycete.n.01', 'name': 'discomycete'}, {'id': 21340, 'synset': 'leotia_lubrica.n.01', 'name': 'Leotia_lubrica'}, {'id': 21341, 'synset': 'mitrula_elegans.n.01', 'name': 'Mitrula_elegans'}, {'id': 21342, 'synset': 'sarcoscypha_coccinea.n.01', 'name': 'Sarcoscypha_coccinea'}, {'id': 21343, 'synset': 'caloscypha_fulgens.n.01', 'name': 'Caloscypha_fulgens'}, {'id': 21344, 'synset': 'aleuria_aurantia.n.01', 'name': 'Aleuria_aurantia'}, {'id': 21345, 'synset': 'elf_cup.n.01', 'name': 'elf_cup'}, {'id': 21346, 'synset': 'peziza_domicilina.n.01', 'name': 'Peziza_domicilina'}, {'id': 21347, 'synset': 'blood_cup.n.01', 'name': 'blood_cup'}, {'id': 21348, 'synset': 'urnula_craterium.n.01', 'name': 'Urnula_craterium'}, {'id': 21349, 'synset': 'galiella_rufa.n.01', 'name': 'Galiella_rufa'}, {'id': 21350, 'synset': 'jafnea_semitosta.n.01', 'name': 'Jafnea_semitosta'}, {'id': 21351, 'synset': 'morel.n.01', 'name': 'morel'}, {'id': 21352, 'synset': 'common_morel.n.01', 'name': 'common_morel'}, {'id': 21353, 'synset': 'disciotis_venosa.n.01', 'name': 'Disciotis_venosa'}, {'id': 21354, 'synset': 'verpa.n.01', 'name': 'Verpa'}, {'id': 21355, 'synset': 'verpa_bohemica.n.01', 'name': 'Verpa_bohemica'}, {'id': 21356, 'synset': 'verpa_conica.n.01', 'name': 'Verpa_conica'}, {'id': 21357, 'synset': 'black_morel.n.01', 'name': 'black_morel'}, {'id': 21358, 'synset': 'morchella_crassipes.n.01', 'name': 'Morchella_crassipes'}, {'id': 21359, 'synset': 'morchella_semilibera.n.01', 'name': 'Morchella_semilibera'}, {'id': 21360, 'synset': 'wynnea_americana.n.01', 'name': 'Wynnea_americana'}, {'id': 21361, 'synset': 'wynnea_sparassoides.n.01', 'name': 'Wynnea_sparassoides'}, {'id': 21362, 'synset': 'false_morel.n.01', 'name': 'false_morel'}, {'id': 21363, 'synset': 'lorchel.n.01', 'name': 'lorchel'}, {'id': 21364, 'synset': 'helvella.n.01', 'name': 'helvella'}, {'id': 21365, 'synset': 'helvella_crispa.n.01', 'name': 'Helvella_crispa'}, {'id': 21366, 'synset': 'helvella_acetabulum.n.01', 'name': 'Helvella_acetabulum'}, {'id': 21367, 'synset': 'helvella_sulcata.n.01', 'name': 'Helvella_sulcata'}, {'id': 21368, 'synset': 'discina.n.01', 'name': 'discina'}, {'id': 21369, 'synset': 'gyromitra.n.01', 'name': 'gyromitra'}, {'id': 21370, 'synset': 'gyromitra_californica.n.01', 'name': 'Gyromitra_californica'}, {'id': 21371, 'synset': 'gyromitra_sphaerospora.n.01', 'name': 'Gyromitra_sphaerospora'}, {'id': 21372, 'synset': 'gyromitra_esculenta.n.01', 'name': 'Gyromitra_esculenta'}, {'id': 21373, 'synset': 'gyromitra_infula.n.01', 'name': 'Gyromitra_infula'}, {'id': 21374, 'synset': 'gyromitra_fastigiata.n.01', 'name': 'Gyromitra_fastigiata'}, {'id': 21375, 'synset': 'gyromitra_gigas.n.01', 'name': 'Gyromitra_gigas'}, {'id': 21376, 'synset': 'gasteromycete.n.01', 'name': 'gasteromycete'}, {'id': 21377, 'synset': 'stinkhorn.n.01', 'name': 'stinkhorn'}, {'id': 21378, 'synset': 'common_stinkhorn.n.01', 'name': 'common_stinkhorn'}, {'id': 21379, 'synset': 'phallus_ravenelii.n.01', 'name': 'Phallus_ravenelii'}, {'id': 21380, 'synset': 'dog_stinkhorn.n.01', 'name': 'dog_stinkhorn'}, {'id': 21381, 'synset': 'calostoma_lutescens.n.01', 'name': 'Calostoma_lutescens'}, {'id': 21382, 'synset': 'calostoma_cinnabarina.n.01', 'name': 'Calostoma_cinnabarina'}, {'id': 21383, 'synset': 'calostoma_ravenelii.n.01', 'name': 'Calostoma_ravenelii'}, {'id': 21384, 'synset': 'stinky_squid.n.01', 'name': 'stinky_squid'}, {'id': 21385, 'synset': 'puffball.n.01', 'name': 'puffball'}, {'id': 21386, 'synset': 'giant_puffball.n.01', 'name': 'giant_puffball'}, {'id': 21387, 'synset': 'earthstar.n.01', 'name': 'earthstar'}, {'id': 21388, 'synset': 'geastrum_coronatum.n.01', 'name': 'Geastrum_coronatum'}, {'id': 21389, 'synset': 'radiigera_fuscogleba.n.01', 'name': 'Radiigera_fuscogleba'}, {'id': 21390, 'synset': 'astreus_pteridis.n.01', 'name': 'Astreus_pteridis'}, {'id': 21391, 'synset': 'astreus_hygrometricus.n.01', 'name': 'Astreus_hygrometricus'}, {'id': 21392, 'synset': "bird's-nest_fungus.n.01", 'name': "bird's-nest_fungus"}, {'id': 21393, 'synset': 'gastrocybe_lateritia.n.01', 'name': 'Gastrocybe_lateritia'}, {'id': 21394, 'synset': 'macowanites_americanus.n.01', 'name': 'Macowanites_americanus'}, {'id': 21395, 'synset': 'polypore.n.01', 'name': 'polypore'}, {'id': 21396, 'synset': 'bracket_fungus.n.01', 'name': 'bracket_fungus'}, {'id': 21397, 'synset': 'albatrellus_dispansus.n.01', 'name': 'Albatrellus_dispansus'}, {'id': 21398, 'synset': 'albatrellus_ovinus.n.01', 'name': 'Albatrellus_ovinus'}, {'id': 21399, 'synset': 'neolentinus_ponderosus.n.01', 'name': 'Neolentinus_ponderosus'}, {'id': 21400, 'synset': 'oligoporus_leucospongia.n.01', 'name': 'Oligoporus_leucospongia'}, {'id': 21401, 'synset': 'polyporus_tenuiculus.n.01', 'name': 'Polyporus_tenuiculus'}, {'id': 21402, 'synset': 'hen-of-the-woods.n.01', 'name': 'hen-of-the-woods'}, {'id': 21403, 'synset': 'polyporus_squamosus.n.01', 'name': 'Polyporus_squamosus'}, {'id': 21404, 'synset': 'beefsteak_fungus.n.01', 'name': 'beefsteak_fungus'}, {'id': 21405, 'synset': 'agaric.n.01', 'name': 'agaric'}, {'id': 21406, 'synset': 'bolete.n.01', 'name': 'bolete'}, {'id': 21407, 'synset': 'boletus_chrysenteron.n.01', 'name': 'Boletus_chrysenteron'}, {'id': 21408, 'synset': 'boletus_edulis.n.01', 'name': 'Boletus_edulis'}, {'id': 21409, 'synset': "frost's_bolete.n.01", 'name': "Frost's_bolete"}, {'id': 21410, 'synset': 'boletus_luridus.n.01', 'name': 'Boletus_luridus'}, {'id': 21411, 'synset': 'boletus_mirabilis.n.01', 'name': 'Boletus_mirabilis'}, {'id': 21412, 'synset': 'boletus_pallidus.n.01', 'name': 'Boletus_pallidus'}, {'id': 21413, 'synset': 'boletus_pulcherrimus.n.01', 'name': 'Boletus_pulcherrimus'}, {'id': 21414, 'synset': 'boletus_pulverulentus.n.01', 'name': 'Boletus_pulverulentus'}, {'id': 21415, 'synset': 'boletus_roxanae.n.01', 'name': 'Boletus_roxanae'}, {'id': 21416, 'synset': 'boletus_subvelutipes.n.01', 'name': 'Boletus_subvelutipes'}, {'id': 21417, 'synset': 'boletus_variipes.n.01', 'name': 'Boletus_variipes'}, {'id': 21418, 'synset': 'boletus_zelleri.n.01', 'name': 'Boletus_zelleri'}, {'id': 21419, 'synset': 'fuscoboletinus_paluster.n.01', 'name': 'Fuscoboletinus_paluster'}, {'id': 21420, 'synset': 'fuscoboletinus_serotinus.n.01', 'name': 'Fuscoboletinus_serotinus'}, {'id': 21421, 'synset': 'leccinum_fibrillosum.n.01', 'name': 'Leccinum_fibrillosum'}, {'id': 21422, 'synset': 'suillus_albivelatus.n.01', 'name': 'Suillus_albivelatus'}, {'id': 21423, 'synset': 'old-man-of-the-woods.n.01', 'name': 'old-man-of-the-woods'}, {'id': 21424, 'synset': 'boletellus_russellii.n.01', 'name': 'Boletellus_russellii'}, {'id': 21425, 'synset': 'jelly_fungus.n.01', 'name': 'jelly_fungus'}, {'id': 21426, 'synset': 'snow_mushroom.n.01', 'name': 'snow_mushroom'}, {'id': 21427, 'synset': "witches'_butter.n.01", 'name': "witches'_butter"}, {'id': 21428, 'synset': 'tremella_foliacea.n.01', 'name': 'Tremella_foliacea'}, {'id': 21429, 'synset': 'tremella_reticulata.n.01', 'name': 'Tremella_reticulata'}, {'id': 21430, 'synset': "jew's-ear.n.01", 'name': "Jew's-ear"}, {'id': 21431, 'synset': 'rust.n.04', 'name': 'rust'}, {'id': 21432, 'synset': 'aecium.n.01', 'name': 'aecium'}, {'id': 21433, 'synset': 'flax_rust.n.01', 'name': 'flax_rust'}, {'id': 21434, 'synset': 'blister_rust.n.02', 'name': 'blister_rust'}, {'id': 21435, 'synset': 'wheat_rust.n.01', 'name': 'wheat_rust'}, {'id': 21436, 'synset': 'apple_rust.n.01', 'name': 'apple_rust'}, {'id': 21437, 'synset': 'smut.n.03', 'name': 'smut'}, {'id': 21438, 'synset': 'covered_smut.n.01', 'name': 'covered_smut'}, {'id': 21439, 'synset': 'loose_smut.n.02', 'name': 'loose_smut'}, {'id': 21440, 'synset': 'cornsmut.n.01', 'name': 'cornsmut'}, {'id': 21441, 'synset': 'boil_smut.n.01', 'name': 'boil_smut'}, {'id': 21442, 'synset': 'sphacelotheca.n.01', 'name': 'Sphacelotheca'}, {'id': 21443, 'synset': 'head_smut.n.01', 'name': 'head_smut'}, {'id': 21444, 'synset': 'bunt.n.04', 'name': 'bunt'}, {'id': 21445, 'synset': 'bunt.n.03', 'name': 'bunt'}, {'id': 21446, 'synset': 'onion_smut.n.01', 'name': 'onion_smut'}, {'id': 21447, 'synset': 'flag_smut_fungus.n.01', 'name': 'flag_smut_fungus'}, {'id': 21448, 'synset': 'wheat_flag_smut.n.01', 'name': 'wheat_flag_smut'}, {'id': 21449, 'synset': 'felt_fungus.n.01', 'name': 'felt_fungus'}, {'id': 21450, 'synset': 'waxycap.n.01', 'name': 'waxycap'}, {'id': 21451, 'synset': 'hygrocybe_acutoconica.n.01', 'name': 'Hygrocybe_acutoconica'}, {'id': 21452, 'synset': 'hygrophorus_borealis.n.01', 'name': 'Hygrophorus_borealis'}, {'id': 21453, 'synset': 'hygrophorus_caeruleus.n.01', 'name': 'Hygrophorus_caeruleus'}, {'id': 21454, 'synset': 'hygrophorus_inocybiformis.n.01', 'name': 'Hygrophorus_inocybiformis'}, {'id': 21455, 'synset': 'hygrophorus_kauffmanii.n.01', 'name': 'Hygrophorus_kauffmanii'}, {'id': 21456, 'synset': 'hygrophorus_marzuolus.n.01', 'name': 'Hygrophorus_marzuolus'}, {'id': 21457, 'synset': 'hygrophorus_purpurascens.n.01', 'name': 'Hygrophorus_purpurascens'}, {'id': 21458, 'synset': 'hygrophorus_russula.n.01', 'name': 'Hygrophorus_russula'}, {'id': 21459, 'synset': 'hygrophorus_sordidus.n.01', 'name': 'Hygrophorus_sordidus'}, {'id': 21460, 'synset': 'hygrophorus_tennesseensis.n.01', 'name': 'Hygrophorus_tennesseensis'}, {'id': 21461, 'synset': 'hygrophorus_turundus.n.01', 'name': 'Hygrophorus_turundus'}, {'id': 21462, 'synset': 'neohygrophorus_angelesianus.n.01', 'name': 'Neohygrophorus_angelesianus'}, {'id': 21463, 'synset': 'cortinarius_armillatus.n.01', 'name': 'Cortinarius_armillatus'}, {'id': 21464, 'synset': 'cortinarius_atkinsonianus.n.01', 'name': 'Cortinarius_atkinsonianus'}, {'id': 21465, 'synset': 'cortinarius_corrugatus.n.01', 'name': 'Cortinarius_corrugatus'}, {'id': 21466, 'synset': 'cortinarius_gentilis.n.01', 'name': 'Cortinarius_gentilis'}, {'id': 21467, 'synset': 'cortinarius_mutabilis.n.01', 'name': 'Cortinarius_mutabilis'}, {'id': 21468, 'synset': 'cortinarius_semisanguineus.n.01', 'name': 'Cortinarius_semisanguineus'}, {'id': 21469, 'synset': 'cortinarius_subfoetidus.n.01', 'name': 'Cortinarius_subfoetidus'}, {'id': 21470, 'synset': 'cortinarius_violaceus.n.01', 'name': 'Cortinarius_violaceus'}, {'id': 21471, 'synset': 'gymnopilus_spectabilis.n.01', 'name': 'Gymnopilus_spectabilis'}, {'id': 21472, 'synset': 'gymnopilus_validipes.n.01', 'name': 'Gymnopilus_validipes'}, {'id': 21473, 'synset': 'gymnopilus_ventricosus.n.01', 'name': 'Gymnopilus_ventricosus'}, {'id': 21474, 'synset': 'mold.n.05', 'name': 'mold'}, {'id': 21475, 'synset': 'mildew.n.02', 'name': 'mildew'}, {'id': 21476, 'synset': 'verticillium.n.01', 'name': 'verticillium'}, {'id': 21477, 'synset': 'monilia.n.01', 'name': 'monilia'}, {'id': 21478, 'synset': 'candida.n.01', 'name': 'candida'}, {'id': 21479, 'synset': 'candida_albicans.n.01', 'name': 'Candida_albicans'}, {'id': 21480, 'synset': 'blastomycete.n.01', 'name': 'blastomycete'}, {'id': 21481, 'synset': 'yellow_spot_fungus.n.01', 'name': 'yellow_spot_fungus'}, {'id': 21482, 'synset': 'green_smut_fungus.n.01', 'name': 'green_smut_fungus'}, {'id': 21483, 'synset': 'dry_rot.n.02', 'name': 'dry_rot'}, {'id': 21484, 'synset': 'rhizoctinia.n.01', 'name': 'rhizoctinia'}, {'id': 21485, 'synset': 'houseplant.n.01', 'name': 'houseplant'}, {'id': 21486, 'synset': 'bedder.n.01', 'name': 'bedder'}, {'id': 21487, 'synset': 'succulent.n.01', 'name': 'succulent'}, {'id': 21488, 'synset': 'cultivar.n.01', 'name': 'cultivar'}, {'id': 21489, 'synset': 'weed.n.01', 'name': 'weed'}, {'id': 21490, 'synset': 'wort.n.01', 'name': 'wort'}, {'id': 21491, 'synset': 'brier.n.02', 'name': 'brier'}, {'id': 21492, 'synset': 'aril.n.01', 'name': 'aril'}, {'id': 21493, 'synset': 'sporophyll.n.01', 'name': 'sporophyll'}, {'id': 21494, 'synset': 'sporangium.n.01', 'name': 'sporangium'}, {'id': 21495, 'synset': 'sporangiophore.n.01', 'name': 'sporangiophore'}, {'id': 21496, 'synset': 'ascus.n.01', 'name': 'ascus'}, {'id': 21497, 'synset': 'ascospore.n.01', 'name': 'ascospore'}, {'id': 21498, 'synset': 'arthrospore.n.02', 'name': 'arthrospore'}, {'id': 21499, 'synset': 'eusporangium.n.01', 'name': 'eusporangium'}, {'id': 21500, 'synset': 'tetrasporangium.n.01', 'name': 'tetrasporangium'}, {'id': 21501, 'synset': 'gametangium.n.01', 'name': 'gametangium'}, {'id': 21502, 'synset': 'sorus.n.02', 'name': 'sorus'}, {'id': 21503, 'synset': 'sorus.n.01', 'name': 'sorus'}, {'id': 21504, 'synset': 'partial_veil.n.01', 'name': 'partial_veil'}, {'id': 21505, 'synset': 'lignum.n.01', 'name': 'lignum'}, {'id': 21506, 'synset': 'vascular_ray.n.01', 'name': 'vascular_ray'}, {'id': 21507, 'synset': 'phloem.n.01', 'name': 'phloem'}, {'id': 21508, 'synset': 'evergreen.n.01', 'name': 'evergreen'}, {'id': 21509, 'synset': 'deciduous_plant.n.01', 'name': 'deciduous_plant'}, {'id': 21510, 'synset': 'poisonous_plant.n.01', 'name': 'poisonous_plant'}, {'id': 21511, 'synset': 'vine.n.01', 'name': 'vine'}, {'id': 21512, 'synset': 'creeper.n.01', 'name': 'creeper'}, {'id': 21513, 'synset': 'tendril.n.01', 'name': 'tendril'}, {'id': 21514, 'synset': 'root_climber.n.01', 'name': 'root_climber'}, {'id': 21515, 'synset': 'lignosae.n.01', 'name': 'lignosae'}, {'id': 21516, 'synset': 'arborescent_plant.n.01', 'name': 'arborescent_plant'}, {'id': 21517, 'synset': 'snag.n.02', 'name': 'snag'}, {'id': 21518, 'synset': 'tree.n.01', 'name': 'tree'}, {'id': 21519, 'synset': 'timber_tree.n.01', 'name': 'timber_tree'}, {'id': 21520, 'synset': 'treelet.n.01', 'name': 'treelet'}, {'id': 21521, 'synset': 'arbor.n.01', 'name': 'arbor'}, {'id': 21522, 'synset': 'bean_tree.n.01', 'name': 'bean_tree'}, {'id': 21523, 'synset': 'pollard.n.01', 'name': 'pollard'}, {'id': 21524, 'synset': 'sapling.n.01', 'name': 'sapling'}, {'id': 21525, 'synset': 'shade_tree.n.01', 'name': 'shade_tree'}, {'id': 21526, 'synset': 'gymnospermous_tree.n.01', 'name': 'gymnospermous_tree'}, {'id': 21527, 'synset': 'conifer.n.01', 'name': 'conifer'}, {'id': 21528, 'synset': 'angiospermous_tree.n.01', 'name': 'angiospermous_tree'}, {'id': 21529, 'synset': 'nut_tree.n.01', 'name': 'nut_tree'}, {'id': 21530, 'synset': 'spice_tree.n.01', 'name': 'spice_tree'}, {'id': 21531, 'synset': 'fever_tree.n.01', 'name': 'fever_tree'}, {'id': 21532, 'synset': 'stump.n.01', 'name': 'stump'}, {'id': 21533, 'synset': 'bonsai.n.01', 'name': 'bonsai'}, {'id': 21534, 'synset': 'ming_tree.n.02', 'name': 'ming_tree'}, {'id': 21535, 'synset': 'ming_tree.n.01', 'name': 'ming_tree'}, {'id': 21536, 'synset': 'undershrub.n.01', 'name': 'undershrub'}, {'id': 21537, 'synset': 'subshrub.n.01', 'name': 'subshrub'}, {'id': 21538, 'synset': 'bramble.n.01', 'name': 'bramble'}, {'id': 21539, 'synset': 'liana.n.01', 'name': 'liana'}, {'id': 21540, 'synset': 'geophyte.n.01', 'name': 'geophyte'}, {'id': 21541, 'synset': 'desert_plant.n.01', 'name': 'desert_plant'}, {'id': 21542, 'synset': 'mesophyte.n.01', 'name': 'mesophyte'}, {'id': 21543, 'synset': 'marsh_plant.n.01', 'name': 'marsh_plant'}, {'id': 21544, 'synset': 'hemiepiphyte.n.01', 'name': 'hemiepiphyte'}, {'id': 21545, 'synset': 'strangler.n.01', 'name': 'strangler'}, {'id': 21546, 'synset': 'lithophyte.n.01', 'name': 'lithophyte'}, {'id': 21547, 'synset': 'saprobe.n.01', 'name': 'saprobe'}, {'id': 21548, 'synset': 'autophyte.n.01', 'name': 'autophyte'}, {'id': 21549, 'synset': 'root.n.01', 'name': 'root'}, {'id': 21550, 'synset': 'taproot.n.01', 'name': 'taproot'}, {'id': 21551, 'synset': 'prop_root.n.01', 'name': 'prop_root'}, {'id': 21552, 'synset': 'prophyll.n.01', 'name': 'prophyll'}, {'id': 21553, 'synset': 'rootstock.n.02', 'name': 'rootstock'}, {'id': 21554, 'synset': 'quickset.n.01', 'name': 'quickset'}, {'id': 21555, 'synset': 'stolon.n.01', 'name': 'stolon'}, {'id': 21556, 'synset': 'tuberous_plant.n.01', 'name': 'tuberous_plant'}, {'id': 21557, 'synset': 'rhizome.n.01', 'name': 'rhizome'}, {'id': 21558, 'synset': 'rachis.n.01', 'name': 'rachis'}, {'id': 21559, 'synset': 'caudex.n.02', 'name': 'caudex'}, {'id': 21560, 'synset': 'cladode.n.01', 'name': 'cladode'}, {'id': 21561, 'synset': 'receptacle.n.02', 'name': 'receptacle'}, {'id': 21562, 'synset': 'scape.n.01', 'name': 'scape'}, {'id': 21563, 'synset': 'umbel.n.01', 'name': 'umbel'}, {'id': 21564, 'synset': 'petiole.n.01', 'name': 'petiole'}, {'id': 21565, 'synset': 'peduncle.n.02', 'name': 'peduncle'}, {'id': 21566, 'synset': 'pedicel.n.01', 'name': 'pedicel'}, {'id': 21567, 'synset': 'flower_cluster.n.01', 'name': 'flower_cluster'}, {'id': 21568, 'synset': 'raceme.n.01', 'name': 'raceme'}, {'id': 21569, 'synset': 'panicle.n.01', 'name': 'panicle'}, {'id': 21570, 'synset': 'thyrse.n.01', 'name': 'thyrse'}, {'id': 21571, 'synset': 'cyme.n.01', 'name': 'cyme'}, {'id': 21572, 'synset': 'cymule.n.01', 'name': 'cymule'}, {'id': 21573, 'synset': 'glomerule.n.01', 'name': 'glomerule'}, {'id': 21574, 'synset': 'scorpioid_cyme.n.01', 'name': 'scorpioid_cyme'}, {'id': 21575, 'synset': 'ear.n.05', 'name': 'ear'}, {'id': 21576, 'synset': 'spadix.n.01', 'name': 'spadix'}, {'id': 21577, 'synset': 'bulbous_plant.n.01', 'name': 'bulbous_plant'}, {'id': 21578, 'synset': 'bulbil.n.01', 'name': 'bulbil'}, {'id': 21579, 'synset': 'cormous_plant.n.01', 'name': 'cormous_plant'}, {'id': 21580, 'synset': 'fruit.n.01', 'name': 'fruit'}, {'id': 21581, 'synset': 'fruitlet.n.01', 'name': 'fruitlet'}, {'id': 21582, 'synset': 'seed.n.01', 'name': 'seed'}, {'id': 21583, 'synset': 'bean.n.02', 'name': 'bean'}, {'id': 21584, 'synset': 'nut.n.01', 'name': 'nut'}, {'id': 21585, 'synset': 'nutlet.n.01', 'name': 'nutlet'}, {'id': 21586, 'synset': 'kernel.n.01', 'name': 'kernel'}, {'id': 21587, 'synset': 'syconium.n.01', 'name': 'syconium'}, {'id': 21588, 'synset': 'berry.n.02', 'name': 'berry'}, {'id': 21589, 'synset': 'aggregate_fruit.n.01', 'name': 'aggregate_fruit'}, {'id': 21590, 'synset': 'simple_fruit.n.01', 'name': 'simple_fruit'}, {'id': 21591, 'synset': 'acinus.n.01', 'name': 'acinus'}, {'id': 21592, 'synset': 'drupe.n.01', 'name': 'drupe'}, {'id': 21593, 'synset': 'drupelet.n.01', 'name': 'drupelet'}, {'id': 21594, 'synset': 'pome.n.01', 'name': 'pome'}, {'id': 21595, 'synset': 'pod.n.02', 'name': 'pod'}, {'id': 21596, 'synset': 'loment.n.01', 'name': 'loment'}, {'id': 21597, 'synset': 'pyxidium.n.01', 'name': 'pyxidium'}, {'id': 21598, 'synset': 'husk.n.02', 'name': 'husk'}, {'id': 21599, 'synset': 'cornhusk.n.01', 'name': 'cornhusk'}, {'id': 21600, 'synset': 'pod.n.01', 'name': 'pod'}, {'id': 21601, 'synset': 'accessory_fruit.n.01', 'name': 'accessory_fruit'}, {'id': 21602, 'synset': 'buckthorn.n.01', 'name': 'buckthorn'}, {'id': 21603, 'synset': 'buckthorn_berry.n.01', 'name': 'buckthorn_berry'}, {'id': 21604, 'synset': 'cascara_buckthorn.n.01', 'name': 'cascara_buckthorn'}, {'id': 21605, 'synset': 'cascara.n.01', 'name': 'cascara'}, {'id': 21606, 'synset': 'carolina_buckthorn.n.01', 'name': 'Carolina_buckthorn'}, {'id': 21607, 'synset': 'coffeeberry.n.01', 'name': 'coffeeberry'}, {'id': 21608, 'synset': 'redberry.n.01', 'name': 'redberry'}, {'id': 21609, 'synset': 'nakedwood.n.01', 'name': 'nakedwood'}, {'id': 21610, 'synset': 'jujube.n.01', 'name': 'jujube'}, {'id': 21611, 'synset': "christ's-thorn.n.01", 'name': "Christ's-thorn"}, {'id': 21612, 'synset': 'hazel.n.01', 'name': 'hazel'}, {'id': 21613, 'synset': 'fox_grape.n.01', 'name': 'fox_grape'}, {'id': 21614, 'synset': 'muscadine.n.01', 'name': 'muscadine'}, {'id': 21615, 'synset': 'vinifera.n.01', 'name': 'vinifera'}, {'id': 21616, 'synset': 'pinot_blanc.n.01', 'name': 'Pinot_blanc'}, {'id': 21617, 'synset': 'sauvignon_grape.n.01', 'name': 'Sauvignon_grape'}, {'id': 21618, 'synset': 'sauvignon_blanc.n.01', 'name': 'Sauvignon_blanc'}, {'id': 21619, 'synset': 'muscadet.n.01', 'name': 'Muscadet'}, {'id': 21620, 'synset': 'riesling.n.01', 'name': 'Riesling'}, {'id': 21621, 'synset': 'zinfandel.n.01', 'name': 'Zinfandel'}, {'id': 21622, 'synset': 'chenin_blanc.n.01', 'name': 'Chenin_blanc'}, {'id': 21623, 'synset': 'malvasia.n.01', 'name': 'malvasia'}, {'id': 21624, 'synset': 'verdicchio.n.01', 'name': 'Verdicchio'}, {'id': 21625, 'synset': 'boston_ivy.n.01', 'name': 'Boston_ivy'}, {'id': 21626, 'synset': 'virginia_creeper.n.01', 'name': 'Virginia_creeper'}, {'id': 21627, 'synset': 'true_pepper.n.01', 'name': 'true_pepper'}, {'id': 21628, 'synset': 'betel.n.01', 'name': 'betel'}, {'id': 21629, 'synset': 'cubeb.n.01', 'name': 'cubeb'}, {'id': 21630, 'synset': 'schizocarp.n.01', 'name': 'schizocarp'}, {'id': 21631, 'synset': 'peperomia.n.01', 'name': 'peperomia'}, {'id': 21632, 'synset': 'watermelon_begonia.n.01', 'name': 'watermelon_begonia'}, {'id': 21633, 'synset': 'yerba_mansa.n.01', 'name': 'yerba_mansa'}, {'id': 21634, 'synset': 'pinna.n.01', 'name': 'pinna'}, {'id': 21635, 'synset': 'frond.n.01', 'name': 'frond'}, {'id': 21636, 'synset': 'bract.n.01', 'name': 'bract'}, {'id': 21637, 'synset': 'bracteole.n.01', 'name': 'bracteole'}, {'id': 21638, 'synset': 'involucre.n.01', 'name': 'involucre'}, {'id': 21639, 'synset': 'glume.n.01', 'name': 'glume'}, {'id': 21640, 'synset': 'palmate_leaf.n.01', 'name': 'palmate_leaf'}, {'id': 21641, 'synset': 'pinnate_leaf.n.01', 'name': 'pinnate_leaf'}, {'id': 21642, 'synset': 'bijugate_leaf.n.01', 'name': 'bijugate_leaf'}, {'id': 21643, 'synset': 'decompound_leaf.n.01', 'name': 'decompound_leaf'}, {'id': 21644, 'synset': 'acuminate_leaf.n.01', 'name': 'acuminate_leaf'}, {'id': 21645, 'synset': 'deltoid_leaf.n.01', 'name': 'deltoid_leaf'}, {'id': 21646, 'synset': 'ensiform_leaf.n.01', 'name': 'ensiform_leaf'}, {'id': 21647, 'synset': 'linear_leaf.n.01', 'name': 'linear_leaf'}, {'id': 21648, 'synset': 'lyrate_leaf.n.01', 'name': 'lyrate_leaf'}, {'id': 21649, 'synset': 'obtuse_leaf.n.01', 'name': 'obtuse_leaf'}, {'id': 21650, 'synset': 'oblanceolate_leaf.n.01', 'name': 'oblanceolate_leaf'}, {'id': 21651, 'synset': 'pandurate_leaf.n.01', 'name': 'pandurate_leaf'}, {'id': 21652, 'synset': 'reniform_leaf.n.01', 'name': 'reniform_leaf'}, {'id': 21653, 'synset': 'spatulate_leaf.n.01', 'name': 'spatulate_leaf'}, {'id': 21654, 'synset': 'even-pinnate_leaf.n.01', 'name': 'even-pinnate_leaf'}, {'id': 21655, 'synset': 'odd-pinnate_leaf.n.01', 'name': 'odd-pinnate_leaf'}, {'id': 21656, 'synset': 'pedate_leaf.n.01', 'name': 'pedate_leaf'}, {'id': 21657, 'synset': 'crenate_leaf.n.01', 'name': 'crenate_leaf'}, {'id': 21658, 'synset': 'dentate_leaf.n.01', 'name': 'dentate_leaf'}, {'id': 21659, 'synset': 'denticulate_leaf.n.01', 'name': 'denticulate_leaf'}, {'id': 21660, 'synset': 'erose_leaf.n.01', 'name': 'erose_leaf'}, {'id': 21661, 'synset': 'runcinate_leaf.n.01', 'name': 'runcinate_leaf'}, {'id': 21662, 'synset': 'prickly-edged_leaf.n.01', 'name': 'prickly-edged_leaf'}, {'id': 21663, 'synset': 'deadwood.n.01', 'name': 'deadwood'}, {'id': 21664, 'synset': 'haulm.n.01', 'name': 'haulm'}, {'id': 21665, 'synset': 'branchlet.n.01', 'name': 'branchlet'}, {'id': 21666, 'synset': 'osier.n.01', 'name': 'osier'}, {'id': 21667, 'synset': 'giant_scrambling_fern.n.01', 'name': 'giant_scrambling_fern'}, {'id': 21668, 'synset': 'umbrella_fern.n.01', 'name': 'umbrella_fern'}, {'id': 21669, 'synset': 'floating_fern.n.02', 'name': 'floating_fern'}, {'id': 21670, 'synset': 'polypody.n.01', 'name': 'polypody'}, {'id': 21671, 'synset': 'licorice_fern.n.01', 'name': 'licorice_fern'}, {'id': 21672, 'synset': 'grey_polypody.n.01', 'name': 'grey_polypody'}, {'id': 21673, 'synset': 'leatherleaf.n.01', 'name': 'leatherleaf'}, {'id': 21674, 'synset': 'rock_polypody.n.01', 'name': 'rock_polypody'}, {'id': 21675, 'synset': 'common_polypody.n.01', 'name': 'common_polypody'}, {'id': 21676, 'synset': "bear's-paw_fern.n.01", 'name': "bear's-paw_fern"}, {'id': 21677, 'synset': 'strap_fern.n.01', 'name': 'strap_fern'}, {'id': 21678, 'synset': 'florida_strap_fern.n.01', 'name': 'Florida_strap_fern'}, {'id': 21679, 'synset': 'basket_fern.n.02', 'name': 'basket_fern'}, {'id': 21680, 'synset': 'snake_polypody.n.01', 'name': 'snake_polypody'}, {'id': 21681, 'synset': "climbing_bird's_nest_fern.n.01", 'name': "climbing_bird's_nest_fern"}, {'id': 21682, 'synset': 'golden_polypody.n.01', 'name': 'golden_polypody'}, {'id': 21683, 'synset': 'staghorn_fern.n.01', 'name': 'staghorn_fern'}, {'id': 21684, 'synset': 'south_american_staghorn.n.01', 'name': 'South_American_staghorn'}, {'id': 21685, 'synset': 'common_staghorn_fern.n.01', 'name': 'common_staghorn_fern'}, {'id': 21686, 'synset': 'felt_fern.n.01', 'name': 'felt_fern'}, {'id': 21687, 'synset': 'potato_fern.n.02', 'name': 'potato_fern'}, {'id': 21688, 'synset': 'myrmecophyte.n.01', 'name': 'myrmecophyte'}, {'id': 21689, 'synset': 'grass_fern.n.01', 'name': 'grass_fern'}, {'id': 21690, 'synset': 'spleenwort.n.01', 'name': 'spleenwort'}, {'id': 21691, 'synset': 'black_spleenwort.n.01', 'name': 'black_spleenwort'}, {'id': 21692, 'synset': "bird's_nest_fern.n.01", 'name': "bird's_nest_fern"}, {'id': 21693, 'synset': 'ebony_spleenwort.n.01', 'name': 'ebony_spleenwort'}, {'id': 21694, 'synset': 'black-stem_spleenwort.n.01', 'name': 'black-stem_spleenwort'}, {'id': 21695, 'synset': 'walking_fern.n.01', 'name': 'walking_fern'}, {'id': 21696, 'synset': 'green_spleenwort.n.01', 'name': 'green_spleenwort'}, {'id': 21697, 'synset': 'mountain_spleenwort.n.01', 'name': 'mountain_spleenwort'}, {'id': 21698, 'synset': 'lobed_spleenwort.n.01', 'name': 'lobed_spleenwort'}, {'id': 21699, 'synset': 'lanceolate_spleenwort.n.01', 'name': 'lanceolate_spleenwort'}, {'id': 21700, 'synset': "hart's-tongue.n.02", 'name': "hart's-tongue"}, {'id': 21701, 'synset': 'scale_fern.n.01', 'name': 'scale_fern'}, {'id': 21702, 'synset': 'scolopendrium.n.01', 'name': 'scolopendrium'}, {'id': 21703, 'synset': 'deer_fern.n.01', 'name': 'deer_fern'}, {'id': 21704, 'synset': 'doodia.n.01', 'name': 'doodia'}, {'id': 21705, 'synset': 'chain_fern.n.01', 'name': 'chain_fern'}, {'id': 21706, 'synset': 'virginia_chain_fern.n.01', 'name': 'Virginia_chain_fern'}, {'id': 21707, 'synset': 'silver_tree_fern.n.01', 'name': 'silver_tree_fern'}, {'id': 21708, 'synset': 'davallia.n.01', 'name': 'davallia'}, {'id': 21709, 'synset': "hare's-foot_fern.n.01", 'name': "hare's-foot_fern"}, {'id': 21710, 'synset': "canary_island_hare's_foot_fern.n.01", 'name': "Canary_Island_hare's_foot_fern"}, {'id': 21711, 'synset': "squirrel's-foot_fern.n.01", 'name': "squirrel's-foot_fern"}, {'id': 21712, 'synset': 'bracken.n.01', 'name': 'bracken'}, {'id': 21713, 'synset': 'soft_tree_fern.n.01', 'name': 'soft_tree_fern'}, {'id': 21714, 'synset': 'scythian_lamb.n.01', 'name': 'Scythian_lamb'}, {'id': 21715, 'synset': 'false_bracken.n.01', 'name': 'false_bracken'}, {'id': 21716, 'synset': 'thyrsopteris.n.01', 'name': 'thyrsopteris'}, {'id': 21717, 'synset': 'shield_fern.n.01', 'name': 'shield_fern'}, {'id': 21718, 'synset': 'broad_buckler-fern.n.01', 'name': 'broad_buckler-fern'}, {'id': 21719, 'synset': 'fragrant_cliff_fern.n.01', 'name': 'fragrant_cliff_fern'}, {'id': 21720, 'synset': "goldie's_fern.n.01", 'name': "Goldie's_fern"}, {'id': 21721, 'synset': 'wood_fern.n.01', 'name': 'wood_fern'}, {'id': 21722, 'synset': 'male_fern.n.01', 'name': 'male_fern'}, {'id': 21723, 'synset': 'marginal_wood_fern.n.01', 'name': 'marginal_wood_fern'}, {'id': 21724, 'synset': 'mountain_male_fern.n.01', 'name': 'mountain_male_fern'}, {'id': 21725, 'synset': 'lady_fern.n.01', 'name': 'lady_fern'}, {'id': 21726, 'synset': 'alpine_lady_fern.n.01', 'name': 'Alpine_lady_fern'}, {'id': 21727, 'synset': 'silvery_spleenwort.n.02', 'name': 'silvery_spleenwort'}, {'id': 21728, 'synset': 'holly_fern.n.02', 'name': 'holly_fern'}, {'id': 21729, 'synset': 'bladder_fern.n.01', 'name': 'bladder_fern'}, {'id': 21730, 'synset': 'brittle_bladder_fern.n.01', 'name': 'brittle_bladder_fern'}, {'id': 21731, 'synset': 'mountain_bladder_fern.n.01', 'name': 'mountain_bladder_fern'}, {'id': 21732, 'synset': 'bulblet_fern.n.01', 'name': 'bulblet_fern'}, {'id': 21733, 'synset': 'silvery_spleenwort.n.01', 'name': 'silvery_spleenwort'}, {'id': 21734, 'synset': 'oak_fern.n.01', 'name': 'oak_fern'}, {'id': 21735, 'synset': 'limestone_fern.n.01', 'name': 'limestone_fern'}, {'id': 21736, 'synset': 'ostrich_fern.n.01', 'name': 'ostrich_fern'}, {'id': 21737, 'synset': "hart's-tongue.n.01", 'name': "hart's-tongue"}, {'id': 21738, 'synset': 'sensitive_fern.n.01', 'name': 'sensitive_fern'}, {'id': 21739, 'synset': 'christmas_fern.n.01', 'name': 'Christmas_fern'}, {'id': 21740, 'synset': 'holly_fern.n.01', 'name': 'holly_fern'}, {'id': 21741, 'synset': "braun's_holly_fern.n.01", 'name': "Braun's_holly_fern"}, {'id': 21742, 'synset': 'western_holly_fern.n.01', 'name': 'western_holly_fern'}, {'id': 21743, 'synset': 'soft_shield_fern.n.01', 'name': 'soft_shield_fern'}, {'id': 21744, 'synset': 'leather_fern.n.02', 'name': 'leather_fern'}, {'id': 21745, 'synset': 'button_fern.n.02', 'name': 'button_fern'}, {'id': 21746, 'synset': 'indian_button_fern.n.01', 'name': 'Indian_button_fern'}, {'id': 21747, 'synset': 'woodsia.n.01', 'name': 'woodsia'}, {'id': 21748, 'synset': 'rusty_woodsia.n.01', 'name': 'rusty_woodsia'}, {'id': 21749, 'synset': 'alpine_woodsia.n.01', 'name': 'Alpine_woodsia'}, {'id': 21750, 'synset': 'smooth_woodsia.n.01', 'name': 'smooth_woodsia'}, {'id': 21751, 'synset': 'boston_fern.n.01', 'name': 'Boston_fern'}, {'id': 21752, 'synset': 'basket_fern.n.01', 'name': 'basket_fern'}, {'id': 21753, 'synset': 'golden_fern.n.02', 'name': 'golden_fern'}, {'id': 21754, 'synset': 'maidenhair.n.01', 'name': 'maidenhair'}, {'id': 21755, 'synset': 'common_maidenhair.n.01', 'name': 'common_maidenhair'}, {'id': 21756, 'synset': 'american_maidenhair_fern.n.01', 'name': 'American_maidenhair_fern'}, {'id': 21757, 'synset': 'bermuda_maidenhair.n.01', 'name': 'Bermuda_maidenhair'}, {'id': 21758, 'synset': 'brittle_maidenhair.n.01', 'name': 'brittle_maidenhair'}, {'id': 21759, 'synset': 'farley_maidenhair.n.01', 'name': 'Farley_maidenhair'}, {'id': 21760, 'synset': 'annual_fern.n.01', 'name': 'annual_fern'}, {'id': 21761, 'synset': 'lip_fern.n.01', 'name': 'lip_fern'}, {'id': 21762, 'synset': 'smooth_lip_fern.n.01', 'name': 'smooth_lip_fern'}, {'id': 21763, 'synset': 'lace_fern.n.01', 'name': 'lace_fern'}, {'id': 21764, 'synset': 'wooly_lip_fern.n.01', 'name': 'wooly_lip_fern'}, {'id': 21765, 'synset': 'southwestern_lip_fern.n.01', 'name': 'southwestern_lip_fern'}, {'id': 21766, 'synset': 'bamboo_fern.n.01', 'name': 'bamboo_fern'}, {'id': 21767, 'synset': 'american_rock_brake.n.01', 'name': 'American_rock_brake'}, {'id': 21768, 'synset': 'european_parsley_fern.n.01', 'name': 'European_parsley_fern'}, {'id': 21769, 'synset': 'hand_fern.n.01', 'name': 'hand_fern'}, {'id': 21770, 'synset': 'cliff_brake.n.01', 'name': 'cliff_brake'}, {'id': 21771, 'synset': 'coffee_fern.n.01', 'name': 'coffee_fern'}, {'id': 21772, 'synset': 'purple_rock_brake.n.01', 'name': 'purple_rock_brake'}, {'id': 21773, 'synset': "bird's-foot_fern.n.01", 'name': "bird's-foot_fern"}, {'id': 21774, 'synset': 'button_fern.n.01', 'name': 'button_fern'}, {'id': 21775, 'synset': 'silver_fern.n.02', 'name': 'silver_fern'}, {'id': 21776, 'synset': 'golden_fern.n.01', 'name': 'golden_fern'}, {'id': 21777, 'synset': 'gold_fern.n.01', 'name': 'gold_fern'}, {'id': 21778, 'synset': 'pteris_cretica.n.01', 'name': 'Pteris_cretica'}, {'id': 21779, 'synset': 'spider_brake.n.01', 'name': 'spider_brake'}, {'id': 21780, 'synset': 'ribbon_fern.n.01', 'name': 'ribbon_fern'}, {'id': 21781, 'synset': 'potato_fern.n.01', 'name': 'potato_fern'}, {'id': 21782, 'synset': 'angiopteris.n.01', 'name': 'angiopteris'}, {'id': 21783, 'synset': 'skeleton_fork_fern.n.01', 'name': 'skeleton_fork_fern'}, {'id': 21784, 'synset': 'horsetail.n.01', 'name': 'horsetail'}, {'id': 21785, 'synset': 'common_horsetail.n.01', 'name': 'common_horsetail'}, {'id': 21786, 'synset': 'swamp_horsetail.n.01', 'name': 'swamp_horsetail'}, {'id': 21787, 'synset': 'scouring_rush.n.01', 'name': 'scouring_rush'}, {'id': 21788, 'synset': 'marsh_horsetail.n.01', 'name': 'marsh_horsetail'}, {'id': 21789, 'synset': 'wood_horsetail.n.01', 'name': 'wood_horsetail'}, {'id': 21790, 'synset': 'variegated_horsetail.n.01', 'name': 'variegated_horsetail'}, {'id': 21791, 'synset': 'club_moss.n.01', 'name': 'club_moss'}, {'id': 21792, 'synset': 'shining_clubmoss.n.01', 'name': 'shining_clubmoss'}, {'id': 21793, 'synset': 'alpine_clubmoss.n.01', 'name': 'alpine_clubmoss'}, {'id': 21794, 'synset': 'fir_clubmoss.n.01', 'name': 'fir_clubmoss'}, {'id': 21795, 'synset': 'ground_cedar.n.01', 'name': 'ground_cedar'}, {'id': 21796, 'synset': 'ground_fir.n.01', 'name': 'ground_fir'}, {'id': 21797, 'synset': 'foxtail_grass.n.01', 'name': 'foxtail_grass'}, {'id': 21798, 'synset': 'spikemoss.n.01', 'name': 'spikemoss'}, {'id': 21799, 'synset': 'meadow_spikemoss.n.01', 'name': 'meadow_spikemoss'}, {'id': 21800, 'synset': 'desert_selaginella.n.01', 'name': 'desert_selaginella'}, {'id': 21801, 'synset': 'resurrection_plant.n.01', 'name': 'resurrection_plant'}, {'id': 21802, 'synset': 'florida_selaginella.n.01', 'name': 'florida_selaginella'}, {'id': 21803, 'synset': 'quillwort.n.01', 'name': 'quillwort'}, {'id': 21804, 'synset': 'earthtongue.n.01', 'name': 'earthtongue'}, {'id': 21805, 'synset': 'snuffbox_fern.n.01', 'name': 'snuffbox_fern'}, {'id': 21806, 'synset': 'christella.n.01', 'name': 'christella'}, {'id': 21807, 'synset': 'mountain_fern.n.01', 'name': 'mountain_fern'}, {'id': 21808, 'synset': 'new_york_fern.n.01', 'name': 'New_York_fern'}, {'id': 21809, 'synset': 'massachusetts_fern.n.01', 'name': 'Massachusetts_fern'}, {'id': 21810, 'synset': 'beech_fern.n.01', 'name': 'beech_fern'}, {'id': 21811, 'synset': 'broad_beech_fern.n.01', 'name': 'broad_beech_fern'}, {'id': 21812, 'synset': 'long_beech_fern.n.01', 'name': 'long_beech_fern'}, {'id': 21813, 'synset': 'shoestring_fungus.n.01', 'name': 'shoestring_fungus'}, {'id': 21814, 'synset': 'armillaria_caligata.n.01', 'name': 'Armillaria_caligata'}, {'id': 21815, 'synset': 'armillaria_ponderosa.n.01', 'name': 'Armillaria_ponderosa'}, {'id': 21816, 'synset': 'armillaria_zelleri.n.01', 'name': 'Armillaria_zelleri'}, {'id': 21817, 'synset': 'honey_mushroom.n.01', 'name': 'honey_mushroom'}, {'id': 21818, 'synset': 'milkweed.n.01', 'name': 'milkweed'}, {'id': 21819, 'synset': 'white_milkweed.n.01', 'name': 'white_milkweed'}, {'id': 21820, 'synset': 'poke_milkweed.n.01', 'name': 'poke_milkweed'}, {'id': 21821, 'synset': 'swamp_milkweed.n.01', 'name': 'swamp_milkweed'}, {'id': 21822, 'synset': "mead's_milkweed.n.01", 'name': "Mead's_milkweed"}, {'id': 21823, 'synset': 'purple_silkweed.n.01', 'name': 'purple_silkweed'}, {'id': 21824, 'synset': 'showy_milkweed.n.01', 'name': 'showy_milkweed'}, {'id': 21825, 'synset': 'poison_milkweed.n.01', 'name': 'poison_milkweed'}, {'id': 21826, 'synset': 'butterfly_weed.n.01', 'name': 'butterfly_weed'}, {'id': 21827, 'synset': 'whorled_milkweed.n.01', 'name': 'whorled_milkweed'}, {'id': 21828, 'synset': 'cruel_plant.n.01', 'name': 'cruel_plant'}, {'id': 21829, 'synset': 'wax_plant.n.01', 'name': 'wax_plant'}, {'id': 21830, 'synset': 'silk_vine.n.01', 'name': 'silk_vine'}, {'id': 21831, 'synset': 'stapelia.n.01', 'name': 'stapelia'}, {'id': 21832, 'synset': 'stapelias_asterias.n.01', 'name': 'Stapelias_asterias'}, {'id': 21833, 'synset': 'stephanotis.n.01', 'name': 'stephanotis'}, {'id': 21834, 'synset': 'madagascar_jasmine.n.01', 'name': 'Madagascar_jasmine'}, {'id': 21835, 'synset': 'negro_vine.n.01', 'name': 'negro_vine'}, {'id': 21836, 'synset': 'zygospore.n.01', 'name': 'zygospore'}, {'id': 21837, 'synset': 'tree_of_knowledge.n.01', 'name': 'tree_of_knowledge'}, {'id': 21838, 'synset': 'orangery.n.01', 'name': 'orangery'}, {'id': 21839, 'synset': 'pocketbook.n.01', 'name': 'pocketbook'}, {'id': 21840, 'synset': 'shit.n.04', 'name': 'shit'}, {'id': 21841, 'synset': 'cordage.n.01', 'name': 'cordage'}, {'id': 21842, 'synset': 'yard.n.01', 'name': 'yard'}, {'id': 21843, 'synset': 'extremum.n.02', 'name': 'extremum'}, {'id': 21844, 'synset': 'leaf_shape.n.01', 'name': 'leaf_shape'}, {'id': 21845, 'synset': 'equilateral.n.01', 'name': 'equilateral'}, {'id': 21846, 'synset': 'figure.n.06', 'name': 'figure'}, {'id': 21847, 'synset': 'pencil.n.03', 'name': 'pencil'}, {'id': 21848, 'synset': 'plane_figure.n.01', 'name': 'plane_figure'}, {'id': 21849, 'synset': 'solid_figure.n.01', 'name': 'solid_figure'}, {'id': 21850, 'synset': 'line.n.04', 'name': 'line'}, {'id': 21851, 'synset': 'bulb.n.04', 'name': 'bulb'}, {'id': 21852, 'synset': 'convex_shape.n.01', 'name': 'convex_shape'}, {'id': 21853, 'synset': 'concave_shape.n.01', 'name': 'concave_shape'}, {'id': 21854, 'synset': 'cylinder.n.01', 'name': 'cylinder'}, {'id': 21855, 'synset': 'round_shape.n.01', 'name': 'round_shape'}, {'id': 21856, 'synset': 'heart.n.07', 'name': 'heart'}, {'id': 21857, 'synset': 'polygon.n.01', 'name': 'polygon'}, {'id': 21858, 'synset': 'convex_polygon.n.01', 'name': 'convex_polygon'}, {'id': 21859, 'synset': 'concave_polygon.n.01', 'name': 'concave_polygon'}, {'id': 21860, 'synset': 'reentrant_polygon.n.01', 'name': 'reentrant_polygon'}, {'id': 21861, 'synset': 'amorphous_shape.n.01', 'name': 'amorphous_shape'}, {'id': 21862, 'synset': 'closed_curve.n.01', 'name': 'closed_curve'}, {'id': 21863, 'synset': 'simple_closed_curve.n.01', 'name': 'simple_closed_curve'}, {'id': 21864, 'synset': 's-shape.n.01', 'name': 'S-shape'}, {'id': 21865, 'synset': 'wave.n.07', 'name': 'wave'}, {'id': 21866, 'synset': 'extrados.n.01', 'name': 'extrados'}, {'id': 21867, 'synset': 'hook.n.02', 'name': 'hook'}, {'id': 21868, 'synset': 'envelope.n.03', 'name': 'envelope'}, {'id': 21869, 'synset': 'bight.n.02', 'name': 'bight'}, {'id': 21870, 'synset': 'diameter.n.02', 'name': 'diameter'}, {'id': 21871, 'synset': 'cone.n.02', 'name': 'cone'}, {'id': 21872, 'synset': 'funnel.n.01', 'name': 'funnel'}, {'id': 21873, 'synset': 'oblong.n.01', 'name': 'oblong'}, {'id': 21874, 'synset': 'circle.n.01', 'name': 'circle'}, {'id': 21875, 'synset': 'circle.n.03', 'name': 'circle'}, {'id': 21876, 'synset': 'equator.n.02', 'name': 'equator'}, {'id': 21877, 'synset': 'scallop.n.01', 'name': 'scallop'}, {'id': 21878, 'synset': 'ring.n.02', 'name': 'ring'}, {'id': 21879, 'synset': 'loop.n.02', 'name': 'loop'}, {'id': 21880, 'synset': 'bight.n.01', 'name': 'bight'}, {'id': 21881, 'synset': 'helix.n.01', 'name': 'helix'}, {'id': 21882, 'synset': 'element_of_a_cone.n.01', 'name': 'element_of_a_cone'}, {'id': 21883, 'synset': 'element_of_a_cylinder.n.01', 'name': 'element_of_a_cylinder'}, {'id': 21884, 'synset': 'ellipse.n.01', 'name': 'ellipse'}, {'id': 21885, 'synset': 'quadrate.n.02', 'name': 'quadrate'}, {'id': 21886, 'synset': 'triangle.n.01', 'name': 'triangle'}, {'id': 21887, 'synset': 'acute_triangle.n.01', 'name': 'acute_triangle'}, {'id': 21888, 'synset': 'isosceles_triangle.n.01', 'name': 'isosceles_triangle'}, {'id': 21889, 'synset': 'obtuse_triangle.n.01', 'name': 'obtuse_triangle'}, {'id': 21890, 'synset': 'right_triangle.n.01', 'name': 'right_triangle'}, {'id': 21891, 'synset': 'scalene_triangle.n.01', 'name': 'scalene_triangle'}, {'id': 21892, 'synset': 'parallel.n.03', 'name': 'parallel'}, {'id': 21893, 'synset': 'trapezoid.n.01', 'name': 'trapezoid'}, {'id': 21894, 'synset': 'star.n.05', 'name': 'star'}, {'id': 21895, 'synset': 'pentagon.n.03', 'name': 'pentagon'}, {'id': 21896, 'synset': 'hexagon.n.01', 'name': 'hexagon'}, {'id': 21897, 'synset': 'heptagon.n.01', 'name': 'heptagon'}, {'id': 21898, 'synset': 'octagon.n.01', 'name': 'octagon'}, {'id': 21899, 'synset': 'nonagon.n.01', 'name': 'nonagon'}, {'id': 21900, 'synset': 'decagon.n.01', 'name': 'decagon'}, {'id': 21901, 'synset': 'rhombus.n.01', 'name': 'rhombus'}, {'id': 21902, 'synset': 'spherical_polygon.n.01', 'name': 'spherical_polygon'}, {'id': 21903, 'synset': 'spherical_triangle.n.01', 'name': 'spherical_triangle'}, {'id': 21904, 'synset': 'convex_polyhedron.n.01', 'name': 'convex_polyhedron'}, {'id': 21905, 'synset': 'concave_polyhedron.n.01', 'name': 'concave_polyhedron'}, {'id': 21906, 'synset': 'cuboid.n.01', 'name': 'cuboid'}, {'id': 21907, 'synset': 'quadrangular_prism.n.01', 'name': 'quadrangular_prism'}, {'id': 21908, 'synset': 'bell.n.05', 'name': 'bell'}, {'id': 21909, 'synset': 'angular_distance.n.01', 'name': 'angular_distance'}, {'id': 21910, 'synset': 'true_anomaly.n.01', 'name': 'true_anomaly'}, {'id': 21911, 'synset': 'spherical_angle.n.01', 'name': 'spherical_angle'}, {'id': 21912, 'synset': 'angle_of_refraction.n.01', 'name': 'angle_of_refraction'}, {'id': 21913, 'synset': 'acute_angle.n.01', 'name': 'acute_angle'}, {'id': 21914, 'synset': 'groove.n.01', 'name': 'groove'}, {'id': 21915, 'synset': 'rut.n.01', 'name': 'rut'}, {'id': 21916, 'synset': 'bulge.n.01', 'name': 'bulge'}, {'id': 21917, 'synset': 'belly.n.03', 'name': 'belly'}, {'id': 21918, 'synset': 'bow.n.05', 'name': 'bow'}, {'id': 21919, 'synset': 'crescent.n.01', 'name': 'crescent'}, {'id': 21920, 'synset': 'ellipsoid.n.01', 'name': 'ellipsoid'}, {'id': 21921, 'synset': 'hypotenuse.n.01', 'name': 'hypotenuse'}, {'id': 21922, 'synset': 'balance.n.04', 'name': 'balance'}, {'id': 21923, 'synset': 'conformation.n.01', 'name': 'conformation'}, {'id': 21924, 'synset': 'symmetry.n.02', 'name': 'symmetry'}, {'id': 21925, 'synset': 'spheroid.n.01', 'name': 'spheroid'}, {'id': 21926, 'synset': 'spherule.n.01', 'name': 'spherule'}, {'id': 21927, 'synset': 'toroid.n.01', 'name': 'toroid'}, {'id': 21928, 'synset': 'column.n.04', 'name': 'column'}, {'id': 21929, 'synset': 'barrel.n.03', 'name': 'barrel'}, {'id': 21930, 'synset': 'pipe.n.03', 'name': 'pipe'}, {'id': 21931, 'synset': 'pellet.n.01', 'name': 'pellet'}, {'id': 21932, 'synset': 'bolus.n.01', 'name': 'bolus'}, {'id': 21933, 'synset': 'dewdrop.n.01', 'name': 'dewdrop'}, {'id': 21934, 'synset': 'ridge.n.02', 'name': 'ridge'}, {'id': 21935, 'synset': 'rim.n.01', 'name': 'rim'}, {'id': 21936, 'synset': 'taper.n.01', 'name': 'taper'}, {'id': 21937, 'synset': 'boundary.n.02', 'name': 'boundary'}, {'id': 21938, 'synset': 'incisure.n.01', 'name': 'incisure'}, {'id': 21939, 'synset': 'notch.n.01', 'name': 'notch'}, {'id': 21940, 'synset': 'wrinkle.n.01', 'name': 'wrinkle'}, {'id': 21941, 'synset': 'dermatoglyphic.n.01', 'name': 'dermatoglyphic'}, {'id': 21942, 'synset': 'frown_line.n.01', 'name': 'frown_line'}, {'id': 21943, 'synset': 'line_of_life.n.01', 'name': 'line_of_life'}, {'id': 21944, 'synset': 'line_of_heart.n.01', 'name': 'line_of_heart'}, {'id': 21945, 'synset': 'crevice.n.01', 'name': 'crevice'}, {'id': 21946, 'synset': 'cleft.n.01', 'name': 'cleft'}, {'id': 21947, 'synset': 'roulette.n.01', 'name': 'roulette'}, {'id': 21948, 'synset': 'node.n.01', 'name': 'node'}, {'id': 21949, 'synset': 'tree.n.02', 'name': 'tree'}, {'id': 21950, 'synset': 'stemma.n.01', 'name': 'stemma'}, {'id': 21951, 'synset': 'brachium.n.01', 'name': 'brachium'}, {'id': 21952, 'synset': 'fork.n.03', 'name': 'fork'}, {'id': 21953, 'synset': 'block.n.03', 'name': 'block'}, {'id': 21954, 'synset': 'ovoid.n.01', 'name': 'ovoid'}, {'id': 21955, 'synset': 'tetrahedron.n.01', 'name': 'tetrahedron'}, {'id': 21956, 'synset': 'pentahedron.n.01', 'name': 'pentahedron'}, {'id': 21957, 'synset': 'hexahedron.n.01', 'name': 'hexahedron'}, {'id': 21958, 'synset': 'regular_polyhedron.n.01', 'name': 'regular_polyhedron'}, {'id': 21959, 'synset': 'polyhedral_angle.n.01', 'name': 'polyhedral_angle'}, {'id': 21960, 'synset': 'cube.n.01', 'name': 'cube'}, {'id': 21961, 'synset': 'truncated_pyramid.n.01', 'name': 'truncated_pyramid'}, {'id': 21962, 'synset': 'truncated_cone.n.01', 'name': 'truncated_cone'}, {'id': 21963, 'synset': 'tail.n.03', 'name': 'tail'}, {'id': 21964, 'synset': 'tongue.n.03', 'name': 'tongue'}, {'id': 21965, 'synset': 'trapezohedron.n.01', 'name': 'trapezohedron'}, {'id': 21966, 'synset': 'wedge.n.01', 'name': 'wedge'}, {'id': 21967, 'synset': 'keel.n.01', 'name': 'keel'}, {'id': 21968, 'synset': 'place.n.06', 'name': 'place'}, {'id': 21969, 'synset': 'herpes.n.01', 'name': 'herpes'}, {'id': 21970, 'synset': 'chlamydia.n.01', 'name': 'chlamydia'}, {'id': 21971, 'synset': 'wall.n.04', 'name': 'wall'}, {'id': 21972, 'synset': 'micronutrient.n.01', 'name': 'micronutrient'}, {'id': 21973, 'synset': 'chyme.n.01', 'name': 'chyme'}, {'id': 21974, 'synset': 'ragweed_pollen.n.01', 'name': 'ragweed_pollen'}, {'id': 21975, 'synset': 'pina_cloth.n.01', 'name': 'pina_cloth'}, {'id': 21976, 'synset': 'chlorobenzylidenemalononitrile.n.01', 'name': 'chlorobenzylidenemalononitrile'}, {'id': 21977, 'synset': 'carbon.n.01', 'name': 'carbon'}, {'id': 21978, 'synset': 'charcoal.n.01', 'name': 'charcoal'}, {'id': 21979, 'synset': 'rock.n.02', 'name': 'rock'}, {'id': 21980, 'synset': 'gravel.n.01', 'name': 'gravel'}, {'id': 21981, 'synset': 'aflatoxin.n.01', 'name': 'aflatoxin'}, {'id': 21982, 'synset': 'alpha-tocopheral.n.01', 'name': 'alpha-tocopheral'}, {'id': 21983, 'synset': 'leopard.n.01', 'name': 'leopard'}, {'id': 21984, 'synset': 'bricks_and_mortar.n.01', 'name': 'bricks_and_mortar'}, {'id': 21985, 'synset': 'lagging.n.01', 'name': 'lagging'}, {'id': 21986, 'synset': 'hydraulic_cement.n.01', 'name': 'hydraulic_cement'}, {'id': 21987, 'synset': 'choline.n.01', 'name': 'choline'}, {'id': 21988, 'synset': 'concrete.n.01', 'name': 'concrete'}, {'id': 21989, 'synset': 'glass_wool.n.01', 'name': 'glass_wool'}, {'id': 21990, 'synset': 'soil.n.02', 'name': 'soil'}, {'id': 21991, 'synset': 'high_explosive.n.01', 'name': 'high_explosive'}, {'id': 21992, 'synset': 'litter.n.02', 'name': 'litter'}, {'id': 21993, 'synset': 'fish_meal.n.01', 'name': 'fish_meal'}, {'id': 21994, 'synset': 'greek_fire.n.01', 'name': 'Greek_fire'}, {'id': 21995, 'synset': 'culture_medium.n.01', 'name': 'culture_medium'}, {'id': 21996, 'synset': 'agar.n.01', 'name': 'agar'}, {'id': 21997, 'synset': 'blood_agar.n.01', 'name': 'blood_agar'}, {'id': 21998, 'synset': 'hip_tile.n.01', 'name': 'hip_tile'}, {'id': 21999, 'synset': 'hyacinth.n.01', 'name': 'hyacinth'}, {'id': 22000, 'synset': 'hydroxide_ion.n.01', 'name': 'hydroxide_ion'}, {'id': 22001, 'synset': 'ice.n.01', 'name': 'ice'}, {'id': 22002, 'synset': 'inositol.n.01', 'name': 'inositol'}, {'id': 22003, 'synset': 'linoleum.n.01', 'name': 'linoleum'}, {'id': 22004, 'synset': 'lithia_water.n.01', 'name': 'lithia_water'}, {'id': 22005, 'synset': 'lodestone.n.01', 'name': 'lodestone'}, {'id': 22006, 'synset': 'pantothenic_acid.n.01', 'name': 'pantothenic_acid'}, {'id': 22007, 'synset': 'paper.n.01', 'name': 'paper'}, {'id': 22008, 'synset': 'papyrus.n.01', 'name': 'papyrus'}, {'id': 22009, 'synset': 'pantile.n.01', 'name': 'pantile'}, {'id': 22010, 'synset': 'blacktop.n.01', 'name': 'blacktop'}, {'id': 22011, 'synset': 'tarmacadam.n.01', 'name': 'tarmacadam'}, {'id': 22012, 'synset': 'paving.n.01', 'name': 'paving'}, {'id': 22013, 'synset': 'plaster.n.01', 'name': 'plaster'}, {'id': 22014, 'synset': 'poison_gas.n.01', 'name': 'poison_gas'}, {'id': 22015, 'synset': 'ridge_tile.n.01', 'name': 'ridge_tile'}, {'id': 22016, 'synset': 'roughcast.n.01', 'name': 'roughcast'}, {'id': 22017, 'synset': 'sand.n.01', 'name': 'sand'}, {'id': 22018, 'synset': 'spackle.n.01', 'name': 'spackle'}, {'id': 22019, 'synset': 'render.n.01', 'name': 'render'}, {'id': 22020, 'synset': 'wattle_and_daub.n.01', 'name': 'wattle_and_daub'}, {'id': 22021, 'synset': 'stucco.n.01', 'name': 'stucco'}, {'id': 22022, 'synset': 'tear_gas.n.01', 'name': 'tear_gas'}, {'id': 22023, 'synset': 'linseed.n.01', 'name': 'linseed'}, {'id': 22024, 'synset': 'vitamin.n.01', 'name': 'vitamin'}, {'id': 22025, 'synset': 'fat-soluble_vitamin.n.01', 'name': 'fat-soluble_vitamin'}, {'id': 22026, 'synset': 'water-soluble_vitamin.n.01', 'name': 'water-soluble_vitamin'}, {'id': 22027, 'synset': 'vitamin_a.n.01', 'name': 'vitamin_A'}, {'id': 22028, 'synset': 'vitamin_a1.n.01', 'name': 'vitamin_A1'}, {'id': 22029, 'synset': 'vitamin_a2.n.01', 'name': 'vitamin_A2'}, {'id': 22030, 'synset': 'b-complex_vitamin.n.01', 'name': 'B-complex_vitamin'}, {'id': 22031, 'synset': 'vitamin_b1.n.01', 'name': 'vitamin_B1'}, {'id': 22032, 'synset': 'vitamin_b12.n.01', 'name': 'vitamin_B12'}, {'id': 22033, 'synset': 'vitamin_b2.n.01', 'name': 'vitamin_B2'}, {'id': 22034, 'synset': 'vitamin_b6.n.01', 'name': 'vitamin_B6'}, {'id': 22035, 'synset': 'vitamin_bc.n.01', 'name': 'vitamin_Bc'}, {'id': 22036, 'synset': 'niacin.n.01', 'name': 'niacin'}, {'id': 22037, 'synset': 'vitamin_d.n.01', 'name': 'vitamin_D'}, {'id': 22038, 'synset': 'vitamin_e.n.01', 'name': 'vitamin_E'}, {'id': 22039, 'synset': 'biotin.n.01', 'name': 'biotin'}, {'id': 22040, 'synset': 'vitamin_k.n.01', 'name': 'vitamin_K'}, {'id': 22041, 'synset': 'vitamin_k1.n.01', 'name': 'vitamin_K1'}, {'id': 22042, 'synset': 'vitamin_k3.n.01', 'name': 'vitamin_K3'}, {'id': 22043, 'synset': 'vitamin_p.n.01', 'name': 'vitamin_P'}, {'id': 22044, 'synset': 'vitamin_c.n.01', 'name': 'vitamin_C'}, {'id': 22045, 'synset': 'planking.n.01', 'name': 'planking'}, {'id': 22046, 'synset': 'chipboard.n.01', 'name': 'chipboard'}, {'id': 22047, 'synset': 'knothole.n.01', 'name': 'knothole'}] # noqa \ No newline at end of file +CATEGORIES = [ + {"name": "aerosol_can", "id": 1, "frequency": "c", "synset": "aerosol.n.02"}, + {"name": "air_conditioner", "id": 2, "frequency": "f", "synset": "air_conditioner.n.01"}, + {"name": "airplane", "id": 3, "frequency": "f", "synset": "airplane.n.01"}, + {"name": "alarm_clock", "id": 4, "frequency": "f", "synset": "alarm_clock.n.01"}, + {"name": "alcohol", "id": 5, "frequency": "c", "synset": "alcohol.n.01"}, + {"name": "alligator", "id": 6, "frequency": "c", "synset": "alligator.n.02"}, + {"name": "almond", "id": 7, "frequency": "c", "synset": "almond.n.02"}, + {"name": "ambulance", "id": 8, "frequency": "c", "synset": "ambulance.n.01"}, + {"name": "amplifier", "id": 9, "frequency": "c", "synset": "amplifier.n.01"}, + {"name": "anklet", "id": 10, "frequency": "c", "synset": "anklet.n.03"}, + {"name": "antenna", "id": 11, "frequency": "f", "synset": "antenna.n.01"}, + {"name": "apple", "id": 12, "frequency": "f", "synset": "apple.n.01"}, + {"name": "applesauce", "id": 13, "frequency": "r", "synset": "applesauce.n.01"}, + {"name": "apricot", "id": 14, "frequency": "r", "synset": "apricot.n.02"}, + {"name": "apron", "id": 15, "frequency": "f", "synset": "apron.n.01"}, + {"name": "aquarium", "id": 16, "frequency": "c", "synset": "aquarium.n.01"}, + {"name": "arctic_(type_of_shoe)", "id": 17, "frequency": "r", "synset": "arctic.n.02"}, + {"name": "armband", "id": 18, "frequency": "c", "synset": "armband.n.02"}, + {"name": "armchair", "id": 19, "frequency": "f", "synset": "armchair.n.01"}, + {"name": "armoire", "id": 20, "frequency": "r", "synset": "armoire.n.01"}, + {"name": "armor", "id": 21, "frequency": "r", "synset": "armor.n.01"}, + {"name": "artichoke", "id": 22, "frequency": "c", "synset": "artichoke.n.02"}, + {"name": "trash_can", "id": 23, "frequency": "f", "synset": "ashcan.n.01"}, + {"name": "ashtray", "id": 24, "frequency": "c", "synset": "ashtray.n.01"}, + {"name": "asparagus", "id": 25, "frequency": "c", "synset": "asparagus.n.02"}, + {"name": "atomizer", "id": 26, "frequency": "c", "synset": "atomizer.n.01"}, + {"name": "avocado", "id": 27, "frequency": "f", "synset": "avocado.n.01"}, + {"name": "award", "id": 28, "frequency": "c", "synset": "award.n.02"}, + {"name": "awning", "id": 29, "frequency": "f", "synset": "awning.n.01"}, + {"name": "ax", "id": 30, "frequency": "r", "synset": "ax.n.01"}, + {"name": "baboon", "id": 31, "frequency": "r", "synset": "baboon.n.01"}, + {"name": "baby_buggy", "id": 32, "frequency": "f", "synset": "baby_buggy.n.01"}, + {"name": "basketball_backboard", "id": 33, "frequency": "c", "synset": "backboard.n.01"}, + {"name": "backpack", "id": 34, "frequency": "f", "synset": "backpack.n.01"}, + {"name": "handbag", "id": 35, "frequency": "f", "synset": "bag.n.04"}, + {"name": "suitcase", "id": 36, "frequency": "f", "synset": "bag.n.06"}, + {"name": "bagel", "id": 37, "frequency": "c", "synset": "bagel.n.01"}, + {"name": "bagpipe", "id": 38, "frequency": "r", "synset": "bagpipe.n.01"}, + {"name": "baguet", "id": 39, "frequency": "r", "synset": "baguet.n.01"}, + {"name": "bait", "id": 40, "frequency": "r", "synset": "bait.n.02"}, + {"name": "ball", "id": 41, "frequency": "f", "synset": "ball.n.06"}, + {"name": "ballet_skirt", "id": 42, "frequency": "r", "synset": "ballet_skirt.n.01"}, + {"name": "balloon", "id": 43, "frequency": "f", "synset": "balloon.n.01"}, + {"name": "bamboo", "id": 44, "frequency": "c", "synset": "bamboo.n.02"}, + {"name": "banana", "id": 45, "frequency": "f", "synset": "banana.n.02"}, + {"name": "Band_Aid", "id": 46, "frequency": "c", "synset": "band_aid.n.01"}, + {"name": "bandage", "id": 47, "frequency": "c", "synset": "bandage.n.01"}, + {"name": "bandanna", "id": 48, "frequency": "f", "synset": "bandanna.n.01"}, + {"name": "banjo", "id": 49, "frequency": "r", "synset": "banjo.n.01"}, + {"name": "banner", "id": 50, "frequency": "f", "synset": "banner.n.01"}, + {"name": "barbell", "id": 51, "frequency": "r", "synset": "barbell.n.01"}, + {"name": "barge", "id": 52, "frequency": "r", "synset": "barge.n.01"}, + {"name": "barrel", "id": 53, "frequency": "f", "synset": "barrel.n.02"}, + {"name": "barrette", "id": 54, "frequency": "c", "synset": "barrette.n.01"}, + {"name": "barrow", "id": 55, "frequency": "c", "synset": "barrow.n.03"}, + {"name": "baseball_base", "id": 56, "frequency": "f", "synset": "base.n.03"}, + {"name": "baseball", "id": 57, "frequency": "f", "synset": "baseball.n.02"}, + {"name": "baseball_bat", "id": 58, "frequency": "f", "synset": "baseball_bat.n.01"}, + {"name": "baseball_cap", "id": 59, "frequency": "f", "synset": "baseball_cap.n.01"}, + {"name": "baseball_glove", "id": 60, "frequency": "f", "synset": "baseball_glove.n.01"}, + {"name": "basket", "id": 61, "frequency": "f", "synset": "basket.n.01"}, + {"name": "basketball", "id": 62, "frequency": "c", "synset": "basketball.n.02"}, + {"name": "bass_horn", "id": 63, "frequency": "r", "synset": "bass_horn.n.01"}, + {"name": "bat_(animal)", "id": 64, "frequency": "c", "synset": "bat.n.01"}, + {"name": "bath_mat", "id": 65, "frequency": "f", "synset": "bath_mat.n.01"}, + {"name": "bath_towel", "id": 66, "frequency": "f", "synset": "bath_towel.n.01"}, + {"name": "bathrobe", "id": 67, "frequency": "c", "synset": "bathrobe.n.01"}, + {"name": "bathtub", "id": 68, "frequency": "f", "synset": "bathtub.n.01"}, + {"name": "batter_(food)", "id": 69, "frequency": "r", "synset": "batter.n.02"}, + {"name": "battery", "id": 70, "frequency": "c", "synset": "battery.n.02"}, + {"name": "beachball", "id": 71, "frequency": "r", "synset": "beach_ball.n.01"}, + {"name": "bead", "id": 72, "frequency": "c", "synset": "bead.n.01"}, + {"name": "bean_curd", "id": 73, "frequency": "c", "synset": "bean_curd.n.01"}, + {"name": "beanbag", "id": 74, "frequency": "c", "synset": "beanbag.n.01"}, + {"name": "beanie", "id": 75, "frequency": "f", "synset": "beanie.n.01"}, + {"name": "bear", "id": 76, "frequency": "f", "synset": "bear.n.01"}, + {"name": "bed", "id": 77, "frequency": "f", "synset": "bed.n.01"}, + {"name": "bedpan", "id": 78, "frequency": "r", "synset": "bedpan.n.01"}, + {"name": "bedspread", "id": 79, "frequency": "f", "synset": "bedspread.n.01"}, + {"name": "cow", "id": 80, "frequency": "f", "synset": "beef.n.01"}, + {"name": "beef_(food)", "id": 81, "frequency": "f", "synset": "beef.n.02"}, + {"name": "beeper", "id": 82, "frequency": "r", "synset": "beeper.n.01"}, + {"name": "beer_bottle", "id": 83, "frequency": "f", "synset": "beer_bottle.n.01"}, + {"name": "beer_can", "id": 84, "frequency": "c", "synset": "beer_can.n.01"}, + {"name": "beetle", "id": 85, "frequency": "r", "synset": "beetle.n.01"}, + {"name": "bell", "id": 86, "frequency": "f", "synset": "bell.n.01"}, + {"name": "bell_pepper", "id": 87, "frequency": "f", "synset": "bell_pepper.n.02"}, + {"name": "belt", "id": 88, "frequency": "f", "synset": "belt.n.02"}, + {"name": "belt_buckle", "id": 89, "frequency": "f", "synset": "belt_buckle.n.01"}, + {"name": "bench", "id": 90, "frequency": "f", "synset": "bench.n.01"}, + {"name": "beret", "id": 91, "frequency": "c", "synset": "beret.n.01"}, + {"name": "bib", "id": 92, "frequency": "c", "synset": "bib.n.02"}, + {"name": "Bible", "id": 93, "frequency": "r", "synset": "bible.n.01"}, + {"name": "bicycle", "id": 94, "frequency": "f", "synset": "bicycle.n.01"}, + {"name": "visor", "id": 95, "frequency": "f", "synset": "bill.n.09"}, + {"name": "billboard", "id": 96, "frequency": "f", "synset": "billboard.n.01"}, + {"name": "binder", "id": 97, "frequency": "c", "synset": "binder.n.03"}, + {"name": "binoculars", "id": 98, "frequency": "c", "synset": "binoculars.n.01"}, + {"name": "bird", "id": 99, "frequency": "f", "synset": "bird.n.01"}, + {"name": "birdfeeder", "id": 100, "frequency": "c", "synset": "bird_feeder.n.01"}, + {"name": "birdbath", "id": 101, "frequency": "c", "synset": "birdbath.n.01"}, + {"name": "birdcage", "id": 102, "frequency": "c", "synset": "birdcage.n.01"}, + {"name": "birdhouse", "id": 103, "frequency": "c", "synset": "birdhouse.n.01"}, + {"name": "birthday_cake", "id": 104, "frequency": "f", "synset": "birthday_cake.n.01"}, + {"name": "birthday_card", "id": 105, "frequency": "r", "synset": "birthday_card.n.01"}, + {"name": "pirate_flag", "id": 106, "frequency": "r", "synset": "black_flag.n.01"}, + {"name": "black_sheep", "id": 107, "frequency": "c", "synset": "black_sheep.n.02"}, + {"name": "blackberry", "id": 108, "frequency": "c", "synset": "blackberry.n.01"}, + {"name": "blackboard", "id": 109, "frequency": "f", "synset": "blackboard.n.01"}, + {"name": "blanket", "id": 110, "frequency": "f", "synset": "blanket.n.01"}, + {"name": "blazer", "id": 111, "frequency": "c", "synset": "blazer.n.01"}, + {"name": "blender", "id": 112, "frequency": "f", "synset": "blender.n.01"}, + {"name": "blimp", "id": 113, "frequency": "r", "synset": "blimp.n.02"}, + {"name": "blinker", "id": 114, "frequency": "f", "synset": "blinker.n.01"}, + {"name": "blouse", "id": 115, "frequency": "f", "synset": "blouse.n.01"}, + {"name": "blueberry", "id": 116, "frequency": "f", "synset": "blueberry.n.02"}, + {"name": "gameboard", "id": 117, "frequency": "r", "synset": "board.n.09"}, + {"name": "boat", "id": 118, "frequency": "f", "synset": "boat.n.01"}, + {"name": "bob", "id": 119, "frequency": "r", "synset": "bob.n.05"}, + {"name": "bobbin", "id": 120, "frequency": "c", "synset": "bobbin.n.01"}, + {"name": "bobby_pin", "id": 121, "frequency": "c", "synset": "bobby_pin.n.01"}, + {"name": "boiled_egg", "id": 122, "frequency": "c", "synset": "boiled_egg.n.01"}, + {"name": "bolo_tie", "id": 123, "frequency": "r", "synset": "bolo_tie.n.01"}, + {"name": "deadbolt", "id": 124, "frequency": "c", "synset": "bolt.n.03"}, + {"name": "bolt", "id": 125, "frequency": "f", "synset": "bolt.n.06"}, + {"name": "bonnet", "id": 126, "frequency": "r", "synset": "bonnet.n.01"}, + {"name": "book", "id": 127, "frequency": "f", "synset": "book.n.01"}, + {"name": "bookcase", "id": 128, "frequency": "c", "synset": "bookcase.n.01"}, + {"name": "booklet", "id": 129, "frequency": "c", "synset": "booklet.n.01"}, + {"name": "bookmark", "id": 130, "frequency": "r", "synset": "bookmark.n.01"}, + {"name": "boom_microphone", "id": 131, "frequency": "r", "synset": "boom.n.04"}, + {"name": "boot", "id": 132, "frequency": "f", "synset": "boot.n.01"}, + {"name": "bottle", "id": 133, "frequency": "f", "synset": "bottle.n.01"}, + {"name": "bottle_opener", "id": 134, "frequency": "c", "synset": "bottle_opener.n.01"}, + {"name": "bouquet", "id": 135, "frequency": "c", "synset": "bouquet.n.01"}, + {"name": "bow_(weapon)", "id": 136, "frequency": "r", "synset": "bow.n.04"}, + {"name": "bow_(decorative_ribbons)", "id": 137, "frequency": "f", "synset": "bow.n.08"}, + {"name": "bow-tie", "id": 138, "frequency": "f", "synset": "bow_tie.n.01"}, + {"name": "bowl", "id": 139, "frequency": "f", "synset": "bowl.n.03"}, + {"name": "pipe_bowl", "id": 140, "frequency": "r", "synset": "bowl.n.08"}, + {"name": "bowler_hat", "id": 141, "frequency": "c", "synset": "bowler_hat.n.01"}, + {"name": "bowling_ball", "id": 142, "frequency": "r", "synset": "bowling_ball.n.01"}, + {"name": "box", "id": 143, "frequency": "f", "synset": "box.n.01"}, + {"name": "boxing_glove", "id": 144, "frequency": "r", "synset": "boxing_glove.n.01"}, + {"name": "suspenders", "id": 145, "frequency": "c", "synset": "brace.n.06"}, + {"name": "bracelet", "id": 146, "frequency": "f", "synset": "bracelet.n.02"}, + {"name": "brass_plaque", "id": 147, "frequency": "r", "synset": "brass.n.07"}, + {"name": "brassiere", "id": 148, "frequency": "c", "synset": "brassiere.n.01"}, + {"name": "bread-bin", "id": 149, "frequency": "c", "synset": "bread-bin.n.01"}, + {"name": "bread", "id": 150, "frequency": "f", "synset": "bread.n.01"}, + {"name": "breechcloth", "id": 151, "frequency": "r", "synset": "breechcloth.n.01"}, + {"name": "bridal_gown", "id": 152, "frequency": "f", "synset": "bridal_gown.n.01"}, + {"name": "briefcase", "id": 153, "frequency": "c", "synset": "briefcase.n.01"}, + {"name": "broccoli", "id": 154, "frequency": "f", "synset": "broccoli.n.01"}, + {"name": "broach", "id": 155, "frequency": "r", "synset": "brooch.n.01"}, + {"name": "broom", "id": 156, "frequency": "c", "synset": "broom.n.01"}, + {"name": "brownie", "id": 157, "frequency": "c", "synset": "brownie.n.03"}, + {"name": "brussels_sprouts", "id": 158, "frequency": "c", "synset": "brussels_sprouts.n.01"}, + {"name": "bubble_gum", "id": 159, "frequency": "r", "synset": "bubble_gum.n.01"}, + {"name": "bucket", "id": 160, "frequency": "f", "synset": "bucket.n.01"}, + {"name": "horse_buggy", "id": 161, "frequency": "r", "synset": "buggy.n.01"}, + {"name": "bull", "id": 162, "frequency": "c", "synset": "bull.n.11"}, + {"name": "bulldog", "id": 163, "frequency": "c", "synset": "bulldog.n.01"}, + {"name": "bulldozer", "id": 164, "frequency": "r", "synset": "bulldozer.n.01"}, + {"name": "bullet_train", "id": 165, "frequency": "c", "synset": "bullet_train.n.01"}, + {"name": "bulletin_board", "id": 166, "frequency": "c", "synset": "bulletin_board.n.02"}, + {"name": "bulletproof_vest", "id": 167, "frequency": "r", "synset": "bulletproof_vest.n.01"}, + {"name": "bullhorn", "id": 168, "frequency": "c", "synset": "bullhorn.n.01"}, + {"name": "bun", "id": 169, "frequency": "f", "synset": "bun.n.01"}, + {"name": "bunk_bed", "id": 170, "frequency": "c", "synset": "bunk_bed.n.01"}, + {"name": "buoy", "id": 171, "frequency": "f", "synset": "buoy.n.01"}, + {"name": "burrito", "id": 172, "frequency": "r", "synset": "burrito.n.01"}, + {"name": "bus_(vehicle)", "id": 173, "frequency": "f", "synset": "bus.n.01"}, + {"name": "business_card", "id": 174, "frequency": "c", "synset": "business_card.n.01"}, + {"name": "butter", "id": 175, "frequency": "f", "synset": "butter.n.01"}, + {"name": "butterfly", "id": 176, "frequency": "c", "synset": "butterfly.n.01"}, + {"name": "button", "id": 177, "frequency": "f", "synset": "button.n.01"}, + {"name": "cab_(taxi)", "id": 178, "frequency": "f", "synset": "cab.n.03"}, + {"name": "cabana", "id": 179, "frequency": "r", "synset": "cabana.n.01"}, + {"name": "cabin_car", "id": 180, "frequency": "c", "synset": "cabin_car.n.01"}, + {"name": "cabinet", "id": 181, "frequency": "f", "synset": "cabinet.n.01"}, + {"name": "locker", "id": 182, "frequency": "r", "synset": "cabinet.n.03"}, + {"name": "cake", "id": 183, "frequency": "f", "synset": "cake.n.03"}, + {"name": "calculator", "id": 184, "frequency": "c", "synset": "calculator.n.02"}, + {"name": "calendar", "id": 185, "frequency": "f", "synset": "calendar.n.02"}, + {"name": "calf", "id": 186, "frequency": "c", "synset": "calf.n.01"}, + {"name": "camcorder", "id": 187, "frequency": "c", "synset": "camcorder.n.01"}, + {"name": "camel", "id": 188, "frequency": "c", "synset": "camel.n.01"}, + {"name": "camera", "id": 189, "frequency": "f", "synset": "camera.n.01"}, + {"name": "camera_lens", "id": 190, "frequency": "c", "synset": "camera_lens.n.01"}, + {"name": "camper_(vehicle)", "id": 191, "frequency": "c", "synset": "camper.n.02"}, + {"name": "can", "id": 192, "frequency": "f", "synset": "can.n.01"}, + {"name": "can_opener", "id": 193, "frequency": "c", "synset": "can_opener.n.01"}, + {"name": "candle", "id": 194, "frequency": "f", "synset": "candle.n.01"}, + {"name": "candle_holder", "id": 195, "frequency": "f", "synset": "candlestick.n.01"}, + {"name": "candy_bar", "id": 196, "frequency": "r", "synset": "candy_bar.n.01"}, + {"name": "candy_cane", "id": 197, "frequency": "c", "synset": "candy_cane.n.01"}, + {"name": "walking_cane", "id": 198, "frequency": "c", "synset": "cane.n.01"}, + {"name": "canister", "id": 199, "frequency": "c", "synset": "canister.n.02"}, + {"name": "canoe", "id": 200, "frequency": "c", "synset": "canoe.n.01"}, + {"name": "cantaloup", "id": 201, "frequency": "c", "synset": "cantaloup.n.02"}, + {"name": "canteen", "id": 202, "frequency": "r", "synset": "canteen.n.01"}, + {"name": "cap_(headwear)", "id": 203, "frequency": "f", "synset": "cap.n.01"}, + {"name": "bottle_cap", "id": 204, "frequency": "f", "synset": "cap.n.02"}, + {"name": "cape", "id": 205, "frequency": "c", "synset": "cape.n.02"}, + {"name": "cappuccino", "id": 206, "frequency": "c", "synset": "cappuccino.n.01"}, + {"name": "car_(automobile)", "id": 207, "frequency": "f", "synset": "car.n.01"}, + {"name": "railcar_(part_of_a_train)", "id": 208, "frequency": "f", "synset": "car.n.02"}, + {"name": "elevator_car", "id": 209, "frequency": "r", "synset": "car.n.04"}, + {"name": "car_battery", "id": 210, "frequency": "r", "synset": "car_battery.n.01"}, + {"name": "identity_card", "id": 211, "frequency": "c", "synset": "card.n.02"}, + {"name": "card", "id": 212, "frequency": "c", "synset": "card.n.03"}, + {"name": "cardigan", "id": 213, "frequency": "c", "synset": "cardigan.n.01"}, + {"name": "cargo_ship", "id": 214, "frequency": "r", "synset": "cargo_ship.n.01"}, + {"name": "carnation", "id": 215, "frequency": "r", "synset": "carnation.n.01"}, + {"name": "horse_carriage", "id": 216, "frequency": "c", "synset": "carriage.n.02"}, + {"name": "carrot", "id": 217, "frequency": "f", "synset": "carrot.n.01"}, + {"name": "tote_bag", "id": 218, "frequency": "f", "synset": "carryall.n.01"}, + {"name": "cart", "id": 219, "frequency": "c", "synset": "cart.n.01"}, + {"name": "carton", "id": 220, "frequency": "c", "synset": "carton.n.02"}, + {"name": "cash_register", "id": 221, "frequency": "c", "synset": "cash_register.n.01"}, + {"name": "casserole", "id": 222, "frequency": "r", "synset": "casserole.n.01"}, + {"name": "cassette", "id": 223, "frequency": "r", "synset": "cassette.n.01"}, + {"name": "cast", "id": 224, "frequency": "c", "synset": "cast.n.05"}, + {"name": "cat", "id": 225, "frequency": "f", "synset": "cat.n.01"}, + {"name": "cauliflower", "id": 226, "frequency": "f", "synset": "cauliflower.n.02"}, + {"name": "cayenne_(spice)", "id": 227, "frequency": "c", "synset": "cayenne.n.02"}, + {"name": "CD_player", "id": 228, "frequency": "c", "synset": "cd_player.n.01"}, + {"name": "celery", "id": 229, "frequency": "f", "synset": "celery.n.01"}, + { + "name": "cellular_telephone", + "id": 230, + "frequency": "f", + "synset": "cellular_telephone.n.01", + }, + {"name": "chain_mail", "id": 231, "frequency": "r", "synset": "chain_mail.n.01"}, + {"name": "chair", "id": 232, "frequency": "f", "synset": "chair.n.01"}, + {"name": "chaise_longue", "id": 233, "frequency": "r", "synset": "chaise_longue.n.01"}, + {"name": "chalice", "id": 234, "frequency": "r", "synset": "chalice.n.01"}, + {"name": "chandelier", "id": 235, "frequency": "f", "synset": "chandelier.n.01"}, + {"name": "chap", "id": 236, "frequency": "r", "synset": "chap.n.04"}, + {"name": "checkbook", "id": 237, "frequency": "r", "synset": "checkbook.n.01"}, + {"name": "checkerboard", "id": 238, "frequency": "r", "synset": "checkerboard.n.01"}, + {"name": "cherry", "id": 239, "frequency": "c", "synset": "cherry.n.03"}, + {"name": "chessboard", "id": 240, "frequency": "r", "synset": "chessboard.n.01"}, + {"name": "chicken_(animal)", "id": 241, "frequency": "c", "synset": "chicken.n.02"}, + {"name": "chickpea", "id": 242, "frequency": "c", "synset": "chickpea.n.01"}, + {"name": "chili_(vegetable)", "id": 243, "frequency": "c", "synset": "chili.n.02"}, + {"name": "chime", "id": 244, "frequency": "r", "synset": "chime.n.01"}, + {"name": "chinaware", "id": 245, "frequency": "r", "synset": "chinaware.n.01"}, + {"name": "crisp_(potato_chip)", "id": 246, "frequency": "c", "synset": "chip.n.04"}, + {"name": "poker_chip", "id": 247, "frequency": "r", "synset": "chip.n.06"}, + {"name": "chocolate_bar", "id": 248, "frequency": "c", "synset": "chocolate_bar.n.01"}, + {"name": "chocolate_cake", "id": 249, "frequency": "c", "synset": "chocolate_cake.n.01"}, + {"name": "chocolate_milk", "id": 250, "frequency": "r", "synset": "chocolate_milk.n.01"}, + {"name": "chocolate_mousse", "id": 251, "frequency": "r", "synset": "chocolate_mousse.n.01"}, + {"name": "choker", "id": 252, "frequency": "f", "synset": "choker.n.03"}, + {"name": "chopping_board", "id": 253, "frequency": "f", "synset": "chopping_board.n.01"}, + {"name": "chopstick", "id": 254, "frequency": "f", "synset": "chopstick.n.01"}, + {"name": "Christmas_tree", "id": 255, "frequency": "f", "synset": "christmas_tree.n.05"}, + {"name": "slide", "id": 256, "frequency": "c", "synset": "chute.n.02"}, + {"name": "cider", "id": 257, "frequency": "r", "synset": "cider.n.01"}, + {"name": "cigar_box", "id": 258, "frequency": "r", "synset": "cigar_box.n.01"}, + {"name": "cigarette", "id": 259, "frequency": "f", "synset": "cigarette.n.01"}, + {"name": "cigarette_case", "id": 260, "frequency": "c", "synset": "cigarette_case.n.01"}, + {"name": "cistern", "id": 261, "frequency": "f", "synset": "cistern.n.02"}, + {"name": "clarinet", "id": 262, "frequency": "r", "synset": "clarinet.n.01"}, + {"name": "clasp", "id": 263, "frequency": "c", "synset": "clasp.n.01"}, + {"name": "cleansing_agent", "id": 264, "frequency": "c", "synset": "cleansing_agent.n.01"}, + {"name": "cleat_(for_securing_rope)", "id": 265, "frequency": "r", "synset": "cleat.n.02"}, + {"name": "clementine", "id": 266, "frequency": "r", "synset": "clementine.n.01"}, + {"name": "clip", "id": 267, "frequency": "c", "synset": "clip.n.03"}, + {"name": "clipboard", "id": 268, "frequency": "c", "synset": "clipboard.n.01"}, + {"name": "clippers_(for_plants)", "id": 269, "frequency": "r", "synset": "clipper.n.03"}, + {"name": "cloak", "id": 270, "frequency": "r", "synset": "cloak.n.02"}, + {"name": "clock", "id": 271, "frequency": "f", "synset": "clock.n.01"}, + {"name": "clock_tower", "id": 272, "frequency": "f", "synset": "clock_tower.n.01"}, + {"name": "clothes_hamper", "id": 273, "frequency": "c", "synset": "clothes_hamper.n.01"}, + {"name": "clothespin", "id": 274, "frequency": "c", "synset": "clothespin.n.01"}, + {"name": "clutch_bag", "id": 275, "frequency": "r", "synset": "clutch_bag.n.01"}, + {"name": "coaster", "id": 276, "frequency": "f", "synset": "coaster.n.03"}, + {"name": "coat", "id": 277, "frequency": "f", "synset": "coat.n.01"}, + {"name": "coat_hanger", "id": 278, "frequency": "c", "synset": "coat_hanger.n.01"}, + {"name": "coatrack", "id": 279, "frequency": "c", "synset": "coatrack.n.01"}, + {"name": "cock", "id": 280, "frequency": "c", "synset": "cock.n.04"}, + {"name": "cockroach", "id": 281, "frequency": "r", "synset": "cockroach.n.01"}, + {"name": "cocoa_(beverage)", "id": 282, "frequency": "r", "synset": "cocoa.n.01"}, + {"name": "coconut", "id": 283, "frequency": "c", "synset": "coconut.n.02"}, + {"name": "coffee_maker", "id": 284, "frequency": "f", "synset": "coffee_maker.n.01"}, + {"name": "coffee_table", "id": 285, "frequency": "f", "synset": "coffee_table.n.01"}, + {"name": "coffeepot", "id": 286, "frequency": "c", "synset": "coffeepot.n.01"}, + {"name": "coil", "id": 287, "frequency": "r", "synset": "coil.n.05"}, + {"name": "coin", "id": 288, "frequency": "c", "synset": "coin.n.01"}, + {"name": "colander", "id": 289, "frequency": "c", "synset": "colander.n.01"}, + {"name": "coleslaw", "id": 290, "frequency": "c", "synset": "coleslaw.n.01"}, + {"name": "coloring_material", "id": 291, "frequency": "r", "synset": "coloring_material.n.01"}, + {"name": "combination_lock", "id": 292, "frequency": "r", "synset": "combination_lock.n.01"}, + {"name": "pacifier", "id": 293, "frequency": "c", "synset": "comforter.n.04"}, + {"name": "comic_book", "id": 294, "frequency": "r", "synset": "comic_book.n.01"}, + {"name": "compass", "id": 295, "frequency": "r", "synset": "compass.n.01"}, + {"name": "computer_keyboard", "id": 296, "frequency": "f", "synset": "computer_keyboard.n.01"}, + {"name": "condiment", "id": 297, "frequency": "f", "synset": "condiment.n.01"}, + {"name": "cone", "id": 298, "frequency": "f", "synset": "cone.n.01"}, + {"name": "control", "id": 299, "frequency": "f", "synset": "control.n.09"}, + {"name": "convertible_(automobile)", "id": 300, "frequency": "r", "synset": "convertible.n.01"}, + {"name": "sofa_bed", "id": 301, "frequency": "r", "synset": "convertible.n.03"}, + {"name": "cooker", "id": 302, "frequency": "r", "synset": "cooker.n.01"}, + {"name": "cookie", "id": 303, "frequency": "f", "synset": "cookie.n.01"}, + {"name": "cooking_utensil", "id": 304, "frequency": "r", "synset": "cooking_utensil.n.01"}, + {"name": "cooler_(for_food)", "id": 305, "frequency": "f", "synset": "cooler.n.01"}, + {"name": "cork_(bottle_plug)", "id": 306, "frequency": "f", "synset": "cork.n.04"}, + {"name": "corkboard", "id": 307, "frequency": "r", "synset": "corkboard.n.01"}, + {"name": "corkscrew", "id": 308, "frequency": "c", "synset": "corkscrew.n.01"}, + {"name": "edible_corn", "id": 309, "frequency": "f", "synset": "corn.n.03"}, + {"name": "cornbread", "id": 310, "frequency": "r", "synset": "cornbread.n.01"}, + {"name": "cornet", "id": 311, "frequency": "c", "synset": "cornet.n.01"}, + {"name": "cornice", "id": 312, "frequency": "c", "synset": "cornice.n.01"}, + {"name": "cornmeal", "id": 313, "frequency": "r", "synset": "cornmeal.n.01"}, + {"name": "corset", "id": 314, "frequency": "c", "synset": "corset.n.01"}, + {"name": "costume", "id": 315, "frequency": "c", "synset": "costume.n.04"}, + {"name": "cougar", "id": 316, "frequency": "r", "synset": "cougar.n.01"}, + {"name": "coverall", "id": 317, "frequency": "r", "synset": "coverall.n.01"}, + {"name": "cowbell", "id": 318, "frequency": "c", "synset": "cowbell.n.01"}, + {"name": "cowboy_hat", "id": 319, "frequency": "f", "synset": "cowboy_hat.n.01"}, + {"name": "crab_(animal)", "id": 320, "frequency": "c", "synset": "crab.n.01"}, + {"name": "crabmeat", "id": 321, "frequency": "r", "synset": "crab.n.05"}, + {"name": "cracker", "id": 322, "frequency": "c", "synset": "cracker.n.01"}, + {"name": "crape", "id": 323, "frequency": "r", "synset": "crape.n.01"}, + {"name": "crate", "id": 324, "frequency": "f", "synset": "crate.n.01"}, + {"name": "crayon", "id": 325, "frequency": "c", "synset": "crayon.n.01"}, + {"name": "cream_pitcher", "id": 326, "frequency": "r", "synset": "cream_pitcher.n.01"}, + {"name": "crescent_roll", "id": 327, "frequency": "c", "synset": "crescent_roll.n.01"}, + {"name": "crib", "id": 328, "frequency": "c", "synset": "crib.n.01"}, + {"name": "crock_pot", "id": 329, "frequency": "c", "synset": "crock.n.03"}, + {"name": "crossbar", "id": 330, "frequency": "f", "synset": "crossbar.n.01"}, + {"name": "crouton", "id": 331, "frequency": "r", "synset": "crouton.n.01"}, + {"name": "crow", "id": 332, "frequency": "c", "synset": "crow.n.01"}, + {"name": "crowbar", "id": 333, "frequency": "r", "synset": "crowbar.n.01"}, + {"name": "crown", "id": 334, "frequency": "c", "synset": "crown.n.04"}, + {"name": "crucifix", "id": 335, "frequency": "c", "synset": "crucifix.n.01"}, + {"name": "cruise_ship", "id": 336, "frequency": "c", "synset": "cruise_ship.n.01"}, + {"name": "police_cruiser", "id": 337, "frequency": "c", "synset": "cruiser.n.01"}, + {"name": "crumb", "id": 338, "frequency": "f", "synset": "crumb.n.03"}, + {"name": "crutch", "id": 339, "frequency": "c", "synset": "crutch.n.01"}, + {"name": "cub_(animal)", "id": 340, "frequency": "c", "synset": "cub.n.03"}, + {"name": "cube", "id": 341, "frequency": "c", "synset": "cube.n.05"}, + {"name": "cucumber", "id": 342, "frequency": "f", "synset": "cucumber.n.02"}, + {"name": "cufflink", "id": 343, "frequency": "c", "synset": "cufflink.n.01"}, + {"name": "cup", "id": 344, "frequency": "f", "synset": "cup.n.01"}, + {"name": "trophy_cup", "id": 345, "frequency": "c", "synset": "cup.n.08"}, + {"name": "cupboard", "id": 346, "frequency": "f", "synset": "cupboard.n.01"}, + {"name": "cupcake", "id": 347, "frequency": "f", "synset": "cupcake.n.01"}, + {"name": "hair_curler", "id": 348, "frequency": "r", "synset": "curler.n.01"}, + {"name": "curling_iron", "id": 349, "frequency": "r", "synset": "curling_iron.n.01"}, + {"name": "curtain", "id": 350, "frequency": "f", "synset": "curtain.n.01"}, + {"name": "cushion", "id": 351, "frequency": "f", "synset": "cushion.n.03"}, + {"name": "cylinder", "id": 352, "frequency": "r", "synset": "cylinder.n.04"}, + {"name": "cymbal", "id": 353, "frequency": "r", "synset": "cymbal.n.01"}, + {"name": "dagger", "id": 354, "frequency": "r", "synset": "dagger.n.01"}, + {"name": "dalmatian", "id": 355, "frequency": "r", "synset": "dalmatian.n.02"}, + {"name": "dartboard", "id": 356, "frequency": "c", "synset": "dartboard.n.01"}, + {"name": "date_(fruit)", "id": 357, "frequency": "r", "synset": "date.n.08"}, + {"name": "deck_chair", "id": 358, "frequency": "f", "synset": "deck_chair.n.01"}, + {"name": "deer", "id": 359, "frequency": "c", "synset": "deer.n.01"}, + {"name": "dental_floss", "id": 360, "frequency": "c", "synset": "dental_floss.n.01"}, + {"name": "desk", "id": 361, "frequency": "f", "synset": "desk.n.01"}, + {"name": "detergent", "id": 362, "frequency": "r", "synset": "detergent.n.01"}, + {"name": "diaper", "id": 363, "frequency": "c", "synset": "diaper.n.01"}, + {"name": "diary", "id": 364, "frequency": "r", "synset": "diary.n.01"}, + {"name": "die", "id": 365, "frequency": "r", "synset": "die.n.01"}, + {"name": "dinghy", "id": 366, "frequency": "r", "synset": "dinghy.n.01"}, + {"name": "dining_table", "id": 367, "frequency": "f", "synset": "dining_table.n.01"}, + {"name": "tux", "id": 368, "frequency": "r", "synset": "dinner_jacket.n.01"}, + {"name": "dish", "id": 369, "frequency": "f", "synset": "dish.n.01"}, + {"name": "dish_antenna", "id": 370, "frequency": "c", "synset": "dish.n.05"}, + {"name": "dishrag", "id": 371, "frequency": "c", "synset": "dishrag.n.01"}, + {"name": "dishtowel", "id": 372, "frequency": "f", "synset": "dishtowel.n.01"}, + {"name": "dishwasher", "id": 373, "frequency": "f", "synset": "dishwasher.n.01"}, + { + "name": "dishwasher_detergent", + "id": 374, + "frequency": "r", + "synset": "dishwasher_detergent.n.01", + }, + {"name": "dispenser", "id": 375, "frequency": "f", "synset": "dispenser.n.01"}, + {"name": "diving_board", "id": 376, "frequency": "r", "synset": "diving_board.n.01"}, + {"name": "Dixie_cup", "id": 377, "frequency": "f", "synset": "dixie_cup.n.01"}, + {"name": "dog", "id": 378, "frequency": "f", "synset": "dog.n.01"}, + {"name": "dog_collar", "id": 379, "frequency": "f", "synset": "dog_collar.n.01"}, + {"name": "doll", "id": 380, "frequency": "f", "synset": "doll.n.01"}, + {"name": "dollar", "id": 381, "frequency": "r", "synset": "dollar.n.02"}, + {"name": "dollhouse", "id": 382, "frequency": "r", "synset": "dollhouse.n.01"}, + {"name": "dolphin", "id": 383, "frequency": "c", "synset": "dolphin.n.02"}, + {"name": "domestic_ass", "id": 384, "frequency": "c", "synset": "domestic_ass.n.01"}, + {"name": "doorknob", "id": 385, "frequency": "f", "synset": "doorknob.n.01"}, + {"name": "doormat", "id": 386, "frequency": "c", "synset": "doormat.n.02"}, + {"name": "doughnut", "id": 387, "frequency": "f", "synset": "doughnut.n.02"}, + {"name": "dove", "id": 388, "frequency": "r", "synset": "dove.n.01"}, + {"name": "dragonfly", "id": 389, "frequency": "r", "synset": "dragonfly.n.01"}, + {"name": "drawer", "id": 390, "frequency": "f", "synset": "drawer.n.01"}, + {"name": "underdrawers", "id": 391, "frequency": "c", "synset": "drawers.n.01"}, + {"name": "dress", "id": 392, "frequency": "f", "synset": "dress.n.01"}, + {"name": "dress_hat", "id": 393, "frequency": "c", "synset": "dress_hat.n.01"}, + {"name": "dress_suit", "id": 394, "frequency": "f", "synset": "dress_suit.n.01"}, + {"name": "dresser", "id": 395, "frequency": "f", "synset": "dresser.n.05"}, + {"name": "drill", "id": 396, "frequency": "c", "synset": "drill.n.01"}, + {"name": "drone", "id": 397, "frequency": "r", "synset": "drone.n.04"}, + {"name": "dropper", "id": 398, "frequency": "r", "synset": "dropper.n.01"}, + {"name": "drum_(musical_instrument)", "id": 399, "frequency": "c", "synset": "drum.n.01"}, + {"name": "drumstick", "id": 400, "frequency": "r", "synset": "drumstick.n.02"}, + {"name": "duck", "id": 401, "frequency": "f", "synset": "duck.n.01"}, + {"name": "duckling", "id": 402, "frequency": "c", "synset": "duckling.n.02"}, + {"name": "duct_tape", "id": 403, "frequency": "c", "synset": "duct_tape.n.01"}, + {"name": "duffel_bag", "id": 404, "frequency": "f", "synset": "duffel_bag.n.01"}, + {"name": "dumbbell", "id": 405, "frequency": "r", "synset": "dumbbell.n.01"}, + {"name": "dumpster", "id": 406, "frequency": "c", "synset": "dumpster.n.01"}, + {"name": "dustpan", "id": 407, "frequency": "r", "synset": "dustpan.n.02"}, + {"name": "eagle", "id": 408, "frequency": "c", "synset": "eagle.n.01"}, + {"name": "earphone", "id": 409, "frequency": "f", "synset": "earphone.n.01"}, + {"name": "earplug", "id": 410, "frequency": "r", "synset": "earplug.n.01"}, + {"name": "earring", "id": 411, "frequency": "f", "synset": "earring.n.01"}, + {"name": "easel", "id": 412, "frequency": "c", "synset": "easel.n.01"}, + {"name": "eclair", "id": 413, "frequency": "r", "synset": "eclair.n.01"}, + {"name": "eel", "id": 414, "frequency": "r", "synset": "eel.n.01"}, + {"name": "egg", "id": 415, "frequency": "f", "synset": "egg.n.02"}, + {"name": "egg_roll", "id": 416, "frequency": "r", "synset": "egg_roll.n.01"}, + {"name": "egg_yolk", "id": 417, "frequency": "c", "synset": "egg_yolk.n.01"}, + {"name": "eggbeater", "id": 418, "frequency": "c", "synset": "eggbeater.n.02"}, + {"name": "eggplant", "id": 419, "frequency": "c", "synset": "eggplant.n.01"}, + {"name": "electric_chair", "id": 420, "frequency": "r", "synset": "electric_chair.n.01"}, + {"name": "refrigerator", "id": 421, "frequency": "f", "synset": "electric_refrigerator.n.01"}, + {"name": "elephant", "id": 422, "frequency": "f", "synset": "elephant.n.01"}, + {"name": "elk", "id": 423, "frequency": "c", "synset": "elk.n.01"}, + {"name": "envelope", "id": 424, "frequency": "c", "synset": "envelope.n.01"}, + {"name": "eraser", "id": 425, "frequency": "c", "synset": "eraser.n.01"}, + {"name": "escargot", "id": 426, "frequency": "r", "synset": "escargot.n.01"}, + {"name": "eyepatch", "id": 427, "frequency": "r", "synset": "eyepatch.n.01"}, + {"name": "falcon", "id": 428, "frequency": "r", "synset": "falcon.n.01"}, + {"name": "fan", "id": 429, "frequency": "f", "synset": "fan.n.01"}, + {"name": "faucet", "id": 430, "frequency": "f", "synset": "faucet.n.01"}, + {"name": "fedora", "id": 431, "frequency": "r", "synset": "fedora.n.01"}, + {"name": "ferret", "id": 432, "frequency": "r", "synset": "ferret.n.02"}, + {"name": "Ferris_wheel", "id": 433, "frequency": "c", "synset": "ferris_wheel.n.01"}, + {"name": "ferry", "id": 434, "frequency": "c", "synset": "ferry.n.01"}, + {"name": "fig_(fruit)", "id": 435, "frequency": "r", "synset": "fig.n.04"}, + {"name": "fighter_jet", "id": 436, "frequency": "c", "synset": "fighter.n.02"}, + {"name": "figurine", "id": 437, "frequency": "f", "synset": "figurine.n.01"}, + {"name": "file_cabinet", "id": 438, "frequency": "c", "synset": "file.n.03"}, + {"name": "file_(tool)", "id": 439, "frequency": "r", "synset": "file.n.04"}, + {"name": "fire_alarm", "id": 440, "frequency": "f", "synset": "fire_alarm.n.02"}, + {"name": "fire_engine", "id": 441, "frequency": "f", "synset": "fire_engine.n.01"}, + {"name": "fire_extinguisher", "id": 442, "frequency": "f", "synset": "fire_extinguisher.n.01"}, + {"name": "fire_hose", "id": 443, "frequency": "c", "synset": "fire_hose.n.01"}, + {"name": "fireplace", "id": 444, "frequency": "f", "synset": "fireplace.n.01"}, + {"name": "fireplug", "id": 445, "frequency": "f", "synset": "fireplug.n.01"}, + {"name": "first-aid_kit", "id": 446, "frequency": "r", "synset": "first-aid_kit.n.01"}, + {"name": "fish", "id": 447, "frequency": "f", "synset": "fish.n.01"}, + {"name": "fish_(food)", "id": 448, "frequency": "c", "synset": "fish.n.02"}, + {"name": "fishbowl", "id": 449, "frequency": "r", "synset": "fishbowl.n.02"}, + {"name": "fishing_rod", "id": 450, "frequency": "c", "synset": "fishing_rod.n.01"}, + {"name": "flag", "id": 451, "frequency": "f", "synset": "flag.n.01"}, + {"name": "flagpole", "id": 452, "frequency": "f", "synset": "flagpole.n.02"}, + {"name": "flamingo", "id": 453, "frequency": "c", "synset": "flamingo.n.01"}, + {"name": "flannel", "id": 454, "frequency": "c", "synset": "flannel.n.01"}, + {"name": "flap", "id": 455, "frequency": "c", "synset": "flap.n.01"}, + {"name": "flash", "id": 456, "frequency": "r", "synset": "flash.n.10"}, + {"name": "flashlight", "id": 457, "frequency": "c", "synset": "flashlight.n.01"}, + {"name": "fleece", "id": 458, "frequency": "r", "synset": "fleece.n.03"}, + {"name": "flip-flop_(sandal)", "id": 459, "frequency": "f", "synset": "flip-flop.n.02"}, + {"name": "flipper_(footwear)", "id": 460, "frequency": "c", "synset": "flipper.n.01"}, + { + "name": "flower_arrangement", + "id": 461, + "frequency": "f", + "synset": "flower_arrangement.n.01", + }, + {"name": "flute_glass", "id": 462, "frequency": "c", "synset": "flute.n.02"}, + {"name": "foal", "id": 463, "frequency": "c", "synset": "foal.n.01"}, + {"name": "folding_chair", "id": 464, "frequency": "c", "synset": "folding_chair.n.01"}, + {"name": "food_processor", "id": 465, "frequency": "c", "synset": "food_processor.n.01"}, + {"name": "football_(American)", "id": 466, "frequency": "c", "synset": "football.n.02"}, + {"name": "football_helmet", "id": 467, "frequency": "r", "synset": "football_helmet.n.01"}, + {"name": "footstool", "id": 468, "frequency": "c", "synset": "footstool.n.01"}, + {"name": "fork", "id": 469, "frequency": "f", "synset": "fork.n.01"}, + {"name": "forklift", "id": 470, "frequency": "c", "synset": "forklift.n.01"}, + {"name": "freight_car", "id": 471, "frequency": "c", "synset": "freight_car.n.01"}, + {"name": "French_toast", "id": 472, "frequency": "c", "synset": "french_toast.n.01"}, + {"name": "freshener", "id": 473, "frequency": "c", "synset": "freshener.n.01"}, + {"name": "frisbee", "id": 474, "frequency": "f", "synset": "frisbee.n.01"}, + {"name": "frog", "id": 475, "frequency": "c", "synset": "frog.n.01"}, + {"name": "fruit_juice", "id": 476, "frequency": "c", "synset": "fruit_juice.n.01"}, + {"name": "frying_pan", "id": 477, "frequency": "f", "synset": "frying_pan.n.01"}, + {"name": "fudge", "id": 478, "frequency": "r", "synset": "fudge.n.01"}, + {"name": "funnel", "id": 479, "frequency": "r", "synset": "funnel.n.02"}, + {"name": "futon", "id": 480, "frequency": "r", "synset": "futon.n.01"}, + {"name": "gag", "id": 481, "frequency": "r", "synset": "gag.n.02"}, + {"name": "garbage", "id": 482, "frequency": "r", "synset": "garbage.n.03"}, + {"name": "garbage_truck", "id": 483, "frequency": "c", "synset": "garbage_truck.n.01"}, + {"name": "garden_hose", "id": 484, "frequency": "c", "synset": "garden_hose.n.01"}, + {"name": "gargle", "id": 485, "frequency": "c", "synset": "gargle.n.01"}, + {"name": "gargoyle", "id": 486, "frequency": "r", "synset": "gargoyle.n.02"}, + {"name": "garlic", "id": 487, "frequency": "c", "synset": "garlic.n.02"}, + {"name": "gasmask", "id": 488, "frequency": "r", "synset": "gasmask.n.01"}, + {"name": "gazelle", "id": 489, "frequency": "c", "synset": "gazelle.n.01"}, + {"name": "gelatin", "id": 490, "frequency": "c", "synset": "gelatin.n.02"}, + {"name": "gemstone", "id": 491, "frequency": "r", "synset": "gem.n.02"}, + {"name": "generator", "id": 492, "frequency": "r", "synset": "generator.n.02"}, + {"name": "giant_panda", "id": 493, "frequency": "c", "synset": "giant_panda.n.01"}, + {"name": "gift_wrap", "id": 494, "frequency": "c", "synset": "gift_wrap.n.01"}, + {"name": "ginger", "id": 495, "frequency": "c", "synset": "ginger.n.03"}, + {"name": "giraffe", "id": 496, "frequency": "f", "synset": "giraffe.n.01"}, + {"name": "cincture", "id": 497, "frequency": "c", "synset": "girdle.n.02"}, + {"name": "glass_(drink_container)", "id": 498, "frequency": "f", "synset": "glass.n.02"}, + {"name": "globe", "id": 499, "frequency": "c", "synset": "globe.n.03"}, + {"name": "glove", "id": 500, "frequency": "f", "synset": "glove.n.02"}, + {"name": "goat", "id": 501, "frequency": "c", "synset": "goat.n.01"}, + {"name": "goggles", "id": 502, "frequency": "f", "synset": "goggles.n.01"}, + {"name": "goldfish", "id": 503, "frequency": "r", "synset": "goldfish.n.01"}, + {"name": "golf_club", "id": 504, "frequency": "c", "synset": "golf_club.n.02"}, + {"name": "golfcart", "id": 505, "frequency": "c", "synset": "golfcart.n.01"}, + {"name": "gondola_(boat)", "id": 506, "frequency": "r", "synset": "gondola.n.02"}, + {"name": "goose", "id": 507, "frequency": "c", "synset": "goose.n.01"}, + {"name": "gorilla", "id": 508, "frequency": "r", "synset": "gorilla.n.01"}, + {"name": "gourd", "id": 509, "frequency": "r", "synset": "gourd.n.02"}, + {"name": "grape", "id": 510, "frequency": "f", "synset": "grape.n.01"}, + {"name": "grater", "id": 511, "frequency": "c", "synset": "grater.n.01"}, + {"name": "gravestone", "id": 512, "frequency": "c", "synset": "gravestone.n.01"}, + {"name": "gravy_boat", "id": 513, "frequency": "r", "synset": "gravy_boat.n.01"}, + {"name": "green_bean", "id": 514, "frequency": "f", "synset": "green_bean.n.02"}, + {"name": "green_onion", "id": 515, "frequency": "f", "synset": "green_onion.n.01"}, + {"name": "griddle", "id": 516, "frequency": "r", "synset": "griddle.n.01"}, + {"name": "grill", "id": 517, "frequency": "f", "synset": "grill.n.02"}, + {"name": "grits", "id": 518, "frequency": "r", "synset": "grits.n.01"}, + {"name": "grizzly", "id": 519, "frequency": "c", "synset": "grizzly.n.01"}, + {"name": "grocery_bag", "id": 520, "frequency": "c", "synset": "grocery_bag.n.01"}, + {"name": "guitar", "id": 521, "frequency": "f", "synset": "guitar.n.01"}, + {"name": "gull", "id": 522, "frequency": "c", "synset": "gull.n.02"}, + {"name": "gun", "id": 523, "frequency": "c", "synset": "gun.n.01"}, + {"name": "hairbrush", "id": 524, "frequency": "f", "synset": "hairbrush.n.01"}, + {"name": "hairnet", "id": 525, "frequency": "c", "synset": "hairnet.n.01"}, + {"name": "hairpin", "id": 526, "frequency": "c", "synset": "hairpin.n.01"}, + {"name": "halter_top", "id": 527, "frequency": "r", "synset": "halter.n.03"}, + {"name": "ham", "id": 528, "frequency": "f", "synset": "ham.n.01"}, + {"name": "hamburger", "id": 529, "frequency": "c", "synset": "hamburger.n.01"}, + {"name": "hammer", "id": 530, "frequency": "c", "synset": "hammer.n.02"}, + {"name": "hammock", "id": 531, "frequency": "c", "synset": "hammock.n.02"}, + {"name": "hamper", "id": 532, "frequency": "r", "synset": "hamper.n.02"}, + {"name": "hamster", "id": 533, "frequency": "c", "synset": "hamster.n.01"}, + {"name": "hair_dryer", "id": 534, "frequency": "f", "synset": "hand_blower.n.01"}, + {"name": "hand_glass", "id": 535, "frequency": "r", "synset": "hand_glass.n.01"}, + {"name": "hand_towel", "id": 536, "frequency": "f", "synset": "hand_towel.n.01"}, + {"name": "handcart", "id": 537, "frequency": "c", "synset": "handcart.n.01"}, + {"name": "handcuff", "id": 538, "frequency": "r", "synset": "handcuff.n.01"}, + {"name": "handkerchief", "id": 539, "frequency": "c", "synset": "handkerchief.n.01"}, + {"name": "handle", "id": 540, "frequency": "f", "synset": "handle.n.01"}, + {"name": "handsaw", "id": 541, "frequency": "r", "synset": "handsaw.n.01"}, + {"name": "hardback_book", "id": 542, "frequency": "r", "synset": "hardback.n.01"}, + {"name": "harmonium", "id": 543, "frequency": "r", "synset": "harmonium.n.01"}, + {"name": "hat", "id": 544, "frequency": "f", "synset": "hat.n.01"}, + {"name": "hatbox", "id": 545, "frequency": "r", "synset": "hatbox.n.01"}, + {"name": "veil", "id": 546, "frequency": "c", "synset": "head_covering.n.01"}, + {"name": "headband", "id": 547, "frequency": "f", "synset": "headband.n.01"}, + {"name": "headboard", "id": 548, "frequency": "f", "synset": "headboard.n.01"}, + {"name": "headlight", "id": 549, "frequency": "f", "synset": "headlight.n.01"}, + {"name": "headscarf", "id": 550, "frequency": "c", "synset": "headscarf.n.01"}, + {"name": "headset", "id": 551, "frequency": "r", "synset": "headset.n.01"}, + {"name": "headstall_(for_horses)", "id": 552, "frequency": "c", "synset": "headstall.n.01"}, + {"name": "heart", "id": 553, "frequency": "c", "synset": "heart.n.02"}, + {"name": "heater", "id": 554, "frequency": "c", "synset": "heater.n.01"}, + {"name": "helicopter", "id": 555, "frequency": "c", "synset": "helicopter.n.01"}, + {"name": "helmet", "id": 556, "frequency": "f", "synset": "helmet.n.02"}, + {"name": "heron", "id": 557, "frequency": "r", "synset": "heron.n.02"}, + {"name": "highchair", "id": 558, "frequency": "c", "synset": "highchair.n.01"}, + {"name": "hinge", "id": 559, "frequency": "f", "synset": "hinge.n.01"}, + {"name": "hippopotamus", "id": 560, "frequency": "r", "synset": "hippopotamus.n.01"}, + {"name": "hockey_stick", "id": 561, "frequency": "r", "synset": "hockey_stick.n.01"}, + {"name": "hog", "id": 562, "frequency": "c", "synset": "hog.n.03"}, + {"name": "home_plate_(baseball)", "id": 563, "frequency": "f", "synset": "home_plate.n.01"}, + {"name": "honey", "id": 564, "frequency": "c", "synset": "honey.n.01"}, + {"name": "fume_hood", "id": 565, "frequency": "f", "synset": "hood.n.06"}, + {"name": "hook", "id": 566, "frequency": "f", "synset": "hook.n.05"}, + {"name": "hookah", "id": 567, "frequency": "r", "synset": "hookah.n.01"}, + {"name": "hornet", "id": 568, "frequency": "r", "synset": "hornet.n.01"}, + {"name": "horse", "id": 569, "frequency": "f", "synset": "horse.n.01"}, + {"name": "hose", "id": 570, "frequency": "f", "synset": "hose.n.03"}, + {"name": "hot-air_balloon", "id": 571, "frequency": "r", "synset": "hot-air_balloon.n.01"}, + {"name": "hotplate", "id": 572, "frequency": "r", "synset": "hot_plate.n.01"}, + {"name": "hot_sauce", "id": 573, "frequency": "c", "synset": "hot_sauce.n.01"}, + {"name": "hourglass", "id": 574, "frequency": "r", "synset": "hourglass.n.01"}, + {"name": "houseboat", "id": 575, "frequency": "r", "synset": "houseboat.n.01"}, + {"name": "hummingbird", "id": 576, "frequency": "c", "synset": "hummingbird.n.01"}, + {"name": "hummus", "id": 577, "frequency": "r", "synset": "hummus.n.01"}, + {"name": "polar_bear", "id": 578, "frequency": "f", "synset": "ice_bear.n.01"}, + {"name": "icecream", "id": 579, "frequency": "c", "synset": "ice_cream.n.01"}, + {"name": "popsicle", "id": 580, "frequency": "r", "synset": "ice_lolly.n.01"}, + {"name": "ice_maker", "id": 581, "frequency": "c", "synset": "ice_maker.n.01"}, + {"name": "ice_pack", "id": 582, "frequency": "r", "synset": "ice_pack.n.01"}, + {"name": "ice_skate", "id": 583, "frequency": "r", "synset": "ice_skate.n.01"}, + {"name": "igniter", "id": 584, "frequency": "c", "synset": "igniter.n.01"}, + {"name": "inhaler", "id": 585, "frequency": "r", "synset": "inhaler.n.01"}, + {"name": "iPod", "id": 586, "frequency": "f", "synset": "ipod.n.01"}, + {"name": "iron_(for_clothing)", "id": 587, "frequency": "c", "synset": "iron.n.04"}, + {"name": "ironing_board", "id": 588, "frequency": "c", "synset": "ironing_board.n.01"}, + {"name": "jacket", "id": 589, "frequency": "f", "synset": "jacket.n.01"}, + {"name": "jam", "id": 590, "frequency": "c", "synset": "jam.n.01"}, + {"name": "jar", "id": 591, "frequency": "f", "synset": "jar.n.01"}, + {"name": "jean", "id": 592, "frequency": "f", "synset": "jean.n.01"}, + {"name": "jeep", "id": 593, "frequency": "c", "synset": "jeep.n.01"}, + {"name": "jelly_bean", "id": 594, "frequency": "r", "synset": "jelly_bean.n.01"}, + {"name": "jersey", "id": 595, "frequency": "f", "synset": "jersey.n.03"}, + {"name": "jet_plane", "id": 596, "frequency": "c", "synset": "jet.n.01"}, + {"name": "jewel", "id": 597, "frequency": "r", "synset": "jewel.n.01"}, + {"name": "jewelry", "id": 598, "frequency": "c", "synset": "jewelry.n.01"}, + {"name": "joystick", "id": 599, "frequency": "r", "synset": "joystick.n.02"}, + {"name": "jumpsuit", "id": 600, "frequency": "c", "synset": "jump_suit.n.01"}, + {"name": "kayak", "id": 601, "frequency": "c", "synset": "kayak.n.01"}, + {"name": "keg", "id": 602, "frequency": "r", "synset": "keg.n.02"}, + {"name": "kennel", "id": 603, "frequency": "r", "synset": "kennel.n.01"}, + {"name": "kettle", "id": 604, "frequency": "c", "synset": "kettle.n.01"}, + {"name": "key", "id": 605, "frequency": "f", "synset": "key.n.01"}, + {"name": "keycard", "id": 606, "frequency": "r", "synset": "keycard.n.01"}, + {"name": "kilt", "id": 607, "frequency": "c", "synset": "kilt.n.01"}, + {"name": "kimono", "id": 608, "frequency": "c", "synset": "kimono.n.01"}, + {"name": "kitchen_sink", "id": 609, "frequency": "f", "synset": "kitchen_sink.n.01"}, + {"name": "kitchen_table", "id": 610, "frequency": "r", "synset": "kitchen_table.n.01"}, + {"name": "kite", "id": 611, "frequency": "f", "synset": "kite.n.03"}, + {"name": "kitten", "id": 612, "frequency": "c", "synset": "kitten.n.01"}, + {"name": "kiwi_fruit", "id": 613, "frequency": "c", "synset": "kiwi.n.03"}, + {"name": "knee_pad", "id": 614, "frequency": "f", "synset": "knee_pad.n.01"}, + {"name": "knife", "id": 615, "frequency": "f", "synset": "knife.n.01"}, + {"name": "knitting_needle", "id": 616, "frequency": "r", "synset": "knitting_needle.n.01"}, + {"name": "knob", "id": 617, "frequency": "f", "synset": "knob.n.02"}, + {"name": "knocker_(on_a_door)", "id": 618, "frequency": "r", "synset": "knocker.n.05"}, + {"name": "koala", "id": 619, "frequency": "r", "synset": "koala.n.01"}, + {"name": "lab_coat", "id": 620, "frequency": "r", "synset": "lab_coat.n.01"}, + {"name": "ladder", "id": 621, "frequency": "f", "synset": "ladder.n.01"}, + {"name": "ladle", "id": 622, "frequency": "c", "synset": "ladle.n.01"}, + {"name": "ladybug", "id": 623, "frequency": "c", "synset": "ladybug.n.01"}, + {"name": "lamb_(animal)", "id": 624, "frequency": "f", "synset": "lamb.n.01"}, + {"name": "lamb-chop", "id": 625, "frequency": "r", "synset": "lamb_chop.n.01"}, + {"name": "lamp", "id": 626, "frequency": "f", "synset": "lamp.n.02"}, + {"name": "lamppost", "id": 627, "frequency": "f", "synset": "lamppost.n.01"}, + {"name": "lampshade", "id": 628, "frequency": "f", "synset": "lampshade.n.01"}, + {"name": "lantern", "id": 629, "frequency": "c", "synset": "lantern.n.01"}, + {"name": "lanyard", "id": 630, "frequency": "f", "synset": "lanyard.n.02"}, + {"name": "laptop_computer", "id": 631, "frequency": "f", "synset": "laptop.n.01"}, + {"name": "lasagna", "id": 632, "frequency": "r", "synset": "lasagna.n.01"}, + {"name": "latch", "id": 633, "frequency": "f", "synset": "latch.n.02"}, + {"name": "lawn_mower", "id": 634, "frequency": "r", "synset": "lawn_mower.n.01"}, + {"name": "leather", "id": 635, "frequency": "r", "synset": "leather.n.01"}, + {"name": "legging_(clothing)", "id": 636, "frequency": "c", "synset": "legging.n.01"}, + {"name": "Lego", "id": 637, "frequency": "c", "synset": "lego.n.01"}, + {"name": "legume", "id": 638, "frequency": "r", "synset": "legume.n.02"}, + {"name": "lemon", "id": 639, "frequency": "f", "synset": "lemon.n.01"}, + {"name": "lemonade", "id": 640, "frequency": "r", "synset": "lemonade.n.01"}, + {"name": "lettuce", "id": 641, "frequency": "f", "synset": "lettuce.n.02"}, + {"name": "license_plate", "id": 642, "frequency": "f", "synset": "license_plate.n.01"}, + {"name": "life_buoy", "id": 643, "frequency": "f", "synset": "life_buoy.n.01"}, + {"name": "life_jacket", "id": 644, "frequency": "f", "synset": "life_jacket.n.01"}, + {"name": "lightbulb", "id": 645, "frequency": "f", "synset": "light_bulb.n.01"}, + {"name": "lightning_rod", "id": 646, "frequency": "r", "synset": "lightning_rod.n.02"}, + {"name": "lime", "id": 647, "frequency": "f", "synset": "lime.n.06"}, + {"name": "limousine", "id": 648, "frequency": "r", "synset": "limousine.n.01"}, + {"name": "lion", "id": 649, "frequency": "c", "synset": "lion.n.01"}, + {"name": "lip_balm", "id": 650, "frequency": "c", "synset": "lip_balm.n.01"}, + {"name": "liquor", "id": 651, "frequency": "r", "synset": "liquor.n.01"}, + {"name": "lizard", "id": 652, "frequency": "c", "synset": "lizard.n.01"}, + {"name": "log", "id": 653, "frequency": "f", "synset": "log.n.01"}, + {"name": "lollipop", "id": 654, "frequency": "c", "synset": "lollipop.n.02"}, + { + "name": "speaker_(stero_equipment)", + "id": 655, + "frequency": "f", + "synset": "loudspeaker.n.01", + }, + {"name": "loveseat", "id": 656, "frequency": "c", "synset": "love_seat.n.01"}, + {"name": "machine_gun", "id": 657, "frequency": "r", "synset": "machine_gun.n.01"}, + {"name": "magazine", "id": 658, "frequency": "f", "synset": "magazine.n.02"}, + {"name": "magnet", "id": 659, "frequency": "f", "synset": "magnet.n.01"}, + {"name": "mail_slot", "id": 660, "frequency": "c", "synset": "mail_slot.n.01"}, + {"name": "mailbox_(at_home)", "id": 661, "frequency": "f", "synset": "mailbox.n.01"}, + {"name": "mallard", "id": 662, "frequency": "r", "synset": "mallard.n.01"}, + {"name": "mallet", "id": 663, "frequency": "r", "synset": "mallet.n.01"}, + {"name": "mammoth", "id": 664, "frequency": "r", "synset": "mammoth.n.01"}, + {"name": "manatee", "id": 665, "frequency": "r", "synset": "manatee.n.01"}, + {"name": "mandarin_orange", "id": 666, "frequency": "c", "synset": "mandarin.n.05"}, + {"name": "manger", "id": 667, "frequency": "c", "synset": "manger.n.01"}, + {"name": "manhole", "id": 668, "frequency": "f", "synset": "manhole.n.01"}, + {"name": "map", "id": 669, "frequency": "f", "synset": "map.n.01"}, + {"name": "marker", "id": 670, "frequency": "f", "synset": "marker.n.03"}, + {"name": "martini", "id": 671, "frequency": "r", "synset": "martini.n.01"}, + {"name": "mascot", "id": 672, "frequency": "r", "synset": "mascot.n.01"}, + {"name": "mashed_potato", "id": 673, "frequency": "c", "synset": "mashed_potato.n.01"}, + {"name": "masher", "id": 674, "frequency": "r", "synset": "masher.n.02"}, + {"name": "mask", "id": 675, "frequency": "f", "synset": "mask.n.04"}, + {"name": "mast", "id": 676, "frequency": "f", "synset": "mast.n.01"}, + {"name": "mat_(gym_equipment)", "id": 677, "frequency": "c", "synset": "mat.n.03"}, + {"name": "matchbox", "id": 678, "frequency": "r", "synset": "matchbox.n.01"}, + {"name": "mattress", "id": 679, "frequency": "f", "synset": "mattress.n.01"}, + {"name": "measuring_cup", "id": 680, "frequency": "c", "synset": "measuring_cup.n.01"}, + {"name": "measuring_stick", "id": 681, "frequency": "c", "synset": "measuring_stick.n.01"}, + {"name": "meatball", "id": 682, "frequency": "c", "synset": "meatball.n.01"}, + {"name": "medicine", "id": 683, "frequency": "c", "synset": "medicine.n.02"}, + {"name": "melon", "id": 684, "frequency": "c", "synset": "melon.n.01"}, + {"name": "microphone", "id": 685, "frequency": "f", "synset": "microphone.n.01"}, + {"name": "microscope", "id": 686, "frequency": "r", "synset": "microscope.n.01"}, + {"name": "microwave_oven", "id": 687, "frequency": "f", "synset": "microwave.n.02"}, + {"name": "milestone", "id": 688, "frequency": "r", "synset": "milestone.n.01"}, + {"name": "milk", "id": 689, "frequency": "f", "synset": "milk.n.01"}, + {"name": "milk_can", "id": 690, "frequency": "r", "synset": "milk_can.n.01"}, + {"name": "milkshake", "id": 691, "frequency": "r", "synset": "milkshake.n.01"}, + {"name": "minivan", "id": 692, "frequency": "f", "synset": "minivan.n.01"}, + {"name": "mint_candy", "id": 693, "frequency": "r", "synset": "mint.n.05"}, + {"name": "mirror", "id": 694, "frequency": "f", "synset": "mirror.n.01"}, + {"name": "mitten", "id": 695, "frequency": "c", "synset": "mitten.n.01"}, + {"name": "mixer_(kitchen_tool)", "id": 696, "frequency": "c", "synset": "mixer.n.04"}, + {"name": "money", "id": 697, "frequency": "c", "synset": "money.n.03"}, + { + "name": "monitor_(computer_equipment) computer_monitor", + "id": 698, + "frequency": "f", + "synset": "monitor.n.04", + }, + {"name": "monkey", "id": 699, "frequency": "c", "synset": "monkey.n.01"}, + {"name": "motor", "id": 700, "frequency": "f", "synset": "motor.n.01"}, + {"name": "motor_scooter", "id": 701, "frequency": "f", "synset": "motor_scooter.n.01"}, + {"name": "motor_vehicle", "id": 702, "frequency": "r", "synset": "motor_vehicle.n.01"}, + {"name": "motorcycle", "id": 703, "frequency": "f", "synset": "motorcycle.n.01"}, + {"name": "mound_(baseball)", "id": 704, "frequency": "f", "synset": "mound.n.01"}, + {"name": "mouse_(computer_equipment)", "id": 705, "frequency": "f", "synset": "mouse.n.04"}, + {"name": "mousepad", "id": 706, "frequency": "f", "synset": "mousepad.n.01"}, + {"name": "muffin", "id": 707, "frequency": "c", "synset": "muffin.n.01"}, + {"name": "mug", "id": 708, "frequency": "f", "synset": "mug.n.04"}, + {"name": "mushroom", "id": 709, "frequency": "f", "synset": "mushroom.n.02"}, + {"name": "music_stool", "id": 710, "frequency": "r", "synset": "music_stool.n.01"}, + { + "name": "musical_instrument", + "id": 711, + "frequency": "c", + "synset": "musical_instrument.n.01", + }, + {"name": "nailfile", "id": 712, "frequency": "r", "synset": "nailfile.n.01"}, + {"name": "napkin", "id": 713, "frequency": "f", "synset": "napkin.n.01"}, + {"name": "neckerchief", "id": 714, "frequency": "r", "synset": "neckerchief.n.01"}, + {"name": "necklace", "id": 715, "frequency": "f", "synset": "necklace.n.01"}, + {"name": "necktie", "id": 716, "frequency": "f", "synset": "necktie.n.01"}, + {"name": "needle", "id": 717, "frequency": "c", "synset": "needle.n.03"}, + {"name": "nest", "id": 718, "frequency": "c", "synset": "nest.n.01"}, + {"name": "newspaper", "id": 719, "frequency": "f", "synset": "newspaper.n.01"}, + {"name": "newsstand", "id": 720, "frequency": "c", "synset": "newsstand.n.01"}, + {"name": "nightshirt", "id": 721, "frequency": "c", "synset": "nightwear.n.01"}, + {"name": "nosebag_(for_animals)", "id": 722, "frequency": "r", "synset": "nosebag.n.01"}, + {"name": "noseband_(for_animals)", "id": 723, "frequency": "c", "synset": "noseband.n.01"}, + {"name": "notebook", "id": 724, "frequency": "f", "synset": "notebook.n.01"}, + {"name": "notepad", "id": 725, "frequency": "c", "synset": "notepad.n.01"}, + {"name": "nut", "id": 726, "frequency": "f", "synset": "nut.n.03"}, + {"name": "nutcracker", "id": 727, "frequency": "r", "synset": "nutcracker.n.01"}, + {"name": "oar", "id": 728, "frequency": "f", "synset": "oar.n.01"}, + {"name": "octopus_(food)", "id": 729, "frequency": "r", "synset": "octopus.n.01"}, + {"name": "octopus_(animal)", "id": 730, "frequency": "r", "synset": "octopus.n.02"}, + {"name": "oil_lamp", "id": 731, "frequency": "c", "synset": "oil_lamp.n.01"}, + {"name": "olive_oil", "id": 732, "frequency": "c", "synset": "olive_oil.n.01"}, + {"name": "omelet", "id": 733, "frequency": "r", "synset": "omelet.n.01"}, + {"name": "onion", "id": 734, "frequency": "f", "synset": "onion.n.01"}, + {"name": "orange_(fruit)", "id": 735, "frequency": "f", "synset": "orange.n.01"}, + {"name": "orange_juice", "id": 736, "frequency": "c", "synset": "orange_juice.n.01"}, + {"name": "ostrich", "id": 737, "frequency": "c", "synset": "ostrich.n.02"}, + {"name": "ottoman", "id": 738, "frequency": "f", "synset": "ottoman.n.03"}, + {"name": "oven", "id": 739, "frequency": "f", "synset": "oven.n.01"}, + {"name": "overalls_(clothing)", "id": 740, "frequency": "c", "synset": "overall.n.01"}, + {"name": "owl", "id": 741, "frequency": "c", "synset": "owl.n.01"}, + {"name": "packet", "id": 742, "frequency": "c", "synset": "packet.n.03"}, + {"name": "inkpad", "id": 743, "frequency": "r", "synset": "pad.n.03"}, + {"name": "pad", "id": 744, "frequency": "c", "synset": "pad.n.04"}, + {"name": "paddle", "id": 745, "frequency": "f", "synset": "paddle.n.04"}, + {"name": "padlock", "id": 746, "frequency": "c", "synset": "padlock.n.01"}, + {"name": "paintbrush", "id": 747, "frequency": "c", "synset": "paintbrush.n.01"}, + {"name": "painting", "id": 748, "frequency": "f", "synset": "painting.n.01"}, + {"name": "pajamas", "id": 749, "frequency": "f", "synset": "pajama.n.02"}, + {"name": "palette", "id": 750, "frequency": "c", "synset": "palette.n.02"}, + {"name": "pan_(for_cooking)", "id": 751, "frequency": "f", "synset": "pan.n.01"}, + {"name": "pan_(metal_container)", "id": 752, "frequency": "r", "synset": "pan.n.03"}, + {"name": "pancake", "id": 753, "frequency": "c", "synset": "pancake.n.01"}, + {"name": "pantyhose", "id": 754, "frequency": "r", "synset": "pantyhose.n.01"}, + {"name": "papaya", "id": 755, "frequency": "r", "synset": "papaya.n.02"}, + {"name": "paper_plate", "id": 756, "frequency": "f", "synset": "paper_plate.n.01"}, + {"name": "paper_towel", "id": 757, "frequency": "f", "synset": "paper_towel.n.01"}, + {"name": "paperback_book", "id": 758, "frequency": "r", "synset": "paperback_book.n.01"}, + {"name": "paperweight", "id": 759, "frequency": "r", "synset": "paperweight.n.01"}, + {"name": "parachute", "id": 760, "frequency": "c", "synset": "parachute.n.01"}, + {"name": "parakeet", "id": 761, "frequency": "c", "synset": "parakeet.n.01"}, + {"name": "parasail_(sports)", "id": 762, "frequency": "c", "synset": "parasail.n.01"}, + {"name": "parasol", "id": 763, "frequency": "c", "synset": "parasol.n.01"}, + {"name": "parchment", "id": 764, "frequency": "r", "synset": "parchment.n.01"}, + {"name": "parka", "id": 765, "frequency": "c", "synset": "parka.n.01"}, + {"name": "parking_meter", "id": 766, "frequency": "f", "synset": "parking_meter.n.01"}, + {"name": "parrot", "id": 767, "frequency": "c", "synset": "parrot.n.01"}, + { + "name": "passenger_car_(part_of_a_train)", + "id": 768, + "frequency": "c", + "synset": "passenger_car.n.01", + }, + {"name": "passenger_ship", "id": 769, "frequency": "r", "synset": "passenger_ship.n.01"}, + {"name": "passport", "id": 770, "frequency": "c", "synset": "passport.n.02"}, + {"name": "pastry", "id": 771, "frequency": "f", "synset": "pastry.n.02"}, + {"name": "patty_(food)", "id": 772, "frequency": "r", "synset": "patty.n.01"}, + {"name": "pea_(food)", "id": 773, "frequency": "c", "synset": "pea.n.01"}, + {"name": "peach", "id": 774, "frequency": "c", "synset": "peach.n.03"}, + {"name": "peanut_butter", "id": 775, "frequency": "c", "synset": "peanut_butter.n.01"}, + {"name": "pear", "id": 776, "frequency": "f", "synset": "pear.n.01"}, + { + "name": "peeler_(tool_for_fruit_and_vegetables)", + "id": 777, + "frequency": "c", + "synset": "peeler.n.03", + }, + {"name": "wooden_leg", "id": 778, "frequency": "r", "synset": "peg.n.04"}, + {"name": "pegboard", "id": 779, "frequency": "r", "synset": "pegboard.n.01"}, + {"name": "pelican", "id": 780, "frequency": "c", "synset": "pelican.n.01"}, + {"name": "pen", "id": 781, "frequency": "f", "synset": "pen.n.01"}, + {"name": "pencil", "id": 782, "frequency": "f", "synset": "pencil.n.01"}, + {"name": "pencil_box", "id": 783, "frequency": "r", "synset": "pencil_box.n.01"}, + {"name": "pencil_sharpener", "id": 784, "frequency": "r", "synset": "pencil_sharpener.n.01"}, + {"name": "pendulum", "id": 785, "frequency": "r", "synset": "pendulum.n.01"}, + {"name": "penguin", "id": 786, "frequency": "c", "synset": "penguin.n.01"}, + {"name": "pennant", "id": 787, "frequency": "r", "synset": "pennant.n.02"}, + {"name": "penny_(coin)", "id": 788, "frequency": "r", "synset": "penny.n.02"}, + {"name": "pepper", "id": 789, "frequency": "f", "synset": "pepper.n.03"}, + {"name": "pepper_mill", "id": 790, "frequency": "c", "synset": "pepper_mill.n.01"}, + {"name": "perfume", "id": 791, "frequency": "c", "synset": "perfume.n.02"}, + {"name": "persimmon", "id": 792, "frequency": "r", "synset": "persimmon.n.02"}, + {"name": "person", "id": 793, "frequency": "f", "synset": "person.n.01"}, + {"name": "pet", "id": 794, "frequency": "c", "synset": "pet.n.01"}, + {"name": "pew_(church_bench)", "id": 795, "frequency": "c", "synset": "pew.n.01"}, + {"name": "phonebook", "id": 796, "frequency": "r", "synset": "phonebook.n.01"}, + {"name": "phonograph_record", "id": 797, "frequency": "c", "synset": "phonograph_record.n.01"}, + {"name": "piano", "id": 798, "frequency": "f", "synset": "piano.n.01"}, + {"name": "pickle", "id": 799, "frequency": "f", "synset": "pickle.n.01"}, + {"name": "pickup_truck", "id": 800, "frequency": "f", "synset": "pickup.n.01"}, + {"name": "pie", "id": 801, "frequency": "c", "synset": "pie.n.01"}, + {"name": "pigeon", "id": 802, "frequency": "c", "synset": "pigeon.n.01"}, + {"name": "piggy_bank", "id": 803, "frequency": "r", "synset": "piggy_bank.n.01"}, + {"name": "pillow", "id": 804, "frequency": "f", "synset": "pillow.n.01"}, + {"name": "pin_(non_jewelry)", "id": 805, "frequency": "r", "synset": "pin.n.09"}, + {"name": "pineapple", "id": 806, "frequency": "f", "synset": "pineapple.n.02"}, + {"name": "pinecone", "id": 807, "frequency": "c", "synset": "pinecone.n.01"}, + {"name": "ping-pong_ball", "id": 808, "frequency": "r", "synset": "ping-pong_ball.n.01"}, + {"name": "pinwheel", "id": 809, "frequency": "r", "synset": "pinwheel.n.03"}, + {"name": "tobacco_pipe", "id": 810, "frequency": "r", "synset": "pipe.n.01"}, + {"name": "pipe", "id": 811, "frequency": "f", "synset": "pipe.n.02"}, + {"name": "pistol", "id": 812, "frequency": "r", "synset": "pistol.n.01"}, + {"name": "pita_(bread)", "id": 813, "frequency": "c", "synset": "pita.n.01"}, + {"name": "pitcher_(vessel_for_liquid)", "id": 814, "frequency": "f", "synset": "pitcher.n.02"}, + {"name": "pitchfork", "id": 815, "frequency": "r", "synset": "pitchfork.n.01"}, + {"name": "pizza", "id": 816, "frequency": "f", "synset": "pizza.n.01"}, + {"name": "place_mat", "id": 817, "frequency": "f", "synset": "place_mat.n.01"}, + {"name": "plate", "id": 818, "frequency": "f", "synset": "plate.n.04"}, + {"name": "platter", "id": 819, "frequency": "c", "synset": "platter.n.01"}, + {"name": "playpen", "id": 820, "frequency": "r", "synset": "playpen.n.01"}, + {"name": "pliers", "id": 821, "frequency": "c", "synset": "pliers.n.01"}, + {"name": "plow_(farm_equipment)", "id": 822, "frequency": "r", "synset": "plow.n.01"}, + {"name": "plume", "id": 823, "frequency": "r", "synset": "plume.n.02"}, + {"name": "pocket_watch", "id": 824, "frequency": "r", "synset": "pocket_watch.n.01"}, + {"name": "pocketknife", "id": 825, "frequency": "c", "synset": "pocketknife.n.01"}, + {"name": "poker_(fire_stirring_tool)", "id": 826, "frequency": "c", "synset": "poker.n.01"}, + {"name": "pole", "id": 827, "frequency": "f", "synset": "pole.n.01"}, + {"name": "polo_shirt", "id": 828, "frequency": "f", "synset": "polo_shirt.n.01"}, + {"name": "poncho", "id": 829, "frequency": "r", "synset": "poncho.n.01"}, + {"name": "pony", "id": 830, "frequency": "c", "synset": "pony.n.05"}, + {"name": "pool_table", "id": 831, "frequency": "r", "synset": "pool_table.n.01"}, + {"name": "pop_(soda)", "id": 832, "frequency": "f", "synset": "pop.n.02"}, + {"name": "postbox_(public)", "id": 833, "frequency": "c", "synset": "postbox.n.01"}, + {"name": "postcard", "id": 834, "frequency": "c", "synset": "postcard.n.01"}, + {"name": "poster", "id": 835, "frequency": "f", "synset": "poster.n.01"}, + {"name": "pot", "id": 836, "frequency": "f", "synset": "pot.n.01"}, + {"name": "flowerpot", "id": 837, "frequency": "f", "synset": "pot.n.04"}, + {"name": "potato", "id": 838, "frequency": "f", "synset": "potato.n.01"}, + {"name": "potholder", "id": 839, "frequency": "c", "synset": "potholder.n.01"}, + {"name": "pottery", "id": 840, "frequency": "c", "synset": "pottery.n.01"}, + {"name": "pouch", "id": 841, "frequency": "c", "synset": "pouch.n.01"}, + {"name": "power_shovel", "id": 842, "frequency": "c", "synset": "power_shovel.n.01"}, + {"name": "prawn", "id": 843, "frequency": "c", "synset": "prawn.n.01"}, + {"name": "pretzel", "id": 844, "frequency": "c", "synset": "pretzel.n.01"}, + {"name": "printer", "id": 845, "frequency": "f", "synset": "printer.n.03"}, + {"name": "projectile_(weapon)", "id": 846, "frequency": "c", "synset": "projectile.n.01"}, + {"name": "projector", "id": 847, "frequency": "c", "synset": "projector.n.02"}, + {"name": "propeller", "id": 848, "frequency": "f", "synset": "propeller.n.01"}, + {"name": "prune", "id": 849, "frequency": "r", "synset": "prune.n.01"}, + {"name": "pudding", "id": 850, "frequency": "r", "synset": "pudding.n.01"}, + {"name": "puffer_(fish)", "id": 851, "frequency": "r", "synset": "puffer.n.02"}, + {"name": "puffin", "id": 852, "frequency": "r", "synset": "puffin.n.01"}, + {"name": "pug-dog", "id": 853, "frequency": "r", "synset": "pug.n.01"}, + {"name": "pumpkin", "id": 854, "frequency": "c", "synset": "pumpkin.n.02"}, + {"name": "puncher", "id": 855, "frequency": "r", "synset": "punch.n.03"}, + {"name": "puppet", "id": 856, "frequency": "r", "synset": "puppet.n.01"}, + {"name": "puppy", "id": 857, "frequency": "c", "synset": "puppy.n.01"}, + {"name": "quesadilla", "id": 858, "frequency": "r", "synset": "quesadilla.n.01"}, + {"name": "quiche", "id": 859, "frequency": "r", "synset": "quiche.n.02"}, + {"name": "quilt", "id": 860, "frequency": "f", "synset": "quilt.n.01"}, + {"name": "rabbit", "id": 861, "frequency": "c", "synset": "rabbit.n.01"}, + {"name": "race_car", "id": 862, "frequency": "r", "synset": "racer.n.02"}, + {"name": "racket", "id": 863, "frequency": "c", "synset": "racket.n.04"}, + {"name": "radar", "id": 864, "frequency": "r", "synset": "radar.n.01"}, + {"name": "radiator", "id": 865, "frequency": "f", "synset": "radiator.n.03"}, + {"name": "radio_receiver", "id": 866, "frequency": "c", "synset": "radio_receiver.n.01"}, + {"name": "radish", "id": 867, "frequency": "c", "synset": "radish.n.03"}, + {"name": "raft", "id": 868, "frequency": "c", "synset": "raft.n.01"}, + {"name": "rag_doll", "id": 869, "frequency": "r", "synset": "rag_doll.n.01"}, + {"name": "raincoat", "id": 870, "frequency": "c", "synset": "raincoat.n.01"}, + {"name": "ram_(animal)", "id": 871, "frequency": "c", "synset": "ram.n.05"}, + {"name": "raspberry", "id": 872, "frequency": "c", "synset": "raspberry.n.02"}, + {"name": "rat", "id": 873, "frequency": "r", "synset": "rat.n.01"}, + {"name": "razorblade", "id": 874, "frequency": "c", "synset": "razorblade.n.01"}, + {"name": "reamer_(juicer)", "id": 875, "frequency": "c", "synset": "reamer.n.01"}, + {"name": "rearview_mirror", "id": 876, "frequency": "f", "synset": "rearview_mirror.n.01"}, + {"name": "receipt", "id": 877, "frequency": "c", "synset": "receipt.n.02"}, + {"name": "recliner", "id": 878, "frequency": "c", "synset": "recliner.n.01"}, + {"name": "record_player", "id": 879, "frequency": "c", "synset": "record_player.n.01"}, + {"name": "reflector", "id": 880, "frequency": "f", "synset": "reflector.n.01"}, + {"name": "remote_control", "id": 881, "frequency": "f", "synset": "remote_control.n.01"}, + {"name": "rhinoceros", "id": 882, "frequency": "c", "synset": "rhinoceros.n.01"}, + {"name": "rib_(food)", "id": 883, "frequency": "r", "synset": "rib.n.03"}, + {"name": "rifle", "id": 884, "frequency": "c", "synset": "rifle.n.01"}, + {"name": "ring", "id": 885, "frequency": "f", "synset": "ring.n.08"}, + {"name": "river_boat", "id": 886, "frequency": "r", "synset": "river_boat.n.01"}, + {"name": "road_map", "id": 887, "frequency": "r", "synset": "road_map.n.02"}, + {"name": "robe", "id": 888, "frequency": "c", "synset": "robe.n.01"}, + {"name": "rocking_chair", "id": 889, "frequency": "c", "synset": "rocking_chair.n.01"}, + {"name": "rodent", "id": 890, "frequency": "r", "synset": "rodent.n.01"}, + {"name": "roller_skate", "id": 891, "frequency": "r", "synset": "roller_skate.n.01"}, + {"name": "Rollerblade", "id": 892, "frequency": "r", "synset": "rollerblade.n.01"}, + {"name": "rolling_pin", "id": 893, "frequency": "c", "synset": "rolling_pin.n.01"}, + {"name": "root_beer", "id": 894, "frequency": "r", "synset": "root_beer.n.01"}, + {"name": "router_(computer_equipment)", "id": 895, "frequency": "c", "synset": "router.n.02"}, + {"name": "rubber_band", "id": 896, "frequency": "f", "synset": "rubber_band.n.01"}, + {"name": "runner_(carpet)", "id": 897, "frequency": "c", "synset": "runner.n.08"}, + {"name": "plastic_bag", "id": 898, "frequency": "f", "synset": "sack.n.01"}, + {"name": "saddle_(on_an_animal)", "id": 899, "frequency": "f", "synset": "saddle.n.01"}, + {"name": "saddle_blanket", "id": 900, "frequency": "f", "synset": "saddle_blanket.n.01"}, + {"name": "saddlebag", "id": 901, "frequency": "c", "synset": "saddlebag.n.01"}, + {"name": "safety_pin", "id": 902, "frequency": "r", "synset": "safety_pin.n.01"}, + {"name": "sail", "id": 903, "frequency": "f", "synset": "sail.n.01"}, + {"name": "salad", "id": 904, "frequency": "f", "synset": "salad.n.01"}, + {"name": "salad_plate", "id": 905, "frequency": "r", "synset": "salad_plate.n.01"}, + {"name": "salami", "id": 906, "frequency": "c", "synset": "salami.n.01"}, + {"name": "salmon_(fish)", "id": 907, "frequency": "c", "synset": "salmon.n.01"}, + {"name": "salmon_(food)", "id": 908, "frequency": "r", "synset": "salmon.n.03"}, + {"name": "salsa", "id": 909, "frequency": "c", "synset": "salsa.n.01"}, + {"name": "saltshaker", "id": 910, "frequency": "f", "synset": "saltshaker.n.01"}, + {"name": "sandal_(type_of_shoe)", "id": 911, "frequency": "f", "synset": "sandal.n.01"}, + {"name": "sandwich", "id": 912, "frequency": "f", "synset": "sandwich.n.01"}, + {"name": "satchel", "id": 913, "frequency": "r", "synset": "satchel.n.01"}, + {"name": "saucepan", "id": 914, "frequency": "r", "synset": "saucepan.n.01"}, + {"name": "saucer", "id": 915, "frequency": "f", "synset": "saucer.n.02"}, + {"name": "sausage", "id": 916, "frequency": "f", "synset": "sausage.n.01"}, + {"name": "sawhorse", "id": 917, "frequency": "r", "synset": "sawhorse.n.01"}, + {"name": "saxophone", "id": 918, "frequency": "r", "synset": "sax.n.02"}, + {"name": "scale_(measuring_instrument)", "id": 919, "frequency": "f", "synset": "scale.n.07"}, + {"name": "scarecrow", "id": 920, "frequency": "r", "synset": "scarecrow.n.01"}, + {"name": "scarf", "id": 921, "frequency": "f", "synset": "scarf.n.01"}, + {"name": "school_bus", "id": 922, "frequency": "c", "synset": "school_bus.n.01"}, + {"name": "scissors", "id": 923, "frequency": "f", "synset": "scissors.n.01"}, + {"name": "scoreboard", "id": 924, "frequency": "f", "synset": "scoreboard.n.01"}, + {"name": "scraper", "id": 925, "frequency": "r", "synset": "scraper.n.01"}, + {"name": "screwdriver", "id": 926, "frequency": "c", "synset": "screwdriver.n.01"}, + {"name": "scrubbing_brush", "id": 927, "frequency": "f", "synset": "scrub_brush.n.01"}, + {"name": "sculpture", "id": 928, "frequency": "c", "synset": "sculpture.n.01"}, + {"name": "seabird", "id": 929, "frequency": "c", "synset": "seabird.n.01"}, + {"name": "seahorse", "id": 930, "frequency": "c", "synset": "seahorse.n.02"}, + {"name": "seaplane", "id": 931, "frequency": "r", "synset": "seaplane.n.01"}, + {"name": "seashell", "id": 932, "frequency": "c", "synset": "seashell.n.01"}, + {"name": "sewing_machine", "id": 933, "frequency": "c", "synset": "sewing_machine.n.01"}, + {"name": "shaker", "id": 934, "frequency": "c", "synset": "shaker.n.03"}, + {"name": "shampoo", "id": 935, "frequency": "c", "synset": "shampoo.n.01"}, + {"name": "shark", "id": 936, "frequency": "c", "synset": "shark.n.01"}, + {"name": "sharpener", "id": 937, "frequency": "r", "synset": "sharpener.n.01"}, + {"name": "Sharpie", "id": 938, "frequency": "r", "synset": "sharpie.n.03"}, + {"name": "shaver_(electric)", "id": 939, "frequency": "r", "synset": "shaver.n.03"}, + {"name": "shaving_cream", "id": 940, "frequency": "c", "synset": "shaving_cream.n.01"}, + {"name": "shawl", "id": 941, "frequency": "r", "synset": "shawl.n.01"}, + {"name": "shears", "id": 942, "frequency": "r", "synset": "shears.n.01"}, + {"name": "sheep", "id": 943, "frequency": "f", "synset": "sheep.n.01"}, + {"name": "shepherd_dog", "id": 944, "frequency": "r", "synset": "shepherd_dog.n.01"}, + {"name": "sherbert", "id": 945, "frequency": "r", "synset": "sherbert.n.01"}, + {"name": "shield", "id": 946, "frequency": "c", "synset": "shield.n.02"}, + {"name": "shirt", "id": 947, "frequency": "f", "synset": "shirt.n.01"}, + {"name": "shoe", "id": 948, "frequency": "f", "synset": "shoe.n.01"}, + {"name": "shopping_bag", "id": 949, "frequency": "f", "synset": "shopping_bag.n.01"}, + {"name": "shopping_cart", "id": 950, "frequency": "c", "synset": "shopping_cart.n.01"}, + {"name": "short_pants", "id": 951, "frequency": "f", "synset": "short_pants.n.01"}, + {"name": "shot_glass", "id": 952, "frequency": "r", "synset": "shot_glass.n.01"}, + {"name": "shoulder_bag", "id": 953, "frequency": "f", "synset": "shoulder_bag.n.01"}, + {"name": "shovel", "id": 954, "frequency": "c", "synset": "shovel.n.01"}, + {"name": "shower_head", "id": 955, "frequency": "f", "synset": "shower.n.01"}, + {"name": "shower_cap", "id": 956, "frequency": "r", "synset": "shower_cap.n.01"}, + {"name": "shower_curtain", "id": 957, "frequency": "f", "synset": "shower_curtain.n.01"}, + {"name": "shredder_(for_paper)", "id": 958, "frequency": "r", "synset": "shredder.n.01"}, + {"name": "signboard", "id": 959, "frequency": "f", "synset": "signboard.n.01"}, + {"name": "silo", "id": 960, "frequency": "c", "synset": "silo.n.01"}, + {"name": "sink", "id": 961, "frequency": "f", "synset": "sink.n.01"}, + {"name": "skateboard", "id": 962, "frequency": "f", "synset": "skateboard.n.01"}, + {"name": "skewer", "id": 963, "frequency": "c", "synset": "skewer.n.01"}, + {"name": "ski", "id": 964, "frequency": "f", "synset": "ski.n.01"}, + {"name": "ski_boot", "id": 965, "frequency": "f", "synset": "ski_boot.n.01"}, + {"name": "ski_parka", "id": 966, "frequency": "f", "synset": "ski_parka.n.01"}, + {"name": "ski_pole", "id": 967, "frequency": "f", "synset": "ski_pole.n.01"}, + {"name": "skirt", "id": 968, "frequency": "f", "synset": "skirt.n.02"}, + {"name": "skullcap", "id": 969, "frequency": "r", "synset": "skullcap.n.01"}, + {"name": "sled", "id": 970, "frequency": "c", "synset": "sled.n.01"}, + {"name": "sleeping_bag", "id": 971, "frequency": "c", "synset": "sleeping_bag.n.01"}, + {"name": "sling_(bandage)", "id": 972, "frequency": "r", "synset": "sling.n.05"}, + {"name": "slipper_(footwear)", "id": 973, "frequency": "c", "synset": "slipper.n.01"}, + {"name": "smoothie", "id": 974, "frequency": "r", "synset": "smoothie.n.02"}, + {"name": "snake", "id": 975, "frequency": "r", "synset": "snake.n.01"}, + {"name": "snowboard", "id": 976, "frequency": "f", "synset": "snowboard.n.01"}, + {"name": "snowman", "id": 977, "frequency": "c", "synset": "snowman.n.01"}, + {"name": "snowmobile", "id": 978, "frequency": "c", "synset": "snowmobile.n.01"}, + {"name": "soap", "id": 979, "frequency": "f", "synset": "soap.n.01"}, + {"name": "soccer_ball", "id": 980, "frequency": "f", "synset": "soccer_ball.n.01"}, + {"name": "sock", "id": 981, "frequency": "f", "synset": "sock.n.01"}, + {"name": "sofa", "id": 982, "frequency": "f", "synset": "sofa.n.01"}, + {"name": "softball", "id": 983, "frequency": "r", "synset": "softball.n.01"}, + {"name": "solar_array", "id": 984, "frequency": "c", "synset": "solar_array.n.01"}, + {"name": "sombrero", "id": 985, "frequency": "r", "synset": "sombrero.n.02"}, + {"name": "soup", "id": 986, "frequency": "f", "synset": "soup.n.01"}, + {"name": "soup_bowl", "id": 987, "frequency": "r", "synset": "soup_bowl.n.01"}, + {"name": "soupspoon", "id": 988, "frequency": "c", "synset": "soupspoon.n.01"}, + {"name": "sour_cream", "id": 989, "frequency": "c", "synset": "sour_cream.n.01"}, + {"name": "soya_milk", "id": 990, "frequency": "r", "synset": "soya_milk.n.01"}, + {"name": "space_shuttle", "id": 991, "frequency": "r", "synset": "space_shuttle.n.01"}, + {"name": "sparkler_(fireworks)", "id": 992, "frequency": "r", "synset": "sparkler.n.02"}, + {"name": "spatula", "id": 993, "frequency": "f", "synset": "spatula.n.02"}, + {"name": "spear", "id": 994, "frequency": "r", "synset": "spear.n.01"}, + {"name": "spectacles", "id": 995, "frequency": "f", "synset": "spectacles.n.01"}, + {"name": "spice_rack", "id": 996, "frequency": "c", "synset": "spice_rack.n.01"}, + {"name": "spider", "id": 997, "frequency": "c", "synset": "spider.n.01"}, + {"name": "crawfish", "id": 998, "frequency": "r", "synset": "spiny_lobster.n.02"}, + {"name": "sponge", "id": 999, "frequency": "c", "synset": "sponge.n.01"}, + {"name": "spoon", "id": 1000, "frequency": "f", "synset": "spoon.n.01"}, + {"name": "sportswear", "id": 1001, "frequency": "c", "synset": "sportswear.n.01"}, + {"name": "spotlight", "id": 1002, "frequency": "c", "synset": "spotlight.n.02"}, + {"name": "squid_(food)", "id": 1003, "frequency": "r", "synset": "squid.n.01"}, + {"name": "squirrel", "id": 1004, "frequency": "c", "synset": "squirrel.n.01"}, + {"name": "stagecoach", "id": 1005, "frequency": "r", "synset": "stagecoach.n.01"}, + {"name": "stapler_(stapling_machine)", "id": 1006, "frequency": "c", "synset": "stapler.n.01"}, + {"name": "starfish", "id": 1007, "frequency": "c", "synset": "starfish.n.01"}, + {"name": "statue_(sculpture)", "id": 1008, "frequency": "f", "synset": "statue.n.01"}, + {"name": "steak_(food)", "id": 1009, "frequency": "c", "synset": "steak.n.01"}, + {"name": "steak_knife", "id": 1010, "frequency": "r", "synset": "steak_knife.n.01"}, + {"name": "steering_wheel", "id": 1011, "frequency": "f", "synset": "steering_wheel.n.01"}, + {"name": "stepladder", "id": 1012, "frequency": "r", "synset": "step_ladder.n.01"}, + {"name": "step_stool", "id": 1013, "frequency": "c", "synset": "step_stool.n.01"}, + {"name": "stereo_(sound_system)", "id": 1014, "frequency": "c", "synset": "stereo.n.01"}, + {"name": "stew", "id": 1015, "frequency": "r", "synset": "stew.n.02"}, + {"name": "stirrer", "id": 1016, "frequency": "r", "synset": "stirrer.n.02"}, + {"name": "stirrup", "id": 1017, "frequency": "f", "synset": "stirrup.n.01"}, + {"name": "stool", "id": 1018, "frequency": "f", "synset": "stool.n.01"}, + {"name": "stop_sign", "id": 1019, "frequency": "f", "synset": "stop_sign.n.01"}, + {"name": "brake_light", "id": 1020, "frequency": "f", "synset": "stoplight.n.01"}, + {"name": "stove", "id": 1021, "frequency": "f", "synset": "stove.n.01"}, + {"name": "strainer", "id": 1022, "frequency": "c", "synset": "strainer.n.01"}, + {"name": "strap", "id": 1023, "frequency": "f", "synset": "strap.n.01"}, + {"name": "straw_(for_drinking)", "id": 1024, "frequency": "f", "synset": "straw.n.04"}, + {"name": "strawberry", "id": 1025, "frequency": "f", "synset": "strawberry.n.01"}, + {"name": "street_sign", "id": 1026, "frequency": "f", "synset": "street_sign.n.01"}, + {"name": "streetlight", "id": 1027, "frequency": "f", "synset": "streetlight.n.01"}, + {"name": "string_cheese", "id": 1028, "frequency": "r", "synset": "string_cheese.n.01"}, + {"name": "stylus", "id": 1029, "frequency": "r", "synset": "stylus.n.02"}, + {"name": "subwoofer", "id": 1030, "frequency": "r", "synset": "subwoofer.n.01"}, + {"name": "sugar_bowl", "id": 1031, "frequency": "r", "synset": "sugar_bowl.n.01"}, + {"name": "sugarcane_(plant)", "id": 1032, "frequency": "r", "synset": "sugarcane.n.01"}, + {"name": "suit_(clothing)", "id": 1033, "frequency": "f", "synset": "suit.n.01"}, + {"name": "sunflower", "id": 1034, "frequency": "c", "synset": "sunflower.n.01"}, + {"name": "sunglasses", "id": 1035, "frequency": "f", "synset": "sunglasses.n.01"}, + {"name": "sunhat", "id": 1036, "frequency": "c", "synset": "sunhat.n.01"}, + {"name": "surfboard", "id": 1037, "frequency": "f", "synset": "surfboard.n.01"}, + {"name": "sushi", "id": 1038, "frequency": "c", "synset": "sushi.n.01"}, + {"name": "mop", "id": 1039, "frequency": "c", "synset": "swab.n.02"}, + {"name": "sweat_pants", "id": 1040, "frequency": "c", "synset": "sweat_pants.n.01"}, + {"name": "sweatband", "id": 1041, "frequency": "c", "synset": "sweatband.n.02"}, + {"name": "sweater", "id": 1042, "frequency": "f", "synset": "sweater.n.01"}, + {"name": "sweatshirt", "id": 1043, "frequency": "f", "synset": "sweatshirt.n.01"}, + {"name": "sweet_potato", "id": 1044, "frequency": "c", "synset": "sweet_potato.n.02"}, + {"name": "swimsuit", "id": 1045, "frequency": "f", "synset": "swimsuit.n.01"}, + {"name": "sword", "id": 1046, "frequency": "c", "synset": "sword.n.01"}, + {"name": "syringe", "id": 1047, "frequency": "r", "synset": "syringe.n.01"}, + {"name": "Tabasco_sauce", "id": 1048, "frequency": "r", "synset": "tabasco.n.02"}, + { + "name": "table-tennis_table", + "id": 1049, + "frequency": "r", + "synset": "table-tennis_table.n.01", + }, + {"name": "table", "id": 1050, "frequency": "f", "synset": "table.n.02"}, + {"name": "table_lamp", "id": 1051, "frequency": "c", "synset": "table_lamp.n.01"}, + {"name": "tablecloth", "id": 1052, "frequency": "f", "synset": "tablecloth.n.01"}, + {"name": "tachometer", "id": 1053, "frequency": "r", "synset": "tachometer.n.01"}, + {"name": "taco", "id": 1054, "frequency": "r", "synset": "taco.n.02"}, + {"name": "tag", "id": 1055, "frequency": "f", "synset": "tag.n.02"}, + {"name": "taillight", "id": 1056, "frequency": "f", "synset": "taillight.n.01"}, + {"name": "tambourine", "id": 1057, "frequency": "r", "synset": "tambourine.n.01"}, + {"name": "army_tank", "id": 1058, "frequency": "r", "synset": "tank.n.01"}, + {"name": "tank_(storage_vessel)", "id": 1059, "frequency": "f", "synset": "tank.n.02"}, + {"name": "tank_top_(clothing)", "id": 1060, "frequency": "f", "synset": "tank_top.n.01"}, + {"name": "tape_(sticky_cloth_or_paper)", "id": 1061, "frequency": "f", "synset": "tape.n.01"}, + {"name": "tape_measure", "id": 1062, "frequency": "c", "synset": "tape.n.04"}, + {"name": "tapestry", "id": 1063, "frequency": "c", "synset": "tapestry.n.02"}, + {"name": "tarp", "id": 1064, "frequency": "f", "synset": "tarpaulin.n.01"}, + {"name": "tartan", "id": 1065, "frequency": "c", "synset": "tartan.n.01"}, + {"name": "tassel", "id": 1066, "frequency": "c", "synset": "tassel.n.01"}, + {"name": "tea_bag", "id": 1067, "frequency": "c", "synset": "tea_bag.n.01"}, + {"name": "teacup", "id": 1068, "frequency": "c", "synset": "teacup.n.02"}, + {"name": "teakettle", "id": 1069, "frequency": "c", "synset": "teakettle.n.01"}, + {"name": "teapot", "id": 1070, "frequency": "f", "synset": "teapot.n.01"}, + {"name": "teddy_bear", "id": 1071, "frequency": "f", "synset": "teddy.n.01"}, + {"name": "telephone", "id": 1072, "frequency": "f", "synset": "telephone.n.01"}, + {"name": "telephone_booth", "id": 1073, "frequency": "c", "synset": "telephone_booth.n.01"}, + {"name": "telephone_pole", "id": 1074, "frequency": "f", "synset": "telephone_pole.n.01"}, + {"name": "telephoto_lens", "id": 1075, "frequency": "r", "synset": "telephoto_lens.n.01"}, + {"name": "television_camera", "id": 1076, "frequency": "c", "synset": "television_camera.n.01"}, + {"name": "television_set", "id": 1077, "frequency": "f", "synset": "television_receiver.n.01"}, + {"name": "tennis_ball", "id": 1078, "frequency": "f", "synset": "tennis_ball.n.01"}, + {"name": "tennis_racket", "id": 1079, "frequency": "f", "synset": "tennis_racket.n.01"}, + {"name": "tequila", "id": 1080, "frequency": "r", "synset": "tequila.n.01"}, + {"name": "thermometer", "id": 1081, "frequency": "c", "synset": "thermometer.n.01"}, + {"name": "thermos_bottle", "id": 1082, "frequency": "c", "synset": "thermos.n.01"}, + {"name": "thermostat", "id": 1083, "frequency": "f", "synset": "thermostat.n.01"}, + {"name": "thimble", "id": 1084, "frequency": "r", "synset": "thimble.n.02"}, + {"name": "thread", "id": 1085, "frequency": "c", "synset": "thread.n.01"}, + {"name": "thumbtack", "id": 1086, "frequency": "c", "synset": "thumbtack.n.01"}, + {"name": "tiara", "id": 1087, "frequency": "c", "synset": "tiara.n.01"}, + {"name": "tiger", "id": 1088, "frequency": "c", "synset": "tiger.n.02"}, + {"name": "tights_(clothing)", "id": 1089, "frequency": "c", "synset": "tights.n.01"}, + {"name": "timer", "id": 1090, "frequency": "c", "synset": "timer.n.01"}, + {"name": "tinfoil", "id": 1091, "frequency": "f", "synset": "tinfoil.n.01"}, + {"name": "tinsel", "id": 1092, "frequency": "c", "synset": "tinsel.n.01"}, + {"name": "tissue_paper", "id": 1093, "frequency": "f", "synset": "tissue.n.02"}, + {"name": "toast_(food)", "id": 1094, "frequency": "c", "synset": "toast.n.01"}, + {"name": "toaster", "id": 1095, "frequency": "f", "synset": "toaster.n.02"}, + {"name": "toaster_oven", "id": 1096, "frequency": "f", "synset": "toaster_oven.n.01"}, + {"name": "toilet", "id": 1097, "frequency": "f", "synset": "toilet.n.02"}, + {"name": "toilet_tissue", "id": 1098, "frequency": "f", "synset": "toilet_tissue.n.01"}, + {"name": "tomato", "id": 1099, "frequency": "f", "synset": "tomato.n.01"}, + {"name": "tongs", "id": 1100, "frequency": "f", "synset": "tongs.n.01"}, + {"name": "toolbox", "id": 1101, "frequency": "c", "synset": "toolbox.n.01"}, + {"name": "toothbrush", "id": 1102, "frequency": "f", "synset": "toothbrush.n.01"}, + {"name": "toothpaste", "id": 1103, "frequency": "f", "synset": "toothpaste.n.01"}, + {"name": "toothpick", "id": 1104, "frequency": "f", "synset": "toothpick.n.01"}, + {"name": "cover", "id": 1105, "frequency": "f", "synset": "top.n.09"}, + {"name": "tortilla", "id": 1106, "frequency": "c", "synset": "tortilla.n.01"}, + {"name": "tow_truck", "id": 1107, "frequency": "c", "synset": "tow_truck.n.01"}, + {"name": "towel", "id": 1108, "frequency": "f", "synset": "towel.n.01"}, + {"name": "towel_rack", "id": 1109, "frequency": "f", "synset": "towel_rack.n.01"}, + {"name": "toy", "id": 1110, "frequency": "f", "synset": "toy.n.03"}, + {"name": "tractor_(farm_equipment)", "id": 1111, "frequency": "c", "synset": "tractor.n.01"}, + {"name": "traffic_light", "id": 1112, "frequency": "f", "synset": "traffic_light.n.01"}, + {"name": "dirt_bike", "id": 1113, "frequency": "c", "synset": "trail_bike.n.01"}, + {"name": "trailer_truck", "id": 1114, "frequency": "f", "synset": "trailer_truck.n.01"}, + {"name": "train_(railroad_vehicle)", "id": 1115, "frequency": "f", "synset": "train.n.01"}, + {"name": "trampoline", "id": 1116, "frequency": "r", "synset": "trampoline.n.01"}, + {"name": "tray", "id": 1117, "frequency": "f", "synset": "tray.n.01"}, + {"name": "trench_coat", "id": 1118, "frequency": "r", "synset": "trench_coat.n.01"}, + { + "name": "triangle_(musical_instrument)", + "id": 1119, + "frequency": "r", + "synset": "triangle.n.05", + }, + {"name": "tricycle", "id": 1120, "frequency": "c", "synset": "tricycle.n.01"}, + {"name": "tripod", "id": 1121, "frequency": "f", "synset": "tripod.n.01"}, + {"name": "trousers", "id": 1122, "frequency": "f", "synset": "trouser.n.01"}, + {"name": "truck", "id": 1123, "frequency": "f", "synset": "truck.n.01"}, + {"name": "truffle_(chocolate)", "id": 1124, "frequency": "r", "synset": "truffle.n.03"}, + {"name": "trunk", "id": 1125, "frequency": "c", "synset": "trunk.n.02"}, + {"name": "vat", "id": 1126, "frequency": "r", "synset": "tub.n.02"}, + {"name": "turban", "id": 1127, "frequency": "c", "synset": "turban.n.01"}, + {"name": "turkey_(food)", "id": 1128, "frequency": "c", "synset": "turkey.n.04"}, + {"name": "turnip", "id": 1129, "frequency": "r", "synset": "turnip.n.01"}, + {"name": "turtle", "id": 1130, "frequency": "c", "synset": "turtle.n.02"}, + {"name": "turtleneck_(clothing)", "id": 1131, "frequency": "c", "synset": "turtleneck.n.01"}, + {"name": "typewriter", "id": 1132, "frequency": "c", "synset": "typewriter.n.01"}, + {"name": "umbrella", "id": 1133, "frequency": "f", "synset": "umbrella.n.01"}, + {"name": "underwear", "id": 1134, "frequency": "f", "synset": "underwear.n.01"}, + {"name": "unicycle", "id": 1135, "frequency": "r", "synset": "unicycle.n.01"}, + {"name": "urinal", "id": 1136, "frequency": "f", "synset": "urinal.n.01"}, + {"name": "urn", "id": 1137, "frequency": "c", "synset": "urn.n.01"}, + {"name": "vacuum_cleaner", "id": 1138, "frequency": "c", "synset": "vacuum.n.04"}, + {"name": "vase", "id": 1139, "frequency": "f", "synset": "vase.n.01"}, + {"name": "vending_machine", "id": 1140, "frequency": "c", "synset": "vending_machine.n.01"}, + {"name": "vent", "id": 1141, "frequency": "f", "synset": "vent.n.01"}, + {"name": "vest", "id": 1142, "frequency": "f", "synset": "vest.n.01"}, + {"name": "videotape", "id": 1143, "frequency": "c", "synset": "videotape.n.01"}, + {"name": "vinegar", "id": 1144, "frequency": "r", "synset": "vinegar.n.01"}, + {"name": "violin", "id": 1145, "frequency": "r", "synset": "violin.n.01"}, + {"name": "vodka", "id": 1146, "frequency": "r", "synset": "vodka.n.01"}, + {"name": "volleyball", "id": 1147, "frequency": "c", "synset": "volleyball.n.02"}, + {"name": "vulture", "id": 1148, "frequency": "r", "synset": "vulture.n.01"}, + {"name": "waffle", "id": 1149, "frequency": "c", "synset": "waffle.n.01"}, + {"name": "waffle_iron", "id": 1150, "frequency": "r", "synset": "waffle_iron.n.01"}, + {"name": "wagon", "id": 1151, "frequency": "c", "synset": "wagon.n.01"}, + {"name": "wagon_wheel", "id": 1152, "frequency": "c", "synset": "wagon_wheel.n.01"}, + {"name": "walking_stick", "id": 1153, "frequency": "c", "synset": "walking_stick.n.01"}, + {"name": "wall_clock", "id": 1154, "frequency": "c", "synset": "wall_clock.n.01"}, + {"name": "wall_socket", "id": 1155, "frequency": "f", "synset": "wall_socket.n.01"}, + {"name": "wallet", "id": 1156, "frequency": "f", "synset": "wallet.n.01"}, + {"name": "walrus", "id": 1157, "frequency": "r", "synset": "walrus.n.01"}, + {"name": "wardrobe", "id": 1158, "frequency": "r", "synset": "wardrobe.n.01"}, + {"name": "washbasin", "id": 1159, "frequency": "r", "synset": "washbasin.n.01"}, + {"name": "automatic_washer", "id": 1160, "frequency": "c", "synset": "washer.n.03"}, + {"name": "watch", "id": 1161, "frequency": "f", "synset": "watch.n.01"}, + {"name": "water_bottle", "id": 1162, "frequency": "f", "synset": "water_bottle.n.01"}, + {"name": "water_cooler", "id": 1163, "frequency": "c", "synset": "water_cooler.n.01"}, + {"name": "water_faucet", "id": 1164, "frequency": "c", "synset": "water_faucet.n.01"}, + {"name": "water_heater", "id": 1165, "frequency": "r", "synset": "water_heater.n.01"}, + {"name": "water_jug", "id": 1166, "frequency": "c", "synset": "water_jug.n.01"}, + {"name": "water_gun", "id": 1167, "frequency": "r", "synset": "water_pistol.n.01"}, + {"name": "water_scooter", "id": 1168, "frequency": "c", "synset": "water_scooter.n.01"}, + {"name": "water_ski", "id": 1169, "frequency": "c", "synset": "water_ski.n.01"}, + {"name": "water_tower", "id": 1170, "frequency": "c", "synset": "water_tower.n.01"}, + {"name": "watering_can", "id": 1171, "frequency": "c", "synset": "watering_can.n.01"}, + {"name": "watermelon", "id": 1172, "frequency": "f", "synset": "watermelon.n.02"}, + {"name": "weathervane", "id": 1173, "frequency": "f", "synset": "weathervane.n.01"}, + {"name": "webcam", "id": 1174, "frequency": "c", "synset": "webcam.n.01"}, + {"name": "wedding_cake", "id": 1175, "frequency": "c", "synset": "wedding_cake.n.01"}, + {"name": "wedding_ring", "id": 1176, "frequency": "c", "synset": "wedding_ring.n.01"}, + {"name": "wet_suit", "id": 1177, "frequency": "f", "synset": "wet_suit.n.01"}, + {"name": "wheel", "id": 1178, "frequency": "f", "synset": "wheel.n.01"}, + {"name": "wheelchair", "id": 1179, "frequency": "c", "synset": "wheelchair.n.01"}, + {"name": "whipped_cream", "id": 1180, "frequency": "c", "synset": "whipped_cream.n.01"}, + {"name": "whistle", "id": 1181, "frequency": "c", "synset": "whistle.n.03"}, + {"name": "wig", "id": 1182, "frequency": "c", "synset": "wig.n.01"}, + {"name": "wind_chime", "id": 1183, "frequency": "c", "synset": "wind_chime.n.01"}, + {"name": "windmill", "id": 1184, "frequency": "c", "synset": "windmill.n.01"}, + {"name": "window_box_(for_plants)", "id": 1185, "frequency": "c", "synset": "window_box.n.01"}, + {"name": "windshield_wiper", "id": 1186, "frequency": "f", "synset": "windshield_wiper.n.01"}, + {"name": "windsock", "id": 1187, "frequency": "c", "synset": "windsock.n.01"}, + {"name": "wine_bottle", "id": 1188, "frequency": "f", "synset": "wine_bottle.n.01"}, + {"name": "wine_bucket", "id": 1189, "frequency": "c", "synset": "wine_bucket.n.01"}, + {"name": "wineglass", "id": 1190, "frequency": "f", "synset": "wineglass.n.01"}, + {"name": "blinder_(for_horses)", "id": 1191, "frequency": "f", "synset": "winker.n.02"}, + {"name": "wok", "id": 1192, "frequency": "c", "synset": "wok.n.01"}, + {"name": "wolf", "id": 1193, "frequency": "r", "synset": "wolf.n.01"}, + {"name": "wooden_spoon", "id": 1194, "frequency": "c", "synset": "wooden_spoon.n.02"}, + {"name": "wreath", "id": 1195, "frequency": "c", "synset": "wreath.n.01"}, + {"name": "wrench", "id": 1196, "frequency": "c", "synset": "wrench.n.03"}, + {"name": "wristband", "id": 1197, "frequency": "f", "synset": "wristband.n.01"}, + {"name": "wristlet", "id": 1198, "frequency": "f", "synset": "wristlet.n.01"}, + {"name": "yacht", "id": 1199, "frequency": "c", "synset": "yacht.n.01"}, + {"name": "yogurt", "id": 1200, "frequency": "c", "synset": "yogurt.n.01"}, + {"name": "yoke_(animal_equipment)", "id": 1201, "frequency": "c", "synset": "yoke.n.07"}, + {"name": "zebra", "id": 1202, "frequency": "f", "synset": "zebra.n.01"}, + {"name": "zucchini", "id": 1203, "frequency": "c", "synset": "zucchini.n.02"}, + {"id": 1204, "synset": "organism.n.01", "name": "organism"}, + {"id": 1205, "synset": "benthos.n.02", "name": "benthos"}, + {"id": 1206, "synset": "heterotroph.n.01", "name": "heterotroph"}, + {"id": 1207, "synset": "cell.n.02", "name": "cell"}, + {"id": 1208, "synset": "animal.n.01", "name": "animal"}, + {"id": 1209, "synset": "plant.n.02", "name": "plant"}, + {"id": 1210, "synset": "food.n.01", "name": "food"}, + {"id": 1211, "synset": "artifact.n.01", "name": "artifact"}, + {"id": 1212, "synset": "hop.n.01", "name": "hop"}, + {"id": 1213, "synset": "check-in.n.01", "name": "check-in"}, + {"id": 1214, "synset": "dressage.n.01", "name": "dressage"}, + {"id": 1215, "synset": "curvet.n.01", "name": "curvet"}, + {"id": 1216, "synset": "piaffe.n.01", "name": "piaffe"}, + {"id": 1217, "synset": "funambulism.n.01", "name": "funambulism"}, + {"id": 1218, "synset": "rock_climbing.n.01", "name": "rock_climbing"}, + {"id": 1219, "synset": "contact_sport.n.01", "name": "contact_sport"}, + {"id": 1220, "synset": "outdoor_sport.n.01", "name": "outdoor_sport"}, + {"id": 1221, "synset": "gymnastics.n.01", "name": "gymnastics"}, + {"id": 1222, "synset": "acrobatics.n.01", "name": "acrobatics"}, + {"id": 1223, "synset": "track_and_field.n.01", "name": "track_and_field"}, + {"id": 1224, "synset": "track.n.11", "name": "track"}, + {"id": 1225, "synset": "jumping.n.01", "name": "jumping"}, + {"id": 1226, "synset": "broad_jump.n.02", "name": "broad_jump"}, + {"id": 1227, "synset": "high_jump.n.02", "name": "high_jump"}, + {"id": 1228, "synset": "fosbury_flop.n.01", "name": "Fosbury_flop"}, + {"id": 1229, "synset": "skiing.n.01", "name": "skiing"}, + {"id": 1230, "synset": "cross-country_skiing.n.01", "name": "cross-country_skiing"}, + {"id": 1231, "synset": "ski_jumping.n.01", "name": "ski_jumping"}, + {"id": 1232, "synset": "water_sport.n.01", "name": "water_sport"}, + {"id": 1233, "synset": "swimming.n.01", "name": "swimming"}, + {"id": 1234, "synset": "bathe.n.01", "name": "bathe"}, + {"id": 1235, "synset": "dip.n.08", "name": "dip"}, + {"id": 1236, "synset": "dive.n.02", "name": "dive"}, + {"id": 1237, "synset": "floating.n.01", "name": "floating"}, + {"id": 1238, "synset": "dead-man's_float.n.01", "name": "dead-man's_float"}, + {"id": 1239, "synset": "belly_flop.n.01", "name": "belly_flop"}, + {"id": 1240, "synset": "cliff_diving.n.01", "name": "cliff_diving"}, + {"id": 1241, "synset": "flip.n.05", "name": "flip"}, + {"id": 1242, "synset": "gainer.n.03", "name": "gainer"}, + {"id": 1243, "synset": "half_gainer.n.01", "name": "half_gainer"}, + {"id": 1244, "synset": "jackknife.n.02", "name": "jackknife"}, + {"id": 1245, "synset": "swan_dive.n.01", "name": "swan_dive"}, + {"id": 1246, "synset": "skin_diving.n.01", "name": "skin_diving"}, + {"id": 1247, "synset": "scuba_diving.n.01", "name": "scuba_diving"}, + {"id": 1248, "synset": "snorkeling.n.01", "name": "snorkeling"}, + {"id": 1249, "synset": "surfing.n.01", "name": "surfing"}, + {"id": 1250, "synset": "water-skiing.n.01", "name": "water-skiing"}, + {"id": 1251, "synset": "rowing.n.01", "name": "rowing"}, + {"id": 1252, "synset": "sculling.n.01", "name": "sculling"}, + {"id": 1253, "synset": "boxing.n.01", "name": "boxing"}, + {"id": 1254, "synset": "professional_boxing.n.01", "name": "professional_boxing"}, + {"id": 1255, "synset": "in-fighting.n.02", "name": "in-fighting"}, + {"id": 1256, "synset": "fight.n.05", "name": "fight"}, + {"id": 1257, "synset": "rope-a-dope.n.01", "name": "rope-a-dope"}, + {"id": 1258, "synset": "spar.n.03", "name": "spar"}, + {"id": 1259, "synset": "archery.n.01", "name": "archery"}, + {"id": 1260, "synset": "sledding.n.01", "name": "sledding"}, + {"id": 1261, "synset": "tobogganing.n.01", "name": "tobogganing"}, + {"id": 1262, "synset": "luging.n.01", "name": "luging"}, + {"id": 1263, "synset": "bobsledding.n.01", "name": "bobsledding"}, + {"id": 1264, "synset": "wrestling.n.02", "name": "wrestling"}, + {"id": 1265, "synset": "greco-roman_wrestling.n.01", "name": "Greco-Roman_wrestling"}, + {"id": 1266, "synset": "professional_wrestling.n.01", "name": "professional_wrestling"}, + {"id": 1267, "synset": "sumo.n.01", "name": "sumo"}, + {"id": 1268, "synset": "skating.n.01", "name": "skating"}, + {"id": 1269, "synset": "ice_skating.n.01", "name": "ice_skating"}, + {"id": 1270, "synset": "figure_skating.n.01", "name": "figure_skating"}, + {"id": 1271, "synset": "rollerblading.n.01", "name": "rollerblading"}, + {"id": 1272, "synset": "roller_skating.n.01", "name": "roller_skating"}, + {"id": 1273, "synset": "skateboarding.n.01", "name": "skateboarding"}, + {"id": 1274, "synset": "speed_skating.n.01", "name": "speed_skating"}, + {"id": 1275, "synset": "racing.n.01", "name": "racing"}, + {"id": 1276, "synset": "auto_racing.n.01", "name": "auto_racing"}, + {"id": 1277, "synset": "boat_racing.n.01", "name": "boat_racing"}, + {"id": 1278, "synset": "hydroplane_racing.n.01", "name": "hydroplane_racing"}, + {"id": 1279, "synset": "camel_racing.n.01", "name": "camel_racing"}, + {"id": 1280, "synset": "greyhound_racing.n.01", "name": "greyhound_racing"}, + {"id": 1281, "synset": "horse_racing.n.01", "name": "horse_racing"}, + {"id": 1282, "synset": "riding.n.01", "name": "riding"}, + {"id": 1283, "synset": "equestrian_sport.n.01", "name": "equestrian_sport"}, + {"id": 1284, "synset": "pony-trekking.n.01", "name": "pony-trekking"}, + {"id": 1285, "synset": "showjumping.n.01", "name": "showjumping"}, + {"id": 1286, "synset": "cross-country_riding.n.01", "name": "cross-country_riding"}, + {"id": 1287, "synset": "cycling.n.01", "name": "cycling"}, + {"id": 1288, "synset": "bicycling.n.01", "name": "bicycling"}, + {"id": 1289, "synset": "motorcycling.n.01", "name": "motorcycling"}, + {"id": 1290, "synset": "dune_cycling.n.01", "name": "dune_cycling"}, + {"id": 1291, "synset": "blood_sport.n.01", "name": "blood_sport"}, + {"id": 1292, "synset": "bullfighting.n.01", "name": "bullfighting"}, + {"id": 1293, "synset": "cockfighting.n.01", "name": "cockfighting"}, + {"id": 1294, "synset": "hunt.n.08", "name": "hunt"}, + {"id": 1295, "synset": "battue.n.01", "name": "battue"}, + {"id": 1296, "synset": "beagling.n.01", "name": "beagling"}, + {"id": 1297, "synset": "coursing.n.01", "name": "coursing"}, + {"id": 1298, "synset": "deer_hunting.n.01", "name": "deer_hunting"}, + {"id": 1299, "synset": "ducking.n.01", "name": "ducking"}, + {"id": 1300, "synset": "fox_hunting.n.01", "name": "fox_hunting"}, + {"id": 1301, "synset": "pigsticking.n.01", "name": "pigsticking"}, + {"id": 1302, "synset": "fishing.n.01", "name": "fishing"}, + {"id": 1303, "synset": "angling.n.01", "name": "angling"}, + {"id": 1304, "synset": "fly-fishing.n.01", "name": "fly-fishing"}, + {"id": 1305, "synset": "troll.n.04", "name": "troll"}, + {"id": 1306, "synset": "casting.n.03", "name": "casting"}, + {"id": 1307, "synset": "bait_casting.n.01", "name": "bait_casting"}, + {"id": 1308, "synset": "fly_casting.n.01", "name": "fly_casting"}, + {"id": 1309, "synset": "overcast.n.04", "name": "overcast"}, + {"id": 1310, "synset": "surf_casting.n.01", "name": "surf_casting"}, + {"id": 1311, "synset": "day_game.n.01", "name": "day_game"}, + {"id": 1312, "synset": "athletic_game.n.01", "name": "athletic_game"}, + {"id": 1313, "synset": "ice_hockey.n.01", "name": "ice_hockey"}, + {"id": 1314, "synset": "tetherball.n.01", "name": "tetherball"}, + {"id": 1315, "synset": "water_polo.n.01", "name": "water_polo"}, + {"id": 1316, "synset": "outdoor_game.n.01", "name": "outdoor_game"}, + {"id": 1317, "synset": "golf.n.01", "name": "golf"}, + {"id": 1318, "synset": "professional_golf.n.01", "name": "professional_golf"}, + {"id": 1319, "synset": "round_of_golf.n.01", "name": "round_of_golf"}, + {"id": 1320, "synset": "medal_play.n.01", "name": "medal_play"}, + {"id": 1321, "synset": "match_play.n.01", "name": "match_play"}, + {"id": 1322, "synset": "miniature_golf.n.01", "name": "miniature_golf"}, + {"id": 1323, "synset": "croquet.n.01", "name": "croquet"}, + {"id": 1324, "synset": "quoits.n.01", "name": "quoits"}, + {"id": 1325, "synset": "shuffleboard.n.01", "name": "shuffleboard"}, + {"id": 1326, "synset": "field_game.n.01", "name": "field_game"}, + {"id": 1327, "synset": "field_hockey.n.01", "name": "field_hockey"}, + {"id": 1328, "synset": "shinny.n.01", "name": "shinny"}, + {"id": 1329, "synset": "football.n.01", "name": "football"}, + {"id": 1330, "synset": "american_football.n.01", "name": "American_football"}, + {"id": 1331, "synset": "professional_football.n.01", "name": "professional_football"}, + {"id": 1332, "synset": "touch_football.n.01", "name": "touch_football"}, + {"id": 1333, "synset": "hurling.n.01", "name": "hurling"}, + {"id": 1334, "synset": "rugby.n.01", "name": "rugby"}, + {"id": 1335, "synset": "ball_game.n.01", "name": "ball_game"}, + {"id": 1336, "synset": "baseball.n.01", "name": "baseball"}, + {"id": 1337, "synset": "ball.n.11", "name": "ball"}, + {"id": 1338, "synset": "professional_baseball.n.01", "name": "professional_baseball"}, + {"id": 1339, "synset": "hardball.n.02", "name": "hardball"}, + {"id": 1340, "synset": "perfect_game.n.01", "name": "perfect_game"}, + {"id": 1341, "synset": "no-hit_game.n.01", "name": "no-hit_game"}, + {"id": 1342, "synset": "one-hitter.n.01", "name": "one-hitter"}, + {"id": 1343, "synset": "two-hitter.n.01", "name": "two-hitter"}, + {"id": 1344, "synset": "three-hitter.n.01", "name": "three-hitter"}, + {"id": 1345, "synset": "four-hitter.n.01", "name": "four-hitter"}, + {"id": 1346, "synset": "five-hitter.n.01", "name": "five-hitter"}, + {"id": 1347, "synset": "softball.n.02", "name": "softball"}, + {"id": 1348, "synset": "rounders.n.01", "name": "rounders"}, + {"id": 1349, "synset": "stickball.n.01", "name": "stickball"}, + {"id": 1350, "synset": "cricket.n.02", "name": "cricket"}, + {"id": 1351, "synset": "lacrosse.n.01", "name": "lacrosse"}, + {"id": 1352, "synset": "polo.n.02", "name": "polo"}, + {"id": 1353, "synset": "pushball.n.01", "name": "pushball"}, + {"id": 1354, "synset": "soccer.n.01", "name": "soccer"}, + {"id": 1355, "synset": "court_game.n.01", "name": "court_game"}, + {"id": 1356, "synset": "handball.n.02", "name": "handball"}, + {"id": 1357, "synset": "racquetball.n.02", "name": "racquetball"}, + {"id": 1358, "synset": "fives.n.01", "name": "fives"}, + {"id": 1359, "synset": "squash.n.03", "name": "squash"}, + {"id": 1360, "synset": "volleyball.n.01", "name": "volleyball"}, + {"id": 1361, "synset": "jai_alai.n.01", "name": "jai_alai"}, + {"id": 1362, "synset": "badminton.n.01", "name": "badminton"}, + {"id": 1363, "synset": "battledore.n.02", "name": "battledore"}, + {"id": 1364, "synset": "basketball.n.01", "name": "basketball"}, + {"id": 1365, "synset": "professional_basketball.n.01", "name": "professional_basketball"}, + {"id": 1366, "synset": "deck_tennis.n.01", "name": "deck_tennis"}, + {"id": 1367, "synset": "netball.n.01", "name": "netball"}, + {"id": 1368, "synset": "tennis.n.01", "name": "tennis"}, + {"id": 1369, "synset": "professional_tennis.n.01", "name": "professional_tennis"}, + {"id": 1370, "synset": "singles.n.02", "name": "singles"}, + {"id": 1371, "synset": "singles.n.01", "name": "singles"}, + {"id": 1372, "synset": "doubles.n.02", "name": "doubles"}, + {"id": 1373, "synset": "doubles.n.01", "name": "doubles"}, + {"id": 1374, "synset": "royal_tennis.n.01", "name": "royal_tennis"}, + {"id": 1375, "synset": "pallone.n.01", "name": "pallone"}, + {"id": 1376, "synset": "sport.n.01", "name": "sport"}, + {"id": 1377, "synset": "clasp.n.02", "name": "clasp"}, + {"id": 1378, "synset": "judo.n.01", "name": "judo"}, + {"id": 1379, "synset": "team_sport.n.01", "name": "team_sport"}, + {"id": 1380, "synset": "last_supper.n.01", "name": "Last_Supper"}, + {"id": 1381, "synset": "seder.n.01", "name": "Seder"}, + {"id": 1382, "synset": "camping.n.01", "name": "camping"}, + {"id": 1383, "synset": "pest.n.04", "name": "pest"}, + {"id": 1384, "synset": "critter.n.01", "name": "critter"}, + {"id": 1385, "synset": "creepy-crawly.n.01", "name": "creepy-crawly"}, + {"id": 1386, "synset": "darter.n.02", "name": "darter"}, + {"id": 1387, "synset": "peeper.n.03", "name": "peeper"}, + {"id": 1388, "synset": "homeotherm.n.01", "name": "homeotherm"}, + {"id": 1389, "synset": "poikilotherm.n.01", "name": "poikilotherm"}, + {"id": 1390, "synset": "range_animal.n.01", "name": "range_animal"}, + {"id": 1391, "synset": "scavenger.n.03", "name": "scavenger"}, + {"id": 1392, "synset": "bottom-feeder.n.02", "name": "bottom-feeder"}, + {"id": 1393, "synset": "bottom-feeder.n.01", "name": "bottom-feeder"}, + {"id": 1394, "synset": "work_animal.n.01", "name": "work_animal"}, + {"id": 1395, "synset": "beast_of_burden.n.01", "name": "beast_of_burden"}, + {"id": 1396, "synset": "draft_animal.n.01", "name": "draft_animal"}, + {"id": 1397, "synset": "pack_animal.n.01", "name": "pack_animal"}, + {"id": 1398, "synset": "domestic_animal.n.01", "name": "domestic_animal"}, + {"id": 1399, "synset": "feeder.n.01", "name": "feeder"}, + {"id": 1400, "synset": "feeder.n.06", "name": "feeder"}, + {"id": 1401, "synset": "stocker.n.01", "name": "stocker"}, + {"id": 1402, "synset": "hatchling.n.01", "name": "hatchling"}, + {"id": 1403, "synset": "head.n.02", "name": "head"}, + {"id": 1404, "synset": "migrator.n.02", "name": "migrator"}, + {"id": 1405, "synset": "molter.n.01", "name": "molter"}, + {"id": 1406, "synset": "stayer.n.01", "name": "stayer"}, + {"id": 1407, "synset": "stunt.n.02", "name": "stunt"}, + {"id": 1408, "synset": "marine_animal.n.01", "name": "marine_animal"}, + {"id": 1409, "synset": "by-catch.n.01", "name": "by-catch"}, + {"id": 1410, "synset": "female.n.01", "name": "female"}, + {"id": 1411, "synset": "hen.n.04", "name": "hen"}, + {"id": 1412, "synset": "male.n.01", "name": "male"}, + {"id": 1413, "synset": "adult.n.02", "name": "adult"}, + {"id": 1414, "synset": "young.n.01", "name": "young"}, + {"id": 1415, "synset": "orphan.n.04", "name": "orphan"}, + {"id": 1416, "synset": "young_mammal.n.01", "name": "young_mammal"}, + {"id": 1417, "synset": "baby.n.06", "name": "baby"}, + {"id": 1418, "synset": "pup.n.01", "name": "pup"}, + {"id": 1419, "synset": "wolf_pup.n.01", "name": "wolf_pup"}, + {"id": 1420, "synset": "lion_cub.n.01", "name": "lion_cub"}, + {"id": 1421, "synset": "bear_cub.n.01", "name": "bear_cub"}, + {"id": 1422, "synset": "tiger_cub.n.01", "name": "tiger_cub"}, + {"id": 1423, "synset": "kit.n.03", "name": "kit"}, + {"id": 1424, "synset": "suckling.n.03", "name": "suckling"}, + {"id": 1425, "synset": "sire.n.03", "name": "sire"}, + {"id": 1426, "synset": "dam.n.03", "name": "dam"}, + {"id": 1427, "synset": "thoroughbred.n.03", "name": "thoroughbred"}, + {"id": 1428, "synset": "giant.n.01", "name": "giant"}, + {"id": 1429, "synset": "mutant.n.02", "name": "mutant"}, + {"id": 1430, "synset": "carnivore.n.02", "name": "carnivore"}, + {"id": 1431, "synset": "herbivore.n.01", "name": "herbivore"}, + {"id": 1432, "synset": "insectivore.n.02", "name": "insectivore"}, + {"id": 1433, "synset": "acrodont.n.01", "name": "acrodont"}, + {"id": 1434, "synset": "pleurodont.n.01", "name": "pleurodont"}, + {"id": 1435, "synset": "microorganism.n.01", "name": "microorganism"}, + {"id": 1436, "synset": "monohybrid.n.01", "name": "monohybrid"}, + {"id": 1437, "synset": "arbovirus.n.01", "name": "arbovirus"}, + {"id": 1438, "synset": "adenovirus.n.01", "name": "adenovirus"}, + {"id": 1439, "synset": "arenavirus.n.01", "name": "arenavirus"}, + {"id": 1440, "synset": "marburg_virus.n.01", "name": "Marburg_virus"}, + {"id": 1441, "synset": "arenaviridae.n.01", "name": "Arenaviridae"}, + {"id": 1442, "synset": "vesiculovirus.n.01", "name": "vesiculovirus"}, + {"id": 1443, "synset": "reoviridae.n.01", "name": "Reoviridae"}, + {"id": 1444, "synset": "variola_major.n.02", "name": "variola_major"}, + {"id": 1445, "synset": "viroid.n.01", "name": "viroid"}, + {"id": 1446, "synset": "coliphage.n.01", "name": "coliphage"}, + {"id": 1447, "synset": "paramyxovirus.n.01", "name": "paramyxovirus"}, + {"id": 1448, "synset": "poliovirus.n.01", "name": "poliovirus"}, + {"id": 1449, "synset": "herpes.n.02", "name": "herpes"}, + {"id": 1450, "synset": "herpes_simplex_1.n.01", "name": "herpes_simplex_1"}, + {"id": 1451, "synset": "herpes_zoster.n.02", "name": "herpes_zoster"}, + {"id": 1452, "synset": "herpes_varicella_zoster.n.01", "name": "herpes_varicella_zoster"}, + {"id": 1453, "synset": "cytomegalovirus.n.01", "name": "cytomegalovirus"}, + {"id": 1454, "synset": "varicella_zoster_virus.n.01", "name": "varicella_zoster_virus"}, + {"id": 1455, "synset": "polyoma.n.01", "name": "polyoma"}, + {"id": 1456, "synset": "lyssavirus.n.01", "name": "lyssavirus"}, + {"id": 1457, "synset": "reovirus.n.01", "name": "reovirus"}, + {"id": 1458, "synset": "rotavirus.n.01", "name": "rotavirus"}, + {"id": 1459, "synset": "moneran.n.01", "name": "moneran"}, + {"id": 1460, "synset": "archaebacteria.n.01", "name": "archaebacteria"}, + {"id": 1461, "synset": "bacteroid.n.01", "name": "bacteroid"}, + {"id": 1462, "synset": "bacillus_anthracis.n.01", "name": "Bacillus_anthracis"}, + {"id": 1463, "synset": "yersinia_pestis.n.01", "name": "Yersinia_pestis"}, + {"id": 1464, "synset": "brucella.n.01", "name": "Brucella"}, + {"id": 1465, "synset": "spirillum.n.02", "name": "spirillum"}, + {"id": 1466, "synset": "botulinus.n.01", "name": "botulinus"}, + {"id": 1467, "synset": "clostridium_perfringens.n.01", "name": "clostridium_perfringens"}, + {"id": 1468, "synset": "cyanobacteria.n.01", "name": "cyanobacteria"}, + {"id": 1469, "synset": "trichodesmium.n.01", "name": "trichodesmium"}, + {"id": 1470, "synset": "nitric_bacteria.n.01", "name": "nitric_bacteria"}, + {"id": 1471, "synset": "spirillum.n.01", "name": "spirillum"}, + {"id": 1472, "synset": "francisella.n.01", "name": "Francisella"}, + {"id": 1473, "synset": "gonococcus.n.01", "name": "gonococcus"}, + { + "id": 1474, + "synset": "corynebacterium_diphtheriae.n.01", + "name": "Corynebacterium_diphtheriae", + }, + {"id": 1475, "synset": "enteric_bacteria.n.01", "name": "enteric_bacteria"}, + {"id": 1476, "synset": "klebsiella.n.01", "name": "klebsiella"}, + {"id": 1477, "synset": "salmonella_typhimurium.n.01", "name": "Salmonella_typhimurium"}, + {"id": 1478, "synset": "typhoid_bacillus.n.01", "name": "typhoid_bacillus"}, + {"id": 1479, "synset": "nitrate_bacterium.n.01", "name": "nitrate_bacterium"}, + {"id": 1480, "synset": "nitrite_bacterium.n.01", "name": "nitrite_bacterium"}, + {"id": 1481, "synset": "actinomycete.n.01", "name": "actinomycete"}, + {"id": 1482, "synset": "streptomyces.n.01", "name": "streptomyces"}, + {"id": 1483, "synset": "streptomyces_erythreus.n.01", "name": "Streptomyces_erythreus"}, + {"id": 1484, "synset": "streptomyces_griseus.n.01", "name": "Streptomyces_griseus"}, + {"id": 1485, "synset": "tubercle_bacillus.n.01", "name": "tubercle_bacillus"}, + {"id": 1486, "synset": "pus-forming_bacteria.n.01", "name": "pus-forming_bacteria"}, + {"id": 1487, "synset": "streptobacillus.n.01", "name": "streptobacillus"}, + {"id": 1488, "synset": "myxobacteria.n.01", "name": "myxobacteria"}, + {"id": 1489, "synset": "staphylococcus.n.01", "name": "staphylococcus"}, + {"id": 1490, "synset": "diplococcus.n.01", "name": "diplococcus"}, + {"id": 1491, "synset": "pneumococcus.n.01", "name": "pneumococcus"}, + {"id": 1492, "synset": "streptococcus.n.01", "name": "streptococcus"}, + {"id": 1493, "synset": "spirochete.n.01", "name": "spirochete"}, + {"id": 1494, "synset": "planktonic_algae.n.01", "name": "planktonic_algae"}, + {"id": 1495, "synset": "zooplankton.n.01", "name": "zooplankton"}, + {"id": 1496, "synset": "parasite.n.01", "name": "parasite"}, + {"id": 1497, "synset": "endoparasite.n.01", "name": "endoparasite"}, + {"id": 1498, "synset": "ectoparasite.n.01", "name": "ectoparasite"}, + {"id": 1499, "synset": "pathogen.n.01", "name": "pathogen"}, + {"id": 1500, "synset": "commensal.n.01", "name": "commensal"}, + {"id": 1501, "synset": "myrmecophile.n.01", "name": "myrmecophile"}, + {"id": 1502, "synset": "protoctist.n.01", "name": "protoctist"}, + {"id": 1503, "synset": "protozoan.n.01", "name": "protozoan"}, + {"id": 1504, "synset": "sarcodinian.n.01", "name": "sarcodinian"}, + {"id": 1505, "synset": "heliozoan.n.01", "name": "heliozoan"}, + {"id": 1506, "synset": "endameba.n.01", "name": "endameba"}, + {"id": 1507, "synset": "ameba.n.01", "name": "ameba"}, + {"id": 1508, "synset": "globigerina.n.01", "name": "globigerina"}, + {"id": 1509, "synset": "testacean.n.01", "name": "testacean"}, + {"id": 1510, "synset": "arcella.n.01", "name": "arcella"}, + {"id": 1511, "synset": "difflugia.n.01", "name": "difflugia"}, + {"id": 1512, "synset": "ciliate.n.01", "name": "ciliate"}, + {"id": 1513, "synset": "paramecium.n.01", "name": "paramecium"}, + {"id": 1514, "synset": "stentor.n.03", "name": "stentor"}, + {"id": 1515, "synset": "alga.n.01", "name": "alga"}, + {"id": 1516, "synset": "arame.n.01", "name": "arame"}, + {"id": 1517, "synset": "seagrass.n.01", "name": "seagrass"}, + {"id": 1518, "synset": "golden_algae.n.01", "name": "golden_algae"}, + {"id": 1519, "synset": "yellow-green_algae.n.01", "name": "yellow-green_algae"}, + {"id": 1520, "synset": "brown_algae.n.01", "name": "brown_algae"}, + {"id": 1521, "synset": "kelp.n.01", "name": "kelp"}, + {"id": 1522, "synset": "fucoid.n.02", "name": "fucoid"}, + {"id": 1523, "synset": "fucoid.n.01", "name": "fucoid"}, + {"id": 1524, "synset": "fucus.n.01", "name": "fucus"}, + {"id": 1525, "synset": "bladderwrack.n.01", "name": "bladderwrack"}, + {"id": 1526, "synset": "green_algae.n.01", "name": "green_algae"}, + {"id": 1527, "synset": "pond_scum.n.01", "name": "pond_scum"}, + {"id": 1528, "synset": "chlorella.n.01", "name": "chlorella"}, + {"id": 1529, "synset": "stonewort.n.01", "name": "stonewort"}, + {"id": 1530, "synset": "desmid.n.01", "name": "desmid"}, + {"id": 1531, "synset": "sea_moss.n.02", "name": "sea_moss"}, + {"id": 1532, "synset": "eukaryote.n.01", "name": "eukaryote"}, + {"id": 1533, "synset": "prokaryote.n.01", "name": "prokaryote"}, + {"id": 1534, "synset": "zooid.n.01", "name": "zooid"}, + {"id": 1535, "synset": "leishmania.n.01", "name": "Leishmania"}, + {"id": 1536, "synset": "zoomastigote.n.01", "name": "zoomastigote"}, + {"id": 1537, "synset": "polymastigote.n.01", "name": "polymastigote"}, + {"id": 1538, "synset": "costia.n.01", "name": "costia"}, + {"id": 1539, "synset": "giardia.n.01", "name": "giardia"}, + {"id": 1540, "synset": "cryptomonad.n.01", "name": "cryptomonad"}, + {"id": 1541, "synset": "sporozoan.n.01", "name": "sporozoan"}, + {"id": 1542, "synset": "sporozoite.n.01", "name": "sporozoite"}, + {"id": 1543, "synset": "trophozoite.n.01", "name": "trophozoite"}, + {"id": 1544, "synset": "merozoite.n.01", "name": "merozoite"}, + {"id": 1545, "synset": "coccidium.n.01", "name": "coccidium"}, + {"id": 1546, "synset": "gregarine.n.01", "name": "gregarine"}, + {"id": 1547, "synset": "plasmodium.n.02", "name": "plasmodium"}, + {"id": 1548, "synset": "leucocytozoan.n.01", "name": "leucocytozoan"}, + {"id": 1549, "synset": "microsporidian.n.01", "name": "microsporidian"}, + {"id": 1550, "synset": "ostariophysi.n.01", "name": "Ostariophysi"}, + {"id": 1551, "synset": "cypriniform_fish.n.01", "name": "cypriniform_fish"}, + {"id": 1552, "synset": "loach.n.01", "name": "loach"}, + {"id": 1553, "synset": "cyprinid.n.01", "name": "cyprinid"}, + {"id": 1554, "synset": "carp.n.02", "name": "carp"}, + {"id": 1555, "synset": "domestic_carp.n.01", "name": "domestic_carp"}, + {"id": 1556, "synset": "leather_carp.n.01", "name": "leather_carp"}, + {"id": 1557, "synset": "mirror_carp.n.01", "name": "mirror_carp"}, + {"id": 1558, "synset": "european_bream.n.01", "name": "European_bream"}, + {"id": 1559, "synset": "tench.n.01", "name": "tench"}, + {"id": 1560, "synset": "dace.n.01", "name": "dace"}, + {"id": 1561, "synset": "chub.n.01", "name": "chub"}, + {"id": 1562, "synset": "shiner.n.04", "name": "shiner"}, + {"id": 1563, "synset": "common_shiner.n.01", "name": "common_shiner"}, + {"id": 1564, "synset": "roach.n.05", "name": "roach"}, + {"id": 1565, "synset": "rudd.n.01", "name": "rudd"}, + {"id": 1566, "synset": "minnow.n.01", "name": "minnow"}, + {"id": 1567, "synset": "gudgeon.n.02", "name": "gudgeon"}, + {"id": 1568, "synset": "crucian_carp.n.01", "name": "crucian_carp"}, + {"id": 1569, "synset": "electric_eel.n.01", "name": "electric_eel"}, + {"id": 1570, "synset": "catostomid.n.01", "name": "catostomid"}, + {"id": 1571, "synset": "buffalo_fish.n.01", "name": "buffalo_fish"}, + {"id": 1572, "synset": "black_buffalo.n.01", "name": "black_buffalo"}, + {"id": 1573, "synset": "hog_sucker.n.01", "name": "hog_sucker"}, + {"id": 1574, "synset": "redhorse.n.01", "name": "redhorse"}, + {"id": 1575, "synset": "cyprinodont.n.01", "name": "cyprinodont"}, + {"id": 1576, "synset": "killifish.n.01", "name": "killifish"}, + {"id": 1577, "synset": "mummichog.n.01", "name": "mummichog"}, + {"id": 1578, "synset": "striped_killifish.n.01", "name": "striped_killifish"}, + {"id": 1579, "synset": "rivulus.n.01", "name": "rivulus"}, + {"id": 1580, "synset": "flagfish.n.01", "name": "flagfish"}, + {"id": 1581, "synset": "swordtail.n.01", "name": "swordtail"}, + {"id": 1582, "synset": "guppy.n.01", "name": "guppy"}, + {"id": 1583, "synset": "topminnow.n.01", "name": "topminnow"}, + {"id": 1584, "synset": "mosquitofish.n.01", "name": "mosquitofish"}, + {"id": 1585, "synset": "platy.n.01", "name": "platy"}, + {"id": 1586, "synset": "mollie.n.01", "name": "mollie"}, + {"id": 1587, "synset": "squirrelfish.n.02", "name": "squirrelfish"}, + {"id": 1588, "synset": "reef_squirrelfish.n.01", "name": "reef_squirrelfish"}, + {"id": 1589, "synset": "deepwater_squirrelfish.n.01", "name": "deepwater_squirrelfish"}, + {"id": 1590, "synset": "holocentrus_ascensionis.n.01", "name": "Holocentrus_ascensionis"}, + {"id": 1591, "synset": "soldierfish.n.01", "name": "soldierfish"}, + {"id": 1592, "synset": "anomalops.n.01", "name": "anomalops"}, + {"id": 1593, "synset": "flashlight_fish.n.01", "name": "flashlight_fish"}, + {"id": 1594, "synset": "john_dory.n.01", "name": "John_Dory"}, + {"id": 1595, "synset": "boarfish.n.02", "name": "boarfish"}, + {"id": 1596, "synset": "boarfish.n.01", "name": "boarfish"}, + {"id": 1597, "synset": "cornetfish.n.01", "name": "cornetfish"}, + {"id": 1598, "synset": "stickleback.n.01", "name": "stickleback"}, + {"id": 1599, "synset": "three-spined_stickleback.n.01", "name": "three-spined_stickleback"}, + {"id": 1600, "synset": "ten-spined_stickleback.n.01", "name": "ten-spined_stickleback"}, + {"id": 1601, "synset": "pipefish.n.01", "name": "pipefish"}, + {"id": 1602, "synset": "dwarf_pipefish.n.01", "name": "dwarf_pipefish"}, + {"id": 1603, "synset": "deepwater_pipefish.n.01", "name": "deepwater_pipefish"}, + {"id": 1604, "synset": "snipefish.n.01", "name": "snipefish"}, + {"id": 1605, "synset": "shrimpfish.n.01", "name": "shrimpfish"}, + {"id": 1606, "synset": "trumpetfish.n.01", "name": "trumpetfish"}, + {"id": 1607, "synset": "pellicle.n.01", "name": "pellicle"}, + {"id": 1608, "synset": "embryo.n.02", "name": "embryo"}, + {"id": 1609, "synset": "fetus.n.01", "name": "fetus"}, + {"id": 1610, "synset": "abortus.n.01", "name": "abortus"}, + {"id": 1611, "synset": "spawn.n.01", "name": "spawn"}, + {"id": 1612, "synset": "blastula.n.01", "name": "blastula"}, + {"id": 1613, "synset": "blastocyst.n.01", "name": "blastocyst"}, + {"id": 1614, "synset": "gastrula.n.01", "name": "gastrula"}, + {"id": 1615, "synset": "morula.n.01", "name": "morula"}, + {"id": 1616, "synset": "yolk.n.02", "name": "yolk"}, + {"id": 1617, "synset": "chordate.n.01", "name": "chordate"}, + {"id": 1618, "synset": "cephalochordate.n.01", "name": "cephalochordate"}, + {"id": 1619, "synset": "lancelet.n.01", "name": "lancelet"}, + {"id": 1620, "synset": "tunicate.n.01", "name": "tunicate"}, + {"id": 1621, "synset": "ascidian.n.01", "name": "ascidian"}, + {"id": 1622, "synset": "sea_squirt.n.01", "name": "sea_squirt"}, + {"id": 1623, "synset": "salp.n.01", "name": "salp"}, + {"id": 1624, "synset": "doliolum.n.01", "name": "doliolum"}, + {"id": 1625, "synset": "larvacean.n.01", "name": "larvacean"}, + {"id": 1626, "synset": "appendicularia.n.01", "name": "appendicularia"}, + {"id": 1627, "synset": "ascidian_tadpole.n.01", "name": "ascidian_tadpole"}, + {"id": 1628, "synset": "vertebrate.n.01", "name": "vertebrate"}, + {"id": 1629, "synset": "amniota.n.01", "name": "Amniota"}, + {"id": 1630, "synset": "amniote.n.01", "name": "amniote"}, + {"id": 1631, "synset": "aquatic_vertebrate.n.01", "name": "aquatic_vertebrate"}, + {"id": 1632, "synset": "jawless_vertebrate.n.01", "name": "jawless_vertebrate"}, + {"id": 1633, "synset": "ostracoderm.n.01", "name": "ostracoderm"}, + {"id": 1634, "synset": "heterostracan.n.01", "name": "heterostracan"}, + {"id": 1635, "synset": "anaspid.n.01", "name": "anaspid"}, + {"id": 1636, "synset": "conodont.n.02", "name": "conodont"}, + {"id": 1637, "synset": "cyclostome.n.01", "name": "cyclostome"}, + {"id": 1638, "synset": "lamprey.n.01", "name": "lamprey"}, + {"id": 1639, "synset": "sea_lamprey.n.01", "name": "sea_lamprey"}, + {"id": 1640, "synset": "hagfish.n.01", "name": "hagfish"}, + {"id": 1641, "synset": "myxine_glutinosa.n.01", "name": "Myxine_glutinosa"}, + {"id": 1642, "synset": "eptatretus.n.01", "name": "eptatretus"}, + {"id": 1643, "synset": "gnathostome.n.01", "name": "gnathostome"}, + {"id": 1644, "synset": "placoderm.n.01", "name": "placoderm"}, + {"id": 1645, "synset": "cartilaginous_fish.n.01", "name": "cartilaginous_fish"}, + {"id": 1646, "synset": "holocephalan.n.01", "name": "holocephalan"}, + {"id": 1647, "synset": "chimaera.n.03", "name": "chimaera"}, + {"id": 1648, "synset": "rabbitfish.n.01", "name": "rabbitfish"}, + {"id": 1649, "synset": "elasmobranch.n.01", "name": "elasmobranch"}, + {"id": 1650, "synset": "cow_shark.n.01", "name": "cow_shark"}, + {"id": 1651, "synset": "mackerel_shark.n.01", "name": "mackerel_shark"}, + {"id": 1652, "synset": "porbeagle.n.01", "name": "porbeagle"}, + {"id": 1653, "synset": "mako.n.01", "name": "mako"}, + {"id": 1654, "synset": "shortfin_mako.n.01", "name": "shortfin_mako"}, + {"id": 1655, "synset": "longfin_mako.n.01", "name": "longfin_mako"}, + {"id": 1656, "synset": "bonito_shark.n.01", "name": "bonito_shark"}, + {"id": 1657, "synset": "great_white_shark.n.01", "name": "great_white_shark"}, + {"id": 1658, "synset": "basking_shark.n.01", "name": "basking_shark"}, + {"id": 1659, "synset": "thresher.n.02", "name": "thresher"}, + {"id": 1660, "synset": "carpet_shark.n.01", "name": "carpet_shark"}, + {"id": 1661, "synset": "nurse_shark.n.01", "name": "nurse_shark"}, + {"id": 1662, "synset": "sand_tiger.n.01", "name": "sand_tiger"}, + {"id": 1663, "synset": "whale_shark.n.01", "name": "whale_shark"}, + {"id": 1664, "synset": "requiem_shark.n.01", "name": "requiem_shark"}, + {"id": 1665, "synset": "bull_shark.n.01", "name": "bull_shark"}, + {"id": 1666, "synset": "sandbar_shark.n.02", "name": "sandbar_shark"}, + {"id": 1667, "synset": "blacktip_shark.n.01", "name": "blacktip_shark"}, + {"id": 1668, "synset": "whitetip_shark.n.02", "name": "whitetip_shark"}, + {"id": 1669, "synset": "dusky_shark.n.01", "name": "dusky_shark"}, + {"id": 1670, "synset": "lemon_shark.n.01", "name": "lemon_shark"}, + {"id": 1671, "synset": "blue_shark.n.01", "name": "blue_shark"}, + {"id": 1672, "synset": "tiger_shark.n.01", "name": "tiger_shark"}, + {"id": 1673, "synset": "soupfin_shark.n.01", "name": "soupfin_shark"}, + {"id": 1674, "synset": "dogfish.n.02", "name": "dogfish"}, + {"id": 1675, "synset": "smooth_dogfish.n.01", "name": "smooth_dogfish"}, + {"id": 1676, "synset": "smoothhound.n.01", "name": "smoothhound"}, + {"id": 1677, "synset": "american_smooth_dogfish.n.01", "name": "American_smooth_dogfish"}, + {"id": 1678, "synset": "florida_smoothhound.n.01", "name": "Florida_smoothhound"}, + {"id": 1679, "synset": "whitetip_shark.n.01", "name": "whitetip_shark"}, + {"id": 1680, "synset": "spiny_dogfish.n.01", "name": "spiny_dogfish"}, + {"id": 1681, "synset": "atlantic_spiny_dogfish.n.01", "name": "Atlantic_spiny_dogfish"}, + {"id": 1682, "synset": "pacific_spiny_dogfish.n.01", "name": "Pacific_spiny_dogfish"}, + {"id": 1683, "synset": "hammerhead.n.03", "name": "hammerhead"}, + {"id": 1684, "synset": "smooth_hammerhead.n.01", "name": "smooth_hammerhead"}, + {"id": 1685, "synset": "smalleye_hammerhead.n.01", "name": "smalleye_hammerhead"}, + {"id": 1686, "synset": "shovelhead.n.01", "name": "shovelhead"}, + {"id": 1687, "synset": "angel_shark.n.01", "name": "angel_shark"}, + {"id": 1688, "synset": "ray.n.07", "name": "ray"}, + {"id": 1689, "synset": "electric_ray.n.01", "name": "electric_ray"}, + {"id": 1690, "synset": "sawfish.n.01", "name": "sawfish"}, + {"id": 1691, "synset": "smalltooth_sawfish.n.01", "name": "smalltooth_sawfish"}, + {"id": 1692, "synset": "guitarfish.n.01", "name": "guitarfish"}, + {"id": 1693, "synset": "stingray.n.01", "name": "stingray"}, + {"id": 1694, "synset": "roughtail_stingray.n.01", "name": "roughtail_stingray"}, + {"id": 1695, "synset": "butterfly_ray.n.01", "name": "butterfly_ray"}, + {"id": 1696, "synset": "eagle_ray.n.01", "name": "eagle_ray"}, + {"id": 1697, "synset": "spotted_eagle_ray.n.01", "name": "spotted_eagle_ray"}, + {"id": 1698, "synset": "cownose_ray.n.01", "name": "cownose_ray"}, + {"id": 1699, "synset": "manta.n.02", "name": "manta"}, + {"id": 1700, "synset": "atlantic_manta.n.01", "name": "Atlantic_manta"}, + {"id": 1701, "synset": "devil_ray.n.01", "name": "devil_ray"}, + {"id": 1702, "synset": "skate.n.02", "name": "skate"}, + {"id": 1703, "synset": "grey_skate.n.01", "name": "grey_skate"}, + {"id": 1704, "synset": "little_skate.n.01", "name": "little_skate"}, + {"id": 1705, "synset": "thorny_skate.n.01", "name": "thorny_skate"}, + {"id": 1706, "synset": "barndoor_skate.n.01", "name": "barndoor_skate"}, + {"id": 1707, "synset": "dickeybird.n.01", "name": "dickeybird"}, + {"id": 1708, "synset": "fledgling.n.02", "name": "fledgling"}, + {"id": 1709, "synset": "nestling.n.01", "name": "nestling"}, + {"id": 1710, "synset": "cock.n.05", "name": "cock"}, + {"id": 1711, "synset": "gamecock.n.01", "name": "gamecock"}, + {"id": 1712, "synset": "hen.n.02", "name": "hen"}, + {"id": 1713, "synset": "nester.n.02", "name": "nester"}, + {"id": 1714, "synset": "night_bird.n.01", "name": "night_bird"}, + {"id": 1715, "synset": "night_raven.n.02", "name": "night_raven"}, + {"id": 1716, "synset": "bird_of_passage.n.02", "name": "bird_of_passage"}, + {"id": 1717, "synset": "archaeopteryx.n.01", "name": "archaeopteryx"}, + {"id": 1718, "synset": "archaeornis.n.01", "name": "archaeornis"}, + {"id": 1719, "synset": "ratite.n.01", "name": "ratite"}, + {"id": 1720, "synset": "carinate.n.01", "name": "carinate"}, + {"id": 1721, "synset": "cassowary.n.01", "name": "cassowary"}, + {"id": 1722, "synset": "emu.n.02", "name": "emu"}, + {"id": 1723, "synset": "kiwi.n.04", "name": "kiwi"}, + {"id": 1724, "synset": "rhea.n.03", "name": "rhea"}, + {"id": 1725, "synset": "rhea.n.02", "name": "rhea"}, + {"id": 1726, "synset": "elephant_bird.n.01", "name": "elephant_bird"}, + {"id": 1727, "synset": "moa.n.01", "name": "moa"}, + {"id": 1728, "synset": "passerine.n.01", "name": "passerine"}, + {"id": 1729, "synset": "nonpasserine_bird.n.01", "name": "nonpasserine_bird"}, + {"id": 1730, "synset": "oscine.n.01", "name": "oscine"}, + {"id": 1731, "synset": "songbird.n.01", "name": "songbird"}, + {"id": 1732, "synset": "honey_eater.n.01", "name": "honey_eater"}, + {"id": 1733, "synset": "accentor.n.01", "name": "accentor"}, + {"id": 1734, "synset": "hedge_sparrow.n.01", "name": "hedge_sparrow"}, + {"id": 1735, "synset": "lark.n.03", "name": "lark"}, + {"id": 1736, "synset": "skylark.n.01", "name": "skylark"}, + {"id": 1737, "synset": "wagtail.n.01", "name": "wagtail"}, + {"id": 1738, "synset": "pipit.n.01", "name": "pipit"}, + {"id": 1739, "synset": "meadow_pipit.n.01", "name": "meadow_pipit"}, + {"id": 1740, "synset": "finch.n.01", "name": "finch"}, + {"id": 1741, "synset": "chaffinch.n.01", "name": "chaffinch"}, + {"id": 1742, "synset": "brambling.n.01", "name": "brambling"}, + {"id": 1743, "synset": "goldfinch.n.02", "name": "goldfinch"}, + {"id": 1744, "synset": "linnet.n.02", "name": "linnet"}, + {"id": 1745, "synset": "siskin.n.01", "name": "siskin"}, + {"id": 1746, "synset": "red_siskin.n.01", "name": "red_siskin"}, + {"id": 1747, "synset": "redpoll.n.02", "name": "redpoll"}, + {"id": 1748, "synset": "redpoll.n.01", "name": "redpoll"}, + {"id": 1749, "synset": "new_world_goldfinch.n.01", "name": "New_World_goldfinch"}, + {"id": 1750, "synset": "pine_siskin.n.01", "name": "pine_siskin"}, + {"id": 1751, "synset": "house_finch.n.01", "name": "house_finch"}, + {"id": 1752, "synset": "purple_finch.n.01", "name": "purple_finch"}, + {"id": 1753, "synset": "canary.n.04", "name": "canary"}, + {"id": 1754, "synset": "common_canary.n.01", "name": "common_canary"}, + {"id": 1755, "synset": "serin.n.01", "name": "serin"}, + {"id": 1756, "synset": "crossbill.n.01", "name": "crossbill"}, + {"id": 1757, "synset": "bullfinch.n.02", "name": "bullfinch"}, + {"id": 1758, "synset": "junco.n.01", "name": "junco"}, + {"id": 1759, "synset": "dark-eyed_junco.n.01", "name": "dark-eyed_junco"}, + {"id": 1760, "synset": "new_world_sparrow.n.01", "name": "New_World_sparrow"}, + {"id": 1761, "synset": "vesper_sparrow.n.01", "name": "vesper_sparrow"}, + {"id": 1762, "synset": "white-throated_sparrow.n.01", "name": "white-throated_sparrow"}, + {"id": 1763, "synset": "white-crowned_sparrow.n.01", "name": "white-crowned_sparrow"}, + {"id": 1764, "synset": "chipping_sparrow.n.01", "name": "chipping_sparrow"}, + {"id": 1765, "synset": "field_sparrow.n.01", "name": "field_sparrow"}, + {"id": 1766, "synset": "tree_sparrow.n.02", "name": "tree_sparrow"}, + {"id": 1767, "synset": "song_sparrow.n.01", "name": "song_sparrow"}, + {"id": 1768, "synset": "swamp_sparrow.n.01", "name": "swamp_sparrow"}, + {"id": 1769, "synset": "bunting.n.02", "name": "bunting"}, + {"id": 1770, "synset": "indigo_bunting.n.01", "name": "indigo_bunting"}, + {"id": 1771, "synset": "ortolan.n.01", "name": "ortolan"}, + {"id": 1772, "synset": "reed_bunting.n.01", "name": "reed_bunting"}, + {"id": 1773, "synset": "yellowhammer.n.02", "name": "yellowhammer"}, + {"id": 1774, "synset": "yellow-breasted_bunting.n.01", "name": "yellow-breasted_bunting"}, + {"id": 1775, "synset": "snow_bunting.n.01", "name": "snow_bunting"}, + {"id": 1776, "synset": "honeycreeper.n.02", "name": "honeycreeper"}, + {"id": 1777, "synset": "banana_quit.n.01", "name": "banana_quit"}, + {"id": 1778, "synset": "sparrow.n.01", "name": "sparrow"}, + {"id": 1779, "synset": "english_sparrow.n.01", "name": "English_sparrow"}, + {"id": 1780, "synset": "tree_sparrow.n.01", "name": "tree_sparrow"}, + {"id": 1781, "synset": "grosbeak.n.01", "name": "grosbeak"}, + {"id": 1782, "synset": "evening_grosbeak.n.01", "name": "evening_grosbeak"}, + {"id": 1783, "synset": "hawfinch.n.01", "name": "hawfinch"}, + {"id": 1784, "synset": "pine_grosbeak.n.01", "name": "pine_grosbeak"}, + {"id": 1785, "synset": "cardinal.n.04", "name": "cardinal"}, + {"id": 1786, "synset": "pyrrhuloxia.n.01", "name": "pyrrhuloxia"}, + {"id": 1787, "synset": "towhee.n.01", "name": "towhee"}, + {"id": 1788, "synset": "chewink.n.01", "name": "chewink"}, + {"id": 1789, "synset": "green-tailed_towhee.n.01", "name": "green-tailed_towhee"}, + {"id": 1790, "synset": "weaver.n.02", "name": "weaver"}, + {"id": 1791, "synset": "baya.n.01", "name": "baya"}, + {"id": 1792, "synset": "whydah.n.01", "name": "whydah"}, + {"id": 1793, "synset": "java_sparrow.n.01", "name": "Java_sparrow"}, + {"id": 1794, "synset": "avadavat.n.01", "name": "avadavat"}, + {"id": 1795, "synset": "grassfinch.n.01", "name": "grassfinch"}, + {"id": 1796, "synset": "zebra_finch.n.01", "name": "zebra_finch"}, + {"id": 1797, "synset": "honeycreeper.n.01", "name": "honeycreeper"}, + {"id": 1798, "synset": "lyrebird.n.01", "name": "lyrebird"}, + {"id": 1799, "synset": "scrubbird.n.01", "name": "scrubbird"}, + {"id": 1800, "synset": "broadbill.n.04", "name": "broadbill"}, + {"id": 1801, "synset": "tyrannid.n.01", "name": "tyrannid"}, + {"id": 1802, "synset": "new_world_flycatcher.n.01", "name": "New_World_flycatcher"}, + {"id": 1803, "synset": "kingbird.n.01", "name": "kingbird"}, + {"id": 1804, "synset": "arkansas_kingbird.n.01", "name": "Arkansas_kingbird"}, + {"id": 1805, "synset": "cassin's_kingbird.n.01", "name": "Cassin's_kingbird"}, + {"id": 1806, "synset": "eastern_kingbird.n.01", "name": "eastern_kingbird"}, + {"id": 1807, "synset": "grey_kingbird.n.01", "name": "grey_kingbird"}, + {"id": 1808, "synset": "pewee.n.01", "name": "pewee"}, + {"id": 1809, "synset": "western_wood_pewee.n.01", "name": "western_wood_pewee"}, + {"id": 1810, "synset": "phoebe.n.03", "name": "phoebe"}, + {"id": 1811, "synset": "vermillion_flycatcher.n.01", "name": "vermillion_flycatcher"}, + {"id": 1812, "synset": "cotinga.n.01", "name": "cotinga"}, + {"id": 1813, "synset": "cock_of_the_rock.n.02", "name": "cock_of_the_rock"}, + {"id": 1814, "synset": "cock_of_the_rock.n.01", "name": "cock_of_the_rock"}, + {"id": 1815, "synset": "manakin.n.03", "name": "manakin"}, + {"id": 1816, "synset": "bellbird.n.01", "name": "bellbird"}, + {"id": 1817, "synset": "umbrella_bird.n.01", "name": "umbrella_bird"}, + {"id": 1818, "synset": "ovenbird.n.02", "name": "ovenbird"}, + {"id": 1819, "synset": "antbird.n.01", "name": "antbird"}, + {"id": 1820, "synset": "ant_thrush.n.01", "name": "ant_thrush"}, + {"id": 1821, "synset": "ant_shrike.n.01", "name": "ant_shrike"}, + {"id": 1822, "synset": "spotted_antbird.n.01", "name": "spotted_antbird"}, + {"id": 1823, "synset": "woodhewer.n.01", "name": "woodhewer"}, + {"id": 1824, "synset": "pitta.n.01", "name": "pitta"}, + {"id": 1825, "synset": "scissortail.n.01", "name": "scissortail"}, + {"id": 1826, "synset": "old_world_flycatcher.n.01", "name": "Old_World_flycatcher"}, + {"id": 1827, "synset": "spotted_flycatcher.n.01", "name": "spotted_flycatcher"}, + {"id": 1828, "synset": "thickhead.n.01", "name": "thickhead"}, + {"id": 1829, "synset": "thrush.n.03", "name": "thrush"}, + {"id": 1830, "synset": "missel_thrush.n.01", "name": "missel_thrush"}, + {"id": 1831, "synset": "song_thrush.n.01", "name": "song_thrush"}, + {"id": 1832, "synset": "fieldfare.n.01", "name": "fieldfare"}, + {"id": 1833, "synset": "redwing.n.02", "name": "redwing"}, + {"id": 1834, "synset": "blackbird.n.02", "name": "blackbird"}, + {"id": 1835, "synset": "ring_ouzel.n.01", "name": "ring_ouzel"}, + {"id": 1836, "synset": "robin.n.02", "name": "robin"}, + {"id": 1837, "synset": "clay-colored_robin.n.01", "name": "clay-colored_robin"}, + {"id": 1838, "synset": "hermit_thrush.n.01", "name": "hermit_thrush"}, + {"id": 1839, "synset": "veery.n.01", "name": "veery"}, + {"id": 1840, "synset": "wood_thrush.n.01", "name": "wood_thrush"}, + {"id": 1841, "synset": "nightingale.n.01", "name": "nightingale"}, + {"id": 1842, "synset": "thrush_nightingale.n.01", "name": "thrush_nightingale"}, + {"id": 1843, "synset": "bulbul.n.01", "name": "bulbul"}, + {"id": 1844, "synset": "old_world_chat.n.01", "name": "Old_World_chat"}, + {"id": 1845, "synset": "stonechat.n.01", "name": "stonechat"}, + {"id": 1846, "synset": "whinchat.n.01", "name": "whinchat"}, + {"id": 1847, "synset": "solitaire.n.03", "name": "solitaire"}, + {"id": 1848, "synset": "redstart.n.02", "name": "redstart"}, + {"id": 1849, "synset": "wheatear.n.01", "name": "wheatear"}, + {"id": 1850, "synset": "bluebird.n.02", "name": "bluebird"}, + {"id": 1851, "synset": "robin.n.01", "name": "robin"}, + {"id": 1852, "synset": "bluethroat.n.01", "name": "bluethroat"}, + {"id": 1853, "synset": "warbler.n.02", "name": "warbler"}, + {"id": 1854, "synset": "gnatcatcher.n.01", "name": "gnatcatcher"}, + {"id": 1855, "synset": "kinglet.n.01", "name": "kinglet"}, + {"id": 1856, "synset": "goldcrest.n.01", "name": "goldcrest"}, + {"id": 1857, "synset": "gold-crowned_kinglet.n.01", "name": "gold-crowned_kinglet"}, + {"id": 1858, "synset": "ruby-crowned_kinglet.n.01", "name": "ruby-crowned_kinglet"}, + {"id": 1859, "synset": "old_world_warbler.n.01", "name": "Old_World_warbler"}, + {"id": 1860, "synset": "blackcap.n.04", "name": "blackcap"}, + {"id": 1861, "synset": "greater_whitethroat.n.01", "name": "greater_whitethroat"}, + {"id": 1862, "synset": "lesser_whitethroat.n.01", "name": "lesser_whitethroat"}, + {"id": 1863, "synset": "wood_warbler.n.02", "name": "wood_warbler"}, + {"id": 1864, "synset": "sedge_warbler.n.01", "name": "sedge_warbler"}, + {"id": 1865, "synset": "wren_warbler.n.01", "name": "wren_warbler"}, + {"id": 1866, "synset": "tailorbird.n.01", "name": "tailorbird"}, + {"id": 1867, "synset": "babbler.n.02", "name": "babbler"}, + {"id": 1868, "synset": "new_world_warbler.n.01", "name": "New_World_warbler"}, + {"id": 1869, "synset": "parula_warbler.n.01", "name": "parula_warbler"}, + {"id": 1870, "synset": "wilson's_warbler.n.01", "name": "Wilson's_warbler"}, + {"id": 1871, "synset": "flycatching_warbler.n.01", "name": "flycatching_warbler"}, + {"id": 1872, "synset": "american_redstart.n.01", "name": "American_redstart"}, + {"id": 1873, "synset": "cape_may_warbler.n.01", "name": "Cape_May_warbler"}, + {"id": 1874, "synset": "yellow_warbler.n.01", "name": "yellow_warbler"}, + {"id": 1875, "synset": "blackburn.n.01", "name": "Blackburn"}, + {"id": 1876, "synset": "audubon's_warbler.n.01", "name": "Audubon's_warbler"}, + {"id": 1877, "synset": "myrtle_warbler.n.01", "name": "myrtle_warbler"}, + {"id": 1878, "synset": "blackpoll.n.01", "name": "blackpoll"}, + {"id": 1879, "synset": "new_world_chat.n.01", "name": "New_World_chat"}, + {"id": 1880, "synset": "yellow-breasted_chat.n.01", "name": "yellow-breasted_chat"}, + {"id": 1881, "synset": "ovenbird.n.01", "name": "ovenbird"}, + {"id": 1882, "synset": "water_thrush.n.01", "name": "water_thrush"}, + {"id": 1883, "synset": "yellowthroat.n.01", "name": "yellowthroat"}, + {"id": 1884, "synset": "common_yellowthroat.n.01", "name": "common_yellowthroat"}, + {"id": 1885, "synset": "riflebird.n.01", "name": "riflebird"}, + {"id": 1886, "synset": "new_world_oriole.n.01", "name": "New_World_oriole"}, + {"id": 1887, "synset": "northern_oriole.n.01", "name": "northern_oriole"}, + {"id": 1888, "synset": "baltimore_oriole.n.01", "name": "Baltimore_oriole"}, + {"id": 1889, "synset": "bullock's_oriole.n.01", "name": "Bullock's_oriole"}, + {"id": 1890, "synset": "orchard_oriole.n.01", "name": "orchard_oriole"}, + {"id": 1891, "synset": "meadowlark.n.01", "name": "meadowlark"}, + {"id": 1892, "synset": "eastern_meadowlark.n.01", "name": "eastern_meadowlark"}, + {"id": 1893, "synset": "western_meadowlark.n.01", "name": "western_meadowlark"}, + {"id": 1894, "synset": "cacique.n.01", "name": "cacique"}, + {"id": 1895, "synset": "bobolink.n.01", "name": "bobolink"}, + {"id": 1896, "synset": "new_world_blackbird.n.01", "name": "New_World_blackbird"}, + {"id": 1897, "synset": "grackle.n.02", "name": "grackle"}, + {"id": 1898, "synset": "purple_grackle.n.01", "name": "purple_grackle"}, + {"id": 1899, "synset": "rusty_blackbird.n.01", "name": "rusty_blackbird"}, + {"id": 1900, "synset": "cowbird.n.01", "name": "cowbird"}, + {"id": 1901, "synset": "red-winged_blackbird.n.01", "name": "red-winged_blackbird"}, + {"id": 1902, "synset": "old_world_oriole.n.01", "name": "Old_World_oriole"}, + {"id": 1903, "synset": "golden_oriole.n.01", "name": "golden_oriole"}, + {"id": 1904, "synset": "fig-bird.n.01", "name": "fig-bird"}, + {"id": 1905, "synset": "starling.n.01", "name": "starling"}, + {"id": 1906, "synset": "common_starling.n.01", "name": "common_starling"}, + {"id": 1907, "synset": "rose-colored_starling.n.01", "name": "rose-colored_starling"}, + {"id": 1908, "synset": "myna.n.01", "name": "myna"}, + {"id": 1909, "synset": "crested_myna.n.01", "name": "crested_myna"}, + {"id": 1910, "synset": "hill_myna.n.01", "name": "hill_myna"}, + {"id": 1911, "synset": "corvine_bird.n.01", "name": "corvine_bird"}, + {"id": 1912, "synset": "american_crow.n.01", "name": "American_crow"}, + {"id": 1913, "synset": "raven.n.01", "name": "raven"}, + {"id": 1914, "synset": "rook.n.02", "name": "rook"}, + {"id": 1915, "synset": "jackdaw.n.01", "name": "jackdaw"}, + {"id": 1916, "synset": "chough.n.01", "name": "chough"}, + {"id": 1917, "synset": "jay.n.02", "name": "jay"}, + {"id": 1918, "synset": "old_world_jay.n.01", "name": "Old_World_jay"}, + {"id": 1919, "synset": "common_european_jay.n.01", "name": "common_European_jay"}, + {"id": 1920, "synset": "new_world_jay.n.01", "name": "New_World_jay"}, + {"id": 1921, "synset": "blue_jay.n.01", "name": "blue_jay"}, + {"id": 1922, "synset": "canada_jay.n.01", "name": "Canada_jay"}, + {"id": 1923, "synset": "rocky_mountain_jay.n.01", "name": "Rocky_Mountain_jay"}, + {"id": 1924, "synset": "nutcracker.n.03", "name": "nutcracker"}, + {"id": 1925, "synset": "common_nutcracker.n.01", "name": "common_nutcracker"}, + {"id": 1926, "synset": "clark's_nutcracker.n.01", "name": "Clark's_nutcracker"}, + {"id": 1927, "synset": "magpie.n.01", "name": "magpie"}, + {"id": 1928, "synset": "european_magpie.n.01", "name": "European_magpie"}, + {"id": 1929, "synset": "american_magpie.n.01", "name": "American_magpie"}, + {"id": 1930, "synset": "australian_magpie.n.01", "name": "Australian_magpie"}, + {"id": 1931, "synset": "butcherbird.n.02", "name": "butcherbird"}, + {"id": 1932, "synset": "currawong.n.01", "name": "currawong"}, + {"id": 1933, "synset": "piping_crow.n.01", "name": "piping_crow"}, + {"id": 1934, "synset": "wren.n.02", "name": "wren"}, + {"id": 1935, "synset": "winter_wren.n.01", "name": "winter_wren"}, + {"id": 1936, "synset": "house_wren.n.01", "name": "house_wren"}, + {"id": 1937, "synset": "marsh_wren.n.01", "name": "marsh_wren"}, + {"id": 1938, "synset": "long-billed_marsh_wren.n.01", "name": "long-billed_marsh_wren"}, + {"id": 1939, "synset": "sedge_wren.n.01", "name": "sedge_wren"}, + {"id": 1940, "synset": "rock_wren.n.02", "name": "rock_wren"}, + {"id": 1941, "synset": "carolina_wren.n.01", "name": "Carolina_wren"}, + {"id": 1942, "synset": "cactus_wren.n.01", "name": "cactus_wren"}, + {"id": 1943, "synset": "mockingbird.n.01", "name": "mockingbird"}, + {"id": 1944, "synset": "blue_mockingbird.n.01", "name": "blue_mockingbird"}, + {"id": 1945, "synset": "catbird.n.02", "name": "catbird"}, + {"id": 1946, "synset": "thrasher.n.02", "name": "thrasher"}, + {"id": 1947, "synset": "brown_thrasher.n.01", "name": "brown_thrasher"}, + {"id": 1948, "synset": "new_zealand_wren.n.01", "name": "New_Zealand_wren"}, + {"id": 1949, "synset": "rock_wren.n.01", "name": "rock_wren"}, + {"id": 1950, "synset": "rifleman_bird.n.01", "name": "rifleman_bird"}, + {"id": 1951, "synset": "creeper.n.03", "name": "creeper"}, + {"id": 1952, "synset": "brown_creeper.n.01", "name": "brown_creeper"}, + {"id": 1953, "synset": "european_creeper.n.01", "name": "European_creeper"}, + {"id": 1954, "synset": "wall_creeper.n.01", "name": "wall_creeper"}, + {"id": 1955, "synset": "european_nuthatch.n.01", "name": "European_nuthatch"}, + {"id": 1956, "synset": "red-breasted_nuthatch.n.01", "name": "red-breasted_nuthatch"}, + {"id": 1957, "synset": "white-breasted_nuthatch.n.01", "name": "white-breasted_nuthatch"}, + {"id": 1958, "synset": "titmouse.n.01", "name": "titmouse"}, + {"id": 1959, "synset": "chickadee.n.01", "name": "chickadee"}, + {"id": 1960, "synset": "black-capped_chickadee.n.01", "name": "black-capped_chickadee"}, + {"id": 1961, "synset": "tufted_titmouse.n.01", "name": "tufted_titmouse"}, + {"id": 1962, "synset": "carolina_chickadee.n.01", "name": "Carolina_chickadee"}, + {"id": 1963, "synset": "blue_tit.n.01", "name": "blue_tit"}, + {"id": 1964, "synset": "bushtit.n.01", "name": "bushtit"}, + {"id": 1965, "synset": "wren-tit.n.01", "name": "wren-tit"}, + {"id": 1966, "synset": "verdin.n.01", "name": "verdin"}, + {"id": 1967, "synset": "fairy_bluebird.n.01", "name": "fairy_bluebird"}, + {"id": 1968, "synset": "swallow.n.03", "name": "swallow"}, + {"id": 1969, "synset": "barn_swallow.n.01", "name": "barn_swallow"}, + {"id": 1970, "synset": "cliff_swallow.n.01", "name": "cliff_swallow"}, + {"id": 1971, "synset": "tree_swallow.n.02", "name": "tree_swallow"}, + {"id": 1972, "synset": "white-bellied_swallow.n.01", "name": "white-bellied_swallow"}, + {"id": 1973, "synset": "martin.n.05", "name": "martin"}, + {"id": 1974, "synset": "house_martin.n.01", "name": "house_martin"}, + {"id": 1975, "synset": "bank_martin.n.01", "name": "bank_martin"}, + {"id": 1976, "synset": "purple_martin.n.01", "name": "purple_martin"}, + {"id": 1977, "synset": "wood_swallow.n.01", "name": "wood_swallow"}, + {"id": 1978, "synset": "tanager.n.01", "name": "tanager"}, + {"id": 1979, "synset": "scarlet_tanager.n.01", "name": "scarlet_tanager"}, + {"id": 1980, "synset": "western_tanager.n.01", "name": "western_tanager"}, + {"id": 1981, "synset": "summer_tanager.n.01", "name": "summer_tanager"}, + {"id": 1982, "synset": "hepatic_tanager.n.01", "name": "hepatic_tanager"}, + {"id": 1983, "synset": "shrike.n.01", "name": "shrike"}, + {"id": 1984, "synset": "butcherbird.n.01", "name": "butcherbird"}, + {"id": 1985, "synset": "european_shrike.n.01", "name": "European_shrike"}, + {"id": 1986, "synset": "northern_shrike.n.01", "name": "northern_shrike"}, + {"id": 1987, "synset": "white-rumped_shrike.n.01", "name": "white-rumped_shrike"}, + {"id": 1988, "synset": "loggerhead_shrike.n.01", "name": "loggerhead_shrike"}, + {"id": 1989, "synset": "migrant_shrike.n.01", "name": "migrant_shrike"}, + {"id": 1990, "synset": "bush_shrike.n.01", "name": "bush_shrike"}, + {"id": 1991, "synset": "black-fronted_bush_shrike.n.01", "name": "black-fronted_bush_shrike"}, + {"id": 1992, "synset": "bowerbird.n.01", "name": "bowerbird"}, + {"id": 1993, "synset": "satin_bowerbird.n.01", "name": "satin_bowerbird"}, + {"id": 1994, "synset": "great_bowerbird.n.01", "name": "great_bowerbird"}, + {"id": 1995, "synset": "water_ouzel.n.01", "name": "water_ouzel"}, + {"id": 1996, "synset": "european_water_ouzel.n.01", "name": "European_water_ouzel"}, + {"id": 1997, "synset": "american_water_ouzel.n.01", "name": "American_water_ouzel"}, + {"id": 1998, "synset": "vireo.n.01", "name": "vireo"}, + {"id": 1999, "synset": "red-eyed_vireo.n.01", "name": "red-eyed_vireo"}, + {"id": 2000, "synset": "solitary_vireo.n.01", "name": "solitary_vireo"}, + {"id": 2001, "synset": "blue-headed_vireo.n.01", "name": "blue-headed_vireo"}, + {"id": 2002, "synset": "waxwing.n.01", "name": "waxwing"}, + {"id": 2003, "synset": "cedar_waxwing.n.01", "name": "cedar_waxwing"}, + {"id": 2004, "synset": "bohemian_waxwing.n.01", "name": "Bohemian_waxwing"}, + {"id": 2005, "synset": "bird_of_prey.n.01", "name": "bird_of_prey"}, + {"id": 2006, "synset": "accipitriformes.n.01", "name": "Accipitriformes"}, + {"id": 2007, "synset": "hawk.n.01", "name": "hawk"}, + {"id": 2008, "synset": "eyas.n.01", "name": "eyas"}, + {"id": 2009, "synset": "tiercel.n.01", "name": "tiercel"}, + {"id": 2010, "synset": "goshawk.n.01", "name": "goshawk"}, + {"id": 2011, "synset": "sparrow_hawk.n.02", "name": "sparrow_hawk"}, + {"id": 2012, "synset": "cooper's_hawk.n.01", "name": "Cooper's_hawk"}, + {"id": 2013, "synset": "chicken_hawk.n.01", "name": "chicken_hawk"}, + {"id": 2014, "synset": "buteonine.n.01", "name": "buteonine"}, + {"id": 2015, "synset": "redtail.n.01", "name": "redtail"}, + {"id": 2016, "synset": "rough-legged_hawk.n.01", "name": "rough-legged_hawk"}, + {"id": 2017, "synset": "red-shouldered_hawk.n.01", "name": "red-shouldered_hawk"}, + {"id": 2018, "synset": "buzzard.n.02", "name": "buzzard"}, + {"id": 2019, "synset": "honey_buzzard.n.01", "name": "honey_buzzard"}, + {"id": 2020, "synset": "kite.n.04", "name": "kite"}, + {"id": 2021, "synset": "black_kite.n.01", "name": "black_kite"}, + {"id": 2022, "synset": "swallow-tailed_kite.n.01", "name": "swallow-tailed_kite"}, + {"id": 2023, "synset": "white-tailed_kite.n.01", "name": "white-tailed_kite"}, + {"id": 2024, "synset": "harrier.n.03", "name": "harrier"}, + {"id": 2025, "synset": "marsh_harrier.n.01", "name": "marsh_harrier"}, + {"id": 2026, "synset": "montagu's_harrier.n.01", "name": "Montagu's_harrier"}, + {"id": 2027, "synset": "marsh_hawk.n.01", "name": "marsh_hawk"}, + {"id": 2028, "synset": "harrier_eagle.n.01", "name": "harrier_eagle"}, + {"id": 2029, "synset": "peregrine.n.01", "name": "peregrine"}, + {"id": 2030, "synset": "falcon-gentle.n.01", "name": "falcon-gentle"}, + {"id": 2031, "synset": "gyrfalcon.n.01", "name": "gyrfalcon"}, + {"id": 2032, "synset": "kestrel.n.02", "name": "kestrel"}, + {"id": 2033, "synset": "sparrow_hawk.n.01", "name": "sparrow_hawk"}, + {"id": 2034, "synset": "pigeon_hawk.n.01", "name": "pigeon_hawk"}, + {"id": 2035, "synset": "hobby.n.03", "name": "hobby"}, + {"id": 2036, "synset": "caracara.n.01", "name": "caracara"}, + {"id": 2037, "synset": "audubon's_caracara.n.01", "name": "Audubon's_caracara"}, + {"id": 2038, "synset": "carancha.n.01", "name": "carancha"}, + {"id": 2039, "synset": "young_bird.n.01", "name": "young_bird"}, + {"id": 2040, "synset": "eaglet.n.01", "name": "eaglet"}, + {"id": 2041, "synset": "harpy.n.04", "name": "harpy"}, + {"id": 2042, "synset": "golden_eagle.n.01", "name": "golden_eagle"}, + {"id": 2043, "synset": "tawny_eagle.n.01", "name": "tawny_eagle"}, + {"id": 2044, "synset": "bald_eagle.n.01", "name": "bald_eagle"}, + {"id": 2045, "synset": "sea_eagle.n.02", "name": "sea_eagle"}, + {"id": 2046, "synset": "kamchatkan_sea_eagle.n.01", "name": "Kamchatkan_sea_eagle"}, + {"id": 2047, "synset": "ern.n.01", "name": "ern"}, + {"id": 2048, "synset": "fishing_eagle.n.01", "name": "fishing_eagle"}, + {"id": 2049, "synset": "osprey.n.01", "name": "osprey"}, + {"id": 2050, "synset": "aegypiidae.n.01", "name": "Aegypiidae"}, + {"id": 2051, "synset": "old_world_vulture.n.01", "name": "Old_World_vulture"}, + {"id": 2052, "synset": "griffon_vulture.n.01", "name": "griffon_vulture"}, + {"id": 2053, "synset": "bearded_vulture.n.01", "name": "bearded_vulture"}, + {"id": 2054, "synset": "egyptian_vulture.n.01", "name": "Egyptian_vulture"}, + {"id": 2055, "synset": "black_vulture.n.02", "name": "black_vulture"}, + {"id": 2056, "synset": "secretary_bird.n.01", "name": "secretary_bird"}, + {"id": 2057, "synset": "new_world_vulture.n.01", "name": "New_World_vulture"}, + {"id": 2058, "synset": "buzzard.n.01", "name": "buzzard"}, + {"id": 2059, "synset": "condor.n.01", "name": "condor"}, + {"id": 2060, "synset": "andean_condor.n.01", "name": "Andean_condor"}, + {"id": 2061, "synset": "california_condor.n.01", "name": "California_condor"}, + {"id": 2062, "synset": "black_vulture.n.01", "name": "black_vulture"}, + {"id": 2063, "synset": "king_vulture.n.01", "name": "king_vulture"}, + {"id": 2064, "synset": "owlet.n.01", "name": "owlet"}, + {"id": 2065, "synset": "little_owl.n.01", "name": "little_owl"}, + {"id": 2066, "synset": "horned_owl.n.01", "name": "horned_owl"}, + {"id": 2067, "synset": "great_horned_owl.n.01", "name": "great_horned_owl"}, + {"id": 2068, "synset": "great_grey_owl.n.01", "name": "great_grey_owl"}, + {"id": 2069, "synset": "tawny_owl.n.01", "name": "tawny_owl"}, + {"id": 2070, "synset": "barred_owl.n.01", "name": "barred_owl"}, + {"id": 2071, "synset": "screech_owl.n.02", "name": "screech_owl"}, + {"id": 2072, "synset": "screech_owl.n.01", "name": "screech_owl"}, + {"id": 2073, "synset": "scops_owl.n.01", "name": "scops_owl"}, + {"id": 2074, "synset": "spotted_owl.n.01", "name": "spotted_owl"}, + {"id": 2075, "synset": "old_world_scops_owl.n.01", "name": "Old_World_scops_owl"}, + {"id": 2076, "synset": "oriental_scops_owl.n.01", "name": "Oriental_scops_owl"}, + {"id": 2077, "synset": "hoot_owl.n.01", "name": "hoot_owl"}, + {"id": 2078, "synset": "hawk_owl.n.01", "name": "hawk_owl"}, + {"id": 2079, "synset": "long-eared_owl.n.01", "name": "long-eared_owl"}, + {"id": 2080, "synset": "laughing_owl.n.01", "name": "laughing_owl"}, + {"id": 2081, "synset": "barn_owl.n.01", "name": "barn_owl"}, + {"id": 2082, "synset": "amphibian.n.03", "name": "amphibian"}, + {"id": 2083, "synset": "ichyostega.n.01", "name": "Ichyostega"}, + {"id": 2084, "synset": "urodele.n.01", "name": "urodele"}, + {"id": 2085, "synset": "salamander.n.01", "name": "salamander"}, + {"id": 2086, "synset": "european_fire_salamander.n.01", "name": "European_fire_salamander"}, + {"id": 2087, "synset": "spotted_salamander.n.02", "name": "spotted_salamander"}, + {"id": 2088, "synset": "alpine_salamander.n.01", "name": "alpine_salamander"}, + {"id": 2089, "synset": "newt.n.01", "name": "newt"}, + {"id": 2090, "synset": "common_newt.n.01", "name": "common_newt"}, + {"id": 2091, "synset": "red_eft.n.01", "name": "red_eft"}, + {"id": 2092, "synset": "pacific_newt.n.01", "name": "Pacific_newt"}, + {"id": 2093, "synset": "rough-skinned_newt.n.01", "name": "rough-skinned_newt"}, + {"id": 2094, "synset": "california_newt.n.01", "name": "California_newt"}, + {"id": 2095, "synset": "eft.n.01", "name": "eft"}, + {"id": 2096, "synset": "ambystomid.n.01", "name": "ambystomid"}, + {"id": 2097, "synset": "mole_salamander.n.01", "name": "mole_salamander"}, + {"id": 2098, "synset": "spotted_salamander.n.01", "name": "spotted_salamander"}, + {"id": 2099, "synset": "tiger_salamander.n.01", "name": "tiger_salamander"}, + {"id": 2100, "synset": "axolotl.n.01", "name": "axolotl"}, + {"id": 2101, "synset": "waterdog.n.01", "name": "waterdog"}, + {"id": 2102, "synset": "hellbender.n.01", "name": "hellbender"}, + {"id": 2103, "synset": "giant_salamander.n.01", "name": "giant_salamander"}, + {"id": 2104, "synset": "olm.n.01", "name": "olm"}, + {"id": 2105, "synset": "mud_puppy.n.01", "name": "mud_puppy"}, + {"id": 2106, "synset": "dicamptodon.n.01", "name": "dicamptodon"}, + {"id": 2107, "synset": "pacific_giant_salamander.n.01", "name": "Pacific_giant_salamander"}, + {"id": 2108, "synset": "olympic_salamander.n.01", "name": "olympic_salamander"}, + {"id": 2109, "synset": "lungless_salamander.n.01", "name": "lungless_salamander"}, + { + "id": 2110, + "synset": "eastern_red-backed_salamander.n.01", + "name": "eastern_red-backed_salamander", + }, + { + "id": 2111, + "synset": "western_red-backed_salamander.n.01", + "name": "western_red-backed_salamander", + }, + {"id": 2112, "synset": "dusky_salamander.n.01", "name": "dusky_salamander"}, + {"id": 2113, "synset": "climbing_salamander.n.01", "name": "climbing_salamander"}, + {"id": 2114, "synset": "arboreal_salamander.n.01", "name": "arboreal_salamander"}, + {"id": 2115, "synset": "slender_salamander.n.01", "name": "slender_salamander"}, + {"id": 2116, "synset": "web-toed_salamander.n.01", "name": "web-toed_salamander"}, + {"id": 2117, "synset": "shasta_salamander.n.01", "name": "Shasta_salamander"}, + {"id": 2118, "synset": "limestone_salamander.n.01", "name": "limestone_salamander"}, + {"id": 2119, "synset": "amphiuma.n.01", "name": "amphiuma"}, + {"id": 2120, "synset": "siren.n.05", "name": "siren"}, + {"id": 2121, "synset": "true_frog.n.01", "name": "true_frog"}, + {"id": 2122, "synset": "wood-frog.n.01", "name": "wood-frog"}, + {"id": 2123, "synset": "leopard_frog.n.01", "name": "leopard_frog"}, + {"id": 2124, "synset": "bullfrog.n.01", "name": "bullfrog"}, + {"id": 2125, "synset": "green_frog.n.01", "name": "green_frog"}, + {"id": 2126, "synset": "cascades_frog.n.01", "name": "cascades_frog"}, + {"id": 2127, "synset": "goliath_frog.n.01", "name": "goliath_frog"}, + {"id": 2128, "synset": "pickerel_frog.n.01", "name": "pickerel_frog"}, + {"id": 2129, "synset": "tarahumara_frog.n.01", "name": "tarahumara_frog"}, + {"id": 2130, "synset": "grass_frog.n.01", "name": "grass_frog"}, + {"id": 2131, "synset": "leptodactylid_frog.n.01", "name": "leptodactylid_frog"}, + {"id": 2132, "synset": "robber_frog.n.02", "name": "robber_frog"}, + {"id": 2133, "synset": "barking_frog.n.01", "name": "barking_frog"}, + {"id": 2134, "synset": "crapaud.n.01", "name": "crapaud"}, + {"id": 2135, "synset": "tree_frog.n.02", "name": "tree_frog"}, + {"id": 2136, "synset": "tailed_frog.n.01", "name": "tailed_frog"}, + {"id": 2137, "synset": "liopelma_hamiltoni.n.01", "name": "Liopelma_hamiltoni"}, + {"id": 2138, "synset": "true_toad.n.01", "name": "true_toad"}, + {"id": 2139, "synset": "bufo.n.01", "name": "bufo"}, + {"id": 2140, "synset": "agua.n.01", "name": "agua"}, + {"id": 2141, "synset": "european_toad.n.01", "name": "European_toad"}, + {"id": 2142, "synset": "natterjack.n.01", "name": "natterjack"}, + {"id": 2143, "synset": "american_toad.n.01", "name": "American_toad"}, + {"id": 2144, "synset": "eurasian_green_toad.n.01", "name": "Eurasian_green_toad"}, + {"id": 2145, "synset": "american_green_toad.n.01", "name": "American_green_toad"}, + {"id": 2146, "synset": "yosemite_toad.n.01", "name": "Yosemite_toad"}, + {"id": 2147, "synset": "texas_toad.n.01", "name": "Texas_toad"}, + {"id": 2148, "synset": "southwestern_toad.n.01", "name": "southwestern_toad"}, + {"id": 2149, "synset": "western_toad.n.01", "name": "western_toad"}, + {"id": 2150, "synset": "obstetrical_toad.n.01", "name": "obstetrical_toad"}, + {"id": 2151, "synset": "midwife_toad.n.01", "name": "midwife_toad"}, + {"id": 2152, "synset": "fire-bellied_toad.n.01", "name": "fire-bellied_toad"}, + {"id": 2153, "synset": "spadefoot.n.01", "name": "spadefoot"}, + {"id": 2154, "synset": "western_spadefoot.n.01", "name": "western_spadefoot"}, + {"id": 2155, "synset": "southern_spadefoot.n.01", "name": "southern_spadefoot"}, + {"id": 2156, "synset": "plains_spadefoot.n.01", "name": "plains_spadefoot"}, + {"id": 2157, "synset": "tree_toad.n.01", "name": "tree_toad"}, + {"id": 2158, "synset": "spring_peeper.n.01", "name": "spring_peeper"}, + {"id": 2159, "synset": "pacific_tree_toad.n.01", "name": "Pacific_tree_toad"}, + {"id": 2160, "synset": "canyon_treefrog.n.01", "name": "canyon_treefrog"}, + {"id": 2161, "synset": "chameleon_tree_frog.n.01", "name": "chameleon_tree_frog"}, + {"id": 2162, "synset": "cricket_frog.n.01", "name": "cricket_frog"}, + {"id": 2163, "synset": "northern_cricket_frog.n.01", "name": "northern_cricket_frog"}, + {"id": 2164, "synset": "eastern_cricket_frog.n.01", "name": "eastern_cricket_frog"}, + {"id": 2165, "synset": "chorus_frog.n.01", "name": "chorus_frog"}, + {"id": 2166, "synset": "lowland_burrowing_treefrog.n.01", "name": "lowland_burrowing_treefrog"}, + { + "id": 2167, + "synset": "western_narrow-mouthed_toad.n.01", + "name": "western_narrow-mouthed_toad", + }, + { + "id": 2168, + "synset": "eastern_narrow-mouthed_toad.n.01", + "name": "eastern_narrow-mouthed_toad", + }, + {"id": 2169, "synset": "sheep_frog.n.01", "name": "sheep_frog"}, + {"id": 2170, "synset": "tongueless_frog.n.01", "name": "tongueless_frog"}, + {"id": 2171, "synset": "surinam_toad.n.01", "name": "Surinam_toad"}, + {"id": 2172, "synset": "african_clawed_frog.n.01", "name": "African_clawed_frog"}, + {"id": 2173, "synset": "south_american_poison_toad.n.01", "name": "South_American_poison_toad"}, + {"id": 2174, "synset": "caecilian.n.01", "name": "caecilian"}, + {"id": 2175, "synset": "reptile.n.01", "name": "reptile"}, + {"id": 2176, "synset": "anapsid.n.01", "name": "anapsid"}, + {"id": 2177, "synset": "diapsid.n.01", "name": "diapsid"}, + {"id": 2178, "synset": "diapsida.n.01", "name": "Diapsida"}, + {"id": 2179, "synset": "chelonian.n.01", "name": "chelonian"}, + {"id": 2180, "synset": "sea_turtle.n.01", "name": "sea_turtle"}, + {"id": 2181, "synset": "green_turtle.n.01", "name": "green_turtle"}, + {"id": 2182, "synset": "loggerhead.n.02", "name": "loggerhead"}, + {"id": 2183, "synset": "ridley.n.01", "name": "ridley"}, + {"id": 2184, "synset": "atlantic_ridley.n.01", "name": "Atlantic_ridley"}, + {"id": 2185, "synset": "pacific_ridley.n.01", "name": "Pacific_ridley"}, + {"id": 2186, "synset": "hawksbill_turtle.n.01", "name": "hawksbill_turtle"}, + {"id": 2187, "synset": "leatherback_turtle.n.01", "name": "leatherback_turtle"}, + {"id": 2188, "synset": "snapping_turtle.n.01", "name": "snapping_turtle"}, + {"id": 2189, "synset": "common_snapping_turtle.n.01", "name": "common_snapping_turtle"}, + {"id": 2190, "synset": "alligator_snapping_turtle.n.01", "name": "alligator_snapping_turtle"}, + {"id": 2191, "synset": "mud_turtle.n.01", "name": "mud_turtle"}, + {"id": 2192, "synset": "musk_turtle.n.01", "name": "musk_turtle"}, + {"id": 2193, "synset": "terrapin.n.01", "name": "terrapin"}, + {"id": 2194, "synset": "diamondback_terrapin.n.01", "name": "diamondback_terrapin"}, + {"id": 2195, "synset": "red-bellied_terrapin.n.01", "name": "red-bellied_terrapin"}, + {"id": 2196, "synset": "slider.n.03", "name": "slider"}, + {"id": 2197, "synset": "cooter.n.01", "name": "cooter"}, + {"id": 2198, "synset": "box_turtle.n.01", "name": "box_turtle"}, + {"id": 2199, "synset": "western_box_turtle.n.01", "name": "Western_box_turtle"}, + {"id": 2200, "synset": "painted_turtle.n.01", "name": "painted_turtle"}, + {"id": 2201, "synset": "tortoise.n.01", "name": "tortoise"}, + {"id": 2202, "synset": "european_tortoise.n.01", "name": "European_tortoise"}, + {"id": 2203, "synset": "giant_tortoise.n.01", "name": "giant_tortoise"}, + {"id": 2204, "synset": "gopher_tortoise.n.01", "name": "gopher_tortoise"}, + {"id": 2205, "synset": "desert_tortoise.n.01", "name": "desert_tortoise"}, + {"id": 2206, "synset": "texas_tortoise.n.01", "name": "Texas_tortoise"}, + {"id": 2207, "synset": "soft-shelled_turtle.n.01", "name": "soft-shelled_turtle"}, + {"id": 2208, "synset": "spiny_softshell.n.01", "name": "spiny_softshell"}, + {"id": 2209, "synset": "smooth_softshell.n.01", "name": "smooth_softshell"}, + {"id": 2210, "synset": "tuatara.n.01", "name": "tuatara"}, + {"id": 2211, "synset": "saurian.n.01", "name": "saurian"}, + {"id": 2212, "synset": "gecko.n.01", "name": "gecko"}, + {"id": 2213, "synset": "flying_gecko.n.01", "name": "flying_gecko"}, + {"id": 2214, "synset": "banded_gecko.n.01", "name": "banded_gecko"}, + {"id": 2215, "synset": "iguanid.n.01", "name": "iguanid"}, + {"id": 2216, "synset": "common_iguana.n.01", "name": "common_iguana"}, + {"id": 2217, "synset": "marine_iguana.n.01", "name": "marine_iguana"}, + {"id": 2218, "synset": "desert_iguana.n.01", "name": "desert_iguana"}, + {"id": 2219, "synset": "chuckwalla.n.01", "name": "chuckwalla"}, + {"id": 2220, "synset": "zebra-tailed_lizard.n.01", "name": "zebra-tailed_lizard"}, + {"id": 2221, "synset": "fringe-toed_lizard.n.01", "name": "fringe-toed_lizard"}, + {"id": 2222, "synset": "earless_lizard.n.01", "name": "earless_lizard"}, + {"id": 2223, "synset": "collared_lizard.n.01", "name": "collared_lizard"}, + {"id": 2224, "synset": "leopard_lizard.n.01", "name": "leopard_lizard"}, + {"id": 2225, "synset": "spiny_lizard.n.02", "name": "spiny_lizard"}, + {"id": 2226, "synset": "fence_lizard.n.01", "name": "fence_lizard"}, + {"id": 2227, "synset": "western_fence_lizard.n.01", "name": "western_fence_lizard"}, + {"id": 2228, "synset": "eastern_fence_lizard.n.01", "name": "eastern_fence_lizard"}, + {"id": 2229, "synset": "sagebrush_lizard.n.01", "name": "sagebrush_lizard"}, + {"id": 2230, "synset": "side-blotched_lizard.n.01", "name": "side-blotched_lizard"}, + {"id": 2231, "synset": "tree_lizard.n.01", "name": "tree_lizard"}, + {"id": 2232, "synset": "horned_lizard.n.01", "name": "horned_lizard"}, + {"id": 2233, "synset": "texas_horned_lizard.n.01", "name": "Texas_horned_lizard"}, + {"id": 2234, "synset": "basilisk.n.03", "name": "basilisk"}, + {"id": 2235, "synset": "american_chameleon.n.01", "name": "American_chameleon"}, + {"id": 2236, "synset": "worm_lizard.n.01", "name": "worm_lizard"}, + {"id": 2237, "synset": "night_lizard.n.01", "name": "night_lizard"}, + {"id": 2238, "synset": "skink.n.01", "name": "skink"}, + {"id": 2239, "synset": "western_skink.n.01", "name": "western_skink"}, + {"id": 2240, "synset": "mountain_skink.n.01", "name": "mountain_skink"}, + {"id": 2241, "synset": "teiid_lizard.n.01", "name": "teiid_lizard"}, + {"id": 2242, "synset": "whiptail.n.01", "name": "whiptail"}, + {"id": 2243, "synset": "racerunner.n.01", "name": "racerunner"}, + {"id": 2244, "synset": "plateau_striped_whiptail.n.01", "name": "plateau_striped_whiptail"}, + { + "id": 2245, + "synset": "chihuahuan_spotted_whiptail.n.01", + "name": "Chihuahuan_spotted_whiptail", + }, + {"id": 2246, "synset": "western_whiptail.n.01", "name": "western_whiptail"}, + {"id": 2247, "synset": "checkered_whiptail.n.01", "name": "checkered_whiptail"}, + {"id": 2248, "synset": "teju.n.01", "name": "teju"}, + {"id": 2249, "synset": "caiman_lizard.n.01", "name": "caiman_lizard"}, + {"id": 2250, "synset": "agamid.n.01", "name": "agamid"}, + {"id": 2251, "synset": "agama.n.01", "name": "agama"}, + {"id": 2252, "synset": "frilled_lizard.n.01", "name": "frilled_lizard"}, + {"id": 2253, "synset": "moloch.n.03", "name": "moloch"}, + {"id": 2254, "synset": "mountain_devil.n.02", "name": "mountain_devil"}, + {"id": 2255, "synset": "anguid_lizard.n.01", "name": "anguid_lizard"}, + {"id": 2256, "synset": "alligator_lizard.n.01", "name": "alligator_lizard"}, + {"id": 2257, "synset": "blindworm.n.01", "name": "blindworm"}, + {"id": 2258, "synset": "glass_lizard.n.01", "name": "glass_lizard"}, + {"id": 2259, "synset": "legless_lizard.n.01", "name": "legless_lizard"}, + {"id": 2260, "synset": "lanthanotus_borneensis.n.01", "name": "Lanthanotus_borneensis"}, + {"id": 2261, "synset": "venomous_lizard.n.01", "name": "venomous_lizard"}, + {"id": 2262, "synset": "gila_monster.n.01", "name": "Gila_monster"}, + {"id": 2263, "synset": "beaded_lizard.n.01", "name": "beaded_lizard"}, + {"id": 2264, "synset": "lacertid_lizard.n.01", "name": "lacertid_lizard"}, + {"id": 2265, "synset": "sand_lizard.n.01", "name": "sand_lizard"}, + {"id": 2266, "synset": "green_lizard.n.01", "name": "green_lizard"}, + {"id": 2267, "synset": "chameleon.n.03", "name": "chameleon"}, + {"id": 2268, "synset": "african_chameleon.n.01", "name": "African_chameleon"}, + {"id": 2269, "synset": "horned_chameleon.n.01", "name": "horned_chameleon"}, + {"id": 2270, "synset": "monitor.n.07", "name": "monitor"}, + {"id": 2271, "synset": "african_monitor.n.01", "name": "African_monitor"}, + {"id": 2272, "synset": "komodo_dragon.n.01", "name": "Komodo_dragon"}, + {"id": 2273, "synset": "crocodilian_reptile.n.01", "name": "crocodilian_reptile"}, + {"id": 2274, "synset": "crocodile.n.01", "name": "crocodile"}, + {"id": 2275, "synset": "african_crocodile.n.01", "name": "African_crocodile"}, + {"id": 2276, "synset": "asian_crocodile.n.01", "name": "Asian_crocodile"}, + {"id": 2277, "synset": "morlett's_crocodile.n.01", "name": "Morlett's_crocodile"}, + {"id": 2278, "synset": "false_gavial.n.01", "name": "false_gavial"}, + {"id": 2279, "synset": "american_alligator.n.01", "name": "American_alligator"}, + {"id": 2280, "synset": "chinese_alligator.n.01", "name": "Chinese_alligator"}, + {"id": 2281, "synset": "caiman.n.01", "name": "caiman"}, + {"id": 2282, "synset": "spectacled_caiman.n.01", "name": "spectacled_caiman"}, + {"id": 2283, "synset": "gavial.n.01", "name": "gavial"}, + {"id": 2284, "synset": "armored_dinosaur.n.01", "name": "armored_dinosaur"}, + {"id": 2285, "synset": "stegosaur.n.01", "name": "stegosaur"}, + {"id": 2286, "synset": "ankylosaur.n.01", "name": "ankylosaur"}, + {"id": 2287, "synset": "edmontonia.n.01", "name": "Edmontonia"}, + {"id": 2288, "synset": "bone-headed_dinosaur.n.01", "name": "bone-headed_dinosaur"}, + {"id": 2289, "synset": "pachycephalosaur.n.01", "name": "pachycephalosaur"}, + {"id": 2290, "synset": "ceratopsian.n.01", "name": "ceratopsian"}, + {"id": 2291, "synset": "protoceratops.n.01", "name": "protoceratops"}, + {"id": 2292, "synset": "triceratops.n.01", "name": "triceratops"}, + {"id": 2293, "synset": "styracosaur.n.01", "name": "styracosaur"}, + {"id": 2294, "synset": "psittacosaur.n.01", "name": "psittacosaur"}, + {"id": 2295, "synset": "ornithopod.n.01", "name": "ornithopod"}, + {"id": 2296, "synset": "hadrosaur.n.01", "name": "hadrosaur"}, + {"id": 2297, "synset": "trachodon.n.01", "name": "trachodon"}, + {"id": 2298, "synset": "saurischian.n.01", "name": "saurischian"}, + {"id": 2299, "synset": "sauropod.n.01", "name": "sauropod"}, + {"id": 2300, "synset": "apatosaur.n.01", "name": "apatosaur"}, + {"id": 2301, "synset": "barosaur.n.01", "name": "barosaur"}, + {"id": 2302, "synset": "diplodocus.n.01", "name": "diplodocus"}, + {"id": 2303, "synset": "argentinosaur.n.01", "name": "argentinosaur"}, + {"id": 2304, "synset": "theropod.n.01", "name": "theropod"}, + {"id": 2305, "synset": "ceratosaur.n.01", "name": "ceratosaur"}, + {"id": 2306, "synset": "coelophysis.n.01", "name": "coelophysis"}, + {"id": 2307, "synset": "tyrannosaur.n.01", "name": "tyrannosaur"}, + {"id": 2308, "synset": "allosaur.n.01", "name": "allosaur"}, + {"id": 2309, "synset": "ornithomimid.n.01", "name": "ornithomimid"}, + {"id": 2310, "synset": "maniraptor.n.01", "name": "maniraptor"}, + {"id": 2311, "synset": "oviraptorid.n.01", "name": "oviraptorid"}, + {"id": 2312, "synset": "velociraptor.n.01", "name": "velociraptor"}, + {"id": 2313, "synset": "deinonychus.n.01", "name": "deinonychus"}, + {"id": 2314, "synset": "utahraptor.n.01", "name": "utahraptor"}, + {"id": 2315, "synset": "synapsid.n.01", "name": "synapsid"}, + {"id": 2316, "synset": "dicynodont.n.01", "name": "dicynodont"}, + {"id": 2317, "synset": "pelycosaur.n.01", "name": "pelycosaur"}, + {"id": 2318, "synset": "dimetrodon.n.01", "name": "dimetrodon"}, + {"id": 2319, "synset": "pterosaur.n.01", "name": "pterosaur"}, + {"id": 2320, "synset": "pterodactyl.n.01", "name": "pterodactyl"}, + {"id": 2321, "synset": "ichthyosaur.n.01", "name": "ichthyosaur"}, + {"id": 2322, "synset": "ichthyosaurus.n.01", "name": "ichthyosaurus"}, + {"id": 2323, "synset": "stenopterygius.n.01", "name": "stenopterygius"}, + {"id": 2324, "synset": "plesiosaur.n.01", "name": "plesiosaur"}, + {"id": 2325, "synset": "nothosaur.n.01", "name": "nothosaur"}, + {"id": 2326, "synset": "colubrid_snake.n.01", "name": "colubrid_snake"}, + {"id": 2327, "synset": "hoop_snake.n.01", "name": "hoop_snake"}, + {"id": 2328, "synset": "thunder_snake.n.01", "name": "thunder_snake"}, + {"id": 2329, "synset": "ringneck_snake.n.01", "name": "ringneck_snake"}, + {"id": 2330, "synset": "hognose_snake.n.01", "name": "hognose_snake"}, + {"id": 2331, "synset": "leaf-nosed_snake.n.01", "name": "leaf-nosed_snake"}, + {"id": 2332, "synset": "green_snake.n.02", "name": "green_snake"}, + {"id": 2333, "synset": "smooth_green_snake.n.01", "name": "smooth_green_snake"}, + {"id": 2334, "synset": "rough_green_snake.n.01", "name": "rough_green_snake"}, + {"id": 2335, "synset": "green_snake.n.01", "name": "green_snake"}, + {"id": 2336, "synset": "racer.n.04", "name": "racer"}, + {"id": 2337, "synset": "blacksnake.n.02", "name": "blacksnake"}, + {"id": 2338, "synset": "blue_racer.n.01", "name": "blue_racer"}, + {"id": 2339, "synset": "horseshoe_whipsnake.n.01", "name": "horseshoe_whipsnake"}, + {"id": 2340, "synset": "whip-snake.n.01", "name": "whip-snake"}, + {"id": 2341, "synset": "coachwhip.n.02", "name": "coachwhip"}, + {"id": 2342, "synset": "california_whipsnake.n.01", "name": "California_whipsnake"}, + {"id": 2343, "synset": "sonoran_whipsnake.n.01", "name": "Sonoran_whipsnake"}, + {"id": 2344, "synset": "rat_snake.n.01", "name": "rat_snake"}, + {"id": 2345, "synset": "corn_snake.n.01", "name": "corn_snake"}, + {"id": 2346, "synset": "black_rat_snake.n.01", "name": "black_rat_snake"}, + {"id": 2347, "synset": "chicken_snake.n.01", "name": "chicken_snake"}, + {"id": 2348, "synset": "indian_rat_snake.n.01", "name": "Indian_rat_snake"}, + {"id": 2349, "synset": "glossy_snake.n.01", "name": "glossy_snake"}, + {"id": 2350, "synset": "bull_snake.n.01", "name": "bull_snake"}, + {"id": 2351, "synset": "gopher_snake.n.02", "name": "gopher_snake"}, + {"id": 2352, "synset": "pine_snake.n.01", "name": "pine_snake"}, + {"id": 2353, "synset": "king_snake.n.01", "name": "king_snake"}, + {"id": 2354, "synset": "common_kingsnake.n.01", "name": "common_kingsnake"}, + {"id": 2355, "synset": "milk_snake.n.01", "name": "milk_snake"}, + {"id": 2356, "synset": "garter_snake.n.01", "name": "garter_snake"}, + {"id": 2357, "synset": "common_garter_snake.n.01", "name": "common_garter_snake"}, + {"id": 2358, "synset": "ribbon_snake.n.01", "name": "ribbon_snake"}, + {"id": 2359, "synset": "western_ribbon_snake.n.01", "name": "Western_ribbon_snake"}, + {"id": 2360, "synset": "lined_snake.n.01", "name": "lined_snake"}, + {"id": 2361, "synset": "ground_snake.n.01", "name": "ground_snake"}, + {"id": 2362, "synset": "eastern_ground_snake.n.01", "name": "eastern_ground_snake"}, + {"id": 2363, "synset": "water_snake.n.01", "name": "water_snake"}, + {"id": 2364, "synset": "common_water_snake.n.01", "name": "common_water_snake"}, + {"id": 2365, "synset": "water_moccasin.n.02", "name": "water_moccasin"}, + {"id": 2366, "synset": "grass_snake.n.01", "name": "grass_snake"}, + {"id": 2367, "synset": "viperine_grass_snake.n.01", "name": "viperine_grass_snake"}, + {"id": 2368, "synset": "red-bellied_snake.n.01", "name": "red-bellied_snake"}, + {"id": 2369, "synset": "sand_snake.n.01", "name": "sand_snake"}, + {"id": 2370, "synset": "banded_sand_snake.n.01", "name": "banded_sand_snake"}, + {"id": 2371, "synset": "black-headed_snake.n.01", "name": "black-headed_snake"}, + {"id": 2372, "synset": "vine_snake.n.01", "name": "vine_snake"}, + {"id": 2373, "synset": "lyre_snake.n.01", "name": "lyre_snake"}, + {"id": 2374, "synset": "sonoran_lyre_snake.n.01", "name": "Sonoran_lyre_snake"}, + {"id": 2375, "synset": "night_snake.n.01", "name": "night_snake"}, + {"id": 2376, "synset": "blind_snake.n.01", "name": "blind_snake"}, + {"id": 2377, "synset": "western_blind_snake.n.01", "name": "western_blind_snake"}, + {"id": 2378, "synset": "indigo_snake.n.01", "name": "indigo_snake"}, + {"id": 2379, "synset": "eastern_indigo_snake.n.01", "name": "eastern_indigo_snake"}, + {"id": 2380, "synset": "constrictor.n.01", "name": "constrictor"}, + {"id": 2381, "synset": "boa.n.02", "name": "boa"}, + {"id": 2382, "synset": "boa_constrictor.n.01", "name": "boa_constrictor"}, + {"id": 2383, "synset": "rubber_boa.n.01", "name": "rubber_boa"}, + {"id": 2384, "synset": "rosy_boa.n.01", "name": "rosy_boa"}, + {"id": 2385, "synset": "anaconda.n.01", "name": "anaconda"}, + {"id": 2386, "synset": "python.n.01", "name": "python"}, + {"id": 2387, "synset": "carpet_snake.n.01", "name": "carpet_snake"}, + {"id": 2388, "synset": "reticulated_python.n.01", "name": "reticulated_python"}, + {"id": 2389, "synset": "indian_python.n.01", "name": "Indian_python"}, + {"id": 2390, "synset": "rock_python.n.01", "name": "rock_python"}, + {"id": 2391, "synset": "amethystine_python.n.01", "name": "amethystine_python"}, + {"id": 2392, "synset": "elapid.n.01", "name": "elapid"}, + {"id": 2393, "synset": "coral_snake.n.02", "name": "coral_snake"}, + {"id": 2394, "synset": "eastern_coral_snake.n.01", "name": "eastern_coral_snake"}, + {"id": 2395, "synset": "western_coral_snake.n.01", "name": "western_coral_snake"}, + {"id": 2396, "synset": "coral_snake.n.01", "name": "coral_snake"}, + {"id": 2397, "synset": "african_coral_snake.n.01", "name": "African_coral_snake"}, + {"id": 2398, "synset": "australian_coral_snake.n.01", "name": "Australian_coral_snake"}, + {"id": 2399, "synset": "copperhead.n.02", "name": "copperhead"}, + {"id": 2400, "synset": "cobra.n.01", "name": "cobra"}, + {"id": 2401, "synset": "indian_cobra.n.01", "name": "Indian_cobra"}, + {"id": 2402, "synset": "asp.n.02", "name": "asp"}, + {"id": 2403, "synset": "black-necked_cobra.n.01", "name": "black-necked_cobra"}, + {"id": 2404, "synset": "hamadryad.n.02", "name": "hamadryad"}, + {"id": 2405, "synset": "ringhals.n.01", "name": "ringhals"}, + {"id": 2406, "synset": "mamba.n.01", "name": "mamba"}, + {"id": 2407, "synset": "black_mamba.n.01", "name": "black_mamba"}, + {"id": 2408, "synset": "green_mamba.n.01", "name": "green_mamba"}, + {"id": 2409, "synset": "death_adder.n.01", "name": "death_adder"}, + {"id": 2410, "synset": "tiger_snake.n.01", "name": "tiger_snake"}, + {"id": 2411, "synset": "australian_blacksnake.n.01", "name": "Australian_blacksnake"}, + {"id": 2412, "synset": "krait.n.01", "name": "krait"}, + {"id": 2413, "synset": "banded_krait.n.01", "name": "banded_krait"}, + {"id": 2414, "synset": "taipan.n.01", "name": "taipan"}, + {"id": 2415, "synset": "sea_snake.n.01", "name": "sea_snake"}, + {"id": 2416, "synset": "viper.n.01", "name": "viper"}, + {"id": 2417, "synset": "adder.n.03", "name": "adder"}, + {"id": 2418, "synset": "asp.n.01", "name": "asp"}, + {"id": 2419, "synset": "puff_adder.n.01", "name": "puff_adder"}, + {"id": 2420, "synset": "gaboon_viper.n.01", "name": "gaboon_viper"}, + {"id": 2421, "synset": "horned_viper.n.01", "name": "horned_viper"}, + {"id": 2422, "synset": "pit_viper.n.01", "name": "pit_viper"}, + {"id": 2423, "synset": "copperhead.n.01", "name": "copperhead"}, + {"id": 2424, "synset": "water_moccasin.n.01", "name": "water_moccasin"}, + {"id": 2425, "synset": "rattlesnake.n.01", "name": "rattlesnake"}, + {"id": 2426, "synset": "diamondback.n.01", "name": "diamondback"}, + {"id": 2427, "synset": "timber_rattlesnake.n.01", "name": "timber_rattlesnake"}, + {"id": 2428, "synset": "canebrake_rattlesnake.n.01", "name": "canebrake_rattlesnake"}, + {"id": 2429, "synset": "prairie_rattlesnake.n.01", "name": "prairie_rattlesnake"}, + {"id": 2430, "synset": "sidewinder.n.01", "name": "sidewinder"}, + {"id": 2431, "synset": "western_diamondback.n.01", "name": "Western_diamondback"}, + {"id": 2432, "synset": "rock_rattlesnake.n.01", "name": "rock_rattlesnake"}, + {"id": 2433, "synset": "tiger_rattlesnake.n.01", "name": "tiger_rattlesnake"}, + {"id": 2434, "synset": "mojave_rattlesnake.n.01", "name": "Mojave_rattlesnake"}, + {"id": 2435, "synset": "speckled_rattlesnake.n.01", "name": "speckled_rattlesnake"}, + {"id": 2436, "synset": "massasauga.n.02", "name": "massasauga"}, + {"id": 2437, "synset": "ground_rattler.n.01", "name": "ground_rattler"}, + {"id": 2438, "synset": "fer-de-lance.n.01", "name": "fer-de-lance"}, + {"id": 2439, "synset": "carcase.n.01", "name": "carcase"}, + {"id": 2440, "synset": "carrion.n.01", "name": "carrion"}, + {"id": 2441, "synset": "arthropod.n.01", "name": "arthropod"}, + {"id": 2442, "synset": "trilobite.n.01", "name": "trilobite"}, + {"id": 2443, "synset": "arachnid.n.01", "name": "arachnid"}, + {"id": 2444, "synset": "harvestman.n.01", "name": "harvestman"}, + {"id": 2445, "synset": "scorpion.n.03", "name": "scorpion"}, + {"id": 2446, "synset": "false_scorpion.n.01", "name": "false_scorpion"}, + {"id": 2447, "synset": "book_scorpion.n.01", "name": "book_scorpion"}, + {"id": 2448, "synset": "whip-scorpion.n.01", "name": "whip-scorpion"}, + {"id": 2449, "synset": "vinegarroon.n.01", "name": "vinegarroon"}, + {"id": 2450, "synset": "orb-weaving_spider.n.01", "name": "orb-weaving_spider"}, + { + "id": 2451, + "synset": "black_and_gold_garden_spider.n.01", + "name": "black_and_gold_garden_spider", + }, + {"id": 2452, "synset": "barn_spider.n.01", "name": "barn_spider"}, + {"id": 2453, "synset": "garden_spider.n.01", "name": "garden_spider"}, + {"id": 2454, "synset": "comb-footed_spider.n.01", "name": "comb-footed_spider"}, + {"id": 2455, "synset": "black_widow.n.01", "name": "black_widow"}, + {"id": 2456, "synset": "tarantula.n.02", "name": "tarantula"}, + {"id": 2457, "synset": "wolf_spider.n.01", "name": "wolf_spider"}, + {"id": 2458, "synset": "european_wolf_spider.n.01", "name": "European_wolf_spider"}, + {"id": 2459, "synset": "trap-door_spider.n.01", "name": "trap-door_spider"}, + {"id": 2460, "synset": "acarine.n.01", "name": "acarine"}, + {"id": 2461, "synset": "tick.n.02", "name": "tick"}, + {"id": 2462, "synset": "hard_tick.n.01", "name": "hard_tick"}, + {"id": 2463, "synset": "ixodes_dammini.n.01", "name": "Ixodes_dammini"}, + {"id": 2464, "synset": "ixodes_neotomae.n.01", "name": "Ixodes_neotomae"}, + {"id": 2465, "synset": "ixodes_pacificus.n.01", "name": "Ixodes_pacificus"}, + {"id": 2466, "synset": "ixodes_scapularis.n.01", "name": "Ixodes_scapularis"}, + {"id": 2467, "synset": "sheep-tick.n.02", "name": "sheep-tick"}, + {"id": 2468, "synset": "ixodes_persulcatus.n.01", "name": "Ixodes_persulcatus"}, + {"id": 2469, "synset": "ixodes_dentatus.n.01", "name": "Ixodes_dentatus"}, + {"id": 2470, "synset": "ixodes_spinipalpis.n.01", "name": "Ixodes_spinipalpis"}, + {"id": 2471, "synset": "wood_tick.n.01", "name": "wood_tick"}, + {"id": 2472, "synset": "soft_tick.n.01", "name": "soft_tick"}, + {"id": 2473, "synset": "mite.n.02", "name": "mite"}, + {"id": 2474, "synset": "web-spinning_mite.n.01", "name": "web-spinning_mite"}, + {"id": 2475, "synset": "acarid.n.01", "name": "acarid"}, + {"id": 2476, "synset": "trombidiid.n.01", "name": "trombidiid"}, + {"id": 2477, "synset": "trombiculid.n.01", "name": "trombiculid"}, + {"id": 2478, "synset": "harvest_mite.n.01", "name": "harvest_mite"}, + {"id": 2479, "synset": "acarus.n.01", "name": "acarus"}, + {"id": 2480, "synset": "itch_mite.n.01", "name": "itch_mite"}, + {"id": 2481, "synset": "rust_mite.n.01", "name": "rust_mite"}, + {"id": 2482, "synset": "spider_mite.n.01", "name": "spider_mite"}, + {"id": 2483, "synset": "red_spider.n.01", "name": "red_spider"}, + {"id": 2484, "synset": "myriapod.n.01", "name": "myriapod"}, + {"id": 2485, "synset": "garden_centipede.n.01", "name": "garden_centipede"}, + {"id": 2486, "synset": "tardigrade.n.01", "name": "tardigrade"}, + {"id": 2487, "synset": "centipede.n.01", "name": "centipede"}, + {"id": 2488, "synset": "house_centipede.n.01", "name": "house_centipede"}, + {"id": 2489, "synset": "millipede.n.01", "name": "millipede"}, + {"id": 2490, "synset": "sea_spider.n.01", "name": "sea_spider"}, + {"id": 2491, "synset": "merostomata.n.01", "name": "Merostomata"}, + {"id": 2492, "synset": "horseshoe_crab.n.01", "name": "horseshoe_crab"}, + {"id": 2493, "synset": "asian_horseshoe_crab.n.01", "name": "Asian_horseshoe_crab"}, + {"id": 2494, "synset": "eurypterid.n.01", "name": "eurypterid"}, + {"id": 2495, "synset": "tongue_worm.n.01", "name": "tongue_worm"}, + {"id": 2496, "synset": "gallinaceous_bird.n.01", "name": "gallinaceous_bird"}, + {"id": 2497, "synset": "domestic_fowl.n.01", "name": "domestic_fowl"}, + {"id": 2498, "synset": "dorking.n.01", "name": "Dorking"}, + {"id": 2499, "synset": "plymouth_rock.n.02", "name": "Plymouth_Rock"}, + {"id": 2500, "synset": "cornish.n.02", "name": "Cornish"}, + {"id": 2501, "synset": "rock_cornish.n.01", "name": "Rock_Cornish"}, + {"id": 2502, "synset": "game_fowl.n.01", "name": "game_fowl"}, + {"id": 2503, "synset": "cochin.n.01", "name": "cochin"}, + {"id": 2504, "synset": "jungle_fowl.n.01", "name": "jungle_fowl"}, + {"id": 2505, "synset": "jungle_cock.n.01", "name": "jungle_cock"}, + {"id": 2506, "synset": "jungle_hen.n.01", "name": "jungle_hen"}, + {"id": 2507, "synset": "red_jungle_fowl.n.01", "name": "red_jungle_fowl"}, + {"id": 2508, "synset": "bantam.n.01", "name": "bantam"}, + {"id": 2509, "synset": "chick.n.01", "name": "chick"}, + {"id": 2510, "synset": "cockerel.n.01", "name": "cockerel"}, + {"id": 2511, "synset": "capon.n.02", "name": "capon"}, + {"id": 2512, "synset": "hen.n.01", "name": "hen"}, + {"id": 2513, "synset": "cackler.n.01", "name": "cackler"}, + {"id": 2514, "synset": "brood_hen.n.01", "name": "brood_hen"}, + {"id": 2515, "synset": "mother_hen.n.02", "name": "mother_hen"}, + {"id": 2516, "synset": "layer.n.04", "name": "layer"}, + {"id": 2517, "synset": "pullet.n.02", "name": "pullet"}, + {"id": 2518, "synset": "spring_chicken.n.02", "name": "spring_chicken"}, + {"id": 2519, "synset": "rhode_island_red.n.01", "name": "Rhode_Island_red"}, + {"id": 2520, "synset": "dominique.n.01", "name": "Dominique"}, + {"id": 2521, "synset": "orpington.n.01", "name": "Orpington"}, + {"id": 2522, "synset": "turkey.n.01", "name": "turkey"}, + {"id": 2523, "synset": "turkey_cock.n.01", "name": "turkey_cock"}, + {"id": 2524, "synset": "ocellated_turkey.n.01", "name": "ocellated_turkey"}, + {"id": 2525, "synset": "grouse.n.02", "name": "grouse"}, + {"id": 2526, "synset": "black_grouse.n.01", "name": "black_grouse"}, + {"id": 2527, "synset": "european_black_grouse.n.01", "name": "European_black_grouse"}, + {"id": 2528, "synset": "asian_black_grouse.n.01", "name": "Asian_black_grouse"}, + {"id": 2529, "synset": "blackcock.n.01", "name": "blackcock"}, + {"id": 2530, "synset": "greyhen.n.01", "name": "greyhen"}, + {"id": 2531, "synset": "ptarmigan.n.01", "name": "ptarmigan"}, + {"id": 2532, "synset": "red_grouse.n.01", "name": "red_grouse"}, + {"id": 2533, "synset": "moorhen.n.02", "name": "moorhen"}, + {"id": 2534, "synset": "capercaillie.n.01", "name": "capercaillie"}, + {"id": 2535, "synset": "spruce_grouse.n.01", "name": "spruce_grouse"}, + {"id": 2536, "synset": "sage_grouse.n.01", "name": "sage_grouse"}, + {"id": 2537, "synset": "ruffed_grouse.n.01", "name": "ruffed_grouse"}, + {"id": 2538, "synset": "sharp-tailed_grouse.n.01", "name": "sharp-tailed_grouse"}, + {"id": 2539, "synset": "prairie_chicken.n.01", "name": "prairie_chicken"}, + {"id": 2540, "synset": "greater_prairie_chicken.n.01", "name": "greater_prairie_chicken"}, + {"id": 2541, "synset": "lesser_prairie_chicken.n.01", "name": "lesser_prairie_chicken"}, + {"id": 2542, "synset": "heath_hen.n.01", "name": "heath_hen"}, + {"id": 2543, "synset": "guan.n.01", "name": "guan"}, + {"id": 2544, "synset": "curassow.n.01", "name": "curassow"}, + {"id": 2545, "synset": "piping_guan.n.01", "name": "piping_guan"}, + {"id": 2546, "synset": "chachalaca.n.01", "name": "chachalaca"}, + {"id": 2547, "synset": "texas_chachalaca.n.01", "name": "Texas_chachalaca"}, + {"id": 2548, "synset": "megapode.n.01", "name": "megapode"}, + {"id": 2549, "synset": "mallee_fowl.n.01", "name": "mallee_fowl"}, + {"id": 2550, "synset": "mallee_hen.n.01", "name": "mallee_hen"}, + {"id": 2551, "synset": "brush_turkey.n.01", "name": "brush_turkey"}, + {"id": 2552, "synset": "maleo.n.01", "name": "maleo"}, + {"id": 2553, "synset": "phasianid.n.01", "name": "phasianid"}, + {"id": 2554, "synset": "pheasant.n.01", "name": "pheasant"}, + {"id": 2555, "synset": "ring-necked_pheasant.n.01", "name": "ring-necked_pheasant"}, + {"id": 2556, "synset": "afropavo.n.01", "name": "afropavo"}, + {"id": 2557, "synset": "argus.n.02", "name": "argus"}, + {"id": 2558, "synset": "golden_pheasant.n.01", "name": "golden_pheasant"}, + {"id": 2559, "synset": "bobwhite.n.01", "name": "bobwhite"}, + {"id": 2560, "synset": "northern_bobwhite.n.01", "name": "northern_bobwhite"}, + {"id": 2561, "synset": "old_world_quail.n.01", "name": "Old_World_quail"}, + {"id": 2562, "synset": "migratory_quail.n.01", "name": "migratory_quail"}, + {"id": 2563, "synset": "monal.n.01", "name": "monal"}, + {"id": 2564, "synset": "peafowl.n.01", "name": "peafowl"}, + {"id": 2565, "synset": "peachick.n.01", "name": "peachick"}, + {"id": 2566, "synset": "peacock.n.02", "name": "peacock"}, + {"id": 2567, "synset": "peahen.n.01", "name": "peahen"}, + {"id": 2568, "synset": "blue_peafowl.n.01", "name": "blue_peafowl"}, + {"id": 2569, "synset": "green_peafowl.n.01", "name": "green_peafowl"}, + {"id": 2570, "synset": "quail.n.02", "name": "quail"}, + {"id": 2571, "synset": "california_quail.n.01", "name": "California_quail"}, + {"id": 2572, "synset": "tragopan.n.01", "name": "tragopan"}, + {"id": 2573, "synset": "partridge.n.03", "name": "partridge"}, + {"id": 2574, "synset": "hungarian_partridge.n.01", "name": "Hungarian_partridge"}, + {"id": 2575, "synset": "red-legged_partridge.n.01", "name": "red-legged_partridge"}, + {"id": 2576, "synset": "greek_partridge.n.01", "name": "Greek_partridge"}, + {"id": 2577, "synset": "mountain_quail.n.01", "name": "mountain_quail"}, + {"id": 2578, "synset": "guinea_fowl.n.01", "name": "guinea_fowl"}, + {"id": 2579, "synset": "guinea_hen.n.02", "name": "guinea_hen"}, + {"id": 2580, "synset": "hoatzin.n.01", "name": "hoatzin"}, + {"id": 2581, "synset": "tinamou.n.01", "name": "tinamou"}, + {"id": 2582, "synset": "columbiform_bird.n.01", "name": "columbiform_bird"}, + {"id": 2583, "synset": "dodo.n.02", "name": "dodo"}, + {"id": 2584, "synset": "pouter_pigeon.n.01", "name": "pouter_pigeon"}, + {"id": 2585, "synset": "rock_dove.n.01", "name": "rock_dove"}, + {"id": 2586, "synset": "band-tailed_pigeon.n.01", "name": "band-tailed_pigeon"}, + {"id": 2587, "synset": "wood_pigeon.n.01", "name": "wood_pigeon"}, + {"id": 2588, "synset": "turtledove.n.02", "name": "turtledove"}, + {"id": 2589, "synset": "streptopelia_turtur.n.01", "name": "Streptopelia_turtur"}, + {"id": 2590, "synset": "ringdove.n.01", "name": "ringdove"}, + {"id": 2591, "synset": "australian_turtledove.n.01", "name": "Australian_turtledove"}, + {"id": 2592, "synset": "mourning_dove.n.01", "name": "mourning_dove"}, + {"id": 2593, "synset": "domestic_pigeon.n.01", "name": "domestic_pigeon"}, + {"id": 2594, "synset": "squab.n.03", "name": "squab"}, + {"id": 2595, "synset": "fairy_swallow.n.01", "name": "fairy_swallow"}, + {"id": 2596, "synset": "roller.n.07", "name": "roller"}, + {"id": 2597, "synset": "homing_pigeon.n.01", "name": "homing_pigeon"}, + {"id": 2598, "synset": "carrier_pigeon.n.01", "name": "carrier_pigeon"}, + {"id": 2599, "synset": "passenger_pigeon.n.01", "name": "passenger_pigeon"}, + {"id": 2600, "synset": "sandgrouse.n.01", "name": "sandgrouse"}, + {"id": 2601, "synset": "painted_sandgrouse.n.01", "name": "painted_sandgrouse"}, + {"id": 2602, "synset": "pin-tailed_sandgrouse.n.01", "name": "pin-tailed_sandgrouse"}, + {"id": 2603, "synset": "pallas's_sandgrouse.n.01", "name": "pallas's_sandgrouse"}, + {"id": 2604, "synset": "popinjay.n.02", "name": "popinjay"}, + {"id": 2605, "synset": "poll.n.04", "name": "poll"}, + {"id": 2606, "synset": "african_grey.n.01", "name": "African_grey"}, + {"id": 2607, "synset": "amazon.n.04", "name": "amazon"}, + {"id": 2608, "synset": "macaw.n.01", "name": "macaw"}, + {"id": 2609, "synset": "kea.n.01", "name": "kea"}, + {"id": 2610, "synset": "cockatoo.n.01", "name": "cockatoo"}, + {"id": 2611, "synset": "sulphur-crested_cockatoo.n.01", "name": "sulphur-crested_cockatoo"}, + {"id": 2612, "synset": "pink_cockatoo.n.01", "name": "pink_cockatoo"}, + {"id": 2613, "synset": "cockateel.n.01", "name": "cockateel"}, + {"id": 2614, "synset": "lovebird.n.02", "name": "lovebird"}, + {"id": 2615, "synset": "lory.n.01", "name": "lory"}, + {"id": 2616, "synset": "lorikeet.n.01", "name": "lorikeet"}, + {"id": 2617, "synset": "varied_lorikeet.n.01", "name": "varied_Lorikeet"}, + {"id": 2618, "synset": "rainbow_lorikeet.n.01", "name": "rainbow_lorikeet"}, + {"id": 2619, "synset": "carolina_parakeet.n.01", "name": "Carolina_parakeet"}, + {"id": 2620, "synset": "budgerigar.n.01", "name": "budgerigar"}, + {"id": 2621, "synset": "ring-necked_parakeet.n.01", "name": "ring-necked_parakeet"}, + {"id": 2622, "synset": "cuculiform_bird.n.01", "name": "cuculiform_bird"}, + {"id": 2623, "synset": "cuckoo.n.02", "name": "cuckoo"}, + {"id": 2624, "synset": "european_cuckoo.n.01", "name": "European_cuckoo"}, + {"id": 2625, "synset": "black-billed_cuckoo.n.01", "name": "black-billed_cuckoo"}, + {"id": 2626, "synset": "roadrunner.n.01", "name": "roadrunner"}, + {"id": 2627, "synset": "ani.n.01", "name": "ani"}, + {"id": 2628, "synset": "coucal.n.01", "name": "coucal"}, + {"id": 2629, "synset": "crow_pheasant.n.01", "name": "crow_pheasant"}, + {"id": 2630, "synset": "touraco.n.01", "name": "touraco"}, + {"id": 2631, "synset": "coraciiform_bird.n.01", "name": "coraciiform_bird"}, + {"id": 2632, "synset": "roller.n.06", "name": "roller"}, + {"id": 2633, "synset": "european_roller.n.01", "name": "European_roller"}, + {"id": 2634, "synset": "ground_roller.n.01", "name": "ground_roller"}, + {"id": 2635, "synset": "kingfisher.n.01", "name": "kingfisher"}, + {"id": 2636, "synset": "eurasian_kingfisher.n.01", "name": "Eurasian_kingfisher"}, + {"id": 2637, "synset": "belted_kingfisher.n.01", "name": "belted_kingfisher"}, + {"id": 2638, "synset": "kookaburra.n.01", "name": "kookaburra"}, + {"id": 2639, "synset": "bee_eater.n.01", "name": "bee_eater"}, + {"id": 2640, "synset": "hornbill.n.01", "name": "hornbill"}, + {"id": 2641, "synset": "hoopoe.n.01", "name": "hoopoe"}, + {"id": 2642, "synset": "euopean_hoopoe.n.01", "name": "Euopean_hoopoe"}, + {"id": 2643, "synset": "wood_hoopoe.n.01", "name": "wood_hoopoe"}, + {"id": 2644, "synset": "motmot.n.01", "name": "motmot"}, + {"id": 2645, "synset": "tody.n.01", "name": "tody"}, + {"id": 2646, "synset": "apodiform_bird.n.01", "name": "apodiform_bird"}, + {"id": 2647, "synset": "swift.n.03", "name": "swift"}, + {"id": 2648, "synset": "european_swift.n.01", "name": "European_swift"}, + {"id": 2649, "synset": "chimney_swift.n.01", "name": "chimney_swift"}, + {"id": 2650, "synset": "swiftlet.n.01", "name": "swiftlet"}, + {"id": 2651, "synset": "tree_swift.n.01", "name": "tree_swift"}, + {"id": 2652, "synset": "archilochus_colubris.n.01", "name": "Archilochus_colubris"}, + {"id": 2653, "synset": "thornbill.n.01", "name": "thornbill"}, + {"id": 2654, "synset": "goatsucker.n.01", "name": "goatsucker"}, + {"id": 2655, "synset": "european_goatsucker.n.01", "name": "European_goatsucker"}, + {"id": 2656, "synset": "chuck-will's-widow.n.01", "name": "chuck-will's-widow"}, + {"id": 2657, "synset": "whippoorwill.n.01", "name": "whippoorwill"}, + {"id": 2658, "synset": "poorwill.n.01", "name": "poorwill"}, + {"id": 2659, "synset": "frogmouth.n.01", "name": "frogmouth"}, + {"id": 2660, "synset": "oilbird.n.01", "name": "oilbird"}, + {"id": 2661, "synset": "piciform_bird.n.01", "name": "piciform_bird"}, + {"id": 2662, "synset": "woodpecker.n.01", "name": "woodpecker"}, + {"id": 2663, "synset": "green_woodpecker.n.01", "name": "green_woodpecker"}, + {"id": 2664, "synset": "downy_woodpecker.n.01", "name": "downy_woodpecker"}, + {"id": 2665, "synset": "flicker.n.02", "name": "flicker"}, + {"id": 2666, "synset": "yellow-shafted_flicker.n.01", "name": "yellow-shafted_flicker"}, + {"id": 2667, "synset": "gilded_flicker.n.01", "name": "gilded_flicker"}, + {"id": 2668, "synset": "red-shafted_flicker.n.01", "name": "red-shafted_flicker"}, + {"id": 2669, "synset": "ivorybill.n.01", "name": "ivorybill"}, + {"id": 2670, "synset": "redheaded_woodpecker.n.01", "name": "redheaded_woodpecker"}, + {"id": 2671, "synset": "sapsucker.n.01", "name": "sapsucker"}, + {"id": 2672, "synset": "yellow-bellied_sapsucker.n.01", "name": "yellow-bellied_sapsucker"}, + {"id": 2673, "synset": "red-breasted_sapsucker.n.01", "name": "red-breasted_sapsucker"}, + {"id": 2674, "synset": "wryneck.n.02", "name": "wryneck"}, + {"id": 2675, "synset": "piculet.n.01", "name": "piculet"}, + {"id": 2676, "synset": "barbet.n.01", "name": "barbet"}, + {"id": 2677, "synset": "puffbird.n.01", "name": "puffbird"}, + {"id": 2678, "synset": "honey_guide.n.01", "name": "honey_guide"}, + {"id": 2679, "synset": "jacamar.n.01", "name": "jacamar"}, + {"id": 2680, "synset": "toucan.n.01", "name": "toucan"}, + {"id": 2681, "synset": "toucanet.n.01", "name": "toucanet"}, + {"id": 2682, "synset": "trogon.n.01", "name": "trogon"}, + {"id": 2683, "synset": "quetzal.n.02", "name": "quetzal"}, + {"id": 2684, "synset": "resplendent_quetzel.n.01", "name": "resplendent_quetzel"}, + {"id": 2685, "synset": "aquatic_bird.n.01", "name": "aquatic_bird"}, + {"id": 2686, "synset": "waterfowl.n.01", "name": "waterfowl"}, + {"id": 2687, "synset": "anseriform_bird.n.01", "name": "anseriform_bird"}, + {"id": 2688, "synset": "drake.n.02", "name": "drake"}, + {"id": 2689, "synset": "quack-quack.n.01", "name": "quack-quack"}, + {"id": 2690, "synset": "diving_duck.n.01", "name": "diving_duck"}, + {"id": 2691, "synset": "dabbling_duck.n.01", "name": "dabbling_duck"}, + {"id": 2692, "synset": "black_duck.n.01", "name": "black_duck"}, + {"id": 2693, "synset": "teal.n.02", "name": "teal"}, + {"id": 2694, "synset": "greenwing.n.01", "name": "greenwing"}, + {"id": 2695, "synset": "bluewing.n.01", "name": "bluewing"}, + {"id": 2696, "synset": "garganey.n.01", "name": "garganey"}, + {"id": 2697, "synset": "widgeon.n.01", "name": "widgeon"}, + {"id": 2698, "synset": "american_widgeon.n.01", "name": "American_widgeon"}, + {"id": 2699, "synset": "shoveler.n.02", "name": "shoveler"}, + {"id": 2700, "synset": "pintail.n.01", "name": "pintail"}, + {"id": 2701, "synset": "sheldrake.n.02", "name": "sheldrake"}, + {"id": 2702, "synset": "shelduck.n.01", "name": "shelduck"}, + {"id": 2703, "synset": "ruddy_duck.n.01", "name": "ruddy_duck"}, + {"id": 2704, "synset": "bufflehead.n.01", "name": "bufflehead"}, + {"id": 2705, "synset": "goldeneye.n.02", "name": "goldeneye"}, + {"id": 2706, "synset": "barrow's_goldeneye.n.01", "name": "Barrow's_goldeneye"}, + {"id": 2707, "synset": "canvasback.n.01", "name": "canvasback"}, + {"id": 2708, "synset": "pochard.n.01", "name": "pochard"}, + {"id": 2709, "synset": "redhead.n.02", "name": "redhead"}, + {"id": 2710, "synset": "scaup.n.01", "name": "scaup"}, + {"id": 2711, "synset": "greater_scaup.n.01", "name": "greater_scaup"}, + {"id": 2712, "synset": "lesser_scaup.n.01", "name": "lesser_scaup"}, + {"id": 2713, "synset": "wild_duck.n.01", "name": "wild_duck"}, + {"id": 2714, "synset": "wood_duck.n.01", "name": "wood_duck"}, + {"id": 2715, "synset": "wood_drake.n.01", "name": "wood_drake"}, + {"id": 2716, "synset": "mandarin_duck.n.01", "name": "mandarin_duck"}, + {"id": 2717, "synset": "muscovy_duck.n.01", "name": "muscovy_duck"}, + {"id": 2718, "synset": "sea_duck.n.01", "name": "sea_duck"}, + {"id": 2719, "synset": "eider.n.01", "name": "eider"}, + {"id": 2720, "synset": "scoter.n.01", "name": "scoter"}, + {"id": 2721, "synset": "common_scoter.n.01", "name": "common_scoter"}, + {"id": 2722, "synset": "old_squaw.n.01", "name": "old_squaw"}, + {"id": 2723, "synset": "merganser.n.01", "name": "merganser"}, + {"id": 2724, "synset": "goosander.n.01", "name": "goosander"}, + {"id": 2725, "synset": "american_merganser.n.01", "name": "American_merganser"}, + {"id": 2726, "synset": "red-breasted_merganser.n.01", "name": "red-breasted_merganser"}, + {"id": 2727, "synset": "smew.n.01", "name": "smew"}, + {"id": 2728, "synset": "hooded_merganser.n.01", "name": "hooded_merganser"}, + {"id": 2729, "synset": "gosling.n.01", "name": "gosling"}, + {"id": 2730, "synset": "gander.n.01", "name": "gander"}, + {"id": 2731, "synset": "chinese_goose.n.01", "name": "Chinese_goose"}, + {"id": 2732, "synset": "greylag.n.01", "name": "greylag"}, + {"id": 2733, "synset": "blue_goose.n.01", "name": "blue_goose"}, + {"id": 2734, "synset": "snow_goose.n.01", "name": "snow_goose"}, + {"id": 2735, "synset": "brant.n.01", "name": "brant"}, + {"id": 2736, "synset": "common_brant_goose.n.01", "name": "common_brant_goose"}, + {"id": 2737, "synset": "honker.n.03", "name": "honker"}, + {"id": 2738, "synset": "barnacle_goose.n.01", "name": "barnacle_goose"}, + {"id": 2739, "synset": "coscoroba.n.01", "name": "coscoroba"}, + {"id": 2740, "synset": "swan.n.01", "name": "swan"}, + {"id": 2741, "synset": "cob.n.04", "name": "cob"}, + {"id": 2742, "synset": "pen.n.05", "name": "pen"}, + {"id": 2743, "synset": "cygnet.n.01", "name": "cygnet"}, + {"id": 2744, "synset": "mute_swan.n.01", "name": "mute_swan"}, + {"id": 2745, "synset": "whooper.n.02", "name": "whooper"}, + {"id": 2746, "synset": "tundra_swan.n.01", "name": "tundra_swan"}, + {"id": 2747, "synset": "whistling_swan.n.01", "name": "whistling_swan"}, + {"id": 2748, "synset": "bewick's_swan.n.01", "name": "Bewick's_swan"}, + {"id": 2749, "synset": "trumpeter.n.04", "name": "trumpeter"}, + {"id": 2750, "synset": "black_swan.n.01", "name": "black_swan"}, + {"id": 2751, "synset": "screamer.n.03", "name": "screamer"}, + {"id": 2752, "synset": "horned_screamer.n.01", "name": "horned_screamer"}, + {"id": 2753, "synset": "crested_screamer.n.01", "name": "crested_screamer"}, + {"id": 2754, "synset": "chaja.n.01", "name": "chaja"}, + {"id": 2755, "synset": "mammal.n.01", "name": "mammal"}, + {"id": 2756, "synset": "female_mammal.n.01", "name": "female_mammal"}, + {"id": 2757, "synset": "tusker.n.01", "name": "tusker"}, + {"id": 2758, "synset": "prototherian.n.01", "name": "prototherian"}, + {"id": 2759, "synset": "monotreme.n.01", "name": "monotreme"}, + {"id": 2760, "synset": "echidna.n.02", "name": "echidna"}, + {"id": 2761, "synset": "echidna.n.01", "name": "echidna"}, + {"id": 2762, "synset": "platypus.n.01", "name": "platypus"}, + {"id": 2763, "synset": "marsupial.n.01", "name": "marsupial"}, + {"id": 2764, "synset": "opossum.n.02", "name": "opossum"}, + {"id": 2765, "synset": "common_opossum.n.01", "name": "common_opossum"}, + {"id": 2766, "synset": "crab-eating_opossum.n.01", "name": "crab-eating_opossum"}, + {"id": 2767, "synset": "opossum_rat.n.01", "name": "opossum_rat"}, + {"id": 2768, "synset": "bandicoot.n.01", "name": "bandicoot"}, + {"id": 2769, "synset": "rabbit-eared_bandicoot.n.01", "name": "rabbit-eared_bandicoot"}, + {"id": 2770, "synset": "kangaroo.n.01", "name": "kangaroo"}, + {"id": 2771, "synset": "giant_kangaroo.n.01", "name": "giant_kangaroo"}, + {"id": 2772, "synset": "wallaby.n.01", "name": "wallaby"}, + {"id": 2773, "synset": "common_wallaby.n.01", "name": "common_wallaby"}, + {"id": 2774, "synset": "hare_wallaby.n.01", "name": "hare_wallaby"}, + {"id": 2775, "synset": "nail-tailed_wallaby.n.01", "name": "nail-tailed_wallaby"}, + {"id": 2776, "synset": "rock_wallaby.n.01", "name": "rock_wallaby"}, + {"id": 2777, "synset": "pademelon.n.01", "name": "pademelon"}, + {"id": 2778, "synset": "tree_wallaby.n.01", "name": "tree_wallaby"}, + {"id": 2779, "synset": "musk_kangaroo.n.01", "name": "musk_kangaroo"}, + {"id": 2780, "synset": "rat_kangaroo.n.01", "name": "rat_kangaroo"}, + {"id": 2781, "synset": "potoroo.n.01", "name": "potoroo"}, + {"id": 2782, "synset": "bettong.n.01", "name": "bettong"}, + {"id": 2783, "synset": "jerboa_kangaroo.n.01", "name": "jerboa_kangaroo"}, + {"id": 2784, "synset": "phalanger.n.01", "name": "phalanger"}, + {"id": 2785, "synset": "cuscus.n.01", "name": "cuscus"}, + {"id": 2786, "synset": "brush-tailed_phalanger.n.01", "name": "brush-tailed_phalanger"}, + {"id": 2787, "synset": "flying_phalanger.n.01", "name": "flying_phalanger"}, + {"id": 2788, "synset": "wombat.n.01", "name": "wombat"}, + {"id": 2789, "synset": "dasyurid_marsupial.n.01", "name": "dasyurid_marsupial"}, + {"id": 2790, "synset": "dasyure.n.01", "name": "dasyure"}, + {"id": 2791, "synset": "eastern_dasyure.n.01", "name": "eastern_dasyure"}, + {"id": 2792, "synset": "native_cat.n.01", "name": "native_cat"}, + {"id": 2793, "synset": "thylacine.n.01", "name": "thylacine"}, + {"id": 2794, "synset": "tasmanian_devil.n.01", "name": "Tasmanian_devil"}, + {"id": 2795, "synset": "pouched_mouse.n.01", "name": "pouched_mouse"}, + {"id": 2796, "synset": "numbat.n.01", "name": "numbat"}, + {"id": 2797, "synset": "pouched_mole.n.01", "name": "pouched_mole"}, + {"id": 2798, "synset": "placental.n.01", "name": "placental"}, + {"id": 2799, "synset": "livestock.n.01", "name": "livestock"}, + {"id": 2800, "synset": "cow.n.02", "name": "cow"}, + {"id": 2801, "synset": "calf.n.04", "name": "calf"}, + {"id": 2802, "synset": "yearling.n.03", "name": "yearling"}, + {"id": 2803, "synset": "buck.n.05", "name": "buck"}, + {"id": 2804, "synset": "doe.n.02", "name": "doe"}, + {"id": 2805, "synset": "insectivore.n.01", "name": "insectivore"}, + {"id": 2806, "synset": "mole.n.06", "name": "mole"}, + {"id": 2807, "synset": "starnose_mole.n.01", "name": "starnose_mole"}, + {"id": 2808, "synset": "brewer's_mole.n.01", "name": "brewer's_mole"}, + {"id": 2809, "synset": "golden_mole.n.01", "name": "golden_mole"}, + {"id": 2810, "synset": "shrew_mole.n.01", "name": "shrew_mole"}, + {"id": 2811, "synset": "asiatic_shrew_mole.n.01", "name": "Asiatic_shrew_mole"}, + {"id": 2812, "synset": "american_shrew_mole.n.01", "name": "American_shrew_mole"}, + {"id": 2813, "synset": "shrew.n.02", "name": "shrew"}, + {"id": 2814, "synset": "common_shrew.n.01", "name": "common_shrew"}, + {"id": 2815, "synset": "masked_shrew.n.01", "name": "masked_shrew"}, + {"id": 2816, "synset": "short-tailed_shrew.n.01", "name": "short-tailed_shrew"}, + {"id": 2817, "synset": "water_shrew.n.01", "name": "water_shrew"}, + {"id": 2818, "synset": "american_water_shrew.n.01", "name": "American_water_shrew"}, + {"id": 2819, "synset": "european_water_shrew.n.01", "name": "European_water_shrew"}, + {"id": 2820, "synset": "mediterranean_water_shrew.n.01", "name": "Mediterranean_water_shrew"}, + {"id": 2821, "synset": "least_shrew.n.01", "name": "least_shrew"}, + {"id": 2822, "synset": "hedgehog.n.02", "name": "hedgehog"}, + {"id": 2823, "synset": "tenrec.n.01", "name": "tenrec"}, + {"id": 2824, "synset": "tailless_tenrec.n.01", "name": "tailless_tenrec"}, + {"id": 2825, "synset": "otter_shrew.n.01", "name": "otter_shrew"}, + {"id": 2826, "synset": "eiderdown.n.02", "name": "eiderdown"}, + {"id": 2827, "synset": "aftershaft.n.01", "name": "aftershaft"}, + {"id": 2828, "synset": "sickle_feather.n.01", "name": "sickle_feather"}, + {"id": 2829, "synset": "contour_feather.n.01", "name": "contour_feather"}, + {"id": 2830, "synset": "bastard_wing.n.01", "name": "bastard_wing"}, + {"id": 2831, "synset": "saddle_hackle.n.01", "name": "saddle_hackle"}, + {"id": 2832, "synset": "encolure.n.01", "name": "encolure"}, + {"id": 2833, "synset": "hair.n.06", "name": "hair"}, + {"id": 2834, "synset": "squama.n.01", "name": "squama"}, + {"id": 2835, "synset": "scute.n.01", "name": "scute"}, + {"id": 2836, "synset": "sclerite.n.01", "name": "sclerite"}, + {"id": 2837, "synset": "plastron.n.05", "name": "plastron"}, + {"id": 2838, "synset": "scallop_shell.n.01", "name": "scallop_shell"}, + {"id": 2839, "synset": "oyster_shell.n.01", "name": "oyster_shell"}, + {"id": 2840, "synset": "theca.n.02", "name": "theca"}, + {"id": 2841, "synset": "invertebrate.n.01", "name": "invertebrate"}, + {"id": 2842, "synset": "sponge.n.04", "name": "sponge"}, + {"id": 2843, "synset": "choanocyte.n.01", "name": "choanocyte"}, + {"id": 2844, "synset": "glass_sponge.n.01", "name": "glass_sponge"}, + {"id": 2845, "synset": "venus's_flower_basket.n.01", "name": "Venus's_flower_basket"}, + {"id": 2846, "synset": "metazoan.n.01", "name": "metazoan"}, + {"id": 2847, "synset": "coelenterate.n.01", "name": "coelenterate"}, + {"id": 2848, "synset": "planula.n.01", "name": "planula"}, + {"id": 2849, "synset": "polyp.n.02", "name": "polyp"}, + {"id": 2850, "synset": "medusa.n.02", "name": "medusa"}, + {"id": 2851, "synset": "jellyfish.n.02", "name": "jellyfish"}, + {"id": 2852, "synset": "scyphozoan.n.01", "name": "scyphozoan"}, + {"id": 2853, "synset": "chrysaora_quinquecirrha.n.01", "name": "Chrysaora_quinquecirrha"}, + {"id": 2854, "synset": "hydrozoan.n.01", "name": "hydrozoan"}, + {"id": 2855, "synset": "hydra.n.04", "name": "hydra"}, + {"id": 2856, "synset": "siphonophore.n.01", "name": "siphonophore"}, + {"id": 2857, "synset": "nanomia.n.01", "name": "nanomia"}, + {"id": 2858, "synset": "portuguese_man-of-war.n.01", "name": "Portuguese_man-of-war"}, + {"id": 2859, "synset": "praya.n.01", "name": "praya"}, + {"id": 2860, "synset": "apolemia.n.01", "name": "apolemia"}, + {"id": 2861, "synset": "anthozoan.n.01", "name": "anthozoan"}, + {"id": 2862, "synset": "sea_anemone.n.01", "name": "sea_anemone"}, + {"id": 2863, "synset": "actinia.n.02", "name": "actinia"}, + {"id": 2864, "synset": "sea_pen.n.01", "name": "sea_pen"}, + {"id": 2865, "synset": "coral.n.04", "name": "coral"}, + {"id": 2866, "synset": "gorgonian.n.01", "name": "gorgonian"}, + {"id": 2867, "synset": "sea_feather.n.01", "name": "sea_feather"}, + {"id": 2868, "synset": "sea_fan.n.01", "name": "sea_fan"}, + {"id": 2869, "synset": "red_coral.n.02", "name": "red_coral"}, + {"id": 2870, "synset": "stony_coral.n.01", "name": "stony_coral"}, + {"id": 2871, "synset": "brain_coral.n.01", "name": "brain_coral"}, + {"id": 2872, "synset": "staghorn_coral.n.01", "name": "staghorn_coral"}, + {"id": 2873, "synset": "mushroom_coral.n.01", "name": "mushroom_coral"}, + {"id": 2874, "synset": "ctenophore.n.01", "name": "ctenophore"}, + {"id": 2875, "synset": "beroe.n.01", "name": "beroe"}, + {"id": 2876, "synset": "platyctenean.n.01", "name": "platyctenean"}, + {"id": 2877, "synset": "sea_gooseberry.n.01", "name": "sea_gooseberry"}, + {"id": 2878, "synset": "venus's_girdle.n.01", "name": "Venus's_girdle"}, + {"id": 2879, "synset": "worm.n.01", "name": "worm"}, + {"id": 2880, "synset": "helminth.n.01", "name": "helminth"}, + {"id": 2881, "synset": "woodworm.n.01", "name": "woodworm"}, + {"id": 2882, "synset": "woodborer.n.01", "name": "woodborer"}, + {"id": 2883, "synset": "acanthocephalan.n.01", "name": "acanthocephalan"}, + {"id": 2884, "synset": "arrowworm.n.01", "name": "arrowworm"}, + {"id": 2885, "synset": "bladder_worm.n.01", "name": "bladder_worm"}, + {"id": 2886, "synset": "flatworm.n.01", "name": "flatworm"}, + {"id": 2887, "synset": "planarian.n.01", "name": "planarian"}, + {"id": 2888, "synset": "fluke.n.05", "name": "fluke"}, + {"id": 2889, "synset": "cercaria.n.01", "name": "cercaria"}, + {"id": 2890, "synset": "liver_fluke.n.01", "name": "liver_fluke"}, + {"id": 2891, "synset": "fasciolopsis_buski.n.01", "name": "Fasciolopsis_buski"}, + {"id": 2892, "synset": "schistosome.n.01", "name": "schistosome"}, + {"id": 2893, "synset": "tapeworm.n.01", "name": "tapeworm"}, + {"id": 2894, "synset": "echinococcus.n.01", "name": "echinococcus"}, + {"id": 2895, "synset": "taenia.n.02", "name": "taenia"}, + {"id": 2896, "synset": "ribbon_worm.n.01", "name": "ribbon_worm"}, + {"id": 2897, "synset": "beard_worm.n.01", "name": "beard_worm"}, + {"id": 2898, "synset": "rotifer.n.01", "name": "rotifer"}, + {"id": 2899, "synset": "nematode.n.01", "name": "nematode"}, + {"id": 2900, "synset": "common_roundworm.n.01", "name": "common_roundworm"}, + {"id": 2901, "synset": "chicken_roundworm.n.01", "name": "chicken_roundworm"}, + {"id": 2902, "synset": "pinworm.n.01", "name": "pinworm"}, + {"id": 2903, "synset": "eelworm.n.01", "name": "eelworm"}, + {"id": 2904, "synset": "vinegar_eel.n.01", "name": "vinegar_eel"}, + {"id": 2905, "synset": "trichina.n.01", "name": "trichina"}, + {"id": 2906, "synset": "hookworm.n.01", "name": "hookworm"}, + {"id": 2907, "synset": "filaria.n.02", "name": "filaria"}, + {"id": 2908, "synset": "guinea_worm.n.02", "name": "Guinea_worm"}, + {"id": 2909, "synset": "annelid.n.01", "name": "annelid"}, + {"id": 2910, "synset": "archiannelid.n.01", "name": "archiannelid"}, + {"id": 2911, "synset": "oligochaete.n.01", "name": "oligochaete"}, + {"id": 2912, "synset": "earthworm.n.01", "name": "earthworm"}, + {"id": 2913, "synset": "polychaete.n.01", "name": "polychaete"}, + {"id": 2914, "synset": "lugworm.n.01", "name": "lugworm"}, + {"id": 2915, "synset": "sea_mouse.n.01", "name": "sea_mouse"}, + {"id": 2916, "synset": "bloodworm.n.01", "name": "bloodworm"}, + {"id": 2917, "synset": "leech.n.01", "name": "leech"}, + {"id": 2918, "synset": "medicinal_leech.n.01", "name": "medicinal_leech"}, + {"id": 2919, "synset": "horseleech.n.01", "name": "horseleech"}, + {"id": 2920, "synset": "mollusk.n.01", "name": "mollusk"}, + {"id": 2921, "synset": "scaphopod.n.01", "name": "scaphopod"}, + {"id": 2922, "synset": "tooth_shell.n.01", "name": "tooth_shell"}, + {"id": 2923, "synset": "gastropod.n.01", "name": "gastropod"}, + {"id": 2924, "synset": "abalone.n.01", "name": "abalone"}, + {"id": 2925, "synset": "ormer.n.01", "name": "ormer"}, + {"id": 2926, "synset": "scorpion_shell.n.01", "name": "scorpion_shell"}, + {"id": 2927, "synset": "conch.n.01", "name": "conch"}, + {"id": 2928, "synset": "giant_conch.n.01", "name": "giant_conch"}, + {"id": 2929, "synset": "snail.n.01", "name": "snail"}, + {"id": 2930, "synset": "edible_snail.n.01", "name": "edible_snail"}, + {"id": 2931, "synset": "garden_snail.n.01", "name": "garden_snail"}, + {"id": 2932, "synset": "brown_snail.n.01", "name": "brown_snail"}, + {"id": 2933, "synset": "helix_hortensis.n.01", "name": "Helix_hortensis"}, + {"id": 2934, "synset": "slug.n.07", "name": "slug"}, + {"id": 2935, "synset": "seasnail.n.02", "name": "seasnail"}, + {"id": 2936, "synset": "neritid.n.01", "name": "neritid"}, + {"id": 2937, "synset": "nerita.n.01", "name": "nerita"}, + {"id": 2938, "synset": "bleeding_tooth.n.01", "name": "bleeding_tooth"}, + {"id": 2939, "synset": "neritina.n.01", "name": "neritina"}, + {"id": 2940, "synset": "whelk.n.02", "name": "whelk"}, + {"id": 2941, "synset": "moon_shell.n.01", "name": "moon_shell"}, + {"id": 2942, "synset": "periwinkle.n.04", "name": "periwinkle"}, + {"id": 2943, "synset": "limpet.n.02", "name": "limpet"}, + {"id": 2944, "synset": "common_limpet.n.01", "name": "common_limpet"}, + {"id": 2945, "synset": "keyhole_limpet.n.01", "name": "keyhole_limpet"}, + {"id": 2946, "synset": "river_limpet.n.01", "name": "river_limpet"}, + {"id": 2947, "synset": "sea_slug.n.01", "name": "sea_slug"}, + {"id": 2948, "synset": "sea_hare.n.01", "name": "sea_hare"}, + {"id": 2949, "synset": "hermissenda_crassicornis.n.01", "name": "Hermissenda_crassicornis"}, + {"id": 2950, "synset": "bubble_shell.n.01", "name": "bubble_shell"}, + {"id": 2951, "synset": "physa.n.01", "name": "physa"}, + {"id": 2952, "synset": "cowrie.n.01", "name": "cowrie"}, + {"id": 2953, "synset": "money_cowrie.n.01", "name": "money_cowrie"}, + {"id": 2954, "synset": "tiger_cowrie.n.01", "name": "tiger_cowrie"}, + {"id": 2955, "synset": "solenogaster.n.01", "name": "solenogaster"}, + {"id": 2956, "synset": "chiton.n.02", "name": "chiton"}, + {"id": 2957, "synset": "bivalve.n.01", "name": "bivalve"}, + {"id": 2958, "synset": "spat.n.03", "name": "spat"}, + {"id": 2959, "synset": "clam.n.01", "name": "clam"}, + {"id": 2960, "synset": "soft-shell_clam.n.02", "name": "soft-shell_clam"}, + {"id": 2961, "synset": "quahog.n.02", "name": "quahog"}, + {"id": 2962, "synset": "littleneck.n.02", "name": "littleneck"}, + {"id": 2963, "synset": "cherrystone.n.02", "name": "cherrystone"}, + {"id": 2964, "synset": "geoduck.n.01", "name": "geoduck"}, + {"id": 2965, "synset": "razor_clam.n.01", "name": "razor_clam"}, + {"id": 2966, "synset": "giant_clam.n.01", "name": "giant_clam"}, + {"id": 2967, "synset": "cockle.n.02", "name": "cockle"}, + {"id": 2968, "synset": "edible_cockle.n.01", "name": "edible_cockle"}, + {"id": 2969, "synset": "oyster.n.01", "name": "oyster"}, + {"id": 2970, "synset": "japanese_oyster.n.01", "name": "Japanese_oyster"}, + {"id": 2971, "synset": "virginia_oyster.n.01", "name": "Virginia_oyster"}, + {"id": 2972, "synset": "pearl_oyster.n.01", "name": "pearl_oyster"}, + {"id": 2973, "synset": "saddle_oyster.n.01", "name": "saddle_oyster"}, + {"id": 2974, "synset": "window_oyster.n.01", "name": "window_oyster"}, + {"id": 2975, "synset": "ark_shell.n.01", "name": "ark_shell"}, + {"id": 2976, "synset": "blood_clam.n.01", "name": "blood_clam"}, + {"id": 2977, "synset": "mussel.n.02", "name": "mussel"}, + {"id": 2978, "synset": "marine_mussel.n.01", "name": "marine_mussel"}, + {"id": 2979, "synset": "edible_mussel.n.01", "name": "edible_mussel"}, + {"id": 2980, "synset": "freshwater_mussel.n.01", "name": "freshwater_mussel"}, + {"id": 2981, "synset": "pearly-shelled_mussel.n.01", "name": "pearly-shelled_mussel"}, + {"id": 2982, "synset": "thin-shelled_mussel.n.01", "name": "thin-shelled_mussel"}, + {"id": 2983, "synset": "zebra_mussel.n.01", "name": "zebra_mussel"}, + {"id": 2984, "synset": "scallop.n.04", "name": "scallop"}, + {"id": 2985, "synset": "bay_scallop.n.02", "name": "bay_scallop"}, + {"id": 2986, "synset": "sea_scallop.n.02", "name": "sea_scallop"}, + {"id": 2987, "synset": "shipworm.n.01", "name": "shipworm"}, + {"id": 2988, "synset": "teredo.n.01", "name": "teredo"}, + {"id": 2989, "synset": "piddock.n.01", "name": "piddock"}, + {"id": 2990, "synset": "cephalopod.n.01", "name": "cephalopod"}, + {"id": 2991, "synset": "chambered_nautilus.n.01", "name": "chambered_nautilus"}, + {"id": 2992, "synset": "octopod.n.01", "name": "octopod"}, + {"id": 2993, "synset": "paper_nautilus.n.01", "name": "paper_nautilus"}, + {"id": 2994, "synset": "decapod.n.02", "name": "decapod"}, + {"id": 2995, "synset": "squid.n.02", "name": "squid"}, + {"id": 2996, "synset": "loligo.n.01", "name": "loligo"}, + {"id": 2997, "synset": "ommastrephes.n.01", "name": "ommastrephes"}, + {"id": 2998, "synset": "architeuthis.n.01", "name": "architeuthis"}, + {"id": 2999, "synset": "cuttlefish.n.01", "name": "cuttlefish"}, + {"id": 3000, "synset": "spirula.n.01", "name": "spirula"}, + {"id": 3001, "synset": "crustacean.n.01", "name": "crustacean"}, + {"id": 3002, "synset": "malacostracan_crustacean.n.01", "name": "malacostracan_crustacean"}, + {"id": 3003, "synset": "decapod_crustacean.n.01", "name": "decapod_crustacean"}, + {"id": 3004, "synset": "brachyuran.n.01", "name": "brachyuran"}, + {"id": 3005, "synset": "stone_crab.n.02", "name": "stone_crab"}, + {"id": 3006, "synset": "hard-shell_crab.n.01", "name": "hard-shell_crab"}, + {"id": 3007, "synset": "soft-shell_crab.n.02", "name": "soft-shell_crab"}, + {"id": 3008, "synset": "dungeness_crab.n.02", "name": "Dungeness_crab"}, + {"id": 3009, "synset": "rock_crab.n.01", "name": "rock_crab"}, + {"id": 3010, "synset": "jonah_crab.n.01", "name": "Jonah_crab"}, + {"id": 3011, "synset": "swimming_crab.n.01", "name": "swimming_crab"}, + {"id": 3012, "synset": "english_lady_crab.n.01", "name": "English_lady_crab"}, + {"id": 3013, "synset": "american_lady_crab.n.01", "name": "American_lady_crab"}, + {"id": 3014, "synset": "blue_crab.n.02", "name": "blue_crab"}, + {"id": 3015, "synset": "fiddler_crab.n.01", "name": "fiddler_crab"}, + {"id": 3016, "synset": "pea_crab.n.01", "name": "pea_crab"}, + {"id": 3017, "synset": "king_crab.n.03", "name": "king_crab"}, + {"id": 3018, "synset": "spider_crab.n.01", "name": "spider_crab"}, + {"id": 3019, "synset": "european_spider_crab.n.01", "name": "European_spider_crab"}, + {"id": 3020, "synset": "giant_crab.n.01", "name": "giant_crab"}, + {"id": 3021, "synset": "lobster.n.02", "name": "lobster"}, + {"id": 3022, "synset": "true_lobster.n.01", "name": "true_lobster"}, + {"id": 3023, "synset": "american_lobster.n.02", "name": "American_lobster"}, + {"id": 3024, "synset": "european_lobster.n.02", "name": "European_lobster"}, + {"id": 3025, "synset": "cape_lobster.n.01", "name": "Cape_lobster"}, + {"id": 3026, "synset": "norway_lobster.n.01", "name": "Norway_lobster"}, + {"id": 3027, "synset": "crayfish.n.03", "name": "crayfish"}, + {"id": 3028, "synset": "old_world_crayfish.n.01", "name": "Old_World_crayfish"}, + {"id": 3029, "synset": "american_crayfish.n.01", "name": "American_crayfish"}, + {"id": 3030, "synset": "hermit_crab.n.01", "name": "hermit_crab"}, + {"id": 3031, "synset": "shrimp.n.03", "name": "shrimp"}, + {"id": 3032, "synset": "snapping_shrimp.n.01", "name": "snapping_shrimp"}, + {"id": 3033, "synset": "prawn.n.02", "name": "prawn"}, + {"id": 3034, "synset": "long-clawed_prawn.n.01", "name": "long-clawed_prawn"}, + {"id": 3035, "synset": "tropical_prawn.n.01", "name": "tropical_prawn"}, + {"id": 3036, "synset": "krill.n.01", "name": "krill"}, + {"id": 3037, "synset": "euphausia_pacifica.n.01", "name": "Euphausia_pacifica"}, + {"id": 3038, "synset": "opossum_shrimp.n.01", "name": "opossum_shrimp"}, + {"id": 3039, "synset": "stomatopod.n.01", "name": "stomatopod"}, + {"id": 3040, "synset": "mantis_shrimp.n.01", "name": "mantis_shrimp"}, + {"id": 3041, "synset": "squilla.n.01", "name": "squilla"}, + {"id": 3042, "synset": "isopod.n.01", "name": "isopod"}, + {"id": 3043, "synset": "woodlouse.n.01", "name": "woodlouse"}, + {"id": 3044, "synset": "pill_bug.n.01", "name": "pill_bug"}, + {"id": 3045, "synset": "sow_bug.n.01", "name": "sow_bug"}, + {"id": 3046, "synset": "sea_louse.n.01", "name": "sea_louse"}, + {"id": 3047, "synset": "amphipod.n.01", "name": "amphipod"}, + {"id": 3048, "synset": "skeleton_shrimp.n.01", "name": "skeleton_shrimp"}, + {"id": 3049, "synset": "whale_louse.n.01", "name": "whale_louse"}, + {"id": 3050, "synset": "daphnia.n.01", "name": "daphnia"}, + {"id": 3051, "synset": "fairy_shrimp.n.01", "name": "fairy_shrimp"}, + {"id": 3052, "synset": "brine_shrimp.n.01", "name": "brine_shrimp"}, + {"id": 3053, "synset": "tadpole_shrimp.n.01", "name": "tadpole_shrimp"}, + {"id": 3054, "synset": "copepod.n.01", "name": "copepod"}, + {"id": 3055, "synset": "cyclops.n.02", "name": "cyclops"}, + {"id": 3056, "synset": "seed_shrimp.n.01", "name": "seed_shrimp"}, + {"id": 3057, "synset": "barnacle.n.01", "name": "barnacle"}, + {"id": 3058, "synset": "acorn_barnacle.n.01", "name": "acorn_barnacle"}, + {"id": 3059, "synset": "goose_barnacle.n.01", "name": "goose_barnacle"}, + {"id": 3060, "synset": "onychophoran.n.01", "name": "onychophoran"}, + {"id": 3061, "synset": "wading_bird.n.01", "name": "wading_bird"}, + {"id": 3062, "synset": "stork.n.01", "name": "stork"}, + {"id": 3063, "synset": "white_stork.n.01", "name": "white_stork"}, + {"id": 3064, "synset": "black_stork.n.01", "name": "black_stork"}, + {"id": 3065, "synset": "adjutant_bird.n.01", "name": "adjutant_bird"}, + {"id": 3066, "synset": "marabou.n.01", "name": "marabou"}, + {"id": 3067, "synset": "openbill.n.01", "name": "openbill"}, + {"id": 3068, "synset": "jabiru.n.03", "name": "jabiru"}, + {"id": 3069, "synset": "saddlebill.n.01", "name": "saddlebill"}, + {"id": 3070, "synset": "policeman_bird.n.01", "name": "policeman_bird"}, + {"id": 3071, "synset": "wood_ibis.n.02", "name": "wood_ibis"}, + {"id": 3072, "synset": "shoebill.n.01", "name": "shoebill"}, + {"id": 3073, "synset": "ibis.n.01", "name": "ibis"}, + {"id": 3074, "synset": "wood_ibis.n.01", "name": "wood_ibis"}, + {"id": 3075, "synset": "sacred_ibis.n.01", "name": "sacred_ibis"}, + {"id": 3076, "synset": "spoonbill.n.01", "name": "spoonbill"}, + {"id": 3077, "synset": "common_spoonbill.n.01", "name": "common_spoonbill"}, + {"id": 3078, "synset": "roseate_spoonbill.n.01", "name": "roseate_spoonbill"}, + {"id": 3079, "synset": "great_blue_heron.n.01", "name": "great_blue_heron"}, + {"id": 3080, "synset": "great_white_heron.n.03", "name": "great_white_heron"}, + {"id": 3081, "synset": "egret.n.01", "name": "egret"}, + {"id": 3082, "synset": "little_blue_heron.n.01", "name": "little_blue_heron"}, + {"id": 3083, "synset": "snowy_egret.n.01", "name": "snowy_egret"}, + {"id": 3084, "synset": "little_egret.n.01", "name": "little_egret"}, + {"id": 3085, "synset": "great_white_heron.n.02", "name": "great_white_heron"}, + {"id": 3086, "synset": "american_egret.n.01", "name": "American_egret"}, + {"id": 3087, "synset": "cattle_egret.n.01", "name": "cattle_egret"}, + {"id": 3088, "synset": "night_heron.n.01", "name": "night_heron"}, + {"id": 3089, "synset": "black-crowned_night_heron.n.01", "name": "black-crowned_night_heron"}, + {"id": 3090, "synset": "yellow-crowned_night_heron.n.01", "name": "yellow-crowned_night_heron"}, + {"id": 3091, "synset": "boatbill.n.01", "name": "boatbill"}, + {"id": 3092, "synset": "bittern.n.01", "name": "bittern"}, + {"id": 3093, "synset": "american_bittern.n.01", "name": "American_bittern"}, + {"id": 3094, "synset": "european_bittern.n.01", "name": "European_bittern"}, + {"id": 3095, "synset": "least_bittern.n.01", "name": "least_bittern"}, + {"id": 3096, "synset": "crane.n.05", "name": "crane"}, + {"id": 3097, "synset": "whooping_crane.n.01", "name": "whooping_crane"}, + {"id": 3098, "synset": "courlan.n.01", "name": "courlan"}, + {"id": 3099, "synset": "limpkin.n.01", "name": "limpkin"}, + {"id": 3100, "synset": "crested_cariama.n.01", "name": "crested_cariama"}, + {"id": 3101, "synset": "chunga.n.01", "name": "chunga"}, + {"id": 3102, "synset": "rail.n.05", "name": "rail"}, + {"id": 3103, "synset": "weka.n.01", "name": "weka"}, + {"id": 3104, "synset": "crake.n.01", "name": "crake"}, + {"id": 3105, "synset": "corncrake.n.01", "name": "corncrake"}, + {"id": 3106, "synset": "spotted_crake.n.01", "name": "spotted_crake"}, + {"id": 3107, "synset": "gallinule.n.01", "name": "gallinule"}, + {"id": 3108, "synset": "florida_gallinule.n.01", "name": "Florida_gallinule"}, + {"id": 3109, "synset": "moorhen.n.01", "name": "moorhen"}, + {"id": 3110, "synset": "purple_gallinule.n.01", "name": "purple_gallinule"}, + {"id": 3111, "synset": "european_gallinule.n.01", "name": "European_gallinule"}, + {"id": 3112, "synset": "american_gallinule.n.01", "name": "American_gallinule"}, + {"id": 3113, "synset": "notornis.n.01", "name": "notornis"}, + {"id": 3114, "synset": "coot.n.01", "name": "coot"}, + {"id": 3115, "synset": "american_coot.n.01", "name": "American_coot"}, + {"id": 3116, "synset": "old_world_coot.n.01", "name": "Old_World_coot"}, + {"id": 3117, "synset": "bustard.n.01", "name": "bustard"}, + {"id": 3118, "synset": "great_bustard.n.01", "name": "great_bustard"}, + {"id": 3119, "synset": "plain_turkey.n.01", "name": "plain_turkey"}, + {"id": 3120, "synset": "button_quail.n.01", "name": "button_quail"}, + {"id": 3121, "synset": "striped_button_quail.n.01", "name": "striped_button_quail"}, + {"id": 3122, "synset": "plain_wanderer.n.01", "name": "plain_wanderer"}, + {"id": 3123, "synset": "trumpeter.n.03", "name": "trumpeter"}, + {"id": 3124, "synset": "brazilian_trumpeter.n.01", "name": "Brazilian_trumpeter"}, + {"id": 3125, "synset": "shorebird.n.01", "name": "shorebird"}, + {"id": 3126, "synset": "plover.n.01", "name": "plover"}, + {"id": 3127, "synset": "piping_plover.n.01", "name": "piping_plover"}, + {"id": 3128, "synset": "killdeer.n.01", "name": "killdeer"}, + {"id": 3129, "synset": "dotterel.n.01", "name": "dotterel"}, + {"id": 3130, "synset": "golden_plover.n.01", "name": "golden_plover"}, + {"id": 3131, "synset": "lapwing.n.01", "name": "lapwing"}, + {"id": 3132, "synset": "turnstone.n.01", "name": "turnstone"}, + {"id": 3133, "synset": "ruddy_turnstone.n.01", "name": "ruddy_turnstone"}, + {"id": 3134, "synset": "black_turnstone.n.01", "name": "black_turnstone"}, + {"id": 3135, "synset": "sandpiper.n.01", "name": "sandpiper"}, + {"id": 3136, "synset": "surfbird.n.01", "name": "surfbird"}, + {"id": 3137, "synset": "european_sandpiper.n.01", "name": "European_sandpiper"}, + {"id": 3138, "synset": "spotted_sandpiper.n.01", "name": "spotted_sandpiper"}, + {"id": 3139, "synset": "least_sandpiper.n.01", "name": "least_sandpiper"}, + {"id": 3140, "synset": "red-backed_sandpiper.n.01", "name": "red-backed_sandpiper"}, + {"id": 3141, "synset": "greenshank.n.01", "name": "greenshank"}, + {"id": 3142, "synset": "redshank.n.01", "name": "redshank"}, + {"id": 3143, "synset": "yellowlegs.n.01", "name": "yellowlegs"}, + {"id": 3144, "synset": "greater_yellowlegs.n.01", "name": "greater_yellowlegs"}, + {"id": 3145, "synset": "lesser_yellowlegs.n.01", "name": "lesser_yellowlegs"}, + {"id": 3146, "synset": "pectoral_sandpiper.n.01", "name": "pectoral_sandpiper"}, + {"id": 3147, "synset": "knot.n.07", "name": "knot"}, + {"id": 3148, "synset": "curlew_sandpiper.n.01", "name": "curlew_sandpiper"}, + {"id": 3149, "synset": "sanderling.n.01", "name": "sanderling"}, + {"id": 3150, "synset": "upland_sandpiper.n.01", "name": "upland_sandpiper"}, + {"id": 3151, "synset": "ruff.n.03", "name": "ruff"}, + {"id": 3152, "synset": "reeve.n.01", "name": "reeve"}, + {"id": 3153, "synset": "tattler.n.02", "name": "tattler"}, + {"id": 3154, "synset": "polynesian_tattler.n.01", "name": "Polynesian_tattler"}, + {"id": 3155, "synset": "willet.n.01", "name": "willet"}, + {"id": 3156, "synset": "woodcock.n.01", "name": "woodcock"}, + {"id": 3157, "synset": "eurasian_woodcock.n.01", "name": "Eurasian_woodcock"}, + {"id": 3158, "synset": "american_woodcock.n.01", "name": "American_woodcock"}, + {"id": 3159, "synset": "snipe.n.01", "name": "snipe"}, + {"id": 3160, "synset": "whole_snipe.n.01", "name": "whole_snipe"}, + {"id": 3161, "synset": "wilson's_snipe.n.01", "name": "Wilson's_snipe"}, + {"id": 3162, "synset": "great_snipe.n.01", "name": "great_snipe"}, + {"id": 3163, "synset": "jacksnipe.n.01", "name": "jacksnipe"}, + {"id": 3164, "synset": "dowitcher.n.01", "name": "dowitcher"}, + {"id": 3165, "synset": "greyback.n.02", "name": "greyback"}, + {"id": 3166, "synset": "red-breasted_snipe.n.01", "name": "red-breasted_snipe"}, + {"id": 3167, "synset": "curlew.n.01", "name": "curlew"}, + {"id": 3168, "synset": "european_curlew.n.01", "name": "European_curlew"}, + {"id": 3169, "synset": "eskimo_curlew.n.01", "name": "Eskimo_curlew"}, + {"id": 3170, "synset": "godwit.n.01", "name": "godwit"}, + {"id": 3171, "synset": "hudsonian_godwit.n.01", "name": "Hudsonian_godwit"}, + {"id": 3172, "synset": "stilt.n.04", "name": "stilt"}, + {"id": 3173, "synset": "black-necked_stilt.n.01", "name": "black-necked_stilt"}, + {"id": 3174, "synset": "black-winged_stilt.n.01", "name": "black-winged_stilt"}, + {"id": 3175, "synset": "white-headed_stilt.n.01", "name": "white-headed_stilt"}, + {"id": 3176, "synset": "kaki.n.02", "name": "kaki"}, + {"id": 3177, "synset": "stilt.n.03", "name": "stilt"}, + {"id": 3178, "synset": "banded_stilt.n.01", "name": "banded_stilt"}, + {"id": 3179, "synset": "avocet.n.01", "name": "avocet"}, + {"id": 3180, "synset": "oystercatcher.n.01", "name": "oystercatcher"}, + {"id": 3181, "synset": "phalarope.n.01", "name": "phalarope"}, + {"id": 3182, "synset": "red_phalarope.n.01", "name": "red_phalarope"}, + {"id": 3183, "synset": "northern_phalarope.n.01", "name": "northern_phalarope"}, + {"id": 3184, "synset": "wilson's_phalarope.n.01", "name": "Wilson's_phalarope"}, + {"id": 3185, "synset": "pratincole.n.01", "name": "pratincole"}, + {"id": 3186, "synset": "courser.n.04", "name": "courser"}, + {"id": 3187, "synset": "cream-colored_courser.n.01", "name": "cream-colored_courser"}, + {"id": 3188, "synset": "crocodile_bird.n.01", "name": "crocodile_bird"}, + {"id": 3189, "synset": "stone_curlew.n.01", "name": "stone_curlew"}, + {"id": 3190, "synset": "coastal_diving_bird.n.01", "name": "coastal_diving_bird"}, + {"id": 3191, "synset": "larid.n.01", "name": "larid"}, + {"id": 3192, "synset": "mew.n.02", "name": "mew"}, + {"id": 3193, "synset": "black-backed_gull.n.01", "name": "black-backed_gull"}, + {"id": 3194, "synset": "herring_gull.n.01", "name": "herring_gull"}, + {"id": 3195, "synset": "laughing_gull.n.01", "name": "laughing_gull"}, + {"id": 3196, "synset": "ivory_gull.n.01", "name": "ivory_gull"}, + {"id": 3197, "synset": "kittiwake.n.01", "name": "kittiwake"}, + {"id": 3198, "synset": "tern.n.01", "name": "tern"}, + {"id": 3199, "synset": "sea_swallow.n.01", "name": "sea_swallow"}, + {"id": 3200, "synset": "skimmer.n.04", "name": "skimmer"}, + {"id": 3201, "synset": "jaeger.n.01", "name": "jaeger"}, + {"id": 3202, "synset": "parasitic_jaeger.n.01", "name": "parasitic_jaeger"}, + {"id": 3203, "synset": "skua.n.01", "name": "skua"}, + {"id": 3204, "synset": "great_skua.n.01", "name": "great_skua"}, + {"id": 3205, "synset": "auk.n.01", "name": "auk"}, + {"id": 3206, "synset": "auklet.n.01", "name": "auklet"}, + {"id": 3207, "synset": "razorbill.n.01", "name": "razorbill"}, + {"id": 3208, "synset": "little_auk.n.01", "name": "little_auk"}, + {"id": 3209, "synset": "guillemot.n.01", "name": "guillemot"}, + {"id": 3210, "synset": "black_guillemot.n.01", "name": "black_guillemot"}, + {"id": 3211, "synset": "pigeon_guillemot.n.01", "name": "pigeon_guillemot"}, + {"id": 3212, "synset": "murre.n.01", "name": "murre"}, + {"id": 3213, "synset": "common_murre.n.01", "name": "common_murre"}, + {"id": 3214, "synset": "thick-billed_murre.n.01", "name": "thick-billed_murre"}, + {"id": 3215, "synset": "atlantic_puffin.n.01", "name": "Atlantic_puffin"}, + {"id": 3216, "synset": "horned_puffin.n.01", "name": "horned_puffin"}, + {"id": 3217, "synset": "tufted_puffin.n.01", "name": "tufted_puffin"}, + {"id": 3218, "synset": "gaviiform_seabird.n.01", "name": "gaviiform_seabird"}, + {"id": 3219, "synset": "loon.n.02", "name": "loon"}, + {"id": 3220, "synset": "podicipitiform_seabird.n.01", "name": "podicipitiform_seabird"}, + {"id": 3221, "synset": "grebe.n.01", "name": "grebe"}, + {"id": 3222, "synset": "great_crested_grebe.n.01", "name": "great_crested_grebe"}, + {"id": 3223, "synset": "red-necked_grebe.n.01", "name": "red-necked_grebe"}, + {"id": 3224, "synset": "black-necked_grebe.n.01", "name": "black-necked_grebe"}, + {"id": 3225, "synset": "dabchick.n.01", "name": "dabchick"}, + {"id": 3226, "synset": "pied-billed_grebe.n.01", "name": "pied-billed_grebe"}, + {"id": 3227, "synset": "pelecaniform_seabird.n.01", "name": "pelecaniform_seabird"}, + {"id": 3228, "synset": "white_pelican.n.01", "name": "white_pelican"}, + {"id": 3229, "synset": "old_world_white_pelican.n.01", "name": "Old_world_white_pelican"}, + {"id": 3230, "synset": "frigate_bird.n.01", "name": "frigate_bird"}, + {"id": 3231, "synset": "gannet.n.01", "name": "gannet"}, + {"id": 3232, "synset": "solan.n.01", "name": "solan"}, + {"id": 3233, "synset": "booby.n.02", "name": "booby"}, + {"id": 3234, "synset": "cormorant.n.01", "name": "cormorant"}, + {"id": 3235, "synset": "snakebird.n.01", "name": "snakebird"}, + {"id": 3236, "synset": "water_turkey.n.01", "name": "water_turkey"}, + {"id": 3237, "synset": "tropic_bird.n.01", "name": "tropic_bird"}, + {"id": 3238, "synset": "sphenisciform_seabird.n.01", "name": "sphenisciform_seabird"}, + {"id": 3239, "synset": "adelie.n.01", "name": "Adelie"}, + {"id": 3240, "synset": "king_penguin.n.01", "name": "king_penguin"}, + {"id": 3241, "synset": "emperor_penguin.n.01", "name": "emperor_penguin"}, + {"id": 3242, "synset": "jackass_penguin.n.01", "name": "jackass_penguin"}, + {"id": 3243, "synset": "rock_hopper.n.01", "name": "rock_hopper"}, + {"id": 3244, "synset": "pelagic_bird.n.01", "name": "pelagic_bird"}, + {"id": 3245, "synset": "procellariiform_seabird.n.01", "name": "procellariiform_seabird"}, + {"id": 3246, "synset": "albatross.n.02", "name": "albatross"}, + {"id": 3247, "synset": "wandering_albatross.n.01", "name": "wandering_albatross"}, + {"id": 3248, "synset": "black-footed_albatross.n.01", "name": "black-footed_albatross"}, + {"id": 3249, "synset": "petrel.n.01", "name": "petrel"}, + {"id": 3250, "synset": "white-chinned_petrel.n.01", "name": "white-chinned_petrel"}, + {"id": 3251, "synset": "giant_petrel.n.01", "name": "giant_petrel"}, + {"id": 3252, "synset": "fulmar.n.01", "name": "fulmar"}, + {"id": 3253, "synset": "shearwater.n.01", "name": "shearwater"}, + {"id": 3254, "synset": "manx_shearwater.n.01", "name": "Manx_shearwater"}, + {"id": 3255, "synset": "storm_petrel.n.01", "name": "storm_petrel"}, + {"id": 3256, "synset": "stormy_petrel.n.01", "name": "stormy_petrel"}, + {"id": 3257, "synset": "mother_carey's_chicken.n.01", "name": "Mother_Carey's_chicken"}, + {"id": 3258, "synset": "diving_petrel.n.01", "name": "diving_petrel"}, + {"id": 3259, "synset": "aquatic_mammal.n.01", "name": "aquatic_mammal"}, + {"id": 3260, "synset": "cetacean.n.01", "name": "cetacean"}, + {"id": 3261, "synset": "whale.n.02", "name": "whale"}, + {"id": 3262, "synset": "baleen_whale.n.01", "name": "baleen_whale"}, + {"id": 3263, "synset": "right_whale.n.01", "name": "right_whale"}, + {"id": 3264, "synset": "bowhead.n.01", "name": "bowhead"}, + {"id": 3265, "synset": "rorqual.n.01", "name": "rorqual"}, + {"id": 3266, "synset": "blue_whale.n.01", "name": "blue_whale"}, + {"id": 3267, "synset": "finback.n.01", "name": "finback"}, + {"id": 3268, "synset": "sei_whale.n.01", "name": "sei_whale"}, + {"id": 3269, "synset": "lesser_rorqual.n.01", "name": "lesser_rorqual"}, + {"id": 3270, "synset": "humpback.n.03", "name": "humpback"}, + {"id": 3271, "synset": "grey_whale.n.01", "name": "grey_whale"}, + {"id": 3272, "synset": "toothed_whale.n.01", "name": "toothed_whale"}, + {"id": 3273, "synset": "sperm_whale.n.01", "name": "sperm_whale"}, + {"id": 3274, "synset": "pygmy_sperm_whale.n.01", "name": "pygmy_sperm_whale"}, + {"id": 3275, "synset": "dwarf_sperm_whale.n.01", "name": "dwarf_sperm_whale"}, + {"id": 3276, "synset": "beaked_whale.n.01", "name": "beaked_whale"}, + {"id": 3277, "synset": "bottle-nosed_whale.n.01", "name": "bottle-nosed_whale"}, + {"id": 3278, "synset": "common_dolphin.n.01", "name": "common_dolphin"}, + {"id": 3279, "synset": "bottlenose_dolphin.n.01", "name": "bottlenose_dolphin"}, + { + "id": 3280, + "synset": "atlantic_bottlenose_dolphin.n.01", + "name": "Atlantic_bottlenose_dolphin", + }, + {"id": 3281, "synset": "pacific_bottlenose_dolphin.n.01", "name": "Pacific_bottlenose_dolphin"}, + {"id": 3282, "synset": "porpoise.n.01", "name": "porpoise"}, + {"id": 3283, "synset": "harbor_porpoise.n.01", "name": "harbor_porpoise"}, + {"id": 3284, "synset": "vaquita.n.01", "name": "vaquita"}, + {"id": 3285, "synset": "grampus.n.02", "name": "grampus"}, + {"id": 3286, "synset": "killer_whale.n.01", "name": "killer_whale"}, + {"id": 3287, "synset": "pilot_whale.n.01", "name": "pilot_whale"}, + {"id": 3288, "synset": "river_dolphin.n.01", "name": "river_dolphin"}, + {"id": 3289, "synset": "narwhal.n.01", "name": "narwhal"}, + {"id": 3290, "synset": "white_whale.n.01", "name": "white_whale"}, + {"id": 3291, "synset": "sea_cow.n.01", "name": "sea_cow"}, + {"id": 3292, "synset": "dugong.n.01", "name": "dugong"}, + {"id": 3293, "synset": "steller's_sea_cow.n.01", "name": "Steller's_sea_cow"}, + {"id": 3294, "synset": "carnivore.n.01", "name": "carnivore"}, + {"id": 3295, "synset": "omnivore.n.02", "name": "omnivore"}, + {"id": 3296, "synset": "pinniped_mammal.n.01", "name": "pinniped_mammal"}, + {"id": 3297, "synset": "seal.n.09", "name": "seal"}, + {"id": 3298, "synset": "crabeater_seal.n.01", "name": "crabeater_seal"}, + {"id": 3299, "synset": "eared_seal.n.01", "name": "eared_seal"}, + {"id": 3300, "synset": "fur_seal.n.02", "name": "fur_seal"}, + {"id": 3301, "synset": "guadalupe_fur_seal.n.01", "name": "guadalupe_fur_seal"}, + {"id": 3302, "synset": "fur_seal.n.01", "name": "fur_seal"}, + {"id": 3303, "synset": "alaska_fur_seal.n.01", "name": "Alaska_fur_seal"}, + {"id": 3304, "synset": "sea_lion.n.01", "name": "sea_lion"}, + {"id": 3305, "synset": "south_american_sea_lion.n.01", "name": "South_American_sea_lion"}, + {"id": 3306, "synset": "california_sea_lion.n.01", "name": "California_sea_lion"}, + {"id": 3307, "synset": "australian_sea_lion.n.01", "name": "Australian_sea_lion"}, + {"id": 3308, "synset": "steller_sea_lion.n.01", "name": "Steller_sea_lion"}, + {"id": 3309, "synset": "earless_seal.n.01", "name": "earless_seal"}, + {"id": 3310, "synset": "harbor_seal.n.01", "name": "harbor_seal"}, + {"id": 3311, "synset": "harp_seal.n.01", "name": "harp_seal"}, + {"id": 3312, "synset": "elephant_seal.n.01", "name": "elephant_seal"}, + {"id": 3313, "synset": "bearded_seal.n.01", "name": "bearded_seal"}, + {"id": 3314, "synset": "hooded_seal.n.01", "name": "hooded_seal"}, + {"id": 3315, "synset": "atlantic_walrus.n.01", "name": "Atlantic_walrus"}, + {"id": 3316, "synset": "pacific_walrus.n.01", "name": "Pacific_walrus"}, + {"id": 3317, "synset": "fissipedia.n.01", "name": "Fissipedia"}, + {"id": 3318, "synset": "fissiped_mammal.n.01", "name": "fissiped_mammal"}, + {"id": 3319, "synset": "aardvark.n.01", "name": "aardvark"}, + {"id": 3320, "synset": "canine.n.02", "name": "canine"}, + {"id": 3321, "synset": "bitch.n.04", "name": "bitch"}, + {"id": 3322, "synset": "brood_bitch.n.01", "name": "brood_bitch"}, + {"id": 3323, "synset": "pooch.n.01", "name": "pooch"}, + {"id": 3324, "synset": "cur.n.01", "name": "cur"}, + {"id": 3325, "synset": "feist.n.01", "name": "feist"}, + {"id": 3326, "synset": "pariah_dog.n.01", "name": "pariah_dog"}, + {"id": 3327, "synset": "lapdog.n.01", "name": "lapdog"}, + {"id": 3328, "synset": "toy_dog.n.01", "name": "toy_dog"}, + {"id": 3329, "synset": "chihuahua.n.03", "name": "Chihuahua"}, + {"id": 3330, "synset": "japanese_spaniel.n.01", "name": "Japanese_spaniel"}, + {"id": 3331, "synset": "maltese_dog.n.01", "name": "Maltese_dog"}, + {"id": 3332, "synset": "pekinese.n.01", "name": "Pekinese"}, + {"id": 3333, "synset": "shih-tzu.n.01", "name": "Shih-Tzu"}, + {"id": 3334, "synset": "toy_spaniel.n.01", "name": "toy_spaniel"}, + {"id": 3335, "synset": "english_toy_spaniel.n.01", "name": "English_toy_spaniel"}, + {"id": 3336, "synset": "blenheim_spaniel.n.01", "name": "Blenheim_spaniel"}, + {"id": 3337, "synset": "king_charles_spaniel.n.01", "name": "King_Charles_spaniel"}, + {"id": 3338, "synset": "papillon.n.01", "name": "papillon"}, + {"id": 3339, "synset": "toy_terrier.n.01", "name": "toy_terrier"}, + {"id": 3340, "synset": "hunting_dog.n.01", "name": "hunting_dog"}, + {"id": 3341, "synset": "courser.n.03", "name": "courser"}, + {"id": 3342, "synset": "rhodesian_ridgeback.n.01", "name": "Rhodesian_ridgeback"}, + {"id": 3343, "synset": "hound.n.01", "name": "hound"}, + {"id": 3344, "synset": "afghan_hound.n.01", "name": "Afghan_hound"}, + {"id": 3345, "synset": "basset.n.01", "name": "basset"}, + {"id": 3346, "synset": "beagle.n.01", "name": "beagle"}, + {"id": 3347, "synset": "bloodhound.n.01", "name": "bloodhound"}, + {"id": 3348, "synset": "bluetick.n.01", "name": "bluetick"}, + {"id": 3349, "synset": "boarhound.n.01", "name": "boarhound"}, + {"id": 3350, "synset": "coonhound.n.01", "name": "coonhound"}, + {"id": 3351, "synset": "coondog.n.01", "name": "coondog"}, + {"id": 3352, "synset": "black-and-tan_coonhound.n.01", "name": "black-and-tan_coonhound"}, + {"id": 3353, "synset": "dachshund.n.01", "name": "dachshund"}, + {"id": 3354, "synset": "sausage_dog.n.01", "name": "sausage_dog"}, + {"id": 3355, "synset": "foxhound.n.01", "name": "foxhound"}, + {"id": 3356, "synset": "american_foxhound.n.01", "name": "American_foxhound"}, + {"id": 3357, "synset": "walker_hound.n.01", "name": "Walker_hound"}, + {"id": 3358, "synset": "english_foxhound.n.01", "name": "English_foxhound"}, + {"id": 3359, "synset": "harrier.n.02", "name": "harrier"}, + {"id": 3360, "synset": "plott_hound.n.01", "name": "Plott_hound"}, + {"id": 3361, "synset": "redbone.n.01", "name": "redbone"}, + {"id": 3362, "synset": "wolfhound.n.01", "name": "wolfhound"}, + {"id": 3363, "synset": "borzoi.n.01", "name": "borzoi"}, + {"id": 3364, "synset": "irish_wolfhound.n.01", "name": "Irish_wolfhound"}, + {"id": 3365, "synset": "greyhound.n.01", "name": "greyhound"}, + {"id": 3366, "synset": "italian_greyhound.n.01", "name": "Italian_greyhound"}, + {"id": 3367, "synset": "whippet.n.01", "name": "whippet"}, + {"id": 3368, "synset": "ibizan_hound.n.01", "name": "Ibizan_hound"}, + {"id": 3369, "synset": "norwegian_elkhound.n.01", "name": "Norwegian_elkhound"}, + {"id": 3370, "synset": "otterhound.n.01", "name": "otterhound"}, + {"id": 3371, "synset": "saluki.n.01", "name": "Saluki"}, + {"id": 3372, "synset": "scottish_deerhound.n.01", "name": "Scottish_deerhound"}, + {"id": 3373, "synset": "staghound.n.01", "name": "staghound"}, + {"id": 3374, "synset": "weimaraner.n.01", "name": "Weimaraner"}, + {"id": 3375, "synset": "terrier.n.01", "name": "terrier"}, + {"id": 3376, "synset": "bullterrier.n.01", "name": "bullterrier"}, + {"id": 3377, "synset": "staffordshire_bullterrier.n.01", "name": "Staffordshire_bullterrier"}, + { + "id": 3378, + "synset": "american_staffordshire_terrier.n.01", + "name": "American_Staffordshire_terrier", + }, + {"id": 3379, "synset": "bedlington_terrier.n.01", "name": "Bedlington_terrier"}, + {"id": 3380, "synset": "border_terrier.n.01", "name": "Border_terrier"}, + {"id": 3381, "synset": "kerry_blue_terrier.n.01", "name": "Kerry_blue_terrier"}, + {"id": 3382, "synset": "irish_terrier.n.01", "name": "Irish_terrier"}, + {"id": 3383, "synset": "norfolk_terrier.n.01", "name": "Norfolk_terrier"}, + {"id": 3384, "synset": "norwich_terrier.n.01", "name": "Norwich_terrier"}, + {"id": 3385, "synset": "yorkshire_terrier.n.01", "name": "Yorkshire_terrier"}, + {"id": 3386, "synset": "rat_terrier.n.01", "name": "rat_terrier"}, + {"id": 3387, "synset": "manchester_terrier.n.01", "name": "Manchester_terrier"}, + {"id": 3388, "synset": "toy_manchester.n.01", "name": "toy_Manchester"}, + {"id": 3389, "synset": "fox_terrier.n.01", "name": "fox_terrier"}, + {"id": 3390, "synset": "smooth-haired_fox_terrier.n.01", "name": "smooth-haired_fox_terrier"}, + {"id": 3391, "synset": "wire-haired_fox_terrier.n.01", "name": "wire-haired_fox_terrier"}, + {"id": 3392, "synset": "wirehair.n.01", "name": "wirehair"}, + {"id": 3393, "synset": "lakeland_terrier.n.01", "name": "Lakeland_terrier"}, + {"id": 3394, "synset": "welsh_terrier.n.01", "name": "Welsh_terrier"}, + {"id": 3395, "synset": "sealyham_terrier.n.01", "name": "Sealyham_terrier"}, + {"id": 3396, "synset": "airedale.n.01", "name": "Airedale"}, + {"id": 3397, "synset": "cairn.n.02", "name": "cairn"}, + {"id": 3398, "synset": "australian_terrier.n.01", "name": "Australian_terrier"}, + {"id": 3399, "synset": "dandie_dinmont.n.01", "name": "Dandie_Dinmont"}, + {"id": 3400, "synset": "boston_bull.n.01", "name": "Boston_bull"}, + {"id": 3401, "synset": "schnauzer.n.01", "name": "schnauzer"}, + {"id": 3402, "synset": "miniature_schnauzer.n.01", "name": "miniature_schnauzer"}, + {"id": 3403, "synset": "giant_schnauzer.n.01", "name": "giant_schnauzer"}, + {"id": 3404, "synset": "standard_schnauzer.n.01", "name": "standard_schnauzer"}, + {"id": 3405, "synset": "scotch_terrier.n.01", "name": "Scotch_terrier"}, + {"id": 3406, "synset": "tibetan_terrier.n.01", "name": "Tibetan_terrier"}, + {"id": 3407, "synset": "silky_terrier.n.01", "name": "silky_terrier"}, + {"id": 3408, "synset": "skye_terrier.n.01", "name": "Skye_terrier"}, + {"id": 3409, "synset": "clydesdale_terrier.n.01", "name": "Clydesdale_terrier"}, + { + "id": 3410, + "synset": "soft-coated_wheaten_terrier.n.01", + "name": "soft-coated_wheaten_terrier", + }, + { + "id": 3411, + "synset": "west_highland_white_terrier.n.01", + "name": "West_Highland_white_terrier", + }, + {"id": 3412, "synset": "lhasa.n.02", "name": "Lhasa"}, + {"id": 3413, "synset": "sporting_dog.n.01", "name": "sporting_dog"}, + {"id": 3414, "synset": "bird_dog.n.01", "name": "bird_dog"}, + {"id": 3415, "synset": "water_dog.n.02", "name": "water_dog"}, + {"id": 3416, "synset": "retriever.n.01", "name": "retriever"}, + {"id": 3417, "synset": "flat-coated_retriever.n.01", "name": "flat-coated_retriever"}, + {"id": 3418, "synset": "curly-coated_retriever.n.01", "name": "curly-coated_retriever"}, + {"id": 3419, "synset": "golden_retriever.n.01", "name": "golden_retriever"}, + {"id": 3420, "synset": "labrador_retriever.n.01", "name": "Labrador_retriever"}, + {"id": 3421, "synset": "chesapeake_bay_retriever.n.01", "name": "Chesapeake_Bay_retriever"}, + {"id": 3422, "synset": "pointer.n.04", "name": "pointer"}, + { + "id": 3423, + "synset": "german_short-haired_pointer.n.01", + "name": "German_short-haired_pointer", + }, + {"id": 3424, "synset": "setter.n.02", "name": "setter"}, + {"id": 3425, "synset": "vizsla.n.01", "name": "vizsla"}, + {"id": 3426, "synset": "english_setter.n.01", "name": "English_setter"}, + {"id": 3427, "synset": "irish_setter.n.01", "name": "Irish_setter"}, + {"id": 3428, "synset": "gordon_setter.n.01", "name": "Gordon_setter"}, + {"id": 3429, "synset": "spaniel.n.01", "name": "spaniel"}, + {"id": 3430, "synset": "brittany_spaniel.n.01", "name": "Brittany_spaniel"}, + {"id": 3431, "synset": "clumber.n.01", "name": "clumber"}, + {"id": 3432, "synset": "field_spaniel.n.01", "name": "field_spaniel"}, + {"id": 3433, "synset": "springer_spaniel.n.01", "name": "springer_spaniel"}, + {"id": 3434, "synset": "english_springer.n.01", "name": "English_springer"}, + {"id": 3435, "synset": "welsh_springer_spaniel.n.01", "name": "Welsh_springer_spaniel"}, + {"id": 3436, "synset": "cocker_spaniel.n.01", "name": "cocker_spaniel"}, + {"id": 3437, "synset": "sussex_spaniel.n.01", "name": "Sussex_spaniel"}, + {"id": 3438, "synset": "water_spaniel.n.01", "name": "water_spaniel"}, + {"id": 3439, "synset": "american_water_spaniel.n.01", "name": "American_water_spaniel"}, + {"id": 3440, "synset": "irish_water_spaniel.n.01", "name": "Irish_water_spaniel"}, + {"id": 3441, "synset": "griffon.n.03", "name": "griffon"}, + {"id": 3442, "synset": "working_dog.n.01", "name": "working_dog"}, + {"id": 3443, "synset": "watchdog.n.02", "name": "watchdog"}, + {"id": 3444, "synset": "kuvasz.n.01", "name": "kuvasz"}, + {"id": 3445, "synset": "attack_dog.n.01", "name": "attack_dog"}, + {"id": 3446, "synset": "housedog.n.01", "name": "housedog"}, + {"id": 3447, "synset": "schipperke.n.01", "name": "schipperke"}, + {"id": 3448, "synset": "belgian_sheepdog.n.01", "name": "Belgian_sheepdog"}, + {"id": 3449, "synset": "groenendael.n.01", "name": "groenendael"}, + {"id": 3450, "synset": "malinois.n.01", "name": "malinois"}, + {"id": 3451, "synset": "briard.n.01", "name": "briard"}, + {"id": 3452, "synset": "kelpie.n.02", "name": "kelpie"}, + {"id": 3453, "synset": "komondor.n.01", "name": "komondor"}, + {"id": 3454, "synset": "old_english_sheepdog.n.01", "name": "Old_English_sheepdog"}, + {"id": 3455, "synset": "shetland_sheepdog.n.01", "name": "Shetland_sheepdog"}, + {"id": 3456, "synset": "collie.n.01", "name": "collie"}, + {"id": 3457, "synset": "border_collie.n.01", "name": "Border_collie"}, + {"id": 3458, "synset": "bouvier_des_flandres.n.01", "name": "Bouvier_des_Flandres"}, + {"id": 3459, "synset": "rottweiler.n.01", "name": "Rottweiler"}, + {"id": 3460, "synset": "german_shepherd.n.01", "name": "German_shepherd"}, + {"id": 3461, "synset": "police_dog.n.01", "name": "police_dog"}, + {"id": 3462, "synset": "pinscher.n.01", "name": "pinscher"}, + {"id": 3463, "synset": "doberman.n.01", "name": "Doberman"}, + {"id": 3464, "synset": "miniature_pinscher.n.01", "name": "miniature_pinscher"}, + {"id": 3465, "synset": "sennenhunde.n.01", "name": "Sennenhunde"}, + {"id": 3466, "synset": "greater_swiss_mountain_dog.n.01", "name": "Greater_Swiss_Mountain_dog"}, + {"id": 3467, "synset": "bernese_mountain_dog.n.01", "name": "Bernese_mountain_dog"}, + {"id": 3468, "synset": "appenzeller.n.01", "name": "Appenzeller"}, + {"id": 3469, "synset": "entlebucher.n.01", "name": "EntleBucher"}, + {"id": 3470, "synset": "boxer.n.04", "name": "boxer"}, + {"id": 3471, "synset": "mastiff.n.01", "name": "mastiff"}, + {"id": 3472, "synset": "bull_mastiff.n.01", "name": "bull_mastiff"}, + {"id": 3473, "synset": "tibetan_mastiff.n.01", "name": "Tibetan_mastiff"}, + {"id": 3474, "synset": "french_bulldog.n.01", "name": "French_bulldog"}, + {"id": 3475, "synset": "great_dane.n.01", "name": "Great_Dane"}, + {"id": 3476, "synset": "guide_dog.n.01", "name": "guide_dog"}, + {"id": 3477, "synset": "seeing_eye_dog.n.01", "name": "Seeing_Eye_dog"}, + {"id": 3478, "synset": "hearing_dog.n.01", "name": "hearing_dog"}, + {"id": 3479, "synset": "saint_bernard.n.01", "name": "Saint_Bernard"}, + {"id": 3480, "synset": "seizure-alert_dog.n.01", "name": "seizure-alert_dog"}, + {"id": 3481, "synset": "sled_dog.n.01", "name": "sled_dog"}, + {"id": 3482, "synset": "eskimo_dog.n.01", "name": "Eskimo_dog"}, + {"id": 3483, "synset": "malamute.n.01", "name": "malamute"}, + {"id": 3484, "synset": "siberian_husky.n.01", "name": "Siberian_husky"}, + {"id": 3485, "synset": "liver-spotted_dalmatian.n.01", "name": "liver-spotted_dalmatian"}, + {"id": 3486, "synset": "affenpinscher.n.01", "name": "affenpinscher"}, + {"id": 3487, "synset": "basenji.n.01", "name": "basenji"}, + {"id": 3488, "synset": "leonberg.n.01", "name": "Leonberg"}, + {"id": 3489, "synset": "newfoundland.n.01", "name": "Newfoundland"}, + {"id": 3490, "synset": "great_pyrenees.n.01", "name": "Great_Pyrenees"}, + {"id": 3491, "synset": "spitz.n.01", "name": "spitz"}, + {"id": 3492, "synset": "samoyed.n.03", "name": "Samoyed"}, + {"id": 3493, "synset": "pomeranian.n.01", "name": "Pomeranian"}, + {"id": 3494, "synset": "chow.n.03", "name": "chow"}, + {"id": 3495, "synset": "keeshond.n.01", "name": "keeshond"}, + {"id": 3496, "synset": "griffon.n.02", "name": "griffon"}, + {"id": 3497, "synset": "brabancon_griffon.n.01", "name": "Brabancon_griffon"}, + {"id": 3498, "synset": "corgi.n.01", "name": "corgi"}, + {"id": 3499, "synset": "pembroke.n.01", "name": "Pembroke"}, + {"id": 3500, "synset": "cardigan.n.02", "name": "Cardigan"}, + {"id": 3501, "synset": "poodle.n.01", "name": "poodle"}, + {"id": 3502, "synset": "toy_poodle.n.01", "name": "toy_poodle"}, + {"id": 3503, "synset": "miniature_poodle.n.01", "name": "miniature_poodle"}, + {"id": 3504, "synset": "standard_poodle.n.01", "name": "standard_poodle"}, + {"id": 3505, "synset": "large_poodle.n.01", "name": "large_poodle"}, + {"id": 3506, "synset": "mexican_hairless.n.01", "name": "Mexican_hairless"}, + {"id": 3507, "synset": "timber_wolf.n.01", "name": "timber_wolf"}, + {"id": 3508, "synset": "white_wolf.n.01", "name": "white_wolf"}, + {"id": 3509, "synset": "red_wolf.n.01", "name": "red_wolf"}, + {"id": 3510, "synset": "coyote.n.01", "name": "coyote"}, + {"id": 3511, "synset": "coydog.n.01", "name": "coydog"}, + {"id": 3512, "synset": "jackal.n.01", "name": "jackal"}, + {"id": 3513, "synset": "wild_dog.n.01", "name": "wild_dog"}, + {"id": 3514, "synset": "dingo.n.01", "name": "dingo"}, + {"id": 3515, "synset": "dhole.n.01", "name": "dhole"}, + {"id": 3516, "synset": "crab-eating_dog.n.01", "name": "crab-eating_dog"}, + {"id": 3517, "synset": "raccoon_dog.n.01", "name": "raccoon_dog"}, + {"id": 3518, "synset": "african_hunting_dog.n.01", "name": "African_hunting_dog"}, + {"id": 3519, "synset": "hyena.n.01", "name": "hyena"}, + {"id": 3520, "synset": "striped_hyena.n.01", "name": "striped_hyena"}, + {"id": 3521, "synset": "brown_hyena.n.01", "name": "brown_hyena"}, + {"id": 3522, "synset": "spotted_hyena.n.01", "name": "spotted_hyena"}, + {"id": 3523, "synset": "aardwolf.n.01", "name": "aardwolf"}, + {"id": 3524, "synset": "fox.n.01", "name": "fox"}, + {"id": 3525, "synset": "vixen.n.02", "name": "vixen"}, + {"id": 3526, "synset": "reynard.n.01", "name": "Reynard"}, + {"id": 3527, "synset": "red_fox.n.03", "name": "red_fox"}, + {"id": 3528, "synset": "black_fox.n.01", "name": "black_fox"}, + {"id": 3529, "synset": "silver_fox.n.01", "name": "silver_fox"}, + {"id": 3530, "synset": "red_fox.n.02", "name": "red_fox"}, + {"id": 3531, "synset": "kit_fox.n.02", "name": "kit_fox"}, + {"id": 3532, "synset": "kit_fox.n.01", "name": "kit_fox"}, + {"id": 3533, "synset": "arctic_fox.n.01", "name": "Arctic_fox"}, + {"id": 3534, "synset": "blue_fox.n.01", "name": "blue_fox"}, + {"id": 3535, "synset": "grey_fox.n.01", "name": "grey_fox"}, + {"id": 3536, "synset": "feline.n.01", "name": "feline"}, + {"id": 3537, "synset": "domestic_cat.n.01", "name": "domestic_cat"}, + {"id": 3538, "synset": "kitty.n.04", "name": "kitty"}, + {"id": 3539, "synset": "mouser.n.01", "name": "mouser"}, + {"id": 3540, "synset": "alley_cat.n.01", "name": "alley_cat"}, + {"id": 3541, "synset": "stray.n.01", "name": "stray"}, + {"id": 3542, "synset": "tom.n.02", "name": "tom"}, + {"id": 3543, "synset": "gib.n.02", "name": "gib"}, + {"id": 3544, "synset": "tabby.n.02", "name": "tabby"}, + {"id": 3545, "synset": "tabby.n.01", "name": "tabby"}, + {"id": 3546, "synset": "tiger_cat.n.02", "name": "tiger_cat"}, + {"id": 3547, "synset": "tortoiseshell.n.03", "name": "tortoiseshell"}, + {"id": 3548, "synset": "persian_cat.n.01", "name": "Persian_cat"}, + {"id": 3549, "synset": "angora.n.04", "name": "Angora"}, + {"id": 3550, "synset": "siamese_cat.n.01", "name": "Siamese_cat"}, + {"id": 3551, "synset": "blue_point_siamese.n.01", "name": "blue_point_Siamese"}, + {"id": 3552, "synset": "burmese_cat.n.01", "name": "Burmese_cat"}, + {"id": 3553, "synset": "egyptian_cat.n.01", "name": "Egyptian_cat"}, + {"id": 3554, "synset": "maltese.n.03", "name": "Maltese"}, + {"id": 3555, "synset": "abyssinian.n.01", "name": "Abyssinian"}, + {"id": 3556, "synset": "manx.n.02", "name": "Manx"}, + {"id": 3557, "synset": "wildcat.n.03", "name": "wildcat"}, + {"id": 3558, "synset": "sand_cat.n.01", "name": "sand_cat"}, + {"id": 3559, "synset": "european_wildcat.n.01", "name": "European_wildcat"}, + {"id": 3560, "synset": "ocelot.n.01", "name": "ocelot"}, + {"id": 3561, "synset": "jaguarundi.n.01", "name": "jaguarundi"}, + {"id": 3562, "synset": "kaffir_cat.n.01", "name": "kaffir_cat"}, + {"id": 3563, "synset": "jungle_cat.n.01", "name": "jungle_cat"}, + {"id": 3564, "synset": "serval.n.01", "name": "serval"}, + {"id": 3565, "synset": "leopard_cat.n.01", "name": "leopard_cat"}, + {"id": 3566, "synset": "margay.n.01", "name": "margay"}, + {"id": 3567, "synset": "manul.n.01", "name": "manul"}, + {"id": 3568, "synset": "lynx.n.02", "name": "lynx"}, + {"id": 3569, "synset": "common_lynx.n.01", "name": "common_lynx"}, + {"id": 3570, "synset": "canada_lynx.n.01", "name": "Canada_lynx"}, + {"id": 3571, "synset": "bobcat.n.01", "name": "bobcat"}, + {"id": 3572, "synset": "spotted_lynx.n.01", "name": "spotted_lynx"}, + {"id": 3573, "synset": "caracal.n.01", "name": "caracal"}, + {"id": 3574, "synset": "big_cat.n.01", "name": "big_cat"}, + {"id": 3575, "synset": "leopard.n.02", "name": "leopard"}, + {"id": 3576, "synset": "leopardess.n.01", "name": "leopardess"}, + {"id": 3577, "synset": "panther.n.02", "name": "panther"}, + {"id": 3578, "synset": "snow_leopard.n.01", "name": "snow_leopard"}, + {"id": 3579, "synset": "jaguar.n.01", "name": "jaguar"}, + {"id": 3580, "synset": "lioness.n.01", "name": "lioness"}, + {"id": 3581, "synset": "lionet.n.01", "name": "lionet"}, + {"id": 3582, "synset": "bengal_tiger.n.01", "name": "Bengal_tiger"}, + {"id": 3583, "synset": "tigress.n.01", "name": "tigress"}, + {"id": 3584, "synset": "liger.n.01", "name": "liger"}, + {"id": 3585, "synset": "tiglon.n.01", "name": "tiglon"}, + {"id": 3586, "synset": "cheetah.n.01", "name": "cheetah"}, + {"id": 3587, "synset": "saber-toothed_tiger.n.01", "name": "saber-toothed_tiger"}, + {"id": 3588, "synset": "smiledon_californicus.n.01", "name": "Smiledon_californicus"}, + {"id": 3589, "synset": "brown_bear.n.01", "name": "brown_bear"}, + {"id": 3590, "synset": "bruin.n.01", "name": "bruin"}, + {"id": 3591, "synset": "syrian_bear.n.01", "name": "Syrian_bear"}, + {"id": 3592, "synset": "alaskan_brown_bear.n.01", "name": "Alaskan_brown_bear"}, + {"id": 3593, "synset": "american_black_bear.n.01", "name": "American_black_bear"}, + {"id": 3594, "synset": "cinnamon_bear.n.01", "name": "cinnamon_bear"}, + {"id": 3595, "synset": "asiatic_black_bear.n.01", "name": "Asiatic_black_bear"}, + {"id": 3596, "synset": "sloth_bear.n.01", "name": "sloth_bear"}, + {"id": 3597, "synset": "viverrine.n.01", "name": "viverrine"}, + {"id": 3598, "synset": "civet.n.01", "name": "civet"}, + {"id": 3599, "synset": "large_civet.n.01", "name": "large_civet"}, + {"id": 3600, "synset": "small_civet.n.01", "name": "small_civet"}, + {"id": 3601, "synset": "binturong.n.01", "name": "binturong"}, + {"id": 3602, "synset": "cryptoprocta.n.01", "name": "Cryptoprocta"}, + {"id": 3603, "synset": "fossa.n.03", "name": "fossa"}, + {"id": 3604, "synset": "fanaloka.n.01", "name": "fanaloka"}, + {"id": 3605, "synset": "genet.n.03", "name": "genet"}, + {"id": 3606, "synset": "banded_palm_civet.n.01", "name": "banded_palm_civet"}, + {"id": 3607, "synset": "mongoose.n.01", "name": "mongoose"}, + {"id": 3608, "synset": "indian_mongoose.n.01", "name": "Indian_mongoose"}, + {"id": 3609, "synset": "ichneumon.n.01", "name": "ichneumon"}, + {"id": 3610, "synset": "palm_cat.n.01", "name": "palm_cat"}, + {"id": 3611, "synset": "meerkat.n.01", "name": "meerkat"}, + {"id": 3612, "synset": "slender-tailed_meerkat.n.01", "name": "slender-tailed_meerkat"}, + {"id": 3613, "synset": "suricate.n.01", "name": "suricate"}, + {"id": 3614, "synset": "fruit_bat.n.01", "name": "fruit_bat"}, + {"id": 3615, "synset": "flying_fox.n.01", "name": "flying_fox"}, + {"id": 3616, "synset": "pteropus_capestratus.n.01", "name": "Pteropus_capestratus"}, + {"id": 3617, "synset": "pteropus_hypomelanus.n.01", "name": "Pteropus_hypomelanus"}, + {"id": 3618, "synset": "harpy.n.03", "name": "harpy"}, + {"id": 3619, "synset": "cynopterus_sphinx.n.01", "name": "Cynopterus_sphinx"}, + {"id": 3620, "synset": "carnivorous_bat.n.01", "name": "carnivorous_bat"}, + {"id": 3621, "synset": "mouse-eared_bat.n.01", "name": "mouse-eared_bat"}, + {"id": 3622, "synset": "leafnose_bat.n.01", "name": "leafnose_bat"}, + {"id": 3623, "synset": "macrotus.n.01", "name": "macrotus"}, + {"id": 3624, "synset": "spearnose_bat.n.01", "name": "spearnose_bat"}, + {"id": 3625, "synset": "phyllostomus_hastatus.n.01", "name": "Phyllostomus_hastatus"}, + {"id": 3626, "synset": "hognose_bat.n.01", "name": "hognose_bat"}, + {"id": 3627, "synset": "horseshoe_bat.n.02", "name": "horseshoe_bat"}, + {"id": 3628, "synset": "horseshoe_bat.n.01", "name": "horseshoe_bat"}, + {"id": 3629, "synset": "orange_bat.n.01", "name": "orange_bat"}, + {"id": 3630, "synset": "false_vampire.n.01", "name": "false_vampire"}, + {"id": 3631, "synset": "big-eared_bat.n.01", "name": "big-eared_bat"}, + {"id": 3632, "synset": "vespertilian_bat.n.01", "name": "vespertilian_bat"}, + {"id": 3633, "synset": "frosted_bat.n.01", "name": "frosted_bat"}, + {"id": 3634, "synset": "red_bat.n.01", "name": "red_bat"}, + {"id": 3635, "synset": "brown_bat.n.01", "name": "brown_bat"}, + {"id": 3636, "synset": "little_brown_bat.n.01", "name": "little_brown_bat"}, + {"id": 3637, "synset": "cave_myotis.n.01", "name": "cave_myotis"}, + {"id": 3638, "synset": "big_brown_bat.n.01", "name": "big_brown_bat"}, + {"id": 3639, "synset": "serotine.n.01", "name": "serotine"}, + {"id": 3640, "synset": "pallid_bat.n.01", "name": "pallid_bat"}, + {"id": 3641, "synset": "pipistrelle.n.01", "name": "pipistrelle"}, + {"id": 3642, "synset": "eastern_pipistrel.n.01", "name": "eastern_pipistrel"}, + {"id": 3643, "synset": "jackass_bat.n.01", "name": "jackass_bat"}, + {"id": 3644, "synset": "long-eared_bat.n.01", "name": "long-eared_bat"}, + {"id": 3645, "synset": "western_big-eared_bat.n.01", "name": "western_big-eared_bat"}, + {"id": 3646, "synset": "freetail.n.01", "name": "freetail"}, + {"id": 3647, "synset": "guano_bat.n.01", "name": "guano_bat"}, + {"id": 3648, "synset": "pocketed_bat.n.01", "name": "pocketed_bat"}, + {"id": 3649, "synset": "mastiff_bat.n.01", "name": "mastiff_bat"}, + {"id": 3650, "synset": "vampire_bat.n.01", "name": "vampire_bat"}, + {"id": 3651, "synset": "desmodus_rotundus.n.01", "name": "Desmodus_rotundus"}, + {"id": 3652, "synset": "hairy-legged_vampire_bat.n.01", "name": "hairy-legged_vampire_bat"}, + {"id": 3653, "synset": "predator.n.02", "name": "predator"}, + {"id": 3654, "synset": "prey.n.02", "name": "prey"}, + {"id": 3655, "synset": "game.n.04", "name": "game"}, + {"id": 3656, "synset": "big_game.n.01", "name": "big_game"}, + {"id": 3657, "synset": "game_bird.n.01", "name": "game_bird"}, + {"id": 3658, "synset": "fossorial_mammal.n.01", "name": "fossorial_mammal"}, + {"id": 3659, "synset": "tetrapod.n.01", "name": "tetrapod"}, + {"id": 3660, "synset": "quadruped.n.01", "name": "quadruped"}, + {"id": 3661, "synset": "hexapod.n.01", "name": "hexapod"}, + {"id": 3662, "synset": "biped.n.01", "name": "biped"}, + {"id": 3663, "synset": "insect.n.01", "name": "insect"}, + {"id": 3664, "synset": "social_insect.n.01", "name": "social_insect"}, + {"id": 3665, "synset": "holometabola.n.01", "name": "holometabola"}, + {"id": 3666, "synset": "defoliator.n.01", "name": "defoliator"}, + {"id": 3667, "synset": "pollinator.n.01", "name": "pollinator"}, + {"id": 3668, "synset": "gallfly.n.03", "name": "gallfly"}, + {"id": 3669, "synset": "scorpion_fly.n.01", "name": "scorpion_fly"}, + {"id": 3670, "synset": "hanging_fly.n.01", "name": "hanging_fly"}, + {"id": 3671, "synset": "collembolan.n.01", "name": "collembolan"}, + {"id": 3672, "synset": "tiger_beetle.n.01", "name": "tiger_beetle"}, + {"id": 3673, "synset": "two-spotted_ladybug.n.01", "name": "two-spotted_ladybug"}, + {"id": 3674, "synset": "mexican_bean_beetle.n.01", "name": "Mexican_bean_beetle"}, + {"id": 3675, "synset": "hippodamia_convergens.n.01", "name": "Hippodamia_convergens"}, + {"id": 3676, "synset": "vedalia.n.01", "name": "vedalia"}, + {"id": 3677, "synset": "ground_beetle.n.01", "name": "ground_beetle"}, + {"id": 3678, "synset": "bombardier_beetle.n.01", "name": "bombardier_beetle"}, + {"id": 3679, "synset": "calosoma.n.01", "name": "calosoma"}, + {"id": 3680, "synset": "searcher.n.03", "name": "searcher"}, + {"id": 3681, "synset": "firefly.n.02", "name": "firefly"}, + {"id": 3682, "synset": "glowworm.n.01", "name": "glowworm"}, + {"id": 3683, "synset": "long-horned_beetle.n.01", "name": "long-horned_beetle"}, + {"id": 3684, "synset": "sawyer.n.02", "name": "sawyer"}, + {"id": 3685, "synset": "pine_sawyer.n.01", "name": "pine_sawyer"}, + {"id": 3686, "synset": "leaf_beetle.n.01", "name": "leaf_beetle"}, + {"id": 3687, "synset": "flea_beetle.n.01", "name": "flea_beetle"}, + {"id": 3688, "synset": "colorado_potato_beetle.n.01", "name": "Colorado_potato_beetle"}, + {"id": 3689, "synset": "carpet_beetle.n.01", "name": "carpet_beetle"}, + {"id": 3690, "synset": "buffalo_carpet_beetle.n.01", "name": "buffalo_carpet_beetle"}, + {"id": 3691, "synset": "black_carpet_beetle.n.01", "name": "black_carpet_beetle"}, + {"id": 3692, "synset": "clerid_beetle.n.01", "name": "clerid_beetle"}, + {"id": 3693, "synset": "bee_beetle.n.01", "name": "bee_beetle"}, + {"id": 3694, "synset": "lamellicorn_beetle.n.01", "name": "lamellicorn_beetle"}, + {"id": 3695, "synset": "scarabaeid_beetle.n.01", "name": "scarabaeid_beetle"}, + {"id": 3696, "synset": "dung_beetle.n.01", "name": "dung_beetle"}, + {"id": 3697, "synset": "scarab.n.01", "name": "scarab"}, + {"id": 3698, "synset": "tumblebug.n.01", "name": "tumblebug"}, + {"id": 3699, "synset": "dorbeetle.n.01", "name": "dorbeetle"}, + {"id": 3700, "synset": "june_beetle.n.01", "name": "June_beetle"}, + {"id": 3701, "synset": "green_june_beetle.n.01", "name": "green_June_beetle"}, + {"id": 3702, "synset": "japanese_beetle.n.01", "name": "Japanese_beetle"}, + {"id": 3703, "synset": "oriental_beetle.n.01", "name": "Oriental_beetle"}, + {"id": 3704, "synset": "rhinoceros_beetle.n.01", "name": "rhinoceros_beetle"}, + {"id": 3705, "synset": "melolonthid_beetle.n.01", "name": "melolonthid_beetle"}, + {"id": 3706, "synset": "cockchafer.n.01", "name": "cockchafer"}, + {"id": 3707, "synset": "rose_chafer.n.02", "name": "rose_chafer"}, + {"id": 3708, "synset": "rose_chafer.n.01", "name": "rose_chafer"}, + {"id": 3709, "synset": "stag_beetle.n.01", "name": "stag_beetle"}, + {"id": 3710, "synset": "elaterid_beetle.n.01", "name": "elaterid_beetle"}, + {"id": 3711, "synset": "click_beetle.n.01", "name": "click_beetle"}, + {"id": 3712, "synset": "firefly.n.01", "name": "firefly"}, + {"id": 3713, "synset": "wireworm.n.01", "name": "wireworm"}, + {"id": 3714, "synset": "water_beetle.n.01", "name": "water_beetle"}, + {"id": 3715, "synset": "whirligig_beetle.n.01", "name": "whirligig_beetle"}, + {"id": 3716, "synset": "deathwatch_beetle.n.01", "name": "deathwatch_beetle"}, + {"id": 3717, "synset": "weevil.n.01", "name": "weevil"}, + {"id": 3718, "synset": "snout_beetle.n.01", "name": "snout_beetle"}, + {"id": 3719, "synset": "boll_weevil.n.01", "name": "boll_weevil"}, + {"id": 3720, "synset": "blister_beetle.n.01", "name": "blister_beetle"}, + {"id": 3721, "synset": "oil_beetle.n.01", "name": "oil_beetle"}, + {"id": 3722, "synset": "spanish_fly.n.01", "name": "Spanish_fly"}, + {"id": 3723, "synset": "dutch-elm_beetle.n.01", "name": "Dutch-elm_beetle"}, + {"id": 3724, "synset": "bark_beetle.n.01", "name": "bark_beetle"}, + {"id": 3725, "synset": "spruce_bark_beetle.n.01", "name": "spruce_bark_beetle"}, + {"id": 3726, "synset": "rove_beetle.n.01", "name": "rove_beetle"}, + {"id": 3727, "synset": "darkling_beetle.n.01", "name": "darkling_beetle"}, + {"id": 3728, "synset": "mealworm.n.01", "name": "mealworm"}, + {"id": 3729, "synset": "flour_beetle.n.01", "name": "flour_beetle"}, + {"id": 3730, "synset": "seed_beetle.n.01", "name": "seed_beetle"}, + {"id": 3731, "synset": "pea_weevil.n.01", "name": "pea_weevil"}, + {"id": 3732, "synset": "bean_weevil.n.01", "name": "bean_weevil"}, + {"id": 3733, "synset": "rice_weevil.n.01", "name": "rice_weevil"}, + {"id": 3734, "synset": "asian_longhorned_beetle.n.01", "name": "Asian_longhorned_beetle"}, + {"id": 3735, "synset": "web_spinner.n.01", "name": "web_spinner"}, + {"id": 3736, "synset": "louse.n.01", "name": "louse"}, + {"id": 3737, "synset": "common_louse.n.01", "name": "common_louse"}, + {"id": 3738, "synset": "head_louse.n.01", "name": "head_louse"}, + {"id": 3739, "synset": "body_louse.n.01", "name": "body_louse"}, + {"id": 3740, "synset": "crab_louse.n.01", "name": "crab_louse"}, + {"id": 3741, "synset": "bird_louse.n.01", "name": "bird_louse"}, + {"id": 3742, "synset": "flea.n.01", "name": "flea"}, + {"id": 3743, "synset": "pulex_irritans.n.01", "name": "Pulex_irritans"}, + {"id": 3744, "synset": "dog_flea.n.01", "name": "dog_flea"}, + {"id": 3745, "synset": "cat_flea.n.01", "name": "cat_flea"}, + {"id": 3746, "synset": "chigoe.n.01", "name": "chigoe"}, + {"id": 3747, "synset": "sticktight.n.02", "name": "sticktight"}, + {"id": 3748, "synset": "dipterous_insect.n.01", "name": "dipterous_insect"}, + {"id": 3749, "synset": "gall_midge.n.01", "name": "gall_midge"}, + {"id": 3750, "synset": "hessian_fly.n.01", "name": "Hessian_fly"}, + {"id": 3751, "synset": "fly.n.01", "name": "fly"}, + {"id": 3752, "synset": "housefly.n.01", "name": "housefly"}, + {"id": 3753, "synset": "tsetse_fly.n.01", "name": "tsetse_fly"}, + {"id": 3754, "synset": "blowfly.n.01", "name": "blowfly"}, + {"id": 3755, "synset": "bluebottle.n.02", "name": "bluebottle"}, + {"id": 3756, "synset": "greenbottle.n.01", "name": "greenbottle"}, + {"id": 3757, "synset": "flesh_fly.n.01", "name": "flesh_fly"}, + {"id": 3758, "synset": "tachina_fly.n.01", "name": "tachina_fly"}, + {"id": 3759, "synset": "gadfly.n.02", "name": "gadfly"}, + {"id": 3760, "synset": "botfly.n.01", "name": "botfly"}, + {"id": 3761, "synset": "human_botfly.n.01", "name": "human_botfly"}, + {"id": 3762, "synset": "sheep_botfly.n.01", "name": "sheep_botfly"}, + {"id": 3763, "synset": "warble_fly.n.01", "name": "warble_fly"}, + {"id": 3764, "synset": "horsefly.n.02", "name": "horsefly"}, + {"id": 3765, "synset": "bee_fly.n.01", "name": "bee_fly"}, + {"id": 3766, "synset": "robber_fly.n.01", "name": "robber_fly"}, + {"id": 3767, "synset": "fruit_fly.n.01", "name": "fruit_fly"}, + {"id": 3768, "synset": "apple_maggot.n.01", "name": "apple_maggot"}, + {"id": 3769, "synset": "mediterranean_fruit_fly.n.01", "name": "Mediterranean_fruit_fly"}, + {"id": 3770, "synset": "drosophila.n.01", "name": "drosophila"}, + {"id": 3771, "synset": "vinegar_fly.n.01", "name": "vinegar_fly"}, + {"id": 3772, "synset": "leaf_miner.n.01", "name": "leaf_miner"}, + {"id": 3773, "synset": "louse_fly.n.01", "name": "louse_fly"}, + {"id": 3774, "synset": "horse_tick.n.01", "name": "horse_tick"}, + {"id": 3775, "synset": "sheep_ked.n.01", "name": "sheep_ked"}, + {"id": 3776, "synset": "horn_fly.n.01", "name": "horn_fly"}, + {"id": 3777, "synset": "mosquito.n.01", "name": "mosquito"}, + {"id": 3778, "synset": "wiggler.n.02", "name": "wiggler"}, + {"id": 3779, "synset": "gnat.n.02", "name": "gnat"}, + {"id": 3780, "synset": "yellow-fever_mosquito.n.01", "name": "yellow-fever_mosquito"}, + {"id": 3781, "synset": "asian_tiger_mosquito.n.01", "name": "Asian_tiger_mosquito"}, + {"id": 3782, "synset": "anopheline.n.01", "name": "anopheline"}, + {"id": 3783, "synset": "malarial_mosquito.n.01", "name": "malarial_mosquito"}, + {"id": 3784, "synset": "common_mosquito.n.01", "name": "common_mosquito"}, + {"id": 3785, "synset": "culex_quinquefasciatus.n.01", "name": "Culex_quinquefasciatus"}, + {"id": 3786, "synset": "gnat.n.01", "name": "gnat"}, + {"id": 3787, "synset": "punkie.n.01", "name": "punkie"}, + {"id": 3788, "synset": "midge.n.01", "name": "midge"}, + {"id": 3789, "synset": "fungus_gnat.n.02", "name": "fungus_gnat"}, + {"id": 3790, "synset": "psychodid.n.01", "name": "psychodid"}, + {"id": 3791, "synset": "sand_fly.n.01", "name": "sand_fly"}, + {"id": 3792, "synset": "fungus_gnat.n.01", "name": "fungus_gnat"}, + {"id": 3793, "synset": "armyworm.n.03", "name": "armyworm"}, + {"id": 3794, "synset": "crane_fly.n.01", "name": "crane_fly"}, + {"id": 3795, "synset": "blackfly.n.02", "name": "blackfly"}, + {"id": 3796, "synset": "hymenopterous_insect.n.01", "name": "hymenopterous_insect"}, + {"id": 3797, "synset": "bee.n.01", "name": "bee"}, + {"id": 3798, "synset": "drone.n.01", "name": "drone"}, + {"id": 3799, "synset": "queen_bee.n.01", "name": "queen_bee"}, + {"id": 3800, "synset": "worker.n.03", "name": "worker"}, + {"id": 3801, "synset": "soldier.n.02", "name": "soldier"}, + {"id": 3802, "synset": "worker_bee.n.01", "name": "worker_bee"}, + {"id": 3803, "synset": "honeybee.n.01", "name": "honeybee"}, + {"id": 3804, "synset": "africanized_bee.n.01", "name": "Africanized_bee"}, + {"id": 3805, "synset": "black_bee.n.01", "name": "black_bee"}, + {"id": 3806, "synset": "carniolan_bee.n.01", "name": "Carniolan_bee"}, + {"id": 3807, "synset": "italian_bee.n.01", "name": "Italian_bee"}, + {"id": 3808, "synset": "carpenter_bee.n.01", "name": "carpenter_bee"}, + {"id": 3809, "synset": "bumblebee.n.01", "name": "bumblebee"}, + {"id": 3810, "synset": "cuckoo-bumblebee.n.01", "name": "cuckoo-bumblebee"}, + {"id": 3811, "synset": "andrena.n.01", "name": "andrena"}, + {"id": 3812, "synset": "nomia_melanderi.n.01", "name": "Nomia_melanderi"}, + {"id": 3813, "synset": "leaf-cutting_bee.n.01", "name": "leaf-cutting_bee"}, + {"id": 3814, "synset": "mason_bee.n.01", "name": "mason_bee"}, + {"id": 3815, "synset": "potter_bee.n.01", "name": "potter_bee"}, + {"id": 3816, "synset": "wasp.n.02", "name": "wasp"}, + {"id": 3817, "synset": "vespid.n.01", "name": "vespid"}, + {"id": 3818, "synset": "paper_wasp.n.01", "name": "paper_wasp"}, + {"id": 3819, "synset": "giant_hornet.n.01", "name": "giant_hornet"}, + {"id": 3820, "synset": "common_wasp.n.01", "name": "common_wasp"}, + {"id": 3821, "synset": "bald-faced_hornet.n.01", "name": "bald-faced_hornet"}, + {"id": 3822, "synset": "yellow_jacket.n.02", "name": "yellow_jacket"}, + {"id": 3823, "synset": "polistes_annularis.n.01", "name": "Polistes_annularis"}, + {"id": 3824, "synset": "mason_wasp.n.02", "name": "mason_wasp"}, + {"id": 3825, "synset": "potter_wasp.n.01", "name": "potter_wasp"}, + {"id": 3826, "synset": "mutillidae.n.01", "name": "Mutillidae"}, + {"id": 3827, "synset": "velvet_ant.n.01", "name": "velvet_ant"}, + {"id": 3828, "synset": "sphecoid_wasp.n.01", "name": "sphecoid_wasp"}, + {"id": 3829, "synset": "mason_wasp.n.01", "name": "mason_wasp"}, + {"id": 3830, "synset": "digger_wasp.n.01", "name": "digger_wasp"}, + {"id": 3831, "synset": "cicada_killer.n.01", "name": "cicada_killer"}, + {"id": 3832, "synset": "mud_dauber.n.01", "name": "mud_dauber"}, + {"id": 3833, "synset": "gall_wasp.n.01", "name": "gall_wasp"}, + {"id": 3834, "synset": "chalcid_fly.n.01", "name": "chalcid_fly"}, + {"id": 3835, "synset": "strawworm.n.02", "name": "strawworm"}, + {"id": 3836, "synset": "chalcis_fly.n.01", "name": "chalcis_fly"}, + {"id": 3837, "synset": "ichneumon_fly.n.01", "name": "ichneumon_fly"}, + {"id": 3838, "synset": "sawfly.n.01", "name": "sawfly"}, + {"id": 3839, "synset": "birch_leaf_miner.n.01", "name": "birch_leaf_miner"}, + {"id": 3840, "synset": "ant.n.01", "name": "ant"}, + {"id": 3841, "synset": "pharaoh_ant.n.01", "name": "pharaoh_ant"}, + {"id": 3842, "synset": "little_black_ant.n.01", "name": "little_black_ant"}, + {"id": 3843, "synset": "army_ant.n.01", "name": "army_ant"}, + {"id": 3844, "synset": "carpenter_ant.n.01", "name": "carpenter_ant"}, + {"id": 3845, "synset": "fire_ant.n.01", "name": "fire_ant"}, + {"id": 3846, "synset": "wood_ant.n.01", "name": "wood_ant"}, + {"id": 3847, "synset": "slave_ant.n.01", "name": "slave_ant"}, + {"id": 3848, "synset": "formica_fusca.n.01", "name": "Formica_fusca"}, + {"id": 3849, "synset": "slave-making_ant.n.01", "name": "slave-making_ant"}, + {"id": 3850, "synset": "sanguinary_ant.n.01", "name": "sanguinary_ant"}, + {"id": 3851, "synset": "bulldog_ant.n.01", "name": "bulldog_ant"}, + {"id": 3852, "synset": "amazon_ant.n.01", "name": "Amazon_ant"}, + {"id": 3853, "synset": "termite.n.01", "name": "termite"}, + {"id": 3854, "synset": "dry-wood_termite.n.01", "name": "dry-wood_termite"}, + {"id": 3855, "synset": "reticulitermes_lucifugus.n.01", "name": "Reticulitermes_lucifugus"}, + {"id": 3856, "synset": "mastotermes_darwiniensis.n.01", "name": "Mastotermes_darwiniensis"}, + { + "id": 3857, + "synset": "mastotermes_electrodominicus.n.01", + "name": "Mastotermes_electrodominicus", + }, + {"id": 3858, "synset": "powder-post_termite.n.01", "name": "powder-post_termite"}, + {"id": 3859, "synset": "orthopterous_insect.n.01", "name": "orthopterous_insect"}, + {"id": 3860, "synset": "grasshopper.n.01", "name": "grasshopper"}, + {"id": 3861, "synset": "short-horned_grasshopper.n.01", "name": "short-horned_grasshopper"}, + {"id": 3862, "synset": "locust.n.01", "name": "locust"}, + {"id": 3863, "synset": "migratory_locust.n.01", "name": "migratory_locust"}, + {"id": 3864, "synset": "migratory_grasshopper.n.01", "name": "migratory_grasshopper"}, + {"id": 3865, "synset": "long-horned_grasshopper.n.01", "name": "long-horned_grasshopper"}, + {"id": 3866, "synset": "katydid.n.01", "name": "katydid"}, + {"id": 3867, "synset": "mormon_cricket.n.01", "name": "mormon_cricket"}, + {"id": 3868, "synset": "sand_cricket.n.01", "name": "sand_cricket"}, + {"id": 3869, "synset": "cricket.n.01", "name": "cricket"}, + {"id": 3870, "synset": "mole_cricket.n.01", "name": "mole_cricket"}, + {"id": 3871, "synset": "european_house_cricket.n.01", "name": "European_house_cricket"}, + {"id": 3872, "synset": "field_cricket.n.01", "name": "field_cricket"}, + {"id": 3873, "synset": "tree_cricket.n.01", "name": "tree_cricket"}, + {"id": 3874, "synset": "snowy_tree_cricket.n.01", "name": "snowy_tree_cricket"}, + {"id": 3875, "synset": "phasmid.n.01", "name": "phasmid"}, + {"id": 3876, "synset": "walking_stick.n.02", "name": "walking_stick"}, + {"id": 3877, "synset": "diapheromera.n.01", "name": "diapheromera"}, + {"id": 3878, "synset": "walking_leaf.n.02", "name": "walking_leaf"}, + {"id": 3879, "synset": "oriental_cockroach.n.01", "name": "oriental_cockroach"}, + {"id": 3880, "synset": "american_cockroach.n.01", "name": "American_cockroach"}, + {"id": 3881, "synset": "australian_cockroach.n.01", "name": "Australian_cockroach"}, + {"id": 3882, "synset": "german_cockroach.n.01", "name": "German_cockroach"}, + {"id": 3883, "synset": "giant_cockroach.n.01", "name": "giant_cockroach"}, + {"id": 3884, "synset": "mantis.n.01", "name": "mantis"}, + {"id": 3885, "synset": "praying_mantis.n.01", "name": "praying_mantis"}, + {"id": 3886, "synset": "bug.n.01", "name": "bug"}, + {"id": 3887, "synset": "hemipterous_insect.n.01", "name": "hemipterous_insect"}, + {"id": 3888, "synset": "leaf_bug.n.01", "name": "leaf_bug"}, + {"id": 3889, "synset": "mirid_bug.n.01", "name": "mirid_bug"}, + {"id": 3890, "synset": "four-lined_plant_bug.n.01", "name": "four-lined_plant_bug"}, + {"id": 3891, "synset": "lygus_bug.n.01", "name": "lygus_bug"}, + {"id": 3892, "synset": "tarnished_plant_bug.n.01", "name": "tarnished_plant_bug"}, + {"id": 3893, "synset": "lace_bug.n.01", "name": "lace_bug"}, + {"id": 3894, "synset": "lygaeid.n.01", "name": "lygaeid"}, + {"id": 3895, "synset": "chinch_bug.n.01", "name": "chinch_bug"}, + {"id": 3896, "synset": "coreid_bug.n.01", "name": "coreid_bug"}, + {"id": 3897, "synset": "squash_bug.n.01", "name": "squash_bug"}, + {"id": 3898, "synset": "leaf-footed_bug.n.01", "name": "leaf-footed_bug"}, + {"id": 3899, "synset": "bedbug.n.01", "name": "bedbug"}, + {"id": 3900, "synset": "backswimmer.n.01", "name": "backswimmer"}, + {"id": 3901, "synset": "true_bug.n.01", "name": "true_bug"}, + {"id": 3902, "synset": "heteropterous_insect.n.01", "name": "heteropterous_insect"}, + {"id": 3903, "synset": "water_bug.n.01", "name": "water_bug"}, + {"id": 3904, "synset": "giant_water_bug.n.01", "name": "giant_water_bug"}, + {"id": 3905, "synset": "water_scorpion.n.01", "name": "water_scorpion"}, + {"id": 3906, "synset": "water_boatman.n.01", "name": "water_boatman"}, + {"id": 3907, "synset": "water_strider.n.01", "name": "water_strider"}, + {"id": 3908, "synset": "common_pond-skater.n.01", "name": "common_pond-skater"}, + {"id": 3909, "synset": "assassin_bug.n.01", "name": "assassin_bug"}, + {"id": 3910, "synset": "conenose.n.01", "name": "conenose"}, + {"id": 3911, "synset": "wheel_bug.n.01", "name": "wheel_bug"}, + {"id": 3912, "synset": "firebug.n.02", "name": "firebug"}, + {"id": 3913, "synset": "cotton_stainer.n.01", "name": "cotton_stainer"}, + {"id": 3914, "synset": "homopterous_insect.n.01", "name": "homopterous_insect"}, + {"id": 3915, "synset": "whitefly.n.01", "name": "whitefly"}, + {"id": 3916, "synset": "citrus_whitefly.n.01", "name": "citrus_whitefly"}, + {"id": 3917, "synset": "greenhouse_whitefly.n.01", "name": "greenhouse_whitefly"}, + {"id": 3918, "synset": "sweet-potato_whitefly.n.01", "name": "sweet-potato_whitefly"}, + {"id": 3919, "synset": "superbug.n.02", "name": "superbug"}, + {"id": 3920, "synset": "cotton_strain.n.01", "name": "cotton_strain"}, + {"id": 3921, "synset": "coccid_insect.n.01", "name": "coccid_insect"}, + {"id": 3922, "synset": "scale_insect.n.01", "name": "scale_insect"}, + {"id": 3923, "synset": "soft_scale.n.01", "name": "soft_scale"}, + {"id": 3924, "synset": "brown_soft_scale.n.01", "name": "brown_soft_scale"}, + {"id": 3925, "synset": "armored_scale.n.01", "name": "armored_scale"}, + {"id": 3926, "synset": "san_jose_scale.n.01", "name": "San_Jose_scale"}, + {"id": 3927, "synset": "cochineal_insect.n.01", "name": "cochineal_insect"}, + {"id": 3928, "synset": "mealybug.n.01", "name": "mealybug"}, + {"id": 3929, "synset": "citrophilous_mealybug.n.01", "name": "citrophilous_mealybug"}, + {"id": 3930, "synset": "comstock_mealybug.n.01", "name": "Comstock_mealybug"}, + {"id": 3931, "synset": "citrus_mealybug.n.01", "name": "citrus_mealybug"}, + {"id": 3932, "synset": "plant_louse.n.01", "name": "plant_louse"}, + {"id": 3933, "synset": "aphid.n.01", "name": "aphid"}, + {"id": 3934, "synset": "apple_aphid.n.01", "name": "apple_aphid"}, + {"id": 3935, "synset": "blackfly.n.01", "name": "blackfly"}, + {"id": 3936, "synset": "greenfly.n.01", "name": "greenfly"}, + {"id": 3937, "synset": "green_peach_aphid.n.01", "name": "green_peach_aphid"}, + {"id": 3938, "synset": "ant_cow.n.01", "name": "ant_cow"}, + {"id": 3939, "synset": "woolly_aphid.n.01", "name": "woolly_aphid"}, + {"id": 3940, "synset": "woolly_apple_aphid.n.01", "name": "woolly_apple_aphid"}, + {"id": 3941, "synset": "woolly_alder_aphid.n.01", "name": "woolly_alder_aphid"}, + {"id": 3942, "synset": "adelgid.n.01", "name": "adelgid"}, + {"id": 3943, "synset": "balsam_woolly_aphid.n.01", "name": "balsam_woolly_aphid"}, + {"id": 3944, "synset": "spruce_gall_aphid.n.01", "name": "spruce_gall_aphid"}, + {"id": 3945, "synset": "woolly_adelgid.n.01", "name": "woolly_adelgid"}, + {"id": 3946, "synset": "jumping_plant_louse.n.01", "name": "jumping_plant_louse"}, + {"id": 3947, "synset": "cicada.n.01", "name": "cicada"}, + {"id": 3948, "synset": "dog-day_cicada.n.01", "name": "dog-day_cicada"}, + {"id": 3949, "synset": "seventeen-year_locust.n.01", "name": "seventeen-year_locust"}, + {"id": 3950, "synset": "spittle_insect.n.01", "name": "spittle_insect"}, + {"id": 3951, "synset": "froghopper.n.01", "name": "froghopper"}, + {"id": 3952, "synset": "meadow_spittlebug.n.01", "name": "meadow_spittlebug"}, + {"id": 3953, "synset": "pine_spittlebug.n.01", "name": "pine_spittlebug"}, + {"id": 3954, "synset": "saratoga_spittlebug.n.01", "name": "Saratoga_spittlebug"}, + {"id": 3955, "synset": "leafhopper.n.01", "name": "leafhopper"}, + {"id": 3956, "synset": "plant_hopper.n.01", "name": "plant_hopper"}, + {"id": 3957, "synset": "treehopper.n.01", "name": "treehopper"}, + {"id": 3958, "synset": "lantern_fly.n.01", "name": "lantern_fly"}, + {"id": 3959, "synset": "psocopterous_insect.n.01", "name": "psocopterous_insect"}, + {"id": 3960, "synset": "psocid.n.01", "name": "psocid"}, + {"id": 3961, "synset": "bark-louse.n.01", "name": "bark-louse"}, + {"id": 3962, "synset": "booklouse.n.01", "name": "booklouse"}, + {"id": 3963, "synset": "common_booklouse.n.01", "name": "common_booklouse"}, + {"id": 3964, "synset": "ephemerid.n.01", "name": "ephemerid"}, + {"id": 3965, "synset": "mayfly.n.01", "name": "mayfly"}, + {"id": 3966, "synset": "stonefly.n.01", "name": "stonefly"}, + {"id": 3967, "synset": "neuropteron.n.01", "name": "neuropteron"}, + {"id": 3968, "synset": "ant_lion.n.02", "name": "ant_lion"}, + {"id": 3969, "synset": "doodlebug.n.03", "name": "doodlebug"}, + {"id": 3970, "synset": "lacewing.n.01", "name": "lacewing"}, + {"id": 3971, "synset": "aphid_lion.n.01", "name": "aphid_lion"}, + {"id": 3972, "synset": "green_lacewing.n.01", "name": "green_lacewing"}, + {"id": 3973, "synset": "brown_lacewing.n.01", "name": "brown_lacewing"}, + {"id": 3974, "synset": "dobson.n.02", "name": "dobson"}, + {"id": 3975, "synset": "hellgrammiate.n.01", "name": "hellgrammiate"}, + {"id": 3976, "synset": "fish_fly.n.01", "name": "fish_fly"}, + {"id": 3977, "synset": "alderfly.n.01", "name": "alderfly"}, + {"id": 3978, "synset": "snakefly.n.01", "name": "snakefly"}, + {"id": 3979, "synset": "mantispid.n.01", "name": "mantispid"}, + {"id": 3980, "synset": "odonate.n.01", "name": "odonate"}, + {"id": 3981, "synset": "damselfly.n.01", "name": "damselfly"}, + {"id": 3982, "synset": "trichopterous_insect.n.01", "name": "trichopterous_insect"}, + {"id": 3983, "synset": "caddis_fly.n.01", "name": "caddis_fly"}, + {"id": 3984, "synset": "caseworm.n.01", "name": "caseworm"}, + {"id": 3985, "synset": "caddisworm.n.01", "name": "caddisworm"}, + {"id": 3986, "synset": "thysanuran_insect.n.01", "name": "thysanuran_insect"}, + {"id": 3987, "synset": "bristletail.n.01", "name": "bristletail"}, + {"id": 3988, "synset": "silverfish.n.01", "name": "silverfish"}, + {"id": 3989, "synset": "firebrat.n.01", "name": "firebrat"}, + {"id": 3990, "synset": "jumping_bristletail.n.01", "name": "jumping_bristletail"}, + {"id": 3991, "synset": "thysanopter.n.01", "name": "thysanopter"}, + {"id": 3992, "synset": "thrips.n.01", "name": "thrips"}, + {"id": 3993, "synset": "tobacco_thrips.n.01", "name": "tobacco_thrips"}, + {"id": 3994, "synset": "onion_thrips.n.01", "name": "onion_thrips"}, + {"id": 3995, "synset": "earwig.n.01", "name": "earwig"}, + {"id": 3996, "synset": "common_european_earwig.n.01", "name": "common_European_earwig"}, + {"id": 3997, "synset": "lepidopterous_insect.n.01", "name": "lepidopterous_insect"}, + {"id": 3998, "synset": "nymphalid.n.01", "name": "nymphalid"}, + {"id": 3999, "synset": "mourning_cloak.n.01", "name": "mourning_cloak"}, + {"id": 4000, "synset": "tortoiseshell.n.02", "name": "tortoiseshell"}, + {"id": 4001, "synset": "painted_beauty.n.01", "name": "painted_beauty"}, + {"id": 4002, "synset": "admiral.n.02", "name": "admiral"}, + {"id": 4003, "synset": "red_admiral.n.01", "name": "red_admiral"}, + {"id": 4004, "synset": "white_admiral.n.02", "name": "white_admiral"}, + {"id": 4005, "synset": "banded_purple.n.01", "name": "banded_purple"}, + {"id": 4006, "synset": "red-spotted_purple.n.01", "name": "red-spotted_purple"}, + {"id": 4007, "synset": "viceroy.n.02", "name": "viceroy"}, + {"id": 4008, "synset": "anglewing.n.01", "name": "anglewing"}, + {"id": 4009, "synset": "ringlet.n.04", "name": "ringlet"}, + {"id": 4010, "synset": "comma.n.02", "name": "comma"}, + {"id": 4011, "synset": "fritillary.n.02", "name": "fritillary"}, + {"id": 4012, "synset": "silverspot.n.01", "name": "silverspot"}, + {"id": 4013, "synset": "emperor_butterfly.n.01", "name": "emperor_butterfly"}, + {"id": 4014, "synset": "purple_emperor.n.01", "name": "purple_emperor"}, + {"id": 4015, "synset": "peacock.n.01", "name": "peacock"}, + {"id": 4016, "synset": "danaid.n.01", "name": "danaid"}, + {"id": 4017, "synset": "monarch.n.02", "name": "monarch"}, + {"id": 4018, "synset": "pierid.n.01", "name": "pierid"}, + {"id": 4019, "synset": "cabbage_butterfly.n.01", "name": "cabbage_butterfly"}, + {"id": 4020, "synset": "small_white.n.01", "name": "small_white"}, + {"id": 4021, "synset": "large_white.n.01", "name": "large_white"}, + {"id": 4022, "synset": "southern_cabbage_butterfly.n.01", "name": "southern_cabbage_butterfly"}, + {"id": 4023, "synset": "sulphur_butterfly.n.01", "name": "sulphur_butterfly"}, + {"id": 4024, "synset": "lycaenid.n.01", "name": "lycaenid"}, + {"id": 4025, "synset": "blue.n.07", "name": "blue"}, + {"id": 4026, "synset": "copper.n.05", "name": "copper"}, + {"id": 4027, "synset": "american_copper.n.01", "name": "American_copper"}, + {"id": 4028, "synset": "hairstreak.n.01", "name": "hairstreak"}, + {"id": 4029, "synset": "strymon_melinus.n.01", "name": "Strymon_melinus"}, + {"id": 4030, "synset": "moth.n.01", "name": "moth"}, + {"id": 4031, "synset": "moth_miller.n.01", "name": "moth_miller"}, + {"id": 4032, "synset": "tortricid.n.01", "name": "tortricid"}, + {"id": 4033, "synset": "leaf_roller.n.01", "name": "leaf_roller"}, + {"id": 4034, "synset": "tea_tortrix.n.01", "name": "tea_tortrix"}, + {"id": 4035, "synset": "orange_tortrix.n.01", "name": "orange_tortrix"}, + {"id": 4036, "synset": "codling_moth.n.01", "name": "codling_moth"}, + {"id": 4037, "synset": "lymantriid.n.01", "name": "lymantriid"}, + {"id": 4038, "synset": "tussock_caterpillar.n.01", "name": "tussock_caterpillar"}, + {"id": 4039, "synset": "gypsy_moth.n.01", "name": "gypsy_moth"}, + {"id": 4040, "synset": "browntail.n.01", "name": "browntail"}, + {"id": 4041, "synset": "gold-tail_moth.n.01", "name": "gold-tail_moth"}, + {"id": 4042, "synset": "geometrid.n.01", "name": "geometrid"}, + {"id": 4043, "synset": "paleacrita_vernata.n.01", "name": "Paleacrita_vernata"}, + {"id": 4044, "synset": "alsophila_pometaria.n.01", "name": "Alsophila_pometaria"}, + {"id": 4045, "synset": "cankerworm.n.01", "name": "cankerworm"}, + {"id": 4046, "synset": "spring_cankerworm.n.01", "name": "spring_cankerworm"}, + {"id": 4047, "synset": "fall_cankerworm.n.01", "name": "fall_cankerworm"}, + {"id": 4048, "synset": "measuring_worm.n.01", "name": "measuring_worm"}, + {"id": 4049, "synset": "pyralid.n.01", "name": "pyralid"}, + {"id": 4050, "synset": "bee_moth.n.01", "name": "bee_moth"}, + {"id": 4051, "synset": "corn_borer.n.02", "name": "corn_borer"}, + {"id": 4052, "synset": "mediterranean_flour_moth.n.01", "name": "Mediterranean_flour_moth"}, + {"id": 4053, "synset": "tobacco_moth.n.01", "name": "tobacco_moth"}, + {"id": 4054, "synset": "almond_moth.n.01", "name": "almond_moth"}, + {"id": 4055, "synset": "raisin_moth.n.01", "name": "raisin_moth"}, + {"id": 4056, "synset": "tineoid.n.01", "name": "tineoid"}, + {"id": 4057, "synset": "tineid.n.01", "name": "tineid"}, + {"id": 4058, "synset": "clothes_moth.n.01", "name": "clothes_moth"}, + {"id": 4059, "synset": "casemaking_clothes_moth.n.01", "name": "casemaking_clothes_moth"}, + {"id": 4060, "synset": "webbing_clothes_moth.n.01", "name": "webbing_clothes_moth"}, + {"id": 4061, "synset": "carpet_moth.n.01", "name": "carpet_moth"}, + {"id": 4062, "synset": "gelechiid.n.01", "name": "gelechiid"}, + {"id": 4063, "synset": "grain_moth.n.01", "name": "grain_moth"}, + {"id": 4064, "synset": "angoumois_moth.n.01", "name": "angoumois_moth"}, + {"id": 4065, "synset": "potato_moth.n.01", "name": "potato_moth"}, + {"id": 4066, "synset": "potato_tuberworm.n.01", "name": "potato_tuberworm"}, + {"id": 4067, "synset": "noctuid_moth.n.01", "name": "noctuid_moth"}, + {"id": 4068, "synset": "cutworm.n.01", "name": "cutworm"}, + {"id": 4069, "synset": "underwing.n.01", "name": "underwing"}, + {"id": 4070, "synset": "red_underwing.n.01", "name": "red_underwing"}, + {"id": 4071, "synset": "antler_moth.n.01", "name": "antler_moth"}, + {"id": 4072, "synset": "heliothis_moth.n.01", "name": "heliothis_moth"}, + {"id": 4073, "synset": "army_cutworm.n.01", "name": "army_cutworm"}, + {"id": 4074, "synset": "armyworm.n.02", "name": "armyworm"}, + {"id": 4075, "synset": "armyworm.n.01", "name": "armyworm"}, + {"id": 4076, "synset": "spodoptera_exigua.n.02", "name": "Spodoptera_exigua"}, + {"id": 4077, "synset": "beet_armyworm.n.01", "name": "beet_armyworm"}, + {"id": 4078, "synset": "spodoptera_frugiperda.n.02", "name": "Spodoptera_frugiperda"}, + {"id": 4079, "synset": "fall_armyworm.n.01", "name": "fall_armyworm"}, + {"id": 4080, "synset": "hawkmoth.n.01", "name": "hawkmoth"}, + {"id": 4081, "synset": "manduca_sexta.n.02", "name": "Manduca_sexta"}, + {"id": 4082, "synset": "tobacco_hornworm.n.01", "name": "tobacco_hornworm"}, + {"id": 4083, "synset": "manduca_quinquemaculata.n.02", "name": "Manduca_quinquemaculata"}, + {"id": 4084, "synset": "tomato_hornworm.n.01", "name": "tomato_hornworm"}, + {"id": 4085, "synset": "death's-head_moth.n.01", "name": "death's-head_moth"}, + {"id": 4086, "synset": "bombycid.n.01", "name": "bombycid"}, + {"id": 4087, "synset": "domestic_silkworm_moth.n.01", "name": "domestic_silkworm_moth"}, + {"id": 4088, "synset": "silkworm.n.01", "name": "silkworm"}, + {"id": 4089, "synset": "saturniid.n.01", "name": "saturniid"}, + {"id": 4090, "synset": "emperor.n.03", "name": "emperor"}, + {"id": 4091, "synset": "imperial_moth.n.01", "name": "imperial_moth"}, + {"id": 4092, "synset": "giant_silkworm_moth.n.01", "name": "giant_silkworm_moth"}, + {"id": 4093, "synset": "silkworm.n.02", "name": "silkworm"}, + {"id": 4094, "synset": "luna_moth.n.01", "name": "luna_moth"}, + {"id": 4095, "synset": "cecropia.n.02", "name": "cecropia"}, + {"id": 4096, "synset": "cynthia_moth.n.01", "name": "cynthia_moth"}, + {"id": 4097, "synset": "ailanthus_silkworm.n.01", "name": "ailanthus_silkworm"}, + {"id": 4098, "synset": "io_moth.n.01", "name": "io_moth"}, + {"id": 4099, "synset": "polyphemus_moth.n.01", "name": "polyphemus_moth"}, + {"id": 4100, "synset": "pernyi_moth.n.01", "name": "pernyi_moth"}, + {"id": 4101, "synset": "tussah.n.01", "name": "tussah"}, + {"id": 4102, "synset": "atlas_moth.n.01", "name": "atlas_moth"}, + {"id": 4103, "synset": "arctiid.n.01", "name": "arctiid"}, + {"id": 4104, "synset": "tiger_moth.n.01", "name": "tiger_moth"}, + {"id": 4105, "synset": "cinnabar.n.02", "name": "cinnabar"}, + {"id": 4106, "synset": "lasiocampid.n.01", "name": "lasiocampid"}, + {"id": 4107, "synset": "eggar.n.01", "name": "eggar"}, + {"id": 4108, "synset": "tent-caterpillar_moth.n.02", "name": "tent-caterpillar_moth"}, + {"id": 4109, "synset": "tent_caterpillar.n.01", "name": "tent_caterpillar"}, + {"id": 4110, "synset": "tent-caterpillar_moth.n.01", "name": "tent-caterpillar_moth"}, + {"id": 4111, "synset": "forest_tent_caterpillar.n.01", "name": "forest_tent_caterpillar"}, + {"id": 4112, "synset": "lappet.n.03", "name": "lappet"}, + {"id": 4113, "synset": "lappet_caterpillar.n.01", "name": "lappet_caterpillar"}, + {"id": 4114, "synset": "webworm.n.01", "name": "webworm"}, + {"id": 4115, "synset": "webworm_moth.n.01", "name": "webworm_moth"}, + {"id": 4116, "synset": "hyphantria_cunea.n.02", "name": "Hyphantria_cunea"}, + {"id": 4117, "synset": "fall_webworm.n.01", "name": "fall_webworm"}, + {"id": 4118, "synset": "garden_webworm.n.01", "name": "garden_webworm"}, + {"id": 4119, "synset": "instar.n.01", "name": "instar"}, + {"id": 4120, "synset": "caterpillar.n.01", "name": "caterpillar"}, + {"id": 4121, "synset": "corn_borer.n.01", "name": "corn_borer"}, + {"id": 4122, "synset": "bollworm.n.01", "name": "bollworm"}, + {"id": 4123, "synset": "pink_bollworm.n.01", "name": "pink_bollworm"}, + {"id": 4124, "synset": "corn_earworm.n.01", "name": "corn_earworm"}, + {"id": 4125, "synset": "cabbageworm.n.01", "name": "cabbageworm"}, + {"id": 4126, "synset": "woolly_bear.n.01", "name": "woolly_bear"}, + {"id": 4127, "synset": "woolly_bear_moth.n.01", "name": "woolly_bear_moth"}, + {"id": 4128, "synset": "larva.n.01", "name": "larva"}, + {"id": 4129, "synset": "nymph.n.02", "name": "nymph"}, + {"id": 4130, "synset": "leptocephalus.n.01", "name": "leptocephalus"}, + {"id": 4131, "synset": "grub.n.02", "name": "grub"}, + {"id": 4132, "synset": "maggot.n.01", "name": "maggot"}, + {"id": 4133, "synset": "leatherjacket.n.03", "name": "leatherjacket"}, + {"id": 4134, "synset": "pupa.n.01", "name": "pupa"}, + {"id": 4135, "synset": "chrysalis.n.01", "name": "chrysalis"}, + {"id": 4136, "synset": "imago.n.02", "name": "imago"}, + {"id": 4137, "synset": "queen.n.01", "name": "queen"}, + {"id": 4138, "synset": "phoronid.n.01", "name": "phoronid"}, + {"id": 4139, "synset": "bryozoan.n.01", "name": "bryozoan"}, + {"id": 4140, "synset": "brachiopod.n.01", "name": "brachiopod"}, + {"id": 4141, "synset": "peanut_worm.n.01", "name": "peanut_worm"}, + {"id": 4142, "synset": "echinoderm.n.01", "name": "echinoderm"}, + {"id": 4143, "synset": "brittle_star.n.01", "name": "brittle_star"}, + {"id": 4144, "synset": "basket_star.n.01", "name": "basket_star"}, + {"id": 4145, "synset": "astrophyton_muricatum.n.01", "name": "Astrophyton_muricatum"}, + {"id": 4146, "synset": "sea_urchin.n.01", "name": "sea_urchin"}, + {"id": 4147, "synset": "edible_sea_urchin.n.01", "name": "edible_sea_urchin"}, + {"id": 4148, "synset": "sand_dollar.n.01", "name": "sand_dollar"}, + {"id": 4149, "synset": "heart_urchin.n.01", "name": "heart_urchin"}, + {"id": 4150, "synset": "crinoid.n.01", "name": "crinoid"}, + {"id": 4151, "synset": "sea_lily.n.01", "name": "sea_lily"}, + {"id": 4152, "synset": "feather_star.n.01", "name": "feather_star"}, + {"id": 4153, "synset": "sea_cucumber.n.01", "name": "sea_cucumber"}, + {"id": 4154, "synset": "trepang.n.01", "name": "trepang"}, + {"id": 4155, "synset": "duplicidentata.n.01", "name": "Duplicidentata"}, + {"id": 4156, "synset": "lagomorph.n.01", "name": "lagomorph"}, + {"id": 4157, "synset": "leporid.n.01", "name": "leporid"}, + {"id": 4158, "synset": "rabbit_ears.n.02", "name": "rabbit_ears"}, + {"id": 4159, "synset": "lapin.n.02", "name": "lapin"}, + {"id": 4160, "synset": "bunny.n.02", "name": "bunny"}, + {"id": 4161, "synset": "european_rabbit.n.01", "name": "European_rabbit"}, + {"id": 4162, "synset": "wood_rabbit.n.01", "name": "wood_rabbit"}, + {"id": 4163, "synset": "eastern_cottontail.n.01", "name": "eastern_cottontail"}, + {"id": 4164, "synset": "swamp_rabbit.n.02", "name": "swamp_rabbit"}, + {"id": 4165, "synset": "marsh_hare.n.01", "name": "marsh_hare"}, + {"id": 4166, "synset": "hare.n.01", "name": "hare"}, + {"id": 4167, "synset": "leveret.n.01", "name": "leveret"}, + {"id": 4168, "synset": "european_hare.n.01", "name": "European_hare"}, + {"id": 4169, "synset": "jackrabbit.n.01", "name": "jackrabbit"}, + {"id": 4170, "synset": "white-tailed_jackrabbit.n.01", "name": "white-tailed_jackrabbit"}, + {"id": 4171, "synset": "blacktail_jackrabbit.n.01", "name": "blacktail_jackrabbit"}, + {"id": 4172, "synset": "polar_hare.n.01", "name": "polar_hare"}, + {"id": 4173, "synset": "snowshoe_hare.n.01", "name": "snowshoe_hare"}, + {"id": 4174, "synset": "belgian_hare.n.01", "name": "Belgian_hare"}, + {"id": 4175, "synset": "angora.n.03", "name": "Angora"}, + {"id": 4176, "synset": "pika.n.01", "name": "pika"}, + {"id": 4177, "synset": "little_chief_hare.n.01", "name": "little_chief_hare"}, + {"id": 4178, "synset": "collared_pika.n.01", "name": "collared_pika"}, + {"id": 4179, "synset": "mouse.n.01", "name": "mouse"}, + {"id": 4180, "synset": "pocket_rat.n.01", "name": "pocket_rat"}, + {"id": 4181, "synset": "murine.n.01", "name": "murine"}, + {"id": 4182, "synset": "house_mouse.n.01", "name": "house_mouse"}, + {"id": 4183, "synset": "harvest_mouse.n.02", "name": "harvest_mouse"}, + {"id": 4184, "synset": "field_mouse.n.02", "name": "field_mouse"}, + {"id": 4185, "synset": "nude_mouse.n.01", "name": "nude_mouse"}, + {"id": 4186, "synset": "european_wood_mouse.n.01", "name": "European_wood_mouse"}, + {"id": 4187, "synset": "brown_rat.n.01", "name": "brown_rat"}, + {"id": 4188, "synset": "wharf_rat.n.02", "name": "wharf_rat"}, + {"id": 4189, "synset": "sewer_rat.n.01", "name": "sewer_rat"}, + {"id": 4190, "synset": "black_rat.n.01", "name": "black_rat"}, + {"id": 4191, "synset": "bandicoot_rat.n.01", "name": "bandicoot_rat"}, + {"id": 4192, "synset": "jerboa_rat.n.01", "name": "jerboa_rat"}, + {"id": 4193, "synset": "kangaroo_mouse.n.02", "name": "kangaroo_mouse"}, + {"id": 4194, "synset": "water_rat.n.03", "name": "water_rat"}, + {"id": 4195, "synset": "beaver_rat.n.01", "name": "beaver_rat"}, + {"id": 4196, "synset": "new_world_mouse.n.01", "name": "New_World_mouse"}, + {"id": 4197, "synset": "american_harvest_mouse.n.01", "name": "American_harvest_mouse"}, + {"id": 4198, "synset": "wood_mouse.n.01", "name": "wood_mouse"}, + {"id": 4199, "synset": "white-footed_mouse.n.01", "name": "white-footed_mouse"}, + {"id": 4200, "synset": "deer_mouse.n.01", "name": "deer_mouse"}, + {"id": 4201, "synset": "cactus_mouse.n.01", "name": "cactus_mouse"}, + {"id": 4202, "synset": "cotton_mouse.n.01", "name": "cotton_mouse"}, + {"id": 4203, "synset": "pygmy_mouse.n.01", "name": "pygmy_mouse"}, + {"id": 4204, "synset": "grasshopper_mouse.n.01", "name": "grasshopper_mouse"}, + {"id": 4205, "synset": "muskrat.n.02", "name": "muskrat"}, + {"id": 4206, "synset": "round-tailed_muskrat.n.01", "name": "round-tailed_muskrat"}, + {"id": 4207, "synset": "cotton_rat.n.01", "name": "cotton_rat"}, + {"id": 4208, "synset": "wood_rat.n.01", "name": "wood_rat"}, + {"id": 4209, "synset": "dusky-footed_wood_rat.n.01", "name": "dusky-footed_wood_rat"}, + {"id": 4210, "synset": "vole.n.01", "name": "vole"}, + {"id": 4211, "synset": "packrat.n.02", "name": "packrat"}, + {"id": 4212, "synset": "dusky-footed_woodrat.n.01", "name": "dusky-footed_woodrat"}, + {"id": 4213, "synset": "eastern_woodrat.n.01", "name": "eastern_woodrat"}, + {"id": 4214, "synset": "rice_rat.n.01", "name": "rice_rat"}, + {"id": 4215, "synset": "pine_vole.n.01", "name": "pine_vole"}, + {"id": 4216, "synset": "meadow_vole.n.01", "name": "meadow_vole"}, + {"id": 4217, "synset": "water_vole.n.02", "name": "water_vole"}, + {"id": 4218, "synset": "prairie_vole.n.01", "name": "prairie_vole"}, + {"id": 4219, "synset": "water_vole.n.01", "name": "water_vole"}, + {"id": 4220, "synset": "red-backed_mouse.n.01", "name": "red-backed_mouse"}, + {"id": 4221, "synset": "phenacomys.n.01", "name": "phenacomys"}, + {"id": 4222, "synset": "eurasian_hamster.n.01", "name": "Eurasian_hamster"}, + {"id": 4223, "synset": "golden_hamster.n.01", "name": "golden_hamster"}, + {"id": 4224, "synset": "gerbil.n.01", "name": "gerbil"}, + {"id": 4225, "synset": "jird.n.01", "name": "jird"}, + {"id": 4226, "synset": "tamarisk_gerbil.n.01", "name": "tamarisk_gerbil"}, + {"id": 4227, "synset": "sand_rat.n.02", "name": "sand_rat"}, + {"id": 4228, "synset": "lemming.n.01", "name": "lemming"}, + {"id": 4229, "synset": "european_lemming.n.01", "name": "European_lemming"}, + {"id": 4230, "synset": "brown_lemming.n.01", "name": "brown_lemming"}, + {"id": 4231, "synset": "grey_lemming.n.01", "name": "grey_lemming"}, + {"id": 4232, "synset": "pied_lemming.n.01", "name": "pied_lemming"}, + { + "id": 4233, + "synset": "hudson_bay_collared_lemming.n.01", + "name": "Hudson_bay_collared_lemming", + }, + {"id": 4234, "synset": "southern_bog_lemming.n.01", "name": "southern_bog_lemming"}, + {"id": 4235, "synset": "northern_bog_lemming.n.01", "name": "northern_bog_lemming"}, + {"id": 4236, "synset": "porcupine.n.01", "name": "porcupine"}, + {"id": 4237, "synset": "old_world_porcupine.n.01", "name": "Old_World_porcupine"}, + {"id": 4238, "synset": "brush-tailed_porcupine.n.01", "name": "brush-tailed_porcupine"}, + {"id": 4239, "synset": "long-tailed_porcupine.n.01", "name": "long-tailed_porcupine"}, + {"id": 4240, "synset": "new_world_porcupine.n.01", "name": "New_World_porcupine"}, + {"id": 4241, "synset": "canada_porcupine.n.01", "name": "Canada_porcupine"}, + {"id": 4242, "synset": "pocket_mouse.n.01", "name": "pocket_mouse"}, + {"id": 4243, "synset": "silky_pocket_mouse.n.01", "name": "silky_pocket_mouse"}, + {"id": 4244, "synset": "plains_pocket_mouse.n.01", "name": "plains_pocket_mouse"}, + {"id": 4245, "synset": "hispid_pocket_mouse.n.01", "name": "hispid_pocket_mouse"}, + {"id": 4246, "synset": "mexican_pocket_mouse.n.01", "name": "Mexican_pocket_mouse"}, + {"id": 4247, "synset": "kangaroo_rat.n.01", "name": "kangaroo_rat"}, + {"id": 4248, "synset": "ord_kangaroo_rat.n.01", "name": "Ord_kangaroo_rat"}, + {"id": 4249, "synset": "kangaroo_mouse.n.01", "name": "kangaroo_mouse"}, + {"id": 4250, "synset": "jumping_mouse.n.01", "name": "jumping_mouse"}, + {"id": 4251, "synset": "meadow_jumping_mouse.n.01", "name": "meadow_jumping_mouse"}, + {"id": 4252, "synset": "jerboa.n.01", "name": "jerboa"}, + {"id": 4253, "synset": "typical_jerboa.n.01", "name": "typical_jerboa"}, + {"id": 4254, "synset": "jaculus_jaculus.n.01", "name": "Jaculus_jaculus"}, + {"id": 4255, "synset": "dormouse.n.01", "name": "dormouse"}, + {"id": 4256, "synset": "loir.n.01", "name": "loir"}, + {"id": 4257, "synset": "hazel_mouse.n.01", "name": "hazel_mouse"}, + {"id": 4258, "synset": "lerot.n.01", "name": "lerot"}, + {"id": 4259, "synset": "gopher.n.04", "name": "gopher"}, + {"id": 4260, "synset": "plains_pocket_gopher.n.01", "name": "plains_pocket_gopher"}, + {"id": 4261, "synset": "southeastern_pocket_gopher.n.01", "name": "southeastern_pocket_gopher"}, + {"id": 4262, "synset": "valley_pocket_gopher.n.01", "name": "valley_pocket_gopher"}, + {"id": 4263, "synset": "northern_pocket_gopher.n.01", "name": "northern_pocket_gopher"}, + {"id": 4264, "synset": "tree_squirrel.n.01", "name": "tree_squirrel"}, + {"id": 4265, "synset": "eastern_grey_squirrel.n.01", "name": "eastern_grey_squirrel"}, + {"id": 4266, "synset": "western_grey_squirrel.n.01", "name": "western_grey_squirrel"}, + {"id": 4267, "synset": "fox_squirrel.n.01", "name": "fox_squirrel"}, + {"id": 4268, "synset": "black_squirrel.n.01", "name": "black_squirrel"}, + {"id": 4269, "synset": "red_squirrel.n.02", "name": "red_squirrel"}, + {"id": 4270, "synset": "american_red_squirrel.n.01", "name": "American_red_squirrel"}, + {"id": 4271, "synset": "chickeree.n.01", "name": "chickeree"}, + {"id": 4272, "synset": "antelope_squirrel.n.01", "name": "antelope_squirrel"}, + {"id": 4273, "synset": "ground_squirrel.n.02", "name": "ground_squirrel"}, + {"id": 4274, "synset": "mantled_ground_squirrel.n.01", "name": "mantled_ground_squirrel"}, + {"id": 4275, "synset": "suslik.n.01", "name": "suslik"}, + {"id": 4276, "synset": "flickertail.n.01", "name": "flickertail"}, + {"id": 4277, "synset": "rock_squirrel.n.01", "name": "rock_squirrel"}, + {"id": 4278, "synset": "arctic_ground_squirrel.n.01", "name": "Arctic_ground_squirrel"}, + {"id": 4279, "synset": "prairie_dog.n.01", "name": "prairie_dog"}, + {"id": 4280, "synset": "blacktail_prairie_dog.n.01", "name": "blacktail_prairie_dog"}, + {"id": 4281, "synset": "whitetail_prairie_dog.n.01", "name": "whitetail_prairie_dog"}, + {"id": 4282, "synset": "eastern_chipmunk.n.01", "name": "eastern_chipmunk"}, + {"id": 4283, "synset": "chipmunk.n.01", "name": "chipmunk"}, + {"id": 4284, "synset": "baronduki.n.01", "name": "baronduki"}, + {"id": 4285, "synset": "american_flying_squirrel.n.01", "name": "American_flying_squirrel"}, + {"id": 4286, "synset": "southern_flying_squirrel.n.01", "name": "southern_flying_squirrel"}, + {"id": 4287, "synset": "northern_flying_squirrel.n.01", "name": "northern_flying_squirrel"}, + {"id": 4288, "synset": "marmot.n.01", "name": "marmot"}, + {"id": 4289, "synset": "groundhog.n.01", "name": "groundhog"}, + {"id": 4290, "synset": "hoary_marmot.n.01", "name": "hoary_marmot"}, + {"id": 4291, "synset": "yellowbelly_marmot.n.01", "name": "yellowbelly_marmot"}, + {"id": 4292, "synset": "asiatic_flying_squirrel.n.01", "name": "Asiatic_flying_squirrel"}, + {"id": 4293, "synset": "beaver.n.07", "name": "beaver"}, + {"id": 4294, "synset": "old_world_beaver.n.01", "name": "Old_World_beaver"}, + {"id": 4295, "synset": "new_world_beaver.n.01", "name": "New_World_beaver"}, + {"id": 4296, "synset": "mountain_beaver.n.01", "name": "mountain_beaver"}, + {"id": 4297, "synset": "cavy.n.01", "name": "cavy"}, + {"id": 4298, "synset": "guinea_pig.n.02", "name": "guinea_pig"}, + {"id": 4299, "synset": "aperea.n.01", "name": "aperea"}, + {"id": 4300, "synset": "mara.n.02", "name": "mara"}, + {"id": 4301, "synset": "capybara.n.01", "name": "capybara"}, + {"id": 4302, "synset": "agouti.n.01", "name": "agouti"}, + {"id": 4303, "synset": "paca.n.01", "name": "paca"}, + {"id": 4304, "synset": "mountain_paca.n.01", "name": "mountain_paca"}, + {"id": 4305, "synset": "coypu.n.01", "name": "coypu"}, + {"id": 4306, "synset": "chinchilla.n.03", "name": "chinchilla"}, + {"id": 4307, "synset": "mountain_chinchilla.n.01", "name": "mountain_chinchilla"}, + {"id": 4308, "synset": "viscacha.n.01", "name": "viscacha"}, + {"id": 4309, "synset": "abrocome.n.01", "name": "abrocome"}, + {"id": 4310, "synset": "mole_rat.n.02", "name": "mole_rat"}, + {"id": 4311, "synset": "mole_rat.n.01", "name": "mole_rat"}, + {"id": 4312, "synset": "sand_rat.n.01", "name": "sand_rat"}, + {"id": 4313, "synset": "naked_mole_rat.n.01", "name": "naked_mole_rat"}, + {"id": 4314, "synset": "queen.n.09", "name": "queen"}, + {"id": 4315, "synset": "damaraland_mole_rat.n.01", "name": "Damaraland_mole_rat"}, + {"id": 4316, "synset": "ungulata.n.01", "name": "Ungulata"}, + {"id": 4317, "synset": "ungulate.n.01", "name": "ungulate"}, + {"id": 4318, "synset": "unguiculate.n.01", "name": "unguiculate"}, + {"id": 4319, "synset": "dinoceras.n.01", "name": "dinoceras"}, + {"id": 4320, "synset": "hyrax.n.01", "name": "hyrax"}, + {"id": 4321, "synset": "rock_hyrax.n.01", "name": "rock_hyrax"}, + {"id": 4322, "synset": "odd-toed_ungulate.n.01", "name": "odd-toed_ungulate"}, + {"id": 4323, "synset": "equine.n.01", "name": "equine"}, + {"id": 4324, "synset": "roan.n.02", "name": "roan"}, + {"id": 4325, "synset": "stablemate.n.01", "name": "stablemate"}, + {"id": 4326, "synset": "gee-gee.n.01", "name": "gee-gee"}, + {"id": 4327, "synset": "eohippus.n.01", "name": "eohippus"}, + {"id": 4328, "synset": "filly.n.01", "name": "filly"}, + {"id": 4329, "synset": "colt.n.01", "name": "colt"}, + {"id": 4330, "synset": "male_horse.n.01", "name": "male_horse"}, + {"id": 4331, "synset": "ridgeling.n.01", "name": "ridgeling"}, + {"id": 4332, "synset": "stallion.n.01", "name": "stallion"}, + {"id": 4333, "synset": "stud.n.04", "name": "stud"}, + {"id": 4334, "synset": "gelding.n.01", "name": "gelding"}, + {"id": 4335, "synset": "mare.n.01", "name": "mare"}, + {"id": 4336, "synset": "broodmare.n.01", "name": "broodmare"}, + {"id": 4337, "synset": "saddle_horse.n.01", "name": "saddle_horse"}, + {"id": 4338, "synset": "remount.n.01", "name": "remount"}, + {"id": 4339, "synset": "palfrey.n.01", "name": "palfrey"}, + {"id": 4340, "synset": "warhorse.n.03", "name": "warhorse"}, + {"id": 4341, "synset": "cavalry_horse.n.01", "name": "cavalry_horse"}, + {"id": 4342, "synset": "charger.n.01", "name": "charger"}, + {"id": 4343, "synset": "steed.n.01", "name": "steed"}, + {"id": 4344, "synset": "prancer.n.01", "name": "prancer"}, + {"id": 4345, "synset": "hack.n.08", "name": "hack"}, + {"id": 4346, "synset": "cow_pony.n.01", "name": "cow_pony"}, + {"id": 4347, "synset": "quarter_horse.n.01", "name": "quarter_horse"}, + {"id": 4348, "synset": "morgan.n.06", "name": "Morgan"}, + {"id": 4349, "synset": "tennessee_walker.n.01", "name": "Tennessee_walker"}, + {"id": 4350, "synset": "american_saddle_horse.n.01", "name": "American_saddle_horse"}, + {"id": 4351, "synset": "appaloosa.n.01", "name": "Appaloosa"}, + {"id": 4352, "synset": "arabian.n.02", "name": "Arabian"}, + {"id": 4353, "synset": "lippizan.n.01", "name": "Lippizan"}, + {"id": 4354, "synset": "pony.n.01", "name": "pony"}, + {"id": 4355, "synset": "polo_pony.n.01", "name": "polo_pony"}, + {"id": 4356, "synset": "mustang.n.01", "name": "mustang"}, + {"id": 4357, "synset": "bronco.n.01", "name": "bronco"}, + {"id": 4358, "synset": "bucking_bronco.n.01", "name": "bucking_bronco"}, + {"id": 4359, "synset": "buckskin.n.01", "name": "buckskin"}, + {"id": 4360, "synset": "crowbait.n.01", "name": "crowbait"}, + {"id": 4361, "synset": "dun.n.01", "name": "dun"}, + {"id": 4362, "synset": "grey.n.07", "name": "grey"}, + {"id": 4363, "synset": "wild_horse.n.01", "name": "wild_horse"}, + {"id": 4364, "synset": "tarpan.n.01", "name": "tarpan"}, + {"id": 4365, "synset": "przewalski's_horse.n.01", "name": "Przewalski's_horse"}, + {"id": 4366, "synset": "cayuse.n.01", "name": "cayuse"}, + {"id": 4367, "synset": "hack.n.07", "name": "hack"}, + {"id": 4368, "synset": "hack.n.06", "name": "hack"}, + {"id": 4369, "synset": "plow_horse.n.01", "name": "plow_horse"}, + {"id": 4370, "synset": "shetland_pony.n.01", "name": "Shetland_pony"}, + {"id": 4371, "synset": "welsh_pony.n.01", "name": "Welsh_pony"}, + {"id": 4372, "synset": "exmoor.n.02", "name": "Exmoor"}, + {"id": 4373, "synset": "racehorse.n.01", "name": "racehorse"}, + {"id": 4374, "synset": "thoroughbred.n.02", "name": "thoroughbred"}, + {"id": 4375, "synset": "steeplechaser.n.01", "name": "steeplechaser"}, + {"id": 4376, "synset": "racer.n.03", "name": "racer"}, + {"id": 4377, "synset": "finisher.n.06", "name": "finisher"}, + {"id": 4378, "synset": "pony.n.02", "name": "pony"}, + {"id": 4379, "synset": "yearling.n.02", "name": "yearling"}, + {"id": 4380, "synset": "dark_horse.n.02", "name": "dark_horse"}, + {"id": 4381, "synset": "mudder.n.01", "name": "mudder"}, + {"id": 4382, "synset": "nonstarter.n.02", "name": "nonstarter"}, + {"id": 4383, "synset": "stalking-horse.n.04", "name": "stalking-horse"}, + {"id": 4384, "synset": "harness_horse.n.01", "name": "harness_horse"}, + {"id": 4385, "synset": "cob.n.02", "name": "cob"}, + {"id": 4386, "synset": "hackney.n.02", "name": "hackney"}, + {"id": 4387, "synset": "workhorse.n.02", "name": "workhorse"}, + {"id": 4388, "synset": "draft_horse.n.01", "name": "draft_horse"}, + {"id": 4389, "synset": "packhorse.n.01", "name": "packhorse"}, + {"id": 4390, "synset": "carthorse.n.01", "name": "carthorse"}, + {"id": 4391, "synset": "clydesdale.n.01", "name": "Clydesdale"}, + {"id": 4392, "synset": "percheron.n.01", "name": "Percheron"}, + {"id": 4393, "synset": "farm_horse.n.01", "name": "farm_horse"}, + {"id": 4394, "synset": "shire.n.02", "name": "shire"}, + {"id": 4395, "synset": "pole_horse.n.02", "name": "pole_horse"}, + {"id": 4396, "synset": "post_horse.n.01", "name": "post_horse"}, + {"id": 4397, "synset": "coach_horse.n.01", "name": "coach_horse"}, + {"id": 4398, "synset": "pacer.n.02", "name": "pacer"}, + {"id": 4399, "synset": "pacer.n.01", "name": "pacer"}, + {"id": 4400, "synset": "trotting_horse.n.01", "name": "trotting_horse"}, + {"id": 4401, "synset": "pole_horse.n.01", "name": "pole_horse"}, + {"id": 4402, "synset": "stepper.n.03", "name": "stepper"}, + {"id": 4403, "synset": "chestnut.n.06", "name": "chestnut"}, + {"id": 4404, "synset": "liver_chestnut.n.01", "name": "liver_chestnut"}, + {"id": 4405, "synset": "bay.n.07", "name": "bay"}, + {"id": 4406, "synset": "sorrel.n.05", "name": "sorrel"}, + {"id": 4407, "synset": "palomino.n.01", "name": "palomino"}, + {"id": 4408, "synset": "pinto.n.01", "name": "pinto"}, + {"id": 4409, "synset": "ass.n.03", "name": "ass"}, + {"id": 4410, "synset": "burro.n.01", "name": "burro"}, + {"id": 4411, "synset": "moke.n.01", "name": "moke"}, + {"id": 4412, "synset": "jack.n.12", "name": "jack"}, + {"id": 4413, "synset": "jennet.n.01", "name": "jennet"}, + {"id": 4414, "synset": "mule.n.01", "name": "mule"}, + {"id": 4415, "synset": "hinny.n.01", "name": "hinny"}, + {"id": 4416, "synset": "wild_ass.n.01", "name": "wild_ass"}, + {"id": 4417, "synset": "african_wild_ass.n.01", "name": "African_wild_ass"}, + {"id": 4418, "synset": "kiang.n.01", "name": "kiang"}, + {"id": 4419, "synset": "onager.n.02", "name": "onager"}, + {"id": 4420, "synset": "chigetai.n.01", "name": "chigetai"}, + {"id": 4421, "synset": "common_zebra.n.01", "name": "common_zebra"}, + {"id": 4422, "synset": "mountain_zebra.n.01", "name": "mountain_zebra"}, + {"id": 4423, "synset": "grevy's_zebra.n.01", "name": "grevy's_zebra"}, + {"id": 4424, "synset": "quagga.n.01", "name": "quagga"}, + {"id": 4425, "synset": "indian_rhinoceros.n.01", "name": "Indian_rhinoceros"}, + {"id": 4426, "synset": "woolly_rhinoceros.n.01", "name": "woolly_rhinoceros"}, + {"id": 4427, "synset": "white_rhinoceros.n.01", "name": "white_rhinoceros"}, + {"id": 4428, "synset": "black_rhinoceros.n.01", "name": "black_rhinoceros"}, + {"id": 4429, "synset": "tapir.n.01", "name": "tapir"}, + {"id": 4430, "synset": "new_world_tapir.n.01", "name": "New_World_tapir"}, + {"id": 4431, "synset": "malayan_tapir.n.01", "name": "Malayan_tapir"}, + {"id": 4432, "synset": "even-toed_ungulate.n.01", "name": "even-toed_ungulate"}, + {"id": 4433, "synset": "swine.n.01", "name": "swine"}, + {"id": 4434, "synset": "piglet.n.01", "name": "piglet"}, + {"id": 4435, "synset": "sucking_pig.n.01", "name": "sucking_pig"}, + {"id": 4436, "synset": "porker.n.01", "name": "porker"}, + {"id": 4437, "synset": "boar.n.02", "name": "boar"}, + {"id": 4438, "synset": "sow.n.01", "name": "sow"}, + {"id": 4439, "synset": "razorback.n.01", "name": "razorback"}, + {"id": 4440, "synset": "wild_boar.n.01", "name": "wild_boar"}, + {"id": 4441, "synset": "babirusa.n.01", "name": "babirusa"}, + {"id": 4442, "synset": "warthog.n.01", "name": "warthog"}, + {"id": 4443, "synset": "peccary.n.01", "name": "peccary"}, + {"id": 4444, "synset": "collared_peccary.n.01", "name": "collared_peccary"}, + {"id": 4445, "synset": "white-lipped_peccary.n.01", "name": "white-lipped_peccary"}, + {"id": 4446, "synset": "ruminant.n.01", "name": "ruminant"}, + {"id": 4447, "synset": "bovid.n.01", "name": "bovid"}, + {"id": 4448, "synset": "bovine.n.01", "name": "bovine"}, + {"id": 4449, "synset": "ox.n.02", "name": "ox"}, + {"id": 4450, "synset": "cattle.n.01", "name": "cattle"}, + {"id": 4451, "synset": "ox.n.01", "name": "ox"}, + {"id": 4452, "synset": "stirk.n.01", "name": "stirk"}, + {"id": 4453, "synset": "bullock.n.02", "name": "bullock"}, + {"id": 4454, "synset": "bull.n.01", "name": "bull"}, + {"id": 4455, "synset": "cow.n.01", "name": "cow"}, + {"id": 4456, "synset": "heifer.n.01", "name": "heifer"}, + {"id": 4457, "synset": "bullock.n.01", "name": "bullock"}, + {"id": 4458, "synset": "dogie.n.01", "name": "dogie"}, + {"id": 4459, "synset": "maverick.n.02", "name": "maverick"}, + {"id": 4460, "synset": "longhorn.n.01", "name": "longhorn"}, + {"id": 4461, "synset": "brahman.n.04", "name": "Brahman"}, + {"id": 4462, "synset": "zebu.n.01", "name": "zebu"}, + {"id": 4463, "synset": "aurochs.n.02", "name": "aurochs"}, + {"id": 4464, "synset": "yak.n.02", "name": "yak"}, + {"id": 4465, "synset": "banteng.n.01", "name": "banteng"}, + {"id": 4466, "synset": "welsh.n.03", "name": "Welsh"}, + {"id": 4467, "synset": "red_poll.n.01", "name": "red_poll"}, + {"id": 4468, "synset": "santa_gertrudis.n.01", "name": "Santa_Gertrudis"}, + {"id": 4469, "synset": "aberdeen_angus.n.01", "name": "Aberdeen_Angus"}, + {"id": 4470, "synset": "africander.n.01", "name": "Africander"}, + {"id": 4471, "synset": "dairy_cattle.n.01", "name": "dairy_cattle"}, + {"id": 4472, "synset": "ayrshire.n.01", "name": "Ayrshire"}, + {"id": 4473, "synset": "brown_swiss.n.01", "name": "Brown_Swiss"}, + {"id": 4474, "synset": "charolais.n.01", "name": "Charolais"}, + {"id": 4475, "synset": "jersey.n.05", "name": "Jersey"}, + {"id": 4476, "synset": "devon.n.02", "name": "Devon"}, + {"id": 4477, "synset": "grade.n.09", "name": "grade"}, + {"id": 4478, "synset": "durham.n.02", "name": "Durham"}, + {"id": 4479, "synset": "milking_shorthorn.n.01", "name": "milking_shorthorn"}, + {"id": 4480, "synset": "galloway.n.02", "name": "Galloway"}, + {"id": 4481, "synset": "friesian.n.01", "name": "Friesian"}, + {"id": 4482, "synset": "guernsey.n.02", "name": "Guernsey"}, + {"id": 4483, "synset": "hereford.n.01", "name": "Hereford"}, + {"id": 4484, "synset": "cattalo.n.01", "name": "cattalo"}, + {"id": 4485, "synset": "old_world_buffalo.n.01", "name": "Old_World_buffalo"}, + {"id": 4486, "synset": "water_buffalo.n.01", "name": "water_buffalo"}, + {"id": 4487, "synset": "indian_buffalo.n.01", "name": "Indian_buffalo"}, + {"id": 4488, "synset": "carabao.n.01", "name": "carabao"}, + {"id": 4489, "synset": "anoa.n.01", "name": "anoa"}, + {"id": 4490, "synset": "tamarau.n.01", "name": "tamarau"}, + {"id": 4491, "synset": "cape_buffalo.n.01", "name": "Cape_buffalo"}, + {"id": 4492, "synset": "asian_wild_ox.n.01", "name": "Asian_wild_ox"}, + {"id": 4493, "synset": "gaur.n.01", "name": "gaur"}, + {"id": 4494, "synset": "gayal.n.01", "name": "gayal"}, + {"id": 4495, "synset": "bison.n.01", "name": "bison"}, + {"id": 4496, "synset": "american_bison.n.01", "name": "American_bison"}, + {"id": 4497, "synset": "wisent.n.01", "name": "wisent"}, + {"id": 4498, "synset": "musk_ox.n.01", "name": "musk_ox"}, + {"id": 4499, "synset": "ewe.n.03", "name": "ewe"}, + {"id": 4500, "synset": "wether.n.01", "name": "wether"}, + {"id": 4501, "synset": "lambkin.n.01", "name": "lambkin"}, + {"id": 4502, "synset": "baa-lamb.n.01", "name": "baa-lamb"}, + {"id": 4503, "synset": "hog.n.02", "name": "hog"}, + {"id": 4504, "synset": "teg.n.01", "name": "teg"}, + {"id": 4505, "synset": "persian_lamb.n.02", "name": "Persian_lamb"}, + {"id": 4506, "synset": "domestic_sheep.n.01", "name": "domestic_sheep"}, + {"id": 4507, "synset": "cotswold.n.01", "name": "Cotswold"}, + {"id": 4508, "synset": "hampshire.n.02", "name": "Hampshire"}, + {"id": 4509, "synset": "lincoln.n.03", "name": "Lincoln"}, + {"id": 4510, "synset": "exmoor.n.01", "name": "Exmoor"}, + {"id": 4511, "synset": "cheviot.n.01", "name": "Cheviot"}, + {"id": 4512, "synset": "broadtail.n.02", "name": "broadtail"}, + {"id": 4513, "synset": "longwool.n.01", "name": "longwool"}, + {"id": 4514, "synset": "merino.n.01", "name": "merino"}, + {"id": 4515, "synset": "rambouillet.n.01", "name": "Rambouillet"}, + {"id": 4516, "synset": "wild_sheep.n.01", "name": "wild_sheep"}, + {"id": 4517, "synset": "argali.n.01", "name": "argali"}, + {"id": 4518, "synset": "marco_polo_sheep.n.01", "name": "Marco_Polo_sheep"}, + {"id": 4519, "synset": "urial.n.01", "name": "urial"}, + {"id": 4520, "synset": "dall_sheep.n.01", "name": "Dall_sheep"}, + {"id": 4521, "synset": "mountain_sheep.n.01", "name": "mountain_sheep"}, + {"id": 4522, "synset": "bighorn.n.02", "name": "bighorn"}, + {"id": 4523, "synset": "mouflon.n.01", "name": "mouflon"}, + {"id": 4524, "synset": "aoudad.n.01", "name": "aoudad"}, + {"id": 4525, "synset": "kid.n.05", "name": "kid"}, + {"id": 4526, "synset": "billy.n.02", "name": "billy"}, + {"id": 4527, "synset": "nanny.n.02", "name": "nanny"}, + {"id": 4528, "synset": "domestic_goat.n.01", "name": "domestic_goat"}, + {"id": 4529, "synset": "cashmere_goat.n.01", "name": "Cashmere_goat"}, + {"id": 4530, "synset": "angora.n.02", "name": "Angora"}, + {"id": 4531, "synset": "wild_goat.n.01", "name": "wild_goat"}, + {"id": 4532, "synset": "bezoar_goat.n.01", "name": "bezoar_goat"}, + {"id": 4533, "synset": "markhor.n.01", "name": "markhor"}, + {"id": 4534, "synset": "ibex.n.01", "name": "ibex"}, + {"id": 4535, "synset": "goat_antelope.n.01", "name": "goat_antelope"}, + {"id": 4536, "synset": "mountain_goat.n.01", "name": "mountain_goat"}, + {"id": 4537, "synset": "goral.n.01", "name": "goral"}, + {"id": 4538, "synset": "serow.n.01", "name": "serow"}, + {"id": 4539, "synset": "chamois.n.02", "name": "chamois"}, + {"id": 4540, "synset": "takin.n.01", "name": "takin"}, + {"id": 4541, "synset": "antelope.n.01", "name": "antelope"}, + {"id": 4542, "synset": "blackbuck.n.01", "name": "blackbuck"}, + {"id": 4543, "synset": "gerenuk.n.01", "name": "gerenuk"}, + {"id": 4544, "synset": "addax.n.01", "name": "addax"}, + {"id": 4545, "synset": "gnu.n.01", "name": "gnu"}, + {"id": 4546, "synset": "dik-dik.n.01", "name": "dik-dik"}, + {"id": 4547, "synset": "hartebeest.n.01", "name": "hartebeest"}, + {"id": 4548, "synset": "sassaby.n.01", "name": "sassaby"}, + {"id": 4549, "synset": "impala.n.01", "name": "impala"}, + {"id": 4550, "synset": "thomson's_gazelle.n.01", "name": "Thomson's_gazelle"}, + {"id": 4551, "synset": "gazella_subgutturosa.n.01", "name": "Gazella_subgutturosa"}, + {"id": 4552, "synset": "springbok.n.01", "name": "springbok"}, + {"id": 4553, "synset": "bongo.n.02", "name": "bongo"}, + {"id": 4554, "synset": "kudu.n.01", "name": "kudu"}, + {"id": 4555, "synset": "greater_kudu.n.01", "name": "greater_kudu"}, + {"id": 4556, "synset": "lesser_kudu.n.01", "name": "lesser_kudu"}, + {"id": 4557, "synset": "harnessed_antelope.n.01", "name": "harnessed_antelope"}, + {"id": 4558, "synset": "nyala.n.02", "name": "nyala"}, + {"id": 4559, "synset": "mountain_nyala.n.01", "name": "mountain_nyala"}, + {"id": 4560, "synset": "bushbuck.n.01", "name": "bushbuck"}, + {"id": 4561, "synset": "nilgai.n.01", "name": "nilgai"}, + {"id": 4562, "synset": "sable_antelope.n.01", "name": "sable_antelope"}, + {"id": 4563, "synset": "saiga.n.01", "name": "saiga"}, + {"id": 4564, "synset": "steenbok.n.01", "name": "steenbok"}, + {"id": 4565, "synset": "eland.n.01", "name": "eland"}, + {"id": 4566, "synset": "common_eland.n.01", "name": "common_eland"}, + {"id": 4567, "synset": "giant_eland.n.01", "name": "giant_eland"}, + {"id": 4568, "synset": "kob.n.01", "name": "kob"}, + {"id": 4569, "synset": "lechwe.n.01", "name": "lechwe"}, + {"id": 4570, "synset": "waterbuck.n.01", "name": "waterbuck"}, + {"id": 4571, "synset": "puku.n.01", "name": "puku"}, + {"id": 4572, "synset": "oryx.n.01", "name": "oryx"}, + {"id": 4573, "synset": "gemsbok.n.01", "name": "gemsbok"}, + {"id": 4574, "synset": "forest_goat.n.01", "name": "forest_goat"}, + {"id": 4575, "synset": "pronghorn.n.01", "name": "pronghorn"}, + {"id": 4576, "synset": "stag.n.02", "name": "stag"}, + {"id": 4577, "synset": "royal.n.02", "name": "royal"}, + {"id": 4578, "synset": "pricket.n.02", "name": "pricket"}, + {"id": 4579, "synset": "fawn.n.02", "name": "fawn"}, + {"id": 4580, "synset": "red_deer.n.01", "name": "red_deer"}, + {"id": 4581, "synset": "hart.n.03", "name": "hart"}, + {"id": 4582, "synset": "hind.n.02", "name": "hind"}, + {"id": 4583, "synset": "brocket.n.02", "name": "brocket"}, + {"id": 4584, "synset": "sambar.n.01", "name": "sambar"}, + {"id": 4585, "synset": "wapiti.n.01", "name": "wapiti"}, + {"id": 4586, "synset": "japanese_deer.n.01", "name": "Japanese_deer"}, + {"id": 4587, "synset": "virginia_deer.n.01", "name": "Virginia_deer"}, + {"id": 4588, "synset": "mule_deer.n.01", "name": "mule_deer"}, + {"id": 4589, "synset": "black-tailed_deer.n.01", "name": "black-tailed_deer"}, + {"id": 4590, "synset": "fallow_deer.n.01", "name": "fallow_deer"}, + {"id": 4591, "synset": "roe_deer.n.01", "name": "roe_deer"}, + {"id": 4592, "synset": "roebuck.n.01", "name": "roebuck"}, + {"id": 4593, "synset": "caribou.n.01", "name": "caribou"}, + {"id": 4594, "synset": "woodland_caribou.n.01", "name": "woodland_caribou"}, + {"id": 4595, "synset": "barren_ground_caribou.n.01", "name": "barren_ground_caribou"}, + {"id": 4596, "synset": "brocket.n.01", "name": "brocket"}, + {"id": 4597, "synset": "muntjac.n.01", "name": "muntjac"}, + {"id": 4598, "synset": "musk_deer.n.01", "name": "musk_deer"}, + {"id": 4599, "synset": "pere_david's_deer.n.01", "name": "pere_david's_deer"}, + {"id": 4600, "synset": "chevrotain.n.01", "name": "chevrotain"}, + {"id": 4601, "synset": "kanchil.n.01", "name": "kanchil"}, + {"id": 4602, "synset": "napu.n.01", "name": "napu"}, + {"id": 4603, "synset": "water_chevrotain.n.01", "name": "water_chevrotain"}, + {"id": 4604, "synset": "arabian_camel.n.01", "name": "Arabian_camel"}, + {"id": 4605, "synset": "bactrian_camel.n.01", "name": "Bactrian_camel"}, + {"id": 4606, "synset": "llama.n.01", "name": "llama"}, + {"id": 4607, "synset": "domestic_llama.n.01", "name": "domestic_llama"}, + {"id": 4608, "synset": "guanaco.n.01", "name": "guanaco"}, + {"id": 4609, "synset": "alpaca.n.03", "name": "alpaca"}, + {"id": 4610, "synset": "vicuna.n.03", "name": "vicuna"}, + {"id": 4611, "synset": "okapi.n.01", "name": "okapi"}, + {"id": 4612, "synset": "musteline_mammal.n.01", "name": "musteline_mammal"}, + {"id": 4613, "synset": "weasel.n.02", "name": "weasel"}, + {"id": 4614, "synset": "ermine.n.02", "name": "ermine"}, + {"id": 4615, "synset": "stoat.n.01", "name": "stoat"}, + {"id": 4616, "synset": "new_world_least_weasel.n.01", "name": "New_World_least_weasel"}, + {"id": 4617, "synset": "old_world_least_weasel.n.01", "name": "Old_World_least_weasel"}, + {"id": 4618, "synset": "longtail_weasel.n.01", "name": "longtail_weasel"}, + {"id": 4619, "synset": "mink.n.03", "name": "mink"}, + {"id": 4620, "synset": "american_mink.n.01", "name": "American_mink"}, + {"id": 4621, "synset": "polecat.n.02", "name": "polecat"}, + {"id": 4622, "synset": "black-footed_ferret.n.01", "name": "black-footed_ferret"}, + {"id": 4623, "synset": "muishond.n.01", "name": "muishond"}, + {"id": 4624, "synset": "snake_muishond.n.01", "name": "snake_muishond"}, + {"id": 4625, "synset": "striped_muishond.n.01", "name": "striped_muishond"}, + {"id": 4626, "synset": "otter.n.02", "name": "otter"}, + {"id": 4627, "synset": "river_otter.n.01", "name": "river_otter"}, + {"id": 4628, "synset": "eurasian_otter.n.01", "name": "Eurasian_otter"}, + {"id": 4629, "synset": "sea_otter.n.01", "name": "sea_otter"}, + {"id": 4630, "synset": "skunk.n.04", "name": "skunk"}, + {"id": 4631, "synset": "striped_skunk.n.01", "name": "striped_skunk"}, + {"id": 4632, "synset": "hooded_skunk.n.01", "name": "hooded_skunk"}, + {"id": 4633, "synset": "hog-nosed_skunk.n.01", "name": "hog-nosed_skunk"}, + {"id": 4634, "synset": "spotted_skunk.n.01", "name": "spotted_skunk"}, + {"id": 4635, "synset": "badger.n.02", "name": "badger"}, + {"id": 4636, "synset": "american_badger.n.01", "name": "American_badger"}, + {"id": 4637, "synset": "eurasian_badger.n.01", "name": "Eurasian_badger"}, + {"id": 4638, "synset": "ratel.n.01", "name": "ratel"}, + {"id": 4639, "synset": "ferret_badger.n.01", "name": "ferret_badger"}, + {"id": 4640, "synset": "hog_badger.n.01", "name": "hog_badger"}, + {"id": 4641, "synset": "wolverine.n.03", "name": "wolverine"}, + {"id": 4642, "synset": "glutton.n.02", "name": "glutton"}, + {"id": 4643, "synset": "grison.n.01", "name": "grison"}, + {"id": 4644, "synset": "marten.n.01", "name": "marten"}, + {"id": 4645, "synset": "pine_marten.n.01", "name": "pine_marten"}, + {"id": 4646, "synset": "sable.n.05", "name": "sable"}, + {"id": 4647, "synset": "american_marten.n.01", "name": "American_marten"}, + {"id": 4648, "synset": "stone_marten.n.01", "name": "stone_marten"}, + {"id": 4649, "synset": "fisher.n.02", "name": "fisher"}, + {"id": 4650, "synset": "yellow-throated_marten.n.01", "name": "yellow-throated_marten"}, + {"id": 4651, "synset": "tayra.n.01", "name": "tayra"}, + {"id": 4652, "synset": "fictional_animal.n.01", "name": "fictional_animal"}, + {"id": 4653, "synset": "pachyderm.n.01", "name": "pachyderm"}, + {"id": 4654, "synset": "edentate.n.01", "name": "edentate"}, + {"id": 4655, "synset": "armadillo.n.01", "name": "armadillo"}, + {"id": 4656, "synset": "peba.n.01", "name": "peba"}, + {"id": 4657, "synset": "apar.n.01", "name": "apar"}, + {"id": 4658, "synset": "tatouay.n.01", "name": "tatouay"}, + {"id": 4659, "synset": "peludo.n.01", "name": "peludo"}, + {"id": 4660, "synset": "giant_armadillo.n.01", "name": "giant_armadillo"}, + {"id": 4661, "synset": "pichiciago.n.01", "name": "pichiciago"}, + {"id": 4662, "synset": "sloth.n.02", "name": "sloth"}, + {"id": 4663, "synset": "three-toed_sloth.n.01", "name": "three-toed_sloth"}, + {"id": 4664, "synset": "two-toed_sloth.n.02", "name": "two-toed_sloth"}, + {"id": 4665, "synset": "two-toed_sloth.n.01", "name": "two-toed_sloth"}, + {"id": 4666, "synset": "megatherian.n.01", "name": "megatherian"}, + {"id": 4667, "synset": "mylodontid.n.01", "name": "mylodontid"}, + {"id": 4668, "synset": "anteater.n.02", "name": "anteater"}, + {"id": 4669, "synset": "ant_bear.n.01", "name": "ant_bear"}, + {"id": 4670, "synset": "silky_anteater.n.01", "name": "silky_anteater"}, + {"id": 4671, "synset": "tamandua.n.01", "name": "tamandua"}, + {"id": 4672, "synset": "pangolin.n.01", "name": "pangolin"}, + {"id": 4673, "synset": "coronet.n.02", "name": "coronet"}, + {"id": 4674, "synset": "scapular.n.01", "name": "scapular"}, + {"id": 4675, "synset": "tadpole.n.01", "name": "tadpole"}, + {"id": 4676, "synset": "primate.n.02", "name": "primate"}, + {"id": 4677, "synset": "simian.n.01", "name": "simian"}, + {"id": 4678, "synset": "ape.n.01", "name": "ape"}, + {"id": 4679, "synset": "anthropoid.n.02", "name": "anthropoid"}, + {"id": 4680, "synset": "anthropoid_ape.n.01", "name": "anthropoid_ape"}, + {"id": 4681, "synset": "hominoid.n.01", "name": "hominoid"}, + {"id": 4682, "synset": "hominid.n.01", "name": "hominid"}, + {"id": 4683, "synset": "homo.n.02", "name": "homo"}, + {"id": 4684, "synset": "world.n.08", "name": "world"}, + {"id": 4685, "synset": "homo_erectus.n.01", "name": "Homo_erectus"}, + {"id": 4686, "synset": "pithecanthropus.n.01", "name": "Pithecanthropus"}, + {"id": 4687, "synset": "java_man.n.01", "name": "Java_man"}, + {"id": 4688, "synset": "peking_man.n.01", "name": "Peking_man"}, + {"id": 4689, "synset": "sinanthropus.n.01", "name": "Sinanthropus"}, + {"id": 4690, "synset": "homo_soloensis.n.01", "name": "Homo_soloensis"}, + {"id": 4691, "synset": "javanthropus.n.01", "name": "Javanthropus"}, + {"id": 4692, "synset": "homo_habilis.n.01", "name": "Homo_habilis"}, + {"id": 4693, "synset": "homo_sapiens.n.01", "name": "Homo_sapiens"}, + {"id": 4694, "synset": "neandertal_man.n.01", "name": "Neandertal_man"}, + {"id": 4695, "synset": "cro-magnon.n.01", "name": "Cro-magnon"}, + {"id": 4696, "synset": "homo_sapiens_sapiens.n.01", "name": "Homo_sapiens_sapiens"}, + {"id": 4697, "synset": "australopithecine.n.01", "name": "australopithecine"}, + {"id": 4698, "synset": "australopithecus_afarensis.n.01", "name": "Australopithecus_afarensis"}, + {"id": 4699, "synset": "australopithecus_africanus.n.01", "name": "Australopithecus_africanus"}, + {"id": 4700, "synset": "australopithecus_boisei.n.01", "name": "Australopithecus_boisei"}, + {"id": 4701, "synset": "zinjanthropus.n.01", "name": "Zinjanthropus"}, + {"id": 4702, "synset": "australopithecus_robustus.n.01", "name": "Australopithecus_robustus"}, + {"id": 4703, "synset": "paranthropus.n.01", "name": "Paranthropus"}, + {"id": 4704, "synset": "sivapithecus.n.01", "name": "Sivapithecus"}, + {"id": 4705, "synset": "rudapithecus.n.01", "name": "rudapithecus"}, + {"id": 4706, "synset": "proconsul.n.03", "name": "proconsul"}, + {"id": 4707, "synset": "aegyptopithecus.n.01", "name": "Aegyptopithecus"}, + {"id": 4708, "synset": "great_ape.n.01", "name": "great_ape"}, + {"id": 4709, "synset": "orangutan.n.01", "name": "orangutan"}, + {"id": 4710, "synset": "western_lowland_gorilla.n.01", "name": "western_lowland_gorilla"}, + {"id": 4711, "synset": "eastern_lowland_gorilla.n.01", "name": "eastern_lowland_gorilla"}, + {"id": 4712, "synset": "mountain_gorilla.n.01", "name": "mountain_gorilla"}, + {"id": 4713, "synset": "silverback.n.01", "name": "silverback"}, + {"id": 4714, "synset": "chimpanzee.n.01", "name": "chimpanzee"}, + {"id": 4715, "synset": "western_chimpanzee.n.01", "name": "western_chimpanzee"}, + {"id": 4716, "synset": "eastern_chimpanzee.n.01", "name": "eastern_chimpanzee"}, + {"id": 4717, "synset": "central_chimpanzee.n.01", "name": "central_chimpanzee"}, + {"id": 4718, "synset": "pygmy_chimpanzee.n.01", "name": "pygmy_chimpanzee"}, + {"id": 4719, "synset": "lesser_ape.n.01", "name": "lesser_ape"}, + {"id": 4720, "synset": "gibbon.n.02", "name": "gibbon"}, + {"id": 4721, "synset": "siamang.n.01", "name": "siamang"}, + {"id": 4722, "synset": "old_world_monkey.n.01", "name": "Old_World_monkey"}, + {"id": 4723, "synset": "guenon.n.01", "name": "guenon"}, + {"id": 4724, "synset": "talapoin.n.01", "name": "talapoin"}, + {"id": 4725, "synset": "grivet.n.01", "name": "grivet"}, + {"id": 4726, "synset": "vervet.n.01", "name": "vervet"}, + {"id": 4727, "synset": "green_monkey.n.01", "name": "green_monkey"}, + {"id": 4728, "synset": "mangabey.n.01", "name": "mangabey"}, + {"id": 4729, "synset": "patas.n.01", "name": "patas"}, + {"id": 4730, "synset": "chacma.n.01", "name": "chacma"}, + {"id": 4731, "synset": "mandrill.n.01", "name": "mandrill"}, + {"id": 4732, "synset": "drill.n.02", "name": "drill"}, + {"id": 4733, "synset": "macaque.n.01", "name": "macaque"}, + {"id": 4734, "synset": "rhesus.n.01", "name": "rhesus"}, + {"id": 4735, "synset": "bonnet_macaque.n.01", "name": "bonnet_macaque"}, + {"id": 4736, "synset": "barbary_ape.n.01", "name": "Barbary_ape"}, + {"id": 4737, "synset": "crab-eating_macaque.n.01", "name": "crab-eating_macaque"}, + {"id": 4738, "synset": "langur.n.01", "name": "langur"}, + {"id": 4739, "synset": "entellus.n.01", "name": "entellus"}, + {"id": 4740, "synset": "colobus.n.01", "name": "colobus"}, + {"id": 4741, "synset": "guereza.n.01", "name": "guereza"}, + {"id": 4742, "synset": "proboscis_monkey.n.01", "name": "proboscis_monkey"}, + {"id": 4743, "synset": "new_world_monkey.n.01", "name": "New_World_monkey"}, + {"id": 4744, "synset": "marmoset.n.01", "name": "marmoset"}, + {"id": 4745, "synset": "true_marmoset.n.01", "name": "true_marmoset"}, + {"id": 4746, "synset": "pygmy_marmoset.n.01", "name": "pygmy_marmoset"}, + {"id": 4747, "synset": "tamarin.n.01", "name": "tamarin"}, + {"id": 4748, "synset": "silky_tamarin.n.01", "name": "silky_tamarin"}, + {"id": 4749, "synset": "pinche.n.01", "name": "pinche"}, + {"id": 4750, "synset": "capuchin.n.02", "name": "capuchin"}, + {"id": 4751, "synset": "douroucouli.n.01", "name": "douroucouli"}, + {"id": 4752, "synset": "howler_monkey.n.01", "name": "howler_monkey"}, + {"id": 4753, "synset": "saki.n.03", "name": "saki"}, + {"id": 4754, "synset": "uakari.n.01", "name": "uakari"}, + {"id": 4755, "synset": "titi.n.03", "name": "titi"}, + {"id": 4756, "synset": "spider_monkey.n.01", "name": "spider_monkey"}, + {"id": 4757, "synset": "squirrel_monkey.n.01", "name": "squirrel_monkey"}, + {"id": 4758, "synset": "woolly_monkey.n.01", "name": "woolly_monkey"}, + {"id": 4759, "synset": "tree_shrew.n.01", "name": "tree_shrew"}, + {"id": 4760, "synset": "prosimian.n.01", "name": "prosimian"}, + {"id": 4761, "synset": "lemur.n.01", "name": "lemur"}, + {"id": 4762, "synset": "madagascar_cat.n.01", "name": "Madagascar_cat"}, + {"id": 4763, "synset": "aye-aye.n.01", "name": "aye-aye"}, + {"id": 4764, "synset": "slender_loris.n.01", "name": "slender_loris"}, + {"id": 4765, "synset": "slow_loris.n.01", "name": "slow_loris"}, + {"id": 4766, "synset": "potto.n.02", "name": "potto"}, + {"id": 4767, "synset": "angwantibo.n.01", "name": "angwantibo"}, + {"id": 4768, "synset": "galago.n.01", "name": "galago"}, + {"id": 4769, "synset": "indri.n.01", "name": "indri"}, + {"id": 4770, "synset": "woolly_indris.n.01", "name": "woolly_indris"}, + {"id": 4771, "synset": "tarsier.n.01", "name": "tarsier"}, + {"id": 4772, "synset": "tarsius_syrichta.n.01", "name": "Tarsius_syrichta"}, + {"id": 4773, "synset": "tarsius_glis.n.01", "name": "Tarsius_glis"}, + {"id": 4774, "synset": "flying_lemur.n.01", "name": "flying_lemur"}, + {"id": 4775, "synset": "cynocephalus_variegatus.n.01", "name": "Cynocephalus_variegatus"}, + {"id": 4776, "synset": "proboscidean.n.01", "name": "proboscidean"}, + {"id": 4777, "synset": "rogue_elephant.n.01", "name": "rogue_elephant"}, + {"id": 4778, "synset": "indian_elephant.n.01", "name": "Indian_elephant"}, + {"id": 4779, "synset": "african_elephant.n.01", "name": "African_elephant"}, + {"id": 4780, "synset": "woolly_mammoth.n.01", "name": "woolly_mammoth"}, + {"id": 4781, "synset": "columbian_mammoth.n.01", "name": "columbian_mammoth"}, + {"id": 4782, "synset": "imperial_mammoth.n.01", "name": "imperial_mammoth"}, + {"id": 4783, "synset": "mastodon.n.01", "name": "mastodon"}, + {"id": 4784, "synset": "plantigrade_mammal.n.01", "name": "plantigrade_mammal"}, + {"id": 4785, "synset": "digitigrade_mammal.n.01", "name": "digitigrade_mammal"}, + {"id": 4786, "synset": "procyonid.n.01", "name": "procyonid"}, + {"id": 4787, "synset": "raccoon.n.02", "name": "raccoon"}, + {"id": 4788, "synset": "common_raccoon.n.01", "name": "common_raccoon"}, + {"id": 4789, "synset": "crab-eating_raccoon.n.01", "name": "crab-eating_raccoon"}, + {"id": 4790, "synset": "bassarisk.n.01", "name": "bassarisk"}, + {"id": 4791, "synset": "kinkajou.n.01", "name": "kinkajou"}, + {"id": 4792, "synset": "coati.n.01", "name": "coati"}, + {"id": 4793, "synset": "lesser_panda.n.01", "name": "lesser_panda"}, + {"id": 4794, "synset": "twitterer.n.01", "name": "twitterer"}, + {"id": 4795, "synset": "fingerling.n.01", "name": "fingerling"}, + {"id": 4796, "synset": "game_fish.n.01", "name": "game_fish"}, + {"id": 4797, "synset": "food_fish.n.01", "name": "food_fish"}, + {"id": 4798, "synset": "rough_fish.n.01", "name": "rough_fish"}, + {"id": 4799, "synset": "groundfish.n.01", "name": "groundfish"}, + {"id": 4800, "synset": "young_fish.n.01", "name": "young_fish"}, + {"id": 4801, "synset": "parr.n.03", "name": "parr"}, + {"id": 4802, "synset": "mouthbreeder.n.01", "name": "mouthbreeder"}, + {"id": 4803, "synset": "spawner.n.01", "name": "spawner"}, + {"id": 4804, "synset": "barracouta.n.01", "name": "barracouta"}, + {"id": 4805, "synset": "crossopterygian.n.01", "name": "crossopterygian"}, + {"id": 4806, "synset": "coelacanth.n.01", "name": "coelacanth"}, + {"id": 4807, "synset": "lungfish.n.01", "name": "lungfish"}, + {"id": 4808, "synset": "ceratodus.n.01", "name": "ceratodus"}, + {"id": 4809, "synset": "catfish.n.03", "name": "catfish"}, + {"id": 4810, "synset": "silurid.n.01", "name": "silurid"}, + {"id": 4811, "synset": "european_catfish.n.01", "name": "European_catfish"}, + {"id": 4812, "synset": "electric_catfish.n.01", "name": "electric_catfish"}, + {"id": 4813, "synset": "bullhead.n.02", "name": "bullhead"}, + {"id": 4814, "synset": "horned_pout.n.01", "name": "horned_pout"}, + {"id": 4815, "synset": "brown_bullhead.n.01", "name": "brown_bullhead"}, + {"id": 4816, "synset": "channel_catfish.n.01", "name": "channel_catfish"}, + {"id": 4817, "synset": "blue_catfish.n.01", "name": "blue_catfish"}, + {"id": 4818, "synset": "flathead_catfish.n.01", "name": "flathead_catfish"}, + {"id": 4819, "synset": "armored_catfish.n.01", "name": "armored_catfish"}, + {"id": 4820, "synset": "sea_catfish.n.01", "name": "sea_catfish"}, + {"id": 4821, "synset": "gadoid.n.01", "name": "gadoid"}, + {"id": 4822, "synset": "cod.n.03", "name": "cod"}, + {"id": 4823, "synset": "codling.n.01", "name": "codling"}, + {"id": 4824, "synset": "atlantic_cod.n.01", "name": "Atlantic_cod"}, + {"id": 4825, "synset": "pacific_cod.n.01", "name": "Pacific_cod"}, + {"id": 4826, "synset": "whiting.n.06", "name": "whiting"}, + {"id": 4827, "synset": "burbot.n.01", "name": "burbot"}, + {"id": 4828, "synset": "haddock.n.02", "name": "haddock"}, + {"id": 4829, "synset": "pollack.n.03", "name": "pollack"}, + {"id": 4830, "synset": "hake.n.02", "name": "hake"}, + {"id": 4831, "synset": "silver_hake.n.01", "name": "silver_hake"}, + {"id": 4832, "synset": "ling.n.04", "name": "ling"}, + {"id": 4833, "synset": "cusk.n.02", "name": "cusk"}, + {"id": 4834, "synset": "grenadier.n.02", "name": "grenadier"}, + {"id": 4835, "synset": "eel.n.02", "name": "eel"}, + {"id": 4836, "synset": "elver.n.02", "name": "elver"}, + {"id": 4837, "synset": "common_eel.n.01", "name": "common_eel"}, + {"id": 4838, "synset": "tuna.n.04", "name": "tuna"}, + {"id": 4839, "synset": "moray.n.01", "name": "moray"}, + {"id": 4840, "synset": "conger.n.01", "name": "conger"}, + {"id": 4841, "synset": "teleost_fish.n.01", "name": "teleost_fish"}, + {"id": 4842, "synset": "beaked_salmon.n.01", "name": "beaked_salmon"}, + {"id": 4843, "synset": "clupeid_fish.n.01", "name": "clupeid_fish"}, + {"id": 4844, "synset": "whitebait.n.02", "name": "whitebait"}, + {"id": 4845, "synset": "brit.n.02", "name": "brit"}, + {"id": 4846, "synset": "shad.n.02", "name": "shad"}, + {"id": 4847, "synset": "common_american_shad.n.01", "name": "common_American_shad"}, + {"id": 4848, "synset": "river_shad.n.01", "name": "river_shad"}, + {"id": 4849, "synset": "allice_shad.n.01", "name": "allice_shad"}, + {"id": 4850, "synset": "alewife.n.02", "name": "alewife"}, + {"id": 4851, "synset": "menhaden.n.01", "name": "menhaden"}, + {"id": 4852, "synset": "herring.n.02", "name": "herring"}, + {"id": 4853, "synset": "atlantic_herring.n.01", "name": "Atlantic_herring"}, + {"id": 4854, "synset": "pacific_herring.n.01", "name": "Pacific_herring"}, + {"id": 4855, "synset": "sardine.n.02", "name": "sardine"}, + {"id": 4856, "synset": "sild.n.01", "name": "sild"}, + {"id": 4857, "synset": "brisling.n.02", "name": "brisling"}, + {"id": 4858, "synset": "pilchard.n.02", "name": "pilchard"}, + {"id": 4859, "synset": "pacific_sardine.n.01", "name": "Pacific_sardine"}, + {"id": 4860, "synset": "anchovy.n.02", "name": "anchovy"}, + {"id": 4861, "synset": "mediterranean_anchovy.n.01", "name": "mediterranean_anchovy"}, + {"id": 4862, "synset": "salmonid.n.01", "name": "salmonid"}, + {"id": 4863, "synset": "parr.n.02", "name": "parr"}, + {"id": 4864, "synset": "blackfish.n.02", "name": "blackfish"}, + {"id": 4865, "synset": "redfish.n.03", "name": "redfish"}, + {"id": 4866, "synset": "atlantic_salmon.n.02", "name": "Atlantic_salmon"}, + {"id": 4867, "synset": "landlocked_salmon.n.01", "name": "landlocked_salmon"}, + {"id": 4868, "synset": "sockeye.n.02", "name": "sockeye"}, + {"id": 4869, "synset": "chinook.n.05", "name": "chinook"}, + {"id": 4870, "synset": "coho.n.02", "name": "coho"}, + {"id": 4871, "synset": "trout.n.02", "name": "trout"}, + {"id": 4872, "synset": "brown_trout.n.01", "name": "brown_trout"}, + {"id": 4873, "synset": "rainbow_trout.n.02", "name": "rainbow_trout"}, + {"id": 4874, "synset": "sea_trout.n.03", "name": "sea_trout"}, + {"id": 4875, "synset": "lake_trout.n.02", "name": "lake_trout"}, + {"id": 4876, "synset": "brook_trout.n.02", "name": "brook_trout"}, + {"id": 4877, "synset": "char.n.03", "name": "char"}, + {"id": 4878, "synset": "arctic_char.n.01", "name": "Arctic_char"}, + {"id": 4879, "synset": "whitefish.n.03", "name": "whitefish"}, + {"id": 4880, "synset": "lake_whitefish.n.01", "name": "lake_whitefish"}, + {"id": 4881, "synset": "cisco.n.02", "name": "cisco"}, + {"id": 4882, "synset": "round_whitefish.n.01", "name": "round_whitefish"}, + {"id": 4883, "synset": "smelt.n.02", "name": "smelt"}, + {"id": 4884, "synset": "sparling.n.02", "name": "sparling"}, + {"id": 4885, "synset": "capelin.n.01", "name": "capelin"}, + {"id": 4886, "synset": "tarpon.n.01", "name": "tarpon"}, + {"id": 4887, "synset": "ladyfish.n.01", "name": "ladyfish"}, + {"id": 4888, "synset": "bonefish.n.01", "name": "bonefish"}, + {"id": 4889, "synset": "argentine.n.01", "name": "argentine"}, + {"id": 4890, "synset": "lanternfish.n.01", "name": "lanternfish"}, + {"id": 4891, "synset": "lizardfish.n.01", "name": "lizardfish"}, + {"id": 4892, "synset": "lancetfish.n.01", "name": "lancetfish"}, + {"id": 4893, "synset": "opah.n.01", "name": "opah"}, + {"id": 4894, "synset": "new_world_opah.n.01", "name": "New_World_opah"}, + {"id": 4895, "synset": "ribbonfish.n.02", "name": "ribbonfish"}, + {"id": 4896, "synset": "dealfish.n.01", "name": "dealfish"}, + {"id": 4897, "synset": "oarfish.n.01", "name": "oarfish"}, + {"id": 4898, "synset": "batfish.n.01", "name": "batfish"}, + {"id": 4899, "synset": "goosefish.n.01", "name": "goosefish"}, + {"id": 4900, "synset": "toadfish.n.01", "name": "toadfish"}, + {"id": 4901, "synset": "oyster_fish.n.01", "name": "oyster_fish"}, + {"id": 4902, "synset": "frogfish.n.01", "name": "frogfish"}, + {"id": 4903, "synset": "sargassum_fish.n.01", "name": "sargassum_fish"}, + {"id": 4904, "synset": "needlefish.n.01", "name": "needlefish"}, + {"id": 4905, "synset": "timucu.n.01", "name": "timucu"}, + {"id": 4906, "synset": "flying_fish.n.01", "name": "flying_fish"}, + {"id": 4907, "synset": "monoplane_flying_fish.n.01", "name": "monoplane_flying_fish"}, + {"id": 4908, "synset": "halfbeak.n.01", "name": "halfbeak"}, + {"id": 4909, "synset": "saury.n.01", "name": "saury"}, + {"id": 4910, "synset": "spiny-finned_fish.n.01", "name": "spiny-finned_fish"}, + {"id": 4911, "synset": "lingcod.n.02", "name": "lingcod"}, + {"id": 4912, "synset": "percoid_fish.n.01", "name": "percoid_fish"}, + {"id": 4913, "synset": "perch.n.07", "name": "perch"}, + {"id": 4914, "synset": "climbing_perch.n.01", "name": "climbing_perch"}, + {"id": 4915, "synset": "perch.n.06", "name": "perch"}, + {"id": 4916, "synset": "yellow_perch.n.01", "name": "yellow_perch"}, + {"id": 4917, "synset": "european_perch.n.01", "name": "European_perch"}, + {"id": 4918, "synset": "pike-perch.n.01", "name": "pike-perch"}, + {"id": 4919, "synset": "walleye.n.02", "name": "walleye"}, + {"id": 4920, "synset": "blue_pike.n.01", "name": "blue_pike"}, + {"id": 4921, "synset": "snail_darter.n.01", "name": "snail_darter"}, + {"id": 4922, "synset": "cusk-eel.n.01", "name": "cusk-eel"}, + {"id": 4923, "synset": "brotula.n.01", "name": "brotula"}, + {"id": 4924, "synset": "pearlfish.n.01", "name": "pearlfish"}, + {"id": 4925, "synset": "robalo.n.01", "name": "robalo"}, + {"id": 4926, "synset": "snook.n.01", "name": "snook"}, + {"id": 4927, "synset": "pike.n.05", "name": "pike"}, + {"id": 4928, "synset": "northern_pike.n.01", "name": "northern_pike"}, + {"id": 4929, "synset": "muskellunge.n.02", "name": "muskellunge"}, + {"id": 4930, "synset": "pickerel.n.02", "name": "pickerel"}, + {"id": 4931, "synset": "chain_pickerel.n.01", "name": "chain_pickerel"}, + {"id": 4932, "synset": "redfin_pickerel.n.01", "name": "redfin_pickerel"}, + {"id": 4933, "synset": "sunfish.n.03", "name": "sunfish"}, + {"id": 4934, "synset": "crappie.n.02", "name": "crappie"}, + {"id": 4935, "synset": "black_crappie.n.01", "name": "black_crappie"}, + {"id": 4936, "synset": "white_crappie.n.01", "name": "white_crappie"}, + {"id": 4937, "synset": "freshwater_bream.n.02", "name": "freshwater_bream"}, + {"id": 4938, "synset": "pumpkinseed.n.01", "name": "pumpkinseed"}, + {"id": 4939, "synset": "bluegill.n.01", "name": "bluegill"}, + {"id": 4940, "synset": "spotted_sunfish.n.01", "name": "spotted_sunfish"}, + {"id": 4941, "synset": "freshwater_bass.n.02", "name": "freshwater_bass"}, + {"id": 4942, "synset": "rock_bass.n.02", "name": "rock_bass"}, + {"id": 4943, "synset": "black_bass.n.02", "name": "black_bass"}, + {"id": 4944, "synset": "kentucky_black_bass.n.01", "name": "Kentucky_black_bass"}, + {"id": 4945, "synset": "smallmouth.n.01", "name": "smallmouth"}, + {"id": 4946, "synset": "largemouth.n.01", "name": "largemouth"}, + {"id": 4947, "synset": "bass.n.08", "name": "bass"}, + {"id": 4948, "synset": "serranid_fish.n.01", "name": "serranid_fish"}, + {"id": 4949, "synset": "white_perch.n.01", "name": "white_perch"}, + {"id": 4950, "synset": "yellow_bass.n.01", "name": "yellow_bass"}, + {"id": 4951, "synset": "blackmouth_bass.n.01", "name": "blackmouth_bass"}, + {"id": 4952, "synset": "rock_sea_bass.n.01", "name": "rock_sea_bass"}, + {"id": 4953, "synset": "striped_bass.n.02", "name": "striped_bass"}, + {"id": 4954, "synset": "stone_bass.n.01", "name": "stone_bass"}, + {"id": 4955, "synset": "grouper.n.02", "name": "grouper"}, + {"id": 4956, "synset": "hind.n.01", "name": "hind"}, + {"id": 4957, "synset": "rock_hind.n.01", "name": "rock_hind"}, + {"id": 4958, "synset": "creole-fish.n.01", "name": "creole-fish"}, + {"id": 4959, "synset": "jewfish.n.02", "name": "jewfish"}, + {"id": 4960, "synset": "soapfish.n.01", "name": "soapfish"}, + {"id": 4961, "synset": "surfperch.n.01", "name": "surfperch"}, + {"id": 4962, "synset": "rainbow_seaperch.n.01", "name": "rainbow_seaperch"}, + {"id": 4963, "synset": "bigeye.n.01", "name": "bigeye"}, + {"id": 4964, "synset": "catalufa.n.01", "name": "catalufa"}, + {"id": 4965, "synset": "cardinalfish.n.01", "name": "cardinalfish"}, + {"id": 4966, "synset": "flame_fish.n.01", "name": "flame_fish"}, + {"id": 4967, "synset": "tilefish.n.02", "name": "tilefish"}, + {"id": 4968, "synset": "bluefish.n.01", "name": "bluefish"}, + {"id": 4969, "synset": "cobia.n.01", "name": "cobia"}, + {"id": 4970, "synset": "remora.n.01", "name": "remora"}, + {"id": 4971, "synset": "sharksucker.n.01", "name": "sharksucker"}, + {"id": 4972, "synset": "whale_sucker.n.01", "name": "whale_sucker"}, + {"id": 4973, "synset": "carangid_fish.n.01", "name": "carangid_fish"}, + {"id": 4974, "synset": "jack.n.11", "name": "jack"}, + {"id": 4975, "synset": "crevalle_jack.n.01", "name": "crevalle_jack"}, + {"id": 4976, "synset": "yellow_jack.n.03", "name": "yellow_jack"}, + {"id": 4977, "synset": "runner.n.10", "name": "runner"}, + {"id": 4978, "synset": "rainbow_runner.n.01", "name": "rainbow_runner"}, + {"id": 4979, "synset": "leatherjacket.n.02", "name": "leatherjacket"}, + {"id": 4980, "synset": "threadfish.n.01", "name": "threadfish"}, + {"id": 4981, "synset": "moonfish.n.01", "name": "moonfish"}, + {"id": 4982, "synset": "lookdown.n.01", "name": "lookdown"}, + {"id": 4983, "synset": "amberjack.n.01", "name": "amberjack"}, + {"id": 4984, "synset": "yellowtail.n.02", "name": "yellowtail"}, + {"id": 4985, "synset": "kingfish.n.05", "name": "kingfish"}, + {"id": 4986, "synset": "pompano.n.02", "name": "pompano"}, + {"id": 4987, "synset": "florida_pompano.n.01", "name": "Florida_pompano"}, + {"id": 4988, "synset": "permit.n.03", "name": "permit"}, + {"id": 4989, "synset": "scad.n.01", "name": "scad"}, + {"id": 4990, "synset": "horse_mackerel.n.03", "name": "horse_mackerel"}, + {"id": 4991, "synset": "horse_mackerel.n.02", "name": "horse_mackerel"}, + {"id": 4992, "synset": "bigeye_scad.n.01", "name": "bigeye_scad"}, + {"id": 4993, "synset": "mackerel_scad.n.01", "name": "mackerel_scad"}, + {"id": 4994, "synset": "round_scad.n.01", "name": "round_scad"}, + {"id": 4995, "synset": "dolphinfish.n.02", "name": "dolphinfish"}, + {"id": 4996, "synset": "coryphaena_hippurus.n.01", "name": "Coryphaena_hippurus"}, + {"id": 4997, "synset": "coryphaena_equisetis.n.01", "name": "Coryphaena_equisetis"}, + {"id": 4998, "synset": "pomfret.n.01", "name": "pomfret"}, + {"id": 4999, "synset": "characin.n.01", "name": "characin"}, + {"id": 5000, "synset": "tetra.n.01", "name": "tetra"}, + {"id": 5001, "synset": "cardinal_tetra.n.01", "name": "cardinal_tetra"}, + {"id": 5002, "synset": "piranha.n.02", "name": "piranha"}, + {"id": 5003, "synset": "cichlid.n.01", "name": "cichlid"}, + {"id": 5004, "synset": "bolti.n.01", "name": "bolti"}, + {"id": 5005, "synset": "snapper.n.05", "name": "snapper"}, + {"id": 5006, "synset": "red_snapper.n.02", "name": "red_snapper"}, + {"id": 5007, "synset": "grey_snapper.n.01", "name": "grey_snapper"}, + {"id": 5008, "synset": "mutton_snapper.n.01", "name": "mutton_snapper"}, + {"id": 5009, "synset": "schoolmaster.n.03", "name": "schoolmaster"}, + {"id": 5010, "synset": "yellowtail.n.01", "name": "yellowtail"}, + {"id": 5011, "synset": "grunt.n.03", "name": "grunt"}, + {"id": 5012, "synset": "margate.n.01", "name": "margate"}, + {"id": 5013, "synset": "spanish_grunt.n.01", "name": "Spanish_grunt"}, + {"id": 5014, "synset": "tomtate.n.01", "name": "tomtate"}, + {"id": 5015, "synset": "cottonwick.n.01", "name": "cottonwick"}, + {"id": 5016, "synset": "sailor's-choice.n.02", "name": "sailor's-choice"}, + {"id": 5017, "synset": "porkfish.n.01", "name": "porkfish"}, + {"id": 5018, "synset": "pompon.n.02", "name": "pompon"}, + {"id": 5019, "synset": "pigfish.n.02", "name": "pigfish"}, + {"id": 5020, "synset": "sparid.n.01", "name": "sparid"}, + {"id": 5021, "synset": "sea_bream.n.02", "name": "sea_bream"}, + {"id": 5022, "synset": "porgy.n.02", "name": "porgy"}, + {"id": 5023, "synset": "red_porgy.n.01", "name": "red_porgy"}, + {"id": 5024, "synset": "european_sea_bream.n.01", "name": "European_sea_bream"}, + {"id": 5025, "synset": "atlantic_sea_bream.n.01", "name": "Atlantic_sea_bream"}, + {"id": 5026, "synset": "sheepshead.n.01", "name": "sheepshead"}, + {"id": 5027, "synset": "pinfish.n.01", "name": "pinfish"}, + {"id": 5028, "synset": "sheepshead_porgy.n.01", "name": "sheepshead_porgy"}, + {"id": 5029, "synset": "snapper.n.04", "name": "snapper"}, + {"id": 5030, "synset": "black_bream.n.01", "name": "black_bream"}, + {"id": 5031, "synset": "scup.n.04", "name": "scup"}, + {"id": 5032, "synset": "scup.n.03", "name": "scup"}, + {"id": 5033, "synset": "sciaenid_fish.n.01", "name": "sciaenid_fish"}, + {"id": 5034, "synset": "striped_drum.n.01", "name": "striped_drum"}, + {"id": 5035, "synset": "jackknife-fish.n.01", "name": "jackknife-fish"}, + {"id": 5036, "synset": "silver_perch.n.01", "name": "silver_perch"}, + {"id": 5037, "synset": "red_drum.n.01", "name": "red_drum"}, + {"id": 5038, "synset": "mulloway.n.01", "name": "mulloway"}, + {"id": 5039, "synset": "maigre.n.01", "name": "maigre"}, + {"id": 5040, "synset": "croaker.n.02", "name": "croaker"}, + {"id": 5041, "synset": "atlantic_croaker.n.01", "name": "Atlantic_croaker"}, + {"id": 5042, "synset": "yellowfin_croaker.n.01", "name": "yellowfin_croaker"}, + {"id": 5043, "synset": "whiting.n.04", "name": "whiting"}, + {"id": 5044, "synset": "kingfish.n.04", "name": "kingfish"}, + {"id": 5045, "synset": "king_whiting.n.01", "name": "king_whiting"}, + {"id": 5046, "synset": "northern_whiting.n.01", "name": "northern_whiting"}, + {"id": 5047, "synset": "corbina.n.01", "name": "corbina"}, + {"id": 5048, "synset": "white_croaker.n.02", "name": "white_croaker"}, + {"id": 5049, "synset": "white_croaker.n.01", "name": "white_croaker"}, + {"id": 5050, "synset": "sea_trout.n.02", "name": "sea_trout"}, + {"id": 5051, "synset": "weakfish.n.02", "name": "weakfish"}, + {"id": 5052, "synset": "spotted_weakfish.n.01", "name": "spotted_weakfish"}, + {"id": 5053, "synset": "mullet.n.03", "name": "mullet"}, + {"id": 5054, "synset": "goatfish.n.01", "name": "goatfish"}, + {"id": 5055, "synset": "red_goatfish.n.01", "name": "red_goatfish"}, + {"id": 5056, "synset": "yellow_goatfish.n.01", "name": "yellow_goatfish"}, + {"id": 5057, "synset": "mullet.n.02", "name": "mullet"}, + {"id": 5058, "synset": "striped_mullet.n.01", "name": "striped_mullet"}, + {"id": 5059, "synset": "white_mullet.n.01", "name": "white_mullet"}, + {"id": 5060, "synset": "liza.n.01", "name": "liza"}, + {"id": 5061, "synset": "silversides.n.01", "name": "silversides"}, + {"id": 5062, "synset": "jacksmelt.n.01", "name": "jacksmelt"}, + {"id": 5063, "synset": "barracuda.n.01", "name": "barracuda"}, + {"id": 5064, "synset": "great_barracuda.n.01", "name": "great_barracuda"}, + {"id": 5065, "synset": "sweeper.n.03", "name": "sweeper"}, + {"id": 5066, "synset": "sea_chub.n.01", "name": "sea_chub"}, + {"id": 5067, "synset": "bermuda_chub.n.01", "name": "Bermuda_chub"}, + {"id": 5068, "synset": "spadefish.n.01", "name": "spadefish"}, + {"id": 5069, "synset": "butterfly_fish.n.01", "name": "butterfly_fish"}, + {"id": 5070, "synset": "chaetodon.n.01", "name": "chaetodon"}, + {"id": 5071, "synset": "angelfish.n.01", "name": "angelfish"}, + {"id": 5072, "synset": "rock_beauty.n.01", "name": "rock_beauty"}, + {"id": 5073, "synset": "damselfish.n.01", "name": "damselfish"}, + {"id": 5074, "synset": "beaugregory.n.01", "name": "beaugregory"}, + {"id": 5075, "synset": "anemone_fish.n.01", "name": "anemone_fish"}, + {"id": 5076, "synset": "clown_anemone_fish.n.01", "name": "clown_anemone_fish"}, + {"id": 5077, "synset": "sergeant_major.n.02", "name": "sergeant_major"}, + {"id": 5078, "synset": "wrasse.n.01", "name": "wrasse"}, + {"id": 5079, "synset": "pigfish.n.01", "name": "pigfish"}, + {"id": 5080, "synset": "hogfish.n.01", "name": "hogfish"}, + {"id": 5081, "synset": "slippery_dick.n.01", "name": "slippery_dick"}, + {"id": 5082, "synset": "puddingwife.n.01", "name": "puddingwife"}, + {"id": 5083, "synset": "bluehead.n.01", "name": "bluehead"}, + {"id": 5084, "synset": "pearly_razorfish.n.01", "name": "pearly_razorfish"}, + {"id": 5085, "synset": "tautog.n.01", "name": "tautog"}, + {"id": 5086, "synset": "cunner.n.01", "name": "cunner"}, + {"id": 5087, "synset": "parrotfish.n.01", "name": "parrotfish"}, + {"id": 5088, "synset": "threadfin.n.01", "name": "threadfin"}, + {"id": 5089, "synset": "jawfish.n.01", "name": "jawfish"}, + {"id": 5090, "synset": "stargazer.n.03", "name": "stargazer"}, + {"id": 5091, "synset": "sand_stargazer.n.01", "name": "sand_stargazer"}, + {"id": 5092, "synset": "blenny.n.01", "name": "blenny"}, + {"id": 5093, "synset": "shanny.n.01", "name": "shanny"}, + {"id": 5094, "synset": "molly_miller.n.01", "name": "Molly_Miller"}, + {"id": 5095, "synset": "clinid.n.01", "name": "clinid"}, + {"id": 5096, "synset": "pikeblenny.n.01", "name": "pikeblenny"}, + {"id": 5097, "synset": "bluethroat_pikeblenny.n.01", "name": "bluethroat_pikeblenny"}, + {"id": 5098, "synset": "gunnel.n.02", "name": "gunnel"}, + {"id": 5099, "synset": "rock_gunnel.n.01", "name": "rock_gunnel"}, + {"id": 5100, "synset": "eelblenny.n.01", "name": "eelblenny"}, + {"id": 5101, "synset": "wrymouth.n.01", "name": "wrymouth"}, + {"id": 5102, "synset": "wolffish.n.01", "name": "wolffish"}, + {"id": 5103, "synset": "viviparous_eelpout.n.01", "name": "viviparous_eelpout"}, + {"id": 5104, "synset": "ocean_pout.n.01", "name": "ocean_pout"}, + {"id": 5105, "synset": "sand_lance.n.01", "name": "sand_lance"}, + {"id": 5106, "synset": "dragonet.n.01", "name": "dragonet"}, + {"id": 5107, "synset": "goby.n.01", "name": "goby"}, + {"id": 5108, "synset": "mudskipper.n.01", "name": "mudskipper"}, + {"id": 5109, "synset": "sleeper.n.08", "name": "sleeper"}, + {"id": 5110, "synset": "flathead.n.02", "name": "flathead"}, + {"id": 5111, "synset": "archerfish.n.01", "name": "archerfish"}, + {"id": 5112, "synset": "surgeonfish.n.01", "name": "surgeonfish"}, + {"id": 5113, "synset": "gempylid.n.01", "name": "gempylid"}, + {"id": 5114, "synset": "snake_mackerel.n.01", "name": "snake_mackerel"}, + {"id": 5115, "synset": "escolar.n.01", "name": "escolar"}, + {"id": 5116, "synset": "oilfish.n.01", "name": "oilfish"}, + {"id": 5117, "synset": "cutlassfish.n.01", "name": "cutlassfish"}, + {"id": 5118, "synset": "scombroid.n.01", "name": "scombroid"}, + {"id": 5119, "synset": "mackerel.n.02", "name": "mackerel"}, + {"id": 5120, "synset": "common_mackerel.n.01", "name": "common_mackerel"}, + {"id": 5121, "synset": "spanish_mackerel.n.03", "name": "Spanish_mackerel"}, + {"id": 5122, "synset": "chub_mackerel.n.01", "name": "chub_mackerel"}, + {"id": 5123, "synset": "wahoo.n.03", "name": "wahoo"}, + {"id": 5124, "synset": "spanish_mackerel.n.02", "name": "Spanish_mackerel"}, + {"id": 5125, "synset": "king_mackerel.n.01", "name": "king_mackerel"}, + {"id": 5126, "synset": "scomberomorus_maculatus.n.01", "name": "Scomberomorus_maculatus"}, + {"id": 5127, "synset": "cero.n.01", "name": "cero"}, + {"id": 5128, "synset": "sierra.n.02", "name": "sierra"}, + {"id": 5129, "synset": "tuna.n.03", "name": "tuna"}, + {"id": 5130, "synset": "albacore.n.02", "name": "albacore"}, + {"id": 5131, "synset": "bluefin.n.02", "name": "bluefin"}, + {"id": 5132, "synset": "yellowfin.n.01", "name": "yellowfin"}, + {"id": 5133, "synset": "bonito.n.03", "name": "bonito"}, + {"id": 5134, "synset": "skipjack.n.02", "name": "skipjack"}, + {"id": 5135, "synset": "chile_bonito.n.01", "name": "Chile_bonito"}, + {"id": 5136, "synset": "skipjack.n.01", "name": "skipjack"}, + {"id": 5137, "synset": "bonito.n.02", "name": "bonito"}, + {"id": 5138, "synset": "swordfish.n.02", "name": "swordfish"}, + {"id": 5139, "synset": "sailfish.n.02", "name": "sailfish"}, + {"id": 5140, "synset": "atlantic_sailfish.n.01", "name": "Atlantic_sailfish"}, + {"id": 5141, "synset": "billfish.n.02", "name": "billfish"}, + {"id": 5142, "synset": "marlin.n.01", "name": "marlin"}, + {"id": 5143, "synset": "blue_marlin.n.01", "name": "blue_marlin"}, + {"id": 5144, "synset": "black_marlin.n.01", "name": "black_marlin"}, + {"id": 5145, "synset": "striped_marlin.n.01", "name": "striped_marlin"}, + {"id": 5146, "synset": "white_marlin.n.01", "name": "white_marlin"}, + {"id": 5147, "synset": "spearfish.n.01", "name": "spearfish"}, + {"id": 5148, "synset": "louvar.n.01", "name": "louvar"}, + {"id": 5149, "synset": "dollarfish.n.01", "name": "dollarfish"}, + {"id": 5150, "synset": "palometa.n.01", "name": "palometa"}, + {"id": 5151, "synset": "harvestfish.n.01", "name": "harvestfish"}, + {"id": 5152, "synset": "driftfish.n.01", "name": "driftfish"}, + {"id": 5153, "synset": "barrelfish.n.01", "name": "barrelfish"}, + {"id": 5154, "synset": "clingfish.n.01", "name": "clingfish"}, + {"id": 5155, "synset": "tripletail.n.01", "name": "tripletail"}, + {"id": 5156, "synset": "atlantic_tripletail.n.01", "name": "Atlantic_tripletail"}, + {"id": 5157, "synset": "pacific_tripletail.n.01", "name": "Pacific_tripletail"}, + {"id": 5158, "synset": "mojarra.n.01", "name": "mojarra"}, + {"id": 5159, "synset": "yellowfin_mojarra.n.01", "name": "yellowfin_mojarra"}, + {"id": 5160, "synset": "silver_jenny.n.01", "name": "silver_jenny"}, + {"id": 5161, "synset": "whiting.n.03", "name": "whiting"}, + {"id": 5162, "synset": "ganoid.n.01", "name": "ganoid"}, + {"id": 5163, "synset": "bowfin.n.01", "name": "bowfin"}, + {"id": 5164, "synset": "paddlefish.n.01", "name": "paddlefish"}, + {"id": 5165, "synset": "chinese_paddlefish.n.01", "name": "Chinese_paddlefish"}, + {"id": 5166, "synset": "sturgeon.n.01", "name": "sturgeon"}, + {"id": 5167, "synset": "pacific_sturgeon.n.01", "name": "Pacific_sturgeon"}, + {"id": 5168, "synset": "beluga.n.01", "name": "beluga"}, + {"id": 5169, "synset": "gar.n.01", "name": "gar"}, + {"id": 5170, "synset": "scorpaenoid.n.01", "name": "scorpaenoid"}, + {"id": 5171, "synset": "scorpaenid.n.01", "name": "scorpaenid"}, + {"id": 5172, "synset": "scorpionfish.n.01", "name": "scorpionfish"}, + {"id": 5173, "synset": "plumed_scorpionfish.n.01", "name": "plumed_scorpionfish"}, + {"id": 5174, "synset": "lionfish.n.01", "name": "lionfish"}, + {"id": 5175, "synset": "stonefish.n.01", "name": "stonefish"}, + {"id": 5176, "synset": "rockfish.n.02", "name": "rockfish"}, + {"id": 5177, "synset": "copper_rockfish.n.01", "name": "copper_rockfish"}, + {"id": 5178, "synset": "vermillion_rockfish.n.01", "name": "vermillion_rockfish"}, + {"id": 5179, "synset": "red_rockfish.n.02", "name": "red_rockfish"}, + {"id": 5180, "synset": "rosefish.n.02", "name": "rosefish"}, + {"id": 5181, "synset": "bullhead.n.01", "name": "bullhead"}, + {"id": 5182, "synset": "miller's-thumb.n.01", "name": "miller's-thumb"}, + {"id": 5183, "synset": "sea_raven.n.01", "name": "sea_raven"}, + {"id": 5184, "synset": "lumpfish.n.01", "name": "lumpfish"}, + {"id": 5185, "synset": "lumpsucker.n.01", "name": "lumpsucker"}, + {"id": 5186, "synset": "pogge.n.01", "name": "pogge"}, + {"id": 5187, "synset": "greenling.n.01", "name": "greenling"}, + {"id": 5188, "synset": "kelp_greenling.n.01", "name": "kelp_greenling"}, + {"id": 5189, "synset": "painted_greenling.n.01", "name": "painted_greenling"}, + {"id": 5190, "synset": "flathead.n.01", "name": "flathead"}, + {"id": 5191, "synset": "gurnard.n.01", "name": "gurnard"}, + {"id": 5192, "synset": "tub_gurnard.n.01", "name": "tub_gurnard"}, + {"id": 5193, "synset": "sea_robin.n.01", "name": "sea_robin"}, + {"id": 5194, "synset": "northern_sea_robin.n.01", "name": "northern_sea_robin"}, + {"id": 5195, "synset": "flying_gurnard.n.01", "name": "flying_gurnard"}, + {"id": 5196, "synset": "plectognath.n.01", "name": "plectognath"}, + {"id": 5197, "synset": "triggerfish.n.01", "name": "triggerfish"}, + {"id": 5198, "synset": "queen_triggerfish.n.01", "name": "queen_triggerfish"}, + {"id": 5199, "synset": "filefish.n.01", "name": "filefish"}, + {"id": 5200, "synset": "leatherjacket.n.01", "name": "leatherjacket"}, + {"id": 5201, "synset": "boxfish.n.01", "name": "boxfish"}, + {"id": 5202, "synset": "cowfish.n.01", "name": "cowfish"}, + {"id": 5203, "synset": "spiny_puffer.n.01", "name": "spiny_puffer"}, + {"id": 5204, "synset": "porcupinefish.n.01", "name": "porcupinefish"}, + {"id": 5205, "synset": "balloonfish.n.01", "name": "balloonfish"}, + {"id": 5206, "synset": "burrfish.n.01", "name": "burrfish"}, + {"id": 5207, "synset": "ocean_sunfish.n.01", "name": "ocean_sunfish"}, + {"id": 5208, "synset": "sharptail_mola.n.01", "name": "sharptail_mola"}, + {"id": 5209, "synset": "flatfish.n.02", "name": "flatfish"}, + {"id": 5210, "synset": "flounder.n.02", "name": "flounder"}, + {"id": 5211, "synset": "righteye_flounder.n.01", "name": "righteye_flounder"}, + {"id": 5212, "synset": "plaice.n.02", "name": "plaice"}, + {"id": 5213, "synset": "european_flatfish.n.01", "name": "European_flatfish"}, + {"id": 5214, "synset": "yellowtail_flounder.n.02", "name": "yellowtail_flounder"}, + {"id": 5215, "synset": "winter_flounder.n.02", "name": "winter_flounder"}, + {"id": 5216, "synset": "lemon_sole.n.05", "name": "lemon_sole"}, + {"id": 5217, "synset": "american_plaice.n.01", "name": "American_plaice"}, + {"id": 5218, "synset": "halibut.n.02", "name": "halibut"}, + {"id": 5219, "synset": "atlantic_halibut.n.01", "name": "Atlantic_halibut"}, + {"id": 5220, "synset": "pacific_halibut.n.01", "name": "Pacific_halibut"}, + {"id": 5221, "synset": "lefteye_flounder.n.01", "name": "lefteye_flounder"}, + {"id": 5222, "synset": "southern_flounder.n.01", "name": "southern_flounder"}, + {"id": 5223, "synset": "summer_flounder.n.01", "name": "summer_flounder"}, + {"id": 5224, "synset": "whiff.n.02", "name": "whiff"}, + {"id": 5225, "synset": "horned_whiff.n.01", "name": "horned_whiff"}, + {"id": 5226, "synset": "sand_dab.n.02", "name": "sand_dab"}, + {"id": 5227, "synset": "windowpane.n.02", "name": "windowpane"}, + {"id": 5228, "synset": "brill.n.01", "name": "brill"}, + {"id": 5229, "synset": "turbot.n.02", "name": "turbot"}, + {"id": 5230, "synset": "tonguefish.n.01", "name": "tonguefish"}, + {"id": 5231, "synset": "sole.n.04", "name": "sole"}, + {"id": 5232, "synset": "european_sole.n.01", "name": "European_sole"}, + {"id": 5233, "synset": "english_sole.n.02", "name": "English_sole"}, + {"id": 5234, "synset": "hogchoker.n.01", "name": "hogchoker"}, + {"id": 5235, "synset": "aba.n.02", "name": "aba"}, + {"id": 5236, "synset": "abacus.n.02", "name": "abacus"}, + {"id": 5237, "synset": "abandoned_ship.n.01", "name": "abandoned_ship"}, + {"id": 5238, "synset": "a_battery.n.01", "name": "A_battery"}, + {"id": 5239, "synset": "abattoir.n.01", "name": "abattoir"}, + {"id": 5240, "synset": "abaya.n.01", "name": "abaya"}, + {"id": 5241, "synset": "abbe_condenser.n.01", "name": "Abbe_condenser"}, + {"id": 5242, "synset": "abbey.n.03", "name": "abbey"}, + {"id": 5243, "synset": "abbey.n.02", "name": "abbey"}, + {"id": 5244, "synset": "abbey.n.01", "name": "abbey"}, + {"id": 5245, "synset": "abney_level.n.01", "name": "Abney_level"}, + {"id": 5246, "synset": "abrader.n.01", "name": "abrader"}, + {"id": 5247, "synset": "abrading_stone.n.01", "name": "abrading_stone"}, + {"id": 5248, "synset": "abutment.n.02", "name": "abutment"}, + {"id": 5249, "synset": "abutment_arch.n.01", "name": "abutment_arch"}, + {"id": 5250, "synset": "academic_costume.n.01", "name": "academic_costume"}, + {"id": 5251, "synset": "academic_gown.n.01", "name": "academic_gown"}, + {"id": 5252, "synset": "accelerator.n.02", "name": "accelerator"}, + {"id": 5253, "synset": "accelerator.n.04", "name": "accelerator"}, + {"id": 5254, "synset": "accelerator.n.01", "name": "accelerator"}, + {"id": 5255, "synset": "accelerometer.n.01", "name": "accelerometer"}, + {"id": 5256, "synset": "accessory.n.01", "name": "accessory"}, + {"id": 5257, "synset": "accommodating_lens_implant.n.01", "name": "accommodating_lens_implant"}, + {"id": 5258, "synset": "accommodation.n.04", "name": "accommodation"}, + {"id": 5259, "synset": "accordion.n.01", "name": "accordion"}, + {"id": 5260, "synset": "acetate_disk.n.01", "name": "acetate_disk"}, + {"id": 5261, "synset": "acetate_rayon.n.01", "name": "acetate_rayon"}, + {"id": 5262, "synset": "achromatic_lens.n.01", "name": "achromatic_lens"}, + {"id": 5263, "synset": "acoustic_delay_line.n.01", "name": "acoustic_delay_line"}, + {"id": 5264, "synset": "acoustic_device.n.01", "name": "acoustic_device"}, + {"id": 5265, "synset": "acoustic_guitar.n.01", "name": "acoustic_guitar"}, + {"id": 5266, "synset": "acoustic_modem.n.01", "name": "acoustic_modem"}, + {"id": 5267, "synset": "acropolis.n.01", "name": "acropolis"}, + {"id": 5268, "synset": "acrylic.n.04", "name": "acrylic"}, + {"id": 5269, "synset": "acrylic.n.03", "name": "acrylic"}, + {"id": 5270, "synset": "actinometer.n.01", "name": "actinometer"}, + {"id": 5271, "synset": "action.n.07", "name": "action"}, + {"id": 5272, "synset": "active_matrix_screen.n.01", "name": "active_matrix_screen"}, + {"id": 5273, "synset": "actuator.n.01", "name": "actuator"}, + {"id": 5274, "synset": "adapter.n.02", "name": "adapter"}, + {"id": 5275, "synset": "adder.n.02", "name": "adder"}, + {"id": 5276, "synset": "adding_machine.n.01", "name": "adding_machine"}, + {"id": 5277, "synset": "addressing_machine.n.01", "name": "addressing_machine"}, + {"id": 5278, "synset": "adhesive_bandage.n.01", "name": "adhesive_bandage"}, + {"id": 5279, "synset": "adit.n.01", "name": "adit"}, + {"id": 5280, "synset": "adjoining_room.n.01", "name": "adjoining_room"}, + {"id": 5281, "synset": "adjustable_wrench.n.01", "name": "adjustable_wrench"}, + {"id": 5282, "synset": "adobe.n.02", "name": "adobe"}, + {"id": 5283, "synset": "adz.n.01", "name": "adz"}, + {"id": 5284, "synset": "aeolian_harp.n.01", "name": "aeolian_harp"}, + {"id": 5285, "synset": "aerator.n.01", "name": "aerator"}, + {"id": 5286, "synset": "aerial_torpedo.n.01", "name": "aerial_torpedo"}, + {"id": 5287, "synset": "aertex.n.01", "name": "Aertex"}, + {"id": 5288, "synset": "afghan.n.01", "name": "afghan"}, + {"id": 5289, "synset": "afro-wig.n.01", "name": "Afro-wig"}, + {"id": 5290, "synset": "afterburner.n.01", "name": "afterburner"}, + {"id": 5291, "synset": "after-shave.n.01", "name": "after-shave"}, + {"id": 5292, "synset": "agateware.n.01", "name": "agateware"}, + {"id": 5293, "synset": "agglomerator.n.01", "name": "agglomerator"}, + {"id": 5294, "synset": "aglet.n.02", "name": "aglet"}, + {"id": 5295, "synset": "aglet.n.01", "name": "aglet"}, + {"id": 5296, "synset": "agora.n.03", "name": "agora"}, + {"id": 5297, "synset": "aigrette.n.01", "name": "aigrette"}, + {"id": 5298, "synset": "aileron.n.01", "name": "aileron"}, + {"id": 5299, "synset": "air_bag.n.01", "name": "air_bag"}, + {"id": 5300, "synset": "airbrake.n.02", "name": "airbrake"}, + {"id": 5301, "synset": "airbrush.n.01", "name": "airbrush"}, + {"id": 5302, "synset": "airbus.n.01", "name": "airbus"}, + {"id": 5303, "synset": "air_compressor.n.01", "name": "air_compressor"}, + {"id": 5304, "synset": "aircraft.n.01", "name": "aircraft"}, + {"id": 5305, "synset": "aircraft_carrier.n.01", "name": "aircraft_carrier"}, + {"id": 5306, "synset": "aircraft_engine.n.01", "name": "aircraft_engine"}, + {"id": 5307, "synset": "air_cushion.n.02", "name": "air_cushion"}, + {"id": 5308, "synset": "airdock.n.01", "name": "airdock"}, + {"id": 5309, "synset": "airfield.n.01", "name": "airfield"}, + {"id": 5310, "synset": "air_filter.n.01", "name": "air_filter"}, + {"id": 5311, "synset": "airfoil.n.01", "name": "airfoil"}, + {"id": 5312, "synset": "airframe.n.01", "name": "airframe"}, + {"id": 5313, "synset": "air_gun.n.01", "name": "air_gun"}, + {"id": 5314, "synset": "air_hammer.n.01", "name": "air_hammer"}, + {"id": 5315, "synset": "air_horn.n.01", "name": "air_horn"}, + {"id": 5316, "synset": "airing_cupboard.n.01", "name": "airing_cupboard"}, + {"id": 5317, "synset": "airliner.n.01", "name": "airliner"}, + {"id": 5318, "synset": "airmailer.n.01", "name": "airmailer"}, + {"id": 5319, "synset": "airplane_propeller.n.01", "name": "airplane_propeller"}, + {"id": 5320, "synset": "airport.n.01", "name": "airport"}, + {"id": 5321, "synset": "air_pump.n.01", "name": "air_pump"}, + {"id": 5322, "synset": "air_search_radar.n.01", "name": "air_search_radar"}, + {"id": 5323, "synset": "airship.n.01", "name": "airship"}, + {"id": 5324, "synset": "air_terminal.n.01", "name": "air_terminal"}, + {"id": 5325, "synset": "air-to-air_missile.n.01", "name": "air-to-air_missile"}, + {"id": 5326, "synset": "air-to-ground_missile.n.01", "name": "air-to-ground_missile"}, + {"id": 5327, "synset": "aisle.n.03", "name": "aisle"}, + {"id": 5328, "synset": "aladdin's_lamp.n.01", "name": "Aladdin's_lamp"}, + {"id": 5329, "synset": "alarm.n.02", "name": "alarm"}, + {"id": 5330, "synset": "alb.n.01", "name": "alb"}, + {"id": 5331, "synset": "alcazar.n.01", "name": "alcazar"}, + {"id": 5332, "synset": "alcohol_thermometer.n.01", "name": "alcohol_thermometer"}, + {"id": 5333, "synset": "alehouse.n.01", "name": "alehouse"}, + {"id": 5334, "synset": "alembic.n.01", "name": "alembic"}, + {"id": 5335, "synset": "algometer.n.01", "name": "algometer"}, + {"id": 5336, "synset": "alidade.n.02", "name": "alidade"}, + {"id": 5337, "synset": "alidade.n.01", "name": "alidade"}, + {"id": 5338, "synset": "a-line.n.01", "name": "A-line"}, + {"id": 5339, "synset": "allen_screw.n.01", "name": "Allen_screw"}, + {"id": 5340, "synset": "allen_wrench.n.01", "name": "Allen_wrench"}, + {"id": 5341, "synset": "alligator_wrench.n.01", "name": "alligator_wrench"}, + {"id": 5342, "synset": "alms_dish.n.01", "name": "alms_dish"}, + {"id": 5343, "synset": "alpaca.n.02", "name": "alpaca"}, + {"id": 5344, "synset": "alpenstock.n.01", "name": "alpenstock"}, + {"id": 5345, "synset": "altar.n.02", "name": "altar"}, + {"id": 5346, "synset": "altar.n.01", "name": "altar"}, + {"id": 5347, "synset": "altarpiece.n.01", "name": "altarpiece"}, + {"id": 5348, "synset": "altazimuth.n.01", "name": "altazimuth"}, + {"id": 5349, "synset": "alternator.n.01", "name": "alternator"}, + {"id": 5350, "synset": "altimeter.n.01", "name": "altimeter"}, + {"id": 5351, "synset": "amati.n.02", "name": "Amati"}, + {"id": 5352, "synset": "amen_corner.n.01", "name": "amen_corner"}, + {"id": 5353, "synset": "american_organ.n.01", "name": "American_organ"}, + {"id": 5354, "synset": "ammeter.n.01", "name": "ammeter"}, + {"id": 5355, "synset": "ammonia_clock.n.01", "name": "ammonia_clock"}, + {"id": 5356, "synset": "ammunition.n.01", "name": "ammunition"}, + {"id": 5357, "synset": "amphibian.n.02", "name": "amphibian"}, + {"id": 5358, "synset": "amphibian.n.01", "name": "amphibian"}, + {"id": 5359, "synset": "amphitheater.n.02", "name": "amphitheater"}, + {"id": 5360, "synset": "amphitheater.n.01", "name": "amphitheater"}, + {"id": 5361, "synset": "amphora.n.01", "name": "amphora"}, + {"id": 5362, "synset": "ampulla.n.02", "name": "ampulla"}, + {"id": 5363, "synset": "amusement_arcade.n.01", "name": "amusement_arcade"}, + {"id": 5364, "synset": "analog_clock.n.01", "name": "analog_clock"}, + {"id": 5365, "synset": "analog_computer.n.01", "name": "analog_computer"}, + {"id": 5366, "synset": "analog_watch.n.01", "name": "analog_watch"}, + {"id": 5367, "synset": "analytical_balance.n.01", "name": "analytical_balance"}, + {"id": 5368, "synset": "analyzer.n.01", "name": "analyzer"}, + {"id": 5369, "synset": "anamorphosis.n.02", "name": "anamorphosis"}, + {"id": 5370, "synset": "anastigmat.n.01", "name": "anastigmat"}, + {"id": 5371, "synset": "anchor.n.01", "name": "anchor"}, + {"id": 5372, "synset": "anchor_chain.n.01", "name": "anchor_chain"}, + {"id": 5373, "synset": "anchor_light.n.01", "name": "anchor_light"}, + {"id": 5374, "synset": "and_circuit.n.01", "name": "AND_circuit"}, + {"id": 5375, "synset": "andiron.n.01", "name": "andiron"}, + {"id": 5376, "synset": "android.n.01", "name": "android"}, + {"id": 5377, "synset": "anechoic_chamber.n.01", "name": "anechoic_chamber"}, + {"id": 5378, "synset": "anemometer.n.01", "name": "anemometer"}, + {"id": 5379, "synset": "aneroid_barometer.n.01", "name": "aneroid_barometer"}, + {"id": 5380, "synset": "angiocardiogram.n.01", "name": "angiocardiogram"}, + {"id": 5381, "synset": "angioscope.n.01", "name": "angioscope"}, + {"id": 5382, "synset": "angle_bracket.n.02", "name": "angle_bracket"}, + {"id": 5383, "synset": "angledozer.n.01", "name": "angledozer"}, + {"id": 5384, "synset": "ankle_brace.n.01", "name": "ankle_brace"}, + {"id": 5385, "synset": "anklet.n.02", "name": "anklet"}, + {"id": 5386, "synset": "anklet.n.01", "name": "anklet"}, + {"id": 5387, "synset": "ankus.n.01", "name": "ankus"}, + {"id": 5388, "synset": "anode.n.01", "name": "anode"}, + {"id": 5389, "synset": "anode.n.02", "name": "anode"}, + {"id": 5390, "synset": "answering_machine.n.01", "name": "answering_machine"}, + {"id": 5391, "synset": "anteroom.n.01", "name": "anteroom"}, + {"id": 5392, "synset": "antiaircraft.n.01", "name": "antiaircraft"}, + {"id": 5393, "synset": "antiballistic_missile.n.01", "name": "antiballistic_missile"}, + {"id": 5394, "synset": "antifouling_paint.n.01", "name": "antifouling_paint"}, + {"id": 5395, "synset": "anti-g_suit.n.01", "name": "anti-G_suit"}, + {"id": 5396, "synset": "antimacassar.n.01", "name": "antimacassar"}, + {"id": 5397, "synset": "antiperspirant.n.01", "name": "antiperspirant"}, + {"id": 5398, "synset": "anti-submarine_rocket.n.01", "name": "anti-submarine_rocket"}, + {"id": 5399, "synset": "anvil.n.01", "name": "anvil"}, + {"id": 5400, "synset": "ao_dai.n.01", "name": "ao_dai"}, + {"id": 5401, "synset": "apadana.n.01", "name": "apadana"}, + {"id": 5402, "synset": "apartment.n.01", "name": "apartment"}, + {"id": 5403, "synset": "apartment_building.n.01", "name": "apartment_building"}, + {"id": 5404, "synset": "aperture.n.03", "name": "aperture"}, + {"id": 5405, "synset": "aperture.n.01", "name": "aperture"}, + {"id": 5406, "synset": "apiary.n.01", "name": "apiary"}, + {"id": 5407, "synset": "apparatus.n.01", "name": "apparatus"}, + {"id": 5408, "synset": "apparel.n.01", "name": "apparel"}, + {"id": 5409, "synset": "applecart.n.02", "name": "applecart"}, + {"id": 5410, "synset": "appliance.n.02", "name": "appliance"}, + {"id": 5411, "synset": "appliance.n.01", "name": "appliance"}, + {"id": 5412, "synset": "applicator.n.01", "name": "applicator"}, + {"id": 5413, "synset": "appointment.n.03", "name": "appointment"}, + {"id": 5414, "synset": "apron_string.n.01", "name": "apron_string"}, + {"id": 5415, "synset": "apse.n.01", "name": "apse"}, + {"id": 5416, "synset": "aqualung.n.01", "name": "aqualung"}, + {"id": 5417, "synset": "aquaplane.n.01", "name": "aquaplane"}, + {"id": 5418, "synset": "arabesque.n.02", "name": "arabesque"}, + {"id": 5419, "synset": "arbor.n.03", "name": "arbor"}, + {"id": 5420, "synset": "arcade.n.02", "name": "arcade"}, + {"id": 5421, "synset": "arch.n.04", "name": "arch"}, + {"id": 5422, "synset": "architecture.n.01", "name": "architecture"}, + {"id": 5423, "synset": "architrave.n.02", "name": "architrave"}, + {"id": 5424, "synset": "arch_support.n.01", "name": "arch_support"}, + {"id": 5425, "synset": "arc_lamp.n.01", "name": "arc_lamp"}, + {"id": 5426, "synset": "area.n.05", "name": "area"}, + {"id": 5427, "synset": "areaway.n.01", "name": "areaway"}, + {"id": 5428, "synset": "argyle.n.03", "name": "argyle"}, + {"id": 5429, "synset": "ark.n.02", "name": "ark"}, + {"id": 5430, "synset": "arm.n.04", "name": "arm"}, + {"id": 5431, "synset": "armament.n.01", "name": "armament"}, + {"id": 5432, "synset": "armature.n.01", "name": "armature"}, + {"id": 5433, "synset": "armet.n.01", "name": "armet"}, + {"id": 5434, "synset": "arm_guard.n.01", "name": "arm_guard"}, + {"id": 5435, "synset": "armhole.n.01", "name": "armhole"}, + {"id": 5436, "synset": "armilla.n.02", "name": "armilla"}, + {"id": 5437, "synset": "armlet.n.01", "name": "armlet"}, + {"id": 5438, "synset": "armored_car.n.02", "name": "armored_car"}, + {"id": 5439, "synset": "armored_car.n.01", "name": "armored_car"}, + {"id": 5440, "synset": "armored_personnel_carrier.n.01", "name": "armored_personnel_carrier"}, + {"id": 5441, "synset": "armored_vehicle.n.01", "name": "armored_vehicle"}, + {"id": 5442, "synset": "armor_plate.n.01", "name": "armor_plate"}, + {"id": 5443, "synset": "armory.n.04", "name": "armory"}, + {"id": 5444, "synset": "armrest.n.01", "name": "armrest"}, + {"id": 5445, "synset": "arquebus.n.01", "name": "arquebus"}, + {"id": 5446, "synset": "array.n.04", "name": "array"}, + {"id": 5447, "synset": "array.n.03", "name": "array"}, + {"id": 5448, "synset": "arrester.n.01", "name": "arrester"}, + {"id": 5449, "synset": "arrow.n.02", "name": "arrow"}, + {"id": 5450, "synset": "arsenal.n.01", "name": "arsenal"}, + {"id": 5451, "synset": "arterial_road.n.01", "name": "arterial_road"}, + {"id": 5452, "synset": "arthrogram.n.01", "name": "arthrogram"}, + {"id": 5453, "synset": "arthroscope.n.01", "name": "arthroscope"}, + {"id": 5454, "synset": "artificial_heart.n.01", "name": "artificial_heart"}, + {"id": 5455, "synset": "artificial_horizon.n.01", "name": "artificial_horizon"}, + {"id": 5456, "synset": "artificial_joint.n.01", "name": "artificial_joint"}, + {"id": 5457, "synset": "artificial_kidney.n.01", "name": "artificial_kidney"}, + {"id": 5458, "synset": "artificial_skin.n.01", "name": "artificial_skin"}, + {"id": 5459, "synset": "artillery.n.01", "name": "artillery"}, + {"id": 5460, "synset": "artillery_shell.n.01", "name": "artillery_shell"}, + {"id": 5461, "synset": "artist's_loft.n.01", "name": "artist's_loft"}, + {"id": 5462, "synset": "art_school.n.01", "name": "art_school"}, + {"id": 5463, "synset": "ascot.n.01", "name": "ascot"}, + {"id": 5464, "synset": "ash-pan.n.01", "name": "ash-pan"}, + {"id": 5465, "synset": "aspergill.n.01", "name": "aspergill"}, + {"id": 5466, "synset": "aspersorium.n.01", "name": "aspersorium"}, + {"id": 5467, "synset": "aspirator.n.01", "name": "aspirator"}, + {"id": 5468, "synset": "aspirin_powder.n.01", "name": "aspirin_powder"}, + {"id": 5469, "synset": "assault_gun.n.02", "name": "assault_gun"}, + {"id": 5470, "synset": "assault_rifle.n.01", "name": "assault_rifle"}, + {"id": 5471, "synset": "assegai.n.01", "name": "assegai"}, + {"id": 5472, "synset": "assembly.n.01", "name": "assembly"}, + {"id": 5473, "synset": "assembly.n.05", "name": "assembly"}, + {"id": 5474, "synset": "assembly_hall.n.01", "name": "assembly_hall"}, + {"id": 5475, "synset": "assembly_plant.n.01", "name": "assembly_plant"}, + {"id": 5476, "synset": "astatic_coils.n.01", "name": "astatic_coils"}, + {"id": 5477, "synset": "astatic_galvanometer.n.01", "name": "astatic_galvanometer"}, + {"id": 5478, "synset": "astrodome.n.01", "name": "astrodome"}, + {"id": 5479, "synset": "astrolabe.n.01", "name": "astrolabe"}, + {"id": 5480, "synset": "astronomical_telescope.n.01", "name": "astronomical_telescope"}, + {"id": 5481, "synset": "astronomy_satellite.n.01", "name": "astronomy_satellite"}, + {"id": 5482, "synset": "athenaeum.n.02", "name": "athenaeum"}, + {"id": 5483, "synset": "athletic_sock.n.01", "name": "athletic_sock"}, + {"id": 5484, "synset": "athletic_supporter.n.01", "name": "athletic_supporter"}, + {"id": 5485, "synset": "atlas.n.04", "name": "atlas"}, + {"id": 5486, "synset": "atmometer.n.01", "name": "atmometer"}, + {"id": 5487, "synset": "atom_bomb.n.01", "name": "atom_bomb"}, + {"id": 5488, "synset": "atomic_clock.n.01", "name": "atomic_clock"}, + {"id": 5489, "synset": "atomic_pile.n.01", "name": "atomic_pile"}, + {"id": 5490, "synset": "atrium.n.02", "name": "atrium"}, + {"id": 5491, "synset": "attache_case.n.01", "name": "attache_case"}, + {"id": 5492, "synset": "attachment.n.04", "name": "attachment"}, + {"id": 5493, "synset": "attack_submarine.n.01", "name": "attack_submarine"}, + {"id": 5494, "synset": "attenuator.n.01", "name": "attenuator"}, + {"id": 5495, "synset": "attic.n.04", "name": "attic"}, + {"id": 5496, "synset": "attic_fan.n.01", "name": "attic_fan"}, + {"id": 5497, "synset": "attire.n.01", "name": "attire"}, + {"id": 5498, "synset": "audio_amplifier.n.01", "name": "audio_amplifier"}, + {"id": 5499, "synset": "audiocassette.n.01", "name": "audiocassette"}, + {"id": 5500, "synset": "audio_cd.n.01", "name": "audio_CD"}, + {"id": 5501, "synset": "audiometer.n.01", "name": "audiometer"}, + {"id": 5502, "synset": "audio_system.n.01", "name": "audio_system"}, + {"id": 5503, "synset": "audiotape.n.02", "name": "audiotape"}, + {"id": 5504, "synset": "audiotape.n.01", "name": "audiotape"}, + {"id": 5505, "synset": "audiovisual.n.01", "name": "audiovisual"}, + {"id": 5506, "synset": "auditorium.n.01", "name": "auditorium"}, + {"id": 5507, "synset": "auger.n.02", "name": "auger"}, + {"id": 5508, "synset": "autobahn.n.01", "name": "autobahn"}, + {"id": 5509, "synset": "autoclave.n.01", "name": "autoclave"}, + {"id": 5510, "synset": "autofocus.n.01", "name": "autofocus"}, + {"id": 5511, "synset": "autogiro.n.01", "name": "autogiro"}, + {"id": 5512, "synset": "autoinjector.n.01", "name": "autoinjector"}, + {"id": 5513, "synset": "autoloader.n.01", "name": "autoloader"}, + {"id": 5514, "synset": "automat.n.02", "name": "automat"}, + {"id": 5515, "synset": "automat.n.01", "name": "automat"}, + {"id": 5516, "synset": "automatic_choke.n.01", "name": "automatic_choke"}, + {"id": 5517, "synset": "automatic_firearm.n.01", "name": "automatic_firearm"}, + {"id": 5518, "synset": "automatic_pistol.n.01", "name": "automatic_pistol"}, + {"id": 5519, "synset": "automatic_rifle.n.01", "name": "automatic_rifle"}, + {"id": 5520, "synset": "automatic_transmission.n.01", "name": "automatic_transmission"}, + {"id": 5521, "synset": "automation.n.03", "name": "automation"}, + {"id": 5522, "synset": "automaton.n.02", "name": "automaton"}, + {"id": 5523, "synset": "automobile_engine.n.01", "name": "automobile_engine"}, + {"id": 5524, "synset": "automobile_factory.n.01", "name": "automobile_factory"}, + {"id": 5525, "synset": "automobile_horn.n.01", "name": "automobile_horn"}, + {"id": 5526, "synset": "autopilot.n.02", "name": "autopilot"}, + {"id": 5527, "synset": "autoradiograph.n.01", "name": "autoradiograph"}, + {"id": 5528, "synset": "autostrada.n.01", "name": "autostrada"}, + {"id": 5529, "synset": "auxiliary_boiler.n.01", "name": "auxiliary_boiler"}, + {"id": 5530, "synset": "auxiliary_engine.n.01", "name": "auxiliary_engine"}, + {"id": 5531, "synset": "auxiliary_pump.n.01", "name": "auxiliary_pump"}, + { + "id": 5532, + "synset": "auxiliary_research_submarine.n.01", + "name": "auxiliary_research_submarine", + }, + {"id": 5533, "synset": "auxiliary_storage.n.01", "name": "auxiliary_storage"}, + {"id": 5534, "synset": "aviary.n.01", "name": "aviary"}, + {"id": 5535, "synset": "awl.n.01", "name": "awl"}, + {"id": 5536, "synset": "ax_handle.n.01", "name": "ax_handle"}, + {"id": 5537, "synset": "ax_head.n.01", "name": "ax_head"}, + {"id": 5538, "synset": "axis.n.06", "name": "axis"}, + {"id": 5539, "synset": "axle.n.01", "name": "axle"}, + {"id": 5540, "synset": "axle_bar.n.01", "name": "axle_bar"}, + {"id": 5541, "synset": "axletree.n.01", "name": "axletree"}, + {"id": 5542, "synset": "babushka.n.01", "name": "babushka"}, + {"id": 5543, "synset": "baby_bed.n.01", "name": "baby_bed"}, + {"id": 5544, "synset": "baby_grand.n.01", "name": "baby_grand"}, + {"id": 5545, "synset": "baby_powder.n.01", "name": "baby_powder"}, + {"id": 5546, "synset": "baby_shoe.n.01", "name": "baby_shoe"}, + {"id": 5547, "synset": "back.n.08", "name": "back"}, + {"id": 5548, "synset": "back.n.07", "name": "back"}, + {"id": 5549, "synset": "backbench.n.01", "name": "backbench"}, + {"id": 5550, "synset": "backboard.n.02", "name": "backboard"}, + {"id": 5551, "synset": "backbone.n.05", "name": "backbone"}, + {"id": 5552, "synset": "back_brace.n.01", "name": "back_brace"}, + {"id": 5553, "synset": "backgammon_board.n.01", "name": "backgammon_board"}, + {"id": 5554, "synset": "background.n.07", "name": "background"}, + {"id": 5555, "synset": "backhoe.n.01", "name": "backhoe"}, + {"id": 5556, "synset": "backlighting.n.01", "name": "backlighting"}, + {"id": 5557, "synset": "backpacking_tent.n.01", "name": "backpacking_tent"}, + {"id": 5558, "synset": "backplate.n.01", "name": "backplate"}, + {"id": 5559, "synset": "back_porch.n.01", "name": "back_porch"}, + {"id": 5560, "synset": "backsaw.n.01", "name": "backsaw"}, + {"id": 5561, "synset": "backscratcher.n.02", "name": "backscratcher"}, + {"id": 5562, "synset": "backseat.n.02", "name": "backseat"}, + {"id": 5563, "synset": "backspace_key.n.01", "name": "backspace_key"}, + {"id": 5564, "synset": "backstairs.n.01", "name": "backstairs"}, + {"id": 5565, "synset": "backstay.n.01", "name": "backstay"}, + {"id": 5566, "synset": "backstop.n.02", "name": "backstop"}, + {"id": 5567, "synset": "backsword.n.02", "name": "backsword"}, + {"id": 5568, "synset": "backup_system.n.01", "name": "backup_system"}, + {"id": 5569, "synset": "badminton_court.n.01", "name": "badminton_court"}, + {"id": 5570, "synset": "badminton_equipment.n.01", "name": "badminton_equipment"}, + {"id": 5571, "synset": "badminton_racket.n.01", "name": "badminton_racket"}, + {"id": 5572, "synset": "bag.n.01", "name": "bag"}, + {"id": 5573, "synset": "baggage.n.01", "name": "baggage"}, + {"id": 5574, "synset": "baggage.n.03", "name": "baggage"}, + {"id": 5575, "synset": "baggage_car.n.01", "name": "baggage_car"}, + {"id": 5576, "synset": "baggage_claim.n.01", "name": "baggage_claim"}, + {"id": 5577, "synset": "bailey.n.04", "name": "bailey"}, + {"id": 5578, "synset": "bailey.n.03", "name": "bailey"}, + {"id": 5579, "synset": "bailey_bridge.n.01", "name": "Bailey_bridge"}, + {"id": 5580, "synset": "bain-marie.n.01", "name": "bain-marie"}, + {"id": 5581, "synset": "baize.n.01", "name": "baize"}, + {"id": 5582, "synset": "bakery.n.01", "name": "bakery"}, + {"id": 5583, "synset": "balaclava.n.01", "name": "balaclava"}, + {"id": 5584, "synset": "balalaika.n.01", "name": "balalaika"}, + {"id": 5585, "synset": "balance.n.12", "name": "balance"}, + {"id": 5586, "synset": "balance_beam.n.01", "name": "balance_beam"}, + {"id": 5587, "synset": "balance_wheel.n.01", "name": "balance_wheel"}, + {"id": 5588, "synset": "balbriggan.n.01", "name": "balbriggan"}, + {"id": 5589, "synset": "balcony.n.02", "name": "balcony"}, + {"id": 5590, "synset": "balcony.n.01", "name": "balcony"}, + {"id": 5591, "synset": "baldachin.n.01", "name": "baldachin"}, + {"id": 5592, "synset": "baldric.n.01", "name": "baldric"}, + {"id": 5593, "synset": "bale.n.01", "name": "bale"}, + {"id": 5594, "synset": "baling_wire.n.01", "name": "baling_wire"}, + {"id": 5595, "synset": "ball.n.01", "name": "ball"}, + {"id": 5596, "synset": "ball_and_chain.n.01", "name": "ball_and_chain"}, + {"id": 5597, "synset": "ball-and-socket_joint.n.02", "name": "ball-and-socket_joint"}, + {"id": 5598, "synset": "ballast.n.05", "name": "ballast"}, + {"id": 5599, "synset": "ball_bearing.n.01", "name": "ball_bearing"}, + {"id": 5600, "synset": "ball_cartridge.n.01", "name": "ball_cartridge"}, + {"id": 5601, "synset": "ballcock.n.01", "name": "ballcock"}, + {"id": 5602, "synset": "balldress.n.01", "name": "balldress"}, + {"id": 5603, "synset": "ball_gown.n.01", "name": "ball_gown"}, + {"id": 5604, "synset": "ballistic_galvanometer.n.01", "name": "ballistic_galvanometer"}, + {"id": 5605, "synset": "ballistic_missile.n.01", "name": "ballistic_missile"}, + {"id": 5606, "synset": "ballistic_pendulum.n.01", "name": "ballistic_pendulum"}, + {"id": 5607, "synset": "ballistocardiograph.n.01", "name": "ballistocardiograph"}, + {"id": 5608, "synset": "balloon_bomb.n.01", "name": "balloon_bomb"}, + {"id": 5609, "synset": "balloon_sail.n.01", "name": "balloon_sail"}, + {"id": 5610, "synset": "ballot_box.n.01", "name": "ballot_box"}, + {"id": 5611, "synset": "ballpark.n.01", "name": "ballpark"}, + {"id": 5612, "synset": "ball-peen_hammer.n.01", "name": "ball-peen_hammer"}, + {"id": 5613, "synset": "ballpoint.n.01", "name": "ballpoint"}, + {"id": 5614, "synset": "ballroom.n.01", "name": "ballroom"}, + {"id": 5615, "synset": "ball_valve.n.01", "name": "ball_valve"}, + {"id": 5616, "synset": "balsa_raft.n.01", "name": "balsa_raft"}, + {"id": 5617, "synset": "baluster.n.01", "name": "baluster"}, + {"id": 5618, "synset": "banana_boat.n.01", "name": "banana_boat"}, + {"id": 5619, "synset": "band.n.13", "name": "band"}, + {"id": 5620, "synset": "bandbox.n.01", "name": "bandbox"}, + {"id": 5621, "synset": "banderilla.n.01", "name": "banderilla"}, + {"id": 5622, "synset": "bandoleer.n.01", "name": "bandoleer"}, + {"id": 5623, "synset": "bandoneon.n.01", "name": "bandoneon"}, + {"id": 5624, "synset": "bandsaw.n.01", "name": "bandsaw"}, + {"id": 5625, "synset": "bandwagon.n.02", "name": "bandwagon"}, + {"id": 5626, "synset": "bangalore_torpedo.n.01", "name": "bangalore_torpedo"}, + {"id": 5627, "synset": "bangle.n.02", "name": "bangle"}, + {"id": 5628, "synset": "bannister.n.02", "name": "bannister"}, + {"id": 5629, "synset": "banquette.n.01", "name": "banquette"}, + {"id": 5630, "synset": "banyan.n.02", "name": "banyan"}, + {"id": 5631, "synset": "baptismal_font.n.01", "name": "baptismal_font"}, + {"id": 5632, "synset": "bar.n.03", "name": "bar"}, + {"id": 5633, "synset": "bar.n.02", "name": "bar"}, + {"id": 5634, "synset": "barbecue.n.03", "name": "barbecue"}, + {"id": 5635, "synset": "barbed_wire.n.01", "name": "barbed_wire"}, + {"id": 5636, "synset": "barber_chair.n.01", "name": "barber_chair"}, + {"id": 5637, "synset": "barbershop.n.01", "name": "barbershop"}, + {"id": 5638, "synset": "barbette_carriage.n.01", "name": "barbette_carriage"}, + {"id": 5639, "synset": "barbican.n.01", "name": "barbican"}, + {"id": 5640, "synset": "bar_bit.n.01", "name": "bar_bit"}, + {"id": 5641, "synset": "bareboat.n.01", "name": "bareboat"}, + {"id": 5642, "synset": "barge_pole.n.01", "name": "barge_pole"}, + {"id": 5643, "synset": "baritone.n.03", "name": "baritone"}, + {"id": 5644, "synset": "bark.n.03", "name": "bark"}, + {"id": 5645, "synset": "bar_magnet.n.01", "name": "bar_magnet"}, + {"id": 5646, "synset": "bar_mask.n.01", "name": "bar_mask"}, + {"id": 5647, "synset": "barn.n.01", "name": "barn"}, + {"id": 5648, "synset": "barndoor.n.01", "name": "barndoor"}, + {"id": 5649, "synset": "barn_door.n.01", "name": "barn_door"}, + {"id": 5650, "synset": "barnyard.n.01", "name": "barnyard"}, + {"id": 5651, "synset": "barograph.n.01", "name": "barograph"}, + {"id": 5652, "synset": "barometer.n.01", "name": "barometer"}, + {"id": 5653, "synset": "barong.n.01", "name": "barong"}, + {"id": 5654, "synset": "barouche.n.01", "name": "barouche"}, + {"id": 5655, "synset": "bar_printer.n.01", "name": "bar_printer"}, + {"id": 5656, "synset": "barrack.n.01", "name": "barrack"}, + {"id": 5657, "synset": "barrage_balloon.n.01", "name": "barrage_balloon"}, + {"id": 5658, "synset": "barrel.n.01", "name": "barrel"}, + {"id": 5659, "synset": "barrelhouse.n.01", "name": "barrelhouse"}, + {"id": 5660, "synset": "barrel_knot.n.01", "name": "barrel_knot"}, + {"id": 5661, "synset": "barrel_organ.n.01", "name": "barrel_organ"}, + {"id": 5662, "synset": "barrel_vault.n.01", "name": "barrel_vault"}, + {"id": 5663, "synset": "barricade.n.02", "name": "barricade"}, + {"id": 5664, "synset": "barrier.n.01", "name": "barrier"}, + {"id": 5665, "synset": "barroom.n.01", "name": "barroom"}, + {"id": 5666, "synset": "bascule.n.01", "name": "bascule"}, + {"id": 5667, "synset": "base.n.08", "name": "base"}, + {"id": 5668, "synset": "baseball_equipment.n.01", "name": "baseball_equipment"}, + {"id": 5669, "synset": "basement.n.01", "name": "basement"}, + {"id": 5670, "synset": "basement.n.02", "name": "basement"}, + { + "id": 5671, + "synset": "basic_point_defense_missile_system.n.01", + "name": "basic_point_defense_missile_system", + }, + {"id": 5672, "synset": "basilica.n.02", "name": "basilica"}, + {"id": 5673, "synset": "basilica.n.01", "name": "basilica"}, + {"id": 5674, "synset": "basilisk.n.02", "name": "basilisk"}, + {"id": 5675, "synset": "basin.n.01", "name": "basin"}, + {"id": 5676, "synset": "basinet.n.01", "name": "basinet"}, + {"id": 5677, "synset": "basket.n.03", "name": "basket"}, + {"id": 5678, "synset": "basketball_court.n.01", "name": "basketball_court"}, + {"id": 5679, "synset": "basketball_equipment.n.01", "name": "basketball_equipment"}, + {"id": 5680, "synset": "basket_weave.n.01", "name": "basket_weave"}, + {"id": 5681, "synset": "bass.n.07", "name": "bass"}, + {"id": 5682, "synset": "bass_clarinet.n.01", "name": "bass_clarinet"}, + {"id": 5683, "synset": "bass_drum.n.01", "name": "bass_drum"}, + {"id": 5684, "synset": "basset_horn.n.01", "name": "basset_horn"}, + {"id": 5685, "synset": "bass_fiddle.n.01", "name": "bass_fiddle"}, + {"id": 5686, "synset": "bass_guitar.n.01", "name": "bass_guitar"}, + {"id": 5687, "synset": "bassinet.n.01", "name": "bassinet"}, + {"id": 5688, "synset": "bassinet.n.02", "name": "bassinet"}, + {"id": 5689, "synset": "bassoon.n.01", "name": "bassoon"}, + {"id": 5690, "synset": "baster.n.03", "name": "baster"}, + {"id": 5691, "synset": "bastinado.n.01", "name": "bastinado"}, + {"id": 5692, "synset": "bastion.n.03", "name": "bastion"}, + {"id": 5693, "synset": "bastion.n.02", "name": "bastion"}, + {"id": 5694, "synset": "bat.n.05", "name": "bat"}, + {"id": 5695, "synset": "bath.n.01", "name": "bath"}, + {"id": 5696, "synset": "bath_chair.n.01", "name": "bath_chair"}, + {"id": 5697, "synset": "bathhouse.n.02", "name": "bathhouse"}, + {"id": 5698, "synset": "bathhouse.n.01", "name": "bathhouse"}, + {"id": 5699, "synset": "bathing_cap.n.01", "name": "bathing_cap"}, + {"id": 5700, "synset": "bath_oil.n.01", "name": "bath_oil"}, + {"id": 5701, "synset": "bathroom.n.01", "name": "bathroom"}, + {"id": 5702, "synset": "bath_salts.n.01", "name": "bath_salts"}, + {"id": 5703, "synset": "bathyscaphe.n.01", "name": "bathyscaphe"}, + {"id": 5704, "synset": "bathysphere.n.01", "name": "bathysphere"}, + {"id": 5705, "synset": "batik.n.01", "name": "batik"}, + {"id": 5706, "synset": "batiste.n.01", "name": "batiste"}, + {"id": 5707, "synset": "baton.n.01", "name": "baton"}, + {"id": 5708, "synset": "baton.n.05", "name": "baton"}, + {"id": 5709, "synset": "baton.n.04", "name": "baton"}, + {"id": 5710, "synset": "baton.n.03", "name": "baton"}, + {"id": 5711, "synset": "battering_ram.n.01", "name": "battering_ram"}, + {"id": 5712, "synset": "batter's_box.n.01", "name": "batter's_box"}, + {"id": 5713, "synset": "battery.n.05", "name": "battery"}, + {"id": 5714, "synset": "batting_cage.n.01", "name": "batting_cage"}, + {"id": 5715, "synset": "batting_glove.n.01", "name": "batting_glove"}, + {"id": 5716, "synset": "batting_helmet.n.01", "name": "batting_helmet"}, + {"id": 5717, "synset": "battle-ax.n.01", "name": "battle-ax"}, + {"id": 5718, "synset": "battle_cruiser.n.01", "name": "battle_cruiser"}, + {"id": 5719, "synset": "battle_dress.n.01", "name": "battle_dress"}, + {"id": 5720, "synset": "battlement.n.01", "name": "battlement"}, + {"id": 5721, "synset": "battleship.n.01", "name": "battleship"}, + {"id": 5722, "synset": "battle_sight.n.01", "name": "battle_sight"}, + {"id": 5723, "synset": "bay.n.05", "name": "bay"}, + {"id": 5724, "synset": "bay.n.04", "name": "bay"}, + {"id": 5725, "synset": "bayonet.n.01", "name": "bayonet"}, + {"id": 5726, "synset": "bay_rum.n.01", "name": "bay_rum"}, + {"id": 5727, "synset": "bay_window.n.02", "name": "bay_window"}, + {"id": 5728, "synset": "bazaar.n.01", "name": "bazaar"}, + {"id": 5729, "synset": "bazaar.n.02", "name": "bazaar"}, + {"id": 5730, "synset": "bazooka.n.01", "name": "bazooka"}, + {"id": 5731, "synset": "b_battery.n.01", "name": "B_battery"}, + {"id": 5732, "synset": "bb_gun.n.01", "name": "BB_gun"}, + {"id": 5733, "synset": "beach_house.n.01", "name": "beach_house"}, + {"id": 5734, "synset": "beach_towel.n.01", "name": "beach_towel"}, + {"id": 5735, "synset": "beach_wagon.n.01", "name": "beach_wagon"}, + {"id": 5736, "synset": "beachwear.n.01", "name": "beachwear"}, + {"id": 5737, "synset": "beacon.n.03", "name": "beacon"}, + {"id": 5738, "synset": "beading_plane.n.01", "name": "beading_plane"}, + {"id": 5739, "synset": "beaker.n.02", "name": "beaker"}, + {"id": 5740, "synset": "beaker.n.01", "name": "beaker"}, + {"id": 5741, "synset": "beam.n.02", "name": "beam"}, + {"id": 5742, "synset": "beam_balance.n.01", "name": "beam_balance"}, + {"id": 5743, "synset": "bearing.n.06", "name": "bearing"}, + {"id": 5744, "synset": "bearing_rein.n.01", "name": "bearing_rein"}, + {"id": 5745, "synset": "bearing_wall.n.01", "name": "bearing_wall"}, + {"id": 5746, "synset": "bearskin.n.02", "name": "bearskin"}, + {"id": 5747, "synset": "beater.n.02", "name": "beater"}, + {"id": 5748, "synset": "beating-reed_instrument.n.01", "name": "beating-reed_instrument"}, + {"id": 5749, "synset": "beaver.n.06", "name": "beaver"}, + {"id": 5750, "synset": "beaver.n.05", "name": "beaver"}, + {"id": 5751, "synset": "beckman_thermometer.n.01", "name": "Beckman_thermometer"}, + {"id": 5752, "synset": "bed.n.08", "name": "bed"}, + {"id": 5753, "synset": "bed_and_breakfast.n.01", "name": "bed_and_breakfast"}, + {"id": 5754, "synset": "bedclothes.n.01", "name": "bedclothes"}, + {"id": 5755, "synset": "bedford_cord.n.01", "name": "Bedford_cord"}, + {"id": 5756, "synset": "bed_jacket.n.01", "name": "bed_jacket"}, + {"id": 5757, "synset": "bedpost.n.01", "name": "bedpost"}, + {"id": 5758, "synset": "bedroll.n.01", "name": "bedroll"}, + {"id": 5759, "synset": "bedroom.n.01", "name": "bedroom"}, + {"id": 5760, "synset": "bedroom_furniture.n.01", "name": "bedroom_furniture"}, + {"id": 5761, "synset": "bedsitting_room.n.01", "name": "bedsitting_room"}, + {"id": 5762, "synset": "bedspring.n.01", "name": "bedspring"}, + {"id": 5763, "synset": "bedstead.n.01", "name": "bedstead"}, + {"id": 5764, "synset": "beefcake.n.01", "name": "beefcake"}, + {"id": 5765, "synset": "beehive.n.04", "name": "beehive"}, + {"id": 5766, "synset": "beer_barrel.n.01", "name": "beer_barrel"}, + {"id": 5767, "synset": "beer_garden.n.01", "name": "beer_garden"}, + {"id": 5768, "synset": "beer_glass.n.01", "name": "beer_glass"}, + {"id": 5769, "synset": "beer_hall.n.01", "name": "beer_hall"}, + {"id": 5770, "synset": "beer_mat.n.01", "name": "beer_mat"}, + {"id": 5771, "synset": "beer_mug.n.01", "name": "beer_mug"}, + {"id": 5772, "synset": "belaying_pin.n.01", "name": "belaying_pin"}, + {"id": 5773, "synset": "belfry.n.02", "name": "belfry"}, + {"id": 5774, "synset": "bell_arch.n.01", "name": "bell_arch"}, + {"id": 5775, "synset": "bellarmine.n.02", "name": "bellarmine"}, + {"id": 5776, "synset": "bellbottom_trousers.n.01", "name": "bellbottom_trousers"}, + {"id": 5777, "synset": "bell_cote.n.01", "name": "bell_cote"}, + {"id": 5778, "synset": "bell_foundry.n.01", "name": "bell_foundry"}, + {"id": 5779, "synset": "bell_gable.n.01", "name": "bell_gable"}, + {"id": 5780, "synset": "bell_jar.n.01", "name": "bell_jar"}, + {"id": 5781, "synset": "bellows.n.01", "name": "bellows"}, + {"id": 5782, "synset": "bellpull.n.01", "name": "bellpull"}, + {"id": 5783, "synset": "bell_push.n.01", "name": "bell_push"}, + {"id": 5784, "synset": "bell_seat.n.01", "name": "bell_seat"}, + {"id": 5785, "synset": "bell_tent.n.01", "name": "bell_tent"}, + {"id": 5786, "synset": "bell_tower.n.01", "name": "bell_tower"}, + {"id": 5787, "synset": "bellyband.n.01", "name": "bellyband"}, + {"id": 5788, "synset": "belt.n.06", "name": "belt"}, + {"id": 5789, "synset": "belting.n.01", "name": "belting"}, + {"id": 5790, "synset": "bench_clamp.n.01", "name": "bench_clamp"}, + {"id": 5791, "synset": "bench_hook.n.01", "name": "bench_hook"}, + {"id": 5792, "synset": "bench_lathe.n.01", "name": "bench_lathe"}, + {"id": 5793, "synset": "bench_press.n.02", "name": "bench_press"}, + {"id": 5794, "synset": "bender.n.01", "name": "bender"}, + {"id": 5795, "synset": "berlin.n.03", "name": "berlin"}, + {"id": 5796, "synset": "bermuda_shorts.n.01", "name": "Bermuda_shorts"}, + {"id": 5797, "synset": "berth.n.03", "name": "berth"}, + {"id": 5798, "synset": "besom.n.01", "name": "besom"}, + {"id": 5799, "synset": "bessemer_converter.n.01", "name": "Bessemer_converter"}, + {"id": 5800, "synset": "bethel.n.01", "name": "bethel"}, + {"id": 5801, "synset": "betting_shop.n.01", "name": "betting_shop"}, + {"id": 5802, "synset": "bevatron.n.01", "name": "bevatron"}, + {"id": 5803, "synset": "bevel.n.02", "name": "bevel"}, + {"id": 5804, "synset": "bevel_gear.n.01", "name": "bevel_gear"}, + {"id": 5805, "synset": "b-flat_clarinet.n.01", "name": "B-flat_clarinet"}, + {"id": 5806, "synset": "bib.n.01", "name": "bib"}, + {"id": 5807, "synset": "bib-and-tucker.n.01", "name": "bib-and-tucker"}, + {"id": 5808, "synset": "bicorn.n.01", "name": "bicorn"}, + {"id": 5809, "synset": "bicycle-built-for-two.n.01", "name": "bicycle-built-for-two"}, + {"id": 5810, "synset": "bicycle_chain.n.01", "name": "bicycle_chain"}, + {"id": 5811, "synset": "bicycle_clip.n.01", "name": "bicycle_clip"}, + {"id": 5812, "synset": "bicycle_pump.n.01", "name": "bicycle_pump"}, + {"id": 5813, "synset": "bicycle_rack.n.01", "name": "bicycle_rack"}, + {"id": 5814, "synset": "bicycle_seat.n.01", "name": "bicycle_seat"}, + {"id": 5815, "synset": "bicycle_wheel.n.01", "name": "bicycle_wheel"}, + {"id": 5816, "synset": "bidet.n.01", "name": "bidet"}, + {"id": 5817, "synset": "bier.n.02", "name": "bier"}, + {"id": 5818, "synset": "bier.n.01", "name": "bier"}, + {"id": 5819, "synset": "bi-fold_door.n.01", "name": "bi-fold_door"}, + {"id": 5820, "synset": "bifocals.n.01", "name": "bifocals"}, + {"id": 5821, "synset": "big_blue.n.01", "name": "Big_Blue"}, + {"id": 5822, "synset": "big_board.n.02", "name": "big_board"}, + {"id": 5823, "synset": "bight.n.04", "name": "bight"}, + {"id": 5824, "synset": "bikini.n.02", "name": "bikini"}, + {"id": 5825, "synset": "bikini_pants.n.01", "name": "bikini_pants"}, + {"id": 5826, "synset": "bilge.n.02", "name": "bilge"}, + {"id": 5827, "synset": "bilge_keel.n.01", "name": "bilge_keel"}, + {"id": 5828, "synset": "bilge_pump.n.01", "name": "bilge_pump"}, + {"id": 5829, "synset": "bilge_well.n.01", "name": "bilge_well"}, + {"id": 5830, "synset": "bill.n.08", "name": "bill"}, + {"id": 5831, "synset": "billiard_ball.n.01", "name": "billiard_ball"}, + {"id": 5832, "synset": "billiard_room.n.01", "name": "billiard_room"}, + {"id": 5833, "synset": "bin.n.01", "name": "bin"}, + {"id": 5834, "synset": "binder.n.04", "name": "binder"}, + {"id": 5835, "synset": "bindery.n.01", "name": "bindery"}, + {"id": 5836, "synset": "binding.n.05", "name": "binding"}, + {"id": 5837, "synset": "bin_liner.n.01", "name": "bin_liner"}, + {"id": 5838, "synset": "binnacle.n.01", "name": "binnacle"}, + {"id": 5839, "synset": "binocular_microscope.n.01", "name": "binocular_microscope"}, + {"id": 5840, "synset": "biochip.n.01", "name": "biochip"}, + {"id": 5841, "synset": "biohazard_suit.n.01", "name": "biohazard_suit"}, + {"id": 5842, "synset": "bioscope.n.02", "name": "bioscope"}, + {"id": 5843, "synset": "biplane.n.01", "name": "biplane"}, + {"id": 5844, "synset": "birch.n.03", "name": "birch"}, + {"id": 5845, "synset": "birchbark_canoe.n.01", "name": "birchbark_canoe"}, + {"id": 5846, "synset": "birdcall.n.02", "name": "birdcall"}, + {"id": 5847, "synset": "bird_shot.n.01", "name": "bird_shot"}, + {"id": 5848, "synset": "biretta.n.01", "name": "biretta"}, + {"id": 5849, "synset": "bishop.n.03", "name": "bishop"}, + {"id": 5850, "synset": "bistro.n.01", "name": "bistro"}, + {"id": 5851, "synset": "bit.n.11", "name": "bit"}, + {"id": 5852, "synset": "bit.n.05", "name": "bit"}, + {"id": 5853, "synset": "bite_plate.n.01", "name": "bite_plate"}, + {"id": 5854, "synset": "bitewing.n.01", "name": "bitewing"}, + {"id": 5855, "synset": "bitumastic.n.01", "name": "bitumastic"}, + {"id": 5856, "synset": "black.n.07", "name": "black"}, + {"id": 5857, "synset": "black.n.06", "name": "black"}, + {"id": 5858, "synset": "blackboard_eraser.n.01", "name": "blackboard_eraser"}, + {"id": 5859, "synset": "black_box.n.01", "name": "black_box"}, + {"id": 5860, "synset": "blackface.n.01", "name": "blackface"}, + {"id": 5861, "synset": "blackjack.n.02", "name": "blackjack"}, + {"id": 5862, "synset": "black_tie.n.02", "name": "black_tie"}, + {"id": 5863, "synset": "blackwash.n.03", "name": "blackwash"}, + {"id": 5864, "synset": "bladder.n.02", "name": "bladder"}, + {"id": 5865, "synset": "blade.n.09", "name": "blade"}, + {"id": 5866, "synset": "blade.n.08", "name": "blade"}, + {"id": 5867, "synset": "blade.n.07", "name": "blade"}, + {"id": 5868, "synset": "blank.n.04", "name": "blank"}, + {"id": 5869, "synset": "blast_furnace.n.01", "name": "blast_furnace"}, + {"id": 5870, "synset": "blasting_cap.n.01", "name": "blasting_cap"}, + {"id": 5871, "synset": "blind.n.03", "name": "blind"}, + {"id": 5872, "synset": "blind_curve.n.01", "name": "blind_curve"}, + {"id": 5873, "synset": "blindfold.n.01", "name": "blindfold"}, + {"id": 5874, "synset": "bling.n.01", "name": "bling"}, + {"id": 5875, "synset": "blister_pack.n.01", "name": "blister_pack"}, + {"id": 5876, "synset": "block.n.05", "name": "block"}, + {"id": 5877, "synset": "blockade.n.02", "name": "blockade"}, + {"id": 5878, "synset": "blockade-runner.n.01", "name": "blockade-runner"}, + {"id": 5879, "synset": "block_and_tackle.n.01", "name": "block_and_tackle"}, + {"id": 5880, "synset": "blockbuster.n.01", "name": "blockbuster"}, + {"id": 5881, "synset": "blockhouse.n.01", "name": "blockhouse"}, + {"id": 5882, "synset": "block_plane.n.01", "name": "block_plane"}, + {"id": 5883, "synset": "bloodmobile.n.01", "name": "bloodmobile"}, + {"id": 5884, "synset": "bloomers.n.01", "name": "bloomers"}, + {"id": 5885, "synset": "blower.n.01", "name": "blower"}, + {"id": 5886, "synset": "blowtorch.n.01", "name": "blowtorch"}, + {"id": 5887, "synset": "blucher.n.02", "name": "blucher"}, + {"id": 5888, "synset": "bludgeon.n.01", "name": "bludgeon"}, + {"id": 5889, "synset": "blue.n.02", "name": "blue"}, + {"id": 5890, "synset": "blue_chip.n.02", "name": "blue_chip"}, + {"id": 5891, "synset": "blunderbuss.n.01", "name": "blunderbuss"}, + {"id": 5892, "synset": "blunt_file.n.01", "name": "blunt_file"}, + {"id": 5893, "synset": "boarding.n.02", "name": "boarding"}, + {"id": 5894, "synset": "boarding_house.n.01", "name": "boarding_house"}, + {"id": 5895, "synset": "boardroom.n.01", "name": "boardroom"}, + {"id": 5896, "synset": "boards.n.02", "name": "boards"}, + {"id": 5897, "synset": "boater.n.01", "name": "boater"}, + {"id": 5898, "synset": "boat_hook.n.01", "name": "boat_hook"}, + {"id": 5899, "synset": "boathouse.n.01", "name": "boathouse"}, + {"id": 5900, "synset": "boatswain's_chair.n.01", "name": "boatswain's_chair"}, + {"id": 5901, "synset": "boat_train.n.01", "name": "boat_train"}, + {"id": 5902, "synset": "boatyard.n.01", "name": "boatyard"}, + {"id": 5903, "synset": "bobsled.n.02", "name": "bobsled"}, + {"id": 5904, "synset": "bobsled.n.01", "name": "bobsled"}, + {"id": 5905, "synset": "bocce_ball.n.01", "name": "bocce_ball"}, + {"id": 5906, "synset": "bodega.n.01", "name": "bodega"}, + {"id": 5907, "synset": "bodice.n.01", "name": "bodice"}, + {"id": 5908, "synset": "bodkin.n.04", "name": "bodkin"}, + {"id": 5909, "synset": "bodkin.n.03", "name": "bodkin"}, + {"id": 5910, "synset": "bodkin.n.02", "name": "bodkin"}, + {"id": 5911, "synset": "body.n.11", "name": "body"}, + {"id": 5912, "synset": "body_armor.n.01", "name": "body_armor"}, + {"id": 5913, "synset": "body_lotion.n.01", "name": "body_lotion"}, + {"id": 5914, "synset": "body_stocking.n.01", "name": "body_stocking"}, + {"id": 5915, "synset": "body_plethysmograph.n.01", "name": "body_plethysmograph"}, + {"id": 5916, "synset": "body_pad.n.01", "name": "body_pad"}, + {"id": 5917, "synset": "bodywork.n.01", "name": "bodywork"}, + {"id": 5918, "synset": "bofors_gun.n.01", "name": "Bofors_gun"}, + {"id": 5919, "synset": "bogy.n.01", "name": "bogy"}, + {"id": 5920, "synset": "boiler.n.01", "name": "boiler"}, + {"id": 5921, "synset": "boiling_water_reactor.n.01", "name": "boiling_water_reactor"}, + {"id": 5922, "synset": "bolero.n.02", "name": "bolero"}, + {"id": 5923, "synset": "bollard.n.01", "name": "bollard"}, + {"id": 5924, "synset": "bolo.n.02", "name": "bolo"}, + {"id": 5925, "synset": "bolt.n.02", "name": "bolt"}, + {"id": 5926, "synset": "bolt_cutter.n.01", "name": "bolt_cutter"}, + {"id": 5927, "synset": "bomb.n.01", "name": "bomb"}, + {"id": 5928, "synset": "bombazine.n.01", "name": "bombazine"}, + {"id": 5929, "synset": "bomb_calorimeter.n.01", "name": "bomb_calorimeter"}, + {"id": 5930, "synset": "bomber.n.01", "name": "bomber"}, + {"id": 5931, "synset": "bomber_jacket.n.01", "name": "bomber_jacket"}, + {"id": 5932, "synset": "bomblet.n.01", "name": "bomblet"}, + {"id": 5933, "synset": "bomb_rack.n.01", "name": "bomb_rack"}, + {"id": 5934, "synset": "bombshell.n.03", "name": "bombshell"}, + {"id": 5935, "synset": "bomb_shelter.n.01", "name": "bomb_shelter"}, + {"id": 5936, "synset": "bone-ash_cup.n.01", "name": "bone-ash_cup"}, + {"id": 5937, "synset": "bone_china.n.01", "name": "bone_china"}, + {"id": 5938, "synset": "bones.n.01", "name": "bones"}, + {"id": 5939, "synset": "boneshaker.n.01", "name": "boneshaker"}, + {"id": 5940, "synset": "bongo.n.01", "name": "bongo"}, + {"id": 5941, "synset": "book.n.11", "name": "book"}, + {"id": 5942, "synset": "book_bag.n.01", "name": "book_bag"}, + {"id": 5943, "synset": "bookbindery.n.01", "name": "bookbindery"}, + {"id": 5944, "synset": "bookend.n.01", "name": "bookend"}, + {"id": 5945, "synset": "bookmobile.n.01", "name": "bookmobile"}, + {"id": 5946, "synset": "bookshelf.n.01", "name": "bookshelf"}, + {"id": 5947, "synset": "bookshop.n.01", "name": "bookshop"}, + {"id": 5948, "synset": "boom.n.05", "name": "boom"}, + {"id": 5949, "synset": "boomerang.n.01", "name": "boomerang"}, + {"id": 5950, "synset": "booster.n.05", "name": "booster"}, + {"id": 5951, "synset": "booster.n.04", "name": "booster"}, + {"id": 5952, "synset": "boot.n.04", "name": "boot"}, + {"id": 5953, "synset": "boot_camp.n.01", "name": "boot_camp"}, + {"id": 5954, "synset": "bootee.n.01", "name": "bootee"}, + {"id": 5955, "synset": "booth.n.02", "name": "booth"}, + {"id": 5956, "synset": "booth.n.04", "name": "booth"}, + {"id": 5957, "synset": "booth.n.01", "name": "booth"}, + {"id": 5958, "synset": "boothose.n.01", "name": "boothose"}, + {"id": 5959, "synset": "bootjack.n.01", "name": "bootjack"}, + {"id": 5960, "synset": "bootlace.n.01", "name": "bootlace"}, + {"id": 5961, "synset": "bootleg.n.02", "name": "bootleg"}, + {"id": 5962, "synset": "bootstrap.n.01", "name": "bootstrap"}, + {"id": 5963, "synset": "bore_bit.n.01", "name": "bore_bit"}, + {"id": 5964, "synset": "boron_chamber.n.01", "name": "boron_chamber"}, + {"id": 5965, "synset": "borstal.n.01", "name": "borstal"}, + {"id": 5966, "synset": "bosom.n.03", "name": "bosom"}, + {"id": 5967, "synset": "boston_rocker.n.01", "name": "Boston_rocker"}, + {"id": 5968, "synset": "bota.n.01", "name": "bota"}, + {"id": 5969, "synset": "bottle.n.03", "name": "bottle"}, + {"id": 5970, "synset": "bottle_bank.n.01", "name": "bottle_bank"}, + {"id": 5971, "synset": "bottlebrush.n.01", "name": "bottlebrush"}, + {"id": 5972, "synset": "bottlecap.n.01", "name": "bottlecap"}, + {"id": 5973, "synset": "bottling_plant.n.01", "name": "bottling_plant"}, + {"id": 5974, "synset": "bottom.n.07", "name": "bottom"}, + {"id": 5975, "synset": "boucle.n.01", "name": "boucle"}, + {"id": 5976, "synset": "boudoir.n.01", "name": "boudoir"}, + {"id": 5977, "synset": "boulle.n.01", "name": "boulle"}, + {"id": 5978, "synset": "bouncing_betty.n.01", "name": "bouncing_betty"}, + {"id": 5979, "synset": "boutique.n.01", "name": "boutique"}, + {"id": 5980, "synset": "boutonniere.n.01", "name": "boutonniere"}, + {"id": 5981, "synset": "bow.n.02", "name": "bow"}, + {"id": 5982, "synset": "bow.n.01", "name": "bow"}, + {"id": 5983, "synset": "bow_and_arrow.n.01", "name": "bow_and_arrow"}, + {"id": 5984, "synset": "bowed_stringed_instrument.n.01", "name": "bowed_stringed_instrument"}, + {"id": 5985, "synset": "bowie_knife.n.01", "name": "Bowie_knife"}, + {"id": 5986, "synset": "bowl.n.01", "name": "bowl"}, + {"id": 5987, "synset": "bowl.n.07", "name": "bowl"}, + {"id": 5988, "synset": "bowline.n.01", "name": "bowline"}, + {"id": 5989, "synset": "bowling_alley.n.01", "name": "bowling_alley"}, + {"id": 5990, "synset": "bowling_equipment.n.01", "name": "bowling_equipment"}, + {"id": 5991, "synset": "bowling_pin.n.01", "name": "bowling_pin"}, + {"id": 5992, "synset": "bowling_shoe.n.01", "name": "bowling_shoe"}, + {"id": 5993, "synset": "bowsprit.n.01", "name": "bowsprit"}, + {"id": 5994, "synset": "bowstring.n.01", "name": "bowstring"}, + {"id": 5995, "synset": "box.n.02", "name": "box"}, + {"id": 5996, "synset": "box.n.08", "name": "box"}, + {"id": 5997, "synset": "box_beam.n.01", "name": "box_beam"}, + {"id": 5998, "synset": "box_camera.n.01", "name": "box_camera"}, + {"id": 5999, "synset": "boxcar.n.01", "name": "boxcar"}, + {"id": 6000, "synset": "box_coat.n.01", "name": "box_coat"}, + {"id": 6001, "synset": "boxing_equipment.n.01", "name": "boxing_equipment"}, + {"id": 6002, "synset": "box_office.n.02", "name": "box_office"}, + {"id": 6003, "synset": "box_spring.n.01", "name": "box_spring"}, + {"id": 6004, "synset": "box_wrench.n.01", "name": "box_wrench"}, + {"id": 6005, "synset": "brace.n.09", "name": "brace"}, + {"id": 6006, "synset": "brace.n.07", "name": "brace"}, + {"id": 6007, "synset": "brace.n.01", "name": "brace"}, + {"id": 6008, "synset": "brace_and_bit.n.01", "name": "brace_and_bit"}, + {"id": 6009, "synset": "bracer.n.01", "name": "bracer"}, + {"id": 6010, "synset": "brace_wrench.n.01", "name": "brace_wrench"}, + {"id": 6011, "synset": "bracket.n.04", "name": "bracket"}, + {"id": 6012, "synset": "bradawl.n.01", "name": "bradawl"}, + {"id": 6013, "synset": "brake.n.01", "name": "brake"}, + {"id": 6014, "synset": "brake.n.05", "name": "brake"}, + {"id": 6015, "synset": "brake_band.n.01", "name": "brake_band"}, + {"id": 6016, "synset": "brake_cylinder.n.01", "name": "brake_cylinder"}, + {"id": 6017, "synset": "brake_disk.n.01", "name": "brake_disk"}, + {"id": 6018, "synset": "brake_drum.n.01", "name": "brake_drum"}, + {"id": 6019, "synset": "brake_lining.n.01", "name": "brake_lining"}, + {"id": 6020, "synset": "brake_pad.n.01", "name": "brake_pad"}, + {"id": 6021, "synset": "brake_pedal.n.01", "name": "brake_pedal"}, + {"id": 6022, "synset": "brake_shoe.n.01", "name": "brake_shoe"}, + {"id": 6023, "synset": "brake_system.n.01", "name": "brake_system"}, + {"id": 6024, "synset": "brass.n.02", "name": "brass"}, + {"id": 6025, "synset": "brass.n.05", "name": "brass"}, + {"id": 6026, "synset": "brassard.n.01", "name": "brassard"}, + {"id": 6027, "synset": "brasserie.n.01", "name": "brasserie"}, + {"id": 6028, "synset": "brassie.n.01", "name": "brassie"}, + {"id": 6029, "synset": "brass_knucks.n.01", "name": "brass_knucks"}, + {"id": 6030, "synset": "brattice.n.01", "name": "brattice"}, + {"id": 6031, "synset": "brazier.n.01", "name": "brazier"}, + {"id": 6032, "synset": "breadbasket.n.03", "name": "breadbasket"}, + {"id": 6033, "synset": "bread_knife.n.01", "name": "bread_knife"}, + {"id": 6034, "synset": "breakable.n.01", "name": "breakable"}, + {"id": 6035, "synset": "breakfast_area.n.01", "name": "breakfast_area"}, + {"id": 6036, "synset": "breakfast_table.n.01", "name": "breakfast_table"}, + {"id": 6037, "synset": "breakwater.n.01", "name": "breakwater"}, + {"id": 6038, "synset": "breast_drill.n.01", "name": "breast_drill"}, + {"id": 6039, "synset": "breast_implant.n.01", "name": "breast_implant"}, + {"id": 6040, "synset": "breastplate.n.01", "name": "breastplate"}, + {"id": 6041, "synset": "breast_pocket.n.01", "name": "breast_pocket"}, + {"id": 6042, "synset": "breathalyzer.n.01", "name": "breathalyzer"}, + {"id": 6043, "synset": "breechblock.n.01", "name": "breechblock"}, + {"id": 6044, "synset": "breeches.n.01", "name": "breeches"}, + {"id": 6045, "synset": "breeches_buoy.n.01", "name": "breeches_buoy"}, + {"id": 6046, "synset": "breechloader.n.01", "name": "breechloader"}, + {"id": 6047, "synset": "breeder_reactor.n.01", "name": "breeder_reactor"}, + {"id": 6048, "synset": "bren.n.01", "name": "Bren"}, + {"id": 6049, "synset": "brewpub.n.01", "name": "brewpub"}, + {"id": 6050, "synset": "brick.n.01", "name": "brick"}, + {"id": 6051, "synset": "brickkiln.n.01", "name": "brickkiln"}, + {"id": 6052, "synset": "bricklayer's_hammer.n.01", "name": "bricklayer's_hammer"}, + {"id": 6053, "synset": "brick_trowel.n.01", "name": "brick_trowel"}, + {"id": 6054, "synset": "brickwork.n.01", "name": "brickwork"}, + {"id": 6055, "synset": "bridge.n.01", "name": "bridge"}, + {"id": 6056, "synset": "bridge.n.08", "name": "bridge"}, + {"id": 6057, "synset": "bridle.n.01", "name": "bridle"}, + {"id": 6058, "synset": "bridle_path.n.01", "name": "bridle_path"}, + {"id": 6059, "synset": "bridoon.n.01", "name": "bridoon"}, + {"id": 6060, "synset": "briefcase_bomb.n.01", "name": "briefcase_bomb"}, + {"id": 6061, "synset": "briefcase_computer.n.01", "name": "briefcase_computer"}, + {"id": 6062, "synset": "briefs.n.01", "name": "briefs"}, + {"id": 6063, "synset": "brig.n.02", "name": "brig"}, + {"id": 6064, "synset": "brig.n.01", "name": "brig"}, + {"id": 6065, "synset": "brigandine.n.01", "name": "brigandine"}, + {"id": 6066, "synset": "brigantine.n.01", "name": "brigantine"}, + {"id": 6067, "synset": "brilliantine.n.01", "name": "brilliantine"}, + {"id": 6068, "synset": "brilliant_pebble.n.01", "name": "brilliant_pebble"}, + {"id": 6069, "synset": "brim.n.02", "name": "brim"}, + {"id": 6070, "synset": "bristle_brush.n.01", "name": "bristle_brush"}, + {"id": 6071, "synset": "britches.n.01", "name": "britches"}, + {"id": 6072, "synset": "broad_arrow.n.03", "name": "broad_arrow"}, + {"id": 6073, "synset": "broadax.n.01", "name": "broadax"}, + {"id": 6074, "synset": "brochette.n.01", "name": "brochette"}, + {"id": 6075, "synset": "broadcaster.n.02", "name": "broadcaster"}, + {"id": 6076, "synset": "broadcloth.n.02", "name": "broadcloth"}, + {"id": 6077, "synset": "broadcloth.n.01", "name": "broadcloth"}, + {"id": 6078, "synset": "broad_hatchet.n.01", "name": "broad_hatchet"}, + {"id": 6079, "synset": "broadloom.n.01", "name": "broadloom"}, + {"id": 6080, "synset": "broadside.n.03", "name": "broadside"}, + {"id": 6081, "synset": "broadsword.n.01", "name": "broadsword"}, + {"id": 6082, "synset": "brocade.n.01", "name": "brocade"}, + {"id": 6083, "synset": "brogan.n.01", "name": "brogan"}, + {"id": 6084, "synset": "broiler.n.01", "name": "broiler"}, + {"id": 6085, "synset": "broken_arch.n.01", "name": "broken_arch"}, + {"id": 6086, "synset": "bronchoscope.n.01", "name": "bronchoscope"}, + {"id": 6087, "synset": "broom_closet.n.01", "name": "broom_closet"}, + {"id": 6088, "synset": "broomstick.n.01", "name": "broomstick"}, + {"id": 6089, "synset": "brougham.n.01", "name": "brougham"}, + {"id": 6090, "synset": "browning_automatic_rifle.n.01", "name": "Browning_automatic_rifle"}, + {"id": 6091, "synset": "browning_machine_gun.n.01", "name": "Browning_machine_gun"}, + {"id": 6092, "synset": "brownstone.n.02", "name": "brownstone"}, + {"id": 6093, "synset": "brunch_coat.n.01", "name": "brunch_coat"}, + {"id": 6094, "synset": "brush.n.02", "name": "brush"}, + {"id": 6095, "synset": "brussels_carpet.n.01", "name": "Brussels_carpet"}, + {"id": 6096, "synset": "brussels_lace.n.01", "name": "Brussels_lace"}, + {"id": 6097, "synset": "bubble.n.04", "name": "bubble"}, + {"id": 6098, "synset": "bubble_chamber.n.01", "name": "bubble_chamber"}, + {"id": 6099, "synset": "bubble_jet_printer.n.01", "name": "bubble_jet_printer"}, + {"id": 6100, "synset": "buckboard.n.01", "name": "buckboard"}, + {"id": 6101, "synset": "bucket_seat.n.01", "name": "bucket_seat"}, + {"id": 6102, "synset": "bucket_shop.n.02", "name": "bucket_shop"}, + {"id": 6103, "synset": "buckle.n.01", "name": "buckle"}, + {"id": 6104, "synset": "buckram.n.01", "name": "buckram"}, + {"id": 6105, "synset": "bucksaw.n.01", "name": "bucksaw"}, + {"id": 6106, "synset": "buckskins.n.01", "name": "buckskins"}, + {"id": 6107, "synset": "buff.n.05", "name": "buff"}, + {"id": 6108, "synset": "buffer.n.05", "name": "buffer"}, + {"id": 6109, "synset": "buffer.n.04", "name": "buffer"}, + {"id": 6110, "synset": "buffet.n.01", "name": "buffet"}, + {"id": 6111, "synset": "buffing_wheel.n.01", "name": "buffing_wheel"}, + {"id": 6112, "synset": "bugle.n.01", "name": "bugle"}, + {"id": 6113, "synset": "building.n.01", "name": "building"}, + {"id": 6114, "synset": "building_complex.n.01", "name": "building_complex"}, + {"id": 6115, "synset": "bulldog_clip.n.01", "name": "bulldog_clip"}, + {"id": 6116, "synset": "bulldog_wrench.n.01", "name": "bulldog_wrench"}, + {"id": 6117, "synset": "bullet.n.01", "name": "bullet"}, + {"id": 6118, "synset": "bullion.n.02", "name": "bullion"}, + {"id": 6119, "synset": "bullnose.n.01", "name": "bullnose"}, + {"id": 6120, "synset": "bullpen.n.02", "name": "bullpen"}, + {"id": 6121, "synset": "bullpen.n.01", "name": "bullpen"}, + {"id": 6122, "synset": "bullring.n.01", "name": "bullring"}, + {"id": 6123, "synset": "bulwark.n.02", "name": "bulwark"}, + {"id": 6124, "synset": "bumboat.n.01", "name": "bumboat"}, + {"id": 6125, "synset": "bumper.n.02", "name": "bumper"}, + {"id": 6126, "synset": "bumper.n.01", "name": "bumper"}, + {"id": 6127, "synset": "bumper_car.n.01", "name": "bumper_car"}, + {"id": 6128, "synset": "bumper_guard.n.01", "name": "bumper_guard"}, + {"id": 6129, "synset": "bumper_jack.n.01", "name": "bumper_jack"}, + {"id": 6130, "synset": "bundle.n.02", "name": "bundle"}, + {"id": 6131, "synset": "bung.n.01", "name": "bung"}, + {"id": 6132, "synset": "bungalow.n.01", "name": "bungalow"}, + {"id": 6133, "synset": "bungee.n.01", "name": "bungee"}, + {"id": 6134, "synset": "bunghole.n.02", "name": "bunghole"}, + {"id": 6135, "synset": "bunk.n.03", "name": "bunk"}, + {"id": 6136, "synset": "bunk.n.01", "name": "bunk"}, + {"id": 6137, "synset": "bunker.n.01", "name": "bunker"}, + {"id": 6138, "synset": "bunker.n.03", "name": "bunker"}, + {"id": 6139, "synset": "bunker.n.02", "name": "bunker"}, + {"id": 6140, "synset": "bunsen_burner.n.01", "name": "bunsen_burner"}, + {"id": 6141, "synset": "bunting.n.01", "name": "bunting"}, + {"id": 6142, "synset": "bur.n.02", "name": "bur"}, + {"id": 6143, "synset": "burberry.n.01", "name": "Burberry"}, + {"id": 6144, "synset": "burette.n.01", "name": "burette"}, + {"id": 6145, "synset": "burglar_alarm.n.02", "name": "burglar_alarm"}, + {"id": 6146, "synset": "burial_chamber.n.01", "name": "burial_chamber"}, + {"id": 6147, "synset": "burial_garment.n.01", "name": "burial_garment"}, + {"id": 6148, "synset": "burial_mound.n.01", "name": "burial_mound"}, + {"id": 6149, "synset": "burin.n.01", "name": "burin"}, + {"id": 6150, "synset": "burqa.n.01", "name": "burqa"}, + {"id": 6151, "synset": "burlap.n.01", "name": "burlap"}, + {"id": 6152, "synset": "burn_bag.n.01", "name": "burn_bag"}, + {"id": 6153, "synset": "burner.n.01", "name": "burner"}, + {"id": 6154, "synset": "burnous.n.01", "name": "burnous"}, + {"id": 6155, "synset": "burp_gun.n.01", "name": "burp_gun"}, + {"id": 6156, "synset": "burr.n.04", "name": "burr"}, + {"id": 6157, "synset": "bushel_basket.n.01", "name": "bushel_basket"}, + {"id": 6158, "synset": "bushing.n.02", "name": "bushing"}, + {"id": 6159, "synset": "bush_jacket.n.01", "name": "bush_jacket"}, + {"id": 6160, "synset": "business_suit.n.01", "name": "business_suit"}, + {"id": 6161, "synset": "buskin.n.01", "name": "buskin"}, + {"id": 6162, "synset": "bustier.n.01", "name": "bustier"}, + {"id": 6163, "synset": "bustle.n.02", "name": "bustle"}, + {"id": 6164, "synset": "butcher_knife.n.01", "name": "butcher_knife"}, + {"id": 6165, "synset": "butcher_shop.n.01", "name": "butcher_shop"}, + {"id": 6166, "synset": "butter_dish.n.01", "name": "butter_dish"}, + {"id": 6167, "synset": "butterfly_valve.n.01", "name": "butterfly_valve"}, + {"id": 6168, "synset": "butter_knife.n.01", "name": "butter_knife"}, + {"id": 6169, "synset": "butt_hinge.n.01", "name": "butt_hinge"}, + {"id": 6170, "synset": "butt_joint.n.01", "name": "butt_joint"}, + {"id": 6171, "synset": "buttonhook.n.01", "name": "buttonhook"}, + {"id": 6172, "synset": "buttress.n.01", "name": "buttress"}, + {"id": 6173, "synset": "butt_shaft.n.01", "name": "butt_shaft"}, + {"id": 6174, "synset": "butt_weld.n.01", "name": "butt_weld"}, + {"id": 6175, "synset": "buzz_bomb.n.01", "name": "buzz_bomb"}, + {"id": 6176, "synset": "buzzer.n.02", "name": "buzzer"}, + {"id": 6177, "synset": "bvd.n.01", "name": "BVD"}, + {"id": 6178, "synset": "bypass_condenser.n.01", "name": "bypass_condenser"}, + {"id": 6179, "synset": "byway.n.01", "name": "byway"}, + {"id": 6180, "synset": "cab.n.02", "name": "cab"}, + {"id": 6181, "synset": "cab.n.01", "name": "cab"}, + {"id": 6182, "synset": "cabaret.n.01", "name": "cabaret"}, + {"id": 6183, "synset": "caber.n.01", "name": "caber"}, + {"id": 6184, "synset": "cabin.n.03", "name": "cabin"}, + {"id": 6185, "synset": "cabin.n.02", "name": "cabin"}, + {"id": 6186, "synset": "cabin_class.n.01", "name": "cabin_class"}, + {"id": 6187, "synset": "cabin_cruiser.n.01", "name": "cabin_cruiser"}, + {"id": 6188, "synset": "cabinet.n.04", "name": "cabinet"}, + {"id": 6189, "synset": "cabinetwork.n.01", "name": "cabinetwork"}, + {"id": 6190, "synset": "cabin_liner.n.01", "name": "cabin_liner"}, + {"id": 6191, "synset": "cable.n.06", "name": "cable"}, + {"id": 6192, "synset": "cable.n.02", "name": "cable"}, + {"id": 6193, "synset": "cable_car.n.01", "name": "cable_car"}, + {"id": 6194, "synset": "cache.n.03", "name": "cache"}, + {"id": 6195, "synset": "caddy.n.01", "name": "caddy"}, + {"id": 6196, "synset": "caesium_clock.n.01", "name": "caesium_clock"}, + {"id": 6197, "synset": "cafe.n.01", "name": "cafe"}, + {"id": 6198, "synset": "cafeteria.n.01", "name": "cafeteria"}, + {"id": 6199, "synset": "cafeteria_tray.n.01", "name": "cafeteria_tray"}, + {"id": 6200, "synset": "caff.n.01", "name": "caff"}, + {"id": 6201, "synset": "caftan.n.02", "name": "caftan"}, + {"id": 6202, "synset": "caftan.n.01", "name": "caftan"}, + {"id": 6203, "synset": "cage.n.01", "name": "cage"}, + {"id": 6204, "synset": "cage.n.04", "name": "cage"}, + {"id": 6205, "synset": "cagoule.n.01", "name": "cagoule"}, + {"id": 6206, "synset": "caisson.n.02", "name": "caisson"}, + {"id": 6207, "synset": "calash.n.02", "name": "calash"}, + {"id": 6208, "synset": "calceus.n.01", "name": "calceus"}, + {"id": 6209, "synset": "calcimine.n.01", "name": "calcimine"}, + {"id": 6210, "synset": "caldron.n.01", "name": "caldron"}, + {"id": 6211, "synset": "calico.n.01", "name": "calico"}, + {"id": 6212, "synset": "caliper.n.01", "name": "caliper"}, + {"id": 6213, "synset": "call-board.n.01", "name": "call-board"}, + {"id": 6214, "synset": "call_center.n.01", "name": "call_center"}, + {"id": 6215, "synset": "caller_id.n.01", "name": "caller_ID"}, + {"id": 6216, "synset": "calliope.n.02", "name": "calliope"}, + {"id": 6217, "synset": "calorimeter.n.01", "name": "calorimeter"}, + {"id": 6218, "synset": "calpac.n.01", "name": "calpac"}, + {"id": 6219, "synset": "camail.n.01", "name": "camail"}, + {"id": 6220, "synset": "camber_arch.n.01", "name": "camber_arch"}, + {"id": 6221, "synset": "cambric.n.01", "name": "cambric"}, + {"id": 6222, "synset": "camel's_hair.n.01", "name": "camel's_hair"}, + {"id": 6223, "synset": "camera_lucida.n.01", "name": "camera_lucida"}, + {"id": 6224, "synset": "camera_obscura.n.01", "name": "camera_obscura"}, + {"id": 6225, "synset": "camera_tripod.n.01", "name": "camera_tripod"}, + {"id": 6226, "synset": "camise.n.01", "name": "camise"}, + {"id": 6227, "synset": "camisole.n.02", "name": "camisole"}, + {"id": 6228, "synset": "camisole.n.01", "name": "camisole"}, + {"id": 6229, "synset": "camlet.n.02", "name": "camlet"}, + {"id": 6230, "synset": "camouflage.n.03", "name": "camouflage"}, + {"id": 6231, "synset": "camouflage.n.02", "name": "camouflage"}, + {"id": 6232, "synset": "camp.n.01", "name": "camp"}, + {"id": 6233, "synset": "camp.n.03", "name": "camp"}, + {"id": 6234, "synset": "camp.n.07", "name": "camp"}, + {"id": 6235, "synset": "campaign_hat.n.01", "name": "campaign_hat"}, + {"id": 6236, "synset": "campanile.n.01", "name": "campanile"}, + {"id": 6237, "synset": "camp_chair.n.01", "name": "camp_chair"}, + {"id": 6238, "synset": "camper_trailer.n.01", "name": "camper_trailer"}, + {"id": 6239, "synset": "campstool.n.01", "name": "campstool"}, + {"id": 6240, "synset": "camshaft.n.01", "name": "camshaft"}, + {"id": 6241, "synset": "canal.n.03", "name": "canal"}, + {"id": 6242, "synset": "canal_boat.n.01", "name": "canal_boat"}, + {"id": 6243, "synset": "candelabrum.n.01", "name": "candelabrum"}, + {"id": 6244, "synset": "candid_camera.n.01", "name": "candid_camera"}, + {"id": 6245, "synset": "candlepin.n.01", "name": "candlepin"}, + {"id": 6246, "synset": "candlesnuffer.n.01", "name": "candlesnuffer"}, + {"id": 6247, "synset": "candlewick.n.02", "name": "candlewick"}, + {"id": 6248, "synset": "candy_thermometer.n.01", "name": "candy_thermometer"}, + {"id": 6249, "synset": "cane.n.03", "name": "cane"}, + {"id": 6250, "synset": "cangue.n.01", "name": "cangue"}, + {"id": 6251, "synset": "cannery.n.01", "name": "cannery"}, + {"id": 6252, "synset": "cannikin.n.02", "name": "cannikin"}, + {"id": 6253, "synset": "cannikin.n.01", "name": "cannikin"}, + {"id": 6254, "synset": "cannon.n.01", "name": "cannon"}, + {"id": 6255, "synset": "cannon.n.04", "name": "cannon"}, + {"id": 6256, "synset": "cannon.n.03", "name": "cannon"}, + {"id": 6257, "synset": "cannon.n.02", "name": "cannon"}, + {"id": 6258, "synset": "cannonball.n.01", "name": "cannonball"}, + {"id": 6259, "synset": "canopic_jar.n.01", "name": "canopic_jar"}, + {"id": 6260, "synset": "canopy.n.03", "name": "canopy"}, + {"id": 6261, "synset": "canopy.n.02", "name": "canopy"}, + {"id": 6262, "synset": "canopy.n.01", "name": "canopy"}, + {"id": 6263, "synset": "canteen.n.05", "name": "canteen"}, + {"id": 6264, "synset": "canteen.n.04", "name": "canteen"}, + {"id": 6265, "synset": "canteen.n.03", "name": "canteen"}, + {"id": 6266, "synset": "canteen.n.02", "name": "canteen"}, + {"id": 6267, "synset": "cant_hook.n.01", "name": "cant_hook"}, + {"id": 6268, "synset": "cantilever.n.01", "name": "cantilever"}, + {"id": 6269, "synset": "cantilever_bridge.n.01", "name": "cantilever_bridge"}, + {"id": 6270, "synset": "cantle.n.01", "name": "cantle"}, + {"id": 6271, "synset": "canton_crepe.n.01", "name": "Canton_crepe"}, + {"id": 6272, "synset": "canvas.n.01", "name": "canvas"}, + {"id": 6273, "synset": "canvas.n.06", "name": "canvas"}, + {"id": 6274, "synset": "canvas_tent.n.01", "name": "canvas_tent"}, + {"id": 6275, "synset": "cap.n.04", "name": "cap"}, + {"id": 6276, "synset": "capacitor.n.01", "name": "capacitor"}, + {"id": 6277, "synset": "caparison.n.01", "name": "caparison"}, + {"id": 6278, "synset": "capital_ship.n.01", "name": "capital_ship"}, + {"id": 6279, "synset": "capitol.n.01", "name": "capitol"}, + {"id": 6280, "synset": "cap_opener.n.01", "name": "cap_opener"}, + {"id": 6281, "synset": "capote.n.02", "name": "capote"}, + {"id": 6282, "synset": "capote.n.01", "name": "capote"}, + {"id": 6283, "synset": "cap_screw.n.01", "name": "cap_screw"}, + {"id": 6284, "synset": "capstan.n.01", "name": "capstan"}, + {"id": 6285, "synset": "capstone.n.02", "name": "capstone"}, + {"id": 6286, "synset": "capsule.n.01", "name": "capsule"}, + {"id": 6287, "synset": "captain's_chair.n.01", "name": "captain's_chair"}, + {"id": 6288, "synset": "carabiner.n.01", "name": "carabiner"}, + {"id": 6289, "synset": "carafe.n.01", "name": "carafe"}, + {"id": 6290, "synset": "caravansary.n.01", "name": "caravansary"}, + {"id": 6291, "synset": "carbine.n.01", "name": "carbine"}, + {"id": 6292, "synset": "car_bomb.n.01", "name": "car_bomb"}, + {"id": 6293, "synset": "carbon_arc_lamp.n.01", "name": "carbon_arc_lamp"}, + {"id": 6294, "synset": "carboy.n.01", "name": "carboy"}, + {"id": 6295, "synset": "carburetor.n.01", "name": "carburetor"}, + {"id": 6296, "synset": "car_carrier.n.01", "name": "car_carrier"}, + {"id": 6297, "synset": "cardcase.n.01", "name": "cardcase"}, + {"id": 6298, "synset": "cardiac_monitor.n.01", "name": "cardiac_monitor"}, + {"id": 6299, "synset": "card_index.n.01", "name": "card_index"}, + {"id": 6300, "synset": "cardiograph.n.01", "name": "cardiograph"}, + {"id": 6301, "synset": "cardioid_microphone.n.01", "name": "cardioid_microphone"}, + {"id": 6302, "synset": "car_door.n.01", "name": "car_door"}, + {"id": 6303, "synset": "cardroom.n.01", "name": "cardroom"}, + {"id": 6304, "synset": "card_table.n.02", "name": "card_table"}, + {"id": 6305, "synset": "card_table.n.01", "name": "card_table"}, + {"id": 6306, "synset": "car-ferry.n.01", "name": "car-ferry"}, + {"id": 6307, "synset": "cargo_area.n.01", "name": "cargo_area"}, + {"id": 6308, "synset": "cargo_container.n.01", "name": "cargo_container"}, + {"id": 6309, "synset": "cargo_door.n.01", "name": "cargo_door"}, + {"id": 6310, "synset": "cargo_hatch.n.01", "name": "cargo_hatch"}, + {"id": 6311, "synset": "cargo_helicopter.n.01", "name": "cargo_helicopter"}, + {"id": 6312, "synset": "cargo_liner.n.01", "name": "cargo_liner"}, + {"id": 6313, "synset": "carillon.n.01", "name": "carillon"}, + {"id": 6314, "synset": "car_mirror.n.01", "name": "car_mirror"}, + {"id": 6315, "synset": "caroche.n.01", "name": "caroche"}, + {"id": 6316, "synset": "carousel.n.02", "name": "carousel"}, + {"id": 6317, "synset": "carpenter's_hammer.n.01", "name": "carpenter's_hammer"}, + {"id": 6318, "synset": "carpenter's_kit.n.01", "name": "carpenter's_kit"}, + {"id": 6319, "synset": "carpenter's_level.n.01", "name": "carpenter's_level"}, + {"id": 6320, "synset": "carpenter's_mallet.n.01", "name": "carpenter's_mallet"}, + {"id": 6321, "synset": "carpenter's_rule.n.01", "name": "carpenter's_rule"}, + {"id": 6322, "synset": "carpenter's_square.n.01", "name": "carpenter's_square"}, + {"id": 6323, "synset": "carpetbag.n.01", "name": "carpetbag"}, + {"id": 6324, "synset": "carpet_beater.n.01", "name": "carpet_beater"}, + {"id": 6325, "synset": "carpet_loom.n.01", "name": "carpet_loom"}, + {"id": 6326, "synset": "carpet_pad.n.01", "name": "carpet_pad"}, + {"id": 6327, "synset": "carpet_sweeper.n.01", "name": "carpet_sweeper"}, + {"id": 6328, "synset": "carpet_tack.n.01", "name": "carpet_tack"}, + {"id": 6329, "synset": "carport.n.01", "name": "carport"}, + {"id": 6330, "synset": "carrack.n.01", "name": "carrack"}, + {"id": 6331, "synset": "carrel.n.02", "name": "carrel"}, + {"id": 6332, "synset": "carriage.n.04", "name": "carriage"}, + {"id": 6333, "synset": "carriage_bolt.n.01", "name": "carriage_bolt"}, + {"id": 6334, "synset": "carriageway.n.01", "name": "carriageway"}, + {"id": 6335, "synset": "carriage_wrench.n.01", "name": "carriage_wrench"}, + {"id": 6336, "synset": "carrick_bend.n.01", "name": "carrick_bend"}, + {"id": 6337, "synset": "carrier.n.10", "name": "carrier"}, + {"id": 6338, "synset": "carrycot.n.01", "name": "carrycot"}, + {"id": 6339, "synset": "car_seat.n.01", "name": "car_seat"}, + {"id": 6340, "synset": "car_tire.n.01", "name": "car_tire"}, + {"id": 6341, "synset": "cartouche.n.01", "name": "cartouche"}, + {"id": 6342, "synset": "car_train.n.01", "name": "car_train"}, + {"id": 6343, "synset": "cartridge.n.01", "name": "cartridge"}, + {"id": 6344, "synset": "cartridge.n.04", "name": "cartridge"}, + {"id": 6345, "synset": "cartridge_belt.n.01", "name": "cartridge_belt"}, + {"id": 6346, "synset": "cartridge_extractor.n.01", "name": "cartridge_extractor"}, + {"id": 6347, "synset": "cartridge_fuse.n.01", "name": "cartridge_fuse"}, + {"id": 6348, "synset": "cartridge_holder.n.01", "name": "cartridge_holder"}, + {"id": 6349, "synset": "cartwheel.n.01", "name": "cartwheel"}, + {"id": 6350, "synset": "carving_fork.n.01", "name": "carving_fork"}, + {"id": 6351, "synset": "carving_knife.n.01", "name": "carving_knife"}, + {"id": 6352, "synset": "car_wheel.n.01", "name": "car_wheel"}, + {"id": 6353, "synset": "caryatid.n.01", "name": "caryatid"}, + {"id": 6354, "synset": "cascade_liquefier.n.01", "name": "cascade_liquefier"}, + {"id": 6355, "synset": "cascade_transformer.n.01", "name": "cascade_transformer"}, + {"id": 6356, "synset": "case.n.05", "name": "case"}, + {"id": 6357, "synset": "case.n.20", "name": "case"}, + {"id": 6358, "synset": "case.n.18", "name": "case"}, + {"id": 6359, "synset": "casein_paint.n.01", "name": "casein_paint"}, + {"id": 6360, "synset": "case_knife.n.02", "name": "case_knife"}, + {"id": 6361, "synset": "case_knife.n.01", "name": "case_knife"}, + {"id": 6362, "synset": "casement.n.01", "name": "casement"}, + {"id": 6363, "synset": "casement_window.n.01", "name": "casement_window"}, + {"id": 6364, "synset": "casern.n.01", "name": "casern"}, + {"id": 6365, "synset": "case_shot.n.01", "name": "case_shot"}, + {"id": 6366, "synset": "cash_bar.n.01", "name": "cash_bar"}, + {"id": 6367, "synset": "cashbox.n.01", "name": "cashbox"}, + {"id": 6368, "synset": "cash_machine.n.01", "name": "cash_machine"}, + {"id": 6369, "synset": "cashmere.n.01", "name": "cashmere"}, + {"id": 6370, "synset": "casing.n.03", "name": "casing"}, + {"id": 6371, "synset": "casino.n.01", "name": "casino"}, + {"id": 6372, "synset": "casket.n.02", "name": "casket"}, + {"id": 6373, "synset": "casque.n.01", "name": "casque"}, + {"id": 6374, "synset": "casquet.n.01", "name": "casquet"}, + {"id": 6375, "synset": "cassegrainian_telescope.n.01", "name": "Cassegrainian_telescope"}, + {"id": 6376, "synset": "casserole.n.02", "name": "casserole"}, + {"id": 6377, "synset": "cassette_deck.n.01", "name": "cassette_deck"}, + {"id": 6378, "synset": "cassette_player.n.01", "name": "cassette_player"}, + {"id": 6379, "synset": "cassette_recorder.n.01", "name": "cassette_recorder"}, + {"id": 6380, "synset": "cassette_tape.n.01", "name": "cassette_tape"}, + {"id": 6381, "synset": "cassock.n.01", "name": "cassock"}, + {"id": 6382, "synset": "caster.n.03", "name": "caster"}, + {"id": 6383, "synset": "caster.n.02", "name": "caster"}, + {"id": 6384, "synset": "castle.n.02", "name": "castle"}, + {"id": 6385, "synset": "castle.n.03", "name": "castle"}, + {"id": 6386, "synset": "catacomb.n.01", "name": "catacomb"}, + {"id": 6387, "synset": "catafalque.n.01", "name": "catafalque"}, + {"id": 6388, "synset": "catalytic_converter.n.01", "name": "catalytic_converter"}, + {"id": 6389, "synset": "catalytic_cracker.n.01", "name": "catalytic_cracker"}, + {"id": 6390, "synset": "catamaran.n.01", "name": "catamaran"}, + {"id": 6391, "synset": "catapult.n.03", "name": "catapult"}, + {"id": 6392, "synset": "catapult.n.02", "name": "catapult"}, + {"id": 6393, "synset": "catboat.n.01", "name": "catboat"}, + {"id": 6394, "synset": "cat_box.n.01", "name": "cat_box"}, + {"id": 6395, "synset": "catch.n.07", "name": "catch"}, + {"id": 6396, "synset": "catchall.n.01", "name": "catchall"}, + {"id": 6397, "synset": "catcher's_mask.n.01", "name": "catcher's_mask"}, + {"id": 6398, "synset": "catchment.n.01", "name": "catchment"}, + {"id": 6399, "synset": "caterpillar.n.02", "name": "Caterpillar"}, + {"id": 6400, "synset": "cathedra.n.01", "name": "cathedra"}, + {"id": 6401, "synset": "cathedral.n.01", "name": "cathedral"}, + {"id": 6402, "synset": "cathedral.n.02", "name": "cathedral"}, + {"id": 6403, "synset": "catheter.n.01", "name": "catheter"}, + {"id": 6404, "synset": "cathode.n.01", "name": "cathode"}, + {"id": 6405, "synset": "cathode-ray_tube.n.01", "name": "cathode-ray_tube"}, + {"id": 6406, "synset": "cat-o'-nine-tails.n.01", "name": "cat-o'-nine-tails"}, + {"id": 6407, "synset": "cat's-paw.n.02", "name": "cat's-paw"}, + {"id": 6408, "synset": "catsup_bottle.n.01", "name": "catsup_bottle"}, + {"id": 6409, "synset": "cattle_car.n.01", "name": "cattle_car"}, + {"id": 6410, "synset": "cattle_guard.n.01", "name": "cattle_guard"}, + {"id": 6411, "synset": "cattleship.n.01", "name": "cattleship"}, + {"id": 6412, "synset": "cautery.n.01", "name": "cautery"}, + {"id": 6413, "synset": "cavalier_hat.n.01", "name": "cavalier_hat"}, + {"id": 6414, "synset": "cavalry_sword.n.01", "name": "cavalry_sword"}, + {"id": 6415, "synset": "cavetto.n.01", "name": "cavetto"}, + {"id": 6416, "synset": "cavity_wall.n.01", "name": "cavity_wall"}, + {"id": 6417, "synset": "c_battery.n.01", "name": "C_battery"}, + {"id": 6418, "synset": "c-clamp.n.01", "name": "C-clamp"}, + {"id": 6419, "synset": "cd_drive.n.01", "name": "CD_drive"}, + {"id": 6420, "synset": "cd-r.n.01", "name": "CD-R"}, + {"id": 6421, "synset": "cd-rom.n.01", "name": "CD-ROM"}, + {"id": 6422, "synset": "cd-rom_drive.n.01", "name": "CD-ROM_drive"}, + {"id": 6423, "synset": "cedar_chest.n.01", "name": "cedar_chest"}, + {"id": 6424, "synset": "ceiling.n.01", "name": "ceiling"}, + {"id": 6425, "synset": "celesta.n.01", "name": "celesta"}, + {"id": 6426, "synset": "cell.n.03", "name": "cell"}, + {"id": 6427, "synset": "cell.n.07", "name": "cell"}, + {"id": 6428, "synset": "cellar.n.03", "name": "cellar"}, + {"id": 6429, "synset": "cellblock.n.01", "name": "cellblock"}, + {"id": 6430, "synset": "cello.n.01", "name": "cello"}, + {"id": 6431, "synset": "cellophane.n.01", "name": "cellophane"}, + {"id": 6432, "synset": "cellulose_tape.n.01", "name": "cellulose_tape"}, + {"id": 6433, "synset": "cenotaph.n.01", "name": "cenotaph"}, + {"id": 6434, "synset": "censer.n.01", "name": "censer"}, + {"id": 6435, "synset": "center.n.03", "name": "center"}, + {"id": 6436, "synset": "center_punch.n.01", "name": "center_punch"}, + {"id": 6437, "synset": "centigrade_thermometer.n.01", "name": "Centigrade_thermometer"}, + {"id": 6438, "synset": "central_processing_unit.n.01", "name": "central_processing_unit"}, + {"id": 6439, "synset": "centrifugal_pump.n.01", "name": "centrifugal_pump"}, + {"id": 6440, "synset": "centrifuge.n.01", "name": "centrifuge"}, + {"id": 6441, "synset": "ceramic.n.01", "name": "ceramic"}, + {"id": 6442, "synset": "ceramic_ware.n.01", "name": "ceramic_ware"}, + {"id": 6443, "synset": "cereal_bowl.n.01", "name": "cereal_bowl"}, + {"id": 6444, "synset": "cereal_box.n.01", "name": "cereal_box"}, + {"id": 6445, "synset": "cerecloth.n.01", "name": "cerecloth"}, + {"id": 6446, "synset": "cesspool.n.01", "name": "cesspool"}, + {"id": 6447, "synset": "chachka.n.02", "name": "chachka"}, + {"id": 6448, "synset": "chador.n.01", "name": "chador"}, + {"id": 6449, "synset": "chafing_dish.n.01", "name": "chafing_dish"}, + {"id": 6450, "synset": "chain.n.03", "name": "chain"}, + {"id": 6451, "synset": "chain.n.05", "name": "chain"}, + {"id": 6452, "synset": "chainlink_fence.n.01", "name": "chainlink_fence"}, + {"id": 6453, "synset": "chain_printer.n.01", "name": "chain_printer"}, + {"id": 6454, "synset": "chain_saw.n.01", "name": "chain_saw"}, + {"id": 6455, "synset": "chain_store.n.01", "name": "chain_store"}, + {"id": 6456, "synset": "chain_tongs.n.01", "name": "chain_tongs"}, + {"id": 6457, "synset": "chain_wrench.n.01", "name": "chain_wrench"}, + {"id": 6458, "synset": "chair.n.05", "name": "chair"}, + {"id": 6459, "synset": "chair_of_state.n.01", "name": "chair_of_state"}, + {"id": 6460, "synset": "chairlift.n.01", "name": "chairlift"}, + {"id": 6461, "synset": "chaise.n.02", "name": "chaise"}, + {"id": 6462, "synset": "chalet.n.01", "name": "chalet"}, + {"id": 6463, "synset": "chalk.n.04", "name": "chalk"}, + {"id": 6464, "synset": "challis.n.01", "name": "challis"}, + {"id": 6465, "synset": "chamberpot.n.01", "name": "chamberpot"}, + {"id": 6466, "synset": "chambray.n.01", "name": "chambray"}, + {"id": 6467, "synset": "chamfer_bit.n.01", "name": "chamfer_bit"}, + {"id": 6468, "synset": "chamfer_plane.n.01", "name": "chamfer_plane"}, + {"id": 6469, "synset": "chamois_cloth.n.01", "name": "chamois_cloth"}, + {"id": 6470, "synset": "chancel.n.01", "name": "chancel"}, + {"id": 6471, "synset": "chancellery.n.01", "name": "chancellery"}, + {"id": 6472, "synset": "chancery.n.02", "name": "chancery"}, + {"id": 6473, "synset": "chandlery.n.01", "name": "chandlery"}, + {"id": 6474, "synset": "chanfron.n.01", "name": "chanfron"}, + {"id": 6475, "synset": "chanter.n.01", "name": "chanter"}, + {"id": 6476, "synset": "chantry.n.02", "name": "chantry"}, + {"id": 6477, "synset": "chapel.n.01", "name": "chapel"}, + {"id": 6478, "synset": "chapterhouse.n.02", "name": "chapterhouse"}, + {"id": 6479, "synset": "chapterhouse.n.01", "name": "chapterhouse"}, + {"id": 6480, "synset": "character_printer.n.01", "name": "character_printer"}, + {"id": 6481, "synset": "charcuterie.n.01", "name": "charcuterie"}, + { + "id": 6482, + "synset": "charge-exchange_accelerator.n.01", + "name": "charge-exchange_accelerator", + }, + {"id": 6483, "synset": "charger.n.02", "name": "charger"}, + {"id": 6484, "synset": "chariot.n.01", "name": "chariot"}, + {"id": 6485, "synset": "chariot.n.02", "name": "chariot"}, + {"id": 6486, "synset": "charnel_house.n.01", "name": "charnel_house"}, + {"id": 6487, "synset": "chassis.n.03", "name": "chassis"}, + {"id": 6488, "synset": "chassis.n.02", "name": "chassis"}, + {"id": 6489, "synset": "chasuble.n.01", "name": "chasuble"}, + {"id": 6490, "synset": "chateau.n.01", "name": "chateau"}, + {"id": 6491, "synset": "chatelaine.n.02", "name": "chatelaine"}, + {"id": 6492, "synset": "checker.n.03", "name": "checker"}, + {"id": 6493, "synset": "checkout.n.03", "name": "checkout"}, + {"id": 6494, "synset": "cheekpiece.n.01", "name": "cheekpiece"}, + {"id": 6495, "synset": "cheeseboard.n.01", "name": "cheeseboard"}, + {"id": 6496, "synset": "cheesecloth.n.01", "name": "cheesecloth"}, + {"id": 6497, "synset": "cheese_cutter.n.01", "name": "cheese_cutter"}, + {"id": 6498, "synset": "cheese_press.n.01", "name": "cheese_press"}, + {"id": 6499, "synset": "chemical_bomb.n.01", "name": "chemical_bomb"}, + {"id": 6500, "synset": "chemical_plant.n.01", "name": "chemical_plant"}, + {"id": 6501, "synset": "chemical_reactor.n.01", "name": "chemical_reactor"}, + {"id": 6502, "synset": "chemise.n.02", "name": "chemise"}, + {"id": 6503, "synset": "chemise.n.01", "name": "chemise"}, + {"id": 6504, "synset": "chenille.n.02", "name": "chenille"}, + {"id": 6505, "synset": "chessman.n.01", "name": "chessman"}, + {"id": 6506, "synset": "chest.n.02", "name": "chest"}, + {"id": 6507, "synset": "chesterfield.n.02", "name": "chesterfield"}, + {"id": 6508, "synset": "chest_of_drawers.n.01", "name": "chest_of_drawers"}, + {"id": 6509, "synset": "chest_protector.n.01", "name": "chest_protector"}, + {"id": 6510, "synset": "cheval-de-frise.n.01", "name": "cheval-de-frise"}, + {"id": 6511, "synset": "cheval_glass.n.01", "name": "cheval_glass"}, + {"id": 6512, "synset": "chicane.n.02", "name": "chicane"}, + {"id": 6513, "synset": "chicken_coop.n.01", "name": "chicken_coop"}, + {"id": 6514, "synset": "chicken_wire.n.01", "name": "chicken_wire"}, + {"id": 6515, "synset": "chicken_yard.n.01", "name": "chicken_yard"}, + {"id": 6516, "synset": "chiffon.n.01", "name": "chiffon"}, + {"id": 6517, "synset": "chiffonier.n.01", "name": "chiffonier"}, + {"id": 6518, "synset": "child's_room.n.01", "name": "child's_room"}, + {"id": 6519, "synset": "chimney_breast.n.01", "name": "chimney_breast"}, + {"id": 6520, "synset": "chimney_corner.n.01", "name": "chimney_corner"}, + {"id": 6521, "synset": "china.n.02", "name": "china"}, + {"id": 6522, "synset": "china_cabinet.n.01", "name": "china_cabinet"}, + {"id": 6523, "synset": "chinchilla.n.02", "name": "chinchilla"}, + {"id": 6524, "synset": "chinese_lantern.n.01", "name": "Chinese_lantern"}, + {"id": 6525, "synset": "chinese_puzzle.n.01", "name": "Chinese_puzzle"}, + {"id": 6526, "synset": "chinning_bar.n.01", "name": "chinning_bar"}, + {"id": 6527, "synset": "chino.n.02", "name": "chino"}, + {"id": 6528, "synset": "chino.n.01", "name": "chino"}, + {"id": 6529, "synset": "chin_rest.n.01", "name": "chin_rest"}, + {"id": 6530, "synset": "chin_strap.n.01", "name": "chin_strap"}, + {"id": 6531, "synset": "chintz.n.01", "name": "chintz"}, + {"id": 6532, "synset": "chip.n.07", "name": "chip"}, + {"id": 6533, "synset": "chisel.n.01", "name": "chisel"}, + {"id": 6534, "synset": "chlamys.n.02", "name": "chlamys"}, + {"id": 6535, "synset": "choir.n.03", "name": "choir"}, + {"id": 6536, "synset": "choir_loft.n.01", "name": "choir_loft"}, + {"id": 6537, "synset": "choke.n.02", "name": "choke"}, + {"id": 6538, "synset": "choke.n.01", "name": "choke"}, + {"id": 6539, "synset": "chokey.n.01", "name": "chokey"}, + {"id": 6540, "synset": "choo-choo.n.01", "name": "choo-choo"}, + {"id": 6541, "synset": "chopine.n.01", "name": "chopine"}, + {"id": 6542, "synset": "chordophone.n.01", "name": "chordophone"}, + {"id": 6543, "synset": "christmas_stocking.n.01", "name": "Christmas_stocking"}, + {"id": 6544, "synset": "chronograph.n.01", "name": "chronograph"}, + {"id": 6545, "synset": "chronometer.n.01", "name": "chronometer"}, + {"id": 6546, "synset": "chronoscope.n.01", "name": "chronoscope"}, + {"id": 6547, "synset": "chuck.n.03", "name": "chuck"}, + {"id": 6548, "synset": "chuck_wagon.n.01", "name": "chuck_wagon"}, + {"id": 6549, "synset": "chukka.n.02", "name": "chukka"}, + {"id": 6550, "synset": "church.n.02", "name": "church"}, + {"id": 6551, "synset": "church_bell.n.01", "name": "church_bell"}, + {"id": 6552, "synset": "church_hat.n.01", "name": "church_hat"}, + {"id": 6553, "synset": "church_key.n.01", "name": "church_key"}, + {"id": 6554, "synset": "church_tower.n.01", "name": "church_tower"}, + {"id": 6555, "synset": "churidars.n.01", "name": "churidars"}, + {"id": 6556, "synset": "churn.n.01", "name": "churn"}, + {"id": 6557, "synset": "ciderpress.n.01", "name": "ciderpress"}, + {"id": 6558, "synset": "cigar_band.n.01", "name": "cigar_band"}, + {"id": 6559, "synset": "cigar_cutter.n.01", "name": "cigar_cutter"}, + {"id": 6560, "synset": "cigarette_butt.n.01", "name": "cigarette_butt"}, + {"id": 6561, "synset": "cigarette_holder.n.01", "name": "cigarette_holder"}, + {"id": 6562, "synset": "cigar_lighter.n.01", "name": "cigar_lighter"}, + {"id": 6563, "synset": "cinch.n.02", "name": "cinch"}, + {"id": 6564, "synset": "cinema.n.02", "name": "cinema"}, + {"id": 6565, "synset": "cinquefoil.n.02", "name": "cinquefoil"}, + {"id": 6566, "synset": "circle.n.08", "name": "circle"}, + {"id": 6567, "synset": "circlet.n.02", "name": "circlet"}, + {"id": 6568, "synset": "circuit.n.01", "name": "circuit"}, + {"id": 6569, "synset": "circuit_board.n.01", "name": "circuit_board"}, + {"id": 6570, "synset": "circuit_breaker.n.01", "name": "circuit_breaker"}, + {"id": 6571, "synset": "circuitry.n.01", "name": "circuitry"}, + {"id": 6572, "synset": "circular_plane.n.01", "name": "circular_plane"}, + {"id": 6573, "synset": "circular_saw.n.01", "name": "circular_saw"}, + {"id": 6574, "synset": "circus_tent.n.01", "name": "circus_tent"}, + {"id": 6575, "synset": "cistern.n.03", "name": "cistern"}, + {"id": 6576, "synset": "cittern.n.01", "name": "cittern"}, + {"id": 6577, "synset": "city_hall.n.01", "name": "city_hall"}, + {"id": 6578, "synset": "cityscape.n.02", "name": "cityscape"}, + {"id": 6579, "synset": "city_university.n.01", "name": "city_university"}, + {"id": 6580, "synset": "civies.n.01", "name": "civies"}, + {"id": 6581, "synset": "civilian_clothing.n.01", "name": "civilian_clothing"}, + {"id": 6582, "synset": "clack_valve.n.01", "name": "clack_valve"}, + {"id": 6583, "synset": "clamp.n.01", "name": "clamp"}, + {"id": 6584, "synset": "clamshell.n.02", "name": "clamshell"}, + {"id": 6585, "synset": "clapper.n.03", "name": "clapper"}, + {"id": 6586, "synset": "clapperboard.n.01", "name": "clapperboard"}, + {"id": 6587, "synset": "clarence.n.01", "name": "clarence"}, + {"id": 6588, "synset": "clark_cell.n.01", "name": "Clark_cell"}, + {"id": 6589, "synset": "clasp_knife.n.01", "name": "clasp_knife"}, + {"id": 6590, "synset": "classroom.n.01", "name": "classroom"}, + {"id": 6591, "synset": "clavichord.n.01", "name": "clavichord"}, + {"id": 6592, "synset": "clavier.n.02", "name": "clavier"}, + {"id": 6593, "synset": "clay_pigeon.n.01", "name": "clay_pigeon"}, + {"id": 6594, "synset": "claymore_mine.n.01", "name": "claymore_mine"}, + {"id": 6595, "synset": "claymore.n.01", "name": "claymore"}, + {"id": 6596, "synset": "cleaners.n.01", "name": "cleaners"}, + {"id": 6597, "synset": "cleaning_implement.n.01", "name": "cleaning_implement"}, + {"id": 6598, "synset": "cleaning_pad.n.01", "name": "cleaning_pad"}, + {"id": 6599, "synset": "clean_room.n.01", "name": "clean_room"}, + {"id": 6600, "synset": "clearway.n.01", "name": "clearway"}, + {"id": 6601, "synset": "cleat.n.01", "name": "cleat"}, + {"id": 6602, "synset": "cleats.n.01", "name": "cleats"}, + {"id": 6603, "synset": "cleaver.n.01", "name": "cleaver"}, + {"id": 6604, "synset": "clerestory.n.01", "name": "clerestory"}, + {"id": 6605, "synset": "clevis.n.01", "name": "clevis"}, + {"id": 6606, "synset": "clews.n.01", "name": "clews"}, + {"id": 6607, "synset": "cliff_dwelling.n.01", "name": "cliff_dwelling"}, + {"id": 6608, "synset": "climbing_frame.n.01", "name": "climbing_frame"}, + {"id": 6609, "synset": "clinch.n.03", "name": "clinch"}, + {"id": 6610, "synset": "clinch.n.02", "name": "clinch"}, + {"id": 6611, "synset": "clincher.n.03", "name": "clincher"}, + {"id": 6612, "synset": "clinic.n.03", "name": "clinic"}, + {"id": 6613, "synset": "clinical_thermometer.n.01", "name": "clinical_thermometer"}, + {"id": 6614, "synset": "clinker.n.02", "name": "clinker"}, + {"id": 6615, "synset": "clinometer.n.01", "name": "clinometer"}, + {"id": 6616, "synset": "clip_lead.n.01", "name": "clip_lead"}, + {"id": 6617, "synset": "clip-on.n.01", "name": "clip-on"}, + {"id": 6618, "synset": "clipper.n.04", "name": "clipper"}, + {"id": 6619, "synset": "clipper.n.02", "name": "clipper"}, + {"id": 6620, "synset": "cloak.n.01", "name": "cloak"}, + {"id": 6621, "synset": "cloakroom.n.02", "name": "cloakroom"}, + {"id": 6622, "synset": "cloche.n.02", "name": "cloche"}, + {"id": 6623, "synset": "cloche.n.01", "name": "cloche"}, + {"id": 6624, "synset": "clock_pendulum.n.01", "name": "clock_pendulum"}, + {"id": 6625, "synset": "clock_radio.n.01", "name": "clock_radio"}, + {"id": 6626, "synset": "clockwork.n.01", "name": "clockwork"}, + {"id": 6627, "synset": "clog.n.01", "name": "clog"}, + {"id": 6628, "synset": "cloisonne.n.01", "name": "cloisonne"}, + {"id": 6629, "synset": "cloister.n.02", "name": "cloister"}, + {"id": 6630, "synset": "closed_circuit.n.01", "name": "closed_circuit"}, + {"id": 6631, "synset": "closed-circuit_television.n.01", "name": "closed-circuit_television"}, + {"id": 6632, "synset": "closed_loop.n.01", "name": "closed_loop"}, + {"id": 6633, "synset": "closet.n.04", "name": "closet"}, + {"id": 6634, "synset": "closeup_lens.n.01", "name": "closeup_lens"}, + {"id": 6635, "synset": "cloth_cap.n.01", "name": "cloth_cap"}, + {"id": 6636, "synset": "cloth_covering.n.01", "name": "cloth_covering"}, + {"id": 6637, "synset": "clothesbrush.n.01", "name": "clothesbrush"}, + {"id": 6638, "synset": "clothes_closet.n.01", "name": "clothes_closet"}, + {"id": 6639, "synset": "clothes_dryer.n.01", "name": "clothes_dryer"}, + {"id": 6640, "synset": "clotheshorse.n.01", "name": "clotheshorse"}, + {"id": 6641, "synset": "clothes_tree.n.01", "name": "clothes_tree"}, + {"id": 6642, "synset": "clothing.n.01", "name": "clothing"}, + {"id": 6643, "synset": "clothing_store.n.01", "name": "clothing_store"}, + {"id": 6644, "synset": "clout_nail.n.01", "name": "clout_nail"}, + {"id": 6645, "synset": "clove_hitch.n.01", "name": "clove_hitch"}, + {"id": 6646, "synset": "club_car.n.01", "name": "club_car"}, + {"id": 6647, "synset": "clubroom.n.01", "name": "clubroom"}, + {"id": 6648, "synset": "cluster_bomb.n.01", "name": "cluster_bomb"}, + {"id": 6649, "synset": "clutch.n.07", "name": "clutch"}, + {"id": 6650, "synset": "clutch.n.06", "name": "clutch"}, + {"id": 6651, "synset": "coach.n.04", "name": "coach"}, + {"id": 6652, "synset": "coach_house.n.01", "name": "coach_house"}, + {"id": 6653, "synset": "coal_car.n.01", "name": "coal_car"}, + {"id": 6654, "synset": "coal_chute.n.01", "name": "coal_chute"}, + {"id": 6655, "synset": "coal_house.n.01", "name": "coal_house"}, + {"id": 6656, "synset": "coal_shovel.n.01", "name": "coal_shovel"}, + {"id": 6657, "synset": "coaming.n.01", "name": "coaming"}, + {"id": 6658, "synset": "coaster_brake.n.01", "name": "coaster_brake"}, + {"id": 6659, "synset": "coat_button.n.01", "name": "coat_button"}, + {"id": 6660, "synset": "coat_closet.n.01", "name": "coat_closet"}, + {"id": 6661, "synset": "coatdress.n.01", "name": "coatdress"}, + {"id": 6662, "synset": "coatee.n.01", "name": "coatee"}, + {"id": 6663, "synset": "coating.n.01", "name": "coating"}, + {"id": 6664, "synset": "coating.n.03", "name": "coating"}, + {"id": 6665, "synset": "coat_of_paint.n.01", "name": "coat_of_paint"}, + {"id": 6666, "synset": "coattail.n.01", "name": "coattail"}, + {"id": 6667, "synset": "coaxial_cable.n.01", "name": "coaxial_cable"}, + {"id": 6668, "synset": "cobweb.n.03", "name": "cobweb"}, + {"id": 6669, "synset": "cobweb.n.01", "name": "cobweb"}, + { + "id": 6670, + "synset": "cockcroft_and_walton_accelerator.n.01", + "name": "Cockcroft_and_Walton_accelerator", + }, + {"id": 6671, "synset": "cocked_hat.n.01", "name": "cocked_hat"}, + {"id": 6672, "synset": "cockhorse.n.01", "name": "cockhorse"}, + {"id": 6673, "synset": "cockleshell.n.01", "name": "cockleshell"}, + {"id": 6674, "synset": "cockpit.n.01", "name": "cockpit"}, + {"id": 6675, "synset": "cockpit.n.03", "name": "cockpit"}, + {"id": 6676, "synset": "cockpit.n.02", "name": "cockpit"}, + {"id": 6677, "synset": "cockscomb.n.03", "name": "cockscomb"}, + {"id": 6678, "synset": "cocktail_dress.n.01", "name": "cocktail_dress"}, + {"id": 6679, "synset": "cocktail_lounge.n.01", "name": "cocktail_lounge"}, + {"id": 6680, "synset": "cocktail_shaker.n.01", "name": "cocktail_shaker"}, + {"id": 6681, "synset": "cocotte.n.02", "name": "cocotte"}, + {"id": 6682, "synset": "codpiece.n.01", "name": "codpiece"}, + {"id": 6683, "synset": "coelostat.n.01", "name": "coelostat"}, + {"id": 6684, "synset": "coffee_can.n.01", "name": "coffee_can"}, + {"id": 6685, "synset": "coffee_cup.n.01", "name": "coffee_cup"}, + {"id": 6686, "synset": "coffee_filter.n.01", "name": "coffee_filter"}, + {"id": 6687, "synset": "coffee_mill.n.01", "name": "coffee_mill"}, + {"id": 6688, "synset": "coffee_mug.n.01", "name": "coffee_mug"}, + {"id": 6689, "synset": "coffee_stall.n.01", "name": "coffee_stall"}, + {"id": 6690, "synset": "coffee_urn.n.01", "name": "coffee_urn"}, + {"id": 6691, "synset": "coffer.n.02", "name": "coffer"}, + {"id": 6692, "synset": "coffey_still.n.01", "name": "Coffey_still"}, + {"id": 6693, "synset": "coffin.n.01", "name": "coffin"}, + {"id": 6694, "synset": "cog.n.02", "name": "cog"}, + {"id": 6695, "synset": "coif.n.02", "name": "coif"}, + {"id": 6696, "synset": "coil.n.01", "name": "coil"}, + {"id": 6697, "synset": "coil.n.06", "name": "coil"}, + {"id": 6698, "synset": "coil.n.03", "name": "coil"}, + {"id": 6699, "synset": "coil_spring.n.01", "name": "coil_spring"}, + {"id": 6700, "synset": "coin_box.n.01", "name": "coin_box"}, + {"id": 6701, "synset": "cold_cathode.n.01", "name": "cold_cathode"}, + {"id": 6702, "synset": "cold_chisel.n.01", "name": "cold_chisel"}, + {"id": 6703, "synset": "cold_cream.n.01", "name": "cold_cream"}, + {"id": 6704, "synset": "cold_frame.n.01", "name": "cold_frame"}, + {"id": 6705, "synset": "collar.n.01", "name": "collar"}, + {"id": 6706, "synset": "collar.n.03", "name": "collar"}, + {"id": 6707, "synset": "college.n.03", "name": "college"}, + {"id": 6708, "synset": "collet.n.02", "name": "collet"}, + {"id": 6709, "synset": "collider.n.01", "name": "collider"}, + {"id": 6710, "synset": "colliery.n.01", "name": "colliery"}, + {"id": 6711, "synset": "collimator.n.02", "name": "collimator"}, + {"id": 6712, "synset": "collimator.n.01", "name": "collimator"}, + {"id": 6713, "synset": "cologne.n.02", "name": "cologne"}, + {"id": 6714, "synset": "colonnade.n.01", "name": "colonnade"}, + {"id": 6715, "synset": "colonoscope.n.01", "name": "colonoscope"}, + {"id": 6716, "synset": "colorimeter.n.01", "name": "colorimeter"}, + {"id": 6717, "synset": "colors.n.02", "name": "colors"}, + {"id": 6718, "synset": "color_television.n.01", "name": "color_television"}, + {"id": 6719, "synset": "color_tube.n.01", "name": "color_tube"}, + {"id": 6720, "synset": "color_wash.n.01", "name": "color_wash"}, + {"id": 6721, "synset": "colt.n.02", "name": "Colt"}, + {"id": 6722, "synset": "colter.n.01", "name": "colter"}, + {"id": 6723, "synset": "columbarium.n.03", "name": "columbarium"}, + {"id": 6724, "synset": "columbarium.n.02", "name": "columbarium"}, + {"id": 6725, "synset": "column.n.07", "name": "column"}, + {"id": 6726, "synset": "column.n.06", "name": "column"}, + {"id": 6727, "synset": "comb.n.01", "name": "comb"}, + {"id": 6728, "synset": "comb.n.03", "name": "comb"}, + {"id": 6729, "synset": "comber.n.03", "name": "comber"}, + {"id": 6730, "synset": "combination_plane.n.01", "name": "combination_plane"}, + {"id": 6731, "synset": "combine.n.01", "name": "combine"}, + {"id": 6732, "synset": "command_module.n.01", "name": "command_module"}, + {"id": 6733, "synset": "commissary.n.01", "name": "commissary"}, + {"id": 6734, "synset": "commissary.n.02", "name": "commissary"}, + {"id": 6735, "synset": "commodity.n.01", "name": "commodity"}, + {"id": 6736, "synset": "common_ax.n.01", "name": "common_ax"}, + {"id": 6737, "synset": "common_room.n.01", "name": "common_room"}, + {"id": 6738, "synset": "communications_satellite.n.01", "name": "communications_satellite"}, + {"id": 6739, "synset": "communication_system.n.01", "name": "communication_system"}, + {"id": 6740, "synset": "community_center.n.01", "name": "community_center"}, + {"id": 6741, "synset": "commutator.n.01", "name": "commutator"}, + {"id": 6742, "synset": "commuter.n.01", "name": "commuter"}, + {"id": 6743, "synset": "compact.n.01", "name": "compact"}, + {"id": 6744, "synset": "compact.n.03", "name": "compact"}, + {"id": 6745, "synset": "compact_disk.n.01", "name": "compact_disk"}, + {"id": 6746, "synset": "compact-disk_burner.n.01", "name": "compact-disk_burner"}, + {"id": 6747, "synset": "companionway.n.01", "name": "companionway"}, + {"id": 6748, "synset": "compartment.n.02", "name": "compartment"}, + {"id": 6749, "synset": "compartment.n.01", "name": "compartment"}, + {"id": 6750, "synset": "compass.n.04", "name": "compass"}, + {"id": 6751, "synset": "compass_card.n.01", "name": "compass_card"}, + {"id": 6752, "synset": "compass_saw.n.01", "name": "compass_saw"}, + {"id": 6753, "synset": "compound.n.03", "name": "compound"}, + {"id": 6754, "synset": "compound_lens.n.01", "name": "compound_lens"}, + {"id": 6755, "synset": "compound_lever.n.01", "name": "compound_lever"}, + {"id": 6756, "synset": "compound_microscope.n.01", "name": "compound_microscope"}, + {"id": 6757, "synset": "compress.n.01", "name": "compress"}, + {"id": 6758, "synset": "compression_bandage.n.01", "name": "compression_bandage"}, + {"id": 6759, "synset": "compressor.n.01", "name": "compressor"}, + {"id": 6760, "synset": "computer.n.01", "name": "computer"}, + {"id": 6761, "synset": "computer_circuit.n.01", "name": "computer_circuit"}, + { + "id": 6762, + "synset": "computerized_axial_tomography_scanner.n.01", + "name": "computerized_axial_tomography_scanner", + }, + {"id": 6763, "synset": "computer_monitor.n.01", "name": "computer_monitor"}, + {"id": 6764, "synset": "computer_network.n.01", "name": "computer_network"}, + {"id": 6765, "synset": "computer_screen.n.01", "name": "computer_screen"}, + {"id": 6766, "synset": "computer_store.n.01", "name": "computer_store"}, + {"id": 6767, "synset": "computer_system.n.01", "name": "computer_system"}, + {"id": 6768, "synset": "concentration_camp.n.01", "name": "concentration_camp"}, + {"id": 6769, "synset": "concert_grand.n.01", "name": "concert_grand"}, + {"id": 6770, "synset": "concert_hall.n.01", "name": "concert_hall"}, + {"id": 6771, "synset": "concertina.n.02", "name": "concertina"}, + {"id": 6772, "synset": "concertina.n.01", "name": "concertina"}, + {"id": 6773, "synset": "concrete_mixer.n.01", "name": "concrete_mixer"}, + {"id": 6774, "synset": "condensation_pump.n.01", "name": "condensation_pump"}, + {"id": 6775, "synset": "condenser.n.04", "name": "condenser"}, + {"id": 6776, "synset": "condenser.n.03", "name": "condenser"}, + {"id": 6777, "synset": "condenser.n.02", "name": "condenser"}, + {"id": 6778, "synset": "condenser_microphone.n.01", "name": "condenser_microphone"}, + {"id": 6779, "synset": "condominium.n.02", "name": "condominium"}, + {"id": 6780, "synset": "condominium.n.01", "name": "condominium"}, + {"id": 6781, "synset": "conductor.n.04", "name": "conductor"}, + {"id": 6782, "synset": "cone_clutch.n.01", "name": "cone_clutch"}, + {"id": 6783, "synset": "confectionery.n.02", "name": "confectionery"}, + {"id": 6784, "synset": "conference_center.n.01", "name": "conference_center"}, + {"id": 6785, "synset": "conference_room.n.01", "name": "conference_room"}, + {"id": 6786, "synset": "conference_table.n.01", "name": "conference_table"}, + {"id": 6787, "synset": "confessional.n.01", "name": "confessional"}, + {"id": 6788, "synset": "conformal_projection.n.01", "name": "conformal_projection"}, + {"id": 6789, "synset": "congress_boot.n.01", "name": "congress_boot"}, + {"id": 6790, "synset": "conic_projection.n.01", "name": "conic_projection"}, + {"id": 6791, "synset": "connecting_rod.n.01", "name": "connecting_rod"}, + {"id": 6792, "synset": "connecting_room.n.01", "name": "connecting_room"}, + {"id": 6793, "synset": "connection.n.03", "name": "connection"}, + {"id": 6794, "synset": "conning_tower.n.02", "name": "conning_tower"}, + {"id": 6795, "synset": "conning_tower.n.01", "name": "conning_tower"}, + {"id": 6796, "synset": "conservatory.n.03", "name": "conservatory"}, + {"id": 6797, "synset": "conservatory.n.02", "name": "conservatory"}, + {"id": 6798, "synset": "console.n.03", "name": "console"}, + {"id": 6799, "synset": "console.n.02", "name": "console"}, + {"id": 6800, "synset": "console_table.n.01", "name": "console_table"}, + {"id": 6801, "synset": "consulate.n.01", "name": "consulate"}, + {"id": 6802, "synset": "contact.n.07", "name": "contact"}, + {"id": 6803, "synset": "contact.n.09", "name": "contact"}, + {"id": 6804, "synset": "container.n.01", "name": "container"}, + {"id": 6805, "synset": "container_ship.n.01", "name": "container_ship"}, + {"id": 6806, "synset": "containment.n.02", "name": "containment"}, + {"id": 6807, "synset": "contrabassoon.n.01", "name": "contrabassoon"}, + {"id": 6808, "synset": "control_center.n.01", "name": "control_center"}, + {"id": 6809, "synset": "control_circuit.n.01", "name": "control_circuit"}, + {"id": 6810, "synset": "control_key.n.01", "name": "control_key"}, + {"id": 6811, "synset": "control_panel.n.01", "name": "control_panel"}, + {"id": 6812, "synset": "control_rod.n.01", "name": "control_rod"}, + {"id": 6813, "synset": "control_room.n.01", "name": "control_room"}, + {"id": 6814, "synset": "control_system.n.01", "name": "control_system"}, + {"id": 6815, "synset": "control_tower.n.01", "name": "control_tower"}, + {"id": 6816, "synset": "convector.n.01", "name": "convector"}, + {"id": 6817, "synset": "convenience_store.n.01", "name": "convenience_store"}, + {"id": 6818, "synset": "convent.n.01", "name": "convent"}, + {"id": 6819, "synset": "conventicle.n.02", "name": "conventicle"}, + {"id": 6820, "synset": "converging_lens.n.01", "name": "converging_lens"}, + {"id": 6821, "synset": "converter.n.01", "name": "converter"}, + {"id": 6822, "synset": "conveyance.n.03", "name": "conveyance"}, + {"id": 6823, "synset": "conveyer_belt.n.01", "name": "conveyer_belt"}, + {"id": 6824, "synset": "cookfire.n.01", "name": "cookfire"}, + {"id": 6825, "synset": "cookhouse.n.02", "name": "cookhouse"}, + {"id": 6826, "synset": "cookie_cutter.n.01", "name": "cookie_cutter"}, + {"id": 6827, "synset": "cookie_jar.n.01", "name": "cookie_jar"}, + {"id": 6828, "synset": "cookie_sheet.n.01", "name": "cookie_sheet"}, + {"id": 6829, "synset": "cookstove.n.01", "name": "cookstove"}, + {"id": 6830, "synset": "coolant_system.n.01", "name": "coolant_system"}, + {"id": 6831, "synset": "cooling_system.n.02", "name": "cooling_system"}, + {"id": 6832, "synset": "cooling_system.n.01", "name": "cooling_system"}, + {"id": 6833, "synset": "cooling_tower.n.01", "name": "cooling_tower"}, + {"id": 6834, "synset": "coonskin_cap.n.01", "name": "coonskin_cap"}, + {"id": 6835, "synset": "cope.n.02", "name": "cope"}, + {"id": 6836, "synset": "coping_saw.n.01", "name": "coping_saw"}, + {"id": 6837, "synset": "copperware.n.01", "name": "copperware"}, + {"id": 6838, "synset": "copyholder.n.01", "name": "copyholder"}, + {"id": 6839, "synset": "coquille.n.02", "name": "coquille"}, + {"id": 6840, "synset": "coracle.n.01", "name": "coracle"}, + {"id": 6841, "synset": "corbel.n.01", "name": "corbel"}, + {"id": 6842, "synset": "corbel_arch.n.01", "name": "corbel_arch"}, + {"id": 6843, "synset": "corbel_step.n.01", "name": "corbel_step"}, + {"id": 6844, "synset": "corbie_gable.n.01", "name": "corbie_gable"}, + {"id": 6845, "synset": "cord.n.04", "name": "cord"}, + {"id": 6846, "synset": "cord.n.03", "name": "cord"}, + {"id": 6847, "synset": "cordage.n.02", "name": "cordage"}, + {"id": 6848, "synset": "cords.n.01", "name": "cords"}, + {"id": 6849, "synset": "core.n.10", "name": "core"}, + {"id": 6850, "synset": "core_bit.n.01", "name": "core_bit"}, + {"id": 6851, "synset": "core_drill.n.01", "name": "core_drill"}, + {"id": 6852, "synset": "corer.n.01", "name": "corer"}, + {"id": 6853, "synset": "corker.n.02", "name": "corker"}, + {"id": 6854, "synset": "corncrib.n.01", "name": "corncrib"}, + {"id": 6855, "synset": "corner.n.11", "name": "corner"}, + {"id": 6856, "synset": "corner.n.03", "name": "corner"}, + {"id": 6857, "synset": "corner_post.n.01", "name": "corner_post"}, + {"id": 6858, "synset": "cornice.n.03", "name": "cornice"}, + {"id": 6859, "synset": "cornice.n.02", "name": "cornice"}, + {"id": 6860, "synset": "correctional_institution.n.01", "name": "correctional_institution"}, + {"id": 6861, "synset": "corrugated_fastener.n.01", "name": "corrugated_fastener"}, + {"id": 6862, "synset": "corselet.n.01", "name": "corselet"}, + {"id": 6863, "synset": "cosmetic.n.01", "name": "cosmetic"}, + {"id": 6864, "synset": "cosmotron.n.01", "name": "cosmotron"}, + {"id": 6865, "synset": "costume.n.01", "name": "costume"}, + {"id": 6866, "synset": "costume.n.02", "name": "costume"}, + {"id": 6867, "synset": "costume.n.03", "name": "costume"}, + {"id": 6868, "synset": "cosy.n.01", "name": "cosy"}, + {"id": 6869, "synset": "cot.n.03", "name": "cot"}, + {"id": 6870, "synset": "cottage_tent.n.01", "name": "cottage_tent"}, + {"id": 6871, "synset": "cotter.n.03", "name": "cotter"}, + {"id": 6872, "synset": "cotter_pin.n.01", "name": "cotter_pin"}, + {"id": 6873, "synset": "cotton.n.02", "name": "cotton"}, + {"id": 6874, "synset": "cotton_flannel.n.01", "name": "cotton_flannel"}, + {"id": 6875, "synset": "cotton_mill.n.01", "name": "cotton_mill"}, + {"id": 6876, "synset": "couch.n.03", "name": "couch"}, + {"id": 6877, "synset": "couch.n.02", "name": "couch"}, + {"id": 6878, "synset": "couchette.n.01", "name": "couchette"}, + {"id": 6879, "synset": "coude_telescope.n.01", "name": "coude_telescope"}, + {"id": 6880, "synset": "counter.n.01", "name": "counter"}, + {"id": 6881, "synset": "counter.n.03", "name": "counter"}, + {"id": 6882, "synset": "counter.n.02", "name": "counter"}, + {"id": 6883, "synset": "counterbore.n.01", "name": "counterbore"}, + {"id": 6884, "synset": "counter_tube.n.01", "name": "counter_tube"}, + {"id": 6885, "synset": "country_house.n.01", "name": "country_house"}, + {"id": 6886, "synset": "country_store.n.01", "name": "country_store"}, + {"id": 6887, "synset": "coupe.n.01", "name": "coupe"}, + {"id": 6888, "synset": "coupling.n.02", "name": "coupling"}, + {"id": 6889, "synset": "court.n.10", "name": "court"}, + {"id": 6890, "synset": "court.n.04", "name": "court"}, + {"id": 6891, "synset": "court.n.02", "name": "court"}, + {"id": 6892, "synset": "court.n.09", "name": "court"}, + {"id": 6893, "synset": "courtelle.n.01", "name": "Courtelle"}, + {"id": 6894, "synset": "courthouse.n.02", "name": "courthouse"}, + {"id": 6895, "synset": "courthouse.n.01", "name": "courthouse"}, + {"id": 6896, "synset": "covered_bridge.n.01", "name": "covered_bridge"}, + {"id": 6897, "synset": "covered_couch.n.01", "name": "covered_couch"}, + {"id": 6898, "synset": "covered_wagon.n.01", "name": "covered_wagon"}, + {"id": 6899, "synset": "covering.n.02", "name": "covering"}, + {"id": 6900, "synset": "coverlet.n.01", "name": "coverlet"}, + {"id": 6901, "synset": "cover_plate.n.01", "name": "cover_plate"}, + {"id": 6902, "synset": "cowbarn.n.01", "name": "cowbarn"}, + {"id": 6903, "synset": "cowboy_boot.n.01", "name": "cowboy_boot"}, + {"id": 6904, "synset": "cowhide.n.03", "name": "cowhide"}, + {"id": 6905, "synset": "cowl.n.02", "name": "cowl"}, + {"id": 6906, "synset": "cow_pen.n.01", "name": "cow_pen"}, + {"id": 6907, "synset": "cpu_board.n.01", "name": "CPU_board"}, + {"id": 6908, "synset": "crackle.n.02", "name": "crackle"}, + {"id": 6909, "synset": "cradle.n.01", "name": "cradle"}, + {"id": 6910, "synset": "craft.n.02", "name": "craft"}, + {"id": 6911, "synset": "cramp.n.03", "name": "cramp"}, + {"id": 6912, "synset": "crampon.n.02", "name": "crampon"}, + {"id": 6913, "synset": "crampon.n.01", "name": "crampon"}, + {"id": 6914, "synset": "crane.n.04", "name": "crane"}, + {"id": 6915, "synset": "craniometer.n.01", "name": "craniometer"}, + {"id": 6916, "synset": "crank.n.04", "name": "crank"}, + {"id": 6917, "synset": "crankcase.n.01", "name": "crankcase"}, + {"id": 6918, "synset": "crankshaft.n.01", "name": "crankshaft"}, + {"id": 6919, "synset": "crash_barrier.n.01", "name": "crash_barrier"}, + {"id": 6920, "synset": "crash_helmet.n.01", "name": "crash_helmet"}, + {"id": 6921, "synset": "cravat.n.01", "name": "cravat"}, + {"id": 6922, "synset": "crazy_quilt.n.01", "name": "crazy_quilt"}, + {"id": 6923, "synset": "cream.n.03", "name": "cream"}, + {"id": 6924, "synset": "creche.n.01", "name": "creche"}, + {"id": 6925, "synset": "creche.n.02", "name": "creche"}, + {"id": 6926, "synset": "credenza.n.01", "name": "credenza"}, + {"id": 6927, "synset": "creel.n.01", "name": "creel"}, + {"id": 6928, "synset": "crematory.n.02", "name": "crematory"}, + {"id": 6929, "synset": "crematory.n.01", "name": "crematory"}, + {"id": 6930, "synset": "crepe.n.03", "name": "crepe"}, + {"id": 6931, "synset": "crepe_de_chine.n.01", "name": "crepe_de_Chine"}, + {"id": 6932, "synset": "crescent_wrench.n.01", "name": "crescent_wrench"}, + {"id": 6933, "synset": "cretonne.n.01", "name": "cretonne"}, + {"id": 6934, "synset": "crib.n.03", "name": "crib"}, + {"id": 6935, "synset": "cricket_ball.n.01", "name": "cricket_ball"}, + {"id": 6936, "synset": "cricket_bat.n.01", "name": "cricket_bat"}, + {"id": 6937, "synset": "cricket_equipment.n.01", "name": "cricket_equipment"}, + {"id": 6938, "synset": "cringle.n.01", "name": "cringle"}, + {"id": 6939, "synset": "crinoline.n.03", "name": "crinoline"}, + {"id": 6940, "synset": "crinoline.n.02", "name": "crinoline"}, + {"id": 6941, "synset": "crochet_needle.n.01", "name": "crochet_needle"}, + {"id": 6942, "synset": "crock_pot.n.01", "name": "Crock_Pot"}, + {"id": 6943, "synset": "crook.n.03", "name": "crook"}, + {"id": 6944, "synset": "crookes_radiometer.n.01", "name": "Crookes_radiometer"}, + {"id": 6945, "synset": "crookes_tube.n.01", "name": "Crookes_tube"}, + {"id": 6946, "synset": "croquet_ball.n.01", "name": "croquet_ball"}, + {"id": 6947, "synset": "croquet_equipment.n.01", "name": "croquet_equipment"}, + {"id": 6948, "synset": "croquet_mallet.n.01", "name": "croquet_mallet"}, + {"id": 6949, "synset": "cross.n.01", "name": "cross"}, + {"id": 6950, "synset": "crossbar.n.03", "name": "crossbar"}, + {"id": 6951, "synset": "crossbar.n.02", "name": "crossbar"}, + {"id": 6952, "synset": "crossbench.n.01", "name": "crossbench"}, + {"id": 6953, "synset": "cross_bit.n.01", "name": "cross_bit"}, + {"id": 6954, "synset": "crossbow.n.01", "name": "crossbow"}, + {"id": 6955, "synset": "crosscut_saw.n.01", "name": "crosscut_saw"}, + {"id": 6956, "synset": "crossjack.n.01", "name": "crossjack"}, + {"id": 6957, "synset": "crosspiece.n.02", "name": "crosspiece"}, + {"id": 6958, "synset": "crotchet.n.04", "name": "crotchet"}, + {"id": 6959, "synset": "croupier's_rake.n.01", "name": "croupier's_rake"}, + {"id": 6960, "synset": "crown.n.11", "name": "crown"}, + {"id": 6961, "synset": "crown_jewels.n.01", "name": "crown_jewels"}, + {"id": 6962, "synset": "crown_lens.n.01", "name": "crown_lens"}, + {"id": 6963, "synset": "crow's_nest.n.01", "name": "crow's_nest"}, + {"id": 6964, "synset": "crucible.n.01", "name": "crucible"}, + {"id": 6965, "synset": "cruet.n.01", "name": "cruet"}, + {"id": 6966, "synset": "cruet-stand.n.01", "name": "cruet-stand"}, + {"id": 6967, "synset": "cruise_control.n.01", "name": "cruise_control"}, + {"id": 6968, "synset": "cruise_missile.n.01", "name": "cruise_missile"}, + {"id": 6969, "synset": "cruiser.n.02", "name": "cruiser"}, + {"id": 6970, "synset": "crupper.n.01", "name": "crupper"}, + {"id": 6971, "synset": "cruse.n.01", "name": "cruse"}, + {"id": 6972, "synset": "crusher.n.01", "name": "crusher"}, + {"id": 6973, "synset": "cryometer.n.01", "name": "cryometer"}, + {"id": 6974, "synset": "cryoscope.n.01", "name": "cryoscope"}, + {"id": 6975, "synset": "cryostat.n.01", "name": "cryostat"}, + {"id": 6976, "synset": "crypt.n.01", "name": "crypt"}, + {"id": 6977, "synset": "crystal.n.06", "name": "crystal"}, + {"id": 6978, "synset": "crystal_detector.n.01", "name": "crystal_detector"}, + {"id": 6979, "synset": "crystal_microphone.n.01", "name": "crystal_microphone"}, + {"id": 6980, "synset": "crystal_oscillator.n.01", "name": "crystal_oscillator"}, + {"id": 6981, "synset": "crystal_set.n.01", "name": "crystal_set"}, + {"id": 6982, "synset": "cubitiere.n.01", "name": "cubitiere"}, + {"id": 6983, "synset": "cucking_stool.n.01", "name": "cucking_stool"}, + {"id": 6984, "synset": "cuckoo_clock.n.01", "name": "cuckoo_clock"}, + {"id": 6985, "synset": "cuddy.n.01", "name": "cuddy"}, + {"id": 6986, "synset": "cudgel.n.01", "name": "cudgel"}, + {"id": 6987, "synset": "cue.n.04", "name": "cue"}, + {"id": 6988, "synset": "cue_ball.n.01", "name": "cue_ball"}, + {"id": 6989, "synset": "cuff.n.01", "name": "cuff"}, + {"id": 6990, "synset": "cuirass.n.01", "name": "cuirass"}, + {"id": 6991, "synset": "cuisse.n.01", "name": "cuisse"}, + {"id": 6992, "synset": "cul.n.01", "name": "cul"}, + {"id": 6993, "synset": "culdoscope.n.01", "name": "culdoscope"}, + {"id": 6994, "synset": "cullis.n.01", "name": "cullis"}, + {"id": 6995, "synset": "culotte.n.01", "name": "culotte"}, + {"id": 6996, "synset": "cultivator.n.02", "name": "cultivator"}, + {"id": 6997, "synset": "culverin.n.02", "name": "culverin"}, + {"id": 6998, "synset": "culverin.n.01", "name": "culverin"}, + {"id": 6999, "synset": "culvert.n.01", "name": "culvert"}, + {"id": 7000, "synset": "cup_hook.n.01", "name": "cup_hook"}, + {"id": 7001, "synset": "cupola.n.02", "name": "cupola"}, + {"id": 7002, "synset": "cupola.n.01", "name": "cupola"}, + {"id": 7003, "synset": "curb.n.02", "name": "curb"}, + {"id": 7004, "synset": "curb_roof.n.01", "name": "curb_roof"}, + {"id": 7005, "synset": "curbstone.n.01", "name": "curbstone"}, + {"id": 7006, "synset": "curette.n.01", "name": "curette"}, + {"id": 7007, "synset": "currycomb.n.01", "name": "currycomb"}, + {"id": 7008, "synset": "cursor.n.01", "name": "cursor"}, + {"id": 7009, "synset": "customhouse.n.01", "name": "customhouse"}, + {"id": 7010, "synset": "cutaway.n.01", "name": "cutaway"}, + {"id": 7011, "synset": "cutlas.n.01", "name": "cutlas"}, + {"id": 7012, "synset": "cutoff.n.03", "name": "cutoff"}, + {"id": 7013, "synset": "cutout.n.01", "name": "cutout"}, + {"id": 7014, "synset": "cutter.n.06", "name": "cutter"}, + {"id": 7015, "synset": "cutter.n.05", "name": "cutter"}, + {"id": 7016, "synset": "cutting_implement.n.01", "name": "cutting_implement"}, + {"id": 7017, "synset": "cutting_room.n.01", "name": "cutting_room"}, + {"id": 7018, "synset": "cutty_stool.n.01", "name": "cutty_stool"}, + {"id": 7019, "synset": "cutwork.n.01", "name": "cutwork"}, + {"id": 7020, "synset": "cybercafe.n.01", "name": "cybercafe"}, + {"id": 7021, "synset": "cyclopean_masonry.n.01", "name": "cyclopean_masonry"}, + {"id": 7022, "synset": "cyclostyle.n.01", "name": "cyclostyle"}, + {"id": 7023, "synset": "cyclotron.n.01", "name": "cyclotron"}, + {"id": 7024, "synset": "cylinder.n.03", "name": "cylinder"}, + {"id": 7025, "synset": "cylinder_lock.n.01", "name": "cylinder_lock"}, + {"id": 7026, "synset": "dacha.n.01", "name": "dacha"}, + {"id": 7027, "synset": "dacron.n.01", "name": "Dacron"}, + {"id": 7028, "synset": "dado.n.02", "name": "dado"}, + {"id": 7029, "synset": "dado_plane.n.01", "name": "dado_plane"}, + {"id": 7030, "synset": "dairy.n.01", "name": "dairy"}, + {"id": 7031, "synset": "dais.n.01", "name": "dais"}, + {"id": 7032, "synset": "daisy_print_wheel.n.01", "name": "daisy_print_wheel"}, + {"id": 7033, "synset": "daisywheel_printer.n.01", "name": "daisywheel_printer"}, + {"id": 7034, "synset": "dam.n.01", "name": "dam"}, + {"id": 7035, "synset": "damask.n.02", "name": "damask"}, + {"id": 7036, "synset": "dampener.n.01", "name": "dampener"}, + {"id": 7037, "synset": "damper.n.02", "name": "damper"}, + {"id": 7038, "synset": "damper_block.n.01", "name": "damper_block"}, + {"id": 7039, "synset": "dark_lantern.n.01", "name": "dark_lantern"}, + {"id": 7040, "synset": "darkroom.n.01", "name": "darkroom"}, + {"id": 7041, "synset": "darning_needle.n.01", "name": "darning_needle"}, + {"id": 7042, "synset": "dart.n.02", "name": "dart"}, + {"id": 7043, "synset": "dart.n.01", "name": "dart"}, + {"id": 7044, "synset": "dashboard.n.02", "name": "dashboard"}, + {"id": 7045, "synset": "dashiki.n.01", "name": "dashiki"}, + {"id": 7046, "synset": "dash-pot.n.01", "name": "dash-pot"}, + {"id": 7047, "synset": "data_converter.n.01", "name": "data_converter"}, + {"id": 7048, "synset": "data_input_device.n.01", "name": "data_input_device"}, + {"id": 7049, "synset": "data_multiplexer.n.01", "name": "data_multiplexer"}, + {"id": 7050, "synset": "data_system.n.01", "name": "data_system"}, + {"id": 7051, "synset": "davenport.n.03", "name": "davenport"}, + {"id": 7052, "synset": "davenport.n.02", "name": "davenport"}, + {"id": 7053, "synset": "davit.n.01", "name": "davit"}, + {"id": 7054, "synset": "daybed.n.01", "name": "daybed"}, + {"id": 7055, "synset": "daybook.n.02", "name": "daybook"}, + {"id": 7056, "synset": "day_nursery.n.01", "name": "day_nursery"}, + {"id": 7057, "synset": "day_school.n.03", "name": "day_school"}, + {"id": 7058, "synset": "dead_axle.n.01", "name": "dead_axle"}, + {"id": 7059, "synset": "deadeye.n.02", "name": "deadeye"}, + {"id": 7060, "synset": "deadhead.n.02", "name": "deadhead"}, + {"id": 7061, "synset": "deanery.n.01", "name": "deanery"}, + {"id": 7062, "synset": "deathbed.n.02", "name": "deathbed"}, + {"id": 7063, "synset": "death_camp.n.01", "name": "death_camp"}, + {"id": 7064, "synset": "death_house.n.01", "name": "death_house"}, + {"id": 7065, "synset": "death_knell.n.02", "name": "death_knell"}, + {"id": 7066, "synset": "death_seat.n.01", "name": "death_seat"}, + {"id": 7067, "synset": "deck.n.02", "name": "deck"}, + {"id": 7068, "synset": "deck.n.04", "name": "deck"}, + {"id": 7069, "synset": "deck-house.n.01", "name": "deck-house"}, + {"id": 7070, "synset": "deckle.n.02", "name": "deckle"}, + {"id": 7071, "synset": "deckle_edge.n.01", "name": "deckle_edge"}, + {"id": 7072, "synset": "declinometer.n.01", "name": "declinometer"}, + {"id": 7073, "synset": "decoder.n.02", "name": "decoder"}, + {"id": 7074, "synset": "decolletage.n.01", "name": "decolletage"}, + {"id": 7075, "synset": "decoupage.n.01", "name": "decoupage"}, + {"id": 7076, "synset": "dedicated_file_server.n.01", "name": "dedicated_file_server"}, + {"id": 7077, "synset": "deep-freeze.n.01", "name": "deep-freeze"}, + {"id": 7078, "synset": "deerstalker.n.01", "name": "deerstalker"}, + {"id": 7079, "synset": "defense_system.n.01", "name": "defense_system"}, + {"id": 7080, "synset": "defensive_structure.n.01", "name": "defensive_structure"}, + {"id": 7081, "synset": "defibrillator.n.01", "name": "defibrillator"}, + {"id": 7082, "synset": "defilade.n.01", "name": "defilade"}, + {"id": 7083, "synset": "deflector.n.01", "name": "deflector"}, + {"id": 7084, "synset": "delayed_action.n.01", "name": "delayed_action"}, + {"id": 7085, "synset": "delay_line.n.01", "name": "delay_line"}, + {"id": 7086, "synset": "delft.n.01", "name": "delft"}, + {"id": 7087, "synset": "delicatessen.n.02", "name": "delicatessen"}, + {"id": 7088, "synset": "delivery_truck.n.01", "name": "delivery_truck"}, + {"id": 7089, "synset": "delta_wing.n.01", "name": "delta_wing"}, + {"id": 7090, "synset": "demijohn.n.01", "name": "demijohn"}, + {"id": 7091, "synset": "demitasse.n.02", "name": "demitasse"}, + {"id": 7092, "synset": "den.n.04", "name": "den"}, + {"id": 7093, "synset": "denim.n.02", "name": "denim"}, + {"id": 7094, "synset": "densimeter.n.01", "name": "densimeter"}, + {"id": 7095, "synset": "densitometer.n.01", "name": "densitometer"}, + {"id": 7096, "synset": "dental_appliance.n.01", "name": "dental_appliance"}, + {"id": 7097, "synset": "dental_implant.n.01", "name": "dental_implant"}, + {"id": 7098, "synset": "dentist's_drill.n.01", "name": "dentist's_drill"}, + {"id": 7099, "synset": "denture.n.01", "name": "denture"}, + {"id": 7100, "synset": "deodorant.n.01", "name": "deodorant"}, + {"id": 7101, "synset": "department_store.n.01", "name": "department_store"}, + {"id": 7102, "synset": "departure_lounge.n.01", "name": "departure_lounge"}, + {"id": 7103, "synset": "depilatory.n.02", "name": "depilatory"}, + {"id": 7104, "synset": "depressor.n.03", "name": "depressor"}, + {"id": 7105, "synset": "depth_finder.n.01", "name": "depth_finder"}, + {"id": 7106, "synset": "depth_gauge.n.01", "name": "depth_gauge"}, + {"id": 7107, "synset": "derrick.n.02", "name": "derrick"}, + {"id": 7108, "synset": "derrick.n.01", "name": "derrick"}, + {"id": 7109, "synset": "derringer.n.01", "name": "derringer"}, + {"id": 7110, "synset": "desk_phone.n.01", "name": "desk_phone"}, + {"id": 7111, "synset": "desktop_computer.n.01", "name": "desktop_computer"}, + {"id": 7112, "synset": "dessert_spoon.n.01", "name": "dessert_spoon"}, + {"id": 7113, "synset": "destroyer.n.01", "name": "destroyer"}, + {"id": 7114, "synset": "destroyer_escort.n.01", "name": "destroyer_escort"}, + {"id": 7115, "synset": "detached_house.n.01", "name": "detached_house"}, + {"id": 7116, "synset": "detector.n.01", "name": "detector"}, + {"id": 7117, "synset": "detector.n.03", "name": "detector"}, + {"id": 7118, "synset": "detention_home.n.01", "name": "detention_home"}, + {"id": 7119, "synset": "detonating_fuse.n.01", "name": "detonating_fuse"}, + {"id": 7120, "synset": "detonator.n.01", "name": "detonator"}, + {"id": 7121, "synset": "developer.n.02", "name": "developer"}, + {"id": 7122, "synset": "device.n.01", "name": "device"}, + {"id": 7123, "synset": "dewar_flask.n.01", "name": "Dewar_flask"}, + {"id": 7124, "synset": "dhoti.n.01", "name": "dhoti"}, + {"id": 7125, "synset": "dhow.n.01", "name": "dhow"}, + {"id": 7126, "synset": "dial.n.04", "name": "dial"}, + {"id": 7127, "synset": "dial.n.03", "name": "dial"}, + {"id": 7128, "synset": "dial.n.02", "name": "dial"}, + {"id": 7129, "synset": "dialog_box.n.01", "name": "dialog_box"}, + {"id": 7130, "synset": "dial_telephone.n.01", "name": "dial_telephone"}, + {"id": 7131, "synset": "dialyzer.n.01", "name": "dialyzer"}, + {"id": 7132, "synset": "diamante.n.02", "name": "diamante"}, + {"id": 7133, "synset": "diaper.n.02", "name": "diaper"}, + {"id": 7134, "synset": "diaphone.n.01", "name": "diaphone"}, + {"id": 7135, "synset": "diaphragm.n.01", "name": "diaphragm"}, + {"id": 7136, "synset": "diaphragm.n.04", "name": "diaphragm"}, + {"id": 7137, "synset": "diathermy_machine.n.01", "name": "diathermy_machine"}, + {"id": 7138, "synset": "dibble.n.01", "name": "dibble"}, + {"id": 7139, "synset": "dice_cup.n.01", "name": "dice_cup"}, + {"id": 7140, "synset": "dicer.n.01", "name": "dicer"}, + {"id": 7141, "synset": "dickey.n.02", "name": "dickey"}, + {"id": 7142, "synset": "dickey.n.01", "name": "dickey"}, + {"id": 7143, "synset": "dictaphone.n.01", "name": "Dictaphone"}, + {"id": 7144, "synset": "die.n.03", "name": "die"}, + {"id": 7145, "synset": "diesel.n.02", "name": "diesel"}, + {"id": 7146, "synset": "diesel-electric_locomotive.n.01", "name": "diesel-electric_locomotive"}, + { + "id": 7147, + "synset": "diesel-hydraulic_locomotive.n.01", + "name": "diesel-hydraulic_locomotive", + }, + {"id": 7148, "synset": "diesel_locomotive.n.01", "name": "diesel_locomotive"}, + {"id": 7149, "synset": "diestock.n.01", "name": "diestock"}, + {"id": 7150, "synset": "differential_analyzer.n.01", "name": "differential_analyzer"}, + {"id": 7151, "synset": "differential_gear.n.01", "name": "differential_gear"}, + {"id": 7152, "synset": "diffuser.n.02", "name": "diffuser"}, + {"id": 7153, "synset": "diffuser.n.01", "name": "diffuser"}, + {"id": 7154, "synset": "digester.n.01", "name": "digester"}, + {"id": 7155, "synset": "diggings.n.02", "name": "diggings"}, + {"id": 7156, "synset": "digital-analog_converter.n.01", "name": "digital-analog_converter"}, + {"id": 7157, "synset": "digital_audiotape.n.01", "name": "digital_audiotape"}, + {"id": 7158, "synset": "digital_camera.n.01", "name": "digital_camera"}, + {"id": 7159, "synset": "digital_clock.n.01", "name": "digital_clock"}, + {"id": 7160, "synset": "digital_computer.n.01", "name": "digital_computer"}, + {"id": 7161, "synset": "digital_display.n.01", "name": "digital_display"}, + {"id": 7162, "synset": "digital_subscriber_line.n.01", "name": "digital_subscriber_line"}, + {"id": 7163, "synset": "digital_voltmeter.n.01", "name": "digital_voltmeter"}, + {"id": 7164, "synset": "digital_watch.n.01", "name": "digital_watch"}, + {"id": 7165, "synset": "digitizer.n.01", "name": "digitizer"}, + {"id": 7166, "synset": "dilator.n.03", "name": "dilator"}, + {"id": 7167, "synset": "dildo.n.01", "name": "dildo"}, + {"id": 7168, "synset": "dimity.n.01", "name": "dimity"}, + {"id": 7169, "synset": "dimmer.n.01", "name": "dimmer"}, + {"id": 7170, "synset": "diner.n.03", "name": "diner"}, + {"id": 7171, "synset": "dinette.n.01", "name": "dinette"}, + {"id": 7172, "synset": "dining_area.n.01", "name": "dining_area"}, + {"id": 7173, "synset": "dining_car.n.01", "name": "dining_car"}, + {"id": 7174, "synset": "dining-hall.n.01", "name": "dining-hall"}, + {"id": 7175, "synset": "dining_room.n.01", "name": "dining_room"}, + {"id": 7176, "synset": "dining-room_furniture.n.01", "name": "dining-room_furniture"}, + {"id": 7177, "synset": "dining-room_table.n.01", "name": "dining-room_table"}, + {"id": 7178, "synset": "dinner_bell.n.01", "name": "dinner_bell"}, + {"id": 7179, "synset": "dinner_dress.n.01", "name": "dinner_dress"}, + {"id": 7180, "synset": "dinner_napkin.n.01", "name": "dinner_napkin"}, + {"id": 7181, "synset": "dinner_pail.n.01", "name": "dinner_pail"}, + {"id": 7182, "synset": "dinner_table.n.01", "name": "dinner_table"}, + {"id": 7183, "synset": "dinner_theater.n.01", "name": "dinner_theater"}, + {"id": 7184, "synset": "diode.n.02", "name": "diode"}, + {"id": 7185, "synset": "diode.n.01", "name": "diode"}, + {"id": 7186, "synset": "dip.n.07", "name": "dip"}, + {"id": 7187, "synset": "diplomatic_building.n.01", "name": "diplomatic_building"}, + {"id": 7188, "synset": "dipole.n.02", "name": "dipole"}, + {"id": 7189, "synset": "dipper.n.01", "name": "dipper"}, + {"id": 7190, "synset": "dipstick.n.01", "name": "dipstick"}, + {"id": 7191, "synset": "dip_switch.n.01", "name": "DIP_switch"}, + {"id": 7192, "synset": "directional_antenna.n.01", "name": "directional_antenna"}, + {"id": 7193, "synset": "directional_microphone.n.01", "name": "directional_microphone"}, + {"id": 7194, "synset": "direction_finder.n.01", "name": "direction_finder"}, + {"id": 7195, "synset": "dirk.n.01", "name": "dirk"}, + {"id": 7196, "synset": "dirndl.n.02", "name": "dirndl"}, + {"id": 7197, "synset": "dirndl.n.01", "name": "dirndl"}, + {"id": 7198, "synset": "dirty_bomb.n.01", "name": "dirty_bomb"}, + {"id": 7199, "synset": "discharge_lamp.n.01", "name": "discharge_lamp"}, + {"id": 7200, "synset": "discharge_pipe.n.01", "name": "discharge_pipe"}, + {"id": 7201, "synset": "disco.n.02", "name": "disco"}, + {"id": 7202, "synset": "discount_house.n.01", "name": "discount_house"}, + {"id": 7203, "synset": "discus.n.02", "name": "discus"}, + {"id": 7204, "synset": "disguise.n.02", "name": "disguise"}, + {"id": 7205, "synset": "dishpan.n.01", "name": "dishpan"}, + {"id": 7206, "synset": "dish_rack.n.01", "name": "dish_rack"}, + {"id": 7207, "synset": "disk.n.02", "name": "disk"}, + {"id": 7208, "synset": "disk_brake.n.01", "name": "disk_brake"}, + {"id": 7209, "synset": "disk_clutch.n.01", "name": "disk_clutch"}, + {"id": 7210, "synset": "disk_controller.n.01", "name": "disk_controller"}, + {"id": 7211, "synset": "disk_drive.n.01", "name": "disk_drive"}, + {"id": 7212, "synset": "diskette.n.01", "name": "diskette"}, + {"id": 7213, "synset": "disk_harrow.n.01", "name": "disk_harrow"}, + {"id": 7214, "synset": "dispatch_case.n.01", "name": "dispatch_case"}, + {"id": 7215, "synset": "dispensary.n.01", "name": "dispensary"}, + {"id": 7216, "synset": "display.n.06", "name": "display"}, + {"id": 7217, "synset": "display_adapter.n.01", "name": "display_adapter"}, + {"id": 7218, "synset": "display_panel.n.01", "name": "display_panel"}, + {"id": 7219, "synset": "display_window.n.01", "name": "display_window"}, + {"id": 7220, "synset": "disposal.n.04", "name": "disposal"}, + {"id": 7221, "synset": "disrupting_explosive.n.01", "name": "disrupting_explosive"}, + {"id": 7222, "synset": "distaff.n.02", "name": "distaff"}, + {"id": 7223, "synset": "distillery.n.01", "name": "distillery"}, + {"id": 7224, "synset": "distributor.n.04", "name": "distributor"}, + {"id": 7225, "synset": "distributor_cam.n.01", "name": "distributor_cam"}, + {"id": 7226, "synset": "distributor_cap.n.01", "name": "distributor_cap"}, + {"id": 7227, "synset": "distributor_housing.n.01", "name": "distributor_housing"}, + {"id": 7228, "synset": "distributor_point.n.01", "name": "distributor_point"}, + {"id": 7229, "synset": "ditch.n.01", "name": "ditch"}, + {"id": 7230, "synset": "ditch_spade.n.01", "name": "ditch_spade"}, + {"id": 7231, "synset": "ditty_bag.n.01", "name": "ditty_bag"}, + {"id": 7232, "synset": "divan.n.01", "name": "divan"}, + {"id": 7233, "synset": "divan.n.04", "name": "divan"}, + {"id": 7234, "synset": "dive_bomber.n.01", "name": "dive_bomber"}, + {"id": 7235, "synset": "diverging_lens.n.01", "name": "diverging_lens"}, + {"id": 7236, "synset": "divided_highway.n.01", "name": "divided_highway"}, + {"id": 7237, "synset": "divider.n.04", "name": "divider"}, + {"id": 7238, "synset": "diving_bell.n.01", "name": "diving_bell"}, + {"id": 7239, "synset": "divining_rod.n.01", "name": "divining_rod"}, + {"id": 7240, "synset": "diving_suit.n.01", "name": "diving_suit"}, + {"id": 7241, "synset": "dixie.n.02", "name": "dixie"}, + {"id": 7242, "synset": "dock.n.05", "name": "dock"}, + {"id": 7243, "synset": "doeskin.n.02", "name": "doeskin"}, + {"id": 7244, "synset": "dogcart.n.01", "name": "dogcart"}, + {"id": 7245, "synset": "doggie_bag.n.01", "name": "doggie_bag"}, + {"id": 7246, "synset": "dogsled.n.01", "name": "dogsled"}, + {"id": 7247, "synset": "dog_wrench.n.01", "name": "dog_wrench"}, + {"id": 7248, "synset": "doily.n.01", "name": "doily"}, + {"id": 7249, "synset": "dolly.n.02", "name": "dolly"}, + {"id": 7250, "synset": "dolman.n.02", "name": "dolman"}, + {"id": 7251, "synset": "dolman.n.01", "name": "dolman"}, + {"id": 7252, "synset": "dolman_sleeve.n.01", "name": "dolman_sleeve"}, + {"id": 7253, "synset": "dolmen.n.01", "name": "dolmen"}, + {"id": 7254, "synset": "dome.n.04", "name": "dome"}, + {"id": 7255, "synset": "dome.n.03", "name": "dome"}, + {"id": 7256, "synset": "domino.n.03", "name": "domino"}, + {"id": 7257, "synset": "dongle.n.01", "name": "dongle"}, + {"id": 7258, "synset": "donkey_jacket.n.01", "name": "donkey_jacket"}, + {"id": 7259, "synset": "door.n.01", "name": "door"}, + {"id": 7260, "synset": "door.n.05", "name": "door"}, + {"id": 7261, "synset": "door.n.04", "name": "door"}, + {"id": 7262, "synset": "doorbell.n.01", "name": "doorbell"}, + {"id": 7263, "synset": "doorframe.n.01", "name": "doorframe"}, + {"id": 7264, "synset": "doorjamb.n.01", "name": "doorjamb"}, + {"id": 7265, "synset": "doorlock.n.01", "name": "doorlock"}, + {"id": 7266, "synset": "doornail.n.01", "name": "doornail"}, + {"id": 7267, "synset": "doorplate.n.01", "name": "doorplate"}, + {"id": 7268, "synset": "doorsill.n.01", "name": "doorsill"}, + {"id": 7269, "synset": "doorstop.n.01", "name": "doorstop"}, + {"id": 7270, "synset": "doppler_radar.n.01", "name": "Doppler_radar"}, + {"id": 7271, "synset": "dormer.n.01", "name": "dormer"}, + {"id": 7272, "synset": "dormer_window.n.01", "name": "dormer_window"}, + {"id": 7273, "synset": "dormitory.n.01", "name": "dormitory"}, + {"id": 7274, "synset": "dormitory.n.02", "name": "dormitory"}, + {"id": 7275, "synset": "dosemeter.n.01", "name": "dosemeter"}, + {"id": 7276, "synset": "dossal.n.01", "name": "dossal"}, + {"id": 7277, "synset": "dot_matrix_printer.n.01", "name": "dot_matrix_printer"}, + {"id": 7278, "synset": "double_bed.n.01", "name": "double_bed"}, + {"id": 7279, "synset": "double-bitted_ax.n.01", "name": "double-bitted_ax"}, + {"id": 7280, "synset": "double_boiler.n.01", "name": "double_boiler"}, + {"id": 7281, "synset": "double-breasted_jacket.n.01", "name": "double-breasted_jacket"}, + {"id": 7282, "synset": "double-breasted_suit.n.01", "name": "double-breasted_suit"}, + {"id": 7283, "synset": "double_door.n.01", "name": "double_door"}, + {"id": 7284, "synset": "double_glazing.n.01", "name": "double_glazing"}, + {"id": 7285, "synset": "double-hung_window.n.01", "name": "double-hung_window"}, + {"id": 7286, "synset": "double_knit.n.01", "name": "double_knit"}, + {"id": 7287, "synset": "doubler.n.01", "name": "doubler"}, + {"id": 7288, "synset": "double_reed.n.02", "name": "double_reed"}, + {"id": 7289, "synset": "double-reed_instrument.n.01", "name": "double-reed_instrument"}, + {"id": 7290, "synset": "doublet.n.01", "name": "doublet"}, + {"id": 7291, "synset": "doubletree.n.01", "name": "doubletree"}, + {"id": 7292, "synset": "douche.n.01", "name": "douche"}, + {"id": 7293, "synset": "dovecote.n.01", "name": "dovecote"}, + {"id": 7294, "synset": "dover's_powder.n.01", "name": "Dover's_powder"}, + {"id": 7295, "synset": "dovetail.n.01", "name": "dovetail"}, + {"id": 7296, "synset": "dovetail_plane.n.01", "name": "dovetail_plane"}, + {"id": 7297, "synset": "dowel.n.01", "name": "dowel"}, + {"id": 7298, "synset": "downstage.n.01", "name": "downstage"}, + {"id": 7299, "synset": "drafting_instrument.n.01", "name": "drafting_instrument"}, + {"id": 7300, "synset": "drafting_table.n.01", "name": "drafting_table"}, + {"id": 7301, "synset": "dragunov.n.01", "name": "Dragunov"}, + {"id": 7302, "synset": "drainage_ditch.n.01", "name": "drainage_ditch"}, + {"id": 7303, "synset": "drainage_system.n.01", "name": "drainage_system"}, + {"id": 7304, "synset": "drain_basket.n.01", "name": "drain_basket"}, + {"id": 7305, "synset": "drainplug.n.01", "name": "drainplug"}, + {"id": 7306, "synset": "drape.n.03", "name": "drape"}, + {"id": 7307, "synset": "drapery.n.02", "name": "drapery"}, + {"id": 7308, "synset": "drawbar.n.01", "name": "drawbar"}, + {"id": 7309, "synset": "drawbridge.n.01", "name": "drawbridge"}, + {"id": 7310, "synset": "drawing_chalk.n.01", "name": "drawing_chalk"}, + {"id": 7311, "synset": "drawing_room.n.01", "name": "drawing_room"}, + {"id": 7312, "synset": "drawing_room.n.02", "name": "drawing_room"}, + {"id": 7313, "synset": "drawknife.n.01", "name": "drawknife"}, + {"id": 7314, "synset": "drawstring_bag.n.01", "name": "drawstring_bag"}, + {"id": 7315, "synset": "dray.n.01", "name": "dray"}, + {"id": 7316, "synset": "dreadnought.n.01", "name": "dreadnought"}, + {"id": 7317, "synset": "dredge.n.01", "name": "dredge"}, + {"id": 7318, "synset": "dredger.n.01", "name": "dredger"}, + {"id": 7319, "synset": "dredging_bucket.n.01", "name": "dredging_bucket"}, + {"id": 7320, "synset": "dress_blues.n.01", "name": "dress_blues"}, + {"id": 7321, "synset": "dressing.n.04", "name": "dressing"}, + {"id": 7322, "synset": "dressing_case.n.01", "name": "dressing_case"}, + {"id": 7323, "synset": "dressing_gown.n.01", "name": "dressing_gown"}, + {"id": 7324, "synset": "dressing_room.n.01", "name": "dressing_room"}, + {"id": 7325, "synset": "dressing_sack.n.01", "name": "dressing_sack"}, + {"id": 7326, "synset": "dressing_table.n.01", "name": "dressing_table"}, + {"id": 7327, "synset": "dress_rack.n.01", "name": "dress_rack"}, + {"id": 7328, "synset": "dress_shirt.n.01", "name": "dress_shirt"}, + {"id": 7329, "synset": "dress_uniform.n.01", "name": "dress_uniform"}, + {"id": 7330, "synset": "drift_net.n.01", "name": "drift_net"}, + {"id": 7331, "synset": "electric_drill.n.01", "name": "electric_drill"}, + {"id": 7332, "synset": "drilling_platform.n.01", "name": "drilling_platform"}, + {"id": 7333, "synset": "drill_press.n.01", "name": "drill_press"}, + {"id": 7334, "synset": "drill_rig.n.01", "name": "drill_rig"}, + {"id": 7335, "synset": "drinking_fountain.n.01", "name": "drinking_fountain"}, + {"id": 7336, "synset": "drinking_vessel.n.01", "name": "drinking_vessel"}, + {"id": 7337, "synset": "drip_loop.n.01", "name": "drip_loop"}, + {"id": 7338, "synset": "drip_mat.n.01", "name": "drip_mat"}, + {"id": 7339, "synset": "drip_pan.n.02", "name": "drip_pan"}, + {"id": 7340, "synset": "dripping_pan.n.01", "name": "dripping_pan"}, + {"id": 7341, "synset": "drip_pot.n.01", "name": "drip_pot"}, + {"id": 7342, "synset": "drive.n.02", "name": "drive"}, + {"id": 7343, "synset": "drive.n.10", "name": "drive"}, + {"id": 7344, "synset": "drive_line.n.01", "name": "drive_line"}, + {"id": 7345, "synset": "driver.n.05", "name": "driver"}, + {"id": 7346, "synset": "driveshaft.n.01", "name": "driveshaft"}, + {"id": 7347, "synset": "driveway.n.01", "name": "driveway"}, + {"id": 7348, "synset": "driving_iron.n.01", "name": "driving_iron"}, + {"id": 7349, "synset": "driving_wheel.n.01", "name": "driving_wheel"}, + {"id": 7350, "synset": "drogue.n.04", "name": "drogue"}, + {"id": 7351, "synset": "drogue_parachute.n.01", "name": "drogue_parachute"}, + {"id": 7352, "synset": "drone.n.05", "name": "drone"}, + {"id": 7353, "synset": "drop_arch.n.01", "name": "drop_arch"}, + {"id": 7354, "synset": "drop_cloth.n.02", "name": "drop_cloth"}, + {"id": 7355, "synset": "drop_curtain.n.01", "name": "drop_curtain"}, + {"id": 7356, "synset": "drop_forge.n.01", "name": "drop_forge"}, + {"id": 7357, "synset": "drop-leaf_table.n.01", "name": "drop-leaf_table"}, + {"id": 7358, "synset": "droshky.n.01", "name": "droshky"}, + {"id": 7359, "synset": "drove.n.03", "name": "drove"}, + {"id": 7360, "synset": "drugget.n.01", "name": "drugget"}, + {"id": 7361, "synset": "drugstore.n.01", "name": "drugstore"}, + {"id": 7362, "synset": "drum.n.04", "name": "drum"}, + {"id": 7363, "synset": "drum_brake.n.01", "name": "drum_brake"}, + {"id": 7364, "synset": "drumhead.n.01", "name": "drumhead"}, + {"id": 7365, "synset": "drum_printer.n.01", "name": "drum_printer"}, + {"id": 7366, "synset": "drum_sander.n.01", "name": "drum_sander"}, + {"id": 7367, "synset": "dry_battery.n.01", "name": "dry_battery"}, + {"id": 7368, "synset": "dry-bulb_thermometer.n.01", "name": "dry-bulb_thermometer"}, + {"id": 7369, "synset": "dry_cell.n.01", "name": "dry_cell"}, + {"id": 7370, "synset": "dry_dock.n.01", "name": "dry_dock"}, + {"id": 7371, "synset": "dryer.n.01", "name": "dryer"}, + {"id": 7372, "synset": "dry_fly.n.01", "name": "dry_fly"}, + {"id": 7373, "synset": "dry_kiln.n.01", "name": "dry_kiln"}, + {"id": 7374, "synset": "dry_masonry.n.01", "name": "dry_masonry"}, + {"id": 7375, "synset": "dry_point.n.02", "name": "dry_point"}, + {"id": 7376, "synset": "dry_wall.n.02", "name": "dry_wall"}, + {"id": 7377, "synset": "dual_scan_display.n.01", "name": "dual_scan_display"}, + {"id": 7378, "synset": "duck.n.04", "name": "duck"}, + {"id": 7379, "synset": "duckboard.n.01", "name": "duckboard"}, + {"id": 7380, "synset": "duckpin.n.01", "name": "duckpin"}, + {"id": 7381, "synset": "dudeen.n.01", "name": "dudeen"}, + {"id": 7382, "synset": "duffel.n.02", "name": "duffel"}, + {"id": 7383, "synset": "duffel_coat.n.01", "name": "duffel_coat"}, + {"id": 7384, "synset": "dugout.n.01", "name": "dugout"}, + {"id": 7385, "synset": "dugout_canoe.n.01", "name": "dugout_canoe"}, + {"id": 7386, "synset": "dulciana.n.01", "name": "dulciana"}, + {"id": 7387, "synset": "dulcimer.n.02", "name": "dulcimer"}, + {"id": 7388, "synset": "dulcimer.n.01", "name": "dulcimer"}, + {"id": 7389, "synset": "dumb_bomb.n.01", "name": "dumb_bomb"}, + {"id": 7390, "synset": "dumbwaiter.n.01", "name": "dumbwaiter"}, + {"id": 7391, "synset": "dumdum.n.01", "name": "dumdum"}, + {"id": 7392, "synset": "dumpcart.n.01", "name": "dumpcart"}, + {"id": 7393, "synset": "dump_truck.n.01", "name": "dump_truck"}, + {"id": 7394, "synset": "dumpy_level.n.01", "name": "Dumpy_level"}, + {"id": 7395, "synset": "dunce_cap.n.01", "name": "dunce_cap"}, + {"id": 7396, "synset": "dune_buggy.n.01", "name": "dune_buggy"}, + {"id": 7397, "synset": "dungeon.n.02", "name": "dungeon"}, + {"id": 7398, "synset": "duplex_apartment.n.01", "name": "duplex_apartment"}, + {"id": 7399, "synset": "duplex_house.n.01", "name": "duplex_house"}, + {"id": 7400, "synset": "duplicator.n.01", "name": "duplicator"}, + {"id": 7401, "synset": "dust_bag.n.01", "name": "dust_bag"}, + {"id": 7402, "synset": "dustcloth.n.01", "name": "dustcloth"}, + {"id": 7403, "synset": "dust_cover.n.03", "name": "dust_cover"}, + {"id": 7404, "synset": "dust_cover.n.02", "name": "dust_cover"}, + {"id": 7405, "synset": "dustmop.n.01", "name": "dustmop"}, + {"id": 7406, "synset": "dutch_oven.n.01", "name": "Dutch_oven"}, + {"id": 7407, "synset": "dutch_oven.n.02", "name": "Dutch_oven"}, + {"id": 7408, "synset": "dwelling.n.01", "name": "dwelling"}, + {"id": 7409, "synset": "dye-works.n.01", "name": "dye-works"}, + {"id": 7410, "synset": "dynamo.n.01", "name": "dynamo"}, + {"id": 7411, "synset": "dynamometer.n.01", "name": "dynamometer"}, + {"id": 7412, "synset": "eames_chair.n.01", "name": "Eames_chair"}, + {"id": 7413, "synset": "earflap.n.01", "name": "earflap"}, + {"id": 7414, "synset": "early_warning_radar.n.01", "name": "early_warning_radar"}, + {"id": 7415, "synset": "early_warning_system.n.01", "name": "early_warning_system"}, + {"id": 7416, "synset": "earmuff.n.01", "name": "earmuff"}, + {"id": 7417, "synset": "earplug.n.02", "name": "earplug"}, + {"id": 7418, "synset": "earthenware.n.01", "name": "earthenware"}, + {"id": 7419, "synset": "earthwork.n.01", "name": "earthwork"}, + {"id": 7420, "synset": "easy_chair.n.01", "name": "easy_chair"}, + {"id": 7421, "synset": "eaves.n.01", "name": "eaves"}, + {"id": 7422, "synset": "ecclesiastical_attire.n.01", "name": "ecclesiastical_attire"}, + {"id": 7423, "synset": "echinus.n.01", "name": "echinus"}, + {"id": 7424, "synset": "echocardiograph.n.01", "name": "echocardiograph"}, + {"id": 7425, "synset": "edger.n.02", "name": "edger"}, + {"id": 7426, "synset": "edge_tool.n.01", "name": "edge_tool"}, + {"id": 7427, "synset": "efficiency_apartment.n.01", "name": "efficiency_apartment"}, + {"id": 7428, "synset": "egg-and-dart.n.01", "name": "egg-and-dart"}, + {"id": 7429, "synset": "egg_timer.n.01", "name": "egg_timer"}, + {"id": 7430, "synset": "eiderdown.n.01", "name": "eiderdown"}, + {"id": 7431, "synset": "eight_ball.n.01", "name": "eight_ball"}, + {"id": 7432, "synset": "ejection_seat.n.01", "name": "ejection_seat"}, + {"id": 7433, "synset": "elastic.n.02", "name": "elastic"}, + {"id": 7434, "synset": "elastic_bandage.n.01", "name": "elastic_bandage"}, + {"id": 7435, "synset": "elastoplast.n.01", "name": "Elastoplast"}, + {"id": 7436, "synset": "elbow.n.04", "name": "elbow"}, + {"id": 7437, "synset": "elbow_pad.n.01", "name": "elbow_pad"}, + {"id": 7438, "synset": "electric.n.01", "name": "electric"}, + {"id": 7439, "synset": "electrical_cable.n.01", "name": "electrical_cable"}, + {"id": 7440, "synset": "electrical_contact.n.01", "name": "electrical_contact"}, + {"id": 7441, "synset": "electrical_converter.n.01", "name": "electrical_converter"}, + {"id": 7442, "synset": "electrical_device.n.01", "name": "electrical_device"}, + {"id": 7443, "synset": "electrical_system.n.02", "name": "electrical_system"}, + {"id": 7444, "synset": "electric_bell.n.01", "name": "electric_bell"}, + {"id": 7445, "synset": "electric_blanket.n.01", "name": "electric_blanket"}, + {"id": 7446, "synset": "electric_clock.n.01", "name": "electric_clock"}, + {"id": 7447, "synset": "electric-discharge_lamp.n.01", "name": "electric-discharge_lamp"}, + {"id": 7448, "synset": "electric_fan.n.01", "name": "electric_fan"}, + {"id": 7449, "synset": "electric_frying_pan.n.01", "name": "electric_frying_pan"}, + {"id": 7450, "synset": "electric_furnace.n.01", "name": "electric_furnace"}, + {"id": 7451, "synset": "electric_guitar.n.01", "name": "electric_guitar"}, + {"id": 7452, "synset": "electric_hammer.n.01", "name": "electric_hammer"}, + {"id": 7453, "synset": "electric_heater.n.01", "name": "electric_heater"}, + {"id": 7454, "synset": "electric_lamp.n.01", "name": "electric_lamp"}, + {"id": 7455, "synset": "electric_locomotive.n.01", "name": "electric_locomotive"}, + {"id": 7456, "synset": "electric_meter.n.01", "name": "electric_meter"}, + {"id": 7457, "synset": "electric_mixer.n.01", "name": "electric_mixer"}, + {"id": 7458, "synset": "electric_motor.n.01", "name": "electric_motor"}, + {"id": 7459, "synset": "electric_organ.n.01", "name": "electric_organ"}, + {"id": 7460, "synset": "electric_range.n.01", "name": "electric_range"}, + {"id": 7461, "synset": "electric_toothbrush.n.01", "name": "electric_toothbrush"}, + {"id": 7462, "synset": "electric_typewriter.n.01", "name": "electric_typewriter"}, + { + "id": 7463, + "synset": "electro-acoustic_transducer.n.01", + "name": "electro-acoustic_transducer", + }, + {"id": 7464, "synset": "electrode.n.01", "name": "electrode"}, + {"id": 7465, "synset": "electrodynamometer.n.01", "name": "electrodynamometer"}, + {"id": 7466, "synset": "electroencephalograph.n.01", "name": "electroencephalograph"}, + {"id": 7467, "synset": "electrograph.n.01", "name": "electrograph"}, + {"id": 7468, "synset": "electrolytic.n.01", "name": "electrolytic"}, + {"id": 7469, "synset": "electrolytic_cell.n.01", "name": "electrolytic_cell"}, + {"id": 7470, "synset": "electromagnet.n.01", "name": "electromagnet"}, + {"id": 7471, "synset": "electrometer.n.01", "name": "electrometer"}, + {"id": 7472, "synset": "electromyograph.n.01", "name": "electromyograph"}, + {"id": 7473, "synset": "electron_accelerator.n.01", "name": "electron_accelerator"}, + {"id": 7474, "synset": "electron_gun.n.01", "name": "electron_gun"}, + {"id": 7475, "synset": "electronic_balance.n.01", "name": "electronic_balance"}, + {"id": 7476, "synset": "electronic_converter.n.01", "name": "electronic_converter"}, + {"id": 7477, "synset": "electronic_device.n.01", "name": "electronic_device"}, + {"id": 7478, "synset": "electronic_equipment.n.01", "name": "electronic_equipment"}, + {"id": 7479, "synset": "electronic_fetal_monitor.n.01", "name": "electronic_fetal_monitor"}, + {"id": 7480, "synset": "electronic_instrument.n.01", "name": "electronic_instrument"}, + {"id": 7481, "synset": "electronic_voltmeter.n.01", "name": "electronic_voltmeter"}, + {"id": 7482, "synset": "electron_microscope.n.01", "name": "electron_microscope"}, + {"id": 7483, "synset": "electron_multiplier.n.01", "name": "electron_multiplier"}, + {"id": 7484, "synset": "electrophorus.n.01", "name": "electrophorus"}, + {"id": 7485, "synset": "electroscope.n.01", "name": "electroscope"}, + {"id": 7486, "synset": "electrostatic_generator.n.01", "name": "electrostatic_generator"}, + {"id": 7487, "synset": "electrostatic_printer.n.01", "name": "electrostatic_printer"}, + {"id": 7488, "synset": "elevator.n.01", "name": "elevator"}, + {"id": 7489, "synset": "elevator.n.02", "name": "elevator"}, + {"id": 7490, "synset": "elevator_shaft.n.01", "name": "elevator_shaft"}, + {"id": 7491, "synset": "embankment.n.01", "name": "embankment"}, + {"id": 7492, "synset": "embassy.n.01", "name": "embassy"}, + {"id": 7493, "synset": "embellishment.n.02", "name": "embellishment"}, + {"id": 7494, "synset": "emergency_room.n.01", "name": "emergency_room"}, + {"id": 7495, "synset": "emesis_basin.n.01", "name": "emesis_basin"}, + {"id": 7496, "synset": "emitter.n.01", "name": "emitter"}, + {"id": 7497, "synset": "empty.n.01", "name": "empty"}, + {"id": 7498, "synset": "emulsion.n.02", "name": "emulsion"}, + {"id": 7499, "synset": "enamel.n.04", "name": "enamel"}, + {"id": 7500, "synset": "enamel.n.03", "name": "enamel"}, + {"id": 7501, "synset": "enamelware.n.01", "name": "enamelware"}, + {"id": 7502, "synset": "encaustic.n.01", "name": "encaustic"}, + {"id": 7503, "synset": "encephalogram.n.02", "name": "encephalogram"}, + {"id": 7504, "synset": "enclosure.n.01", "name": "enclosure"}, + {"id": 7505, "synset": "endoscope.n.01", "name": "endoscope"}, + {"id": 7506, "synset": "energizer.n.02", "name": "energizer"}, + {"id": 7507, "synset": "engine.n.01", "name": "engine"}, + {"id": 7508, "synset": "engine.n.04", "name": "engine"}, + {"id": 7509, "synset": "engineering.n.03", "name": "engineering"}, + {"id": 7510, "synset": "enginery.n.01", "name": "enginery"}, + {"id": 7511, "synset": "english_horn.n.01", "name": "English_horn"}, + {"id": 7512, "synset": "english_saddle.n.01", "name": "English_saddle"}, + {"id": 7513, "synset": "enlarger.n.01", "name": "enlarger"}, + {"id": 7514, "synset": "ensemble.n.05", "name": "ensemble"}, + {"id": 7515, "synset": "ensign.n.03", "name": "ensign"}, + {"id": 7516, "synset": "entablature.n.01", "name": "entablature"}, + {"id": 7517, "synset": "entertainment_center.n.01", "name": "entertainment_center"}, + {"id": 7518, "synset": "entrenching_tool.n.01", "name": "entrenching_tool"}, + {"id": 7519, "synset": "entrenchment.n.01", "name": "entrenchment"}, + {"id": 7520, "synset": "envelope.n.02", "name": "envelope"}, + {"id": 7521, "synset": "envelope.n.06", "name": "envelope"}, + {"id": 7522, "synset": "eolith.n.01", "name": "eolith"}, + {"id": 7523, "synset": "epauliere.n.01", "name": "epauliere"}, + {"id": 7524, "synset": "epee.n.01", "name": "epee"}, + {"id": 7525, "synset": "epergne.n.01", "name": "epergne"}, + {"id": 7526, "synset": "epicyclic_train.n.01", "name": "epicyclic_train"}, + {"id": 7527, "synset": "epidiascope.n.01", "name": "epidiascope"}, + {"id": 7528, "synset": "epilating_wax.n.01", "name": "epilating_wax"}, + {"id": 7529, "synset": "equalizer.n.01", "name": "equalizer"}, + {"id": 7530, "synset": "equatorial.n.01", "name": "equatorial"}, + {"id": 7531, "synset": "equipment.n.01", "name": "equipment"}, + { + "id": 7532, + "synset": "erasable_programmable_read-only_memory.n.01", + "name": "erasable_programmable_read-only_memory", + }, + {"id": 7533, "synset": "erecting_prism.n.01", "name": "erecting_prism"}, + {"id": 7534, "synset": "erection.n.02", "name": "erection"}, + {"id": 7535, "synset": "erlenmeyer_flask.n.01", "name": "Erlenmeyer_flask"}, + {"id": 7536, "synset": "escape_hatch.n.01", "name": "escape_hatch"}, + {"id": 7537, "synset": "escapement.n.01", "name": "escapement"}, + {"id": 7538, "synset": "escape_wheel.n.01", "name": "escape_wheel"}, + {"id": 7539, "synset": "escarpment.n.02", "name": "escarpment"}, + {"id": 7540, "synset": "escutcheon.n.03", "name": "escutcheon"}, + {"id": 7541, "synset": "esophagoscope.n.01", "name": "esophagoscope"}, + {"id": 7542, "synset": "espadrille.n.01", "name": "espadrille"}, + {"id": 7543, "synset": "espalier.n.01", "name": "espalier"}, + {"id": 7544, "synset": "espresso_maker.n.01", "name": "espresso_maker"}, + {"id": 7545, "synset": "espresso_shop.n.01", "name": "espresso_shop"}, + {"id": 7546, "synset": "establishment.n.04", "name": "establishment"}, + {"id": 7547, "synset": "estaminet.n.01", "name": "estaminet"}, + {"id": 7548, "synset": "estradiol_patch.n.01", "name": "estradiol_patch"}, + {"id": 7549, "synset": "etagere.n.01", "name": "etagere"}, + {"id": 7550, "synset": "etamine.n.01", "name": "etamine"}, + {"id": 7551, "synset": "etching.n.02", "name": "etching"}, + {"id": 7552, "synset": "ethernet.n.01", "name": "ethernet"}, + {"id": 7553, "synset": "ethernet_cable.n.01", "name": "ethernet_cable"}, + {"id": 7554, "synset": "eton_jacket.n.01", "name": "Eton_jacket"}, + {"id": 7555, "synset": "etui.n.01", "name": "etui"}, + {"id": 7556, "synset": "eudiometer.n.01", "name": "eudiometer"}, + {"id": 7557, "synset": "euphonium.n.01", "name": "euphonium"}, + {"id": 7558, "synset": "evaporative_cooler.n.01", "name": "evaporative_cooler"}, + {"id": 7559, "synset": "evening_bag.n.01", "name": "evening_bag"}, + {"id": 7560, "synset": "exercise_bike.n.01", "name": "exercise_bike"}, + {"id": 7561, "synset": "exercise_device.n.01", "name": "exercise_device"}, + {"id": 7562, "synset": "exhaust.n.02", "name": "exhaust"}, + {"id": 7563, "synset": "exhaust_fan.n.01", "name": "exhaust_fan"}, + {"id": 7564, "synset": "exhaust_valve.n.01", "name": "exhaust_valve"}, + {"id": 7565, "synset": "exhibition_hall.n.01", "name": "exhibition_hall"}, + {"id": 7566, "synset": "exocet.n.01", "name": "Exocet"}, + {"id": 7567, "synset": "expansion_bit.n.01", "name": "expansion_bit"}, + {"id": 7568, "synset": "expansion_bolt.n.01", "name": "expansion_bolt"}, + {"id": 7569, "synset": "explosive_detection_system.n.01", "name": "explosive_detection_system"}, + {"id": 7570, "synset": "explosive_device.n.01", "name": "explosive_device"}, + {"id": 7571, "synset": "explosive_trace_detection.n.01", "name": "explosive_trace_detection"}, + {"id": 7572, "synset": "express.n.02", "name": "express"}, + {"id": 7573, "synset": "extension.n.10", "name": "extension"}, + {"id": 7574, "synset": "extension_cord.n.01", "name": "extension_cord"}, + {"id": 7575, "synset": "external-combustion_engine.n.01", "name": "external-combustion_engine"}, + {"id": 7576, "synset": "external_drive.n.01", "name": "external_drive"}, + {"id": 7577, "synset": "extractor.n.01", "name": "extractor"}, + {"id": 7578, "synset": "eyebrow_pencil.n.01", "name": "eyebrow_pencil"}, + {"id": 7579, "synset": "eyecup.n.01", "name": "eyecup"}, + {"id": 7580, "synset": "eyeliner.n.01", "name": "eyeliner"}, + {"id": 7581, "synset": "eyepiece.n.01", "name": "eyepiece"}, + {"id": 7582, "synset": "eyeshadow.n.01", "name": "eyeshadow"}, + {"id": 7583, "synset": "fabric.n.01", "name": "fabric"}, + {"id": 7584, "synset": "facade.n.01", "name": "facade"}, + {"id": 7585, "synset": "face_guard.n.01", "name": "face_guard"}, + {"id": 7586, "synset": "face_mask.n.01", "name": "face_mask"}, + {"id": 7587, "synset": "faceplate.n.01", "name": "faceplate"}, + {"id": 7588, "synset": "face_powder.n.01", "name": "face_powder"}, + {"id": 7589, "synset": "face_veil.n.01", "name": "face_veil"}, + {"id": 7590, "synset": "facing.n.03", "name": "facing"}, + {"id": 7591, "synset": "facing.n.01", "name": "facing"}, + {"id": 7592, "synset": "facing.n.02", "name": "facing"}, + {"id": 7593, "synset": "facsimile.n.02", "name": "facsimile"}, + {"id": 7594, "synset": "factory.n.01", "name": "factory"}, + {"id": 7595, "synset": "factory_ship.n.01", "name": "factory_ship"}, + {"id": 7596, "synset": "fagot.n.02", "name": "fagot"}, + {"id": 7597, "synset": "fagot_stitch.n.01", "name": "fagot_stitch"}, + {"id": 7598, "synset": "fahrenheit_thermometer.n.01", "name": "Fahrenheit_thermometer"}, + {"id": 7599, "synset": "faience.n.01", "name": "faience"}, + {"id": 7600, "synset": "faille.n.01", "name": "faille"}, + {"id": 7601, "synset": "fairlead.n.01", "name": "fairlead"}, + {"id": 7602, "synset": "fairy_light.n.01", "name": "fairy_light"}, + {"id": 7603, "synset": "falchion.n.01", "name": "falchion"}, + {"id": 7604, "synset": "fallboard.n.01", "name": "fallboard"}, + {"id": 7605, "synset": "fallout_shelter.n.01", "name": "fallout_shelter"}, + {"id": 7606, "synset": "false_face.n.01", "name": "false_face"}, + {"id": 7607, "synset": "false_teeth.n.01", "name": "false_teeth"}, + {"id": 7608, "synset": "family_room.n.01", "name": "family_room"}, + {"id": 7609, "synset": "fan_belt.n.01", "name": "fan_belt"}, + {"id": 7610, "synset": "fan_blade.n.01", "name": "fan_blade"}, + {"id": 7611, "synset": "fancy_dress.n.01", "name": "fancy_dress"}, + {"id": 7612, "synset": "fanion.n.01", "name": "fanion"}, + {"id": 7613, "synset": "fanlight.n.03", "name": "fanlight"}, + {"id": 7614, "synset": "fanjet.n.02", "name": "fanjet"}, + {"id": 7615, "synset": "fanjet.n.01", "name": "fanjet"}, + {"id": 7616, "synset": "fanny_pack.n.01", "name": "fanny_pack"}, + {"id": 7617, "synset": "fan_tracery.n.01", "name": "fan_tracery"}, + {"id": 7618, "synset": "fan_vaulting.n.01", "name": "fan_vaulting"}, + {"id": 7619, "synset": "farm_building.n.01", "name": "farm_building"}, + {"id": 7620, "synset": "farmer's_market.n.01", "name": "farmer's_market"}, + {"id": 7621, "synset": "farmhouse.n.01", "name": "farmhouse"}, + {"id": 7622, "synset": "farm_machine.n.01", "name": "farm_machine"}, + {"id": 7623, "synset": "farmplace.n.01", "name": "farmplace"}, + {"id": 7624, "synset": "farmyard.n.01", "name": "farmyard"}, + {"id": 7625, "synset": "farthingale.n.01", "name": "farthingale"}, + {"id": 7626, "synset": "fastener.n.02", "name": "fastener"}, + {"id": 7627, "synset": "fast_reactor.n.01", "name": "fast_reactor"}, + {"id": 7628, "synset": "fat_farm.n.01", "name": "fat_farm"}, + {"id": 7629, "synset": "fatigues.n.01", "name": "fatigues"}, + {"id": 7630, "synset": "fauld.n.01", "name": "fauld"}, + {"id": 7631, "synset": "fauteuil.n.01", "name": "fauteuil"}, + {"id": 7632, "synset": "feather_boa.n.01", "name": "feather_boa"}, + {"id": 7633, "synset": "featheredge.n.01", "name": "featheredge"}, + {"id": 7634, "synset": "feedback_circuit.n.01", "name": "feedback_circuit"}, + {"id": 7635, "synset": "feedlot.n.01", "name": "feedlot"}, + {"id": 7636, "synset": "fell.n.02", "name": "fell"}, + {"id": 7637, "synset": "felloe.n.01", "name": "felloe"}, + {"id": 7638, "synset": "felt.n.01", "name": "felt"}, + {"id": 7639, "synset": "felt-tip_pen.n.01", "name": "felt-tip_pen"}, + {"id": 7640, "synset": "felucca.n.01", "name": "felucca"}, + {"id": 7641, "synset": "fence.n.01", "name": "fence"}, + {"id": 7642, "synset": "fencing_mask.n.01", "name": "fencing_mask"}, + {"id": 7643, "synset": "fencing_sword.n.01", "name": "fencing_sword"}, + {"id": 7644, "synset": "fender.n.01", "name": "fender"}, + {"id": 7645, "synset": "fender.n.02", "name": "fender"}, + {"id": 7646, "synset": "ferrule.n.01", "name": "ferrule"}, + {"id": 7647, "synset": "ferule.n.01", "name": "ferule"}, + {"id": 7648, "synset": "festoon.n.01", "name": "festoon"}, + {"id": 7649, "synset": "fetoscope.n.01", "name": "fetoscope"}, + {"id": 7650, "synset": "fetter.n.01", "name": "fetter"}, + {"id": 7651, "synset": "fez.n.02", "name": "fez"}, + {"id": 7652, "synset": "fiber.n.05", "name": "fiber"}, + {"id": 7653, "synset": "fiber_optic_cable.n.01", "name": "fiber_optic_cable"}, + {"id": 7654, "synset": "fiberscope.n.01", "name": "fiberscope"}, + {"id": 7655, "synset": "fichu.n.01", "name": "fichu"}, + {"id": 7656, "synset": "fiddlestick.n.01", "name": "fiddlestick"}, + {"id": 7657, "synset": "field_artillery.n.01", "name": "field_artillery"}, + {"id": 7658, "synset": "field_coil.n.01", "name": "field_coil"}, + {"id": 7659, "synset": "field-effect_transistor.n.01", "name": "field-effect_transistor"}, + {"id": 7660, "synset": "field-emission_microscope.n.01", "name": "field-emission_microscope"}, + {"id": 7661, "synset": "field_glass.n.01", "name": "field_glass"}, + {"id": 7662, "synset": "field_hockey_ball.n.01", "name": "field_hockey_ball"}, + {"id": 7663, "synset": "field_hospital.n.01", "name": "field_hospital"}, + {"id": 7664, "synset": "field_house.n.01", "name": "field_house"}, + {"id": 7665, "synset": "field_lens.n.01", "name": "field_lens"}, + {"id": 7666, "synset": "field_magnet.n.01", "name": "field_magnet"}, + { + "id": 7667, + "synset": "field-sequential_color_television.n.01", + "name": "field-sequential_color_television", + }, + {"id": 7668, "synset": "field_tent.n.01", "name": "field_tent"}, + {"id": 7669, "synset": "fieldwork.n.01", "name": "fieldwork"}, + {"id": 7670, "synset": "fife.n.01", "name": "fife"}, + {"id": 7671, "synset": "fifth_wheel.n.02", "name": "fifth_wheel"}, + {"id": 7672, "synset": "fighting_chair.n.01", "name": "fighting_chair"}, + {"id": 7673, "synset": "fig_leaf.n.02", "name": "fig_leaf"}, + {"id": 7674, "synset": "figure_eight.n.01", "name": "figure_eight"}, + {"id": 7675, "synset": "figure_loom.n.01", "name": "figure_loom"}, + {"id": 7676, "synset": "figure_skate.n.01", "name": "figure_skate"}, + {"id": 7677, "synset": "filament.n.04", "name": "filament"}, + {"id": 7678, "synset": "filature.n.01", "name": "filature"}, + {"id": 7679, "synset": "file_folder.n.01", "name": "file_folder"}, + {"id": 7680, "synset": "file_server.n.01", "name": "file_server"}, + {"id": 7681, "synset": "filigree.n.01", "name": "filigree"}, + {"id": 7682, "synset": "filling.n.05", "name": "filling"}, + {"id": 7683, "synset": "film.n.03", "name": "film"}, + {"id": 7684, "synset": "film.n.05", "name": "film"}, + {"id": 7685, "synset": "film_advance.n.01", "name": "film_advance"}, + {"id": 7686, "synset": "filter.n.01", "name": "filter"}, + {"id": 7687, "synset": "filter.n.02", "name": "filter"}, + {"id": 7688, "synset": "finder.n.03", "name": "finder"}, + {"id": 7689, "synset": "finery.n.01", "name": "finery"}, + {"id": 7690, "synset": "fine-tooth_comb.n.01", "name": "fine-tooth_comb"}, + {"id": 7691, "synset": "finger.n.03", "name": "finger"}, + {"id": 7692, "synset": "fingerboard.n.03", "name": "fingerboard"}, + {"id": 7693, "synset": "finger_bowl.n.01", "name": "finger_bowl"}, + {"id": 7694, "synset": "finger_paint.n.01", "name": "finger_paint"}, + {"id": 7695, "synset": "finger-painting.n.01", "name": "finger-painting"}, + {"id": 7696, "synset": "finger_plate.n.01", "name": "finger_plate"}, + {"id": 7697, "synset": "fingerstall.n.01", "name": "fingerstall"}, + {"id": 7698, "synset": "finish_coat.n.02", "name": "finish_coat"}, + {"id": 7699, "synset": "finish_coat.n.01", "name": "finish_coat"}, + {"id": 7700, "synset": "finisher.n.05", "name": "finisher"}, + {"id": 7701, "synset": "fin_keel.n.01", "name": "fin_keel"}, + {"id": 7702, "synset": "fipple.n.01", "name": "fipple"}, + {"id": 7703, "synset": "fipple_flute.n.01", "name": "fipple_flute"}, + {"id": 7704, "synset": "fire.n.04", "name": "fire"}, + {"id": 7705, "synset": "firearm.n.01", "name": "firearm"}, + {"id": 7706, "synset": "fire_bell.n.01", "name": "fire_bell"}, + {"id": 7707, "synset": "fireboat.n.01", "name": "fireboat"}, + {"id": 7708, "synset": "firebox.n.01", "name": "firebox"}, + {"id": 7709, "synset": "firebrick.n.01", "name": "firebrick"}, + {"id": 7710, "synset": "fire_control_radar.n.01", "name": "fire_control_radar"}, + {"id": 7711, "synset": "fire_control_system.n.01", "name": "fire_control_system"}, + {"id": 7712, "synset": "fire_iron.n.01", "name": "fire_iron"}, + {"id": 7713, "synset": "fireman's_ax.n.01", "name": "fireman's_ax"}, + {"id": 7714, "synset": "fire_screen.n.01", "name": "fire_screen"}, + {"id": 7715, "synset": "fire_tongs.n.01", "name": "fire_tongs"}, + {"id": 7716, "synset": "fire_tower.n.01", "name": "fire_tower"}, + {"id": 7717, "synset": "firewall.n.02", "name": "firewall"}, + {"id": 7718, "synset": "firing_chamber.n.01", "name": "firing_chamber"}, + {"id": 7719, "synset": "firing_pin.n.01", "name": "firing_pin"}, + {"id": 7720, "synset": "firkin.n.02", "name": "firkin"}, + {"id": 7721, "synset": "firmer_chisel.n.01", "name": "firmer_chisel"}, + {"id": 7722, "synset": "first-aid_station.n.01", "name": "first-aid_station"}, + {"id": 7723, "synset": "first_base.n.01", "name": "first_base"}, + {"id": 7724, "synset": "first_class.n.03", "name": "first_class"}, + {"id": 7725, "synset": "fisherman's_bend.n.01", "name": "fisherman's_bend"}, + {"id": 7726, "synset": "fisherman's_knot.n.01", "name": "fisherman's_knot"}, + {"id": 7727, "synset": "fisherman's_lure.n.01", "name": "fisherman's_lure"}, + {"id": 7728, "synset": "fishhook.n.01", "name": "fishhook"}, + {"id": 7729, "synset": "fishing_boat.n.01", "name": "fishing_boat"}, + {"id": 7730, "synset": "fishing_gear.n.01", "name": "fishing_gear"}, + {"id": 7731, "synset": "fish_joint.n.01", "name": "fish_joint"}, + {"id": 7732, "synset": "fish_knife.n.01", "name": "fish_knife"}, + {"id": 7733, "synset": "fishnet.n.01", "name": "fishnet"}, + {"id": 7734, "synset": "fish_slice.n.01", "name": "fish_slice"}, + {"id": 7735, "synset": "fitment.n.01", "name": "fitment"}, + {"id": 7736, "synset": "fixative.n.02", "name": "fixative"}, + {"id": 7737, "synset": "fixer-upper.n.01", "name": "fixer-upper"}, + {"id": 7738, "synset": "flageolet.n.02", "name": "flageolet"}, + {"id": 7739, "synset": "flagon.n.01", "name": "flagon"}, + {"id": 7740, "synset": "flagship.n.02", "name": "flagship"}, + {"id": 7741, "synset": "flail.n.01", "name": "flail"}, + {"id": 7742, "synset": "flambeau.n.01", "name": "flambeau"}, + {"id": 7743, "synset": "flamethrower.n.01", "name": "flamethrower"}, + {"id": 7744, "synset": "flange.n.01", "name": "flange"}, + {"id": 7745, "synset": "flannel.n.03", "name": "flannel"}, + {"id": 7746, "synset": "flannelette.n.01", "name": "flannelette"}, + {"id": 7747, "synset": "flap.n.05", "name": "flap"}, + {"id": 7748, "synset": "flash.n.09", "name": "flash"}, + {"id": 7749, "synset": "flash_camera.n.01", "name": "flash_camera"}, + {"id": 7750, "synset": "flasher.n.02", "name": "flasher"}, + {"id": 7751, "synset": "flashlight_battery.n.01", "name": "flashlight_battery"}, + {"id": 7752, "synset": "flash_memory.n.01", "name": "flash_memory"}, + {"id": 7753, "synset": "flask.n.01", "name": "flask"}, + {"id": 7754, "synset": "flat_arch.n.01", "name": "flat_arch"}, + {"id": 7755, "synset": "flatbed.n.02", "name": "flatbed"}, + {"id": 7756, "synset": "flatbed_press.n.01", "name": "flatbed_press"}, + {"id": 7757, "synset": "flat_bench.n.01", "name": "flat_bench"}, + {"id": 7758, "synset": "flatcar.n.01", "name": "flatcar"}, + {"id": 7759, "synset": "flat_file.n.01", "name": "flat_file"}, + {"id": 7760, "synset": "flatlet.n.01", "name": "flatlet"}, + {"id": 7761, "synset": "flat_panel_display.n.01", "name": "flat_panel_display"}, + {"id": 7762, "synset": "flats.n.01", "name": "flats"}, + {"id": 7763, "synset": "flat_tip_screwdriver.n.01", "name": "flat_tip_screwdriver"}, + { + "id": 7764, + "synset": "fleet_ballistic_missile_submarine.n.01", + "name": "fleet_ballistic_missile_submarine", + }, + {"id": 7765, "synset": "fleur-de-lis.n.02", "name": "fleur-de-lis"}, + {"id": 7766, "synset": "flight_simulator.n.01", "name": "flight_simulator"}, + {"id": 7767, "synset": "flintlock.n.02", "name": "flintlock"}, + {"id": 7768, "synset": "flintlock.n.01", "name": "flintlock"}, + {"id": 7769, "synset": "float.n.05", "name": "float"}, + {"id": 7770, "synset": "floating_dock.n.01", "name": "floating_dock"}, + {"id": 7771, "synset": "floatplane.n.01", "name": "floatplane"}, + {"id": 7772, "synset": "flood.n.03", "name": "flood"}, + {"id": 7773, "synset": "floor.n.01", "name": "floor"}, + {"id": 7774, "synset": "floor.n.02", "name": "floor"}, + {"id": 7775, "synset": "floor.n.09", "name": "floor"}, + {"id": 7776, "synset": "floorboard.n.02", "name": "floorboard"}, + {"id": 7777, "synset": "floor_cover.n.01", "name": "floor_cover"}, + {"id": 7778, "synset": "floor_joist.n.01", "name": "floor_joist"}, + {"id": 7779, "synset": "floor_lamp.n.01", "name": "floor_lamp"}, + {"id": 7780, "synset": "flophouse.n.01", "name": "flophouse"}, + {"id": 7781, "synset": "florist.n.02", "name": "florist"}, + {"id": 7782, "synset": "floss.n.01", "name": "floss"}, + {"id": 7783, "synset": "flotsam.n.01", "name": "flotsam"}, + {"id": 7784, "synset": "flour_bin.n.01", "name": "flour_bin"}, + {"id": 7785, "synset": "flour_mill.n.01", "name": "flour_mill"}, + {"id": 7786, "synset": "flowerbed.n.01", "name": "flowerbed"}, + {"id": 7787, "synset": "flugelhorn.n.01", "name": "flugelhorn"}, + {"id": 7788, "synset": "fluid_drive.n.01", "name": "fluid_drive"}, + {"id": 7789, "synset": "fluid_flywheel.n.01", "name": "fluid_flywheel"}, + {"id": 7790, "synset": "flume.n.02", "name": "flume"}, + {"id": 7791, "synset": "fluorescent_lamp.n.01", "name": "fluorescent_lamp"}, + {"id": 7792, "synset": "fluoroscope.n.01", "name": "fluoroscope"}, + {"id": 7793, "synset": "flush_toilet.n.01", "name": "flush_toilet"}, + {"id": 7794, "synset": "flute.n.01", "name": "flute"}, + {"id": 7795, "synset": "flux_applicator.n.01", "name": "flux_applicator"}, + {"id": 7796, "synset": "fluxmeter.n.01", "name": "fluxmeter"}, + {"id": 7797, "synset": "fly.n.05", "name": "fly"}, + {"id": 7798, "synset": "flying_boat.n.01", "name": "flying_boat"}, + {"id": 7799, "synset": "flying_buttress.n.01", "name": "flying_buttress"}, + {"id": 7800, "synset": "flying_carpet.n.01", "name": "flying_carpet"}, + {"id": 7801, "synset": "flying_jib.n.01", "name": "flying_jib"}, + {"id": 7802, "synset": "fly_rod.n.01", "name": "fly_rod"}, + {"id": 7803, "synset": "fly_tent.n.01", "name": "fly_tent"}, + {"id": 7804, "synset": "flytrap.n.01", "name": "flytrap"}, + {"id": 7805, "synset": "flywheel.n.01", "name": "flywheel"}, + {"id": 7806, "synset": "fob.n.03", "name": "fob"}, + {"id": 7807, "synset": "foghorn.n.02", "name": "foghorn"}, + {"id": 7808, "synset": "foglamp.n.01", "name": "foglamp"}, + {"id": 7809, "synset": "foil.n.05", "name": "foil"}, + {"id": 7810, "synset": "fold.n.06", "name": "fold"}, + {"id": 7811, "synset": "folder.n.02", "name": "folder"}, + {"id": 7812, "synset": "folding_door.n.01", "name": "folding_door"}, + {"id": 7813, "synset": "folding_saw.n.01", "name": "folding_saw"}, + {"id": 7814, "synset": "food_court.n.01", "name": "food_court"}, + {"id": 7815, "synset": "food_hamper.n.01", "name": "food_hamper"}, + {"id": 7816, "synset": "foot.n.11", "name": "foot"}, + {"id": 7817, "synset": "footage.n.01", "name": "footage"}, + {"id": 7818, "synset": "football_stadium.n.01", "name": "football_stadium"}, + {"id": 7819, "synset": "footbath.n.01", "name": "footbath"}, + {"id": 7820, "synset": "foot_brake.n.01", "name": "foot_brake"}, + {"id": 7821, "synset": "footbridge.n.01", "name": "footbridge"}, + {"id": 7822, "synset": "foothold.n.02", "name": "foothold"}, + {"id": 7823, "synset": "footlocker.n.01", "name": "footlocker"}, + {"id": 7824, "synset": "foot_rule.n.01", "name": "foot_rule"}, + {"id": 7825, "synset": "footwear.n.02", "name": "footwear"}, + {"id": 7826, "synset": "footwear.n.01", "name": "footwear"}, + {"id": 7827, "synset": "forceps.n.01", "name": "forceps"}, + {"id": 7828, "synset": "force_pump.n.01", "name": "force_pump"}, + {"id": 7829, "synset": "fore-and-after.n.01", "name": "fore-and-after"}, + {"id": 7830, "synset": "fore-and-aft_sail.n.01", "name": "fore-and-aft_sail"}, + {"id": 7831, "synset": "forecastle.n.01", "name": "forecastle"}, + {"id": 7832, "synset": "forecourt.n.01", "name": "forecourt"}, + {"id": 7833, "synset": "foredeck.n.01", "name": "foredeck"}, + {"id": 7834, "synset": "fore_edge.n.01", "name": "fore_edge"}, + {"id": 7835, "synset": "foreground.n.02", "name": "foreground"}, + {"id": 7836, "synset": "foremast.n.01", "name": "foremast"}, + {"id": 7837, "synset": "fore_plane.n.01", "name": "fore_plane"}, + {"id": 7838, "synset": "foresail.n.01", "name": "foresail"}, + {"id": 7839, "synset": "forestay.n.01", "name": "forestay"}, + {"id": 7840, "synset": "foretop.n.01", "name": "foretop"}, + {"id": 7841, "synset": "fore-topmast.n.01", "name": "fore-topmast"}, + {"id": 7842, "synset": "fore-topsail.n.01", "name": "fore-topsail"}, + {"id": 7843, "synset": "forge.n.01", "name": "forge"}, + {"id": 7844, "synset": "fork.n.04", "name": "fork"}, + {"id": 7845, "synset": "formalwear.n.01", "name": "formalwear"}, + {"id": 7846, "synset": "formica.n.01", "name": "Formica"}, + {"id": 7847, "synset": "fortification.n.01", "name": "fortification"}, + {"id": 7848, "synset": "fortress.n.01", "name": "fortress"}, + {"id": 7849, "synset": "forty-five.n.01", "name": "forty-five"}, + {"id": 7850, "synset": "foucault_pendulum.n.01", "name": "Foucault_pendulum"}, + {"id": 7851, "synset": "foulard.n.01", "name": "foulard"}, + {"id": 7852, "synset": "foul-weather_gear.n.01", "name": "foul-weather_gear"}, + {"id": 7853, "synset": "foundation_garment.n.01", "name": "foundation_garment"}, + {"id": 7854, "synset": "foundry.n.01", "name": "foundry"}, + {"id": 7855, "synset": "fountain.n.01", "name": "fountain"}, + {"id": 7856, "synset": "fountain_pen.n.01", "name": "fountain_pen"}, + {"id": 7857, "synset": "four-in-hand.n.01", "name": "four-in-hand"}, + {"id": 7858, "synset": "four-poster.n.01", "name": "four-poster"}, + {"id": 7859, "synset": "four-pounder.n.01", "name": "four-pounder"}, + {"id": 7860, "synset": "four-stroke_engine.n.01", "name": "four-stroke_engine"}, + {"id": 7861, "synset": "four-wheel_drive.n.02", "name": "four-wheel_drive"}, + {"id": 7862, "synset": "four-wheel_drive.n.01", "name": "four-wheel_drive"}, + {"id": 7863, "synset": "four-wheeler.n.01", "name": "four-wheeler"}, + {"id": 7864, "synset": "fowling_piece.n.01", "name": "fowling_piece"}, + {"id": 7865, "synset": "foxhole.n.01", "name": "foxhole"}, + {"id": 7866, "synset": "fragmentation_bomb.n.01", "name": "fragmentation_bomb"}, + {"id": 7867, "synset": "frail.n.02", "name": "frail"}, + {"id": 7868, "synset": "fraise.n.02", "name": "fraise"}, + {"id": 7869, "synset": "frame.n.10", "name": "frame"}, + {"id": 7870, "synset": "frame.n.01", "name": "frame"}, + {"id": 7871, "synset": "frame_buffer.n.01", "name": "frame_buffer"}, + {"id": 7872, "synset": "framework.n.03", "name": "framework"}, + {"id": 7873, "synset": "francis_turbine.n.01", "name": "Francis_turbine"}, + {"id": 7874, "synset": "franking_machine.n.01", "name": "franking_machine"}, + {"id": 7875, "synset": "free_house.n.01", "name": "free_house"}, + {"id": 7876, "synset": "free-reed.n.01", "name": "free-reed"}, + {"id": 7877, "synset": "free-reed_instrument.n.01", "name": "free-reed_instrument"}, + {"id": 7878, "synset": "freewheel.n.01", "name": "freewheel"}, + {"id": 7879, "synset": "freight_elevator.n.01", "name": "freight_elevator"}, + {"id": 7880, "synset": "freight_liner.n.01", "name": "freight_liner"}, + {"id": 7881, "synset": "freight_train.n.01", "name": "freight_train"}, + {"id": 7882, "synset": "french_door.n.01", "name": "French_door"}, + {"id": 7883, "synset": "french_horn.n.01", "name": "French_horn"}, + {"id": 7884, "synset": "french_polish.n.02", "name": "French_polish"}, + {"id": 7885, "synset": "french_roof.n.01", "name": "French_roof"}, + {"id": 7886, "synset": "french_window.n.01", "name": "French_window"}, + {"id": 7887, "synset": "fresnel_lens.n.01", "name": "Fresnel_lens"}, + {"id": 7888, "synset": "fret.n.04", "name": "fret"}, + {"id": 7889, "synset": "friary.n.01", "name": "friary"}, + {"id": 7890, "synset": "friction_clutch.n.01", "name": "friction_clutch"}, + {"id": 7891, "synset": "frieze.n.02", "name": "frieze"}, + {"id": 7892, "synset": "frieze.n.01", "name": "frieze"}, + {"id": 7893, "synset": "frigate.n.02", "name": "frigate"}, + {"id": 7894, "synset": "frigate.n.01", "name": "frigate"}, + {"id": 7895, "synset": "frill.n.03", "name": "frill"}, + {"id": 7896, "synset": "frock.n.01", "name": "frock"}, + {"id": 7897, "synset": "frock_coat.n.01", "name": "frock_coat"}, + {"id": 7898, "synset": "frontlet.n.01", "name": "frontlet"}, + {"id": 7899, "synset": "front_porch.n.01", "name": "front_porch"}, + {"id": 7900, "synset": "front_projector.n.01", "name": "front_projector"}, + {"id": 7901, "synset": "fruit_machine.n.01", "name": "fruit_machine"}, + {"id": 7902, "synset": "fuel_filter.n.01", "name": "fuel_filter"}, + {"id": 7903, "synset": "fuel_gauge.n.01", "name": "fuel_gauge"}, + {"id": 7904, "synset": "fuel_injection.n.01", "name": "fuel_injection"}, + {"id": 7905, "synset": "fuel_system.n.01", "name": "fuel_system"}, + {"id": 7906, "synset": "full-dress_uniform.n.01", "name": "full-dress_uniform"}, + {"id": 7907, "synset": "full_metal_jacket.n.01", "name": "full_metal_jacket"}, + {"id": 7908, "synset": "full_skirt.n.01", "name": "full_skirt"}, + {"id": 7909, "synset": "fumigator.n.02", "name": "fumigator"}, + {"id": 7910, "synset": "funeral_home.n.01", "name": "funeral_home"}, + {"id": 7911, "synset": "funny_wagon.n.01", "name": "funny_wagon"}, + {"id": 7912, "synset": "fur.n.03", "name": "fur"}, + {"id": 7913, "synset": "fur_coat.n.01", "name": "fur_coat"}, + {"id": 7914, "synset": "fur_hat.n.01", "name": "fur_hat"}, + {"id": 7915, "synset": "furnace.n.01", "name": "furnace"}, + {"id": 7916, "synset": "furnace_lining.n.01", "name": "furnace_lining"}, + {"id": 7917, "synset": "furnace_room.n.01", "name": "furnace_room"}, + {"id": 7918, "synset": "furnishing.n.02", "name": "furnishing"}, + {"id": 7919, "synset": "furnishing.n.01", "name": "furnishing"}, + {"id": 7920, "synset": "furniture.n.01", "name": "furniture"}, + {"id": 7921, "synset": "fur-piece.n.01", "name": "fur-piece"}, + {"id": 7922, "synset": "furrow.n.01", "name": "furrow"}, + {"id": 7923, "synset": "fuse.n.01", "name": "fuse"}, + {"id": 7924, "synset": "fusee_drive.n.01", "name": "fusee_drive"}, + {"id": 7925, "synset": "fuselage.n.01", "name": "fuselage"}, + {"id": 7926, "synset": "fusil.n.01", "name": "fusil"}, + {"id": 7927, "synset": "fustian.n.02", "name": "fustian"}, + {"id": 7928, "synset": "gabardine.n.01", "name": "gabardine"}, + {"id": 7929, "synset": "gable.n.01", "name": "gable"}, + {"id": 7930, "synset": "gable_roof.n.01", "name": "gable_roof"}, + {"id": 7931, "synset": "gadgetry.n.01", "name": "gadgetry"}, + {"id": 7932, "synset": "gaff.n.03", "name": "gaff"}, + {"id": 7933, "synset": "gaff.n.02", "name": "gaff"}, + {"id": 7934, "synset": "gaff.n.01", "name": "gaff"}, + {"id": 7935, "synset": "gaffsail.n.01", "name": "gaffsail"}, + {"id": 7936, "synset": "gaff_topsail.n.01", "name": "gaff_topsail"}, + {"id": 7937, "synset": "gaiter.n.03", "name": "gaiter"}, + {"id": 7938, "synset": "gaiter.n.02", "name": "gaiter"}, + {"id": 7939, "synset": "galilean_telescope.n.01", "name": "Galilean_telescope"}, + {"id": 7940, "synset": "galleon.n.01", "name": "galleon"}, + {"id": 7941, "synset": "gallery.n.04", "name": "gallery"}, + {"id": 7942, "synset": "gallery.n.03", "name": "gallery"}, + {"id": 7943, "synset": "galley.n.04", "name": "galley"}, + {"id": 7944, "synset": "galley.n.03", "name": "galley"}, + {"id": 7945, "synset": "galley.n.02", "name": "galley"}, + {"id": 7946, "synset": "gallows.n.01", "name": "gallows"}, + {"id": 7947, "synset": "gallows_tree.n.01", "name": "gallows_tree"}, + {"id": 7948, "synset": "galvanometer.n.01", "name": "galvanometer"}, + {"id": 7949, "synset": "gambling_house.n.01", "name": "gambling_house"}, + {"id": 7950, "synset": "gambrel.n.01", "name": "gambrel"}, + {"id": 7951, "synset": "game.n.09", "name": "game"}, + {"id": 7952, "synset": "gamebag.n.01", "name": "gamebag"}, + {"id": 7953, "synset": "game_equipment.n.01", "name": "game_equipment"}, + {"id": 7954, "synset": "gaming_table.n.01", "name": "gaming_table"}, + {"id": 7955, "synset": "gamp.n.01", "name": "gamp"}, + {"id": 7956, "synset": "gangplank.n.01", "name": "gangplank"}, + {"id": 7957, "synset": "gangsaw.n.01", "name": "gangsaw"}, + {"id": 7958, "synset": "gangway.n.01", "name": "gangway"}, + {"id": 7959, "synset": "gantlet.n.04", "name": "gantlet"}, + {"id": 7960, "synset": "gantry.n.01", "name": "gantry"}, + {"id": 7961, "synset": "garage.n.01", "name": "garage"}, + {"id": 7962, "synset": "garage.n.02", "name": "garage"}, + {"id": 7963, "synset": "garand_rifle.n.01", "name": "Garand_rifle"}, + {"id": 7964, "synset": "garboard.n.01", "name": "garboard"}, + {"id": 7965, "synset": "garden.n.01", "name": "garden"}, + {"id": 7966, "synset": "garden.n.03", "name": "garden"}, + {"id": 7967, "synset": "garden_rake.n.01", "name": "garden_rake"}, + {"id": 7968, "synset": "garden_spade.n.01", "name": "garden_spade"}, + {"id": 7969, "synset": "garden_tool.n.01", "name": "garden_tool"}, + {"id": 7970, "synset": "garden_trowel.n.01", "name": "garden_trowel"}, + {"id": 7971, "synset": "gargoyle.n.01", "name": "gargoyle"}, + {"id": 7972, "synset": "garibaldi.n.02", "name": "garibaldi"}, + {"id": 7973, "synset": "garlic_press.n.01", "name": "garlic_press"}, + {"id": 7974, "synset": "garment.n.01", "name": "garment"}, + {"id": 7975, "synset": "garment_bag.n.01", "name": "garment_bag"}, + {"id": 7976, "synset": "garrison_cap.n.01", "name": "garrison_cap"}, + {"id": 7977, "synset": "garrote.n.01", "name": "garrote"}, + {"id": 7978, "synset": "garter.n.01", "name": "garter"}, + {"id": 7979, "synset": "garter_belt.n.01", "name": "garter_belt"}, + {"id": 7980, "synset": "garter_stitch.n.01", "name": "garter_stitch"}, + {"id": 7981, "synset": "gas_guzzler.n.01", "name": "gas_guzzler"}, + {"id": 7982, "synset": "gas_shell.n.01", "name": "gas_shell"}, + {"id": 7983, "synset": "gas_bracket.n.01", "name": "gas_bracket"}, + {"id": 7984, "synset": "gas_burner.n.01", "name": "gas_burner"}, + {"id": 7985, "synset": "gas-cooled_reactor.n.01", "name": "gas-cooled_reactor"}, + {"id": 7986, "synset": "gas-discharge_tube.n.01", "name": "gas-discharge_tube"}, + {"id": 7987, "synset": "gas_engine.n.01", "name": "gas_engine"}, + {"id": 7988, "synset": "gas_fixture.n.01", "name": "gas_fixture"}, + {"id": 7989, "synset": "gas_furnace.n.01", "name": "gas_furnace"}, + {"id": 7990, "synset": "gas_gun.n.01", "name": "gas_gun"}, + {"id": 7991, "synset": "gas_heater.n.01", "name": "gas_heater"}, + {"id": 7992, "synset": "gas_holder.n.01", "name": "gas_holder"}, + {"id": 7993, "synset": "gasket.n.01", "name": "gasket"}, + {"id": 7994, "synset": "gas_lamp.n.01", "name": "gas_lamp"}, + {"id": 7995, "synset": "gas_maser.n.01", "name": "gas_maser"}, + {"id": 7996, "synset": "gas_meter.n.01", "name": "gas_meter"}, + {"id": 7997, "synset": "gasoline_engine.n.01", "name": "gasoline_engine"}, + {"id": 7998, "synset": "gasoline_gauge.n.01", "name": "gasoline_gauge"}, + {"id": 7999, "synset": "gas_oven.n.02", "name": "gas_oven"}, + {"id": 8000, "synset": "gas_oven.n.01", "name": "gas_oven"}, + {"id": 8001, "synset": "gas_pump.n.01", "name": "gas_pump"}, + {"id": 8002, "synset": "gas_range.n.01", "name": "gas_range"}, + {"id": 8003, "synset": "gas_ring.n.01", "name": "gas_ring"}, + {"id": 8004, "synset": "gas_tank.n.01", "name": "gas_tank"}, + {"id": 8005, "synset": "gas_thermometer.n.01", "name": "gas_thermometer"}, + {"id": 8006, "synset": "gastroscope.n.01", "name": "gastroscope"}, + {"id": 8007, "synset": "gas_turbine.n.01", "name": "gas_turbine"}, + {"id": 8008, "synset": "gas-turbine_ship.n.01", "name": "gas-turbine_ship"}, + {"id": 8009, "synset": "gat.n.01", "name": "gat"}, + {"id": 8010, "synset": "gate.n.01", "name": "gate"}, + {"id": 8011, "synset": "gatehouse.n.01", "name": "gatehouse"}, + {"id": 8012, "synset": "gateleg_table.n.01", "name": "gateleg_table"}, + {"id": 8013, "synset": "gatepost.n.01", "name": "gatepost"}, + {"id": 8014, "synset": "gathered_skirt.n.01", "name": "gathered_skirt"}, + {"id": 8015, "synset": "gatling_gun.n.01", "name": "Gatling_gun"}, + {"id": 8016, "synset": "gauge.n.01", "name": "gauge"}, + {"id": 8017, "synset": "gauntlet.n.03", "name": "gauntlet"}, + {"id": 8018, "synset": "gauntlet.n.02", "name": "gauntlet"}, + {"id": 8019, "synset": "gauze.n.02", "name": "gauze"}, + {"id": 8020, "synset": "gauze.n.01", "name": "gauze"}, + {"id": 8021, "synset": "gavel.n.01", "name": "gavel"}, + {"id": 8022, "synset": "gazebo.n.01", "name": "gazebo"}, + {"id": 8023, "synset": "gear.n.01", "name": "gear"}, + {"id": 8024, "synset": "gear.n.04", "name": "gear"}, + {"id": 8025, "synset": "gear.n.03", "name": "gear"}, + {"id": 8026, "synset": "gearbox.n.01", "name": "gearbox"}, + {"id": 8027, "synset": "gearing.n.01", "name": "gearing"}, + {"id": 8028, "synset": "gearset.n.01", "name": "gearset"}, + {"id": 8029, "synset": "gearshift.n.01", "name": "gearshift"}, + {"id": 8030, "synset": "geiger_counter.n.01", "name": "Geiger_counter"}, + {"id": 8031, "synset": "geiger_tube.n.01", "name": "Geiger_tube"}, + {"id": 8032, "synset": "gene_chip.n.01", "name": "gene_chip"}, + {"id": 8033, "synset": "general-purpose_bomb.n.01", "name": "general-purpose_bomb"}, + {"id": 8034, "synset": "generator.n.01", "name": "generator"}, + {"id": 8035, "synset": "generator.n.04", "name": "generator"}, + {"id": 8036, "synset": "geneva_gown.n.01", "name": "Geneva_gown"}, + {"id": 8037, "synset": "geodesic_dome.n.01", "name": "geodesic_dome"}, + {"id": 8038, "synset": "georgette.n.01", "name": "georgette"}, + {"id": 8039, "synset": "gharry.n.01", "name": "gharry"}, + {"id": 8040, "synset": "ghat.n.01", "name": "ghat"}, + {"id": 8041, "synset": "ghetto_blaster.n.01", "name": "ghetto_blaster"}, + {"id": 8042, "synset": "gift_shop.n.01", "name": "gift_shop"}, + {"id": 8043, "synset": "gift_wrapping.n.01", "name": "gift_wrapping"}, + {"id": 8044, "synset": "gig.n.05", "name": "gig"}, + {"id": 8045, "synset": "gig.n.04", "name": "gig"}, + {"id": 8046, "synset": "gig.n.01", "name": "gig"}, + {"id": 8047, "synset": "gig.n.03", "name": "gig"}, + {"id": 8048, "synset": "gildhall.n.01", "name": "gildhall"}, + {"id": 8049, "synset": "gill_net.n.01", "name": "gill_net"}, + {"id": 8050, "synset": "gilt.n.01", "name": "gilt"}, + {"id": 8051, "synset": "gimbal.n.01", "name": "gimbal"}, + {"id": 8052, "synset": "gingham.n.01", "name": "gingham"}, + {"id": 8053, "synset": "girandole.n.01", "name": "girandole"}, + {"id": 8054, "synset": "girder.n.01", "name": "girder"}, + {"id": 8055, "synset": "glass.n.07", "name": "glass"}, + {"id": 8056, "synset": "glass_cutter.n.03", "name": "glass_cutter"}, + {"id": 8057, "synset": "glasses_case.n.01", "name": "glasses_case"}, + {"id": 8058, "synset": "glebe_house.n.01", "name": "glebe_house"}, + {"id": 8059, "synset": "glengarry.n.01", "name": "Glengarry"}, + {"id": 8060, "synset": "glider.n.01", "name": "glider"}, + {"id": 8061, "synset": "global_positioning_system.n.01", "name": "Global_Positioning_System"}, + {"id": 8062, "synset": "glockenspiel.n.01", "name": "glockenspiel"}, + {"id": 8063, "synset": "glory_hole.n.01", "name": "glory_hole"}, + {"id": 8064, "synset": "glove_compartment.n.01", "name": "glove_compartment"}, + {"id": 8065, "synset": "glow_lamp.n.01", "name": "glow_lamp"}, + {"id": 8066, "synset": "glow_tube.n.01", "name": "glow_tube"}, + {"id": 8067, "synset": "glyptic_art.n.01", "name": "glyptic_art"}, + {"id": 8068, "synset": "glyptics.n.01", "name": "glyptics"}, + {"id": 8069, "synset": "gnomon.n.01", "name": "gnomon"}, + {"id": 8070, "synset": "goal.n.03", "name": "goal"}, + {"id": 8071, "synset": "goalmouth.n.01", "name": "goalmouth"}, + {"id": 8072, "synset": "goalpost.n.01", "name": "goalpost"}, + {"id": 8073, "synset": "goblet.n.01", "name": "goblet"}, + {"id": 8074, "synset": "godown.n.01", "name": "godown"}, + {"id": 8075, "synset": "go-kart.n.01", "name": "go-kart"}, + {"id": 8076, "synset": "gold_plate.n.02", "name": "gold_plate"}, + {"id": 8077, "synset": "golf_bag.n.01", "name": "golf_bag"}, + {"id": 8078, "synset": "golf_ball.n.01", "name": "golf_ball"}, + {"id": 8079, "synset": "golf-club_head.n.01", "name": "golf-club_head"}, + {"id": 8080, "synset": "golf_equipment.n.01", "name": "golf_equipment"}, + {"id": 8081, "synset": "golf_glove.n.01", "name": "golf_glove"}, + {"id": 8082, "synset": "golliwog.n.01", "name": "golliwog"}, + {"id": 8083, "synset": "gong.n.01", "name": "gong"}, + {"id": 8084, "synset": "goniometer.n.01", "name": "goniometer"}, + {"id": 8085, "synset": "gordian_knot.n.02", "name": "Gordian_knot"}, + {"id": 8086, "synset": "gorget.n.01", "name": "gorget"}, + {"id": 8087, "synset": "gossamer.n.01", "name": "gossamer"}, + {"id": 8088, "synset": "gothic_arch.n.01", "name": "Gothic_arch"}, + {"id": 8089, "synset": "gouache.n.01", "name": "gouache"}, + {"id": 8090, "synset": "gouge.n.02", "name": "gouge"}, + {"id": 8091, "synset": "gourd.n.01", "name": "gourd"}, + {"id": 8092, "synset": "government_building.n.01", "name": "government_building"}, + {"id": 8093, "synset": "government_office.n.01", "name": "government_office"}, + {"id": 8094, "synset": "gown.n.01", "name": "gown"}, + {"id": 8095, "synset": "gown.n.05", "name": "gown"}, + {"id": 8096, "synset": "gown.n.04", "name": "gown"}, + {"id": 8097, "synset": "grab.n.01", "name": "grab"}, + {"id": 8098, "synset": "grab_bag.n.02", "name": "grab_bag"}, + {"id": 8099, "synset": "grab_bar.n.01", "name": "grab_bar"}, + {"id": 8100, "synset": "grace_cup.n.01", "name": "grace_cup"}, + {"id": 8101, "synset": "grade_separation.n.01", "name": "grade_separation"}, + {"id": 8102, "synset": "graduated_cylinder.n.01", "name": "graduated_cylinder"}, + {"id": 8103, "synset": "graffito.n.01", "name": "graffito"}, + {"id": 8104, "synset": "gramophone.n.01", "name": "gramophone"}, + {"id": 8105, "synset": "granary.n.01", "name": "granary"}, + {"id": 8106, "synset": "grandfather_clock.n.01", "name": "grandfather_clock"}, + {"id": 8107, "synset": "grand_piano.n.01", "name": "grand_piano"}, + {"id": 8108, "synset": "graniteware.n.01", "name": "graniteware"}, + {"id": 8109, "synset": "granny_knot.n.01", "name": "granny_knot"}, + {"id": 8110, "synset": "grape_arbor.n.01", "name": "grape_arbor"}, + {"id": 8111, "synset": "grapnel.n.02", "name": "grapnel"}, + {"id": 8112, "synset": "grapnel.n.01", "name": "grapnel"}, + {"id": 8113, "synset": "grass_skirt.n.01", "name": "grass_skirt"}, + {"id": 8114, "synset": "grate.n.01", "name": "grate"}, + {"id": 8115, "synset": "grate.n.03", "name": "grate"}, + {"id": 8116, "synset": "graver.n.01", "name": "graver"}, + {"id": 8117, "synset": "gravimeter.n.02", "name": "gravimeter"}, + {"id": 8118, "synset": "gravure.n.03", "name": "gravure"}, + {"id": 8119, "synset": "grey.n.06", "name": "grey"}, + {"id": 8120, "synset": "grease-gun.n.01", "name": "grease-gun"}, + {"id": 8121, "synset": "greasepaint.n.01", "name": "greasepaint"}, + {"id": 8122, "synset": "greasy_spoon.n.01", "name": "greasy_spoon"}, + {"id": 8123, "synset": "greatcoat.n.01", "name": "greatcoat"}, + {"id": 8124, "synset": "great_hall.n.01", "name": "great_hall"}, + {"id": 8125, "synset": "greave.n.01", "name": "greave"}, + {"id": 8126, "synset": "greengrocery.n.02", "name": "greengrocery"}, + {"id": 8127, "synset": "greenhouse.n.01", "name": "greenhouse"}, + {"id": 8128, "synset": "grenade.n.01", "name": "grenade"}, + {"id": 8129, "synset": "grid.n.05", "name": "grid"}, + {"id": 8130, "synset": "grille.n.02", "name": "grille"}, + {"id": 8131, "synset": "grillroom.n.01", "name": "grillroom"}, + {"id": 8132, "synset": "grinder.n.04", "name": "grinder"}, + {"id": 8133, "synset": "grinding_wheel.n.01", "name": "grinding_wheel"}, + {"id": 8134, "synset": "grindstone.n.01", "name": "grindstone"}, + {"id": 8135, "synset": "gripsack.n.01", "name": "gripsack"}, + {"id": 8136, "synset": "gristmill.n.01", "name": "gristmill"}, + {"id": 8137, "synset": "grocery_store.n.01", "name": "grocery_store"}, + {"id": 8138, "synset": "grogram.n.01", "name": "grogram"}, + {"id": 8139, "synset": "groined_vault.n.01", "name": "groined_vault"}, + {"id": 8140, "synset": "groover.n.01", "name": "groover"}, + {"id": 8141, "synset": "grosgrain.n.01", "name": "grosgrain"}, + {"id": 8142, "synset": "gros_point.n.01", "name": "gros_point"}, + {"id": 8143, "synset": "ground.n.09", "name": "ground"}, + {"id": 8144, "synset": "ground_bait.n.01", "name": "ground_bait"}, + {"id": 8145, "synset": "ground_control.n.01", "name": "ground_control"}, + {"id": 8146, "synset": "ground_floor.n.01", "name": "ground_floor"}, + {"id": 8147, "synset": "groundsheet.n.01", "name": "groundsheet"}, + {"id": 8148, "synset": "g-string.n.01", "name": "G-string"}, + {"id": 8149, "synset": "guard.n.03", "name": "guard"}, + {"id": 8150, "synset": "guard_boat.n.01", "name": "guard_boat"}, + {"id": 8151, "synset": "guardroom.n.02", "name": "guardroom"}, + {"id": 8152, "synset": "guardroom.n.01", "name": "guardroom"}, + {"id": 8153, "synset": "guard_ship.n.01", "name": "guard_ship"}, + {"id": 8154, "synset": "guard's_van.n.01", "name": "guard's_van"}, + {"id": 8155, "synset": "gueridon.n.01", "name": "gueridon"}, + {"id": 8156, "synset": "guarnerius.n.03", "name": "Guarnerius"}, + {"id": 8157, "synset": "guesthouse.n.01", "name": "guesthouse"}, + {"id": 8158, "synset": "guestroom.n.01", "name": "guestroom"}, + {"id": 8159, "synset": "guidance_system.n.01", "name": "guidance_system"}, + {"id": 8160, "synset": "guided_missile.n.01", "name": "guided_missile"}, + {"id": 8161, "synset": "guided_missile_cruiser.n.01", "name": "guided_missile_cruiser"}, + {"id": 8162, "synset": "guided_missile_frigate.n.01", "name": "guided_missile_frigate"}, + {"id": 8163, "synset": "guildhall.n.01", "name": "guildhall"}, + {"id": 8164, "synset": "guilloche.n.01", "name": "guilloche"}, + {"id": 8165, "synset": "guillotine.n.02", "name": "guillotine"}, + {"id": 8166, "synset": "guimpe.n.02", "name": "guimpe"}, + {"id": 8167, "synset": "guimpe.n.01", "name": "guimpe"}, + {"id": 8168, "synset": "guitar_pick.n.01", "name": "guitar_pick"}, + {"id": 8169, "synset": "gulag.n.01", "name": "gulag"}, + {"id": 8170, "synset": "gunboat.n.01", "name": "gunboat"}, + {"id": 8171, "synset": "gun_carriage.n.01", "name": "gun_carriage"}, + {"id": 8172, "synset": "gun_case.n.01", "name": "gun_case"}, + {"id": 8173, "synset": "gun_emplacement.n.01", "name": "gun_emplacement"}, + {"id": 8174, "synset": "gun_enclosure.n.01", "name": "gun_enclosure"}, + {"id": 8175, "synset": "gunlock.n.01", "name": "gunlock"}, + {"id": 8176, "synset": "gunnery.n.01", "name": "gunnery"}, + {"id": 8177, "synset": "gunnysack.n.01", "name": "gunnysack"}, + {"id": 8178, "synset": "gun_pendulum.n.01", "name": "gun_pendulum"}, + {"id": 8179, "synset": "gun_room.n.01", "name": "gun_room"}, + {"id": 8180, "synset": "gunsight.n.01", "name": "gunsight"}, + {"id": 8181, "synset": "gun_trigger.n.01", "name": "gun_trigger"}, + {"id": 8182, "synset": "gurney.n.01", "name": "gurney"}, + {"id": 8183, "synset": "gusher.n.01", "name": "gusher"}, + {"id": 8184, "synset": "gusset.n.03", "name": "gusset"}, + {"id": 8185, "synset": "gusset.n.02", "name": "gusset"}, + {"id": 8186, "synset": "guy.n.03", "name": "guy"}, + {"id": 8187, "synset": "gymnastic_apparatus.n.01", "name": "gymnastic_apparatus"}, + {"id": 8188, "synset": "gym_shoe.n.01", "name": "gym_shoe"}, + {"id": 8189, "synset": "gym_suit.n.01", "name": "gym_suit"}, + {"id": 8190, "synset": "gymslip.n.01", "name": "gymslip"}, + {"id": 8191, "synset": "gypsy_cab.n.01", "name": "gypsy_cab"}, + {"id": 8192, "synset": "gyrocompass.n.01", "name": "gyrocompass"}, + {"id": 8193, "synset": "gyroscope.n.01", "name": "gyroscope"}, + {"id": 8194, "synset": "gyrostabilizer.n.01", "name": "gyrostabilizer"}, + {"id": 8195, "synset": "habergeon.n.01", "name": "habergeon"}, + {"id": 8196, "synset": "habit.n.03", "name": "habit"}, + {"id": 8197, "synset": "habit.n.05", "name": "habit"}, + {"id": 8198, "synset": "hacienda.n.02", "name": "hacienda"}, + {"id": 8199, "synset": "hacksaw.n.01", "name": "hacksaw"}, + {"id": 8200, "synset": "haft.n.01", "name": "haft"}, + {"id": 8201, "synset": "haircloth.n.01", "name": "haircloth"}, + {"id": 8202, "synset": "hairdressing.n.01", "name": "hairdressing"}, + {"id": 8203, "synset": "hairpiece.n.01", "name": "hairpiece"}, + {"id": 8204, "synset": "hair_shirt.n.01", "name": "hair_shirt"}, + {"id": 8205, "synset": "hair_slide.n.01", "name": "hair_slide"}, + {"id": 8206, "synset": "hair_spray.n.01", "name": "hair_spray"}, + {"id": 8207, "synset": "hairspring.n.01", "name": "hairspring"}, + {"id": 8208, "synset": "hair_trigger.n.01", "name": "hair_trigger"}, + {"id": 8209, "synset": "halberd.n.01", "name": "halberd"}, + {"id": 8210, "synset": "half_binding.n.01", "name": "half_binding"}, + {"id": 8211, "synset": "half_hatchet.n.01", "name": "half_hatchet"}, + {"id": 8212, "synset": "half_hitch.n.01", "name": "half_hitch"}, + {"id": 8213, "synset": "half_track.n.01", "name": "half_track"}, + {"id": 8214, "synset": "hall.n.13", "name": "hall"}, + {"id": 8215, "synset": "hall.n.03", "name": "hall"}, + {"id": 8216, "synset": "hall.n.12", "name": "hall"}, + {"id": 8217, "synset": "hall_of_fame.n.01", "name": "Hall_of_Fame"}, + {"id": 8218, "synset": "hall_of_residence.n.01", "name": "hall_of_residence"}, + {"id": 8219, "synset": "hallstand.n.01", "name": "hallstand"}, + {"id": 8220, "synset": "halter.n.01", "name": "halter"}, + {"id": 8221, "synset": "hame.n.01", "name": "hame"}, + {"id": 8222, "synset": "hammer.n.07", "name": "hammer"}, + {"id": 8223, "synset": "hammer.n.05", "name": "hammer"}, + {"id": 8224, "synset": "hammerhead.n.02", "name": "hammerhead"}, + {"id": 8225, "synset": "hand.n.08", "name": "hand"}, + {"id": 8226, "synset": "handball.n.01", "name": "handball"}, + {"id": 8227, "synset": "handbarrow.n.01", "name": "handbarrow"}, + {"id": 8228, "synset": "handbell.n.01", "name": "handbell"}, + {"id": 8229, "synset": "handbow.n.01", "name": "handbow"}, + {"id": 8230, "synset": "hand_brake.n.01", "name": "hand_brake"}, + {"id": 8231, "synset": "hand_calculator.n.01", "name": "hand_calculator"}, + {"id": 8232, "synset": "handcar.n.01", "name": "handcar"}, + {"id": 8233, "synset": "hand_cream.n.01", "name": "hand_cream"}, + {"id": 8234, "synset": "hand_drill.n.01", "name": "hand_drill"}, + {"id": 8235, "synset": "hand_glass.n.02", "name": "hand_glass"}, + {"id": 8236, "synset": "hand_grenade.n.01", "name": "hand_grenade"}, + {"id": 8237, "synset": "hand-held_computer.n.01", "name": "hand-held_computer"}, + {"id": 8238, "synset": "handhold.n.01", "name": "handhold"}, + {"id": 8239, "synset": "handlebar.n.01", "name": "handlebar"}, + {"id": 8240, "synset": "handloom.n.01", "name": "handloom"}, + {"id": 8241, "synset": "hand_lotion.n.01", "name": "hand_lotion"}, + {"id": 8242, "synset": "hand_luggage.n.01", "name": "hand_luggage"}, + {"id": 8243, "synset": "hand-me-down.n.01", "name": "hand-me-down"}, + {"id": 8244, "synset": "hand_mower.n.01", "name": "hand_mower"}, + {"id": 8245, "synset": "hand_pump.n.01", "name": "hand_pump"}, + {"id": 8246, "synset": "handrest.n.01", "name": "handrest"}, + {"id": 8247, "synset": "handset.n.01", "name": "handset"}, + {"id": 8248, "synset": "hand_shovel.n.01", "name": "hand_shovel"}, + {"id": 8249, "synset": "handspike.n.01", "name": "handspike"}, + {"id": 8250, "synset": "handstamp.n.01", "name": "handstamp"}, + {"id": 8251, "synset": "hand_throttle.n.01", "name": "hand_throttle"}, + {"id": 8252, "synset": "hand_tool.n.01", "name": "hand_tool"}, + {"id": 8253, "synset": "hand_truck.n.01", "name": "hand_truck"}, + {"id": 8254, "synset": "handwear.n.01", "name": "handwear"}, + {"id": 8255, "synset": "handwheel.n.02", "name": "handwheel"}, + {"id": 8256, "synset": "handwheel.n.01", "name": "handwheel"}, + {"id": 8257, "synset": "hangar_queen.n.01", "name": "hangar_queen"}, + {"id": 8258, "synset": "hanger.n.02", "name": "hanger"}, + {"id": 8259, "synset": "hang_glider.n.02", "name": "hang_glider"}, + {"id": 8260, "synset": "hangman's_rope.n.01", "name": "hangman's_rope"}, + {"id": 8261, "synset": "hank.n.01", "name": "hank"}, + {"id": 8262, "synset": "hansom.n.01", "name": "hansom"}, + {"id": 8263, "synset": "harbor.n.02", "name": "harbor"}, + {"id": 8264, "synset": "hard_disc.n.01", "name": "hard_disc"}, + {"id": 8265, "synset": "hard_hat.n.02", "name": "hard_hat"}, + {"id": 8266, "synset": "hardtop.n.01", "name": "hardtop"}, + {"id": 8267, "synset": "hardware.n.02", "name": "hardware"}, + {"id": 8268, "synset": "hardware_store.n.01", "name": "hardware_store"}, + {"id": 8269, "synset": "harmonica.n.01", "name": "harmonica"}, + {"id": 8270, "synset": "harness.n.02", "name": "harness"}, + {"id": 8271, "synset": "harness.n.01", "name": "harness"}, + {"id": 8272, "synset": "harp.n.01", "name": "harp"}, + {"id": 8273, "synset": "harp.n.02", "name": "harp"}, + {"id": 8274, "synset": "harpoon.n.01", "name": "harpoon"}, + {"id": 8275, "synset": "harpoon_gun.n.01", "name": "harpoon_gun"}, + {"id": 8276, "synset": "harpoon_log.n.01", "name": "harpoon_log"}, + {"id": 8277, "synset": "harpsichord.n.01", "name": "harpsichord"}, + {"id": 8278, "synset": "harris_tweed.n.01", "name": "Harris_Tweed"}, + {"id": 8279, "synset": "harrow.n.01", "name": "harrow"}, + {"id": 8280, "synset": "harvester.n.02", "name": "harvester"}, + {"id": 8281, "synset": "hash_house.n.01", "name": "hash_house"}, + {"id": 8282, "synset": "hasp.n.01", "name": "hasp"}, + {"id": 8283, "synset": "hatch.n.03", "name": "hatch"}, + {"id": 8284, "synset": "hatchback.n.02", "name": "hatchback"}, + {"id": 8285, "synset": "hatchback.n.01", "name": "hatchback"}, + {"id": 8286, "synset": "hatchel.n.01", "name": "hatchel"}, + {"id": 8287, "synset": "hatchet.n.02", "name": "hatchet"}, + {"id": 8288, "synset": "hatpin.n.01", "name": "hatpin"}, + {"id": 8289, "synset": "hauberk.n.01", "name": "hauberk"}, + {"id": 8290, "synset": "hawaiian_guitar.n.01", "name": "Hawaiian_guitar"}, + {"id": 8291, "synset": "hawse.n.01", "name": "hawse"}, + {"id": 8292, "synset": "hawser.n.01", "name": "hawser"}, + {"id": 8293, "synset": "hawser_bend.n.01", "name": "hawser_bend"}, + {"id": 8294, "synset": "hay_bale.n.01", "name": "hay_bale"}, + {"id": 8295, "synset": "hayfork.n.01", "name": "hayfork"}, + {"id": 8296, "synset": "hayloft.n.01", "name": "hayloft"}, + {"id": 8297, "synset": "haymaker.n.01", "name": "haymaker"}, + {"id": 8298, "synset": "hayrack.n.02", "name": "hayrack"}, + {"id": 8299, "synset": "hayrack.n.01", "name": "hayrack"}, + {"id": 8300, "synset": "hazard.n.03", "name": "hazard"}, + {"id": 8301, "synset": "head.n.31", "name": "head"}, + {"id": 8302, "synset": "head.n.30", "name": "head"}, + {"id": 8303, "synset": "head.n.29", "name": "head"}, + {"id": 8304, "synset": "headdress.n.01", "name": "headdress"}, + {"id": 8305, "synset": "header.n.05", "name": "header"}, + {"id": 8306, "synset": "header.n.04", "name": "header"}, + {"id": 8307, "synset": "header.n.03", "name": "header"}, + {"id": 8308, "synset": "header.n.02", "name": "header"}, + {"id": 8309, "synset": "headfast.n.01", "name": "headfast"}, + {"id": 8310, "synset": "head_gasket.n.01", "name": "head_gasket"}, + {"id": 8311, "synset": "head_gate.n.02", "name": "head_gate"}, + {"id": 8312, "synset": "headgear.n.03", "name": "headgear"}, + {"id": 8313, "synset": "headpiece.n.02", "name": "headpiece"}, + {"id": 8314, "synset": "headpin.n.01", "name": "headpin"}, + {"id": 8315, "synset": "headquarters.n.01", "name": "headquarters"}, + {"id": 8316, "synset": "headrace.n.01", "name": "headrace"}, + {"id": 8317, "synset": "headrest.n.02", "name": "headrest"}, + {"id": 8318, "synset": "headsail.n.01", "name": "headsail"}, + {"id": 8319, "synset": "head_shop.n.01", "name": "head_shop"}, + {"id": 8320, "synset": "headstock.n.01", "name": "headstock"}, + {"id": 8321, "synset": "health_spa.n.01", "name": "health_spa"}, + {"id": 8322, "synset": "hearing_aid.n.02", "name": "hearing_aid"}, + {"id": 8323, "synset": "hearing_aid.n.01", "name": "hearing_aid"}, + {"id": 8324, "synset": "hearse.n.01", "name": "hearse"}, + {"id": 8325, "synset": "hearth.n.02", "name": "hearth"}, + {"id": 8326, "synset": "hearthrug.n.01", "name": "hearthrug"}, + {"id": 8327, "synset": "heart-lung_machine.n.01", "name": "heart-lung_machine"}, + {"id": 8328, "synset": "heat_engine.n.01", "name": "heat_engine"}, + {"id": 8329, "synset": "heat_exchanger.n.01", "name": "heat_exchanger"}, + {"id": 8330, "synset": "heating_pad.n.01", "name": "heating_pad"}, + {"id": 8331, "synset": "heat_lamp.n.01", "name": "heat_lamp"}, + {"id": 8332, "synset": "heat_pump.n.01", "name": "heat_pump"}, + {"id": 8333, "synset": "heat-seeking_missile.n.01", "name": "heat-seeking_missile"}, + {"id": 8334, "synset": "heat_shield.n.01", "name": "heat_shield"}, + {"id": 8335, "synset": "heat_sink.n.01", "name": "heat_sink"}, + {"id": 8336, "synset": "heaume.n.01", "name": "heaume"}, + {"id": 8337, "synset": "heaver.n.01", "name": "heaver"}, + {"id": 8338, "synset": "heavier-than-air_craft.n.01", "name": "heavier-than-air_craft"}, + {"id": 8339, "synset": "heckelphone.n.01", "name": "heckelphone"}, + {"id": 8340, "synset": "hectograph.n.01", "name": "hectograph"}, + {"id": 8341, "synset": "hedge.n.01", "name": "hedge"}, + {"id": 8342, "synset": "hedge_trimmer.n.01", "name": "hedge_trimmer"}, + {"id": 8343, "synset": "helicon.n.01", "name": "helicon"}, + {"id": 8344, "synset": "heliograph.n.01", "name": "heliograph"}, + {"id": 8345, "synset": "heliometer.n.01", "name": "heliometer"}, + {"id": 8346, "synset": "helm.n.01", "name": "helm"}, + {"id": 8347, "synset": "helmet.n.01", "name": "helmet"}, + {"id": 8348, "synset": "hematocrit.n.02", "name": "hematocrit"}, + {"id": 8349, "synset": "hemming-stitch.n.01", "name": "hemming-stitch"}, + {"id": 8350, "synset": "hemostat.n.01", "name": "hemostat"}, + {"id": 8351, "synset": "hemstitch.n.01", "name": "hemstitch"}, + {"id": 8352, "synset": "henroost.n.01", "name": "henroost"}, + {"id": 8353, "synset": "heraldry.n.02", "name": "heraldry"}, + {"id": 8354, "synset": "hermitage.n.01", "name": "hermitage"}, + {"id": 8355, "synset": "herringbone.n.01", "name": "herringbone"}, + {"id": 8356, "synset": "herringbone.n.02", "name": "herringbone"}, + {"id": 8357, "synset": "herschelian_telescope.n.01", "name": "Herschelian_telescope"}, + {"id": 8358, "synset": "hessian_boot.n.01", "name": "Hessian_boot"}, + {"id": 8359, "synset": "heterodyne_receiver.n.01", "name": "heterodyne_receiver"}, + {"id": 8360, "synset": "hibachi.n.01", "name": "hibachi"}, + {"id": 8361, "synset": "hideaway.n.02", "name": "hideaway"}, + {"id": 8362, "synset": "hi-fi.n.01", "name": "hi-fi"}, + {"id": 8363, "synset": "high_altar.n.01", "name": "high_altar"}, + {"id": 8364, "synset": "high-angle_gun.n.01", "name": "high-angle_gun"}, + {"id": 8365, "synset": "highball_glass.n.01", "name": "highball_glass"}, + {"id": 8366, "synset": "highboard.n.01", "name": "highboard"}, + {"id": 8367, "synset": "highboy.n.01", "name": "highboy"}, + {"id": 8368, "synset": "high_gear.n.01", "name": "high_gear"}, + {"id": 8369, "synset": "high-hat_cymbal.n.01", "name": "high-hat_cymbal"}, + {"id": 8370, "synset": "highlighter.n.02", "name": "highlighter"}, + {"id": 8371, "synset": "highlighter.n.01", "name": "highlighter"}, + {"id": 8372, "synset": "high-pass_filter.n.01", "name": "high-pass_filter"}, + {"id": 8373, "synset": "high-rise.n.01", "name": "high-rise"}, + {"id": 8374, "synset": "high_table.n.01", "name": "high_table"}, + {"id": 8375, "synset": "high-warp_loom.n.01", "name": "high-warp_loom"}, + {"id": 8376, "synset": "hijab.n.01", "name": "hijab"}, + {"id": 8377, "synset": "hinging_post.n.01", "name": "hinging_post"}, + {"id": 8378, "synset": "hip_boot.n.01", "name": "hip_boot"}, + {"id": 8379, "synset": "hipflask.n.01", "name": "hipflask"}, + {"id": 8380, "synset": "hip_pad.n.01", "name": "hip_pad"}, + {"id": 8381, "synset": "hip_pocket.n.01", "name": "hip_pocket"}, + {"id": 8382, "synset": "hippodrome.n.01", "name": "hippodrome"}, + {"id": 8383, "synset": "hip_roof.n.01", "name": "hip_roof"}, + {"id": 8384, "synset": "hitch.n.05", "name": "hitch"}, + {"id": 8385, "synset": "hitch.n.04", "name": "hitch"}, + {"id": 8386, "synset": "hitching_post.n.01", "name": "hitching_post"}, + {"id": 8387, "synset": "hitchrack.n.01", "name": "hitchrack"}, + {"id": 8388, "synset": "hob.n.03", "name": "hob"}, + {"id": 8389, "synset": "hobble_skirt.n.01", "name": "hobble_skirt"}, + {"id": 8390, "synset": "hockey_skate.n.01", "name": "hockey_skate"}, + {"id": 8391, "synset": "hod.n.01", "name": "hod"}, + {"id": 8392, "synset": "hodoscope.n.01", "name": "hodoscope"}, + {"id": 8393, "synset": "hoe.n.01", "name": "hoe"}, + {"id": 8394, "synset": "hoe_handle.n.01", "name": "hoe_handle"}, + {"id": 8395, "synset": "hogshead.n.02", "name": "hogshead"}, + {"id": 8396, "synset": "hoist.n.01", "name": "hoist"}, + {"id": 8397, "synset": "hold.n.07", "name": "hold"}, + {"id": 8398, "synset": "holder.n.01", "name": "holder"}, + {"id": 8399, "synset": "holding_cell.n.01", "name": "holding_cell"}, + {"id": 8400, "synset": "holding_device.n.01", "name": "holding_device"}, + {"id": 8401, "synset": "holding_pen.n.01", "name": "holding_pen"}, + {"id": 8402, "synset": "hollowware.n.01", "name": "hollowware"}, + {"id": 8403, "synset": "holster.n.01", "name": "holster"}, + {"id": 8404, "synset": "holster.n.02", "name": "holster"}, + {"id": 8405, "synset": "holy_of_holies.n.02", "name": "holy_of_holies"}, + {"id": 8406, "synset": "home.n.09", "name": "home"}, + {"id": 8407, "synset": "home_appliance.n.01", "name": "home_appliance"}, + {"id": 8408, "synset": "home_computer.n.01", "name": "home_computer"}, + {"id": 8409, "synset": "home_room.n.01", "name": "home_room"}, + {"id": 8410, "synset": "homespun.n.01", "name": "homespun"}, + {"id": 8411, "synset": "homestead.n.03", "name": "homestead"}, + {"id": 8412, "synset": "home_theater.n.01", "name": "home_theater"}, + {"id": 8413, "synset": "homing_torpedo.n.01", "name": "homing_torpedo"}, + {"id": 8414, "synset": "hone.n.01", "name": "hone"}, + {"id": 8415, "synset": "honeycomb.n.02", "name": "honeycomb"}, + {"id": 8416, "synset": "hood.n.09", "name": "hood"}, + {"id": 8417, "synset": "hood.n.08", "name": "hood"}, + {"id": 8418, "synset": "hood.n.07", "name": "hood"}, + {"id": 8419, "synset": "hood.n.05", "name": "hood"}, + {"id": 8420, "synset": "hood_latch.n.01", "name": "hood_latch"}, + {"id": 8421, "synset": "hook.n.04", "name": "hook"}, + {"id": 8422, "synset": "hook.n.01", "name": "hook"}, + {"id": 8423, "synset": "hook_and_eye.n.01", "name": "hook_and_eye"}, + {"id": 8424, "synset": "hookup.n.02", "name": "hookup"}, + {"id": 8425, "synset": "hookup.n.01", "name": "hookup"}, + {"id": 8426, "synset": "hook_wrench.n.01", "name": "hook_wrench"}, + {"id": 8427, "synset": "hoopskirt.n.01", "name": "hoopskirt"}, + {"id": 8428, "synset": "hoosegow.n.01", "name": "hoosegow"}, + {"id": 8429, "synset": "hoover.n.04", "name": "Hoover"}, + {"id": 8430, "synset": "hope_chest.n.01", "name": "hope_chest"}, + {"id": 8431, "synset": "hopper.n.01", "name": "hopper"}, + {"id": 8432, "synset": "hopsacking.n.01", "name": "hopsacking"}, + {"id": 8433, "synset": "horizontal_bar.n.01", "name": "horizontal_bar"}, + {"id": 8434, "synset": "horizontal_stabilizer.n.01", "name": "horizontal_stabilizer"}, + {"id": 8435, "synset": "horizontal_tail.n.01", "name": "horizontal_tail"}, + {"id": 8436, "synset": "horn.n.09", "name": "horn"}, + {"id": 8437, "synset": "horn.n.01", "name": "horn"}, + {"id": 8438, "synset": "horn.n.08", "name": "horn"}, + {"id": 8439, "synset": "horn_button.n.01", "name": "horn_button"}, + {"id": 8440, "synset": "hornpipe.n.03", "name": "hornpipe"}, + {"id": 8441, "synset": "horse.n.02", "name": "horse"}, + {"id": 8442, "synset": "horsebox.n.01", "name": "horsebox"}, + {"id": 8443, "synset": "horsecar.n.01", "name": "horsecar"}, + {"id": 8444, "synset": "horse_cart.n.01", "name": "horse_cart"}, + {"id": 8445, "synset": "horsecloth.n.01", "name": "horsecloth"}, + {"id": 8446, "synset": "horse-drawn_vehicle.n.01", "name": "horse-drawn_vehicle"}, + {"id": 8447, "synset": "horsehair.n.02", "name": "horsehair"}, + {"id": 8448, "synset": "horsehair_wig.n.01", "name": "horsehair_wig"}, + {"id": 8449, "synset": "horseless_carriage.n.01", "name": "horseless_carriage"}, + {"id": 8450, "synset": "horse_pistol.n.01", "name": "horse_pistol"}, + {"id": 8451, "synset": "horseshoe.n.02", "name": "horseshoe"}, + {"id": 8452, "synset": "horseshoe.n.01", "name": "horseshoe"}, + {"id": 8453, "synset": "horse-trail.n.01", "name": "horse-trail"}, + {"id": 8454, "synset": "horsewhip.n.01", "name": "horsewhip"}, + {"id": 8455, "synset": "hose.n.02", "name": "hose"}, + {"id": 8456, "synset": "hosiery.n.01", "name": "hosiery"}, + {"id": 8457, "synset": "hospice.n.01", "name": "hospice"}, + {"id": 8458, "synset": "hospital.n.01", "name": "hospital"}, + {"id": 8459, "synset": "hospital_bed.n.01", "name": "hospital_bed"}, + {"id": 8460, "synset": "hospital_room.n.01", "name": "hospital_room"}, + {"id": 8461, "synset": "hospital_ship.n.01", "name": "hospital_ship"}, + {"id": 8462, "synset": "hospital_train.n.01", "name": "hospital_train"}, + {"id": 8463, "synset": "hostel.n.02", "name": "hostel"}, + {"id": 8464, "synset": "hostel.n.01", "name": "hostel"}, + {"id": 8465, "synset": "hotel.n.01", "name": "hotel"}, + {"id": 8466, "synset": "hotel-casino.n.02", "name": "hotel-casino"}, + {"id": 8467, "synset": "hotel-casino.n.01", "name": "hotel-casino"}, + {"id": 8468, "synset": "hotel_room.n.01", "name": "hotel_room"}, + {"id": 8469, "synset": "hot_line.n.01", "name": "hot_line"}, + {"id": 8470, "synset": "hot_pants.n.02", "name": "hot_pants"}, + {"id": 8471, "synset": "hot_rod.n.01", "name": "hot_rod"}, + {"id": 8472, "synset": "hot_spot.n.03", "name": "hot_spot"}, + {"id": 8473, "synset": "hot_tub.n.01", "name": "hot_tub"}, + {"id": 8474, "synset": "hot-water_bottle.n.01", "name": "hot-water_bottle"}, + {"id": 8475, "synset": "houndstooth_check.n.01", "name": "houndstooth_check"}, + {"id": 8476, "synset": "hour_hand.n.01", "name": "hour_hand"}, + {"id": 8477, "synset": "house.n.01", "name": "house"}, + {"id": 8478, "synset": "house.n.12", "name": "house"}, + {"id": 8479, "synset": "houselights.n.01", "name": "houselights"}, + {"id": 8480, "synset": "house_of_cards.n.02", "name": "house_of_cards"}, + {"id": 8481, "synset": "house_of_correction.n.01", "name": "house_of_correction"}, + {"id": 8482, "synset": "house_paint.n.01", "name": "house_paint"}, + {"id": 8483, "synset": "housetop.n.01", "name": "housetop"}, + {"id": 8484, "synset": "housing.n.01", "name": "housing"}, + {"id": 8485, "synset": "hovel.n.01", "name": "hovel"}, + {"id": 8486, "synset": "hovercraft.n.01", "name": "hovercraft"}, + {"id": 8487, "synset": "howdah.n.01", "name": "howdah"}, + {"id": 8488, "synset": "huarache.n.01", "name": "huarache"}, + {"id": 8489, "synset": "hub-and-spoke.n.01", "name": "hub-and-spoke"}, + {"id": 8490, "synset": "hubcap.n.01", "name": "hubcap"}, + {"id": 8491, "synset": "huck.n.01", "name": "huck"}, + {"id": 8492, "synset": "hug-me-tight.n.01", "name": "hug-me-tight"}, + {"id": 8493, "synset": "hula-hoop.n.01", "name": "hula-hoop"}, + {"id": 8494, "synset": "hulk.n.02", "name": "hulk"}, + {"id": 8495, "synset": "hull.n.06", "name": "hull"}, + {"id": 8496, "synset": "humeral_veil.n.01", "name": "humeral_veil"}, + {"id": 8497, "synset": "humvee.n.01", "name": "Humvee"}, + {"id": 8498, "synset": "hunter.n.04", "name": "hunter"}, + {"id": 8499, "synset": "hunting_knife.n.01", "name": "hunting_knife"}, + {"id": 8500, "synset": "hurdle.n.01", "name": "hurdle"}, + {"id": 8501, "synset": "hurricane_deck.n.01", "name": "hurricane_deck"}, + {"id": 8502, "synset": "hurricane_lamp.n.01", "name": "hurricane_lamp"}, + {"id": 8503, "synset": "hut.n.01", "name": "hut"}, + {"id": 8504, "synset": "hutch.n.01", "name": "hutch"}, + {"id": 8505, "synset": "hutment.n.01", "name": "hutment"}, + {"id": 8506, "synset": "hydraulic_brake.n.01", "name": "hydraulic_brake"}, + {"id": 8507, "synset": "hydraulic_press.n.01", "name": "hydraulic_press"}, + {"id": 8508, "synset": "hydraulic_pump.n.01", "name": "hydraulic_pump"}, + {"id": 8509, "synset": "hydraulic_system.n.01", "name": "hydraulic_system"}, + {"id": 8510, "synset": "hydraulic_transmission.n.01", "name": "hydraulic_transmission"}, + {"id": 8511, "synset": "hydroelectric_turbine.n.01", "name": "hydroelectric_turbine"}, + {"id": 8512, "synset": "hydrofoil.n.02", "name": "hydrofoil"}, + {"id": 8513, "synset": "hydrofoil.n.01", "name": "hydrofoil"}, + {"id": 8514, "synset": "hydrogen_bomb.n.01", "name": "hydrogen_bomb"}, + {"id": 8515, "synset": "hydrometer.n.01", "name": "hydrometer"}, + {"id": 8516, "synset": "hygrodeik.n.01", "name": "hygrodeik"}, + {"id": 8517, "synset": "hygrometer.n.01", "name": "hygrometer"}, + {"id": 8518, "synset": "hygroscope.n.01", "name": "hygroscope"}, + {"id": 8519, "synset": "hyperbaric_chamber.n.01", "name": "hyperbaric_chamber"}, + {"id": 8520, "synset": "hypercoaster.n.01", "name": "hypercoaster"}, + {"id": 8521, "synset": "hypermarket.n.01", "name": "hypermarket"}, + {"id": 8522, "synset": "hypodermic_needle.n.01", "name": "hypodermic_needle"}, + {"id": 8523, "synset": "hypodermic_syringe.n.01", "name": "hypodermic_syringe"}, + {"id": 8524, "synset": "hypsometer.n.01", "name": "hypsometer"}, + {"id": 8525, "synset": "hysterosalpingogram.n.01", "name": "hysterosalpingogram"}, + {"id": 8526, "synset": "i-beam.n.01", "name": "I-beam"}, + {"id": 8527, "synset": "ice_ax.n.01", "name": "ice_ax"}, + {"id": 8528, "synset": "iceboat.n.02", "name": "iceboat"}, + {"id": 8529, "synset": "icebreaker.n.01", "name": "icebreaker"}, + {"id": 8530, "synset": "iced-tea_spoon.n.01", "name": "iced-tea_spoon"}, + {"id": 8531, "synset": "ice_hockey_rink.n.01", "name": "ice_hockey_rink"}, + {"id": 8532, "synset": "ice_machine.n.01", "name": "ice_machine"}, + {"id": 8533, "synset": "icepick.n.01", "name": "icepick"}, + {"id": 8534, "synset": "ice_rink.n.01", "name": "ice_rink"}, + {"id": 8535, "synset": "ice_tongs.n.01", "name": "ice_tongs"}, + {"id": 8536, "synset": "icetray.n.01", "name": "icetray"}, + {"id": 8537, "synset": "iconoscope.n.01", "name": "iconoscope"}, + {"id": 8538, "synset": "identikit.n.01", "name": "Identikit"}, + {"id": 8539, "synset": "idle_pulley.n.01", "name": "idle_pulley"}, + {"id": 8540, "synset": "igloo.n.01", "name": "igloo"}, + {"id": 8541, "synset": "ignition_coil.n.01", "name": "ignition_coil"}, + {"id": 8542, "synset": "ignition_key.n.01", "name": "ignition_key"}, + {"id": 8543, "synset": "ignition_switch.n.01", "name": "ignition_switch"}, + {"id": 8544, "synset": "imaret.n.01", "name": "imaret"}, + {"id": 8545, "synset": "immovable_bandage.n.01", "name": "immovable_bandage"}, + {"id": 8546, "synset": "impact_printer.n.01", "name": "impact_printer"}, + {"id": 8547, "synset": "impeller.n.01", "name": "impeller"}, + {"id": 8548, "synset": "implant.n.01", "name": "implant"}, + {"id": 8549, "synset": "implement.n.01", "name": "implement"}, + {"id": 8550, "synset": "impression.n.07", "name": "impression"}, + {"id": 8551, "synset": "imprint.n.05", "name": "imprint"}, + { + "id": 8552, + "synset": "improvised_explosive_device.n.01", + "name": "improvised_explosive_device", + }, + {"id": 8553, "synset": "impulse_turbine.n.01", "name": "impulse_turbine"}, + {"id": 8554, "synset": "in-basket.n.01", "name": "in-basket"}, + {"id": 8555, "synset": "incendiary_bomb.n.01", "name": "incendiary_bomb"}, + {"id": 8556, "synset": "incinerator.n.01", "name": "incinerator"}, + {"id": 8557, "synset": "inclined_plane.n.01", "name": "inclined_plane"}, + {"id": 8558, "synset": "inclinometer.n.02", "name": "inclinometer"}, + {"id": 8559, "synset": "inclinometer.n.01", "name": "inclinometer"}, + {"id": 8560, "synset": "incrustation.n.03", "name": "incrustation"}, + {"id": 8561, "synset": "incubator.n.01", "name": "incubator"}, + {"id": 8562, "synset": "index_register.n.01", "name": "index_register"}, + {"id": 8563, "synset": "indiaman.n.01", "name": "Indiaman"}, + {"id": 8564, "synset": "indian_club.n.01", "name": "Indian_club"}, + {"id": 8565, "synset": "indicator.n.03", "name": "indicator"}, + {"id": 8566, "synset": "induction_coil.n.01", "name": "induction_coil"}, + {"id": 8567, "synset": "inductor.n.01", "name": "inductor"}, + {"id": 8568, "synset": "industrial_watercourse.n.01", "name": "industrial_watercourse"}, + {"id": 8569, "synset": "inertial_guidance_system.n.01", "name": "inertial_guidance_system"}, + {"id": 8570, "synset": "inflater.n.01", "name": "inflater"}, + {"id": 8571, "synset": "injector.n.01", "name": "injector"}, + {"id": 8572, "synset": "ink_bottle.n.01", "name": "ink_bottle"}, + {"id": 8573, "synset": "ink_eraser.n.01", "name": "ink_eraser"}, + {"id": 8574, "synset": "ink-jet_printer.n.01", "name": "ink-jet_printer"}, + {"id": 8575, "synset": "inkle.n.01", "name": "inkle"}, + {"id": 8576, "synset": "inkstand.n.02", "name": "inkstand"}, + {"id": 8577, "synset": "inkwell.n.01", "name": "inkwell"}, + {"id": 8578, "synset": "inlay.n.01", "name": "inlay"}, + {"id": 8579, "synset": "inside_caliper.n.01", "name": "inside_caliper"}, + {"id": 8580, "synset": "insole.n.01", "name": "insole"}, + {"id": 8581, "synset": "instep.n.02", "name": "instep"}, + {"id": 8582, "synset": "instillator.n.01", "name": "instillator"}, + {"id": 8583, "synset": "institution.n.02", "name": "institution"}, + {"id": 8584, "synset": "instrument.n.01", "name": "instrument"}, + {"id": 8585, "synset": "instrument_of_punishment.n.01", "name": "instrument_of_punishment"}, + {"id": 8586, "synset": "instrument_of_torture.n.01", "name": "instrument_of_torture"}, + {"id": 8587, "synset": "intaglio.n.02", "name": "intaglio"}, + {"id": 8588, "synset": "intake_valve.n.01", "name": "intake_valve"}, + {"id": 8589, "synset": "integrated_circuit.n.01", "name": "integrated_circuit"}, + {"id": 8590, "synset": "integrator.n.01", "name": "integrator"}, + {"id": 8591, "synset": "intelnet.n.01", "name": "Intelnet"}, + {"id": 8592, "synset": "interceptor.n.01", "name": "interceptor"}, + {"id": 8593, "synset": "interchange.n.01", "name": "interchange"}, + {"id": 8594, "synset": "intercommunication_system.n.01", "name": "intercommunication_system"}, + { + "id": 8595, + "synset": "intercontinental_ballistic_missile.n.01", + "name": "intercontinental_ballistic_missile", + }, + {"id": 8596, "synset": "interface.n.04", "name": "interface"}, + {"id": 8597, "synset": "interferometer.n.01", "name": "interferometer"}, + {"id": 8598, "synset": "interior_door.n.01", "name": "interior_door"}, + {"id": 8599, "synset": "internal-combustion_engine.n.01", "name": "internal-combustion_engine"}, + {"id": 8600, "synset": "internal_drive.n.01", "name": "internal_drive"}, + {"id": 8601, "synset": "internet.n.01", "name": "internet"}, + {"id": 8602, "synset": "interphone.n.01", "name": "interphone"}, + {"id": 8603, "synset": "interrupter.n.01", "name": "interrupter"}, + {"id": 8604, "synset": "intersection.n.02", "name": "intersection"}, + {"id": 8605, "synset": "interstice.n.02", "name": "interstice"}, + {"id": 8606, "synset": "intraocular_lens.n.01", "name": "intraocular_lens"}, + {"id": 8607, "synset": "intravenous_pyelogram.n.01", "name": "intravenous_pyelogram"}, + {"id": 8608, "synset": "inverter.n.01", "name": "inverter"}, + {"id": 8609, "synset": "ion_engine.n.01", "name": "ion_engine"}, + {"id": 8610, "synset": "ionization_chamber.n.01", "name": "ionization_chamber"}, + {"id": 8611, "synset": "video_ipod.n.01", "name": "video_iPod"}, + {"id": 8612, "synset": "iron.n.02", "name": "iron"}, + {"id": 8613, "synset": "iron.n.03", "name": "iron"}, + {"id": 8614, "synset": "irons.n.01", "name": "irons"}, + {"id": 8615, "synset": "ironclad.n.01", "name": "ironclad"}, + {"id": 8616, "synset": "iron_foundry.n.01", "name": "iron_foundry"}, + {"id": 8617, "synset": "iron_horse.n.01", "name": "iron_horse"}, + {"id": 8618, "synset": "ironing.n.01", "name": "ironing"}, + {"id": 8619, "synset": "iron_lung.n.01", "name": "iron_lung"}, + {"id": 8620, "synset": "ironmongery.n.01", "name": "ironmongery"}, + {"id": 8621, "synset": "ironworks.n.01", "name": "ironworks"}, + {"id": 8622, "synset": "irrigation_ditch.n.01", "name": "irrigation_ditch"}, + {"id": 8623, "synset": "izar.n.01", "name": "izar"}, + {"id": 8624, "synset": "jabot.n.01", "name": "jabot"}, + {"id": 8625, "synset": "jack.n.10", "name": "jack"}, + {"id": 8626, "synset": "jack.n.07", "name": "jack"}, + {"id": 8627, "synset": "jack.n.06", "name": "jack"}, + {"id": 8628, "synset": "jack.n.05", "name": "jack"}, + {"id": 8629, "synset": "jacket.n.02", "name": "jacket"}, + {"id": 8630, "synset": "jacket.n.05", "name": "jacket"}, + {"id": 8631, "synset": "jack-in-the-box.n.01", "name": "jack-in-the-box"}, + {"id": 8632, "synset": "jack-o'-lantern.n.02", "name": "jack-o'-lantern"}, + {"id": 8633, "synset": "jack_plane.n.01", "name": "jack_plane"}, + {"id": 8634, "synset": "jacob's_ladder.n.02", "name": "Jacob's_ladder"}, + {"id": 8635, "synset": "jaconet.n.01", "name": "jaconet"}, + {"id": 8636, "synset": "jacquard_loom.n.01", "name": "Jacquard_loom"}, + {"id": 8637, "synset": "jacquard.n.02", "name": "jacquard"}, + {"id": 8638, "synset": "jag.n.03", "name": "jag"}, + {"id": 8639, "synset": "jail.n.01", "name": "jail"}, + {"id": 8640, "synset": "jalousie.n.02", "name": "jalousie"}, + {"id": 8641, "synset": "jamb.n.01", "name": "jamb"}, + {"id": 8642, "synset": "jammer.n.01", "name": "jammer"}, + {"id": 8643, "synset": "jampot.n.01", "name": "jampot"}, + {"id": 8644, "synset": "japan.n.04", "name": "japan"}, + {"id": 8645, "synset": "jarvik_heart.n.01", "name": "Jarvik_heart"}, + {"id": 8646, "synset": "jaunting_car.n.01", "name": "jaunting_car"}, + {"id": 8647, "synset": "javelin.n.02", "name": "javelin"}, + {"id": 8648, "synset": "jaw.n.03", "name": "jaw"}, + {"id": 8649, "synset": "jaws_of_life.n.01", "name": "Jaws_of_Life"}, + {"id": 8650, "synset": "jellaba.n.01", "name": "jellaba"}, + {"id": 8651, "synset": "jerkin.n.01", "name": "jerkin"}, + {"id": 8652, "synset": "jeroboam.n.02", "name": "jeroboam"}, + {"id": 8653, "synset": "jersey.n.04", "name": "jersey"}, + {"id": 8654, "synset": "jet_bridge.n.01", "name": "jet_bridge"}, + {"id": 8655, "synset": "jet_engine.n.01", "name": "jet_engine"}, + {"id": 8656, "synset": "jetliner.n.01", "name": "jetliner"}, + {"id": 8657, "synset": "jeweler's_glass.n.01", "name": "jeweler's_glass"}, + {"id": 8658, "synset": "jewelled_headdress.n.01", "name": "jewelled_headdress"}, + {"id": 8659, "synset": "jew's_harp.n.01", "name": "jew's_harp"}, + {"id": 8660, "synset": "jib.n.01", "name": "jib"}, + {"id": 8661, "synset": "jibboom.n.01", "name": "jibboom"}, + {"id": 8662, "synset": "jig.n.03", "name": "jig"}, + {"id": 8663, "synset": "jig.n.02", "name": "jig"}, + {"id": 8664, "synset": "jiggermast.n.01", "name": "jiggermast"}, + {"id": 8665, "synset": "jigsaw.n.02", "name": "jigsaw"}, + {"id": 8666, "synset": "jigsaw_puzzle.n.01", "name": "jigsaw_puzzle"}, + {"id": 8667, "synset": "jinrikisha.n.01", "name": "jinrikisha"}, + {"id": 8668, "synset": "jobcentre.n.01", "name": "jobcentre"}, + {"id": 8669, "synset": "jodhpurs.n.01", "name": "jodhpurs"}, + {"id": 8670, "synset": "jodhpur.n.01", "name": "jodhpur"}, + {"id": 8671, "synset": "joinery.n.01", "name": "joinery"}, + {"id": 8672, "synset": "joint.n.05", "name": "joint"}, + { + "id": 8673, + "synset": "joint_direct_attack_munition.n.01", + "name": "Joint_Direct_Attack_Munition", + }, + {"id": 8674, "synset": "jointer.n.01", "name": "jointer"}, + {"id": 8675, "synset": "joist.n.01", "name": "joist"}, + {"id": 8676, "synset": "jolly_boat.n.01", "name": "jolly_boat"}, + {"id": 8677, "synset": "jorum.n.01", "name": "jorum"}, + {"id": 8678, "synset": "joss_house.n.01", "name": "joss_house"}, + {"id": 8679, "synset": "journal_bearing.n.01", "name": "journal_bearing"}, + {"id": 8680, "synset": "journal_box.n.01", "name": "journal_box"}, + {"id": 8681, "synset": "jungle_gym.n.01", "name": "jungle_gym"}, + {"id": 8682, "synset": "junk.n.02", "name": "junk"}, + {"id": 8683, "synset": "jug.n.01", "name": "jug"}, + {"id": 8684, "synset": "jukebox.n.01", "name": "jukebox"}, + {"id": 8685, "synset": "jumbojet.n.01", "name": "jumbojet"}, + {"id": 8686, "synset": "jumper.n.07", "name": "jumper"}, + {"id": 8687, "synset": "jumper.n.06", "name": "jumper"}, + {"id": 8688, "synset": "jumper.n.05", "name": "jumper"}, + {"id": 8689, "synset": "jumper.n.04", "name": "jumper"}, + {"id": 8690, "synset": "jumper_cable.n.01", "name": "jumper_cable"}, + {"id": 8691, "synset": "jump_seat.n.01", "name": "jump_seat"}, + {"id": 8692, "synset": "jump_suit.n.02", "name": "jump_suit"}, + {"id": 8693, "synset": "junction.n.01", "name": "junction"}, + {"id": 8694, "synset": "junction.n.04", "name": "junction"}, + {"id": 8695, "synset": "junction_barrier.n.01", "name": "junction_barrier"}, + {"id": 8696, "synset": "junk_shop.n.01", "name": "junk_shop"}, + {"id": 8697, "synset": "jury_box.n.01", "name": "jury_box"}, + {"id": 8698, "synset": "jury_mast.n.01", "name": "jury_mast"}, + {"id": 8699, "synset": "kachina.n.03", "name": "kachina"}, + {"id": 8700, "synset": "kaffiyeh.n.01", "name": "kaffiyeh"}, + {"id": 8701, "synset": "kalansuwa.n.01", "name": "kalansuwa"}, + {"id": 8702, "synset": "kalashnikov.n.01", "name": "Kalashnikov"}, + {"id": 8703, "synset": "kameez.n.01", "name": "kameez"}, + {"id": 8704, "synset": "kanzu.n.01", "name": "kanzu"}, + {"id": 8705, "synset": "katharometer.n.01", "name": "katharometer"}, + {"id": 8706, "synset": "kazoo.n.01", "name": "kazoo"}, + {"id": 8707, "synset": "keel.n.03", "name": "keel"}, + {"id": 8708, "synset": "keelboat.n.01", "name": "keelboat"}, + {"id": 8709, "synset": "keelson.n.01", "name": "keelson"}, + {"id": 8710, "synset": "keep.n.02", "name": "keep"}, + {"id": 8711, "synset": "kepi.n.01", "name": "kepi"}, + {"id": 8712, "synset": "keratoscope.n.01", "name": "keratoscope"}, + {"id": 8713, "synset": "kerchief.n.01", "name": "kerchief"}, + {"id": 8714, "synset": "ketch.n.01", "name": "ketch"}, + {"id": 8715, "synset": "kettle.n.04", "name": "kettle"}, + {"id": 8716, "synset": "key.n.15", "name": "key"}, + {"id": 8717, "synset": "keyboard.n.01", "name": "keyboard"}, + {"id": 8718, "synset": "keyboard_buffer.n.01", "name": "keyboard_buffer"}, + {"id": 8719, "synset": "keyboard_instrument.n.01", "name": "keyboard_instrument"}, + {"id": 8720, "synset": "keyhole.n.01", "name": "keyhole"}, + {"id": 8721, "synset": "keyhole_saw.n.01", "name": "keyhole_saw"}, + {"id": 8722, "synset": "khadi.n.01", "name": "khadi"}, + {"id": 8723, "synset": "khaki.n.01", "name": "khaki"}, + {"id": 8724, "synset": "khakis.n.01", "name": "khakis"}, + {"id": 8725, "synset": "khimar.n.01", "name": "khimar"}, + {"id": 8726, "synset": "khukuri.n.01", "name": "khukuri"}, + {"id": 8727, "synset": "kick_pleat.n.01", "name": "kick_pleat"}, + {"id": 8728, "synset": "kicksorter.n.01", "name": "kicksorter"}, + {"id": 8729, "synset": "kickstand.n.01", "name": "kickstand"}, + {"id": 8730, "synset": "kick_starter.n.01", "name": "kick_starter"}, + {"id": 8731, "synset": "kid_glove.n.01", "name": "kid_glove"}, + {"id": 8732, "synset": "kiln.n.01", "name": "kiln"}, + {"id": 8733, "synset": "kinescope.n.01", "name": "kinescope"}, + {"id": 8734, "synset": "kinetoscope.n.01", "name": "Kinetoscope"}, + {"id": 8735, "synset": "king.n.10", "name": "king"}, + {"id": 8736, "synset": "king.n.08", "name": "king"}, + {"id": 8737, "synset": "kingbolt.n.01", "name": "kingbolt"}, + {"id": 8738, "synset": "king_post.n.01", "name": "king_post"}, + {"id": 8739, "synset": "kipp's_apparatus.n.01", "name": "Kipp's_apparatus"}, + {"id": 8740, "synset": "kirk.n.01", "name": "kirk"}, + {"id": 8741, "synset": "kirpan.n.01", "name": "kirpan"}, + {"id": 8742, "synset": "kirtle.n.02", "name": "kirtle"}, + {"id": 8743, "synset": "kirtle.n.01", "name": "kirtle"}, + {"id": 8744, "synset": "kit.n.02", "name": "kit"}, + {"id": 8745, "synset": "kit.n.01", "name": "kit"}, + {"id": 8746, "synset": "kitbag.n.01", "name": "kitbag"}, + {"id": 8747, "synset": "kitchen.n.01", "name": "kitchen"}, + {"id": 8748, "synset": "kitchen_appliance.n.01", "name": "kitchen_appliance"}, + {"id": 8749, "synset": "kitchenette.n.01", "name": "kitchenette"}, + {"id": 8750, "synset": "kitchen_utensil.n.01", "name": "kitchen_utensil"}, + {"id": 8751, "synset": "kitchenware.n.01", "name": "kitchenware"}, + {"id": 8752, "synset": "kite_balloon.n.01", "name": "kite_balloon"}, + {"id": 8753, "synset": "klaxon.n.01", "name": "klaxon"}, + {"id": 8754, "synset": "klieg_light.n.01", "name": "klieg_light"}, + {"id": 8755, "synset": "klystron.n.01", "name": "klystron"}, + {"id": 8756, "synset": "knee_brace.n.01", "name": "knee_brace"}, + {"id": 8757, "synset": "knee-high.n.01", "name": "knee-high"}, + {"id": 8758, "synset": "knee_piece.n.01", "name": "knee_piece"}, + {"id": 8759, "synset": "knife.n.02", "name": "knife"}, + {"id": 8760, "synset": "knife_blade.n.01", "name": "knife_blade"}, + {"id": 8761, "synset": "knight.n.02", "name": "knight"}, + {"id": 8762, "synset": "knit.n.01", "name": "knit"}, + {"id": 8763, "synset": "knitting_machine.n.01", "name": "knitting_machine"}, + {"id": 8764, "synset": "knitwear.n.01", "name": "knitwear"}, + {"id": 8765, "synset": "knob.n.01", "name": "knob"}, + {"id": 8766, "synset": "knob.n.04", "name": "knob"}, + {"id": 8767, "synset": "knobble.n.01", "name": "knobble"}, + {"id": 8768, "synset": "knobkerrie.n.01", "name": "knobkerrie"}, + {"id": 8769, "synset": "knot.n.02", "name": "knot"}, + {"id": 8770, "synset": "knuckle_joint.n.02", "name": "knuckle_joint"}, + {"id": 8771, "synset": "kohl.n.01", "name": "kohl"}, + {"id": 8772, "synset": "koto.n.01", "name": "koto"}, + {"id": 8773, "synset": "kraal.n.02", "name": "kraal"}, + {"id": 8774, "synset": "kremlin.n.02", "name": "kremlin"}, + {"id": 8775, "synset": "kris.n.01", "name": "kris"}, + {"id": 8776, "synset": "krummhorn.n.01", "name": "krummhorn"}, + {"id": 8777, "synset": "kundt's_tube.n.01", "name": "Kundt's_tube"}, + {"id": 8778, "synset": "kurdistan.n.02", "name": "Kurdistan"}, + {"id": 8779, "synset": "kurta.n.01", "name": "kurta"}, + {"id": 8780, "synset": "kylix.n.01", "name": "kylix"}, + {"id": 8781, "synset": "kymograph.n.01", "name": "kymograph"}, + {"id": 8782, "synset": "lab_bench.n.01", "name": "lab_bench"}, + {"id": 8783, "synset": "lace.n.02", "name": "lace"}, + {"id": 8784, "synset": "lacquer.n.02", "name": "lacquer"}, + {"id": 8785, "synset": "lacquerware.n.01", "name": "lacquerware"}, + {"id": 8786, "synset": "lacrosse_ball.n.01", "name": "lacrosse_ball"}, + {"id": 8787, "synset": "ladder-back.n.02", "name": "ladder-back"}, + {"id": 8788, "synset": "ladder-back.n.01", "name": "ladder-back"}, + {"id": 8789, "synset": "ladder_truck.n.01", "name": "ladder_truck"}, + {"id": 8790, "synset": "ladies'_room.n.01", "name": "ladies'_room"}, + {"id": 8791, "synset": "lady_chapel.n.01", "name": "lady_chapel"}, + {"id": 8792, "synset": "lagerphone.n.01", "name": "lagerphone"}, + {"id": 8793, "synset": "lag_screw.n.01", "name": "lag_screw"}, + {"id": 8794, "synset": "lake_dwelling.n.01", "name": "lake_dwelling"}, + {"id": 8795, "synset": "lally.n.01", "name": "lally"}, + {"id": 8796, "synset": "lamasery.n.01", "name": "lamasery"}, + {"id": 8797, "synset": "lambrequin.n.02", "name": "lambrequin"}, + {"id": 8798, "synset": "lame.n.02", "name": "lame"}, + {"id": 8799, "synset": "laminar_flow_clean_room.n.01", "name": "laminar_flow_clean_room"}, + {"id": 8800, "synset": "laminate.n.01", "name": "laminate"}, + {"id": 8801, "synset": "lamination.n.01", "name": "lamination"}, + {"id": 8802, "synset": "lamp.n.01", "name": "lamp"}, + {"id": 8803, "synset": "lamp_house.n.01", "name": "lamp_house"}, + {"id": 8804, "synset": "lanai.n.02", "name": "lanai"}, + {"id": 8805, "synset": "lancet_arch.n.01", "name": "lancet_arch"}, + {"id": 8806, "synset": "lancet_window.n.01", "name": "lancet_window"}, + {"id": 8807, "synset": "landau.n.02", "name": "landau"}, + {"id": 8808, "synset": "lander.n.02", "name": "lander"}, + {"id": 8809, "synset": "landing_craft.n.01", "name": "landing_craft"}, + {"id": 8810, "synset": "landing_flap.n.01", "name": "landing_flap"}, + {"id": 8811, "synset": "landing_gear.n.01", "name": "landing_gear"}, + {"id": 8812, "synset": "landing_net.n.01", "name": "landing_net"}, + {"id": 8813, "synset": "landing_skid.n.01", "name": "landing_skid"}, + {"id": 8814, "synset": "land_line.n.01", "name": "land_line"}, + {"id": 8815, "synset": "land_mine.n.01", "name": "land_mine"}, + {"id": 8816, "synset": "land_office.n.01", "name": "land_office"}, + {"id": 8817, "synset": "lanolin.n.02", "name": "lanolin"}, + {"id": 8818, "synset": "lanyard.n.01", "name": "lanyard"}, + {"id": 8819, "synset": "lap.n.03", "name": "lap"}, + {"id": 8820, "synset": "laparoscope.n.01", "name": "laparoscope"}, + {"id": 8821, "synset": "lapboard.n.01", "name": "lapboard"}, + {"id": 8822, "synset": "lapel.n.01", "name": "lapel"}, + {"id": 8823, "synset": "lap_joint.n.01", "name": "lap_joint"}, + {"id": 8824, "synset": "laryngoscope.n.01", "name": "laryngoscope"}, + {"id": 8825, "synset": "laser.n.01", "name": "laser"}, + {"id": 8826, "synset": "laser-guided_bomb.n.01", "name": "laser-guided_bomb"}, + {"id": 8827, "synset": "laser_printer.n.01", "name": "laser_printer"}, + {"id": 8828, "synset": "lash.n.02", "name": "lash"}, + {"id": 8829, "synset": "lashing.n.02", "name": "lashing"}, + {"id": 8830, "synset": "lasso.n.02", "name": "lasso"}, + {"id": 8831, "synset": "latch.n.01", "name": "latch"}, + {"id": 8832, "synset": "latchet.n.01", "name": "latchet"}, + {"id": 8833, "synset": "latchkey.n.01", "name": "latchkey"}, + {"id": 8834, "synset": "lateen.n.01", "name": "lateen"}, + {"id": 8835, "synset": "latex_paint.n.01", "name": "latex_paint"}, + {"id": 8836, "synset": "lath.n.01", "name": "lath"}, + {"id": 8837, "synset": "lathe.n.01", "name": "lathe"}, + {"id": 8838, "synset": "latrine.n.01", "name": "latrine"}, + {"id": 8839, "synset": "lattice.n.03", "name": "lattice"}, + {"id": 8840, "synset": "launch.n.01", "name": "launch"}, + {"id": 8841, "synset": "launcher.n.01", "name": "launcher"}, + {"id": 8842, "synset": "laundry.n.01", "name": "laundry"}, + {"id": 8843, "synset": "laundry_cart.n.01", "name": "laundry_cart"}, + {"id": 8844, "synset": "laundry_truck.n.01", "name": "laundry_truck"}, + {"id": 8845, "synset": "lavalava.n.01", "name": "lavalava"}, + {"id": 8846, "synset": "lavaliere.n.01", "name": "lavaliere"}, + {"id": 8847, "synset": "laver.n.02", "name": "laver"}, + {"id": 8848, "synset": "lawn_chair.n.01", "name": "lawn_chair"}, + {"id": 8849, "synset": "lawn_furniture.n.01", "name": "lawn_furniture"}, + {"id": 8850, "synset": "layette.n.01", "name": "layette"}, + {"id": 8851, "synset": "lead-acid_battery.n.01", "name": "lead-acid_battery"}, + {"id": 8852, "synset": "lead-in.n.02", "name": "lead-in"}, + {"id": 8853, "synset": "leading_rein.n.01", "name": "leading_rein"}, + {"id": 8854, "synset": "lead_pencil.n.01", "name": "lead_pencil"}, + {"id": 8855, "synset": "leaf_spring.n.01", "name": "leaf_spring"}, + {"id": 8856, "synset": "lean-to.n.01", "name": "lean-to"}, + {"id": 8857, "synset": "lean-to_tent.n.01", "name": "lean-to_tent"}, + {"id": 8858, "synset": "leash.n.01", "name": "leash"}, + {"id": 8859, "synset": "leatherette.n.01", "name": "leatherette"}, + {"id": 8860, "synset": "leather_strip.n.01", "name": "leather_strip"}, + {"id": 8861, "synset": "leclanche_cell.n.01", "name": "Leclanche_cell"}, + {"id": 8862, "synset": "lectern.n.01", "name": "lectern"}, + {"id": 8863, "synset": "lecture_room.n.01", "name": "lecture_room"}, + {"id": 8864, "synset": "lederhosen.n.01", "name": "lederhosen"}, + {"id": 8865, "synset": "ledger_board.n.01", "name": "ledger_board"}, + {"id": 8866, "synset": "leg.n.07", "name": "leg"}, + {"id": 8867, "synset": "leg.n.03", "name": "leg"}, + {"id": 8868, "synset": "leiden_jar.n.01", "name": "Leiden_jar"}, + {"id": 8869, "synset": "leisure_wear.n.01", "name": "leisure_wear"}, + {"id": 8870, "synset": "lens.n.01", "name": "lens"}, + {"id": 8871, "synset": "lens.n.05", "name": "lens"}, + {"id": 8872, "synset": "lens_cap.n.01", "name": "lens_cap"}, + {"id": 8873, "synset": "lens_implant.n.01", "name": "lens_implant"}, + {"id": 8874, "synset": "leotard.n.01", "name": "leotard"}, + {"id": 8875, "synset": "letter_case.n.01", "name": "letter_case"}, + {"id": 8876, "synset": "letter_opener.n.01", "name": "letter_opener"}, + {"id": 8877, "synset": "levee.n.03", "name": "levee"}, + {"id": 8878, "synset": "level.n.05", "name": "level"}, + {"id": 8879, "synset": "lever.n.01", "name": "lever"}, + {"id": 8880, "synset": "lever.n.03", "name": "lever"}, + {"id": 8881, "synset": "lever.n.02", "name": "lever"}, + {"id": 8882, "synset": "lever_lock.n.01", "name": "lever_lock"}, + {"id": 8883, "synset": "levi's.n.01", "name": "Levi's"}, + {"id": 8884, "synset": "liberty_ship.n.01", "name": "Liberty_ship"}, + {"id": 8885, "synset": "library.n.01", "name": "library"}, + {"id": 8886, "synset": "library.n.05", "name": "library"}, + {"id": 8887, "synset": "lid.n.02", "name": "lid"}, + {"id": 8888, "synset": "liebig_condenser.n.01", "name": "Liebig_condenser"}, + {"id": 8889, "synset": "lie_detector.n.01", "name": "lie_detector"}, + {"id": 8890, "synset": "lifeboat.n.01", "name": "lifeboat"}, + {"id": 8891, "synset": "life_office.n.01", "name": "life_office"}, + {"id": 8892, "synset": "life_preserver.n.01", "name": "life_preserver"}, + {"id": 8893, "synset": "life-support_system.n.02", "name": "life-support_system"}, + {"id": 8894, "synset": "life-support_system.n.01", "name": "life-support_system"}, + {"id": 8895, "synset": "lifting_device.n.01", "name": "lifting_device"}, + {"id": 8896, "synset": "lift_pump.n.01", "name": "lift_pump"}, + {"id": 8897, "synset": "ligament.n.02", "name": "ligament"}, + {"id": 8898, "synset": "ligature.n.03", "name": "ligature"}, + {"id": 8899, "synset": "light.n.02", "name": "light"}, + {"id": 8900, "synset": "light_arm.n.01", "name": "light_arm"}, + {"id": 8901, "synset": "light_circuit.n.01", "name": "light_circuit"}, + {"id": 8902, "synset": "light-emitting_diode.n.01", "name": "light-emitting_diode"}, + {"id": 8903, "synset": "lighter.n.02", "name": "lighter"}, + {"id": 8904, "synset": "lighter-than-air_craft.n.01", "name": "lighter-than-air_craft"}, + {"id": 8905, "synset": "light_filter.n.01", "name": "light_filter"}, + {"id": 8906, "synset": "lighting.n.02", "name": "lighting"}, + {"id": 8907, "synset": "light_machine_gun.n.01", "name": "light_machine_gun"}, + {"id": 8908, "synset": "light_meter.n.01", "name": "light_meter"}, + {"id": 8909, "synset": "light_microscope.n.01", "name": "light_microscope"}, + {"id": 8910, "synset": "light_pen.n.01", "name": "light_pen"}, + {"id": 8911, "synset": "lightship.n.01", "name": "lightship"}, + {"id": 8912, "synset": "lilo.n.01", "name": "Lilo"}, + {"id": 8913, "synset": "limber.n.01", "name": "limber"}, + {"id": 8914, "synset": "limekiln.n.01", "name": "limekiln"}, + {"id": 8915, "synset": "limiter.n.01", "name": "limiter"}, + {"id": 8916, "synset": "linear_accelerator.n.01", "name": "linear_accelerator"}, + {"id": 8917, "synset": "linen.n.01", "name": "linen"}, + {"id": 8918, "synset": "line_printer.n.01", "name": "line_printer"}, + {"id": 8919, "synset": "liner.n.04", "name": "liner"}, + {"id": 8920, "synset": "liner.n.03", "name": "liner"}, + {"id": 8921, "synset": "lingerie.n.01", "name": "lingerie"}, + {"id": 8922, "synset": "lining.n.01", "name": "lining"}, + {"id": 8923, "synset": "link.n.09", "name": "link"}, + {"id": 8924, "synset": "linkage.n.03", "name": "linkage"}, + {"id": 8925, "synset": "link_trainer.n.01", "name": "Link_trainer"}, + {"id": 8926, "synset": "linocut.n.02", "name": "linocut"}, + {"id": 8927, "synset": "linoleum_knife.n.01", "name": "linoleum_knife"}, + {"id": 8928, "synset": "linotype.n.01", "name": "Linotype"}, + {"id": 8929, "synset": "linsey-woolsey.n.01", "name": "linsey-woolsey"}, + {"id": 8930, "synset": "linstock.n.01", "name": "linstock"}, + {"id": 8931, "synset": "lion-jaw_forceps.n.01", "name": "lion-jaw_forceps"}, + {"id": 8932, "synset": "lip-gloss.n.01", "name": "lip-gloss"}, + {"id": 8933, "synset": "lipstick.n.01", "name": "lipstick"}, + {"id": 8934, "synset": "liqueur_glass.n.01", "name": "liqueur_glass"}, + {"id": 8935, "synset": "liquid_crystal_display.n.01", "name": "liquid_crystal_display"}, + {"id": 8936, "synset": "liquid_metal_reactor.n.01", "name": "liquid_metal_reactor"}, + {"id": 8937, "synset": "lisle.n.01", "name": "lisle"}, + {"id": 8938, "synset": "lister.n.03", "name": "lister"}, + {"id": 8939, "synset": "litterbin.n.01", "name": "litterbin"}, + {"id": 8940, "synset": "little_theater.n.01", "name": "little_theater"}, + {"id": 8941, "synset": "live_axle.n.01", "name": "live_axle"}, + {"id": 8942, "synset": "living_quarters.n.01", "name": "living_quarters"}, + {"id": 8943, "synset": "living_room.n.01", "name": "living_room"}, + {"id": 8944, "synset": "load.n.09", "name": "load"}, + {"id": 8945, "synset": "loafer.n.02", "name": "Loafer"}, + {"id": 8946, "synset": "loaner.n.02", "name": "loaner"}, + {"id": 8947, "synset": "lobe.n.04", "name": "lobe"}, + {"id": 8948, "synset": "lobster_pot.n.01", "name": "lobster_pot"}, + {"id": 8949, "synset": "local.n.01", "name": "local"}, + {"id": 8950, "synset": "local_area_network.n.01", "name": "local_area_network"}, + {"id": 8951, "synset": "local_oscillator.n.01", "name": "local_oscillator"}, + {"id": 8952, "synset": "lochaber_ax.n.01", "name": "Lochaber_ax"}, + {"id": 8953, "synset": "lock.n.01", "name": "lock"}, + {"id": 8954, "synset": "lock.n.05", "name": "lock"}, + {"id": 8955, "synset": "lock.n.04", "name": "lock"}, + {"id": 8956, "synset": "lock.n.03", "name": "lock"}, + {"id": 8957, "synset": "lockage.n.02", "name": "lockage"}, + {"id": 8958, "synset": "locker.n.02", "name": "locker"}, + {"id": 8959, "synset": "locker_room.n.01", "name": "locker_room"}, + {"id": 8960, "synset": "locket.n.01", "name": "locket"}, + {"id": 8961, "synset": "lock-gate.n.01", "name": "lock-gate"}, + {"id": 8962, "synset": "locking_pliers.n.01", "name": "locking_pliers"}, + {"id": 8963, "synset": "lockring.n.01", "name": "lockring"}, + {"id": 8964, "synset": "lockstitch.n.01", "name": "lockstitch"}, + {"id": 8965, "synset": "lockup.n.01", "name": "lockup"}, + {"id": 8966, "synset": "locomotive.n.01", "name": "locomotive"}, + {"id": 8967, "synset": "lodge.n.05", "name": "lodge"}, + {"id": 8968, "synset": "lodge.n.04", "name": "lodge"}, + {"id": 8969, "synset": "lodge.n.03", "name": "lodge"}, + {"id": 8970, "synset": "lodging_house.n.01", "name": "lodging_house"}, + {"id": 8971, "synset": "loft.n.02", "name": "loft"}, + {"id": 8972, "synset": "loft.n.04", "name": "loft"}, + {"id": 8973, "synset": "loft.n.01", "name": "loft"}, + {"id": 8974, "synset": "log_cabin.n.01", "name": "log_cabin"}, + {"id": 8975, "synset": "loggia.n.01", "name": "loggia"}, + {"id": 8976, "synset": "longbow.n.01", "name": "longbow"}, + {"id": 8977, "synset": "long_iron.n.01", "name": "long_iron"}, + {"id": 8978, "synset": "long_johns.n.01", "name": "long_johns"}, + {"id": 8979, "synset": "long_sleeve.n.01", "name": "long_sleeve"}, + {"id": 8980, "synset": "long_tom.n.01", "name": "long_tom"}, + {"id": 8981, "synset": "long_trousers.n.01", "name": "long_trousers"}, + {"id": 8982, "synset": "long_underwear.n.01", "name": "long_underwear"}, + {"id": 8983, "synset": "looking_glass.n.01", "name": "looking_glass"}, + {"id": 8984, "synset": "lookout.n.03", "name": "lookout"}, + {"id": 8985, "synset": "loom.n.01", "name": "loom"}, + {"id": 8986, "synset": "loop_knot.n.01", "name": "loop_knot"}, + {"id": 8987, "synset": "lorgnette.n.01", "name": "lorgnette"}, + {"id": 8988, "synset": "lorraine_cross.n.01", "name": "Lorraine_cross"}, + {"id": 8989, "synset": "lorry.n.02", "name": "lorry"}, + {"id": 8990, "synset": "lota.n.01", "name": "lota"}, + {"id": 8991, "synset": "lotion.n.01", "name": "lotion"}, + {"id": 8992, "synset": "lounge.n.02", "name": "lounge"}, + {"id": 8993, "synset": "lounger.n.03", "name": "lounger"}, + {"id": 8994, "synset": "lounging_jacket.n.01", "name": "lounging_jacket"}, + {"id": 8995, "synset": "lounging_pajama.n.01", "name": "lounging_pajama"}, + {"id": 8996, "synset": "loungewear.n.01", "name": "loungewear"}, + {"id": 8997, "synset": "loupe.n.01", "name": "loupe"}, + {"id": 8998, "synset": "louvered_window.n.01", "name": "louvered_window"}, + {"id": 8999, "synset": "love_knot.n.01", "name": "love_knot"}, + {"id": 9000, "synset": "loving_cup.n.01", "name": "loving_cup"}, + {"id": 9001, "synset": "lowboy.n.01", "name": "lowboy"}, + {"id": 9002, "synset": "low-pass_filter.n.01", "name": "low-pass_filter"}, + {"id": 9003, "synset": "low-warp-loom.n.01", "name": "low-warp-loom"}, + {"id": 9004, "synset": "lp.n.01", "name": "LP"}, + {"id": 9005, "synset": "l-plate.n.01", "name": "L-plate"}, + {"id": 9006, "synset": "lubber's_hole.n.01", "name": "lubber's_hole"}, + {"id": 9007, "synset": "lubricating_system.n.01", "name": "lubricating_system"}, + {"id": 9008, "synset": "luff.n.01", "name": "luff"}, + {"id": 9009, "synset": "lug.n.03", "name": "lug"}, + {"id": 9010, "synset": "luge.n.01", "name": "luge"}, + {"id": 9011, "synset": "luger.n.01", "name": "Luger"}, + {"id": 9012, "synset": "luggage_carrier.n.01", "name": "luggage_carrier"}, + {"id": 9013, "synset": "luggage_compartment.n.01", "name": "luggage_compartment"}, + {"id": 9014, "synset": "luggage_rack.n.01", "name": "luggage_rack"}, + {"id": 9015, "synset": "lugger.n.01", "name": "lugger"}, + {"id": 9016, "synset": "lugsail.n.01", "name": "lugsail"}, + {"id": 9017, "synset": "lug_wrench.n.01", "name": "lug_wrench"}, + {"id": 9018, "synset": "lumberjack.n.02", "name": "lumberjack"}, + {"id": 9019, "synset": "lumbermill.n.01", "name": "lumbermill"}, + {"id": 9020, "synset": "lunar_excursion_module.n.01", "name": "lunar_excursion_module"}, + {"id": 9021, "synset": "lunchroom.n.01", "name": "lunchroom"}, + {"id": 9022, "synset": "lunette.n.01", "name": "lunette"}, + {"id": 9023, "synset": "lungi.n.01", "name": "lungi"}, + {"id": 9024, "synset": "lunula.n.02", "name": "lunula"}, + {"id": 9025, "synset": "lusterware.n.01", "name": "lusterware"}, + {"id": 9026, "synset": "lute.n.02", "name": "lute"}, + {"id": 9027, "synset": "luxury_liner.n.01", "name": "luxury_liner"}, + {"id": 9028, "synset": "lyceum.n.02", "name": "lyceum"}, + {"id": 9029, "synset": "lychgate.n.01", "name": "lychgate"}, + {"id": 9030, "synset": "lyre.n.01", "name": "lyre"}, + {"id": 9031, "synset": "machete.n.01", "name": "machete"}, + {"id": 9032, "synset": "machicolation.n.01", "name": "machicolation"}, + {"id": 9033, "synset": "machine.n.01", "name": "machine"}, + {"id": 9034, "synset": "machine.n.04", "name": "machine"}, + {"id": 9035, "synset": "machine_bolt.n.01", "name": "machine_bolt"}, + {"id": 9036, "synset": "machinery.n.01", "name": "machinery"}, + {"id": 9037, "synset": "machine_screw.n.01", "name": "machine_screw"}, + {"id": 9038, "synset": "machine_tool.n.01", "name": "machine_tool"}, + {"id": 9039, "synset": "machinist's_vise.n.01", "name": "machinist's_vise"}, + {"id": 9040, "synset": "machmeter.n.01", "name": "machmeter"}, + {"id": 9041, "synset": "mackinaw.n.04", "name": "mackinaw"}, + {"id": 9042, "synset": "mackinaw.n.03", "name": "mackinaw"}, + {"id": 9043, "synset": "mackinaw.n.01", "name": "mackinaw"}, + {"id": 9044, "synset": "mackintosh.n.01", "name": "mackintosh"}, + {"id": 9045, "synset": "macrame.n.01", "name": "macrame"}, + {"id": 9046, "synset": "madras.n.03", "name": "madras"}, + {"id": 9047, "synset": "mae_west.n.02", "name": "Mae_West"}, + {"id": 9048, "synset": "magazine_rack.n.01", "name": "magazine_rack"}, + {"id": 9049, "synset": "magic_lantern.n.01", "name": "magic_lantern"}, + {"id": 9050, "synset": "magnetic_bottle.n.01", "name": "magnetic_bottle"}, + {"id": 9051, "synset": "magnetic_compass.n.01", "name": "magnetic_compass"}, + {"id": 9052, "synset": "magnetic_core_memory.n.01", "name": "magnetic_core_memory"}, + {"id": 9053, "synset": "magnetic_disk.n.01", "name": "magnetic_disk"}, + {"id": 9054, "synset": "magnetic_head.n.01", "name": "magnetic_head"}, + {"id": 9055, "synset": "magnetic_mine.n.01", "name": "magnetic_mine"}, + {"id": 9056, "synset": "magnetic_needle.n.01", "name": "magnetic_needle"}, + {"id": 9057, "synset": "magnetic_recorder.n.01", "name": "magnetic_recorder"}, + {"id": 9058, "synset": "magnetic_stripe.n.01", "name": "magnetic_stripe"}, + {"id": 9059, "synset": "magnetic_tape.n.01", "name": "magnetic_tape"}, + {"id": 9060, "synset": "magneto.n.01", "name": "magneto"}, + {"id": 9061, "synset": "magnetometer.n.01", "name": "magnetometer"}, + {"id": 9062, "synset": "magnetron.n.01", "name": "magnetron"}, + {"id": 9063, "synset": "magnifier.n.01", "name": "magnifier"}, + {"id": 9064, "synset": "magnum.n.01", "name": "magnum"}, + {"id": 9065, "synset": "magnus_hitch.n.01", "name": "magnus_hitch"}, + {"id": 9066, "synset": "mail.n.03", "name": "mail"}, + {"id": 9067, "synset": "mailbag.n.02", "name": "mailbag"}, + {"id": 9068, "synset": "mailbag.n.01", "name": "mailbag"}, + {"id": 9069, "synset": "mailboat.n.01", "name": "mailboat"}, + {"id": 9070, "synset": "mail_car.n.01", "name": "mail_car"}, + {"id": 9071, "synset": "maildrop.n.01", "name": "maildrop"}, + {"id": 9072, "synset": "mailer.n.04", "name": "mailer"}, + {"id": 9073, "synset": "maillot.n.02", "name": "maillot"}, + {"id": 9074, "synset": "maillot.n.01", "name": "maillot"}, + {"id": 9075, "synset": "mailsorter.n.01", "name": "mailsorter"}, + {"id": 9076, "synset": "mail_train.n.01", "name": "mail_train"}, + {"id": 9077, "synset": "mainframe.n.01", "name": "mainframe"}, + {"id": 9078, "synset": "mainmast.n.01", "name": "mainmast"}, + {"id": 9079, "synset": "main_rotor.n.01", "name": "main_rotor"}, + {"id": 9080, "synset": "mainsail.n.01", "name": "mainsail"}, + {"id": 9081, "synset": "mainspring.n.01", "name": "mainspring"}, + {"id": 9082, "synset": "main-topmast.n.01", "name": "main-topmast"}, + {"id": 9083, "synset": "main-topsail.n.01", "name": "main-topsail"}, + {"id": 9084, "synset": "main_yard.n.01", "name": "main_yard"}, + {"id": 9085, "synset": "maisonette.n.02", "name": "maisonette"}, + {"id": 9086, "synset": "majolica.n.01", "name": "majolica"}, + {"id": 9087, "synset": "makeup.n.01", "name": "makeup"}, + {"id": 9088, "synset": "maksutov_telescope.n.01", "name": "Maksutov_telescope"}, + {"id": 9089, "synset": "malacca.n.02", "name": "malacca"}, + {"id": 9090, "synset": "mallet.n.03", "name": "mallet"}, + {"id": 9091, "synset": "mallet.n.02", "name": "mallet"}, + {"id": 9092, "synset": "mammogram.n.01", "name": "mammogram"}, + {"id": 9093, "synset": "mandola.n.01", "name": "mandola"}, + {"id": 9094, "synset": "mandolin.n.01", "name": "mandolin"}, + {"id": 9095, "synset": "mangle.n.01", "name": "mangle"}, + {"id": 9096, "synset": "manhole_cover.n.01", "name": "manhole_cover"}, + {"id": 9097, "synset": "man-of-war.n.01", "name": "man-of-war"}, + {"id": 9098, "synset": "manometer.n.01", "name": "manometer"}, + {"id": 9099, "synset": "manor.n.01", "name": "manor"}, + {"id": 9100, "synset": "manor_hall.n.01", "name": "manor_hall"}, + {"id": 9101, "synset": "manpad.n.01", "name": "MANPAD"}, + {"id": 9102, "synset": "mansard.n.01", "name": "mansard"}, + {"id": 9103, "synset": "manse.n.02", "name": "manse"}, + {"id": 9104, "synset": "mansion.n.02", "name": "mansion"}, + {"id": 9105, "synset": "mantel.n.01", "name": "mantel"}, + {"id": 9106, "synset": "mantelet.n.02", "name": "mantelet"}, + {"id": 9107, "synset": "mantilla.n.01", "name": "mantilla"}, + {"id": 9108, "synset": "mao_jacket.n.01", "name": "Mao_jacket"}, + {"id": 9109, "synset": "maquiladora.n.01", "name": "maquiladora"}, + {"id": 9110, "synset": "maraca.n.01", "name": "maraca"}, + {"id": 9111, "synset": "marble.n.02", "name": "marble"}, + {"id": 9112, "synset": "marching_order.n.01", "name": "marching_order"}, + {"id": 9113, "synset": "marimba.n.01", "name": "marimba"}, + {"id": 9114, "synset": "marina.n.01", "name": "marina"}, + {"id": 9115, "synset": "marketplace.n.02", "name": "marketplace"}, + {"id": 9116, "synset": "marlinespike.n.01", "name": "marlinespike"}, + {"id": 9117, "synset": "marocain.n.01", "name": "marocain"}, + {"id": 9118, "synset": "marquee.n.02", "name": "marquee"}, + {"id": 9119, "synset": "marquetry.n.01", "name": "marquetry"}, + {"id": 9120, "synset": "marriage_bed.n.01", "name": "marriage_bed"}, + {"id": 9121, "synset": "martello_tower.n.01", "name": "martello_tower"}, + {"id": 9122, "synset": "martingale.n.01", "name": "martingale"}, + {"id": 9123, "synset": "mascara.n.01", "name": "mascara"}, + {"id": 9124, "synset": "maser.n.01", "name": "maser"}, + {"id": 9125, "synset": "mashie.n.01", "name": "mashie"}, + {"id": 9126, "synset": "mashie_niblick.n.01", "name": "mashie_niblick"}, + {"id": 9127, "synset": "masjid.n.01", "name": "masjid"}, + {"id": 9128, "synset": "mask.n.01", "name": "mask"}, + {"id": 9129, "synset": "masonite.n.01", "name": "Masonite"}, + {"id": 9130, "synset": "mason_jar.n.01", "name": "Mason_jar"}, + {"id": 9131, "synset": "masonry.n.01", "name": "masonry"}, + {"id": 9132, "synset": "mason's_level.n.01", "name": "mason's_level"}, + {"id": 9133, "synset": "massage_parlor.n.02", "name": "massage_parlor"}, + {"id": 9134, "synset": "massage_parlor.n.01", "name": "massage_parlor"}, + {"id": 9135, "synset": "mass_spectrograph.n.01", "name": "mass_spectrograph"}, + {"id": 9136, "synset": "mass_spectrometer.n.01", "name": "mass_spectrometer"}, + {"id": 9137, "synset": "mast.n.04", "name": "mast"}, + {"id": 9138, "synset": "mastaba.n.01", "name": "mastaba"}, + {"id": 9139, "synset": "master_bedroom.n.01", "name": "master_bedroom"}, + {"id": 9140, "synset": "masterpiece.n.01", "name": "masterpiece"}, + {"id": 9141, "synset": "mat.n.01", "name": "mat"}, + {"id": 9142, "synset": "match.n.01", "name": "match"}, + {"id": 9143, "synset": "match.n.03", "name": "match"}, + {"id": 9144, "synset": "matchboard.n.01", "name": "matchboard"}, + {"id": 9145, "synset": "matchbook.n.01", "name": "matchbook"}, + {"id": 9146, "synset": "matchlock.n.01", "name": "matchlock"}, + {"id": 9147, "synset": "match_plane.n.01", "name": "match_plane"}, + {"id": 9148, "synset": "matchstick.n.01", "name": "matchstick"}, + {"id": 9149, "synset": "material.n.04", "name": "material"}, + {"id": 9150, "synset": "materiel.n.01", "name": "materiel"}, + {"id": 9151, "synset": "maternity_hospital.n.01", "name": "maternity_hospital"}, + {"id": 9152, "synset": "maternity_ward.n.01", "name": "maternity_ward"}, + {"id": 9153, "synset": "matrix.n.06", "name": "matrix"}, + {"id": 9154, "synset": "matthew_walker.n.01", "name": "Matthew_Walker"}, + {"id": 9155, "synset": "matting.n.01", "name": "matting"}, + {"id": 9156, "synset": "mattock.n.01", "name": "mattock"}, + {"id": 9157, "synset": "mattress_cover.n.01", "name": "mattress_cover"}, + {"id": 9158, "synset": "maul.n.01", "name": "maul"}, + {"id": 9159, "synset": "maulstick.n.01", "name": "maulstick"}, + {"id": 9160, "synset": "mauser.n.02", "name": "Mauser"}, + {"id": 9161, "synset": "mausoleum.n.01", "name": "mausoleum"}, + {"id": 9162, "synset": "maxi.n.01", "name": "maxi"}, + {"id": 9163, "synset": "maxim_gun.n.01", "name": "Maxim_gun"}, + { + "id": 9164, + "synset": "maximum_and_minimum_thermometer.n.01", + "name": "maximum_and_minimum_thermometer", + }, + {"id": 9165, "synset": "maypole.n.01", "name": "maypole"}, + {"id": 9166, "synset": "maze.n.01", "name": "maze"}, + {"id": 9167, "synset": "mazer.n.01", "name": "mazer"}, + {"id": 9168, "synset": "means.n.02", "name": "means"}, + {"id": 9169, "synset": "measure.n.09", "name": "measure"}, + {"id": 9170, "synset": "measuring_instrument.n.01", "name": "measuring_instrument"}, + {"id": 9171, "synset": "meat_counter.n.01", "name": "meat_counter"}, + {"id": 9172, "synset": "meat_grinder.n.01", "name": "meat_grinder"}, + {"id": 9173, "synset": "meat_hook.n.01", "name": "meat_hook"}, + {"id": 9174, "synset": "meat_house.n.02", "name": "meat_house"}, + {"id": 9175, "synset": "meat_safe.n.01", "name": "meat_safe"}, + {"id": 9176, "synset": "meat_thermometer.n.01", "name": "meat_thermometer"}, + {"id": 9177, "synset": "mechanical_device.n.01", "name": "mechanical_device"}, + {"id": 9178, "synset": "mechanical_piano.n.01", "name": "mechanical_piano"}, + {"id": 9179, "synset": "mechanical_system.n.01", "name": "mechanical_system"}, + {"id": 9180, "synset": "mechanism.n.05", "name": "mechanism"}, + {"id": 9181, "synset": "medical_building.n.01", "name": "medical_building"}, + {"id": 9182, "synset": "medical_instrument.n.01", "name": "medical_instrument"}, + {"id": 9183, "synset": "medicine_ball.n.01", "name": "medicine_ball"}, + {"id": 9184, "synset": "medicine_chest.n.01", "name": "medicine_chest"}, + {"id": 9185, "synset": "medline.n.01", "name": "MEDLINE"}, + {"id": 9186, "synset": "megalith.n.01", "name": "megalith"}, + {"id": 9187, "synset": "megaphone.n.01", "name": "megaphone"}, + {"id": 9188, "synset": "memorial.n.03", "name": "memorial"}, + {"id": 9189, "synset": "memory.n.04", "name": "memory"}, + {"id": 9190, "synset": "memory_chip.n.01", "name": "memory_chip"}, + {"id": 9191, "synset": "memory_device.n.01", "name": "memory_device"}, + {"id": 9192, "synset": "menagerie.n.02", "name": "menagerie"}, + {"id": 9193, "synset": "mending.n.01", "name": "mending"}, + {"id": 9194, "synset": "menhir.n.01", "name": "menhir"}, + {"id": 9195, "synset": "menorah.n.02", "name": "menorah"}, + {"id": 9196, "synset": "menorah.n.01", "name": "Menorah"}, + {"id": 9197, "synset": "man's_clothing.n.01", "name": "man's_clothing"}, + {"id": 9198, "synset": "men's_room.n.01", "name": "men's_room"}, + {"id": 9199, "synset": "mercantile_establishment.n.01", "name": "mercantile_establishment"}, + {"id": 9200, "synset": "mercury_barometer.n.01", "name": "mercury_barometer"}, + {"id": 9201, "synset": "mercury_cell.n.01", "name": "mercury_cell"}, + {"id": 9202, "synset": "mercury_thermometer.n.01", "name": "mercury_thermometer"}, + {"id": 9203, "synset": "mercury-vapor_lamp.n.01", "name": "mercury-vapor_lamp"}, + {"id": 9204, "synset": "mercy_seat.n.02", "name": "mercy_seat"}, + {"id": 9205, "synset": "merlon.n.01", "name": "merlon"}, + {"id": 9206, "synset": "mess.n.05", "name": "mess"}, + {"id": 9207, "synset": "mess_jacket.n.01", "name": "mess_jacket"}, + {"id": 9208, "synset": "mess_kit.n.01", "name": "mess_kit"}, + {"id": 9209, "synset": "messuage.n.01", "name": "messuage"}, + {"id": 9210, "synset": "metal_detector.n.01", "name": "metal_detector"}, + {"id": 9211, "synset": "metallic.n.01", "name": "metallic"}, + {"id": 9212, "synset": "metal_screw.n.01", "name": "metal_screw"}, + {"id": 9213, "synset": "metal_wood.n.01", "name": "metal_wood"}, + {"id": 9214, "synset": "meteorological_balloon.n.01", "name": "meteorological_balloon"}, + {"id": 9215, "synset": "meter.n.02", "name": "meter"}, + {"id": 9216, "synset": "meterstick.n.01", "name": "meterstick"}, + {"id": 9217, "synset": "metronome.n.01", "name": "metronome"}, + {"id": 9218, "synset": "mezzanine.n.02", "name": "mezzanine"}, + {"id": 9219, "synset": "mezzanine.n.01", "name": "mezzanine"}, + {"id": 9220, "synset": "microbalance.n.01", "name": "microbalance"}, + {"id": 9221, "synset": "microbrewery.n.01", "name": "microbrewery"}, + {"id": 9222, "synset": "microfiche.n.01", "name": "microfiche"}, + {"id": 9223, "synset": "microfilm.n.01", "name": "microfilm"}, + {"id": 9224, "synset": "micrometer.n.02", "name": "micrometer"}, + {"id": 9225, "synset": "microprocessor.n.01", "name": "microprocessor"}, + {"id": 9226, "synset": "microtome.n.01", "name": "microtome"}, + { + "id": 9227, + "synset": "microwave_diathermy_machine.n.01", + "name": "microwave_diathermy_machine", + }, + { + "id": 9228, + "synset": "microwave_linear_accelerator.n.01", + "name": "microwave_linear_accelerator", + }, + {"id": 9229, "synset": "middy.n.01", "name": "middy"}, + {"id": 9230, "synset": "midiron.n.01", "name": "midiron"}, + {"id": 9231, "synset": "mihrab.n.02", "name": "mihrab"}, + {"id": 9232, "synset": "mihrab.n.01", "name": "mihrab"}, + {"id": 9233, "synset": "military_hospital.n.01", "name": "military_hospital"}, + {"id": 9234, "synset": "military_quarters.n.01", "name": "military_quarters"}, + {"id": 9235, "synset": "military_uniform.n.01", "name": "military_uniform"}, + {"id": 9236, "synset": "military_vehicle.n.01", "name": "military_vehicle"}, + {"id": 9237, "synset": "milk_bar.n.01", "name": "milk_bar"}, + {"id": 9238, "synset": "milk_float.n.01", "name": "milk_float"}, + {"id": 9239, "synset": "milking_machine.n.01", "name": "milking_machine"}, + {"id": 9240, "synset": "milking_stool.n.01", "name": "milking_stool"}, + {"id": 9241, "synset": "milk_wagon.n.01", "name": "milk_wagon"}, + {"id": 9242, "synset": "mill.n.04", "name": "mill"}, + {"id": 9243, "synset": "milldam.n.01", "name": "milldam"}, + {"id": 9244, "synset": "miller.n.05", "name": "miller"}, + {"id": 9245, "synset": "milliammeter.n.01", "name": "milliammeter"}, + {"id": 9246, "synset": "millinery.n.02", "name": "millinery"}, + {"id": 9247, "synset": "millinery.n.01", "name": "millinery"}, + {"id": 9248, "synset": "milling.n.01", "name": "milling"}, + {"id": 9249, "synset": "millivoltmeter.n.01", "name": "millivoltmeter"}, + {"id": 9250, "synset": "millstone.n.03", "name": "millstone"}, + {"id": 9251, "synset": "millstone.n.02", "name": "millstone"}, + {"id": 9252, "synset": "millwheel.n.01", "name": "millwheel"}, + {"id": 9253, "synset": "mimeograph.n.01", "name": "mimeograph"}, + {"id": 9254, "synset": "minaret.n.01", "name": "minaret"}, + {"id": 9255, "synset": "mincer.n.01", "name": "mincer"}, + {"id": 9256, "synset": "mine.n.02", "name": "mine"}, + {"id": 9257, "synset": "mine_detector.n.01", "name": "mine_detector"}, + {"id": 9258, "synset": "minelayer.n.01", "name": "minelayer"}, + {"id": 9259, "synset": "mineshaft.n.01", "name": "mineshaft"}, + {"id": 9260, "synset": "minibar.n.01", "name": "minibar"}, + {"id": 9261, "synset": "minibike.n.01", "name": "minibike"}, + {"id": 9262, "synset": "minibus.n.01", "name": "minibus"}, + {"id": 9263, "synset": "minicar.n.01", "name": "minicar"}, + {"id": 9264, "synset": "minicomputer.n.01", "name": "minicomputer"}, + {"id": 9265, "synset": "ministry.n.02", "name": "ministry"}, + {"id": 9266, "synset": "miniskirt.n.01", "name": "miniskirt"}, + {"id": 9267, "synset": "minisub.n.01", "name": "minisub"}, + {"id": 9268, "synset": "miniver.n.01", "name": "miniver"}, + {"id": 9269, "synset": "mink.n.02", "name": "mink"}, + {"id": 9270, "synset": "minster.n.01", "name": "minster"}, + {"id": 9271, "synset": "mint.n.06", "name": "mint"}, + {"id": 9272, "synset": "minute_hand.n.01", "name": "minute_hand"}, + {"id": 9273, "synset": "minuteman.n.02", "name": "Minuteman"}, + {"id": 9274, "synset": "missile.n.01", "name": "missile"}, + {"id": 9275, "synset": "missile_defense_system.n.01", "name": "missile_defense_system"}, + {"id": 9276, "synset": "miter_box.n.01", "name": "miter_box"}, + {"id": 9277, "synset": "miter_joint.n.01", "name": "miter_joint"}, + {"id": 9278, "synset": "mixer.n.03", "name": "mixer"}, + {"id": 9279, "synset": "mixing_bowl.n.01", "name": "mixing_bowl"}, + {"id": 9280, "synset": "mixing_faucet.n.01", "name": "mixing_faucet"}, + {"id": 9281, "synset": "mizzen.n.02", "name": "mizzen"}, + {"id": 9282, "synset": "mizzenmast.n.01", "name": "mizzenmast"}, + {"id": 9283, "synset": "mobcap.n.01", "name": "mobcap"}, + {"id": 9284, "synset": "mobile_home.n.01", "name": "mobile_home"}, + {"id": 9285, "synset": "moccasin.n.01", "name": "moccasin"}, + {"id": 9286, "synset": "mock-up.n.01", "name": "mock-up"}, + {"id": 9287, "synset": "mod_con.n.01", "name": "mod_con"}, + {"id": 9288, "synset": "model_t.n.01", "name": "Model_T"}, + {"id": 9289, "synset": "modem.n.01", "name": "modem"}, + {"id": 9290, "synset": "modillion.n.01", "name": "modillion"}, + {"id": 9291, "synset": "module.n.03", "name": "module"}, + {"id": 9292, "synset": "module.n.02", "name": "module"}, + {"id": 9293, "synset": "mohair.n.01", "name": "mohair"}, + {"id": 9294, "synset": "moire.n.01", "name": "moire"}, + {"id": 9295, "synset": "mold.n.02", "name": "mold"}, + {"id": 9296, "synset": "moldboard.n.01", "name": "moldboard"}, + {"id": 9297, "synset": "moldboard_plow.n.01", "name": "moldboard_plow"}, + {"id": 9298, "synset": "moleskin.n.01", "name": "moleskin"}, + {"id": 9299, "synset": "molotov_cocktail.n.01", "name": "Molotov_cocktail"}, + {"id": 9300, "synset": "monastery.n.01", "name": "monastery"}, + {"id": 9301, "synset": "monastic_habit.n.01", "name": "monastic_habit"}, + {"id": 9302, "synset": "moneybag.n.01", "name": "moneybag"}, + {"id": 9303, "synset": "money_belt.n.01", "name": "money_belt"}, + {"id": 9304, "synset": "monitor.n.06", "name": "monitor"}, + {"id": 9305, "synset": "monitor.n.05", "name": "monitor"}, + {"id": 9306, "synset": "monkey-wrench.n.01", "name": "monkey-wrench"}, + {"id": 9307, "synset": "monk's_cloth.n.01", "name": "monk's_cloth"}, + {"id": 9308, "synset": "monochrome.n.01", "name": "monochrome"}, + {"id": 9309, "synset": "monocle.n.01", "name": "monocle"}, + {"id": 9310, "synset": "monofocal_lens_implant.n.01", "name": "monofocal_lens_implant"}, + {"id": 9311, "synset": "monoplane.n.01", "name": "monoplane"}, + {"id": 9312, "synset": "monotype.n.02", "name": "monotype"}, + {"id": 9313, "synset": "monstrance.n.02", "name": "monstrance"}, + {"id": 9314, "synset": "mooring_tower.n.01", "name": "mooring_tower"}, + {"id": 9315, "synset": "moorish_arch.n.01", "name": "Moorish_arch"}, + {"id": 9316, "synset": "moped.n.01", "name": "moped"}, + {"id": 9317, "synset": "mop_handle.n.01", "name": "mop_handle"}, + {"id": 9318, "synset": "moquette.n.01", "name": "moquette"}, + {"id": 9319, "synset": "morgue.n.01", "name": "morgue"}, + {"id": 9320, "synset": "morion.n.01", "name": "morion"}, + {"id": 9321, "synset": "morning_dress.n.02", "name": "morning_dress"}, + {"id": 9322, "synset": "morning_dress.n.01", "name": "morning_dress"}, + {"id": 9323, "synset": "morning_room.n.01", "name": "morning_room"}, + {"id": 9324, "synset": "morris_chair.n.01", "name": "Morris_chair"}, + {"id": 9325, "synset": "mortar.n.01", "name": "mortar"}, + {"id": 9326, "synset": "mortar.n.03", "name": "mortar"}, + {"id": 9327, "synset": "mortarboard.n.02", "name": "mortarboard"}, + {"id": 9328, "synset": "mortise_joint.n.02", "name": "mortise_joint"}, + {"id": 9329, "synset": "mosaic.n.05", "name": "mosaic"}, + {"id": 9330, "synset": "mosque.n.01", "name": "mosque"}, + {"id": 9331, "synset": "mosquito_net.n.01", "name": "mosquito_net"}, + {"id": 9332, "synset": "motel.n.01", "name": "motel"}, + {"id": 9333, "synset": "motel_room.n.01", "name": "motel_room"}, + {"id": 9334, "synset": "mother_hubbard.n.01", "name": "Mother_Hubbard"}, + {"id": 9335, "synset": "motion-picture_camera.n.01", "name": "motion-picture_camera"}, + {"id": 9336, "synset": "motion-picture_film.n.01", "name": "motion-picture_film"}, + {"id": 9337, "synset": "motley.n.03", "name": "motley"}, + {"id": 9338, "synset": "motley.n.02", "name": "motley"}, + {"id": 9339, "synset": "motorboat.n.01", "name": "motorboat"}, + {"id": 9340, "synset": "motor_hotel.n.01", "name": "motor_hotel"}, + {"id": 9341, "synset": "motorized_wheelchair.n.01", "name": "motorized_wheelchair"}, + {"id": 9342, "synset": "mound.n.04", "name": "mound"}, + {"id": 9343, "synset": "mount.n.04", "name": "mount"}, + {"id": 9344, "synset": "mountain_bike.n.01", "name": "mountain_bike"}, + {"id": 9345, "synset": "mountain_tent.n.01", "name": "mountain_tent"}, + {"id": 9346, "synset": "mouse_button.n.01", "name": "mouse_button"}, + {"id": 9347, "synset": "mousetrap.n.01", "name": "mousetrap"}, + {"id": 9348, "synset": "mousse.n.03", "name": "mousse"}, + {"id": 9349, "synset": "mouthpiece.n.06", "name": "mouthpiece"}, + {"id": 9350, "synset": "mouthpiece.n.02", "name": "mouthpiece"}, + {"id": 9351, "synset": "mouthpiece.n.04", "name": "mouthpiece"}, + {"id": 9352, "synset": "movement.n.10", "name": "movement"}, + {"id": 9353, "synset": "movie_projector.n.01", "name": "movie_projector"}, + {"id": 9354, "synset": "moving-coil_galvanometer.n.01", "name": "moving-coil_galvanometer"}, + {"id": 9355, "synset": "moving_van.n.01", "name": "moving_van"}, + {"id": 9356, "synset": "mud_brick.n.01", "name": "mud_brick"}, + {"id": 9357, "synset": "mudguard.n.01", "name": "mudguard"}, + {"id": 9358, "synset": "mudhif.n.01", "name": "mudhif"}, + {"id": 9359, "synset": "muff.n.01", "name": "muff"}, + {"id": 9360, "synset": "muffle.n.01", "name": "muffle"}, + {"id": 9361, "synset": "muffler.n.02", "name": "muffler"}, + {"id": 9362, "synset": "mufti.n.02", "name": "mufti"}, + {"id": 9363, "synset": "mulch.n.01", "name": "mulch"}, + {"id": 9364, "synset": "mule.n.02", "name": "mule"}, + {"id": 9365, "synset": "multichannel_recorder.n.01", "name": "multichannel_recorder"}, + {"id": 9366, "synset": "multiengine_airplane.n.01", "name": "multiengine_airplane"}, + {"id": 9367, "synset": "multiplex.n.02", "name": "multiplex"}, + {"id": 9368, "synset": "multiplexer.n.01", "name": "multiplexer"}, + {"id": 9369, "synset": "multiprocessor.n.01", "name": "multiprocessor"}, + {"id": 9370, "synset": "multistage_rocket.n.01", "name": "multistage_rocket"}, + {"id": 9371, "synset": "munition.n.02", "name": "munition"}, + {"id": 9372, "synset": "murphy_bed.n.01", "name": "Murphy_bed"}, + {"id": 9373, "synset": "musette.n.01", "name": "musette"}, + {"id": 9374, "synset": "musette_pipe.n.01", "name": "musette_pipe"}, + {"id": 9375, "synset": "museum.n.01", "name": "museum"}, + {"id": 9376, "synset": "mushroom_anchor.n.01", "name": "mushroom_anchor"}, + {"id": 9377, "synset": "music_box.n.01", "name": "music_box"}, + {"id": 9378, "synset": "music_hall.n.01", "name": "music_hall"}, + {"id": 9379, "synset": "music_school.n.02", "name": "music_school"}, + {"id": 9380, "synset": "music_stand.n.01", "name": "music_stand"}, + {"id": 9381, "synset": "musket.n.01", "name": "musket"}, + {"id": 9382, "synset": "musket_ball.n.01", "name": "musket_ball"}, + {"id": 9383, "synset": "muslin.n.01", "name": "muslin"}, + {"id": 9384, "synset": "mustache_cup.n.01", "name": "mustache_cup"}, + {"id": 9385, "synset": "mustard_plaster.n.01", "name": "mustard_plaster"}, + {"id": 9386, "synset": "mute.n.02", "name": "mute"}, + {"id": 9387, "synset": "muzzle_loader.n.01", "name": "muzzle_loader"}, + {"id": 9388, "synset": "muzzle.n.03", "name": "muzzle"}, + {"id": 9389, "synset": "myelogram.n.01", "name": "myelogram"}, + {"id": 9390, "synset": "nacelle.n.01", "name": "nacelle"}, + {"id": 9391, "synset": "nail.n.02", "name": "nail"}, + {"id": 9392, "synset": "nailbrush.n.01", "name": "nailbrush"}, + {"id": 9393, "synset": "nailhead.n.02", "name": "nailhead"}, + {"id": 9394, "synset": "nailhead.n.01", "name": "nailhead"}, + {"id": 9395, "synset": "nail_polish.n.01", "name": "nail_polish"}, + {"id": 9396, "synset": "nainsook.n.01", "name": "nainsook"}, + {"id": 9397, "synset": "napier's_bones.n.01", "name": "Napier's_bones"}, + {"id": 9398, "synset": "nard.n.01", "name": "nard"}, + {"id": 9399, "synset": "narrowbody_aircraft.n.01", "name": "narrowbody_aircraft"}, + {"id": 9400, "synset": "narrow_wale.n.01", "name": "narrow_wale"}, + {"id": 9401, "synset": "narthex.n.02", "name": "narthex"}, + {"id": 9402, "synset": "narthex.n.01", "name": "narthex"}, + {"id": 9403, "synset": "nasotracheal_tube.n.01", "name": "nasotracheal_tube"}, + {"id": 9404, "synset": "national_monument.n.01", "name": "national_monument"}, + {"id": 9405, "synset": "nautilus.n.01", "name": "nautilus"}, + {"id": 9406, "synset": "navigational_system.n.01", "name": "navigational_system"}, + {"id": 9407, "synset": "naval_equipment.n.01", "name": "naval_equipment"}, + {"id": 9408, "synset": "naval_gun.n.01", "name": "naval_gun"}, + {"id": 9409, "synset": "naval_missile.n.01", "name": "naval_missile"}, + {"id": 9410, "synset": "naval_radar.n.01", "name": "naval_radar"}, + {"id": 9411, "synset": "naval_tactical_data_system.n.01", "name": "naval_tactical_data_system"}, + {"id": 9412, "synset": "naval_weaponry.n.01", "name": "naval_weaponry"}, + {"id": 9413, "synset": "nave.n.01", "name": "nave"}, + {"id": 9414, "synset": "navigational_instrument.n.01", "name": "navigational_instrument"}, + {"id": 9415, "synset": "nebuchadnezzar.n.02", "name": "nebuchadnezzar"}, + {"id": 9416, "synset": "neckband.n.01", "name": "neckband"}, + {"id": 9417, "synset": "neck_brace.n.01", "name": "neck_brace"}, + {"id": 9418, "synset": "neckcloth.n.01", "name": "neckcloth"}, + {"id": 9419, "synset": "necklet.n.01", "name": "necklet"}, + {"id": 9420, "synset": "neckline.n.01", "name": "neckline"}, + {"id": 9421, "synset": "neckpiece.n.01", "name": "neckpiece"}, + {"id": 9422, "synset": "neckwear.n.01", "name": "neckwear"}, + {"id": 9423, "synset": "needle.n.02", "name": "needle"}, + {"id": 9424, "synset": "needlenose_pliers.n.01", "name": "needlenose_pliers"}, + {"id": 9425, "synset": "needlework.n.01", "name": "needlework"}, + {"id": 9426, "synset": "negative.n.02", "name": "negative"}, + {"id": 9427, "synset": "negative_magnetic_pole.n.01", "name": "negative_magnetic_pole"}, + {"id": 9428, "synset": "negative_pole.n.01", "name": "negative_pole"}, + {"id": 9429, "synset": "negligee.n.01", "name": "negligee"}, + {"id": 9430, "synset": "neolith.n.01", "name": "neolith"}, + {"id": 9431, "synset": "neon_lamp.n.01", "name": "neon_lamp"}, + {"id": 9432, "synset": "nephoscope.n.01", "name": "nephoscope"}, + {"id": 9433, "synset": "nest.n.05", "name": "nest"}, + {"id": 9434, "synset": "nest_egg.n.02", "name": "nest_egg"}, + {"id": 9435, "synset": "net.n.06", "name": "net"}, + {"id": 9436, "synset": "net.n.02", "name": "net"}, + {"id": 9437, "synset": "net.n.05", "name": "net"}, + {"id": 9438, "synset": "net.n.04", "name": "net"}, + {"id": 9439, "synset": "network.n.05", "name": "network"}, + {"id": 9440, "synset": "network.n.04", "name": "network"}, + {"id": 9441, "synset": "neutron_bomb.n.01", "name": "neutron_bomb"}, + {"id": 9442, "synset": "newel.n.02", "name": "newel"}, + {"id": 9443, "synset": "newel_post.n.01", "name": "newel_post"}, + {"id": 9444, "synset": "newspaper.n.03", "name": "newspaper"}, + {"id": 9445, "synset": "newsroom.n.03", "name": "newsroom"}, + {"id": 9446, "synset": "newsroom.n.02", "name": "newsroom"}, + {"id": 9447, "synset": "newtonian_telescope.n.01", "name": "Newtonian_telescope"}, + {"id": 9448, "synset": "nib.n.01", "name": "nib"}, + {"id": 9449, "synset": "niblick.n.01", "name": "niblick"}, + {"id": 9450, "synset": "nicad.n.01", "name": "nicad"}, + {"id": 9451, "synset": "nickel-iron_battery.n.01", "name": "nickel-iron_battery"}, + {"id": 9452, "synset": "nicol_prism.n.01", "name": "Nicol_prism"}, + {"id": 9453, "synset": "night_bell.n.01", "name": "night_bell"}, + {"id": 9454, "synset": "nightcap.n.02", "name": "nightcap"}, + {"id": 9455, "synset": "nightgown.n.01", "name": "nightgown"}, + {"id": 9456, "synset": "night_latch.n.01", "name": "night_latch"}, + {"id": 9457, "synset": "night-light.n.01", "name": "night-light"}, + {"id": 9458, "synset": "nightshirt.n.01", "name": "nightshirt"}, + {"id": 9459, "synset": "ninepin.n.01", "name": "ninepin"}, + {"id": 9460, "synset": "ninepin_ball.n.01", "name": "ninepin_ball"}, + {"id": 9461, "synset": "ninon.n.01", "name": "ninon"}, + {"id": 9462, "synset": "nipple.n.02", "name": "nipple"}, + {"id": 9463, "synset": "nipple_shield.n.01", "name": "nipple_shield"}, + {"id": 9464, "synset": "niqab.n.01", "name": "niqab"}, + {"id": 9465, "synset": "nissen_hut.n.01", "name": "Nissen_hut"}, + {"id": 9466, "synset": "nogging.n.01", "name": "nogging"}, + {"id": 9467, "synset": "noisemaker.n.01", "name": "noisemaker"}, + {"id": 9468, "synset": "nonsmoker.n.02", "name": "nonsmoker"}, + {"id": 9469, "synset": "non-volatile_storage.n.01", "name": "non-volatile_storage"}, + {"id": 9470, "synset": "norfolk_jacket.n.01", "name": "Norfolk_jacket"}, + {"id": 9471, "synset": "noria.n.01", "name": "noria"}, + {"id": 9472, "synset": "nose_flute.n.01", "name": "nose_flute"}, + {"id": 9473, "synset": "nosewheel.n.01", "name": "nosewheel"}, + {"id": 9474, "synset": "notebook.n.02", "name": "notebook"}, + {"id": 9475, "synset": "nuclear-powered_ship.n.01", "name": "nuclear-powered_ship"}, + {"id": 9476, "synset": "nuclear_reactor.n.01", "name": "nuclear_reactor"}, + {"id": 9477, "synset": "nuclear_rocket.n.01", "name": "nuclear_rocket"}, + {"id": 9478, "synset": "nuclear_weapon.n.01", "name": "nuclear_weapon"}, + {"id": 9479, "synset": "nude.n.01", "name": "nude"}, + {"id": 9480, "synset": "numdah.n.01", "name": "numdah"}, + {"id": 9481, "synset": "nun's_habit.n.01", "name": "nun's_habit"}, + {"id": 9482, "synset": "nursery.n.01", "name": "nursery"}, + {"id": 9483, "synset": "nut_and_bolt.n.01", "name": "nut_and_bolt"}, + {"id": 9484, "synset": "nylon.n.02", "name": "nylon"}, + {"id": 9485, "synset": "nylons.n.01", "name": "nylons"}, + {"id": 9486, "synset": "oast.n.01", "name": "oast"}, + {"id": 9487, "synset": "oast_house.n.01", "name": "oast_house"}, + {"id": 9488, "synset": "obelisk.n.01", "name": "obelisk"}, + {"id": 9489, "synset": "object_ball.n.01", "name": "object_ball"}, + {"id": 9490, "synset": "objective.n.02", "name": "objective"}, + {"id": 9491, "synset": "oblique_bandage.n.01", "name": "oblique_bandage"}, + {"id": 9492, "synset": "oboe.n.01", "name": "oboe"}, + {"id": 9493, "synset": "oboe_da_caccia.n.01", "name": "oboe_da_caccia"}, + {"id": 9494, "synset": "oboe_d'amore.n.01", "name": "oboe_d'amore"}, + {"id": 9495, "synset": "observation_dome.n.01", "name": "observation_dome"}, + {"id": 9496, "synset": "observatory.n.01", "name": "observatory"}, + {"id": 9497, "synset": "obstacle.n.02", "name": "obstacle"}, + {"id": 9498, "synset": "obturator.n.01", "name": "obturator"}, + {"id": 9499, "synset": "ocarina.n.01", "name": "ocarina"}, + {"id": 9500, "synset": "octant.n.01", "name": "octant"}, + {"id": 9501, "synset": "odd-leg_caliper.n.01", "name": "odd-leg_caliper"}, + {"id": 9502, "synset": "odometer.n.01", "name": "odometer"}, + {"id": 9503, "synset": "oeil_de_boeuf.n.01", "name": "oeil_de_boeuf"}, + {"id": 9504, "synset": "office.n.01", "name": "office"}, + {"id": 9505, "synset": "office_building.n.01", "name": "office_building"}, + {"id": 9506, "synset": "office_furniture.n.01", "name": "office_furniture"}, + {"id": 9507, "synset": "officer's_mess.n.01", "name": "officer's_mess"}, + {"id": 9508, "synset": "off-line_equipment.n.01", "name": "off-line_equipment"}, + {"id": 9509, "synset": "ogee.n.01", "name": "ogee"}, + {"id": 9510, "synset": "ogee_arch.n.01", "name": "ogee_arch"}, + {"id": 9511, "synset": "ohmmeter.n.01", "name": "ohmmeter"}, + {"id": 9512, "synset": "oil.n.02", "name": "oil"}, + {"id": 9513, "synset": "oilcan.n.01", "name": "oilcan"}, + {"id": 9514, "synset": "oilcloth.n.01", "name": "oilcloth"}, + {"id": 9515, "synset": "oil_filter.n.01", "name": "oil_filter"}, + {"id": 9516, "synset": "oil_heater.n.01", "name": "oil_heater"}, + {"id": 9517, "synset": "oil_paint.n.01", "name": "oil_paint"}, + {"id": 9518, "synset": "oil_pump.n.01", "name": "oil_pump"}, + {"id": 9519, "synset": "oil_refinery.n.01", "name": "oil_refinery"}, + {"id": 9520, "synset": "oilskin.n.01", "name": "oilskin"}, + {"id": 9521, "synset": "oil_slick.n.01", "name": "oil_slick"}, + {"id": 9522, "synset": "oilstone.n.01", "name": "oilstone"}, + {"id": 9523, "synset": "oil_tanker.n.01", "name": "oil_tanker"}, + {"id": 9524, "synset": "old_school_tie.n.01", "name": "old_school_tie"}, + {"id": 9525, "synset": "olive_drab.n.03", "name": "olive_drab"}, + {"id": 9526, "synset": "olive_drab.n.02", "name": "olive_drab"}, + {"id": 9527, "synset": "olympian_zeus.n.01", "name": "Olympian_Zeus"}, + {"id": 9528, "synset": "omelet_pan.n.01", "name": "omelet_pan"}, + {"id": 9529, "synset": "omnidirectional_antenna.n.01", "name": "omnidirectional_antenna"}, + {"id": 9530, "synset": "omnirange.n.01", "name": "omnirange"}, + {"id": 9531, "synset": "onion_dome.n.01", "name": "onion_dome"}, + {"id": 9532, "synset": "open-air_market.n.01", "name": "open-air_market"}, + {"id": 9533, "synset": "open_circuit.n.01", "name": "open_circuit"}, + {"id": 9534, "synset": "open-end_wrench.n.01", "name": "open-end_wrench"}, + {"id": 9535, "synset": "opener.n.03", "name": "opener"}, + {"id": 9536, "synset": "open-hearth_furnace.n.01", "name": "open-hearth_furnace"}, + {"id": 9537, "synset": "openside_plane.n.01", "name": "openside_plane"}, + {"id": 9538, "synset": "open_sight.n.01", "name": "open_sight"}, + {"id": 9539, "synset": "openwork.n.01", "name": "openwork"}, + {"id": 9540, "synset": "opera.n.03", "name": "opera"}, + {"id": 9541, "synset": "opera_cloak.n.01", "name": "opera_cloak"}, + {"id": 9542, "synset": "operating_microscope.n.01", "name": "operating_microscope"}, + {"id": 9543, "synset": "operating_room.n.01", "name": "operating_room"}, + {"id": 9544, "synset": "operating_table.n.01", "name": "operating_table"}, + {"id": 9545, "synset": "ophthalmoscope.n.01", "name": "ophthalmoscope"}, + {"id": 9546, "synset": "optical_device.n.01", "name": "optical_device"}, + {"id": 9547, "synset": "optical_disk.n.01", "name": "optical_disk"}, + {"id": 9548, "synset": "optical_instrument.n.01", "name": "optical_instrument"}, + {"id": 9549, "synset": "optical_pyrometer.n.01", "name": "optical_pyrometer"}, + {"id": 9550, "synset": "optical_telescope.n.01", "name": "optical_telescope"}, + {"id": 9551, "synset": "orchestra_pit.n.01", "name": "orchestra_pit"}, + {"id": 9552, "synset": "ordinary.n.04", "name": "ordinary"}, + {"id": 9553, "synset": "organ.n.05", "name": "organ"}, + {"id": 9554, "synset": "organdy.n.01", "name": "organdy"}, + { + "id": 9555, + "synset": "organic_light-emitting_diode.n.01", + "name": "organic_light-emitting_diode", + }, + {"id": 9556, "synset": "organ_loft.n.01", "name": "organ_loft"}, + {"id": 9557, "synset": "organ_pipe.n.01", "name": "organ_pipe"}, + {"id": 9558, "synset": "organza.n.01", "name": "organza"}, + {"id": 9559, "synset": "oriel.n.01", "name": "oriel"}, + {"id": 9560, "synset": "oriflamme.n.02", "name": "oriflamme"}, + {"id": 9561, "synset": "o_ring.n.01", "name": "O_ring"}, + {"id": 9562, "synset": "orlon.n.01", "name": "Orlon"}, + {"id": 9563, "synset": "orlop_deck.n.01", "name": "orlop_deck"}, + {"id": 9564, "synset": "orphanage.n.02", "name": "orphanage"}, + {"id": 9565, "synset": "orphrey.n.01", "name": "orphrey"}, + {"id": 9566, "synset": "orrery.n.01", "name": "orrery"}, + {"id": 9567, "synset": "orthicon.n.01", "name": "orthicon"}, + {"id": 9568, "synset": "orthochromatic_film.n.01", "name": "orthochromatic_film"}, + {"id": 9569, "synset": "orthopter.n.01", "name": "orthopter"}, + {"id": 9570, "synset": "orthoscope.n.01", "name": "orthoscope"}, + {"id": 9571, "synset": "oscillograph.n.01", "name": "oscillograph"}, + {"id": 9572, "synset": "oscilloscope.n.01", "name": "oscilloscope"}, + {"id": 9573, "synset": "ossuary.n.01", "name": "ossuary"}, + {"id": 9574, "synset": "otoscope.n.01", "name": "otoscope"}, + {"id": 9575, "synset": "oubliette.n.01", "name": "oubliette"}, + {"id": 9576, "synset": "out-basket.n.01", "name": "out-basket"}, + {"id": 9577, "synset": "outboard_motor.n.01", "name": "outboard_motor"}, + {"id": 9578, "synset": "outboard_motorboat.n.01", "name": "outboard_motorboat"}, + {"id": 9579, "synset": "outbuilding.n.01", "name": "outbuilding"}, + {"id": 9580, "synset": "outerwear.n.01", "name": "outerwear"}, + {"id": 9581, "synset": "outfall.n.01", "name": "outfall"}, + {"id": 9582, "synset": "outfit.n.02", "name": "outfit"}, + {"id": 9583, "synset": "outfitter.n.02", "name": "outfitter"}, + {"id": 9584, "synset": "outhouse.n.01", "name": "outhouse"}, + {"id": 9585, "synset": "output_device.n.01", "name": "output_device"}, + {"id": 9586, "synset": "outrigger.n.01", "name": "outrigger"}, + {"id": 9587, "synset": "outrigger_canoe.n.01", "name": "outrigger_canoe"}, + {"id": 9588, "synset": "outside_caliper.n.01", "name": "outside_caliper"}, + {"id": 9589, "synset": "outside_mirror.n.01", "name": "outside_mirror"}, + {"id": 9590, "synset": "outwork.n.01", "name": "outwork"}, + {"id": 9591, "synset": "oven_thermometer.n.01", "name": "oven_thermometer"}, + {"id": 9592, "synset": "overall.n.02", "name": "overall"}, + {"id": 9593, "synset": "overcoat.n.02", "name": "overcoat"}, + {"id": 9594, "synset": "overdrive.n.02", "name": "overdrive"}, + {"id": 9595, "synset": "overgarment.n.01", "name": "overgarment"}, + {"id": 9596, "synset": "overhand_knot.n.01", "name": "overhand_knot"}, + {"id": 9597, "synset": "overhang.n.01", "name": "overhang"}, + {"id": 9598, "synset": "overhead_projector.n.01", "name": "overhead_projector"}, + {"id": 9599, "synset": "overmantel.n.01", "name": "overmantel"}, + {"id": 9600, "synset": "overnighter.n.02", "name": "overnighter"}, + {"id": 9601, "synset": "overpass.n.01", "name": "overpass"}, + {"id": 9602, "synset": "override.n.01", "name": "override"}, + {"id": 9603, "synset": "overshoe.n.01", "name": "overshoe"}, + {"id": 9604, "synset": "overskirt.n.01", "name": "overskirt"}, + {"id": 9605, "synset": "oxbow.n.03", "name": "oxbow"}, + {"id": 9606, "synset": "oxbridge.n.01", "name": "Oxbridge"}, + {"id": 9607, "synset": "oxcart.n.01", "name": "oxcart"}, + {"id": 9608, "synset": "oxeye.n.03", "name": "oxeye"}, + {"id": 9609, "synset": "oxford.n.04", "name": "oxford"}, + {"id": 9610, "synset": "oximeter.n.01", "name": "oximeter"}, + {"id": 9611, "synset": "oxyacetylene_torch.n.01", "name": "oxyacetylene_torch"}, + {"id": 9612, "synset": "oxygen_mask.n.01", "name": "oxygen_mask"}, + {"id": 9613, "synset": "oyster_bar.n.01", "name": "oyster_bar"}, + {"id": 9614, "synset": "oyster_bed.n.01", "name": "oyster_bed"}, + {"id": 9615, "synset": "pace_car.n.01", "name": "pace_car"}, + {"id": 9616, "synset": "pacemaker.n.03", "name": "pacemaker"}, + {"id": 9617, "synset": "pack.n.03", "name": "pack"}, + {"id": 9618, "synset": "pack.n.09", "name": "pack"}, + {"id": 9619, "synset": "pack.n.07", "name": "pack"}, + {"id": 9620, "synset": "package.n.02", "name": "package"}, + {"id": 9621, "synset": "package_store.n.01", "name": "package_store"}, + {"id": 9622, "synset": "packaging.n.03", "name": "packaging"}, + {"id": 9623, "synset": "packing_box.n.02", "name": "packing_box"}, + {"id": 9624, "synset": "packinghouse.n.02", "name": "packinghouse"}, + {"id": 9625, "synset": "packinghouse.n.01", "name": "packinghouse"}, + {"id": 9626, "synset": "packing_needle.n.01", "name": "packing_needle"}, + {"id": 9627, "synset": "packsaddle.n.01", "name": "packsaddle"}, + {"id": 9628, "synset": "paddle.n.02", "name": "paddle"}, + {"id": 9629, "synset": "paddle.n.01", "name": "paddle"}, + {"id": 9630, "synset": "paddle_box.n.01", "name": "paddle_box"}, + {"id": 9631, "synset": "paddle_steamer.n.01", "name": "paddle_steamer"}, + {"id": 9632, "synset": "paddlewheel.n.01", "name": "paddlewheel"}, + {"id": 9633, "synset": "paddock.n.01", "name": "paddock"}, + {"id": 9634, "synset": "page_printer.n.01", "name": "page_printer"}, + {"id": 9635, "synset": "paint.n.01", "name": "paint"}, + {"id": 9636, "synset": "paintball.n.01", "name": "paintball"}, + {"id": 9637, "synset": "paintball_gun.n.01", "name": "paintball_gun"}, + {"id": 9638, "synset": "paintbox.n.01", "name": "paintbox"}, + {"id": 9639, "synset": "paisley.n.01", "name": "paisley"}, + {"id": 9640, "synset": "pajama.n.01", "name": "pajama"}, + {"id": 9641, "synset": "palace.n.04", "name": "palace"}, + {"id": 9642, "synset": "palace.n.01", "name": "palace"}, + {"id": 9643, "synset": "palace.n.03", "name": "palace"}, + {"id": 9644, "synset": "palanquin.n.01", "name": "palanquin"}, + {"id": 9645, "synset": "paleolith.n.01", "name": "paleolith"}, + {"id": 9646, "synset": "palestra.n.01", "name": "palestra"}, + {"id": 9647, "synset": "palette_knife.n.01", "name": "palette_knife"}, + {"id": 9648, "synset": "palisade.n.01", "name": "palisade"}, + {"id": 9649, "synset": "pallet.n.03", "name": "pallet"}, + {"id": 9650, "synset": "pallette.n.01", "name": "pallette"}, + {"id": 9651, "synset": "pallium.n.04", "name": "pallium"}, + {"id": 9652, "synset": "pallium.n.03", "name": "pallium"}, + {"id": 9653, "synset": "pancake_turner.n.01", "name": "pancake_turner"}, + {"id": 9654, "synset": "panchromatic_film.n.01", "name": "panchromatic_film"}, + {"id": 9655, "synset": "panda_car.n.01", "name": "panda_car"}, + {"id": 9656, "synset": "paneling.n.01", "name": "paneling"}, + {"id": 9657, "synset": "panhandle.n.02", "name": "panhandle"}, + {"id": 9658, "synset": "panic_button.n.01", "name": "panic_button"}, + {"id": 9659, "synset": "pannier.n.02", "name": "pannier"}, + {"id": 9660, "synset": "pannier.n.01", "name": "pannier"}, + {"id": 9661, "synset": "pannikin.n.01", "name": "pannikin"}, + {"id": 9662, "synset": "panopticon.n.02", "name": "panopticon"}, + {"id": 9663, "synset": "panopticon.n.01", "name": "panopticon"}, + {"id": 9664, "synset": "panpipe.n.01", "name": "panpipe"}, + {"id": 9665, "synset": "pantaloon.n.03", "name": "pantaloon"}, + {"id": 9666, "synset": "pantechnicon.n.01", "name": "pantechnicon"}, + {"id": 9667, "synset": "pantheon.n.03", "name": "pantheon"}, + {"id": 9668, "synset": "pantheon.n.02", "name": "pantheon"}, + {"id": 9669, "synset": "pantie.n.01", "name": "pantie"}, + {"id": 9670, "synset": "panting.n.02", "name": "panting"}, + {"id": 9671, "synset": "pant_leg.n.01", "name": "pant_leg"}, + {"id": 9672, "synset": "pantograph.n.01", "name": "pantograph"}, + {"id": 9673, "synset": "pantry.n.01", "name": "pantry"}, + {"id": 9674, "synset": "pants_suit.n.01", "name": "pants_suit"}, + {"id": 9675, "synset": "panty_girdle.n.01", "name": "panty_girdle"}, + {"id": 9676, "synset": "panzer.n.01", "name": "panzer"}, + {"id": 9677, "synset": "paper_chain.n.01", "name": "paper_chain"}, + {"id": 9678, "synset": "paper_clip.n.01", "name": "paper_clip"}, + {"id": 9679, "synset": "paper_cutter.n.01", "name": "paper_cutter"}, + {"id": 9680, "synset": "paper_fastener.n.01", "name": "paper_fastener"}, + {"id": 9681, "synset": "paper_feed.n.01", "name": "paper_feed"}, + {"id": 9682, "synset": "paper_mill.n.01", "name": "paper_mill"}, + {"id": 9683, "synset": "parabolic_mirror.n.01", "name": "parabolic_mirror"}, + {"id": 9684, "synset": "parabolic_reflector.n.01", "name": "parabolic_reflector"}, + {"id": 9685, "synset": "parallel_bars.n.01", "name": "parallel_bars"}, + {"id": 9686, "synset": "parallel_circuit.n.01", "name": "parallel_circuit"}, + {"id": 9687, "synset": "parallel_interface.n.01", "name": "parallel_interface"}, + {"id": 9688, "synset": "parang.n.01", "name": "parang"}, + {"id": 9689, "synset": "parapet.n.02", "name": "parapet"}, + {"id": 9690, "synset": "parapet.n.01", "name": "parapet"}, + {"id": 9691, "synset": "parer.n.02", "name": "parer"}, + {"id": 9692, "synset": "parfait_glass.n.01", "name": "parfait_glass"}, + {"id": 9693, "synset": "pargeting.n.02", "name": "pargeting"}, + {"id": 9694, "synset": "pari-mutuel_machine.n.01", "name": "pari-mutuel_machine"}, + {"id": 9695, "synset": "park_bench.n.01", "name": "park_bench"}, + {"id": 9696, "synset": "parlor.n.01", "name": "parlor"}, + {"id": 9697, "synset": "parquet.n.01", "name": "parquet"}, + {"id": 9698, "synset": "parquetry.n.01", "name": "parquetry"}, + {"id": 9699, "synset": "parsonage.n.01", "name": "parsonage"}, + {"id": 9700, "synset": "parsons_table.n.01", "name": "Parsons_table"}, + {"id": 9701, "synset": "partial_denture.n.01", "name": "partial_denture"}, + {"id": 9702, "synset": "particle_detector.n.01", "name": "particle_detector"}, + {"id": 9703, "synset": "partition.n.01", "name": "partition"}, + {"id": 9704, "synset": "parts_bin.n.01", "name": "parts_bin"}, + {"id": 9705, "synset": "party_line.n.02", "name": "party_line"}, + {"id": 9706, "synset": "party_wall.n.01", "name": "party_wall"}, + {"id": 9707, "synset": "parvis.n.01", "name": "parvis"}, + {"id": 9708, "synset": "passenger_train.n.01", "name": "passenger_train"}, + {"id": 9709, "synset": "passenger_van.n.01", "name": "passenger_van"}, + {"id": 9710, "synset": "passe-partout.n.02", "name": "passe-partout"}, + {"id": 9711, "synset": "passive_matrix_display.n.01", "name": "passive_matrix_display"}, + {"id": 9712, "synset": "passkey.n.01", "name": "passkey"}, + {"id": 9713, "synset": "pass-through.n.01", "name": "pass-through"}, + {"id": 9714, "synset": "pastry_cart.n.01", "name": "pastry_cart"}, + {"id": 9715, "synset": "patch.n.03", "name": "patch"}, + {"id": 9716, "synset": "patchcord.n.01", "name": "patchcord"}, + {"id": 9717, "synset": "patchouli.n.02", "name": "patchouli"}, + {"id": 9718, "synset": "patch_pocket.n.01", "name": "patch_pocket"}, + {"id": 9719, "synset": "patchwork.n.02", "name": "patchwork"}, + {"id": 9720, "synset": "patent_log.n.01", "name": "patent_log"}, + {"id": 9721, "synset": "paternoster.n.02", "name": "paternoster"}, + {"id": 9722, "synset": "patina.n.01", "name": "patina"}, + {"id": 9723, "synset": "patio.n.01", "name": "patio"}, + {"id": 9724, "synset": "patisserie.n.01", "name": "patisserie"}, + {"id": 9725, "synset": "patka.n.01", "name": "patka"}, + {"id": 9726, "synset": "patrol_boat.n.01", "name": "patrol_boat"}, + {"id": 9727, "synset": "patty-pan.n.01", "name": "patty-pan"}, + {"id": 9728, "synset": "pave.n.01", "name": "pave"}, + {"id": 9729, "synset": "pavilion.n.01", "name": "pavilion"}, + {"id": 9730, "synset": "pavior.n.01", "name": "pavior"}, + {"id": 9731, "synset": "pavis.n.01", "name": "pavis"}, + {"id": 9732, "synset": "pawn.n.03", "name": "pawn"}, + {"id": 9733, "synset": "pawnbroker's_shop.n.01", "name": "pawnbroker's_shop"}, + {"id": 9734, "synset": "pay-phone.n.01", "name": "pay-phone"}, + {"id": 9735, "synset": "pc_board.n.01", "name": "PC_board"}, + {"id": 9736, "synset": "peach_orchard.n.01", "name": "peach_orchard"}, + {"id": 9737, "synset": "pea_jacket.n.01", "name": "pea_jacket"}, + {"id": 9738, "synset": "peavey.n.01", "name": "peavey"}, + {"id": 9739, "synset": "pectoral.n.02", "name": "pectoral"}, + {"id": 9740, "synset": "pedal.n.02", "name": "pedal"}, + {"id": 9741, "synset": "pedal_pusher.n.01", "name": "pedal_pusher"}, + {"id": 9742, "synset": "pedestal.n.03", "name": "pedestal"}, + {"id": 9743, "synset": "pedestal_table.n.01", "name": "pedestal_table"}, + {"id": 9744, "synset": "pedestrian_crossing.n.01", "name": "pedestrian_crossing"}, + {"id": 9745, "synset": "pedicab.n.01", "name": "pedicab"}, + {"id": 9746, "synset": "pediment.n.01", "name": "pediment"}, + {"id": 9747, "synset": "pedometer.n.01", "name": "pedometer"}, + {"id": 9748, "synset": "peep_sight.n.01", "name": "peep_sight"}, + {"id": 9749, "synset": "peg.n.01", "name": "peg"}, + {"id": 9750, "synset": "peg.n.06", "name": "peg"}, + {"id": 9751, "synset": "peg.n.05", "name": "peg"}, + {"id": 9752, "synset": "pelham.n.01", "name": "Pelham"}, + {"id": 9753, "synset": "pelican_crossing.n.01", "name": "pelican_crossing"}, + {"id": 9754, "synset": "pelisse.n.01", "name": "pelisse"}, + {"id": 9755, "synset": "pelvimeter.n.01", "name": "pelvimeter"}, + {"id": 9756, "synset": "penal_colony.n.01", "name": "penal_colony"}, + {"id": 9757, "synset": "penal_institution.n.01", "name": "penal_institution"}, + {"id": 9758, "synset": "penalty_box.n.01", "name": "penalty_box"}, + {"id": 9759, "synset": "pen-and-ink.n.01", "name": "pen-and-ink"}, + {"id": 9760, "synset": "pencil.n.04", "name": "pencil"}, + {"id": 9761, "synset": "pendant_earring.n.01", "name": "pendant_earring"}, + {"id": 9762, "synset": "pendulum_clock.n.01", "name": "pendulum_clock"}, + {"id": 9763, "synset": "pendulum_watch.n.01", "name": "pendulum_watch"}, + {"id": 9764, "synset": "penetration_bomb.n.01", "name": "penetration_bomb"}, + {"id": 9765, "synset": "penile_implant.n.01", "name": "penile_implant"}, + {"id": 9766, "synset": "penitentiary.n.01", "name": "penitentiary"}, + {"id": 9767, "synset": "penknife.n.01", "name": "penknife"}, + {"id": 9768, "synset": "penlight.n.01", "name": "penlight"}, + {"id": 9769, "synset": "pennant.n.03", "name": "pennant"}, + {"id": 9770, "synset": "pennywhistle.n.01", "name": "pennywhistle"}, + {"id": 9771, "synset": "penthouse.n.01", "name": "penthouse"}, + {"id": 9772, "synset": "pentode.n.01", "name": "pentode"}, + {"id": 9773, "synset": "peplos.n.01", "name": "peplos"}, + {"id": 9774, "synset": "peplum.n.01", "name": "peplum"}, + {"id": 9775, "synset": "pepper_shaker.n.01", "name": "pepper_shaker"}, + {"id": 9776, "synset": "pepper_spray.n.01", "name": "pepper_spray"}, + {"id": 9777, "synset": "percale.n.01", "name": "percale"}, + {"id": 9778, "synset": "percolator.n.01", "name": "percolator"}, + {"id": 9779, "synset": "percussion_cap.n.01", "name": "percussion_cap"}, + {"id": 9780, "synset": "percussion_instrument.n.01", "name": "percussion_instrument"}, + {"id": 9781, "synset": "perforation.n.01", "name": "perforation"}, + {"id": 9782, "synset": "perfumery.n.03", "name": "perfumery"}, + {"id": 9783, "synset": "perfumery.n.02", "name": "perfumery"}, + {"id": 9784, "synset": "perfumery.n.01", "name": "perfumery"}, + {"id": 9785, "synset": "peripheral.n.01", "name": "peripheral"}, + {"id": 9786, "synset": "periscope.n.01", "name": "periscope"}, + {"id": 9787, "synset": "peristyle.n.01", "name": "peristyle"}, + {"id": 9788, "synset": "periwig.n.01", "name": "periwig"}, + {"id": 9789, "synset": "permanent_press.n.01", "name": "permanent_press"}, + {"id": 9790, "synset": "perpetual_motion_machine.n.01", "name": "perpetual_motion_machine"}, + {"id": 9791, "synset": "personal_computer.n.01", "name": "personal_computer"}, + {"id": 9792, "synset": "personal_digital_assistant.n.01", "name": "personal_digital_assistant"}, + {"id": 9793, "synset": "personnel_carrier.n.01", "name": "personnel_carrier"}, + {"id": 9794, "synset": "pestle.n.03", "name": "pestle"}, + {"id": 9795, "synset": "pestle.n.02", "name": "pestle"}, + {"id": 9796, "synset": "petcock.n.01", "name": "petcock"}, + {"id": 9797, "synset": "petri_dish.n.01", "name": "Petri_dish"}, + {"id": 9798, "synset": "petrolatum_gauze.n.01", "name": "petrolatum_gauze"}, + {"id": 9799, "synset": "pet_shop.n.01", "name": "pet_shop"}, + {"id": 9800, "synset": "petticoat.n.01", "name": "petticoat"}, + {"id": 9801, "synset": "phial.n.01", "name": "phial"}, + {"id": 9802, "synset": "phillips_screw.n.01", "name": "Phillips_screw"}, + {"id": 9803, "synset": "phillips_screwdriver.n.01", "name": "Phillips_screwdriver"}, + {"id": 9804, "synset": "phonograph_needle.n.01", "name": "phonograph_needle"}, + {"id": 9805, "synset": "photocathode.n.01", "name": "photocathode"}, + {"id": 9806, "synset": "photocoagulator.n.01", "name": "photocoagulator"}, + {"id": 9807, "synset": "photocopier.n.01", "name": "photocopier"}, + {"id": 9808, "synset": "photographic_equipment.n.01", "name": "photographic_equipment"}, + {"id": 9809, "synset": "photographic_paper.n.01", "name": "photographic_paper"}, + {"id": 9810, "synset": "photometer.n.01", "name": "photometer"}, + {"id": 9811, "synset": "photomicrograph.n.01", "name": "photomicrograph"}, + {"id": 9812, "synset": "photostat.n.02", "name": "Photostat"}, + {"id": 9813, "synset": "photostat.n.01", "name": "photostat"}, + {"id": 9814, "synset": "physical_pendulum.n.01", "name": "physical_pendulum"}, + {"id": 9815, "synset": "piano_action.n.01", "name": "piano_action"}, + {"id": 9816, "synset": "piano_keyboard.n.01", "name": "piano_keyboard"}, + {"id": 9817, "synset": "piano_wire.n.01", "name": "piano_wire"}, + {"id": 9818, "synset": "piccolo.n.01", "name": "piccolo"}, + {"id": 9819, "synset": "pick.n.07", "name": "pick"}, + {"id": 9820, "synset": "pick.n.06", "name": "pick"}, + {"id": 9821, "synset": "pick.n.05", "name": "pick"}, + {"id": 9822, "synset": "pickelhaube.n.01", "name": "pickelhaube"}, + {"id": 9823, "synset": "picket_boat.n.01", "name": "picket_boat"}, + {"id": 9824, "synset": "picket_fence.n.01", "name": "picket_fence"}, + {"id": 9825, "synset": "picket_ship.n.01", "name": "picket_ship"}, + {"id": 9826, "synset": "pickle_barrel.n.01", "name": "pickle_barrel"}, + {"id": 9827, "synset": "picture_frame.n.01", "name": "picture_frame"}, + {"id": 9828, "synset": "picture_hat.n.01", "name": "picture_hat"}, + {"id": 9829, "synset": "picture_rail.n.01", "name": "picture_rail"}, + {"id": 9830, "synset": "picture_window.n.01", "name": "picture_window"}, + {"id": 9831, "synset": "piece_of_cloth.n.01", "name": "piece_of_cloth"}, + {"id": 9832, "synset": "pied-a-terre.n.01", "name": "pied-a-terre"}, + {"id": 9833, "synset": "pier.n.03", "name": "pier"}, + {"id": 9834, "synset": "pier.n.02", "name": "pier"}, + {"id": 9835, "synset": "pier_arch.n.01", "name": "pier_arch"}, + {"id": 9836, "synset": "pier_glass.n.01", "name": "pier_glass"}, + {"id": 9837, "synset": "pier_table.n.01", "name": "pier_table"}, + {"id": 9838, "synset": "pieta.n.01", "name": "pieta"}, + {"id": 9839, "synset": "piezometer.n.01", "name": "piezometer"}, + {"id": 9840, "synset": "pig_bed.n.01", "name": "pig_bed"}, + {"id": 9841, "synset": "piggery.n.01", "name": "piggery"}, + {"id": 9842, "synset": "pilaster.n.01", "name": "pilaster"}, + {"id": 9843, "synset": "pile.n.06", "name": "pile"}, + {"id": 9844, "synset": "pile_driver.n.01", "name": "pile_driver"}, + {"id": 9845, "synset": "pill_bottle.n.01", "name": "pill_bottle"}, + {"id": 9846, "synset": "pillbox.n.01", "name": "pillbox"}, + {"id": 9847, "synset": "pillion.n.01", "name": "pillion"}, + {"id": 9848, "synset": "pillory.n.01", "name": "pillory"}, + {"id": 9849, "synset": "pillow_block.n.01", "name": "pillow_block"}, + {"id": 9850, "synset": "pillow_lace.n.01", "name": "pillow_lace"}, + {"id": 9851, "synset": "pillow_sham.n.01", "name": "pillow_sham"}, + {"id": 9852, "synset": "pilot_bit.n.01", "name": "pilot_bit"}, + {"id": 9853, "synset": "pilot_boat.n.01", "name": "pilot_boat"}, + {"id": 9854, "synset": "pilot_burner.n.01", "name": "pilot_burner"}, + {"id": 9855, "synset": "pilot_cloth.n.01", "name": "pilot_cloth"}, + {"id": 9856, "synset": "pilot_engine.n.01", "name": "pilot_engine"}, + {"id": 9857, "synset": "pilothouse.n.01", "name": "pilothouse"}, + {"id": 9858, "synset": "pilot_light.n.02", "name": "pilot_light"}, + {"id": 9859, "synset": "pin.n.08", "name": "pin"}, + {"id": 9860, "synset": "pin.n.07", "name": "pin"}, + {"id": 9861, "synset": "pinata.n.01", "name": "pinata"}, + {"id": 9862, "synset": "pinball_machine.n.01", "name": "pinball_machine"}, + {"id": 9863, "synset": "pince-nez.n.01", "name": "pince-nez"}, + {"id": 9864, "synset": "pincer.n.01", "name": "pincer"}, + {"id": 9865, "synset": "pinch_bar.n.01", "name": "pinch_bar"}, + {"id": 9866, "synset": "pincurl_clip.n.01", "name": "pincurl_clip"}, + {"id": 9867, "synset": "pinfold.n.01", "name": "pinfold"}, + {"id": 9868, "synset": "pinhead.n.02", "name": "pinhead"}, + {"id": 9869, "synset": "pinion.n.01", "name": "pinion"}, + {"id": 9870, "synset": "pinnacle.n.01", "name": "pinnacle"}, + {"id": 9871, "synset": "pinprick.n.02", "name": "pinprick"}, + {"id": 9872, "synset": "pinstripe.n.03", "name": "pinstripe"}, + {"id": 9873, "synset": "pinstripe.n.02", "name": "pinstripe"}, + {"id": 9874, "synset": "pinstripe.n.01", "name": "pinstripe"}, + {"id": 9875, "synset": "pintle.n.01", "name": "pintle"}, + {"id": 9876, "synset": "pinwheel.n.02", "name": "pinwheel"}, + {"id": 9877, "synset": "tabor_pipe.n.01", "name": "tabor_pipe"}, + {"id": 9878, "synset": "pipe.n.04", "name": "pipe"}, + {"id": 9879, "synset": "pipe_bomb.n.01", "name": "pipe_bomb"}, + {"id": 9880, "synset": "pipe_cleaner.n.01", "name": "pipe_cleaner"}, + {"id": 9881, "synset": "pipe_cutter.n.01", "name": "pipe_cutter"}, + {"id": 9882, "synset": "pipefitting.n.01", "name": "pipefitting"}, + {"id": 9883, "synset": "pipet.n.01", "name": "pipet"}, + {"id": 9884, "synset": "pipe_vise.n.01", "name": "pipe_vise"}, + {"id": 9885, "synset": "pipe_wrench.n.01", "name": "pipe_wrench"}, + {"id": 9886, "synset": "pique.n.01", "name": "pique"}, + {"id": 9887, "synset": "pirate.n.03", "name": "pirate"}, + {"id": 9888, "synset": "piste.n.02", "name": "piste"}, + {"id": 9889, "synset": "pistol_grip.n.01", "name": "pistol_grip"}, + {"id": 9890, "synset": "piston.n.02", "name": "piston"}, + {"id": 9891, "synset": "piston_ring.n.01", "name": "piston_ring"}, + {"id": 9892, "synset": "piston_rod.n.01", "name": "piston_rod"}, + {"id": 9893, "synset": "pit.n.07", "name": "pit"}, + {"id": 9894, "synset": "pitching_wedge.n.01", "name": "pitching_wedge"}, + {"id": 9895, "synset": "pitch_pipe.n.01", "name": "pitch_pipe"}, + {"id": 9896, "synset": "pith_hat.n.01", "name": "pith_hat"}, + {"id": 9897, "synset": "piton.n.01", "name": "piton"}, + {"id": 9898, "synset": "pitot-static_tube.n.01", "name": "Pitot-static_tube"}, + {"id": 9899, "synset": "pitot_tube.n.01", "name": "Pitot_tube"}, + {"id": 9900, "synset": "pitsaw.n.01", "name": "pitsaw"}, + {"id": 9901, "synset": "pivot.n.02", "name": "pivot"}, + {"id": 9902, "synset": "pivoting_window.n.01", "name": "pivoting_window"}, + {"id": 9903, "synset": "pizzeria.n.01", "name": "pizzeria"}, + {"id": 9904, "synset": "place_of_business.n.01", "name": "place_of_business"}, + {"id": 9905, "synset": "place_of_worship.n.01", "name": "place_of_worship"}, + {"id": 9906, "synset": "placket.n.01", "name": "placket"}, + {"id": 9907, "synset": "planchet.n.01", "name": "planchet"}, + {"id": 9908, "synset": "plane.n.05", "name": "plane"}, + {"id": 9909, "synset": "plane.n.04", "name": "plane"}, + {"id": 9910, "synset": "plane_seat.n.01", "name": "plane_seat"}, + {"id": 9911, "synset": "planetarium.n.03", "name": "planetarium"}, + {"id": 9912, "synset": "planetarium.n.02", "name": "planetarium"}, + {"id": 9913, "synset": "planetarium.n.01", "name": "planetarium"}, + {"id": 9914, "synset": "planetary_gear.n.01", "name": "planetary_gear"}, + {"id": 9915, "synset": "plank-bed.n.01", "name": "plank-bed"}, + {"id": 9916, "synset": "planking.n.02", "name": "planking"}, + {"id": 9917, "synset": "planner.n.02", "name": "planner"}, + {"id": 9918, "synset": "plant.n.01", "name": "plant"}, + {"id": 9919, "synset": "planter.n.03", "name": "planter"}, + {"id": 9920, "synset": "plaster.n.05", "name": "plaster"}, + {"id": 9921, "synset": "plasterboard.n.01", "name": "plasterboard"}, + {"id": 9922, "synset": "plastering_trowel.n.01", "name": "plastering_trowel"}, + {"id": 9923, "synset": "plastic_bag.n.01", "name": "plastic_bag"}, + {"id": 9924, "synset": "plastic_bomb.n.01", "name": "plastic_bomb"}, + {"id": 9925, "synset": "plastic_laminate.n.01", "name": "plastic_laminate"}, + {"id": 9926, "synset": "plastic_wrap.n.01", "name": "plastic_wrap"}, + {"id": 9927, "synset": "plastron.n.03", "name": "plastron"}, + {"id": 9928, "synset": "plastron.n.02", "name": "plastron"}, + {"id": 9929, "synset": "plastron.n.01", "name": "plastron"}, + {"id": 9930, "synset": "plate.n.14", "name": "plate"}, + {"id": 9931, "synset": "plate.n.13", "name": "plate"}, + {"id": 9932, "synset": "plate.n.12", "name": "plate"}, + {"id": 9933, "synset": "platen.n.03", "name": "platen"}, + {"id": 9934, "synset": "platen.n.01", "name": "platen"}, + {"id": 9935, "synset": "plate_rack.n.01", "name": "plate_rack"}, + {"id": 9936, "synset": "plate_rail.n.01", "name": "plate_rail"}, + {"id": 9937, "synset": "platform.n.01", "name": "platform"}, + {"id": 9938, "synset": "platform.n.04", "name": "platform"}, + {"id": 9939, "synset": "platform.n.03", "name": "platform"}, + {"id": 9940, "synset": "platform_bed.n.01", "name": "platform_bed"}, + {"id": 9941, "synset": "platform_rocker.n.01", "name": "platform_rocker"}, + {"id": 9942, "synset": "plating.n.01", "name": "plating"}, + {"id": 9943, "synset": "playback.n.02", "name": "playback"}, + {"id": 9944, "synset": "playbox.n.01", "name": "playbox"}, + {"id": 9945, "synset": "playground.n.02", "name": "playground"}, + {"id": 9946, "synset": "playsuit.n.01", "name": "playsuit"}, + {"id": 9947, "synset": "plaza.n.02", "name": "plaza"}, + {"id": 9948, "synset": "pleat.n.01", "name": "pleat"}, + {"id": 9949, "synset": "plenum.n.02", "name": "plenum"}, + {"id": 9950, "synset": "plethysmograph.n.01", "name": "plethysmograph"}, + {"id": 9951, "synset": "pleximeter.n.01", "name": "pleximeter"}, + {"id": 9952, "synset": "plexor.n.01", "name": "plexor"}, + {"id": 9953, "synset": "plimsoll.n.02", "name": "plimsoll"}, + {"id": 9954, "synset": "plotter.n.04", "name": "plotter"}, + {"id": 9955, "synset": "plug.n.01", "name": "plug"}, + {"id": 9956, "synset": "plug.n.05", "name": "plug"}, + {"id": 9957, "synset": "plug_fuse.n.01", "name": "plug_fuse"}, + {"id": 9958, "synset": "plughole.n.01", "name": "plughole"}, + {"id": 9959, "synset": "plumb_bob.n.01", "name": "plumb_bob"}, + {"id": 9960, "synset": "plumb_level.n.01", "name": "plumb_level"}, + {"id": 9961, "synset": "plunger.n.03", "name": "plunger"}, + {"id": 9962, "synset": "plus_fours.n.01", "name": "plus_fours"}, + {"id": 9963, "synset": "plush.n.01", "name": "plush"}, + {"id": 9964, "synset": "plywood.n.01", "name": "plywood"}, + {"id": 9965, "synset": "pneumatic_drill.n.01", "name": "pneumatic_drill"}, + {"id": 9966, "synset": "p-n_junction.n.01", "name": "p-n_junction"}, + {"id": 9967, "synset": "p-n-p_transistor.n.01", "name": "p-n-p_transistor"}, + {"id": 9968, "synset": "poacher.n.02", "name": "poacher"}, + {"id": 9969, "synset": "pocket.n.01", "name": "pocket"}, + {"id": 9970, "synset": "pocket_battleship.n.01", "name": "pocket_battleship"}, + {"id": 9971, "synset": "pocketcomb.n.01", "name": "pocketcomb"}, + {"id": 9972, "synset": "pocket_flap.n.01", "name": "pocket_flap"}, + {"id": 9973, "synset": "pocket-handkerchief.n.01", "name": "pocket-handkerchief"}, + {"id": 9974, "synset": "pod.n.04", "name": "pod"}, + {"id": 9975, "synset": "pogo_stick.n.01", "name": "pogo_stick"}, + {"id": 9976, "synset": "point-and-shoot_camera.n.01", "name": "point-and-shoot_camera"}, + {"id": 9977, "synset": "pointed_arch.n.01", "name": "pointed_arch"}, + {"id": 9978, "synset": "pointing_trowel.n.01", "name": "pointing_trowel"}, + {"id": 9979, "synset": "point_lace.n.01", "name": "point_lace"}, + {"id": 9980, "synset": "polarimeter.n.01", "name": "polarimeter"}, + {"id": 9981, "synset": "polaroid.n.01", "name": "Polaroid"}, + {"id": 9982, "synset": "polaroid_camera.n.01", "name": "Polaroid_camera"}, + {"id": 9983, "synset": "pole.n.09", "name": "pole"}, + {"id": 9984, "synset": "poleax.n.02", "name": "poleax"}, + {"id": 9985, "synset": "poleax.n.01", "name": "poleax"}, + {"id": 9986, "synset": "police_boat.n.01", "name": "police_boat"}, + {"id": 9987, "synset": "police_van.n.01", "name": "police_van"}, + {"id": 9988, "synset": "polling_booth.n.01", "name": "polling_booth"}, + {"id": 9989, "synset": "polo_ball.n.01", "name": "polo_ball"}, + {"id": 9990, "synset": "polo_mallet.n.01", "name": "polo_mallet"}, + {"id": 9991, "synset": "polonaise.n.01", "name": "polonaise"}, + {"id": 9992, "synset": "polyester.n.03", "name": "polyester"}, + {"id": 9993, "synset": "polygraph.n.01", "name": "polygraph"}, + {"id": 9994, "synset": "pomade.n.01", "name": "pomade"}, + {"id": 9995, "synset": "pommel_horse.n.01", "name": "pommel_horse"}, + {"id": 9996, "synset": "pongee.n.01", "name": "pongee"}, + {"id": 9997, "synset": "poniard.n.01", "name": "poniard"}, + {"id": 9998, "synset": "pontifical.n.01", "name": "pontifical"}, + {"id": 9999, "synset": "pontoon.n.01", "name": "pontoon"}, + {"id": 10000, "synset": "pontoon_bridge.n.01", "name": "pontoon_bridge"}, + {"id": 10001, "synset": "pony_cart.n.01", "name": "pony_cart"}, + {"id": 10002, "synset": "pool_ball.n.01", "name": "pool_ball"}, + {"id": 10003, "synset": "poolroom.n.01", "name": "poolroom"}, + {"id": 10004, "synset": "poop_deck.n.01", "name": "poop_deck"}, + {"id": 10005, "synset": "poor_box.n.01", "name": "poor_box"}, + {"id": 10006, "synset": "poorhouse.n.01", "name": "poorhouse"}, + {"id": 10007, "synset": "pop_bottle.n.01", "name": "pop_bottle"}, + {"id": 10008, "synset": "popgun.n.01", "name": "popgun"}, + {"id": 10009, "synset": "poplin.n.01", "name": "poplin"}, + {"id": 10010, "synset": "popper.n.03", "name": "popper"}, + {"id": 10011, "synset": "poppet.n.01", "name": "poppet"}, + {"id": 10012, "synset": "pop_tent.n.01", "name": "pop_tent"}, + {"id": 10013, "synset": "porcelain.n.01", "name": "porcelain"}, + {"id": 10014, "synset": "porch.n.01", "name": "porch"}, + {"id": 10015, "synset": "porkpie.n.01", "name": "porkpie"}, + {"id": 10016, "synset": "porringer.n.01", "name": "porringer"}, + {"id": 10017, "synset": "portable.n.01", "name": "portable"}, + {"id": 10018, "synset": "portable_computer.n.01", "name": "portable_computer"}, + {"id": 10019, "synset": "portable_circular_saw.n.01", "name": "portable_circular_saw"}, + {"id": 10020, "synset": "portcullis.n.01", "name": "portcullis"}, + {"id": 10021, "synset": "porte-cochere.n.02", "name": "porte-cochere"}, + {"id": 10022, "synset": "porte-cochere.n.01", "name": "porte-cochere"}, + {"id": 10023, "synset": "portfolio.n.01", "name": "portfolio"}, + {"id": 10024, "synset": "porthole.n.01", "name": "porthole"}, + {"id": 10025, "synset": "portico.n.01", "name": "portico"}, + {"id": 10026, "synset": "portiere.n.01", "name": "portiere"}, + {"id": 10027, "synset": "portmanteau.n.02", "name": "portmanteau"}, + {"id": 10028, "synset": "portrait_camera.n.01", "name": "portrait_camera"}, + {"id": 10029, "synset": "portrait_lens.n.01", "name": "portrait_lens"}, + {"id": 10030, "synset": "positive_pole.n.02", "name": "positive_pole"}, + {"id": 10031, "synset": "positive_pole.n.01", "name": "positive_pole"}, + { + "id": 10032, + "synset": "positron_emission_tomography_scanner.n.01", + "name": "positron_emission_tomography_scanner", + }, + {"id": 10033, "synset": "post.n.04", "name": "post"}, + {"id": 10034, "synset": "postage_meter.n.01", "name": "postage_meter"}, + {"id": 10035, "synset": "post_and_lintel.n.01", "name": "post_and_lintel"}, + {"id": 10036, "synset": "post_chaise.n.01", "name": "post_chaise"}, + {"id": 10037, "synset": "postern.n.01", "name": "postern"}, + {"id": 10038, "synset": "post_exchange.n.01", "name": "post_exchange"}, + {"id": 10039, "synset": "posthole_digger.n.01", "name": "posthole_digger"}, + {"id": 10040, "synset": "post_horn.n.01", "name": "post_horn"}, + {"id": 10041, "synset": "posthouse.n.01", "name": "posthouse"}, + {"id": 10042, "synset": "potbelly.n.02", "name": "potbelly"}, + {"id": 10043, "synset": "potemkin_village.n.01", "name": "Potemkin_village"}, + {"id": 10044, "synset": "potential_divider.n.01", "name": "potential_divider"}, + {"id": 10045, "synset": "potentiometer.n.02", "name": "potentiometer"}, + {"id": 10046, "synset": "potentiometer.n.01", "name": "potentiometer"}, + {"id": 10047, "synset": "potpourri.n.03", "name": "potpourri"}, + {"id": 10048, "synset": "potsherd.n.01", "name": "potsherd"}, + {"id": 10049, "synset": "potter's_wheel.n.01", "name": "potter's_wheel"}, + {"id": 10050, "synset": "pottle.n.01", "name": "pottle"}, + {"id": 10051, "synset": "potty_seat.n.01", "name": "potty_seat"}, + {"id": 10052, "synset": "poultice.n.01", "name": "poultice"}, + {"id": 10053, "synset": "pound.n.13", "name": "pound"}, + {"id": 10054, "synset": "pound_net.n.01", "name": "pound_net"}, + {"id": 10055, "synset": "powder.n.03", "name": "powder"}, + {"id": 10056, "synset": "powder_and_shot.n.01", "name": "powder_and_shot"}, + {"id": 10057, "synset": "powdered_mustard.n.01", "name": "powdered_mustard"}, + {"id": 10058, "synset": "powder_horn.n.01", "name": "powder_horn"}, + {"id": 10059, "synset": "powder_keg.n.02", "name": "powder_keg"}, + {"id": 10060, "synset": "power_brake.n.01", "name": "power_brake"}, + {"id": 10061, "synset": "power_cord.n.01", "name": "power_cord"}, + {"id": 10062, "synset": "power_drill.n.01", "name": "power_drill"}, + {"id": 10063, "synset": "power_line.n.01", "name": "power_line"}, + {"id": 10064, "synset": "power_loom.n.01", "name": "power_loom"}, + {"id": 10065, "synset": "power_mower.n.01", "name": "power_mower"}, + {"id": 10066, "synset": "power_pack.n.01", "name": "power_pack"}, + {"id": 10067, "synset": "power_saw.n.01", "name": "power_saw"}, + {"id": 10068, "synset": "power_steering.n.01", "name": "power_steering"}, + {"id": 10069, "synset": "power_takeoff.n.01", "name": "power_takeoff"}, + {"id": 10070, "synset": "power_tool.n.01", "name": "power_tool"}, + {"id": 10071, "synset": "praetorium.n.01", "name": "praetorium"}, + {"id": 10072, "synset": "prayer_rug.n.01", "name": "prayer_rug"}, + {"id": 10073, "synset": "prayer_shawl.n.01", "name": "prayer_shawl"}, + {"id": 10074, "synset": "precipitator.n.01", "name": "precipitator"}, + {"id": 10075, "synset": "prefab.n.01", "name": "prefab"}, + {"id": 10076, "synset": "presbytery.n.01", "name": "presbytery"}, + {"id": 10077, "synset": "presence_chamber.n.01", "name": "presence_chamber"}, + {"id": 10078, "synset": "press.n.07", "name": "press"}, + {"id": 10079, "synset": "press.n.03", "name": "press"}, + {"id": 10080, "synset": "press.n.06", "name": "press"}, + {"id": 10081, "synset": "press_box.n.01", "name": "press_box"}, + {"id": 10082, "synset": "press_gallery.n.01", "name": "press_gallery"}, + {"id": 10083, "synset": "press_of_sail.n.01", "name": "press_of_sail"}, + {"id": 10084, "synset": "pressure_cabin.n.01", "name": "pressure_cabin"}, + {"id": 10085, "synset": "pressure_cooker.n.01", "name": "pressure_cooker"}, + {"id": 10086, "synset": "pressure_dome.n.01", "name": "pressure_dome"}, + {"id": 10087, "synset": "pressure_gauge.n.01", "name": "pressure_gauge"}, + {"id": 10088, "synset": "pressurized_water_reactor.n.01", "name": "pressurized_water_reactor"}, + {"id": 10089, "synset": "pressure_suit.n.01", "name": "pressure_suit"}, + {"id": 10090, "synset": "pricket.n.01", "name": "pricket"}, + {"id": 10091, "synset": "prie-dieu.n.01", "name": "prie-dieu"}, + {"id": 10092, "synset": "primary_coil.n.01", "name": "primary_coil"}, + {"id": 10093, "synset": "primus_stove.n.01", "name": "Primus_stove"}, + {"id": 10094, "synset": "prince_albert.n.02", "name": "Prince_Albert"}, + {"id": 10095, "synset": "print.n.06", "name": "print"}, + {"id": 10096, "synset": "print_buffer.n.01", "name": "print_buffer"}, + {"id": 10097, "synset": "printed_circuit.n.01", "name": "printed_circuit"}, + {"id": 10098, "synset": "printer.n.02", "name": "printer"}, + {"id": 10099, "synset": "printer_cable.n.01", "name": "printer_cable"}, + {"id": 10100, "synset": "priory.n.01", "name": "priory"}, + {"id": 10101, "synset": "prison.n.01", "name": "prison"}, + {"id": 10102, "synset": "prison_camp.n.01", "name": "prison_camp"}, + {"id": 10103, "synset": "privateer.n.02", "name": "privateer"}, + {"id": 10104, "synset": "private_line.n.01", "name": "private_line"}, + {"id": 10105, "synset": "privet_hedge.n.01", "name": "privet_hedge"}, + {"id": 10106, "synset": "probe.n.02", "name": "probe"}, + {"id": 10107, "synset": "proctoscope.n.01", "name": "proctoscope"}, + {"id": 10108, "synset": "prod.n.02", "name": "prod"}, + {"id": 10109, "synset": "production_line.n.01", "name": "production_line"}, + {"id": 10110, "synset": "projector.n.01", "name": "projector"}, + {"id": 10111, "synset": "prolonge.n.01", "name": "prolonge"}, + {"id": 10112, "synset": "prolonge_knot.n.01", "name": "prolonge_knot"}, + {"id": 10113, "synset": "prompter.n.02", "name": "prompter"}, + {"id": 10114, "synset": "prong.n.01", "name": "prong"}, + {"id": 10115, "synset": "propeller_plane.n.01", "name": "propeller_plane"}, + {"id": 10116, "synset": "propjet.n.01", "name": "propjet"}, + {"id": 10117, "synset": "proportional_counter_tube.n.01", "name": "proportional_counter_tube"}, + {"id": 10118, "synset": "propulsion_system.n.01", "name": "propulsion_system"}, + {"id": 10119, "synset": "proscenium.n.02", "name": "proscenium"}, + {"id": 10120, "synset": "proscenium_arch.n.01", "name": "proscenium_arch"}, + {"id": 10121, "synset": "prosthesis.n.01", "name": "prosthesis"}, + {"id": 10122, "synset": "protective_covering.n.01", "name": "protective_covering"}, + {"id": 10123, "synset": "protective_garment.n.01", "name": "protective_garment"}, + {"id": 10124, "synset": "proton_accelerator.n.01", "name": "proton_accelerator"}, + {"id": 10125, "synset": "protractor.n.01", "name": "protractor"}, + {"id": 10126, "synset": "pruner.n.02", "name": "pruner"}, + {"id": 10127, "synset": "pruning_knife.n.01", "name": "pruning_knife"}, + {"id": 10128, "synset": "pruning_saw.n.01", "name": "pruning_saw"}, + {"id": 10129, "synset": "pruning_shears.n.01", "name": "pruning_shears"}, + {"id": 10130, "synset": "psaltery.n.01", "name": "psaltery"}, + {"id": 10131, "synset": "psychrometer.n.01", "name": "psychrometer"}, + {"id": 10132, "synset": "pt_boat.n.01", "name": "PT_boat"}, + {"id": 10133, "synset": "public_address_system.n.01", "name": "public_address_system"}, + {"id": 10134, "synset": "public_house.n.01", "name": "public_house"}, + {"id": 10135, "synset": "public_toilet.n.01", "name": "public_toilet"}, + {"id": 10136, "synset": "public_transport.n.01", "name": "public_transport"}, + {"id": 10137, "synset": "public_works.n.01", "name": "public_works"}, + {"id": 10138, "synset": "puck.n.02", "name": "puck"}, + {"id": 10139, "synset": "pull.n.04", "name": "pull"}, + {"id": 10140, "synset": "pullback.n.01", "name": "pullback"}, + {"id": 10141, "synset": "pull_chain.n.01", "name": "pull_chain"}, + {"id": 10142, "synset": "pulley.n.01", "name": "pulley"}, + {"id": 10143, "synset": "pull-off.n.01", "name": "pull-off"}, + {"id": 10144, "synset": "pullman.n.01", "name": "Pullman"}, + {"id": 10145, "synset": "pullover.n.01", "name": "pullover"}, + {"id": 10146, "synset": "pull-through.n.01", "name": "pull-through"}, + {"id": 10147, "synset": "pulse_counter.n.01", "name": "pulse_counter"}, + {"id": 10148, "synset": "pulse_generator.n.01", "name": "pulse_generator"}, + {"id": 10149, "synset": "pulse_timing_circuit.n.01", "name": "pulse_timing_circuit"}, + {"id": 10150, "synset": "pump.n.01", "name": "pump"}, + {"id": 10151, "synset": "pump.n.03", "name": "pump"}, + {"id": 10152, "synset": "pump_action.n.01", "name": "pump_action"}, + {"id": 10153, "synset": "pump_house.n.01", "name": "pump_house"}, + {"id": 10154, "synset": "pump_room.n.01", "name": "pump_room"}, + {"id": 10155, "synset": "pump-type_pliers.n.01", "name": "pump-type_pliers"}, + {"id": 10156, "synset": "pump_well.n.01", "name": "pump_well"}, + {"id": 10157, "synset": "punchboard.n.01", "name": "punchboard"}, + {"id": 10158, "synset": "punch_bowl.n.01", "name": "punch_bowl"}, + {"id": 10159, "synset": "punching_bag.n.02", "name": "punching_bag"}, + {"id": 10160, "synset": "punch_pliers.n.01", "name": "punch_pliers"}, + {"id": 10161, "synset": "punch_press.n.01", "name": "punch_press"}, + {"id": 10162, "synset": "punnet.n.01", "name": "punnet"}, + {"id": 10163, "synset": "punt.n.02", "name": "punt"}, + {"id": 10164, "synset": "pup_tent.n.01", "name": "pup_tent"}, + {"id": 10165, "synset": "purdah.n.03", "name": "purdah"}, + {"id": 10166, "synset": "purifier.n.01", "name": "purifier"}, + {"id": 10167, "synset": "purl.n.02", "name": "purl"}, + {"id": 10168, "synset": "purse.n.03", "name": "purse"}, + {"id": 10169, "synset": "push-bike.n.01", "name": "push-bike"}, + {"id": 10170, "synset": "push_broom.n.01", "name": "push_broom"}, + {"id": 10171, "synset": "push_button.n.01", "name": "push_button"}, + {"id": 10172, "synset": "push-button_radio.n.01", "name": "push-button_radio"}, + {"id": 10173, "synset": "pusher.n.04", "name": "pusher"}, + {"id": 10174, "synset": "put-put.n.01", "name": "put-put"}, + {"id": 10175, "synset": "puttee.n.01", "name": "puttee"}, + {"id": 10176, "synset": "putter.n.02", "name": "putter"}, + {"id": 10177, "synset": "putty_knife.n.01", "name": "putty_knife"}, + {"id": 10178, "synset": "puzzle.n.02", "name": "puzzle"}, + {"id": 10179, "synset": "pylon.n.02", "name": "pylon"}, + {"id": 10180, "synset": "pylon.n.01", "name": "pylon"}, + {"id": 10181, "synset": "pyramidal_tent.n.01", "name": "pyramidal_tent"}, + {"id": 10182, "synset": "pyrograph.n.01", "name": "pyrograph"}, + {"id": 10183, "synset": "pyrometer.n.01", "name": "pyrometer"}, + {"id": 10184, "synset": "pyrometric_cone.n.01", "name": "pyrometric_cone"}, + {"id": 10185, "synset": "pyrostat.n.01", "name": "pyrostat"}, + {"id": 10186, "synset": "pyx.n.02", "name": "pyx"}, + {"id": 10187, "synset": "pyx.n.01", "name": "pyx"}, + {"id": 10188, "synset": "pyxis.n.03", "name": "pyxis"}, + {"id": 10189, "synset": "quad.n.04", "name": "quad"}, + {"id": 10190, "synset": "quadrant.n.04", "name": "quadrant"}, + {"id": 10191, "synset": "quadraphony.n.01", "name": "quadraphony"}, + {"id": 10192, "synset": "quartering.n.02", "name": "quartering"}, + {"id": 10193, "synset": "quarterstaff.n.01", "name": "quarterstaff"}, + {"id": 10194, "synset": "quartz_battery.n.01", "name": "quartz_battery"}, + {"id": 10195, "synset": "quartz_lamp.n.01", "name": "quartz_lamp"}, + {"id": 10196, "synset": "queen.n.08", "name": "queen"}, + {"id": 10197, "synset": "queen.n.07", "name": "queen"}, + {"id": 10198, "synset": "queen_post.n.01", "name": "queen_post"}, + {"id": 10199, "synset": "quern.n.01", "name": "quern"}, + {"id": 10200, "synset": "quill.n.01", "name": "quill"}, + {"id": 10201, "synset": "quilted_bedspread.n.01", "name": "quilted_bedspread"}, + {"id": 10202, "synset": "quilting.n.02", "name": "quilting"}, + {"id": 10203, "synset": "quipu.n.01", "name": "quipu"}, + {"id": 10204, "synset": "quirk_molding.n.01", "name": "quirk_molding"}, + {"id": 10205, "synset": "quirt.n.01", "name": "quirt"}, + {"id": 10206, "synset": "quiver.n.03", "name": "quiver"}, + {"id": 10207, "synset": "quoin.n.02", "name": "quoin"}, + {"id": 10208, "synset": "quoit.n.01", "name": "quoit"}, + {"id": 10209, "synset": "qwerty_keyboard.n.01", "name": "QWERTY_keyboard"}, + {"id": 10210, "synset": "rabbet.n.01", "name": "rabbet"}, + {"id": 10211, "synset": "rabbet_joint.n.01", "name": "rabbet_joint"}, + {"id": 10212, "synset": "rabbit_ears.n.01", "name": "rabbit_ears"}, + {"id": 10213, "synset": "rabbit_hutch.n.01", "name": "rabbit_hutch"}, + {"id": 10214, "synset": "raceabout.n.01", "name": "raceabout"}, + {"id": 10215, "synset": "raceway.n.01", "name": "raceway"}, + {"id": 10216, "synset": "racing_boat.n.01", "name": "racing_boat"}, + {"id": 10217, "synset": "racing_gig.n.01", "name": "racing_gig"}, + {"id": 10218, "synset": "racing_skiff.n.01", "name": "racing_skiff"}, + {"id": 10219, "synset": "rack.n.05", "name": "rack"}, + {"id": 10220, "synset": "rack.n.01", "name": "rack"}, + {"id": 10221, "synset": "rack.n.04", "name": "rack"}, + {"id": 10222, "synset": "rack_and_pinion.n.01", "name": "rack_and_pinion"}, + {"id": 10223, "synset": "racquetball.n.01", "name": "racquetball"}, + {"id": 10224, "synset": "radial.n.01", "name": "radial"}, + {"id": 10225, "synset": "radial_engine.n.01", "name": "radial_engine"}, + {"id": 10226, "synset": "radiation_pyrometer.n.01", "name": "radiation_pyrometer"}, + {"id": 10227, "synset": "radiator.n.02", "name": "radiator"}, + {"id": 10228, "synset": "radiator_cap.n.01", "name": "radiator_cap"}, + {"id": 10229, "synset": "radiator_hose.n.01", "name": "radiator_hose"}, + {"id": 10230, "synset": "radio.n.03", "name": "radio"}, + {"id": 10231, "synset": "radio_antenna.n.01", "name": "radio_antenna"}, + {"id": 10232, "synset": "radio_chassis.n.01", "name": "radio_chassis"}, + {"id": 10233, "synset": "radio_compass.n.01", "name": "radio_compass"}, + {"id": 10234, "synset": "radiogram.n.02", "name": "radiogram"}, + {"id": 10235, "synset": "radio_interferometer.n.01", "name": "radio_interferometer"}, + {"id": 10236, "synset": "radio_link.n.01", "name": "radio_link"}, + {"id": 10237, "synset": "radiometer.n.01", "name": "radiometer"}, + {"id": 10238, "synset": "radiomicrometer.n.01", "name": "radiomicrometer"}, + {"id": 10239, "synset": "radio-phonograph.n.01", "name": "radio-phonograph"}, + {"id": 10240, "synset": "radiotelegraph.n.02", "name": "radiotelegraph"}, + {"id": 10241, "synset": "radiotelephone.n.02", "name": "radiotelephone"}, + {"id": 10242, "synset": "radio_telescope.n.01", "name": "radio_telescope"}, + {"id": 10243, "synset": "radiotherapy_equipment.n.01", "name": "radiotherapy_equipment"}, + {"id": 10244, "synset": "radio_transmitter.n.01", "name": "radio_transmitter"}, + {"id": 10245, "synset": "radome.n.01", "name": "radome"}, + {"id": 10246, "synset": "rafter.n.01", "name": "rafter"}, + {"id": 10247, "synset": "raft_foundation.n.01", "name": "raft_foundation"}, + {"id": 10248, "synset": "rag.n.01", "name": "rag"}, + {"id": 10249, "synset": "ragbag.n.02", "name": "ragbag"}, + {"id": 10250, "synset": "raglan.n.01", "name": "raglan"}, + {"id": 10251, "synset": "raglan_sleeve.n.01", "name": "raglan_sleeve"}, + {"id": 10252, "synset": "rail.n.04", "name": "rail"}, + {"id": 10253, "synset": "rail_fence.n.01", "name": "rail_fence"}, + {"id": 10254, "synset": "railhead.n.01", "name": "railhead"}, + {"id": 10255, "synset": "railing.n.01", "name": "railing"}, + {"id": 10256, "synset": "railing.n.02", "name": "railing"}, + {"id": 10257, "synset": "railroad_bed.n.01", "name": "railroad_bed"}, + {"id": 10258, "synset": "railroad_tunnel.n.01", "name": "railroad_tunnel"}, + {"id": 10259, "synset": "rain_barrel.n.01", "name": "rain_barrel"}, + {"id": 10260, "synset": "rain_gauge.n.01", "name": "rain_gauge"}, + {"id": 10261, "synset": "rain_stick.n.01", "name": "rain_stick"}, + {"id": 10262, "synset": "rake.n.03", "name": "rake"}, + {"id": 10263, "synset": "rake_handle.n.01", "name": "rake_handle"}, + {"id": 10264, "synset": "ram_disk.n.01", "name": "RAM_disk"}, + {"id": 10265, "synset": "ramekin.n.02", "name": "ramekin"}, + {"id": 10266, "synset": "ramjet.n.01", "name": "ramjet"}, + {"id": 10267, "synset": "rammer.n.01", "name": "rammer"}, + {"id": 10268, "synset": "ramp.n.01", "name": "ramp"}, + {"id": 10269, "synset": "rampant_arch.n.01", "name": "rampant_arch"}, + {"id": 10270, "synset": "rampart.n.01", "name": "rampart"}, + {"id": 10271, "synset": "ramrod.n.01", "name": "ramrod"}, + {"id": 10272, "synset": "ramrod.n.03", "name": "ramrod"}, + {"id": 10273, "synset": "ranch.n.01", "name": "ranch"}, + {"id": 10274, "synset": "ranch_house.n.01", "name": "ranch_house"}, + {"id": 10275, "synset": "random-access_memory.n.01", "name": "random-access_memory"}, + {"id": 10276, "synset": "rangefinder.n.01", "name": "rangefinder"}, + {"id": 10277, "synset": "range_hood.n.01", "name": "range_hood"}, + {"id": 10278, "synset": "range_pole.n.01", "name": "range_pole"}, + {"id": 10279, "synset": "rapier.n.01", "name": "rapier"}, + {"id": 10280, "synset": "rariora.n.01", "name": "rariora"}, + {"id": 10281, "synset": "rasp.n.02", "name": "rasp"}, + {"id": 10282, "synset": "ratchet.n.01", "name": "ratchet"}, + {"id": 10283, "synset": "ratchet_wheel.n.01", "name": "ratchet_wheel"}, + {"id": 10284, "synset": "rathskeller.n.01", "name": "rathskeller"}, + {"id": 10285, "synset": "ratline.n.01", "name": "ratline"}, + {"id": 10286, "synset": "rat-tail_file.n.01", "name": "rat-tail_file"}, + {"id": 10287, "synset": "rattan.n.03", "name": "rattan"}, + {"id": 10288, "synset": "rattrap.n.03", "name": "rattrap"}, + {"id": 10289, "synset": "rayon.n.01", "name": "rayon"}, + {"id": 10290, "synset": "razor.n.01", "name": "razor"}, + { + "id": 10291, + "synset": "reaction-propulsion_engine.n.01", + "name": "reaction-propulsion_engine", + }, + {"id": 10292, "synset": "reaction_turbine.n.01", "name": "reaction_turbine"}, + {"id": 10293, "synset": "reactor.n.01", "name": "reactor"}, + {"id": 10294, "synset": "reading_lamp.n.01", "name": "reading_lamp"}, + {"id": 10295, "synset": "reading_room.n.01", "name": "reading_room"}, + {"id": 10296, "synset": "read-only_memory.n.01", "name": "read-only_memory"}, + {"id": 10297, "synset": "read-only_memory_chip.n.01", "name": "read-only_memory_chip"}, + {"id": 10298, "synset": "readout.n.03", "name": "readout"}, + {"id": 10299, "synset": "read/write_head.n.01", "name": "read/write_head"}, + {"id": 10300, "synset": "ready-to-wear.n.01", "name": "ready-to-wear"}, + {"id": 10301, "synset": "real_storage.n.01", "name": "real_storage"}, + {"id": 10302, "synset": "reamer.n.02", "name": "reamer"}, + {"id": 10303, "synset": "reaumur_thermometer.n.01", "name": "Reaumur_thermometer"}, + {"id": 10304, "synset": "rebozo.n.01", "name": "rebozo"}, + {"id": 10305, "synset": "receiver.n.01", "name": "receiver"}, + {"id": 10306, "synset": "receptacle.n.01", "name": "receptacle"}, + {"id": 10307, "synset": "reception_desk.n.01", "name": "reception_desk"}, + {"id": 10308, "synset": "reception_room.n.01", "name": "reception_room"}, + {"id": 10309, "synset": "recess.n.04", "name": "recess"}, + {"id": 10310, "synset": "reciprocating_engine.n.01", "name": "reciprocating_engine"}, + {"id": 10311, "synset": "reconnaissance_plane.n.01", "name": "reconnaissance_plane"}, + {"id": 10312, "synset": "reconnaissance_vehicle.n.01", "name": "reconnaissance_vehicle"}, + {"id": 10313, "synset": "record_changer.n.01", "name": "record_changer"}, + {"id": 10314, "synset": "recorder.n.01", "name": "recorder"}, + {"id": 10315, "synset": "recording.n.03", "name": "recording"}, + {"id": 10316, "synset": "recording_system.n.01", "name": "recording_system"}, + {"id": 10317, "synset": "record_sleeve.n.01", "name": "record_sleeve"}, + {"id": 10318, "synset": "recovery_room.n.01", "name": "recovery_room"}, + {"id": 10319, "synset": "recreational_vehicle.n.01", "name": "recreational_vehicle"}, + {"id": 10320, "synset": "recreation_room.n.01", "name": "recreation_room"}, + {"id": 10321, "synset": "recycling_bin.n.01", "name": "recycling_bin"}, + {"id": 10322, "synset": "recycling_plant.n.01", "name": "recycling_plant"}, + {"id": 10323, "synset": "redbrick_university.n.01", "name": "redbrick_university"}, + {"id": 10324, "synset": "red_carpet.n.01", "name": "red_carpet"}, + {"id": 10325, "synset": "redoubt.n.02", "name": "redoubt"}, + {"id": 10326, "synset": "redoubt.n.01", "name": "redoubt"}, + {"id": 10327, "synset": "reduction_gear.n.01", "name": "reduction_gear"}, + {"id": 10328, "synset": "reed_pipe.n.01", "name": "reed_pipe"}, + {"id": 10329, "synset": "reed_stop.n.01", "name": "reed_stop"}, + {"id": 10330, "synset": "reef_knot.n.01", "name": "reef_knot"}, + {"id": 10331, "synset": "reel.n.03", "name": "reel"}, + {"id": 10332, "synset": "reel.n.01", "name": "reel"}, + {"id": 10333, "synset": "refectory.n.01", "name": "refectory"}, + {"id": 10334, "synset": "refectory_table.n.01", "name": "refectory_table"}, + {"id": 10335, "synset": "refinery.n.01", "name": "refinery"}, + {"id": 10336, "synset": "reflecting_telescope.n.01", "name": "reflecting_telescope"}, + {"id": 10337, "synset": "reflectometer.n.01", "name": "reflectometer"}, + {"id": 10338, "synset": "reflex_camera.n.01", "name": "reflex_camera"}, + {"id": 10339, "synset": "reflux_condenser.n.01", "name": "reflux_condenser"}, + {"id": 10340, "synset": "reformatory.n.01", "name": "reformatory"}, + {"id": 10341, "synset": "reformer.n.02", "name": "reformer"}, + {"id": 10342, "synset": "refracting_telescope.n.01", "name": "refracting_telescope"}, + {"id": 10343, "synset": "refractometer.n.01", "name": "refractometer"}, + {"id": 10344, "synset": "refrigeration_system.n.01", "name": "refrigeration_system"}, + {"id": 10345, "synset": "refrigerator.n.01", "name": "refrigerator"}, + {"id": 10346, "synset": "refrigerator_car.n.01", "name": "refrigerator_car"}, + {"id": 10347, "synset": "refuge.n.03", "name": "refuge"}, + {"id": 10348, "synset": "regalia.n.01", "name": "regalia"}, + {"id": 10349, "synset": "regimentals.n.01", "name": "regimentals"}, + {"id": 10350, "synset": "regulator.n.01", "name": "regulator"}, + {"id": 10351, "synset": "rein.n.01", "name": "rein"}, + {"id": 10352, "synset": "relay.n.05", "name": "relay"}, + {"id": 10353, "synset": "release.n.08", "name": "release"}, + {"id": 10354, "synset": "religious_residence.n.01", "name": "religious_residence"}, + {"id": 10355, "synset": "reliquary.n.01", "name": "reliquary"}, + {"id": 10356, "synset": "remote_terminal.n.01", "name": "remote_terminal"}, + {"id": 10357, "synset": "removable_disk.n.01", "name": "removable_disk"}, + {"id": 10358, "synset": "rendering.n.05", "name": "rendering"}, + {"id": 10359, "synset": "rep.n.02", "name": "rep"}, + {"id": 10360, "synset": "repair_shop.n.01", "name": "repair_shop"}, + {"id": 10361, "synset": "repeater.n.04", "name": "repeater"}, + {"id": 10362, "synset": "repeating_firearm.n.01", "name": "repeating_firearm"}, + {"id": 10363, "synset": "repository.n.03", "name": "repository"}, + {"id": 10364, "synset": "reproducer.n.01", "name": "reproducer"}, + {"id": 10365, "synset": "rerebrace.n.01", "name": "rerebrace"}, + {"id": 10366, "synset": "rescue_equipment.n.01", "name": "rescue_equipment"}, + {"id": 10367, "synset": "research_center.n.01", "name": "research_center"}, + {"id": 10368, "synset": "reseau.n.02", "name": "reseau"}, + {"id": 10369, "synset": "reservoir.n.03", "name": "reservoir"}, + {"id": 10370, "synset": "reset.n.01", "name": "reset"}, + {"id": 10371, "synset": "reset_button.n.01", "name": "reset_button"}, + {"id": 10372, "synset": "residence.n.02", "name": "residence"}, + {"id": 10373, "synset": "resistance_pyrometer.n.01", "name": "resistance_pyrometer"}, + {"id": 10374, "synset": "resistor.n.01", "name": "resistor"}, + {"id": 10375, "synset": "resonator.n.03", "name": "resonator"}, + {"id": 10376, "synset": "resonator.n.01", "name": "resonator"}, + {"id": 10377, "synset": "resort_hotel.n.02", "name": "resort_hotel"}, + {"id": 10378, "synset": "respirator.n.01", "name": "respirator"}, + {"id": 10379, "synset": "restaurant.n.01", "name": "restaurant"}, + {"id": 10380, "synset": "rest_house.n.01", "name": "rest_house"}, + {"id": 10381, "synset": "restraint.n.06", "name": "restraint"}, + {"id": 10382, "synset": "resuscitator.n.01", "name": "resuscitator"}, + {"id": 10383, "synset": "retainer.n.03", "name": "retainer"}, + {"id": 10384, "synset": "retaining_wall.n.01", "name": "retaining_wall"}, + {"id": 10385, "synset": "reticle.n.01", "name": "reticle"}, + {"id": 10386, "synset": "reticulation.n.02", "name": "reticulation"}, + {"id": 10387, "synset": "reticule.n.01", "name": "reticule"}, + {"id": 10388, "synset": "retort.n.02", "name": "retort"}, + {"id": 10389, "synset": "retractor.n.01", "name": "retractor"}, + {"id": 10390, "synset": "return_key.n.01", "name": "return_key"}, + {"id": 10391, "synset": "reverberatory_furnace.n.01", "name": "reverberatory_furnace"}, + {"id": 10392, "synset": "revers.n.01", "name": "revers"}, + {"id": 10393, "synset": "reverse.n.02", "name": "reverse"}, + {"id": 10394, "synset": "reversible.n.01", "name": "reversible"}, + {"id": 10395, "synset": "revetment.n.02", "name": "revetment"}, + {"id": 10396, "synset": "revetment.n.01", "name": "revetment"}, + {"id": 10397, "synset": "revolver.n.01", "name": "revolver"}, + {"id": 10398, "synset": "revolving_door.n.02", "name": "revolving_door"}, + {"id": 10399, "synset": "rheometer.n.01", "name": "rheometer"}, + {"id": 10400, "synset": "rheostat.n.01", "name": "rheostat"}, + {"id": 10401, "synset": "rhinoscope.n.01", "name": "rhinoscope"}, + {"id": 10402, "synset": "rib.n.01", "name": "rib"}, + {"id": 10403, "synset": "riband.n.01", "name": "riband"}, + {"id": 10404, "synset": "ribbed_vault.n.01", "name": "ribbed_vault"}, + {"id": 10405, "synset": "ribbing.n.01", "name": "ribbing"}, + {"id": 10406, "synset": "ribbon_development.n.01", "name": "ribbon_development"}, + {"id": 10407, "synset": "rib_joint_pliers.n.01", "name": "rib_joint_pliers"}, + {"id": 10408, "synset": "ricer.n.01", "name": "ricer"}, + {"id": 10409, "synset": "riddle.n.02", "name": "riddle"}, + {"id": 10410, "synset": "ride.n.02", "name": "ride"}, + {"id": 10411, "synset": "ridge.n.06", "name": "ridge"}, + {"id": 10412, "synset": "ridge_rope.n.01", "name": "ridge_rope"}, + {"id": 10413, "synset": "riding_boot.n.01", "name": "riding_boot"}, + {"id": 10414, "synset": "riding_crop.n.01", "name": "riding_crop"}, + {"id": 10415, "synset": "riding_mower.n.01", "name": "riding_mower"}, + {"id": 10416, "synset": "rifle_ball.n.01", "name": "rifle_ball"}, + {"id": 10417, "synset": "rifle_grenade.n.01", "name": "rifle_grenade"}, + {"id": 10418, "synset": "rig.n.01", "name": "rig"}, + {"id": 10419, "synset": "rigger.n.02", "name": "rigger"}, + {"id": 10420, "synset": "rigger.n.04", "name": "rigger"}, + {"id": 10421, "synset": "rigging.n.01", "name": "rigging"}, + {"id": 10422, "synset": "rigout.n.01", "name": "rigout"}, + {"id": 10423, "synset": "ringlet.n.03", "name": "ringlet"}, + {"id": 10424, "synset": "rings.n.01", "name": "rings"}, + {"id": 10425, "synset": "rink.n.01", "name": "rink"}, + {"id": 10426, "synset": "riot_gun.n.01", "name": "riot_gun"}, + {"id": 10427, "synset": "ripcord.n.02", "name": "ripcord"}, + {"id": 10428, "synset": "ripcord.n.01", "name": "ripcord"}, + {"id": 10429, "synset": "ripping_bar.n.01", "name": "ripping_bar"}, + {"id": 10430, "synset": "ripping_chisel.n.01", "name": "ripping_chisel"}, + {"id": 10431, "synset": "ripsaw.n.01", "name": "ripsaw"}, + {"id": 10432, "synset": "riser.n.03", "name": "riser"}, + {"id": 10433, "synset": "riser.n.02", "name": "riser"}, + {"id": 10434, "synset": "ritz.n.03", "name": "Ritz"}, + {"id": 10435, "synset": "rivet.n.02", "name": "rivet"}, + {"id": 10436, "synset": "riveting_machine.n.01", "name": "riveting_machine"}, + {"id": 10437, "synset": "roach_clip.n.01", "name": "roach_clip"}, + {"id": 10438, "synset": "road.n.01", "name": "road"}, + {"id": 10439, "synset": "roadbed.n.01", "name": "roadbed"}, + {"id": 10440, "synset": "roadblock.n.02", "name": "roadblock"}, + {"id": 10441, "synset": "roadhouse.n.01", "name": "roadhouse"}, + {"id": 10442, "synset": "roadster.n.01", "name": "roadster"}, + {"id": 10443, "synset": "roadway.n.01", "name": "roadway"}, + {"id": 10444, "synset": "roaster.n.04", "name": "roaster"}, + {"id": 10445, "synset": "robotics_equipment.n.01", "name": "robotics_equipment"}, + {"id": 10446, "synset": "rochon_prism.n.01", "name": "Rochon_prism"}, + {"id": 10447, "synset": "rock_bit.n.01", "name": "rock_bit"}, + {"id": 10448, "synset": "rocker.n.07", "name": "rocker"}, + {"id": 10449, "synset": "rocker.n.05", "name": "rocker"}, + {"id": 10450, "synset": "rocker_arm.n.01", "name": "rocker_arm"}, + {"id": 10451, "synset": "rocket.n.02", "name": "rocket"}, + {"id": 10452, "synset": "rocket.n.01", "name": "rocket"}, + {"id": 10453, "synset": "rod.n.01", "name": "rod"}, + {"id": 10454, "synset": "rodeo.n.02", "name": "rodeo"}, + {"id": 10455, "synset": "roll.n.04", "name": "roll"}, + {"id": 10456, "synset": "roller.n.04", "name": "roller"}, + {"id": 10457, "synset": "roller.n.03", "name": "roller"}, + {"id": 10458, "synset": "roller_bandage.n.01", "name": "roller_bandage"}, + {"id": 10459, "synset": "in-line_skate.n.01", "name": "in-line_skate"}, + {"id": 10460, "synset": "roller_blind.n.01", "name": "roller_blind"}, + {"id": 10461, "synset": "roller_coaster.n.02", "name": "roller_coaster"}, + {"id": 10462, "synset": "roller_towel.n.01", "name": "roller_towel"}, + {"id": 10463, "synset": "roll_film.n.01", "name": "roll_film"}, + {"id": 10464, "synset": "rolling_hitch.n.01", "name": "rolling_hitch"}, + {"id": 10465, "synset": "rolling_mill.n.01", "name": "rolling_mill"}, + {"id": 10466, "synset": "rolling_stock.n.01", "name": "rolling_stock"}, + {"id": 10467, "synset": "roll-on.n.02", "name": "roll-on"}, + {"id": 10468, "synset": "roll-on.n.01", "name": "roll-on"}, + {"id": 10469, "synset": "roll-on_roll-off.n.01", "name": "roll-on_roll-off"}, + {"id": 10470, "synset": "rolodex.n.01", "name": "Rolodex"}, + {"id": 10471, "synset": "roman_arch.n.01", "name": "Roman_arch"}, + {"id": 10472, "synset": "roman_building.n.01", "name": "Roman_building"}, + {"id": 10473, "synset": "romper.n.02", "name": "romper"}, + {"id": 10474, "synset": "rood_screen.n.01", "name": "rood_screen"}, + {"id": 10475, "synset": "roof.n.01", "name": "roof"}, + {"id": 10476, "synset": "roof.n.02", "name": "roof"}, + {"id": 10477, "synset": "roofing.n.01", "name": "roofing"}, + {"id": 10478, "synset": "room.n.01", "name": "room"}, + {"id": 10479, "synset": "roomette.n.01", "name": "roomette"}, + {"id": 10480, "synset": "room_light.n.01", "name": "room_light"}, + {"id": 10481, "synset": "roost.n.01", "name": "roost"}, + {"id": 10482, "synset": "rope.n.01", "name": "rope"}, + {"id": 10483, "synset": "rope_bridge.n.01", "name": "rope_bridge"}, + {"id": 10484, "synset": "rope_tow.n.01", "name": "rope_tow"}, + {"id": 10485, "synset": "rose_water.n.01", "name": "rose_water"}, + {"id": 10486, "synset": "rose_window.n.01", "name": "rose_window"}, + {"id": 10487, "synset": "rosin_bag.n.01", "name": "rosin_bag"}, + {"id": 10488, "synset": "rotary_actuator.n.01", "name": "rotary_actuator"}, + {"id": 10489, "synset": "rotary_engine.n.01", "name": "rotary_engine"}, + {"id": 10490, "synset": "rotary_press.n.01", "name": "rotary_press"}, + {"id": 10491, "synset": "rotating_mechanism.n.01", "name": "rotating_mechanism"}, + {"id": 10492, "synset": "rotating_shaft.n.01", "name": "rotating_shaft"}, + {"id": 10493, "synset": "rotisserie.n.02", "name": "rotisserie"}, + {"id": 10494, "synset": "rotisserie.n.01", "name": "rotisserie"}, + {"id": 10495, "synset": "rotor.n.03", "name": "rotor"}, + {"id": 10496, "synset": "rotor.n.01", "name": "rotor"}, + {"id": 10497, "synset": "rotor.n.02", "name": "rotor"}, + {"id": 10498, "synset": "rotor_blade.n.01", "name": "rotor_blade"}, + {"id": 10499, "synset": "rotor_head.n.01", "name": "rotor_head"}, + {"id": 10500, "synset": "rotunda.n.02", "name": "rotunda"}, + {"id": 10501, "synset": "rotunda.n.01", "name": "rotunda"}, + {"id": 10502, "synset": "rouge.n.01", "name": "rouge"}, + {"id": 10503, "synset": "roughcast.n.02", "name": "roughcast"}, + {"id": 10504, "synset": "rouleau.n.02", "name": "rouleau"}, + {"id": 10505, "synset": "roulette.n.02", "name": "roulette"}, + {"id": 10506, "synset": "roulette_ball.n.01", "name": "roulette_ball"}, + {"id": 10507, "synset": "roulette_wheel.n.01", "name": "roulette_wheel"}, + {"id": 10508, "synset": "round.n.01", "name": "round"}, + {"id": 10509, "synset": "round_arch.n.01", "name": "round_arch"}, + {"id": 10510, "synset": "round-bottom_flask.n.01", "name": "round-bottom_flask"}, + {"id": 10511, "synset": "roundel.n.02", "name": "roundel"}, + {"id": 10512, "synset": "round_file.n.01", "name": "round_file"}, + {"id": 10513, "synset": "roundhouse.n.01", "name": "roundhouse"}, + {"id": 10514, "synset": "router.n.03", "name": "router"}, + {"id": 10515, "synset": "router_plane.n.01", "name": "router_plane"}, + {"id": 10516, "synset": "rowel.n.01", "name": "rowel"}, + {"id": 10517, "synset": "row_house.n.01", "name": "row_house"}, + {"id": 10518, "synset": "rowing_boat.n.01", "name": "rowing_boat"}, + {"id": 10519, "synset": "rowlock_arch.n.01", "name": "rowlock_arch"}, + {"id": 10520, "synset": "royal.n.01", "name": "royal"}, + {"id": 10521, "synset": "royal_mast.n.01", "name": "royal_mast"}, + {"id": 10522, "synset": "rubber_boot.n.01", "name": "rubber_boot"}, + {"id": 10523, "synset": "rubber_bullet.n.01", "name": "rubber_bullet"}, + {"id": 10524, "synset": "rubber_eraser.n.01", "name": "rubber_eraser"}, + {"id": 10525, "synset": "rudder.n.02", "name": "rudder"}, + {"id": 10526, "synset": "rudder.n.01", "name": "rudder"}, + {"id": 10527, "synset": "rudder_blade.n.01", "name": "rudder_blade"}, + {"id": 10528, "synset": "rug.n.01", "name": "rug"}, + {"id": 10529, "synset": "rugby_ball.n.01", "name": "rugby_ball"}, + {"id": 10530, "synset": "ruin.n.02", "name": "ruin"}, + {"id": 10531, "synset": "rule.n.12", "name": "rule"}, + {"id": 10532, "synset": "rumble.n.02", "name": "rumble"}, + {"id": 10533, "synset": "rumble_seat.n.01", "name": "rumble_seat"}, + {"id": 10534, "synset": "rummer.n.01", "name": "rummer"}, + {"id": 10535, "synset": "rumpus_room.n.01", "name": "rumpus_room"}, + {"id": 10536, "synset": "runcible_spoon.n.01", "name": "runcible_spoon"}, + {"id": 10537, "synset": "rundle.n.01", "name": "rundle"}, + {"id": 10538, "synset": "running_shoe.n.01", "name": "running_shoe"}, + {"id": 10539, "synset": "running_suit.n.01", "name": "running_suit"}, + {"id": 10540, "synset": "runway.n.04", "name": "runway"}, + {"id": 10541, "synset": "rushlight.n.01", "name": "rushlight"}, + {"id": 10542, "synset": "russet.n.01", "name": "russet"}, + {"id": 10543, "synset": "rya.n.01", "name": "rya"}, + {"id": 10544, "synset": "saber.n.01", "name": "saber"}, + {"id": 10545, "synset": "saber_saw.n.01", "name": "saber_saw"}, + {"id": 10546, "synset": "sable.n.04", "name": "sable"}, + {"id": 10547, "synset": "sable.n.01", "name": "sable"}, + {"id": 10548, "synset": "sable_coat.n.01", "name": "sable_coat"}, + {"id": 10549, "synset": "sabot.n.01", "name": "sabot"}, + {"id": 10550, "synset": "sachet.n.01", "name": "sachet"}, + {"id": 10551, "synset": "sack.n.05", "name": "sack"}, + {"id": 10552, "synset": "sackbut.n.01", "name": "sackbut"}, + {"id": 10553, "synset": "sackcloth.n.02", "name": "sackcloth"}, + {"id": 10554, "synset": "sackcloth.n.01", "name": "sackcloth"}, + {"id": 10555, "synset": "sack_coat.n.01", "name": "sack_coat"}, + {"id": 10556, "synset": "sacking.n.01", "name": "sacking"}, + {"id": 10557, "synset": "saddle_oxford.n.01", "name": "saddle_oxford"}, + {"id": 10558, "synset": "saddlery.n.02", "name": "saddlery"}, + {"id": 10559, "synset": "saddle_seat.n.01", "name": "saddle_seat"}, + {"id": 10560, "synset": "saddle_stitch.n.01", "name": "saddle_stitch"}, + {"id": 10561, "synset": "safe.n.01", "name": "safe"}, + {"id": 10562, "synset": "safe.n.02", "name": "safe"}, + {"id": 10563, "synset": "safe-deposit.n.01", "name": "safe-deposit"}, + {"id": 10564, "synset": "safe_house.n.01", "name": "safe_house"}, + {"id": 10565, "synset": "safety_arch.n.01", "name": "safety_arch"}, + {"id": 10566, "synset": "safety_belt.n.01", "name": "safety_belt"}, + {"id": 10567, "synset": "safety_bicycle.n.01", "name": "safety_bicycle"}, + {"id": 10568, "synset": "safety_bolt.n.01", "name": "safety_bolt"}, + {"id": 10569, "synset": "safety_curtain.n.01", "name": "safety_curtain"}, + {"id": 10570, "synset": "safety_fuse.n.01", "name": "safety_fuse"}, + {"id": 10571, "synset": "safety_lamp.n.01", "name": "safety_lamp"}, + {"id": 10572, "synset": "safety_match.n.01", "name": "safety_match"}, + {"id": 10573, "synset": "safety_net.n.02", "name": "safety_net"}, + {"id": 10574, "synset": "safety_rail.n.01", "name": "safety_rail"}, + {"id": 10575, "synset": "safety_razor.n.01", "name": "safety_razor"}, + {"id": 10576, "synset": "safety_valve.n.01", "name": "safety_valve"}, + {"id": 10577, "synset": "sail.n.03", "name": "sail"}, + {"id": 10578, "synset": "sailboat.n.01", "name": "sailboat"}, + {"id": 10579, "synset": "sailcloth.n.01", "name": "sailcloth"}, + {"id": 10580, "synset": "sailing_vessel.n.01", "name": "sailing_vessel"}, + {"id": 10581, "synset": "sailing_warship.n.01", "name": "sailing_warship"}, + {"id": 10582, "synset": "sailor_cap.n.01", "name": "sailor_cap"}, + {"id": 10583, "synset": "sailor_suit.n.01", "name": "sailor_suit"}, + {"id": 10584, "synset": "salad_bar.n.01", "name": "salad_bar"}, + {"id": 10585, "synset": "salad_bowl.n.02", "name": "salad_bowl"}, + {"id": 10586, "synset": "salinometer.n.01", "name": "salinometer"}, + {"id": 10587, "synset": "sallet.n.01", "name": "sallet"}, + {"id": 10588, "synset": "salon.n.03", "name": "salon"}, + {"id": 10589, "synset": "salon.n.01", "name": "salon"}, + {"id": 10590, "synset": "salon.n.02", "name": "salon"}, + {"id": 10591, "synset": "saltbox.n.01", "name": "saltbox"}, + {"id": 10592, "synset": "saltcellar.n.01", "name": "saltcellar"}, + {"id": 10593, "synset": "saltworks.n.01", "name": "saltworks"}, + {"id": 10594, "synset": "salver.n.01", "name": "salver"}, + {"id": 10595, "synset": "salwar.n.01", "name": "salwar"}, + {"id": 10596, "synset": "sam_browne_belt.n.01", "name": "Sam_Browne_belt"}, + {"id": 10597, "synset": "samisen.n.01", "name": "samisen"}, + {"id": 10598, "synset": "samite.n.01", "name": "samite"}, + {"id": 10599, "synset": "samovar.n.01", "name": "samovar"}, + {"id": 10600, "synset": "sampan.n.01", "name": "sampan"}, + {"id": 10601, "synset": "sandbag.n.01", "name": "sandbag"}, + {"id": 10602, "synset": "sandblaster.n.01", "name": "sandblaster"}, + {"id": 10603, "synset": "sandbox.n.01", "name": "sandbox"}, + {"id": 10604, "synset": "sandglass.n.01", "name": "sandglass"}, + {"id": 10605, "synset": "sand_wedge.n.01", "name": "sand_wedge"}, + {"id": 10606, "synset": "sandwich_board.n.01", "name": "sandwich_board"}, + {"id": 10607, "synset": "sanitary_napkin.n.01", "name": "sanitary_napkin"}, + {"id": 10608, "synset": "cling_film.n.01", "name": "cling_film"}, + {"id": 10609, "synset": "sarcenet.n.01", "name": "sarcenet"}, + {"id": 10610, "synset": "sarcophagus.n.01", "name": "sarcophagus"}, + {"id": 10611, "synset": "sari.n.01", "name": "sari"}, + {"id": 10612, "synset": "sarong.n.01", "name": "sarong"}, + {"id": 10613, "synset": "sash.n.01", "name": "sash"}, + {"id": 10614, "synset": "sash_fastener.n.01", "name": "sash_fastener"}, + {"id": 10615, "synset": "sash_window.n.01", "name": "sash_window"}, + {"id": 10616, "synset": "sateen.n.01", "name": "sateen"}, + {"id": 10617, "synset": "satellite.n.01", "name": "satellite"}, + {"id": 10618, "synset": "satellite_receiver.n.01", "name": "satellite_receiver"}, + {"id": 10619, "synset": "satellite_television.n.01", "name": "satellite_television"}, + {"id": 10620, "synset": "satellite_transmitter.n.01", "name": "satellite_transmitter"}, + {"id": 10621, "synset": "satin.n.01", "name": "satin"}, + {"id": 10622, "synset": "saturday_night_special.n.01", "name": "Saturday_night_special"}, + {"id": 10623, "synset": "saucepot.n.01", "name": "saucepot"}, + {"id": 10624, "synset": "sauna.n.01", "name": "sauna"}, + {"id": 10625, "synset": "savings_bank.n.02", "name": "savings_bank"}, + {"id": 10626, "synset": "saw.n.02", "name": "saw"}, + {"id": 10627, "synset": "sawed-off_shotgun.n.01", "name": "sawed-off_shotgun"}, + {"id": 10628, "synset": "sawmill.n.01", "name": "sawmill"}, + {"id": 10629, "synset": "saw_set.n.01", "name": "saw_set"}, + {"id": 10630, "synset": "saxhorn.n.01", "name": "saxhorn"}, + {"id": 10631, "synset": "scabbard.n.01", "name": "scabbard"}, + {"id": 10632, "synset": "scaffolding.n.01", "name": "scaffolding"}, + {"id": 10633, "synset": "scale.n.08", "name": "scale"}, + {"id": 10634, "synset": "scaler.n.01", "name": "scaler"}, + {"id": 10635, "synset": "scaling_ladder.n.01", "name": "scaling_ladder"}, + {"id": 10636, "synset": "scalpel.n.01", "name": "scalpel"}, + {"id": 10637, "synset": "scanner.n.04", "name": "scanner"}, + {"id": 10638, "synset": "scanner.n.03", "name": "scanner"}, + {"id": 10639, "synset": "scanner.n.02", "name": "scanner"}, + {"id": 10640, "synset": "scantling.n.01", "name": "scantling"}, + {"id": 10641, "synset": "scarf_joint.n.01", "name": "scarf_joint"}, + {"id": 10642, "synset": "scatter_rug.n.01", "name": "scatter_rug"}, + {"id": 10643, "synset": "scauper.n.01", "name": "scauper"}, + {"id": 10644, "synset": "schmidt_telescope.n.01", "name": "Schmidt_telescope"}, + {"id": 10645, "synset": "school.n.02", "name": "school"}, + {"id": 10646, "synset": "schoolbag.n.01", "name": "schoolbag"}, + {"id": 10647, "synset": "school_bell.n.01", "name": "school_bell"}, + {"id": 10648, "synset": "school_ship.n.01", "name": "school_ship"}, + {"id": 10649, "synset": "school_system.n.01", "name": "school_system"}, + {"id": 10650, "synset": "schooner.n.02", "name": "schooner"}, + {"id": 10651, "synset": "schooner.n.01", "name": "schooner"}, + {"id": 10652, "synset": "scientific_instrument.n.01", "name": "scientific_instrument"}, + {"id": 10653, "synset": "scimitar.n.01", "name": "scimitar"}, + {"id": 10654, "synset": "scintillation_counter.n.01", "name": "scintillation_counter"}, + {"id": 10655, "synset": "sclerometer.n.01", "name": "sclerometer"}, + {"id": 10656, "synset": "scoinson_arch.n.01", "name": "scoinson_arch"}, + {"id": 10657, "synset": "sconce.n.04", "name": "sconce"}, + {"id": 10658, "synset": "sconce.n.03", "name": "sconce"}, + {"id": 10659, "synset": "scoop.n.06", "name": "scoop"}, + {"id": 10660, "synset": "scooter.n.02", "name": "scooter"}, + {"id": 10661, "synset": "scouring_pad.n.01", "name": "scouring_pad"}, + {"id": 10662, "synset": "scow.n.02", "name": "scow"}, + {"id": 10663, "synset": "scow.n.01", "name": "scow"}, + {"id": 10664, "synset": "scratcher.n.03", "name": "scratcher"}, + {"id": 10665, "synset": "screen.n.05", "name": "screen"}, + {"id": 10666, "synset": "screen.n.04", "name": "screen"}, + {"id": 10667, "synset": "screen.n.09", "name": "screen"}, + {"id": 10668, "synset": "screen.n.03", "name": "screen"}, + {"id": 10669, "synset": "screen_door.n.01", "name": "screen_door"}, + {"id": 10670, "synset": "screening.n.02", "name": "screening"}, + {"id": 10671, "synset": "screw.n.04", "name": "screw"}, + {"id": 10672, "synset": "screw.n.03", "name": "screw"}, + {"id": 10673, "synset": "screw.n.02", "name": "screw"}, + {"id": 10674, "synset": "screw_eye.n.01", "name": "screw_eye"}, + {"id": 10675, "synset": "screw_key.n.01", "name": "screw_key"}, + {"id": 10676, "synset": "screw_thread.n.01", "name": "screw_thread"}, + {"id": 10677, "synset": "screwtop.n.01", "name": "screwtop"}, + {"id": 10678, "synset": "screw_wrench.n.01", "name": "screw_wrench"}, + {"id": 10679, "synset": "scriber.n.01", "name": "scriber"}, + {"id": 10680, "synset": "scrim.n.01", "name": "scrim"}, + {"id": 10681, "synset": "scrimshaw.n.01", "name": "scrimshaw"}, + {"id": 10682, "synset": "scriptorium.n.01", "name": "scriptorium"}, + {"id": 10683, "synset": "scrubber.n.03", "name": "scrubber"}, + {"id": 10684, "synset": "scrub_plane.n.01", "name": "scrub_plane"}, + {"id": 10685, "synset": "scuffer.n.01", "name": "scuffer"}, + {"id": 10686, "synset": "scuffle.n.02", "name": "scuffle"}, + {"id": 10687, "synset": "scull.n.02", "name": "scull"}, + {"id": 10688, "synset": "scull.n.01", "name": "scull"}, + {"id": 10689, "synset": "scullery.n.01", "name": "scullery"}, + {"id": 10690, "synset": "scuttle.n.01", "name": "scuttle"}, + {"id": 10691, "synset": "scyphus.n.01", "name": "scyphus"}, + {"id": 10692, "synset": "scythe.n.01", "name": "scythe"}, + {"id": 10693, "synset": "seabag.n.01", "name": "seabag"}, + {"id": 10694, "synset": "sea_boat.n.01", "name": "sea_boat"}, + {"id": 10695, "synset": "sea_chest.n.01", "name": "sea_chest"}, + {"id": 10696, "synset": "sealing_wax.n.01", "name": "sealing_wax"}, + {"id": 10697, "synset": "sealskin.n.02", "name": "sealskin"}, + {"id": 10698, "synset": "seam.n.01", "name": "seam"}, + {"id": 10699, "synset": "searchlight.n.01", "name": "searchlight"}, + {"id": 10700, "synset": "searing_iron.n.01", "name": "searing_iron"}, + {"id": 10701, "synset": "seat.n.04", "name": "seat"}, + {"id": 10702, "synset": "seat.n.03", "name": "seat"}, + {"id": 10703, "synset": "seat.n.09", "name": "seat"}, + {"id": 10704, "synset": "seat_belt.n.01", "name": "seat_belt"}, + {"id": 10705, "synset": "secateurs.n.01", "name": "secateurs"}, + {"id": 10706, "synset": "secondary_coil.n.01", "name": "secondary_coil"}, + {"id": 10707, "synset": "second_balcony.n.01", "name": "second_balcony"}, + {"id": 10708, "synset": "second_base.n.01", "name": "second_base"}, + {"id": 10709, "synset": "second_hand.n.02", "name": "second_hand"}, + {"id": 10710, "synset": "secretary.n.04", "name": "secretary"}, + {"id": 10711, "synset": "sectional.n.01", "name": "sectional"}, + {"id": 10712, "synset": "security_blanket.n.02", "name": "security_blanket"}, + {"id": 10713, "synset": "security_system.n.02", "name": "security_system"}, + {"id": 10714, "synset": "security_system.n.01", "name": "security_system"}, + {"id": 10715, "synset": "sedan.n.01", "name": "sedan"}, + {"id": 10716, "synset": "sedan.n.02", "name": "sedan"}, + {"id": 10717, "synset": "seeder.n.02", "name": "seeder"}, + {"id": 10718, "synset": "seeker.n.02", "name": "seeker"}, + {"id": 10719, "synset": "seersucker.n.01", "name": "seersucker"}, + {"id": 10720, "synset": "segmental_arch.n.01", "name": "segmental_arch"}, + {"id": 10721, "synset": "segway.n.01", "name": "Segway"}, + {"id": 10722, "synset": "seidel.n.01", "name": "seidel"}, + {"id": 10723, "synset": "seine.n.02", "name": "seine"}, + {"id": 10724, "synset": "seismograph.n.01", "name": "seismograph"}, + {"id": 10725, "synset": "selector.n.02", "name": "selector"}, + {"id": 10726, "synset": "selenium_cell.n.01", "name": "selenium_cell"}, + {"id": 10727, "synset": "self-propelled_vehicle.n.01", "name": "self-propelled_vehicle"}, + { + "id": 10728, + "synset": "self-registering_thermometer.n.01", + "name": "self-registering_thermometer", + }, + {"id": 10729, "synset": "self-starter.n.02", "name": "self-starter"}, + {"id": 10730, "synset": "selsyn.n.01", "name": "selsyn"}, + {"id": 10731, "synset": "selvage.n.02", "name": "selvage"}, + {"id": 10732, "synset": "semaphore.n.01", "name": "semaphore"}, + {"id": 10733, "synset": "semiautomatic_firearm.n.01", "name": "semiautomatic_firearm"}, + {"id": 10734, "synset": "semiautomatic_pistol.n.01", "name": "semiautomatic_pistol"}, + {"id": 10735, "synset": "semiconductor_device.n.01", "name": "semiconductor_device"}, + {"id": 10736, "synset": "semi-detached_house.n.01", "name": "semi-detached_house"}, + {"id": 10737, "synset": "semigloss.n.01", "name": "semigloss"}, + {"id": 10738, "synset": "semitrailer.n.01", "name": "semitrailer"}, + {"id": 10739, "synset": "sennit.n.01", "name": "sennit"}, + {"id": 10740, "synset": "sensitometer.n.01", "name": "sensitometer"}, + {"id": 10741, "synset": "sentry_box.n.01", "name": "sentry_box"}, + {"id": 10742, "synset": "separate.n.02", "name": "separate"}, + {"id": 10743, "synset": "septic_tank.n.01", "name": "septic_tank"}, + {"id": 10744, "synset": "sequence.n.03", "name": "sequence"}, + {"id": 10745, "synset": "sequencer.n.01", "name": "sequencer"}, + {"id": 10746, "synset": "serape.n.01", "name": "serape"}, + {"id": 10747, "synset": "serge.n.01", "name": "serge"}, + {"id": 10748, "synset": "serger.n.01", "name": "serger"}, + {"id": 10749, "synset": "serial_port.n.01", "name": "serial_port"}, + {"id": 10750, "synset": "serpent.n.03", "name": "serpent"}, + {"id": 10751, "synset": "serration.n.03", "name": "serration"}, + {"id": 10752, "synset": "server.n.04", "name": "server"}, + {"id": 10753, "synset": "server.n.03", "name": "server"}, + {"id": 10754, "synset": "service_club.n.02", "name": "service_club"}, + {"id": 10755, "synset": "serving_cart.n.01", "name": "serving_cart"}, + {"id": 10756, "synset": "serving_dish.n.01", "name": "serving_dish"}, + {"id": 10757, "synset": "servo.n.01", "name": "servo"}, + {"id": 10758, "synset": "set.n.13", "name": "set"}, + {"id": 10759, "synset": "set_gun.n.01", "name": "set_gun"}, + {"id": 10760, "synset": "setscrew.n.02", "name": "setscrew"}, + {"id": 10761, "synset": "setscrew.n.01", "name": "setscrew"}, + {"id": 10762, "synset": "set_square.n.01", "name": "set_square"}, + {"id": 10763, "synset": "settee.n.02", "name": "settee"}, + {"id": 10764, "synset": "settle.n.01", "name": "settle"}, + {"id": 10765, "synset": "settlement_house.n.01", "name": "settlement_house"}, + {"id": 10766, "synset": "seventy-eight.n.02", "name": "seventy-eight"}, + { + "id": 10767, + "synset": "seven_wonders_of_the_ancient_world.n.01", + "name": "Seven_Wonders_of_the_Ancient_World", + }, + {"id": 10768, "synset": "sewage_disposal_plant.n.01", "name": "sewage_disposal_plant"}, + {"id": 10769, "synset": "sewer.n.01", "name": "sewer"}, + {"id": 10770, "synset": "sewing_basket.n.01", "name": "sewing_basket"}, + {"id": 10771, "synset": "sewing_kit.n.01", "name": "sewing_kit"}, + {"id": 10772, "synset": "sewing_needle.n.01", "name": "sewing_needle"}, + {"id": 10773, "synset": "sewing_room.n.01", "name": "sewing_room"}, + {"id": 10774, "synset": "sextant.n.02", "name": "sextant"}, + {"id": 10775, "synset": "sgraffito.n.01", "name": "sgraffito"}, + {"id": 10776, "synset": "shackle.n.01", "name": "shackle"}, + {"id": 10777, "synset": "shackle.n.02", "name": "shackle"}, + {"id": 10778, "synset": "shade.n.03", "name": "shade"}, + {"id": 10779, "synset": "shadow_box.n.01", "name": "shadow_box"}, + {"id": 10780, "synset": "shaft.n.03", "name": "shaft"}, + {"id": 10781, "synset": "shag_rug.n.01", "name": "shag_rug"}, + {"id": 10782, "synset": "shank.n.04", "name": "shank"}, + {"id": 10783, "synset": "shank.n.03", "name": "shank"}, + {"id": 10784, "synset": "shantung.n.01", "name": "shantung"}, + {"id": 10785, "synset": "shaper.n.02", "name": "shaper"}, + {"id": 10786, "synset": "shaping_tool.n.01", "name": "shaping_tool"}, + {"id": 10787, "synset": "sharkskin.n.01", "name": "sharkskin"}, + {"id": 10788, "synset": "shaving_brush.n.01", "name": "shaving_brush"}, + {"id": 10789, "synset": "shaving_foam.n.01", "name": "shaving_foam"}, + {"id": 10790, "synset": "shawm.n.01", "name": "shawm"}, + {"id": 10791, "synset": "sheath.n.01", "name": "sheath"}, + {"id": 10792, "synset": "sheathing.n.01", "name": "sheathing"}, + {"id": 10793, "synset": "shed.n.01", "name": "shed"}, + {"id": 10794, "synset": "sheep_bell.n.01", "name": "sheep_bell"}, + {"id": 10795, "synset": "sheepshank.n.01", "name": "sheepshank"}, + {"id": 10796, "synset": "sheepskin_coat.n.01", "name": "sheepskin_coat"}, + {"id": 10797, "synset": "sheepwalk.n.01", "name": "sheepwalk"}, + {"id": 10798, "synset": "sheet.n.03", "name": "sheet"}, + {"id": 10799, "synset": "sheet_bend.n.01", "name": "sheet_bend"}, + {"id": 10800, "synset": "sheeting.n.01", "name": "sheeting"}, + {"id": 10801, "synset": "sheet_pile.n.01", "name": "sheet_pile"}, + {"id": 10802, "synset": "sheetrock.n.01", "name": "Sheetrock"}, + {"id": 10803, "synset": "shelf.n.01", "name": "shelf"}, + {"id": 10804, "synset": "shelf_bracket.n.01", "name": "shelf_bracket"}, + {"id": 10805, "synset": "shell.n.01", "name": "shell"}, + {"id": 10806, "synset": "shell.n.08", "name": "shell"}, + {"id": 10807, "synset": "shell.n.07", "name": "shell"}, + {"id": 10808, "synset": "shellac.n.02", "name": "shellac"}, + {"id": 10809, "synset": "shelter.n.01", "name": "shelter"}, + {"id": 10810, "synset": "shelter.n.02", "name": "shelter"}, + {"id": 10811, "synset": "shelter.n.05", "name": "shelter"}, + {"id": 10812, "synset": "sheltered_workshop.n.01", "name": "sheltered_workshop"}, + {"id": 10813, "synset": "sheraton.n.01", "name": "Sheraton"}, + {"id": 10814, "synset": "shield.n.01", "name": "shield"}, + {"id": 10815, "synset": "shielding.n.03", "name": "shielding"}, + {"id": 10816, "synset": "shift_key.n.01", "name": "shift_key"}, + {"id": 10817, "synset": "shillelagh.n.01", "name": "shillelagh"}, + {"id": 10818, "synset": "shim.n.01", "name": "shim"}, + {"id": 10819, "synset": "shingle.n.03", "name": "shingle"}, + {"id": 10820, "synset": "shin_guard.n.01", "name": "shin_guard"}, + {"id": 10821, "synset": "ship.n.01", "name": "ship"}, + {"id": 10822, "synset": "shipboard_system.n.01", "name": "shipboard_system"}, + {"id": 10823, "synset": "shipping.n.02", "name": "shipping"}, + {"id": 10824, "synset": "shipping_room.n.01", "name": "shipping_room"}, + { + "id": 10825, + "synset": "ship-towed_long-range_acoustic_detection_system.n.01", + "name": "ship-towed_long-range_acoustic_detection_system", + }, + {"id": 10826, "synset": "shipwreck.n.01", "name": "shipwreck"}, + {"id": 10827, "synset": "shirt_button.n.01", "name": "shirt_button"}, + {"id": 10828, "synset": "shirtdress.n.01", "name": "shirtdress"}, + {"id": 10829, "synset": "shirtfront.n.01", "name": "shirtfront"}, + {"id": 10830, "synset": "shirting.n.01", "name": "shirting"}, + {"id": 10831, "synset": "shirtsleeve.n.01", "name": "shirtsleeve"}, + {"id": 10832, "synset": "shirttail.n.02", "name": "shirttail"}, + {"id": 10833, "synset": "shirtwaist.n.01", "name": "shirtwaist"}, + {"id": 10834, "synset": "shiv.n.01", "name": "shiv"}, + {"id": 10835, "synset": "shock_absorber.n.01", "name": "shock_absorber"}, + {"id": 10836, "synset": "shoe.n.02", "name": "shoe"}, + {"id": 10837, "synset": "shoebox.n.02", "name": "shoebox"}, + {"id": 10838, "synset": "shoehorn.n.01", "name": "shoehorn"}, + {"id": 10839, "synset": "shoe_shop.n.01", "name": "shoe_shop"}, + {"id": 10840, "synset": "shoetree.n.01", "name": "shoetree"}, + {"id": 10841, "synset": "shofar.n.01", "name": "shofar"}, + {"id": 10842, "synset": "shoji.n.01", "name": "shoji"}, + {"id": 10843, "synset": "shooting_brake.n.01", "name": "shooting_brake"}, + {"id": 10844, "synset": "shooting_lodge.n.01", "name": "shooting_lodge"}, + {"id": 10845, "synset": "shooting_stick.n.01", "name": "shooting_stick"}, + {"id": 10846, "synset": "shop.n.01", "name": "shop"}, + {"id": 10847, "synset": "shop_bell.n.01", "name": "shop_bell"}, + {"id": 10848, "synset": "shopping_basket.n.01", "name": "shopping_basket"}, + {"id": 10849, "synset": "short_circuit.n.01", "name": "short_circuit"}, + {"id": 10850, "synset": "short_iron.n.01", "name": "short_iron"}, + {"id": 10851, "synset": "short_sleeve.n.01", "name": "short_sleeve"}, + { + "id": 10852, + "synset": "shortwave_diathermy_machine.n.01", + "name": "shortwave_diathermy_machine", + }, + {"id": 10853, "synset": "shot.n.12", "name": "shot"}, + {"id": 10854, "synset": "shotgun.n.01", "name": "shotgun"}, + {"id": 10855, "synset": "shotgun_shell.n.01", "name": "shotgun_shell"}, + {"id": 10856, "synset": "shot_tower.n.01", "name": "shot_tower"}, + {"id": 10857, "synset": "shoulder.n.04", "name": "shoulder"}, + {"id": 10858, "synset": "shouldered_arch.n.01", "name": "shouldered_arch"}, + {"id": 10859, "synset": "shoulder_holster.n.01", "name": "shoulder_holster"}, + {"id": 10860, "synset": "shoulder_pad.n.01", "name": "shoulder_pad"}, + {"id": 10861, "synset": "shoulder_patch.n.01", "name": "shoulder_patch"}, + {"id": 10862, "synset": "shovel.n.03", "name": "shovel"}, + {"id": 10863, "synset": "shovel_hat.n.01", "name": "shovel_hat"}, + {"id": 10864, "synset": "showboat.n.01", "name": "showboat"}, + {"id": 10865, "synset": "shower_room.n.01", "name": "shower_room"}, + {"id": 10866, "synset": "shower_stall.n.01", "name": "shower_stall"}, + {"id": 10867, "synset": "showroom.n.01", "name": "showroom"}, + {"id": 10868, "synset": "shrapnel.n.01", "name": "shrapnel"}, + {"id": 10869, "synset": "shrimper.n.01", "name": "shrimper"}, + {"id": 10870, "synset": "shrine.n.01", "name": "shrine"}, + {"id": 10871, "synset": "shrink-wrap.n.01", "name": "shrink-wrap"}, + {"id": 10872, "synset": "shunt.n.03", "name": "shunt"}, + {"id": 10873, "synset": "shunt.n.02", "name": "shunt"}, + {"id": 10874, "synset": "shunter.n.01", "name": "shunter"}, + {"id": 10875, "synset": "shutter.n.02", "name": "shutter"}, + {"id": 10876, "synset": "shutter.n.01", "name": "shutter"}, + {"id": 10877, "synset": "shuttle.n.03", "name": "shuttle"}, + {"id": 10878, "synset": "shuttle.n.02", "name": "shuttle"}, + {"id": 10879, "synset": "shuttle_bus.n.01", "name": "shuttle_bus"}, + {"id": 10880, "synset": "shuttlecock.n.01", "name": "shuttlecock"}, + {"id": 10881, "synset": "shuttle_helicopter.n.01", "name": "shuttle_helicopter"}, + {"id": 10882, "synset": "sibley_tent.n.01", "name": "Sibley_tent"}, + {"id": 10883, "synset": "sickbay.n.01", "name": "sickbay"}, + {"id": 10884, "synset": "sickbed.n.01", "name": "sickbed"}, + {"id": 10885, "synset": "sickle.n.01", "name": "sickle"}, + {"id": 10886, "synset": "sickroom.n.01", "name": "sickroom"}, + {"id": 10887, "synset": "sideboard.n.02", "name": "sideboard"}, + {"id": 10888, "synset": "sidecar.n.02", "name": "sidecar"}, + {"id": 10889, "synset": "side_chapel.n.01", "name": "side_chapel"}, + {"id": 10890, "synset": "sidelight.n.01", "name": "sidelight"}, + {"id": 10891, "synset": "sidesaddle.n.01", "name": "sidesaddle"}, + {"id": 10892, "synset": "sidewalk.n.01", "name": "sidewalk"}, + {"id": 10893, "synset": "sidewall.n.02", "name": "sidewall"}, + {"id": 10894, "synset": "side-wheeler.n.01", "name": "side-wheeler"}, + {"id": 10895, "synset": "sidewinder.n.02", "name": "sidewinder"}, + {"id": 10896, "synset": "sieve.n.01", "name": "sieve"}, + {"id": 10897, "synset": "sifter.n.01", "name": "sifter"}, + {"id": 10898, "synset": "sights.n.01", "name": "sights"}, + {"id": 10899, "synset": "sigmoidoscope.n.01", "name": "sigmoidoscope"}, + {"id": 10900, "synset": "signal_box.n.01", "name": "signal_box"}, + {"id": 10901, "synset": "signaling_device.n.01", "name": "signaling_device"}, + {"id": 10902, "synset": "silencer.n.02", "name": "silencer"}, + {"id": 10903, "synset": "silent_butler.n.01", "name": "silent_butler"}, + {"id": 10904, "synset": "silex.n.02", "name": "Silex"}, + {"id": 10905, "synset": "silk.n.01", "name": "silk"}, + {"id": 10906, "synset": "silks.n.01", "name": "silks"}, + {"id": 10907, "synset": "silver_plate.n.02", "name": "silver_plate"}, + {"id": 10908, "synset": "silverpoint.n.01", "name": "silverpoint"}, + {"id": 10909, "synset": "simple_pendulum.n.01", "name": "simple_pendulum"}, + {"id": 10910, "synset": "simulator.n.01", "name": "simulator"}, + {"id": 10911, "synset": "single_bed.n.01", "name": "single_bed"}, + {"id": 10912, "synset": "single-breasted_jacket.n.01", "name": "single-breasted_jacket"}, + {"id": 10913, "synset": "single-breasted_suit.n.01", "name": "single-breasted_suit"}, + {"id": 10914, "synset": "single_prop.n.01", "name": "single_prop"}, + {"id": 10915, "synset": "single-reed_instrument.n.01", "name": "single-reed_instrument"}, + {"id": 10916, "synset": "single-rotor_helicopter.n.01", "name": "single-rotor_helicopter"}, + {"id": 10917, "synset": "singlestick.n.01", "name": "singlestick"}, + {"id": 10918, "synset": "singlet.n.01", "name": "singlet"}, + {"id": 10919, "synset": "siren.n.04", "name": "siren"}, + {"id": 10920, "synset": "sister_ship.n.01", "name": "sister_ship"}, + {"id": 10921, "synset": "sitar.n.01", "name": "sitar"}, + {"id": 10922, "synset": "sitz_bath.n.01", "name": "sitz_bath"}, + {"id": 10923, "synset": "six-pack.n.01", "name": "six-pack"}, + {"id": 10924, "synset": "skate.n.01", "name": "skate"}, + {"id": 10925, "synset": "skeg.n.01", "name": "skeg"}, + {"id": 10926, "synset": "skein.n.01", "name": "skein"}, + {"id": 10927, "synset": "skeleton.n.04", "name": "skeleton"}, + {"id": 10928, "synset": "skeleton_key.n.01", "name": "skeleton_key"}, + {"id": 10929, "synset": "skep.n.02", "name": "skep"}, + {"id": 10930, "synset": "skep.n.01", "name": "skep"}, + {"id": 10931, "synset": "sketch.n.01", "name": "sketch"}, + {"id": 10932, "synset": "sketcher.n.02", "name": "sketcher"}, + {"id": 10933, "synset": "skew_arch.n.01", "name": "skew_arch"}, + {"id": 10934, "synset": "ski_binding.n.01", "name": "ski_binding"}, + {"id": 10935, "synset": "skibob.n.01", "name": "skibob"}, + {"id": 10936, "synset": "ski_cap.n.01", "name": "ski_cap"}, + {"id": 10937, "synset": "skidder.n.03", "name": "skidder"}, + {"id": 10938, "synset": "skid_lid.n.01", "name": "skid_lid"}, + {"id": 10939, "synset": "skiff.n.01", "name": "skiff"}, + {"id": 10940, "synset": "ski_jump.n.01", "name": "ski_jump"}, + {"id": 10941, "synset": "ski_lodge.n.01", "name": "ski_lodge"}, + {"id": 10942, "synset": "ski_mask.n.01", "name": "ski_mask"}, + {"id": 10943, "synset": "skimmer.n.02", "name": "skimmer"}, + {"id": 10944, "synset": "ski-plane.n.01", "name": "ski-plane"}, + {"id": 10945, "synset": "ski_rack.n.01", "name": "ski_rack"}, + {"id": 10946, "synset": "skirt.n.01", "name": "skirt"}, + {"id": 10947, "synset": "ski_tow.n.01", "name": "ski_tow"}, + {"id": 10948, "synset": "skivvies.n.01", "name": "Skivvies"}, + {"id": 10949, "synset": "skybox.n.01", "name": "skybox"}, + {"id": 10950, "synset": "skyhook.n.02", "name": "skyhook"}, + {"id": 10951, "synset": "skylight.n.01", "name": "skylight"}, + {"id": 10952, "synset": "skysail.n.01", "name": "skysail"}, + {"id": 10953, "synset": "skyscraper.n.01", "name": "skyscraper"}, + {"id": 10954, "synset": "skywalk.n.01", "name": "skywalk"}, + {"id": 10955, "synset": "slacks.n.01", "name": "slacks"}, + {"id": 10956, "synset": "slack_suit.n.01", "name": "slack_suit"}, + {"id": 10957, "synset": "slasher.n.02", "name": "slasher"}, + {"id": 10958, "synset": "slash_pocket.n.01", "name": "slash_pocket"}, + {"id": 10959, "synset": "slat.n.01", "name": "slat"}, + {"id": 10960, "synset": "slate.n.01", "name": "slate"}, + {"id": 10961, "synset": "slate_pencil.n.01", "name": "slate_pencil"}, + {"id": 10962, "synset": "slate_roof.n.01", "name": "slate_roof"}, + {"id": 10963, "synset": "sleeper.n.07", "name": "sleeper"}, + {"id": 10964, "synset": "sleeper.n.06", "name": "sleeper"}, + {"id": 10965, "synset": "sleeping_car.n.01", "name": "sleeping_car"}, + {"id": 10966, "synset": "sleeve.n.01", "name": "sleeve"}, + {"id": 10967, "synset": "sleeve.n.02", "name": "sleeve"}, + {"id": 10968, "synset": "sleigh_bed.n.01", "name": "sleigh_bed"}, + {"id": 10969, "synset": "sleigh_bell.n.01", "name": "sleigh_bell"}, + {"id": 10970, "synset": "slice_bar.n.01", "name": "slice_bar"}, + {"id": 10971, "synset": "slicer.n.03", "name": "slicer"}, + {"id": 10972, "synset": "slicer.n.02", "name": "slicer"}, + {"id": 10973, "synset": "slide.n.04", "name": "slide"}, + {"id": 10974, "synset": "slide_fastener.n.01", "name": "slide_fastener"}, + {"id": 10975, "synset": "slide_projector.n.01", "name": "slide_projector"}, + {"id": 10976, "synset": "slide_rule.n.01", "name": "slide_rule"}, + {"id": 10977, "synset": "slide_valve.n.01", "name": "slide_valve"}, + {"id": 10978, "synset": "sliding_door.n.01", "name": "sliding_door"}, + {"id": 10979, "synset": "sliding_seat.n.01", "name": "sliding_seat"}, + {"id": 10980, "synset": "sliding_window.n.01", "name": "sliding_window"}, + {"id": 10981, "synset": "sling.n.04", "name": "sling"}, + {"id": 10982, "synset": "slingback.n.01", "name": "slingback"}, + {"id": 10983, "synset": "slinger_ring.n.01", "name": "slinger_ring"}, + {"id": 10984, "synset": "slip_clutch.n.01", "name": "slip_clutch"}, + {"id": 10985, "synset": "slipcover.n.01", "name": "slipcover"}, + {"id": 10986, "synset": "slip-joint_pliers.n.01", "name": "slip-joint_pliers"}, + {"id": 10987, "synset": "slipknot.n.01", "name": "slipknot"}, + {"id": 10988, "synset": "slip-on.n.01", "name": "slip-on"}, + {"id": 10989, "synset": "slip_ring.n.01", "name": "slip_ring"}, + {"id": 10990, "synset": "slit_lamp.n.01", "name": "slit_lamp"}, + {"id": 10991, "synset": "slit_trench.n.01", "name": "slit_trench"}, + {"id": 10992, "synset": "sloop.n.01", "name": "sloop"}, + {"id": 10993, "synset": "sloop_of_war.n.01", "name": "sloop_of_war"}, + {"id": 10994, "synset": "slop_basin.n.01", "name": "slop_basin"}, + {"id": 10995, "synset": "slop_pail.n.01", "name": "slop_pail"}, + {"id": 10996, "synset": "slops.n.02", "name": "slops"}, + {"id": 10997, "synset": "slopshop.n.01", "name": "slopshop"}, + {"id": 10998, "synset": "slot.n.07", "name": "slot"}, + {"id": 10999, "synset": "slot_machine.n.01", "name": "slot_machine"}, + {"id": 11000, "synset": "sluice.n.01", "name": "sluice"}, + {"id": 11001, "synset": "smack.n.03", "name": "smack"}, + {"id": 11002, "synset": "small_boat.n.01", "name": "small_boat"}, + { + "id": 11003, + "synset": "small_computer_system_interface.n.01", + "name": "small_computer_system_interface", + }, + {"id": 11004, "synset": "small_ship.n.01", "name": "small_ship"}, + {"id": 11005, "synset": "small_stores.n.01", "name": "small_stores"}, + {"id": 11006, "synset": "smart_bomb.n.01", "name": "smart_bomb"}, + {"id": 11007, "synset": "smelling_bottle.n.01", "name": "smelling_bottle"}, + {"id": 11008, "synset": "smocking.n.01", "name": "smocking"}, + {"id": 11009, "synset": "smoke_bomb.n.01", "name": "smoke_bomb"}, + {"id": 11010, "synset": "smokehouse.n.01", "name": "smokehouse"}, + {"id": 11011, "synset": "smoker.n.03", "name": "smoker"}, + {"id": 11012, "synset": "smoke_screen.n.01", "name": "smoke_screen"}, + {"id": 11013, "synset": "smoking_room.n.01", "name": "smoking_room"}, + {"id": 11014, "synset": "smoothbore.n.01", "name": "smoothbore"}, + {"id": 11015, "synset": "smooth_plane.n.01", "name": "smooth_plane"}, + {"id": 11016, "synset": "snack_bar.n.01", "name": "snack_bar"}, + {"id": 11017, "synset": "snaffle.n.01", "name": "snaffle"}, + {"id": 11018, "synset": "snap.n.10", "name": "snap"}, + {"id": 11019, "synset": "snap_brim.n.01", "name": "snap_brim"}, + {"id": 11020, "synset": "snap-brim_hat.n.01", "name": "snap-brim_hat"}, + {"id": 11021, "synset": "snare.n.05", "name": "snare"}, + {"id": 11022, "synset": "snare_drum.n.01", "name": "snare_drum"}, + {"id": 11023, "synset": "snatch_block.n.01", "name": "snatch_block"}, + {"id": 11024, "synset": "snifter.n.01", "name": "snifter"}, + {"id": 11025, "synset": "sniper_rifle.n.01", "name": "sniper_rifle"}, + {"id": 11026, "synset": "snips.n.01", "name": "snips"}, + {"id": 11027, "synset": "sno-cat.n.01", "name": "Sno-cat"}, + {"id": 11028, "synset": "snood.n.01", "name": "snood"}, + {"id": 11029, "synset": "snorkel.n.02", "name": "snorkel"}, + {"id": 11030, "synset": "snorkel.n.01", "name": "snorkel"}, + {"id": 11031, "synset": "snowbank.n.01", "name": "snowbank"}, + {"id": 11032, "synset": "snowplow.n.01", "name": "snowplow"}, + {"id": 11033, "synset": "snowshoe.n.01", "name": "snowshoe"}, + {"id": 11034, "synset": "snowsuit.n.01", "name": "snowsuit"}, + {"id": 11035, "synset": "snow_thrower.n.01", "name": "snow_thrower"}, + {"id": 11036, "synset": "snuffbox.n.01", "name": "snuffbox"}, + {"id": 11037, "synset": "snuffer.n.01", "name": "snuffer"}, + {"id": 11038, "synset": "snuffers.n.01", "name": "snuffers"}, + {"id": 11039, "synset": "soapbox.n.01", "name": "soapbox"}, + {"id": 11040, "synset": "soap_dish.n.01", "name": "soap_dish"}, + {"id": 11041, "synset": "soap_dispenser.n.01", "name": "soap_dispenser"}, + {"id": 11042, "synset": "soap_pad.n.01", "name": "soap_pad"}, + {"id": 11043, "synset": "socket.n.02", "name": "socket"}, + {"id": 11044, "synset": "socket_wrench.n.01", "name": "socket_wrench"}, + {"id": 11045, "synset": "socle.n.01", "name": "socle"}, + {"id": 11046, "synset": "soda_can.n.01", "name": "soda_can"}, + {"id": 11047, "synset": "soda_fountain.n.02", "name": "soda_fountain"}, + {"id": 11048, "synset": "soda_fountain.n.01", "name": "soda_fountain"}, + {"id": 11049, "synset": "sod_house.n.01", "name": "sod_house"}, + {"id": 11050, "synset": "sodium-vapor_lamp.n.01", "name": "sodium-vapor_lamp"}, + {"id": 11051, "synset": "soffit.n.01", "name": "soffit"}, + {"id": 11052, "synset": "soft_pedal.n.01", "name": "soft_pedal"}, + {"id": 11053, "synset": "soil_pipe.n.01", "name": "soil_pipe"}, + {"id": 11054, "synset": "solar_cell.n.01", "name": "solar_cell"}, + {"id": 11055, "synset": "solar_dish.n.01", "name": "solar_dish"}, + {"id": 11056, "synset": "solar_heater.n.01", "name": "solar_heater"}, + {"id": 11057, "synset": "solar_house.n.01", "name": "solar_house"}, + {"id": 11058, "synset": "solar_telescope.n.01", "name": "solar_telescope"}, + {"id": 11059, "synset": "solar_thermal_system.n.01", "name": "solar_thermal_system"}, + {"id": 11060, "synset": "soldering_iron.n.01", "name": "soldering_iron"}, + {"id": 11061, "synset": "solenoid.n.01", "name": "solenoid"}, + {"id": 11062, "synset": "solleret.n.01", "name": "solleret"}, + {"id": 11063, "synset": "sonic_depth_finder.n.01", "name": "sonic_depth_finder"}, + {"id": 11064, "synset": "sonogram.n.01", "name": "sonogram"}, + {"id": 11065, "synset": "sonograph.n.01", "name": "sonograph"}, + {"id": 11066, "synset": "sorter.n.02", "name": "sorter"}, + {"id": 11067, "synset": "souk.n.01", "name": "souk"}, + {"id": 11068, "synset": "sound_bow.n.01", "name": "sound_bow"}, + {"id": 11069, "synset": "soundbox.n.01", "name": "soundbox"}, + {"id": 11070, "synset": "sound_camera.n.01", "name": "sound_camera"}, + {"id": 11071, "synset": "sounder.n.01", "name": "sounder"}, + {"id": 11072, "synset": "sound_film.n.01", "name": "sound_film"}, + {"id": 11073, "synset": "sounding_board.n.02", "name": "sounding_board"}, + {"id": 11074, "synset": "sounding_rocket.n.01", "name": "sounding_rocket"}, + {"id": 11075, "synset": "sound_recording.n.01", "name": "sound_recording"}, + {"id": 11076, "synset": "sound_spectrograph.n.01", "name": "sound_spectrograph"}, + {"id": 11077, "synset": "soup_ladle.n.01", "name": "soup_ladle"}, + {"id": 11078, "synset": "source_of_illumination.n.01", "name": "source_of_illumination"}, + {"id": 11079, "synset": "sourdine.n.02", "name": "sourdine"}, + {"id": 11080, "synset": "soutache.n.01", "name": "soutache"}, + {"id": 11081, "synset": "soutane.n.01", "name": "soutane"}, + {"id": 11082, "synset": "sou'wester.n.02", "name": "sou'wester"}, + {"id": 11083, "synset": "soybean_future.n.01", "name": "soybean_future"}, + {"id": 11084, "synset": "space_bar.n.01", "name": "space_bar"}, + {"id": 11085, "synset": "space_capsule.n.01", "name": "space_capsule"}, + {"id": 11086, "synset": "spacecraft.n.01", "name": "spacecraft"}, + {"id": 11087, "synset": "space_heater.n.01", "name": "space_heater"}, + {"id": 11088, "synset": "space_helmet.n.01", "name": "space_helmet"}, + {"id": 11089, "synset": "space_rocket.n.01", "name": "space_rocket"}, + {"id": 11090, "synset": "space_station.n.01", "name": "space_station"}, + {"id": 11091, "synset": "spacesuit.n.01", "name": "spacesuit"}, + {"id": 11092, "synset": "spade.n.02", "name": "spade"}, + {"id": 11093, "synset": "spade_bit.n.01", "name": "spade_bit"}, + {"id": 11094, "synset": "spaghetti_junction.n.01", "name": "spaghetti_junction"}, + {"id": 11095, "synset": "spandau.n.01", "name": "Spandau"}, + {"id": 11096, "synset": "spandex.n.01", "name": "spandex"}, + {"id": 11097, "synset": "spandrel.n.01", "name": "spandrel"}, + {"id": 11098, "synset": "spanker.n.02", "name": "spanker"}, + {"id": 11099, "synset": "spar.n.02", "name": "spar"}, + {"id": 11100, "synset": "sparge_pipe.n.01", "name": "sparge_pipe"}, + {"id": 11101, "synset": "spark_arrester.n.02", "name": "spark_arrester"}, + {"id": 11102, "synset": "spark_arrester.n.01", "name": "spark_arrester"}, + {"id": 11103, "synset": "spark_chamber.n.01", "name": "spark_chamber"}, + {"id": 11104, "synset": "spark_coil.n.01", "name": "spark_coil"}, + {"id": 11105, "synset": "spark_gap.n.01", "name": "spark_gap"}, + {"id": 11106, "synset": "spark_lever.n.01", "name": "spark_lever"}, + {"id": 11107, "synset": "spark_plug.n.01", "name": "spark_plug"}, + {"id": 11108, "synset": "sparkplug_wrench.n.01", "name": "sparkplug_wrench"}, + {"id": 11109, "synset": "spark_transmitter.n.01", "name": "spark_transmitter"}, + {"id": 11110, "synset": "spat.n.02", "name": "spat"}, + {"id": 11111, "synset": "spatula.n.01", "name": "spatula"}, + {"id": 11112, "synset": "speakerphone.n.01", "name": "speakerphone"}, + {"id": 11113, "synset": "speaking_trumpet.n.01", "name": "speaking_trumpet"}, + {"id": 11114, "synset": "spear.n.02", "name": "spear"}, + {"id": 11115, "synset": "specialty_store.n.01", "name": "specialty_store"}, + {"id": 11116, "synset": "specimen_bottle.n.01", "name": "specimen_bottle"}, + {"id": 11117, "synset": "spectacle.n.02", "name": "spectacle"}, + {"id": 11118, "synset": "spectator_pump.n.01", "name": "spectator_pump"}, + {"id": 11119, "synset": "spectrograph.n.01", "name": "spectrograph"}, + {"id": 11120, "synset": "spectrophotometer.n.01", "name": "spectrophotometer"}, + {"id": 11121, "synset": "spectroscope.n.01", "name": "spectroscope"}, + {"id": 11122, "synset": "speculum.n.02", "name": "speculum"}, + {"id": 11123, "synset": "speedboat.n.01", "name": "speedboat"}, + {"id": 11124, "synset": "speed_bump.n.01", "name": "speed_bump"}, + {"id": 11125, "synset": "speedometer.n.01", "name": "speedometer"}, + {"id": 11126, "synset": "speed_skate.n.01", "name": "speed_skate"}, + {"id": 11127, "synset": "spherometer.n.01", "name": "spherometer"}, + {"id": 11128, "synset": "sphygmomanometer.n.01", "name": "sphygmomanometer"}, + {"id": 11129, "synset": "spicemill.n.01", "name": "spicemill"}, + {"id": 11130, "synset": "spider.n.03", "name": "spider"}, + {"id": 11131, "synset": "spider_web.n.01", "name": "spider_web"}, + {"id": 11132, "synset": "spike.n.02", "name": "spike"}, + {"id": 11133, "synset": "spike.n.11", "name": "spike"}, + {"id": 11134, "synset": "spindle.n.04", "name": "spindle"}, + {"id": 11135, "synset": "spindle.n.03", "name": "spindle"}, + {"id": 11136, "synset": "spindle.n.02", "name": "spindle"}, + {"id": 11137, "synset": "spin_dryer.n.01", "name": "spin_dryer"}, + {"id": 11138, "synset": "spinet.n.02", "name": "spinet"}, + {"id": 11139, "synset": "spinet.n.01", "name": "spinet"}, + {"id": 11140, "synset": "spinnaker.n.01", "name": "spinnaker"}, + {"id": 11141, "synset": "spinner.n.03", "name": "spinner"}, + {"id": 11142, "synset": "spinning_frame.n.01", "name": "spinning_frame"}, + {"id": 11143, "synset": "spinning_jenny.n.01", "name": "spinning_jenny"}, + {"id": 11144, "synset": "spinning_machine.n.01", "name": "spinning_machine"}, + {"id": 11145, "synset": "spinning_rod.n.01", "name": "spinning_rod"}, + {"id": 11146, "synset": "spinning_wheel.n.01", "name": "spinning_wheel"}, + {"id": 11147, "synset": "spiral_bandage.n.01", "name": "spiral_bandage"}, + { + "id": 11148, + "synset": "spiral_ratchet_screwdriver.n.01", + "name": "spiral_ratchet_screwdriver", + }, + {"id": 11149, "synset": "spiral_spring.n.01", "name": "spiral_spring"}, + {"id": 11150, "synset": "spirit_lamp.n.01", "name": "spirit_lamp"}, + {"id": 11151, "synset": "spirit_stove.n.01", "name": "spirit_stove"}, + {"id": 11152, "synset": "spirometer.n.01", "name": "spirometer"}, + {"id": 11153, "synset": "spit.n.03", "name": "spit"}, + {"id": 11154, "synset": "spittoon.n.01", "name": "spittoon"}, + {"id": 11155, "synset": "splashboard.n.02", "name": "splashboard"}, + {"id": 11156, "synset": "splasher.n.01", "name": "splasher"}, + {"id": 11157, "synset": "splice.n.01", "name": "splice"}, + {"id": 11158, "synset": "splicer.n.03", "name": "splicer"}, + {"id": 11159, "synset": "splint.n.02", "name": "splint"}, + {"id": 11160, "synset": "split_rail.n.01", "name": "split_rail"}, + {"id": 11161, "synset": "spode.n.02", "name": "Spode"}, + {"id": 11162, "synset": "spoiler.n.05", "name": "spoiler"}, + {"id": 11163, "synset": "spoiler.n.04", "name": "spoiler"}, + {"id": 11164, "synset": "spoke.n.01", "name": "spoke"}, + {"id": 11165, "synset": "spokeshave.n.01", "name": "spokeshave"}, + {"id": 11166, "synset": "sponge_cloth.n.01", "name": "sponge_cloth"}, + {"id": 11167, "synset": "sponge_mop.n.01", "name": "sponge_mop"}, + {"id": 11168, "synset": "spoon.n.03", "name": "spoon"}, + {"id": 11169, "synset": "spork.n.01", "name": "Spork"}, + {"id": 11170, "synset": "sporran.n.01", "name": "sporran"}, + {"id": 11171, "synset": "sport_kite.n.01", "name": "sport_kite"}, + {"id": 11172, "synset": "sports_car.n.01", "name": "sports_car"}, + {"id": 11173, "synset": "sports_equipment.n.01", "name": "sports_equipment"}, + {"id": 11174, "synset": "sports_implement.n.01", "name": "sports_implement"}, + {"id": 11175, "synset": "sport_utility.n.01", "name": "sport_utility"}, + {"id": 11176, "synset": "spot.n.07", "name": "spot"}, + {"id": 11177, "synset": "spot_weld.n.01", "name": "spot_weld"}, + {"id": 11178, "synset": "spouter.n.02", "name": "spouter"}, + {"id": 11179, "synset": "sprag.n.01", "name": "sprag"}, + {"id": 11180, "synset": "spray_gun.n.01", "name": "spray_gun"}, + {"id": 11181, "synset": "spray_paint.n.01", "name": "spray_paint"}, + {"id": 11182, "synset": "spreader.n.01", "name": "spreader"}, + {"id": 11183, "synset": "sprig.n.02", "name": "sprig"}, + {"id": 11184, "synset": "spring.n.02", "name": "spring"}, + {"id": 11185, "synset": "spring_balance.n.01", "name": "spring_balance"}, + {"id": 11186, "synset": "springboard.n.01", "name": "springboard"}, + {"id": 11187, "synset": "sprinkler.n.01", "name": "sprinkler"}, + {"id": 11188, "synset": "sprinkler_system.n.01", "name": "sprinkler_system"}, + {"id": 11189, "synset": "sprit.n.01", "name": "sprit"}, + {"id": 11190, "synset": "spritsail.n.01", "name": "spritsail"}, + {"id": 11191, "synset": "sprocket.n.02", "name": "sprocket"}, + {"id": 11192, "synset": "sprocket.n.01", "name": "sprocket"}, + {"id": 11193, "synset": "spun_yarn.n.01", "name": "spun_yarn"}, + {"id": 11194, "synset": "spur.n.04", "name": "spur"}, + {"id": 11195, "synset": "spur_gear.n.01", "name": "spur_gear"}, + {"id": 11196, "synset": "sputnik.n.01", "name": "sputnik"}, + {"id": 11197, "synset": "spy_satellite.n.01", "name": "spy_satellite"}, + {"id": 11198, "synset": "squad_room.n.01", "name": "squad_room"}, + {"id": 11199, "synset": "square.n.08", "name": "square"}, + {"id": 11200, "synset": "square_knot.n.01", "name": "square_knot"}, + {"id": 11201, "synset": "square-rigger.n.01", "name": "square-rigger"}, + {"id": 11202, "synset": "square_sail.n.01", "name": "square_sail"}, + {"id": 11203, "synset": "squash_ball.n.01", "name": "squash_ball"}, + {"id": 11204, "synset": "squash_racket.n.01", "name": "squash_racket"}, + {"id": 11205, "synset": "squawk_box.n.01", "name": "squawk_box"}, + {"id": 11206, "synset": "squeegee.n.01", "name": "squeegee"}, + {"id": 11207, "synset": "squeezer.n.01", "name": "squeezer"}, + {"id": 11208, "synset": "squelch_circuit.n.01", "name": "squelch_circuit"}, + {"id": 11209, "synset": "squinch.n.01", "name": "squinch"}, + {"id": 11210, "synset": "stabilizer.n.03", "name": "stabilizer"}, + {"id": 11211, "synset": "stabilizer.n.02", "name": "stabilizer"}, + {"id": 11212, "synset": "stabilizer_bar.n.01", "name": "stabilizer_bar"}, + {"id": 11213, "synset": "stable.n.01", "name": "stable"}, + {"id": 11214, "synset": "stable_gear.n.01", "name": "stable_gear"}, + {"id": 11215, "synset": "stabling.n.01", "name": "stabling"}, + {"id": 11216, "synset": "stacks.n.02", "name": "stacks"}, + {"id": 11217, "synset": "staddle.n.01", "name": "staddle"}, + {"id": 11218, "synset": "stadium.n.01", "name": "stadium"}, + {"id": 11219, "synset": "stage.n.03", "name": "stage"}, + {"id": 11220, "synset": "stained-glass_window.n.01", "name": "stained-glass_window"}, + {"id": 11221, "synset": "stair-carpet.n.01", "name": "stair-carpet"}, + {"id": 11222, "synset": "stair-rod.n.01", "name": "stair-rod"}, + {"id": 11223, "synset": "stairwell.n.01", "name": "stairwell"}, + {"id": 11224, "synset": "stake.n.05", "name": "stake"}, + {"id": 11225, "synset": "stall.n.03", "name": "stall"}, + {"id": 11226, "synset": "stall.n.01", "name": "stall"}, + {"id": 11227, "synset": "stamp.n.08", "name": "stamp"}, + {"id": 11228, "synset": "stamp_mill.n.01", "name": "stamp_mill"}, + {"id": 11229, "synset": "stamping_machine.n.01", "name": "stamping_machine"}, + {"id": 11230, "synset": "stanchion.n.01", "name": "stanchion"}, + {"id": 11231, "synset": "stand.n.04", "name": "stand"}, + {"id": 11232, "synset": "standard.n.05", "name": "standard"}, + {"id": 11233, "synset": "standard_cell.n.01", "name": "standard_cell"}, + {"id": 11234, "synset": "standard_transmission.n.01", "name": "standard_transmission"}, + {"id": 11235, "synset": "standing_press.n.01", "name": "standing_press"}, + {"id": 11236, "synset": "stanhope.n.01", "name": "stanhope"}, + {"id": 11237, "synset": "stanley_steamer.n.01", "name": "Stanley_Steamer"}, + {"id": 11238, "synset": "staple.n.05", "name": "staple"}, + {"id": 11239, "synset": "staple.n.04", "name": "staple"}, + {"id": 11240, "synset": "staple_gun.n.01", "name": "staple_gun"}, + {"id": 11241, "synset": "starship.n.01", "name": "starship"}, + {"id": 11242, "synset": "starter.n.01", "name": "starter"}, + {"id": 11243, "synset": "starting_gate.n.01", "name": "starting_gate"}, + {"id": 11244, "synset": "stassano_furnace.n.01", "name": "Stassano_furnace"}, + {"id": 11245, "synset": "statehouse.n.01", "name": "Statehouse"}, + {"id": 11246, "synset": "stately_home.n.01", "name": "stately_home"}, + {"id": 11247, "synset": "state_prison.n.01", "name": "state_prison"}, + {"id": 11248, "synset": "stateroom.n.01", "name": "stateroom"}, + {"id": 11249, "synset": "static_tube.n.01", "name": "static_tube"}, + {"id": 11250, "synset": "station.n.01", "name": "station"}, + {"id": 11251, "synset": "stator.n.01", "name": "stator"}, + {"id": 11252, "synset": "stay.n.05", "name": "stay"}, + {"id": 11253, "synset": "staysail.n.01", "name": "staysail"}, + {"id": 11254, "synset": "steakhouse.n.01", "name": "steakhouse"}, + {"id": 11255, "synset": "stealth_aircraft.n.01", "name": "stealth_aircraft"}, + {"id": 11256, "synset": "stealth_bomber.n.01", "name": "stealth_bomber"}, + {"id": 11257, "synset": "stealth_fighter.n.01", "name": "stealth_fighter"}, + {"id": 11258, "synset": "steam_bath.n.01", "name": "steam_bath"}, + {"id": 11259, "synset": "steamboat.n.01", "name": "steamboat"}, + {"id": 11260, "synset": "steam_chest.n.01", "name": "steam_chest"}, + {"id": 11261, "synset": "steam_engine.n.01", "name": "steam_engine"}, + {"id": 11262, "synset": "steamer.n.03", "name": "steamer"}, + {"id": 11263, "synset": "steamer.n.02", "name": "steamer"}, + {"id": 11264, "synset": "steam_iron.n.01", "name": "steam_iron"}, + {"id": 11265, "synset": "steam_locomotive.n.01", "name": "steam_locomotive"}, + {"id": 11266, "synset": "steamroller.n.02", "name": "steamroller"}, + {"id": 11267, "synset": "steam_shovel.n.01", "name": "steam_shovel"}, + {"id": 11268, "synset": "steam_turbine.n.01", "name": "steam_turbine"}, + {"id": 11269, "synset": "steam_whistle.n.01", "name": "steam_whistle"}, + {"id": 11270, "synset": "steel.n.03", "name": "steel"}, + {"id": 11271, "synset": "steel_arch_bridge.n.01", "name": "steel_arch_bridge"}, + {"id": 11272, "synset": "steel_drum.n.01", "name": "steel_drum"}, + {"id": 11273, "synset": "steel_mill.n.01", "name": "steel_mill"}, + {"id": 11274, "synset": "steel-wool_pad.n.01", "name": "steel-wool_pad"}, + {"id": 11275, "synset": "steelyard.n.01", "name": "steelyard"}, + {"id": 11276, "synset": "steeple.n.01", "name": "steeple"}, + {"id": 11277, "synset": "steerage.n.01", "name": "steerage"}, + {"id": 11278, "synset": "steering_gear.n.01", "name": "steering_gear"}, + {"id": 11279, "synset": "steering_linkage.n.01", "name": "steering_linkage"}, + {"id": 11280, "synset": "steering_system.n.01", "name": "steering_system"}, + {"id": 11281, "synset": "stele.n.02", "name": "stele"}, + {"id": 11282, "synset": "stem-winder.n.01", "name": "stem-winder"}, + {"id": 11283, "synset": "stencil.n.01", "name": "stencil"}, + {"id": 11284, "synset": "sten_gun.n.01", "name": "Sten_gun"}, + {"id": 11285, "synset": "stenograph.n.02", "name": "stenograph"}, + {"id": 11286, "synset": "step.n.04", "name": "step"}, + {"id": 11287, "synset": "step-down_transformer.n.01", "name": "step-down_transformer"}, + {"id": 11288, "synset": "step-up_transformer.n.01", "name": "step-up_transformer"}, + {"id": 11289, "synset": "stereoscope.n.01", "name": "stereoscope"}, + {"id": 11290, "synset": "stern_chaser.n.01", "name": "stern_chaser"}, + {"id": 11291, "synset": "sternpost.n.01", "name": "sternpost"}, + {"id": 11292, "synset": "sternwheeler.n.01", "name": "sternwheeler"}, + {"id": 11293, "synset": "stethoscope.n.01", "name": "stethoscope"}, + {"id": 11294, "synset": "stewing_pan.n.01", "name": "stewing_pan"}, + {"id": 11295, "synset": "stick.n.01", "name": "stick"}, + {"id": 11296, "synset": "stick.n.07", "name": "stick"}, + {"id": 11297, "synset": "stick.n.03", "name": "stick"}, + {"id": 11298, "synset": "stick.n.06", "name": "stick"}, + {"id": 11299, "synset": "stile.n.01", "name": "stile"}, + {"id": 11300, "synset": "stiletto.n.01", "name": "stiletto"}, + {"id": 11301, "synset": "still.n.03", "name": "still"}, + {"id": 11302, "synset": "stillroom.n.01", "name": "stillroom"}, + {"id": 11303, "synset": "stillson_wrench.n.01", "name": "Stillson_wrench"}, + {"id": 11304, "synset": "stilt.n.02", "name": "stilt"}, + {"id": 11305, "synset": "stinger.n.03", "name": "Stinger"}, + {"id": 11306, "synset": "stink_bomb.n.01", "name": "stink_bomb"}, + {"id": 11307, "synset": "stirrup_pump.n.01", "name": "stirrup_pump"}, + {"id": 11308, "synset": "stob.n.01", "name": "stob"}, + {"id": 11309, "synset": "stock.n.03", "name": "stock"}, + {"id": 11310, "synset": "stockade.n.01", "name": "stockade"}, + {"id": 11311, "synset": "stockcar.n.01", "name": "stockcar"}, + {"id": 11312, "synset": "stock_car.n.02", "name": "stock_car"}, + {"id": 11313, "synset": "stockinet.n.01", "name": "stockinet"}, + {"id": 11314, "synset": "stocking.n.01", "name": "stocking"}, + {"id": 11315, "synset": "stock-in-trade.n.01", "name": "stock-in-trade"}, + {"id": 11316, "synset": "stockpot.n.01", "name": "stockpot"}, + {"id": 11317, "synset": "stockroom.n.01", "name": "stockroom"}, + {"id": 11318, "synset": "stocks.n.03", "name": "stocks"}, + {"id": 11319, "synset": "stock_saddle.n.01", "name": "stock_saddle"}, + {"id": 11320, "synset": "stockyard.n.01", "name": "stockyard"}, + {"id": 11321, "synset": "stole.n.01", "name": "stole"}, + {"id": 11322, "synset": "stomacher.n.01", "name": "stomacher"}, + {"id": 11323, "synset": "stomach_pump.n.01", "name": "stomach_pump"}, + {"id": 11324, "synset": "stone_wall.n.01", "name": "stone_wall"}, + {"id": 11325, "synset": "stoneware.n.01", "name": "stoneware"}, + {"id": 11326, "synset": "stonework.n.01", "name": "stonework"}, + {"id": 11327, "synset": "stoop.n.03", "name": "stoop"}, + {"id": 11328, "synset": "stop_bath.n.01", "name": "stop_bath"}, + {"id": 11329, "synset": "stopcock.n.01", "name": "stopcock"}, + {"id": 11330, "synset": "stopper_knot.n.01", "name": "stopper_knot"}, + {"id": 11331, "synset": "stopwatch.n.01", "name": "stopwatch"}, + {"id": 11332, "synset": "storage_battery.n.01", "name": "storage_battery"}, + {"id": 11333, "synset": "storage_cell.n.01", "name": "storage_cell"}, + {"id": 11334, "synset": "storage_ring.n.01", "name": "storage_ring"}, + {"id": 11335, "synset": "storage_space.n.01", "name": "storage_space"}, + {"id": 11336, "synset": "storeroom.n.01", "name": "storeroom"}, + {"id": 11337, "synset": "storm_cellar.n.01", "name": "storm_cellar"}, + {"id": 11338, "synset": "storm_door.n.01", "name": "storm_door"}, + {"id": 11339, "synset": "storm_window.n.01", "name": "storm_window"}, + {"id": 11340, "synset": "stoup.n.02", "name": "stoup"}, + {"id": 11341, "synset": "stoup.n.01", "name": "stoup"}, + {"id": 11342, "synset": "stove.n.02", "name": "stove"}, + {"id": 11343, "synset": "stove_bolt.n.01", "name": "stove_bolt"}, + {"id": 11344, "synset": "stovepipe.n.01", "name": "stovepipe"}, + {"id": 11345, "synset": "stovepipe_iron.n.01", "name": "stovepipe_iron"}, + {"id": 11346, "synset": "stradavarius.n.01", "name": "Stradavarius"}, + {"id": 11347, "synset": "straight_chair.n.01", "name": "straight_chair"}, + {"id": 11348, "synset": "straightedge.n.01", "name": "straightedge"}, + {"id": 11349, "synset": "straightener.n.01", "name": "straightener"}, + {"id": 11350, "synset": "straight_flute.n.01", "name": "straight_flute"}, + {"id": 11351, "synset": "straight_pin.n.01", "name": "straight_pin"}, + {"id": 11352, "synset": "straight_razor.n.01", "name": "straight_razor"}, + {"id": 11353, "synset": "straitjacket.n.02", "name": "straitjacket"}, + {"id": 11354, "synset": "strap.n.04", "name": "strap"}, + {"id": 11355, "synset": "strap_hinge.n.01", "name": "strap_hinge"}, + {"id": 11356, "synset": "strapless.n.01", "name": "strapless"}, + {"id": 11357, "synset": "streamer_fly.n.01", "name": "streamer_fly"}, + {"id": 11358, "synset": "streamliner.n.01", "name": "streamliner"}, + {"id": 11359, "synset": "street.n.01", "name": "street"}, + {"id": 11360, "synset": "street.n.02", "name": "street"}, + {"id": 11361, "synset": "streetcar.n.01", "name": "streetcar"}, + {"id": 11362, "synset": "street_clothes.n.01", "name": "street_clothes"}, + {"id": 11363, "synset": "stretcher.n.03", "name": "stretcher"}, + {"id": 11364, "synset": "stretcher.n.01", "name": "stretcher"}, + {"id": 11365, "synset": "stretch_pants.n.01", "name": "stretch_pants"}, + {"id": 11366, "synset": "strickle.n.02", "name": "strickle"}, + {"id": 11367, "synset": "strickle.n.01", "name": "strickle"}, + {"id": 11368, "synset": "stringed_instrument.n.01", "name": "stringed_instrument"}, + {"id": 11369, "synset": "stringer.n.04", "name": "stringer"}, + {"id": 11370, "synset": "stringer.n.03", "name": "stringer"}, + {"id": 11371, "synset": "string_tie.n.01", "name": "string_tie"}, + {"id": 11372, "synset": "strip.n.05", "name": "strip"}, + {"id": 11373, "synset": "strip_lighting.n.01", "name": "strip_lighting"}, + {"id": 11374, "synset": "strip_mall.n.01", "name": "strip_mall"}, + {"id": 11375, "synset": "stroboscope.n.01", "name": "stroboscope"}, + {"id": 11376, "synset": "strongbox.n.01", "name": "strongbox"}, + {"id": 11377, "synset": "stronghold.n.01", "name": "stronghold"}, + {"id": 11378, "synset": "strongroom.n.01", "name": "strongroom"}, + {"id": 11379, "synset": "strop.n.01", "name": "strop"}, + {"id": 11380, "synset": "structural_member.n.01", "name": "structural_member"}, + {"id": 11381, "synset": "structure.n.01", "name": "structure"}, + {"id": 11382, "synset": "student_center.n.01", "name": "student_center"}, + {"id": 11383, "synset": "student_lamp.n.01", "name": "student_lamp"}, + {"id": 11384, "synset": "student_union.n.01", "name": "student_union"}, + {"id": 11385, "synset": "stud_finder.n.01", "name": "stud_finder"}, + {"id": 11386, "synset": "studio_apartment.n.01", "name": "studio_apartment"}, + {"id": 11387, "synset": "studio_couch.n.01", "name": "studio_couch"}, + {"id": 11388, "synset": "study.n.05", "name": "study"}, + {"id": 11389, "synset": "study_hall.n.02", "name": "study_hall"}, + {"id": 11390, "synset": "stuffing_nut.n.01", "name": "stuffing_nut"}, + {"id": 11391, "synset": "stump.n.03", "name": "stump"}, + {"id": 11392, "synset": "stun_gun.n.01", "name": "stun_gun"}, + {"id": 11393, "synset": "stupa.n.01", "name": "stupa"}, + {"id": 11394, "synset": "sty.n.02", "name": "sty"}, + {"id": 11395, "synset": "stylus.n.01", "name": "stylus"}, + {"id": 11396, "synset": "sub-assembly.n.01", "name": "sub-assembly"}, + {"id": 11397, "synset": "subcompact.n.01", "name": "subcompact"}, + {"id": 11398, "synset": "submachine_gun.n.01", "name": "submachine_gun"}, + {"id": 11399, "synset": "submarine.n.01", "name": "submarine"}, + {"id": 11400, "synset": "submarine_torpedo.n.01", "name": "submarine_torpedo"}, + {"id": 11401, "synset": "submersible.n.02", "name": "submersible"}, + {"id": 11402, "synset": "submersible.n.01", "name": "submersible"}, + {"id": 11403, "synset": "subtracter.n.02", "name": "subtracter"}, + {"id": 11404, "synset": "subway_token.n.01", "name": "subway_token"}, + {"id": 11405, "synset": "subway_train.n.01", "name": "subway_train"}, + {"id": 11406, "synset": "suction_cup.n.01", "name": "suction_cup"}, + {"id": 11407, "synset": "suction_pump.n.01", "name": "suction_pump"}, + {"id": 11408, "synset": "sudatorium.n.01", "name": "sudatorium"}, + {"id": 11409, "synset": "suede_cloth.n.01", "name": "suede_cloth"}, + {"id": 11410, "synset": "sugar_refinery.n.01", "name": "sugar_refinery"}, + {"id": 11411, "synset": "sugar_spoon.n.01", "name": "sugar_spoon"}, + {"id": 11412, "synset": "suite.n.02", "name": "suite"}, + {"id": 11413, "synset": "suiting.n.01", "name": "suiting"}, + {"id": 11414, "synset": "sulky.n.01", "name": "sulky"}, + {"id": 11415, "synset": "summer_house.n.01", "name": "summer_house"}, + {"id": 11416, "synset": "sumo_ring.n.01", "name": "sumo_ring"}, + {"id": 11417, "synset": "sump.n.01", "name": "sump"}, + {"id": 11418, "synset": "sump_pump.n.01", "name": "sump_pump"}, + {"id": 11419, "synset": "sunbonnet.n.01", "name": "sunbonnet"}, + {"id": 11420, "synset": "sunday_best.n.01", "name": "Sunday_best"}, + {"id": 11421, "synset": "sun_deck.n.01", "name": "sun_deck"}, + {"id": 11422, "synset": "sundial.n.01", "name": "sundial"}, + {"id": 11423, "synset": "sundress.n.01", "name": "sundress"}, + {"id": 11424, "synset": "sundries.n.01", "name": "sundries"}, + {"id": 11425, "synset": "sun_gear.n.01", "name": "sun_gear"}, + {"id": 11426, "synset": "sunglass.n.01", "name": "sunglass"}, + {"id": 11427, "synset": "sunlamp.n.01", "name": "sunlamp"}, + {"id": 11428, "synset": "sun_parlor.n.01", "name": "sun_parlor"}, + {"id": 11429, "synset": "sunroof.n.01", "name": "sunroof"}, + {"id": 11430, "synset": "sunscreen.n.01", "name": "sunscreen"}, + {"id": 11431, "synset": "sunsuit.n.01", "name": "sunsuit"}, + {"id": 11432, "synset": "supercharger.n.01", "name": "supercharger"}, + {"id": 11433, "synset": "supercomputer.n.01", "name": "supercomputer"}, + { + "id": 11434, + "synset": "superconducting_supercollider.n.01", + "name": "superconducting_supercollider", + }, + {"id": 11435, "synset": "superhighway.n.02", "name": "superhighway"}, + {"id": 11436, "synset": "supermarket.n.01", "name": "supermarket"}, + {"id": 11437, "synset": "superstructure.n.01", "name": "superstructure"}, + {"id": 11438, "synset": "supertanker.n.01", "name": "supertanker"}, + {"id": 11439, "synset": "supper_club.n.01", "name": "supper_club"}, + {"id": 11440, "synset": "supplejack.n.01", "name": "supplejack"}, + {"id": 11441, "synset": "supply_chamber.n.01", "name": "supply_chamber"}, + {"id": 11442, "synset": "supply_closet.n.01", "name": "supply_closet"}, + {"id": 11443, "synset": "support.n.10", "name": "support"}, + {"id": 11444, "synset": "support.n.07", "name": "support"}, + {"id": 11445, "synset": "support_column.n.01", "name": "support_column"}, + {"id": 11446, "synset": "support_hose.n.01", "name": "support_hose"}, + {"id": 11447, "synset": "supporting_structure.n.01", "name": "supporting_structure"}, + {"id": 11448, "synset": "supporting_tower.n.01", "name": "supporting_tower"}, + {"id": 11449, "synset": "surcoat.n.02", "name": "surcoat"}, + {"id": 11450, "synset": "surface_gauge.n.01", "name": "surface_gauge"}, + {"id": 11451, "synset": "surface_lift.n.01", "name": "surface_lift"}, + {"id": 11452, "synset": "surface_search_radar.n.01", "name": "surface_search_radar"}, + {"id": 11453, "synset": "surface_ship.n.01", "name": "surface_ship"}, + {"id": 11454, "synset": "surface-to-air_missile.n.01", "name": "surface-to-air_missile"}, + { + "id": 11455, + "synset": "surface-to-air_missile_system.n.01", + "name": "surface-to-air_missile_system", + }, + {"id": 11456, "synset": "surfboat.n.01", "name": "surfboat"}, + {"id": 11457, "synset": "surcoat.n.01", "name": "surcoat"}, + {"id": 11458, "synset": "surgeon's_knot.n.01", "name": "surgeon's_knot"}, + {"id": 11459, "synset": "surgery.n.02", "name": "surgery"}, + {"id": 11460, "synset": "surge_suppressor.n.01", "name": "surge_suppressor"}, + {"id": 11461, "synset": "surgical_dressing.n.01", "name": "surgical_dressing"}, + {"id": 11462, "synset": "surgical_instrument.n.01", "name": "surgical_instrument"}, + {"id": 11463, "synset": "surgical_knife.n.01", "name": "surgical_knife"}, + {"id": 11464, "synset": "surplice.n.01", "name": "surplice"}, + {"id": 11465, "synset": "surrey.n.02", "name": "surrey"}, + {"id": 11466, "synset": "surtout.n.01", "name": "surtout"}, + {"id": 11467, "synset": "surveillance_system.n.01", "name": "surveillance_system"}, + {"id": 11468, "synset": "surveying_instrument.n.01", "name": "surveying_instrument"}, + {"id": 11469, "synset": "surveyor's_level.n.01", "name": "surveyor's_level"}, + {"id": 11470, "synset": "sushi_bar.n.01", "name": "sushi_bar"}, + {"id": 11471, "synset": "suspension.n.05", "name": "suspension"}, + {"id": 11472, "synset": "suspension_bridge.n.01", "name": "suspension_bridge"}, + {"id": 11473, "synset": "suspensory.n.01", "name": "suspensory"}, + {"id": 11474, "synset": "sustaining_pedal.n.01", "name": "sustaining_pedal"}, + {"id": 11475, "synset": "suture.n.02", "name": "suture"}, + {"id": 11476, "synset": "swab.n.01", "name": "swab"}, + {"id": 11477, "synset": "swaddling_clothes.n.01", "name": "swaddling_clothes"}, + {"id": 11478, "synset": "swag.n.03", "name": "swag"}, + {"id": 11479, "synset": "swage_block.n.01", "name": "swage_block"}, + {"id": 11480, "synset": "swagger_stick.n.01", "name": "swagger_stick"}, + {"id": 11481, "synset": "swallow-tailed_coat.n.01", "name": "swallow-tailed_coat"}, + {"id": 11482, "synset": "swamp_buggy.n.01", "name": "swamp_buggy"}, + {"id": 11483, "synset": "swan's_down.n.01", "name": "swan's_down"}, + {"id": 11484, "synset": "swathe.n.01", "name": "swathe"}, + {"id": 11485, "synset": "swatter.n.01", "name": "swatter"}, + {"id": 11486, "synset": "sweat_bag.n.01", "name": "sweat_bag"}, + {"id": 11487, "synset": "sweatband.n.01", "name": "sweatband"}, + {"id": 11488, "synset": "sweatshop.n.01", "name": "sweatshop"}, + {"id": 11489, "synset": "sweat_suit.n.01", "name": "sweat_suit"}, + {"id": 11490, "synset": "sweep.n.04", "name": "sweep"}, + {"id": 11491, "synset": "sweep_hand.n.01", "name": "sweep_hand"}, + {"id": 11492, "synset": "swimming_trunks.n.01", "name": "swimming_trunks"}, + {"id": 11493, "synset": "swing.n.02", "name": "swing"}, + {"id": 11494, "synset": "swing_door.n.01", "name": "swing_door"}, + {"id": 11495, "synset": "switch.n.01", "name": "switch"}, + {"id": 11496, "synset": "switchblade.n.01", "name": "switchblade"}, + {"id": 11497, "synset": "switch_engine.n.01", "name": "switch_engine"}, + {"id": 11498, "synset": "swivel.n.01", "name": "swivel"}, + {"id": 11499, "synset": "swivel_chair.n.01", "name": "swivel_chair"}, + {"id": 11500, "synset": "swizzle_stick.n.01", "name": "swizzle_stick"}, + {"id": 11501, "synset": "sword_cane.n.01", "name": "sword_cane"}, + {"id": 11502, "synset": "s_wrench.n.01", "name": "S_wrench"}, + {"id": 11503, "synset": "synagogue.n.01", "name": "synagogue"}, + {"id": 11504, "synset": "synchrocyclotron.n.01", "name": "synchrocyclotron"}, + {"id": 11505, "synset": "synchroflash.n.01", "name": "synchroflash"}, + {"id": 11506, "synset": "synchromesh.n.01", "name": "synchromesh"}, + {"id": 11507, "synset": "synchronous_converter.n.01", "name": "synchronous_converter"}, + {"id": 11508, "synset": "synchronous_motor.n.01", "name": "synchronous_motor"}, + {"id": 11509, "synset": "synchrotron.n.01", "name": "synchrotron"}, + {"id": 11510, "synset": "synchroscope.n.01", "name": "synchroscope"}, + {"id": 11511, "synset": "synthesizer.n.02", "name": "synthesizer"}, + {"id": 11512, "synset": "system.n.01", "name": "system"}, + {"id": 11513, "synset": "tabard.n.01", "name": "tabard"}, + {"id": 11514, "synset": "tabernacle.n.02", "name": "Tabernacle"}, + {"id": 11515, "synset": "tabi.n.01", "name": "tabi"}, + {"id": 11516, "synset": "tab_key.n.01", "name": "tab_key"}, + {"id": 11517, "synset": "table.n.03", "name": "table"}, + {"id": 11518, "synset": "tablefork.n.01", "name": "tablefork"}, + {"id": 11519, "synset": "table_knife.n.01", "name": "table_knife"}, + {"id": 11520, "synset": "table_saw.n.01", "name": "table_saw"}, + {"id": 11521, "synset": "tablespoon.n.02", "name": "tablespoon"}, + {"id": 11522, "synset": "tablet-armed_chair.n.01", "name": "tablet-armed_chair"}, + {"id": 11523, "synset": "table-tennis_racquet.n.01", "name": "table-tennis_racquet"}, + {"id": 11524, "synset": "tabletop.n.01", "name": "tabletop"}, + {"id": 11525, "synset": "tableware.n.01", "name": "tableware"}, + {"id": 11526, "synset": "tabor.n.01", "name": "tabor"}, + {"id": 11527, "synset": "taboret.n.01", "name": "taboret"}, + {"id": 11528, "synset": "tachistoscope.n.01", "name": "tachistoscope"}, + {"id": 11529, "synset": "tachograph.n.01", "name": "tachograph"}, + {"id": 11530, "synset": "tachymeter.n.01", "name": "tachymeter"}, + {"id": 11531, "synset": "tack.n.02", "name": "tack"}, + {"id": 11532, "synset": "tack_hammer.n.01", "name": "tack_hammer"}, + {"id": 11533, "synset": "taffeta.n.01", "name": "taffeta"}, + {"id": 11534, "synset": "taffrail.n.01", "name": "taffrail"}, + {"id": 11535, "synset": "tailgate.n.01", "name": "tailgate"}, + {"id": 11536, "synset": "tailor-made.n.01", "name": "tailor-made"}, + {"id": 11537, "synset": "tailor's_chalk.n.01", "name": "tailor's_chalk"}, + {"id": 11538, "synset": "tailpipe.n.01", "name": "tailpipe"}, + {"id": 11539, "synset": "tail_rotor.n.01", "name": "tail_rotor"}, + {"id": 11540, "synset": "tailstock.n.01", "name": "tailstock"}, + {"id": 11541, "synset": "take-up.n.01", "name": "take-up"}, + {"id": 11542, "synset": "talaria.n.01", "name": "talaria"}, + {"id": 11543, "synset": "talcum.n.02", "name": "talcum"}, + {"id": 11544, "synset": "tam.n.01", "name": "tam"}, + {"id": 11545, "synset": "tambour.n.02", "name": "tambour"}, + {"id": 11546, "synset": "tambour.n.01", "name": "tambour"}, + {"id": 11547, "synset": "tammy.n.01", "name": "tammy"}, + {"id": 11548, "synset": "tamp.n.01", "name": "tamp"}, + {"id": 11549, "synset": "tampax.n.01", "name": "Tampax"}, + {"id": 11550, "synset": "tampion.n.01", "name": "tampion"}, + {"id": 11551, "synset": "tampon.n.01", "name": "tampon"}, + {"id": 11552, "synset": "tandoor.n.01", "name": "tandoor"}, + {"id": 11553, "synset": "tangram.n.01", "name": "tangram"}, + {"id": 11554, "synset": "tankard.n.01", "name": "tankard"}, + {"id": 11555, "synset": "tank_car.n.01", "name": "tank_car"}, + {"id": 11556, "synset": "tank_destroyer.n.01", "name": "tank_destroyer"}, + {"id": 11557, "synset": "tank_engine.n.01", "name": "tank_engine"}, + {"id": 11558, "synset": "tanker_plane.n.01", "name": "tanker_plane"}, + {"id": 11559, "synset": "tank_shell.n.01", "name": "tank_shell"}, + {"id": 11560, "synset": "tannoy.n.01", "name": "tannoy"}, + {"id": 11561, "synset": "tap.n.06", "name": "tap"}, + {"id": 11562, "synset": "tapa.n.02", "name": "tapa"}, + {"id": 11563, "synset": "tape.n.02", "name": "tape"}, + {"id": 11564, "synset": "tape_deck.n.01", "name": "tape_deck"}, + {"id": 11565, "synset": "tape_drive.n.01", "name": "tape_drive"}, + {"id": 11566, "synset": "tape_player.n.01", "name": "tape_player"}, + {"id": 11567, "synset": "tape_recorder.n.01", "name": "tape_recorder"}, + {"id": 11568, "synset": "taper_file.n.01", "name": "taper_file"}, + {"id": 11569, "synset": "tappet.n.01", "name": "tappet"}, + {"id": 11570, "synset": "tap_wrench.n.01", "name": "tap_wrench"}, + {"id": 11571, "synset": "tare.n.05", "name": "tare"}, + {"id": 11572, "synset": "target.n.04", "name": "target"}, + {"id": 11573, "synset": "target_acquisition_system.n.01", "name": "target_acquisition_system"}, + {"id": 11574, "synset": "tarmacadam.n.02", "name": "tarmacadam"}, + {"id": 11575, "synset": "tasset.n.01", "name": "tasset"}, + {"id": 11576, "synset": "tattoo.n.02", "name": "tattoo"}, + {"id": 11577, "synset": "tavern.n.01", "name": "tavern"}, + {"id": 11578, "synset": "tawse.n.01", "name": "tawse"}, + {"id": 11579, "synset": "taximeter.n.01", "name": "taximeter"}, + {"id": 11580, "synset": "t-bar_lift.n.01", "name": "T-bar_lift"}, + {"id": 11581, "synset": "tea_bag.n.02", "name": "tea_bag"}, + {"id": 11582, "synset": "tea_ball.n.01", "name": "tea_ball"}, + {"id": 11583, "synset": "tea_cart.n.01", "name": "tea_cart"}, + {"id": 11584, "synset": "tea_chest.n.01", "name": "tea_chest"}, + {"id": 11585, "synset": "teaching_aid.n.01", "name": "teaching_aid"}, + {"id": 11586, "synset": "tea_gown.n.01", "name": "tea_gown"}, + {"id": 11587, "synset": "tea_maker.n.01", "name": "tea_maker"}, + {"id": 11588, "synset": "teashop.n.01", "name": "teashop"}, + {"id": 11589, "synset": "teaspoon.n.02", "name": "teaspoon"}, + {"id": 11590, "synset": "tea-strainer.n.01", "name": "tea-strainer"}, + {"id": 11591, "synset": "tea_table.n.01", "name": "tea_table"}, + {"id": 11592, "synset": "tea_tray.n.01", "name": "tea_tray"}, + {"id": 11593, "synset": "tea_urn.n.01", "name": "tea_urn"}, + {"id": 11594, "synset": "tee.n.03", "name": "tee"}, + {"id": 11595, "synset": "tee_hinge.n.01", "name": "tee_hinge"}, + {"id": 11596, "synset": "telecom_hotel.n.01", "name": "telecom_hotel"}, + {"id": 11597, "synset": "telecommunication_system.n.01", "name": "telecommunication_system"}, + {"id": 11598, "synset": "telegraph.n.01", "name": "telegraph"}, + {"id": 11599, "synset": "telegraph_key.n.01", "name": "telegraph_key"}, + {"id": 11600, "synset": "telemeter.n.01", "name": "telemeter"}, + {"id": 11601, "synset": "telephone_bell.n.01", "name": "telephone_bell"}, + {"id": 11602, "synset": "telephone_cord.n.01", "name": "telephone_cord"}, + {"id": 11603, "synset": "telephone_jack.n.01", "name": "telephone_jack"}, + {"id": 11604, "synset": "telephone_line.n.02", "name": "telephone_line"}, + {"id": 11605, "synset": "telephone_plug.n.01", "name": "telephone_plug"}, + {"id": 11606, "synset": "telephone_receiver.n.01", "name": "telephone_receiver"}, + {"id": 11607, "synset": "telephone_system.n.01", "name": "telephone_system"}, + {"id": 11608, "synset": "telephone_wire.n.01", "name": "telephone_wire"}, + {"id": 11609, "synset": "teleprompter.n.01", "name": "Teleprompter"}, + {"id": 11610, "synset": "telescope.n.01", "name": "telescope"}, + {"id": 11611, "synset": "telescopic_sight.n.01", "name": "telescopic_sight"}, + {"id": 11612, "synset": "telethermometer.n.01", "name": "telethermometer"}, + {"id": 11613, "synset": "teletypewriter.n.01", "name": "teletypewriter"}, + {"id": 11614, "synset": "television.n.02", "name": "television"}, + {"id": 11615, "synset": "television_antenna.n.01", "name": "television_antenna"}, + {"id": 11616, "synset": "television_equipment.n.01", "name": "television_equipment"}, + {"id": 11617, "synset": "television_monitor.n.01", "name": "television_monitor"}, + {"id": 11618, "synset": "television_room.n.01", "name": "television_room"}, + {"id": 11619, "synset": "television_transmitter.n.01", "name": "television_transmitter"}, + {"id": 11620, "synset": "telpher.n.01", "name": "telpher"}, + {"id": 11621, "synset": "telpherage.n.01", "name": "telpherage"}, + {"id": 11622, "synset": "tempera.n.01", "name": "tempera"}, + {"id": 11623, "synset": "temple.n.01", "name": "temple"}, + {"id": 11624, "synset": "temple.n.03", "name": "temple"}, + {"id": 11625, "synset": "temporary_hookup.n.01", "name": "temporary_hookup"}, + {"id": 11626, "synset": "tender.n.06", "name": "tender"}, + {"id": 11627, "synset": "tender.n.05", "name": "tender"}, + {"id": 11628, "synset": "tender.n.04", "name": "tender"}, + {"id": 11629, "synset": "tenement.n.01", "name": "tenement"}, + {"id": 11630, "synset": "tennis_camp.n.01", "name": "tennis_camp"}, + {"id": 11631, "synset": "tenon.n.01", "name": "tenon"}, + {"id": 11632, "synset": "tenor_drum.n.01", "name": "tenor_drum"}, + {"id": 11633, "synset": "tenoroon.n.01", "name": "tenoroon"}, + {"id": 11634, "synset": "tenpenny_nail.n.01", "name": "tenpenny_nail"}, + {"id": 11635, "synset": "tenpin.n.01", "name": "tenpin"}, + {"id": 11636, "synset": "tensimeter.n.01", "name": "tensimeter"}, + {"id": 11637, "synset": "tensiometer.n.03", "name": "tensiometer"}, + {"id": 11638, "synset": "tensiometer.n.02", "name": "tensiometer"}, + {"id": 11639, "synset": "tensiometer.n.01", "name": "tensiometer"}, + {"id": 11640, "synset": "tent.n.01", "name": "tent"}, + {"id": 11641, "synset": "tenter.n.01", "name": "tenter"}, + {"id": 11642, "synset": "tenterhook.n.01", "name": "tenterhook"}, + {"id": 11643, "synset": "tent-fly.n.01", "name": "tent-fly"}, + {"id": 11644, "synset": "tent_peg.n.01", "name": "tent_peg"}, + {"id": 11645, "synset": "tepee.n.01", "name": "tepee"}, + {"id": 11646, "synset": "terminal.n.02", "name": "terminal"}, + {"id": 11647, "synset": "terminal.n.04", "name": "terminal"}, + {"id": 11648, "synset": "terraced_house.n.01", "name": "terraced_house"}, + {"id": 11649, "synset": "terra_cotta.n.01", "name": "terra_cotta"}, + {"id": 11650, "synset": "terrarium.n.01", "name": "terrarium"}, + {"id": 11651, "synset": "terra_sigillata.n.01", "name": "terra_sigillata"}, + {"id": 11652, "synset": "terry.n.02", "name": "terry"}, + {"id": 11653, "synset": "tesla_coil.n.01", "name": "Tesla_coil"}, + {"id": 11654, "synset": "tessera.n.01", "name": "tessera"}, + {"id": 11655, "synset": "test_equipment.n.01", "name": "test_equipment"}, + {"id": 11656, "synset": "test_rocket.n.01", "name": "test_rocket"}, + {"id": 11657, "synset": "test_room.n.01", "name": "test_room"}, + {"id": 11658, "synset": "testudo.n.01", "name": "testudo"}, + {"id": 11659, "synset": "tetraskelion.n.01", "name": "tetraskelion"}, + {"id": 11660, "synset": "tetrode.n.01", "name": "tetrode"}, + {"id": 11661, "synset": "textile_machine.n.01", "name": "textile_machine"}, + {"id": 11662, "synset": "textile_mill.n.01", "name": "textile_mill"}, + {"id": 11663, "synset": "thatch.n.04", "name": "thatch"}, + {"id": 11664, "synset": "theater.n.01", "name": "theater"}, + {"id": 11665, "synset": "theater_curtain.n.01", "name": "theater_curtain"}, + {"id": 11666, "synset": "theater_light.n.01", "name": "theater_light"}, + {"id": 11667, "synset": "theodolite.n.01", "name": "theodolite"}, + {"id": 11668, "synset": "theremin.n.01", "name": "theremin"}, + {"id": 11669, "synset": "thermal_printer.n.01", "name": "thermal_printer"}, + {"id": 11670, "synset": "thermal_reactor.n.01", "name": "thermal_reactor"}, + {"id": 11671, "synset": "thermocouple.n.01", "name": "thermocouple"}, + { + "id": 11672, + "synset": "thermoelectric_thermometer.n.01", + "name": "thermoelectric_thermometer", + }, + {"id": 11673, "synset": "thermograph.n.02", "name": "thermograph"}, + {"id": 11674, "synset": "thermograph.n.01", "name": "thermograph"}, + {"id": 11675, "synset": "thermohydrometer.n.01", "name": "thermohydrometer"}, + {"id": 11676, "synset": "thermojunction.n.01", "name": "thermojunction"}, + {"id": 11677, "synset": "thermonuclear_reactor.n.01", "name": "thermonuclear_reactor"}, + {"id": 11678, "synset": "thermopile.n.01", "name": "thermopile"}, + {"id": 11679, "synset": "thigh_pad.n.01", "name": "thigh_pad"}, + {"id": 11680, "synset": "thill.n.01", "name": "thill"}, + {"id": 11681, "synset": "thinning_shears.n.01", "name": "thinning_shears"}, + {"id": 11682, "synset": "third_base.n.01", "name": "third_base"}, + {"id": 11683, "synset": "third_gear.n.01", "name": "third_gear"}, + {"id": 11684, "synset": "third_rail.n.01", "name": "third_rail"}, + {"id": 11685, "synset": "thong.n.03", "name": "thong"}, + {"id": 11686, "synset": "thong.n.02", "name": "thong"}, + {"id": 11687, "synset": "three-centered_arch.n.01", "name": "three-centered_arch"}, + {"id": 11688, "synset": "three-decker.n.02", "name": "three-decker"}, + {"id": 11689, "synset": "three-dimensional_radar.n.01", "name": "three-dimensional_radar"}, + {"id": 11690, "synset": "three-piece_suit.n.01", "name": "three-piece_suit"}, + {"id": 11691, "synset": "three-quarter_binding.n.01", "name": "three-quarter_binding"}, + {"id": 11692, "synset": "three-way_switch.n.01", "name": "three-way_switch"}, + {"id": 11693, "synset": "thresher.n.01", "name": "thresher"}, + {"id": 11694, "synset": "threshing_floor.n.01", "name": "threshing_floor"}, + {"id": 11695, "synset": "thriftshop.n.01", "name": "thriftshop"}, + {"id": 11696, "synset": "throat_protector.n.01", "name": "throat_protector"}, + {"id": 11697, "synset": "throne.n.01", "name": "throne"}, + {"id": 11698, "synset": "thrust_bearing.n.01", "name": "thrust_bearing"}, + {"id": 11699, "synset": "thruster.n.02", "name": "thruster"}, + {"id": 11700, "synset": "thumb.n.02", "name": "thumb"}, + {"id": 11701, "synset": "thumbhole.n.02", "name": "thumbhole"}, + {"id": 11702, "synset": "thumbscrew.n.02", "name": "thumbscrew"}, + {"id": 11703, "synset": "thumbstall.n.01", "name": "thumbstall"}, + {"id": 11704, "synset": "thunderer.n.02", "name": "thunderer"}, + {"id": 11705, "synset": "thwart.n.01", "name": "thwart"}, + {"id": 11706, "synset": "ticking.n.02", "name": "ticking"}, + {"id": 11707, "synset": "tickler_coil.n.01", "name": "tickler_coil"}, + {"id": 11708, "synset": "tie.n.04", "name": "tie"}, + {"id": 11709, "synset": "tie.n.08", "name": "tie"}, + {"id": 11710, "synset": "tie_rack.n.01", "name": "tie_rack"}, + {"id": 11711, "synset": "tie_rod.n.01", "name": "tie_rod"}, + {"id": 11712, "synset": "tile.n.01", "name": "tile"}, + {"id": 11713, "synset": "tile_cutter.n.01", "name": "tile_cutter"}, + {"id": 11714, "synset": "tile_roof.n.01", "name": "tile_roof"}, + {"id": 11715, "synset": "tiller.n.03", "name": "tiller"}, + {"id": 11716, "synset": "tilter.n.02", "name": "tilter"}, + {"id": 11717, "synset": "tilt-top_table.n.01", "name": "tilt-top_table"}, + {"id": 11718, "synset": "timber.n.02", "name": "timber"}, + {"id": 11719, "synset": "timber.n.03", "name": "timber"}, + {"id": 11720, "synset": "timber_hitch.n.01", "name": "timber_hitch"}, + {"id": 11721, "synset": "timbrel.n.01", "name": "timbrel"}, + {"id": 11722, "synset": "time_bomb.n.02", "name": "time_bomb"}, + {"id": 11723, "synset": "time_capsule.n.01", "name": "time_capsule"}, + {"id": 11724, "synset": "time_clock.n.01", "name": "time_clock"}, + { + "id": 11725, + "synset": "time-delay_measuring_instrument.n.01", + "name": "time-delay_measuring_instrument", + }, + {"id": 11726, "synset": "time-fuse.n.01", "name": "time-fuse"}, + {"id": 11727, "synset": "timepiece.n.01", "name": "timepiece"}, + {"id": 11728, "synset": "timer.n.03", "name": "timer"}, + {"id": 11729, "synset": "time-switch.n.01", "name": "time-switch"}, + {"id": 11730, "synset": "tin.n.02", "name": "tin"}, + {"id": 11731, "synset": "tinderbox.n.02", "name": "tinderbox"}, + {"id": 11732, "synset": "tine.n.01", "name": "tine"}, + {"id": 11733, "synset": "tippet.n.01", "name": "tippet"}, + {"id": 11734, "synset": "tire_chain.n.01", "name": "tire_chain"}, + {"id": 11735, "synset": "tire_iron.n.01", "name": "tire_iron"}, + {"id": 11736, "synset": "titfer.n.01", "name": "titfer"}, + {"id": 11737, "synset": "tithe_barn.n.01", "name": "tithe_barn"}, + {"id": 11738, "synset": "titrator.n.01", "name": "titrator"}, + {"id": 11739, "synset": "toasting_fork.n.01", "name": "toasting_fork"}, + {"id": 11740, "synset": "toastrack.n.01", "name": "toastrack"}, + {"id": 11741, "synset": "tobacco_pouch.n.01", "name": "tobacco_pouch"}, + {"id": 11742, "synset": "tobacco_shop.n.01", "name": "tobacco_shop"}, + {"id": 11743, "synset": "toboggan.n.01", "name": "toboggan"}, + {"id": 11744, "synset": "toby.n.01", "name": "toby"}, + {"id": 11745, "synset": "tocsin.n.02", "name": "tocsin"}, + {"id": 11746, "synset": "toe.n.02", "name": "toe"}, + {"id": 11747, "synset": "toecap.n.01", "name": "toecap"}, + {"id": 11748, "synset": "toehold.n.02", "name": "toehold"}, + {"id": 11749, "synset": "toga.n.01", "name": "toga"}, + {"id": 11750, "synset": "toga_virilis.n.01", "name": "toga_virilis"}, + {"id": 11751, "synset": "toggle.n.03", "name": "toggle"}, + {"id": 11752, "synset": "toggle_bolt.n.01", "name": "toggle_bolt"}, + {"id": 11753, "synset": "toggle_joint.n.01", "name": "toggle_joint"}, + {"id": 11754, "synset": "toggle_switch.n.01", "name": "toggle_switch"}, + {"id": 11755, "synset": "togs.n.01", "name": "togs"}, + {"id": 11756, "synset": "toilet.n.01", "name": "toilet"}, + {"id": 11757, "synset": "toilet_bag.n.01", "name": "toilet_bag"}, + {"id": 11758, "synset": "toilet_bowl.n.01", "name": "toilet_bowl"}, + {"id": 11759, "synset": "toilet_kit.n.01", "name": "toilet_kit"}, + {"id": 11760, "synset": "toilet_powder.n.01", "name": "toilet_powder"}, + {"id": 11761, "synset": "toiletry.n.01", "name": "toiletry"}, + {"id": 11762, "synset": "toilet_seat.n.01", "name": "toilet_seat"}, + {"id": 11763, "synset": "toilet_water.n.01", "name": "toilet_water"}, + {"id": 11764, "synset": "tokamak.n.01", "name": "tokamak"}, + {"id": 11765, "synset": "token.n.03", "name": "token"}, + {"id": 11766, "synset": "tollbooth.n.01", "name": "tollbooth"}, + {"id": 11767, "synset": "toll_bridge.n.01", "name": "toll_bridge"}, + {"id": 11768, "synset": "tollgate.n.01", "name": "tollgate"}, + {"id": 11769, "synset": "toll_line.n.01", "name": "toll_line"}, + {"id": 11770, "synset": "tomahawk.n.01", "name": "tomahawk"}, + {"id": 11771, "synset": "tommy_gun.n.01", "name": "Tommy_gun"}, + {"id": 11772, "synset": "tomograph.n.01", "name": "tomograph"}, + {"id": 11773, "synset": "tone_arm.n.01", "name": "tone_arm"}, + {"id": 11774, "synset": "toner.n.03", "name": "toner"}, + {"id": 11775, "synset": "tongue.n.07", "name": "tongue"}, + {"id": 11776, "synset": "tongue_and_groove_joint.n.01", "name": "tongue_and_groove_joint"}, + {"id": 11777, "synset": "tongue_depressor.n.01", "name": "tongue_depressor"}, + {"id": 11778, "synset": "tonometer.n.01", "name": "tonometer"}, + {"id": 11779, "synset": "tool.n.01", "name": "tool"}, + {"id": 11780, "synset": "tool_bag.n.01", "name": "tool_bag"}, + {"id": 11781, "synset": "toolshed.n.01", "name": "toolshed"}, + {"id": 11782, "synset": "tooth.n.02", "name": "tooth"}, + {"id": 11783, "synset": "tooth.n.05", "name": "tooth"}, + {"id": 11784, "synset": "top.n.10", "name": "top"}, + {"id": 11785, "synset": "topgallant.n.02", "name": "topgallant"}, + {"id": 11786, "synset": "topgallant.n.01", "name": "topgallant"}, + {"id": 11787, "synset": "topiary.n.01", "name": "topiary"}, + {"id": 11788, "synset": "topknot.n.01", "name": "topknot"}, + {"id": 11789, "synset": "topmast.n.01", "name": "topmast"}, + {"id": 11790, "synset": "topper.n.05", "name": "topper"}, + {"id": 11791, "synset": "topsail.n.01", "name": "topsail"}, + {"id": 11792, "synset": "toque.n.01", "name": "toque"}, + {"id": 11793, "synset": "torch.n.01", "name": "torch"}, + {"id": 11794, "synset": "torpedo.n.06", "name": "torpedo"}, + {"id": 11795, "synset": "torpedo.n.05", "name": "torpedo"}, + {"id": 11796, "synset": "torpedo.n.03", "name": "torpedo"}, + {"id": 11797, "synset": "torpedo_boat.n.01", "name": "torpedo_boat"}, + {"id": 11798, "synset": "torpedo-boat_destroyer.n.01", "name": "torpedo-boat_destroyer"}, + {"id": 11799, "synset": "torpedo_tube.n.01", "name": "torpedo_tube"}, + {"id": 11800, "synset": "torque_converter.n.01", "name": "torque_converter"}, + {"id": 11801, "synset": "torque_wrench.n.01", "name": "torque_wrench"}, + {"id": 11802, "synset": "torture_chamber.n.01", "name": "torture_chamber"}, + {"id": 11803, "synset": "totem_pole.n.01", "name": "totem_pole"}, + {"id": 11804, "synset": "touch_screen.n.01", "name": "touch_screen"}, + {"id": 11805, "synset": "toupee.n.01", "name": "toupee"}, + {"id": 11806, "synset": "touring_car.n.01", "name": "touring_car"}, + {"id": 11807, "synset": "tourist_class.n.01", "name": "tourist_class"}, + {"id": 11808, "synset": "toweling.n.01", "name": "toweling"}, + {"id": 11809, "synset": "towel_rail.n.01", "name": "towel_rail"}, + {"id": 11810, "synset": "tower.n.01", "name": "tower"}, + {"id": 11811, "synset": "town_hall.n.01", "name": "town_hall"}, + {"id": 11812, "synset": "towpath.n.01", "name": "towpath"}, + {"id": 11813, "synset": "toy_box.n.01", "name": "toy_box"}, + {"id": 11814, "synset": "toyshop.n.01", "name": "toyshop"}, + {"id": 11815, "synset": "trace_detector.n.01", "name": "trace_detector"}, + {"id": 11816, "synset": "track.n.09", "name": "track"}, + {"id": 11817, "synset": "track.n.08", "name": "track"}, + {"id": 11818, "synset": "trackball.n.01", "name": "trackball"}, + {"id": 11819, "synset": "tracked_vehicle.n.01", "name": "tracked_vehicle"}, + {"id": 11820, "synset": "tract_house.n.01", "name": "tract_house"}, + {"id": 11821, "synset": "tract_housing.n.01", "name": "tract_housing"}, + {"id": 11822, "synset": "traction_engine.n.01", "name": "traction_engine"}, + {"id": 11823, "synset": "tractor.n.02", "name": "tractor"}, + {"id": 11824, "synset": "trailer.n.04", "name": "trailer"}, + {"id": 11825, "synset": "trailer.n.03", "name": "trailer"}, + {"id": 11826, "synset": "trailer_camp.n.01", "name": "trailer_camp"}, + {"id": 11827, "synset": "trailing_edge.n.01", "name": "trailing_edge"}, + {"id": 11828, "synset": "tramline.n.01", "name": "tramline"}, + {"id": 11829, "synset": "trammel.n.02", "name": "trammel"}, + {"id": 11830, "synset": "tramp_steamer.n.01", "name": "tramp_steamer"}, + {"id": 11831, "synset": "tramway.n.01", "name": "tramway"}, + {"id": 11832, "synset": "transdermal_patch.n.01", "name": "transdermal_patch"}, + {"id": 11833, "synset": "transept.n.01", "name": "transept"}, + {"id": 11834, "synset": "transformer.n.01", "name": "transformer"}, + {"id": 11835, "synset": "transistor.n.01", "name": "transistor"}, + {"id": 11836, "synset": "transit_instrument.n.01", "name": "transit_instrument"}, + {"id": 11837, "synset": "transmission.n.05", "name": "transmission"}, + {"id": 11838, "synset": "transmission_shaft.n.01", "name": "transmission_shaft"}, + {"id": 11839, "synset": "transmitter.n.03", "name": "transmitter"}, + {"id": 11840, "synset": "transom.n.02", "name": "transom"}, + {"id": 11841, "synset": "transom.n.01", "name": "transom"}, + {"id": 11842, "synset": "transponder.n.01", "name": "transponder"}, + {"id": 11843, "synset": "transporter.n.02", "name": "transporter"}, + {"id": 11844, "synset": "transporter.n.01", "name": "transporter"}, + {"id": 11845, "synset": "transport_ship.n.01", "name": "transport_ship"}, + {"id": 11846, "synset": "trap.n.01", "name": "trap"}, + {"id": 11847, "synset": "trap_door.n.01", "name": "trap_door"}, + {"id": 11848, "synset": "trapeze.n.01", "name": "trapeze"}, + {"id": 11849, "synset": "trave.n.01", "name": "trave"}, + {"id": 11850, "synset": "travel_iron.n.01", "name": "travel_iron"}, + {"id": 11851, "synset": "trawl.n.02", "name": "trawl"}, + {"id": 11852, "synset": "trawl.n.01", "name": "trawl"}, + {"id": 11853, "synset": "trawler.n.02", "name": "trawler"}, + {"id": 11854, "synset": "tray_cloth.n.01", "name": "tray_cloth"}, + {"id": 11855, "synset": "tread.n.04", "name": "tread"}, + {"id": 11856, "synset": "tread.n.03", "name": "tread"}, + {"id": 11857, "synset": "treadmill.n.02", "name": "treadmill"}, + {"id": 11858, "synset": "treadmill.n.01", "name": "treadmill"}, + {"id": 11859, "synset": "treasure_chest.n.01", "name": "treasure_chest"}, + {"id": 11860, "synset": "treasure_ship.n.01", "name": "treasure_ship"}, + {"id": 11861, "synset": "treenail.n.01", "name": "treenail"}, + {"id": 11862, "synset": "trefoil_arch.n.01", "name": "trefoil_arch"}, + {"id": 11863, "synset": "trellis.n.01", "name": "trellis"}, + {"id": 11864, "synset": "trench.n.01", "name": "trench"}, + {"id": 11865, "synset": "trench_knife.n.01", "name": "trench_knife"}, + {"id": 11866, "synset": "trepan.n.02", "name": "trepan"}, + {"id": 11867, "synset": "trepan.n.01", "name": "trepan"}, + {"id": 11868, "synset": "trestle.n.02", "name": "trestle"}, + {"id": 11869, "synset": "trestle.n.01", "name": "trestle"}, + {"id": 11870, "synset": "trestle_bridge.n.01", "name": "trestle_bridge"}, + {"id": 11871, "synset": "trestle_table.n.01", "name": "trestle_table"}, + {"id": 11872, "synset": "trestlework.n.01", "name": "trestlework"}, + {"id": 11873, "synset": "trews.n.01", "name": "trews"}, + {"id": 11874, "synset": "trial_balloon.n.02", "name": "trial_balloon"}, + {"id": 11875, "synset": "triangle.n.04", "name": "triangle"}, + {"id": 11876, "synset": "triclinium.n.02", "name": "triclinium"}, + {"id": 11877, "synset": "triclinium.n.01", "name": "triclinium"}, + {"id": 11878, "synset": "tricorn.n.01", "name": "tricorn"}, + {"id": 11879, "synset": "tricot.n.01", "name": "tricot"}, + {"id": 11880, "synset": "trident.n.01", "name": "trident"}, + {"id": 11881, "synset": "trigger.n.02", "name": "trigger"}, + {"id": 11882, "synset": "trimaran.n.01", "name": "trimaran"}, + {"id": 11883, "synset": "trimmer.n.02", "name": "trimmer"}, + {"id": 11884, "synset": "trimmer_arch.n.01", "name": "trimmer_arch"}, + {"id": 11885, "synset": "triode.n.01", "name": "triode"}, + {"id": 11886, "synset": "triptych.n.01", "name": "triptych"}, + {"id": 11887, "synset": "trip_wire.n.02", "name": "trip_wire"}, + {"id": 11888, "synset": "trireme.n.01", "name": "trireme"}, + {"id": 11889, "synset": "triskelion.n.01", "name": "triskelion"}, + {"id": 11890, "synset": "triumphal_arch.n.01", "name": "triumphal_arch"}, + {"id": 11891, "synset": "trivet.n.02", "name": "trivet"}, + {"id": 11892, "synset": "trivet.n.01", "name": "trivet"}, + {"id": 11893, "synset": "troika.n.01", "name": "troika"}, + {"id": 11894, "synset": "troll.n.03", "name": "troll"}, + {"id": 11895, "synset": "trolleybus.n.01", "name": "trolleybus"}, + {"id": 11896, "synset": "trombone.n.01", "name": "trombone"}, + {"id": 11897, "synset": "troop_carrier.n.01", "name": "troop_carrier"}, + {"id": 11898, "synset": "troopship.n.01", "name": "troopship"}, + {"id": 11899, "synset": "trophy_case.n.01", "name": "trophy_case"}, + {"id": 11900, "synset": "trough.n.05", "name": "trough"}, + {"id": 11901, "synset": "trouser.n.02", "name": "trouser"}, + {"id": 11902, "synset": "trouser_cuff.n.01", "name": "trouser_cuff"}, + {"id": 11903, "synset": "trouser_press.n.01", "name": "trouser_press"}, + {"id": 11904, "synset": "trousseau.n.01", "name": "trousseau"}, + {"id": 11905, "synset": "trowel.n.01", "name": "trowel"}, + {"id": 11906, "synset": "trumpet_arch.n.01", "name": "trumpet_arch"}, + {"id": 11907, "synset": "truncheon.n.01", "name": "truncheon"}, + {"id": 11908, "synset": "trundle_bed.n.01", "name": "trundle_bed"}, + {"id": 11909, "synset": "trunk_hose.n.01", "name": "trunk_hose"}, + {"id": 11910, "synset": "trunk_lid.n.01", "name": "trunk_lid"}, + {"id": 11911, "synset": "trunk_line.n.02", "name": "trunk_line"}, + {"id": 11912, "synset": "truss.n.02", "name": "truss"}, + {"id": 11913, "synset": "truss_bridge.n.01", "name": "truss_bridge"}, + {"id": 11914, "synset": "try_square.n.01", "name": "try_square"}, + {"id": 11915, "synset": "t-square.n.01", "name": "T-square"}, + {"id": 11916, "synset": "tube.n.02", "name": "tube"}, + {"id": 11917, "synset": "tuck_box.n.01", "name": "tuck_box"}, + {"id": 11918, "synset": "tucker.n.04", "name": "tucker"}, + {"id": 11919, "synset": "tucker-bag.n.01", "name": "tucker-bag"}, + {"id": 11920, "synset": "tuck_shop.n.01", "name": "tuck_shop"}, + {"id": 11921, "synset": "tudor_arch.n.01", "name": "Tudor_arch"}, + {"id": 11922, "synset": "tudung.n.01", "name": "tudung"}, + {"id": 11923, "synset": "tugboat.n.01", "name": "tugboat"}, + {"id": 11924, "synset": "tulle.n.01", "name": "tulle"}, + {"id": 11925, "synset": "tumble-dryer.n.01", "name": "tumble-dryer"}, + {"id": 11926, "synset": "tumbler.n.02", "name": "tumbler"}, + {"id": 11927, "synset": "tumbrel.n.01", "name": "tumbrel"}, + {"id": 11928, "synset": "tun.n.01", "name": "tun"}, + {"id": 11929, "synset": "tunic.n.02", "name": "tunic"}, + {"id": 11930, "synset": "tuning_fork.n.01", "name": "tuning_fork"}, + {"id": 11931, "synset": "tupik.n.01", "name": "tupik"}, + {"id": 11932, "synset": "turbine.n.01", "name": "turbine"}, + {"id": 11933, "synset": "turbogenerator.n.01", "name": "turbogenerator"}, + {"id": 11934, "synset": "tureen.n.01", "name": "tureen"}, + {"id": 11935, "synset": "turkish_bath.n.01", "name": "Turkish_bath"}, + {"id": 11936, "synset": "turkish_towel.n.01", "name": "Turkish_towel"}, + {"id": 11937, "synset": "turk's_head.n.01", "name": "Turk's_head"}, + {"id": 11938, "synset": "turnbuckle.n.01", "name": "turnbuckle"}, + {"id": 11939, "synset": "turner.n.08", "name": "turner"}, + {"id": 11940, "synset": "turnery.n.01", "name": "turnery"}, + {"id": 11941, "synset": "turnpike.n.01", "name": "turnpike"}, + {"id": 11942, "synset": "turnspit.n.01", "name": "turnspit"}, + {"id": 11943, "synset": "turnstile.n.01", "name": "turnstile"}, + {"id": 11944, "synset": "turntable.n.01", "name": "turntable"}, + {"id": 11945, "synset": "turntable.n.02", "name": "turntable"}, + {"id": 11946, "synset": "turret.n.01", "name": "turret"}, + {"id": 11947, "synset": "turret_clock.n.01", "name": "turret_clock"}, + {"id": 11948, "synset": "tweed.n.01", "name": "tweed"}, + {"id": 11949, "synset": "tweeter.n.01", "name": "tweeter"}, + {"id": 11950, "synset": "twenty-two.n.02", "name": "twenty-two"}, + {"id": 11951, "synset": "twenty-two_pistol.n.01", "name": "twenty-two_pistol"}, + {"id": 11952, "synset": "twenty-two_rifle.n.01", "name": "twenty-two_rifle"}, + {"id": 11953, "synset": "twill.n.02", "name": "twill"}, + {"id": 11954, "synset": "twill.n.01", "name": "twill"}, + {"id": 11955, "synset": "twin_bed.n.01", "name": "twin_bed"}, + {"id": 11956, "synset": "twinjet.n.01", "name": "twinjet"}, + {"id": 11957, "synset": "twist_bit.n.01", "name": "twist_bit"}, + {"id": 11958, "synset": "two-by-four.n.01", "name": "two-by-four"}, + {"id": 11959, "synset": "two-man_tent.n.01", "name": "two-man_tent"}, + {"id": 11960, "synset": "two-piece.n.01", "name": "two-piece"}, + {"id": 11961, "synset": "typesetting_machine.n.01", "name": "typesetting_machine"}, + {"id": 11962, "synset": "typewriter_carriage.n.01", "name": "typewriter_carriage"}, + {"id": 11963, "synset": "typewriter_keyboard.n.01", "name": "typewriter_keyboard"}, + {"id": 11964, "synset": "tyrolean.n.02", "name": "tyrolean"}, + {"id": 11965, "synset": "uke.n.01", "name": "uke"}, + {"id": 11966, "synset": "ulster.n.02", "name": "ulster"}, + {"id": 11967, "synset": "ultracentrifuge.n.01", "name": "ultracentrifuge"}, + {"id": 11968, "synset": "ultramicroscope.n.01", "name": "ultramicroscope"}, + {"id": 11969, "synset": "ultrasuede.n.01", "name": "Ultrasuede"}, + {"id": 11970, "synset": "ultraviolet_lamp.n.01", "name": "ultraviolet_lamp"}, + {"id": 11971, "synset": "umbrella_tent.n.01", "name": "umbrella_tent"}, + {"id": 11972, "synset": "undercarriage.n.01", "name": "undercarriage"}, + {"id": 11973, "synset": "undercoat.n.01", "name": "undercoat"}, + {"id": 11974, "synset": "undergarment.n.01", "name": "undergarment"}, + {"id": 11975, "synset": "underpants.n.01", "name": "underpants"}, + {"id": 11976, "synset": "undies.n.01", "name": "undies"}, + {"id": 11977, "synset": "uneven_parallel_bars.n.01", "name": "uneven_parallel_bars"}, + {"id": 11978, "synset": "uniform.n.01", "name": "uniform"}, + {"id": 11979, "synset": "universal_joint.n.01", "name": "universal_joint"}, + {"id": 11980, "synset": "university.n.02", "name": "university"}, + {"id": 11981, "synset": "upholstery.n.01", "name": "upholstery"}, + {"id": 11982, "synset": "upholstery_material.n.01", "name": "upholstery_material"}, + {"id": 11983, "synset": "upholstery_needle.n.01", "name": "upholstery_needle"}, + {"id": 11984, "synset": "uplift.n.02", "name": "uplift"}, + {"id": 11985, "synset": "upper_berth.n.01", "name": "upper_berth"}, + {"id": 11986, "synset": "upright.n.02", "name": "upright"}, + {"id": 11987, "synset": "upset.n.04", "name": "upset"}, + {"id": 11988, "synset": "upstairs.n.01", "name": "upstairs"}, + {"id": 11989, "synset": "urceole.n.01", "name": "urceole"}, + {"id": 11990, "synset": "urn.n.02", "name": "urn"}, + {"id": 11991, "synset": "used-car.n.01", "name": "used-car"}, + {"id": 11992, "synset": "utensil.n.01", "name": "utensil"}, + {"id": 11993, "synset": "uzi.n.01", "name": "Uzi"}, + {"id": 11994, "synset": "vacation_home.n.01", "name": "vacation_home"}, + {"id": 11995, "synset": "vacuum_chamber.n.01", "name": "vacuum_chamber"}, + {"id": 11996, "synset": "vacuum_flask.n.01", "name": "vacuum_flask"}, + {"id": 11997, "synset": "vacuum_gauge.n.01", "name": "vacuum_gauge"}, + {"id": 11998, "synset": "valenciennes.n.02", "name": "Valenciennes"}, + {"id": 11999, "synset": "valise.n.01", "name": "valise"}, + {"id": 12000, "synset": "valve.n.03", "name": "valve"}, + {"id": 12001, "synset": "valve.n.02", "name": "valve"}, + {"id": 12002, "synset": "valve-in-head_engine.n.01", "name": "valve-in-head_engine"}, + {"id": 12003, "synset": "vambrace.n.01", "name": "vambrace"}, + {"id": 12004, "synset": "van.n.05", "name": "van"}, + {"id": 12005, "synset": "van.n.04", "name": "van"}, + {"id": 12006, "synset": "vane.n.02", "name": "vane"}, + {"id": 12007, "synset": "vaporizer.n.01", "name": "vaporizer"}, + {"id": 12008, "synset": "variable-pitch_propeller.n.01", "name": "variable-pitch_propeller"}, + {"id": 12009, "synset": "variometer.n.01", "name": "variometer"}, + {"id": 12010, "synset": "varnish.n.01", "name": "varnish"}, + {"id": 12011, "synset": "vault.n.03", "name": "vault"}, + {"id": 12012, "synset": "vault.n.02", "name": "vault"}, + {"id": 12013, "synset": "vaulting_horse.n.01", "name": "vaulting_horse"}, + {"id": 12014, "synset": "vehicle.n.01", "name": "vehicle"}, + {"id": 12015, "synset": "velcro.n.01", "name": "Velcro"}, + {"id": 12016, "synset": "velocipede.n.01", "name": "velocipede"}, + {"id": 12017, "synset": "velour.n.01", "name": "velour"}, + {"id": 12018, "synset": "velvet.n.01", "name": "velvet"}, + {"id": 12019, "synset": "velveteen.n.01", "name": "velveteen"}, + {"id": 12020, "synset": "veneer.n.01", "name": "veneer"}, + {"id": 12021, "synset": "venetian_blind.n.01", "name": "Venetian_blind"}, + {"id": 12022, "synset": "venn_diagram.n.01", "name": "Venn_diagram"}, + {"id": 12023, "synset": "ventilation.n.02", "name": "ventilation"}, + {"id": 12024, "synset": "ventilation_shaft.n.01", "name": "ventilation_shaft"}, + {"id": 12025, "synset": "ventilator.n.01", "name": "ventilator"}, + {"id": 12026, "synset": "veranda.n.01", "name": "veranda"}, + {"id": 12027, "synset": "verdigris.n.02", "name": "verdigris"}, + {"id": 12028, "synset": "vernier_caliper.n.01", "name": "vernier_caliper"}, + {"id": 12029, "synset": "vernier_scale.n.01", "name": "vernier_scale"}, + {"id": 12030, "synset": "vertical_file.n.01", "name": "vertical_file"}, + {"id": 12031, "synset": "vertical_stabilizer.n.01", "name": "vertical_stabilizer"}, + {"id": 12032, "synset": "vertical_tail.n.01", "name": "vertical_tail"}, + {"id": 12033, "synset": "very_pistol.n.01", "name": "Very_pistol"}, + {"id": 12034, "synset": "vessel.n.02", "name": "vessel"}, + {"id": 12035, "synset": "vessel.n.03", "name": "vessel"}, + {"id": 12036, "synset": "vestiture.n.01", "name": "vestiture"}, + {"id": 12037, "synset": "vestment.n.01", "name": "vestment"}, + {"id": 12038, "synset": "vest_pocket.n.01", "name": "vest_pocket"}, + {"id": 12039, "synset": "vestry.n.02", "name": "vestry"}, + {"id": 12040, "synset": "viaduct.n.01", "name": "viaduct"}, + {"id": 12041, "synset": "vibraphone.n.01", "name": "vibraphone"}, + {"id": 12042, "synset": "vibrator.n.02", "name": "vibrator"}, + {"id": 12043, "synset": "vibrator.n.01", "name": "vibrator"}, + {"id": 12044, "synset": "victrola.n.01", "name": "Victrola"}, + {"id": 12045, "synset": "vicuna.n.02", "name": "vicuna"}, + {"id": 12046, "synset": "videocassette.n.01", "name": "videocassette"}, + {"id": 12047, "synset": "videocassette_recorder.n.01", "name": "videocassette_recorder"}, + {"id": 12048, "synset": "videodisk.n.01", "name": "videodisk"}, + {"id": 12049, "synset": "video_recording.n.01", "name": "video_recording"}, + {"id": 12050, "synset": "videotape.n.02", "name": "videotape"}, + {"id": 12051, "synset": "vigil_light.n.01", "name": "vigil_light"}, + {"id": 12052, "synset": "villa.n.04", "name": "villa"}, + {"id": 12053, "synset": "villa.n.03", "name": "villa"}, + {"id": 12054, "synset": "villa.n.02", "name": "villa"}, + {"id": 12055, "synset": "viol.n.01", "name": "viol"}, + {"id": 12056, "synset": "viola.n.03", "name": "viola"}, + {"id": 12057, "synset": "viola_da_braccio.n.01", "name": "viola_da_braccio"}, + {"id": 12058, "synset": "viola_da_gamba.n.01", "name": "viola_da_gamba"}, + {"id": 12059, "synset": "viola_d'amore.n.01", "name": "viola_d'amore"}, + {"id": 12060, "synset": "virginal.n.01", "name": "virginal"}, + {"id": 12061, "synset": "viscometer.n.01", "name": "viscometer"}, + {"id": 12062, "synset": "viscose_rayon.n.01", "name": "viscose_rayon"}, + {"id": 12063, "synset": "vise.n.01", "name": "vise"}, + {"id": 12064, "synset": "visor.n.01", "name": "visor"}, + {"id": 12065, "synset": "visual_display_unit.n.01", "name": "visual_display_unit"}, + {"id": 12066, "synset": "vivarium.n.01", "name": "vivarium"}, + {"id": 12067, "synset": "viyella.n.01", "name": "Viyella"}, + {"id": 12068, "synset": "voile.n.01", "name": "voile"}, + {"id": 12069, "synset": "volleyball_net.n.01", "name": "volleyball_net"}, + {"id": 12070, "synset": "voltage_regulator.n.01", "name": "voltage_regulator"}, + {"id": 12071, "synset": "voltaic_cell.n.01", "name": "voltaic_cell"}, + {"id": 12072, "synset": "voltaic_pile.n.01", "name": "voltaic_pile"}, + {"id": 12073, "synset": "voltmeter.n.01", "name": "voltmeter"}, + {"id": 12074, "synset": "vomitory.n.01", "name": "vomitory"}, + {"id": 12075, "synset": "von_neumann_machine.n.01", "name": "von_Neumann_machine"}, + {"id": 12076, "synset": "voting_booth.n.01", "name": "voting_booth"}, + {"id": 12077, "synset": "voting_machine.n.01", "name": "voting_machine"}, + {"id": 12078, "synset": "voussoir.n.01", "name": "voussoir"}, + {"id": 12079, "synset": "vox_angelica.n.01", "name": "vox_angelica"}, + {"id": 12080, "synset": "vox_humana.n.01", "name": "vox_humana"}, + {"id": 12081, "synset": "waders.n.01", "name": "waders"}, + {"id": 12082, "synset": "wading_pool.n.01", "name": "wading_pool"}, + {"id": 12083, "synset": "wagon.n.04", "name": "wagon"}, + {"id": 12084, "synset": "wagon_tire.n.01", "name": "wagon_tire"}, + {"id": 12085, "synset": "wain.n.03", "name": "wain"}, + {"id": 12086, "synset": "wainscot.n.02", "name": "wainscot"}, + {"id": 12087, "synset": "wainscoting.n.01", "name": "wainscoting"}, + {"id": 12088, "synset": "waist_pack.n.01", "name": "waist_pack"}, + {"id": 12089, "synset": "walker.n.06", "name": "walker"}, + {"id": 12090, "synset": "walker.n.05", "name": "walker"}, + {"id": 12091, "synset": "walker.n.04", "name": "walker"}, + {"id": 12092, "synset": "walkie-talkie.n.01", "name": "walkie-talkie"}, + {"id": 12093, "synset": "walk-in.n.04", "name": "walk-in"}, + {"id": 12094, "synset": "walking_shoe.n.01", "name": "walking_shoe"}, + {"id": 12095, "synset": "walkman.n.01", "name": "Walkman"}, + {"id": 12096, "synset": "walk-up_apartment.n.01", "name": "walk-up_apartment"}, + {"id": 12097, "synset": "wall.n.01", "name": "wall"}, + {"id": 12098, "synset": "wall.n.07", "name": "wall"}, + {"id": 12099, "synset": "wall_tent.n.01", "name": "wall_tent"}, + {"id": 12100, "synset": "wall_unit.n.01", "name": "wall_unit"}, + {"id": 12101, "synset": "wand.n.01", "name": "wand"}, + {"id": 12102, "synset": "wankel_engine.n.01", "name": "Wankel_engine"}, + {"id": 12103, "synset": "ward.n.03", "name": "ward"}, + {"id": 12104, "synset": "wardroom.n.01", "name": "wardroom"}, + {"id": 12105, "synset": "warehouse.n.01", "name": "warehouse"}, + {"id": 12106, "synset": "warming_pan.n.01", "name": "warming_pan"}, + {"id": 12107, "synset": "war_paint.n.02", "name": "war_paint"}, + {"id": 12108, "synset": "warplane.n.01", "name": "warplane"}, + {"id": 12109, "synset": "war_room.n.01", "name": "war_room"}, + {"id": 12110, "synset": "warship.n.01", "name": "warship"}, + {"id": 12111, "synset": "wash.n.01", "name": "wash"}, + {"id": 12112, "synset": "wash-and-wear.n.01", "name": "wash-and-wear"}, + {"id": 12113, "synset": "washbasin.n.02", "name": "washbasin"}, + {"id": 12114, "synset": "washboard.n.02", "name": "washboard"}, + {"id": 12115, "synset": "washboard.n.01", "name": "washboard"}, + {"id": 12116, "synset": "washer.n.02", "name": "washer"}, + {"id": 12117, "synset": "washhouse.n.01", "name": "washhouse"}, + {"id": 12118, "synset": "washroom.n.01", "name": "washroom"}, + {"id": 12119, "synset": "washstand.n.01", "name": "washstand"}, + {"id": 12120, "synset": "washtub.n.01", "name": "washtub"}, + {"id": 12121, "synset": "wastepaper_basket.n.01", "name": "wastepaper_basket"}, + {"id": 12122, "synset": "watch_cap.n.01", "name": "watch_cap"}, + {"id": 12123, "synset": "watch_case.n.01", "name": "watch_case"}, + {"id": 12124, "synset": "watch_glass.n.01", "name": "watch_glass"}, + {"id": 12125, "synset": "watchtower.n.01", "name": "watchtower"}, + {"id": 12126, "synset": "water-base_paint.n.01", "name": "water-base_paint"}, + {"id": 12127, "synset": "water_bed.n.01", "name": "water_bed"}, + {"id": 12128, "synset": "water_butt.n.01", "name": "water_butt"}, + {"id": 12129, "synset": "water_cart.n.01", "name": "water_cart"}, + {"id": 12130, "synset": "water_chute.n.01", "name": "water_chute"}, + {"id": 12131, "synset": "water_closet.n.01", "name": "water_closet"}, + {"id": 12132, "synset": "watercolor.n.02", "name": "watercolor"}, + {"id": 12133, "synset": "water-cooled_reactor.n.01", "name": "water-cooled_reactor"}, + {"id": 12134, "synset": "water_filter.n.01", "name": "water_filter"}, + {"id": 12135, "synset": "water_gauge.n.01", "name": "water_gauge"}, + {"id": 12136, "synset": "water_glass.n.02", "name": "water_glass"}, + {"id": 12137, "synset": "water_hazard.n.01", "name": "water_hazard"}, + {"id": 12138, "synset": "watering_cart.n.01", "name": "watering_cart"}, + {"id": 12139, "synset": "water_jacket.n.01", "name": "water_jacket"}, + {"id": 12140, "synset": "water_jump.n.01", "name": "water_jump"}, + {"id": 12141, "synset": "water_level.n.04", "name": "water_level"}, + {"id": 12142, "synset": "water_meter.n.01", "name": "water_meter"}, + {"id": 12143, "synset": "water_mill.n.01", "name": "water_mill"}, + {"id": 12144, "synset": "waterproof.n.01", "name": "waterproof"}, + {"id": 12145, "synset": "waterproofing.n.02", "name": "waterproofing"}, + {"id": 12146, "synset": "water_pump.n.01", "name": "water_pump"}, + {"id": 12147, "synset": "waterspout.n.03", "name": "waterspout"}, + {"id": 12148, "synset": "water_wagon.n.01", "name": "water_wagon"}, + {"id": 12149, "synset": "waterwheel.n.02", "name": "waterwheel"}, + {"id": 12150, "synset": "waterwheel.n.01", "name": "waterwheel"}, + {"id": 12151, "synset": "water_wings.n.01", "name": "water_wings"}, + {"id": 12152, "synset": "waterworks.n.02", "name": "waterworks"}, + {"id": 12153, "synset": "wattmeter.n.01", "name": "wattmeter"}, + {"id": 12154, "synset": "waxwork.n.02", "name": "waxwork"}, + {"id": 12155, "synset": "ways.n.01", "name": "ways"}, + {"id": 12156, "synset": "weapon.n.01", "name": "weapon"}, + {"id": 12157, "synset": "weaponry.n.01", "name": "weaponry"}, + {"id": 12158, "synset": "weapons_carrier.n.01", "name": "weapons_carrier"}, + {"id": 12159, "synset": "weathercock.n.01", "name": "weathercock"}, + {"id": 12160, "synset": "weatherglass.n.01", "name": "weatherglass"}, + {"id": 12161, "synset": "weather_satellite.n.01", "name": "weather_satellite"}, + {"id": 12162, "synset": "weather_ship.n.01", "name": "weather_ship"}, + {"id": 12163, "synset": "web.n.02", "name": "web"}, + {"id": 12164, "synset": "web.n.06", "name": "web"}, + {"id": 12165, "synset": "webbing.n.03", "name": "webbing"}, + {"id": 12166, "synset": "wedge.n.06", "name": "wedge"}, + {"id": 12167, "synset": "wedge.n.05", "name": "wedge"}, + {"id": 12168, "synset": "wedgie.n.01", "name": "wedgie"}, + {"id": 12169, "synset": "wedgwood.n.02", "name": "Wedgwood"}, + {"id": 12170, "synset": "weeder.n.02", "name": "weeder"}, + {"id": 12171, "synset": "weeds.n.01", "name": "weeds"}, + {"id": 12172, "synset": "weekender.n.02", "name": "weekender"}, + {"id": 12173, "synset": "weighbridge.n.01", "name": "weighbridge"}, + {"id": 12174, "synset": "weight.n.02", "name": "weight"}, + {"id": 12175, "synset": "weir.n.01", "name": "weir"}, + {"id": 12176, "synset": "weir.n.02", "name": "weir"}, + {"id": 12177, "synset": "welcome_wagon.n.01", "name": "welcome_wagon"}, + {"id": 12178, "synset": "weld.n.03", "name": "weld"}, + {"id": 12179, "synset": "welder's_mask.n.01", "name": "welder's_mask"}, + {"id": 12180, "synset": "weldment.n.01", "name": "weldment"}, + {"id": 12181, "synset": "well.n.02", "name": "well"}, + {"id": 12182, "synset": "wellhead.n.02", "name": "wellhead"}, + {"id": 12183, "synset": "welt.n.02", "name": "welt"}, + {"id": 12184, "synset": "weston_cell.n.01", "name": "Weston_cell"}, + {"id": 12185, "synset": "wet_bar.n.01", "name": "wet_bar"}, + {"id": 12186, "synset": "wet-bulb_thermometer.n.01", "name": "wet-bulb_thermometer"}, + {"id": 12187, "synset": "wet_cell.n.01", "name": "wet_cell"}, + {"id": 12188, "synset": "wet_fly.n.01", "name": "wet_fly"}, + {"id": 12189, "synset": "whaleboat.n.01", "name": "whaleboat"}, + {"id": 12190, "synset": "whaler.n.02", "name": "whaler"}, + {"id": 12191, "synset": "whaling_gun.n.01", "name": "whaling_gun"}, + {"id": 12192, "synset": "wheel.n.04", "name": "wheel"}, + {"id": 12193, "synset": "wheel_and_axle.n.01", "name": "wheel_and_axle"}, + {"id": 12194, "synset": "wheeled_vehicle.n.01", "name": "wheeled_vehicle"}, + {"id": 12195, "synset": "wheelwork.n.01", "name": "wheelwork"}, + {"id": 12196, "synset": "wherry.n.02", "name": "wherry"}, + {"id": 12197, "synset": "wherry.n.01", "name": "wherry"}, + {"id": 12198, "synset": "whetstone.n.01", "name": "whetstone"}, + {"id": 12199, "synset": "whiffletree.n.01", "name": "whiffletree"}, + {"id": 12200, "synset": "whip.n.01", "name": "whip"}, + {"id": 12201, "synset": "whipcord.n.02", "name": "whipcord"}, + {"id": 12202, "synset": "whipping_post.n.01", "name": "whipping_post"}, + {"id": 12203, "synset": "whipstitch.n.01", "name": "whipstitch"}, + {"id": 12204, "synset": "whirler.n.02", "name": "whirler"}, + {"id": 12205, "synset": "whisk.n.02", "name": "whisk"}, + {"id": 12206, "synset": "whisk.n.01", "name": "whisk"}, + {"id": 12207, "synset": "whiskey_bottle.n.01", "name": "whiskey_bottle"}, + {"id": 12208, "synset": "whiskey_jug.n.01", "name": "whiskey_jug"}, + {"id": 12209, "synset": "whispering_gallery.n.01", "name": "whispering_gallery"}, + {"id": 12210, "synset": "whistle.n.04", "name": "whistle"}, + {"id": 12211, "synset": "white.n.11", "name": "white"}, + {"id": 12212, "synset": "white_goods.n.01", "name": "white_goods"}, + {"id": 12213, "synset": "whitewash.n.02", "name": "whitewash"}, + {"id": 12214, "synset": "whorehouse.n.01", "name": "whorehouse"}, + {"id": 12215, "synset": "wick.n.02", "name": "wick"}, + {"id": 12216, "synset": "wicker.n.02", "name": "wicker"}, + {"id": 12217, "synset": "wicker_basket.n.01", "name": "wicker_basket"}, + {"id": 12218, "synset": "wicket.n.02", "name": "wicket"}, + {"id": 12219, "synset": "wicket.n.01", "name": "wicket"}, + {"id": 12220, "synset": "wickiup.n.01", "name": "wickiup"}, + {"id": 12221, "synset": "wide-angle_lens.n.01", "name": "wide-angle_lens"}, + {"id": 12222, "synset": "widebody_aircraft.n.01", "name": "widebody_aircraft"}, + {"id": 12223, "synset": "wide_wale.n.01", "name": "wide_wale"}, + {"id": 12224, "synset": "widow's_walk.n.01", "name": "widow's_walk"}, + {"id": 12225, "synset": "wiffle.n.01", "name": "Wiffle"}, + {"id": 12226, "synset": "wigwam.n.01", "name": "wigwam"}, + {"id": 12227, "synset": "wilton.n.01", "name": "Wilton"}, + {"id": 12228, "synset": "wimple.n.01", "name": "wimple"}, + {"id": 12229, "synset": "wincey.n.01", "name": "wincey"}, + {"id": 12230, "synset": "winceyette.n.01", "name": "winceyette"}, + {"id": 12231, "synset": "winch.n.01", "name": "winch"}, + {"id": 12232, "synset": "winchester.n.02", "name": "Winchester"}, + {"id": 12233, "synset": "windbreak.n.01", "name": "windbreak"}, + {"id": 12234, "synset": "winder.n.02", "name": "winder"}, + {"id": 12235, "synset": "wind_instrument.n.01", "name": "wind_instrument"}, + {"id": 12236, "synset": "windjammer.n.01", "name": "windjammer"}, + {"id": 12237, "synset": "windmill.n.02", "name": "windmill"}, + {"id": 12238, "synset": "window.n.01", "name": "window"}, + {"id": 12239, "synset": "window.n.08", "name": "window"}, + {"id": 12240, "synset": "window_blind.n.01", "name": "window_blind"}, + {"id": 12241, "synset": "window_envelope.n.01", "name": "window_envelope"}, + {"id": 12242, "synset": "window_frame.n.01", "name": "window_frame"}, + {"id": 12243, "synset": "window_screen.n.01", "name": "window_screen"}, + {"id": 12244, "synset": "window_seat.n.01", "name": "window_seat"}, + {"id": 12245, "synset": "window_shade.n.01", "name": "window_shade"}, + {"id": 12246, "synset": "windowsill.n.01", "name": "windowsill"}, + {"id": 12247, "synset": "windshield.n.01", "name": "windshield"}, + {"id": 12248, "synset": "windsor_chair.n.01", "name": "Windsor_chair"}, + {"id": 12249, "synset": "windsor_knot.n.01", "name": "Windsor_knot"}, + {"id": 12250, "synset": "windsor_tie.n.01", "name": "Windsor_tie"}, + {"id": 12251, "synset": "wind_tee.n.01", "name": "wind_tee"}, + {"id": 12252, "synset": "wind_tunnel.n.01", "name": "wind_tunnel"}, + {"id": 12253, "synset": "wind_turbine.n.01", "name": "wind_turbine"}, + {"id": 12254, "synset": "wine_bar.n.01", "name": "wine_bar"}, + {"id": 12255, "synset": "wine_cask.n.01", "name": "wine_cask"}, + {"id": 12256, "synset": "winepress.n.01", "name": "winepress"}, + {"id": 12257, "synset": "winery.n.01", "name": "winery"}, + {"id": 12258, "synset": "wineskin.n.01", "name": "wineskin"}, + {"id": 12259, "synset": "wing.n.02", "name": "wing"}, + {"id": 12260, "synset": "wing_chair.n.01", "name": "wing_chair"}, + {"id": 12261, "synset": "wing_nut.n.02", "name": "wing_nut"}, + {"id": 12262, "synset": "wing_tip.n.02", "name": "wing_tip"}, + {"id": 12263, "synset": "wing_tip.n.01", "name": "wing_tip"}, + {"id": 12264, "synset": "wiper.n.02", "name": "wiper"}, + {"id": 12265, "synset": "wiper_motor.n.01", "name": "wiper_motor"}, + {"id": 12266, "synset": "wire.n.01", "name": "wire"}, + {"id": 12267, "synset": "wire.n.02", "name": "wire"}, + {"id": 12268, "synset": "wire_cloth.n.01", "name": "wire_cloth"}, + {"id": 12269, "synset": "wire_cutter.n.01", "name": "wire_cutter"}, + {"id": 12270, "synset": "wire_gauge.n.01", "name": "wire_gauge"}, + { + "id": 12271, + "synset": "wireless_local_area_network.n.01", + "name": "wireless_local_area_network", + }, + {"id": 12272, "synset": "wire_matrix_printer.n.01", "name": "wire_matrix_printer"}, + {"id": 12273, "synset": "wire_recorder.n.01", "name": "wire_recorder"}, + {"id": 12274, "synset": "wire_stripper.n.01", "name": "wire_stripper"}, + {"id": 12275, "synset": "wirework.n.01", "name": "wirework"}, + {"id": 12276, "synset": "wiring.n.01", "name": "wiring"}, + {"id": 12277, "synset": "wishing_cap.n.01", "name": "wishing_cap"}, + {"id": 12278, "synset": "witness_box.n.01", "name": "witness_box"}, + {"id": 12279, "synset": "woman's_clothing.n.01", "name": "woman's_clothing"}, + {"id": 12280, "synset": "wood.n.08", "name": "wood"}, + {"id": 12281, "synset": "woodcarving.n.01", "name": "woodcarving"}, + {"id": 12282, "synset": "wood_chisel.n.01", "name": "wood_chisel"}, + {"id": 12283, "synset": "woodenware.n.01", "name": "woodenware"}, + {"id": 12284, "synset": "woodscrew.n.01", "name": "woodscrew"}, + {"id": 12285, "synset": "woodshed.n.01", "name": "woodshed"}, + {"id": 12286, "synset": "wood_vise.n.01", "name": "wood_vise"}, + {"id": 12287, "synset": "woodwind.n.01", "name": "woodwind"}, + {"id": 12288, "synset": "woof.n.01", "name": "woof"}, + {"id": 12289, "synset": "woofer.n.01", "name": "woofer"}, + {"id": 12290, "synset": "wool.n.01", "name": "wool"}, + {"id": 12291, "synset": "workbasket.n.01", "name": "workbasket"}, + {"id": 12292, "synset": "workbench.n.01", "name": "workbench"}, + {"id": 12293, "synset": "work-clothing.n.01", "name": "work-clothing"}, + {"id": 12294, "synset": "workhouse.n.02", "name": "workhouse"}, + {"id": 12295, "synset": "workhouse.n.01", "name": "workhouse"}, + {"id": 12296, "synset": "workpiece.n.01", "name": "workpiece"}, + {"id": 12297, "synset": "workroom.n.01", "name": "workroom"}, + {"id": 12298, "synset": "works.n.04", "name": "works"}, + {"id": 12299, "synset": "work-shirt.n.01", "name": "work-shirt"}, + {"id": 12300, "synset": "workstation.n.01", "name": "workstation"}, + {"id": 12301, "synset": "worktable.n.01", "name": "worktable"}, + {"id": 12302, "synset": "workwear.n.01", "name": "workwear"}, + {"id": 12303, "synset": "world_wide_web.n.01", "name": "World_Wide_Web"}, + {"id": 12304, "synset": "worm_fence.n.01", "name": "worm_fence"}, + {"id": 12305, "synset": "worm_gear.n.01", "name": "worm_gear"}, + {"id": 12306, "synset": "worm_wheel.n.01", "name": "worm_wheel"}, + {"id": 12307, "synset": "worsted.n.01", "name": "worsted"}, + {"id": 12308, "synset": "worsted.n.02", "name": "worsted"}, + {"id": 12309, "synset": "wrap.n.01", "name": "wrap"}, + {"id": 12310, "synset": "wraparound.n.01", "name": "wraparound"}, + {"id": 12311, "synset": "wrapping.n.01", "name": "wrapping"}, + {"id": 12312, "synset": "wreck.n.04", "name": "wreck"}, + {"id": 12313, "synset": "wrestling_mat.n.01", "name": "wrestling_mat"}, + {"id": 12314, "synset": "wringer.n.01", "name": "wringer"}, + {"id": 12315, "synset": "wrist_pad.n.01", "name": "wrist_pad"}, + {"id": 12316, "synset": "wrist_pin.n.01", "name": "wrist_pin"}, + {"id": 12317, "synset": "wristwatch.n.01", "name": "wristwatch"}, + {"id": 12318, "synset": "writing_arm.n.01", "name": "writing_arm"}, + {"id": 12319, "synset": "writing_desk.n.02", "name": "writing_desk"}, + {"id": 12320, "synset": "writing_desk.n.01", "name": "writing_desk"}, + {"id": 12321, "synset": "writing_implement.n.01", "name": "writing_implement"}, + {"id": 12322, "synset": "xerographic_printer.n.01", "name": "xerographic_printer"}, + {"id": 12323, "synset": "xerox.n.02", "name": "Xerox"}, + {"id": 12324, "synset": "x-ray_film.n.01", "name": "X-ray_film"}, + {"id": 12325, "synset": "x-ray_machine.n.01", "name": "X-ray_machine"}, + {"id": 12326, "synset": "x-ray_tube.n.01", "name": "X-ray_tube"}, + {"id": 12327, "synset": "yacht_chair.n.01", "name": "yacht_chair"}, + {"id": 12328, "synset": "yagi.n.01", "name": "yagi"}, + {"id": 12329, "synset": "yard.n.09", "name": "yard"}, + {"id": 12330, "synset": "yard.n.08", "name": "yard"}, + {"id": 12331, "synset": "yardarm.n.01", "name": "yardarm"}, + {"id": 12332, "synset": "yard_marker.n.01", "name": "yard_marker"}, + {"id": 12333, "synset": "yardstick.n.02", "name": "yardstick"}, + {"id": 12334, "synset": "yarmulke.n.01", "name": "yarmulke"}, + {"id": 12335, "synset": "yashmak.n.01", "name": "yashmak"}, + {"id": 12336, "synset": "yataghan.n.01", "name": "yataghan"}, + {"id": 12337, "synset": "yawl.n.02", "name": "yawl"}, + {"id": 12338, "synset": "yawl.n.01", "name": "yawl"}, + {"id": 12339, "synset": "yoke.n.01", "name": "yoke"}, + {"id": 12340, "synset": "yoke.n.06", "name": "yoke"}, + {"id": 12341, "synset": "yurt.n.01", "name": "yurt"}, + {"id": 12342, "synset": "zamboni.n.01", "name": "Zamboni"}, + {"id": 12343, "synset": "zero.n.04", "name": "zero"}, + {"id": 12344, "synset": "ziggurat.n.01", "name": "ziggurat"}, + {"id": 12345, "synset": "zill.n.01", "name": "zill"}, + {"id": 12346, "synset": "zip_gun.n.01", "name": "zip_gun"}, + {"id": 12347, "synset": "zither.n.01", "name": "zither"}, + {"id": 12348, "synset": "zoot_suit.n.01", "name": "zoot_suit"}, + {"id": 12349, "synset": "shading.n.01", "name": "shading"}, + {"id": 12350, "synset": "grain.n.10", "name": "grain"}, + {"id": 12351, "synset": "wood_grain.n.01", "name": "wood_grain"}, + {"id": 12352, "synset": "graining.n.01", "name": "graining"}, + {"id": 12353, "synset": "marbleization.n.01", "name": "marbleization"}, + {"id": 12354, "synset": "light.n.07", "name": "light"}, + {"id": 12355, "synset": "aura.n.02", "name": "aura"}, + {"id": 12356, "synset": "sunniness.n.01", "name": "sunniness"}, + {"id": 12357, "synset": "glint.n.02", "name": "glint"}, + {"id": 12358, "synset": "opalescence.n.01", "name": "opalescence"}, + {"id": 12359, "synset": "polish.n.01", "name": "polish"}, + { + "id": 12360, + "synset": "primary_color_for_pigments.n.01", + "name": "primary_color_for_pigments", + }, + {"id": 12361, "synset": "primary_color_for_light.n.01", "name": "primary_color_for_light"}, + {"id": 12362, "synset": "colorlessness.n.01", "name": "colorlessness"}, + {"id": 12363, "synset": "mottle.n.01", "name": "mottle"}, + {"id": 12364, "synset": "achromia.n.01", "name": "achromia"}, + {"id": 12365, "synset": "shade.n.02", "name": "shade"}, + {"id": 12366, "synset": "chromatic_color.n.01", "name": "chromatic_color"}, + {"id": 12367, "synset": "black.n.01", "name": "black"}, + {"id": 12368, "synset": "coal_black.n.01", "name": "coal_black"}, + {"id": 12369, "synset": "alabaster.n.03", "name": "alabaster"}, + {"id": 12370, "synset": "bone.n.03", "name": "bone"}, + {"id": 12371, "synset": "gray.n.01", "name": "gray"}, + {"id": 12372, "synset": "ash_grey.n.01", "name": "ash_grey"}, + {"id": 12373, "synset": "charcoal.n.03", "name": "charcoal"}, + {"id": 12374, "synset": "sanguine.n.01", "name": "sanguine"}, + {"id": 12375, "synset": "turkey_red.n.01", "name": "Turkey_red"}, + {"id": 12376, "synset": "crimson.n.01", "name": "crimson"}, + {"id": 12377, "synset": "dark_red.n.01", "name": "dark_red"}, + {"id": 12378, "synset": "claret.n.01", "name": "claret"}, + {"id": 12379, "synset": "fuschia.n.01", "name": "fuschia"}, + {"id": 12380, "synset": "maroon.n.02", "name": "maroon"}, + {"id": 12381, "synset": "orange.n.02", "name": "orange"}, + {"id": 12382, "synset": "reddish_orange.n.01", "name": "reddish_orange"}, + {"id": 12383, "synset": "yellow.n.01", "name": "yellow"}, + {"id": 12384, "synset": "gamboge.n.02", "name": "gamboge"}, + {"id": 12385, "synset": "pale_yellow.n.01", "name": "pale_yellow"}, + {"id": 12386, "synset": "green.n.01", "name": "green"}, + {"id": 12387, "synset": "greenishness.n.01", "name": "greenishness"}, + {"id": 12388, "synset": "sea_green.n.01", "name": "sea_green"}, + {"id": 12389, "synset": "sage_green.n.01", "name": "sage_green"}, + {"id": 12390, "synset": "bottle_green.n.01", "name": "bottle_green"}, + {"id": 12391, "synset": "emerald.n.03", "name": "emerald"}, + {"id": 12392, "synset": "olive_green.n.01", "name": "olive_green"}, + {"id": 12393, "synset": "jade_green.n.01", "name": "jade_green"}, + {"id": 12394, "synset": "blue.n.01", "name": "blue"}, + {"id": 12395, "synset": "azure.n.01", "name": "azure"}, + {"id": 12396, "synset": "steel_blue.n.01", "name": "steel_blue"}, + {"id": 12397, "synset": "greenish_blue.n.01", "name": "greenish_blue"}, + {"id": 12398, "synset": "purplish_blue.n.01", "name": "purplish_blue"}, + {"id": 12399, "synset": "purple.n.01", "name": "purple"}, + {"id": 12400, "synset": "tyrian_purple.n.02", "name": "Tyrian_purple"}, + {"id": 12401, "synset": "indigo.n.03", "name": "indigo"}, + {"id": 12402, "synset": "lavender.n.02", "name": "lavender"}, + {"id": 12403, "synset": "reddish_purple.n.01", "name": "reddish_purple"}, + {"id": 12404, "synset": "pink.n.01", "name": "pink"}, + {"id": 12405, "synset": "carnation.n.02", "name": "carnation"}, + {"id": 12406, "synset": "rose.n.03", "name": "rose"}, + {"id": 12407, "synset": "chestnut.n.04", "name": "chestnut"}, + {"id": 12408, "synset": "chocolate.n.03", "name": "chocolate"}, + {"id": 12409, "synset": "light_brown.n.01", "name": "light_brown"}, + {"id": 12410, "synset": "tan.n.02", "name": "tan"}, + {"id": 12411, "synset": "beige.n.01", "name": "beige"}, + {"id": 12412, "synset": "reddish_brown.n.01", "name": "reddish_brown"}, + {"id": 12413, "synset": "brick_red.n.01", "name": "brick_red"}, + {"id": 12414, "synset": "copper.n.04", "name": "copper"}, + {"id": 12415, "synset": "indian_red.n.03", "name": "Indian_red"}, + {"id": 12416, "synset": "puce.n.01", "name": "puce"}, + {"id": 12417, "synset": "olive.n.05", "name": "olive"}, + {"id": 12418, "synset": "ultramarine.n.02", "name": "ultramarine"}, + {"id": 12419, "synset": "complementary_color.n.01", "name": "complementary_color"}, + {"id": 12420, "synset": "pigmentation.n.02", "name": "pigmentation"}, + {"id": 12421, "synset": "complexion.n.01", "name": "complexion"}, + {"id": 12422, "synset": "ruddiness.n.01", "name": "ruddiness"}, + {"id": 12423, "synset": "nonsolid_color.n.01", "name": "nonsolid_color"}, + {"id": 12424, "synset": "aposematic_coloration.n.01", "name": "aposematic_coloration"}, + {"id": 12425, "synset": "cryptic_coloration.n.01", "name": "cryptic_coloration"}, + {"id": 12426, "synset": "ring.n.01", "name": "ring"}, + {"id": 12427, "synset": "center_of_curvature.n.01", "name": "center_of_curvature"}, + {"id": 12428, "synset": "cadaver.n.01", "name": "cadaver"}, + {"id": 12429, "synset": "mandibular_notch.n.01", "name": "mandibular_notch"}, + {"id": 12430, "synset": "rib.n.05", "name": "rib"}, + {"id": 12431, "synset": "skin.n.01", "name": "skin"}, + {"id": 12432, "synset": "skin_graft.n.01", "name": "skin_graft"}, + {"id": 12433, "synset": "epidermal_cell.n.01", "name": "epidermal_cell"}, + {"id": 12434, "synset": "melanocyte.n.01", "name": "melanocyte"}, + {"id": 12435, "synset": "prickle_cell.n.01", "name": "prickle_cell"}, + {"id": 12436, "synset": "columnar_cell.n.01", "name": "columnar_cell"}, + {"id": 12437, "synset": "spongioblast.n.01", "name": "spongioblast"}, + {"id": 12438, "synset": "squamous_cell.n.01", "name": "squamous_cell"}, + {"id": 12439, "synset": "amyloid_plaque.n.01", "name": "amyloid_plaque"}, + {"id": 12440, "synset": "dental_plaque.n.01", "name": "dental_plaque"}, + {"id": 12441, "synset": "macule.n.01", "name": "macule"}, + {"id": 12442, "synset": "freckle.n.01", "name": "freckle"}, + {"id": 12443, "synset": "bouffant.n.01", "name": "bouffant"}, + {"id": 12444, "synset": "sausage_curl.n.01", "name": "sausage_curl"}, + {"id": 12445, "synset": "forelock.n.01", "name": "forelock"}, + {"id": 12446, "synset": "spit_curl.n.01", "name": "spit_curl"}, + {"id": 12447, "synset": "pigtail.n.01", "name": "pigtail"}, + {"id": 12448, "synset": "pageboy.n.02", "name": "pageboy"}, + {"id": 12449, "synset": "pompadour.n.02", "name": "pompadour"}, + {"id": 12450, "synset": "thatch.n.01", "name": "thatch"}, + {"id": 12451, "synset": "soup-strainer.n.01", "name": "soup-strainer"}, + {"id": 12452, "synset": "mustachio.n.01", "name": "mustachio"}, + {"id": 12453, "synset": "walrus_mustache.n.01", "name": "walrus_mustache"}, + {"id": 12454, "synset": "stubble.n.02", "name": "stubble"}, + {"id": 12455, "synset": "vandyke_beard.n.01", "name": "vandyke_beard"}, + {"id": 12456, "synset": "soul_patch.n.01", "name": "soul_patch"}, + {"id": 12457, "synset": "esophageal_smear.n.01", "name": "esophageal_smear"}, + {"id": 12458, "synset": "paraduodenal_smear.n.01", "name": "paraduodenal_smear"}, + {"id": 12459, "synset": "specimen.n.02", "name": "specimen"}, + {"id": 12460, "synset": "punctum.n.01", "name": "punctum"}, + {"id": 12461, "synset": "glenoid_fossa.n.02", "name": "glenoid_fossa"}, + {"id": 12462, "synset": "diastema.n.01", "name": "diastema"}, + {"id": 12463, "synset": "marrow.n.01", "name": "marrow"}, + {"id": 12464, "synset": "mouth.n.01", "name": "mouth"}, + {"id": 12465, "synset": "canthus.n.01", "name": "canthus"}, + {"id": 12466, "synset": "milk.n.02", "name": "milk"}, + {"id": 12467, "synset": "mother's_milk.n.01", "name": "mother's_milk"}, + {"id": 12468, "synset": "colostrum.n.01", "name": "colostrum"}, + {"id": 12469, "synset": "vein.n.01", "name": "vein"}, + {"id": 12470, "synset": "ganglion_cell.n.01", "name": "ganglion_cell"}, + {"id": 12471, "synset": "x_chromosome.n.01", "name": "X_chromosome"}, + {"id": 12472, "synset": "embryonic_cell.n.01", "name": "embryonic_cell"}, + {"id": 12473, "synset": "myeloblast.n.01", "name": "myeloblast"}, + {"id": 12474, "synset": "sideroblast.n.01", "name": "sideroblast"}, + {"id": 12475, "synset": "osteocyte.n.01", "name": "osteocyte"}, + {"id": 12476, "synset": "megalocyte.n.01", "name": "megalocyte"}, + {"id": 12477, "synset": "leukocyte.n.01", "name": "leukocyte"}, + {"id": 12478, "synset": "histiocyte.n.01", "name": "histiocyte"}, + {"id": 12479, "synset": "fixed_phagocyte.n.01", "name": "fixed_phagocyte"}, + {"id": 12480, "synset": "lymphocyte.n.01", "name": "lymphocyte"}, + {"id": 12481, "synset": "monoblast.n.01", "name": "monoblast"}, + {"id": 12482, "synset": "neutrophil.n.01", "name": "neutrophil"}, + {"id": 12483, "synset": "microphage.n.01", "name": "microphage"}, + {"id": 12484, "synset": "sickle_cell.n.01", "name": "sickle_cell"}, + {"id": 12485, "synset": "siderocyte.n.01", "name": "siderocyte"}, + {"id": 12486, "synset": "spherocyte.n.01", "name": "spherocyte"}, + {"id": 12487, "synset": "ootid.n.01", "name": "ootid"}, + {"id": 12488, "synset": "oocyte.n.01", "name": "oocyte"}, + {"id": 12489, "synset": "spermatid.n.01", "name": "spermatid"}, + {"id": 12490, "synset": "leydig_cell.n.01", "name": "Leydig_cell"}, + {"id": 12491, "synset": "striated_muscle_cell.n.01", "name": "striated_muscle_cell"}, + {"id": 12492, "synset": "smooth_muscle_cell.n.01", "name": "smooth_muscle_cell"}, + {"id": 12493, "synset": "ranvier's_nodes.n.01", "name": "Ranvier's_nodes"}, + {"id": 12494, "synset": "neuroglia.n.01", "name": "neuroglia"}, + {"id": 12495, "synset": "astrocyte.n.01", "name": "astrocyte"}, + {"id": 12496, "synset": "protoplasmic_astrocyte.n.01", "name": "protoplasmic_astrocyte"}, + {"id": 12497, "synset": "oligodendrocyte.n.01", "name": "oligodendrocyte"}, + {"id": 12498, "synset": "proprioceptor.n.01", "name": "proprioceptor"}, + {"id": 12499, "synset": "dendrite.n.01", "name": "dendrite"}, + {"id": 12500, "synset": "sensory_fiber.n.01", "name": "sensory_fiber"}, + {"id": 12501, "synset": "subarachnoid_space.n.01", "name": "subarachnoid_space"}, + {"id": 12502, "synset": "cerebral_cortex.n.01", "name": "cerebral_cortex"}, + {"id": 12503, "synset": "renal_cortex.n.01", "name": "renal_cortex"}, + {"id": 12504, "synset": "prepuce.n.02", "name": "prepuce"}, + {"id": 12505, "synset": "head.n.01", "name": "head"}, + {"id": 12506, "synset": "scalp.n.01", "name": "scalp"}, + {"id": 12507, "synset": "frontal_eminence.n.01", "name": "frontal_eminence"}, + {"id": 12508, "synset": "suture.n.01", "name": "suture"}, + {"id": 12509, "synset": "foramen_magnum.n.01", "name": "foramen_magnum"}, + {"id": 12510, "synset": "esophagogastric_junction.n.01", "name": "esophagogastric_junction"}, + {"id": 12511, "synset": "heel.n.02", "name": "heel"}, + {"id": 12512, "synset": "cuticle.n.01", "name": "cuticle"}, + {"id": 12513, "synset": "hangnail.n.01", "name": "hangnail"}, + {"id": 12514, "synset": "exoskeleton.n.01", "name": "exoskeleton"}, + {"id": 12515, "synset": "abdominal_wall.n.01", "name": "abdominal_wall"}, + {"id": 12516, "synset": "lemon.n.04", "name": "lemon"}, + {"id": 12517, "synset": "coordinate_axis.n.01", "name": "coordinate_axis"}, + {"id": 12518, "synset": "landscape.n.04", "name": "landscape"}, + {"id": 12519, "synset": "medium.n.01", "name": "medium"}, + {"id": 12520, "synset": "vehicle.n.02", "name": "vehicle"}, + {"id": 12521, "synset": "paper.n.04", "name": "paper"}, + {"id": 12522, "synset": "channel.n.01", "name": "channel"}, + {"id": 12523, "synset": "film.n.02", "name": "film"}, + {"id": 12524, "synset": "silver_screen.n.01", "name": "silver_screen"}, + {"id": 12525, "synset": "free_press.n.01", "name": "free_press"}, + {"id": 12526, "synset": "press.n.02", "name": "press"}, + {"id": 12527, "synset": "print_media.n.01", "name": "print_media"}, + {"id": 12528, "synset": "storage_medium.n.01", "name": "storage_medium"}, + {"id": 12529, "synset": "magnetic_storage_medium.n.01", "name": "magnetic_storage_medium"}, + {"id": 12530, "synset": "journalism.n.01", "name": "journalism"}, + {"id": 12531, "synset": "fleet_street.n.02", "name": "Fleet_Street"}, + {"id": 12532, "synset": "photojournalism.n.01", "name": "photojournalism"}, + {"id": 12533, "synset": "news_photography.n.01", "name": "news_photography"}, + {"id": 12534, "synset": "rotogravure.n.02", "name": "rotogravure"}, + {"id": 12535, "synset": "daily.n.01", "name": "daily"}, + {"id": 12536, "synset": "gazette.n.01", "name": "gazette"}, + {"id": 12537, "synset": "school_newspaper.n.01", "name": "school_newspaper"}, + {"id": 12538, "synset": "tabloid.n.02", "name": "tabloid"}, + {"id": 12539, "synset": "yellow_journalism.n.01", "name": "yellow_journalism"}, + {"id": 12540, "synset": "telecommunication.n.01", "name": "telecommunication"}, + {"id": 12541, "synset": "telephone.n.02", "name": "telephone"}, + {"id": 12542, "synset": "voice_mail.n.01", "name": "voice_mail"}, + {"id": 12543, "synset": "call.n.01", "name": "call"}, + {"id": 12544, "synset": "call-back.n.01", "name": "call-back"}, + {"id": 12545, "synset": "collect_call.n.01", "name": "collect_call"}, + {"id": 12546, "synset": "call_forwarding.n.01", "name": "call_forwarding"}, + {"id": 12547, "synset": "call-in.n.01", "name": "call-in"}, + {"id": 12548, "synset": "call_waiting.n.01", "name": "call_waiting"}, + {"id": 12549, "synset": "crank_call.n.01", "name": "crank_call"}, + {"id": 12550, "synset": "local_call.n.01", "name": "local_call"}, + {"id": 12551, "synset": "long_distance.n.01", "name": "long_distance"}, + {"id": 12552, "synset": "toll_call.n.01", "name": "toll_call"}, + {"id": 12553, "synset": "wake-up_call.n.02", "name": "wake-up_call"}, + {"id": 12554, "synset": "three-way_calling.n.01", "name": "three-way_calling"}, + {"id": 12555, "synset": "telegraphy.n.01", "name": "telegraphy"}, + {"id": 12556, "synset": "cable.n.01", "name": "cable"}, + {"id": 12557, "synset": "wireless.n.02", "name": "wireless"}, + {"id": 12558, "synset": "radiotelegraph.n.01", "name": "radiotelegraph"}, + {"id": 12559, "synset": "radiotelephone.n.01", "name": "radiotelephone"}, + {"id": 12560, "synset": "broadcasting.n.02", "name": "broadcasting"}, + {"id": 12561, "synset": "rediffusion.n.01", "name": "Rediffusion"}, + {"id": 12562, "synset": "multiplex.n.01", "name": "multiplex"}, + {"id": 12563, "synset": "radio.n.01", "name": "radio"}, + {"id": 12564, "synset": "television.n.01", "name": "television"}, + {"id": 12565, "synset": "cable_television.n.01", "name": "cable_television"}, + { + "id": 12566, + "synset": "high-definition_television.n.01", + "name": "high-definition_television", + }, + {"id": 12567, "synset": "reception.n.03", "name": "reception"}, + {"id": 12568, "synset": "signal_detection.n.01", "name": "signal_detection"}, + {"id": 12569, "synset": "hakham.n.01", "name": "Hakham"}, + {"id": 12570, "synset": "web_site.n.01", "name": "web_site"}, + {"id": 12571, "synset": "chat_room.n.01", "name": "chat_room"}, + {"id": 12572, "synset": "portal_site.n.01", "name": "portal_site"}, + {"id": 12573, "synset": "jotter.n.01", "name": "jotter"}, + {"id": 12574, "synset": "breviary.n.01", "name": "breviary"}, + {"id": 12575, "synset": "wordbook.n.01", "name": "wordbook"}, + {"id": 12576, "synset": "desk_dictionary.n.01", "name": "desk_dictionary"}, + {"id": 12577, "synset": "reckoner.n.02", "name": "reckoner"}, + {"id": 12578, "synset": "document.n.01", "name": "document"}, + {"id": 12579, "synset": "album.n.01", "name": "album"}, + {"id": 12580, "synset": "concept_album.n.01", "name": "concept_album"}, + {"id": 12581, "synset": "rock_opera.n.01", "name": "rock_opera"}, + {"id": 12582, "synset": "tribute_album.n.01", "name": "tribute_album"}, + {"id": 12583, "synset": "magazine.n.01", "name": "magazine"}, + {"id": 12584, "synset": "colour_supplement.n.01", "name": "colour_supplement"}, + {"id": 12585, "synset": "news_magazine.n.01", "name": "news_magazine"}, + {"id": 12586, "synset": "pulp.n.04", "name": "pulp"}, + {"id": 12587, "synset": "slick.n.02", "name": "slick"}, + {"id": 12588, "synset": "trade_magazine.n.01", "name": "trade_magazine"}, + {"id": 12589, "synset": "movie.n.01", "name": "movie"}, + {"id": 12590, "synset": "outtake.n.01", "name": "outtake"}, + {"id": 12591, "synset": "shoot-'em-up.n.01", "name": "shoot-'em-up"}, + {"id": 12592, "synset": "spaghetti_western.n.01", "name": "spaghetti_Western"}, + {"id": 12593, "synset": "encyclical.n.01", "name": "encyclical"}, + {"id": 12594, "synset": "crossword_puzzle.n.01", "name": "crossword_puzzle"}, + {"id": 12595, "synset": "sign.n.02", "name": "sign"}, + {"id": 12596, "synset": "swastika.n.01", "name": "swastika"}, + {"id": 12597, "synset": "concert.n.01", "name": "concert"}, + {"id": 12598, "synset": "artwork.n.01", "name": "artwork"}, + {"id": 12599, "synset": "lobe.n.03", "name": "lobe"}, + {"id": 12600, "synset": "book_jacket.n.01", "name": "book_jacket"}, + {"id": 12601, "synset": "cairn.n.01", "name": "cairn"}, + {"id": 12602, "synset": "three-day_event.n.01", "name": "three-day_event"}, + {"id": 12603, "synset": "comfort_food.n.01", "name": "comfort_food"}, + {"id": 12604, "synset": "comestible.n.01", "name": "comestible"}, + {"id": 12605, "synset": "tuck.n.01", "name": "tuck"}, + {"id": 12606, "synset": "course.n.07", "name": "course"}, + {"id": 12607, "synset": "dainty.n.01", "name": "dainty"}, + {"id": 12608, "synset": "dish.n.02", "name": "dish"}, + {"id": 12609, "synset": "fast_food.n.01", "name": "fast_food"}, + {"id": 12610, "synset": "finger_food.n.01", "name": "finger_food"}, + {"id": 12611, "synset": "ingesta.n.01", "name": "ingesta"}, + {"id": 12612, "synset": "kosher.n.01", "name": "kosher"}, + {"id": 12613, "synset": "fare.n.04", "name": "fare"}, + {"id": 12614, "synset": "diet.n.03", "name": "diet"}, + {"id": 12615, "synset": "diet.n.01", "name": "diet"}, + {"id": 12616, "synset": "dietary.n.01", "name": "dietary"}, + {"id": 12617, "synset": "balanced_diet.n.01", "name": "balanced_diet"}, + {"id": 12618, "synset": "bland_diet.n.01", "name": "bland_diet"}, + {"id": 12619, "synset": "clear_liquid_diet.n.01", "name": "clear_liquid_diet"}, + {"id": 12620, "synset": "diabetic_diet.n.01", "name": "diabetic_diet"}, + {"id": 12621, "synset": "dietary_supplement.n.01", "name": "dietary_supplement"}, + {"id": 12622, "synset": "carbohydrate_loading.n.01", "name": "carbohydrate_loading"}, + {"id": 12623, "synset": "fad_diet.n.01", "name": "fad_diet"}, + {"id": 12624, "synset": "gluten-free_diet.n.01", "name": "gluten-free_diet"}, + {"id": 12625, "synset": "high-protein_diet.n.01", "name": "high-protein_diet"}, + {"id": 12626, "synset": "high-vitamin_diet.n.01", "name": "high-vitamin_diet"}, + {"id": 12627, "synset": "light_diet.n.01", "name": "light_diet"}, + {"id": 12628, "synset": "liquid_diet.n.01", "name": "liquid_diet"}, + {"id": 12629, "synset": "low-calorie_diet.n.01", "name": "low-calorie_diet"}, + {"id": 12630, "synset": "low-fat_diet.n.01", "name": "low-fat_diet"}, + {"id": 12631, "synset": "low-sodium_diet.n.01", "name": "low-sodium_diet"}, + {"id": 12632, "synset": "macrobiotic_diet.n.01", "name": "macrobiotic_diet"}, + {"id": 12633, "synset": "reducing_diet.n.01", "name": "reducing_diet"}, + {"id": 12634, "synset": "soft_diet.n.01", "name": "soft_diet"}, + {"id": 12635, "synset": "vegetarianism.n.01", "name": "vegetarianism"}, + {"id": 12636, "synset": "menu.n.02", "name": "menu"}, + {"id": 12637, "synset": "chow.n.02", "name": "chow"}, + {"id": 12638, "synset": "board.n.04", "name": "board"}, + {"id": 12639, "synset": "mess.n.04", "name": "mess"}, + {"id": 12640, "synset": "ration.n.01", "name": "ration"}, + {"id": 12641, "synset": "field_ration.n.01", "name": "field_ration"}, + {"id": 12642, "synset": "k_ration.n.01", "name": "K_ration"}, + {"id": 12643, "synset": "c-ration.n.01", "name": "C-ration"}, + {"id": 12644, "synset": "foodstuff.n.02", "name": "foodstuff"}, + {"id": 12645, "synset": "starches.n.01", "name": "starches"}, + {"id": 12646, "synset": "breadstuff.n.02", "name": "breadstuff"}, + {"id": 12647, "synset": "coloring.n.01", "name": "coloring"}, + {"id": 12648, "synset": "concentrate.n.02", "name": "concentrate"}, + {"id": 12649, "synset": "tomato_concentrate.n.01", "name": "tomato_concentrate"}, + {"id": 12650, "synset": "meal.n.03", "name": "meal"}, + {"id": 12651, "synset": "kibble.n.01", "name": "kibble"}, + {"id": 12652, "synset": "farina.n.01", "name": "farina"}, + {"id": 12653, "synset": "matzo_meal.n.01", "name": "matzo_meal"}, + {"id": 12654, "synset": "oatmeal.n.02", "name": "oatmeal"}, + {"id": 12655, "synset": "pea_flour.n.01", "name": "pea_flour"}, + {"id": 12656, "synset": "roughage.n.01", "name": "roughage"}, + {"id": 12657, "synset": "bran.n.02", "name": "bran"}, + {"id": 12658, "synset": "flour.n.01", "name": "flour"}, + {"id": 12659, "synset": "plain_flour.n.01", "name": "plain_flour"}, + {"id": 12660, "synset": "wheat_flour.n.01", "name": "wheat_flour"}, + {"id": 12661, "synset": "whole_wheat_flour.n.01", "name": "whole_wheat_flour"}, + {"id": 12662, "synset": "soybean_meal.n.01", "name": "soybean_meal"}, + {"id": 12663, "synset": "semolina.n.01", "name": "semolina"}, + {"id": 12664, "synset": "corn_gluten_feed.n.01", "name": "corn_gluten_feed"}, + {"id": 12665, "synset": "nutriment.n.01", "name": "nutriment"}, + {"id": 12666, "synset": "commissariat.n.01", "name": "commissariat"}, + {"id": 12667, "synset": "larder.n.01", "name": "larder"}, + {"id": 12668, "synset": "frozen_food.n.01", "name": "frozen_food"}, + {"id": 12669, "synset": "canned_food.n.01", "name": "canned_food"}, + {"id": 12670, "synset": "canned_meat.n.01", "name": "canned_meat"}, + {"id": 12671, "synset": "spam.n.01", "name": "Spam"}, + {"id": 12672, "synset": "dehydrated_food.n.01", "name": "dehydrated_food"}, + {"id": 12673, "synset": "square_meal.n.01", "name": "square_meal"}, + {"id": 12674, "synset": "meal.n.01", "name": "meal"}, + {"id": 12675, "synset": "potluck.n.01", "name": "potluck"}, + {"id": 12676, "synset": "refection.n.01", "name": "refection"}, + {"id": 12677, "synset": "refreshment.n.01", "name": "refreshment"}, + {"id": 12678, "synset": "breakfast.n.01", "name": "breakfast"}, + {"id": 12679, "synset": "continental_breakfast.n.01", "name": "continental_breakfast"}, + {"id": 12680, "synset": "brunch.n.01", "name": "brunch"}, + {"id": 12681, "synset": "lunch.n.01", "name": "lunch"}, + {"id": 12682, "synset": "business_lunch.n.01", "name": "business_lunch"}, + {"id": 12683, "synset": "high_tea.n.01", "name": "high_tea"}, + {"id": 12684, "synset": "tea.n.02", "name": "tea"}, + {"id": 12685, "synset": "dinner.n.01", "name": "dinner"}, + {"id": 12686, "synset": "supper.n.01", "name": "supper"}, + {"id": 12687, "synset": "buffet.n.02", "name": "buffet"}, + {"id": 12688, "synset": "picnic.n.03", "name": "picnic"}, + {"id": 12689, "synset": "cookout.n.01", "name": "cookout"}, + {"id": 12690, "synset": "barbecue.n.02", "name": "barbecue"}, + {"id": 12691, "synset": "clambake.n.01", "name": "clambake"}, + {"id": 12692, "synset": "fish_fry.n.01", "name": "fish_fry"}, + {"id": 12693, "synset": "bite.n.04", "name": "bite"}, + {"id": 12694, "synset": "nosh.n.01", "name": "nosh"}, + {"id": 12695, "synset": "nosh-up.n.01", "name": "nosh-up"}, + {"id": 12696, "synset": "ploughman's_lunch.n.01", "name": "ploughman's_lunch"}, + {"id": 12697, "synset": "coffee_break.n.01", "name": "coffee_break"}, + {"id": 12698, "synset": "banquet.n.02", "name": "banquet"}, + {"id": 12699, "synset": "entree.n.01", "name": "entree"}, + {"id": 12700, "synset": "piece_de_resistance.n.02", "name": "piece_de_resistance"}, + {"id": 12701, "synset": "plate.n.08", "name": "plate"}, + {"id": 12702, "synset": "adobo.n.01", "name": "adobo"}, + {"id": 12703, "synset": "side_dish.n.01", "name": "side_dish"}, + {"id": 12704, "synset": "special.n.02", "name": "special"}, + {"id": 12705, "synset": "chicken_casserole.n.01", "name": "chicken_casserole"}, + {"id": 12706, "synset": "chicken_cacciatore.n.01", "name": "chicken_cacciatore"}, + {"id": 12707, "synset": "antipasto.n.01", "name": "antipasto"}, + {"id": 12708, "synset": "appetizer.n.01", "name": "appetizer"}, + {"id": 12709, "synset": "canape.n.01", "name": "canape"}, + {"id": 12710, "synset": "cocktail.n.02", "name": "cocktail"}, + {"id": 12711, "synset": "fruit_cocktail.n.01", "name": "fruit_cocktail"}, + {"id": 12712, "synset": "crab_cocktail.n.01", "name": "crab_cocktail"}, + {"id": 12713, "synset": "shrimp_cocktail.n.01", "name": "shrimp_cocktail"}, + {"id": 12714, "synset": "hors_d'oeuvre.n.01", "name": "hors_d'oeuvre"}, + {"id": 12715, "synset": "relish.n.02", "name": "relish"}, + {"id": 12716, "synset": "dip.n.04", "name": "dip"}, + {"id": 12717, "synset": "bean_dip.n.01", "name": "bean_dip"}, + {"id": 12718, "synset": "cheese_dip.n.01", "name": "cheese_dip"}, + {"id": 12719, "synset": "clam_dip.n.01", "name": "clam_dip"}, + {"id": 12720, "synset": "guacamole.n.01", "name": "guacamole"}, + {"id": 12721, "synset": "soup_du_jour.n.01", "name": "soup_du_jour"}, + {"id": 12722, "synset": "alphabet_soup.n.02", "name": "alphabet_soup"}, + {"id": 12723, "synset": "consomme.n.01", "name": "consomme"}, + {"id": 12724, "synset": "madrilene.n.01", "name": "madrilene"}, + {"id": 12725, "synset": "bisque.n.01", "name": "bisque"}, + {"id": 12726, "synset": "borsch.n.01", "name": "borsch"}, + {"id": 12727, "synset": "broth.n.02", "name": "broth"}, + {"id": 12728, "synset": "barley_water.n.01", "name": "barley_water"}, + {"id": 12729, "synset": "bouillon.n.01", "name": "bouillon"}, + {"id": 12730, "synset": "beef_broth.n.01", "name": "beef_broth"}, + {"id": 12731, "synset": "chicken_broth.n.01", "name": "chicken_broth"}, + {"id": 12732, "synset": "broth.n.01", "name": "broth"}, + {"id": 12733, "synset": "stock_cube.n.01", "name": "stock_cube"}, + {"id": 12734, "synset": "chicken_soup.n.01", "name": "chicken_soup"}, + {"id": 12735, "synset": "cock-a-leekie.n.01", "name": "cock-a-leekie"}, + {"id": 12736, "synset": "gazpacho.n.01", "name": "gazpacho"}, + {"id": 12737, "synset": "gumbo.n.04", "name": "gumbo"}, + {"id": 12738, "synset": "julienne.n.02", "name": "julienne"}, + {"id": 12739, "synset": "marmite.n.01", "name": "marmite"}, + {"id": 12740, "synset": "mock_turtle_soup.n.01", "name": "mock_turtle_soup"}, + {"id": 12741, "synset": "mulligatawny.n.01", "name": "mulligatawny"}, + {"id": 12742, "synset": "oxtail_soup.n.01", "name": "oxtail_soup"}, + {"id": 12743, "synset": "pea_soup.n.01", "name": "pea_soup"}, + {"id": 12744, "synset": "pepper_pot.n.01", "name": "pepper_pot"}, + {"id": 12745, "synset": "petite_marmite.n.01", "name": "petite_marmite"}, + {"id": 12746, "synset": "potage.n.01", "name": "potage"}, + {"id": 12747, "synset": "pottage.n.01", "name": "pottage"}, + {"id": 12748, "synset": "turtle_soup.n.01", "name": "turtle_soup"}, + {"id": 12749, "synset": "eggdrop_soup.n.01", "name": "eggdrop_soup"}, + {"id": 12750, "synset": "chowder.n.01", "name": "chowder"}, + {"id": 12751, "synset": "corn_chowder.n.01", "name": "corn_chowder"}, + {"id": 12752, "synset": "clam_chowder.n.01", "name": "clam_chowder"}, + {"id": 12753, "synset": "manhattan_clam_chowder.n.01", "name": "Manhattan_clam_chowder"}, + {"id": 12754, "synset": "new_england_clam_chowder.n.01", "name": "New_England_clam_chowder"}, + {"id": 12755, "synset": "fish_chowder.n.01", "name": "fish_chowder"}, + {"id": 12756, "synset": "won_ton.n.02", "name": "won_ton"}, + {"id": 12757, "synset": "split-pea_soup.n.01", "name": "split-pea_soup"}, + {"id": 12758, "synset": "green_pea_soup.n.01", "name": "green_pea_soup"}, + {"id": 12759, "synset": "lentil_soup.n.01", "name": "lentil_soup"}, + {"id": 12760, "synset": "scotch_broth.n.01", "name": "Scotch_broth"}, + {"id": 12761, "synset": "vichyssoise.n.01", "name": "vichyssoise"}, + {"id": 12762, "synset": "bigos.n.01", "name": "bigos"}, + {"id": 12763, "synset": "brunswick_stew.n.01", "name": "Brunswick_stew"}, + {"id": 12764, "synset": "burgoo.n.03", "name": "burgoo"}, + {"id": 12765, "synset": "burgoo.n.02", "name": "burgoo"}, + {"id": 12766, "synset": "olla_podrida.n.01", "name": "olla_podrida"}, + {"id": 12767, "synset": "mulligan_stew.n.01", "name": "mulligan_stew"}, + {"id": 12768, "synset": "purloo.n.01", "name": "purloo"}, + {"id": 12769, "synset": "goulash.n.01", "name": "goulash"}, + {"id": 12770, "synset": "hotchpotch.n.02", "name": "hotchpotch"}, + {"id": 12771, "synset": "hot_pot.n.01", "name": "hot_pot"}, + {"id": 12772, "synset": "beef_goulash.n.01", "name": "beef_goulash"}, + {"id": 12773, "synset": "pork-and-veal_goulash.n.01", "name": "pork-and-veal_goulash"}, + {"id": 12774, "synset": "porkholt.n.01", "name": "porkholt"}, + {"id": 12775, "synset": "irish_stew.n.01", "name": "Irish_stew"}, + {"id": 12776, "synset": "oyster_stew.n.01", "name": "oyster_stew"}, + {"id": 12777, "synset": "lobster_stew.n.01", "name": "lobster_stew"}, + {"id": 12778, "synset": "lobscouse.n.01", "name": "lobscouse"}, + {"id": 12779, "synset": "fish_stew.n.01", "name": "fish_stew"}, + {"id": 12780, "synset": "bouillabaisse.n.01", "name": "bouillabaisse"}, + {"id": 12781, "synset": "matelote.n.01", "name": "matelote"}, + {"id": 12782, "synset": "paella.n.01", "name": "paella"}, + {"id": 12783, "synset": "fricassee.n.01", "name": "fricassee"}, + {"id": 12784, "synset": "chicken_stew.n.01", "name": "chicken_stew"}, + {"id": 12785, "synset": "turkey_stew.n.01", "name": "turkey_stew"}, + {"id": 12786, "synset": "beef_stew.n.01", "name": "beef_stew"}, + {"id": 12787, "synset": "ragout.n.01", "name": "ragout"}, + {"id": 12788, "synset": "ratatouille.n.01", "name": "ratatouille"}, + {"id": 12789, "synset": "salmi.n.01", "name": "salmi"}, + {"id": 12790, "synset": "pot-au-feu.n.01", "name": "pot-au-feu"}, + {"id": 12791, "synset": "slumgullion.n.01", "name": "slumgullion"}, + {"id": 12792, "synset": "smorgasbord.n.02", "name": "smorgasbord"}, + {"id": 12793, "synset": "viand.n.01", "name": "viand"}, + {"id": 12794, "synset": "ready-mix.n.01", "name": "ready-mix"}, + {"id": 12795, "synset": "brownie_mix.n.01", "name": "brownie_mix"}, + {"id": 12796, "synset": "cake_mix.n.01", "name": "cake_mix"}, + {"id": 12797, "synset": "lemonade_mix.n.01", "name": "lemonade_mix"}, + {"id": 12798, "synset": "self-rising_flour.n.01", "name": "self-rising_flour"}, + {"id": 12799, "synset": "choice_morsel.n.01", "name": "choice_morsel"}, + {"id": 12800, "synset": "savory.n.04", "name": "savory"}, + {"id": 12801, "synset": "calf's-foot_jelly.n.01", "name": "calf's-foot_jelly"}, + {"id": 12802, "synset": "caramel.n.02", "name": "caramel"}, + {"id": 12803, "synset": "lump_sugar.n.01", "name": "lump_sugar"}, + {"id": 12804, "synset": "cane_sugar.n.02", "name": "cane_sugar"}, + {"id": 12805, "synset": "castor_sugar.n.01", "name": "castor_sugar"}, + {"id": 12806, "synset": "powdered_sugar.n.01", "name": "powdered_sugar"}, + {"id": 12807, "synset": "granulated_sugar.n.01", "name": "granulated_sugar"}, + {"id": 12808, "synset": "icing_sugar.n.01", "name": "icing_sugar"}, + {"id": 12809, "synset": "corn_sugar.n.02", "name": "corn_sugar"}, + {"id": 12810, "synset": "brown_sugar.n.01", "name": "brown_sugar"}, + {"id": 12811, "synset": "demerara.n.05", "name": "demerara"}, + {"id": 12812, "synset": "sweet.n.03", "name": "sweet"}, + {"id": 12813, "synset": "confectionery.n.01", "name": "confectionery"}, + {"id": 12814, "synset": "confiture.n.01", "name": "confiture"}, + {"id": 12815, "synset": "sweetmeat.n.01", "name": "sweetmeat"}, + {"id": 12816, "synset": "candy.n.01", "name": "candy"}, + {"id": 12817, "synset": "carob_bar.n.01", "name": "carob_bar"}, + {"id": 12818, "synset": "hardbake.n.01", "name": "hardbake"}, + {"id": 12819, "synset": "hard_candy.n.01", "name": "hard_candy"}, + {"id": 12820, "synset": "barley-sugar.n.01", "name": "barley-sugar"}, + {"id": 12821, "synset": "brandyball.n.01", "name": "brandyball"}, + {"id": 12822, "synset": "jawbreaker.n.01", "name": "jawbreaker"}, + {"id": 12823, "synset": "lemon_drop.n.01", "name": "lemon_drop"}, + {"id": 12824, "synset": "sourball.n.01", "name": "sourball"}, + {"id": 12825, "synset": "patty.n.03", "name": "patty"}, + {"id": 12826, "synset": "peppermint_patty.n.01", "name": "peppermint_patty"}, + {"id": 12827, "synset": "bonbon.n.01", "name": "bonbon"}, + {"id": 12828, "synset": "brittle.n.01", "name": "brittle"}, + {"id": 12829, "synset": "peanut_brittle.n.01", "name": "peanut_brittle"}, + {"id": 12830, "synset": "chewing_gum.n.01", "name": "chewing_gum"}, + {"id": 12831, "synset": "gum_ball.n.01", "name": "gum_ball"}, + {"id": 12832, "synset": "butterscotch.n.01", "name": "butterscotch"}, + {"id": 12833, "synset": "candied_fruit.n.01", "name": "candied_fruit"}, + {"id": 12834, "synset": "candied_apple.n.01", "name": "candied_apple"}, + {"id": 12835, "synset": "crystallized_ginger.n.01", "name": "crystallized_ginger"}, + {"id": 12836, "synset": "grapefruit_peel.n.01", "name": "grapefruit_peel"}, + {"id": 12837, "synset": "lemon_peel.n.02", "name": "lemon_peel"}, + {"id": 12838, "synset": "orange_peel.n.02", "name": "orange_peel"}, + {"id": 12839, "synset": "candied_citrus_peel.n.01", "name": "candied_citrus_peel"}, + {"id": 12840, "synset": "candy_corn.n.01", "name": "candy_corn"}, + {"id": 12841, "synset": "caramel.n.01", "name": "caramel"}, + {"id": 12842, "synset": "center.n.14", "name": "center"}, + {"id": 12843, "synset": "comfit.n.01", "name": "comfit"}, + {"id": 12844, "synset": "cotton_candy.n.01", "name": "cotton_candy"}, + {"id": 12845, "synset": "dragee.n.02", "name": "dragee"}, + {"id": 12846, "synset": "dragee.n.01", "name": "dragee"}, + {"id": 12847, "synset": "fondant.n.01", "name": "fondant"}, + {"id": 12848, "synset": "chocolate_fudge.n.01", "name": "chocolate_fudge"}, + {"id": 12849, "synset": "divinity.n.03", "name": "divinity"}, + {"id": 12850, "synset": "penuche.n.01", "name": "penuche"}, + {"id": 12851, "synset": "gumdrop.n.01", "name": "gumdrop"}, + {"id": 12852, "synset": "jujube.n.03", "name": "jujube"}, + {"id": 12853, "synset": "honey_crisp.n.01", "name": "honey_crisp"}, + {"id": 12854, "synset": "horehound.n.02", "name": "horehound"}, + {"id": 12855, "synset": "peppermint.n.03", "name": "peppermint"}, + {"id": 12856, "synset": "kiss.n.03", "name": "kiss"}, + {"id": 12857, "synset": "molasses_kiss.n.01", "name": "molasses_kiss"}, + {"id": 12858, "synset": "meringue_kiss.n.01", "name": "meringue_kiss"}, + {"id": 12859, "synset": "chocolate_kiss.n.01", "name": "chocolate_kiss"}, + {"id": 12860, "synset": "licorice.n.02", "name": "licorice"}, + {"id": 12861, "synset": "life_saver.n.01", "name": "Life_Saver"}, + {"id": 12862, "synset": "lozenge.n.01", "name": "lozenge"}, + {"id": 12863, "synset": "cachou.n.01", "name": "cachou"}, + {"id": 12864, "synset": "cough_drop.n.01", "name": "cough_drop"}, + {"id": 12865, "synset": "marshmallow.n.01", "name": "marshmallow"}, + {"id": 12866, "synset": "marzipan.n.01", "name": "marzipan"}, + {"id": 12867, "synset": "nougat.n.01", "name": "nougat"}, + {"id": 12868, "synset": "nougat_bar.n.01", "name": "nougat_bar"}, + {"id": 12869, "synset": "nut_bar.n.01", "name": "nut_bar"}, + {"id": 12870, "synset": "peanut_bar.n.01", "name": "peanut_bar"}, + {"id": 12871, "synset": "popcorn_ball.n.01", "name": "popcorn_ball"}, + {"id": 12872, "synset": "praline.n.01", "name": "praline"}, + {"id": 12873, "synset": "rock_candy.n.02", "name": "rock_candy"}, + {"id": 12874, "synset": "rock_candy.n.01", "name": "rock_candy"}, + {"id": 12875, "synset": "sugar_candy.n.01", "name": "sugar_candy"}, + {"id": 12876, "synset": "sugarplum.n.01", "name": "sugarplum"}, + {"id": 12877, "synset": "taffy.n.01", "name": "taffy"}, + {"id": 12878, "synset": "molasses_taffy.n.01", "name": "molasses_taffy"}, + {"id": 12879, "synset": "turkish_delight.n.01", "name": "Turkish_Delight"}, + {"id": 12880, "synset": "dessert.n.01", "name": "dessert"}, + {"id": 12881, "synset": "ambrosia.n.04", "name": "ambrosia"}, + {"id": 12882, "synset": "ambrosia.n.03", "name": "ambrosia"}, + {"id": 12883, "synset": "baked_alaska.n.01", "name": "baked_Alaska"}, + {"id": 12884, "synset": "blancmange.n.01", "name": "blancmange"}, + {"id": 12885, "synset": "charlotte.n.02", "name": "charlotte"}, + {"id": 12886, "synset": "compote.n.01", "name": "compote"}, + {"id": 12887, "synset": "dumpling.n.02", "name": "dumpling"}, + {"id": 12888, "synset": "flan.n.01", "name": "flan"}, + {"id": 12889, "synset": "frozen_dessert.n.01", "name": "frozen_dessert"}, + {"id": 12890, "synset": "junket.n.01", "name": "junket"}, + {"id": 12891, "synset": "mousse.n.02", "name": "mousse"}, + {"id": 12892, "synset": "mousse.n.01", "name": "mousse"}, + {"id": 12893, "synset": "pavlova.n.02", "name": "pavlova"}, + {"id": 12894, "synset": "peach_melba.n.01", "name": "peach_melba"}, + {"id": 12895, "synset": "whip.n.03", "name": "whip"}, + {"id": 12896, "synset": "prune_whip.n.01", "name": "prune_whip"}, + {"id": 12897, "synset": "pudding.n.03", "name": "pudding"}, + {"id": 12898, "synset": "pudding.n.02", "name": "pudding"}, + {"id": 12899, "synset": "syllabub.n.02", "name": "syllabub"}, + {"id": 12900, "synset": "tiramisu.n.01", "name": "tiramisu"}, + {"id": 12901, "synset": "trifle.n.01", "name": "trifle"}, + {"id": 12902, "synset": "tipsy_cake.n.01", "name": "tipsy_cake"}, + {"id": 12903, "synset": "jello.n.01", "name": "jello"}, + {"id": 12904, "synset": "apple_dumpling.n.01", "name": "apple_dumpling"}, + {"id": 12905, "synset": "ice.n.05", "name": "ice"}, + {"id": 12906, "synset": "water_ice.n.02", "name": "water_ice"}, + {"id": 12907, "synset": "ice-cream_cone.n.01", "name": "ice-cream_cone"}, + {"id": 12908, "synset": "chocolate_ice_cream.n.01", "name": "chocolate_ice_cream"}, + {"id": 12909, "synset": "neapolitan_ice_cream.n.01", "name": "Neapolitan_ice_cream"}, + {"id": 12910, "synset": "peach_ice_cream.n.01", "name": "peach_ice_cream"}, + {"id": 12911, "synset": "strawberry_ice_cream.n.01", "name": "strawberry_ice_cream"}, + {"id": 12912, "synset": "tutti-frutti.n.01", "name": "tutti-frutti"}, + {"id": 12913, "synset": "vanilla_ice_cream.n.01", "name": "vanilla_ice_cream"}, + {"id": 12914, "synset": "ice_milk.n.01", "name": "ice_milk"}, + {"id": 12915, "synset": "frozen_yogurt.n.01", "name": "frozen_yogurt"}, + {"id": 12916, "synset": "snowball.n.03", "name": "snowball"}, + {"id": 12917, "synset": "snowball.n.02", "name": "snowball"}, + {"id": 12918, "synset": "parfait.n.01", "name": "parfait"}, + {"id": 12919, "synset": "ice-cream_sundae.n.01", "name": "ice-cream_sundae"}, + {"id": 12920, "synset": "split.n.07", "name": "split"}, + {"id": 12921, "synset": "banana_split.n.01", "name": "banana_split"}, + {"id": 12922, "synset": "frozen_pudding.n.01", "name": "frozen_pudding"}, + {"id": 12923, "synset": "frozen_custard.n.01", "name": "frozen_custard"}, + {"id": 12924, "synset": "flummery.n.01", "name": "flummery"}, + {"id": 12925, "synset": "fish_mousse.n.01", "name": "fish_mousse"}, + {"id": 12926, "synset": "chicken_mousse.n.01", "name": "chicken_mousse"}, + {"id": 12927, "synset": "plum_pudding.n.01", "name": "plum_pudding"}, + {"id": 12928, "synset": "carrot_pudding.n.01", "name": "carrot_pudding"}, + {"id": 12929, "synset": "corn_pudding.n.01", "name": "corn_pudding"}, + {"id": 12930, "synset": "steamed_pudding.n.01", "name": "steamed_pudding"}, + {"id": 12931, "synset": "duff.n.01", "name": "duff"}, + {"id": 12932, "synset": "vanilla_pudding.n.01", "name": "vanilla_pudding"}, + {"id": 12933, "synset": "chocolate_pudding.n.01", "name": "chocolate_pudding"}, + {"id": 12934, "synset": "brown_betty.n.01", "name": "brown_Betty"}, + {"id": 12935, "synset": "nesselrode.n.01", "name": "Nesselrode"}, + {"id": 12936, "synset": "pease_pudding.n.01", "name": "pease_pudding"}, + {"id": 12937, "synset": "custard.n.01", "name": "custard"}, + {"id": 12938, "synset": "creme_caramel.n.01", "name": "creme_caramel"}, + {"id": 12939, "synset": "creme_anglais.n.01", "name": "creme_anglais"}, + {"id": 12940, "synset": "creme_brulee.n.01", "name": "creme_brulee"}, + {"id": 12941, "synset": "fruit_custard.n.01", "name": "fruit_custard"}, + {"id": 12942, "synset": "tapioca.n.01", "name": "tapioca"}, + {"id": 12943, "synset": "tapioca_pudding.n.01", "name": "tapioca_pudding"}, + {"id": 12944, "synset": "roly-poly.n.02", "name": "roly-poly"}, + {"id": 12945, "synset": "suet_pudding.n.01", "name": "suet_pudding"}, + {"id": 12946, "synset": "bavarian_cream.n.01", "name": "Bavarian_cream"}, + {"id": 12947, "synset": "maraschino.n.02", "name": "maraschino"}, + {"id": 12948, "synset": "nonpareil.n.02", "name": "nonpareil"}, + {"id": 12949, "synset": "zabaglione.n.01", "name": "zabaglione"}, + {"id": 12950, "synset": "garnish.n.01", "name": "garnish"}, + {"id": 12951, "synset": "pastry.n.01", "name": "pastry"}, + {"id": 12952, "synset": "turnover.n.02", "name": "turnover"}, + {"id": 12953, "synset": "apple_turnover.n.01", "name": "apple_turnover"}, + {"id": 12954, "synset": "knish.n.01", "name": "knish"}, + {"id": 12955, "synset": "pirogi.n.01", "name": "pirogi"}, + {"id": 12956, "synset": "samosa.n.01", "name": "samosa"}, + {"id": 12957, "synset": "timbale.n.01", "name": "timbale"}, + {"id": 12958, "synset": "puff_paste.n.01", "name": "puff_paste"}, + {"id": 12959, "synset": "phyllo.n.01", "name": "phyllo"}, + {"id": 12960, "synset": "puff_batter.n.01", "name": "puff_batter"}, + {"id": 12961, "synset": "ice-cream_cake.n.01", "name": "ice-cream_cake"}, + {"id": 12962, "synset": "fish_cake.n.01", "name": "fish_cake"}, + {"id": 12963, "synset": "fish_stick.n.01", "name": "fish_stick"}, + {"id": 12964, "synset": "conserve.n.01", "name": "conserve"}, + {"id": 12965, "synset": "apple_butter.n.01", "name": "apple_butter"}, + {"id": 12966, "synset": "chowchow.n.02", "name": "chowchow"}, + {"id": 12967, "synset": "lemon_curd.n.01", "name": "lemon_curd"}, + {"id": 12968, "synset": "strawberry_jam.n.01", "name": "strawberry_jam"}, + {"id": 12969, "synset": "jelly.n.02", "name": "jelly"}, + {"id": 12970, "synset": "apple_jelly.n.01", "name": "apple_jelly"}, + {"id": 12971, "synset": "crabapple_jelly.n.01", "name": "crabapple_jelly"}, + {"id": 12972, "synset": "grape_jelly.n.01", "name": "grape_jelly"}, + {"id": 12973, "synset": "marmalade.n.01", "name": "marmalade"}, + {"id": 12974, "synset": "orange_marmalade.n.01", "name": "orange_marmalade"}, + {"id": 12975, "synset": "gelatin_dessert.n.01", "name": "gelatin_dessert"}, + {"id": 12976, "synset": "buffalo_wing.n.01", "name": "buffalo_wing"}, + {"id": 12977, "synset": "barbecued_wing.n.01", "name": "barbecued_wing"}, + {"id": 12978, "synset": "mess.n.03", "name": "mess"}, + {"id": 12979, "synset": "mince.n.01", "name": "mince"}, + {"id": 12980, "synset": "puree.n.01", "name": "puree"}, + {"id": 12981, "synset": "barbecue.n.01", "name": "barbecue"}, + {"id": 12982, "synset": "biryani.n.01", "name": "biryani"}, + {"id": 12983, "synset": "escalope_de_veau_orloff.n.01", "name": "escalope_de_veau_Orloff"}, + {"id": 12984, "synset": "saute.n.01", "name": "saute"}, + {"id": 12985, "synset": "veal_parmesan.n.01", "name": "veal_parmesan"}, + {"id": 12986, "synset": "veal_cordon_bleu.n.01", "name": "veal_cordon_bleu"}, + {"id": 12987, "synset": "margarine.n.01", "name": "margarine"}, + {"id": 12988, "synset": "mincemeat.n.01", "name": "mincemeat"}, + {"id": 12989, "synset": "stuffing.n.01", "name": "stuffing"}, + {"id": 12990, "synset": "turkey_stuffing.n.01", "name": "turkey_stuffing"}, + {"id": 12991, "synset": "oyster_stuffing.n.01", "name": "oyster_stuffing"}, + {"id": 12992, "synset": "forcemeat.n.01", "name": "forcemeat"}, + {"id": 12993, "synset": "anadama_bread.n.01", "name": "anadama_bread"}, + {"id": 12994, "synset": "bap.n.01", "name": "bap"}, + {"id": 12995, "synset": "barmbrack.n.01", "name": "barmbrack"}, + {"id": 12996, "synset": "breadstick.n.01", "name": "breadstick"}, + {"id": 12997, "synset": "grissino.n.01", "name": "grissino"}, + {"id": 12998, "synset": "brown_bread.n.02", "name": "brown_bread"}, + {"id": 12999, "synset": "tea_bread.n.01", "name": "tea_bread"}, + {"id": 13000, "synset": "caraway_seed_bread.n.01", "name": "caraway_seed_bread"}, + {"id": 13001, "synset": "challah.n.01", "name": "challah"}, + {"id": 13002, "synset": "cinnamon_bread.n.01", "name": "cinnamon_bread"}, + {"id": 13003, "synset": "cracked-wheat_bread.n.01", "name": "cracked-wheat_bread"}, + {"id": 13004, "synset": "dark_bread.n.01", "name": "dark_bread"}, + {"id": 13005, "synset": "english_muffin.n.01", "name": "English_muffin"}, + {"id": 13006, "synset": "flatbread.n.01", "name": "flatbread"}, + {"id": 13007, "synset": "garlic_bread.n.01", "name": "garlic_bread"}, + {"id": 13008, "synset": "gluten_bread.n.01", "name": "gluten_bread"}, + {"id": 13009, "synset": "graham_bread.n.01", "name": "graham_bread"}, + {"id": 13010, "synset": "host.n.09", "name": "Host"}, + {"id": 13011, "synset": "flatbrod.n.01", "name": "flatbrod"}, + {"id": 13012, "synset": "bannock.n.01", "name": "bannock"}, + {"id": 13013, "synset": "chapatti.n.01", "name": "chapatti"}, + {"id": 13014, "synset": "loaf_of_bread.n.01", "name": "loaf_of_bread"}, + {"id": 13015, "synset": "french_loaf.n.01", "name": "French_loaf"}, + {"id": 13016, "synset": "matzo.n.01", "name": "matzo"}, + {"id": 13017, "synset": "nan.n.04", "name": "nan"}, + {"id": 13018, "synset": "onion_bread.n.01", "name": "onion_bread"}, + {"id": 13019, "synset": "raisin_bread.n.01", "name": "raisin_bread"}, + {"id": 13020, "synset": "quick_bread.n.01", "name": "quick_bread"}, + {"id": 13021, "synset": "banana_bread.n.01", "name": "banana_bread"}, + {"id": 13022, "synset": "date_bread.n.01", "name": "date_bread"}, + {"id": 13023, "synset": "date-nut_bread.n.01", "name": "date-nut_bread"}, + {"id": 13024, "synset": "nut_bread.n.01", "name": "nut_bread"}, + {"id": 13025, "synset": "oatcake.n.01", "name": "oatcake"}, + {"id": 13026, "synset": "irish_soda_bread.n.01", "name": "Irish_soda_bread"}, + {"id": 13027, "synset": "skillet_bread.n.01", "name": "skillet_bread"}, + {"id": 13028, "synset": "rye_bread.n.01", "name": "rye_bread"}, + {"id": 13029, "synset": "black_bread.n.01", "name": "black_bread"}, + {"id": 13030, "synset": "jewish_rye_bread.n.01", "name": "Jewish_rye_bread"}, + {"id": 13031, "synset": "limpa.n.01", "name": "limpa"}, + {"id": 13032, "synset": "swedish_rye_bread.n.01", "name": "Swedish_rye_bread"}, + {"id": 13033, "synset": "salt-rising_bread.n.01", "name": "salt-rising_bread"}, + {"id": 13034, "synset": "simnel.n.01", "name": "simnel"}, + {"id": 13035, "synset": "sour_bread.n.01", "name": "sour_bread"}, + {"id": 13036, "synset": "wafer.n.03", "name": "wafer"}, + {"id": 13037, "synset": "white_bread.n.01", "name": "white_bread"}, + {"id": 13038, "synset": "french_bread.n.01", "name": "French_bread"}, + {"id": 13039, "synset": "italian_bread.n.01", "name": "Italian_bread"}, + {"id": 13040, "synset": "corn_cake.n.01", "name": "corn_cake"}, + {"id": 13041, "synset": "skillet_corn_bread.n.01", "name": "skillet_corn_bread"}, + {"id": 13042, "synset": "ashcake.n.01", "name": "ashcake"}, + {"id": 13043, "synset": "hoecake.n.01", "name": "hoecake"}, + {"id": 13044, "synset": "cornpone.n.01", "name": "cornpone"}, + {"id": 13045, "synset": "corn_dab.n.01", "name": "corn_dab"}, + {"id": 13046, "synset": "hush_puppy.n.01", "name": "hush_puppy"}, + {"id": 13047, "synset": "johnnycake.n.01", "name": "johnnycake"}, + {"id": 13048, "synset": "shawnee_cake.n.01", "name": "Shawnee_cake"}, + {"id": 13049, "synset": "spoon_bread.n.01", "name": "spoon_bread"}, + {"id": 13050, "synset": "cinnamon_toast.n.01", "name": "cinnamon_toast"}, + {"id": 13051, "synset": "orange_toast.n.01", "name": "orange_toast"}, + {"id": 13052, "synset": "melba_toast.n.01", "name": "Melba_toast"}, + {"id": 13053, "synset": "zwieback.n.01", "name": "zwieback"}, + {"id": 13054, "synset": "frankfurter_bun.n.01", "name": "frankfurter_bun"}, + {"id": 13055, "synset": "hamburger_bun.n.01", "name": "hamburger_bun"}, + {"id": 13056, "synset": "bran_muffin.n.01", "name": "bran_muffin"}, + {"id": 13057, "synset": "corn_muffin.n.01", "name": "corn_muffin"}, + {"id": 13058, "synset": "yorkshire_pudding.n.01", "name": "Yorkshire_pudding"}, + {"id": 13059, "synset": "popover.n.01", "name": "popover"}, + {"id": 13060, "synset": "scone.n.01", "name": "scone"}, + {"id": 13061, "synset": "drop_scone.n.01", "name": "drop_scone"}, + {"id": 13062, "synset": "cross_bun.n.01", "name": "cross_bun"}, + {"id": 13063, "synset": "brioche.n.01", "name": "brioche"}, + {"id": 13064, "synset": "hard_roll.n.01", "name": "hard_roll"}, + {"id": 13065, "synset": "soft_roll.n.01", "name": "soft_roll"}, + {"id": 13066, "synset": "kaiser_roll.n.01", "name": "kaiser_roll"}, + {"id": 13067, "synset": "parker_house_roll.n.01", "name": "Parker_House_roll"}, + {"id": 13068, "synset": "clover-leaf_roll.n.01", "name": "clover-leaf_roll"}, + {"id": 13069, "synset": "onion_roll.n.01", "name": "onion_roll"}, + {"id": 13070, "synset": "bialy.n.01", "name": "bialy"}, + {"id": 13071, "synset": "sweet_roll.n.01", "name": "sweet_roll"}, + {"id": 13072, "synset": "bear_claw.n.01", "name": "bear_claw"}, + {"id": 13073, "synset": "cinnamon_roll.n.01", "name": "cinnamon_roll"}, + {"id": 13074, "synset": "honey_bun.n.01", "name": "honey_bun"}, + {"id": 13075, "synset": "pinwheel_roll.n.01", "name": "pinwheel_roll"}, + {"id": 13076, "synset": "danish.n.02", "name": "danish"}, + {"id": 13077, "synset": "onion_bagel.n.01", "name": "onion_bagel"}, + {"id": 13078, "synset": "biscuit.n.01", "name": "biscuit"}, + {"id": 13079, "synset": "rolled_biscuit.n.01", "name": "rolled_biscuit"}, + {"id": 13080, "synset": "baking-powder_biscuit.n.01", "name": "baking-powder_biscuit"}, + {"id": 13081, "synset": "buttermilk_biscuit.n.01", "name": "buttermilk_biscuit"}, + {"id": 13082, "synset": "shortcake.n.01", "name": "shortcake"}, + {"id": 13083, "synset": "hardtack.n.01", "name": "hardtack"}, + {"id": 13084, "synset": "saltine.n.01", "name": "saltine"}, + {"id": 13085, "synset": "soda_cracker.n.01", "name": "soda_cracker"}, + {"id": 13086, "synset": "oyster_cracker.n.01", "name": "oyster_cracker"}, + {"id": 13087, "synset": "water_biscuit.n.01", "name": "water_biscuit"}, + {"id": 13088, "synset": "graham_cracker.n.01", "name": "graham_cracker"}, + {"id": 13089, "synset": "soft_pretzel.n.01", "name": "soft_pretzel"}, + {"id": 13090, "synset": "sandwich_plate.n.01", "name": "sandwich_plate"}, + {"id": 13091, "synset": "butty.n.01", "name": "butty"}, + {"id": 13092, "synset": "ham_sandwich.n.01", "name": "ham_sandwich"}, + {"id": 13093, "synset": "chicken_sandwich.n.01", "name": "chicken_sandwich"}, + {"id": 13094, "synset": "club_sandwich.n.01", "name": "club_sandwich"}, + {"id": 13095, "synset": "open-face_sandwich.n.01", "name": "open-face_sandwich"}, + {"id": 13096, "synset": "cheeseburger.n.01", "name": "cheeseburger"}, + {"id": 13097, "synset": "tunaburger.n.01", "name": "tunaburger"}, + {"id": 13098, "synset": "hotdog.n.02", "name": "hotdog"}, + {"id": 13099, "synset": "sloppy_joe.n.01", "name": "Sloppy_Joe"}, + {"id": 13100, "synset": "bomber.n.03", "name": "bomber"}, + {"id": 13101, "synset": "gyro.n.01", "name": "gyro"}, + { + "id": 13102, + "synset": "bacon-lettuce-tomato_sandwich.n.01", + "name": "bacon-lettuce-tomato_sandwich", + }, + {"id": 13103, "synset": "reuben.n.02", "name": "Reuben"}, + {"id": 13104, "synset": "western.n.02", "name": "western"}, + {"id": 13105, "synset": "wrap.n.02", "name": "wrap"}, + {"id": 13106, "synset": "spaghetti.n.01", "name": "spaghetti"}, + {"id": 13107, "synset": "hasty_pudding.n.01", "name": "hasty_pudding"}, + {"id": 13108, "synset": "gruel.n.01", "name": "gruel"}, + {"id": 13109, "synset": "congee.n.01", "name": "congee"}, + {"id": 13110, "synset": "skilly.n.01", "name": "skilly"}, + {"id": 13111, "synset": "edible_fruit.n.01", "name": "edible_fruit"}, + {"id": 13112, "synset": "vegetable.n.01", "name": "vegetable"}, + {"id": 13113, "synset": "julienne.n.01", "name": "julienne"}, + {"id": 13114, "synset": "raw_vegetable.n.01", "name": "raw_vegetable"}, + {"id": 13115, "synset": "crudites.n.01", "name": "crudites"}, + {"id": 13116, "synset": "celery_stick.n.01", "name": "celery_stick"}, + {"id": 13117, "synset": "legume.n.03", "name": "legume"}, + {"id": 13118, "synset": "pulse.n.04", "name": "pulse"}, + {"id": 13119, "synset": "potherb.n.01", "name": "potherb"}, + {"id": 13120, "synset": "greens.n.01", "name": "greens"}, + {"id": 13121, "synset": "chop-suey_greens.n.02", "name": "chop-suey_greens"}, + {"id": 13122, "synset": "solanaceous_vegetable.n.01", "name": "solanaceous_vegetable"}, + {"id": 13123, "synset": "root_vegetable.n.01", "name": "root_vegetable"}, + {"id": 13124, "synset": "baked_potato.n.01", "name": "baked_potato"}, + {"id": 13125, "synset": "french_fries.n.01", "name": "french_fries"}, + {"id": 13126, "synset": "home_fries.n.01", "name": "home_fries"}, + {"id": 13127, "synset": "jacket_potato.n.01", "name": "jacket_potato"}, + {"id": 13128, "synset": "potato_skin.n.01", "name": "potato_skin"}, + {"id": 13129, "synset": "uruguay_potato.n.02", "name": "Uruguay_potato"}, + {"id": 13130, "synset": "yam.n.04", "name": "yam"}, + {"id": 13131, "synset": "yam.n.03", "name": "yam"}, + {"id": 13132, "synset": "snack_food.n.01", "name": "snack_food"}, + {"id": 13133, "synset": "corn_chip.n.01", "name": "corn_chip"}, + {"id": 13134, "synset": "tortilla_chip.n.01", "name": "tortilla_chip"}, + {"id": 13135, "synset": "nacho.n.01", "name": "nacho"}, + {"id": 13136, "synset": "pieplant.n.01", "name": "pieplant"}, + {"id": 13137, "synset": "cruciferous_vegetable.n.01", "name": "cruciferous_vegetable"}, + {"id": 13138, "synset": "mustard.n.03", "name": "mustard"}, + {"id": 13139, "synset": "cabbage.n.01", "name": "cabbage"}, + {"id": 13140, "synset": "kale.n.03", "name": "kale"}, + {"id": 13141, "synset": "collards.n.01", "name": "collards"}, + {"id": 13142, "synset": "chinese_cabbage.n.02", "name": "Chinese_cabbage"}, + {"id": 13143, "synset": "bok_choy.n.02", "name": "bok_choy"}, + {"id": 13144, "synset": "head_cabbage.n.02", "name": "head_cabbage"}, + {"id": 13145, "synset": "red_cabbage.n.02", "name": "red_cabbage"}, + {"id": 13146, "synset": "savoy_cabbage.n.02", "name": "savoy_cabbage"}, + {"id": 13147, "synset": "broccoli.n.02", "name": "broccoli"}, + {"id": 13148, "synset": "broccoli_rabe.n.02", "name": "broccoli_rabe"}, + {"id": 13149, "synset": "squash.n.02", "name": "squash"}, + {"id": 13150, "synset": "summer_squash.n.02", "name": "summer_squash"}, + {"id": 13151, "synset": "yellow_squash.n.02", "name": "yellow_squash"}, + {"id": 13152, "synset": "crookneck.n.01", "name": "crookneck"}, + {"id": 13153, "synset": "marrow.n.04", "name": "marrow"}, + {"id": 13154, "synset": "cocozelle.n.02", "name": "cocozelle"}, + {"id": 13155, "synset": "pattypan_squash.n.02", "name": "pattypan_squash"}, + {"id": 13156, "synset": "spaghetti_squash.n.02", "name": "spaghetti_squash"}, + {"id": 13157, "synset": "winter_squash.n.02", "name": "winter_squash"}, + {"id": 13158, "synset": "acorn_squash.n.02", "name": "acorn_squash"}, + {"id": 13159, "synset": "butternut_squash.n.02", "name": "butternut_squash"}, + {"id": 13160, "synset": "hubbard_squash.n.02", "name": "hubbard_squash"}, + {"id": 13161, "synset": "turban_squash.n.02", "name": "turban_squash"}, + {"id": 13162, "synset": "buttercup_squash.n.02", "name": "buttercup_squash"}, + {"id": 13163, "synset": "cushaw.n.02", "name": "cushaw"}, + {"id": 13164, "synset": "winter_crookneck_squash.n.02", "name": "winter_crookneck_squash"}, + {"id": 13165, "synset": "gherkin.n.02", "name": "gherkin"}, + {"id": 13166, "synset": "artichoke_heart.n.01", "name": "artichoke_heart"}, + {"id": 13167, "synset": "jerusalem_artichoke.n.03", "name": "Jerusalem_artichoke"}, + {"id": 13168, "synset": "bamboo_shoot.n.01", "name": "bamboo_shoot"}, + {"id": 13169, "synset": "sprout.n.02", "name": "sprout"}, + {"id": 13170, "synset": "bean_sprout.n.01", "name": "bean_sprout"}, + {"id": 13171, "synset": "alfalfa_sprout.n.01", "name": "alfalfa_sprout"}, + {"id": 13172, "synset": "beet.n.02", "name": "beet"}, + {"id": 13173, "synset": "beet_green.n.01", "name": "beet_green"}, + {"id": 13174, "synset": "sugar_beet.n.02", "name": "sugar_beet"}, + {"id": 13175, "synset": "mangel-wurzel.n.02", "name": "mangel-wurzel"}, + {"id": 13176, "synset": "chard.n.02", "name": "chard"}, + {"id": 13177, "synset": "pepper.n.04", "name": "pepper"}, + {"id": 13178, "synset": "sweet_pepper.n.02", "name": "sweet_pepper"}, + {"id": 13179, "synset": "green_pepper.n.01", "name": "green_pepper"}, + {"id": 13180, "synset": "globe_pepper.n.01", "name": "globe_pepper"}, + {"id": 13181, "synset": "pimento.n.02", "name": "pimento"}, + {"id": 13182, "synset": "hot_pepper.n.02", "name": "hot_pepper"}, + {"id": 13183, "synset": "jalapeno.n.02", "name": "jalapeno"}, + {"id": 13184, "synset": "chipotle.n.01", "name": "chipotle"}, + {"id": 13185, "synset": "cayenne.n.03", "name": "cayenne"}, + {"id": 13186, "synset": "tabasco.n.03", "name": "tabasco"}, + {"id": 13187, "synset": "onion.n.03", "name": "onion"}, + {"id": 13188, "synset": "bermuda_onion.n.01", "name": "Bermuda_onion"}, + {"id": 13189, "synset": "vidalia_onion.n.01", "name": "Vidalia_onion"}, + {"id": 13190, "synset": "spanish_onion.n.01", "name": "Spanish_onion"}, + {"id": 13191, "synset": "purple_onion.n.01", "name": "purple_onion"}, + {"id": 13192, "synset": "leek.n.02", "name": "leek"}, + {"id": 13193, "synset": "shallot.n.03", "name": "shallot"}, + {"id": 13194, "synset": "salad_green.n.01", "name": "salad_green"}, + {"id": 13195, "synset": "lettuce.n.03", "name": "lettuce"}, + {"id": 13196, "synset": "butterhead_lettuce.n.01", "name": "butterhead_lettuce"}, + {"id": 13197, "synset": "buttercrunch.n.01", "name": "buttercrunch"}, + {"id": 13198, "synset": "bibb_lettuce.n.01", "name": "Bibb_lettuce"}, + {"id": 13199, "synset": "boston_lettuce.n.01", "name": "Boston_lettuce"}, + {"id": 13200, "synset": "crisphead_lettuce.n.01", "name": "crisphead_lettuce"}, + {"id": 13201, "synset": "cos.n.02", "name": "cos"}, + {"id": 13202, "synset": "leaf_lettuce.n.02", "name": "leaf_lettuce"}, + {"id": 13203, "synset": "celtuce.n.02", "name": "celtuce"}, + {"id": 13204, "synset": "bean.n.01", "name": "bean"}, + {"id": 13205, "synset": "goa_bean.n.02", "name": "goa_bean"}, + {"id": 13206, "synset": "lentil.n.01", "name": "lentil"}, + {"id": 13207, "synset": "green_pea.n.01", "name": "green_pea"}, + {"id": 13208, "synset": "marrowfat_pea.n.01", "name": "marrowfat_pea"}, + {"id": 13209, "synset": "snow_pea.n.02", "name": "snow_pea"}, + {"id": 13210, "synset": "sugar_snap_pea.n.02", "name": "sugar_snap_pea"}, + {"id": 13211, "synset": "split-pea.n.01", "name": "split-pea"}, + {"id": 13212, "synset": "chickpea.n.03", "name": "chickpea"}, + {"id": 13213, "synset": "cajan_pea.n.02", "name": "cajan_pea"}, + {"id": 13214, "synset": "field_pea.n.03", "name": "field_pea"}, + {"id": 13215, "synset": "mushy_peas.n.01", "name": "mushy_peas"}, + {"id": 13216, "synset": "black-eyed_pea.n.03", "name": "black-eyed_pea"}, + {"id": 13217, "synset": "common_bean.n.02", "name": "common_bean"}, + {"id": 13218, "synset": "kidney_bean.n.02", "name": "kidney_bean"}, + {"id": 13219, "synset": "navy_bean.n.01", "name": "navy_bean"}, + {"id": 13220, "synset": "pinto_bean.n.01", "name": "pinto_bean"}, + {"id": 13221, "synset": "frijole.n.02", "name": "frijole"}, + {"id": 13222, "synset": "black_bean.n.01", "name": "black_bean"}, + {"id": 13223, "synset": "fresh_bean.n.01", "name": "fresh_bean"}, + {"id": 13224, "synset": "flageolet.n.01", "name": "flageolet"}, + {"id": 13225, "synset": "green_bean.n.01", "name": "green_bean"}, + {"id": 13226, "synset": "snap_bean.n.01", "name": "snap_bean"}, + {"id": 13227, "synset": "string_bean.n.01", "name": "string_bean"}, + {"id": 13228, "synset": "kentucky_wonder.n.01", "name": "Kentucky_wonder"}, + {"id": 13229, "synset": "scarlet_runner.n.03", "name": "scarlet_runner"}, + {"id": 13230, "synset": "haricot_vert.n.01", "name": "haricot_vert"}, + {"id": 13231, "synset": "wax_bean.n.02", "name": "wax_bean"}, + {"id": 13232, "synset": "shell_bean.n.02", "name": "shell_bean"}, + {"id": 13233, "synset": "lima_bean.n.03", "name": "lima_bean"}, + {"id": 13234, "synset": "fordhooks.n.01", "name": "Fordhooks"}, + {"id": 13235, "synset": "sieva_bean.n.02", "name": "sieva_bean"}, + {"id": 13236, "synset": "fava_bean.n.02", "name": "fava_bean"}, + {"id": 13237, "synset": "soy.n.04", "name": "soy"}, + {"id": 13238, "synset": "green_soybean.n.01", "name": "green_soybean"}, + {"id": 13239, "synset": "field_soybean.n.01", "name": "field_soybean"}, + {"id": 13240, "synset": "cardoon.n.02", "name": "cardoon"}, + {"id": 13241, "synset": "carrot.n.03", "name": "carrot"}, + {"id": 13242, "synset": "carrot_stick.n.01", "name": "carrot_stick"}, + {"id": 13243, "synset": "celery.n.02", "name": "celery"}, + {"id": 13244, "synset": "pascal_celery.n.01", "name": "pascal_celery"}, + {"id": 13245, "synset": "celeriac.n.02", "name": "celeriac"}, + {"id": 13246, "synset": "chicory.n.04", "name": "chicory"}, + {"id": 13247, "synset": "radicchio.n.01", "name": "radicchio"}, + {"id": 13248, "synset": "coffee_substitute.n.01", "name": "coffee_substitute"}, + {"id": 13249, "synset": "chicory.n.03", "name": "chicory"}, + {"id": 13250, "synset": "postum.n.01", "name": "Postum"}, + {"id": 13251, "synset": "chicory_escarole.n.01", "name": "chicory_escarole"}, + {"id": 13252, "synset": "belgian_endive.n.01", "name": "Belgian_endive"}, + {"id": 13253, "synset": "sweet_corn.n.02", "name": "sweet_corn"}, + {"id": 13254, "synset": "hominy.n.01", "name": "hominy"}, + {"id": 13255, "synset": "lye_hominy.n.01", "name": "lye_hominy"}, + {"id": 13256, "synset": "pearl_hominy.n.01", "name": "pearl_hominy"}, + {"id": 13257, "synset": "popcorn.n.02", "name": "popcorn"}, + {"id": 13258, "synset": "cress.n.02", "name": "cress"}, + {"id": 13259, "synset": "watercress.n.02", "name": "watercress"}, + {"id": 13260, "synset": "garden_cress.n.01", "name": "garden_cress"}, + {"id": 13261, "synset": "winter_cress.n.02", "name": "winter_cress"}, + {"id": 13262, "synset": "dandelion_green.n.02", "name": "dandelion_green"}, + {"id": 13263, "synset": "gumbo.n.03", "name": "gumbo"}, + {"id": 13264, "synset": "kohlrabi.n.02", "name": "kohlrabi"}, + {"id": 13265, "synset": "lamb's-quarter.n.01", "name": "lamb's-quarter"}, + {"id": 13266, "synset": "wild_spinach.n.03", "name": "wild_spinach"}, + {"id": 13267, "synset": "beefsteak_tomato.n.01", "name": "beefsteak_tomato"}, + {"id": 13268, "synset": "cherry_tomato.n.02", "name": "cherry_tomato"}, + {"id": 13269, "synset": "plum_tomato.n.02", "name": "plum_tomato"}, + {"id": 13270, "synset": "tomatillo.n.03", "name": "tomatillo"}, + {"id": 13271, "synset": "mushroom.n.05", "name": "mushroom"}, + {"id": 13272, "synset": "stuffed_mushroom.n.01", "name": "stuffed_mushroom"}, + {"id": 13273, "synset": "salsify.n.03", "name": "salsify"}, + {"id": 13274, "synset": "oyster_plant.n.03", "name": "oyster_plant"}, + {"id": 13275, "synset": "scorzonera.n.02", "name": "scorzonera"}, + {"id": 13276, "synset": "parsnip.n.03", "name": "parsnip"}, + {"id": 13277, "synset": "radish.n.01", "name": "radish"}, + {"id": 13278, "synset": "turnip.n.02", "name": "turnip"}, + {"id": 13279, "synset": "white_turnip.n.02", "name": "white_turnip"}, + {"id": 13280, "synset": "rutabaga.n.01", "name": "rutabaga"}, + {"id": 13281, "synset": "turnip_greens.n.01", "name": "turnip_greens"}, + {"id": 13282, "synset": "sorrel.n.04", "name": "sorrel"}, + {"id": 13283, "synset": "french_sorrel.n.02", "name": "French_sorrel"}, + {"id": 13284, "synset": "spinach.n.02", "name": "spinach"}, + {"id": 13285, "synset": "taro.n.03", "name": "taro"}, + {"id": 13286, "synset": "truffle.n.02", "name": "truffle"}, + {"id": 13287, "synset": "edible_nut.n.01", "name": "edible_nut"}, + {"id": 13288, "synset": "bunya_bunya.n.02", "name": "bunya_bunya"}, + {"id": 13289, "synset": "peanut.n.04", "name": "peanut"}, + {"id": 13290, "synset": "freestone.n.01", "name": "freestone"}, + {"id": 13291, "synset": "cling.n.01", "name": "cling"}, + {"id": 13292, "synset": "windfall.n.01", "name": "windfall"}, + {"id": 13293, "synset": "crab_apple.n.03", "name": "crab_apple"}, + {"id": 13294, "synset": "eating_apple.n.01", "name": "eating_apple"}, + {"id": 13295, "synset": "baldwin.n.03", "name": "Baldwin"}, + {"id": 13296, "synset": "cortland.n.01", "name": "Cortland"}, + {"id": 13297, "synset": "cox's_orange_pippin.n.01", "name": "Cox's_Orange_Pippin"}, + {"id": 13298, "synset": "delicious.n.01", "name": "Delicious"}, + {"id": 13299, "synset": "golden_delicious.n.01", "name": "Golden_Delicious"}, + {"id": 13300, "synset": "red_delicious.n.01", "name": "Red_Delicious"}, + {"id": 13301, "synset": "empire.n.05", "name": "Empire"}, + {"id": 13302, "synset": "grimes'_golden.n.01", "name": "Grimes'_golden"}, + {"id": 13303, "synset": "jonathan.n.01", "name": "Jonathan"}, + {"id": 13304, "synset": "mcintosh.n.01", "name": "McIntosh"}, + {"id": 13305, "synset": "macoun.n.01", "name": "Macoun"}, + {"id": 13306, "synset": "northern_spy.n.01", "name": "Northern_Spy"}, + {"id": 13307, "synset": "pearmain.n.01", "name": "Pearmain"}, + {"id": 13308, "synset": "pippin.n.01", "name": "Pippin"}, + {"id": 13309, "synset": "prima.n.01", "name": "Prima"}, + {"id": 13310, "synset": "stayman.n.01", "name": "Stayman"}, + {"id": 13311, "synset": "winesap.n.01", "name": "Winesap"}, + {"id": 13312, "synset": "stayman_winesap.n.01", "name": "Stayman_Winesap"}, + {"id": 13313, "synset": "cooking_apple.n.01", "name": "cooking_apple"}, + {"id": 13314, "synset": "bramley's_seedling.n.01", "name": "Bramley's_Seedling"}, + {"id": 13315, "synset": "granny_smith.n.01", "name": "Granny_Smith"}, + {"id": 13316, "synset": "lane's_prince_albert.n.01", "name": "Lane's_Prince_Albert"}, + {"id": 13317, "synset": "newtown_wonder.n.01", "name": "Newtown_Wonder"}, + {"id": 13318, "synset": "rome_beauty.n.01", "name": "Rome_Beauty"}, + {"id": 13319, "synset": "berry.n.01", "name": "berry"}, + {"id": 13320, "synset": "bilberry.n.03", "name": "bilberry"}, + {"id": 13321, "synset": "huckleberry.n.03", "name": "huckleberry"}, + {"id": 13322, "synset": "wintergreen.n.03", "name": "wintergreen"}, + {"id": 13323, "synset": "cranberry.n.02", "name": "cranberry"}, + {"id": 13324, "synset": "lingonberry.n.02", "name": "lingonberry"}, + {"id": 13325, "synset": "currant.n.01", "name": "currant"}, + {"id": 13326, "synset": "gooseberry.n.02", "name": "gooseberry"}, + {"id": 13327, "synset": "black_currant.n.02", "name": "black_currant"}, + {"id": 13328, "synset": "red_currant.n.02", "name": "red_currant"}, + {"id": 13329, "synset": "boysenberry.n.02", "name": "boysenberry"}, + {"id": 13330, "synset": "dewberry.n.02", "name": "dewberry"}, + {"id": 13331, "synset": "loganberry.n.02", "name": "loganberry"}, + {"id": 13332, "synset": "saskatoon.n.02", "name": "saskatoon"}, + {"id": 13333, "synset": "sugarberry.n.02", "name": "sugarberry"}, + {"id": 13334, "synset": "acerola.n.02", "name": "acerola"}, + {"id": 13335, "synset": "carambola.n.02", "name": "carambola"}, + {"id": 13336, "synset": "ceriman.n.02", "name": "ceriman"}, + {"id": 13337, "synset": "carissa_plum.n.01", "name": "carissa_plum"}, + {"id": 13338, "synset": "citrus.n.01", "name": "citrus"}, + {"id": 13339, "synset": "temple_orange.n.02", "name": "temple_orange"}, + {"id": 13340, "synset": "clementine.n.02", "name": "clementine"}, + {"id": 13341, "synset": "satsuma.n.02", "name": "satsuma"}, + {"id": 13342, "synset": "tangerine.n.02", "name": "tangerine"}, + {"id": 13343, "synset": "tangelo.n.02", "name": "tangelo"}, + {"id": 13344, "synset": "bitter_orange.n.02", "name": "bitter_orange"}, + {"id": 13345, "synset": "sweet_orange.n.01", "name": "sweet_orange"}, + {"id": 13346, "synset": "jaffa_orange.n.01", "name": "Jaffa_orange"}, + {"id": 13347, "synset": "navel_orange.n.01", "name": "navel_orange"}, + {"id": 13348, "synset": "valencia_orange.n.01", "name": "Valencia_orange"}, + {"id": 13349, "synset": "kumquat.n.02", "name": "kumquat"}, + {"id": 13350, "synset": "key_lime.n.01", "name": "key_lime"}, + {"id": 13351, "synset": "grapefruit.n.02", "name": "grapefruit"}, + {"id": 13352, "synset": "pomelo.n.02", "name": "pomelo"}, + {"id": 13353, "synset": "citrange.n.02", "name": "citrange"}, + {"id": 13354, "synset": "citron.n.01", "name": "citron"}, + {"id": 13355, "synset": "jordan_almond.n.02", "name": "Jordan_almond"}, + {"id": 13356, "synset": "nectarine.n.02", "name": "nectarine"}, + {"id": 13357, "synset": "pitahaya.n.02", "name": "pitahaya"}, + {"id": 13358, "synset": "plum.n.02", "name": "plum"}, + {"id": 13359, "synset": "damson.n.01", "name": "damson"}, + {"id": 13360, "synset": "greengage.n.01", "name": "greengage"}, + {"id": 13361, "synset": "beach_plum.n.02", "name": "beach_plum"}, + {"id": 13362, "synset": "sloe.n.03", "name": "sloe"}, + {"id": 13363, "synset": "victoria_plum.n.01", "name": "Victoria_plum"}, + {"id": 13364, "synset": "dried_fruit.n.01", "name": "dried_fruit"}, + {"id": 13365, "synset": "dried_apricot.n.01", "name": "dried_apricot"}, + {"id": 13366, "synset": "raisin.n.01", "name": "raisin"}, + {"id": 13367, "synset": "seedless_raisin.n.01", "name": "seedless_raisin"}, + {"id": 13368, "synset": "seeded_raisin.n.01", "name": "seeded_raisin"}, + {"id": 13369, "synset": "currant.n.03", "name": "currant"}, + {"id": 13370, "synset": "anchovy_pear.n.02", "name": "anchovy_pear"}, + {"id": 13371, "synset": "passion_fruit.n.01", "name": "passion_fruit"}, + {"id": 13372, "synset": "granadilla.n.04", "name": "granadilla"}, + {"id": 13373, "synset": "sweet_calabash.n.02", "name": "sweet_calabash"}, + {"id": 13374, "synset": "bell_apple.n.01", "name": "bell_apple"}, + {"id": 13375, "synset": "breadfruit.n.02", "name": "breadfruit"}, + {"id": 13376, "synset": "jackfruit.n.02", "name": "jackfruit"}, + {"id": 13377, "synset": "cacao_bean.n.01", "name": "cacao_bean"}, + {"id": 13378, "synset": "cocoa.n.02", "name": "cocoa"}, + {"id": 13379, "synset": "canistel.n.02", "name": "canistel"}, + {"id": 13380, "synset": "melon_ball.n.01", "name": "melon_ball"}, + {"id": 13381, "synset": "muskmelon.n.02", "name": "muskmelon"}, + {"id": 13382, "synset": "winter_melon.n.02", "name": "winter_melon"}, + {"id": 13383, "synset": "honeydew.n.01", "name": "honeydew"}, + {"id": 13384, "synset": "persian_melon.n.02", "name": "Persian_melon"}, + {"id": 13385, "synset": "net_melon.n.02", "name": "net_melon"}, + {"id": 13386, "synset": "casaba.n.01", "name": "casaba"}, + {"id": 13387, "synset": "sweet_cherry.n.02", "name": "sweet_cherry"}, + {"id": 13388, "synset": "bing_cherry.n.01", "name": "bing_cherry"}, + {"id": 13389, "synset": "heart_cherry.n.02", "name": "heart_cherry"}, + {"id": 13390, "synset": "blackheart.n.02", "name": "blackheart"}, + {"id": 13391, "synset": "capulin.n.02", "name": "capulin"}, + {"id": 13392, "synset": "sour_cherry.n.03", "name": "sour_cherry"}, + {"id": 13393, "synset": "amarelle.n.02", "name": "amarelle"}, + {"id": 13394, "synset": "morello.n.02", "name": "morello"}, + {"id": 13395, "synset": "cocoa_plum.n.02", "name": "cocoa_plum"}, + {"id": 13396, "synset": "gherkin.n.01", "name": "gherkin"}, + {"id": 13397, "synset": "fox_grape.n.02", "name": "fox_grape"}, + {"id": 13398, "synset": "concord_grape.n.01", "name": "Concord_grape"}, + {"id": 13399, "synset": "catawba.n.02", "name": "Catawba"}, + {"id": 13400, "synset": "muscadine.n.02", "name": "muscadine"}, + {"id": 13401, "synset": "scuppernong.n.01", "name": "scuppernong"}, + {"id": 13402, "synset": "slipskin_grape.n.01", "name": "slipskin_grape"}, + {"id": 13403, "synset": "vinifera_grape.n.02", "name": "vinifera_grape"}, + {"id": 13404, "synset": "emperor.n.02", "name": "emperor"}, + {"id": 13405, "synset": "muscat.n.04", "name": "muscat"}, + {"id": 13406, "synset": "ribier.n.01", "name": "ribier"}, + {"id": 13407, "synset": "sultana.n.01", "name": "sultana"}, + {"id": 13408, "synset": "tokay.n.02", "name": "Tokay"}, + {"id": 13409, "synset": "flame_tokay.n.01", "name": "flame_tokay"}, + {"id": 13410, "synset": "thompson_seedless.n.01", "name": "Thompson_Seedless"}, + {"id": 13411, "synset": "custard_apple.n.02", "name": "custard_apple"}, + {"id": 13412, "synset": "cherimoya.n.02", "name": "cherimoya"}, + {"id": 13413, "synset": "soursop.n.02", "name": "soursop"}, + {"id": 13414, "synset": "sweetsop.n.02", "name": "sweetsop"}, + {"id": 13415, "synset": "ilama.n.02", "name": "ilama"}, + {"id": 13416, "synset": "pond_apple.n.02", "name": "pond_apple"}, + {"id": 13417, "synset": "papaw.n.02", "name": "papaw"}, + {"id": 13418, "synset": "kai_apple.n.01", "name": "kai_apple"}, + {"id": 13419, "synset": "ketembilla.n.02", "name": "ketembilla"}, + {"id": 13420, "synset": "ackee.n.01", "name": "ackee"}, + {"id": 13421, "synset": "durian.n.02", "name": "durian"}, + {"id": 13422, "synset": "feijoa.n.02", "name": "feijoa"}, + {"id": 13423, "synset": "genip.n.02", "name": "genip"}, + {"id": 13424, "synset": "genipap.n.01", "name": "genipap"}, + {"id": 13425, "synset": "loquat.n.02", "name": "loquat"}, + {"id": 13426, "synset": "mangosteen.n.02", "name": "mangosteen"}, + {"id": 13427, "synset": "mango.n.02", "name": "mango"}, + {"id": 13428, "synset": "sapodilla.n.02", "name": "sapodilla"}, + {"id": 13429, "synset": "sapote.n.02", "name": "sapote"}, + {"id": 13430, "synset": "tamarind.n.02", "name": "tamarind"}, + {"id": 13431, "synset": "elderberry.n.02", "name": "elderberry"}, + {"id": 13432, "synset": "guava.n.03", "name": "guava"}, + {"id": 13433, "synset": "mombin.n.02", "name": "mombin"}, + {"id": 13434, "synset": "hog_plum.n.04", "name": "hog_plum"}, + {"id": 13435, "synset": "hog_plum.n.03", "name": "hog_plum"}, + {"id": 13436, "synset": "jaboticaba.n.02", "name": "jaboticaba"}, + {"id": 13437, "synset": "jujube.n.02", "name": "jujube"}, + {"id": 13438, "synset": "litchi.n.02", "name": "litchi"}, + {"id": 13439, "synset": "longanberry.n.02", "name": "longanberry"}, + {"id": 13440, "synset": "mamey.n.02", "name": "mamey"}, + {"id": 13441, "synset": "marang.n.02", "name": "marang"}, + {"id": 13442, "synset": "medlar.n.04", "name": "medlar"}, + {"id": 13443, "synset": "medlar.n.03", "name": "medlar"}, + {"id": 13444, "synset": "mulberry.n.02", "name": "mulberry"}, + {"id": 13445, "synset": "olive.n.04", "name": "olive"}, + {"id": 13446, "synset": "black_olive.n.01", "name": "black_olive"}, + {"id": 13447, "synset": "green_olive.n.01", "name": "green_olive"}, + {"id": 13448, "synset": "bosc.n.01", "name": "bosc"}, + {"id": 13449, "synset": "anjou.n.02", "name": "anjou"}, + {"id": 13450, "synset": "bartlett.n.03", "name": "bartlett"}, + {"id": 13451, "synset": "seckel.n.01", "name": "seckel"}, + {"id": 13452, "synset": "plantain.n.03", "name": "plantain"}, + {"id": 13453, "synset": "plumcot.n.02", "name": "plumcot"}, + {"id": 13454, "synset": "pomegranate.n.02", "name": "pomegranate"}, + {"id": 13455, "synset": "prickly_pear.n.02", "name": "prickly_pear"}, + {"id": 13456, "synset": "barbados_gooseberry.n.02", "name": "Barbados_gooseberry"}, + {"id": 13457, "synset": "quandong.n.04", "name": "quandong"}, + {"id": 13458, "synset": "quandong_nut.n.01", "name": "quandong_nut"}, + {"id": 13459, "synset": "quince.n.02", "name": "quince"}, + {"id": 13460, "synset": "rambutan.n.02", "name": "rambutan"}, + {"id": 13461, "synset": "pulasan.n.02", "name": "pulasan"}, + {"id": 13462, "synset": "rose_apple.n.02", "name": "rose_apple"}, + {"id": 13463, "synset": "sorb.n.01", "name": "sorb"}, + {"id": 13464, "synset": "sour_gourd.n.02", "name": "sour_gourd"}, + {"id": 13465, "synset": "edible_seed.n.01", "name": "edible_seed"}, + {"id": 13466, "synset": "pumpkin_seed.n.01", "name": "pumpkin_seed"}, + {"id": 13467, "synset": "betel_nut.n.01", "name": "betel_nut"}, + {"id": 13468, "synset": "beechnut.n.01", "name": "beechnut"}, + {"id": 13469, "synset": "walnut.n.01", "name": "walnut"}, + {"id": 13470, "synset": "black_walnut.n.02", "name": "black_walnut"}, + {"id": 13471, "synset": "english_walnut.n.02", "name": "English_walnut"}, + {"id": 13472, "synset": "brazil_nut.n.02", "name": "brazil_nut"}, + {"id": 13473, "synset": "butternut.n.02", "name": "butternut"}, + {"id": 13474, "synset": "souari_nut.n.02", "name": "souari_nut"}, + {"id": 13475, "synset": "cashew.n.02", "name": "cashew"}, + {"id": 13476, "synset": "chestnut.n.03", "name": "chestnut"}, + {"id": 13477, "synset": "chincapin.n.01", "name": "chincapin"}, + {"id": 13478, "synset": "hazelnut.n.02", "name": "hazelnut"}, + {"id": 13479, "synset": "coconut_milk.n.02", "name": "coconut_milk"}, + {"id": 13480, "synset": "grugru_nut.n.01", "name": "grugru_nut"}, + {"id": 13481, "synset": "hickory_nut.n.01", "name": "hickory_nut"}, + {"id": 13482, "synset": "cola_extract.n.01", "name": "cola_extract"}, + {"id": 13483, "synset": "macadamia_nut.n.02", "name": "macadamia_nut"}, + {"id": 13484, "synset": "pecan.n.03", "name": "pecan"}, + {"id": 13485, "synset": "pine_nut.n.01", "name": "pine_nut"}, + {"id": 13486, "synset": "pistachio.n.02", "name": "pistachio"}, + {"id": 13487, "synset": "sunflower_seed.n.01", "name": "sunflower_seed"}, + {"id": 13488, "synset": "anchovy_paste.n.01", "name": "anchovy_paste"}, + {"id": 13489, "synset": "rollmops.n.01", "name": "rollmops"}, + {"id": 13490, "synset": "feed.n.01", "name": "feed"}, + {"id": 13491, "synset": "cattle_cake.n.01", "name": "cattle_cake"}, + {"id": 13492, "synset": "creep_feed.n.01", "name": "creep_feed"}, + {"id": 13493, "synset": "fodder.n.02", "name": "fodder"}, + {"id": 13494, "synset": "feed_grain.n.01", "name": "feed_grain"}, + {"id": 13495, "synset": "eatage.n.01", "name": "eatage"}, + {"id": 13496, "synset": "silage.n.01", "name": "silage"}, + {"id": 13497, "synset": "oil_cake.n.01", "name": "oil_cake"}, + {"id": 13498, "synset": "oil_meal.n.01", "name": "oil_meal"}, + {"id": 13499, "synset": "alfalfa.n.02", "name": "alfalfa"}, + {"id": 13500, "synset": "broad_bean.n.03", "name": "broad_bean"}, + {"id": 13501, "synset": "hay.n.01", "name": "hay"}, + {"id": 13502, "synset": "timothy.n.03", "name": "timothy"}, + {"id": 13503, "synset": "stover.n.01", "name": "stover"}, + {"id": 13504, "synset": "grain.n.02", "name": "grain"}, + {"id": 13505, "synset": "grist.n.01", "name": "grist"}, + {"id": 13506, "synset": "groats.n.01", "name": "groats"}, + {"id": 13507, "synset": "millet.n.03", "name": "millet"}, + {"id": 13508, "synset": "barley.n.01", "name": "barley"}, + {"id": 13509, "synset": "pearl_barley.n.01", "name": "pearl_barley"}, + {"id": 13510, "synset": "buckwheat.n.02", "name": "buckwheat"}, + {"id": 13511, "synset": "bulgur.n.01", "name": "bulgur"}, + {"id": 13512, "synset": "wheat.n.02", "name": "wheat"}, + {"id": 13513, "synset": "cracked_wheat.n.01", "name": "cracked_wheat"}, + {"id": 13514, "synset": "stodge.n.01", "name": "stodge"}, + {"id": 13515, "synset": "wheat_germ.n.01", "name": "wheat_germ"}, + {"id": 13516, "synset": "oat.n.02", "name": "oat"}, + {"id": 13517, "synset": "rice.n.01", "name": "rice"}, + {"id": 13518, "synset": "brown_rice.n.01", "name": "brown_rice"}, + {"id": 13519, "synset": "white_rice.n.01", "name": "white_rice"}, + {"id": 13520, "synset": "wild_rice.n.02", "name": "wild_rice"}, + {"id": 13521, "synset": "paddy.n.03", "name": "paddy"}, + {"id": 13522, "synset": "slop.n.01", "name": "slop"}, + {"id": 13523, "synset": "mash.n.02", "name": "mash"}, + {"id": 13524, "synset": "chicken_feed.n.01", "name": "chicken_feed"}, + {"id": 13525, "synset": "cud.n.01", "name": "cud"}, + {"id": 13526, "synset": "bird_feed.n.01", "name": "bird_feed"}, + {"id": 13527, "synset": "petfood.n.01", "name": "petfood"}, + {"id": 13528, "synset": "dog_food.n.01", "name": "dog_food"}, + {"id": 13529, "synset": "cat_food.n.01", "name": "cat_food"}, + {"id": 13530, "synset": "canary_seed.n.01", "name": "canary_seed"}, + {"id": 13531, "synset": "tossed_salad.n.01", "name": "tossed_salad"}, + {"id": 13532, "synset": "green_salad.n.01", "name": "green_salad"}, + {"id": 13533, "synset": "caesar_salad.n.01", "name": "Caesar_salad"}, + {"id": 13534, "synset": "salmagundi.n.02", "name": "salmagundi"}, + {"id": 13535, "synset": "salad_nicoise.n.01", "name": "salad_nicoise"}, + {"id": 13536, "synset": "combination_salad.n.01", "name": "combination_salad"}, + {"id": 13537, "synset": "chef's_salad.n.01", "name": "chef's_salad"}, + {"id": 13538, "synset": "potato_salad.n.01", "name": "potato_salad"}, + {"id": 13539, "synset": "pasta_salad.n.01", "name": "pasta_salad"}, + {"id": 13540, "synset": "macaroni_salad.n.01", "name": "macaroni_salad"}, + {"id": 13541, "synset": "fruit_salad.n.01", "name": "fruit_salad"}, + {"id": 13542, "synset": "waldorf_salad.n.01", "name": "Waldorf_salad"}, + {"id": 13543, "synset": "crab_louis.n.01", "name": "crab_Louis"}, + {"id": 13544, "synset": "herring_salad.n.01", "name": "herring_salad"}, + {"id": 13545, "synset": "tuna_fish_salad.n.01", "name": "tuna_fish_salad"}, + {"id": 13546, "synset": "chicken_salad.n.01", "name": "chicken_salad"}, + {"id": 13547, "synset": "aspic.n.01", "name": "aspic"}, + {"id": 13548, "synset": "molded_salad.n.01", "name": "molded_salad"}, + {"id": 13549, "synset": "tabbouleh.n.01", "name": "tabbouleh"}, + {"id": 13550, "synset": "ingredient.n.03", "name": "ingredient"}, + {"id": 13551, "synset": "flavorer.n.01", "name": "flavorer"}, + {"id": 13552, "synset": "bouillon_cube.n.01", "name": "bouillon_cube"}, + {"id": 13553, "synset": "herb.n.02", "name": "herb"}, + {"id": 13554, "synset": "fines_herbes.n.01", "name": "fines_herbes"}, + {"id": 13555, "synset": "spice.n.02", "name": "spice"}, + {"id": 13556, "synset": "spearmint_oil.n.01", "name": "spearmint_oil"}, + {"id": 13557, "synset": "lemon_oil.n.01", "name": "lemon_oil"}, + {"id": 13558, "synset": "wintergreen_oil.n.01", "name": "wintergreen_oil"}, + {"id": 13559, "synset": "salt.n.02", "name": "salt"}, + {"id": 13560, "synset": "celery_salt.n.01", "name": "celery_salt"}, + {"id": 13561, "synset": "onion_salt.n.01", "name": "onion_salt"}, + {"id": 13562, "synset": "seasoned_salt.n.01", "name": "seasoned_salt"}, + {"id": 13563, "synset": "sour_salt.n.01", "name": "sour_salt"}, + {"id": 13564, "synset": "five_spice_powder.n.01", "name": "five_spice_powder"}, + {"id": 13565, "synset": "allspice.n.03", "name": "allspice"}, + {"id": 13566, "synset": "cinnamon.n.03", "name": "cinnamon"}, + {"id": 13567, "synset": "stick_cinnamon.n.01", "name": "stick_cinnamon"}, + {"id": 13568, "synset": "clove.n.04", "name": "clove"}, + {"id": 13569, "synset": "cumin.n.02", "name": "cumin"}, + {"id": 13570, "synset": "fennel.n.04", "name": "fennel"}, + {"id": 13571, "synset": "ginger.n.02", "name": "ginger"}, + {"id": 13572, "synset": "mace.n.03", "name": "mace"}, + {"id": 13573, "synset": "nutmeg.n.02", "name": "nutmeg"}, + {"id": 13574, "synset": "black_pepper.n.02", "name": "black_pepper"}, + {"id": 13575, "synset": "white_pepper.n.02", "name": "white_pepper"}, + {"id": 13576, "synset": "sassafras.n.02", "name": "sassafras"}, + {"id": 13577, "synset": "basil.n.03", "name": "basil"}, + {"id": 13578, "synset": "bay_leaf.n.01", "name": "bay_leaf"}, + {"id": 13579, "synset": "borage.n.02", "name": "borage"}, + {"id": 13580, "synset": "hyssop.n.02", "name": "hyssop"}, + {"id": 13581, "synset": "caraway.n.02", "name": "caraway"}, + {"id": 13582, "synset": "chervil.n.02", "name": "chervil"}, + {"id": 13583, "synset": "chives.n.02", "name": "chives"}, + {"id": 13584, "synset": "comfrey.n.02", "name": "comfrey"}, + {"id": 13585, "synset": "coriander.n.03", "name": "coriander"}, + {"id": 13586, "synset": "coriander.n.02", "name": "coriander"}, + {"id": 13587, "synset": "costmary.n.02", "name": "costmary"}, + {"id": 13588, "synset": "fennel.n.03", "name": "fennel"}, + {"id": 13589, "synset": "fennel.n.02", "name": "fennel"}, + {"id": 13590, "synset": "fennel_seed.n.01", "name": "fennel_seed"}, + {"id": 13591, "synset": "fenugreek.n.02", "name": "fenugreek"}, + {"id": 13592, "synset": "clove.n.03", "name": "clove"}, + {"id": 13593, "synset": "garlic_chive.n.02", "name": "garlic_chive"}, + {"id": 13594, "synset": "lemon_balm.n.02", "name": "lemon_balm"}, + {"id": 13595, "synset": "lovage.n.02", "name": "lovage"}, + {"id": 13596, "synset": "marjoram.n.02", "name": "marjoram"}, + {"id": 13597, "synset": "mint.n.04", "name": "mint"}, + {"id": 13598, "synset": "mustard_seed.n.01", "name": "mustard_seed"}, + {"id": 13599, "synset": "mustard.n.02", "name": "mustard"}, + {"id": 13600, "synset": "chinese_mustard.n.02", "name": "Chinese_mustard"}, + {"id": 13601, "synset": "nasturtium.n.03", "name": "nasturtium"}, + {"id": 13602, "synset": "parsley.n.02", "name": "parsley"}, + {"id": 13603, "synset": "salad_burnet.n.02", "name": "salad_burnet"}, + {"id": 13604, "synset": "rosemary.n.02", "name": "rosemary"}, + {"id": 13605, "synset": "rue.n.02", "name": "rue"}, + {"id": 13606, "synset": "sage.n.02", "name": "sage"}, + {"id": 13607, "synset": "clary_sage.n.02", "name": "clary_sage"}, + {"id": 13608, "synset": "savory.n.03", "name": "savory"}, + {"id": 13609, "synset": "summer_savory.n.02", "name": "summer_savory"}, + {"id": 13610, "synset": "winter_savory.n.02", "name": "winter_savory"}, + {"id": 13611, "synset": "sweet_woodruff.n.02", "name": "sweet_woodruff"}, + {"id": 13612, "synset": "sweet_cicely.n.03", "name": "sweet_cicely"}, + {"id": 13613, "synset": "tarragon.n.02", "name": "tarragon"}, + {"id": 13614, "synset": "thyme.n.02", "name": "thyme"}, + {"id": 13615, "synset": "turmeric.n.02", "name": "turmeric"}, + {"id": 13616, "synset": "caper.n.02", "name": "caper"}, + {"id": 13617, "synset": "catsup.n.01", "name": "catsup"}, + {"id": 13618, "synset": "cardamom.n.02", "name": "cardamom"}, + {"id": 13619, "synset": "chili_powder.n.01", "name": "chili_powder"}, + {"id": 13620, "synset": "chili_sauce.n.01", "name": "chili_sauce"}, + {"id": 13621, "synset": "chutney.n.01", "name": "chutney"}, + {"id": 13622, "synset": "steak_sauce.n.01", "name": "steak_sauce"}, + {"id": 13623, "synset": "taco_sauce.n.01", "name": "taco_sauce"}, + {"id": 13624, "synset": "mint_sauce.n.01", "name": "mint_sauce"}, + {"id": 13625, "synset": "cranberry_sauce.n.01", "name": "cranberry_sauce"}, + {"id": 13626, "synset": "curry_powder.n.01", "name": "curry_powder"}, + {"id": 13627, "synset": "curry.n.01", "name": "curry"}, + {"id": 13628, "synset": "lamb_curry.n.01", "name": "lamb_curry"}, + {"id": 13629, "synset": "duck_sauce.n.01", "name": "duck_sauce"}, + {"id": 13630, "synset": "horseradish.n.03", "name": "horseradish"}, + {"id": 13631, "synset": "marinade.n.01", "name": "marinade"}, + {"id": 13632, "synset": "paprika.n.02", "name": "paprika"}, + {"id": 13633, "synset": "spanish_paprika.n.01", "name": "Spanish_paprika"}, + {"id": 13634, "synset": "dill_pickle.n.01", "name": "dill_pickle"}, + {"id": 13635, "synset": "bread_and_butter_pickle.n.01", "name": "bread_and_butter_pickle"}, + {"id": 13636, "synset": "pickle_relish.n.01", "name": "pickle_relish"}, + {"id": 13637, "synset": "piccalilli.n.01", "name": "piccalilli"}, + {"id": 13638, "synset": "sweet_pickle.n.01", "name": "sweet_pickle"}, + {"id": 13639, "synset": "soy_sauce.n.01", "name": "soy_sauce"}, + {"id": 13640, "synset": "tomato_paste.n.01", "name": "tomato_paste"}, + {"id": 13641, "synset": "angelica.n.03", "name": "angelica"}, + {"id": 13642, "synset": "angelica.n.02", "name": "angelica"}, + {"id": 13643, "synset": "almond_extract.n.01", "name": "almond_extract"}, + {"id": 13644, "synset": "anise.n.02", "name": "anise"}, + {"id": 13645, "synset": "chinese_anise.n.02", "name": "Chinese_anise"}, + {"id": 13646, "synset": "juniper_berries.n.01", "name": "juniper_berries"}, + {"id": 13647, "synset": "saffron.n.02", "name": "saffron"}, + {"id": 13648, "synset": "sesame_seed.n.01", "name": "sesame_seed"}, + {"id": 13649, "synset": "caraway_seed.n.01", "name": "caraway_seed"}, + {"id": 13650, "synset": "poppy_seed.n.01", "name": "poppy_seed"}, + {"id": 13651, "synset": "dill.n.02", "name": "dill"}, + {"id": 13652, "synset": "dill_seed.n.01", "name": "dill_seed"}, + {"id": 13653, "synset": "celery_seed.n.01", "name": "celery_seed"}, + {"id": 13654, "synset": "lemon_extract.n.01", "name": "lemon_extract"}, + {"id": 13655, "synset": "monosodium_glutamate.n.01", "name": "monosodium_glutamate"}, + {"id": 13656, "synset": "vanilla_bean.n.01", "name": "vanilla_bean"}, + {"id": 13657, "synset": "cider_vinegar.n.01", "name": "cider_vinegar"}, + {"id": 13658, "synset": "wine_vinegar.n.01", "name": "wine_vinegar"}, + {"id": 13659, "synset": "sauce.n.01", "name": "sauce"}, + {"id": 13660, "synset": "anchovy_sauce.n.01", "name": "anchovy_sauce"}, + {"id": 13661, "synset": "hard_sauce.n.01", "name": "hard_sauce"}, + {"id": 13662, "synset": "horseradish_sauce.n.01", "name": "horseradish_sauce"}, + {"id": 13663, "synset": "bolognese_pasta_sauce.n.01", "name": "bolognese_pasta_sauce"}, + {"id": 13664, "synset": "carbonara.n.01", "name": "carbonara"}, + {"id": 13665, "synset": "tomato_sauce.n.01", "name": "tomato_sauce"}, + {"id": 13666, "synset": "tartare_sauce.n.01", "name": "tartare_sauce"}, + {"id": 13667, "synset": "wine_sauce.n.01", "name": "wine_sauce"}, + {"id": 13668, "synset": "marchand_de_vin.n.01", "name": "marchand_de_vin"}, + {"id": 13669, "synset": "bread_sauce.n.01", "name": "bread_sauce"}, + {"id": 13670, "synset": "plum_sauce.n.01", "name": "plum_sauce"}, + {"id": 13671, "synset": "peach_sauce.n.01", "name": "peach_sauce"}, + {"id": 13672, "synset": "apricot_sauce.n.01", "name": "apricot_sauce"}, + {"id": 13673, "synset": "pesto.n.01", "name": "pesto"}, + {"id": 13674, "synset": "ravigote.n.01", "name": "ravigote"}, + {"id": 13675, "synset": "remoulade_sauce.n.01", "name": "remoulade_sauce"}, + {"id": 13676, "synset": "dressing.n.01", "name": "dressing"}, + {"id": 13677, "synset": "sauce_louis.n.01", "name": "sauce_Louis"}, + {"id": 13678, "synset": "bleu_cheese_dressing.n.01", "name": "bleu_cheese_dressing"}, + {"id": 13679, "synset": "blue_cheese_dressing.n.01", "name": "blue_cheese_dressing"}, + {"id": 13680, "synset": "french_dressing.n.01", "name": "French_dressing"}, + {"id": 13681, "synset": "lorenzo_dressing.n.01", "name": "Lorenzo_dressing"}, + {"id": 13682, "synset": "anchovy_dressing.n.01", "name": "anchovy_dressing"}, + {"id": 13683, "synset": "italian_dressing.n.01", "name": "Italian_dressing"}, + {"id": 13684, "synset": "half-and-half_dressing.n.01", "name": "half-and-half_dressing"}, + {"id": 13685, "synset": "mayonnaise.n.01", "name": "mayonnaise"}, + {"id": 13686, "synset": "green_mayonnaise.n.01", "name": "green_mayonnaise"}, + {"id": 13687, "synset": "aioli.n.01", "name": "aioli"}, + {"id": 13688, "synset": "russian_dressing.n.01", "name": "Russian_dressing"}, + {"id": 13689, "synset": "salad_cream.n.01", "name": "salad_cream"}, + {"id": 13690, "synset": "thousand_island_dressing.n.01", "name": "Thousand_Island_dressing"}, + {"id": 13691, "synset": "barbecue_sauce.n.01", "name": "barbecue_sauce"}, + {"id": 13692, "synset": "hollandaise.n.01", "name": "hollandaise"}, + {"id": 13693, "synset": "bearnaise.n.01", "name": "bearnaise"}, + {"id": 13694, "synset": "bercy.n.01", "name": "Bercy"}, + {"id": 13695, "synset": "bordelaise.n.01", "name": "bordelaise"}, + {"id": 13696, "synset": "bourguignon.n.01", "name": "bourguignon"}, + {"id": 13697, "synset": "brown_sauce.n.02", "name": "brown_sauce"}, + {"id": 13698, "synset": "espagnole.n.01", "name": "Espagnole"}, + {"id": 13699, "synset": "chinese_brown_sauce.n.01", "name": "Chinese_brown_sauce"}, + {"id": 13700, "synset": "blanc.n.01", "name": "blanc"}, + {"id": 13701, "synset": "cheese_sauce.n.01", "name": "cheese_sauce"}, + {"id": 13702, "synset": "chocolate_sauce.n.01", "name": "chocolate_sauce"}, + {"id": 13703, "synset": "hot-fudge_sauce.n.01", "name": "hot-fudge_sauce"}, + {"id": 13704, "synset": "cocktail_sauce.n.01", "name": "cocktail_sauce"}, + {"id": 13705, "synset": "colbert.n.01", "name": "Colbert"}, + {"id": 13706, "synset": "white_sauce.n.01", "name": "white_sauce"}, + {"id": 13707, "synset": "cream_sauce.n.01", "name": "cream_sauce"}, + {"id": 13708, "synset": "mornay_sauce.n.01", "name": "Mornay_sauce"}, + {"id": 13709, "synset": "demiglace.n.01", "name": "demiglace"}, + {"id": 13710, "synset": "gravy.n.02", "name": "gravy"}, + {"id": 13711, "synset": "gravy.n.01", "name": "gravy"}, + {"id": 13712, "synset": "spaghetti_sauce.n.01", "name": "spaghetti_sauce"}, + {"id": 13713, "synset": "marinara.n.01", "name": "marinara"}, + {"id": 13714, "synset": "mole.n.03", "name": "mole"}, + {"id": 13715, "synset": "hunter's_sauce.n.01", "name": "hunter's_sauce"}, + {"id": 13716, "synset": "mushroom_sauce.n.01", "name": "mushroom_sauce"}, + {"id": 13717, "synset": "mustard_sauce.n.01", "name": "mustard_sauce"}, + {"id": 13718, "synset": "nantua.n.01", "name": "Nantua"}, + {"id": 13719, "synset": "hungarian_sauce.n.01", "name": "Hungarian_sauce"}, + {"id": 13720, "synset": "pepper_sauce.n.01", "name": "pepper_sauce"}, + {"id": 13721, "synset": "roux.n.01", "name": "roux"}, + {"id": 13722, "synset": "smitane.n.01", "name": "Smitane"}, + {"id": 13723, "synset": "soubise.n.01", "name": "Soubise"}, + {"id": 13724, "synset": "lyonnaise_sauce.n.01", "name": "Lyonnaise_sauce"}, + {"id": 13725, "synset": "veloute.n.01", "name": "veloute"}, + {"id": 13726, "synset": "allemande.n.01", "name": "allemande"}, + {"id": 13727, "synset": "caper_sauce.n.01", "name": "caper_sauce"}, + {"id": 13728, "synset": "poulette.n.01", "name": "poulette"}, + {"id": 13729, "synset": "curry_sauce.n.01", "name": "curry_sauce"}, + {"id": 13730, "synset": "worcester_sauce.n.01", "name": "Worcester_sauce"}, + {"id": 13731, "synset": "coconut_milk.n.01", "name": "coconut_milk"}, + {"id": 13732, "synset": "egg_white.n.01", "name": "egg_white"}, + {"id": 13733, "synset": "hard-boiled_egg.n.01", "name": "hard-boiled_egg"}, + {"id": 13734, "synset": "easter_egg.n.02", "name": "Easter_egg"}, + {"id": 13735, "synset": "easter_egg.n.01", "name": "Easter_egg"}, + {"id": 13736, "synset": "chocolate_egg.n.01", "name": "chocolate_egg"}, + {"id": 13737, "synset": "candy_egg.n.01", "name": "candy_egg"}, + {"id": 13738, "synset": "poached_egg.n.01", "name": "poached_egg"}, + {"id": 13739, "synset": "scrambled_eggs.n.01", "name": "scrambled_eggs"}, + {"id": 13740, "synset": "deviled_egg.n.01", "name": "deviled_egg"}, + {"id": 13741, "synset": "shirred_egg.n.01", "name": "shirred_egg"}, + {"id": 13742, "synset": "firm_omelet.n.01", "name": "firm_omelet"}, + {"id": 13743, "synset": "french_omelet.n.01", "name": "French_omelet"}, + {"id": 13744, "synset": "fluffy_omelet.n.01", "name": "fluffy_omelet"}, + {"id": 13745, "synset": "western_omelet.n.01", "name": "western_omelet"}, + {"id": 13746, "synset": "souffle.n.01", "name": "souffle"}, + {"id": 13747, "synset": "fried_egg.n.01", "name": "fried_egg"}, + {"id": 13748, "synset": "dairy_product.n.01", "name": "dairy_product"}, + {"id": 13749, "synset": "milk.n.04", "name": "milk"}, + {"id": 13750, "synset": "sour_milk.n.01", "name": "sour_milk"}, + {"id": 13751, "synset": "formula.n.06", "name": "formula"}, + {"id": 13752, "synset": "pasteurized_milk.n.01", "name": "pasteurized_milk"}, + {"id": 13753, "synset": "cows'_milk.n.01", "name": "cows'_milk"}, + {"id": 13754, "synset": "yak's_milk.n.01", "name": "yak's_milk"}, + {"id": 13755, "synset": "goats'_milk.n.01", "name": "goats'_milk"}, + {"id": 13756, "synset": "acidophilus_milk.n.01", "name": "acidophilus_milk"}, + {"id": 13757, "synset": "raw_milk.n.01", "name": "raw_milk"}, + {"id": 13758, "synset": "scalded_milk.n.01", "name": "scalded_milk"}, + {"id": 13759, "synset": "homogenized_milk.n.01", "name": "homogenized_milk"}, + {"id": 13760, "synset": "certified_milk.n.01", "name": "certified_milk"}, + {"id": 13761, "synset": "powdered_milk.n.01", "name": "powdered_milk"}, + {"id": 13762, "synset": "nonfat_dry_milk.n.01", "name": "nonfat_dry_milk"}, + {"id": 13763, "synset": "evaporated_milk.n.01", "name": "evaporated_milk"}, + {"id": 13764, "synset": "condensed_milk.n.01", "name": "condensed_milk"}, + {"id": 13765, "synset": "skim_milk.n.01", "name": "skim_milk"}, + {"id": 13766, "synset": "semi-skimmed_milk.n.01", "name": "semi-skimmed_milk"}, + {"id": 13767, "synset": "whole_milk.n.01", "name": "whole_milk"}, + {"id": 13768, "synset": "low-fat_milk.n.01", "name": "low-fat_milk"}, + {"id": 13769, "synset": "buttermilk.n.01", "name": "buttermilk"}, + {"id": 13770, "synset": "cream.n.02", "name": "cream"}, + {"id": 13771, "synset": "clotted_cream.n.01", "name": "clotted_cream"}, + {"id": 13772, "synset": "double_creme.n.01", "name": "double_creme"}, + {"id": 13773, "synset": "half-and-half.n.01", "name": "half-and-half"}, + {"id": 13774, "synset": "heavy_cream.n.01", "name": "heavy_cream"}, + {"id": 13775, "synset": "light_cream.n.01", "name": "light_cream"}, + {"id": 13776, "synset": "whipping_cream.n.01", "name": "whipping_cream"}, + {"id": 13777, "synset": "clarified_butter.n.01", "name": "clarified_butter"}, + {"id": 13778, "synset": "ghee.n.01", "name": "ghee"}, + {"id": 13779, "synset": "brown_butter.n.01", "name": "brown_butter"}, + {"id": 13780, "synset": "meuniere_butter.n.01", "name": "Meuniere_butter"}, + {"id": 13781, "synset": "blueberry_yogurt.n.01", "name": "blueberry_yogurt"}, + {"id": 13782, "synset": "raita.n.01", "name": "raita"}, + {"id": 13783, "synset": "whey.n.02", "name": "whey"}, + {"id": 13784, "synset": "curd.n.02", "name": "curd"}, + {"id": 13785, "synset": "curd.n.01", "name": "curd"}, + {"id": 13786, "synset": "clabber.n.01", "name": "clabber"}, + {"id": 13787, "synset": "cheese.n.01", "name": "cheese"}, + {"id": 13788, "synset": "paring.n.02", "name": "paring"}, + {"id": 13789, "synset": "cream_cheese.n.01", "name": "cream_cheese"}, + {"id": 13790, "synset": "double_cream.n.01", "name": "double_cream"}, + {"id": 13791, "synset": "mascarpone.n.01", "name": "mascarpone"}, + {"id": 13792, "synset": "triple_cream.n.01", "name": "triple_cream"}, + {"id": 13793, "synset": "cottage_cheese.n.01", "name": "cottage_cheese"}, + {"id": 13794, "synset": "process_cheese.n.01", "name": "process_cheese"}, + {"id": 13795, "synset": "bleu.n.01", "name": "bleu"}, + {"id": 13796, "synset": "stilton.n.01", "name": "Stilton"}, + {"id": 13797, "synset": "roquefort.n.01", "name": "Roquefort"}, + {"id": 13798, "synset": "gorgonzola.n.01", "name": "gorgonzola"}, + {"id": 13799, "synset": "danish_blue.n.01", "name": "Danish_blue"}, + {"id": 13800, "synset": "bavarian_blue.n.01", "name": "Bavarian_blue"}, + {"id": 13801, "synset": "brie.n.01", "name": "Brie"}, + {"id": 13802, "synset": "brick_cheese.n.01", "name": "brick_cheese"}, + {"id": 13803, "synset": "camembert.n.01", "name": "Camembert"}, + {"id": 13804, "synset": "cheddar.n.02", "name": "cheddar"}, + {"id": 13805, "synset": "rat_cheese.n.01", "name": "rat_cheese"}, + {"id": 13806, "synset": "cheshire_cheese.n.01", "name": "Cheshire_cheese"}, + {"id": 13807, "synset": "double_gloucester.n.01", "name": "double_Gloucester"}, + {"id": 13808, "synset": "edam.n.01", "name": "Edam"}, + {"id": 13809, "synset": "goat_cheese.n.01", "name": "goat_cheese"}, + {"id": 13810, "synset": "gouda.n.01", "name": "Gouda"}, + {"id": 13811, "synset": "grated_cheese.n.01", "name": "grated_cheese"}, + {"id": 13812, "synset": "hand_cheese.n.01", "name": "hand_cheese"}, + {"id": 13813, "synset": "liederkranz.n.01", "name": "Liederkranz"}, + {"id": 13814, "synset": "limburger.n.01", "name": "Limburger"}, + {"id": 13815, "synset": "mozzarella.n.01", "name": "mozzarella"}, + {"id": 13816, "synset": "muenster.n.01", "name": "Muenster"}, + {"id": 13817, "synset": "parmesan.n.01", "name": "Parmesan"}, + {"id": 13818, "synset": "quark_cheese.n.01", "name": "quark_cheese"}, + {"id": 13819, "synset": "ricotta.n.01", "name": "ricotta"}, + {"id": 13820, "synset": "swiss_cheese.n.01", "name": "Swiss_cheese"}, + {"id": 13821, "synset": "emmenthal.n.01", "name": "Emmenthal"}, + {"id": 13822, "synset": "gruyere.n.01", "name": "Gruyere"}, + {"id": 13823, "synset": "sapsago.n.01", "name": "sapsago"}, + {"id": 13824, "synset": "velveeta.n.01", "name": "Velveeta"}, + {"id": 13825, "synset": "nut_butter.n.01", "name": "nut_butter"}, + {"id": 13826, "synset": "marshmallow_fluff.n.01", "name": "marshmallow_fluff"}, + {"id": 13827, "synset": "onion_butter.n.01", "name": "onion_butter"}, + {"id": 13828, "synset": "pimento_butter.n.01", "name": "pimento_butter"}, + {"id": 13829, "synset": "shrimp_butter.n.01", "name": "shrimp_butter"}, + {"id": 13830, "synset": "lobster_butter.n.01", "name": "lobster_butter"}, + {"id": 13831, "synset": "yak_butter.n.01", "name": "yak_butter"}, + {"id": 13832, "synset": "spread.n.05", "name": "spread"}, + {"id": 13833, "synset": "cheese_spread.n.01", "name": "cheese_spread"}, + {"id": 13834, "synset": "anchovy_butter.n.01", "name": "anchovy_butter"}, + {"id": 13835, "synset": "fishpaste.n.01", "name": "fishpaste"}, + {"id": 13836, "synset": "garlic_butter.n.01", "name": "garlic_butter"}, + {"id": 13837, "synset": "miso.n.01", "name": "miso"}, + {"id": 13838, "synset": "wasabi.n.02", "name": "wasabi"}, + {"id": 13839, "synset": "snail_butter.n.01", "name": "snail_butter"}, + {"id": 13840, "synset": "pate.n.01", "name": "pate"}, + {"id": 13841, "synset": "duck_pate.n.01", "name": "duck_pate"}, + {"id": 13842, "synset": "foie_gras.n.01", "name": "foie_gras"}, + {"id": 13843, "synset": "tapenade.n.01", "name": "tapenade"}, + {"id": 13844, "synset": "tahini.n.01", "name": "tahini"}, + {"id": 13845, "synset": "sweetening.n.01", "name": "sweetening"}, + {"id": 13846, "synset": "aspartame.n.01", "name": "aspartame"}, + {"id": 13847, "synset": "saccharin.n.01", "name": "saccharin"}, + {"id": 13848, "synset": "sugar.n.01", "name": "sugar"}, + {"id": 13849, "synset": "syrup.n.01", "name": "syrup"}, + {"id": 13850, "synset": "sugar_syrup.n.01", "name": "sugar_syrup"}, + {"id": 13851, "synset": "molasses.n.01", "name": "molasses"}, + {"id": 13852, "synset": "sorghum.n.03", "name": "sorghum"}, + {"id": 13853, "synset": "treacle.n.01", "name": "treacle"}, + {"id": 13854, "synset": "grenadine.n.01", "name": "grenadine"}, + {"id": 13855, "synset": "maple_syrup.n.01", "name": "maple_syrup"}, + {"id": 13856, "synset": "corn_syrup.n.01", "name": "corn_syrup"}, + {"id": 13857, "synset": "miraculous_food.n.01", "name": "miraculous_food"}, + {"id": 13858, "synset": "dough.n.01", "name": "dough"}, + {"id": 13859, "synset": "bread_dough.n.01", "name": "bread_dough"}, + {"id": 13860, "synset": "pancake_batter.n.01", "name": "pancake_batter"}, + {"id": 13861, "synset": "fritter_batter.n.01", "name": "fritter_batter"}, + {"id": 13862, "synset": "coq_au_vin.n.01", "name": "coq_au_vin"}, + {"id": 13863, "synset": "chicken_provencale.n.01", "name": "chicken_provencale"}, + {"id": 13864, "synset": "chicken_and_rice.n.01", "name": "chicken_and_rice"}, + {"id": 13865, "synset": "moo_goo_gai_pan.n.01", "name": "moo_goo_gai_pan"}, + {"id": 13866, "synset": "arroz_con_pollo.n.01", "name": "arroz_con_pollo"}, + {"id": 13867, "synset": "bacon_and_eggs.n.02", "name": "bacon_and_eggs"}, + {"id": 13868, "synset": "barbecued_spareribs.n.01", "name": "barbecued_spareribs"}, + {"id": 13869, "synset": "beef_bourguignonne.n.01", "name": "beef_Bourguignonne"}, + {"id": 13870, "synset": "beef_wellington.n.01", "name": "beef_Wellington"}, + {"id": 13871, "synset": "bitok.n.01", "name": "bitok"}, + {"id": 13872, "synset": "boiled_dinner.n.01", "name": "boiled_dinner"}, + {"id": 13873, "synset": "boston_baked_beans.n.01", "name": "Boston_baked_beans"}, + {"id": 13874, "synset": "bubble_and_squeak.n.01", "name": "bubble_and_squeak"}, + {"id": 13875, "synset": "pasta.n.01", "name": "pasta"}, + {"id": 13876, "synset": "cannelloni.n.01", "name": "cannelloni"}, + {"id": 13877, "synset": "carbonnade_flamande.n.01", "name": "carbonnade_flamande"}, + {"id": 13878, "synset": "cheese_souffle.n.01", "name": "cheese_souffle"}, + {"id": 13879, "synset": "chicken_marengo.n.01", "name": "chicken_Marengo"}, + {"id": 13880, "synset": "chicken_cordon_bleu.n.01", "name": "chicken_cordon_bleu"}, + {"id": 13881, "synset": "maryland_chicken.n.01", "name": "Maryland_chicken"}, + {"id": 13882, "synset": "chicken_paprika.n.01", "name": "chicken_paprika"}, + {"id": 13883, "synset": "chicken_tetrazzini.n.01", "name": "chicken_Tetrazzini"}, + {"id": 13884, "synset": "tetrazzini.n.01", "name": "Tetrazzini"}, + {"id": 13885, "synset": "chicken_kiev.n.01", "name": "chicken_Kiev"}, + {"id": 13886, "synset": "chili.n.01", "name": "chili"}, + {"id": 13887, "synset": "chili_dog.n.01", "name": "chili_dog"}, + {"id": 13888, "synset": "chop_suey.n.01", "name": "chop_suey"}, + {"id": 13889, "synset": "chow_mein.n.01", "name": "chow_mein"}, + {"id": 13890, "synset": "codfish_ball.n.01", "name": "codfish_ball"}, + {"id": 13891, "synset": "coquille.n.01", "name": "coquille"}, + {"id": 13892, "synset": "coquilles_saint-jacques.n.01", "name": "coquilles_Saint-Jacques"}, + {"id": 13893, "synset": "croquette.n.01", "name": "croquette"}, + {"id": 13894, "synset": "cottage_pie.n.01", "name": "cottage_pie"}, + {"id": 13895, "synset": "rissole.n.01", "name": "rissole"}, + {"id": 13896, "synset": "dolmas.n.01", "name": "dolmas"}, + {"id": 13897, "synset": "egg_foo_yong.n.01", "name": "egg_foo_yong"}, + {"id": 13898, "synset": "eggs_benedict.n.01", "name": "eggs_Benedict"}, + {"id": 13899, "synset": "enchilada.n.01", "name": "enchilada"}, + {"id": 13900, "synset": "falafel.n.01", "name": "falafel"}, + {"id": 13901, "synset": "fish_and_chips.n.01", "name": "fish_and_chips"}, + {"id": 13902, "synset": "fondue.n.02", "name": "fondue"}, + {"id": 13903, "synset": "cheese_fondue.n.01", "name": "cheese_fondue"}, + {"id": 13904, "synset": "chocolate_fondue.n.01", "name": "chocolate_fondue"}, + {"id": 13905, "synset": "fondue.n.01", "name": "fondue"}, + {"id": 13906, "synset": "beef_fondue.n.01", "name": "beef_fondue"}, + {"id": 13907, "synset": "fried_rice.n.01", "name": "fried_rice"}, + {"id": 13908, "synset": "frittata.n.01", "name": "frittata"}, + {"id": 13909, "synset": "frog_legs.n.01", "name": "frog_legs"}, + {"id": 13910, "synset": "galantine.n.01", "name": "galantine"}, + {"id": 13911, "synset": "gefilte_fish.n.01", "name": "gefilte_fish"}, + {"id": 13912, "synset": "haggis.n.01", "name": "haggis"}, + {"id": 13913, "synset": "ham_and_eggs.n.01", "name": "ham_and_eggs"}, + {"id": 13914, "synset": "hash.n.01", "name": "hash"}, + {"id": 13915, "synset": "corned_beef_hash.n.01", "name": "corned_beef_hash"}, + {"id": 13916, "synset": "jambalaya.n.01", "name": "jambalaya"}, + {"id": 13917, "synset": "kabob.n.01", "name": "kabob"}, + {"id": 13918, "synset": "kedgeree.n.01", "name": "kedgeree"}, + {"id": 13919, "synset": "souvlaki.n.01", "name": "souvlaki"}, + {"id": 13920, "synset": "seafood_newburg.n.01", "name": "seafood_Newburg"}, + {"id": 13921, "synset": "lobster_newburg.n.01", "name": "lobster_Newburg"}, + {"id": 13922, "synset": "shrimp_newburg.n.01", "name": "shrimp_Newburg"}, + {"id": 13923, "synset": "newburg_sauce.n.01", "name": "Newburg_sauce"}, + {"id": 13924, "synset": "lobster_thermidor.n.01", "name": "lobster_thermidor"}, + {"id": 13925, "synset": "lutefisk.n.01", "name": "lutefisk"}, + {"id": 13926, "synset": "macaroni_and_cheese.n.01", "name": "macaroni_and_cheese"}, + {"id": 13927, "synset": "macedoine.n.01", "name": "macedoine"}, + {"id": 13928, "synset": "porcupine_ball.n.01", "name": "porcupine_ball"}, + {"id": 13929, "synset": "swedish_meatball.n.01", "name": "Swedish_meatball"}, + {"id": 13930, "synset": "meat_loaf.n.01", "name": "meat_loaf"}, + {"id": 13931, "synset": "moussaka.n.01", "name": "moussaka"}, + {"id": 13932, "synset": "osso_buco.n.01", "name": "osso_buco"}, + {"id": 13933, "synset": "marrow.n.03", "name": "marrow"}, + {"id": 13934, "synset": "pheasant_under_glass.n.01", "name": "pheasant_under_glass"}, + {"id": 13935, "synset": "pigs_in_blankets.n.01", "name": "pigs_in_blankets"}, + {"id": 13936, "synset": "pilaf.n.01", "name": "pilaf"}, + {"id": 13937, "synset": "bulgur_pilaf.n.01", "name": "bulgur_pilaf"}, + {"id": 13938, "synset": "sausage_pizza.n.01", "name": "sausage_pizza"}, + {"id": 13939, "synset": "pepperoni_pizza.n.01", "name": "pepperoni_pizza"}, + {"id": 13940, "synset": "cheese_pizza.n.01", "name": "cheese_pizza"}, + {"id": 13941, "synset": "anchovy_pizza.n.01", "name": "anchovy_pizza"}, + {"id": 13942, "synset": "sicilian_pizza.n.01", "name": "Sicilian_pizza"}, + {"id": 13943, "synset": "poi.n.01", "name": "poi"}, + {"id": 13944, "synset": "pork_and_beans.n.01", "name": "pork_and_beans"}, + {"id": 13945, "synset": "porridge.n.01", "name": "porridge"}, + {"id": 13946, "synset": "oatmeal.n.01", "name": "oatmeal"}, + {"id": 13947, "synset": "loblolly.n.01", "name": "loblolly"}, + {"id": 13948, "synset": "potpie.n.01", "name": "potpie"}, + {"id": 13949, "synset": "rijsttaffel.n.01", "name": "rijsttaffel"}, + {"id": 13950, "synset": "risotto.n.01", "name": "risotto"}, + {"id": 13951, "synset": "roulade.n.01", "name": "roulade"}, + {"id": 13952, "synset": "fish_loaf.n.01", "name": "fish_loaf"}, + {"id": 13953, "synset": "salmon_loaf.n.01", "name": "salmon_loaf"}, + {"id": 13954, "synset": "salisbury_steak.n.01", "name": "Salisbury_steak"}, + {"id": 13955, "synset": "sauerbraten.n.01", "name": "sauerbraten"}, + {"id": 13956, "synset": "sauerkraut.n.01", "name": "sauerkraut"}, + {"id": 13957, "synset": "scallopine.n.01", "name": "scallopine"}, + {"id": 13958, "synset": "veal_scallopini.n.01", "name": "veal_scallopini"}, + {"id": 13959, "synset": "scampi.n.01", "name": "scampi"}, + {"id": 13960, "synset": "scotch_egg.n.01", "name": "Scotch_egg"}, + {"id": 13961, "synset": "scotch_woodcock.n.01", "name": "Scotch_woodcock"}, + {"id": 13962, "synset": "scrapple.n.01", "name": "scrapple"}, + {"id": 13963, "synset": "spaghetti_and_meatballs.n.01", "name": "spaghetti_and_meatballs"}, + {"id": 13964, "synset": "spanish_rice.n.01", "name": "Spanish_rice"}, + {"id": 13965, "synset": "steak_tartare.n.01", "name": "steak_tartare"}, + {"id": 13966, "synset": "pepper_steak.n.02", "name": "pepper_steak"}, + {"id": 13967, "synset": "steak_au_poivre.n.01", "name": "steak_au_poivre"}, + {"id": 13968, "synset": "beef_stroganoff.n.01", "name": "beef_Stroganoff"}, + {"id": 13969, "synset": "stuffed_cabbage.n.01", "name": "stuffed_cabbage"}, + {"id": 13970, "synset": "kishke.n.01", "name": "kishke"}, + {"id": 13971, "synset": "stuffed_peppers.n.01", "name": "stuffed_peppers"}, + {"id": 13972, "synset": "stuffed_tomato.n.02", "name": "stuffed_tomato"}, + {"id": 13973, "synset": "stuffed_tomato.n.01", "name": "stuffed_tomato"}, + {"id": 13974, "synset": "succotash.n.01", "name": "succotash"}, + {"id": 13975, "synset": "sukiyaki.n.01", "name": "sukiyaki"}, + {"id": 13976, "synset": "sashimi.n.01", "name": "sashimi"}, + {"id": 13977, "synset": "swiss_steak.n.01", "name": "Swiss_steak"}, + {"id": 13978, "synset": "tamale.n.02", "name": "tamale"}, + {"id": 13979, "synset": "tamale_pie.n.01", "name": "tamale_pie"}, + {"id": 13980, "synset": "tempura.n.01", "name": "tempura"}, + {"id": 13981, "synset": "teriyaki.n.01", "name": "teriyaki"}, + {"id": 13982, "synset": "terrine.n.01", "name": "terrine"}, + {"id": 13983, "synset": "welsh_rarebit.n.01", "name": "Welsh_rarebit"}, + {"id": 13984, "synset": "schnitzel.n.01", "name": "schnitzel"}, + {"id": 13985, "synset": "chicken_taco.n.01", "name": "chicken_taco"}, + {"id": 13986, "synset": "beef_burrito.n.01", "name": "beef_burrito"}, + {"id": 13987, "synset": "tostada.n.01", "name": "tostada"}, + {"id": 13988, "synset": "bean_tostada.n.01", "name": "bean_tostada"}, + {"id": 13989, "synset": "refried_beans.n.01", "name": "refried_beans"}, + {"id": 13990, "synset": "beverage.n.01", "name": "beverage"}, + {"id": 13991, "synset": "wish-wash.n.01", "name": "wish-wash"}, + {"id": 13992, "synset": "concoction.n.01", "name": "concoction"}, + {"id": 13993, "synset": "mix.n.01", "name": "mix"}, + {"id": 13994, "synset": "filling.n.03", "name": "filling"}, + {"id": 13995, "synset": "lekvar.n.01", "name": "lekvar"}, + {"id": 13996, "synset": "potion.n.01", "name": "potion"}, + {"id": 13997, "synset": "elixir.n.03", "name": "elixir"}, + {"id": 13998, "synset": "elixir_of_life.n.01", "name": "elixir_of_life"}, + {"id": 13999, "synset": "philter.n.01", "name": "philter"}, + {"id": 14000, "synset": "proof_spirit.n.01", "name": "proof_spirit"}, + {"id": 14001, "synset": "home_brew.n.01", "name": "home_brew"}, + {"id": 14002, "synset": "hooch.n.01", "name": "hooch"}, + {"id": 14003, "synset": "kava.n.01", "name": "kava"}, + {"id": 14004, "synset": "aperitif.n.01", "name": "aperitif"}, + {"id": 14005, "synset": "brew.n.01", "name": "brew"}, + {"id": 14006, "synset": "beer.n.01", "name": "beer"}, + {"id": 14007, "synset": "draft_beer.n.01", "name": "draft_beer"}, + {"id": 14008, "synset": "suds.n.02", "name": "suds"}, + {"id": 14009, "synset": "munich_beer.n.01", "name": "Munich_beer"}, + {"id": 14010, "synset": "bock.n.01", "name": "bock"}, + {"id": 14011, "synset": "lager.n.02", "name": "lager"}, + {"id": 14012, "synset": "light_beer.n.01", "name": "light_beer"}, + {"id": 14013, "synset": "oktoberfest.n.01", "name": "Oktoberfest"}, + {"id": 14014, "synset": "pilsner.n.01", "name": "Pilsner"}, + {"id": 14015, "synset": "shebeen.n.01", "name": "shebeen"}, + {"id": 14016, "synset": "weissbier.n.01", "name": "Weissbier"}, + {"id": 14017, "synset": "weizenbock.n.01", "name": "Weizenbock"}, + {"id": 14018, "synset": "malt.n.03", "name": "malt"}, + {"id": 14019, "synset": "wort.n.02", "name": "wort"}, + {"id": 14020, "synset": "malt.n.02", "name": "malt"}, + {"id": 14021, "synset": "ale.n.01", "name": "ale"}, + {"id": 14022, "synset": "bitter.n.01", "name": "bitter"}, + {"id": 14023, "synset": "burton.n.03", "name": "Burton"}, + {"id": 14024, "synset": "pale_ale.n.01", "name": "pale_ale"}, + {"id": 14025, "synset": "porter.n.07", "name": "porter"}, + {"id": 14026, "synset": "stout.n.01", "name": "stout"}, + {"id": 14027, "synset": "guinness.n.02", "name": "Guinness"}, + {"id": 14028, "synset": "kvass.n.01", "name": "kvass"}, + {"id": 14029, "synset": "mead.n.03", "name": "mead"}, + {"id": 14030, "synset": "metheglin.n.01", "name": "metheglin"}, + {"id": 14031, "synset": "hydromel.n.01", "name": "hydromel"}, + {"id": 14032, "synset": "oenomel.n.01", "name": "oenomel"}, + {"id": 14033, "synset": "near_beer.n.01", "name": "near_beer"}, + {"id": 14034, "synset": "ginger_beer.n.01", "name": "ginger_beer"}, + {"id": 14035, "synset": "sake.n.02", "name": "sake"}, + {"id": 14036, "synset": "wine.n.01", "name": "wine"}, + {"id": 14037, "synset": "vintage.n.01", "name": "vintage"}, + {"id": 14038, "synset": "red_wine.n.01", "name": "red_wine"}, + {"id": 14039, "synset": "white_wine.n.01", "name": "white_wine"}, + {"id": 14040, "synset": "blush_wine.n.01", "name": "blush_wine"}, + {"id": 14041, "synset": "altar_wine.n.01", "name": "altar_wine"}, + {"id": 14042, "synset": "sparkling_wine.n.01", "name": "sparkling_wine"}, + {"id": 14043, "synset": "champagne.n.01", "name": "champagne"}, + {"id": 14044, "synset": "cold_duck.n.01", "name": "cold_duck"}, + {"id": 14045, "synset": "burgundy.n.02", "name": "Burgundy"}, + {"id": 14046, "synset": "beaujolais.n.01", "name": "Beaujolais"}, + {"id": 14047, "synset": "medoc.n.01", "name": "Medoc"}, + {"id": 14048, "synset": "canary_wine.n.01", "name": "Canary_wine"}, + {"id": 14049, "synset": "chablis.n.02", "name": "Chablis"}, + {"id": 14050, "synset": "montrachet.n.01", "name": "Montrachet"}, + {"id": 14051, "synset": "chardonnay.n.02", "name": "Chardonnay"}, + {"id": 14052, "synset": "pinot_noir.n.02", "name": "Pinot_noir"}, + {"id": 14053, "synset": "pinot_blanc.n.02", "name": "Pinot_blanc"}, + {"id": 14054, "synset": "bordeaux.n.02", "name": "Bordeaux"}, + {"id": 14055, "synset": "claret.n.02", "name": "claret"}, + {"id": 14056, "synset": "chianti.n.01", "name": "Chianti"}, + {"id": 14057, "synset": "cabernet.n.01", "name": "Cabernet"}, + {"id": 14058, "synset": "merlot.n.02", "name": "Merlot"}, + {"id": 14059, "synset": "sauvignon_blanc.n.02", "name": "Sauvignon_blanc"}, + {"id": 14060, "synset": "california_wine.n.01", "name": "California_wine"}, + {"id": 14061, "synset": "cotes_de_provence.n.01", "name": "Cotes_de_Provence"}, + {"id": 14062, "synset": "dessert_wine.n.01", "name": "dessert_wine"}, + {"id": 14063, "synset": "dubonnet.n.01", "name": "Dubonnet"}, + {"id": 14064, "synset": "jug_wine.n.01", "name": "jug_wine"}, + {"id": 14065, "synset": "macon.n.02", "name": "macon"}, + {"id": 14066, "synset": "moselle.n.01", "name": "Moselle"}, + {"id": 14067, "synset": "muscadet.n.02", "name": "Muscadet"}, + {"id": 14068, "synset": "plonk.n.01", "name": "plonk"}, + {"id": 14069, "synset": "retsina.n.01", "name": "retsina"}, + {"id": 14070, "synset": "rhine_wine.n.01", "name": "Rhine_wine"}, + {"id": 14071, "synset": "riesling.n.02", "name": "Riesling"}, + {"id": 14072, "synset": "liebfraumilch.n.01", "name": "liebfraumilch"}, + {"id": 14073, "synset": "rhone_wine.n.01", "name": "Rhone_wine"}, + {"id": 14074, "synset": "rioja.n.01", "name": "Rioja"}, + {"id": 14075, "synset": "sack.n.04", "name": "sack"}, + {"id": 14076, "synset": "saint_emilion.n.01", "name": "Saint_Emilion"}, + {"id": 14077, "synset": "soave.n.01", "name": "Soave"}, + {"id": 14078, "synset": "zinfandel.n.02", "name": "zinfandel"}, + {"id": 14079, "synset": "sauterne.n.01", "name": "Sauterne"}, + {"id": 14080, "synset": "straw_wine.n.01", "name": "straw_wine"}, + {"id": 14081, "synset": "table_wine.n.01", "name": "table_wine"}, + {"id": 14082, "synset": "tokay.n.01", "name": "Tokay"}, + {"id": 14083, "synset": "vin_ordinaire.n.01", "name": "vin_ordinaire"}, + {"id": 14084, "synset": "vermouth.n.01", "name": "vermouth"}, + {"id": 14085, "synset": "sweet_vermouth.n.01", "name": "sweet_vermouth"}, + {"id": 14086, "synset": "dry_vermouth.n.01", "name": "dry_vermouth"}, + {"id": 14087, "synset": "chenin_blanc.n.02", "name": "Chenin_blanc"}, + {"id": 14088, "synset": "verdicchio.n.02", "name": "Verdicchio"}, + {"id": 14089, "synset": "vouvray.n.01", "name": "Vouvray"}, + {"id": 14090, "synset": "yquem.n.01", "name": "Yquem"}, + {"id": 14091, "synset": "generic.n.01", "name": "generic"}, + {"id": 14092, "synset": "varietal.n.01", "name": "varietal"}, + {"id": 14093, "synset": "fortified_wine.n.01", "name": "fortified_wine"}, + {"id": 14094, "synset": "madeira.n.03", "name": "Madeira"}, + {"id": 14095, "synset": "malmsey.n.01", "name": "malmsey"}, + {"id": 14096, "synset": "port.n.02", "name": "port"}, + {"id": 14097, "synset": "sherry.n.01", "name": "sherry"}, + {"id": 14098, "synset": "marsala.n.01", "name": "Marsala"}, + {"id": 14099, "synset": "muscat.n.03", "name": "muscat"}, + {"id": 14100, "synset": "neutral_spirits.n.01", "name": "neutral_spirits"}, + {"id": 14101, "synset": "aqua_vitae.n.01", "name": "aqua_vitae"}, + {"id": 14102, "synset": "eau_de_vie.n.01", "name": "eau_de_vie"}, + {"id": 14103, "synset": "moonshine.n.02", "name": "moonshine"}, + {"id": 14104, "synset": "bathtub_gin.n.01", "name": "bathtub_gin"}, + {"id": 14105, "synset": "aquavit.n.01", "name": "aquavit"}, + {"id": 14106, "synset": "arrack.n.01", "name": "arrack"}, + {"id": 14107, "synset": "bitters.n.01", "name": "bitters"}, + {"id": 14108, "synset": "brandy.n.01", "name": "brandy"}, + {"id": 14109, "synset": "applejack.n.01", "name": "applejack"}, + {"id": 14110, "synset": "calvados.n.01", "name": "Calvados"}, + {"id": 14111, "synset": "armagnac.n.01", "name": "Armagnac"}, + {"id": 14112, "synset": "cognac.n.01", "name": "Cognac"}, + {"id": 14113, "synset": "grappa.n.01", "name": "grappa"}, + {"id": 14114, "synset": "kirsch.n.01", "name": "kirsch"}, + {"id": 14115, "synset": "slivovitz.n.01", "name": "slivovitz"}, + {"id": 14116, "synset": "gin.n.01", "name": "gin"}, + {"id": 14117, "synset": "sloe_gin.n.01", "name": "sloe_gin"}, + {"id": 14118, "synset": "geneva.n.02", "name": "geneva"}, + {"id": 14119, "synset": "grog.n.01", "name": "grog"}, + {"id": 14120, "synset": "ouzo.n.01", "name": "ouzo"}, + {"id": 14121, "synset": "rum.n.01", "name": "rum"}, + {"id": 14122, "synset": "demerara.n.04", "name": "demerara"}, + {"id": 14123, "synset": "jamaica_rum.n.01", "name": "Jamaica_rum"}, + {"id": 14124, "synset": "schnapps.n.01", "name": "schnapps"}, + {"id": 14125, "synset": "pulque.n.01", "name": "pulque"}, + {"id": 14126, "synset": "mescal.n.02", "name": "mescal"}, + {"id": 14127, "synset": "whiskey.n.01", "name": "whiskey"}, + {"id": 14128, "synset": "blended_whiskey.n.01", "name": "blended_whiskey"}, + {"id": 14129, "synset": "bourbon.n.02", "name": "bourbon"}, + {"id": 14130, "synset": "corn_whiskey.n.01", "name": "corn_whiskey"}, + {"id": 14131, "synset": "firewater.n.01", "name": "firewater"}, + {"id": 14132, "synset": "irish.n.02", "name": "Irish"}, + {"id": 14133, "synset": "poteen.n.01", "name": "poteen"}, + {"id": 14134, "synset": "rye.n.03", "name": "rye"}, + {"id": 14135, "synset": "scotch.n.02", "name": "Scotch"}, + {"id": 14136, "synset": "sour_mash.n.02", "name": "sour_mash"}, + {"id": 14137, "synset": "liqueur.n.01", "name": "liqueur"}, + {"id": 14138, "synset": "absinth.n.01", "name": "absinth"}, + {"id": 14139, "synset": "amaretto.n.01", "name": "amaretto"}, + {"id": 14140, "synset": "anisette.n.01", "name": "anisette"}, + {"id": 14141, "synset": "benedictine.n.02", "name": "benedictine"}, + {"id": 14142, "synset": "chartreuse.n.01", "name": "Chartreuse"}, + {"id": 14143, "synset": "coffee_liqueur.n.01", "name": "coffee_liqueur"}, + {"id": 14144, "synset": "creme_de_cacao.n.01", "name": "creme_de_cacao"}, + {"id": 14145, "synset": "creme_de_menthe.n.01", "name": "creme_de_menthe"}, + {"id": 14146, "synset": "creme_de_fraise.n.01", "name": "creme_de_fraise"}, + {"id": 14147, "synset": "drambuie.n.01", "name": "Drambuie"}, + {"id": 14148, "synset": "galliano.n.01", "name": "Galliano"}, + {"id": 14149, "synset": "orange_liqueur.n.01", "name": "orange_liqueur"}, + {"id": 14150, "synset": "curacao.n.02", "name": "curacao"}, + {"id": 14151, "synset": "triple_sec.n.01", "name": "triple_sec"}, + {"id": 14152, "synset": "grand_marnier.n.01", "name": "Grand_Marnier"}, + {"id": 14153, "synset": "kummel.n.01", "name": "kummel"}, + {"id": 14154, "synset": "maraschino.n.01", "name": "maraschino"}, + {"id": 14155, "synset": "pastis.n.01", "name": "pastis"}, + {"id": 14156, "synset": "pernod.n.01", "name": "Pernod"}, + {"id": 14157, "synset": "pousse-cafe.n.01", "name": "pousse-cafe"}, + {"id": 14158, "synset": "kahlua.n.01", "name": "Kahlua"}, + {"id": 14159, "synset": "ratafia.n.01", "name": "ratafia"}, + {"id": 14160, "synset": "sambuca.n.01", "name": "sambuca"}, + {"id": 14161, "synset": "mixed_drink.n.01", "name": "mixed_drink"}, + {"id": 14162, "synset": "cocktail.n.01", "name": "cocktail"}, + {"id": 14163, "synset": "dom_pedro.n.01", "name": "Dom_Pedro"}, + {"id": 14164, "synset": "highball.n.01", "name": "highball"}, + {"id": 14165, "synset": "mixer.n.02", "name": "mixer"}, + {"id": 14166, "synset": "bishop.n.02", "name": "bishop"}, + {"id": 14167, "synset": "bloody_mary.n.02", "name": "Bloody_Mary"}, + {"id": 14168, "synset": "virgin_mary.n.02", "name": "Virgin_Mary"}, + {"id": 14169, "synset": "bullshot.n.01", "name": "bullshot"}, + {"id": 14170, "synset": "cobbler.n.02", "name": "cobbler"}, + {"id": 14171, "synset": "collins.n.02", "name": "collins"}, + {"id": 14172, "synset": "cooler.n.02", "name": "cooler"}, + {"id": 14173, "synset": "refresher.n.02", "name": "refresher"}, + {"id": 14174, "synset": "daiquiri.n.01", "name": "daiquiri"}, + {"id": 14175, "synset": "strawberry_daiquiri.n.01", "name": "strawberry_daiquiri"}, + {"id": 14176, "synset": "nada_daiquiri.n.01", "name": "NADA_daiquiri"}, + {"id": 14177, "synset": "spritzer.n.01", "name": "spritzer"}, + {"id": 14178, "synset": "flip.n.02", "name": "flip"}, + {"id": 14179, "synset": "gimlet.n.01", "name": "gimlet"}, + {"id": 14180, "synset": "gin_and_tonic.n.01", "name": "gin_and_tonic"}, + {"id": 14181, "synset": "grasshopper.n.02", "name": "grasshopper"}, + {"id": 14182, "synset": "harvey_wallbanger.n.01", "name": "Harvey_Wallbanger"}, + {"id": 14183, "synset": "julep.n.01", "name": "julep"}, + {"id": 14184, "synset": "manhattan.n.02", "name": "manhattan"}, + {"id": 14185, "synset": "rob_roy.n.02", "name": "Rob_Roy"}, + {"id": 14186, "synset": "margarita.n.01", "name": "margarita"}, + {"id": 14187, "synset": "gin_and_it.n.01", "name": "gin_and_it"}, + {"id": 14188, "synset": "vodka_martini.n.01", "name": "vodka_martini"}, + {"id": 14189, "synset": "old_fashioned.n.01", "name": "old_fashioned"}, + {"id": 14190, "synset": "pink_lady.n.01", "name": "pink_lady"}, + {"id": 14191, "synset": "sazerac.n.01", "name": "Sazerac"}, + {"id": 14192, "synset": "screwdriver.n.02", "name": "screwdriver"}, + {"id": 14193, "synset": "sidecar.n.01", "name": "sidecar"}, + {"id": 14194, "synset": "scotch_and_soda.n.01", "name": "Scotch_and_soda"}, + {"id": 14195, "synset": "sling.n.01", "name": "sling"}, + {"id": 14196, "synset": "brandy_sling.n.01", "name": "brandy_sling"}, + {"id": 14197, "synset": "gin_sling.n.01", "name": "gin_sling"}, + {"id": 14198, "synset": "rum_sling.n.01", "name": "rum_sling"}, + {"id": 14199, "synset": "sour.n.01", "name": "sour"}, + {"id": 14200, "synset": "whiskey_sour.n.01", "name": "whiskey_sour"}, + {"id": 14201, "synset": "stinger.n.01", "name": "stinger"}, + {"id": 14202, "synset": "swizzle.n.01", "name": "swizzle"}, + {"id": 14203, "synset": "hot_toddy.n.01", "name": "hot_toddy"}, + {"id": 14204, "synset": "zombie.n.05", "name": "zombie"}, + {"id": 14205, "synset": "fizz.n.01", "name": "fizz"}, + {"id": 14206, "synset": "irish_coffee.n.01", "name": "Irish_coffee"}, + {"id": 14207, "synset": "cafe_au_lait.n.01", "name": "cafe_au_lait"}, + {"id": 14208, "synset": "cafe_noir.n.01", "name": "cafe_noir"}, + {"id": 14209, "synset": "decaffeinated_coffee.n.01", "name": "decaffeinated_coffee"}, + {"id": 14210, "synset": "drip_coffee.n.01", "name": "drip_coffee"}, + {"id": 14211, "synset": "espresso.n.01", "name": "espresso"}, + {"id": 14212, "synset": "caffe_latte.n.01", "name": "caffe_latte"}, + {"id": 14213, "synset": "iced_coffee.n.01", "name": "iced_coffee"}, + {"id": 14214, "synset": "instant_coffee.n.01", "name": "instant_coffee"}, + {"id": 14215, "synset": "mocha.n.03", "name": "mocha"}, + {"id": 14216, "synset": "mocha.n.02", "name": "mocha"}, + {"id": 14217, "synset": "cassareep.n.01", "name": "cassareep"}, + {"id": 14218, "synset": "turkish_coffee.n.01", "name": "Turkish_coffee"}, + {"id": 14219, "synset": "hard_cider.n.01", "name": "hard_cider"}, + {"id": 14220, "synset": "scrumpy.n.01", "name": "scrumpy"}, + {"id": 14221, "synset": "sweet_cider.n.01", "name": "sweet_cider"}, + {"id": 14222, "synset": "mulled_cider.n.01", "name": "mulled_cider"}, + {"id": 14223, "synset": "perry.n.04", "name": "perry"}, + {"id": 14224, "synset": "rotgut.n.01", "name": "rotgut"}, + {"id": 14225, "synset": "slug.n.05", "name": "slug"}, + {"id": 14226, "synset": "criollo.n.02", "name": "criollo"}, + {"id": 14227, "synset": "juice.n.01", "name": "juice"}, + {"id": 14228, "synset": "nectar.n.02", "name": "nectar"}, + {"id": 14229, "synset": "apple_juice.n.01", "name": "apple_juice"}, + {"id": 14230, "synset": "cranberry_juice.n.01", "name": "cranberry_juice"}, + {"id": 14231, "synset": "grape_juice.n.01", "name": "grape_juice"}, + {"id": 14232, "synset": "must.n.02", "name": "must"}, + {"id": 14233, "synset": "grapefruit_juice.n.01", "name": "grapefruit_juice"}, + {"id": 14234, "synset": "frozen_orange_juice.n.01", "name": "frozen_orange_juice"}, + {"id": 14235, "synset": "pineapple_juice.n.01", "name": "pineapple_juice"}, + {"id": 14236, "synset": "lemon_juice.n.01", "name": "lemon_juice"}, + {"id": 14237, "synset": "lime_juice.n.01", "name": "lime_juice"}, + {"id": 14238, "synset": "papaya_juice.n.01", "name": "papaya_juice"}, + {"id": 14239, "synset": "tomato_juice.n.01", "name": "tomato_juice"}, + {"id": 14240, "synset": "carrot_juice.n.01", "name": "carrot_juice"}, + {"id": 14241, "synset": "v-8_juice.n.01", "name": "V-8_juice"}, + {"id": 14242, "synset": "koumiss.n.01", "name": "koumiss"}, + {"id": 14243, "synset": "fruit_drink.n.01", "name": "fruit_drink"}, + {"id": 14244, "synset": "limeade.n.01", "name": "limeade"}, + {"id": 14245, "synset": "orangeade.n.01", "name": "orangeade"}, + {"id": 14246, "synset": "malted_milk.n.02", "name": "malted_milk"}, + {"id": 14247, "synset": "mate.n.09", "name": "mate"}, + {"id": 14248, "synset": "mulled_wine.n.01", "name": "mulled_wine"}, + {"id": 14249, "synset": "negus.n.01", "name": "negus"}, + {"id": 14250, "synset": "soft_drink.n.01", "name": "soft_drink"}, + {"id": 14251, "synset": "birch_beer.n.01", "name": "birch_beer"}, + {"id": 14252, "synset": "bitter_lemon.n.01", "name": "bitter_lemon"}, + {"id": 14253, "synset": "cola.n.02", "name": "cola"}, + {"id": 14254, "synset": "cream_soda.n.01", "name": "cream_soda"}, + {"id": 14255, "synset": "egg_cream.n.01", "name": "egg_cream"}, + {"id": 14256, "synset": "ginger_ale.n.01", "name": "ginger_ale"}, + {"id": 14257, "synset": "orange_soda.n.01", "name": "orange_soda"}, + {"id": 14258, "synset": "phosphate.n.02", "name": "phosphate"}, + {"id": 14259, "synset": "coca_cola.n.01", "name": "Coca_Cola"}, + {"id": 14260, "synset": "pepsi.n.01", "name": "Pepsi"}, + {"id": 14261, "synset": "sarsaparilla.n.02", "name": "sarsaparilla"}, + {"id": 14262, "synset": "tonic.n.01", "name": "tonic"}, + {"id": 14263, "synset": "coffee_bean.n.01", "name": "coffee_bean"}, + {"id": 14264, "synset": "coffee.n.01", "name": "coffee"}, + {"id": 14265, "synset": "cafe_royale.n.01", "name": "cafe_royale"}, + {"id": 14266, "synset": "fruit_punch.n.01", "name": "fruit_punch"}, + {"id": 14267, "synset": "milk_punch.n.01", "name": "milk_punch"}, + {"id": 14268, "synset": "mimosa.n.03", "name": "mimosa"}, + {"id": 14269, "synset": "pina_colada.n.01", "name": "pina_colada"}, + {"id": 14270, "synset": "punch.n.02", "name": "punch"}, + {"id": 14271, "synset": "cup.n.06", "name": "cup"}, + {"id": 14272, "synset": "champagne_cup.n.01", "name": "champagne_cup"}, + {"id": 14273, "synset": "claret_cup.n.01", "name": "claret_cup"}, + {"id": 14274, "synset": "wassail.n.01", "name": "wassail"}, + {"id": 14275, "synset": "planter's_punch.n.01", "name": "planter's_punch"}, + {"id": 14276, "synset": "white_russian.n.02", "name": "White_Russian"}, + {"id": 14277, "synset": "fish_house_punch.n.01", "name": "fish_house_punch"}, + {"id": 14278, "synset": "may_wine.n.01", "name": "May_wine"}, + {"id": 14279, "synset": "eggnog.n.01", "name": "eggnog"}, + {"id": 14280, "synset": "cassiri.n.01", "name": "cassiri"}, + {"id": 14281, "synset": "spruce_beer.n.01", "name": "spruce_beer"}, + {"id": 14282, "synset": "rickey.n.01", "name": "rickey"}, + {"id": 14283, "synset": "gin_rickey.n.01", "name": "gin_rickey"}, + {"id": 14284, "synset": "tea.n.05", "name": "tea"}, + {"id": 14285, "synset": "tea.n.01", "name": "tea"}, + {"id": 14286, "synset": "tea-like_drink.n.01", "name": "tea-like_drink"}, + {"id": 14287, "synset": "cambric_tea.n.01", "name": "cambric_tea"}, + {"id": 14288, "synset": "cuppa.n.01", "name": "cuppa"}, + {"id": 14289, "synset": "herb_tea.n.01", "name": "herb_tea"}, + {"id": 14290, "synset": "tisane.n.01", "name": "tisane"}, + {"id": 14291, "synset": "camomile_tea.n.01", "name": "camomile_tea"}, + {"id": 14292, "synset": "ice_tea.n.01", "name": "ice_tea"}, + {"id": 14293, "synset": "sun_tea.n.01", "name": "sun_tea"}, + {"id": 14294, "synset": "black_tea.n.01", "name": "black_tea"}, + {"id": 14295, "synset": "congou.n.01", "name": "congou"}, + {"id": 14296, "synset": "darjeeling.n.01", "name": "Darjeeling"}, + {"id": 14297, "synset": "orange_pekoe.n.01", "name": "orange_pekoe"}, + {"id": 14298, "synset": "souchong.n.01", "name": "souchong"}, + {"id": 14299, "synset": "green_tea.n.01", "name": "green_tea"}, + {"id": 14300, "synset": "hyson.n.01", "name": "hyson"}, + {"id": 14301, "synset": "oolong.n.01", "name": "oolong"}, + {"id": 14302, "synset": "water.n.06", "name": "water"}, + {"id": 14303, "synset": "bottled_water.n.01", "name": "bottled_water"}, + {"id": 14304, "synset": "branch_water.n.01", "name": "branch_water"}, + {"id": 14305, "synset": "spring_water.n.02", "name": "spring_water"}, + {"id": 14306, "synset": "sugar_water.n.01", "name": "sugar_water"}, + {"id": 14307, "synset": "drinking_water.n.01", "name": "drinking_water"}, + {"id": 14308, "synset": "ice_water.n.01", "name": "ice_water"}, + {"id": 14309, "synset": "soda_water.n.01", "name": "soda_water"}, + {"id": 14310, "synset": "mineral_water.n.01", "name": "mineral_water"}, + {"id": 14311, "synset": "seltzer.n.01", "name": "seltzer"}, + {"id": 14312, "synset": "vichy_water.n.01", "name": "Vichy_water"}, + {"id": 14313, "synset": "perishable.n.01", "name": "perishable"}, + {"id": 14314, "synset": "couscous.n.01", "name": "couscous"}, + {"id": 14315, "synset": "ramekin.n.01", "name": "ramekin"}, + {"id": 14316, "synset": "multivitamin.n.01", "name": "multivitamin"}, + {"id": 14317, "synset": "vitamin_pill.n.01", "name": "vitamin_pill"}, + {"id": 14318, "synset": "soul_food.n.01", "name": "soul_food"}, + {"id": 14319, "synset": "mold.n.06", "name": "mold"}, + {"id": 14320, "synset": "people.n.01", "name": "people"}, + {"id": 14321, "synset": "collection.n.01", "name": "collection"}, + {"id": 14322, "synset": "book.n.07", "name": "book"}, + {"id": 14323, "synset": "library.n.02", "name": "library"}, + {"id": 14324, "synset": "baseball_club.n.01", "name": "baseball_club"}, + {"id": 14325, "synset": "crowd.n.01", "name": "crowd"}, + {"id": 14326, "synset": "class.n.02", "name": "class"}, + {"id": 14327, "synset": "core.n.01", "name": "core"}, + {"id": 14328, "synset": "concert_band.n.01", "name": "concert_band"}, + {"id": 14329, "synset": "dance.n.02", "name": "dance"}, + {"id": 14330, "synset": "wedding.n.03", "name": "wedding"}, + {"id": 14331, "synset": "chain.n.01", "name": "chain"}, + {"id": 14332, "synset": "power_breakfast.n.01", "name": "power_breakfast"}, + {"id": 14333, "synset": "aerie.n.02", "name": "aerie"}, + {"id": 14334, "synset": "agora.n.02", "name": "agora"}, + {"id": 14335, "synset": "amusement_park.n.01", "name": "amusement_park"}, + {"id": 14336, "synset": "aphelion.n.01", "name": "aphelion"}, + {"id": 14337, "synset": "apron.n.02", "name": "apron"}, + {"id": 14338, "synset": "interplanetary_space.n.01", "name": "interplanetary_space"}, + {"id": 14339, "synset": "interstellar_space.n.01", "name": "interstellar_space"}, + {"id": 14340, "synset": "intergalactic_space.n.01", "name": "intergalactic_space"}, + {"id": 14341, "synset": "bush.n.02", "name": "bush"}, + {"id": 14342, "synset": "semidesert.n.01", "name": "semidesert"}, + {"id": 14343, "synset": "beam-ends.n.01", "name": "beam-ends"}, + {"id": 14344, "synset": "bridgehead.n.02", "name": "bridgehead"}, + {"id": 14345, "synset": "bus_stop.n.01", "name": "bus_stop"}, + {"id": 14346, "synset": "campsite.n.01", "name": "campsite"}, + {"id": 14347, "synset": "detention_basin.n.01", "name": "detention_basin"}, + {"id": 14348, "synset": "cemetery.n.01", "name": "cemetery"}, + {"id": 14349, "synset": "trichion.n.01", "name": "trichion"}, + {"id": 14350, "synset": "city.n.01", "name": "city"}, + {"id": 14351, "synset": "business_district.n.01", "name": "business_district"}, + {"id": 14352, "synset": "outskirts.n.01", "name": "outskirts"}, + {"id": 14353, "synset": "borough.n.01", "name": "borough"}, + {"id": 14354, "synset": "cow_pasture.n.01", "name": "cow_pasture"}, + {"id": 14355, "synset": "crest.n.01", "name": "crest"}, + {"id": 14356, "synset": "eparchy.n.02", "name": "eparchy"}, + {"id": 14357, "synset": "suburb.n.01", "name": "suburb"}, + {"id": 14358, "synset": "stockbroker_belt.n.01", "name": "stockbroker_belt"}, + {"id": 14359, "synset": "crawlspace.n.01", "name": "crawlspace"}, + {"id": 14360, "synset": "sheikdom.n.01", "name": "sheikdom"}, + {"id": 14361, "synset": "residence.n.01", "name": "residence"}, + {"id": 14362, "synset": "domicile.n.01", "name": "domicile"}, + {"id": 14363, "synset": "dude_ranch.n.01", "name": "dude_ranch"}, + {"id": 14364, "synset": "farmland.n.01", "name": "farmland"}, + {"id": 14365, "synset": "midfield.n.01", "name": "midfield"}, + {"id": 14366, "synset": "firebreak.n.01", "name": "firebreak"}, + {"id": 14367, "synset": "flea_market.n.01", "name": "flea_market"}, + {"id": 14368, "synset": "battlefront.n.01", "name": "battlefront"}, + {"id": 14369, "synset": "garbage_heap.n.01", "name": "garbage_heap"}, + {"id": 14370, "synset": "benthos.n.01", "name": "benthos"}, + {"id": 14371, "synset": "goldfield.n.01", "name": "goldfield"}, + {"id": 14372, "synset": "grainfield.n.01", "name": "grainfield"}, + {"id": 14373, "synset": "half-mast.n.01", "name": "half-mast"}, + {"id": 14374, "synset": "hemline.n.01", "name": "hemline"}, + {"id": 14375, "synset": "heronry.n.01", "name": "heronry"}, + {"id": 14376, "synset": "hipline.n.02", "name": "hipline"}, + {"id": 14377, "synset": "hipline.n.01", "name": "hipline"}, + {"id": 14378, "synset": "hole-in-the-wall.n.01", "name": "hole-in-the-wall"}, + {"id": 14379, "synset": "junkyard.n.01", "name": "junkyard"}, + {"id": 14380, "synset": "isoclinic_line.n.01", "name": "isoclinic_line"}, + {"id": 14381, "synset": "littoral.n.01", "name": "littoral"}, + {"id": 14382, "synset": "magnetic_pole.n.01", "name": "magnetic_pole"}, + {"id": 14383, "synset": "grassland.n.01", "name": "grassland"}, + {"id": 14384, "synset": "mecca.n.02", "name": "mecca"}, + {"id": 14385, "synset": "observer's_meridian.n.01", "name": "observer's_meridian"}, + {"id": 14386, "synset": "prime_meridian.n.01", "name": "prime_meridian"}, + {"id": 14387, "synset": "nombril.n.01", "name": "nombril"}, + {"id": 14388, "synset": "no-parking_zone.n.01", "name": "no-parking_zone"}, + {"id": 14389, "synset": "outdoors.n.01", "name": "outdoors"}, + {"id": 14390, "synset": "fairground.n.01", "name": "fairground"}, + {"id": 14391, "synset": "pasture.n.01", "name": "pasture"}, + {"id": 14392, "synset": "perihelion.n.01", "name": "perihelion"}, + {"id": 14393, "synset": "periselene.n.01", "name": "periselene"}, + {"id": 14394, "synset": "locus_of_infection.n.01", "name": "locus_of_infection"}, + {"id": 14395, "synset": "kasbah.n.01", "name": "kasbah"}, + {"id": 14396, "synset": "waterfront.n.01", "name": "waterfront"}, + {"id": 14397, "synset": "resort.n.01", "name": "resort"}, + {"id": 14398, "synset": "resort_area.n.01", "name": "resort_area"}, + {"id": 14399, "synset": "rough.n.01", "name": "rough"}, + {"id": 14400, "synset": "ashram.n.02", "name": "ashram"}, + {"id": 14401, "synset": "harborage.n.01", "name": "harborage"}, + {"id": 14402, "synset": "scrubland.n.01", "name": "scrubland"}, + {"id": 14403, "synset": "weald.n.01", "name": "weald"}, + {"id": 14404, "synset": "wold.n.01", "name": "wold"}, + {"id": 14405, "synset": "schoolyard.n.01", "name": "schoolyard"}, + {"id": 14406, "synset": "showplace.n.01", "name": "showplace"}, + {"id": 14407, "synset": "bedside.n.01", "name": "bedside"}, + {"id": 14408, "synset": "sideline.n.01", "name": "sideline"}, + {"id": 14409, "synset": "ski_resort.n.01", "name": "ski_resort"}, + {"id": 14410, "synset": "soil_horizon.n.01", "name": "soil_horizon"}, + {"id": 14411, "synset": "geological_horizon.n.01", "name": "geological_horizon"}, + {"id": 14412, "synset": "coal_seam.n.01", "name": "coal_seam"}, + {"id": 14413, "synset": "coalface.n.01", "name": "coalface"}, + {"id": 14414, "synset": "field.n.14", "name": "field"}, + {"id": 14415, "synset": "oilfield.n.01", "name": "oilfield"}, + {"id": 14416, "synset": "temperate_zone.n.01", "name": "Temperate_Zone"}, + {"id": 14417, "synset": "terreplein.n.01", "name": "terreplein"}, + {"id": 14418, "synset": "three-mile_limit.n.01", "name": "three-mile_limit"}, + {"id": 14419, "synset": "desktop.n.01", "name": "desktop"}, + {"id": 14420, "synset": "top.n.01", "name": "top"}, + {"id": 14421, "synset": "kampong.n.01", "name": "kampong"}, + {"id": 14422, "synset": "subtropics.n.01", "name": "subtropics"}, + {"id": 14423, "synset": "barrio.n.02", "name": "barrio"}, + {"id": 14424, "synset": "veld.n.01", "name": "veld"}, + {"id": 14425, "synset": "vertex.n.02", "name": "vertex"}, + {"id": 14426, "synset": "waterline.n.01", "name": "waterline"}, + {"id": 14427, "synset": "high-water_mark.n.01", "name": "high-water_mark"}, + {"id": 14428, "synset": "low-water_mark.n.02", "name": "low-water_mark"}, + {"id": 14429, "synset": "continental_divide.n.01", "name": "continental_divide"}, + {"id": 14430, "synset": "zodiac.n.01", "name": "zodiac"}, + {"id": 14431, "synset": "aegean_island.n.01", "name": "Aegean_island"}, + {"id": 14432, "synset": "sultanate.n.01", "name": "sultanate"}, + {"id": 14433, "synset": "swiss_canton.n.01", "name": "Swiss_canton"}, + {"id": 14434, "synset": "abyssal_zone.n.01", "name": "abyssal_zone"}, + {"id": 14435, "synset": "aerie.n.01", "name": "aerie"}, + {"id": 14436, "synset": "air_bubble.n.01", "name": "air_bubble"}, + {"id": 14437, "synset": "alluvial_flat.n.01", "name": "alluvial_flat"}, + {"id": 14438, "synset": "alp.n.01", "name": "alp"}, + {"id": 14439, "synset": "alpine_glacier.n.01", "name": "Alpine_glacier"}, + {"id": 14440, "synset": "anthill.n.01", "name": "anthill"}, + {"id": 14441, "synset": "aquifer.n.01", "name": "aquifer"}, + {"id": 14442, "synset": "archipelago.n.01", "name": "archipelago"}, + {"id": 14443, "synset": "arete.n.01", "name": "arete"}, + {"id": 14444, "synset": "arroyo.n.01", "name": "arroyo"}, + {"id": 14445, "synset": "ascent.n.01", "name": "ascent"}, + {"id": 14446, "synset": "asterism.n.02", "name": "asterism"}, + {"id": 14447, "synset": "asthenosphere.n.01", "name": "asthenosphere"}, + {"id": 14448, "synset": "atoll.n.01", "name": "atoll"}, + {"id": 14449, "synset": "bank.n.03", "name": "bank"}, + {"id": 14450, "synset": "bank.n.01", "name": "bank"}, + {"id": 14451, "synset": "bar.n.08", "name": "bar"}, + {"id": 14452, "synset": "barbecue_pit.n.01", "name": "barbecue_pit"}, + {"id": 14453, "synset": "barrier_reef.n.01", "name": "barrier_reef"}, + {"id": 14454, "synset": "baryon.n.01", "name": "baryon"}, + {"id": 14455, "synset": "basin.n.03", "name": "basin"}, + {"id": 14456, "synset": "beach.n.01", "name": "beach"}, + {"id": 14457, "synset": "honeycomb.n.01", "name": "honeycomb"}, + {"id": 14458, "synset": "belay.n.01", "name": "belay"}, + {"id": 14459, "synset": "ben.n.01", "name": "ben"}, + {"id": 14460, "synset": "berm.n.01", "name": "berm"}, + {"id": 14461, "synset": "bladder_stone.n.01", "name": "bladder_stone"}, + {"id": 14462, "synset": "bluff.n.01", "name": "bluff"}, + {"id": 14463, "synset": "borrow_pit.n.01", "name": "borrow_pit"}, + {"id": 14464, "synset": "brae.n.01", "name": "brae"}, + {"id": 14465, "synset": "bubble.n.01", "name": "bubble"}, + {"id": 14466, "synset": "burrow.n.01", "name": "burrow"}, + {"id": 14467, "synset": "butte.n.01", "name": "butte"}, + {"id": 14468, "synset": "caldera.n.01", "name": "caldera"}, + {"id": 14469, "synset": "canyon.n.01", "name": "canyon"}, + {"id": 14470, "synset": "canyonside.n.01", "name": "canyonside"}, + {"id": 14471, "synset": "cave.n.01", "name": "cave"}, + {"id": 14472, "synset": "cavern.n.02", "name": "cavern"}, + {"id": 14473, "synset": "chasm.n.01", "name": "chasm"}, + {"id": 14474, "synset": "cirque.n.01", "name": "cirque"}, + {"id": 14475, "synset": "cliff.n.01", "name": "cliff"}, + {"id": 14476, "synset": "cloud.n.02", "name": "cloud"}, + {"id": 14477, "synset": "coast.n.02", "name": "coast"}, + {"id": 14478, "synset": "coastland.n.01", "name": "coastland"}, + {"id": 14479, "synset": "col.n.01", "name": "col"}, + {"id": 14480, "synset": "collector.n.03", "name": "collector"}, + {"id": 14481, "synset": "comet.n.01", "name": "comet"}, + {"id": 14482, "synset": "continental_glacier.n.01", "name": "continental_glacier"}, + {"id": 14483, "synset": "coral_reef.n.01", "name": "coral_reef"}, + {"id": 14484, "synset": "cove.n.02", "name": "cove"}, + {"id": 14485, "synset": "crag.n.01", "name": "crag"}, + {"id": 14486, "synset": "crater.n.03", "name": "crater"}, + {"id": 14487, "synset": "cultivated_land.n.01", "name": "cultivated_land"}, + {"id": 14488, "synset": "dale.n.01", "name": "dale"}, + {"id": 14489, "synset": "defile.n.01", "name": "defile"}, + {"id": 14490, "synset": "delta.n.01", "name": "delta"}, + {"id": 14491, "synset": "descent.n.05", "name": "descent"}, + {"id": 14492, "synset": "diapir.n.01", "name": "diapir"}, + {"id": 14493, "synset": "divot.n.02", "name": "divot"}, + {"id": 14494, "synset": "divot.n.01", "name": "divot"}, + {"id": 14495, "synset": "down.n.04", "name": "down"}, + {"id": 14496, "synset": "downhill.n.01", "name": "downhill"}, + {"id": 14497, "synset": "draw.n.01", "name": "draw"}, + {"id": 14498, "synset": "drey.n.01", "name": "drey"}, + {"id": 14499, "synset": "drumlin.n.01", "name": "drumlin"}, + {"id": 14500, "synset": "dune.n.01", "name": "dune"}, + {"id": 14501, "synset": "escarpment.n.01", "name": "escarpment"}, + {"id": 14502, "synset": "esker.n.01", "name": "esker"}, + {"id": 14503, "synset": "fireball.n.03", "name": "fireball"}, + {"id": 14504, "synset": "flare_star.n.01", "name": "flare_star"}, + {"id": 14505, "synset": "floor.n.04", "name": "floor"}, + {"id": 14506, "synset": "fomite.n.01", "name": "fomite"}, + {"id": 14507, "synset": "foothill.n.01", "name": "foothill"}, + {"id": 14508, "synset": "footwall.n.01", "name": "footwall"}, + {"id": 14509, "synset": "foreland.n.02", "name": "foreland"}, + {"id": 14510, "synset": "foreshore.n.01", "name": "foreshore"}, + {"id": 14511, "synset": "gauge_boson.n.01", "name": "gauge_boson"}, + {"id": 14512, "synset": "geological_formation.n.01", "name": "geological_formation"}, + {"id": 14513, "synset": "geyser.n.01", "name": "geyser"}, + {"id": 14514, "synset": "glacier.n.01", "name": "glacier"}, + {"id": 14515, "synset": "glen.n.01", "name": "glen"}, + {"id": 14516, "synset": "gopher_hole.n.01", "name": "gopher_hole"}, + {"id": 14517, "synset": "gorge.n.01", "name": "gorge"}, + {"id": 14518, "synset": "grotto.n.01", "name": "grotto"}, + {"id": 14519, "synset": "growler.n.02", "name": "growler"}, + {"id": 14520, "synset": "gulch.n.01", "name": "gulch"}, + {"id": 14521, "synset": "gully.n.01", "name": "gully"}, + {"id": 14522, "synset": "hail.n.02", "name": "hail"}, + {"id": 14523, "synset": "highland.n.01", "name": "highland"}, + {"id": 14524, "synset": "hill.n.01", "name": "hill"}, + {"id": 14525, "synset": "hillside.n.01", "name": "hillside"}, + {"id": 14526, "synset": "hole.n.05", "name": "hole"}, + {"id": 14527, "synset": "hollow.n.02", "name": "hollow"}, + {"id": 14528, "synset": "hot_spring.n.01", "name": "hot_spring"}, + {"id": 14529, "synset": "iceberg.n.01", "name": "iceberg"}, + {"id": 14530, "synset": "icecap.n.01", "name": "icecap"}, + {"id": 14531, "synset": "ice_field.n.01", "name": "ice_field"}, + {"id": 14532, "synset": "ice_floe.n.01", "name": "ice_floe"}, + {"id": 14533, "synset": "ice_mass.n.01", "name": "ice_mass"}, + {"id": 14534, "synset": "inclined_fault.n.01", "name": "inclined_fault"}, + {"id": 14535, "synset": "ion.n.01", "name": "ion"}, + {"id": 14536, "synset": "isthmus.n.01", "name": "isthmus"}, + {"id": 14537, "synset": "kidney_stone.n.01", "name": "kidney_stone"}, + {"id": 14538, "synset": "knoll.n.01", "name": "knoll"}, + {"id": 14539, "synset": "kopje.n.01", "name": "kopje"}, + {"id": 14540, "synset": "kuiper_belt.n.01", "name": "Kuiper_belt"}, + {"id": 14541, "synset": "lake_bed.n.01", "name": "lake_bed"}, + {"id": 14542, "synset": "lakefront.n.01", "name": "lakefront"}, + {"id": 14543, "synset": "lakeside.n.01", "name": "lakeside"}, + {"id": 14544, "synset": "landfall.n.01", "name": "landfall"}, + {"id": 14545, "synset": "landfill.n.01", "name": "landfill"}, + {"id": 14546, "synset": "lather.n.04", "name": "lather"}, + {"id": 14547, "synset": "leak.n.01", "name": "leak"}, + {"id": 14548, "synset": "ledge.n.01", "name": "ledge"}, + {"id": 14549, "synset": "lepton.n.02", "name": "lepton"}, + {"id": 14550, "synset": "lithosphere.n.01", "name": "lithosphere"}, + {"id": 14551, "synset": "lowland.n.01", "name": "lowland"}, + {"id": 14552, "synset": "lunar_crater.n.01", "name": "lunar_crater"}, + {"id": 14553, "synset": "maar.n.01", "name": "maar"}, + {"id": 14554, "synset": "massif.n.01", "name": "massif"}, + {"id": 14555, "synset": "meander.n.01", "name": "meander"}, + {"id": 14556, "synset": "mesa.n.01", "name": "mesa"}, + {"id": 14557, "synset": "meteorite.n.01", "name": "meteorite"}, + {"id": 14558, "synset": "microfossil.n.01", "name": "microfossil"}, + {"id": 14559, "synset": "midstream.n.01", "name": "midstream"}, + {"id": 14560, "synset": "molehill.n.01", "name": "molehill"}, + {"id": 14561, "synset": "monocline.n.01", "name": "monocline"}, + {"id": 14562, "synset": "mountain.n.01", "name": "mountain"}, + {"id": 14563, "synset": "mountainside.n.01", "name": "mountainside"}, + {"id": 14564, "synset": "mouth.n.04", "name": "mouth"}, + {"id": 14565, "synset": "mull.n.01", "name": "mull"}, + {"id": 14566, "synset": "natural_depression.n.01", "name": "natural_depression"}, + {"id": 14567, "synset": "natural_elevation.n.01", "name": "natural_elevation"}, + {"id": 14568, "synset": "nullah.n.01", "name": "nullah"}, + {"id": 14569, "synset": "ocean.n.01", "name": "ocean"}, + {"id": 14570, "synset": "ocean_floor.n.01", "name": "ocean_floor"}, + {"id": 14571, "synset": "oceanfront.n.01", "name": "oceanfront"}, + {"id": 14572, "synset": "outcrop.n.01", "name": "outcrop"}, + {"id": 14573, "synset": "oxbow.n.01", "name": "oxbow"}, + {"id": 14574, "synset": "pallasite.n.01", "name": "pallasite"}, + {"id": 14575, "synset": "perforation.n.02", "name": "perforation"}, + {"id": 14576, "synset": "photosphere.n.01", "name": "photosphere"}, + {"id": 14577, "synset": "piedmont.n.02", "name": "piedmont"}, + {"id": 14578, "synset": "piedmont_glacier.n.01", "name": "Piedmont_glacier"}, + {"id": 14579, "synset": "pinetum.n.01", "name": "pinetum"}, + {"id": 14580, "synset": "plage.n.01", "name": "plage"}, + {"id": 14581, "synset": "plain.n.01", "name": "plain"}, + {"id": 14582, "synset": "point.n.11", "name": "point"}, + {"id": 14583, "synset": "polar_glacier.n.01", "name": "polar_glacier"}, + {"id": 14584, "synset": "pothole.n.01", "name": "pothole"}, + {"id": 14585, "synset": "precipice.n.01", "name": "precipice"}, + {"id": 14586, "synset": "promontory.n.01", "name": "promontory"}, + {"id": 14587, "synset": "ptyalith.n.01", "name": "ptyalith"}, + {"id": 14588, "synset": "pulsar.n.01", "name": "pulsar"}, + {"id": 14589, "synset": "quicksand.n.02", "name": "quicksand"}, + {"id": 14590, "synset": "rabbit_burrow.n.01", "name": "rabbit_burrow"}, + {"id": 14591, "synset": "radiator.n.01", "name": "radiator"}, + {"id": 14592, "synset": "rainbow.n.01", "name": "rainbow"}, + {"id": 14593, "synset": "range.n.04", "name": "range"}, + {"id": 14594, "synset": "rangeland.n.01", "name": "rangeland"}, + {"id": 14595, "synset": "ravine.n.01", "name": "ravine"}, + {"id": 14596, "synset": "reef.n.01", "name": "reef"}, + {"id": 14597, "synset": "ridge.n.01", "name": "ridge"}, + {"id": 14598, "synset": "ridge.n.04", "name": "ridge"}, + {"id": 14599, "synset": "rift_valley.n.01", "name": "rift_valley"}, + {"id": 14600, "synset": "riparian_forest.n.01", "name": "riparian_forest"}, + {"id": 14601, "synset": "ripple_mark.n.01", "name": "ripple_mark"}, + {"id": 14602, "synset": "riverbank.n.01", "name": "riverbank"}, + {"id": 14603, "synset": "riverbed.n.01", "name": "riverbed"}, + {"id": 14604, "synset": "rock.n.01", "name": "rock"}, + {"id": 14605, "synset": "roof.n.03", "name": "roof"}, + {"id": 14606, "synset": "saltpan.n.01", "name": "saltpan"}, + {"id": 14607, "synset": "sandbank.n.01", "name": "sandbank"}, + {"id": 14608, "synset": "sandbar.n.01", "name": "sandbar"}, + {"id": 14609, "synset": "sandpit.n.01", "name": "sandpit"}, + {"id": 14610, "synset": "sanitary_landfill.n.01", "name": "sanitary_landfill"}, + {"id": 14611, "synset": "sawpit.n.01", "name": "sawpit"}, + {"id": 14612, "synset": "scablands.n.01", "name": "scablands"}, + {"id": 14613, "synset": "seashore.n.01", "name": "seashore"}, + {"id": 14614, "synset": "seaside.n.01", "name": "seaside"}, + {"id": 14615, "synset": "seif_dune.n.01", "name": "seif_dune"}, + {"id": 14616, "synset": "shell.n.06", "name": "shell"}, + {"id": 14617, "synset": "shiner.n.02", "name": "shiner"}, + {"id": 14618, "synset": "shoal.n.01", "name": "shoal"}, + {"id": 14619, "synset": "shore.n.01", "name": "shore"}, + {"id": 14620, "synset": "shoreline.n.01", "name": "shoreline"}, + {"id": 14621, "synset": "sinkhole.n.01", "name": "sinkhole"}, + {"id": 14622, "synset": "ski_slope.n.01", "name": "ski_slope"}, + {"id": 14623, "synset": "sky.n.01", "name": "sky"}, + {"id": 14624, "synset": "slope.n.01", "name": "slope"}, + {"id": 14625, "synset": "snowcap.n.01", "name": "snowcap"}, + {"id": 14626, "synset": "snowdrift.n.01", "name": "snowdrift"}, + {"id": 14627, "synset": "snowfield.n.01", "name": "snowfield"}, + {"id": 14628, "synset": "soapsuds.n.01", "name": "soapsuds"}, + {"id": 14629, "synset": "spit.n.01", "name": "spit"}, + {"id": 14630, "synset": "spoor.n.01", "name": "spoor"}, + {"id": 14631, "synset": "spume.n.01", "name": "spume"}, + {"id": 14632, "synset": "star.n.03", "name": "star"}, + {"id": 14633, "synset": "steep.n.01", "name": "steep"}, + {"id": 14634, "synset": "steppe.n.01", "name": "steppe"}, + {"id": 14635, "synset": "strand.n.05", "name": "strand"}, + {"id": 14636, "synset": "streambed.n.01", "name": "streambed"}, + {"id": 14637, "synset": "sun.n.01", "name": "sun"}, + {"id": 14638, "synset": "supernova.n.01", "name": "supernova"}, + {"id": 14639, "synset": "swale.n.01", "name": "swale"}, + {"id": 14640, "synset": "swamp.n.01", "name": "swamp"}, + {"id": 14641, "synset": "swell.n.02", "name": "swell"}, + {"id": 14642, "synset": "tableland.n.01", "name": "tableland"}, + {"id": 14643, "synset": "talus.n.01", "name": "talus"}, + {"id": 14644, "synset": "tangle.n.01", "name": "tangle"}, + {"id": 14645, "synset": "tar_pit.n.01", "name": "tar_pit"}, + {"id": 14646, "synset": "terrace.n.02", "name": "terrace"}, + {"id": 14647, "synset": "tidal_basin.n.01", "name": "tidal_basin"}, + {"id": 14648, "synset": "tideland.n.01", "name": "tideland"}, + {"id": 14649, "synset": "tor.n.02", "name": "tor"}, + {"id": 14650, "synset": "tor.n.01", "name": "tor"}, + {"id": 14651, "synset": "trapezium.n.02", "name": "Trapezium"}, + {"id": 14652, "synset": "troposphere.n.01", "name": "troposphere"}, + {"id": 14653, "synset": "tundra.n.01", "name": "tundra"}, + {"id": 14654, "synset": "twinkler.n.01", "name": "twinkler"}, + {"id": 14655, "synset": "uphill.n.01", "name": "uphill"}, + {"id": 14656, "synset": "urolith.n.01", "name": "urolith"}, + {"id": 14657, "synset": "valley.n.01", "name": "valley"}, + { + "id": 14658, + "synset": "vehicle-borne_transmission.n.01", + "name": "vehicle-borne_transmission", + }, + {"id": 14659, "synset": "vein.n.04", "name": "vein"}, + {"id": 14660, "synset": "volcanic_crater.n.01", "name": "volcanic_crater"}, + {"id": 14661, "synset": "volcano.n.02", "name": "volcano"}, + {"id": 14662, "synset": "wadi.n.01", "name": "wadi"}, + {"id": 14663, "synset": "wall.n.05", "name": "wall"}, + {"id": 14664, "synset": "warren.n.03", "name": "warren"}, + {"id": 14665, "synset": "wasp's_nest.n.01", "name": "wasp's_nest"}, + {"id": 14666, "synset": "watercourse.n.01", "name": "watercourse"}, + {"id": 14667, "synset": "waterside.n.01", "name": "waterside"}, + {"id": 14668, "synset": "water_table.n.01", "name": "water_table"}, + {"id": 14669, "synset": "whinstone.n.01", "name": "whinstone"}, + {"id": 14670, "synset": "wormcast.n.02", "name": "wormcast"}, + {"id": 14671, "synset": "xenolith.n.01", "name": "xenolith"}, + {"id": 14672, "synset": "circe.n.01", "name": "Circe"}, + {"id": 14673, "synset": "gryphon.n.01", "name": "gryphon"}, + {"id": 14674, "synset": "spiritual_leader.n.01", "name": "spiritual_leader"}, + {"id": 14675, "synset": "messiah.n.01", "name": "messiah"}, + {"id": 14676, "synset": "rhea_silvia.n.01", "name": "Rhea_Silvia"}, + {"id": 14677, "synset": "number_one.n.01", "name": "number_one"}, + {"id": 14678, "synset": "adventurer.n.01", "name": "adventurer"}, + {"id": 14679, "synset": "anomaly.n.02", "name": "anomaly"}, + {"id": 14680, "synset": "appointee.n.02", "name": "appointee"}, + {"id": 14681, "synset": "argonaut.n.01", "name": "argonaut"}, + {"id": 14682, "synset": "ashkenazi.n.01", "name": "Ashkenazi"}, + {"id": 14683, "synset": "benefactor.n.01", "name": "benefactor"}, + {"id": 14684, "synset": "color-blind_person.n.01", "name": "color-blind_person"}, + {"id": 14685, "synset": "commoner.n.01", "name": "commoner"}, + {"id": 14686, "synset": "conservator.n.02", "name": "conservator"}, + {"id": 14687, "synset": "contrarian.n.01", "name": "contrarian"}, + {"id": 14688, "synset": "contadino.n.01", "name": "contadino"}, + {"id": 14689, "synset": "contestant.n.01", "name": "contestant"}, + {"id": 14690, "synset": "cosigner.n.01", "name": "cosigner"}, + {"id": 14691, "synset": "discussant.n.01", "name": "discussant"}, + {"id": 14692, "synset": "enologist.n.01", "name": "enologist"}, + {"id": 14693, "synset": "entertainer.n.01", "name": "entertainer"}, + {"id": 14694, "synset": "eulogist.n.01", "name": "eulogist"}, + {"id": 14695, "synset": "ex-gambler.n.01", "name": "ex-gambler"}, + {"id": 14696, "synset": "experimenter.n.01", "name": "experimenter"}, + {"id": 14697, "synset": "experimenter.n.02", "name": "experimenter"}, + {"id": 14698, "synset": "exponent.n.02", "name": "exponent"}, + {"id": 14699, "synset": "ex-president.n.01", "name": "ex-president"}, + {"id": 14700, "synset": "face.n.05", "name": "face"}, + {"id": 14701, "synset": "female.n.02", "name": "female"}, + {"id": 14702, "synset": "finisher.n.04", "name": "finisher"}, + {"id": 14703, "synset": "inhabitant.n.01", "name": "inhabitant"}, + {"id": 14704, "synset": "native.n.01", "name": "native"}, + {"id": 14705, "synset": "native.n.02", "name": "native"}, + {"id": 14706, "synset": "juvenile.n.01", "name": "juvenile"}, + {"id": 14707, "synset": "lover.n.01", "name": "lover"}, + {"id": 14708, "synset": "male.n.02", "name": "male"}, + {"id": 14709, "synset": "mediator.n.01", "name": "mediator"}, + {"id": 14710, "synset": "mediatrix.n.01", "name": "mediatrix"}, + {"id": 14711, "synset": "national.n.01", "name": "national"}, + {"id": 14712, "synset": "peer.n.01", "name": "peer"}, + {"id": 14713, "synset": "prize_winner.n.01", "name": "prize_winner"}, + {"id": 14714, "synset": "recipient.n.01", "name": "recipient"}, + {"id": 14715, "synset": "religionist.n.01", "name": "religionist"}, + {"id": 14716, "synset": "sensualist.n.01", "name": "sensualist"}, + {"id": 14717, "synset": "traveler.n.01", "name": "traveler"}, + {"id": 14718, "synset": "unwelcome_person.n.01", "name": "unwelcome_person"}, + {"id": 14719, "synset": "unskilled_person.n.01", "name": "unskilled_person"}, + {"id": 14720, "synset": "worker.n.01", "name": "worker"}, + {"id": 14721, "synset": "wrongdoer.n.01", "name": "wrongdoer"}, + {"id": 14722, "synset": "black_african.n.01", "name": "Black_African"}, + {"id": 14723, "synset": "afrikaner.n.01", "name": "Afrikaner"}, + {"id": 14724, "synset": "aryan.n.01", "name": "Aryan"}, + {"id": 14725, "synset": "black.n.05", "name": "Black"}, + {"id": 14726, "synset": "black_woman.n.01", "name": "Black_woman"}, + {"id": 14727, "synset": "mulatto.n.01", "name": "mulatto"}, + {"id": 14728, "synset": "white.n.01", "name": "White"}, + {"id": 14729, "synset": "circassian.n.01", "name": "Circassian"}, + {"id": 14730, "synset": "semite.n.01", "name": "Semite"}, + {"id": 14731, "synset": "chaldean.n.02", "name": "Chaldean"}, + {"id": 14732, "synset": "elamite.n.01", "name": "Elamite"}, + {"id": 14733, "synset": "white_man.n.01", "name": "white_man"}, + {"id": 14734, "synset": "wasp.n.01", "name": "WASP"}, + {"id": 14735, "synset": "gook.n.02", "name": "gook"}, + {"id": 14736, "synset": "mongol.n.01", "name": "Mongol"}, + {"id": 14737, "synset": "tatar.n.01", "name": "Tatar"}, + {"id": 14738, "synset": "nahuatl.n.01", "name": "Nahuatl"}, + {"id": 14739, "synset": "aztec.n.01", "name": "Aztec"}, + {"id": 14740, "synset": "olmec.n.01", "name": "Olmec"}, + {"id": 14741, "synset": "biloxi.n.01", "name": "Biloxi"}, + {"id": 14742, "synset": "blackfoot.n.01", "name": "Blackfoot"}, + {"id": 14743, "synset": "brule.n.01", "name": "Brule"}, + {"id": 14744, "synset": "caddo.n.01", "name": "Caddo"}, + {"id": 14745, "synset": "cheyenne.n.03", "name": "Cheyenne"}, + {"id": 14746, "synset": "chickasaw.n.01", "name": "Chickasaw"}, + {"id": 14747, "synset": "cocopa.n.01", "name": "Cocopa"}, + {"id": 14748, "synset": "comanche.n.01", "name": "Comanche"}, + {"id": 14749, "synset": "creek.n.02", "name": "Creek"}, + {"id": 14750, "synset": "delaware.n.02", "name": "Delaware"}, + {"id": 14751, "synset": "diegueno.n.01", "name": "Diegueno"}, + {"id": 14752, "synset": "esselen.n.01", "name": "Esselen"}, + {"id": 14753, "synset": "eyeish.n.01", "name": "Eyeish"}, + {"id": 14754, "synset": "havasupai.n.01", "name": "Havasupai"}, + {"id": 14755, "synset": "hunkpapa.n.01", "name": "Hunkpapa"}, + {"id": 14756, "synset": "iowa.n.01", "name": "Iowa"}, + {"id": 14757, "synset": "kalapooia.n.01", "name": "Kalapooia"}, + {"id": 14758, "synset": "kamia.n.01", "name": "Kamia"}, + {"id": 14759, "synset": "kekchi.n.01", "name": "Kekchi"}, + {"id": 14760, "synset": "kichai.n.01", "name": "Kichai"}, + {"id": 14761, "synset": "kickapoo.n.01", "name": "Kickapoo"}, + {"id": 14762, "synset": "kiliwa.n.01", "name": "Kiliwa"}, + {"id": 14763, "synset": "malecite.n.01", "name": "Malecite"}, + {"id": 14764, "synset": "maricopa.n.01", "name": "Maricopa"}, + {"id": 14765, "synset": "mohican.n.01", "name": "Mohican"}, + {"id": 14766, "synset": "muskhogean.n.01", "name": "Muskhogean"}, + {"id": 14767, "synset": "navaho.n.01", "name": "Navaho"}, + {"id": 14768, "synset": "nootka.n.01", "name": "Nootka"}, + {"id": 14769, "synset": "oglala.n.01", "name": "Oglala"}, + {"id": 14770, "synset": "osage.n.01", "name": "Osage"}, + {"id": 14771, "synset": "oneida.n.01", "name": "Oneida"}, + {"id": 14772, "synset": "paiute.n.01", "name": "Paiute"}, + {"id": 14773, "synset": "passamaquody.n.01", "name": "Passamaquody"}, + {"id": 14774, "synset": "penobscot.n.01", "name": "Penobscot"}, + {"id": 14775, "synset": "penutian.n.02", "name": "Penutian"}, + {"id": 14776, "synset": "potawatomi.n.01", "name": "Potawatomi"}, + {"id": 14777, "synset": "powhatan.n.02", "name": "Powhatan"}, + {"id": 14778, "synset": "kachina.n.02", "name": "kachina"}, + {"id": 14779, "synset": "salish.n.02", "name": "Salish"}, + {"id": 14780, "synset": "shahaptian.n.01", "name": "Shahaptian"}, + {"id": 14781, "synset": "shasta.n.01", "name": "Shasta"}, + {"id": 14782, "synset": "shawnee.n.01", "name": "Shawnee"}, + {"id": 14783, "synset": "sihasapa.n.01", "name": "Sihasapa"}, + {"id": 14784, "synset": "teton.n.01", "name": "Teton"}, + {"id": 14785, "synset": "taracahitian.n.01", "name": "Taracahitian"}, + {"id": 14786, "synset": "tarahumara.n.01", "name": "Tarahumara"}, + {"id": 14787, "synset": "tuscarora.n.01", "name": "Tuscarora"}, + {"id": 14788, "synset": "tutelo.n.01", "name": "Tutelo"}, + {"id": 14789, "synset": "yana.n.01", "name": "Yana"}, + {"id": 14790, "synset": "yavapai.n.01", "name": "Yavapai"}, + {"id": 14791, "synset": "yokuts.n.02", "name": "Yokuts"}, + {"id": 14792, "synset": "yuma.n.01", "name": "Yuma"}, + {"id": 14793, "synset": "gadaba.n.01", "name": "Gadaba"}, + {"id": 14794, "synset": "kolam.n.01", "name": "Kolam"}, + {"id": 14795, "synset": "kui.n.01", "name": "Kui"}, + {"id": 14796, "synset": "toda.n.01", "name": "Toda"}, + {"id": 14797, "synset": "tulu.n.01", "name": "Tulu"}, + {"id": 14798, "synset": "gujarati.n.01", "name": "Gujarati"}, + {"id": 14799, "synset": "kashmiri.n.01", "name": "Kashmiri"}, + {"id": 14800, "synset": "punjabi.n.01", "name": "Punjabi"}, + {"id": 14801, "synset": "slav.n.01", "name": "Slav"}, + {"id": 14802, "synset": "anabaptist.n.01", "name": "Anabaptist"}, + {"id": 14803, "synset": "adventist.n.01", "name": "Adventist"}, + {"id": 14804, "synset": "gentile.n.03", "name": "gentile"}, + {"id": 14805, "synset": "gentile.n.02", "name": "gentile"}, + {"id": 14806, "synset": "catholic.n.01", "name": "Catholic"}, + {"id": 14807, "synset": "old_catholic.n.01", "name": "Old_Catholic"}, + {"id": 14808, "synset": "uniat.n.01", "name": "Uniat"}, + {"id": 14809, "synset": "copt.n.02", "name": "Copt"}, + {"id": 14810, "synset": "jewess.n.01", "name": "Jewess"}, + {"id": 14811, "synset": "jihadist.n.01", "name": "Jihadist"}, + {"id": 14812, "synset": "buddhist.n.01", "name": "Buddhist"}, + {"id": 14813, "synset": "zen_buddhist.n.01", "name": "Zen_Buddhist"}, + {"id": 14814, "synset": "mahayanist.n.01", "name": "Mahayanist"}, + {"id": 14815, "synset": "swami.n.01", "name": "swami"}, + {"id": 14816, "synset": "hare_krishna.n.01", "name": "Hare_Krishna"}, + {"id": 14817, "synset": "shintoist.n.01", "name": "Shintoist"}, + {"id": 14818, "synset": "eurafrican.n.01", "name": "Eurafrican"}, + {"id": 14819, "synset": "eurasian.n.01", "name": "Eurasian"}, + {"id": 14820, "synset": "gael.n.01", "name": "Gael"}, + {"id": 14821, "synset": "frank.n.01", "name": "Frank"}, + {"id": 14822, "synset": "afghan.n.02", "name": "Afghan"}, + {"id": 14823, "synset": "albanian.n.01", "name": "Albanian"}, + {"id": 14824, "synset": "algerian.n.01", "name": "Algerian"}, + {"id": 14825, "synset": "altaic.n.01", "name": "Altaic"}, + {"id": 14826, "synset": "andorran.n.01", "name": "Andorran"}, + {"id": 14827, "synset": "angolan.n.01", "name": "Angolan"}, + {"id": 14828, "synset": "anguillan.n.01", "name": "Anguillan"}, + {"id": 14829, "synset": "austrian.n.01", "name": "Austrian"}, + {"id": 14830, "synset": "bahamian.n.01", "name": "Bahamian"}, + {"id": 14831, "synset": "bahraini.n.01", "name": "Bahraini"}, + {"id": 14832, "synset": "basotho.n.01", "name": "Basotho"}, + {"id": 14833, "synset": "herero.n.01", "name": "Herero"}, + {"id": 14834, "synset": "luba.n.01", "name": "Luba"}, + {"id": 14835, "synset": "barbadian.n.01", "name": "Barbadian"}, + {"id": 14836, "synset": "bolivian.n.01", "name": "Bolivian"}, + {"id": 14837, "synset": "bornean.n.01", "name": "Bornean"}, + {"id": 14838, "synset": "carioca.n.01", "name": "Carioca"}, + {"id": 14839, "synset": "tupi.n.01", "name": "Tupi"}, + {"id": 14840, "synset": "bruneian.n.01", "name": "Bruneian"}, + {"id": 14841, "synset": "bulgarian.n.01", "name": "Bulgarian"}, + {"id": 14842, "synset": "byelorussian.n.01", "name": "Byelorussian"}, + {"id": 14843, "synset": "cameroonian.n.01", "name": "Cameroonian"}, + {"id": 14844, "synset": "canadian.n.01", "name": "Canadian"}, + {"id": 14845, "synset": "french_canadian.n.01", "name": "French_Canadian"}, + {"id": 14846, "synset": "central_american.n.01", "name": "Central_American"}, + {"id": 14847, "synset": "chilean.n.01", "name": "Chilean"}, + {"id": 14848, "synset": "congolese.n.01", "name": "Congolese"}, + {"id": 14849, "synset": "cypriot.n.01", "name": "Cypriot"}, + {"id": 14850, "synset": "dane.n.01", "name": "Dane"}, + {"id": 14851, "synset": "djiboutian.n.01", "name": "Djiboutian"}, + {"id": 14852, "synset": "britisher.n.01", "name": "Britisher"}, + {"id": 14853, "synset": "english_person.n.01", "name": "English_person"}, + {"id": 14854, "synset": "englishwoman.n.01", "name": "Englishwoman"}, + {"id": 14855, "synset": "anglo-saxon.n.02", "name": "Anglo-Saxon"}, + {"id": 14856, "synset": "angle.n.03", "name": "Angle"}, + {"id": 14857, "synset": "west_saxon.n.01", "name": "West_Saxon"}, + {"id": 14858, "synset": "lombard.n.01", "name": "Lombard"}, + {"id": 14859, "synset": "limey.n.01", "name": "limey"}, + {"id": 14860, "synset": "cantabrigian.n.01", "name": "Cantabrigian"}, + {"id": 14861, "synset": "cornishman.n.01", "name": "Cornishman"}, + {"id": 14862, "synset": "cornishwoman.n.01", "name": "Cornishwoman"}, + {"id": 14863, "synset": "lancastrian.n.02", "name": "Lancastrian"}, + {"id": 14864, "synset": "lancastrian.n.01", "name": "Lancastrian"}, + {"id": 14865, "synset": "geordie.n.01", "name": "Geordie"}, + {"id": 14866, "synset": "oxonian.n.01", "name": "Oxonian"}, + {"id": 14867, "synset": "ethiopian.n.01", "name": "Ethiopian"}, + {"id": 14868, "synset": "amhara.n.01", "name": "Amhara"}, + {"id": 14869, "synset": "eritrean.n.01", "name": "Eritrean"}, + {"id": 14870, "synset": "finn.n.01", "name": "Finn"}, + {"id": 14871, "synset": "komi.n.01", "name": "Komi"}, + {"id": 14872, "synset": "livonian.n.01", "name": "Livonian"}, + {"id": 14873, "synset": "lithuanian.n.01", "name": "Lithuanian"}, + {"id": 14874, "synset": "selkup.n.01", "name": "Selkup"}, + {"id": 14875, "synset": "parisian.n.01", "name": "Parisian"}, + {"id": 14876, "synset": "parisienne.n.01", "name": "Parisienne"}, + {"id": 14877, "synset": "creole.n.02", "name": "Creole"}, + {"id": 14878, "synset": "creole.n.01", "name": "Creole"}, + {"id": 14879, "synset": "gabonese.n.01", "name": "Gabonese"}, + {"id": 14880, "synset": "greek.n.02", "name": "Greek"}, + {"id": 14881, "synset": "dorian.n.01", "name": "Dorian"}, + {"id": 14882, "synset": "athenian.n.01", "name": "Athenian"}, + {"id": 14883, "synset": "laconian.n.01", "name": "Laconian"}, + {"id": 14884, "synset": "guyanese.n.01", "name": "Guyanese"}, + {"id": 14885, "synset": "haitian.n.01", "name": "Haitian"}, + {"id": 14886, "synset": "malay.n.01", "name": "Malay"}, + {"id": 14887, "synset": "moro.n.01", "name": "Moro"}, + {"id": 14888, "synset": "netherlander.n.01", "name": "Netherlander"}, + {"id": 14889, "synset": "icelander.n.01", "name": "Icelander"}, + {"id": 14890, "synset": "iraqi.n.01", "name": "Iraqi"}, + {"id": 14891, "synset": "irishman.n.01", "name": "Irishman"}, + {"id": 14892, "synset": "irishwoman.n.01", "name": "Irishwoman"}, + {"id": 14893, "synset": "dubliner.n.01", "name": "Dubliner"}, + {"id": 14894, "synset": "italian.n.01", "name": "Italian"}, + {"id": 14895, "synset": "roman.n.01", "name": "Roman"}, + {"id": 14896, "synset": "sabine.n.02", "name": "Sabine"}, + {"id": 14897, "synset": "japanese.n.01", "name": "Japanese"}, + {"id": 14898, "synset": "jordanian.n.01", "name": "Jordanian"}, + {"id": 14899, "synset": "korean.n.01", "name": "Korean"}, + {"id": 14900, "synset": "kenyan.n.01", "name": "Kenyan"}, + {"id": 14901, "synset": "lao.n.01", "name": "Lao"}, + {"id": 14902, "synset": "lapp.n.01", "name": "Lapp"}, + {"id": 14903, "synset": "latin_american.n.01", "name": "Latin_American"}, + {"id": 14904, "synset": "lebanese.n.01", "name": "Lebanese"}, + {"id": 14905, "synset": "levantine.n.01", "name": "Levantine"}, + {"id": 14906, "synset": "liberian.n.01", "name": "Liberian"}, + {"id": 14907, "synset": "luxemburger.n.01", "name": "Luxemburger"}, + {"id": 14908, "synset": "macedonian.n.01", "name": "Macedonian"}, + {"id": 14909, "synset": "sabahan.n.01", "name": "Sabahan"}, + {"id": 14910, "synset": "mexican.n.01", "name": "Mexican"}, + {"id": 14911, "synset": "chicano.n.01", "name": "Chicano"}, + {"id": 14912, "synset": "mexican-american.n.01", "name": "Mexican-American"}, + {"id": 14913, "synset": "namibian.n.01", "name": "Namibian"}, + {"id": 14914, "synset": "nauruan.n.01", "name": "Nauruan"}, + {"id": 14915, "synset": "gurkha.n.02", "name": "Gurkha"}, + {"id": 14916, "synset": "new_zealander.n.01", "name": "New_Zealander"}, + {"id": 14917, "synset": "nicaraguan.n.01", "name": "Nicaraguan"}, + {"id": 14918, "synset": "nigerian.n.01", "name": "Nigerian"}, + {"id": 14919, "synset": "hausa.n.01", "name": "Hausa"}, + {"id": 14920, "synset": "north_american.n.01", "name": "North_American"}, + {"id": 14921, "synset": "nova_scotian.n.01", "name": "Nova_Scotian"}, + {"id": 14922, "synset": "omani.n.01", "name": "Omani"}, + {"id": 14923, "synset": "pakistani.n.01", "name": "Pakistani"}, + {"id": 14924, "synset": "brahui.n.01", "name": "Brahui"}, + {"id": 14925, "synset": "south_american_indian.n.01", "name": "South_American_Indian"}, + {"id": 14926, "synset": "carib.n.01", "name": "Carib"}, + {"id": 14927, "synset": "filipino.n.01", "name": "Filipino"}, + {"id": 14928, "synset": "polynesian.n.01", "name": "Polynesian"}, + {"id": 14929, "synset": "qatari.n.01", "name": "Qatari"}, + {"id": 14930, "synset": "romanian.n.01", "name": "Romanian"}, + {"id": 14931, "synset": "muscovite.n.02", "name": "Muscovite"}, + {"id": 14932, "synset": "georgian.n.02", "name": "Georgian"}, + {"id": 14933, "synset": "sarawakian.n.01", "name": "Sarawakian"}, + {"id": 14934, "synset": "scandinavian.n.01", "name": "Scandinavian"}, + {"id": 14935, "synset": "senegalese.n.01", "name": "Senegalese"}, + {"id": 14936, "synset": "slovene.n.01", "name": "Slovene"}, + {"id": 14937, "synset": "south_african.n.01", "name": "South_African"}, + {"id": 14938, "synset": "south_american.n.01", "name": "South_American"}, + {"id": 14939, "synset": "sudanese.n.01", "name": "Sudanese"}, + {"id": 14940, "synset": "syrian.n.01", "name": "Syrian"}, + {"id": 14941, "synset": "tahitian.n.01", "name": "Tahitian"}, + {"id": 14942, "synset": "tanzanian.n.01", "name": "Tanzanian"}, + {"id": 14943, "synset": "tibetan.n.02", "name": "Tibetan"}, + {"id": 14944, "synset": "togolese.n.01", "name": "Togolese"}, + {"id": 14945, "synset": "tuareg.n.01", "name": "Tuareg"}, + {"id": 14946, "synset": "turki.n.01", "name": "Turki"}, + {"id": 14947, "synset": "chuvash.n.01", "name": "Chuvash"}, + {"id": 14948, "synset": "turkoman.n.01", "name": "Turkoman"}, + {"id": 14949, "synset": "uzbek.n.01", "name": "Uzbek"}, + {"id": 14950, "synset": "ugandan.n.01", "name": "Ugandan"}, + {"id": 14951, "synset": "ukranian.n.01", "name": "Ukranian"}, + {"id": 14952, "synset": "yakut.n.01", "name": "Yakut"}, + {"id": 14953, "synset": "tungus.n.01", "name": "Tungus"}, + {"id": 14954, "synset": "igbo.n.01", "name": "Igbo"}, + {"id": 14955, "synset": "american.n.03", "name": "American"}, + {"id": 14956, "synset": "anglo-american.n.01", "name": "Anglo-American"}, + {"id": 14957, "synset": "alaska_native.n.01", "name": "Alaska_Native"}, + {"id": 14958, "synset": "arkansan.n.01", "name": "Arkansan"}, + {"id": 14959, "synset": "carolinian.n.01", "name": "Carolinian"}, + {"id": 14960, "synset": "coloradan.n.01", "name": "Coloradan"}, + {"id": 14961, "synset": "connecticuter.n.01", "name": "Connecticuter"}, + {"id": 14962, "synset": "delawarean.n.01", "name": "Delawarean"}, + {"id": 14963, "synset": "floridian.n.01", "name": "Floridian"}, + {"id": 14964, "synset": "german_american.n.01", "name": "German_American"}, + {"id": 14965, "synset": "illinoisan.n.01", "name": "Illinoisan"}, + {"id": 14966, "synset": "mainer.n.01", "name": "Mainer"}, + {"id": 14967, "synset": "marylander.n.01", "name": "Marylander"}, + {"id": 14968, "synset": "minnesotan.n.01", "name": "Minnesotan"}, + {"id": 14969, "synset": "nebraskan.n.01", "name": "Nebraskan"}, + {"id": 14970, "synset": "new_hampshirite.n.01", "name": "New_Hampshirite"}, + {"id": 14971, "synset": "new_jerseyan.n.01", "name": "New_Jerseyan"}, + {"id": 14972, "synset": "new_yorker.n.01", "name": "New_Yorker"}, + {"id": 14973, "synset": "north_carolinian.n.01", "name": "North_Carolinian"}, + {"id": 14974, "synset": "oregonian.n.01", "name": "Oregonian"}, + {"id": 14975, "synset": "pennsylvanian.n.02", "name": "Pennsylvanian"}, + {"id": 14976, "synset": "texan.n.01", "name": "Texan"}, + {"id": 14977, "synset": "utahan.n.01", "name": "Utahan"}, + {"id": 14978, "synset": "uruguayan.n.01", "name": "Uruguayan"}, + {"id": 14979, "synset": "vietnamese.n.01", "name": "Vietnamese"}, + {"id": 14980, "synset": "gambian.n.01", "name": "Gambian"}, + {"id": 14981, "synset": "east_german.n.01", "name": "East_German"}, + {"id": 14982, "synset": "berliner.n.01", "name": "Berliner"}, + {"id": 14983, "synset": "prussian.n.01", "name": "Prussian"}, + {"id": 14984, "synset": "ghanian.n.01", "name": "Ghanian"}, + {"id": 14985, "synset": "guinean.n.01", "name": "Guinean"}, + {"id": 14986, "synset": "papuan.n.01", "name": "Papuan"}, + {"id": 14987, "synset": "walloon.n.01", "name": "Walloon"}, + {"id": 14988, "synset": "yemeni.n.01", "name": "Yemeni"}, + {"id": 14989, "synset": "yugoslav.n.01", "name": "Yugoslav"}, + {"id": 14990, "synset": "serbian.n.01", "name": "Serbian"}, + {"id": 14991, "synset": "xhosa.n.01", "name": "Xhosa"}, + {"id": 14992, "synset": "zairese.n.01", "name": "Zairese"}, + {"id": 14993, "synset": "zimbabwean.n.01", "name": "Zimbabwean"}, + {"id": 14994, "synset": "zulu.n.01", "name": "Zulu"}, + {"id": 14995, "synset": "gemini.n.01", "name": "Gemini"}, + {"id": 14996, "synset": "sagittarius.n.01", "name": "Sagittarius"}, + {"id": 14997, "synset": "pisces.n.02", "name": "Pisces"}, + {"id": 14998, "synset": "abbe.n.01", "name": "abbe"}, + {"id": 14999, "synset": "abbess.n.01", "name": "abbess"}, + {"id": 15000, "synset": "abnegator.n.01", "name": "abnegator"}, + {"id": 15001, "synset": "abridger.n.01", "name": "abridger"}, + {"id": 15002, "synset": "abstractor.n.01", "name": "abstractor"}, + {"id": 15003, "synset": "absconder.n.01", "name": "absconder"}, + {"id": 15004, "synset": "absolver.n.01", "name": "absolver"}, + {"id": 15005, "synset": "abecedarian.n.01", "name": "abecedarian"}, + {"id": 15006, "synset": "aberrant.n.01", "name": "aberrant"}, + {"id": 15007, "synset": "abettor.n.01", "name": "abettor"}, + {"id": 15008, "synset": "abhorrer.n.01", "name": "abhorrer"}, + {"id": 15009, "synset": "abomination.n.01", "name": "abomination"}, + {"id": 15010, "synset": "abseiler.n.01", "name": "abseiler"}, + {"id": 15011, "synset": "abstainer.n.01", "name": "abstainer"}, + {"id": 15012, "synset": "academic_administrator.n.01", "name": "academic_administrator"}, + {"id": 15013, "synset": "academician.n.01", "name": "academician"}, + {"id": 15014, "synset": "accessory_before_the_fact.n.01", "name": "accessory_before_the_fact"}, + {"id": 15015, "synset": "companion.n.03", "name": "companion"}, + {"id": 15016, "synset": "accompanist.n.01", "name": "accompanist"}, + {"id": 15017, "synset": "accomplice.n.01", "name": "accomplice"}, + {"id": 15018, "synset": "account_executive.n.01", "name": "account_executive"}, + {"id": 15019, "synset": "accused.n.01", "name": "accused"}, + {"id": 15020, "synset": "accuser.n.01", "name": "accuser"}, + {"id": 15021, "synset": "acid_head.n.01", "name": "acid_head"}, + {"id": 15022, "synset": "acquaintance.n.03", "name": "acquaintance"}, + {"id": 15023, "synset": "acquirer.n.01", "name": "acquirer"}, + {"id": 15024, "synset": "aerialist.n.01", "name": "aerialist"}, + {"id": 15025, "synset": "action_officer.n.01", "name": "action_officer"}, + {"id": 15026, "synset": "active.n.03", "name": "active"}, + {"id": 15027, "synset": "active_citizen.n.01", "name": "active_citizen"}, + {"id": 15028, "synset": "actor.n.01", "name": "actor"}, + {"id": 15029, "synset": "actor.n.02", "name": "actor"}, + {"id": 15030, "synset": "addict.n.01", "name": "addict"}, + {"id": 15031, "synset": "adducer.n.01", "name": "adducer"}, + {"id": 15032, "synset": "adjuster.n.01", "name": "adjuster"}, + {"id": 15033, "synset": "adjutant.n.01", "name": "adjutant"}, + {"id": 15034, "synset": "adjutant_general.n.01", "name": "adjutant_general"}, + {"id": 15035, "synset": "admirer.n.03", "name": "admirer"}, + {"id": 15036, "synset": "adoptee.n.01", "name": "adoptee"}, + {"id": 15037, "synset": "adulterer.n.01", "name": "adulterer"}, + {"id": 15038, "synset": "adulteress.n.01", "name": "adulteress"}, + {"id": 15039, "synset": "advertiser.n.01", "name": "advertiser"}, + {"id": 15040, "synset": "advisee.n.01", "name": "advisee"}, + {"id": 15041, "synset": "advocate.n.01", "name": "advocate"}, + {"id": 15042, "synset": "aeronautical_engineer.n.01", "name": "aeronautical_engineer"}, + {"id": 15043, "synset": "affiliate.n.01", "name": "affiliate"}, + {"id": 15044, "synset": "affluent.n.01", "name": "affluent"}, + {"id": 15045, "synset": "aficionado.n.02", "name": "aficionado"}, + {"id": 15046, "synset": "buck_sergeant.n.01", "name": "buck_sergeant"}, + {"id": 15047, "synset": "agent-in-place.n.01", "name": "agent-in-place"}, + {"id": 15048, "synset": "aggravator.n.01", "name": "aggravator"}, + {"id": 15049, "synset": "agitator.n.01", "name": "agitator"}, + {"id": 15050, "synset": "agnostic.n.02", "name": "agnostic"}, + {"id": 15051, "synset": "agnostic.n.01", "name": "agnostic"}, + {"id": 15052, "synset": "agonist.n.02", "name": "agonist"}, + {"id": 15053, "synset": "agony_aunt.n.01", "name": "agony_aunt"}, + {"id": 15054, "synset": "agriculturist.n.01", "name": "agriculturist"}, + {"id": 15055, "synset": "air_attache.n.01", "name": "air_attache"}, + {"id": 15056, "synset": "air_force_officer.n.01", "name": "air_force_officer"}, + {"id": 15057, "synset": "airhead.n.01", "name": "airhead"}, + {"id": 15058, "synset": "air_traveler.n.01", "name": "air_traveler"}, + {"id": 15059, "synset": "alarmist.n.01", "name": "alarmist"}, + {"id": 15060, "synset": "albino.n.01", "name": "albino"}, + {"id": 15061, "synset": "alcoholic.n.01", "name": "alcoholic"}, + {"id": 15062, "synset": "alderman.n.01", "name": "alderman"}, + {"id": 15063, "synset": "alexic.n.01", "name": "alexic"}, + {"id": 15064, "synset": "alienee.n.01", "name": "alienee"}, + {"id": 15065, "synset": "alienor.n.01", "name": "alienor"}, + {"id": 15066, "synset": "aliterate.n.01", "name": "aliterate"}, + {"id": 15067, "synset": "algebraist.n.01", "name": "algebraist"}, + {"id": 15068, "synset": "allegorizer.n.01", "name": "allegorizer"}, + {"id": 15069, "synset": "alliterator.n.01", "name": "alliterator"}, + {"id": 15070, "synset": "almoner.n.01", "name": "almoner"}, + {"id": 15071, "synset": "alpinist.n.01", "name": "alpinist"}, + {"id": 15072, "synset": "altar_boy.n.01", "name": "altar_boy"}, + {"id": 15073, "synset": "alto.n.01", "name": "alto"}, + {"id": 15074, "synset": "ambassador.n.01", "name": "ambassador"}, + {"id": 15075, "synset": "ambassador.n.02", "name": "ambassador"}, + {"id": 15076, "synset": "ambusher.n.01", "name": "ambusher"}, + {"id": 15077, "synset": "amicus_curiae.n.01", "name": "amicus_curiae"}, + {"id": 15078, "synset": "amoralist.n.01", "name": "amoralist"}, + {"id": 15079, "synset": "amputee.n.01", "name": "amputee"}, + {"id": 15080, "synset": "analogist.n.01", "name": "analogist"}, + {"id": 15081, "synset": "analphabet.n.01", "name": "analphabet"}, + {"id": 15082, "synset": "analyst.n.01", "name": "analyst"}, + {"id": 15083, "synset": "industry_analyst.n.01", "name": "industry_analyst"}, + {"id": 15084, "synset": "market_strategist.n.01", "name": "market_strategist"}, + {"id": 15085, "synset": "anarchist.n.01", "name": "anarchist"}, + {"id": 15086, "synset": "anathema.n.01", "name": "anathema"}, + {"id": 15087, "synset": "ancestor.n.01", "name": "ancestor"}, + {"id": 15088, "synset": "anchor.n.03", "name": "anchor"}, + {"id": 15089, "synset": "ancient.n.02", "name": "ancient"}, + {"id": 15090, "synset": "anecdotist.n.01", "name": "anecdotist"}, + {"id": 15091, "synset": "angler.n.02", "name": "angler"}, + {"id": 15092, "synset": "animator.n.02", "name": "animator"}, + {"id": 15093, "synset": "animist.n.01", "name": "animist"}, + {"id": 15094, "synset": "annotator.n.01", "name": "annotator"}, + {"id": 15095, "synset": "announcer.n.02", "name": "announcer"}, + {"id": 15096, "synset": "announcer.n.01", "name": "announcer"}, + {"id": 15097, "synset": "anti.n.01", "name": "anti"}, + {"id": 15098, "synset": "anti-american.n.01", "name": "anti-American"}, + {"id": 15099, "synset": "anti-semite.n.01", "name": "anti-Semite"}, + {"id": 15100, "synset": "anzac.n.01", "name": "Anzac"}, + {"id": 15101, "synset": "ape-man.n.02", "name": "ape-man"}, + {"id": 15102, "synset": "aphakic.n.01", "name": "aphakic"}, + {"id": 15103, "synset": "appellant.n.01", "name": "appellant"}, + {"id": 15104, "synset": "appointee.n.01", "name": "appointee"}, + {"id": 15105, "synset": "apprehender.n.02", "name": "apprehender"}, + {"id": 15106, "synset": "april_fool.n.01", "name": "April_fool"}, + {"id": 15107, "synset": "aspirant.n.01", "name": "aspirant"}, + {"id": 15108, "synset": "appreciator.n.01", "name": "appreciator"}, + {"id": 15109, "synset": "appropriator.n.01", "name": "appropriator"}, + {"id": 15110, "synset": "arabist.n.01", "name": "Arabist"}, + {"id": 15111, "synset": "archaist.n.01", "name": "archaist"}, + {"id": 15112, "synset": "archbishop.n.01", "name": "archbishop"}, + {"id": 15113, "synset": "archer.n.01", "name": "archer"}, + {"id": 15114, "synset": "architect.n.01", "name": "architect"}, + {"id": 15115, "synset": "archivist.n.01", "name": "archivist"}, + {"id": 15116, "synset": "archpriest.n.01", "name": "archpriest"}, + {"id": 15117, "synset": "aristotelian.n.01", "name": "Aristotelian"}, + {"id": 15118, "synset": "armiger.n.02", "name": "armiger"}, + {"id": 15119, "synset": "army_attache.n.01", "name": "army_attache"}, + {"id": 15120, "synset": "army_engineer.n.01", "name": "army_engineer"}, + {"id": 15121, "synset": "army_officer.n.01", "name": "army_officer"}, + {"id": 15122, "synset": "arranger.n.02", "name": "arranger"}, + {"id": 15123, "synset": "arrival.n.03", "name": "arrival"}, + {"id": 15124, "synset": "arthritic.n.01", "name": "arthritic"}, + {"id": 15125, "synset": "articulator.n.01", "name": "articulator"}, + {"id": 15126, "synset": "artilleryman.n.01", "name": "artilleryman"}, + {"id": 15127, "synset": "artist's_model.n.01", "name": "artist's_model"}, + {"id": 15128, "synset": "assayer.n.01", "name": "assayer"}, + {"id": 15129, "synset": "assemblyman.n.01", "name": "assemblyman"}, + {"id": 15130, "synset": "assemblywoman.n.01", "name": "assemblywoman"}, + {"id": 15131, "synset": "assenter.n.01", "name": "assenter"}, + {"id": 15132, "synset": "asserter.n.01", "name": "asserter"}, + {"id": 15133, "synset": "assignee.n.01", "name": "assignee"}, + {"id": 15134, "synset": "assistant.n.01", "name": "assistant"}, + {"id": 15135, "synset": "assistant_professor.n.01", "name": "assistant_professor"}, + {"id": 15136, "synset": "associate.n.01", "name": "associate"}, + {"id": 15137, "synset": "associate.n.03", "name": "associate"}, + {"id": 15138, "synset": "associate_professor.n.01", "name": "associate_professor"}, + {"id": 15139, "synset": "astronaut.n.01", "name": "astronaut"}, + {"id": 15140, "synset": "cosmographer.n.01", "name": "cosmographer"}, + {"id": 15141, "synset": "atheist.n.01", "name": "atheist"}, + {"id": 15142, "synset": "athlete.n.01", "name": "athlete"}, + {"id": 15143, "synset": "attendant.n.01", "name": "attendant"}, + {"id": 15144, "synset": "attorney_general.n.01", "name": "attorney_general"}, + {"id": 15145, "synset": "auditor.n.02", "name": "auditor"}, + {"id": 15146, "synset": "augur.n.01", "name": "augur"}, + {"id": 15147, "synset": "aunt.n.01", "name": "aunt"}, + {"id": 15148, "synset": "au_pair_girl.n.01", "name": "au_pair_girl"}, + {"id": 15149, "synset": "authoritarian.n.01", "name": "authoritarian"}, + {"id": 15150, "synset": "authority.n.02", "name": "authority"}, + {"id": 15151, "synset": "authorizer.n.01", "name": "authorizer"}, + {"id": 15152, "synset": "automobile_mechanic.n.01", "name": "automobile_mechanic"}, + {"id": 15153, "synset": "aviator.n.01", "name": "aviator"}, + {"id": 15154, "synset": "aviatrix.n.01", "name": "aviatrix"}, + {"id": 15155, "synset": "ayah.n.01", "name": "ayah"}, + {"id": 15156, "synset": "babu.n.01", "name": "babu"}, + {"id": 15157, "synset": "baby.n.05", "name": "baby"}, + {"id": 15158, "synset": "baby.n.04", "name": "baby"}, + {"id": 15159, "synset": "baby_boomer.n.01", "name": "baby_boomer"}, + {"id": 15160, "synset": "baby_farmer.n.01", "name": "baby_farmer"}, + {"id": 15161, "synset": "back.n.04", "name": "back"}, + {"id": 15162, "synset": "backbencher.n.01", "name": "backbencher"}, + {"id": 15163, "synset": "backpacker.n.01", "name": "backpacker"}, + {"id": 15164, "synset": "backroom_boy.n.01", "name": "backroom_boy"}, + {"id": 15165, "synset": "backscratcher.n.01", "name": "backscratcher"}, + {"id": 15166, "synset": "bad_person.n.01", "name": "bad_person"}, + {"id": 15167, "synset": "baggage.n.02", "name": "baggage"}, + {"id": 15168, "synset": "bag_lady.n.01", "name": "bag_lady"}, + {"id": 15169, "synset": "bailee.n.01", "name": "bailee"}, + {"id": 15170, "synset": "bailiff.n.01", "name": "bailiff"}, + {"id": 15171, "synset": "bailor.n.01", "name": "bailor"}, + {"id": 15172, "synset": "bairn.n.01", "name": "bairn"}, + {"id": 15173, "synset": "baker.n.02", "name": "baker"}, + {"id": 15174, "synset": "balancer.n.01", "name": "balancer"}, + {"id": 15175, "synset": "balker.n.01", "name": "balker"}, + {"id": 15176, "synset": "ball-buster.n.01", "name": "ball-buster"}, + {"id": 15177, "synset": "ball_carrier.n.01", "name": "ball_carrier"}, + {"id": 15178, "synset": "ballet_dancer.n.01", "name": "ballet_dancer"}, + {"id": 15179, "synset": "ballet_master.n.01", "name": "ballet_master"}, + {"id": 15180, "synset": "ballet_mistress.n.01", "name": "ballet_mistress"}, + {"id": 15181, "synset": "balletomane.n.01", "name": "balletomane"}, + {"id": 15182, "synset": "ball_hawk.n.01", "name": "ball_hawk"}, + {"id": 15183, "synset": "balloonist.n.01", "name": "balloonist"}, + {"id": 15184, "synset": "ballplayer.n.01", "name": "ballplayer"}, + {"id": 15185, "synset": "bullfighter.n.01", "name": "bullfighter"}, + {"id": 15186, "synset": "banderillero.n.01", "name": "banderillero"}, + {"id": 15187, "synset": "matador.n.01", "name": "matador"}, + {"id": 15188, "synset": "picador.n.01", "name": "picador"}, + {"id": 15189, "synset": "bandsman.n.01", "name": "bandsman"}, + {"id": 15190, "synset": "banker.n.02", "name": "banker"}, + {"id": 15191, "synset": "bank_robber.n.01", "name": "bank_robber"}, + {"id": 15192, "synset": "bankrupt.n.01", "name": "bankrupt"}, + {"id": 15193, "synset": "bantamweight.n.01", "name": "bantamweight"}, + {"id": 15194, "synset": "barmaid.n.01", "name": "barmaid"}, + {"id": 15195, "synset": "baron.n.03", "name": "baron"}, + {"id": 15196, "synset": "baron.n.02", "name": "baron"}, + {"id": 15197, "synset": "baron.n.01", "name": "baron"}, + {"id": 15198, "synset": "bartender.n.01", "name": "bartender"}, + {"id": 15199, "synset": "baseball_coach.n.01", "name": "baseball_coach"}, + {"id": 15200, "synset": "base_runner.n.01", "name": "base_runner"}, + {"id": 15201, "synset": "basketball_player.n.01", "name": "basketball_player"}, + {"id": 15202, "synset": "basketweaver.n.01", "name": "basketweaver"}, + {"id": 15203, "synset": "basket_maker.n.01", "name": "Basket_Maker"}, + {"id": 15204, "synset": "bass.n.03", "name": "bass"}, + {"id": 15205, "synset": "bastard.n.02", "name": "bastard"}, + {"id": 15206, "synset": "bat_boy.n.01", "name": "bat_boy"}, + {"id": 15207, "synset": "bather.n.02", "name": "bather"}, + {"id": 15208, "synset": "batman.n.01", "name": "batman"}, + {"id": 15209, "synset": "baton_twirler.n.01", "name": "baton_twirler"}, + {"id": 15210, "synset": "bavarian.n.01", "name": "Bavarian"}, + {"id": 15211, "synset": "beadsman.n.01", "name": "beadsman"}, + {"id": 15212, "synset": "beard.n.03", "name": "beard"}, + {"id": 15213, "synset": "beatnik.n.01", "name": "beatnik"}, + {"id": 15214, "synset": "beauty_consultant.n.01", "name": "beauty_consultant"}, + {"id": 15215, "synset": "bedouin.n.01", "name": "Bedouin"}, + {"id": 15216, "synset": "bedwetter.n.01", "name": "bedwetter"}, + {"id": 15217, "synset": "beekeeper.n.01", "name": "beekeeper"}, + {"id": 15218, "synset": "beer_drinker.n.01", "name": "beer_drinker"}, + {"id": 15219, "synset": "beggarman.n.01", "name": "beggarman"}, + {"id": 15220, "synset": "beggarwoman.n.01", "name": "beggarwoman"}, + {"id": 15221, "synset": "beldam.n.02", "name": "beldam"}, + {"id": 15222, "synset": "theist.n.01", "name": "theist"}, + {"id": 15223, "synset": "believer.n.01", "name": "believer"}, + {"id": 15224, "synset": "bell_founder.n.01", "name": "bell_founder"}, + {"id": 15225, "synset": "benedick.n.01", "name": "benedick"}, + {"id": 15226, "synset": "berserker.n.01", "name": "berserker"}, + {"id": 15227, "synset": "besieger.n.01", "name": "besieger"}, + {"id": 15228, "synset": "best.n.02", "name": "best"}, + {"id": 15229, "synset": "betrothed.n.01", "name": "betrothed"}, + {"id": 15230, "synset": "big_brother.n.01", "name": "Big_Brother"}, + {"id": 15231, "synset": "bigot.n.01", "name": "bigot"}, + {"id": 15232, "synset": "big_shot.n.01", "name": "big_shot"}, + {"id": 15233, "synset": "big_sister.n.01", "name": "big_sister"}, + {"id": 15234, "synset": "billiard_player.n.01", "name": "billiard_player"}, + {"id": 15235, "synset": "biochemist.n.01", "name": "biochemist"}, + {"id": 15236, "synset": "biographer.n.01", "name": "biographer"}, + {"id": 15237, "synset": "bird_fancier.n.01", "name": "bird_fancier"}, + {"id": 15238, "synset": "birth.n.05", "name": "birth"}, + {"id": 15239, "synset": "birth-control_campaigner.n.01", "name": "birth-control_campaigner"}, + {"id": 15240, "synset": "bisexual.n.01", "name": "bisexual"}, + {"id": 15241, "synset": "black_belt.n.01", "name": "black_belt"}, + {"id": 15242, "synset": "blackmailer.n.01", "name": "blackmailer"}, + {"id": 15243, "synset": "black_muslim.n.01", "name": "Black_Muslim"}, + {"id": 15244, "synset": "blacksmith.n.01", "name": "blacksmith"}, + {"id": 15245, "synset": "blade.n.02", "name": "blade"}, + {"id": 15246, "synset": "blind_date.n.01", "name": "blind_date"}, + {"id": 15247, "synset": "bluecoat.n.01", "name": "bluecoat"}, + {"id": 15248, "synset": "bluestocking.n.01", "name": "bluestocking"}, + {"id": 15249, "synset": "boatbuilder.n.01", "name": "boatbuilder"}, + {"id": 15250, "synset": "boatman.n.01", "name": "boatman"}, + {"id": 15251, "synset": "boatswain.n.01", "name": "boatswain"}, + {"id": 15252, "synset": "bobby.n.01", "name": "bobby"}, + {"id": 15253, "synset": "bodyguard.n.01", "name": "bodyguard"}, + {"id": 15254, "synset": "boffin.n.01", "name": "boffin"}, + {"id": 15255, "synset": "bolshevik.n.01", "name": "Bolshevik"}, + {"id": 15256, "synset": "bolshevik.n.02", "name": "Bolshevik"}, + {"id": 15257, "synset": "bombshell.n.01", "name": "bombshell"}, + {"id": 15258, "synset": "bondman.n.01", "name": "bondman"}, + {"id": 15259, "synset": "bondwoman.n.02", "name": "bondwoman"}, + {"id": 15260, "synset": "bondwoman.n.01", "name": "bondwoman"}, + {"id": 15261, "synset": "bond_servant.n.01", "name": "bond_servant"}, + {"id": 15262, "synset": "book_agent.n.01", "name": "book_agent"}, + {"id": 15263, "synset": "bookbinder.n.01", "name": "bookbinder"}, + {"id": 15264, "synset": "bookkeeper.n.01", "name": "bookkeeper"}, + {"id": 15265, "synset": "bookmaker.n.01", "name": "bookmaker"}, + {"id": 15266, "synset": "bookworm.n.02", "name": "bookworm"}, + {"id": 15267, "synset": "booster.n.03", "name": "booster"}, + {"id": 15268, "synset": "bootblack.n.01", "name": "bootblack"}, + {"id": 15269, "synset": "bootlegger.n.01", "name": "bootlegger"}, + {"id": 15270, "synset": "bootmaker.n.01", "name": "bootmaker"}, + {"id": 15271, "synset": "borderer.n.01", "name": "borderer"}, + {"id": 15272, "synset": "border_patrolman.n.01", "name": "border_patrolman"}, + {"id": 15273, "synset": "botanist.n.01", "name": "botanist"}, + {"id": 15274, "synset": "bottom_feeder.n.01", "name": "bottom_feeder"}, + {"id": 15275, "synset": "boulevardier.n.01", "name": "boulevardier"}, + {"id": 15276, "synset": "bounty_hunter.n.02", "name": "bounty_hunter"}, + {"id": 15277, "synset": "bounty_hunter.n.01", "name": "bounty_hunter"}, + {"id": 15278, "synset": "bourbon.n.03", "name": "Bourbon"}, + {"id": 15279, "synset": "bowler.n.01", "name": "bowler"}, + {"id": 15280, "synset": "slugger.n.02", "name": "slugger"}, + {"id": 15281, "synset": "cub.n.02", "name": "cub"}, + {"id": 15282, "synset": "boy_scout.n.01", "name": "Boy_Scout"}, + {"id": 15283, "synset": "boy_scout.n.02", "name": "boy_scout"}, + {"id": 15284, "synset": "boy_wonder.n.01", "name": "boy_wonder"}, + {"id": 15285, "synset": "bragger.n.01", "name": "bragger"}, + {"id": 15286, "synset": "brahman.n.02", "name": "brahman"}, + {"id": 15287, "synset": "brawler.n.01", "name": "brawler"}, + {"id": 15288, "synset": "breadwinner.n.01", "name": "breadwinner"}, + {"id": 15289, "synset": "breaststroker.n.01", "name": "breaststroker"}, + {"id": 15290, "synset": "breeder.n.01", "name": "breeder"}, + {"id": 15291, "synset": "brick.n.02", "name": "brick"}, + {"id": 15292, "synset": "bride.n.03", "name": "bride"}, + {"id": 15293, "synset": "bridesmaid.n.01", "name": "bridesmaid"}, + {"id": 15294, "synset": "bridge_agent.n.01", "name": "bridge_agent"}, + {"id": 15295, "synset": "broadcast_journalist.n.01", "name": "broadcast_journalist"}, + {"id": 15296, "synset": "brother.n.05", "name": "Brother"}, + {"id": 15297, "synset": "brother-in-law.n.01", "name": "brother-in-law"}, + {"id": 15298, "synset": "browser.n.01", "name": "browser"}, + {"id": 15299, "synset": "brummie.n.01", "name": "Brummie"}, + {"id": 15300, "synset": "buddy.n.01", "name": "buddy"}, + {"id": 15301, "synset": "bull.n.06", "name": "bull"}, + {"id": 15302, "synset": "bully.n.02", "name": "bully"}, + {"id": 15303, "synset": "bunny.n.01", "name": "bunny"}, + {"id": 15304, "synset": "burglar.n.01", "name": "burglar"}, + {"id": 15305, "synset": "bursar.n.01", "name": "bursar"}, + {"id": 15306, "synset": "busboy.n.01", "name": "busboy"}, + {"id": 15307, "synset": "business_editor.n.01", "name": "business_editor"}, + {"id": 15308, "synset": "business_traveler.n.01", "name": "business_traveler"}, + {"id": 15309, "synset": "buster.n.04", "name": "buster"}, + {"id": 15310, "synset": "busybody.n.01", "name": "busybody"}, + {"id": 15311, "synset": "buttinsky.n.01", "name": "buttinsky"}, + {"id": 15312, "synset": "cabinetmaker.n.01", "name": "cabinetmaker"}, + {"id": 15313, "synset": "caddie.n.01", "name": "caddie"}, + {"id": 15314, "synset": "cadet.n.01", "name": "cadet"}, + {"id": 15315, "synset": "caller.n.04", "name": "caller"}, + {"id": 15316, "synset": "call_girl.n.01", "name": "call_girl"}, + {"id": 15317, "synset": "calligrapher.n.01", "name": "calligrapher"}, + {"id": 15318, "synset": "campaigner.n.01", "name": "campaigner"}, + {"id": 15319, "synset": "camper.n.01", "name": "camper"}, + {"id": 15320, "synset": "camp_follower.n.02", "name": "camp_follower"}, + {"id": 15321, "synset": "candidate.n.02", "name": "candidate"}, + {"id": 15322, "synset": "canonist.n.01", "name": "canonist"}, + {"id": 15323, "synset": "capitalist.n.01", "name": "capitalist"}, + {"id": 15324, "synset": "captain.n.07", "name": "captain"}, + {"id": 15325, "synset": "captain.n.06", "name": "captain"}, + {"id": 15326, "synset": "captain.n.01", "name": "captain"}, + {"id": 15327, "synset": "captain.n.05", "name": "captain"}, + {"id": 15328, "synset": "captive.n.02", "name": "captive"}, + {"id": 15329, "synset": "captive.n.03", "name": "captive"}, + {"id": 15330, "synset": "cardinal.n.01", "name": "cardinal"}, + {"id": 15331, "synset": "cardiologist.n.01", "name": "cardiologist"}, + {"id": 15332, "synset": "card_player.n.01", "name": "card_player"}, + {"id": 15333, "synset": "cardsharp.n.01", "name": "cardsharp"}, + {"id": 15334, "synset": "careerist.n.01", "name": "careerist"}, + {"id": 15335, "synset": "career_man.n.01", "name": "career_man"}, + {"id": 15336, "synset": "caregiver.n.02", "name": "caregiver"}, + {"id": 15337, "synset": "caretaker.n.01", "name": "caretaker"}, + {"id": 15338, "synset": "caretaker.n.02", "name": "caretaker"}, + {"id": 15339, "synset": "caricaturist.n.01", "name": "caricaturist"}, + {"id": 15340, "synset": "carillonneur.n.01", "name": "carillonneur"}, + {"id": 15341, "synset": "caroler.n.01", "name": "caroler"}, + {"id": 15342, "synset": "carpenter.n.01", "name": "carpenter"}, + {"id": 15343, "synset": "carper.n.01", "name": "carper"}, + {"id": 15344, "synset": "cartesian.n.01", "name": "Cartesian"}, + {"id": 15345, "synset": "cashier.n.02", "name": "cashier"}, + {"id": 15346, "synset": "casualty.n.02", "name": "casualty"}, + {"id": 15347, "synset": "casualty.n.01", "name": "casualty"}, + {"id": 15348, "synset": "casuist.n.01", "name": "casuist"}, + {"id": 15349, "synset": "catechist.n.01", "name": "catechist"}, + {"id": 15350, "synset": "catechumen.n.01", "name": "catechumen"}, + {"id": 15351, "synset": "caterer.n.01", "name": "caterer"}, + {"id": 15352, "synset": "catholicos.n.01", "name": "Catholicos"}, + {"id": 15353, "synset": "cat_fancier.n.01", "name": "cat_fancier"}, + {"id": 15354, "synset": "cavalier.n.02", "name": "Cavalier"}, + {"id": 15355, "synset": "cavalryman.n.02", "name": "cavalryman"}, + {"id": 15356, "synset": "caveman.n.01", "name": "caveman"}, + {"id": 15357, "synset": "celebrant.n.02", "name": "celebrant"}, + {"id": 15358, "synset": "celebrant.n.01", "name": "celebrant"}, + {"id": 15359, "synset": "celebrity.n.01", "name": "celebrity"}, + {"id": 15360, "synset": "cellist.n.01", "name": "cellist"}, + {"id": 15361, "synset": "censor.n.02", "name": "censor"}, + {"id": 15362, "synset": "censor.n.01", "name": "censor"}, + {"id": 15363, "synset": "centenarian.n.01", "name": "centenarian"}, + {"id": 15364, "synset": "centrist.n.01", "name": "centrist"}, + {"id": 15365, "synset": "centurion.n.01", "name": "centurion"}, + { + "id": 15366, + "synset": "certified_public_accountant.n.01", + "name": "certified_public_accountant", + }, + {"id": 15367, "synset": "chachka.n.01", "name": "chachka"}, + {"id": 15368, "synset": "chambermaid.n.01", "name": "chambermaid"}, + {"id": 15369, "synset": "chameleon.n.01", "name": "chameleon"}, + {"id": 15370, "synset": "champion.n.01", "name": "champion"}, + {"id": 15371, "synset": "chandler.n.02", "name": "chandler"}, + {"id": 15372, "synset": "prison_chaplain.n.01", "name": "prison_chaplain"}, + {"id": 15373, "synset": "charcoal_burner.n.01", "name": "charcoal_burner"}, + {"id": 15374, "synset": "charge_d'affaires.n.01", "name": "charge_d'affaires"}, + {"id": 15375, "synset": "charioteer.n.01", "name": "charioteer"}, + {"id": 15376, "synset": "charmer.n.02", "name": "charmer"}, + {"id": 15377, "synset": "chartered_accountant.n.01", "name": "chartered_accountant"}, + {"id": 15378, "synset": "chartist.n.02", "name": "chartist"}, + {"id": 15379, "synset": "charwoman.n.01", "name": "charwoman"}, + {"id": 15380, "synset": "male_chauvinist.n.01", "name": "male_chauvinist"}, + {"id": 15381, "synset": "cheapskate.n.01", "name": "cheapskate"}, + {"id": 15382, "synset": "chechen.n.01", "name": "Chechen"}, + {"id": 15383, "synset": "checker.n.02", "name": "checker"}, + {"id": 15384, "synset": "cheerer.n.01", "name": "cheerer"}, + {"id": 15385, "synset": "cheerleader.n.02", "name": "cheerleader"}, + {"id": 15386, "synset": "cheerleader.n.01", "name": "cheerleader"}, + {"id": 15387, "synset": "cheops.n.01", "name": "Cheops"}, + {"id": 15388, "synset": "chess_master.n.01", "name": "chess_master"}, + {"id": 15389, "synset": "chief_executive_officer.n.01", "name": "chief_executive_officer"}, + {"id": 15390, "synset": "chief_of_staff.n.01", "name": "chief_of_staff"}, + {"id": 15391, "synset": "chief_petty_officer.n.01", "name": "chief_petty_officer"}, + {"id": 15392, "synset": "chief_secretary.n.01", "name": "Chief_Secretary"}, + {"id": 15393, "synset": "child.n.01", "name": "child"}, + {"id": 15394, "synset": "child.n.02", "name": "child"}, + {"id": 15395, "synset": "child.n.03", "name": "child"}, + {"id": 15396, "synset": "child_prodigy.n.01", "name": "child_prodigy"}, + {"id": 15397, "synset": "chimneysweeper.n.01", "name": "chimneysweeper"}, + {"id": 15398, "synset": "chiropractor.n.01", "name": "chiropractor"}, + {"id": 15399, "synset": "chit.n.01", "name": "chit"}, + {"id": 15400, "synset": "choker.n.02", "name": "choker"}, + {"id": 15401, "synset": "choragus.n.01", "name": "choragus"}, + {"id": 15402, "synset": "choreographer.n.01", "name": "choreographer"}, + {"id": 15403, "synset": "chorus_girl.n.01", "name": "chorus_girl"}, + {"id": 15404, "synset": "chosen.n.01", "name": "chosen"}, + {"id": 15405, "synset": "cicerone.n.01", "name": "cicerone"}, + {"id": 15406, "synset": "cigar_smoker.n.01", "name": "cigar_smoker"}, + {"id": 15407, "synset": "cipher.n.04", "name": "cipher"}, + {"id": 15408, "synset": "circus_acrobat.n.01", "name": "circus_acrobat"}, + {"id": 15409, "synset": "citizen.n.01", "name": "citizen"}, + {"id": 15410, "synset": "city_editor.n.01", "name": "city_editor"}, + {"id": 15411, "synset": "city_father.n.01", "name": "city_father"}, + {"id": 15412, "synset": "city_man.n.01", "name": "city_man"}, + {"id": 15413, "synset": "city_slicker.n.01", "name": "city_slicker"}, + {"id": 15414, "synset": "civic_leader.n.01", "name": "civic_leader"}, + {"id": 15415, "synset": "civil_rights_leader.n.01", "name": "civil_rights_leader"}, + {"id": 15416, "synset": "cleaner.n.03", "name": "cleaner"}, + {"id": 15417, "synset": "clergyman.n.01", "name": "clergyman"}, + {"id": 15418, "synset": "cleric.n.01", "name": "cleric"}, + {"id": 15419, "synset": "clerk.n.01", "name": "clerk"}, + {"id": 15420, "synset": "clever_dick.n.01", "name": "clever_Dick"}, + {"id": 15421, "synset": "climatologist.n.01", "name": "climatologist"}, + {"id": 15422, "synset": "climber.n.04", "name": "climber"}, + {"id": 15423, "synset": "clinician.n.01", "name": "clinician"}, + {"id": 15424, "synset": "closer.n.02", "name": "closer"}, + {"id": 15425, "synset": "closet_queen.n.01", "name": "closet_queen"}, + {"id": 15426, "synset": "clown.n.02", "name": "clown"}, + {"id": 15427, "synset": "clown.n.01", "name": "clown"}, + {"id": 15428, "synset": "coach.n.02", "name": "coach"}, + {"id": 15429, "synset": "coach.n.01", "name": "coach"}, + {"id": 15430, "synset": "pitching_coach.n.01", "name": "pitching_coach"}, + {"id": 15431, "synset": "coachman.n.01", "name": "coachman"}, + {"id": 15432, "synset": "coal_miner.n.01", "name": "coal_miner"}, + {"id": 15433, "synset": "coastguardsman.n.01", "name": "coastguardsman"}, + {"id": 15434, "synset": "cobber.n.01", "name": "cobber"}, + {"id": 15435, "synset": "cobbler.n.01", "name": "cobbler"}, + {"id": 15436, "synset": "codger.n.01", "name": "codger"}, + {"id": 15437, "synset": "co-beneficiary.n.01", "name": "co-beneficiary"}, + {"id": 15438, "synset": "cog.n.01", "name": "cog"}, + {"id": 15439, "synset": "cognitive_neuroscientist.n.01", "name": "cognitive_neuroscientist"}, + {"id": 15440, "synset": "coiffeur.n.01", "name": "coiffeur"}, + {"id": 15441, "synset": "coiner.n.02", "name": "coiner"}, + {"id": 15442, "synset": "collaborator.n.03", "name": "collaborator"}, + {"id": 15443, "synset": "colleen.n.01", "name": "colleen"}, + {"id": 15444, "synset": "college_student.n.01", "name": "college_student"}, + {"id": 15445, "synset": "collegian.n.01", "name": "collegian"}, + {"id": 15446, "synset": "colonial.n.01", "name": "colonial"}, + {"id": 15447, "synset": "colonialist.n.01", "name": "colonialist"}, + {"id": 15448, "synset": "colonizer.n.01", "name": "colonizer"}, + {"id": 15449, "synset": "coloratura.n.01", "name": "coloratura"}, + {"id": 15450, "synset": "color_guard.n.01", "name": "color_guard"}, + {"id": 15451, "synset": "colossus.n.02", "name": "colossus"}, + {"id": 15452, "synset": "comedian.n.02", "name": "comedian"}, + {"id": 15453, "synset": "comedienne.n.02", "name": "comedienne"}, + {"id": 15454, "synset": "comer.n.01", "name": "comer"}, + {"id": 15455, "synset": "commander.n.03", "name": "commander"}, + {"id": 15456, "synset": "commander_in_chief.n.01", "name": "commander_in_chief"}, + {"id": 15457, "synset": "commanding_officer.n.01", "name": "commanding_officer"}, + {"id": 15458, "synset": "commissar.n.01", "name": "commissar"}, + {"id": 15459, "synset": "commissioned_officer.n.01", "name": "commissioned_officer"}, + { + "id": 15460, + "synset": "commissioned_military_officer.n.01", + "name": "commissioned_military_officer", + }, + {"id": 15461, "synset": "commissioner.n.01", "name": "commissioner"}, + {"id": 15462, "synset": "commissioner.n.02", "name": "commissioner"}, + {"id": 15463, "synset": "committee_member.n.01", "name": "committee_member"}, + {"id": 15464, "synset": "committeewoman.n.01", "name": "committeewoman"}, + {"id": 15465, "synset": "commodore.n.01", "name": "commodore"}, + {"id": 15466, "synset": "communicant.n.01", "name": "communicant"}, + {"id": 15467, "synset": "communist.n.02", "name": "communist"}, + {"id": 15468, "synset": "communist.n.01", "name": "Communist"}, + {"id": 15469, "synset": "commuter.n.02", "name": "commuter"}, + {"id": 15470, "synset": "compere.n.01", "name": "compere"}, + {"id": 15471, "synset": "complexifier.n.01", "name": "complexifier"}, + {"id": 15472, "synset": "compulsive.n.01", "name": "compulsive"}, + {"id": 15473, "synset": "computational_linguist.n.01", "name": "computational_linguist"}, + {"id": 15474, "synset": "computer_scientist.n.01", "name": "computer_scientist"}, + {"id": 15475, "synset": "computer_user.n.01", "name": "computer_user"}, + {"id": 15476, "synset": "comrade.n.02", "name": "Comrade"}, + {"id": 15477, "synset": "concert-goer.n.01", "name": "concert-goer"}, + {"id": 15478, "synset": "conciliator.n.01", "name": "conciliator"}, + {"id": 15479, "synset": "conductor.n.03", "name": "conductor"}, + {"id": 15480, "synset": "confectioner.n.01", "name": "confectioner"}, + {"id": 15481, "synset": "confederate.n.01", "name": "Confederate"}, + {"id": 15482, "synset": "confessor.n.01", "name": "confessor"}, + {"id": 15483, "synset": "confidant.n.01", "name": "confidant"}, + {"id": 15484, "synset": "confucian.n.01", "name": "Confucian"}, + {"id": 15485, "synset": "rep.n.01", "name": "rep"}, + {"id": 15486, "synset": "conqueror.n.01", "name": "conqueror"}, + {"id": 15487, "synset": "conservative.n.02", "name": "Conservative"}, + {"id": 15488, "synset": "nonconformist.n.01", "name": "Nonconformist"}, + {"id": 15489, "synset": "anglican.n.01", "name": "Anglican"}, + {"id": 15490, "synset": "consignee.n.01", "name": "consignee"}, + {"id": 15491, "synset": "consigner.n.01", "name": "consigner"}, + {"id": 15492, "synset": "constable.n.01", "name": "constable"}, + {"id": 15493, "synset": "constructivist.n.01", "name": "constructivist"}, + {"id": 15494, "synset": "contractor.n.01", "name": "contractor"}, + {"id": 15495, "synset": "contralto.n.01", "name": "contralto"}, + {"id": 15496, "synset": "contributor.n.02", "name": "contributor"}, + {"id": 15497, "synset": "control_freak.n.01", "name": "control_freak"}, + {"id": 15498, "synset": "convalescent.n.01", "name": "convalescent"}, + {"id": 15499, "synset": "convener.n.01", "name": "convener"}, + {"id": 15500, "synset": "convict.n.01", "name": "convict"}, + {"id": 15501, "synset": "copilot.n.01", "name": "copilot"}, + {"id": 15502, "synset": "copycat.n.01", "name": "copycat"}, + {"id": 15503, "synset": "coreligionist.n.01", "name": "coreligionist"}, + {"id": 15504, "synset": "cornerback.n.01", "name": "cornerback"}, + {"id": 15505, "synset": "corporatist.n.01", "name": "corporatist"}, + {"id": 15506, "synset": "correspondent.n.01", "name": "correspondent"}, + {"id": 15507, "synset": "cosmetician.n.01", "name": "cosmetician"}, + {"id": 15508, "synset": "cosmopolitan.n.01", "name": "cosmopolitan"}, + {"id": 15509, "synset": "cossack.n.01", "name": "Cossack"}, + {"id": 15510, "synset": "cost_accountant.n.01", "name": "cost_accountant"}, + {"id": 15511, "synset": "co-star.n.01", "name": "co-star"}, + {"id": 15512, "synset": "costumier.n.01", "name": "costumier"}, + {"id": 15513, "synset": "cotter.n.02", "name": "cotter"}, + {"id": 15514, "synset": "cotter.n.01", "name": "cotter"}, + {"id": 15515, "synset": "counselor.n.01", "name": "counselor"}, + {"id": 15516, "synset": "counterterrorist.n.01", "name": "counterterrorist"}, + {"id": 15517, "synset": "counterspy.n.01", "name": "counterspy"}, + {"id": 15518, "synset": "countess.n.01", "name": "countess"}, + {"id": 15519, "synset": "compromiser.n.01", "name": "compromiser"}, + {"id": 15520, "synset": "countrywoman.n.01", "name": "countrywoman"}, + {"id": 15521, "synset": "county_agent.n.01", "name": "county_agent"}, + {"id": 15522, "synset": "courtier.n.01", "name": "courtier"}, + {"id": 15523, "synset": "cousin.n.01", "name": "cousin"}, + {"id": 15524, "synset": "cover_girl.n.01", "name": "cover_girl"}, + {"id": 15525, "synset": "cow.n.03", "name": "cow"}, + {"id": 15526, "synset": "craftsman.n.03", "name": "craftsman"}, + {"id": 15527, "synset": "craftsman.n.02", "name": "craftsman"}, + {"id": 15528, "synset": "crapshooter.n.01", "name": "crapshooter"}, + {"id": 15529, "synset": "crazy.n.01", "name": "crazy"}, + {"id": 15530, "synset": "creature.n.02", "name": "creature"}, + {"id": 15531, "synset": "creditor.n.01", "name": "creditor"}, + {"id": 15532, "synset": "creep.n.01", "name": "creep"}, + {"id": 15533, "synset": "criminologist.n.01", "name": "criminologist"}, + {"id": 15534, "synset": "critic.n.02", "name": "critic"}, + {"id": 15535, "synset": "croesus.n.02", "name": "Croesus"}, + {"id": 15536, "synset": "cross-examiner.n.01", "name": "cross-examiner"}, + {"id": 15537, "synset": "crossover_voter.n.01", "name": "crossover_voter"}, + {"id": 15538, "synset": "croupier.n.01", "name": "croupier"}, + {"id": 15539, "synset": "crown_prince.n.01", "name": "crown_prince"}, + {"id": 15540, "synset": "crown_princess.n.01", "name": "crown_princess"}, + {"id": 15541, "synset": "cryptanalyst.n.01", "name": "cryptanalyst"}, + {"id": 15542, "synset": "cub_scout.n.01", "name": "Cub_Scout"}, + {"id": 15543, "synset": "cuckold.n.01", "name": "cuckold"}, + {"id": 15544, "synset": "cultist.n.02", "name": "cultist"}, + {"id": 15545, "synset": "curandera.n.01", "name": "curandera"}, + {"id": 15546, "synset": "curate.n.01", "name": "curate"}, + {"id": 15547, "synset": "curator.n.01", "name": "curator"}, + {"id": 15548, "synset": "customer_agent.n.01", "name": "customer_agent"}, + {"id": 15549, "synset": "cutter.n.02", "name": "cutter"}, + {"id": 15550, "synset": "cyberpunk.n.02", "name": "cyberpunk"}, + {"id": 15551, "synset": "cyborg.n.01", "name": "cyborg"}, + {"id": 15552, "synset": "cymbalist.n.01", "name": "cymbalist"}, + {"id": 15553, "synset": "cynic.n.02", "name": "Cynic"}, + {"id": 15554, "synset": "cytogeneticist.n.01", "name": "cytogeneticist"}, + {"id": 15555, "synset": "cytologist.n.01", "name": "cytologist"}, + {"id": 15556, "synset": "czar.n.02", "name": "czar"}, + {"id": 15557, "synset": "czar.n.01", "name": "czar"}, + {"id": 15558, "synset": "dad.n.01", "name": "dad"}, + {"id": 15559, "synset": "dairyman.n.02", "name": "dairyman"}, + {"id": 15560, "synset": "dalai_lama.n.01", "name": "Dalai_Lama"}, + {"id": 15561, "synset": "dallier.n.01", "name": "dallier"}, + {"id": 15562, "synset": "dancer.n.01", "name": "dancer"}, + {"id": 15563, "synset": "dancer.n.02", "name": "dancer"}, + {"id": 15564, "synset": "clog_dancer.n.01", "name": "clog_dancer"}, + {"id": 15565, "synset": "dancing-master.n.01", "name": "dancing-master"}, + {"id": 15566, "synset": "dark_horse.n.01", "name": "dark_horse"}, + {"id": 15567, "synset": "darling.n.01", "name": "darling"}, + {"id": 15568, "synset": "date.n.02", "name": "date"}, + {"id": 15569, "synset": "daughter.n.01", "name": "daughter"}, + {"id": 15570, "synset": "dawdler.n.01", "name": "dawdler"}, + {"id": 15571, "synset": "day_boarder.n.01", "name": "day_boarder"}, + {"id": 15572, "synset": "day_laborer.n.01", "name": "day_laborer"}, + {"id": 15573, "synset": "deacon.n.01", "name": "deacon"}, + {"id": 15574, "synset": "deaconess.n.01", "name": "deaconess"}, + {"id": 15575, "synset": "deadeye.n.01", "name": "deadeye"}, + {"id": 15576, "synset": "deipnosophist.n.01", "name": "deipnosophist"}, + {"id": 15577, "synset": "dropout.n.02", "name": "dropout"}, + {"id": 15578, "synset": "deadhead.n.01", "name": "deadhead"}, + {"id": 15579, "synset": "deaf_person.n.01", "name": "deaf_person"}, + {"id": 15580, "synset": "debtor.n.01", "name": "debtor"}, + {"id": 15581, "synset": "deckhand.n.01", "name": "deckhand"}, + {"id": 15582, "synset": "defamer.n.01", "name": "defamer"}, + {"id": 15583, "synset": "defense_contractor.n.01", "name": "defense_contractor"}, + {"id": 15584, "synset": "deist.n.01", "name": "deist"}, + {"id": 15585, "synset": "delegate.n.01", "name": "delegate"}, + {"id": 15586, "synset": "deliveryman.n.01", "name": "deliveryman"}, + {"id": 15587, "synset": "demagogue.n.01", "name": "demagogue"}, + {"id": 15588, "synset": "demigod.n.01", "name": "demigod"}, + {"id": 15589, "synset": "demographer.n.01", "name": "demographer"}, + {"id": 15590, "synset": "demonstrator.n.03", "name": "demonstrator"}, + {"id": 15591, "synset": "den_mother.n.02", "name": "den_mother"}, + {"id": 15592, "synset": "department_head.n.01", "name": "department_head"}, + {"id": 15593, "synset": "depositor.n.01", "name": "depositor"}, + {"id": 15594, "synset": "deputy.n.03", "name": "deputy"}, + {"id": 15595, "synset": "dermatologist.n.01", "name": "dermatologist"}, + {"id": 15596, "synset": "descender.n.01", "name": "descender"}, + {"id": 15597, "synset": "designated_hitter.n.01", "name": "designated_hitter"}, + {"id": 15598, "synset": "designer.n.04", "name": "designer"}, + {"id": 15599, "synset": "desk_clerk.n.01", "name": "desk_clerk"}, + {"id": 15600, "synset": "desk_officer.n.01", "name": "desk_officer"}, + {"id": 15601, "synset": "desk_sergeant.n.01", "name": "desk_sergeant"}, + {"id": 15602, "synset": "detainee.n.01", "name": "detainee"}, + {"id": 15603, "synset": "detective.n.01", "name": "detective"}, + {"id": 15604, "synset": "detective.n.02", "name": "detective"}, + {"id": 15605, "synset": "detractor.n.01", "name": "detractor"}, + {"id": 15606, "synset": "developer.n.01", "name": "developer"}, + {"id": 15607, "synset": "deviationist.n.01", "name": "deviationist"}, + {"id": 15608, "synset": "devisee.n.01", "name": "devisee"}, + {"id": 15609, "synset": "devisor.n.01", "name": "devisor"}, + {"id": 15610, "synset": "devourer.n.01", "name": "devourer"}, + {"id": 15611, "synset": "dialectician.n.01", "name": "dialectician"}, + {"id": 15612, "synset": "diarist.n.01", "name": "diarist"}, + {"id": 15613, "synset": "dietician.n.01", "name": "dietician"}, + {"id": 15614, "synset": "diocesan.n.01", "name": "diocesan"}, + {"id": 15615, "synset": "director.n.03", "name": "director"}, + {"id": 15616, "synset": "director.n.02", "name": "director"}, + {"id": 15617, "synset": "dirty_old_man.n.01", "name": "dirty_old_man"}, + {"id": 15618, "synset": "disbeliever.n.01", "name": "disbeliever"}, + {"id": 15619, "synset": "disk_jockey.n.01", "name": "disk_jockey"}, + {"id": 15620, "synset": "dispatcher.n.02", "name": "dispatcher"}, + {"id": 15621, "synset": "distortionist.n.01", "name": "distortionist"}, + {"id": 15622, "synset": "distributor.n.01", "name": "distributor"}, + {"id": 15623, "synset": "district_attorney.n.01", "name": "district_attorney"}, + {"id": 15624, "synset": "district_manager.n.01", "name": "district_manager"}, + {"id": 15625, "synset": "diver.n.02", "name": "diver"}, + {"id": 15626, "synset": "divorcee.n.01", "name": "divorcee"}, + {"id": 15627, "synset": "ex-wife.n.01", "name": "ex-wife"}, + {"id": 15628, "synset": "divorce_lawyer.n.01", "name": "divorce_lawyer"}, + {"id": 15629, "synset": "docent.n.01", "name": "docent"}, + {"id": 15630, "synset": "doctor.n.01", "name": "doctor"}, + {"id": 15631, "synset": "dodo.n.01", "name": "dodo"}, + {"id": 15632, "synset": "doge.n.01", "name": "doge"}, + {"id": 15633, "synset": "dog_in_the_manger.n.01", "name": "dog_in_the_manger"}, + {"id": 15634, "synset": "dogmatist.n.01", "name": "dogmatist"}, + {"id": 15635, "synset": "dolichocephalic.n.01", "name": "dolichocephalic"}, + {"id": 15636, "synset": "domestic_partner.n.01", "name": "domestic_partner"}, + {"id": 15637, "synset": "dominican.n.02", "name": "Dominican"}, + {"id": 15638, "synset": "dominus.n.01", "name": "dominus"}, + {"id": 15639, "synset": "don.n.03", "name": "don"}, + {"id": 15640, "synset": "donatist.n.01", "name": "Donatist"}, + {"id": 15641, "synset": "donna.n.01", "name": "donna"}, + {"id": 15642, "synset": "dosser.n.01", "name": "dosser"}, + {"id": 15643, "synset": "double.n.03", "name": "double"}, + {"id": 15644, "synset": "double-crosser.n.01", "name": "double-crosser"}, + {"id": 15645, "synset": "down-and-out.n.01", "name": "down-and-out"}, + {"id": 15646, "synset": "doyenne.n.01", "name": "doyenne"}, + {"id": 15647, "synset": "draftsman.n.02", "name": "draftsman"}, + {"id": 15648, "synset": "dramatist.n.01", "name": "dramatist"}, + {"id": 15649, "synset": "dreamer.n.01", "name": "dreamer"}, + {"id": 15650, "synset": "dressmaker.n.01", "name": "dressmaker"}, + {"id": 15651, "synset": "dressmaker's_model.n.01", "name": "dressmaker's_model"}, + {"id": 15652, "synset": "dribbler.n.02", "name": "dribbler"}, + {"id": 15653, "synset": "dribbler.n.01", "name": "dribbler"}, + {"id": 15654, "synset": "drinker.n.02", "name": "drinker"}, + {"id": 15655, "synset": "drinker.n.01", "name": "drinker"}, + {"id": 15656, "synset": "drug_addict.n.01", "name": "drug_addict"}, + {"id": 15657, "synset": "drug_user.n.01", "name": "drug_user"}, + {"id": 15658, "synset": "druid.n.01", "name": "Druid"}, + {"id": 15659, "synset": "drum_majorette.n.02", "name": "drum_majorette"}, + {"id": 15660, "synset": "drummer.n.01", "name": "drummer"}, + {"id": 15661, "synset": "drunk.n.02", "name": "drunk"}, + {"id": 15662, "synset": "drunkard.n.01", "name": "drunkard"}, + {"id": 15663, "synset": "druze.n.01", "name": "Druze"}, + {"id": 15664, "synset": "dry.n.01", "name": "dry"}, + {"id": 15665, "synset": "dry_nurse.n.01", "name": "dry_nurse"}, + {"id": 15666, "synset": "duchess.n.01", "name": "duchess"}, + {"id": 15667, "synset": "duke.n.01", "name": "duke"}, + {"id": 15668, "synset": "duffer.n.01", "name": "duffer"}, + {"id": 15669, "synset": "dunker.n.02", "name": "dunker"}, + {"id": 15670, "synset": "dutch_uncle.n.01", "name": "Dutch_uncle"}, + {"id": 15671, "synset": "dyspeptic.n.01", "name": "dyspeptic"}, + {"id": 15672, "synset": "eager_beaver.n.01", "name": "eager_beaver"}, + {"id": 15673, "synset": "earl.n.01", "name": "earl"}, + {"id": 15674, "synset": "earner.n.01", "name": "earner"}, + {"id": 15675, "synset": "eavesdropper.n.01", "name": "eavesdropper"}, + {"id": 15676, "synset": "eccentric.n.01", "name": "eccentric"}, + {"id": 15677, "synset": "eclectic.n.01", "name": "eclectic"}, + {"id": 15678, "synset": "econometrician.n.01", "name": "econometrician"}, + {"id": 15679, "synset": "economist.n.01", "name": "economist"}, + {"id": 15680, "synset": "ectomorph.n.01", "name": "ectomorph"}, + {"id": 15681, "synset": "editor.n.01", "name": "editor"}, + {"id": 15682, "synset": "egocentric.n.01", "name": "egocentric"}, + {"id": 15683, "synset": "egotist.n.01", "name": "egotist"}, + {"id": 15684, "synset": "ejaculator.n.01", "name": "ejaculator"}, + {"id": 15685, "synset": "elder.n.03", "name": "elder"}, + {"id": 15686, "synset": "elder_statesman.n.01", "name": "elder_statesman"}, + {"id": 15687, "synset": "elected_official.n.01", "name": "elected_official"}, + {"id": 15688, "synset": "electrician.n.01", "name": "electrician"}, + {"id": 15689, "synset": "elegist.n.01", "name": "elegist"}, + {"id": 15690, "synset": "elocutionist.n.01", "name": "elocutionist"}, + {"id": 15691, "synset": "emancipator.n.01", "name": "emancipator"}, + {"id": 15692, "synset": "embryologist.n.01", "name": "embryologist"}, + {"id": 15693, "synset": "emeritus.n.01", "name": "emeritus"}, + {"id": 15694, "synset": "emigrant.n.01", "name": "emigrant"}, + {"id": 15695, "synset": "emissary.n.01", "name": "emissary"}, + {"id": 15696, "synset": "empress.n.01", "name": "empress"}, + {"id": 15697, "synset": "employee.n.01", "name": "employee"}, + {"id": 15698, "synset": "employer.n.01", "name": "employer"}, + {"id": 15699, "synset": "enchantress.n.02", "name": "enchantress"}, + {"id": 15700, "synset": "enchantress.n.01", "name": "enchantress"}, + {"id": 15701, "synset": "encyclopedist.n.01", "name": "encyclopedist"}, + {"id": 15702, "synset": "endomorph.n.01", "name": "endomorph"}, + {"id": 15703, "synset": "enemy.n.02", "name": "enemy"}, + {"id": 15704, "synset": "energizer.n.01", "name": "energizer"}, + {"id": 15705, "synset": "end_man.n.02", "name": "end_man"}, + {"id": 15706, "synset": "end_man.n.01", "name": "end_man"}, + {"id": 15707, "synset": "endorser.n.02", "name": "endorser"}, + {"id": 15708, "synset": "enjoyer.n.01", "name": "enjoyer"}, + {"id": 15709, "synset": "enlisted_woman.n.01", "name": "enlisted_woman"}, + {"id": 15710, "synset": "enophile.n.01", "name": "enophile"}, + {"id": 15711, "synset": "entrant.n.04", "name": "entrant"}, + {"id": 15712, "synset": "entrant.n.03", "name": "entrant"}, + {"id": 15713, "synset": "entrepreneur.n.01", "name": "entrepreneur"}, + {"id": 15714, "synset": "envoy.n.01", "name": "envoy"}, + {"id": 15715, "synset": "enzymologist.n.01", "name": "enzymologist"}, + {"id": 15716, "synset": "eparch.n.01", "name": "eparch"}, + {"id": 15717, "synset": "epidemiologist.n.01", "name": "epidemiologist"}, + {"id": 15718, "synset": "epigone.n.01", "name": "epigone"}, + {"id": 15719, "synset": "epileptic.n.01", "name": "epileptic"}, + {"id": 15720, "synset": "episcopalian.n.01", "name": "Episcopalian"}, + {"id": 15721, "synset": "equerry.n.02", "name": "equerry"}, + {"id": 15722, "synset": "equerry.n.01", "name": "equerry"}, + {"id": 15723, "synset": "erotic.n.01", "name": "erotic"}, + {"id": 15724, "synset": "escapee.n.01", "name": "escapee"}, + {"id": 15725, "synset": "escapist.n.01", "name": "escapist"}, + {"id": 15726, "synset": "eskimo.n.01", "name": "Eskimo"}, + {"id": 15727, "synset": "espionage_agent.n.01", "name": "espionage_agent"}, + {"id": 15728, "synset": "esthetician.n.01", "name": "esthetician"}, + {"id": 15729, "synset": "etcher.n.01", "name": "etcher"}, + {"id": 15730, "synset": "ethnologist.n.01", "name": "ethnologist"}, + {"id": 15731, "synset": "etonian.n.01", "name": "Etonian"}, + {"id": 15732, "synset": "etymologist.n.01", "name": "etymologist"}, + {"id": 15733, "synset": "evangelist.n.01", "name": "evangelist"}, + {"id": 15734, "synset": "evangelist.n.02", "name": "Evangelist"}, + {"id": 15735, "synset": "event_planner.n.01", "name": "event_planner"}, + {"id": 15736, "synset": "examiner.n.02", "name": "examiner"}, + {"id": 15737, "synset": "examiner.n.01", "name": "examiner"}, + {"id": 15738, "synset": "exarch.n.03", "name": "exarch"}, + {"id": 15739, "synset": "executant.n.01", "name": "executant"}, + {"id": 15740, "synset": "executive_secretary.n.01", "name": "executive_secretary"}, + {"id": 15741, "synset": "executive_vice_president.n.01", "name": "executive_vice_president"}, + {"id": 15742, "synset": "executrix.n.01", "name": "executrix"}, + {"id": 15743, "synset": "exegete.n.01", "name": "exegete"}, + {"id": 15744, "synset": "exhibitor.n.01", "name": "exhibitor"}, + {"id": 15745, "synset": "exhibitionist.n.02", "name": "exhibitionist"}, + {"id": 15746, "synset": "exile.n.01", "name": "exile"}, + {"id": 15747, "synset": "existentialist.n.01", "name": "existentialist"}, + {"id": 15748, "synset": "exorcist.n.02", "name": "exorcist"}, + {"id": 15749, "synset": "ex-spouse.n.01", "name": "ex-spouse"}, + {"id": 15750, "synset": "extern.n.01", "name": "extern"}, + {"id": 15751, "synset": "extremist.n.01", "name": "extremist"}, + {"id": 15752, "synset": "extrovert.n.01", "name": "extrovert"}, + {"id": 15753, "synset": "eyewitness.n.01", "name": "eyewitness"}, + {"id": 15754, "synset": "facilitator.n.01", "name": "facilitator"}, + {"id": 15755, "synset": "fairy_godmother.n.01", "name": "fairy_godmother"}, + {"id": 15756, "synset": "falangist.n.01", "name": "falangist"}, + {"id": 15757, "synset": "falconer.n.01", "name": "falconer"}, + {"id": 15758, "synset": "falsifier.n.01", "name": "falsifier"}, + {"id": 15759, "synset": "familiar.n.01", "name": "familiar"}, + {"id": 15760, "synset": "fan.n.03", "name": "fan"}, + {"id": 15761, "synset": "fanatic.n.01", "name": "fanatic"}, + {"id": 15762, "synset": "fancier.n.01", "name": "fancier"}, + {"id": 15763, "synset": "farm_boy.n.01", "name": "farm_boy"}, + {"id": 15764, "synset": "farmer.n.01", "name": "farmer"}, + {"id": 15765, "synset": "farmhand.n.01", "name": "farmhand"}, + {"id": 15766, "synset": "fascist.n.01", "name": "fascist"}, + {"id": 15767, "synset": "fascista.n.01", "name": "fascista"}, + {"id": 15768, "synset": "fatalist.n.01", "name": "fatalist"}, + {"id": 15769, "synset": "father.n.01", "name": "father"}, + {"id": 15770, "synset": "father.n.03", "name": "Father"}, + {"id": 15771, "synset": "father-figure.n.01", "name": "father-figure"}, + {"id": 15772, "synset": "father-in-law.n.01", "name": "father-in-law"}, + {"id": 15773, "synset": "fauntleroy.n.01", "name": "Fauntleroy"}, + {"id": 15774, "synset": "fauve.n.01", "name": "Fauve"}, + {"id": 15775, "synset": "favorite_son.n.01", "name": "favorite_son"}, + {"id": 15776, "synset": "featherweight.n.03", "name": "featherweight"}, + {"id": 15777, "synset": "federalist.n.02", "name": "federalist"}, + {"id": 15778, "synset": "fellow_traveler.n.01", "name": "fellow_traveler"}, + {"id": 15779, "synset": "female_aristocrat.n.01", "name": "female_aristocrat"}, + {"id": 15780, "synset": "female_offspring.n.01", "name": "female_offspring"}, + {"id": 15781, "synset": "female_child.n.01", "name": "female_child"}, + {"id": 15782, "synset": "fence.n.02", "name": "fence"}, + {"id": 15783, "synset": "fiance.n.01", "name": "fiance"}, + {"id": 15784, "synset": "fielder.n.02", "name": "fielder"}, + {"id": 15785, "synset": "field_judge.n.01", "name": "field_judge"}, + {"id": 15786, "synset": "fighter_pilot.n.01", "name": "fighter_pilot"}, + {"id": 15787, "synset": "filer.n.01", "name": "filer"}, + {"id": 15788, "synset": "film_director.n.01", "name": "film_director"}, + {"id": 15789, "synset": "finder.n.01", "name": "finder"}, + {"id": 15790, "synset": "fire_chief.n.01", "name": "fire_chief"}, + {"id": 15791, "synset": "fire-eater.n.03", "name": "fire-eater"}, + {"id": 15792, "synset": "fire-eater.n.02", "name": "fire-eater"}, + {"id": 15793, "synset": "fireman.n.04", "name": "fireman"}, + {"id": 15794, "synset": "fire_marshall.n.01", "name": "fire_marshall"}, + {"id": 15795, "synset": "fire_walker.n.01", "name": "fire_walker"}, + {"id": 15796, "synset": "first_baseman.n.01", "name": "first_baseman"}, + {"id": 15797, "synset": "firstborn.n.01", "name": "firstborn"}, + {"id": 15798, "synset": "first_lady.n.02", "name": "first_lady"}, + {"id": 15799, "synset": "first_lieutenant.n.01", "name": "first_lieutenant"}, + {"id": 15800, "synset": "first_offender.n.01", "name": "first_offender"}, + {"id": 15801, "synset": "first_sergeant.n.01", "name": "first_sergeant"}, + {"id": 15802, "synset": "fishmonger.n.01", "name": "fishmonger"}, + {"id": 15803, "synset": "flagellant.n.02", "name": "flagellant"}, + {"id": 15804, "synset": "flag_officer.n.01", "name": "flag_officer"}, + {"id": 15805, "synset": "flak_catcher.n.01", "name": "flak_catcher"}, + {"id": 15806, "synset": "flanker_back.n.01", "name": "flanker_back"}, + {"id": 15807, "synset": "flapper.n.01", "name": "flapper"}, + {"id": 15808, "synset": "flatmate.n.01", "name": "flatmate"}, + {"id": 15809, "synset": "flatterer.n.01", "name": "flatterer"}, + {"id": 15810, "synset": "flibbertigibbet.n.01", "name": "flibbertigibbet"}, + {"id": 15811, "synset": "flight_surgeon.n.01", "name": "flight_surgeon"}, + {"id": 15812, "synset": "floorwalker.n.01", "name": "floorwalker"}, + {"id": 15813, "synset": "flop.n.02", "name": "flop"}, + {"id": 15814, "synset": "florentine.n.01", "name": "Florentine"}, + {"id": 15815, "synset": "flower_girl.n.02", "name": "flower_girl"}, + {"id": 15816, "synset": "flower_girl.n.01", "name": "flower_girl"}, + {"id": 15817, "synset": "flutist.n.01", "name": "flutist"}, + {"id": 15818, "synset": "fly-by-night.n.01", "name": "fly-by-night"}, + {"id": 15819, "synset": "flyweight.n.02", "name": "flyweight"}, + {"id": 15820, "synset": "flyweight.n.01", "name": "flyweight"}, + {"id": 15821, "synset": "foe.n.02", "name": "foe"}, + {"id": 15822, "synset": "folk_dancer.n.01", "name": "folk_dancer"}, + {"id": 15823, "synset": "folk_poet.n.01", "name": "folk_poet"}, + {"id": 15824, "synset": "follower.n.01", "name": "follower"}, + {"id": 15825, "synset": "football_hero.n.01", "name": "football_hero"}, + {"id": 15826, "synset": "football_player.n.01", "name": "football_player"}, + {"id": 15827, "synset": "footman.n.01", "name": "footman"}, + {"id": 15828, "synset": "forefather.n.01", "name": "forefather"}, + {"id": 15829, "synset": "foremother.n.01", "name": "foremother"}, + {"id": 15830, "synset": "foreign_agent.n.01", "name": "foreign_agent"}, + {"id": 15831, "synset": "foreigner.n.02", "name": "foreigner"}, + {"id": 15832, "synset": "boss.n.03", "name": "boss"}, + {"id": 15833, "synset": "foreman.n.02", "name": "foreman"}, + {"id": 15834, "synset": "forester.n.02", "name": "forester"}, + {"id": 15835, "synset": "forewoman.n.02", "name": "forewoman"}, + {"id": 15836, "synset": "forger.n.02", "name": "forger"}, + {"id": 15837, "synset": "forward.n.01", "name": "forward"}, + {"id": 15838, "synset": "foster-brother.n.01", "name": "foster-brother"}, + {"id": 15839, "synset": "foster-father.n.01", "name": "foster-father"}, + {"id": 15840, "synset": "foster-mother.n.01", "name": "foster-mother"}, + {"id": 15841, "synset": "foster-sister.n.01", "name": "foster-sister"}, + {"id": 15842, "synset": "foster-son.n.01", "name": "foster-son"}, + {"id": 15843, "synset": "founder.n.02", "name": "founder"}, + {"id": 15844, "synset": "foundress.n.01", "name": "foundress"}, + {"id": 15845, "synset": "four-minute_man.n.01", "name": "four-minute_man"}, + {"id": 15846, "synset": "framer.n.02", "name": "framer"}, + {"id": 15847, "synset": "francophobe.n.01", "name": "Francophobe"}, + {"id": 15848, "synset": "freak.n.01", "name": "freak"}, + {"id": 15849, "synset": "free_agent.n.02", "name": "free_agent"}, + {"id": 15850, "synset": "free_agent.n.01", "name": "free_agent"}, + {"id": 15851, "synset": "freedom_rider.n.01", "name": "freedom_rider"}, + {"id": 15852, "synset": "free-liver.n.01", "name": "free-liver"}, + {"id": 15853, "synset": "freeloader.n.01", "name": "freeloader"}, + {"id": 15854, "synset": "free_trader.n.01", "name": "free_trader"}, + {"id": 15855, "synset": "freudian.n.01", "name": "Freudian"}, + {"id": 15856, "synset": "friar.n.01", "name": "friar"}, + {"id": 15857, "synset": "monk.n.01", "name": "monk"}, + {"id": 15858, "synset": "frontierswoman.n.01", "name": "frontierswoman"}, + {"id": 15859, "synset": "front_man.n.01", "name": "front_man"}, + {"id": 15860, "synset": "frotteur.n.01", "name": "frotteur"}, + {"id": 15861, "synset": "fucker.n.02", "name": "fucker"}, + {"id": 15862, "synset": "fucker.n.01", "name": "fucker"}, + {"id": 15863, "synset": "fuddy-duddy.n.01", "name": "fuddy-duddy"}, + {"id": 15864, "synset": "fullback.n.01", "name": "fullback"}, + {"id": 15865, "synset": "funambulist.n.01", "name": "funambulist"}, + {"id": 15866, "synset": "fundamentalist.n.01", "name": "fundamentalist"}, + {"id": 15867, "synset": "fundraiser.n.01", "name": "fundraiser"}, + {"id": 15868, "synset": "futurist.n.01", "name": "futurist"}, + {"id": 15869, "synset": "gadgeteer.n.01", "name": "gadgeteer"}, + {"id": 15870, "synset": "gagman.n.02", "name": "gagman"}, + {"id": 15871, "synset": "gagman.n.01", "name": "gagman"}, + {"id": 15872, "synset": "gainer.n.01", "name": "gainer"}, + {"id": 15873, "synset": "gal.n.03", "name": "gal"}, + {"id": 15874, "synset": "galoot.n.01", "name": "galoot"}, + {"id": 15875, "synset": "gambist.n.01", "name": "gambist"}, + {"id": 15876, "synset": "gambler.n.01", "name": "gambler"}, + {"id": 15877, "synset": "gamine.n.02", "name": "gamine"}, + {"id": 15878, "synset": "garbage_man.n.01", "name": "garbage_man"}, + {"id": 15879, "synset": "gardener.n.02", "name": "gardener"}, + {"id": 15880, "synset": "garment_cutter.n.01", "name": "garment_cutter"}, + {"id": 15881, "synset": "garroter.n.01", "name": "garroter"}, + {"id": 15882, "synset": "gasman.n.01", "name": "gasman"}, + {"id": 15883, "synset": "gastroenterologist.n.01", "name": "gastroenterologist"}, + {"id": 15884, "synset": "gatherer.n.01", "name": "gatherer"}, + {"id": 15885, "synset": "gawker.n.01", "name": "gawker"}, + {"id": 15886, "synset": "gendarme.n.01", "name": "gendarme"}, + {"id": 15887, "synset": "general.n.01", "name": "general"}, + {"id": 15888, "synset": "generator.n.03", "name": "generator"}, + {"id": 15889, "synset": "geneticist.n.01", "name": "geneticist"}, + {"id": 15890, "synset": "genitor.n.01", "name": "genitor"}, + {"id": 15891, "synset": "gent.n.01", "name": "gent"}, + {"id": 15892, "synset": "geologist.n.01", "name": "geologist"}, + {"id": 15893, "synset": "geophysicist.n.01", "name": "geophysicist"}, + {"id": 15894, "synset": "ghostwriter.n.01", "name": "ghostwriter"}, + {"id": 15895, "synset": "gibson_girl.n.01", "name": "Gibson_girl"}, + {"id": 15896, "synset": "girl.n.01", "name": "girl"}, + {"id": 15897, "synset": "girlfriend.n.02", "name": "girlfriend"}, + {"id": 15898, "synset": "girlfriend.n.01", "name": "girlfriend"}, + {"id": 15899, "synset": "girl_wonder.n.01", "name": "girl_wonder"}, + {"id": 15900, "synset": "girondist.n.01", "name": "Girondist"}, + {"id": 15901, "synset": "gitano.n.01", "name": "gitano"}, + {"id": 15902, "synset": "gladiator.n.01", "name": "gladiator"}, + {"id": 15903, "synset": "glassblower.n.01", "name": "glassblower"}, + {"id": 15904, "synset": "gleaner.n.02", "name": "gleaner"}, + {"id": 15905, "synset": "goat_herder.n.01", "name": "goat_herder"}, + {"id": 15906, "synset": "godchild.n.01", "name": "godchild"}, + {"id": 15907, "synset": "godfather.n.01", "name": "godfather"}, + {"id": 15908, "synset": "godparent.n.01", "name": "godparent"}, + {"id": 15909, "synset": "godson.n.01", "name": "godson"}, + {"id": 15910, "synset": "gofer.n.01", "name": "gofer"}, + {"id": 15911, "synset": "goffer.n.01", "name": "goffer"}, + {"id": 15912, "synset": "goldsmith.n.01", "name": "goldsmith"}, + {"id": 15913, "synset": "golfer.n.01", "name": "golfer"}, + {"id": 15914, "synset": "gondolier.n.01", "name": "gondolier"}, + {"id": 15915, "synset": "good_guy.n.01", "name": "good_guy"}, + {"id": 15916, "synset": "good_old_boy.n.01", "name": "good_old_boy"}, + {"id": 15917, "synset": "good_samaritan.n.01", "name": "good_Samaritan"}, + {"id": 15918, "synset": "gossip_columnist.n.01", "name": "gossip_columnist"}, + {"id": 15919, "synset": "gouger.n.01", "name": "gouger"}, + {"id": 15920, "synset": "governor_general.n.01", "name": "governor_general"}, + {"id": 15921, "synset": "grabber.n.01", "name": "grabber"}, + {"id": 15922, "synset": "grader.n.01", "name": "grader"}, + {"id": 15923, "synset": "graduate_nurse.n.01", "name": "graduate_nurse"}, + {"id": 15924, "synset": "grammarian.n.01", "name": "grammarian"}, + {"id": 15925, "synset": "granddaughter.n.01", "name": "granddaughter"}, + {"id": 15926, "synset": "grande_dame.n.01", "name": "grande_dame"}, + {"id": 15927, "synset": "grandfather.n.01", "name": "grandfather"}, + {"id": 15928, "synset": "grand_inquisitor.n.01", "name": "Grand_Inquisitor"}, + {"id": 15929, "synset": "grandma.n.01", "name": "grandma"}, + {"id": 15930, "synset": "grandmaster.n.01", "name": "grandmaster"}, + {"id": 15931, "synset": "grandparent.n.01", "name": "grandparent"}, + {"id": 15932, "synset": "grantee.n.01", "name": "grantee"}, + {"id": 15933, "synset": "granter.n.01", "name": "granter"}, + {"id": 15934, "synset": "grass_widower.n.01", "name": "grass_widower"}, + {"id": 15935, "synset": "great-aunt.n.01", "name": "great-aunt"}, + {"id": 15936, "synset": "great_grandchild.n.01", "name": "great_grandchild"}, + {"id": 15937, "synset": "great_granddaughter.n.01", "name": "great_granddaughter"}, + {"id": 15938, "synset": "great_grandmother.n.01", "name": "great_grandmother"}, + {"id": 15939, "synset": "great_grandparent.n.01", "name": "great_grandparent"}, + {"id": 15940, "synset": "great_grandson.n.01", "name": "great_grandson"}, + {"id": 15941, "synset": "great-nephew.n.01", "name": "great-nephew"}, + {"id": 15942, "synset": "great-niece.n.01", "name": "great-niece"}, + {"id": 15943, "synset": "green_beret.n.01", "name": "Green_Beret"}, + {"id": 15944, "synset": "grenadier.n.01", "name": "grenadier"}, + {"id": 15945, "synset": "greeter.n.01", "name": "greeter"}, + {"id": 15946, "synset": "gringo.n.01", "name": "gringo"}, + {"id": 15947, "synset": "grinner.n.01", "name": "grinner"}, + {"id": 15948, "synset": "grocer.n.01", "name": "grocer"}, + {"id": 15949, "synset": "groom.n.03", "name": "groom"}, + {"id": 15950, "synset": "groom.n.01", "name": "groom"}, + {"id": 15951, "synset": "grouch.n.01", "name": "grouch"}, + {"id": 15952, "synset": "group_captain.n.01", "name": "group_captain"}, + {"id": 15953, "synset": "grunter.n.01", "name": "grunter"}, + {"id": 15954, "synset": "prison_guard.n.01", "name": "prison_guard"}, + {"id": 15955, "synset": "guard.n.01", "name": "guard"}, + {"id": 15956, "synset": "guesser.n.01", "name": "guesser"}, + {"id": 15957, "synset": "guest.n.01", "name": "guest"}, + {"id": 15958, "synset": "guest.n.03", "name": "guest"}, + {"id": 15959, "synset": "guest_of_honor.n.01", "name": "guest_of_honor"}, + {"id": 15960, "synset": "guest_worker.n.01", "name": "guest_worker"}, + {"id": 15961, "synset": "guide.n.02", "name": "guide"}, + {"id": 15962, "synset": "guitarist.n.01", "name": "guitarist"}, + {"id": 15963, "synset": "gunnery_sergeant.n.01", "name": "gunnery_sergeant"}, + {"id": 15964, "synset": "guru.n.01", "name": "guru"}, + {"id": 15965, "synset": "guru.n.03", "name": "guru"}, + {"id": 15966, "synset": "guvnor.n.01", "name": "guvnor"}, + {"id": 15967, "synset": "guy.n.01", "name": "guy"}, + {"id": 15968, "synset": "gymnast.n.01", "name": "gymnast"}, + {"id": 15969, "synset": "gym_rat.n.01", "name": "gym_rat"}, + {"id": 15970, "synset": "gynecologist.n.01", "name": "gynecologist"}, + {"id": 15971, "synset": "gypsy.n.02", "name": "Gypsy"}, + {"id": 15972, "synset": "hack.n.01", "name": "hack"}, + {"id": 15973, "synset": "hacker.n.02", "name": "hacker"}, + {"id": 15974, "synset": "haggler.n.01", "name": "haggler"}, + {"id": 15975, "synset": "hairdresser.n.01", "name": "hairdresser"}, + {"id": 15976, "synset": "hakim.n.02", "name": "hakim"}, + {"id": 15977, "synset": "hakka.n.01", "name": "Hakka"}, + {"id": 15978, "synset": "halberdier.n.01", "name": "halberdier"}, + {"id": 15979, "synset": "halfback.n.01", "name": "halfback"}, + {"id": 15980, "synset": "half_blood.n.01", "name": "half_blood"}, + {"id": 15981, "synset": "hand.n.10", "name": "hand"}, + {"id": 15982, "synset": "animal_trainer.n.01", "name": "animal_trainer"}, + {"id": 15983, "synset": "handyman.n.01", "name": "handyman"}, + {"id": 15984, "synset": "hang_glider.n.01", "name": "hang_glider"}, + {"id": 15985, "synset": "hardliner.n.01", "name": "hardliner"}, + {"id": 15986, "synset": "harlequin.n.01", "name": "harlequin"}, + {"id": 15987, "synset": "harmonizer.n.02", "name": "harmonizer"}, + {"id": 15988, "synset": "hash_head.n.01", "name": "hash_head"}, + {"id": 15989, "synset": "hatchet_man.n.01", "name": "hatchet_man"}, + {"id": 15990, "synset": "hater.n.01", "name": "hater"}, + {"id": 15991, "synset": "hatmaker.n.01", "name": "hatmaker"}, + {"id": 15992, "synset": "headman.n.02", "name": "headman"}, + {"id": 15993, "synset": "headmaster.n.01", "name": "headmaster"}, + {"id": 15994, "synset": "head_nurse.n.01", "name": "head_nurse"}, + {"id": 15995, "synset": "hearer.n.01", "name": "hearer"}, + {"id": 15996, "synset": "heartbreaker.n.01", "name": "heartbreaker"}, + {"id": 15997, "synset": "heathen.n.01", "name": "heathen"}, + {"id": 15998, "synset": "heavyweight.n.02", "name": "heavyweight"}, + {"id": 15999, "synset": "heavy.n.01", "name": "heavy"}, + {"id": 16000, "synset": "heckler.n.01", "name": "heckler"}, + {"id": 16001, "synset": "hedger.n.02", "name": "hedger"}, + {"id": 16002, "synset": "hedger.n.01", "name": "hedger"}, + {"id": 16003, "synset": "hedonist.n.01", "name": "hedonist"}, + {"id": 16004, "synset": "heir.n.01", "name": "heir"}, + {"id": 16005, "synset": "heir_apparent.n.01", "name": "heir_apparent"}, + {"id": 16006, "synset": "heiress.n.01", "name": "heiress"}, + {"id": 16007, "synset": "heir_presumptive.n.01", "name": "heir_presumptive"}, + {"id": 16008, "synset": "hellion.n.01", "name": "hellion"}, + {"id": 16009, "synset": "helmsman.n.01", "name": "helmsman"}, + {"id": 16010, "synset": "hire.n.01", "name": "hire"}, + {"id": 16011, "synset": "hematologist.n.01", "name": "hematologist"}, + {"id": 16012, "synset": "hemiplegic.n.01", "name": "hemiplegic"}, + {"id": 16013, "synset": "herald.n.01", "name": "herald"}, + {"id": 16014, "synset": "herbalist.n.01", "name": "herbalist"}, + {"id": 16015, "synset": "herder.n.02", "name": "herder"}, + {"id": 16016, "synset": "hermaphrodite.n.01", "name": "hermaphrodite"}, + {"id": 16017, "synset": "heroine.n.02", "name": "heroine"}, + {"id": 16018, "synset": "heroin_addict.n.01", "name": "heroin_addict"}, + {"id": 16019, "synset": "hero_worshiper.n.01", "name": "hero_worshiper"}, + {"id": 16020, "synset": "herr.n.01", "name": "Herr"}, + {"id": 16021, "synset": "highbinder.n.01", "name": "highbinder"}, + {"id": 16022, "synset": "highbrow.n.01", "name": "highbrow"}, + {"id": 16023, "synset": "high_commissioner.n.01", "name": "high_commissioner"}, + {"id": 16024, "synset": "highflier.n.01", "name": "highflier"}, + {"id": 16025, "synset": "highlander.n.02", "name": "Highlander"}, + {"id": 16026, "synset": "high-muck-a-muck.n.01", "name": "high-muck-a-muck"}, + {"id": 16027, "synset": "high_priest.n.01", "name": "high_priest"}, + {"id": 16028, "synset": "highjacker.n.01", "name": "highjacker"}, + {"id": 16029, "synset": "hireling.n.01", "name": "hireling"}, + {"id": 16030, "synset": "historian.n.01", "name": "historian"}, + {"id": 16031, "synset": "hitchhiker.n.01", "name": "hitchhiker"}, + {"id": 16032, "synset": "hitter.n.02", "name": "hitter"}, + {"id": 16033, "synset": "hobbyist.n.01", "name": "hobbyist"}, + {"id": 16034, "synset": "holdout.n.01", "name": "holdout"}, + {"id": 16035, "synset": "holdover.n.01", "name": "holdover"}, + {"id": 16036, "synset": "holdup_man.n.01", "name": "holdup_man"}, + {"id": 16037, "synset": "homeboy.n.02", "name": "homeboy"}, + {"id": 16038, "synset": "homeboy.n.01", "name": "homeboy"}, + {"id": 16039, "synset": "home_buyer.n.01", "name": "home_buyer"}, + {"id": 16040, "synset": "homegirl.n.01", "name": "homegirl"}, + {"id": 16041, "synset": "homeless.n.01", "name": "homeless"}, + {"id": 16042, "synset": "homeopath.n.01", "name": "homeopath"}, + {"id": 16043, "synset": "honest_woman.n.01", "name": "honest_woman"}, + {"id": 16044, "synset": "honor_guard.n.01", "name": "honor_guard"}, + {"id": 16045, "synset": "hooker.n.05", "name": "hooker"}, + {"id": 16046, "synset": "hoper.n.01", "name": "hoper"}, + {"id": 16047, "synset": "hornist.n.01", "name": "hornist"}, + {"id": 16048, "synset": "horseman.n.01", "name": "horseman"}, + {"id": 16049, "synset": "horse_trader.n.01", "name": "horse_trader"}, + {"id": 16050, "synset": "horsewoman.n.01", "name": "horsewoman"}, + {"id": 16051, "synset": "horse_wrangler.n.01", "name": "horse_wrangler"}, + {"id": 16052, "synset": "horticulturist.n.01", "name": "horticulturist"}, + {"id": 16053, "synset": "hospital_chaplain.n.01", "name": "hospital_chaplain"}, + {"id": 16054, "synset": "host.n.08", "name": "host"}, + {"id": 16055, "synset": "host.n.01", "name": "host"}, + {"id": 16056, "synset": "hostess.n.01", "name": "hostess"}, + {"id": 16057, "synset": "hotelier.n.01", "name": "hotelier"}, + {"id": 16058, "synset": "housekeeper.n.01", "name": "housekeeper"}, + {"id": 16059, "synset": "housemaster.n.01", "name": "housemaster"}, + {"id": 16060, "synset": "housemate.n.01", "name": "housemate"}, + {"id": 16061, "synset": "house_physician.n.01", "name": "house_physician"}, + {"id": 16062, "synset": "house_sitter.n.01", "name": "house_sitter"}, + {"id": 16063, "synset": "housing_commissioner.n.01", "name": "housing_commissioner"}, + {"id": 16064, "synset": "huckster.n.01", "name": "huckster"}, + {"id": 16065, "synset": "hugger.n.01", "name": "hugger"}, + {"id": 16066, "synset": "humanist.n.02", "name": "humanist"}, + {"id": 16067, "synset": "humanitarian.n.01", "name": "humanitarian"}, + {"id": 16068, "synset": "hunk.n.01", "name": "hunk"}, + {"id": 16069, "synset": "huntress.n.01", "name": "huntress"}, + {"id": 16070, "synset": "ex-husband.n.01", "name": "ex-husband"}, + {"id": 16071, "synset": "hydrologist.n.01", "name": "hydrologist"}, + {"id": 16072, "synset": "hyperope.n.01", "name": "hyperope"}, + {"id": 16073, "synset": "hypertensive.n.01", "name": "hypertensive"}, + {"id": 16074, "synset": "hypnotist.n.01", "name": "hypnotist"}, + {"id": 16075, "synset": "hypocrite.n.01", "name": "hypocrite"}, + {"id": 16076, "synset": "iceman.n.01", "name": "iceman"}, + {"id": 16077, "synset": "iconoclast.n.02", "name": "iconoclast"}, + {"id": 16078, "synset": "ideologist.n.01", "name": "ideologist"}, + {"id": 16079, "synset": "idol.n.02", "name": "idol"}, + {"id": 16080, "synset": "idolizer.n.01", "name": "idolizer"}, + {"id": 16081, "synset": "imam.n.01", "name": "imam"}, + {"id": 16082, "synset": "imperialist.n.01", "name": "imperialist"}, + {"id": 16083, "synset": "important_person.n.01", "name": "important_person"}, + {"id": 16084, "synset": "inamorato.n.01", "name": "inamorato"}, + {"id": 16085, "synset": "incumbent.n.01", "name": "incumbent"}, + {"id": 16086, "synset": "incurable.n.01", "name": "incurable"}, + {"id": 16087, "synset": "inductee.n.01", "name": "inductee"}, + {"id": 16088, "synset": "industrialist.n.01", "name": "industrialist"}, + {"id": 16089, "synset": "infanticide.n.01", "name": "infanticide"}, + {"id": 16090, "synset": "inferior.n.01", "name": "inferior"}, + {"id": 16091, "synset": "infernal.n.01", "name": "infernal"}, + {"id": 16092, "synset": "infielder.n.01", "name": "infielder"}, + {"id": 16093, "synset": "infiltrator.n.02", "name": "infiltrator"}, + {"id": 16094, "synset": "informer.n.01", "name": "informer"}, + {"id": 16095, "synset": "ingenue.n.02", "name": "ingenue"}, + {"id": 16096, "synset": "ingenue.n.01", "name": "ingenue"}, + {"id": 16097, "synset": "polymath.n.01", "name": "polymath"}, + {"id": 16098, "synset": "in-law.n.01", "name": "in-law"}, + {"id": 16099, "synset": "inquiry_agent.n.01", "name": "inquiry_agent"}, + {"id": 16100, "synset": "inspector.n.01", "name": "inspector"}, + {"id": 16101, "synset": "inspector_general.n.01", "name": "inspector_general"}, + {"id": 16102, "synset": "instigator.n.02", "name": "instigator"}, + {"id": 16103, "synset": "insurance_broker.n.01", "name": "insurance_broker"}, + {"id": 16104, "synset": "insurgent.n.01", "name": "insurgent"}, + {"id": 16105, "synset": "intelligence_analyst.n.01", "name": "intelligence_analyst"}, + {"id": 16106, "synset": "interior_designer.n.01", "name": "interior_designer"}, + {"id": 16107, "synset": "interlocutor.n.02", "name": "interlocutor"}, + {"id": 16108, "synset": "interlocutor.n.01", "name": "interlocutor"}, + {"id": 16109, "synset": "international_grandmaster.n.01", "name": "International_Grandmaster"}, + {"id": 16110, "synset": "internationalist.n.02", "name": "internationalist"}, + {"id": 16111, "synset": "internist.n.01", "name": "internist"}, + {"id": 16112, "synset": "interpreter.n.01", "name": "interpreter"}, + {"id": 16113, "synset": "interpreter.n.02", "name": "interpreter"}, + {"id": 16114, "synset": "intervenor.n.01", "name": "intervenor"}, + {"id": 16115, "synset": "introvert.n.01", "name": "introvert"}, + {"id": 16116, "synset": "invader.n.01", "name": "invader"}, + {"id": 16117, "synset": "invalidator.n.01", "name": "invalidator"}, + {"id": 16118, "synset": "investigator.n.02", "name": "investigator"}, + {"id": 16119, "synset": "investor.n.01", "name": "investor"}, + {"id": 16120, "synset": "invigilator.n.01", "name": "invigilator"}, + {"id": 16121, "synset": "irreligionist.n.01", "name": "irreligionist"}, + {"id": 16122, "synset": "ivy_leaguer.n.01", "name": "Ivy_Leaguer"}, + {"id": 16123, "synset": "jack_of_all_trades.n.01", "name": "Jack_of_all_trades"}, + {"id": 16124, "synset": "jacksonian.n.01", "name": "Jacksonian"}, + {"id": 16125, "synset": "jane_doe.n.01", "name": "Jane_Doe"}, + {"id": 16126, "synset": "janissary.n.01", "name": "janissary"}, + {"id": 16127, "synset": "jat.n.01", "name": "Jat"}, + {"id": 16128, "synset": "javanese.n.01", "name": "Javanese"}, + {"id": 16129, "synset": "jekyll_and_hyde.n.01", "name": "Jekyll_and_Hyde"}, + {"id": 16130, "synset": "jester.n.01", "name": "jester"}, + {"id": 16131, "synset": "jesuit.n.01", "name": "Jesuit"}, + {"id": 16132, "synset": "jezebel.n.02", "name": "jezebel"}, + {"id": 16133, "synset": "jilt.n.01", "name": "jilt"}, + {"id": 16134, "synset": "jobber.n.01", "name": "jobber"}, + {"id": 16135, "synset": "job_candidate.n.01", "name": "job_candidate"}, + {"id": 16136, "synset": "job's_comforter.n.01", "name": "Job's_comforter"}, + {"id": 16137, "synset": "jockey.n.01", "name": "jockey"}, + {"id": 16138, "synset": "john_doe.n.02", "name": "John_Doe"}, + {"id": 16139, "synset": "journalist.n.01", "name": "journalist"}, + {"id": 16140, "synset": "judge.n.01", "name": "judge"}, + {"id": 16141, "synset": "judge_advocate.n.01", "name": "judge_advocate"}, + {"id": 16142, "synset": "juggler.n.01", "name": "juggler"}, + {"id": 16143, "synset": "jungian.n.01", "name": "Jungian"}, + {"id": 16144, "synset": "junior.n.03", "name": "junior"}, + {"id": 16145, "synset": "junior.n.02", "name": "junior"}, + {"id": 16146, "synset": "junior.n.04", "name": "Junior"}, + {"id": 16147, "synset": "junior_lightweight.n.01", "name": "junior_lightweight"}, + {"id": 16148, "synset": "junior_middleweight.n.01", "name": "junior_middleweight"}, + {"id": 16149, "synset": "jurist.n.01", "name": "jurist"}, + {"id": 16150, "synset": "juror.n.01", "name": "juror"}, + {"id": 16151, "synset": "justice_of_the_peace.n.01", "name": "justice_of_the_peace"}, + {"id": 16152, "synset": "justiciar.n.01", "name": "justiciar"}, + {"id": 16153, "synset": "kachina.n.01", "name": "kachina"}, + {"id": 16154, "synset": "keyboardist.n.01", "name": "keyboardist"}, + {"id": 16155, "synset": "khedive.n.01", "name": "Khedive"}, + {"id": 16156, "synset": "kingmaker.n.02", "name": "kingmaker"}, + {"id": 16157, "synset": "king.n.02", "name": "king"}, + {"id": 16158, "synset": "king's_counsel.n.01", "name": "King's_Counsel"}, + {"id": 16159, "synset": "counsel_to_the_crown.n.01", "name": "Counsel_to_the_Crown"}, + {"id": 16160, "synset": "kin.n.01", "name": "kin"}, + {"id": 16161, "synset": "enate.n.01", "name": "enate"}, + {"id": 16162, "synset": "kink.n.03", "name": "kink"}, + {"id": 16163, "synset": "kinswoman.n.01", "name": "kinswoman"}, + {"id": 16164, "synset": "kisser.n.01", "name": "kisser"}, + {"id": 16165, "synset": "kitchen_help.n.01", "name": "kitchen_help"}, + {"id": 16166, "synset": "kitchen_police.n.01", "name": "kitchen_police"}, + {"id": 16167, "synset": "klansman.n.01", "name": "Klansman"}, + {"id": 16168, "synset": "kleptomaniac.n.01", "name": "kleptomaniac"}, + {"id": 16169, "synset": "kneeler.n.01", "name": "kneeler"}, + {"id": 16170, "synset": "knight.n.01", "name": "knight"}, + {"id": 16171, "synset": "knocker.n.01", "name": "knocker"}, + {"id": 16172, "synset": "knower.n.01", "name": "knower"}, + {"id": 16173, "synset": "know-it-all.n.01", "name": "know-it-all"}, + {"id": 16174, "synset": "kolkhoznik.n.01", "name": "kolkhoznik"}, + {"id": 16175, "synset": "kshatriya.n.01", "name": "Kshatriya"}, + {"id": 16176, "synset": "labor_coach.n.01", "name": "labor_coach"}, + {"id": 16177, "synset": "laborer.n.01", "name": "laborer"}, + {"id": 16178, "synset": "labourite.n.01", "name": "Labourite"}, + {"id": 16179, "synset": "lady.n.01", "name": "lady"}, + {"id": 16180, "synset": "lady-in-waiting.n.01", "name": "lady-in-waiting"}, + {"id": 16181, "synset": "lady's_maid.n.01", "name": "lady's_maid"}, + {"id": 16182, "synset": "lama.n.01", "name": "lama"}, + {"id": 16183, "synset": "lamb.n.04", "name": "lamb"}, + {"id": 16184, "synset": "lame_duck.n.01", "name": "lame_duck"}, + {"id": 16185, "synset": "lamplighter.n.01", "name": "lamplighter"}, + {"id": 16186, "synset": "land_agent.n.02", "name": "land_agent"}, + {"id": 16187, "synset": "landgrave.n.01", "name": "landgrave"}, + {"id": 16188, "synset": "landlubber.n.02", "name": "landlubber"}, + {"id": 16189, "synset": "landlubber.n.01", "name": "landlubber"}, + {"id": 16190, "synset": "landowner.n.01", "name": "landowner"}, + {"id": 16191, "synset": "landscape_architect.n.01", "name": "landscape_architect"}, + {"id": 16192, "synset": "langlaufer.n.01", "name": "langlaufer"}, + {"id": 16193, "synset": "languisher.n.01", "name": "languisher"}, + {"id": 16194, "synset": "lapidary.n.01", "name": "lapidary"}, + {"id": 16195, "synset": "lass.n.01", "name": "lass"}, + {"id": 16196, "synset": "latin.n.03", "name": "Latin"}, + {"id": 16197, "synset": "latin.n.02", "name": "Latin"}, + {"id": 16198, "synset": "latitudinarian.n.01", "name": "latitudinarian"}, + {"id": 16199, "synset": "jehovah's_witness.n.01", "name": "Jehovah's_Witness"}, + {"id": 16200, "synset": "law_agent.n.01", "name": "law_agent"}, + {"id": 16201, "synset": "lawgiver.n.01", "name": "lawgiver"}, + {"id": 16202, "synset": "lawman.n.01", "name": "lawman"}, + {"id": 16203, "synset": "law_student.n.01", "name": "law_student"}, + {"id": 16204, "synset": "lawyer.n.01", "name": "lawyer"}, + {"id": 16205, "synset": "lay_reader.n.01", "name": "lay_reader"}, + {"id": 16206, "synset": "lazybones.n.01", "name": "lazybones"}, + {"id": 16207, "synset": "leaker.n.01", "name": "leaker"}, + {"id": 16208, "synset": "leaseholder.n.01", "name": "leaseholder"}, + {"id": 16209, "synset": "lector.n.02", "name": "lector"}, + {"id": 16210, "synset": "lector.n.01", "name": "lector"}, + {"id": 16211, "synset": "lecturer.n.02", "name": "lecturer"}, + {"id": 16212, "synset": "left-hander.n.02", "name": "left-hander"}, + {"id": 16213, "synset": "legal_representative.n.01", "name": "legal_representative"}, + {"id": 16214, "synset": "legate.n.01", "name": "legate"}, + {"id": 16215, "synset": "legatee.n.01", "name": "legatee"}, + {"id": 16216, "synset": "legionnaire.n.02", "name": "legionnaire"}, + {"id": 16217, "synset": "letterman.n.01", "name": "letterman"}, + {"id": 16218, "synset": "liberator.n.01", "name": "liberator"}, + {"id": 16219, "synset": "licenser.n.01", "name": "licenser"}, + {"id": 16220, "synset": "licentiate.n.01", "name": "licentiate"}, + {"id": 16221, "synset": "lieutenant.n.01", "name": "lieutenant"}, + {"id": 16222, "synset": "lieutenant_colonel.n.01", "name": "lieutenant_colonel"}, + {"id": 16223, "synset": "lieutenant_commander.n.01", "name": "lieutenant_commander"}, + {"id": 16224, "synset": "lieutenant_junior_grade.n.01", "name": "lieutenant_junior_grade"}, + {"id": 16225, "synset": "life.n.08", "name": "life"}, + {"id": 16226, "synset": "lifeguard.n.01", "name": "lifeguard"}, + {"id": 16227, "synset": "life_tenant.n.01", "name": "life_tenant"}, + {"id": 16228, "synset": "light_flyweight.n.01", "name": "light_flyweight"}, + {"id": 16229, "synset": "light_heavyweight.n.03", "name": "light_heavyweight"}, + {"id": 16230, "synset": "light_heavyweight.n.01", "name": "light_heavyweight"}, + {"id": 16231, "synset": "light-o'-love.n.01", "name": "light-o'-love"}, + {"id": 16232, "synset": "lightweight.n.01", "name": "lightweight"}, + {"id": 16233, "synset": "lightweight.n.04", "name": "lightweight"}, + {"id": 16234, "synset": "lightweight.n.03", "name": "lightweight"}, + {"id": 16235, "synset": "lilliputian.n.01", "name": "lilliputian"}, + {"id": 16236, "synset": "limnologist.n.01", "name": "limnologist"}, + {"id": 16237, "synset": "lineman.n.01", "name": "lineman"}, + {"id": 16238, "synset": "line_officer.n.01", "name": "line_officer"}, + {"id": 16239, "synset": "lion-hunter.n.01", "name": "lion-hunter"}, + {"id": 16240, "synset": "lisper.n.01", "name": "lisper"}, + {"id": 16241, "synset": "lister.n.02", "name": "lister"}, + {"id": 16242, "synset": "literary_critic.n.01", "name": "literary_critic"}, + {"id": 16243, "synset": "literate.n.01", "name": "literate"}, + {"id": 16244, "synset": "litigant.n.01", "name": "litigant"}, + {"id": 16245, "synset": "litterer.n.01", "name": "litterer"}, + {"id": 16246, "synset": "little_brother.n.01", "name": "little_brother"}, + {"id": 16247, "synset": "little_sister.n.01", "name": "little_sister"}, + {"id": 16248, "synset": "lobbyist.n.01", "name": "lobbyist"}, + {"id": 16249, "synset": "locksmith.n.01", "name": "locksmith"}, + {"id": 16250, "synset": "locum_tenens.n.01", "name": "locum_tenens"}, + {"id": 16251, "synset": "lord.n.03", "name": "Lord"}, + {"id": 16252, "synset": "loser.n.03", "name": "loser"}, + {"id": 16253, "synset": "loser.n.01", "name": "loser"}, + {"id": 16254, "synset": "failure.n.04", "name": "failure"}, + {"id": 16255, "synset": "lothario.n.01", "name": "Lothario"}, + {"id": 16256, "synset": "loudmouth.n.01", "name": "loudmouth"}, + {"id": 16257, "synset": "lowerclassman.n.01", "name": "lowerclassman"}, + {"id": 16258, "synset": "lowlander.n.01", "name": "Lowlander"}, + {"id": 16259, "synset": "loyalist.n.01", "name": "loyalist"}, + {"id": 16260, "synset": "luddite.n.01", "name": "Luddite"}, + {"id": 16261, "synset": "lumberman.n.01", "name": "lumberman"}, + {"id": 16262, "synset": "lumper.n.02", "name": "lumper"}, + {"id": 16263, "synset": "bedlamite.n.01", "name": "bedlamite"}, + {"id": 16264, "synset": "pyromaniac.n.01", "name": "pyromaniac"}, + {"id": 16265, "synset": "lutist.n.01", "name": "lutist"}, + {"id": 16266, "synset": "lutheran.n.01", "name": "Lutheran"}, + {"id": 16267, "synset": "lyricist.n.01", "name": "lyricist"}, + {"id": 16268, "synset": "macebearer.n.01", "name": "macebearer"}, + {"id": 16269, "synset": "machinist.n.01", "name": "machinist"}, + {"id": 16270, "synset": "madame.n.01", "name": "madame"}, + {"id": 16271, "synset": "maenad.n.01", "name": "maenad"}, + {"id": 16272, "synset": "maestro.n.01", "name": "maestro"}, + {"id": 16273, "synset": "magdalen.n.01", "name": "magdalen"}, + {"id": 16274, "synset": "magician.n.01", "name": "magician"}, + {"id": 16275, "synset": "magus.n.01", "name": "magus"}, + {"id": 16276, "synset": "maharani.n.01", "name": "maharani"}, + {"id": 16277, "synset": "mahatma.n.01", "name": "mahatma"}, + {"id": 16278, "synset": "maid.n.02", "name": "maid"}, + {"id": 16279, "synset": "maid.n.01", "name": "maid"}, + {"id": 16280, "synset": "major.n.01", "name": "major"}, + {"id": 16281, "synset": "major.n.03", "name": "major"}, + {"id": 16282, "synset": "major-domo.n.01", "name": "major-domo"}, + {"id": 16283, "synset": "maker.n.01", "name": "maker"}, + {"id": 16284, "synset": "malahini.n.01", "name": "malahini"}, + {"id": 16285, "synset": "malcontent.n.01", "name": "malcontent"}, + {"id": 16286, "synset": "malik.n.01", "name": "malik"}, + {"id": 16287, "synset": "malingerer.n.01", "name": "malingerer"}, + {"id": 16288, "synset": "malthusian.n.01", "name": "Malthusian"}, + {"id": 16289, "synset": "adonis.n.01", "name": "adonis"}, + {"id": 16290, "synset": "man.n.03", "name": "man"}, + {"id": 16291, "synset": "man.n.05", "name": "man"}, + {"id": 16292, "synset": "manageress.n.01", "name": "manageress"}, + {"id": 16293, "synset": "mandarin.n.03", "name": "mandarin"}, + {"id": 16294, "synset": "maneuverer.n.01", "name": "maneuverer"}, + {"id": 16295, "synset": "maniac.n.02", "name": "maniac"}, + {"id": 16296, "synset": "manichaean.n.01", "name": "Manichaean"}, + {"id": 16297, "synset": "manicurist.n.01", "name": "manicurist"}, + {"id": 16298, "synset": "manipulator.n.02", "name": "manipulator"}, + {"id": 16299, "synset": "man-at-arms.n.01", "name": "man-at-arms"}, + {"id": 16300, "synset": "man_of_action.n.01", "name": "man_of_action"}, + {"id": 16301, "synset": "man_of_letters.n.01", "name": "man_of_letters"}, + {"id": 16302, "synset": "manufacturer.n.02", "name": "manufacturer"}, + {"id": 16303, "synset": "marcher.n.02", "name": "marcher"}, + {"id": 16304, "synset": "marchioness.n.02", "name": "marchioness"}, + {"id": 16305, "synset": "margrave.n.02", "name": "margrave"}, + {"id": 16306, "synset": "margrave.n.01", "name": "margrave"}, + {"id": 16307, "synset": "marine.n.01", "name": "Marine"}, + {"id": 16308, "synset": "marquess.n.02", "name": "marquess"}, + {"id": 16309, "synset": "marquis.n.02", "name": "marquis"}, + {"id": 16310, "synset": "marshal.n.02", "name": "marshal"}, + {"id": 16311, "synset": "martinet.n.01", "name": "martinet"}, + {"id": 16312, "synset": "masochist.n.01", "name": "masochist"}, + {"id": 16313, "synset": "mason.n.04", "name": "mason"}, + {"id": 16314, "synset": "masquerader.n.01", "name": "masquerader"}, + {"id": 16315, "synset": "masseur.n.01", "name": "masseur"}, + {"id": 16316, "synset": "masseuse.n.01", "name": "masseuse"}, + {"id": 16317, "synset": "master.n.04", "name": "master"}, + {"id": 16318, "synset": "master.n.07", "name": "master"}, + {"id": 16319, "synset": "master-at-arms.n.01", "name": "master-at-arms"}, + {"id": 16320, "synset": "master_of_ceremonies.n.01", "name": "master_of_ceremonies"}, + {"id": 16321, "synset": "masturbator.n.01", "name": "masturbator"}, + {"id": 16322, "synset": "matchmaker.n.01", "name": "matchmaker"}, + {"id": 16323, "synset": "mate.n.01", "name": "mate"}, + {"id": 16324, "synset": "mate.n.08", "name": "mate"}, + {"id": 16325, "synset": "mate.n.03", "name": "mate"}, + {"id": 16326, "synset": "mater.n.01", "name": "mater"}, + {"id": 16327, "synset": "material.n.05", "name": "material"}, + {"id": 16328, "synset": "materialist.n.02", "name": "materialist"}, + {"id": 16329, "synset": "matriarch.n.01", "name": "matriarch"}, + {"id": 16330, "synset": "matriarch.n.02", "name": "matriarch"}, + {"id": 16331, "synset": "matriculate.n.01", "name": "matriculate"}, + {"id": 16332, "synset": "matron.n.01", "name": "matron"}, + {"id": 16333, "synset": "mayor.n.01", "name": "mayor"}, + {"id": 16334, "synset": "mayoress.n.01", "name": "mayoress"}, + {"id": 16335, "synset": "mechanical_engineer.n.01", "name": "mechanical_engineer"}, + {"id": 16336, "synset": "medalist.n.02", "name": "medalist"}, + {"id": 16337, "synset": "medical_officer.n.01", "name": "medical_officer"}, + {"id": 16338, "synset": "medical_practitioner.n.01", "name": "medical_practitioner"}, + {"id": 16339, "synset": "medical_scientist.n.01", "name": "medical_scientist"}, + {"id": 16340, "synset": "medium.n.09", "name": "medium"}, + {"id": 16341, "synset": "megalomaniac.n.01", "name": "megalomaniac"}, + {"id": 16342, "synset": "melancholic.n.01", "name": "melancholic"}, + {"id": 16343, "synset": "melkite.n.01", "name": "Melkite"}, + {"id": 16344, "synset": "melter.n.01", "name": "melter"}, + {"id": 16345, "synset": "nonmember.n.01", "name": "nonmember"}, + {"id": 16346, "synset": "board_member.n.01", "name": "board_member"}, + {"id": 16347, "synset": "clansman.n.01", "name": "clansman"}, + {"id": 16348, "synset": "memorizer.n.01", "name": "memorizer"}, + {"id": 16349, "synset": "mendelian.n.01", "name": "Mendelian"}, + {"id": 16350, "synset": "mender.n.01", "name": "mender"}, + {"id": 16351, "synset": "mesoamerican.n.01", "name": "Mesoamerican"}, + {"id": 16352, "synset": "messmate.n.01", "name": "messmate"}, + {"id": 16353, "synset": "mestiza.n.01", "name": "mestiza"}, + {"id": 16354, "synset": "meteorologist.n.01", "name": "meteorologist"}, + {"id": 16355, "synset": "meter_maid.n.01", "name": "meter_maid"}, + {"id": 16356, "synset": "methodist.n.01", "name": "Methodist"}, + {"id": 16357, "synset": "metis.n.01", "name": "Metis"}, + {"id": 16358, "synset": "metropolitan.n.01", "name": "metropolitan"}, + {"id": 16359, "synset": "mezzo-soprano.n.01", "name": "mezzo-soprano"}, + {"id": 16360, "synset": "microeconomist.n.01", "name": "microeconomist"}, + {"id": 16361, "synset": "middle-aged_man.n.01", "name": "middle-aged_man"}, + {"id": 16362, "synset": "middlebrow.n.01", "name": "middlebrow"}, + {"id": 16363, "synset": "middleweight.n.01", "name": "middleweight"}, + {"id": 16364, "synset": "midwife.n.01", "name": "midwife"}, + {"id": 16365, "synset": "mikado.n.01", "name": "mikado"}, + {"id": 16366, "synset": "milanese.n.01", "name": "Milanese"}, + {"id": 16367, "synset": "miler.n.02", "name": "miler"}, + {"id": 16368, "synset": "miles_gloriosus.n.01", "name": "miles_gloriosus"}, + {"id": 16369, "synset": "military_attache.n.01", "name": "military_attache"}, + {"id": 16370, "synset": "military_chaplain.n.01", "name": "military_chaplain"}, + {"id": 16371, "synset": "military_leader.n.01", "name": "military_leader"}, + {"id": 16372, "synset": "military_officer.n.01", "name": "military_officer"}, + {"id": 16373, "synset": "military_policeman.n.01", "name": "military_policeman"}, + {"id": 16374, "synset": "mill_agent.n.01", "name": "mill_agent"}, + {"id": 16375, "synset": "mill-hand.n.01", "name": "mill-hand"}, + {"id": 16376, "synset": "millionairess.n.01", "name": "millionairess"}, + {"id": 16377, "synset": "millwright.n.01", "name": "millwright"}, + {"id": 16378, "synset": "minder.n.01", "name": "minder"}, + {"id": 16379, "synset": "mining_engineer.n.01", "name": "mining_engineer"}, + {"id": 16380, "synset": "minister.n.02", "name": "minister"}, + {"id": 16381, "synset": "ministrant.n.01", "name": "ministrant"}, + {"id": 16382, "synset": "minor_leaguer.n.01", "name": "minor_leaguer"}, + {"id": 16383, "synset": "minuteman.n.01", "name": "Minuteman"}, + {"id": 16384, "synset": "misanthrope.n.01", "name": "misanthrope"}, + {"id": 16385, "synset": "misfit.n.01", "name": "misfit"}, + {"id": 16386, "synset": "mistress.n.03", "name": "mistress"}, + {"id": 16387, "synset": "mistress.n.01", "name": "mistress"}, + {"id": 16388, "synset": "mixed-blood.n.01", "name": "mixed-blood"}, + {"id": 16389, "synset": "model.n.03", "name": "model"}, + {"id": 16390, "synset": "class_act.n.01", "name": "class_act"}, + {"id": 16391, "synset": "modeler.n.01", "name": "modeler"}, + {"id": 16392, "synset": "modifier.n.02", "name": "modifier"}, + {"id": 16393, "synset": "molecular_biologist.n.01", "name": "molecular_biologist"}, + {"id": 16394, "synset": "monegasque.n.01", "name": "Monegasque"}, + {"id": 16395, "synset": "monetarist.n.01", "name": "monetarist"}, + {"id": 16396, "synset": "moneygrubber.n.01", "name": "moneygrubber"}, + {"id": 16397, "synset": "moneymaker.n.01", "name": "moneymaker"}, + {"id": 16398, "synset": "mongoloid.n.01", "name": "Mongoloid"}, + {"id": 16399, "synset": "monolingual.n.01", "name": "monolingual"}, + {"id": 16400, "synset": "monologist.n.01", "name": "monologist"}, + {"id": 16401, "synset": "moonlighter.n.01", "name": "moonlighter"}, + {"id": 16402, "synset": "moralist.n.01", "name": "moralist"}, + {"id": 16403, "synset": "morosoph.n.01", "name": "morosoph"}, + {"id": 16404, "synset": "morris_dancer.n.01", "name": "morris_dancer"}, + {"id": 16405, "synset": "mortal_enemy.n.01", "name": "mortal_enemy"}, + {"id": 16406, "synset": "mortgagee.n.01", "name": "mortgagee"}, + {"id": 16407, "synset": "mortician.n.01", "name": "mortician"}, + {"id": 16408, "synset": "moss-trooper.n.01", "name": "moss-trooper"}, + {"id": 16409, "synset": "mother.n.01", "name": "mother"}, + {"id": 16410, "synset": "mother.n.04", "name": "mother"}, + {"id": 16411, "synset": "mother.n.03", "name": "mother"}, + {"id": 16412, "synset": "mother_figure.n.01", "name": "mother_figure"}, + {"id": 16413, "synset": "mother_hen.n.01", "name": "mother_hen"}, + {"id": 16414, "synset": "mother-in-law.n.01", "name": "mother-in-law"}, + {"id": 16415, "synset": "mother's_boy.n.01", "name": "mother's_boy"}, + {"id": 16416, "synset": "mother's_daughter.n.01", "name": "mother's_daughter"}, + {"id": 16417, "synset": "motorcycle_cop.n.01", "name": "motorcycle_cop"}, + {"id": 16418, "synset": "motorcyclist.n.01", "name": "motorcyclist"}, + {"id": 16419, "synset": "mound_builder.n.01", "name": "Mound_Builder"}, + {"id": 16420, "synset": "mountebank.n.01", "name": "mountebank"}, + {"id": 16421, "synset": "mourner.n.01", "name": "mourner"}, + {"id": 16422, "synset": "mouthpiece.n.03", "name": "mouthpiece"}, + {"id": 16423, "synset": "mover.n.03", "name": "mover"}, + {"id": 16424, "synset": "moviegoer.n.01", "name": "moviegoer"}, + {"id": 16425, "synset": "muffin_man.n.01", "name": "muffin_man"}, + {"id": 16426, "synset": "mugwump.n.02", "name": "mugwump"}, + {"id": 16427, "synset": "mullah.n.01", "name": "Mullah"}, + {"id": 16428, "synset": "muncher.n.01", "name": "muncher"}, + {"id": 16429, "synset": "murderess.n.01", "name": "murderess"}, + {"id": 16430, "synset": "murder_suspect.n.01", "name": "murder_suspect"}, + {"id": 16431, "synset": "musher.n.01", "name": "musher"}, + {"id": 16432, "synset": "musician.n.01", "name": "musician"}, + {"id": 16433, "synset": "musicologist.n.01", "name": "musicologist"}, + {"id": 16434, "synset": "music_teacher.n.01", "name": "music_teacher"}, + {"id": 16435, "synset": "musketeer.n.01", "name": "musketeer"}, + {"id": 16436, "synset": "muslimah.n.01", "name": "Muslimah"}, + {"id": 16437, "synset": "mutilator.n.01", "name": "mutilator"}, + {"id": 16438, "synset": "mutineer.n.01", "name": "mutineer"}, + {"id": 16439, "synset": "mute.n.01", "name": "mute"}, + {"id": 16440, "synset": "mutterer.n.01", "name": "mutterer"}, + {"id": 16441, "synset": "muzzler.n.01", "name": "muzzler"}, + {"id": 16442, "synset": "mycenaen.n.01", "name": "Mycenaen"}, + {"id": 16443, "synset": "mycologist.n.01", "name": "mycologist"}, + {"id": 16444, "synset": "myope.n.01", "name": "myope"}, + {"id": 16445, "synset": "myrmidon.n.01", "name": "myrmidon"}, + {"id": 16446, "synset": "mystic.n.01", "name": "mystic"}, + {"id": 16447, "synset": "mythologist.n.01", "name": "mythologist"}, + {"id": 16448, "synset": "naif.n.01", "name": "naif"}, + {"id": 16449, "synset": "nailer.n.01", "name": "nailer"}, + {"id": 16450, "synset": "namby-pamby.n.01", "name": "namby-pamby"}, + {"id": 16451, "synset": "name_dropper.n.01", "name": "name_dropper"}, + {"id": 16452, "synset": "namer.n.01", "name": "namer"}, + {"id": 16453, "synset": "nan.n.01", "name": "nan"}, + {"id": 16454, "synset": "nanny.n.01", "name": "nanny"}, + {"id": 16455, "synset": "narc.n.01", "name": "narc"}, + {"id": 16456, "synset": "narcissist.n.01", "name": "narcissist"}, + {"id": 16457, "synset": "nark.n.01", "name": "nark"}, + {"id": 16458, "synset": "nationalist.n.02", "name": "nationalist"}, + {"id": 16459, "synset": "nautch_girl.n.01", "name": "nautch_girl"}, + {"id": 16460, "synset": "naval_commander.n.01", "name": "naval_commander"}, + {"id": 16461, "synset": "navy_seal.n.01", "name": "Navy_SEAL"}, + {"id": 16462, "synset": "obstructionist.n.01", "name": "obstructionist"}, + {"id": 16463, "synset": "nazarene.n.02", "name": "Nazarene"}, + {"id": 16464, "synset": "nazarene.n.01", "name": "Nazarene"}, + {"id": 16465, "synset": "nazi.n.01", "name": "Nazi"}, + {"id": 16466, "synset": "nebbish.n.01", "name": "nebbish"}, + {"id": 16467, "synset": "necker.n.01", "name": "necker"}, + {"id": 16468, "synset": "neonate.n.01", "name": "neonate"}, + {"id": 16469, "synset": "nephew.n.01", "name": "nephew"}, + {"id": 16470, "synset": "neurobiologist.n.01", "name": "neurobiologist"}, + {"id": 16471, "synset": "neurologist.n.01", "name": "neurologist"}, + {"id": 16472, "synset": "neurosurgeon.n.01", "name": "neurosurgeon"}, + {"id": 16473, "synset": "neutral.n.01", "name": "neutral"}, + {"id": 16474, "synset": "neutralist.n.01", "name": "neutralist"}, + {"id": 16475, "synset": "newcomer.n.01", "name": "newcomer"}, + {"id": 16476, "synset": "newcomer.n.02", "name": "newcomer"}, + {"id": 16477, "synset": "new_dealer.n.01", "name": "New_Dealer"}, + {"id": 16478, "synset": "newspaper_editor.n.01", "name": "newspaper_editor"}, + {"id": 16479, "synset": "newsreader.n.01", "name": "newsreader"}, + {"id": 16480, "synset": "newtonian.n.01", "name": "Newtonian"}, + {"id": 16481, "synset": "niece.n.01", "name": "niece"}, + {"id": 16482, "synset": "niggard.n.01", "name": "niggard"}, + {"id": 16483, "synset": "night_porter.n.01", "name": "night_porter"}, + {"id": 16484, "synset": "night_rider.n.01", "name": "night_rider"}, + {"id": 16485, "synset": "nimby.n.01", "name": "NIMBY"}, + {"id": 16486, "synset": "niqaabi.n.01", "name": "niqaabi"}, + {"id": 16487, "synset": "nitpicker.n.01", "name": "nitpicker"}, + {"id": 16488, "synset": "nobelist.n.01", "name": "Nobelist"}, + {"id": 16489, "synset": "noc.n.01", "name": "NOC"}, + {"id": 16490, "synset": "noncandidate.n.01", "name": "noncandidate"}, + {"id": 16491, "synset": "noncommissioned_officer.n.01", "name": "noncommissioned_officer"}, + {"id": 16492, "synset": "nondescript.n.01", "name": "nondescript"}, + {"id": 16493, "synset": "nondriver.n.01", "name": "nondriver"}, + {"id": 16494, "synset": "nonparticipant.n.01", "name": "nonparticipant"}, + {"id": 16495, "synset": "nonperson.n.01", "name": "nonperson"}, + {"id": 16496, "synset": "nonresident.n.01", "name": "nonresident"}, + {"id": 16497, "synset": "nonsmoker.n.01", "name": "nonsmoker"}, + {"id": 16498, "synset": "northern_baptist.n.01", "name": "Northern_Baptist"}, + {"id": 16499, "synset": "noticer.n.01", "name": "noticer"}, + {"id": 16500, "synset": "novelist.n.01", "name": "novelist"}, + {"id": 16501, "synset": "novitiate.n.02", "name": "novitiate"}, + {"id": 16502, "synset": "nuclear_chemist.n.01", "name": "nuclear_chemist"}, + {"id": 16503, "synset": "nudger.n.01", "name": "nudger"}, + {"id": 16504, "synset": "nullipara.n.01", "name": "nullipara"}, + {"id": 16505, "synset": "number_theorist.n.01", "name": "number_theorist"}, + {"id": 16506, "synset": "nurse.n.01", "name": "nurse"}, + {"id": 16507, "synset": "nursling.n.01", "name": "nursling"}, + {"id": 16508, "synset": "nymph.n.03", "name": "nymph"}, + {"id": 16509, "synset": "nymphet.n.01", "name": "nymphet"}, + {"id": 16510, "synset": "nympholept.n.01", "name": "nympholept"}, + {"id": 16511, "synset": "nymphomaniac.n.01", "name": "nymphomaniac"}, + {"id": 16512, "synset": "oarswoman.n.01", "name": "oarswoman"}, + {"id": 16513, "synset": "oboist.n.01", "name": "oboist"}, + {"id": 16514, "synset": "obscurantist.n.01", "name": "obscurantist"}, + {"id": 16515, "synset": "observer.n.02", "name": "observer"}, + {"id": 16516, "synset": "obstetrician.n.01", "name": "obstetrician"}, + {"id": 16517, "synset": "occupier.n.02", "name": "occupier"}, + {"id": 16518, "synset": "occultist.n.01", "name": "occultist"}, + {"id": 16519, "synset": "wine_lover.n.01", "name": "wine_lover"}, + {"id": 16520, "synset": "offerer.n.01", "name": "offerer"}, + {"id": 16521, "synset": "office-bearer.n.01", "name": "office-bearer"}, + {"id": 16522, "synset": "office_boy.n.01", "name": "office_boy"}, + {"id": 16523, "synset": "officeholder.n.01", "name": "officeholder"}, + {"id": 16524, "synset": "officiant.n.01", "name": "officiant"}, + {"id": 16525, "synset": "federal.n.02", "name": "Federal"}, + {"id": 16526, "synset": "oilman.n.02", "name": "oilman"}, + {"id": 16527, "synset": "oil_tycoon.n.01", "name": "oil_tycoon"}, + {"id": 16528, "synset": "old-age_pensioner.n.01", "name": "old-age_pensioner"}, + {"id": 16529, "synset": "old_boy.n.02", "name": "old_boy"}, + {"id": 16530, "synset": "old_lady.n.01", "name": "old_lady"}, + {"id": 16531, "synset": "old_man.n.03", "name": "old_man"}, + {"id": 16532, "synset": "oldster.n.01", "name": "oldster"}, + {"id": 16533, "synset": "old-timer.n.02", "name": "old-timer"}, + {"id": 16534, "synset": "old_woman.n.01", "name": "old_woman"}, + {"id": 16535, "synset": "oligarch.n.01", "name": "oligarch"}, + {"id": 16536, "synset": "olympian.n.01", "name": "Olympian"}, + {"id": 16537, "synset": "omnivore.n.01", "name": "omnivore"}, + {"id": 16538, "synset": "oncologist.n.01", "name": "oncologist"}, + {"id": 16539, "synset": "onlooker.n.01", "name": "onlooker"}, + {"id": 16540, "synset": "onomancer.n.01", "name": "onomancer"}, + {"id": 16541, "synset": "operator.n.03", "name": "operator"}, + {"id": 16542, "synset": "opportunist.n.01", "name": "opportunist"}, + {"id": 16543, "synset": "optimist.n.01", "name": "optimist"}, + {"id": 16544, "synset": "orangeman.n.01", "name": "Orangeman"}, + {"id": 16545, "synset": "orator.n.01", "name": "orator"}, + {"id": 16546, "synset": "orderly.n.02", "name": "orderly"}, + {"id": 16547, "synset": "orderly.n.01", "name": "orderly"}, + {"id": 16548, "synset": "orderly_sergeant.n.01", "name": "orderly_sergeant"}, + {"id": 16549, "synset": "ordinand.n.01", "name": "ordinand"}, + {"id": 16550, "synset": "ordinary.n.03", "name": "ordinary"}, + {"id": 16551, "synset": "organ-grinder.n.01", "name": "organ-grinder"}, + {"id": 16552, "synset": "organist.n.01", "name": "organist"}, + {"id": 16553, "synset": "organization_man.n.01", "name": "organization_man"}, + {"id": 16554, "synset": "organizer.n.01", "name": "organizer"}, + {"id": 16555, "synset": "organizer.n.02", "name": "organizer"}, + {"id": 16556, "synset": "originator.n.01", "name": "originator"}, + {"id": 16557, "synset": "ornithologist.n.01", "name": "ornithologist"}, + {"id": 16558, "synset": "orphan.n.01", "name": "orphan"}, + {"id": 16559, "synset": "orphan.n.02", "name": "orphan"}, + {"id": 16560, "synset": "osteopath.n.01", "name": "osteopath"}, + {"id": 16561, "synset": "out-and-outer.n.01", "name": "out-and-outer"}, + {"id": 16562, "synset": "outdoorswoman.n.01", "name": "outdoorswoman"}, + {"id": 16563, "synset": "outfielder.n.02", "name": "outfielder"}, + {"id": 16564, "synset": "outfielder.n.01", "name": "outfielder"}, + {"id": 16565, "synset": "right_fielder.n.01", "name": "right_fielder"}, + {"id": 16566, "synset": "right-handed_pitcher.n.01", "name": "right-handed_pitcher"}, + {"id": 16567, "synset": "outlier.n.01", "name": "outlier"}, + {"id": 16568, "synset": "owner-occupier.n.01", "name": "owner-occupier"}, + {"id": 16569, "synset": "oyabun.n.01", "name": "oyabun"}, + {"id": 16570, "synset": "packrat.n.01", "name": "packrat"}, + {"id": 16571, "synset": "padrone.n.02", "name": "padrone"}, + {"id": 16572, "synset": "padrone.n.01", "name": "padrone"}, + {"id": 16573, "synset": "page.n.04", "name": "page"}, + {"id": 16574, "synset": "painter.n.02", "name": "painter"}, + {"id": 16575, "synset": "paleo-american.n.01", "name": "Paleo-American"}, + {"id": 16576, "synset": "paleontologist.n.01", "name": "paleontologist"}, + {"id": 16577, "synset": "pallbearer.n.01", "name": "pallbearer"}, + {"id": 16578, "synset": "palmist.n.01", "name": "palmist"}, + {"id": 16579, "synset": "pamperer.n.01", "name": "pamperer"}, + {"id": 16580, "synset": "panchen_lama.n.01", "name": "Panchen_Lama"}, + {"id": 16581, "synset": "panelist.n.01", "name": "panelist"}, + {"id": 16582, "synset": "panhandler.n.01", "name": "panhandler"}, + {"id": 16583, "synset": "paparazzo.n.01", "name": "paparazzo"}, + {"id": 16584, "synset": "paperboy.n.01", "name": "paperboy"}, + {"id": 16585, "synset": "paperhanger.n.02", "name": "paperhanger"}, + {"id": 16586, "synset": "paperhanger.n.01", "name": "paperhanger"}, + {"id": 16587, "synset": "papoose.n.01", "name": "papoose"}, + {"id": 16588, "synset": "pardoner.n.02", "name": "pardoner"}, + {"id": 16589, "synset": "paretic.n.01", "name": "paretic"}, + {"id": 16590, "synset": "parishioner.n.01", "name": "parishioner"}, + {"id": 16591, "synset": "park_commissioner.n.01", "name": "park_commissioner"}, + {"id": 16592, "synset": "parliamentarian.n.01", "name": "Parliamentarian"}, + {"id": 16593, "synset": "parliamentary_agent.n.01", "name": "parliamentary_agent"}, + {"id": 16594, "synset": "parodist.n.01", "name": "parodist"}, + {"id": 16595, "synset": "parricide.n.01", "name": "parricide"}, + {"id": 16596, "synset": "parrot.n.02", "name": "parrot"}, + {"id": 16597, "synset": "partaker.n.01", "name": "partaker"}, + {"id": 16598, "synset": "part-timer.n.01", "name": "part-timer"}, + {"id": 16599, "synset": "party.n.05", "name": "party"}, + {"id": 16600, "synset": "party_man.n.01", "name": "party_man"}, + {"id": 16601, "synset": "passenger.n.01", "name": "passenger"}, + {"id": 16602, "synset": "passer.n.03", "name": "passer"}, + {"id": 16603, "synset": "paster.n.01", "name": "paster"}, + {"id": 16604, "synset": "pater.n.01", "name": "pater"}, + {"id": 16605, "synset": "patient.n.01", "name": "patient"}, + {"id": 16606, "synset": "patriarch.n.04", "name": "patriarch"}, + {"id": 16607, "synset": "patriarch.n.03", "name": "patriarch"}, + {"id": 16608, "synset": "patriarch.n.02", "name": "patriarch"}, + {"id": 16609, "synset": "patriot.n.01", "name": "patriot"}, + {"id": 16610, "synset": "patron.n.03", "name": "patron"}, + {"id": 16611, "synset": "patternmaker.n.01", "name": "patternmaker"}, + {"id": 16612, "synset": "pawnbroker.n.01", "name": "pawnbroker"}, + {"id": 16613, "synset": "payer.n.01", "name": "payer"}, + {"id": 16614, "synset": "peacekeeper.n.01", "name": "peacekeeper"}, + {"id": 16615, "synset": "peasant.n.02", "name": "peasant"}, + {"id": 16616, "synset": "pedant.n.01", "name": "pedant"}, + {"id": 16617, "synset": "peddler.n.01", "name": "peddler"}, + {"id": 16618, "synset": "pederast.n.01", "name": "pederast"}, + {"id": 16619, "synset": "penologist.n.01", "name": "penologist"}, + {"id": 16620, "synset": "pentathlete.n.01", "name": "pentathlete"}, + {"id": 16621, "synset": "pentecostal.n.01", "name": "Pentecostal"}, + {"id": 16622, "synset": "percussionist.n.01", "name": "percussionist"}, + {"id": 16623, "synset": "periodontist.n.01", "name": "periodontist"}, + {"id": 16624, "synset": "peshmerga.n.01", "name": "peshmerga"}, + {"id": 16625, "synset": "personality.n.02", "name": "personality"}, + {"id": 16626, "synset": "personal_representative.n.01", "name": "personal_representative"}, + {"id": 16627, "synset": "personage.n.01", "name": "personage"}, + {"id": 16628, "synset": "persona_grata.n.01", "name": "persona_grata"}, + {"id": 16629, "synset": "persona_non_grata.n.01", "name": "persona_non_grata"}, + {"id": 16630, "synset": "personification.n.01", "name": "personification"}, + {"id": 16631, "synset": "perspirer.n.01", "name": "perspirer"}, + {"id": 16632, "synset": "pervert.n.01", "name": "pervert"}, + {"id": 16633, "synset": "pessimist.n.01", "name": "pessimist"}, + {"id": 16634, "synset": "pest.n.03", "name": "pest"}, + {"id": 16635, "synset": "peter_pan.n.01", "name": "Peter_Pan"}, + {"id": 16636, "synset": "petitioner.n.01", "name": "petitioner"}, + {"id": 16637, "synset": "petit_juror.n.01", "name": "petit_juror"}, + {"id": 16638, "synset": "pet_sitter.n.01", "name": "pet_sitter"}, + {"id": 16639, "synset": "petter.n.01", "name": "petter"}, + {"id": 16640, "synset": "pharaoh.n.01", "name": "Pharaoh"}, + {"id": 16641, "synset": "pharmacist.n.01", "name": "pharmacist"}, + {"id": 16642, "synset": "philanthropist.n.01", "name": "philanthropist"}, + {"id": 16643, "synset": "philatelist.n.01", "name": "philatelist"}, + {"id": 16644, "synset": "philosopher.n.02", "name": "philosopher"}, + {"id": 16645, "synset": "phonetician.n.01", "name": "phonetician"}, + {"id": 16646, "synset": "phonologist.n.01", "name": "phonologist"}, + {"id": 16647, "synset": "photojournalist.n.01", "name": "photojournalist"}, + {"id": 16648, "synset": "photometrist.n.01", "name": "photometrist"}, + {"id": 16649, "synset": "physical_therapist.n.01", "name": "physical_therapist"}, + {"id": 16650, "synset": "physicist.n.01", "name": "physicist"}, + {"id": 16651, "synset": "piano_maker.n.01", "name": "piano_maker"}, + {"id": 16652, "synset": "picker.n.01", "name": "picker"}, + {"id": 16653, "synset": "picnicker.n.01", "name": "picnicker"}, + {"id": 16654, "synset": "pilgrim.n.01", "name": "pilgrim"}, + {"id": 16655, "synset": "pill.n.03", "name": "pill"}, + {"id": 16656, "synset": "pillar.n.03", "name": "pillar"}, + {"id": 16657, "synset": "pill_head.n.01", "name": "pill_head"}, + {"id": 16658, "synset": "pilot.n.02", "name": "pilot"}, + {"id": 16659, "synset": "piltdown_man.n.01", "name": "Piltdown_man"}, + {"id": 16660, "synset": "pimp.n.01", "name": "pimp"}, + {"id": 16661, "synset": "pipe_smoker.n.01", "name": "pipe_smoker"}, + {"id": 16662, "synset": "pip-squeak.n.01", "name": "pip-squeak"}, + {"id": 16663, "synset": "pisser.n.01", "name": "pisser"}, + {"id": 16664, "synset": "pitcher.n.01", "name": "pitcher"}, + {"id": 16665, "synset": "pitchman.n.01", "name": "pitchman"}, + {"id": 16666, "synset": "placeman.n.01", "name": "placeman"}, + {"id": 16667, "synset": "placer_miner.n.01", "name": "placer_miner"}, + {"id": 16668, "synset": "plagiarist.n.01", "name": "plagiarist"}, + {"id": 16669, "synset": "plainsman.n.01", "name": "plainsman"}, + {"id": 16670, "synset": "planner.n.01", "name": "planner"}, + {"id": 16671, "synset": "planter.n.01", "name": "planter"}, + {"id": 16672, "synset": "plasterer.n.01", "name": "plasterer"}, + {"id": 16673, "synset": "platinum_blond.n.01", "name": "platinum_blond"}, + {"id": 16674, "synset": "platitudinarian.n.01", "name": "platitudinarian"}, + {"id": 16675, "synset": "playboy.n.01", "name": "playboy"}, + {"id": 16676, "synset": "player.n.01", "name": "player"}, + {"id": 16677, "synset": "playmate.n.01", "name": "playmate"}, + {"id": 16678, "synset": "pleaser.n.01", "name": "pleaser"}, + {"id": 16679, "synset": "pledger.n.01", "name": "pledger"}, + {"id": 16680, "synset": "plenipotentiary.n.01", "name": "plenipotentiary"}, + {"id": 16681, "synset": "plier.n.01", "name": "plier"}, + {"id": 16682, "synset": "plodder.n.03", "name": "plodder"}, + {"id": 16683, "synset": "plodder.n.02", "name": "plodder"}, + {"id": 16684, "synset": "plotter.n.02", "name": "plotter"}, + {"id": 16685, "synset": "plumber.n.01", "name": "plumber"}, + {"id": 16686, "synset": "pluralist.n.02", "name": "pluralist"}, + {"id": 16687, "synset": "pluralist.n.01", "name": "pluralist"}, + {"id": 16688, "synset": "poet.n.01", "name": "poet"}, + {"id": 16689, "synset": "pointsman.n.01", "name": "pointsman"}, + {"id": 16690, "synset": "point_woman.n.01", "name": "point_woman"}, + {"id": 16691, "synset": "policyholder.n.01", "name": "policyholder"}, + {"id": 16692, "synset": "political_prisoner.n.01", "name": "political_prisoner"}, + {"id": 16693, "synset": "political_scientist.n.01", "name": "political_scientist"}, + {"id": 16694, "synset": "politician.n.02", "name": "politician"}, + {"id": 16695, "synset": "politician.n.03", "name": "politician"}, + {"id": 16696, "synset": "pollster.n.01", "name": "pollster"}, + {"id": 16697, "synset": "polluter.n.01", "name": "polluter"}, + {"id": 16698, "synset": "pool_player.n.01", "name": "pool_player"}, + {"id": 16699, "synset": "portraitist.n.01", "name": "portraitist"}, + {"id": 16700, "synset": "poseuse.n.01", "name": "poseuse"}, + {"id": 16701, "synset": "positivist.n.01", "name": "positivist"}, + {"id": 16702, "synset": "postdoc.n.02", "name": "postdoc"}, + {"id": 16703, "synset": "poster_girl.n.01", "name": "poster_girl"}, + {"id": 16704, "synset": "postulator.n.02", "name": "postulator"}, + {"id": 16705, "synset": "private_citizen.n.01", "name": "private_citizen"}, + {"id": 16706, "synset": "problem_solver.n.01", "name": "problem_solver"}, + {"id": 16707, "synset": "pro-lifer.n.01", "name": "pro-lifer"}, + {"id": 16708, "synset": "prosthetist.n.01", "name": "prosthetist"}, + {"id": 16709, "synset": "postulant.n.01", "name": "postulant"}, + {"id": 16710, "synset": "potboy.n.01", "name": "potboy"}, + {"id": 16711, "synset": "poultryman.n.01", "name": "poultryman"}, + {"id": 16712, "synset": "power_user.n.01", "name": "power_user"}, + {"id": 16713, "synset": "power_worker.n.01", "name": "power_worker"}, + {"id": 16714, "synset": "practitioner.n.01", "name": "practitioner"}, + {"id": 16715, "synset": "prayer.n.05", "name": "prayer"}, + {"id": 16716, "synset": "preceptor.n.01", "name": "preceptor"}, + {"id": 16717, "synset": "predecessor.n.01", "name": "predecessor"}, + {"id": 16718, "synset": "preemptor.n.02", "name": "preemptor"}, + {"id": 16719, "synset": "preemptor.n.01", "name": "preemptor"}, + {"id": 16720, "synset": "premature_baby.n.01", "name": "premature_baby"}, + {"id": 16721, "synset": "presbyter.n.01", "name": "presbyter"}, + {"id": 16722, "synset": "presenter.n.02", "name": "presenter"}, + {"id": 16723, "synset": "presentist.n.01", "name": "presentist"}, + {"id": 16724, "synset": "preserver.n.03", "name": "preserver"}, + {"id": 16725, "synset": "president.n.03", "name": "president"}, + { + "id": 16726, + "synset": "president_of_the_united_states.n.01", + "name": "President_of_the_United_States", + }, + {"id": 16727, "synset": "president.n.05", "name": "president"}, + {"id": 16728, "synset": "press_agent.n.01", "name": "press_agent"}, + {"id": 16729, "synset": "press_photographer.n.01", "name": "press_photographer"}, + {"id": 16730, "synset": "priest.n.01", "name": "priest"}, + {"id": 16731, "synset": "prima_ballerina.n.01", "name": "prima_ballerina"}, + {"id": 16732, "synset": "prima_donna.n.02", "name": "prima_donna"}, + {"id": 16733, "synset": "prima_donna.n.01", "name": "prima_donna"}, + {"id": 16734, "synset": "primigravida.n.01", "name": "primigravida"}, + {"id": 16735, "synset": "primordial_dwarf.n.01", "name": "primordial_dwarf"}, + {"id": 16736, "synset": "prince_charming.n.01", "name": "prince_charming"}, + {"id": 16737, "synset": "prince_consort.n.01", "name": "prince_consort"}, + {"id": 16738, "synset": "princeling.n.01", "name": "princeling"}, + {"id": 16739, "synset": "prince_of_wales.n.01", "name": "Prince_of_Wales"}, + {"id": 16740, "synset": "princess.n.01", "name": "princess"}, + {"id": 16741, "synset": "princess_royal.n.01", "name": "princess_royal"}, + {"id": 16742, "synset": "principal.n.06", "name": "principal"}, + {"id": 16743, "synset": "principal.n.02", "name": "principal"}, + {"id": 16744, "synset": "print_seller.n.01", "name": "print_seller"}, + {"id": 16745, "synset": "prior.n.01", "name": "prior"}, + {"id": 16746, "synset": "private.n.01", "name": "private"}, + {"id": 16747, "synset": "probationer.n.01", "name": "probationer"}, + {"id": 16748, "synset": "processor.n.02", "name": "processor"}, + {"id": 16749, "synset": "process-server.n.01", "name": "process-server"}, + {"id": 16750, "synset": "proconsul.n.02", "name": "proconsul"}, + {"id": 16751, "synset": "proconsul.n.01", "name": "proconsul"}, + {"id": 16752, "synset": "proctologist.n.01", "name": "proctologist"}, + {"id": 16753, "synset": "proctor.n.01", "name": "proctor"}, + {"id": 16754, "synset": "procurator.n.02", "name": "procurator"}, + {"id": 16755, "synset": "procurer.n.02", "name": "procurer"}, + {"id": 16756, "synset": "profit_taker.n.01", "name": "profit_taker"}, + {"id": 16757, "synset": "programmer.n.01", "name": "programmer"}, + {"id": 16758, "synset": "promiser.n.01", "name": "promiser"}, + {"id": 16759, "synset": "promoter.n.01", "name": "promoter"}, + {"id": 16760, "synset": "promulgator.n.01", "name": "promulgator"}, + {"id": 16761, "synset": "propagandist.n.01", "name": "propagandist"}, + {"id": 16762, "synset": "propagator.n.02", "name": "propagator"}, + {"id": 16763, "synset": "property_man.n.01", "name": "property_man"}, + {"id": 16764, "synset": "prophetess.n.01", "name": "prophetess"}, + {"id": 16765, "synset": "prophet.n.02", "name": "prophet"}, + {"id": 16766, "synset": "prosecutor.n.01", "name": "prosecutor"}, + {"id": 16767, "synset": "prospector.n.01", "name": "prospector"}, + {"id": 16768, "synset": "protectionist.n.01", "name": "protectionist"}, + {"id": 16769, "synset": "protegee.n.01", "name": "protegee"}, + {"id": 16770, "synset": "protozoologist.n.01", "name": "protozoologist"}, + {"id": 16771, "synset": "provost_marshal.n.01", "name": "provost_marshal"}, + {"id": 16772, "synset": "pruner.n.01", "name": "pruner"}, + {"id": 16773, "synset": "psalmist.n.01", "name": "psalmist"}, + {"id": 16774, "synset": "psephologist.n.01", "name": "psephologist"}, + {"id": 16775, "synset": "psychiatrist.n.01", "name": "psychiatrist"}, + {"id": 16776, "synset": "psychic.n.01", "name": "psychic"}, + {"id": 16777, "synset": "psycholinguist.n.01", "name": "psycholinguist"}, + {"id": 16778, "synset": "psychophysicist.n.01", "name": "psychophysicist"}, + {"id": 16779, "synset": "publican.n.01", "name": "publican"}, + {"id": 16780, "synset": "pudge.n.01", "name": "pudge"}, + {"id": 16781, "synset": "puerpera.n.01", "name": "puerpera"}, + {"id": 16782, "synset": "punching_bag.n.01", "name": "punching_bag"}, + {"id": 16783, "synset": "punter.n.02", "name": "punter"}, + {"id": 16784, "synset": "punter.n.01", "name": "punter"}, + {"id": 16785, "synset": "puppeteer.n.01", "name": "puppeteer"}, + {"id": 16786, "synset": "puppy.n.02", "name": "puppy"}, + {"id": 16787, "synset": "purchasing_agent.n.01", "name": "purchasing_agent"}, + {"id": 16788, "synset": "puritan.n.02", "name": "puritan"}, + {"id": 16789, "synset": "puritan.n.01", "name": "Puritan"}, + {"id": 16790, "synset": "pursuer.n.02", "name": "pursuer"}, + {"id": 16791, "synset": "pusher.n.03", "name": "pusher"}, + {"id": 16792, "synset": "pusher.n.02", "name": "pusher"}, + {"id": 16793, "synset": "pusher.n.01", "name": "pusher"}, + {"id": 16794, "synset": "putz.n.01", "name": "putz"}, + {"id": 16795, "synset": "pygmy.n.02", "name": "Pygmy"}, + {"id": 16796, "synset": "qadi.n.01", "name": "qadi"}, + {"id": 16797, "synset": "quadriplegic.n.01", "name": "quadriplegic"}, + {"id": 16798, "synset": "quadruplet.n.02", "name": "quadruplet"}, + {"id": 16799, "synset": "quaker.n.02", "name": "quaker"}, + {"id": 16800, "synset": "quarter.n.11", "name": "quarter"}, + {"id": 16801, "synset": "quarterback.n.01", "name": "quarterback"}, + {"id": 16802, "synset": "quartermaster.n.01", "name": "quartermaster"}, + {"id": 16803, "synset": "quartermaster_general.n.01", "name": "quartermaster_general"}, + {"id": 16804, "synset": "quebecois.n.01", "name": "Quebecois"}, + {"id": 16805, "synset": "queen.n.02", "name": "queen"}, + {"id": 16806, "synset": "queen_of_england.n.01", "name": "Queen_of_England"}, + {"id": 16807, "synset": "queen.n.03", "name": "queen"}, + {"id": 16808, "synset": "queen.n.04", "name": "queen"}, + {"id": 16809, "synset": "queen_consort.n.01", "name": "queen_consort"}, + {"id": 16810, "synset": "queen_mother.n.01", "name": "queen_mother"}, + {"id": 16811, "synset": "queen's_counsel.n.01", "name": "Queen's_Counsel"}, + {"id": 16812, "synset": "question_master.n.01", "name": "question_master"}, + {"id": 16813, "synset": "quick_study.n.01", "name": "quick_study"}, + {"id": 16814, "synset": "quietist.n.01", "name": "quietist"}, + {"id": 16815, "synset": "quitter.n.01", "name": "quitter"}, + {"id": 16816, "synset": "rabbi.n.01", "name": "rabbi"}, + {"id": 16817, "synset": "racist.n.01", "name": "racist"}, + {"id": 16818, "synset": "radiobiologist.n.01", "name": "radiobiologist"}, + {"id": 16819, "synset": "radiologic_technologist.n.01", "name": "radiologic_technologist"}, + {"id": 16820, "synset": "radiologist.n.01", "name": "radiologist"}, + {"id": 16821, "synset": "rainmaker.n.02", "name": "rainmaker"}, + {"id": 16822, "synset": "raiser.n.01", "name": "raiser"}, + {"id": 16823, "synset": "raja.n.01", "name": "raja"}, + {"id": 16824, "synset": "rake.n.01", "name": "rake"}, + {"id": 16825, "synset": "ramrod.n.02", "name": "ramrod"}, + {"id": 16826, "synset": "ranch_hand.n.01", "name": "ranch_hand"}, + {"id": 16827, "synset": "ranker.n.01", "name": "ranker"}, + {"id": 16828, "synset": "ranter.n.01", "name": "ranter"}, + {"id": 16829, "synset": "rape_suspect.n.01", "name": "rape_suspect"}, + {"id": 16830, "synset": "rapper.n.01", "name": "rapper"}, + {"id": 16831, "synset": "rapporteur.n.01", "name": "rapporteur"}, + {"id": 16832, "synset": "rare_bird.n.01", "name": "rare_bird"}, + {"id": 16833, "synset": "ratepayer.n.01", "name": "ratepayer"}, + {"id": 16834, "synset": "raw_recruit.n.01", "name": "raw_recruit"}, + {"id": 16835, "synset": "reader.n.01", "name": "reader"}, + {"id": 16836, "synset": "reading_teacher.n.01", "name": "reading_teacher"}, + {"id": 16837, "synset": "realist.n.01", "name": "realist"}, + {"id": 16838, "synset": "real_estate_broker.n.01", "name": "real_estate_broker"}, + {"id": 16839, "synset": "rear_admiral.n.01", "name": "rear_admiral"}, + {"id": 16840, "synset": "receiver.n.05", "name": "receiver"}, + {"id": 16841, "synset": "reciter.n.01", "name": "reciter"}, + {"id": 16842, "synset": "recruit.n.02", "name": "recruit"}, + {"id": 16843, "synset": "recruit.n.01", "name": "recruit"}, + {"id": 16844, "synset": "recruiter.n.01", "name": "recruiter"}, + {"id": 16845, "synset": "recruiting-sergeant.n.01", "name": "recruiting-sergeant"}, + {"id": 16846, "synset": "redcap.n.01", "name": "redcap"}, + {"id": 16847, "synset": "redhead.n.01", "name": "redhead"}, + {"id": 16848, "synset": "redneck.n.01", "name": "redneck"}, + {"id": 16849, "synset": "reeler.n.02", "name": "reeler"}, + {"id": 16850, "synset": "reenactor.n.01", "name": "reenactor"}, + {"id": 16851, "synset": "referral.n.01", "name": "referral"}, + {"id": 16852, "synset": "referee.n.01", "name": "referee"}, + {"id": 16853, "synset": "refiner.n.01", "name": "refiner"}, + {"id": 16854, "synset": "reform_jew.n.01", "name": "Reform_Jew"}, + {"id": 16855, "synset": "registered_nurse.n.01", "name": "registered_nurse"}, + {"id": 16856, "synset": "registrar.n.01", "name": "registrar"}, + {"id": 16857, "synset": "regius_professor.n.01", "name": "Regius_professor"}, + {"id": 16858, "synset": "reliever.n.02", "name": "reliever"}, + {"id": 16859, "synset": "anchorite.n.01", "name": "anchorite"}, + {"id": 16860, "synset": "religious_leader.n.01", "name": "religious_leader"}, + {"id": 16861, "synset": "remover.n.02", "name": "remover"}, + {"id": 16862, "synset": "renaissance_man.n.01", "name": "Renaissance_man"}, + {"id": 16863, "synset": "renegade.n.01", "name": "renegade"}, + {"id": 16864, "synset": "rentier.n.01", "name": "rentier"}, + {"id": 16865, "synset": "repairman.n.01", "name": "repairman"}, + {"id": 16866, "synset": "reporter.n.01", "name": "reporter"}, + {"id": 16867, "synset": "newswoman.n.01", "name": "newswoman"}, + {"id": 16868, "synset": "representative.n.01", "name": "representative"}, + {"id": 16869, "synset": "reprobate.n.01", "name": "reprobate"}, + {"id": 16870, "synset": "rescuer.n.02", "name": "rescuer"}, + {"id": 16871, "synset": "reservist.n.01", "name": "reservist"}, + {"id": 16872, "synset": "resident_commissioner.n.01", "name": "resident_commissioner"}, + {"id": 16873, "synset": "respecter.n.01", "name": "respecter"}, + {"id": 16874, "synset": "restaurateur.n.01", "name": "restaurateur"}, + {"id": 16875, "synset": "restrainer.n.02", "name": "restrainer"}, + {"id": 16876, "synset": "retailer.n.01", "name": "retailer"}, + {"id": 16877, "synset": "retiree.n.01", "name": "retiree"}, + {"id": 16878, "synset": "returning_officer.n.01", "name": "returning_officer"}, + {"id": 16879, "synset": "revenant.n.01", "name": "revenant"}, + {"id": 16880, "synset": "revisionist.n.01", "name": "revisionist"}, + {"id": 16881, "synset": "revolutionist.n.01", "name": "revolutionist"}, + {"id": 16882, "synset": "rheumatologist.n.01", "name": "rheumatologist"}, + {"id": 16883, "synset": "rhodesian_man.n.01", "name": "Rhodesian_man"}, + {"id": 16884, "synset": "rhymer.n.01", "name": "rhymer"}, + {"id": 16885, "synset": "rich_person.n.01", "name": "rich_person"}, + {"id": 16886, "synset": "rider.n.03", "name": "rider"}, + {"id": 16887, "synset": "riding_master.n.01", "name": "riding_master"}, + {"id": 16888, "synset": "rifleman.n.02", "name": "rifleman"}, + {"id": 16889, "synset": "right-hander.n.02", "name": "right-hander"}, + {"id": 16890, "synset": "right-hand_man.n.01", "name": "right-hand_man"}, + {"id": 16891, "synset": "ringer.n.03", "name": "ringer"}, + {"id": 16892, "synset": "ringleader.n.01", "name": "ringleader"}, + {"id": 16893, "synset": "roadman.n.02", "name": "roadman"}, + {"id": 16894, "synset": "roarer.n.01", "name": "roarer"}, + {"id": 16895, "synset": "rocket_engineer.n.01", "name": "rocket_engineer"}, + {"id": 16896, "synset": "rocket_scientist.n.01", "name": "rocket_scientist"}, + {"id": 16897, "synset": "rock_star.n.01", "name": "rock_star"}, + {"id": 16898, "synset": "romanov.n.01", "name": "Romanov"}, + {"id": 16899, "synset": "romanticist.n.02", "name": "romanticist"}, + {"id": 16900, "synset": "ropemaker.n.01", "name": "ropemaker"}, + {"id": 16901, "synset": "roper.n.02", "name": "roper"}, + {"id": 16902, "synset": "roper.n.01", "name": "roper"}, + {"id": 16903, "synset": "ropewalker.n.01", "name": "ropewalker"}, + {"id": 16904, "synset": "rosebud.n.02", "name": "rosebud"}, + {"id": 16905, "synset": "rosicrucian.n.02", "name": "Rosicrucian"}, + {"id": 16906, "synset": "mountie.n.01", "name": "Mountie"}, + {"id": 16907, "synset": "rough_rider.n.01", "name": "Rough_Rider"}, + {"id": 16908, "synset": "roundhead.n.01", "name": "roundhead"}, + {"id": 16909, "synset": "civil_authority.n.01", "name": "civil_authority"}, + {"id": 16910, "synset": "runner.n.03", "name": "runner"}, + {"id": 16911, "synset": "runner.n.02", "name": "runner"}, + {"id": 16912, "synset": "runner.n.06", "name": "runner"}, + {"id": 16913, "synset": "running_back.n.01", "name": "running_back"}, + {"id": 16914, "synset": "rusher.n.02", "name": "rusher"}, + {"id": 16915, "synset": "rustic.n.01", "name": "rustic"}, + {"id": 16916, "synset": "saboteur.n.01", "name": "saboteur"}, + {"id": 16917, "synset": "sadist.n.01", "name": "sadist"}, + {"id": 16918, "synset": "sailing_master.n.01", "name": "sailing_master"}, + {"id": 16919, "synset": "sailor.n.01", "name": "sailor"}, + {"id": 16920, "synset": "salesgirl.n.01", "name": "salesgirl"}, + {"id": 16921, "synset": "salesman.n.01", "name": "salesman"}, + {"id": 16922, "synset": "salesperson.n.01", "name": "salesperson"}, + {"id": 16923, "synset": "salvager.n.01", "name": "salvager"}, + {"id": 16924, "synset": "sandwichman.n.01", "name": "sandwichman"}, + {"id": 16925, "synset": "sangoma.n.01", "name": "sangoma"}, + {"id": 16926, "synset": "sannup.n.01", "name": "sannup"}, + {"id": 16927, "synset": "sapper.n.02", "name": "sapper"}, + {"id": 16928, "synset": "sassenach.n.01", "name": "Sassenach"}, + {"id": 16929, "synset": "satrap.n.01", "name": "satrap"}, + {"id": 16930, "synset": "saunterer.n.01", "name": "saunterer"}, + {"id": 16931, "synset": "savoyard.n.01", "name": "Savoyard"}, + {"id": 16932, "synset": "sawyer.n.01", "name": "sawyer"}, + {"id": 16933, "synset": "scalper.n.01", "name": "scalper"}, + {"id": 16934, "synset": "scandalmonger.n.01", "name": "scandalmonger"}, + {"id": 16935, "synset": "scapegrace.n.01", "name": "scapegrace"}, + {"id": 16936, "synset": "scene_painter.n.02", "name": "scene_painter"}, + {"id": 16937, "synset": "schemer.n.01", "name": "schemer"}, + {"id": 16938, "synset": "schizophrenic.n.01", "name": "schizophrenic"}, + {"id": 16939, "synset": "schlemiel.n.01", "name": "schlemiel"}, + {"id": 16940, "synset": "schlockmeister.n.01", "name": "schlockmeister"}, + {"id": 16941, "synset": "scholar.n.01", "name": "scholar"}, + {"id": 16942, "synset": "scholiast.n.01", "name": "scholiast"}, + {"id": 16943, "synset": "schoolchild.n.01", "name": "schoolchild"}, + {"id": 16944, "synset": "schoolfriend.n.01", "name": "schoolfriend"}, + {"id": 16945, "synset": "schoolman.n.01", "name": "Schoolman"}, + {"id": 16946, "synset": "schoolmaster.n.02", "name": "schoolmaster"}, + {"id": 16947, "synset": "schoolmate.n.01", "name": "schoolmate"}, + {"id": 16948, "synset": "scientist.n.01", "name": "scientist"}, + {"id": 16949, "synset": "scion.n.01", "name": "scion"}, + {"id": 16950, "synset": "scoffer.n.02", "name": "scoffer"}, + {"id": 16951, "synset": "scofflaw.n.01", "name": "scofflaw"}, + {"id": 16952, "synset": "scorekeeper.n.01", "name": "scorekeeper"}, + {"id": 16953, "synset": "scorer.n.02", "name": "scorer"}, + {"id": 16954, "synset": "scourer.n.02", "name": "scourer"}, + {"id": 16955, "synset": "scout.n.03", "name": "scout"}, + {"id": 16956, "synset": "scoutmaster.n.01", "name": "scoutmaster"}, + {"id": 16957, "synset": "scrambler.n.01", "name": "scrambler"}, + {"id": 16958, "synset": "scratcher.n.02", "name": "scratcher"}, + {"id": 16959, "synset": "screen_actor.n.01", "name": "screen_actor"}, + {"id": 16960, "synset": "scrutineer.n.01", "name": "scrutineer"}, + {"id": 16961, "synset": "scuba_diver.n.01", "name": "scuba_diver"}, + {"id": 16962, "synset": "sculptor.n.01", "name": "sculptor"}, + {"id": 16963, "synset": "sea_scout.n.01", "name": "Sea_Scout"}, + {"id": 16964, "synset": "seasonal_worker.n.01", "name": "seasonal_worker"}, + {"id": 16965, "synset": "seasoner.n.01", "name": "seasoner"}, + {"id": 16966, "synset": "second_baseman.n.01", "name": "second_baseman"}, + {"id": 16967, "synset": "second_cousin.n.01", "name": "second_cousin"}, + {"id": 16968, "synset": "seconder.n.01", "name": "seconder"}, + {"id": 16969, "synset": "second_fiddle.n.01", "name": "second_fiddle"}, + {"id": 16970, "synset": "second-in-command.n.01", "name": "second-in-command"}, + {"id": 16971, "synset": "second_lieutenant.n.01", "name": "second_lieutenant"}, + {"id": 16972, "synset": "second-rater.n.01", "name": "second-rater"}, + {"id": 16973, "synset": "secretary.n.01", "name": "secretary"}, + {"id": 16974, "synset": "secretary_of_agriculture.n.01", "name": "Secretary_of_Agriculture"}, + { + "id": 16975, + "synset": "secretary_of_health_and_human_services.n.01", + "name": "Secretary_of_Health_and_Human_Services", + }, + {"id": 16976, "synset": "secretary_of_state.n.01", "name": "Secretary_of_State"}, + {"id": 16977, "synset": "secretary_of_the_interior.n.02", "name": "Secretary_of_the_Interior"}, + {"id": 16978, "synset": "sectarian.n.01", "name": "sectarian"}, + {"id": 16979, "synset": "section_hand.n.01", "name": "section_hand"}, + {"id": 16980, "synset": "secularist.n.01", "name": "secularist"}, + {"id": 16981, "synset": "security_consultant.n.01", "name": "security_consultant"}, + {"id": 16982, "synset": "seeded_player.n.01", "name": "seeded_player"}, + {"id": 16983, "synset": "seeder.n.01", "name": "seeder"}, + {"id": 16984, "synset": "seeker.n.01", "name": "seeker"}, + {"id": 16985, "synset": "segregate.n.01", "name": "segregate"}, + {"id": 16986, "synset": "segregator.n.01", "name": "segregator"}, + {"id": 16987, "synset": "selectman.n.01", "name": "selectman"}, + {"id": 16988, "synset": "selectwoman.n.01", "name": "selectwoman"}, + {"id": 16989, "synset": "selfish_person.n.01", "name": "selfish_person"}, + {"id": 16990, "synset": "self-starter.n.01", "name": "self-starter"}, + {"id": 16991, "synset": "seller.n.01", "name": "seller"}, + {"id": 16992, "synset": "selling_agent.n.01", "name": "selling_agent"}, + {"id": 16993, "synset": "semanticist.n.01", "name": "semanticist"}, + {"id": 16994, "synset": "semifinalist.n.01", "name": "semifinalist"}, + {"id": 16995, "synset": "seminarian.n.01", "name": "seminarian"}, + {"id": 16996, "synset": "senator.n.01", "name": "senator"}, + {"id": 16997, "synset": "sendee.n.01", "name": "sendee"}, + {"id": 16998, "synset": "senior.n.01", "name": "senior"}, + {"id": 16999, "synset": "senior_vice_president.n.01", "name": "senior_vice_president"}, + {"id": 17000, "synset": "separatist.n.01", "name": "separatist"}, + {"id": 17001, "synset": "septuagenarian.n.01", "name": "septuagenarian"}, + {"id": 17002, "synset": "serf.n.01", "name": "serf"}, + {"id": 17003, "synset": "spree_killer.n.01", "name": "spree_killer"}, + {"id": 17004, "synset": "serjeant-at-law.n.01", "name": "serjeant-at-law"}, + {"id": 17005, "synset": "server.n.02", "name": "server"}, + {"id": 17006, "synset": "serviceman.n.01", "name": "serviceman"}, + {"id": 17007, "synset": "settler.n.01", "name": "settler"}, + {"id": 17008, "synset": "settler.n.03", "name": "settler"}, + {"id": 17009, "synset": "sex_symbol.n.01", "name": "sex_symbol"}, + {"id": 17010, "synset": "sexton.n.02", "name": "sexton"}, + {"id": 17011, "synset": "shaheed.n.01", "name": "shaheed"}, + {"id": 17012, "synset": "shakespearian.n.01", "name": "Shakespearian"}, + {"id": 17013, "synset": "shanghaier.n.01", "name": "shanghaier"}, + {"id": 17014, "synset": "sharecropper.n.01", "name": "sharecropper"}, + {"id": 17015, "synset": "shaver.n.01", "name": "shaver"}, + {"id": 17016, "synset": "shavian.n.01", "name": "Shavian"}, + {"id": 17017, "synset": "sheep.n.02", "name": "sheep"}, + {"id": 17018, "synset": "sheik.n.01", "name": "sheik"}, + {"id": 17019, "synset": "shelver.n.01", "name": "shelver"}, + {"id": 17020, "synset": "shepherd.n.01", "name": "shepherd"}, + {"id": 17021, "synset": "ship-breaker.n.01", "name": "ship-breaker"}, + {"id": 17022, "synset": "shipmate.n.01", "name": "shipmate"}, + {"id": 17023, "synset": "shipowner.n.01", "name": "shipowner"}, + {"id": 17024, "synset": "shipping_agent.n.01", "name": "shipping_agent"}, + {"id": 17025, "synset": "shirtmaker.n.01", "name": "shirtmaker"}, + {"id": 17026, "synset": "shogun.n.01", "name": "shogun"}, + {"id": 17027, "synset": "shopaholic.n.01", "name": "shopaholic"}, + {"id": 17028, "synset": "shop_girl.n.01", "name": "shop_girl"}, + {"id": 17029, "synset": "shop_steward.n.01", "name": "shop_steward"}, + {"id": 17030, "synset": "shot_putter.n.01", "name": "shot_putter"}, + {"id": 17031, "synset": "shrew.n.01", "name": "shrew"}, + {"id": 17032, "synset": "shuffler.n.01", "name": "shuffler"}, + {"id": 17033, "synset": "shyster.n.01", "name": "shyster"}, + {"id": 17034, "synset": "sibling.n.01", "name": "sibling"}, + {"id": 17035, "synset": "sick_person.n.01", "name": "sick_person"}, + {"id": 17036, "synset": "sightreader.n.01", "name": "sightreader"}, + {"id": 17037, "synset": "signaler.n.01", "name": "signaler"}, + {"id": 17038, "synset": "signer.n.01", "name": "signer"}, + {"id": 17039, "synset": "signor.n.01", "name": "signor"}, + {"id": 17040, "synset": "signora.n.01", "name": "signora"}, + {"id": 17041, "synset": "signore.n.01", "name": "signore"}, + {"id": 17042, "synset": "signorina.n.01", "name": "signorina"}, + {"id": 17043, "synset": "silent_partner.n.01", "name": "silent_partner"}, + {"id": 17044, "synset": "addle-head.n.01", "name": "addle-head"}, + {"id": 17045, "synset": "simperer.n.01", "name": "simperer"}, + {"id": 17046, "synset": "singer.n.01", "name": "singer"}, + {"id": 17047, "synset": "sinologist.n.01", "name": "Sinologist"}, + {"id": 17048, "synset": "sipper.n.01", "name": "sipper"}, + {"id": 17049, "synset": "sirrah.n.01", "name": "sirrah"}, + {"id": 17050, "synset": "sister.n.02", "name": "Sister"}, + {"id": 17051, "synset": "sister.n.01", "name": "sister"}, + {"id": 17052, "synset": "waverer.n.01", "name": "waverer"}, + {"id": 17053, "synset": "sitar_player.n.01", "name": "sitar_player"}, + {"id": 17054, "synset": "sixth-former.n.01", "name": "sixth-former"}, + {"id": 17055, "synset": "skateboarder.n.01", "name": "skateboarder"}, + {"id": 17056, "synset": "skeptic.n.01", "name": "skeptic"}, + {"id": 17057, "synset": "sketcher.n.01", "name": "sketcher"}, + {"id": 17058, "synset": "skidder.n.02", "name": "skidder"}, + {"id": 17059, "synset": "skier.n.01", "name": "skier"}, + {"id": 17060, "synset": "skinny-dipper.n.01", "name": "skinny-dipper"}, + {"id": 17061, "synset": "skin-diver.n.01", "name": "skin-diver"}, + {"id": 17062, "synset": "skinhead.n.01", "name": "skinhead"}, + {"id": 17063, "synset": "slasher.n.01", "name": "slasher"}, + {"id": 17064, "synset": "slattern.n.02", "name": "slattern"}, + {"id": 17065, "synset": "sleeper.n.01", "name": "sleeper"}, + {"id": 17066, "synset": "sleeper.n.02", "name": "sleeper"}, + {"id": 17067, "synset": "sleeping_beauty.n.02", "name": "sleeping_beauty"}, + {"id": 17068, "synset": "sleuth.n.01", "name": "sleuth"}, + {"id": 17069, "synset": "slob.n.01", "name": "slob"}, + {"id": 17070, "synset": "sloganeer.n.01", "name": "sloganeer"}, + {"id": 17071, "synset": "slopseller.n.01", "name": "slopseller"}, + {"id": 17072, "synset": "smasher.n.02", "name": "smasher"}, + {"id": 17073, "synset": "smirker.n.01", "name": "smirker"}, + {"id": 17074, "synset": "smith.n.10", "name": "smith"}, + {"id": 17075, "synset": "smoothie.n.01", "name": "smoothie"}, + {"id": 17076, "synset": "smuggler.n.01", "name": "smuggler"}, + {"id": 17077, "synset": "sneezer.n.01", "name": "sneezer"}, + {"id": 17078, "synset": "snob.n.01", "name": "snob"}, + {"id": 17079, "synset": "snoop.n.01", "name": "snoop"}, + {"id": 17080, "synset": "snorer.n.01", "name": "snorer"}, + {"id": 17081, "synset": "sob_sister.n.01", "name": "sob_sister"}, + {"id": 17082, "synset": "soccer_player.n.01", "name": "soccer_player"}, + {"id": 17083, "synset": "social_anthropologist.n.01", "name": "social_anthropologist"}, + {"id": 17084, "synset": "social_climber.n.01", "name": "social_climber"}, + {"id": 17085, "synset": "socialist.n.01", "name": "socialist"}, + {"id": 17086, "synset": "socializer.n.01", "name": "socializer"}, + {"id": 17087, "synset": "social_scientist.n.01", "name": "social_scientist"}, + {"id": 17088, "synset": "social_secretary.n.01", "name": "social_secretary"}, + {"id": 17089, "synset": "socinian.n.01", "name": "Socinian"}, + {"id": 17090, "synset": "sociolinguist.n.01", "name": "sociolinguist"}, + {"id": 17091, "synset": "sociologist.n.01", "name": "sociologist"}, + {"id": 17092, "synset": "soda_jerk.n.01", "name": "soda_jerk"}, + {"id": 17093, "synset": "sodalist.n.01", "name": "sodalist"}, + {"id": 17094, "synset": "sodomite.n.01", "name": "sodomite"}, + {"id": 17095, "synset": "soldier.n.01", "name": "soldier"}, + {"id": 17096, "synset": "son.n.01", "name": "son"}, + {"id": 17097, "synset": "songster.n.02", "name": "songster"}, + {"id": 17098, "synset": "songstress.n.01", "name": "songstress"}, + {"id": 17099, "synset": "songwriter.n.01", "name": "songwriter"}, + {"id": 17100, "synset": "sorcerer.n.01", "name": "sorcerer"}, + {"id": 17101, "synset": "sorehead.n.01", "name": "sorehead"}, + {"id": 17102, "synset": "soul_mate.n.01", "name": "soul_mate"}, + {"id": 17103, "synset": "southern_baptist.n.01", "name": "Southern_Baptist"}, + {"id": 17104, "synset": "sovereign.n.01", "name": "sovereign"}, + {"id": 17105, "synset": "spacewalker.n.01", "name": "spacewalker"}, + {"id": 17106, "synset": "spanish_american.n.01", "name": "Spanish_American"}, + {"id": 17107, "synset": "sparring_partner.n.01", "name": "sparring_partner"}, + {"id": 17108, "synset": "spastic.n.01", "name": "spastic"}, + {"id": 17109, "synset": "speaker.n.01", "name": "speaker"}, + {"id": 17110, "synset": "native_speaker.n.01", "name": "native_speaker"}, + {"id": 17111, "synset": "speaker.n.03", "name": "Speaker"}, + {"id": 17112, "synset": "speechwriter.n.01", "name": "speechwriter"}, + {"id": 17113, "synset": "specialist.n.02", "name": "specialist"}, + {"id": 17114, "synset": "specifier.n.01", "name": "specifier"}, + {"id": 17115, "synset": "spectator.n.01", "name": "spectator"}, + {"id": 17116, "synset": "speech_therapist.n.01", "name": "speech_therapist"}, + {"id": 17117, "synset": "speedskater.n.01", "name": "speedskater"}, + {"id": 17118, "synset": "spellbinder.n.01", "name": "spellbinder"}, + {"id": 17119, "synset": "sphinx.n.01", "name": "sphinx"}, + {"id": 17120, "synset": "spinster.n.01", "name": "spinster"}, + {"id": 17121, "synset": "split_end.n.01", "name": "split_end"}, + {"id": 17122, "synset": "sport.n.05", "name": "sport"}, + {"id": 17123, "synset": "sport.n.03", "name": "sport"}, + {"id": 17124, "synset": "sporting_man.n.02", "name": "sporting_man"}, + {"id": 17125, "synset": "sports_announcer.n.01", "name": "sports_announcer"}, + {"id": 17126, "synset": "sports_editor.n.01", "name": "sports_editor"}, + {"id": 17127, "synset": "sprog.n.02", "name": "sprog"}, + {"id": 17128, "synset": "square_dancer.n.01", "name": "square_dancer"}, + {"id": 17129, "synset": "square_shooter.n.01", "name": "square_shooter"}, + {"id": 17130, "synset": "squatter.n.02", "name": "squatter"}, + {"id": 17131, "synset": "squire.n.02", "name": "squire"}, + {"id": 17132, "synset": "squire.n.01", "name": "squire"}, + {"id": 17133, "synset": "staff_member.n.01", "name": "staff_member"}, + {"id": 17134, "synset": "staff_sergeant.n.01", "name": "staff_sergeant"}, + {"id": 17135, "synset": "stage_director.n.01", "name": "stage_director"}, + {"id": 17136, "synset": "stainer.n.01", "name": "stainer"}, + {"id": 17137, "synset": "stakeholder.n.01", "name": "stakeholder"}, + {"id": 17138, "synset": "stalker.n.02", "name": "stalker"}, + {"id": 17139, "synset": "stalking-horse.n.01", "name": "stalking-horse"}, + {"id": 17140, "synset": "stammerer.n.01", "name": "stammerer"}, + {"id": 17141, "synset": "stamper.n.02", "name": "stamper"}, + {"id": 17142, "synset": "standee.n.01", "name": "standee"}, + {"id": 17143, "synset": "stand-in.n.01", "name": "stand-in"}, + {"id": 17144, "synset": "star.n.04", "name": "star"}, + {"id": 17145, "synset": "starlet.n.01", "name": "starlet"}, + {"id": 17146, "synset": "starter.n.03", "name": "starter"}, + {"id": 17147, "synset": "statesman.n.01", "name": "statesman"}, + {"id": 17148, "synset": "state_treasurer.n.01", "name": "state_treasurer"}, + {"id": 17149, "synset": "stationer.n.01", "name": "stationer"}, + {"id": 17150, "synset": "stenographer.n.01", "name": "stenographer"}, + {"id": 17151, "synset": "stentor.n.01", "name": "stentor"}, + {"id": 17152, "synset": "stepbrother.n.01", "name": "stepbrother"}, + {"id": 17153, "synset": "stepmother.n.01", "name": "stepmother"}, + {"id": 17154, "synset": "stepparent.n.01", "name": "stepparent"}, + {"id": 17155, "synset": "stevedore.n.01", "name": "stevedore"}, + {"id": 17156, "synset": "steward.n.01", "name": "steward"}, + {"id": 17157, "synset": "steward.n.03", "name": "steward"}, + {"id": 17158, "synset": "steward.n.02", "name": "steward"}, + {"id": 17159, "synset": "stickler.n.01", "name": "stickler"}, + {"id": 17160, "synset": "stiff.n.01", "name": "stiff"}, + {"id": 17161, "synset": "stifler.n.01", "name": "stifler"}, + {"id": 17162, "synset": "stipendiary.n.01", "name": "stipendiary"}, + {"id": 17163, "synset": "stitcher.n.01", "name": "stitcher"}, + {"id": 17164, "synset": "stockjobber.n.01", "name": "stockjobber"}, + {"id": 17165, "synset": "stock_trader.n.01", "name": "stock_trader"}, + {"id": 17166, "synset": "stockist.n.01", "name": "stockist"}, + {"id": 17167, "synset": "stoker.n.02", "name": "stoker"}, + {"id": 17168, "synset": "stooper.n.02", "name": "stooper"}, + {"id": 17169, "synset": "store_detective.n.01", "name": "store_detective"}, + {"id": 17170, "synset": "strafer.n.01", "name": "strafer"}, + {"id": 17171, "synset": "straight_man.n.01", "name": "straight_man"}, + {"id": 17172, "synset": "stranger.n.01", "name": "stranger"}, + {"id": 17173, "synset": "stranger.n.02", "name": "stranger"}, + {"id": 17174, "synset": "strategist.n.01", "name": "strategist"}, + {"id": 17175, "synset": "straw_boss.n.01", "name": "straw_boss"}, + {"id": 17176, "synset": "streetwalker.n.01", "name": "streetwalker"}, + {"id": 17177, "synset": "stretcher-bearer.n.01", "name": "stretcher-bearer"}, + {"id": 17178, "synset": "struggler.n.01", "name": "struggler"}, + {"id": 17179, "synset": "stud.n.01", "name": "stud"}, + {"id": 17180, "synset": "student.n.01", "name": "student"}, + {"id": 17181, "synset": "stumblebum.n.01", "name": "stumblebum"}, + {"id": 17182, "synset": "stylist.n.01", "name": "stylist"}, + {"id": 17183, "synset": "subaltern.n.01", "name": "subaltern"}, + {"id": 17184, "synset": "subcontractor.n.01", "name": "subcontractor"}, + {"id": 17185, "synset": "subduer.n.01", "name": "subduer"}, + {"id": 17186, "synset": "subject.n.06", "name": "subject"}, + {"id": 17187, "synset": "subordinate.n.01", "name": "subordinate"}, + {"id": 17188, "synset": "substitute.n.02", "name": "substitute"}, + {"id": 17189, "synset": "successor.n.03", "name": "successor"}, + {"id": 17190, "synset": "successor.n.01", "name": "successor"}, + {"id": 17191, "synset": "succorer.n.01", "name": "succorer"}, + {"id": 17192, "synset": "sufi.n.01", "name": "Sufi"}, + {"id": 17193, "synset": "suffragan.n.01", "name": "suffragan"}, + {"id": 17194, "synset": "suffragette.n.01", "name": "suffragette"}, + {"id": 17195, "synset": "sugar_daddy.n.01", "name": "sugar_daddy"}, + {"id": 17196, "synset": "suicide_bomber.n.01", "name": "suicide_bomber"}, + {"id": 17197, "synset": "suitor.n.01", "name": "suitor"}, + {"id": 17198, "synset": "sumo_wrestler.n.01", "name": "sumo_wrestler"}, + {"id": 17199, "synset": "sunbather.n.01", "name": "sunbather"}, + {"id": 17200, "synset": "sundowner.n.01", "name": "sundowner"}, + {"id": 17201, "synset": "super_heavyweight.n.01", "name": "super_heavyweight"}, + {"id": 17202, "synset": "superior.n.01", "name": "superior"}, + {"id": 17203, "synset": "supermom.n.01", "name": "supermom"}, + {"id": 17204, "synset": "supernumerary.n.02", "name": "supernumerary"}, + {"id": 17205, "synset": "supremo.n.01", "name": "supremo"}, + {"id": 17206, "synset": "surgeon.n.01", "name": "surgeon"}, + {"id": 17207, "synset": "surgeon_general.n.02", "name": "Surgeon_General"}, + {"id": 17208, "synset": "surgeon_general.n.01", "name": "Surgeon_General"}, + {"id": 17209, "synset": "surpriser.n.01", "name": "surpriser"}, + {"id": 17210, "synset": "surveyor.n.01", "name": "surveyor"}, + {"id": 17211, "synset": "surveyor.n.02", "name": "surveyor"}, + {"id": 17212, "synset": "survivor.n.01", "name": "survivor"}, + {"id": 17213, "synset": "sutler.n.01", "name": "sutler"}, + {"id": 17214, "synset": "sweeper.n.01", "name": "sweeper"}, + {"id": 17215, "synset": "sweetheart.n.01", "name": "sweetheart"}, + {"id": 17216, "synset": "swinger.n.02", "name": "swinger"}, + {"id": 17217, "synset": "switcher.n.01", "name": "switcher"}, + {"id": 17218, "synset": "swot.n.01", "name": "swot"}, + {"id": 17219, "synset": "sycophant.n.01", "name": "sycophant"}, + {"id": 17220, "synset": "sylph.n.01", "name": "sylph"}, + {"id": 17221, "synset": "sympathizer.n.02", "name": "sympathizer"}, + {"id": 17222, "synset": "symphonist.n.01", "name": "symphonist"}, + {"id": 17223, "synset": "syncopator.n.01", "name": "syncopator"}, + {"id": 17224, "synset": "syndic.n.01", "name": "syndic"}, + {"id": 17225, "synset": "tactician.n.01", "name": "tactician"}, + {"id": 17226, "synset": "tagger.n.02", "name": "tagger"}, + {"id": 17227, "synset": "tailback.n.01", "name": "tailback"}, + {"id": 17228, "synset": "tallyman.n.02", "name": "tallyman"}, + {"id": 17229, "synset": "tallyman.n.01", "name": "tallyman"}, + {"id": 17230, "synset": "tanker.n.02", "name": "tanker"}, + {"id": 17231, "synset": "tapper.n.04", "name": "tapper"}, + {"id": 17232, "synset": "tartuffe.n.01", "name": "Tartuffe"}, + {"id": 17233, "synset": "tarzan.n.01", "name": "Tarzan"}, + {"id": 17234, "synset": "taster.n.01", "name": "taster"}, + {"id": 17235, "synset": "tax_assessor.n.01", "name": "tax_assessor"}, + {"id": 17236, "synset": "taxer.n.01", "name": "taxer"}, + {"id": 17237, "synset": "taxi_dancer.n.01", "name": "taxi_dancer"}, + {"id": 17238, "synset": "taxonomist.n.01", "name": "taxonomist"}, + {"id": 17239, "synset": "teacher.n.01", "name": "teacher"}, + {"id": 17240, "synset": "teaching_fellow.n.01", "name": "teaching_fellow"}, + {"id": 17241, "synset": "tearaway.n.01", "name": "tearaway"}, + {"id": 17242, "synset": "technical_sergeant.n.01", "name": "technical_sergeant"}, + {"id": 17243, "synset": "technician.n.02", "name": "technician"}, + {"id": 17244, "synset": "ted.n.01", "name": "Ted"}, + {"id": 17245, "synset": "teetotaler.n.01", "name": "teetotaler"}, + {"id": 17246, "synset": "television_reporter.n.01", "name": "television_reporter"}, + {"id": 17247, "synset": "temporizer.n.01", "name": "temporizer"}, + {"id": 17248, "synset": "tempter.n.01", "name": "tempter"}, + {"id": 17249, "synset": "term_infant.n.01", "name": "term_infant"}, + {"id": 17250, "synset": "toiler.n.01", "name": "toiler"}, + {"id": 17251, "synset": "tenant.n.01", "name": "tenant"}, + {"id": 17252, "synset": "tenant.n.02", "name": "tenant"}, + {"id": 17253, "synset": "tenderfoot.n.01", "name": "tenderfoot"}, + {"id": 17254, "synset": "tennis_player.n.01", "name": "tennis_player"}, + {"id": 17255, "synset": "tennis_pro.n.01", "name": "tennis_pro"}, + {"id": 17256, "synset": "tenor_saxophonist.n.01", "name": "tenor_saxophonist"}, + {"id": 17257, "synset": "termer.n.01", "name": "termer"}, + {"id": 17258, "synset": "terror.n.02", "name": "terror"}, + {"id": 17259, "synset": "tertigravida.n.01", "name": "tertigravida"}, + {"id": 17260, "synset": "testator.n.01", "name": "testator"}, + {"id": 17261, "synset": "testatrix.n.01", "name": "testatrix"}, + {"id": 17262, "synset": "testee.n.01", "name": "testee"}, + {"id": 17263, "synset": "test-tube_baby.n.01", "name": "test-tube_baby"}, + {"id": 17264, "synset": "texas_ranger.n.01", "name": "Texas_Ranger"}, + {"id": 17265, "synset": "thane.n.02", "name": "thane"}, + {"id": 17266, "synset": "theatrical_producer.n.01", "name": "theatrical_producer"}, + {"id": 17267, "synset": "theologian.n.01", "name": "theologian"}, + {"id": 17268, "synset": "theorist.n.01", "name": "theorist"}, + {"id": 17269, "synset": "theosophist.n.01", "name": "theosophist"}, + {"id": 17270, "synset": "therapist.n.01", "name": "therapist"}, + {"id": 17271, "synset": "thessalonian.n.01", "name": "Thessalonian"}, + {"id": 17272, "synset": "thinker.n.01", "name": "thinker"}, + {"id": 17273, "synset": "thinker.n.02", "name": "thinker"}, + {"id": 17274, "synset": "thrower.n.02", "name": "thrower"}, + {"id": 17275, "synset": "thurifer.n.01", "name": "thurifer"}, + {"id": 17276, "synset": "ticket_collector.n.01", "name": "ticket_collector"}, + {"id": 17277, "synset": "tight_end.n.01", "name": "tight_end"}, + {"id": 17278, "synset": "tiler.n.01", "name": "tiler"}, + {"id": 17279, "synset": "timekeeper.n.01", "name": "timekeeper"}, + {"id": 17280, "synset": "timorese.n.01", "name": "Timorese"}, + {"id": 17281, "synset": "tinkerer.n.01", "name": "tinkerer"}, + {"id": 17282, "synset": "tinsmith.n.01", "name": "tinsmith"}, + {"id": 17283, "synset": "tinter.n.01", "name": "tinter"}, + {"id": 17284, "synset": "tippler.n.01", "name": "tippler"}, + {"id": 17285, "synset": "tipster.n.01", "name": "tipster"}, + {"id": 17286, "synset": "t-man.n.01", "name": "T-man"}, + {"id": 17287, "synset": "toastmaster.n.01", "name": "toastmaster"}, + {"id": 17288, "synset": "toast_mistress.n.01", "name": "toast_mistress"}, + {"id": 17289, "synset": "tobogganist.n.01", "name": "tobogganist"}, + {"id": 17290, "synset": "tomboy.n.01", "name": "tomboy"}, + {"id": 17291, "synset": "toolmaker.n.01", "name": "toolmaker"}, + {"id": 17292, "synset": "torchbearer.n.01", "name": "torchbearer"}, + {"id": 17293, "synset": "tory.n.01", "name": "Tory"}, + {"id": 17294, "synset": "tory.n.02", "name": "Tory"}, + {"id": 17295, "synset": "tosser.n.02", "name": "tosser"}, + {"id": 17296, "synset": "tosser.n.01", "name": "tosser"}, + {"id": 17297, "synset": "totalitarian.n.01", "name": "totalitarian"}, + {"id": 17298, "synset": "tourist.n.01", "name": "tourist"}, + {"id": 17299, "synset": "tout.n.02", "name": "tout"}, + {"id": 17300, "synset": "tout.n.01", "name": "tout"}, + {"id": 17301, "synset": "tovarich.n.01", "name": "tovarich"}, + {"id": 17302, "synset": "towhead.n.01", "name": "towhead"}, + {"id": 17303, "synset": "town_clerk.n.01", "name": "town_clerk"}, + {"id": 17304, "synset": "town_crier.n.01", "name": "town_crier"}, + {"id": 17305, "synset": "townsman.n.02", "name": "townsman"}, + {"id": 17306, "synset": "toxicologist.n.01", "name": "toxicologist"}, + {"id": 17307, "synset": "track_star.n.01", "name": "track_star"}, + {"id": 17308, "synset": "trader.n.01", "name": "trader"}, + {"id": 17309, "synset": "trade_unionist.n.01", "name": "trade_unionist"}, + {"id": 17310, "synset": "traditionalist.n.01", "name": "traditionalist"}, + {"id": 17311, "synset": "traffic_cop.n.01", "name": "traffic_cop"}, + {"id": 17312, "synset": "tragedian.n.02", "name": "tragedian"}, + {"id": 17313, "synset": "tragedian.n.01", "name": "tragedian"}, + {"id": 17314, "synset": "tragedienne.n.01", "name": "tragedienne"}, + {"id": 17315, "synset": "trail_boss.n.01", "name": "trail_boss"}, + {"id": 17316, "synset": "trainer.n.01", "name": "trainer"}, + {"id": 17317, "synset": "traitor.n.01", "name": "traitor"}, + {"id": 17318, "synset": "traitress.n.01", "name": "traitress"}, + {"id": 17319, "synset": "transactor.n.01", "name": "transactor"}, + {"id": 17320, "synset": "transcriber.n.03", "name": "transcriber"}, + {"id": 17321, "synset": "transfer.n.02", "name": "transfer"}, + {"id": 17322, "synset": "transferee.n.01", "name": "transferee"}, + {"id": 17323, "synset": "translator.n.01", "name": "translator"}, + {"id": 17324, "synset": "transvestite.n.01", "name": "transvestite"}, + {"id": 17325, "synset": "traveling_salesman.n.01", "name": "traveling_salesman"}, + {"id": 17326, "synset": "traverser.n.01", "name": "traverser"}, + {"id": 17327, "synset": "trawler.n.01", "name": "trawler"}, + {"id": 17328, "synset": "treasury.n.04", "name": "Treasury"}, + {"id": 17329, "synset": "trencher.n.01", "name": "trencher"}, + {"id": 17330, "synset": "trend-setter.n.01", "name": "trend-setter"}, + {"id": 17331, "synset": "tribesman.n.01", "name": "tribesman"}, + {"id": 17332, "synset": "trier.n.02", "name": "trier"}, + {"id": 17333, "synset": "trifler.n.01", "name": "trifler"}, + {"id": 17334, "synset": "trooper.n.02", "name": "trooper"}, + {"id": 17335, "synset": "trooper.n.03", "name": "trooper"}, + {"id": 17336, "synset": "trotskyite.n.01", "name": "Trotskyite"}, + {"id": 17337, "synset": "truant.n.01", "name": "truant"}, + {"id": 17338, "synset": "trumpeter.n.01", "name": "trumpeter"}, + {"id": 17339, "synset": "trusty.n.01", "name": "trusty"}, + {"id": 17340, "synset": "tudor.n.03", "name": "Tudor"}, + {"id": 17341, "synset": "tumbler.n.01", "name": "tumbler"}, + {"id": 17342, "synset": "tutee.n.01", "name": "tutee"}, + {"id": 17343, "synset": "twin.n.01", "name": "twin"}, + {"id": 17344, "synset": "two-timer.n.01", "name": "two-timer"}, + {"id": 17345, "synset": "tyke.n.01", "name": "Tyke"}, + {"id": 17346, "synset": "tympanist.n.01", "name": "tympanist"}, + {"id": 17347, "synset": "typist.n.01", "name": "typist"}, + {"id": 17348, "synset": "tyrant.n.01", "name": "tyrant"}, + {"id": 17349, "synset": "umpire.n.01", "name": "umpire"}, + {"id": 17350, "synset": "understudy.n.01", "name": "understudy"}, + {"id": 17351, "synset": "undesirable.n.01", "name": "undesirable"}, + {"id": 17352, "synset": "unicyclist.n.01", "name": "unicyclist"}, + {"id": 17353, "synset": "unilateralist.n.01", "name": "unilateralist"}, + {"id": 17354, "synset": "unitarian.n.01", "name": "Unitarian"}, + {"id": 17355, "synset": "arminian.n.01", "name": "Arminian"}, + {"id": 17356, "synset": "universal_donor.n.01", "name": "universal_donor"}, + {"id": 17357, "synset": "unix_guru.n.01", "name": "UNIX_guru"}, + {"id": 17358, "synset": "unknown_soldier.n.01", "name": "Unknown_Soldier"}, + {"id": 17359, "synset": "upsetter.n.01", "name": "upsetter"}, + {"id": 17360, "synset": "upstager.n.01", "name": "upstager"}, + {"id": 17361, "synset": "upstart.n.02", "name": "upstart"}, + {"id": 17362, "synset": "upstart.n.01", "name": "upstart"}, + {"id": 17363, "synset": "urchin.n.01", "name": "urchin"}, + {"id": 17364, "synset": "urologist.n.01", "name": "urologist"}, + {"id": 17365, "synset": "usherette.n.01", "name": "usherette"}, + {"id": 17366, "synset": "usher.n.02", "name": "usher"}, + {"id": 17367, "synset": "usurper.n.01", "name": "usurper"}, + {"id": 17368, "synset": "utility_man.n.01", "name": "utility_man"}, + {"id": 17369, "synset": "utilizer.n.01", "name": "utilizer"}, + {"id": 17370, "synset": "utopian.n.01", "name": "Utopian"}, + {"id": 17371, "synset": "uxoricide.n.01", "name": "uxoricide"}, + {"id": 17372, "synset": "vacationer.n.01", "name": "vacationer"}, + {"id": 17373, "synset": "valedictorian.n.01", "name": "valedictorian"}, + {"id": 17374, "synset": "valley_girl.n.01", "name": "valley_girl"}, + {"id": 17375, "synset": "vaulter.n.01", "name": "vaulter"}, + {"id": 17376, "synset": "vegetarian.n.01", "name": "vegetarian"}, + {"id": 17377, "synset": "vegan.n.01", "name": "vegan"}, + {"id": 17378, "synset": "venerator.n.01", "name": "venerator"}, + {"id": 17379, "synset": "venture_capitalist.n.01", "name": "venture_capitalist"}, + {"id": 17380, "synset": "venturer.n.01", "name": "venturer"}, + {"id": 17381, "synset": "vermin.n.01", "name": "vermin"}, + {"id": 17382, "synset": "very_important_person.n.01", "name": "very_important_person"}, + {"id": 17383, "synset": "vibist.n.01", "name": "vibist"}, + {"id": 17384, "synset": "vicar.n.01", "name": "vicar"}, + {"id": 17385, "synset": "vicar.n.03", "name": "vicar"}, + {"id": 17386, "synset": "vicar-general.n.01", "name": "vicar-general"}, + {"id": 17387, "synset": "vice_chancellor.n.01", "name": "vice_chancellor"}, + {"id": 17388, "synset": "vicegerent.n.01", "name": "vicegerent"}, + {"id": 17389, "synset": "vice_president.n.01", "name": "vice_president"}, + {"id": 17390, "synset": "vice-regent.n.01", "name": "vice-regent"}, + {"id": 17391, "synset": "victim.n.02", "name": "victim"}, + {"id": 17392, "synset": "victorian.n.01", "name": "Victorian"}, + {"id": 17393, "synset": "victualer.n.01", "name": "victualer"}, + {"id": 17394, "synset": "vigilante.n.01", "name": "vigilante"}, + {"id": 17395, "synset": "villager.n.01", "name": "villager"}, + {"id": 17396, "synset": "vintager.n.01", "name": "vintager"}, + {"id": 17397, "synset": "vintner.n.01", "name": "vintner"}, + {"id": 17398, "synset": "violator.n.02", "name": "violator"}, + {"id": 17399, "synset": "violator.n.01", "name": "violator"}, + {"id": 17400, "synset": "violist.n.01", "name": "violist"}, + {"id": 17401, "synset": "virago.n.01", "name": "virago"}, + {"id": 17402, "synset": "virologist.n.01", "name": "virologist"}, + {"id": 17403, "synset": "visayan.n.01", "name": "Visayan"}, + {"id": 17404, "synset": "viscountess.n.01", "name": "viscountess"}, + {"id": 17405, "synset": "viscount.n.01", "name": "viscount"}, + {"id": 17406, "synset": "visigoth.n.01", "name": "Visigoth"}, + {"id": 17407, "synset": "visionary.n.01", "name": "visionary"}, + {"id": 17408, "synset": "visiting_fireman.n.01", "name": "visiting_fireman"}, + {"id": 17409, "synset": "visiting_professor.n.01", "name": "visiting_professor"}, + {"id": 17410, "synset": "visualizer.n.01", "name": "visualizer"}, + {"id": 17411, "synset": "vixen.n.01", "name": "vixen"}, + {"id": 17412, "synset": "vizier.n.01", "name": "vizier"}, + {"id": 17413, "synset": "voicer.n.01", "name": "voicer"}, + {"id": 17414, "synset": "volunteer.n.02", "name": "volunteer"}, + {"id": 17415, "synset": "volunteer.n.01", "name": "volunteer"}, + {"id": 17416, "synset": "votary.n.02", "name": "votary"}, + {"id": 17417, "synset": "votary.n.01", "name": "votary"}, + {"id": 17418, "synset": "vouchee.n.01", "name": "vouchee"}, + {"id": 17419, "synset": "vower.n.01", "name": "vower"}, + {"id": 17420, "synset": "voyager.n.01", "name": "voyager"}, + {"id": 17421, "synset": "voyeur.n.01", "name": "voyeur"}, + {"id": 17422, "synset": "vulcanizer.n.01", "name": "vulcanizer"}, + {"id": 17423, "synset": "waffler.n.01", "name": "waffler"}, + {"id": 17424, "synset": "wagnerian.n.01", "name": "Wagnerian"}, + {"id": 17425, "synset": "waif.n.01", "name": "waif"}, + {"id": 17426, "synset": "wailer.n.01", "name": "wailer"}, + {"id": 17427, "synset": "waiter.n.01", "name": "waiter"}, + {"id": 17428, "synset": "waitress.n.01", "name": "waitress"}, + {"id": 17429, "synset": "walking_delegate.n.01", "name": "walking_delegate"}, + {"id": 17430, "synset": "walk-on.n.01", "name": "walk-on"}, + {"id": 17431, "synset": "wallah.n.01", "name": "wallah"}, + {"id": 17432, "synset": "wally.n.01", "name": "wally"}, + {"id": 17433, "synset": "waltzer.n.01", "name": "waltzer"}, + {"id": 17434, "synset": "wanderer.n.01", "name": "wanderer"}, + {"id": 17435, "synset": "wandering_jew.n.01", "name": "Wandering_Jew"}, + {"id": 17436, "synset": "wanton.n.01", "name": "wanton"}, + {"id": 17437, "synset": "warrantee.n.02", "name": "warrantee"}, + {"id": 17438, "synset": "warrantee.n.01", "name": "warrantee"}, + {"id": 17439, "synset": "washer.n.01", "name": "washer"}, + {"id": 17440, "synset": "washerman.n.01", "name": "washerman"}, + {"id": 17441, "synset": "washwoman.n.01", "name": "washwoman"}, + {"id": 17442, "synset": "wassailer.n.01", "name": "wassailer"}, + {"id": 17443, "synset": "wastrel.n.01", "name": "wastrel"}, + {"id": 17444, "synset": "wave.n.09", "name": "Wave"}, + {"id": 17445, "synset": "weatherman.n.01", "name": "weatherman"}, + {"id": 17446, "synset": "weekend_warrior.n.02", "name": "weekend_warrior"}, + {"id": 17447, "synset": "weeder.n.01", "name": "weeder"}, + {"id": 17448, "synset": "welder.n.01", "name": "welder"}, + {"id": 17449, "synset": "welfare_case.n.01", "name": "welfare_case"}, + {"id": 17450, "synset": "westerner.n.01", "name": "westerner"}, + {"id": 17451, "synset": "west-sider.n.01", "name": "West-sider"}, + {"id": 17452, "synset": "wetter.n.02", "name": "wetter"}, + {"id": 17453, "synset": "whaler.n.01", "name": "whaler"}, + {"id": 17454, "synset": "whig.n.02", "name": "Whig"}, + {"id": 17455, "synset": "whiner.n.01", "name": "whiner"}, + {"id": 17456, "synset": "whipper-in.n.01", "name": "whipper-in"}, + {"id": 17457, "synset": "whisperer.n.01", "name": "whisperer"}, + {"id": 17458, "synset": "whiteface.n.02", "name": "whiteface"}, + {"id": 17459, "synset": "carmelite.n.01", "name": "Carmelite"}, + {"id": 17460, "synset": "augustinian.n.01", "name": "Augustinian"}, + {"id": 17461, "synset": "white_hope.n.01", "name": "white_hope"}, + {"id": 17462, "synset": "white_supremacist.n.01", "name": "white_supremacist"}, + {"id": 17463, "synset": "whoremaster.n.02", "name": "whoremaster"}, + {"id": 17464, "synset": "whoremaster.n.01", "name": "whoremaster"}, + {"id": 17465, "synset": "widow.n.01", "name": "widow"}, + {"id": 17466, "synset": "wife.n.01", "name": "wife"}, + {"id": 17467, "synset": "wiggler.n.01", "name": "wiggler"}, + {"id": 17468, "synset": "wimp.n.01", "name": "wimp"}, + {"id": 17469, "synset": "wing_commander.n.01", "name": "wing_commander"}, + {"id": 17470, "synset": "winger.n.01", "name": "winger"}, + {"id": 17471, "synset": "winner.n.02", "name": "winner"}, + {"id": 17472, "synset": "winner.n.01", "name": "winner"}, + {"id": 17473, "synset": "window_dresser.n.01", "name": "window_dresser"}, + {"id": 17474, "synset": "winker.n.01", "name": "winker"}, + {"id": 17475, "synset": "wiper.n.01", "name": "wiper"}, + {"id": 17476, "synset": "wireman.n.01", "name": "wireman"}, + {"id": 17477, "synset": "wise_guy.n.01", "name": "wise_guy"}, + {"id": 17478, "synset": "witch_doctor.n.01", "name": "witch_doctor"}, + {"id": 17479, "synset": "withdrawer.n.05", "name": "withdrawer"}, + {"id": 17480, "synset": "withdrawer.n.01", "name": "withdrawer"}, + {"id": 17481, "synset": "woman.n.01", "name": "woman"}, + {"id": 17482, "synset": "woman.n.02", "name": "woman"}, + {"id": 17483, "synset": "wonder_boy.n.01", "name": "wonder_boy"}, + {"id": 17484, "synset": "wonderer.n.01", "name": "wonderer"}, + {"id": 17485, "synset": "working_girl.n.01", "name": "working_girl"}, + {"id": 17486, "synset": "workman.n.01", "name": "workman"}, + {"id": 17487, "synset": "workmate.n.01", "name": "workmate"}, + {"id": 17488, "synset": "worldling.n.01", "name": "worldling"}, + {"id": 17489, "synset": "worshiper.n.01", "name": "worshiper"}, + {"id": 17490, "synset": "worthy.n.01", "name": "worthy"}, + {"id": 17491, "synset": "wrecker.n.01", "name": "wrecker"}, + {"id": 17492, "synset": "wright.n.07", "name": "wright"}, + {"id": 17493, "synset": "write-in_candidate.n.01", "name": "write-in_candidate"}, + {"id": 17494, "synset": "writer.n.01", "name": "writer"}, + {"id": 17495, "synset": "wykehamist.n.01", "name": "Wykehamist"}, + {"id": 17496, "synset": "yakuza.n.01", "name": "yakuza"}, + {"id": 17497, "synset": "yard_bird.n.01", "name": "yard_bird"}, + {"id": 17498, "synset": "yardie.n.01", "name": "yardie"}, + {"id": 17499, "synset": "yardman.n.01", "name": "yardman"}, + {"id": 17500, "synset": "yardmaster.n.01", "name": "yardmaster"}, + {"id": 17501, "synset": "yenta.n.02", "name": "yenta"}, + {"id": 17502, "synset": "yogi.n.02", "name": "yogi"}, + {"id": 17503, "synset": "young_buck.n.01", "name": "young_buck"}, + {"id": 17504, "synset": "young_turk.n.02", "name": "young_Turk"}, + {"id": 17505, "synset": "young_turk.n.01", "name": "Young_Turk"}, + {"id": 17506, "synset": "zionist.n.01", "name": "Zionist"}, + {"id": 17507, "synset": "zoo_keeper.n.01", "name": "zoo_keeper"}, + {"id": 17508, "synset": "genet.n.01", "name": "Genet"}, + {"id": 17509, "synset": "kennan.n.01", "name": "Kennan"}, + {"id": 17510, "synset": "munro.n.01", "name": "Munro"}, + {"id": 17511, "synset": "popper.n.01", "name": "Popper"}, + {"id": 17512, "synset": "stoker.n.01", "name": "Stoker"}, + {"id": 17513, "synset": "townes.n.01", "name": "Townes"}, + {"id": 17514, "synset": "dust_storm.n.01", "name": "dust_storm"}, + {"id": 17515, "synset": "parhelion.n.01", "name": "parhelion"}, + {"id": 17516, "synset": "snow.n.01", "name": "snow"}, + {"id": 17517, "synset": "facula.n.01", "name": "facula"}, + {"id": 17518, "synset": "wave.n.08", "name": "wave"}, + {"id": 17519, "synset": "microflora.n.01", "name": "microflora"}, + {"id": 17520, "synset": "wilding.n.01", "name": "wilding"}, + {"id": 17521, "synset": "semi-climber.n.01", "name": "semi-climber"}, + {"id": 17522, "synset": "volva.n.01", "name": "volva"}, + {"id": 17523, "synset": "basidiocarp.n.01", "name": "basidiocarp"}, + {"id": 17524, "synset": "domatium.n.01", "name": "domatium"}, + {"id": 17525, "synset": "apomict.n.01", "name": "apomict"}, + {"id": 17526, "synset": "aquatic.n.01", "name": "aquatic"}, + {"id": 17527, "synset": "bryophyte.n.01", "name": "bryophyte"}, + {"id": 17528, "synset": "acrocarp.n.01", "name": "acrocarp"}, + {"id": 17529, "synset": "sphagnum.n.01", "name": "sphagnum"}, + {"id": 17530, "synset": "liverwort.n.01", "name": "liverwort"}, + {"id": 17531, "synset": "hepatica.n.02", "name": "hepatica"}, + {"id": 17532, "synset": "pecopteris.n.01", "name": "pecopteris"}, + {"id": 17533, "synset": "pteridophyte.n.01", "name": "pteridophyte"}, + {"id": 17534, "synset": "fern.n.01", "name": "fern"}, + {"id": 17535, "synset": "fern_ally.n.01", "name": "fern_ally"}, + {"id": 17536, "synset": "spore.n.01", "name": "spore"}, + {"id": 17537, "synset": "carpospore.n.01", "name": "carpospore"}, + {"id": 17538, "synset": "chlamydospore.n.01", "name": "chlamydospore"}, + {"id": 17539, "synset": "conidium.n.01", "name": "conidium"}, + {"id": 17540, "synset": "oospore.n.01", "name": "oospore"}, + {"id": 17541, "synset": "tetraspore.n.01", "name": "tetraspore"}, + {"id": 17542, "synset": "zoospore.n.01", "name": "zoospore"}, + {"id": 17543, "synset": "cryptogam.n.01", "name": "cryptogam"}, + {"id": 17544, "synset": "spermatophyte.n.01", "name": "spermatophyte"}, + {"id": 17545, "synset": "seedling.n.01", "name": "seedling"}, + {"id": 17546, "synset": "annual.n.01", "name": "annual"}, + {"id": 17547, "synset": "biennial.n.01", "name": "biennial"}, + {"id": 17548, "synset": "perennial.n.01", "name": "perennial"}, + {"id": 17549, "synset": "hygrophyte.n.01", "name": "hygrophyte"}, + {"id": 17550, "synset": "gymnosperm.n.01", "name": "gymnosperm"}, + {"id": 17551, "synset": "gnetum.n.01", "name": "gnetum"}, + {"id": 17552, "synset": "catha_edulis.n.01", "name": "Catha_edulis"}, + {"id": 17553, "synset": "ephedra.n.01", "name": "ephedra"}, + {"id": 17554, "synset": "mahuang.n.01", "name": "mahuang"}, + {"id": 17555, "synset": "welwitschia.n.01", "name": "welwitschia"}, + {"id": 17556, "synset": "cycad.n.01", "name": "cycad"}, + {"id": 17557, "synset": "sago_palm.n.02", "name": "sago_palm"}, + {"id": 17558, "synset": "false_sago.n.01", "name": "false_sago"}, + {"id": 17559, "synset": "zamia.n.01", "name": "zamia"}, + {"id": 17560, "synset": "coontie.n.01", "name": "coontie"}, + {"id": 17561, "synset": "ceratozamia.n.01", "name": "ceratozamia"}, + {"id": 17562, "synset": "dioon.n.01", "name": "dioon"}, + {"id": 17563, "synset": "encephalartos.n.01", "name": "encephalartos"}, + {"id": 17564, "synset": "kaffir_bread.n.01", "name": "kaffir_bread"}, + {"id": 17565, "synset": "macrozamia.n.01", "name": "macrozamia"}, + {"id": 17566, "synset": "burrawong.n.01", "name": "burrawong"}, + {"id": 17567, "synset": "pine.n.01", "name": "pine"}, + {"id": 17568, "synset": "pinon.n.01", "name": "pinon"}, + {"id": 17569, "synset": "nut_pine.n.01", "name": "nut_pine"}, + {"id": 17570, "synset": "pinon_pine.n.01", "name": "pinon_pine"}, + {"id": 17571, "synset": "rocky_mountain_pinon.n.01", "name": "Rocky_mountain_pinon"}, + {"id": 17572, "synset": "single-leaf.n.01", "name": "single-leaf"}, + {"id": 17573, "synset": "bishop_pine.n.01", "name": "bishop_pine"}, + { + "id": 17574, + "synset": "california_single-leaf_pinyon.n.01", + "name": "California_single-leaf_pinyon", + }, + {"id": 17575, "synset": "parry's_pinyon.n.01", "name": "Parry's_pinyon"}, + {"id": 17576, "synset": "spruce_pine.n.04", "name": "spruce_pine"}, + {"id": 17577, "synset": "black_pine.n.05", "name": "black_pine"}, + {"id": 17578, "synset": "pitch_pine.n.02", "name": "pitch_pine"}, + {"id": 17579, "synset": "pond_pine.n.01", "name": "pond_pine"}, + {"id": 17580, "synset": "stone_pine.n.01", "name": "stone_pine"}, + {"id": 17581, "synset": "swiss_pine.n.01", "name": "Swiss_pine"}, + {"id": 17582, "synset": "cembra_nut.n.01", "name": "cembra_nut"}, + {"id": 17583, "synset": "swiss_mountain_pine.n.01", "name": "Swiss_mountain_pine"}, + {"id": 17584, "synset": "ancient_pine.n.01", "name": "ancient_pine"}, + {"id": 17585, "synset": "white_pine.n.01", "name": "white_pine"}, + {"id": 17586, "synset": "american_white_pine.n.01", "name": "American_white_pine"}, + {"id": 17587, "synset": "western_white_pine.n.01", "name": "western_white_pine"}, + {"id": 17588, "synset": "southwestern_white_pine.n.01", "name": "southwestern_white_pine"}, + {"id": 17589, "synset": "limber_pine.n.01", "name": "limber_pine"}, + {"id": 17590, "synset": "whitebark_pine.n.01", "name": "whitebark_pine"}, + {"id": 17591, "synset": "yellow_pine.n.01", "name": "yellow_pine"}, + {"id": 17592, "synset": "ponderosa.n.01", "name": "ponderosa"}, + {"id": 17593, "synset": "jeffrey_pine.n.01", "name": "Jeffrey_pine"}, + {"id": 17594, "synset": "shore_pine.n.01", "name": "shore_pine"}, + {"id": 17595, "synset": "sierra_lodgepole_pine.n.01", "name": "Sierra_lodgepole_pine"}, + {"id": 17596, "synset": "loblolly_pine.n.01", "name": "loblolly_pine"}, + {"id": 17597, "synset": "jack_pine.n.01", "name": "jack_pine"}, + {"id": 17598, "synset": "swamp_pine.n.01", "name": "swamp_pine"}, + {"id": 17599, "synset": "longleaf_pine.n.01", "name": "longleaf_pine"}, + {"id": 17600, "synset": "shortleaf_pine.n.01", "name": "shortleaf_pine"}, + {"id": 17601, "synset": "red_pine.n.02", "name": "red_pine"}, + {"id": 17602, "synset": "scotch_pine.n.01", "name": "Scotch_pine"}, + {"id": 17603, "synset": "scrub_pine.n.01", "name": "scrub_pine"}, + {"id": 17604, "synset": "monterey_pine.n.01", "name": "Monterey_pine"}, + {"id": 17605, "synset": "bristlecone_pine.n.01", "name": "bristlecone_pine"}, + {"id": 17606, "synset": "table-mountain_pine.n.01", "name": "table-mountain_pine"}, + {"id": 17607, "synset": "knobcone_pine.n.01", "name": "knobcone_pine"}, + {"id": 17608, "synset": "japanese_red_pine.n.01", "name": "Japanese_red_pine"}, + {"id": 17609, "synset": "japanese_black_pine.n.01", "name": "Japanese_black_pine"}, + {"id": 17610, "synset": "torrey_pine.n.01", "name": "Torrey_pine"}, + {"id": 17611, "synset": "larch.n.02", "name": "larch"}, + {"id": 17612, "synset": "american_larch.n.01", "name": "American_larch"}, + {"id": 17613, "synset": "western_larch.n.01", "name": "western_larch"}, + {"id": 17614, "synset": "subalpine_larch.n.01", "name": "subalpine_larch"}, + {"id": 17615, "synset": "european_larch.n.01", "name": "European_larch"}, + {"id": 17616, "synset": "siberian_larch.n.01", "name": "Siberian_larch"}, + {"id": 17617, "synset": "golden_larch.n.01", "name": "golden_larch"}, + {"id": 17618, "synset": "fir.n.02", "name": "fir"}, + {"id": 17619, "synset": "silver_fir.n.01", "name": "silver_fir"}, + {"id": 17620, "synset": "amabilis_fir.n.01", "name": "amabilis_fir"}, + {"id": 17621, "synset": "european_silver_fir.n.01", "name": "European_silver_fir"}, + {"id": 17622, "synset": "white_fir.n.01", "name": "white_fir"}, + {"id": 17623, "synset": "balsam_fir.n.01", "name": "balsam_fir"}, + {"id": 17624, "synset": "fraser_fir.n.01", "name": "Fraser_fir"}, + {"id": 17625, "synset": "lowland_fir.n.01", "name": "lowland_fir"}, + {"id": 17626, "synset": "alpine_fir.n.01", "name": "Alpine_fir"}, + {"id": 17627, "synset": "santa_lucia_fir.n.01", "name": "Santa_Lucia_fir"}, + {"id": 17628, "synset": "cedar.n.03", "name": "cedar"}, + {"id": 17629, "synset": "cedar_of_lebanon.n.01", "name": "cedar_of_Lebanon"}, + {"id": 17630, "synset": "deodar.n.01", "name": "deodar"}, + {"id": 17631, "synset": "atlas_cedar.n.01", "name": "Atlas_cedar"}, + {"id": 17632, "synset": "spruce.n.02", "name": "spruce"}, + {"id": 17633, "synset": "norway_spruce.n.01", "name": "Norway_spruce"}, + {"id": 17634, "synset": "weeping_spruce.n.01", "name": "weeping_spruce"}, + {"id": 17635, "synset": "engelmann_spruce.n.01", "name": "Engelmann_spruce"}, + {"id": 17636, "synset": "white_spruce.n.01", "name": "white_spruce"}, + {"id": 17637, "synset": "black_spruce.n.01", "name": "black_spruce"}, + {"id": 17638, "synset": "siberian_spruce.n.01", "name": "Siberian_spruce"}, + {"id": 17639, "synset": "sitka_spruce.n.01", "name": "Sitka_spruce"}, + {"id": 17640, "synset": "oriental_spruce.n.01", "name": "oriental_spruce"}, + {"id": 17641, "synset": "colorado_spruce.n.01", "name": "Colorado_spruce"}, + {"id": 17642, "synset": "red_spruce.n.01", "name": "red_spruce"}, + {"id": 17643, "synset": "hemlock.n.04", "name": "hemlock"}, + {"id": 17644, "synset": "eastern_hemlock.n.01", "name": "eastern_hemlock"}, + {"id": 17645, "synset": "carolina_hemlock.n.01", "name": "Carolina_hemlock"}, + {"id": 17646, "synset": "mountain_hemlock.n.01", "name": "mountain_hemlock"}, + {"id": 17647, "synset": "western_hemlock.n.01", "name": "western_hemlock"}, + {"id": 17648, "synset": "douglas_fir.n.02", "name": "douglas_fir"}, + {"id": 17649, "synset": "green_douglas_fir.n.01", "name": "green_douglas_fir"}, + {"id": 17650, "synset": "big-cone_spruce.n.01", "name": "big-cone_spruce"}, + {"id": 17651, "synset": "cathaya.n.01", "name": "Cathaya"}, + {"id": 17652, "synset": "cedar.n.01", "name": "cedar"}, + {"id": 17653, "synset": "cypress.n.02", "name": "cypress"}, + {"id": 17654, "synset": "gowen_cypress.n.01", "name": "gowen_cypress"}, + {"id": 17655, "synset": "pygmy_cypress.n.01", "name": "pygmy_cypress"}, + {"id": 17656, "synset": "santa_cruz_cypress.n.01", "name": "Santa_Cruz_cypress"}, + {"id": 17657, "synset": "arizona_cypress.n.01", "name": "Arizona_cypress"}, + {"id": 17658, "synset": "guadalupe_cypress.n.01", "name": "Guadalupe_cypress"}, + {"id": 17659, "synset": "monterey_cypress.n.01", "name": "Monterey_cypress"}, + {"id": 17660, "synset": "mexican_cypress.n.01", "name": "Mexican_cypress"}, + {"id": 17661, "synset": "italian_cypress.n.01", "name": "Italian_cypress"}, + {"id": 17662, "synset": "king_william_pine.n.01", "name": "King_William_pine"}, + {"id": 17663, "synset": "chilean_cedar.n.01", "name": "Chilean_cedar"}, + {"id": 17664, "synset": "incense_cedar.n.02", "name": "incense_cedar"}, + {"id": 17665, "synset": "southern_white_cedar.n.01", "name": "southern_white_cedar"}, + {"id": 17666, "synset": "oregon_cedar.n.01", "name": "Oregon_cedar"}, + {"id": 17667, "synset": "yellow_cypress.n.01", "name": "yellow_cypress"}, + {"id": 17668, "synset": "japanese_cedar.n.01", "name": "Japanese_cedar"}, + {"id": 17669, "synset": "juniper_berry.n.01", "name": "juniper_berry"}, + {"id": 17670, "synset": "incense_cedar.n.01", "name": "incense_cedar"}, + {"id": 17671, "synset": "kawaka.n.01", "name": "kawaka"}, + {"id": 17672, "synset": "pahautea.n.01", "name": "pahautea"}, + {"id": 17673, "synset": "metasequoia.n.01", "name": "metasequoia"}, + {"id": 17674, "synset": "arborvitae.n.01", "name": "arborvitae"}, + {"id": 17675, "synset": "western_red_cedar.n.01", "name": "western_red_cedar"}, + {"id": 17676, "synset": "american_arborvitae.n.01", "name": "American_arborvitae"}, + {"id": 17677, "synset": "oriental_arborvitae.n.01", "name": "Oriental_arborvitae"}, + {"id": 17678, "synset": "hiba_arborvitae.n.01", "name": "hiba_arborvitae"}, + {"id": 17679, "synset": "keteleeria.n.01", "name": "keteleeria"}, + {"id": 17680, "synset": "wollemi_pine.n.01", "name": "Wollemi_pine"}, + {"id": 17681, "synset": "araucaria.n.01", "name": "araucaria"}, + {"id": 17682, "synset": "monkey_puzzle.n.01", "name": "monkey_puzzle"}, + {"id": 17683, "synset": "norfolk_island_pine.n.01", "name": "norfolk_island_pine"}, + {"id": 17684, "synset": "new_caledonian_pine.n.01", "name": "new_caledonian_pine"}, + {"id": 17685, "synset": "bunya_bunya.n.01", "name": "bunya_bunya"}, + {"id": 17686, "synset": "hoop_pine.n.01", "name": "hoop_pine"}, + {"id": 17687, "synset": "kauri_pine.n.01", "name": "kauri_pine"}, + {"id": 17688, "synset": "kauri.n.02", "name": "kauri"}, + {"id": 17689, "synset": "amboina_pine.n.01", "name": "amboina_pine"}, + {"id": 17690, "synset": "dundathu_pine.n.01", "name": "dundathu_pine"}, + {"id": 17691, "synset": "red_kauri.n.01", "name": "red_kauri"}, + {"id": 17692, "synset": "plum-yew.n.01", "name": "plum-yew"}, + {"id": 17693, "synset": "california_nutmeg.n.01", "name": "California_nutmeg"}, + {"id": 17694, "synset": "stinking_cedar.n.01", "name": "stinking_cedar"}, + {"id": 17695, "synset": "celery_pine.n.01", "name": "celery_pine"}, + {"id": 17696, "synset": "celery_top_pine.n.01", "name": "celery_top_pine"}, + {"id": 17697, "synset": "tanekaha.n.01", "name": "tanekaha"}, + {"id": 17698, "synset": "alpine_celery_pine.n.01", "name": "Alpine_celery_pine"}, + {"id": 17699, "synset": "yellowwood.n.02", "name": "yellowwood"}, + {"id": 17700, "synset": "gymnospermous_yellowwood.n.01", "name": "gymnospermous_yellowwood"}, + {"id": 17701, "synset": "podocarp.n.01", "name": "podocarp"}, + {"id": 17702, "synset": "yacca.n.01", "name": "yacca"}, + {"id": 17703, "synset": "brown_pine.n.01", "name": "brown_pine"}, + {"id": 17704, "synset": "cape_yellowwood.n.01", "name": "cape_yellowwood"}, + {"id": 17705, "synset": "south-african_yellowwood.n.01", "name": "South-African_yellowwood"}, + {"id": 17706, "synset": "alpine_totara.n.01", "name": "alpine_totara"}, + {"id": 17707, "synset": "totara.n.01", "name": "totara"}, + {"id": 17708, "synset": "common_yellowwood.n.01", "name": "common_yellowwood"}, + {"id": 17709, "synset": "kahikatea.n.01", "name": "kahikatea"}, + {"id": 17710, "synset": "rimu.n.01", "name": "rimu"}, + {"id": 17711, "synset": "tarwood.n.02", "name": "tarwood"}, + {"id": 17712, "synset": "common_sickle_pine.n.01", "name": "common_sickle_pine"}, + {"id": 17713, "synset": "yellow-leaf_sickle_pine.n.01", "name": "yellow-leaf_sickle_pine"}, + {"id": 17714, "synset": "tarwood.n.01", "name": "tarwood"}, + {"id": 17715, "synset": "westland_pine.n.01", "name": "westland_pine"}, + {"id": 17716, "synset": "huon_pine.n.01", "name": "huon_pine"}, + {"id": 17717, "synset": "chilean_rimu.n.01", "name": "Chilean_rimu"}, + {"id": 17718, "synset": "mountain_rimu.n.01", "name": "mountain_rimu"}, + {"id": 17719, "synset": "nagi.n.01", "name": "nagi"}, + {"id": 17720, "synset": "miro.n.01", "name": "miro"}, + {"id": 17721, "synset": "matai.n.01", "name": "matai"}, + {"id": 17722, "synset": "plum-fruited_yew.n.01", "name": "plum-fruited_yew"}, + {"id": 17723, "synset": "prince_albert_yew.n.01", "name": "Prince_Albert_yew"}, + {"id": 17724, "synset": "sundacarpus_amara.n.01", "name": "Sundacarpus_amara"}, + {"id": 17725, "synset": "japanese_umbrella_pine.n.01", "name": "Japanese_umbrella_pine"}, + {"id": 17726, "synset": "yew.n.02", "name": "yew"}, + {"id": 17727, "synset": "old_world_yew.n.01", "name": "Old_World_yew"}, + {"id": 17728, "synset": "pacific_yew.n.01", "name": "Pacific_yew"}, + {"id": 17729, "synset": "japanese_yew.n.01", "name": "Japanese_yew"}, + {"id": 17730, "synset": "florida_yew.n.01", "name": "Florida_yew"}, + {"id": 17731, "synset": "new_caledonian_yew.n.01", "name": "New_Caledonian_yew"}, + {"id": 17732, "synset": "white-berry_yew.n.01", "name": "white-berry_yew"}, + {"id": 17733, "synset": "ginkgo.n.01", "name": "ginkgo"}, + {"id": 17734, "synset": "angiosperm.n.01", "name": "angiosperm"}, + {"id": 17735, "synset": "dicot.n.01", "name": "dicot"}, + {"id": 17736, "synset": "monocot.n.01", "name": "monocot"}, + {"id": 17737, "synset": "floret.n.01", "name": "floret"}, + {"id": 17738, "synset": "flower.n.01", "name": "flower"}, + {"id": 17739, "synset": "bloomer.n.01", "name": "bloomer"}, + {"id": 17740, "synset": "wildflower.n.01", "name": "wildflower"}, + {"id": 17741, "synset": "apetalous_flower.n.01", "name": "apetalous_flower"}, + {"id": 17742, "synset": "inflorescence.n.02", "name": "inflorescence"}, + {"id": 17743, "synset": "rosebud.n.01", "name": "rosebud"}, + {"id": 17744, "synset": "gynostegium.n.01", "name": "gynostegium"}, + {"id": 17745, "synset": "pollinium.n.01", "name": "pollinium"}, + {"id": 17746, "synset": "pistil.n.01", "name": "pistil"}, + {"id": 17747, "synset": "gynobase.n.01", "name": "gynobase"}, + {"id": 17748, "synset": "gynophore.n.01", "name": "gynophore"}, + {"id": 17749, "synset": "stylopodium.n.01", "name": "stylopodium"}, + {"id": 17750, "synset": "carpophore.n.01", "name": "carpophore"}, + {"id": 17751, "synset": "cornstalk.n.01", "name": "cornstalk"}, + {"id": 17752, "synset": "petiolule.n.01", "name": "petiolule"}, + {"id": 17753, "synset": "mericarp.n.01", "name": "mericarp"}, + {"id": 17754, "synset": "micropyle.n.01", "name": "micropyle"}, + {"id": 17755, "synset": "germ_tube.n.01", "name": "germ_tube"}, + {"id": 17756, "synset": "pollen_tube.n.01", "name": "pollen_tube"}, + {"id": 17757, "synset": "gemma.n.01", "name": "gemma"}, + {"id": 17758, "synset": "galbulus.n.01", "name": "galbulus"}, + {"id": 17759, "synset": "nectary.n.01", "name": "nectary"}, + {"id": 17760, "synset": "pericarp.n.01", "name": "pericarp"}, + {"id": 17761, "synset": "epicarp.n.01", "name": "epicarp"}, + {"id": 17762, "synset": "mesocarp.n.01", "name": "mesocarp"}, + {"id": 17763, "synset": "pip.n.03", "name": "pip"}, + {"id": 17764, "synset": "silique.n.01", "name": "silique"}, + {"id": 17765, "synset": "cataphyll.n.01", "name": "cataphyll"}, + {"id": 17766, "synset": "perisperm.n.01", "name": "perisperm"}, + {"id": 17767, "synset": "monocarp.n.01", "name": "monocarp"}, + {"id": 17768, "synset": "sporophyte.n.01", "name": "sporophyte"}, + {"id": 17769, "synset": "gametophyte.n.01", "name": "gametophyte"}, + {"id": 17770, "synset": "megasporangium.n.01", "name": "megasporangium"}, + {"id": 17771, "synset": "microspore.n.01", "name": "microspore"}, + {"id": 17772, "synset": "microsporangium.n.01", "name": "microsporangium"}, + {"id": 17773, "synset": "microsporophyll.n.01", "name": "microsporophyll"}, + {"id": 17774, "synset": "archespore.n.01", "name": "archespore"}, + {"id": 17775, "synset": "bonduc_nut.n.01", "name": "bonduc_nut"}, + {"id": 17776, "synset": "job's_tears.n.01", "name": "Job's_tears"}, + {"id": 17777, "synset": "oilseed.n.01", "name": "oilseed"}, + {"id": 17778, "synset": "castor_bean.n.01", "name": "castor_bean"}, + {"id": 17779, "synset": "cottonseed.n.01", "name": "cottonseed"}, + {"id": 17780, "synset": "candlenut.n.02", "name": "candlenut"}, + {"id": 17781, "synset": "peach_pit.n.01", "name": "peach_pit"}, + {"id": 17782, "synset": "hypanthium.n.01", "name": "hypanthium"}, + {"id": 17783, "synset": "petal.n.01", "name": "petal"}, + {"id": 17784, "synset": "corolla.n.01", "name": "corolla"}, + {"id": 17785, "synset": "lip.n.02", "name": "lip"}, + {"id": 17786, "synset": "perianth.n.01", "name": "perianth"}, + {"id": 17787, "synset": "thistledown.n.01", "name": "thistledown"}, + {"id": 17788, "synset": "custard_apple.n.01", "name": "custard_apple"}, + {"id": 17789, "synset": "cherimoya.n.01", "name": "cherimoya"}, + {"id": 17790, "synset": "ilama.n.01", "name": "ilama"}, + {"id": 17791, "synset": "soursop.n.01", "name": "soursop"}, + {"id": 17792, "synset": "bullock's_heart.n.01", "name": "bullock's_heart"}, + {"id": 17793, "synset": "sweetsop.n.01", "name": "sweetsop"}, + {"id": 17794, "synset": "pond_apple.n.01", "name": "pond_apple"}, + {"id": 17795, "synset": "pawpaw.n.02", "name": "pawpaw"}, + {"id": 17796, "synset": "ilang-ilang.n.02", "name": "ilang-ilang"}, + {"id": 17797, "synset": "lancewood.n.02", "name": "lancewood"}, + {"id": 17798, "synset": "guinea_pepper.n.02", "name": "Guinea_pepper"}, + {"id": 17799, "synset": "barberry.n.01", "name": "barberry"}, + {"id": 17800, "synset": "american_barberry.n.01", "name": "American_barberry"}, + {"id": 17801, "synset": "common_barberry.n.01", "name": "common_barberry"}, + {"id": 17802, "synset": "japanese_barberry.n.01", "name": "Japanese_barberry"}, + {"id": 17803, "synset": "oregon_grape.n.02", "name": "Oregon_grape"}, + {"id": 17804, "synset": "oregon_grape.n.01", "name": "Oregon_grape"}, + {"id": 17805, "synset": "mayapple.n.01", "name": "mayapple"}, + {"id": 17806, "synset": "may_apple.n.01", "name": "May_apple"}, + {"id": 17807, "synset": "allspice.n.02", "name": "allspice"}, + {"id": 17808, "synset": "carolina_allspice.n.01", "name": "Carolina_allspice"}, + {"id": 17809, "synset": "spicebush.n.02", "name": "spicebush"}, + {"id": 17810, "synset": "katsura_tree.n.01", "name": "katsura_tree"}, + {"id": 17811, "synset": "laurel.n.01", "name": "laurel"}, + {"id": 17812, "synset": "true_laurel.n.01", "name": "true_laurel"}, + {"id": 17813, "synset": "camphor_tree.n.01", "name": "camphor_tree"}, + {"id": 17814, "synset": "cinnamon.n.02", "name": "cinnamon"}, + {"id": 17815, "synset": "cassia.n.03", "name": "cassia"}, + {"id": 17816, "synset": "cassia_bark.n.01", "name": "cassia_bark"}, + {"id": 17817, "synset": "saigon_cinnamon.n.01", "name": "Saigon_cinnamon"}, + {"id": 17818, "synset": "cinnamon_bark.n.01", "name": "cinnamon_bark"}, + {"id": 17819, "synset": "spicebush.n.01", "name": "spicebush"}, + {"id": 17820, "synset": "avocado.n.02", "name": "avocado"}, + {"id": 17821, "synset": "laurel-tree.n.01", "name": "laurel-tree"}, + {"id": 17822, "synset": "sassafras.n.01", "name": "sassafras"}, + {"id": 17823, "synset": "california_laurel.n.01", "name": "California_laurel"}, + {"id": 17824, "synset": "anise_tree.n.01", "name": "anise_tree"}, + {"id": 17825, "synset": "purple_anise.n.01", "name": "purple_anise"}, + {"id": 17826, "synset": "star_anise.n.02", "name": "star_anise"}, + {"id": 17827, "synset": "star_anise.n.01", "name": "star_anise"}, + {"id": 17828, "synset": "magnolia.n.02", "name": "magnolia"}, + {"id": 17829, "synset": "southern_magnolia.n.01", "name": "southern_magnolia"}, + {"id": 17830, "synset": "umbrella_tree.n.02", "name": "umbrella_tree"}, + {"id": 17831, "synset": "earleaved_umbrella_tree.n.01", "name": "earleaved_umbrella_tree"}, + {"id": 17832, "synset": "cucumber_tree.n.01", "name": "cucumber_tree"}, + {"id": 17833, "synset": "large-leaved_magnolia.n.01", "name": "large-leaved_magnolia"}, + {"id": 17834, "synset": "saucer_magnolia.n.01", "name": "saucer_magnolia"}, + {"id": 17835, "synset": "star_magnolia.n.01", "name": "star_magnolia"}, + {"id": 17836, "synset": "sweet_bay.n.01", "name": "sweet_bay"}, + {"id": 17837, "synset": "manglietia.n.01", "name": "manglietia"}, + {"id": 17838, "synset": "tulip_tree.n.01", "name": "tulip_tree"}, + {"id": 17839, "synset": "moonseed.n.01", "name": "moonseed"}, + {"id": 17840, "synset": "common_moonseed.n.01", "name": "common_moonseed"}, + {"id": 17841, "synset": "carolina_moonseed.n.01", "name": "Carolina_moonseed"}, + {"id": 17842, "synset": "nutmeg.n.01", "name": "nutmeg"}, + {"id": 17843, "synset": "water_nymph.n.02", "name": "water_nymph"}, + {"id": 17844, "synset": "european_white_lily.n.01", "name": "European_white_lily"}, + {"id": 17845, "synset": "southern_spatterdock.n.01", "name": "southern_spatterdock"}, + {"id": 17846, "synset": "lotus.n.01", "name": "lotus"}, + {"id": 17847, "synset": "water_chinquapin.n.01", "name": "water_chinquapin"}, + {"id": 17848, "synset": "water-shield.n.02", "name": "water-shield"}, + {"id": 17849, "synset": "water-shield.n.01", "name": "water-shield"}, + {"id": 17850, "synset": "peony.n.01", "name": "peony"}, + {"id": 17851, "synset": "buttercup.n.01", "name": "buttercup"}, + {"id": 17852, "synset": "meadow_buttercup.n.01", "name": "meadow_buttercup"}, + {"id": 17853, "synset": "water_crowfoot.n.01", "name": "water_crowfoot"}, + {"id": 17854, "synset": "lesser_celandine.n.01", "name": "lesser_celandine"}, + {"id": 17855, "synset": "lesser_spearwort.n.01", "name": "lesser_spearwort"}, + {"id": 17856, "synset": "greater_spearwort.n.01", "name": "greater_spearwort"}, + {"id": 17857, "synset": "western_buttercup.n.01", "name": "western_buttercup"}, + {"id": 17858, "synset": "creeping_buttercup.n.01", "name": "creeping_buttercup"}, + {"id": 17859, "synset": "cursed_crowfoot.n.01", "name": "cursed_crowfoot"}, + {"id": 17860, "synset": "aconite.n.01", "name": "aconite"}, + {"id": 17861, "synset": "monkshood.n.01", "name": "monkshood"}, + {"id": 17862, "synset": "wolfsbane.n.01", "name": "wolfsbane"}, + {"id": 17863, "synset": "baneberry.n.02", "name": "baneberry"}, + {"id": 17864, "synset": "baneberry.n.01", "name": "baneberry"}, + {"id": 17865, "synset": "red_baneberry.n.01", "name": "red_baneberry"}, + {"id": 17866, "synset": "pheasant's-eye.n.01", "name": "pheasant's-eye"}, + {"id": 17867, "synset": "anemone.n.01", "name": "anemone"}, + {"id": 17868, "synset": "alpine_anemone.n.01", "name": "Alpine_anemone"}, + {"id": 17869, "synset": "canada_anemone.n.01", "name": "Canada_anemone"}, + {"id": 17870, "synset": "thimbleweed.n.01", "name": "thimbleweed"}, + {"id": 17871, "synset": "wood_anemone.n.02", "name": "wood_anemone"}, + {"id": 17872, "synset": "wood_anemone.n.01", "name": "wood_anemone"}, + {"id": 17873, "synset": "longheaded_thimbleweed.n.01", "name": "longheaded_thimbleweed"}, + {"id": 17874, "synset": "snowdrop_anemone.n.01", "name": "snowdrop_anemone"}, + {"id": 17875, "synset": "virginia_thimbleweed.n.01", "name": "Virginia_thimbleweed"}, + {"id": 17876, "synset": "rue_anemone.n.01", "name": "rue_anemone"}, + {"id": 17877, "synset": "columbine.n.01", "name": "columbine"}, + {"id": 17878, "synset": "meeting_house.n.01", "name": "meeting_house"}, + {"id": 17879, "synset": "blue_columbine.n.01", "name": "blue_columbine"}, + {"id": 17880, "synset": "granny's_bonnets.n.01", "name": "granny's_bonnets"}, + {"id": 17881, "synset": "marsh_marigold.n.01", "name": "marsh_marigold"}, + {"id": 17882, "synset": "american_bugbane.n.01", "name": "American_bugbane"}, + {"id": 17883, "synset": "black_cohosh.n.01", "name": "black_cohosh"}, + {"id": 17884, "synset": "fetid_bugbane.n.01", "name": "fetid_bugbane"}, + {"id": 17885, "synset": "clematis.n.01", "name": "clematis"}, + {"id": 17886, "synset": "pine_hyacinth.n.01", "name": "pine_hyacinth"}, + {"id": 17887, "synset": "blue_jasmine.n.01", "name": "blue_jasmine"}, + {"id": 17888, "synset": "golden_clematis.n.01", "name": "golden_clematis"}, + {"id": 17889, "synset": "scarlet_clematis.n.01", "name": "scarlet_clematis"}, + {"id": 17890, "synset": "leather_flower.n.02", "name": "leather_flower"}, + {"id": 17891, "synset": "leather_flower.n.01", "name": "leather_flower"}, + {"id": 17892, "synset": "virgin's_bower.n.01", "name": "virgin's_bower"}, + {"id": 17893, "synset": "purple_clematis.n.01", "name": "purple_clematis"}, + {"id": 17894, "synset": "goldthread.n.01", "name": "goldthread"}, + {"id": 17895, "synset": "rocket_larkspur.n.01", "name": "rocket_larkspur"}, + {"id": 17896, "synset": "delphinium.n.01", "name": "delphinium"}, + {"id": 17897, "synset": "larkspur.n.01", "name": "larkspur"}, + {"id": 17898, "synset": "winter_aconite.n.01", "name": "winter_aconite"}, + {"id": 17899, "synset": "lenten_rose.n.01", "name": "lenten_rose"}, + {"id": 17900, "synset": "green_hellebore.n.01", "name": "green_hellebore"}, + {"id": 17901, "synset": "hepatica.n.01", "name": "hepatica"}, + {"id": 17902, "synset": "goldenseal.n.01", "name": "goldenseal"}, + {"id": 17903, "synset": "false_rue_anemone.n.01", "name": "false_rue_anemone"}, + {"id": 17904, "synset": "giant_buttercup.n.01", "name": "giant_buttercup"}, + {"id": 17905, "synset": "nigella.n.01", "name": "nigella"}, + {"id": 17906, "synset": "love-in-a-mist.n.03", "name": "love-in-a-mist"}, + {"id": 17907, "synset": "fennel_flower.n.01", "name": "fennel_flower"}, + {"id": 17908, "synset": "black_caraway.n.01", "name": "black_caraway"}, + {"id": 17909, "synset": "pasqueflower.n.01", "name": "pasqueflower"}, + {"id": 17910, "synset": "meadow_rue.n.01", "name": "meadow_rue"}, + {"id": 17911, "synset": "false_bugbane.n.01", "name": "false_bugbane"}, + {"id": 17912, "synset": "globeflower.n.01", "name": "globeflower"}, + {"id": 17913, "synset": "winter's_bark.n.02", "name": "winter's_bark"}, + {"id": 17914, "synset": "pepper_shrub.n.01", "name": "pepper_shrub"}, + {"id": 17915, "synset": "sweet_gale.n.01", "name": "sweet_gale"}, + {"id": 17916, "synset": "wax_myrtle.n.01", "name": "wax_myrtle"}, + {"id": 17917, "synset": "bay_myrtle.n.01", "name": "bay_myrtle"}, + {"id": 17918, "synset": "bayberry.n.02", "name": "bayberry"}, + {"id": 17919, "synset": "sweet_fern.n.02", "name": "sweet_fern"}, + {"id": 17920, "synset": "corkwood.n.01", "name": "corkwood"}, + {"id": 17921, "synset": "jointed_rush.n.01", "name": "jointed_rush"}, + {"id": 17922, "synset": "toad_rush.n.01", "name": "toad_rush"}, + {"id": 17923, "synset": "slender_rush.n.01", "name": "slender_rush"}, + {"id": 17924, "synset": "zebrawood.n.02", "name": "zebrawood"}, + {"id": 17925, "synset": "connarus_guianensis.n.01", "name": "Connarus_guianensis"}, + {"id": 17926, "synset": "legume.n.01", "name": "legume"}, + {"id": 17927, "synset": "peanut.n.01", "name": "peanut"}, + {"id": 17928, "synset": "granadilla_tree.n.01", "name": "granadilla_tree"}, + {"id": 17929, "synset": "arariba.n.01", "name": "arariba"}, + {"id": 17930, "synset": "tonka_bean.n.01", "name": "tonka_bean"}, + {"id": 17931, "synset": "courbaril.n.01", "name": "courbaril"}, + {"id": 17932, "synset": "melilotus.n.01", "name": "melilotus"}, + {"id": 17933, "synset": "darling_pea.n.01", "name": "darling_pea"}, + {"id": 17934, "synset": "smooth_darling_pea.n.01", "name": "smooth_darling_pea"}, + {"id": 17935, "synset": "clover.n.01", "name": "clover"}, + {"id": 17936, "synset": "alpine_clover.n.01", "name": "alpine_clover"}, + {"id": 17937, "synset": "hop_clover.n.02", "name": "hop_clover"}, + {"id": 17938, "synset": "crimson_clover.n.01", "name": "crimson_clover"}, + {"id": 17939, "synset": "red_clover.n.01", "name": "red_clover"}, + {"id": 17940, "synset": "buffalo_clover.n.02", "name": "buffalo_clover"}, + {"id": 17941, "synset": "white_clover.n.01", "name": "white_clover"}, + {"id": 17942, "synset": "mimosa.n.02", "name": "mimosa"}, + {"id": 17943, "synset": "acacia.n.01", "name": "acacia"}, + {"id": 17944, "synset": "shittah.n.01", "name": "shittah"}, + {"id": 17945, "synset": "wattle.n.03", "name": "wattle"}, + {"id": 17946, "synset": "black_wattle.n.01", "name": "black_wattle"}, + {"id": 17947, "synset": "gidgee.n.01", "name": "gidgee"}, + {"id": 17948, "synset": "catechu.n.02", "name": "catechu"}, + {"id": 17949, "synset": "silver_wattle.n.01", "name": "silver_wattle"}, + {"id": 17950, "synset": "huisache.n.01", "name": "huisache"}, + {"id": 17951, "synset": "lightwood.n.01", "name": "lightwood"}, + {"id": 17952, "synset": "golden_wattle.n.01", "name": "golden_wattle"}, + {"id": 17953, "synset": "fever_tree.n.04", "name": "fever_tree"}, + {"id": 17954, "synset": "coralwood.n.01", "name": "coralwood"}, + {"id": 17955, "synset": "albizzia.n.01", "name": "albizzia"}, + {"id": 17956, "synset": "silk_tree.n.01", "name": "silk_tree"}, + {"id": 17957, "synset": "siris.n.01", "name": "siris"}, + {"id": 17958, "synset": "rain_tree.n.01", "name": "rain_tree"}, + {"id": 17959, "synset": "calliandra.n.01", "name": "calliandra"}, + {"id": 17960, "synset": "conacaste.n.01", "name": "conacaste"}, + {"id": 17961, "synset": "inga.n.01", "name": "inga"}, + {"id": 17962, "synset": "ice-cream_bean.n.01", "name": "ice-cream_bean"}, + {"id": 17963, "synset": "guama.n.01", "name": "guama"}, + {"id": 17964, "synset": "lead_tree.n.01", "name": "lead_tree"}, + {"id": 17965, "synset": "wild_tamarind.n.02", "name": "wild_tamarind"}, + {"id": 17966, "synset": "sabicu.n.02", "name": "sabicu"}, + {"id": 17967, "synset": "nitta_tree.n.01", "name": "nitta_tree"}, + {"id": 17968, "synset": "parkia_javanica.n.01", "name": "Parkia_javanica"}, + {"id": 17969, "synset": "manila_tamarind.n.01", "name": "manila_tamarind"}, + {"id": 17970, "synset": "cat's-claw.n.01", "name": "cat's-claw"}, + {"id": 17971, "synset": "honey_mesquite.n.01", "name": "honey_mesquite"}, + {"id": 17972, "synset": "algarroba.n.03", "name": "algarroba"}, + {"id": 17973, "synset": "screw_bean.n.02", "name": "screw_bean"}, + {"id": 17974, "synset": "screw_bean.n.01", "name": "screw_bean"}, + {"id": 17975, "synset": "dogbane.n.01", "name": "dogbane"}, + {"id": 17976, "synset": "indian_hemp.n.03", "name": "Indian_hemp"}, + {"id": 17977, "synset": "bushman's_poison.n.01", "name": "bushman's_poison"}, + {"id": 17978, "synset": "impala_lily.n.01", "name": "impala_lily"}, + {"id": 17979, "synset": "allamanda.n.01", "name": "allamanda"}, + {"id": 17980, "synset": "common_allamanda.n.01", "name": "common_allamanda"}, + {"id": 17981, "synset": "dita.n.01", "name": "dita"}, + {"id": 17982, "synset": "nepal_trumpet_flower.n.01", "name": "Nepal_trumpet_flower"}, + {"id": 17983, "synset": "carissa.n.01", "name": "carissa"}, + {"id": 17984, "synset": "hedge_thorn.n.01", "name": "hedge_thorn"}, + {"id": 17985, "synset": "natal_plum.n.01", "name": "natal_plum"}, + {"id": 17986, "synset": "periwinkle.n.02", "name": "periwinkle"}, + {"id": 17987, "synset": "ivory_tree.n.01", "name": "ivory_tree"}, + {"id": 17988, "synset": "white_dipladenia.n.01", "name": "white_dipladenia"}, + {"id": 17989, "synset": "chilean_jasmine.n.01", "name": "Chilean_jasmine"}, + {"id": 17990, "synset": "oleander.n.01", "name": "oleander"}, + {"id": 17991, "synset": "frangipani.n.01", "name": "frangipani"}, + {"id": 17992, "synset": "west_indian_jasmine.n.01", "name": "West_Indian_jasmine"}, + {"id": 17993, "synset": "rauwolfia.n.02", "name": "rauwolfia"}, + {"id": 17994, "synset": "snakewood.n.01", "name": "snakewood"}, + {"id": 17995, "synset": "strophanthus_kombe.n.01", "name": "Strophanthus_kombe"}, + {"id": 17996, "synset": "yellow_oleander.n.01", "name": "yellow_oleander"}, + {"id": 17997, "synset": "myrtle.n.01", "name": "myrtle"}, + {"id": 17998, "synset": "large_periwinkle.n.01", "name": "large_periwinkle"}, + {"id": 17999, "synset": "arum.n.02", "name": "arum"}, + {"id": 18000, "synset": "cuckoopint.n.01", "name": "cuckoopint"}, + {"id": 18001, "synset": "black_calla.n.01", "name": "black_calla"}, + {"id": 18002, "synset": "calamus.n.02", "name": "calamus"}, + {"id": 18003, "synset": "alocasia.n.01", "name": "alocasia"}, + {"id": 18004, "synset": "giant_taro.n.01", "name": "giant_taro"}, + {"id": 18005, "synset": "amorphophallus.n.01", "name": "amorphophallus"}, + {"id": 18006, "synset": "pungapung.n.01", "name": "pungapung"}, + {"id": 18007, "synset": "devil's_tongue.n.01", "name": "devil's_tongue"}, + {"id": 18008, "synset": "anthurium.n.01", "name": "anthurium"}, + {"id": 18009, "synset": "flamingo_flower.n.01", "name": "flamingo_flower"}, + {"id": 18010, "synset": "jack-in-the-pulpit.n.01", "name": "jack-in-the-pulpit"}, + {"id": 18011, "synset": "friar's-cowl.n.01", "name": "friar's-cowl"}, + {"id": 18012, "synset": "caladium.n.01", "name": "caladium"}, + {"id": 18013, "synset": "caladium_bicolor.n.01", "name": "Caladium_bicolor"}, + {"id": 18014, "synset": "wild_calla.n.01", "name": "wild_calla"}, + {"id": 18015, "synset": "taro.n.02", "name": "taro"}, + {"id": 18016, "synset": "taro.n.01", "name": "taro"}, + {"id": 18017, "synset": "cryptocoryne.n.01", "name": "cryptocoryne"}, + {"id": 18018, "synset": "dracontium.n.01", "name": "dracontium"}, + {"id": 18019, "synset": "golden_pothos.n.01", "name": "golden_pothos"}, + {"id": 18020, "synset": "skunk_cabbage.n.02", "name": "skunk_cabbage"}, + {"id": 18021, "synset": "monstera.n.01", "name": "monstera"}, + {"id": 18022, "synset": "ceriman.n.01", "name": "ceriman"}, + {"id": 18023, "synset": "nephthytis.n.01", "name": "nephthytis"}, + {"id": 18024, "synset": "nephthytis_afzelii.n.01", "name": "Nephthytis_afzelii"}, + {"id": 18025, "synset": "arrow_arum.n.01", "name": "arrow_arum"}, + {"id": 18026, "synset": "green_arrow_arum.n.01", "name": "green_arrow_arum"}, + {"id": 18027, "synset": "philodendron.n.01", "name": "philodendron"}, + {"id": 18028, "synset": "pistia.n.01", "name": "pistia"}, + {"id": 18029, "synset": "pothos.n.01", "name": "pothos"}, + {"id": 18030, "synset": "spathiphyllum.n.01", "name": "spathiphyllum"}, + {"id": 18031, "synset": "skunk_cabbage.n.01", "name": "skunk_cabbage"}, + {"id": 18032, "synset": "yautia.n.01", "name": "yautia"}, + {"id": 18033, "synset": "calla_lily.n.01", "name": "calla_lily"}, + {"id": 18034, "synset": "pink_calla.n.01", "name": "pink_calla"}, + {"id": 18035, "synset": "golden_calla.n.01", "name": "golden_calla"}, + {"id": 18036, "synset": "duckweed.n.01", "name": "duckweed"}, + {"id": 18037, "synset": "common_duckweed.n.01", "name": "common_duckweed"}, + {"id": 18038, "synset": "star-duckweed.n.01", "name": "star-duckweed"}, + {"id": 18039, "synset": "great_duckweed.n.01", "name": "great_duckweed"}, + {"id": 18040, "synset": "watermeal.n.01", "name": "watermeal"}, + {"id": 18041, "synset": "common_wolffia.n.01", "name": "common_wolffia"}, + {"id": 18042, "synset": "aralia.n.01", "name": "aralia"}, + {"id": 18043, "synset": "american_angelica_tree.n.01", "name": "American_angelica_tree"}, + {"id": 18044, "synset": "american_spikenard.n.01", "name": "American_spikenard"}, + {"id": 18045, "synset": "bristly_sarsaparilla.n.01", "name": "bristly_sarsaparilla"}, + {"id": 18046, "synset": "japanese_angelica_tree.n.01", "name": "Japanese_angelica_tree"}, + {"id": 18047, "synset": "chinese_angelica.n.01", "name": "Chinese_angelica"}, + {"id": 18048, "synset": "ivy.n.01", "name": "ivy"}, + {"id": 18049, "synset": "puka.n.02", "name": "puka"}, + {"id": 18050, "synset": "ginseng.n.02", "name": "ginseng"}, + {"id": 18051, "synset": "ginseng.n.01", "name": "ginseng"}, + {"id": 18052, "synset": "umbrella_tree.n.01", "name": "umbrella_tree"}, + {"id": 18053, "synset": "birthwort.n.01", "name": "birthwort"}, + {"id": 18054, "synset": "dutchman's-pipe.n.01", "name": "Dutchman's-pipe"}, + {"id": 18055, "synset": "virginia_snakeroot.n.01", "name": "Virginia_snakeroot"}, + {"id": 18056, "synset": "canada_ginger.n.01", "name": "Canada_ginger"}, + {"id": 18057, "synset": "heartleaf.n.02", "name": "heartleaf"}, + {"id": 18058, "synset": "heartleaf.n.01", "name": "heartleaf"}, + {"id": 18059, "synset": "asarabacca.n.01", "name": "asarabacca"}, + {"id": 18060, "synset": "caryophyllaceous_plant.n.01", "name": "caryophyllaceous_plant"}, + {"id": 18061, "synset": "corn_cockle.n.01", "name": "corn_cockle"}, + {"id": 18062, "synset": "sandwort.n.03", "name": "sandwort"}, + {"id": 18063, "synset": "mountain_sandwort.n.01", "name": "mountain_sandwort"}, + {"id": 18064, "synset": "pine-barren_sandwort.n.01", "name": "pine-barren_sandwort"}, + {"id": 18065, "synset": "seabeach_sandwort.n.01", "name": "seabeach_sandwort"}, + {"id": 18066, "synset": "rock_sandwort.n.01", "name": "rock_sandwort"}, + {"id": 18067, "synset": "thyme-leaved_sandwort.n.01", "name": "thyme-leaved_sandwort"}, + {"id": 18068, "synset": "mouse-ear_chickweed.n.01", "name": "mouse-ear_chickweed"}, + {"id": 18069, "synset": "snow-in-summer.n.02", "name": "snow-in-summer"}, + {"id": 18070, "synset": "alpine_mouse-ear.n.01", "name": "Alpine_mouse-ear"}, + {"id": 18071, "synset": "pink.n.02", "name": "pink"}, + {"id": 18072, "synset": "sweet_william.n.01", "name": "sweet_William"}, + {"id": 18073, "synset": "china_pink.n.01", "name": "china_pink"}, + {"id": 18074, "synset": "japanese_pink.n.01", "name": "Japanese_pink"}, + {"id": 18075, "synset": "maiden_pink.n.01", "name": "maiden_pink"}, + {"id": 18076, "synset": "cheddar_pink.n.01", "name": "cheddar_pink"}, + {"id": 18077, "synset": "button_pink.n.01", "name": "button_pink"}, + {"id": 18078, "synset": "cottage_pink.n.01", "name": "cottage_pink"}, + {"id": 18079, "synset": "fringed_pink.n.02", "name": "fringed_pink"}, + {"id": 18080, "synset": "drypis.n.01", "name": "drypis"}, + {"id": 18081, "synset": "baby's_breath.n.01", "name": "baby's_breath"}, + {"id": 18082, "synset": "coral_necklace.n.01", "name": "coral_necklace"}, + {"id": 18083, "synset": "lychnis.n.01", "name": "lychnis"}, + {"id": 18084, "synset": "ragged_robin.n.01", "name": "ragged_robin"}, + {"id": 18085, "synset": "scarlet_lychnis.n.01", "name": "scarlet_lychnis"}, + {"id": 18086, "synset": "mullein_pink.n.01", "name": "mullein_pink"}, + {"id": 18087, "synset": "sandwort.n.02", "name": "sandwort"}, + {"id": 18088, "synset": "sandwort.n.01", "name": "sandwort"}, + {"id": 18089, "synset": "soapwort.n.01", "name": "soapwort"}, + {"id": 18090, "synset": "knawel.n.01", "name": "knawel"}, + {"id": 18091, "synset": "silene.n.01", "name": "silene"}, + {"id": 18092, "synset": "moss_campion.n.01", "name": "moss_campion"}, + {"id": 18093, "synset": "wild_pink.n.02", "name": "wild_pink"}, + {"id": 18094, "synset": "red_campion.n.01", "name": "red_campion"}, + {"id": 18095, "synset": "white_campion.n.01", "name": "white_campion"}, + {"id": 18096, "synset": "fire_pink.n.01", "name": "fire_pink"}, + {"id": 18097, "synset": "bladder_campion.n.01", "name": "bladder_campion"}, + {"id": 18098, "synset": "corn_spurry.n.01", "name": "corn_spurry"}, + {"id": 18099, "synset": "sand_spurry.n.01", "name": "sand_spurry"}, + {"id": 18100, "synset": "chickweed.n.01", "name": "chickweed"}, + {"id": 18101, "synset": "common_chickweed.n.01", "name": "common_chickweed"}, + {"id": 18102, "synset": "cowherb.n.01", "name": "cowherb"}, + {"id": 18103, "synset": "hottentot_fig.n.01", "name": "Hottentot_fig"}, + {"id": 18104, "synset": "livingstone_daisy.n.01", "name": "livingstone_daisy"}, + {"id": 18105, "synset": "fig_marigold.n.01", "name": "fig_marigold"}, + {"id": 18106, "synset": "ice_plant.n.01", "name": "ice_plant"}, + {"id": 18107, "synset": "new_zealand_spinach.n.01", "name": "New_Zealand_spinach"}, + {"id": 18108, "synset": "amaranth.n.02", "name": "amaranth"}, + {"id": 18109, "synset": "amaranth.n.01", "name": "amaranth"}, + {"id": 18110, "synset": "tumbleweed.n.04", "name": "tumbleweed"}, + {"id": 18111, "synset": "prince's-feather.n.02", "name": "prince's-feather"}, + {"id": 18112, "synset": "pigweed.n.02", "name": "pigweed"}, + {"id": 18113, "synset": "thorny_amaranth.n.01", "name": "thorny_amaranth"}, + {"id": 18114, "synset": "alligator_weed.n.01", "name": "alligator_weed"}, + {"id": 18115, "synset": "cockscomb.n.01", "name": "cockscomb"}, + {"id": 18116, "synset": "cottonweed.n.02", "name": "cottonweed"}, + {"id": 18117, "synset": "globe_amaranth.n.01", "name": "globe_amaranth"}, + {"id": 18118, "synset": "bloodleaf.n.01", "name": "bloodleaf"}, + {"id": 18119, "synset": "saltwort.n.02", "name": "saltwort"}, + {"id": 18120, "synset": "lamb's-quarters.n.01", "name": "lamb's-quarters"}, + {"id": 18121, "synset": "good-king-henry.n.01", "name": "good-king-henry"}, + {"id": 18122, "synset": "jerusalem_oak.n.01", "name": "Jerusalem_oak"}, + {"id": 18123, "synset": "oak-leaved_goosefoot.n.01", "name": "oak-leaved_goosefoot"}, + {"id": 18124, "synset": "sowbane.n.01", "name": "sowbane"}, + {"id": 18125, "synset": "nettle-leaved_goosefoot.n.01", "name": "nettle-leaved_goosefoot"}, + {"id": 18126, "synset": "red_goosefoot.n.01", "name": "red_goosefoot"}, + {"id": 18127, "synset": "stinking_goosefoot.n.01", "name": "stinking_goosefoot"}, + {"id": 18128, "synset": "orach.n.01", "name": "orach"}, + {"id": 18129, "synset": "saltbush.n.01", "name": "saltbush"}, + {"id": 18130, "synset": "garden_orache.n.01", "name": "garden_orache"}, + {"id": 18131, "synset": "desert_holly.n.01", "name": "desert_holly"}, + {"id": 18132, "synset": "quail_bush.n.01", "name": "quail_bush"}, + {"id": 18133, "synset": "beet.n.01", "name": "beet"}, + {"id": 18134, "synset": "beetroot.n.01", "name": "beetroot"}, + {"id": 18135, "synset": "chard.n.01", "name": "chard"}, + {"id": 18136, "synset": "mangel-wurzel.n.01", "name": "mangel-wurzel"}, + {"id": 18137, "synset": "winged_pigweed.n.01", "name": "winged_pigweed"}, + {"id": 18138, "synset": "halogeton.n.01", "name": "halogeton"}, + {"id": 18139, "synset": "glasswort.n.02", "name": "glasswort"}, + {"id": 18140, "synset": "saltwort.n.01", "name": "saltwort"}, + {"id": 18141, "synset": "russian_thistle.n.01", "name": "Russian_thistle"}, + {"id": 18142, "synset": "greasewood.n.01", "name": "greasewood"}, + {"id": 18143, "synset": "scarlet_musk_flower.n.01", "name": "scarlet_musk_flower"}, + {"id": 18144, "synset": "sand_verbena.n.01", "name": "sand_verbena"}, + {"id": 18145, "synset": "sweet_sand_verbena.n.01", "name": "sweet_sand_verbena"}, + {"id": 18146, "synset": "yellow_sand_verbena.n.01", "name": "yellow_sand_verbena"}, + {"id": 18147, "synset": "beach_pancake.n.01", "name": "beach_pancake"}, + {"id": 18148, "synset": "beach_sand_verbena.n.01", "name": "beach_sand_verbena"}, + {"id": 18149, "synset": "desert_sand_verbena.n.01", "name": "desert_sand_verbena"}, + {"id": 18150, "synset": "trailing_four_o'clock.n.01", "name": "trailing_four_o'clock"}, + {"id": 18151, "synset": "bougainvillea.n.01", "name": "bougainvillea"}, + {"id": 18152, "synset": "umbrellawort.n.01", "name": "umbrellawort"}, + {"id": 18153, "synset": "four_o'clock.n.01", "name": "four_o'clock"}, + {"id": 18154, "synset": "common_four-o'clock.n.01", "name": "common_four-o'clock"}, + {"id": 18155, "synset": "california_four_o'clock.n.01", "name": "California_four_o'clock"}, + {"id": 18156, "synset": "sweet_four_o'clock.n.01", "name": "sweet_four_o'clock"}, + {"id": 18157, "synset": "desert_four_o'clock.n.01", "name": "desert_four_o'clock"}, + {"id": 18158, "synset": "mountain_four_o'clock.n.01", "name": "mountain_four_o'clock"}, + {"id": 18159, "synset": "cockspur.n.02", "name": "cockspur"}, + {"id": 18160, "synset": "rattail_cactus.n.01", "name": "rattail_cactus"}, + {"id": 18161, "synset": "saguaro.n.01", "name": "saguaro"}, + {"id": 18162, "synset": "night-blooming_cereus.n.03", "name": "night-blooming_cereus"}, + {"id": 18163, "synset": "echinocactus.n.01", "name": "echinocactus"}, + {"id": 18164, "synset": "hedgehog_cactus.n.01", "name": "hedgehog_cactus"}, + {"id": 18165, "synset": "golden_barrel_cactus.n.01", "name": "golden_barrel_cactus"}, + {"id": 18166, "synset": "hedgehog_cereus.n.01", "name": "hedgehog_cereus"}, + {"id": 18167, "synset": "rainbow_cactus.n.01", "name": "rainbow_cactus"}, + {"id": 18168, "synset": "epiphyllum.n.01", "name": "epiphyllum"}, + {"id": 18169, "synset": "barrel_cactus.n.01", "name": "barrel_cactus"}, + {"id": 18170, "synset": "night-blooming_cereus.n.02", "name": "night-blooming_cereus"}, + {"id": 18171, "synset": "chichipe.n.01", "name": "chichipe"}, + {"id": 18172, "synset": "mescal.n.01", "name": "mescal"}, + {"id": 18173, "synset": "mescal_button.n.01", "name": "mescal_button"}, + {"id": 18174, "synset": "mammillaria.n.01", "name": "mammillaria"}, + {"id": 18175, "synset": "feather_ball.n.01", "name": "feather_ball"}, + {"id": 18176, "synset": "garambulla.n.01", "name": "garambulla"}, + {"id": 18177, "synset": "knowlton's_cactus.n.01", "name": "Knowlton's_cactus"}, + {"id": 18178, "synset": "nopal.n.02", "name": "nopal"}, + {"id": 18179, "synset": "prickly_pear.n.01", "name": "prickly_pear"}, + {"id": 18180, "synset": "cholla.n.01", "name": "cholla"}, + {"id": 18181, "synset": "nopal.n.01", "name": "nopal"}, + {"id": 18182, "synset": "tuna.n.01", "name": "tuna"}, + {"id": 18183, "synset": "barbados_gooseberry.n.01", "name": "Barbados_gooseberry"}, + {"id": 18184, "synset": "mistletoe_cactus.n.01", "name": "mistletoe_cactus"}, + {"id": 18185, "synset": "christmas_cactus.n.01", "name": "Christmas_cactus"}, + {"id": 18186, "synset": "night-blooming_cereus.n.01", "name": "night-blooming_cereus"}, + {"id": 18187, "synset": "crab_cactus.n.01", "name": "crab_cactus"}, + {"id": 18188, "synset": "pokeweed.n.01", "name": "pokeweed"}, + {"id": 18189, "synset": "indian_poke.n.02", "name": "Indian_poke"}, + {"id": 18190, "synset": "poke.n.01", "name": "poke"}, + {"id": 18191, "synset": "ombu.n.01", "name": "ombu"}, + {"id": 18192, "synset": "bloodberry.n.01", "name": "bloodberry"}, + {"id": 18193, "synset": "portulaca.n.01", "name": "portulaca"}, + {"id": 18194, "synset": "rose_moss.n.01", "name": "rose_moss"}, + {"id": 18195, "synset": "common_purslane.n.01", "name": "common_purslane"}, + {"id": 18196, "synset": "rock_purslane.n.01", "name": "rock_purslane"}, + {"id": 18197, "synset": "red_maids.n.01", "name": "red_maids"}, + {"id": 18198, "synset": "carolina_spring_beauty.n.01", "name": "Carolina_spring_beauty"}, + {"id": 18199, "synset": "spring_beauty.n.01", "name": "spring_beauty"}, + {"id": 18200, "synset": "virginia_spring_beauty.n.01", "name": "Virginia_spring_beauty"}, + {"id": 18201, "synset": "siskiyou_lewisia.n.01", "name": "siskiyou_lewisia"}, + {"id": 18202, "synset": "bitterroot.n.01", "name": "bitterroot"}, + {"id": 18203, "synset": "broad-leaved_montia.n.01", "name": "broad-leaved_montia"}, + {"id": 18204, "synset": "blinks.n.01", "name": "blinks"}, + {"id": 18205, "synset": "toad_lily.n.01", "name": "toad_lily"}, + {"id": 18206, "synset": "winter_purslane.n.01", "name": "winter_purslane"}, + {"id": 18207, "synset": "flame_flower.n.02", "name": "flame_flower"}, + {"id": 18208, "synset": "pigmy_talinum.n.01", "name": "pigmy_talinum"}, + {"id": 18209, "synset": "jewels-of-opar.n.01", "name": "jewels-of-opar"}, + {"id": 18210, "synset": "caper.n.01", "name": "caper"}, + {"id": 18211, "synset": "native_pomegranate.n.01", "name": "native_pomegranate"}, + {"id": 18212, "synset": "caper_tree.n.02", "name": "caper_tree"}, + {"id": 18213, "synset": "caper_tree.n.01", "name": "caper_tree"}, + {"id": 18214, "synset": "common_caper.n.01", "name": "common_caper"}, + {"id": 18215, "synset": "spiderflower.n.01", "name": "spiderflower"}, + {"id": 18216, "synset": "rocky_mountain_bee_plant.n.01", "name": "Rocky_Mountain_bee_plant"}, + {"id": 18217, "synset": "clammyweed.n.01", "name": "clammyweed"}, + {"id": 18218, "synset": "crucifer.n.01", "name": "crucifer"}, + {"id": 18219, "synset": "cress.n.01", "name": "cress"}, + {"id": 18220, "synset": "watercress.n.01", "name": "watercress"}, + {"id": 18221, "synset": "stonecress.n.01", "name": "stonecress"}, + {"id": 18222, "synset": "garlic_mustard.n.01", "name": "garlic_mustard"}, + {"id": 18223, "synset": "alyssum.n.01", "name": "alyssum"}, + {"id": 18224, "synset": "rose_of_jericho.n.02", "name": "rose_of_Jericho"}, + {"id": 18225, "synset": "arabidopsis_thaliana.n.01", "name": "Arabidopsis_thaliana"}, + {"id": 18226, "synset": "arabidopsis_lyrata.n.01", "name": "Arabidopsis_lyrata"}, + {"id": 18227, "synset": "rock_cress.n.01", "name": "rock_cress"}, + {"id": 18228, "synset": "sicklepod.n.02", "name": "sicklepod"}, + {"id": 18229, "synset": "tower_mustard.n.01", "name": "tower_mustard"}, + {"id": 18230, "synset": "horseradish.n.01", "name": "horseradish"}, + {"id": 18231, "synset": "winter_cress.n.01", "name": "winter_cress"}, + {"id": 18232, "synset": "yellow_rocket.n.01", "name": "yellow_rocket"}, + {"id": 18233, "synset": "hoary_alison.n.01", "name": "hoary_alison"}, + {"id": 18234, "synset": "buckler_mustard.n.01", "name": "buckler_mustard"}, + {"id": 18235, "synset": "wild_cabbage.n.01", "name": "wild_cabbage"}, + {"id": 18236, "synset": "cabbage.n.03", "name": "cabbage"}, + {"id": 18237, "synset": "head_cabbage.n.01", "name": "head_cabbage"}, + {"id": 18238, "synset": "savoy_cabbage.n.01", "name": "savoy_cabbage"}, + {"id": 18239, "synset": "brussels_sprout.n.01", "name": "brussels_sprout"}, + {"id": 18240, "synset": "cauliflower.n.01", "name": "cauliflower"}, + {"id": 18241, "synset": "collard.n.01", "name": "collard"}, + {"id": 18242, "synset": "kohlrabi.n.01", "name": "kohlrabi"}, + {"id": 18243, "synset": "turnip_plant.n.01", "name": "turnip_plant"}, + {"id": 18244, "synset": "rutabaga.n.02", "name": "rutabaga"}, + {"id": 18245, "synset": "broccoli_raab.n.01", "name": "broccoli_raab"}, + {"id": 18246, "synset": "mustard.n.01", "name": "mustard"}, + {"id": 18247, "synset": "chinese_mustard.n.01", "name": "chinese_mustard"}, + {"id": 18248, "synset": "bok_choy.n.01", "name": "bok_choy"}, + {"id": 18249, "synset": "rape.n.01", "name": "rape"}, + {"id": 18250, "synset": "rapeseed.n.01", "name": "rapeseed"}, + {"id": 18251, "synset": "shepherd's_purse.n.01", "name": "shepherd's_purse"}, + {"id": 18252, "synset": "lady's_smock.n.01", "name": "lady's_smock"}, + {"id": 18253, "synset": "coral-root_bittercress.n.01", "name": "coral-root_bittercress"}, + {"id": 18254, "synset": "crinkleroot.n.01", "name": "crinkleroot"}, + {"id": 18255, "synset": "american_watercress.n.01", "name": "American_watercress"}, + {"id": 18256, "synset": "spring_cress.n.01", "name": "spring_cress"}, + {"id": 18257, "synset": "purple_cress.n.01", "name": "purple_cress"}, + {"id": 18258, "synset": "wallflower.n.02", "name": "wallflower"}, + {"id": 18259, "synset": "prairie_rocket.n.02", "name": "prairie_rocket"}, + {"id": 18260, "synset": "scurvy_grass.n.01", "name": "scurvy_grass"}, + {"id": 18261, "synset": "sea_kale.n.01", "name": "sea_kale"}, + {"id": 18262, "synset": "tansy_mustard.n.01", "name": "tansy_mustard"}, + {"id": 18263, "synset": "draba.n.01", "name": "draba"}, + {"id": 18264, "synset": "wallflower.n.01", "name": "wallflower"}, + {"id": 18265, "synset": "prairie_rocket.n.01", "name": "prairie_rocket"}, + {"id": 18266, "synset": "siberian_wall_flower.n.01", "name": "Siberian_wall_flower"}, + {"id": 18267, "synset": "western_wall_flower.n.01", "name": "western_wall_flower"}, + {"id": 18268, "synset": "wormseed_mustard.n.01", "name": "wormseed_mustard"}, + {"id": 18269, "synset": "heliophila.n.01", "name": "heliophila"}, + {"id": 18270, "synset": "damask_violet.n.01", "name": "damask_violet"}, + {"id": 18271, "synset": "tansy-leaved_rocket.n.01", "name": "tansy-leaved_rocket"}, + {"id": 18272, "synset": "candytuft.n.01", "name": "candytuft"}, + {"id": 18273, "synset": "woad.n.02", "name": "woad"}, + {"id": 18274, "synset": "dyer's_woad.n.01", "name": "dyer's_woad"}, + {"id": 18275, "synset": "bladderpod.n.04", "name": "bladderpod"}, + {"id": 18276, "synset": "sweet_alyssum.n.01", "name": "sweet_alyssum"}, + {"id": 18277, "synset": "malcolm_stock.n.01", "name": "Malcolm_stock"}, + {"id": 18278, "synset": "virginian_stock.n.01", "name": "Virginian_stock"}, + {"id": 18279, "synset": "stock.n.12", "name": "stock"}, + {"id": 18280, "synset": "brompton_stock.n.01", "name": "brompton_stock"}, + {"id": 18281, "synset": "bladderpod.n.03", "name": "bladderpod"}, + {"id": 18282, "synset": "chamois_cress.n.01", "name": "chamois_cress"}, + {"id": 18283, "synset": "radish_plant.n.01", "name": "radish_plant"}, + {"id": 18284, "synset": "jointed_charlock.n.01", "name": "jointed_charlock"}, + {"id": 18285, "synset": "radish.n.04", "name": "radish"}, + {"id": 18286, "synset": "radish.n.02", "name": "radish"}, + {"id": 18287, "synset": "marsh_cress.n.01", "name": "marsh_cress"}, + {"id": 18288, "synset": "great_yellowcress.n.01", "name": "great_yellowcress"}, + {"id": 18289, "synset": "schizopetalon.n.01", "name": "schizopetalon"}, + {"id": 18290, "synset": "field_mustard.n.01", "name": "field_mustard"}, + {"id": 18291, "synset": "hedge_mustard.n.01", "name": "hedge_mustard"}, + {"id": 18292, "synset": "desert_plume.n.01", "name": "desert_plume"}, + {"id": 18293, "synset": "pennycress.n.01", "name": "pennycress"}, + {"id": 18294, "synset": "field_pennycress.n.01", "name": "field_pennycress"}, + {"id": 18295, "synset": "fringepod.n.01", "name": "fringepod"}, + {"id": 18296, "synset": "bladderpod.n.02", "name": "bladderpod"}, + {"id": 18297, "synset": "wasabi.n.01", "name": "wasabi"}, + {"id": 18298, "synset": "poppy.n.01", "name": "poppy"}, + {"id": 18299, "synset": "iceland_poppy.n.02", "name": "Iceland_poppy"}, + {"id": 18300, "synset": "western_poppy.n.01", "name": "western_poppy"}, + {"id": 18301, "synset": "prickly_poppy.n.02", "name": "prickly_poppy"}, + {"id": 18302, "synset": "iceland_poppy.n.01", "name": "Iceland_poppy"}, + {"id": 18303, "synset": "oriental_poppy.n.01", "name": "oriental_poppy"}, + {"id": 18304, "synset": "corn_poppy.n.01", "name": "corn_poppy"}, + {"id": 18305, "synset": "opium_poppy.n.01", "name": "opium_poppy"}, + {"id": 18306, "synset": "prickly_poppy.n.01", "name": "prickly_poppy"}, + {"id": 18307, "synset": "mexican_poppy.n.01", "name": "Mexican_poppy"}, + {"id": 18308, "synset": "bocconia.n.02", "name": "bocconia"}, + {"id": 18309, "synset": "celandine.n.02", "name": "celandine"}, + {"id": 18310, "synset": "corydalis.n.01", "name": "corydalis"}, + {"id": 18311, "synset": "climbing_corydalis.n.01", "name": "climbing_corydalis"}, + {"id": 18312, "synset": "california_poppy.n.01", "name": "California_poppy"}, + {"id": 18313, "synset": "horn_poppy.n.01", "name": "horn_poppy"}, + {"id": 18314, "synset": "golden_cup.n.01", "name": "golden_cup"}, + {"id": 18315, "synset": "plume_poppy.n.01", "name": "plume_poppy"}, + {"id": 18316, "synset": "blue_poppy.n.01", "name": "blue_poppy"}, + {"id": 18317, "synset": "welsh_poppy.n.01", "name": "Welsh_poppy"}, + {"id": 18318, "synset": "creamcups.n.01", "name": "creamcups"}, + {"id": 18319, "synset": "matilija_poppy.n.01", "name": "matilija_poppy"}, + {"id": 18320, "synset": "wind_poppy.n.01", "name": "wind_poppy"}, + {"id": 18321, "synset": "celandine_poppy.n.01", "name": "celandine_poppy"}, + {"id": 18322, "synset": "climbing_fumitory.n.01", "name": "climbing_fumitory"}, + {"id": 18323, "synset": "bleeding_heart.n.01", "name": "bleeding_heart"}, + {"id": 18324, "synset": "dutchman's_breeches.n.01", "name": "Dutchman's_breeches"}, + {"id": 18325, "synset": "squirrel_corn.n.01", "name": "squirrel_corn"}, + {"id": 18326, "synset": "composite.n.02", "name": "composite"}, + {"id": 18327, "synset": "compass_plant.n.02", "name": "compass_plant"}, + {"id": 18328, "synset": "everlasting.n.01", "name": "everlasting"}, + {"id": 18329, "synset": "achillea.n.01", "name": "achillea"}, + {"id": 18330, "synset": "yarrow.n.01", "name": "yarrow"}, + { + "id": 18331, + "synset": "pink-and-white_everlasting.n.01", + "name": "pink-and-white_everlasting", + }, + {"id": 18332, "synset": "white_snakeroot.n.01", "name": "white_snakeroot"}, + {"id": 18333, "synset": "ageratum.n.02", "name": "ageratum"}, + {"id": 18334, "synset": "common_ageratum.n.01", "name": "common_ageratum"}, + {"id": 18335, "synset": "sweet_sultan.n.03", "name": "sweet_sultan"}, + {"id": 18336, "synset": "ragweed.n.02", "name": "ragweed"}, + {"id": 18337, "synset": "common_ragweed.n.01", "name": "common_ragweed"}, + {"id": 18338, "synset": "great_ragweed.n.01", "name": "great_ragweed"}, + {"id": 18339, "synset": "western_ragweed.n.01", "name": "western_ragweed"}, + {"id": 18340, "synset": "ammobium.n.01", "name": "ammobium"}, + {"id": 18341, "synset": "winged_everlasting.n.01", "name": "winged_everlasting"}, + {"id": 18342, "synset": "pellitory.n.02", "name": "pellitory"}, + {"id": 18343, "synset": "pearly_everlasting.n.01", "name": "pearly_everlasting"}, + {"id": 18344, "synset": "andryala.n.01", "name": "andryala"}, + {"id": 18345, "synset": "plantain-leaved_pussytoes.n.01", "name": "plantain-leaved_pussytoes"}, + {"id": 18346, "synset": "field_pussytoes.n.01", "name": "field_pussytoes"}, + {"id": 18347, "synset": "solitary_pussytoes.n.01", "name": "solitary_pussytoes"}, + {"id": 18348, "synset": "mountain_everlasting.n.01", "name": "mountain_everlasting"}, + {"id": 18349, "synset": "mayweed.n.01", "name": "mayweed"}, + {"id": 18350, "synset": "yellow_chamomile.n.01", "name": "yellow_chamomile"}, + {"id": 18351, "synset": "corn_chamomile.n.01", "name": "corn_chamomile"}, + {"id": 18352, "synset": "woolly_daisy.n.01", "name": "woolly_daisy"}, + {"id": 18353, "synset": "burdock.n.01", "name": "burdock"}, + {"id": 18354, "synset": "great_burdock.n.01", "name": "great_burdock"}, + {"id": 18355, "synset": "african_daisy.n.03", "name": "African_daisy"}, + {"id": 18356, "synset": "blue-eyed_african_daisy.n.01", "name": "blue-eyed_African_daisy"}, + {"id": 18357, "synset": "marguerite.n.02", "name": "marguerite"}, + {"id": 18358, "synset": "silversword.n.01", "name": "silversword"}, + {"id": 18359, "synset": "arnica.n.02", "name": "arnica"}, + {"id": 18360, "synset": "heartleaf_arnica.n.01", "name": "heartleaf_arnica"}, + {"id": 18361, "synset": "arnica_montana.n.01", "name": "Arnica_montana"}, + {"id": 18362, "synset": "lamb_succory.n.01", "name": "lamb_succory"}, + {"id": 18363, "synset": "artemisia.n.01", "name": "artemisia"}, + {"id": 18364, "synset": "mugwort.n.01", "name": "mugwort"}, + {"id": 18365, "synset": "sweet_wormwood.n.01", "name": "sweet_wormwood"}, + {"id": 18366, "synset": "field_wormwood.n.01", "name": "field_wormwood"}, + {"id": 18367, "synset": "tarragon.n.01", "name": "tarragon"}, + {"id": 18368, "synset": "sand_sage.n.01", "name": "sand_sage"}, + {"id": 18369, "synset": "wormwood_sage.n.01", "name": "wormwood_sage"}, + {"id": 18370, "synset": "western_mugwort.n.01", "name": "western_mugwort"}, + {"id": 18371, "synset": "roman_wormwood.n.01", "name": "Roman_wormwood"}, + {"id": 18372, "synset": "bud_brush.n.01", "name": "bud_brush"}, + {"id": 18373, "synset": "common_mugwort.n.01", "name": "common_mugwort"}, + {"id": 18374, "synset": "aster.n.01", "name": "aster"}, + {"id": 18375, "synset": "wood_aster.n.01", "name": "wood_aster"}, + {"id": 18376, "synset": "whorled_aster.n.01", "name": "whorled_aster"}, + {"id": 18377, "synset": "heath_aster.n.02", "name": "heath_aster"}, + {"id": 18378, "synset": "heart-leaved_aster.n.01", "name": "heart-leaved_aster"}, + {"id": 18379, "synset": "white_wood_aster.n.01", "name": "white_wood_aster"}, + {"id": 18380, "synset": "bushy_aster.n.01", "name": "bushy_aster"}, + {"id": 18381, "synset": "heath_aster.n.01", "name": "heath_aster"}, + {"id": 18382, "synset": "white_prairie_aster.n.01", "name": "white_prairie_aster"}, + {"id": 18383, "synset": "stiff_aster.n.01", "name": "stiff_aster"}, + {"id": 18384, "synset": "goldilocks.n.01", "name": "goldilocks"}, + {"id": 18385, "synset": "large-leaved_aster.n.01", "name": "large-leaved_aster"}, + {"id": 18386, "synset": "new_england_aster.n.01", "name": "New_England_aster"}, + {"id": 18387, "synset": "michaelmas_daisy.n.01", "name": "Michaelmas_daisy"}, + {"id": 18388, "synset": "upland_white_aster.n.01", "name": "upland_white_aster"}, + {"id": 18389, "synset": "short's_aster.n.01", "name": "Short's_aster"}, + {"id": 18390, "synset": "sea_aster.n.01", "name": "sea_aster"}, + {"id": 18391, "synset": "prairie_aster.n.01", "name": "prairie_aster"}, + {"id": 18392, "synset": "annual_salt-marsh_aster.n.01", "name": "annual_salt-marsh_aster"}, + {"id": 18393, "synset": "aromatic_aster.n.01", "name": "aromatic_aster"}, + {"id": 18394, "synset": "arrow_leaved_aster.n.01", "name": "arrow_leaved_aster"}, + {"id": 18395, "synset": "azure_aster.n.01", "name": "azure_aster"}, + {"id": 18396, "synset": "bog_aster.n.01", "name": "bog_aster"}, + {"id": 18397, "synset": "crooked-stemmed_aster.n.01", "name": "crooked-stemmed_aster"}, + {"id": 18398, "synset": "eastern_silvery_aster.n.01", "name": "Eastern_silvery_aster"}, + {"id": 18399, "synset": "flat-topped_white_aster.n.01", "name": "flat-topped_white_aster"}, + {"id": 18400, "synset": "late_purple_aster.n.01", "name": "late_purple_aster"}, + {"id": 18401, "synset": "panicled_aster.n.01", "name": "panicled_aster"}, + { + "id": 18402, + "synset": "perennial_salt_marsh_aster.n.01", + "name": "perennial_salt_marsh_aster", + }, + {"id": 18403, "synset": "purple-stemmed_aster.n.01", "name": "purple-stemmed_aster"}, + {"id": 18404, "synset": "rough-leaved_aster.n.01", "name": "rough-leaved_aster"}, + {"id": 18405, "synset": "rush_aster.n.01", "name": "rush_aster"}, + {"id": 18406, "synset": "schreiber's_aster.n.01", "name": "Schreiber's_aster"}, + {"id": 18407, "synset": "small_white_aster.n.01", "name": "small_white_aster"}, + {"id": 18408, "synset": "smooth_aster.n.01", "name": "smooth_aster"}, + {"id": 18409, "synset": "southern_aster.n.01", "name": "southern_aster"}, + {"id": 18410, "synset": "starved_aster.n.01", "name": "starved_aster"}, + {"id": 18411, "synset": "tradescant's_aster.n.01", "name": "tradescant's_aster"}, + {"id": 18412, "synset": "wavy-leaved_aster.n.01", "name": "wavy-leaved_aster"}, + {"id": 18413, "synset": "western_silvery_aster.n.01", "name": "Western_silvery_aster"}, + {"id": 18414, "synset": "willow_aster.n.01", "name": "willow_aster"}, + {"id": 18415, "synset": "ayapana.n.01", "name": "ayapana"}, + {"id": 18416, "synset": "mule_fat.n.01", "name": "mule_fat"}, + {"id": 18417, "synset": "balsamroot.n.01", "name": "balsamroot"}, + {"id": 18418, "synset": "daisy.n.01", "name": "daisy"}, + {"id": 18419, "synset": "common_daisy.n.01", "name": "common_daisy"}, + {"id": 18420, "synset": "bur_marigold.n.01", "name": "bur_marigold"}, + {"id": 18421, "synset": "spanish_needles.n.02", "name": "Spanish_needles"}, + {"id": 18422, "synset": "tickseed_sunflower.n.01", "name": "tickseed_sunflower"}, + {"id": 18423, "synset": "european_beggar-ticks.n.01", "name": "European_beggar-ticks"}, + {"id": 18424, "synset": "slender_knapweed.n.01", "name": "slender_knapweed"}, + {"id": 18425, "synset": "false_chamomile.n.01", "name": "false_chamomile"}, + {"id": 18426, "synset": "swan_river_daisy.n.01", "name": "Swan_River_daisy"}, + {"id": 18427, "synset": "woodland_oxeye.n.01", "name": "woodland_oxeye"}, + {"id": 18428, "synset": "indian_plantain.n.01", "name": "Indian_plantain"}, + {"id": 18429, "synset": "calendula.n.01", "name": "calendula"}, + {"id": 18430, "synset": "common_marigold.n.01", "name": "common_marigold"}, + {"id": 18431, "synset": "china_aster.n.01", "name": "China_aster"}, + {"id": 18432, "synset": "thistle.n.01", "name": "thistle"}, + {"id": 18433, "synset": "welted_thistle.n.01", "name": "welted_thistle"}, + {"id": 18434, "synset": "musk_thistle.n.01", "name": "musk_thistle"}, + {"id": 18435, "synset": "carline_thistle.n.01", "name": "carline_thistle"}, + {"id": 18436, "synset": "stemless_carline_thistle.n.01", "name": "stemless_carline_thistle"}, + {"id": 18437, "synset": "common_carline_thistle.n.01", "name": "common_carline_thistle"}, + {"id": 18438, "synset": "safflower.n.01", "name": "safflower"}, + {"id": 18439, "synset": "safflower_seed.n.01", "name": "safflower_seed"}, + {"id": 18440, "synset": "catananche.n.01", "name": "catananche"}, + {"id": 18441, "synset": "blue_succory.n.01", "name": "blue_succory"}, + {"id": 18442, "synset": "centaury.n.02", "name": "centaury"}, + {"id": 18443, "synset": "dusty_miller.n.03", "name": "dusty_miller"}, + {"id": 18444, "synset": "cornflower.n.02", "name": "cornflower"}, + {"id": 18445, "synset": "star-thistle.n.01", "name": "star-thistle"}, + {"id": 18446, "synset": "knapweed.n.01", "name": "knapweed"}, + {"id": 18447, "synset": "sweet_sultan.n.02", "name": "sweet_sultan"}, + {"id": 18448, "synset": "great_knapweed.n.01", "name": "great_knapweed"}, + {"id": 18449, "synset": "barnaby's_thistle.n.01", "name": "Barnaby's_thistle"}, + {"id": 18450, "synset": "chamomile.n.01", "name": "chamomile"}, + {"id": 18451, "synset": "chaenactis.n.01", "name": "chaenactis"}, + {"id": 18452, "synset": "chrysanthemum.n.02", "name": "chrysanthemum"}, + {"id": 18453, "synset": "corn_marigold.n.01", "name": "corn_marigold"}, + {"id": 18454, "synset": "crown_daisy.n.01", "name": "crown_daisy"}, + {"id": 18455, "synset": "chop-suey_greens.n.01", "name": "chop-suey_greens"}, + {"id": 18456, "synset": "golden_aster.n.01", "name": "golden_aster"}, + {"id": 18457, "synset": "maryland_golden_aster.n.01", "name": "Maryland_golden_aster"}, + {"id": 18458, "synset": "goldenbush.n.02", "name": "goldenbush"}, + {"id": 18459, "synset": "rabbit_brush.n.01", "name": "rabbit_brush"}, + {"id": 18460, "synset": "chicory.n.02", "name": "chicory"}, + {"id": 18461, "synset": "endive.n.01", "name": "endive"}, + {"id": 18462, "synset": "chicory.n.01", "name": "chicory"}, + {"id": 18463, "synset": "plume_thistle.n.01", "name": "plume_thistle"}, + {"id": 18464, "synset": "canada_thistle.n.01", "name": "Canada_thistle"}, + {"id": 18465, "synset": "field_thistle.n.01", "name": "field_thistle"}, + {"id": 18466, "synset": "woolly_thistle.n.02", "name": "woolly_thistle"}, + {"id": 18467, "synset": "european_woolly_thistle.n.01", "name": "European_woolly_thistle"}, + {"id": 18468, "synset": "melancholy_thistle.n.01", "name": "melancholy_thistle"}, + {"id": 18469, "synset": "brook_thistle.n.01", "name": "brook_thistle"}, + {"id": 18470, "synset": "bull_thistle.n.01", "name": "bull_thistle"}, + {"id": 18471, "synset": "blessed_thistle.n.02", "name": "blessed_thistle"}, + {"id": 18472, "synset": "mistflower.n.01", "name": "mistflower"}, + {"id": 18473, "synset": "horseweed.n.02", "name": "horseweed"}, + {"id": 18474, "synset": "coreopsis.n.01", "name": "coreopsis"}, + {"id": 18475, "synset": "giant_coreopsis.n.01", "name": "giant_coreopsis"}, + {"id": 18476, "synset": "sea_dahlia.n.01", "name": "sea_dahlia"}, + {"id": 18477, "synset": "calliopsis.n.01", "name": "calliopsis"}, + {"id": 18478, "synset": "cosmos.n.02", "name": "cosmos"}, + {"id": 18479, "synset": "brass_buttons.n.01", "name": "brass_buttons"}, + {"id": 18480, "synset": "billy_buttons.n.01", "name": "billy_buttons"}, + {"id": 18481, "synset": "hawk's-beard.n.01", "name": "hawk's-beard"}, + {"id": 18482, "synset": "artichoke.n.01", "name": "artichoke"}, + {"id": 18483, "synset": "cardoon.n.01", "name": "cardoon"}, + {"id": 18484, "synset": "dahlia.n.01", "name": "dahlia"}, + {"id": 18485, "synset": "german_ivy.n.01", "name": "German_ivy"}, + {"id": 18486, "synset": "florist's_chrysanthemum.n.01", "name": "florist's_chrysanthemum"}, + {"id": 18487, "synset": "cape_marigold.n.01", "name": "cape_marigold"}, + {"id": 18488, "synset": "leopard's-bane.n.01", "name": "leopard's-bane"}, + {"id": 18489, "synset": "coneflower.n.03", "name": "coneflower"}, + {"id": 18490, "synset": "globe_thistle.n.01", "name": "globe_thistle"}, + {"id": 18491, "synset": "elephant's-foot.n.02", "name": "elephant's-foot"}, + {"id": 18492, "synset": "tassel_flower.n.01", "name": "tassel_flower"}, + {"id": 18493, "synset": "brittlebush.n.01", "name": "brittlebush"}, + {"id": 18494, "synset": "sunray.n.02", "name": "sunray"}, + {"id": 18495, "synset": "engelmannia.n.01", "name": "engelmannia"}, + {"id": 18496, "synset": "fireweed.n.02", "name": "fireweed"}, + {"id": 18497, "synset": "fleabane.n.02", "name": "fleabane"}, + {"id": 18498, "synset": "blue_fleabane.n.01", "name": "blue_fleabane"}, + {"id": 18499, "synset": "daisy_fleabane.n.01", "name": "daisy_fleabane"}, + {"id": 18500, "synset": "orange_daisy.n.01", "name": "orange_daisy"}, + {"id": 18501, "synset": "spreading_fleabane.n.01", "name": "spreading_fleabane"}, + {"id": 18502, "synset": "seaside_daisy.n.01", "name": "seaside_daisy"}, + {"id": 18503, "synset": "philadelphia_fleabane.n.01", "name": "Philadelphia_fleabane"}, + {"id": 18504, "synset": "robin's_plantain.n.01", "name": "robin's_plantain"}, + {"id": 18505, "synset": "showy_daisy.n.01", "name": "showy_daisy"}, + {"id": 18506, "synset": "woolly_sunflower.n.01", "name": "woolly_sunflower"}, + {"id": 18507, "synset": "golden_yarrow.n.01", "name": "golden_yarrow"}, + {"id": 18508, "synset": "dog_fennel.n.01", "name": "dog_fennel"}, + {"id": 18509, "synset": "joe-pye_weed.n.02", "name": "Joe-Pye_weed"}, + {"id": 18510, "synset": "boneset.n.02", "name": "boneset"}, + {"id": 18511, "synset": "joe-pye_weed.n.01", "name": "Joe-Pye_weed"}, + {"id": 18512, "synset": "blue_daisy.n.01", "name": "blue_daisy"}, + {"id": 18513, "synset": "kingfisher_daisy.n.01", "name": "kingfisher_daisy"}, + {"id": 18514, "synset": "cotton_rose.n.02", "name": "cotton_rose"}, + {"id": 18515, "synset": "herba_impia.n.01", "name": "herba_impia"}, + {"id": 18516, "synset": "gaillardia.n.01", "name": "gaillardia"}, + {"id": 18517, "synset": "gazania.n.01", "name": "gazania"}, + {"id": 18518, "synset": "treasure_flower.n.01", "name": "treasure_flower"}, + {"id": 18519, "synset": "african_daisy.n.02", "name": "African_daisy"}, + {"id": 18520, "synset": "barberton_daisy.n.01", "name": "Barberton_daisy"}, + {"id": 18521, "synset": "desert_sunflower.n.01", "name": "desert_sunflower"}, + {"id": 18522, "synset": "cudweed.n.01", "name": "cudweed"}, + {"id": 18523, "synset": "chafeweed.n.01", "name": "chafeweed"}, + {"id": 18524, "synset": "gumweed.n.01", "name": "gumweed"}, + {"id": 18525, "synset": "grindelia_robusta.n.01", "name": "Grindelia_robusta"}, + {"id": 18526, "synset": "curlycup_gumweed.n.01", "name": "curlycup_gumweed"}, + {"id": 18527, "synset": "little-head_snakeweed.n.01", "name": "little-head_snakeweed"}, + {"id": 18528, "synset": "rabbitweed.n.01", "name": "rabbitweed"}, + {"id": 18529, "synset": "broomweed.n.01", "name": "broomweed"}, + {"id": 18530, "synset": "velvet_plant.n.02", "name": "velvet_plant"}, + {"id": 18531, "synset": "goldenbush.n.01", "name": "goldenbush"}, + {"id": 18532, "synset": "camphor_daisy.n.01", "name": "camphor_daisy"}, + {"id": 18533, "synset": "yellow_spiny_daisy.n.01", "name": "yellow_spiny_daisy"}, + {"id": 18534, "synset": "hoary_golden_bush.n.01", "name": "hoary_golden_bush"}, + {"id": 18535, "synset": "sneezeweed.n.01", "name": "sneezeweed"}, + {"id": 18536, "synset": "orange_sneezeweed.n.01", "name": "orange_sneezeweed"}, + {"id": 18537, "synset": "rosilla.n.01", "name": "rosilla"}, + {"id": 18538, "synset": "swamp_sunflower.n.01", "name": "swamp_sunflower"}, + {"id": 18539, "synset": "common_sunflower.n.01", "name": "common_sunflower"}, + {"id": 18540, "synset": "giant_sunflower.n.01", "name": "giant_sunflower"}, + {"id": 18541, "synset": "showy_sunflower.n.01", "name": "showy_sunflower"}, + {"id": 18542, "synset": "maximilian's_sunflower.n.01", "name": "Maximilian's_sunflower"}, + {"id": 18543, "synset": "prairie_sunflower.n.01", "name": "prairie_sunflower"}, + {"id": 18544, "synset": "jerusalem_artichoke.n.02", "name": "Jerusalem_artichoke"}, + {"id": 18545, "synset": "jerusalem_artichoke.n.01", "name": "Jerusalem_artichoke"}, + {"id": 18546, "synset": "strawflower.n.03", "name": "strawflower"}, + {"id": 18547, "synset": "heliopsis.n.01", "name": "heliopsis"}, + {"id": 18548, "synset": "strawflower.n.02", "name": "strawflower"}, + {"id": 18549, "synset": "hairy_golden_aster.n.01", "name": "hairy_golden_aster"}, + {"id": 18550, "synset": "hawkweed.n.02", "name": "hawkweed"}, + {"id": 18551, "synset": "rattlesnake_weed.n.01", "name": "rattlesnake_weed"}, + {"id": 18552, "synset": "alpine_coltsfoot.n.01", "name": "alpine_coltsfoot"}, + {"id": 18553, "synset": "alpine_gold.n.01", "name": "alpine_gold"}, + {"id": 18554, "synset": "dwarf_hulsea.n.01", "name": "dwarf_hulsea"}, + {"id": 18555, "synset": "cat's-ear.n.02", "name": "cat's-ear"}, + {"id": 18556, "synset": "inula.n.01", "name": "inula"}, + {"id": 18557, "synset": "marsh_elder.n.01", "name": "marsh_elder"}, + {"id": 18558, "synset": "burweed_marsh_elder.n.01", "name": "burweed_marsh_elder"}, + {"id": 18559, "synset": "krigia.n.01", "name": "krigia"}, + {"id": 18560, "synset": "dwarf_dandelion.n.01", "name": "dwarf_dandelion"}, + {"id": 18561, "synset": "garden_lettuce.n.01", "name": "garden_lettuce"}, + {"id": 18562, "synset": "cos_lettuce.n.01", "name": "cos_lettuce"}, + {"id": 18563, "synset": "leaf_lettuce.n.01", "name": "leaf_lettuce"}, + {"id": 18564, "synset": "celtuce.n.01", "name": "celtuce"}, + {"id": 18565, "synset": "prickly_lettuce.n.01", "name": "prickly_lettuce"}, + {"id": 18566, "synset": "goldfields.n.01", "name": "goldfields"}, + {"id": 18567, "synset": "tidytips.n.01", "name": "tidytips"}, + {"id": 18568, "synset": "hawkbit.n.01", "name": "hawkbit"}, + {"id": 18569, "synset": "fall_dandelion.n.01", "name": "fall_dandelion"}, + {"id": 18570, "synset": "edelweiss.n.01", "name": "edelweiss"}, + {"id": 18571, "synset": "oxeye_daisy.n.02", "name": "oxeye_daisy"}, + {"id": 18572, "synset": "oxeye_daisy.n.01", "name": "oxeye_daisy"}, + {"id": 18573, "synset": "shasta_daisy.n.01", "name": "shasta_daisy"}, + {"id": 18574, "synset": "pyrenees_daisy.n.01", "name": "Pyrenees_daisy"}, + {"id": 18575, "synset": "north_island_edelweiss.n.01", "name": "north_island_edelweiss"}, + {"id": 18576, "synset": "blazing_star.n.02", "name": "blazing_star"}, + {"id": 18577, "synset": "dotted_gayfeather.n.01", "name": "dotted_gayfeather"}, + {"id": 18578, "synset": "dense_blazing_star.n.01", "name": "dense_blazing_star"}, + {"id": 18579, "synset": "texas_star.n.02", "name": "Texas_star"}, + {"id": 18580, "synset": "african_daisy.n.01", "name": "African_daisy"}, + {"id": 18581, "synset": "tahoka_daisy.n.01", "name": "tahoka_daisy"}, + {"id": 18582, "synset": "sticky_aster.n.01", "name": "sticky_aster"}, + {"id": 18583, "synset": "mojave_aster.n.01", "name": "Mojave_aster"}, + {"id": 18584, "synset": "tarweed.n.01", "name": "tarweed"}, + {"id": 18585, "synset": "sweet_false_chamomile.n.01", "name": "sweet_false_chamomile"}, + {"id": 18586, "synset": "pineapple_weed.n.01", "name": "pineapple_weed"}, + {"id": 18587, "synset": "climbing_hempweed.n.01", "name": "climbing_hempweed"}, + {"id": 18588, "synset": "mutisia.n.01", "name": "mutisia"}, + {"id": 18589, "synset": "rattlesnake_root.n.02", "name": "rattlesnake_root"}, + {"id": 18590, "synset": "white_lettuce.n.01", "name": "white_lettuce"}, + {"id": 18591, "synset": "daisybush.n.01", "name": "daisybush"}, + {"id": 18592, "synset": "new_zealand_daisybush.n.01", "name": "New_Zealand_daisybush"}, + {"id": 18593, "synset": "cotton_thistle.n.01", "name": "cotton_thistle"}, + {"id": 18594, "synset": "othonna.n.01", "name": "othonna"}, + {"id": 18595, "synset": "cascade_everlasting.n.01", "name": "cascade_everlasting"}, + {"id": 18596, "synset": "butterweed.n.02", "name": "butterweed"}, + {"id": 18597, "synset": "american_feverfew.n.01", "name": "American_feverfew"}, + {"id": 18598, "synset": "cineraria.n.01", "name": "cineraria"}, + {"id": 18599, "synset": "florest's_cineraria.n.01", "name": "florest's_cineraria"}, + {"id": 18600, "synset": "butterbur.n.01", "name": "butterbur"}, + {"id": 18601, "synset": "winter_heliotrope.n.01", "name": "winter_heliotrope"}, + {"id": 18602, "synset": "sweet_coltsfoot.n.01", "name": "sweet_coltsfoot"}, + {"id": 18603, "synset": "oxtongue.n.01", "name": "oxtongue"}, + {"id": 18604, "synset": "hawkweed.n.01", "name": "hawkweed"}, + {"id": 18605, "synset": "mouse-ear_hawkweed.n.01", "name": "mouse-ear_hawkweed"}, + {"id": 18606, "synset": "stevia.n.02", "name": "stevia"}, + {"id": 18607, "synset": "rattlesnake_root.n.01", "name": "rattlesnake_root"}, + {"id": 18608, "synset": "fleabane.n.01", "name": "fleabane"}, + {"id": 18609, "synset": "sheep_plant.n.01", "name": "sheep_plant"}, + {"id": 18610, "synset": "coneflower.n.02", "name": "coneflower"}, + {"id": 18611, "synset": "mexican_hat.n.01", "name": "Mexican_hat"}, + {"id": 18612, "synset": "long-head_coneflower.n.01", "name": "long-head_coneflower"}, + {"id": 18613, "synset": "prairie_coneflower.n.01", "name": "prairie_coneflower"}, + {"id": 18614, "synset": "swan_river_everlasting.n.01", "name": "Swan_River_everlasting"}, + {"id": 18615, "synset": "coneflower.n.01", "name": "coneflower"}, + {"id": 18616, "synset": "black-eyed_susan.n.03", "name": "black-eyed_Susan"}, + {"id": 18617, "synset": "cutleaved_coneflower.n.01", "name": "cutleaved_coneflower"}, + {"id": 18618, "synset": "golden_glow.n.01", "name": "golden_glow"}, + {"id": 18619, "synset": "lavender_cotton.n.01", "name": "lavender_cotton"}, + {"id": 18620, "synset": "creeping_zinnia.n.01", "name": "creeping_zinnia"}, + {"id": 18621, "synset": "golden_thistle.n.01", "name": "golden_thistle"}, + {"id": 18622, "synset": "spanish_oyster_plant.n.01", "name": "Spanish_oyster_plant"}, + {"id": 18623, "synset": "nodding_groundsel.n.01", "name": "nodding_groundsel"}, + {"id": 18624, "synset": "dusty_miller.n.02", "name": "dusty_miller"}, + {"id": 18625, "synset": "butterweed.n.01", "name": "butterweed"}, + {"id": 18626, "synset": "ragwort.n.01", "name": "ragwort"}, + {"id": 18627, "synset": "arrowleaf_groundsel.n.01", "name": "arrowleaf_groundsel"}, + {"id": 18628, "synset": "black_salsify.n.01", "name": "black_salsify"}, + {"id": 18629, "synset": "white-topped_aster.n.01", "name": "white-topped_aster"}, + { + "id": 18630, + "synset": "narrow-leaved_white-topped_aster.n.01", + "name": "narrow-leaved_white-topped_aster", + }, + {"id": 18631, "synset": "silver_sage.n.01", "name": "silver_sage"}, + {"id": 18632, "synset": "sea_wormwood.n.01", "name": "sea_wormwood"}, + {"id": 18633, "synset": "sawwort.n.01", "name": "sawwort"}, + {"id": 18634, "synset": "rosinweed.n.01", "name": "rosinweed"}, + {"id": 18635, "synset": "milk_thistle.n.02", "name": "milk_thistle"}, + {"id": 18636, "synset": "goldenrod.n.01", "name": "goldenrod"}, + {"id": 18637, "synset": "silverrod.n.01", "name": "silverrod"}, + {"id": 18638, "synset": "meadow_goldenrod.n.01", "name": "meadow_goldenrod"}, + {"id": 18639, "synset": "missouri_goldenrod.n.01", "name": "Missouri_goldenrod"}, + {"id": 18640, "synset": "alpine_goldenrod.n.01", "name": "alpine_goldenrod"}, + {"id": 18641, "synset": "grey_goldenrod.n.01", "name": "grey_goldenrod"}, + {"id": 18642, "synset": "blue_mountain_tea.n.01", "name": "Blue_Mountain_tea"}, + {"id": 18643, "synset": "dyer's_weed.n.01", "name": "dyer's_weed"}, + {"id": 18644, "synset": "seaside_goldenrod.n.01", "name": "seaside_goldenrod"}, + {"id": 18645, "synset": "narrow_goldenrod.n.01", "name": "narrow_goldenrod"}, + {"id": 18646, "synset": "boott's_goldenrod.n.01", "name": "Boott's_goldenrod"}, + {"id": 18647, "synset": "elliott's_goldenrod.n.01", "name": "Elliott's_goldenrod"}, + {"id": 18648, "synset": "ohio_goldenrod.n.01", "name": "Ohio_goldenrod"}, + {"id": 18649, "synset": "rough-stemmed_goldenrod.n.01", "name": "rough-stemmed_goldenrod"}, + {"id": 18650, "synset": "showy_goldenrod.n.01", "name": "showy_goldenrod"}, + {"id": 18651, "synset": "tall_goldenrod.n.01", "name": "tall_goldenrod"}, + {"id": 18652, "synset": "zigzag_goldenrod.n.01", "name": "zigzag_goldenrod"}, + {"id": 18653, "synset": "sow_thistle.n.01", "name": "sow_thistle"}, + {"id": 18654, "synset": "milkweed.n.02", "name": "milkweed"}, + {"id": 18655, "synset": "stevia.n.01", "name": "stevia"}, + {"id": 18656, "synset": "stokes'_aster.n.01", "name": "stokes'_aster"}, + {"id": 18657, "synset": "marigold.n.01", "name": "marigold"}, + {"id": 18658, "synset": "african_marigold.n.01", "name": "African_marigold"}, + {"id": 18659, "synset": "french_marigold.n.01", "name": "French_marigold"}, + {"id": 18660, "synset": "painted_daisy.n.01", "name": "painted_daisy"}, + {"id": 18661, "synset": "pyrethrum.n.02", "name": "pyrethrum"}, + {"id": 18662, "synset": "northern_dune_tansy.n.01", "name": "northern_dune_tansy"}, + {"id": 18663, "synset": "feverfew.n.01", "name": "feverfew"}, + {"id": 18664, "synset": "dusty_miller.n.01", "name": "dusty_miller"}, + {"id": 18665, "synset": "tansy.n.01", "name": "tansy"}, + {"id": 18666, "synset": "dandelion.n.01", "name": "dandelion"}, + {"id": 18667, "synset": "common_dandelion.n.01", "name": "common_dandelion"}, + {"id": 18668, "synset": "dandelion_green.n.01", "name": "dandelion_green"}, + {"id": 18669, "synset": "russian_dandelion.n.01", "name": "Russian_dandelion"}, + {"id": 18670, "synset": "stemless_hymenoxys.n.01", "name": "stemless_hymenoxys"}, + {"id": 18671, "synset": "mexican_sunflower.n.01", "name": "Mexican_sunflower"}, + {"id": 18672, "synset": "easter_daisy.n.01", "name": "Easter_daisy"}, + {"id": 18673, "synset": "yellow_salsify.n.01", "name": "yellow_salsify"}, + {"id": 18674, "synset": "salsify.n.02", "name": "salsify"}, + {"id": 18675, "synset": "meadow_salsify.n.01", "name": "meadow_salsify"}, + {"id": 18676, "synset": "scentless_camomile.n.01", "name": "scentless_camomile"}, + {"id": 18677, "synset": "turfing_daisy.n.01", "name": "turfing_daisy"}, + {"id": 18678, "synset": "coltsfoot.n.02", "name": "coltsfoot"}, + {"id": 18679, "synset": "ursinia.n.01", "name": "ursinia"}, + {"id": 18680, "synset": "crownbeard.n.01", "name": "crownbeard"}, + {"id": 18681, "synset": "wingstem.n.01", "name": "wingstem"}, + {"id": 18682, "synset": "cowpen_daisy.n.01", "name": "cowpen_daisy"}, + {"id": 18683, "synset": "gravelweed.n.01", "name": "gravelweed"}, + {"id": 18684, "synset": "virginia_crownbeard.n.01", "name": "Virginia_crownbeard"}, + {"id": 18685, "synset": "ironweed.n.01", "name": "ironweed"}, + {"id": 18686, "synset": "mule's_ears.n.01", "name": "mule's_ears"}, + {"id": 18687, "synset": "white-rayed_mule's_ears.n.01", "name": "white-rayed_mule's_ears"}, + {"id": 18688, "synset": "cocklebur.n.01", "name": "cocklebur"}, + {"id": 18689, "synset": "xeranthemum.n.01", "name": "xeranthemum"}, + {"id": 18690, "synset": "immortelle.n.01", "name": "immortelle"}, + {"id": 18691, "synset": "zinnia.n.01", "name": "zinnia"}, + {"id": 18692, "synset": "white_zinnia.n.01", "name": "white_zinnia"}, + {"id": 18693, "synset": "little_golden_zinnia.n.01", "name": "little_golden_zinnia"}, + {"id": 18694, "synset": "blazing_star.n.01", "name": "blazing_star"}, + {"id": 18695, "synset": "bartonia.n.01", "name": "bartonia"}, + {"id": 18696, "synset": "achene.n.01", "name": "achene"}, + {"id": 18697, "synset": "samara.n.01", "name": "samara"}, + {"id": 18698, "synset": "campanula.n.01", "name": "campanula"}, + {"id": 18699, "synset": "creeping_bellflower.n.01", "name": "creeping_bellflower"}, + {"id": 18700, "synset": "canterbury_bell.n.02", "name": "Canterbury_bell"}, + {"id": 18701, "synset": "tall_bellflower.n.01", "name": "tall_bellflower"}, + {"id": 18702, "synset": "marsh_bellflower.n.01", "name": "marsh_bellflower"}, + {"id": 18703, "synset": "clustered_bellflower.n.01", "name": "clustered_bellflower"}, + {"id": 18704, "synset": "peach_bells.n.01", "name": "peach_bells"}, + {"id": 18705, "synset": "chimney_plant.n.01", "name": "chimney_plant"}, + {"id": 18706, "synset": "rampion.n.01", "name": "rampion"}, + {"id": 18707, "synset": "tussock_bellflower.n.01", "name": "tussock_bellflower"}, + {"id": 18708, "synset": "orchid.n.01", "name": "orchid"}, + {"id": 18709, "synset": "orchis.n.01", "name": "orchis"}, + {"id": 18710, "synset": "male_orchis.n.01", "name": "male_orchis"}, + {"id": 18711, "synset": "butterfly_orchid.n.05", "name": "butterfly_orchid"}, + {"id": 18712, "synset": "showy_orchis.n.01", "name": "showy_orchis"}, + {"id": 18713, "synset": "aerides.n.01", "name": "aerides"}, + {"id": 18714, "synset": "angrecum.n.01", "name": "angrecum"}, + {"id": 18715, "synset": "jewel_orchid.n.01", "name": "jewel_orchid"}, + {"id": 18716, "synset": "puttyroot.n.01", "name": "puttyroot"}, + {"id": 18717, "synset": "arethusa.n.01", "name": "arethusa"}, + {"id": 18718, "synset": "bog_rose.n.01", "name": "bog_rose"}, + {"id": 18719, "synset": "bletia.n.01", "name": "bletia"}, + {"id": 18720, "synset": "bletilla_striata.n.01", "name": "Bletilla_striata"}, + {"id": 18721, "synset": "brassavola.n.01", "name": "brassavola"}, + {"id": 18722, "synset": "spider_orchid.n.03", "name": "spider_orchid"}, + {"id": 18723, "synset": "spider_orchid.n.02", "name": "spider_orchid"}, + {"id": 18724, "synset": "caladenia.n.01", "name": "caladenia"}, + {"id": 18725, "synset": "calanthe.n.01", "name": "calanthe"}, + {"id": 18726, "synset": "grass_pink.n.01", "name": "grass_pink"}, + {"id": 18727, "synset": "calypso.n.01", "name": "calypso"}, + {"id": 18728, "synset": "cattleya.n.01", "name": "cattleya"}, + {"id": 18729, "synset": "helleborine.n.03", "name": "helleborine"}, + {"id": 18730, "synset": "red_helleborine.n.01", "name": "red_helleborine"}, + {"id": 18731, "synset": "spreading_pogonia.n.01", "name": "spreading_pogonia"}, + {"id": 18732, "synset": "rosebud_orchid.n.01", "name": "rosebud_orchid"}, + {"id": 18733, "synset": "satyr_orchid.n.01", "name": "satyr_orchid"}, + {"id": 18734, "synset": "frog_orchid.n.02", "name": "frog_orchid"}, + {"id": 18735, "synset": "coelogyne.n.01", "name": "coelogyne"}, + {"id": 18736, "synset": "coral_root.n.01", "name": "coral_root"}, + {"id": 18737, "synset": "spotted_coral_root.n.01", "name": "spotted_coral_root"}, + {"id": 18738, "synset": "striped_coral_root.n.01", "name": "striped_coral_root"}, + {"id": 18739, "synset": "early_coral_root.n.01", "name": "early_coral_root"}, + {"id": 18740, "synset": "swan_orchid.n.01", "name": "swan_orchid"}, + {"id": 18741, "synset": "cymbid.n.01", "name": "cymbid"}, + {"id": 18742, "synset": "cypripedia.n.01", "name": "cypripedia"}, + {"id": 18743, "synset": "lady's_slipper.n.01", "name": "lady's_slipper"}, + {"id": 18744, "synset": "moccasin_flower.n.01", "name": "moccasin_flower"}, + {"id": 18745, "synset": "common_lady's-slipper.n.01", "name": "common_lady's-slipper"}, + {"id": 18746, "synset": "ram's-head.n.01", "name": "ram's-head"}, + {"id": 18747, "synset": "yellow_lady's_slipper.n.01", "name": "yellow_lady's_slipper"}, + { + "id": 18748, + "synset": "large_yellow_lady's_slipper.n.01", + "name": "large_yellow_lady's_slipper", + }, + {"id": 18749, "synset": "california_lady's_slipper.n.01", "name": "California_lady's_slipper"}, + {"id": 18750, "synset": "clustered_lady's_slipper.n.01", "name": "clustered_lady's_slipper"}, + {"id": 18751, "synset": "mountain_lady's_slipper.n.01", "name": "mountain_lady's_slipper"}, + {"id": 18752, "synset": "marsh_orchid.n.01", "name": "marsh_orchid"}, + {"id": 18753, "synset": "common_spotted_orchid.n.01", "name": "common_spotted_orchid"}, + {"id": 18754, "synset": "dendrobium.n.01", "name": "dendrobium"}, + {"id": 18755, "synset": "disa.n.01", "name": "disa"}, + {"id": 18756, "synset": "phantom_orchid.n.01", "name": "phantom_orchid"}, + {"id": 18757, "synset": "tulip_orchid.n.01", "name": "tulip_orchid"}, + {"id": 18758, "synset": "butterfly_orchid.n.04", "name": "butterfly_orchid"}, + {"id": 18759, "synset": "butterfly_orchid.n.03", "name": "butterfly_orchid"}, + {"id": 18760, "synset": "epidendron.n.01", "name": "epidendron"}, + {"id": 18761, "synset": "helleborine.n.02", "name": "helleborine"}, + {"id": 18762, "synset": "epipactis_helleborine.n.01", "name": "Epipactis_helleborine"}, + {"id": 18763, "synset": "stream_orchid.n.01", "name": "stream_orchid"}, + {"id": 18764, "synset": "tongueflower.n.01", "name": "tongueflower"}, + {"id": 18765, "synset": "rattlesnake_plantain.n.01", "name": "rattlesnake_plantain"}, + {"id": 18766, "synset": "fragrant_orchid.n.01", "name": "fragrant_orchid"}, + { + "id": 18767, + "synset": "short-spurred_fragrant_orchid.n.01", + "name": "short-spurred_fragrant_orchid", + }, + {"id": 18768, "synset": "fringed_orchis.n.01", "name": "fringed_orchis"}, + {"id": 18769, "synset": "frog_orchid.n.01", "name": "frog_orchid"}, + {"id": 18770, "synset": "rein_orchid.n.01", "name": "rein_orchid"}, + {"id": 18771, "synset": "bog_rein_orchid.n.01", "name": "bog_rein_orchid"}, + {"id": 18772, "synset": "white_fringed_orchis.n.01", "name": "white_fringed_orchis"}, + {"id": 18773, "synset": "elegant_habenaria.n.01", "name": "elegant_Habenaria"}, + {"id": 18774, "synset": "purple-fringed_orchid.n.02", "name": "purple-fringed_orchid"}, + {"id": 18775, "synset": "coastal_rein_orchid.n.01", "name": "coastal_rein_orchid"}, + {"id": 18776, "synset": "hooker's_orchid.n.01", "name": "Hooker's_orchid"}, + {"id": 18777, "synset": "ragged_orchid.n.01", "name": "ragged_orchid"}, + {"id": 18778, "synset": "prairie_orchid.n.01", "name": "prairie_orchid"}, + {"id": 18779, "synset": "snowy_orchid.n.01", "name": "snowy_orchid"}, + {"id": 18780, "synset": "round-leaved_rein_orchid.n.01", "name": "round-leaved_rein_orchid"}, + {"id": 18781, "synset": "purple_fringeless_orchid.n.01", "name": "purple_fringeless_orchid"}, + {"id": 18782, "synset": "purple-fringed_orchid.n.01", "name": "purple-fringed_orchid"}, + {"id": 18783, "synset": "alaska_rein_orchid.n.01", "name": "Alaska_rein_orchid"}, + {"id": 18784, "synset": "crested_coral_root.n.01", "name": "crested_coral_root"}, + {"id": 18785, "synset": "texas_purple_spike.n.01", "name": "Texas_purple_spike"}, + {"id": 18786, "synset": "lizard_orchid.n.01", "name": "lizard_orchid"}, + {"id": 18787, "synset": "laelia.n.01", "name": "laelia"}, + {"id": 18788, "synset": "liparis.n.01", "name": "liparis"}, + {"id": 18789, "synset": "twayblade.n.02", "name": "twayblade"}, + {"id": 18790, "synset": "fen_orchid.n.01", "name": "fen_orchid"}, + {"id": 18791, "synset": "broad-leaved_twayblade.n.01", "name": "broad-leaved_twayblade"}, + {"id": 18792, "synset": "lesser_twayblade.n.01", "name": "lesser_twayblade"}, + {"id": 18793, "synset": "twayblade.n.01", "name": "twayblade"}, + {"id": 18794, "synset": "green_adder's_mouth.n.01", "name": "green_adder's_mouth"}, + {"id": 18795, "synset": "masdevallia.n.01", "name": "masdevallia"}, + {"id": 18796, "synset": "maxillaria.n.01", "name": "maxillaria"}, + {"id": 18797, "synset": "pansy_orchid.n.01", "name": "pansy_orchid"}, + {"id": 18798, "synset": "odontoglossum.n.01", "name": "odontoglossum"}, + {"id": 18799, "synset": "oncidium.n.01", "name": "oncidium"}, + {"id": 18800, "synset": "bee_orchid.n.01", "name": "bee_orchid"}, + {"id": 18801, "synset": "fly_orchid.n.02", "name": "fly_orchid"}, + {"id": 18802, "synset": "spider_orchid.n.01", "name": "spider_orchid"}, + {"id": 18803, "synset": "early_spider_orchid.n.01", "name": "early_spider_orchid"}, + {"id": 18804, "synset": "venus'_slipper.n.01", "name": "Venus'_slipper"}, + {"id": 18805, "synset": "phaius.n.01", "name": "phaius"}, + {"id": 18806, "synset": "moth_orchid.n.01", "name": "moth_orchid"}, + {"id": 18807, "synset": "butterfly_plant.n.01", "name": "butterfly_plant"}, + {"id": 18808, "synset": "rattlesnake_orchid.n.01", "name": "rattlesnake_orchid"}, + {"id": 18809, "synset": "lesser_butterfly_orchid.n.01", "name": "lesser_butterfly_orchid"}, + {"id": 18810, "synset": "greater_butterfly_orchid.n.01", "name": "greater_butterfly_orchid"}, + { + "id": 18811, + "synset": "prairie_white-fringed_orchid.n.01", + "name": "prairie_white-fringed_orchid", + }, + {"id": 18812, "synset": "tangle_orchid.n.01", "name": "tangle_orchid"}, + {"id": 18813, "synset": "indian_crocus.n.01", "name": "Indian_crocus"}, + {"id": 18814, "synset": "pleurothallis.n.01", "name": "pleurothallis"}, + {"id": 18815, "synset": "pogonia.n.01", "name": "pogonia"}, + {"id": 18816, "synset": "butterfly_orchid.n.01", "name": "butterfly_orchid"}, + {"id": 18817, "synset": "psychopsis_krameriana.n.01", "name": "Psychopsis_krameriana"}, + {"id": 18818, "synset": "psychopsis_papilio.n.01", "name": "Psychopsis_papilio"}, + {"id": 18819, "synset": "helmet_orchid.n.01", "name": "helmet_orchid"}, + {"id": 18820, "synset": "foxtail_orchid.n.01", "name": "foxtail_orchid"}, + {"id": 18821, "synset": "orange-blossom_orchid.n.01", "name": "orange-blossom_orchid"}, + {"id": 18822, "synset": "sobralia.n.01", "name": "sobralia"}, + {"id": 18823, "synset": "ladies'_tresses.n.01", "name": "ladies'_tresses"}, + {"id": 18824, "synset": "screw_augur.n.01", "name": "screw_augur"}, + {"id": 18825, "synset": "hooded_ladies'_tresses.n.01", "name": "hooded_ladies'_tresses"}, + {"id": 18826, "synset": "western_ladies'_tresses.n.01", "name": "western_ladies'_tresses"}, + {"id": 18827, "synset": "european_ladies'_tresses.n.01", "name": "European_ladies'_tresses"}, + {"id": 18828, "synset": "stanhopea.n.01", "name": "stanhopea"}, + {"id": 18829, "synset": "stelis.n.01", "name": "stelis"}, + {"id": 18830, "synset": "fly_orchid.n.01", "name": "fly_orchid"}, + {"id": 18831, "synset": "vanda.n.01", "name": "vanda"}, + {"id": 18832, "synset": "blue_orchid.n.01", "name": "blue_orchid"}, + {"id": 18833, "synset": "vanilla.n.01", "name": "vanilla"}, + {"id": 18834, "synset": "vanilla_orchid.n.01", "name": "vanilla_orchid"}, + {"id": 18835, "synset": "yam.n.02", "name": "yam"}, + {"id": 18836, "synset": "yam.n.01", "name": "yam"}, + {"id": 18837, "synset": "white_yam.n.01", "name": "white_yam"}, + {"id": 18838, "synset": "cinnamon_vine.n.01", "name": "cinnamon_vine"}, + {"id": 18839, "synset": "elephant's-foot.n.01", "name": "elephant's-foot"}, + {"id": 18840, "synset": "wild_yam.n.01", "name": "wild_yam"}, + {"id": 18841, "synset": "cush-cush.n.01", "name": "cush-cush"}, + {"id": 18842, "synset": "black_bryony.n.01", "name": "black_bryony"}, + {"id": 18843, "synset": "primrose.n.01", "name": "primrose"}, + {"id": 18844, "synset": "english_primrose.n.01", "name": "English_primrose"}, + {"id": 18845, "synset": "cowslip.n.01", "name": "cowslip"}, + {"id": 18846, "synset": "oxlip.n.01", "name": "oxlip"}, + {"id": 18847, "synset": "chinese_primrose.n.01", "name": "Chinese_primrose"}, + {"id": 18848, "synset": "polyanthus.n.01", "name": "polyanthus"}, + {"id": 18849, "synset": "pimpernel.n.02", "name": "pimpernel"}, + {"id": 18850, "synset": "scarlet_pimpernel.n.01", "name": "scarlet_pimpernel"}, + {"id": 18851, "synset": "bog_pimpernel.n.01", "name": "bog_pimpernel"}, + {"id": 18852, "synset": "chaffweed.n.01", "name": "chaffweed"}, + {"id": 18853, "synset": "cyclamen.n.01", "name": "cyclamen"}, + {"id": 18854, "synset": "sowbread.n.01", "name": "sowbread"}, + {"id": 18855, "synset": "sea_milkwort.n.01", "name": "sea_milkwort"}, + {"id": 18856, "synset": "featherfoil.n.01", "name": "featherfoil"}, + {"id": 18857, "synset": "water_gillyflower.n.01", "name": "water_gillyflower"}, + {"id": 18858, "synset": "water_violet.n.01", "name": "water_violet"}, + {"id": 18859, "synset": "loosestrife.n.02", "name": "loosestrife"}, + {"id": 18860, "synset": "gooseneck_loosestrife.n.01", "name": "gooseneck_loosestrife"}, + {"id": 18861, "synset": "yellow_pimpernel.n.01", "name": "yellow_pimpernel"}, + {"id": 18862, "synset": "fringed_loosestrife.n.01", "name": "fringed_loosestrife"}, + {"id": 18863, "synset": "moneywort.n.01", "name": "moneywort"}, + {"id": 18864, "synset": "swamp_candles.n.01", "name": "swamp_candles"}, + {"id": 18865, "synset": "whorled_loosestrife.n.01", "name": "whorled_loosestrife"}, + {"id": 18866, "synset": "water_pimpernel.n.01", "name": "water_pimpernel"}, + {"id": 18867, "synset": "brookweed.n.02", "name": "brookweed"}, + {"id": 18868, "synset": "brookweed.n.01", "name": "brookweed"}, + {"id": 18869, "synset": "coralberry.n.02", "name": "coralberry"}, + {"id": 18870, "synset": "marlberry.n.01", "name": "marlberry"}, + {"id": 18871, "synset": "plumbago.n.02", "name": "plumbago"}, + {"id": 18872, "synset": "leadwort.n.01", "name": "leadwort"}, + {"id": 18873, "synset": "thrift.n.01", "name": "thrift"}, + {"id": 18874, "synset": "sea_lavender.n.01", "name": "sea_lavender"}, + {"id": 18875, "synset": "barbasco.n.01", "name": "barbasco"}, + {"id": 18876, "synset": "gramineous_plant.n.01", "name": "gramineous_plant"}, + {"id": 18877, "synset": "grass.n.01", "name": "grass"}, + {"id": 18878, "synset": "midgrass.n.01", "name": "midgrass"}, + {"id": 18879, "synset": "shortgrass.n.01", "name": "shortgrass"}, + {"id": 18880, "synset": "sword_grass.n.01", "name": "sword_grass"}, + {"id": 18881, "synset": "tallgrass.n.01", "name": "tallgrass"}, + {"id": 18882, "synset": "herbage.n.01", "name": "herbage"}, + {"id": 18883, "synset": "goat_grass.n.01", "name": "goat_grass"}, + {"id": 18884, "synset": "wheatgrass.n.01", "name": "wheatgrass"}, + {"id": 18885, "synset": "crested_wheatgrass.n.01", "name": "crested_wheatgrass"}, + {"id": 18886, "synset": "bearded_wheatgrass.n.01", "name": "bearded_wheatgrass"}, + {"id": 18887, "synset": "western_wheatgrass.n.01", "name": "western_wheatgrass"}, + {"id": 18888, "synset": "intermediate_wheatgrass.n.01", "name": "intermediate_wheatgrass"}, + {"id": 18889, "synset": "slender_wheatgrass.n.01", "name": "slender_wheatgrass"}, + {"id": 18890, "synset": "velvet_bent.n.01", "name": "velvet_bent"}, + {"id": 18891, "synset": "cloud_grass.n.01", "name": "cloud_grass"}, + {"id": 18892, "synset": "meadow_foxtail.n.01", "name": "meadow_foxtail"}, + {"id": 18893, "synset": "foxtail.n.01", "name": "foxtail"}, + {"id": 18894, "synset": "broom_grass.n.01", "name": "broom_grass"}, + {"id": 18895, "synset": "broom_sedge.n.01", "name": "broom_sedge"}, + {"id": 18896, "synset": "tall_oat_grass.n.01", "name": "tall_oat_grass"}, + {"id": 18897, "synset": "toetoe.n.02", "name": "toetoe"}, + {"id": 18898, "synset": "oat.n.01", "name": "oat"}, + {"id": 18899, "synset": "cereal_oat.n.01", "name": "cereal_oat"}, + {"id": 18900, "synset": "wild_oat.n.01", "name": "wild_oat"}, + {"id": 18901, "synset": "slender_wild_oat.n.01", "name": "slender_wild_oat"}, + {"id": 18902, "synset": "wild_red_oat.n.01", "name": "wild_red_oat"}, + {"id": 18903, "synset": "brome.n.01", "name": "brome"}, + {"id": 18904, "synset": "chess.n.01", "name": "chess"}, + {"id": 18905, "synset": "field_brome.n.01", "name": "field_brome"}, + {"id": 18906, "synset": "grama.n.01", "name": "grama"}, + {"id": 18907, "synset": "black_grama.n.01", "name": "black_grama"}, + {"id": 18908, "synset": "buffalo_grass.n.02", "name": "buffalo_grass"}, + {"id": 18909, "synset": "reed_grass.n.01", "name": "reed_grass"}, + {"id": 18910, "synset": "feather_reed_grass.n.01", "name": "feather_reed_grass"}, + {"id": 18911, "synset": "australian_reed_grass.n.01", "name": "Australian_reed_grass"}, + {"id": 18912, "synset": "burgrass.n.01", "name": "burgrass"}, + {"id": 18913, "synset": "buffel_grass.n.01", "name": "buffel_grass"}, + {"id": 18914, "synset": "rhodes_grass.n.01", "name": "Rhodes_grass"}, + {"id": 18915, "synset": "pampas_grass.n.01", "name": "pampas_grass"}, + {"id": 18916, "synset": "giant_star_grass.n.01", "name": "giant_star_grass"}, + {"id": 18917, "synset": "orchard_grass.n.01", "name": "orchard_grass"}, + {"id": 18918, "synset": "egyptian_grass.n.01", "name": "Egyptian_grass"}, + {"id": 18919, "synset": "crabgrass.n.01", "name": "crabgrass"}, + {"id": 18920, "synset": "smooth_crabgrass.n.01", "name": "smooth_crabgrass"}, + {"id": 18921, "synset": "large_crabgrass.n.01", "name": "large_crabgrass"}, + {"id": 18922, "synset": "barnyard_grass.n.01", "name": "barnyard_grass"}, + {"id": 18923, "synset": "japanese_millet.n.01", "name": "Japanese_millet"}, + {"id": 18924, "synset": "yardgrass.n.01", "name": "yardgrass"}, + {"id": 18925, "synset": "finger_millet.n.01", "name": "finger_millet"}, + {"id": 18926, "synset": "lyme_grass.n.01", "name": "lyme_grass"}, + {"id": 18927, "synset": "wild_rye.n.01", "name": "wild_rye"}, + {"id": 18928, "synset": "giant_ryegrass.n.01", "name": "giant_ryegrass"}, + {"id": 18929, "synset": "sea_lyme_grass.n.01", "name": "sea_lyme_grass"}, + {"id": 18930, "synset": "canada_wild_rye.n.01", "name": "Canada_wild_rye"}, + {"id": 18931, "synset": "teff.n.01", "name": "teff"}, + {"id": 18932, "synset": "weeping_love_grass.n.01", "name": "weeping_love_grass"}, + {"id": 18933, "synset": "plume_grass.n.01", "name": "plume_grass"}, + {"id": 18934, "synset": "ravenna_grass.n.01", "name": "Ravenna_grass"}, + {"id": 18935, "synset": "fescue.n.01", "name": "fescue"}, + {"id": 18936, "synset": "reed_meadow_grass.n.01", "name": "reed_meadow_grass"}, + {"id": 18937, "synset": "velvet_grass.n.01", "name": "velvet_grass"}, + {"id": 18938, "synset": "creeping_soft_grass.n.01", "name": "creeping_soft_grass"}, + {"id": 18939, "synset": "barleycorn.n.01", "name": "barleycorn"}, + {"id": 18940, "synset": "barley_grass.n.01", "name": "barley_grass"}, + {"id": 18941, "synset": "little_barley.n.01", "name": "little_barley"}, + {"id": 18942, "synset": "rye_grass.n.01", "name": "rye_grass"}, + {"id": 18943, "synset": "perennial_ryegrass.n.01", "name": "perennial_ryegrass"}, + {"id": 18944, "synset": "italian_ryegrass.n.01", "name": "Italian_ryegrass"}, + {"id": 18945, "synset": "darnel.n.01", "name": "darnel"}, + {"id": 18946, "synset": "nimblewill.n.01", "name": "nimblewill"}, + {"id": 18947, "synset": "cultivated_rice.n.01", "name": "cultivated_rice"}, + {"id": 18948, "synset": "ricegrass.n.01", "name": "ricegrass"}, + {"id": 18949, "synset": "smilo.n.01", "name": "smilo"}, + {"id": 18950, "synset": "switch_grass.n.01", "name": "switch_grass"}, + {"id": 18951, "synset": "broomcorn_millet.n.01", "name": "broomcorn_millet"}, + {"id": 18952, "synset": "goose_grass.n.03", "name": "goose_grass"}, + {"id": 18953, "synset": "dallisgrass.n.01", "name": "dallisgrass"}, + {"id": 18954, "synset": "bahia_grass.n.01", "name": "Bahia_grass"}, + {"id": 18955, "synset": "knotgrass.n.01", "name": "knotgrass"}, + {"id": 18956, "synset": "fountain_grass.n.01", "name": "fountain_grass"}, + {"id": 18957, "synset": "reed_canary_grass.n.01", "name": "reed_canary_grass"}, + {"id": 18958, "synset": "canary_grass.n.01", "name": "canary_grass"}, + {"id": 18959, "synset": "timothy.n.01", "name": "timothy"}, + {"id": 18960, "synset": "bluegrass.n.01", "name": "bluegrass"}, + {"id": 18961, "synset": "meadowgrass.n.01", "name": "meadowgrass"}, + {"id": 18962, "synset": "wood_meadowgrass.n.01", "name": "wood_meadowgrass"}, + {"id": 18963, "synset": "noble_cane.n.01", "name": "noble_cane"}, + {"id": 18964, "synset": "munj.n.01", "name": "munj"}, + {"id": 18965, "synset": "broom_beard_grass.n.01", "name": "broom_beard_grass"}, + {"id": 18966, "synset": "bluestem.n.01", "name": "bluestem"}, + {"id": 18967, "synset": "rye.n.02", "name": "rye"}, + {"id": 18968, "synset": "bristlegrass.n.01", "name": "bristlegrass"}, + {"id": 18969, "synset": "giant_foxtail.n.01", "name": "giant_foxtail"}, + {"id": 18970, "synset": "yellow_bristlegrass.n.01", "name": "yellow_bristlegrass"}, + {"id": 18971, "synset": "green_bristlegrass.n.01", "name": "green_bristlegrass"}, + {"id": 18972, "synset": "siberian_millet.n.01", "name": "Siberian_millet"}, + {"id": 18973, "synset": "german_millet.n.01", "name": "German_millet"}, + {"id": 18974, "synset": "millet.n.01", "name": "millet"}, + {"id": 18975, "synset": "rattan.n.02", "name": "rattan"}, + {"id": 18976, "synset": "malacca.n.01", "name": "malacca"}, + {"id": 18977, "synset": "reed.n.01", "name": "reed"}, + {"id": 18978, "synset": "sorghum.n.01", "name": "sorghum"}, + {"id": 18979, "synset": "grain_sorghum.n.01", "name": "grain_sorghum"}, + {"id": 18980, "synset": "durra.n.01", "name": "durra"}, + {"id": 18981, "synset": "feterita.n.01", "name": "feterita"}, + {"id": 18982, "synset": "hegari.n.01", "name": "hegari"}, + {"id": 18983, "synset": "kaoliang.n.01", "name": "kaoliang"}, + {"id": 18984, "synset": "milo.n.01", "name": "milo"}, + {"id": 18985, "synset": "shallu.n.01", "name": "shallu"}, + {"id": 18986, "synset": "broomcorn.n.01", "name": "broomcorn"}, + {"id": 18987, "synset": "cordgrass.n.01", "name": "cordgrass"}, + {"id": 18988, "synset": "salt_reed_grass.n.01", "name": "salt_reed_grass"}, + {"id": 18989, "synset": "prairie_cordgrass.n.01", "name": "prairie_cordgrass"}, + {"id": 18990, "synset": "smut_grass.n.01", "name": "smut_grass"}, + {"id": 18991, "synset": "sand_dropseed.n.01", "name": "sand_dropseed"}, + {"id": 18992, "synset": "rush_grass.n.01", "name": "rush_grass"}, + {"id": 18993, "synset": "st._augustine_grass.n.01", "name": "St._Augustine_grass"}, + {"id": 18994, "synset": "grain.n.08", "name": "grain"}, + {"id": 18995, "synset": "cereal.n.01", "name": "cereal"}, + {"id": 18996, "synset": "wheat.n.01", "name": "wheat"}, + {"id": 18997, "synset": "wheat_berry.n.01", "name": "wheat_berry"}, + {"id": 18998, "synset": "durum.n.01", "name": "durum"}, + {"id": 18999, "synset": "spelt.n.01", "name": "spelt"}, + {"id": 19000, "synset": "emmer.n.01", "name": "emmer"}, + {"id": 19001, "synset": "wild_wheat.n.01", "name": "wild_wheat"}, + {"id": 19002, "synset": "corn.n.01", "name": "corn"}, + {"id": 19003, "synset": "mealie.n.01", "name": "mealie"}, + {"id": 19004, "synset": "corn.n.02", "name": "corn"}, + {"id": 19005, "synset": "dent_corn.n.01", "name": "dent_corn"}, + {"id": 19006, "synset": "flint_corn.n.01", "name": "flint_corn"}, + {"id": 19007, "synset": "popcorn.n.01", "name": "popcorn"}, + {"id": 19008, "synset": "zoysia.n.01", "name": "zoysia"}, + {"id": 19009, "synset": "manila_grass.n.01", "name": "Manila_grass"}, + {"id": 19010, "synset": "korean_lawn_grass.n.01", "name": "Korean_lawn_grass"}, + {"id": 19011, "synset": "common_bamboo.n.01", "name": "common_bamboo"}, + {"id": 19012, "synset": "giant_bamboo.n.01", "name": "giant_bamboo"}, + {"id": 19013, "synset": "umbrella_plant.n.03", "name": "umbrella_plant"}, + {"id": 19014, "synset": "chufa.n.01", "name": "chufa"}, + {"id": 19015, "synset": "galingale.n.01", "name": "galingale"}, + {"id": 19016, "synset": "nutgrass.n.01", "name": "nutgrass"}, + {"id": 19017, "synset": "sand_sedge.n.01", "name": "sand_sedge"}, + {"id": 19018, "synset": "cypress_sedge.n.01", "name": "cypress_sedge"}, + {"id": 19019, "synset": "cotton_grass.n.01", "name": "cotton_grass"}, + {"id": 19020, "synset": "common_cotton_grass.n.01", "name": "common_cotton_grass"}, + {"id": 19021, "synset": "hardstem_bulrush.n.01", "name": "hardstem_bulrush"}, + {"id": 19022, "synset": "wool_grass.n.01", "name": "wool_grass"}, + {"id": 19023, "synset": "spike_rush.n.01", "name": "spike_rush"}, + {"id": 19024, "synset": "water_chestnut.n.02", "name": "water_chestnut"}, + {"id": 19025, "synset": "needle_spike_rush.n.01", "name": "needle_spike_rush"}, + {"id": 19026, "synset": "creeping_spike_rush.n.01", "name": "creeping_spike_rush"}, + {"id": 19027, "synset": "pandanus.n.02", "name": "pandanus"}, + {"id": 19028, "synset": "textile_screw_pine.n.01", "name": "textile_screw_pine"}, + {"id": 19029, "synset": "cattail.n.01", "name": "cattail"}, + {"id": 19030, "synset": "cat's-tail.n.01", "name": "cat's-tail"}, + {"id": 19031, "synset": "bur_reed.n.01", "name": "bur_reed"}, + {"id": 19032, "synset": "grain.n.07", "name": "grain"}, + {"id": 19033, "synset": "kernel.n.02", "name": "kernel"}, + {"id": 19034, "synset": "rye.n.01", "name": "rye"}, + {"id": 19035, "synset": "gourd.n.03", "name": "gourd"}, + {"id": 19036, "synset": "pumpkin.n.01", "name": "pumpkin"}, + {"id": 19037, "synset": "squash.n.01", "name": "squash"}, + {"id": 19038, "synset": "summer_squash.n.01", "name": "summer_squash"}, + {"id": 19039, "synset": "yellow_squash.n.01", "name": "yellow_squash"}, + {"id": 19040, "synset": "marrow.n.02", "name": "marrow"}, + {"id": 19041, "synset": "zucchini.n.01", "name": "zucchini"}, + {"id": 19042, "synset": "cocozelle.n.01", "name": "cocozelle"}, + {"id": 19043, "synset": "cymling.n.01", "name": "cymling"}, + {"id": 19044, "synset": "spaghetti_squash.n.01", "name": "spaghetti_squash"}, + {"id": 19045, "synset": "winter_squash.n.01", "name": "winter_squash"}, + {"id": 19046, "synset": "acorn_squash.n.01", "name": "acorn_squash"}, + {"id": 19047, "synset": "hubbard_squash.n.01", "name": "hubbard_squash"}, + {"id": 19048, "synset": "turban_squash.n.01", "name": "turban_squash"}, + {"id": 19049, "synset": "buttercup_squash.n.01", "name": "buttercup_squash"}, + {"id": 19050, "synset": "butternut_squash.n.01", "name": "butternut_squash"}, + {"id": 19051, "synset": "winter_crookneck.n.01", "name": "winter_crookneck"}, + {"id": 19052, "synset": "cushaw.n.01", "name": "cushaw"}, + {"id": 19053, "synset": "prairie_gourd.n.02", "name": "prairie_gourd"}, + {"id": 19054, "synset": "prairie_gourd.n.01", "name": "prairie_gourd"}, + {"id": 19055, "synset": "bryony.n.01", "name": "bryony"}, + {"id": 19056, "synset": "white_bryony.n.01", "name": "white_bryony"}, + {"id": 19057, "synset": "sweet_melon.n.01", "name": "sweet_melon"}, + {"id": 19058, "synset": "cantaloupe.n.01", "name": "cantaloupe"}, + {"id": 19059, "synset": "winter_melon.n.01", "name": "winter_melon"}, + {"id": 19060, "synset": "net_melon.n.01", "name": "net_melon"}, + {"id": 19061, "synset": "cucumber.n.01", "name": "cucumber"}, + {"id": 19062, "synset": "squirting_cucumber.n.01", "name": "squirting_cucumber"}, + {"id": 19063, "synset": "bottle_gourd.n.01", "name": "bottle_gourd"}, + {"id": 19064, "synset": "luffa.n.02", "name": "luffa"}, + {"id": 19065, "synset": "loofah.n.02", "name": "loofah"}, + {"id": 19066, "synset": "angled_loofah.n.01", "name": "angled_loofah"}, + {"id": 19067, "synset": "loofa.n.01", "name": "loofa"}, + {"id": 19068, "synset": "balsam_apple.n.01", "name": "balsam_apple"}, + {"id": 19069, "synset": "balsam_pear.n.01", "name": "balsam_pear"}, + {"id": 19070, "synset": "lobelia.n.01", "name": "lobelia"}, + {"id": 19071, "synset": "water_lobelia.n.01", "name": "water_lobelia"}, + {"id": 19072, "synset": "mallow.n.01", "name": "mallow"}, + {"id": 19073, "synset": "musk_mallow.n.02", "name": "musk_mallow"}, + {"id": 19074, "synset": "common_mallow.n.01", "name": "common_mallow"}, + {"id": 19075, "synset": "okra.n.02", "name": "okra"}, + {"id": 19076, "synset": "okra.n.01", "name": "okra"}, + {"id": 19077, "synset": "abelmosk.n.01", "name": "abelmosk"}, + {"id": 19078, "synset": "flowering_maple.n.01", "name": "flowering_maple"}, + {"id": 19079, "synset": "velvetleaf.n.02", "name": "velvetleaf"}, + {"id": 19080, "synset": "hollyhock.n.02", "name": "hollyhock"}, + {"id": 19081, "synset": "rose_mallow.n.02", "name": "rose_mallow"}, + {"id": 19082, "synset": "althea.n.01", "name": "althea"}, + {"id": 19083, "synset": "marsh_mallow.n.01", "name": "marsh_mallow"}, + {"id": 19084, "synset": "poppy_mallow.n.01", "name": "poppy_mallow"}, + {"id": 19085, "synset": "fringed_poppy_mallow.n.01", "name": "fringed_poppy_mallow"}, + {"id": 19086, "synset": "purple_poppy_mallow.n.01", "name": "purple_poppy_mallow"}, + {"id": 19087, "synset": "clustered_poppy_mallow.n.01", "name": "clustered_poppy_mallow"}, + {"id": 19088, "synset": "sea_island_cotton.n.01", "name": "sea_island_cotton"}, + {"id": 19089, "synset": "levant_cotton.n.01", "name": "Levant_cotton"}, + {"id": 19090, "synset": "upland_cotton.n.01", "name": "upland_cotton"}, + {"id": 19091, "synset": "peruvian_cotton.n.01", "name": "Peruvian_cotton"}, + {"id": 19092, "synset": "wild_cotton.n.01", "name": "wild_cotton"}, + {"id": 19093, "synset": "kenaf.n.02", "name": "kenaf"}, + {"id": 19094, "synset": "sorrel_tree.n.02", "name": "sorrel_tree"}, + {"id": 19095, "synset": "rose_mallow.n.01", "name": "rose_mallow"}, + {"id": 19096, "synset": "cotton_rose.n.01", "name": "cotton_rose"}, + {"id": 19097, "synset": "roselle.n.01", "name": "roselle"}, + {"id": 19098, "synset": "mahoe.n.01", "name": "mahoe"}, + {"id": 19099, "synset": "flower-of-an-hour.n.01", "name": "flower-of-an-hour"}, + {"id": 19100, "synset": "lacebark.n.01", "name": "lacebark"}, + {"id": 19101, "synset": "wild_hollyhock.n.02", "name": "wild_hollyhock"}, + {"id": 19102, "synset": "mountain_hollyhock.n.01", "name": "mountain_hollyhock"}, + {"id": 19103, "synset": "seashore_mallow.n.01", "name": "seashore_mallow"}, + {"id": 19104, "synset": "salt_marsh_mallow.n.01", "name": "salt_marsh_mallow"}, + {"id": 19105, "synset": "chaparral_mallow.n.01", "name": "chaparral_mallow"}, + {"id": 19106, "synset": "malope.n.01", "name": "malope"}, + {"id": 19107, "synset": "false_mallow.n.02", "name": "false_mallow"}, + {"id": 19108, "synset": "waxmallow.n.01", "name": "waxmallow"}, + {"id": 19109, "synset": "glade_mallow.n.01", "name": "glade_mallow"}, + {"id": 19110, "synset": "pavonia.n.01", "name": "pavonia"}, + {"id": 19111, "synset": "ribbon_tree.n.01", "name": "ribbon_tree"}, + {"id": 19112, "synset": "bush_hibiscus.n.01", "name": "bush_hibiscus"}, + {"id": 19113, "synset": "virginia_mallow.n.01", "name": "Virginia_mallow"}, + {"id": 19114, "synset": "queensland_hemp.n.01", "name": "Queensland_hemp"}, + {"id": 19115, "synset": "indian_mallow.n.01", "name": "Indian_mallow"}, + {"id": 19116, "synset": "checkerbloom.n.01", "name": "checkerbloom"}, + {"id": 19117, "synset": "globe_mallow.n.01", "name": "globe_mallow"}, + {"id": 19118, "synset": "prairie_mallow.n.01", "name": "prairie_mallow"}, + {"id": 19119, "synset": "tulipwood_tree.n.01", "name": "tulipwood_tree"}, + {"id": 19120, "synset": "portia_tree.n.01", "name": "portia_tree"}, + {"id": 19121, "synset": "red_silk-cotton_tree.n.01", "name": "red_silk-cotton_tree"}, + {"id": 19122, "synset": "cream-of-tartar_tree.n.01", "name": "cream-of-tartar_tree"}, + {"id": 19123, "synset": "baobab.n.01", "name": "baobab"}, + {"id": 19124, "synset": "kapok.n.02", "name": "kapok"}, + {"id": 19125, "synset": "durian.n.01", "name": "durian"}, + {"id": 19126, "synset": "montezuma.n.01", "name": "Montezuma"}, + {"id": 19127, "synset": "shaving-brush_tree.n.01", "name": "shaving-brush_tree"}, + {"id": 19128, "synset": "quandong.n.03", "name": "quandong"}, + {"id": 19129, "synset": "quandong.n.02", "name": "quandong"}, + {"id": 19130, "synset": "makomako.n.01", "name": "makomako"}, + {"id": 19131, "synset": "jamaican_cherry.n.01", "name": "Jamaican_cherry"}, + {"id": 19132, "synset": "breakax.n.01", "name": "breakax"}, + {"id": 19133, "synset": "sterculia.n.01", "name": "sterculia"}, + {"id": 19134, "synset": "panama_tree.n.01", "name": "Panama_tree"}, + {"id": 19135, "synset": "kalumpang.n.01", "name": "kalumpang"}, + {"id": 19136, "synset": "bottle-tree.n.01", "name": "bottle-tree"}, + {"id": 19137, "synset": "flame_tree.n.04", "name": "flame_tree"}, + {"id": 19138, "synset": "flame_tree.n.03", "name": "flame_tree"}, + {"id": 19139, "synset": "kurrajong.n.01", "name": "kurrajong"}, + {"id": 19140, "synset": "queensland_bottletree.n.01", "name": "Queensland_bottletree"}, + {"id": 19141, "synset": "kola.n.01", "name": "kola"}, + {"id": 19142, "synset": "kola_nut.n.01", "name": "kola_nut"}, + {"id": 19143, "synset": "chinese_parasol_tree.n.01", "name": "Chinese_parasol_tree"}, + {"id": 19144, "synset": "flannelbush.n.01", "name": "flannelbush"}, + {"id": 19145, "synset": "screw_tree.n.01", "name": "screw_tree"}, + {"id": 19146, "synset": "nut-leaved_screw_tree.n.01", "name": "nut-leaved_screw_tree"}, + {"id": 19147, "synset": "red_beech.n.02", "name": "red_beech"}, + {"id": 19148, "synset": "looking_glass_tree.n.01", "name": "looking_glass_tree"}, + {"id": 19149, "synset": "looking-glass_plant.n.01", "name": "looking-glass_plant"}, + {"id": 19150, "synset": "honey_bell.n.01", "name": "honey_bell"}, + {"id": 19151, "synset": "mayeng.n.01", "name": "mayeng"}, + {"id": 19152, "synset": "silver_tree.n.02", "name": "silver_tree"}, + {"id": 19153, "synset": "cacao.n.01", "name": "cacao"}, + {"id": 19154, "synset": "obeche.n.02", "name": "obeche"}, + {"id": 19155, "synset": "linden.n.02", "name": "linden"}, + {"id": 19156, "synset": "american_basswood.n.01", "name": "American_basswood"}, + {"id": 19157, "synset": "small-leaved_linden.n.01", "name": "small-leaved_linden"}, + {"id": 19158, "synset": "white_basswood.n.01", "name": "white_basswood"}, + {"id": 19159, "synset": "japanese_linden.n.01", "name": "Japanese_linden"}, + {"id": 19160, "synset": "silver_lime.n.01", "name": "silver_lime"}, + {"id": 19161, "synset": "corchorus.n.01", "name": "corchorus"}, + {"id": 19162, "synset": "african_hemp.n.02", "name": "African_hemp"}, + {"id": 19163, "synset": "herb.n.01", "name": "herb"}, + {"id": 19164, "synset": "protea.n.01", "name": "protea"}, + {"id": 19165, "synset": "honeypot.n.01", "name": "honeypot"}, + {"id": 19166, "synset": "honeyflower.n.02", "name": "honeyflower"}, + {"id": 19167, "synset": "banksia.n.01", "name": "banksia"}, + {"id": 19168, "synset": "honeysuckle.n.02", "name": "honeysuckle"}, + {"id": 19169, "synset": "smoke_bush.n.02", "name": "smoke_bush"}, + {"id": 19170, "synset": "chilean_firebush.n.01", "name": "Chilean_firebush"}, + {"id": 19171, "synset": "chilean_nut.n.01", "name": "Chilean_nut"}, + {"id": 19172, "synset": "grevillea.n.01", "name": "grevillea"}, + {"id": 19173, "synset": "red-flowered_silky_oak.n.01", "name": "red-flowered_silky_oak"}, + {"id": 19174, "synset": "silky_oak.n.01", "name": "silky_oak"}, + {"id": 19175, "synset": "beefwood.n.05", "name": "beefwood"}, + {"id": 19176, "synset": "cushion_flower.n.01", "name": "cushion_flower"}, + {"id": 19177, "synset": "rewa-rewa.n.01", "name": "rewa-rewa"}, + {"id": 19178, "synset": "honeyflower.n.01", "name": "honeyflower"}, + {"id": 19179, "synset": "silver_tree.n.01", "name": "silver_tree"}, + {"id": 19180, "synset": "lomatia.n.01", "name": "lomatia"}, + {"id": 19181, "synset": "macadamia.n.01", "name": "macadamia"}, + {"id": 19182, "synset": "macadamia_integrifolia.n.01", "name": "Macadamia_integrifolia"}, + {"id": 19183, "synset": "macadamia_nut.n.01", "name": "macadamia_nut"}, + {"id": 19184, "synset": "queensland_nut.n.01", "name": "Queensland_nut"}, + {"id": 19185, "synset": "prickly_ash.n.02", "name": "prickly_ash"}, + {"id": 19186, "synset": "geebung.n.01", "name": "geebung"}, + {"id": 19187, "synset": "wheel_tree.n.01", "name": "wheel_tree"}, + {"id": 19188, "synset": "scrub_beefwood.n.01", "name": "scrub_beefwood"}, + {"id": 19189, "synset": "waratah.n.02", "name": "waratah"}, + {"id": 19190, "synset": "waratah.n.01", "name": "waratah"}, + {"id": 19191, "synset": "casuarina.n.01", "name": "casuarina"}, + {"id": 19192, "synset": "she-oak.n.01", "name": "she-oak"}, + {"id": 19193, "synset": "beefwood.n.03", "name": "beefwood"}, + {"id": 19194, "synset": "australian_pine.n.01", "name": "Australian_pine"}, + {"id": 19195, "synset": "heath.n.01", "name": "heath"}, + {"id": 19196, "synset": "tree_heath.n.02", "name": "tree_heath"}, + {"id": 19197, "synset": "briarroot.n.01", "name": "briarroot"}, + {"id": 19198, "synset": "winter_heath.n.01", "name": "winter_heath"}, + {"id": 19199, "synset": "bell_heather.n.02", "name": "bell_heather"}, + {"id": 19200, "synset": "cornish_heath.n.01", "name": "Cornish_heath"}, + {"id": 19201, "synset": "spanish_heath.n.01", "name": "Spanish_heath"}, + {"id": 19202, "synset": "prince-of-wales'-heath.n.01", "name": "Prince-of-Wales'-heath"}, + {"id": 19203, "synset": "bog_rosemary.n.01", "name": "bog_rosemary"}, + {"id": 19204, "synset": "marsh_andromeda.n.01", "name": "marsh_andromeda"}, + {"id": 19205, "synset": "madrona.n.01", "name": "madrona"}, + {"id": 19206, "synset": "strawberry_tree.n.01", "name": "strawberry_tree"}, + {"id": 19207, "synset": "bearberry.n.03", "name": "bearberry"}, + {"id": 19208, "synset": "alpine_bearberry.n.01", "name": "alpine_bearberry"}, + {"id": 19209, "synset": "heartleaf_manzanita.n.01", "name": "heartleaf_manzanita"}, + {"id": 19210, "synset": "parry_manzanita.n.01", "name": "Parry_manzanita"}, + {"id": 19211, "synset": "spike_heath.n.01", "name": "spike_heath"}, + {"id": 19212, "synset": "bryanthus.n.01", "name": "bryanthus"}, + {"id": 19213, "synset": "leatherleaf.n.02", "name": "leatherleaf"}, + {"id": 19214, "synset": "connemara_heath.n.01", "name": "Connemara_heath"}, + {"id": 19215, "synset": "trailing_arbutus.n.01", "name": "trailing_arbutus"}, + {"id": 19216, "synset": "creeping_snowberry.n.01", "name": "creeping_snowberry"}, + {"id": 19217, "synset": "salal.n.01", "name": "salal"}, + {"id": 19218, "synset": "huckleberry.n.02", "name": "huckleberry"}, + {"id": 19219, "synset": "black_huckleberry.n.01", "name": "black_huckleberry"}, + {"id": 19220, "synset": "dangleberry.n.01", "name": "dangleberry"}, + {"id": 19221, "synset": "box_huckleberry.n.01", "name": "box_huckleberry"}, + {"id": 19222, "synset": "kalmia.n.01", "name": "kalmia"}, + {"id": 19223, "synset": "mountain_laurel.n.01", "name": "mountain_laurel"}, + {"id": 19224, "synset": "swamp_laurel.n.01", "name": "swamp_laurel"}, + {"id": 19225, "synset": "trapper's_tea.n.01", "name": "trapper's_tea"}, + {"id": 19226, "synset": "wild_rosemary.n.01", "name": "wild_rosemary"}, + {"id": 19227, "synset": "sand_myrtle.n.01", "name": "sand_myrtle"}, + {"id": 19228, "synset": "leucothoe.n.01", "name": "leucothoe"}, + {"id": 19229, "synset": "dog_laurel.n.01", "name": "dog_laurel"}, + {"id": 19230, "synset": "sweet_bells.n.01", "name": "sweet_bells"}, + {"id": 19231, "synset": "alpine_azalea.n.01", "name": "alpine_azalea"}, + {"id": 19232, "synset": "staggerbush.n.01", "name": "staggerbush"}, + {"id": 19233, "synset": "maleberry.n.01", "name": "maleberry"}, + {"id": 19234, "synset": "fetterbush.n.02", "name": "fetterbush"}, + {"id": 19235, "synset": "false_azalea.n.01", "name": "false_azalea"}, + {"id": 19236, "synset": "minniebush.n.01", "name": "minniebush"}, + {"id": 19237, "synset": "sorrel_tree.n.01", "name": "sorrel_tree"}, + {"id": 19238, "synset": "mountain_heath.n.01", "name": "mountain_heath"}, + {"id": 19239, "synset": "purple_heather.n.01", "name": "purple_heather"}, + {"id": 19240, "synset": "fetterbush.n.01", "name": "fetterbush"}, + {"id": 19241, "synset": "rhododendron.n.01", "name": "rhododendron"}, + {"id": 19242, "synset": "coast_rhododendron.n.01", "name": "coast_rhododendron"}, + {"id": 19243, "synset": "rosebay.n.01", "name": "rosebay"}, + {"id": 19244, "synset": "swamp_azalea.n.01", "name": "swamp_azalea"}, + {"id": 19245, "synset": "azalea.n.01", "name": "azalea"}, + {"id": 19246, "synset": "cranberry.n.01", "name": "cranberry"}, + {"id": 19247, "synset": "american_cranberry.n.01", "name": "American_cranberry"}, + {"id": 19248, "synset": "european_cranberry.n.01", "name": "European_cranberry"}, + {"id": 19249, "synset": "blueberry.n.01", "name": "blueberry"}, + {"id": 19250, "synset": "farkleberry.n.01", "name": "farkleberry"}, + {"id": 19251, "synset": "low-bush_blueberry.n.01", "name": "low-bush_blueberry"}, + {"id": 19252, "synset": "rabbiteye_blueberry.n.01", "name": "rabbiteye_blueberry"}, + {"id": 19253, "synset": "dwarf_bilberry.n.01", "name": "dwarf_bilberry"}, + {"id": 19254, "synset": "evergreen_blueberry.n.01", "name": "evergreen_blueberry"}, + {"id": 19255, "synset": "evergreen_huckleberry.n.01", "name": "evergreen_huckleberry"}, + {"id": 19256, "synset": "bilberry.n.02", "name": "bilberry"}, + {"id": 19257, "synset": "bilberry.n.01", "name": "bilberry"}, + {"id": 19258, "synset": "bog_bilberry.n.01", "name": "bog_bilberry"}, + {"id": 19259, "synset": "dryland_blueberry.n.01", "name": "dryland_blueberry"}, + {"id": 19260, "synset": "grouseberry.n.01", "name": "grouseberry"}, + {"id": 19261, "synset": "deerberry.n.01", "name": "deerberry"}, + {"id": 19262, "synset": "cowberry.n.01", "name": "cowberry"}, + {"id": 19263, "synset": "diapensia.n.01", "name": "diapensia"}, + {"id": 19264, "synset": "galax.n.01", "name": "galax"}, + {"id": 19265, "synset": "pyxie.n.01", "name": "pyxie"}, + {"id": 19266, "synset": "shortia.n.01", "name": "shortia"}, + {"id": 19267, "synset": "oconee_bells.n.01", "name": "oconee_bells"}, + {"id": 19268, "synset": "australian_heath.n.01", "name": "Australian_heath"}, + {"id": 19269, "synset": "epacris.n.01", "name": "epacris"}, + {"id": 19270, "synset": "common_heath.n.02", "name": "common_heath"}, + {"id": 19271, "synset": "common_heath.n.01", "name": "common_heath"}, + {"id": 19272, "synset": "port_jackson_heath.n.01", "name": "Port_Jackson_heath"}, + {"id": 19273, "synset": "native_cranberry.n.01", "name": "native_cranberry"}, + {"id": 19274, "synset": "pink_fivecorner.n.01", "name": "pink_fivecorner"}, + {"id": 19275, "synset": "wintergreen.n.01", "name": "wintergreen"}, + {"id": 19276, "synset": "false_wintergreen.n.01", "name": "false_wintergreen"}, + {"id": 19277, "synset": "lesser_wintergreen.n.01", "name": "lesser_wintergreen"}, + {"id": 19278, "synset": "wild_lily_of_the_valley.n.02", "name": "wild_lily_of_the_valley"}, + {"id": 19279, "synset": "wild_lily_of_the_valley.n.01", "name": "wild_lily_of_the_valley"}, + {"id": 19280, "synset": "pipsissewa.n.01", "name": "pipsissewa"}, + {"id": 19281, "synset": "love-in-winter.n.01", "name": "love-in-winter"}, + {"id": 19282, "synset": "one-flowered_wintergreen.n.01", "name": "one-flowered_wintergreen"}, + {"id": 19283, "synset": "indian_pipe.n.01", "name": "Indian_pipe"}, + {"id": 19284, "synset": "pinesap.n.01", "name": "pinesap"}, + {"id": 19285, "synset": "beech.n.01", "name": "beech"}, + {"id": 19286, "synset": "common_beech.n.01", "name": "common_beech"}, + {"id": 19287, "synset": "copper_beech.n.01", "name": "copper_beech"}, + {"id": 19288, "synset": "american_beech.n.01", "name": "American_beech"}, + {"id": 19289, "synset": "weeping_beech.n.01", "name": "weeping_beech"}, + {"id": 19290, "synset": "japanese_beech.n.01", "name": "Japanese_beech"}, + {"id": 19291, "synset": "chestnut.n.02", "name": "chestnut"}, + {"id": 19292, "synset": "american_chestnut.n.01", "name": "American_chestnut"}, + {"id": 19293, "synset": "european_chestnut.n.01", "name": "European_chestnut"}, + {"id": 19294, "synset": "chinese_chestnut.n.01", "name": "Chinese_chestnut"}, + {"id": 19295, "synset": "japanese_chestnut.n.01", "name": "Japanese_chestnut"}, + {"id": 19296, "synset": "allegheny_chinkapin.n.01", "name": "Allegheny_chinkapin"}, + {"id": 19297, "synset": "ozark_chinkapin.n.01", "name": "Ozark_chinkapin"}, + {"id": 19298, "synset": "oak_chestnut.n.01", "name": "oak_chestnut"}, + {"id": 19299, "synset": "giant_chinkapin.n.01", "name": "giant_chinkapin"}, + {"id": 19300, "synset": "dwarf_golden_chinkapin.n.01", "name": "dwarf_golden_chinkapin"}, + {"id": 19301, "synset": "tanbark_oak.n.01", "name": "tanbark_oak"}, + {"id": 19302, "synset": "japanese_oak.n.02", "name": "Japanese_oak"}, + {"id": 19303, "synset": "southern_beech.n.01", "name": "southern_beech"}, + {"id": 19304, "synset": "myrtle_beech.n.01", "name": "myrtle_beech"}, + {"id": 19305, "synset": "coigue.n.01", "name": "Coigue"}, + {"id": 19306, "synset": "new_zealand_beech.n.01", "name": "New_Zealand_beech"}, + {"id": 19307, "synset": "silver_beech.n.01", "name": "silver_beech"}, + {"id": 19308, "synset": "roble_beech.n.01", "name": "roble_beech"}, + {"id": 19309, "synset": "rauli_beech.n.01", "name": "rauli_beech"}, + {"id": 19310, "synset": "black_beech.n.01", "name": "black_beech"}, + {"id": 19311, "synset": "hard_beech.n.01", "name": "hard_beech"}, + {"id": 19312, "synset": "acorn.n.01", "name": "acorn"}, + {"id": 19313, "synset": "cupule.n.01", "name": "cupule"}, + {"id": 19314, "synset": "oak.n.02", "name": "oak"}, + {"id": 19315, "synset": "live_oak.n.01", "name": "live_oak"}, + {"id": 19316, "synset": "coast_live_oak.n.01", "name": "coast_live_oak"}, + {"id": 19317, "synset": "white_oak.n.01", "name": "white_oak"}, + {"id": 19318, "synset": "american_white_oak.n.01", "name": "American_white_oak"}, + {"id": 19319, "synset": "arizona_white_oak.n.01", "name": "Arizona_white_oak"}, + {"id": 19320, "synset": "swamp_white_oak.n.01", "name": "swamp_white_oak"}, + {"id": 19321, "synset": "european_turkey_oak.n.01", "name": "European_turkey_oak"}, + {"id": 19322, "synset": "canyon_oak.n.01", "name": "canyon_oak"}, + {"id": 19323, "synset": "scarlet_oak.n.01", "name": "scarlet_oak"}, + {"id": 19324, "synset": "jack_oak.n.02", "name": "jack_oak"}, + {"id": 19325, "synset": "red_oak.n.01", "name": "red_oak"}, + {"id": 19326, "synset": "southern_red_oak.n.01", "name": "southern_red_oak"}, + {"id": 19327, "synset": "oregon_white_oak.n.01", "name": "Oregon_white_oak"}, + {"id": 19328, "synset": "holm_oak.n.02", "name": "holm_oak"}, + {"id": 19329, "synset": "bear_oak.n.01", "name": "bear_oak"}, + {"id": 19330, "synset": "shingle_oak.n.01", "name": "shingle_oak"}, + {"id": 19331, "synset": "bluejack_oak.n.01", "name": "bluejack_oak"}, + {"id": 19332, "synset": "california_black_oak.n.01", "name": "California_black_oak"}, + {"id": 19333, "synset": "american_turkey_oak.n.01", "name": "American_turkey_oak"}, + {"id": 19334, "synset": "laurel_oak.n.01", "name": "laurel_oak"}, + {"id": 19335, "synset": "california_white_oak.n.01", "name": "California_white_oak"}, + {"id": 19336, "synset": "overcup_oak.n.01", "name": "overcup_oak"}, + {"id": 19337, "synset": "bur_oak.n.01", "name": "bur_oak"}, + {"id": 19338, "synset": "scrub_oak.n.01", "name": "scrub_oak"}, + {"id": 19339, "synset": "blackjack_oak.n.01", "name": "blackjack_oak"}, + {"id": 19340, "synset": "swamp_chestnut_oak.n.01", "name": "swamp_chestnut_oak"}, + {"id": 19341, "synset": "japanese_oak.n.01", "name": "Japanese_oak"}, + {"id": 19342, "synset": "chestnut_oak.n.01", "name": "chestnut_oak"}, + {"id": 19343, "synset": "chinquapin_oak.n.01", "name": "chinquapin_oak"}, + {"id": 19344, "synset": "myrtle_oak.n.01", "name": "myrtle_oak"}, + {"id": 19345, "synset": "water_oak.n.01", "name": "water_oak"}, + {"id": 19346, "synset": "nuttall_oak.n.01", "name": "Nuttall_oak"}, + {"id": 19347, "synset": "durmast.n.01", "name": "durmast"}, + {"id": 19348, "synset": "basket_oak.n.01", "name": "basket_oak"}, + {"id": 19349, "synset": "pin_oak.n.01", "name": "pin_oak"}, + {"id": 19350, "synset": "willow_oak.n.01", "name": "willow_oak"}, + {"id": 19351, "synset": "dwarf_chinkapin_oak.n.01", "name": "dwarf_chinkapin_oak"}, + {"id": 19352, "synset": "common_oak.n.01", "name": "common_oak"}, + {"id": 19353, "synset": "northern_red_oak.n.01", "name": "northern_red_oak"}, + {"id": 19354, "synset": "shumard_oak.n.01", "name": "Shumard_oak"}, + {"id": 19355, "synset": "post_oak.n.01", "name": "post_oak"}, + {"id": 19356, "synset": "cork_oak.n.01", "name": "cork_oak"}, + {"id": 19357, "synset": "spanish_oak.n.01", "name": "Spanish_oak"}, + {"id": 19358, "synset": "huckleberry_oak.n.01", "name": "huckleberry_oak"}, + {"id": 19359, "synset": "chinese_cork_oak.n.01", "name": "Chinese_cork_oak"}, + {"id": 19360, "synset": "black_oak.n.01", "name": "black_oak"}, + {"id": 19361, "synset": "southern_live_oak.n.01", "name": "southern_live_oak"}, + {"id": 19362, "synset": "interior_live_oak.n.01", "name": "interior_live_oak"}, + {"id": 19363, "synset": "mast.n.02", "name": "mast"}, + {"id": 19364, "synset": "birch.n.02", "name": "birch"}, + {"id": 19365, "synset": "yellow_birch.n.01", "name": "yellow_birch"}, + {"id": 19366, "synset": "american_white_birch.n.01", "name": "American_white_birch"}, + {"id": 19367, "synset": "grey_birch.n.01", "name": "grey_birch"}, + {"id": 19368, "synset": "silver_birch.n.01", "name": "silver_birch"}, + {"id": 19369, "synset": "downy_birch.n.01", "name": "downy_birch"}, + {"id": 19370, "synset": "black_birch.n.02", "name": "black_birch"}, + {"id": 19371, "synset": "sweet_birch.n.01", "name": "sweet_birch"}, + {"id": 19372, "synset": "yukon_white_birch.n.01", "name": "Yukon_white_birch"}, + {"id": 19373, "synset": "swamp_birch.n.01", "name": "swamp_birch"}, + {"id": 19374, "synset": "newfoundland_dwarf_birch.n.01", "name": "Newfoundland_dwarf_birch"}, + {"id": 19375, "synset": "alder.n.02", "name": "alder"}, + {"id": 19376, "synset": "common_alder.n.01", "name": "common_alder"}, + {"id": 19377, "synset": "grey_alder.n.01", "name": "grey_alder"}, + {"id": 19378, "synset": "seaside_alder.n.01", "name": "seaside_alder"}, + {"id": 19379, "synset": "white_alder.n.01", "name": "white_alder"}, + {"id": 19380, "synset": "red_alder.n.01", "name": "red_alder"}, + {"id": 19381, "synset": "speckled_alder.n.01", "name": "speckled_alder"}, + {"id": 19382, "synset": "smooth_alder.n.01", "name": "smooth_alder"}, + {"id": 19383, "synset": "green_alder.n.02", "name": "green_alder"}, + {"id": 19384, "synset": "green_alder.n.01", "name": "green_alder"}, + {"id": 19385, "synset": "hornbeam.n.01", "name": "hornbeam"}, + {"id": 19386, "synset": "european_hornbeam.n.01", "name": "European_hornbeam"}, + {"id": 19387, "synset": "american_hornbeam.n.01", "name": "American_hornbeam"}, + {"id": 19388, "synset": "hop_hornbeam.n.01", "name": "hop_hornbeam"}, + {"id": 19389, "synset": "old_world_hop_hornbeam.n.01", "name": "Old_World_hop_hornbeam"}, + {"id": 19390, "synset": "eastern_hop_hornbeam.n.01", "name": "Eastern_hop_hornbeam"}, + {"id": 19391, "synset": "hazelnut.n.01", "name": "hazelnut"}, + {"id": 19392, "synset": "american_hazel.n.01", "name": "American_hazel"}, + {"id": 19393, "synset": "cobnut.n.01", "name": "cobnut"}, + {"id": 19394, "synset": "beaked_hazelnut.n.01", "name": "beaked_hazelnut"}, + {"id": 19395, "synset": "centaury.n.01", "name": "centaury"}, + {"id": 19396, "synset": "rosita.n.01", "name": "rosita"}, + {"id": 19397, "synset": "lesser_centaury.n.01", "name": "lesser_centaury"}, + {"id": 19398, "synset": "seaside_centaury.n.01", "name": "seaside_centaury"}, + {"id": 19399, "synset": "slender_centaury.n.01", "name": "slender_centaury"}, + {"id": 19400, "synset": "prairie_gentian.n.01", "name": "prairie_gentian"}, + {"id": 19401, "synset": "persian_violet.n.01", "name": "Persian_violet"}, + {"id": 19402, "synset": "columbo.n.01", "name": "columbo"}, + {"id": 19403, "synset": "gentian.n.01", "name": "gentian"}, + {"id": 19404, "synset": "gentianella.n.02", "name": "gentianella"}, + {"id": 19405, "synset": "closed_gentian.n.02", "name": "closed_gentian"}, + {"id": 19406, "synset": "explorer's_gentian.n.01", "name": "explorer's_gentian"}, + {"id": 19407, "synset": "closed_gentian.n.01", "name": "closed_gentian"}, + {"id": 19408, "synset": "great_yellow_gentian.n.01", "name": "great_yellow_gentian"}, + {"id": 19409, "synset": "marsh_gentian.n.01", "name": "marsh_gentian"}, + {"id": 19410, "synset": "soapwort_gentian.n.01", "name": "soapwort_gentian"}, + {"id": 19411, "synset": "striped_gentian.n.01", "name": "striped_gentian"}, + {"id": 19412, "synset": "agueweed.n.01", "name": "agueweed"}, + {"id": 19413, "synset": "felwort.n.01", "name": "felwort"}, + {"id": 19414, "synset": "fringed_gentian.n.01", "name": "fringed_gentian"}, + {"id": 19415, "synset": "gentianopsis_crinita.n.01", "name": "Gentianopsis_crinita"}, + {"id": 19416, "synset": "gentianopsis_detonsa.n.01", "name": "Gentianopsis_detonsa"}, + {"id": 19417, "synset": "gentianopsid_procera.n.01", "name": "Gentianopsid_procera"}, + {"id": 19418, "synset": "gentianopsis_thermalis.n.01", "name": "Gentianopsis_thermalis"}, + {"id": 19419, "synset": "tufted_gentian.n.01", "name": "tufted_gentian"}, + {"id": 19420, "synset": "spurred_gentian.n.01", "name": "spurred_gentian"}, + {"id": 19421, "synset": "sabbatia.n.01", "name": "sabbatia"}, + {"id": 19422, "synset": "toothbrush_tree.n.01", "name": "toothbrush_tree"}, + {"id": 19423, "synset": "olive_tree.n.01", "name": "olive_tree"}, + {"id": 19424, "synset": "olive.n.02", "name": "olive"}, + {"id": 19425, "synset": "olive.n.01", "name": "olive"}, + {"id": 19426, "synset": "black_maire.n.01", "name": "black_maire"}, + {"id": 19427, "synset": "white_maire.n.01", "name": "white_maire"}, + {"id": 19428, "synset": "fringe_tree.n.01", "name": "fringe_tree"}, + {"id": 19429, "synset": "fringe_bush.n.01", "name": "fringe_bush"}, + {"id": 19430, "synset": "forestiera.n.01", "name": "forestiera"}, + {"id": 19431, "synset": "forsythia.n.01", "name": "forsythia"}, + {"id": 19432, "synset": "ash.n.02", "name": "ash"}, + {"id": 19433, "synset": "white_ash.n.02", "name": "white_ash"}, + {"id": 19434, "synset": "swamp_ash.n.01", "name": "swamp_ash"}, + {"id": 19435, "synset": "flowering_ash.n.03", "name": "flowering_ash"}, + {"id": 19436, "synset": "european_ash.n.01", "name": "European_ash"}, + {"id": 19437, "synset": "oregon_ash.n.01", "name": "Oregon_ash"}, + {"id": 19438, "synset": "black_ash.n.01", "name": "black_ash"}, + {"id": 19439, "synset": "manna_ash.n.01", "name": "manna_ash"}, + {"id": 19440, "synset": "red_ash.n.01", "name": "red_ash"}, + {"id": 19441, "synset": "green_ash.n.01", "name": "green_ash"}, + {"id": 19442, "synset": "blue_ash.n.01", "name": "blue_ash"}, + {"id": 19443, "synset": "mountain_ash.n.03", "name": "mountain_ash"}, + {"id": 19444, "synset": "pumpkin_ash.n.01", "name": "pumpkin_ash"}, + {"id": 19445, "synset": "arizona_ash.n.01", "name": "Arizona_ash"}, + {"id": 19446, "synset": "jasmine.n.01", "name": "jasmine"}, + {"id": 19447, "synset": "primrose_jasmine.n.01", "name": "primrose_jasmine"}, + {"id": 19448, "synset": "winter_jasmine.n.01", "name": "winter_jasmine"}, + {"id": 19449, "synset": "common_jasmine.n.01", "name": "common_jasmine"}, + {"id": 19450, "synset": "privet.n.01", "name": "privet"}, + {"id": 19451, "synset": "amur_privet.n.01", "name": "Amur_privet"}, + {"id": 19452, "synset": "japanese_privet.n.01", "name": "Japanese_privet"}, + {"id": 19453, "synset": "ligustrum_obtusifolium.n.01", "name": "Ligustrum_obtusifolium"}, + {"id": 19454, "synset": "common_privet.n.01", "name": "common_privet"}, + {"id": 19455, "synset": "devilwood.n.01", "name": "devilwood"}, + {"id": 19456, "synset": "mock_privet.n.01", "name": "mock_privet"}, + {"id": 19457, "synset": "lilac.n.01", "name": "lilac"}, + {"id": 19458, "synset": "himalayan_lilac.n.01", "name": "Himalayan_lilac"}, + {"id": 19459, "synset": "persian_lilac.n.02", "name": "Persian_lilac"}, + {"id": 19460, "synset": "japanese_tree_lilac.n.01", "name": "Japanese_tree_lilac"}, + {"id": 19461, "synset": "japanese_lilac.n.01", "name": "Japanese_lilac"}, + {"id": 19462, "synset": "common_lilac.n.01", "name": "common_lilac"}, + {"id": 19463, "synset": "bloodwort.n.01", "name": "bloodwort"}, + {"id": 19464, "synset": "kangaroo_paw.n.01", "name": "kangaroo_paw"}, + {"id": 19465, "synset": "virginian_witch_hazel.n.01", "name": "Virginian_witch_hazel"}, + {"id": 19466, "synset": "vernal_witch_hazel.n.01", "name": "vernal_witch_hazel"}, + {"id": 19467, "synset": "winter_hazel.n.01", "name": "winter_hazel"}, + {"id": 19468, "synset": "fothergilla.n.01", "name": "fothergilla"}, + {"id": 19469, "synset": "liquidambar.n.02", "name": "liquidambar"}, + {"id": 19470, "synset": "sweet_gum.n.03", "name": "sweet_gum"}, + {"id": 19471, "synset": "iron_tree.n.01", "name": "iron_tree"}, + {"id": 19472, "synset": "walnut.n.03", "name": "walnut"}, + {"id": 19473, "synset": "california_black_walnut.n.01", "name": "California_black_walnut"}, + {"id": 19474, "synset": "butternut.n.01", "name": "butternut"}, + {"id": 19475, "synset": "black_walnut.n.01", "name": "black_walnut"}, + {"id": 19476, "synset": "english_walnut.n.01", "name": "English_walnut"}, + {"id": 19477, "synset": "hickory.n.02", "name": "hickory"}, + {"id": 19478, "synset": "water_hickory.n.01", "name": "water_hickory"}, + {"id": 19479, "synset": "pignut.n.01", "name": "pignut"}, + {"id": 19480, "synset": "bitternut.n.01", "name": "bitternut"}, + {"id": 19481, "synset": "pecan.n.02", "name": "pecan"}, + {"id": 19482, "synset": "big_shellbark.n.01", "name": "big_shellbark"}, + {"id": 19483, "synset": "nutmeg_hickory.n.01", "name": "nutmeg_hickory"}, + {"id": 19484, "synset": "shagbark.n.01", "name": "shagbark"}, + {"id": 19485, "synset": "mockernut.n.01", "name": "mockernut"}, + {"id": 19486, "synset": "wing_nut.n.01", "name": "wing_nut"}, + {"id": 19487, "synset": "caucasian_walnut.n.01", "name": "Caucasian_walnut"}, + {"id": 19488, "synset": "dhawa.n.01", "name": "dhawa"}, + {"id": 19489, "synset": "combretum.n.01", "name": "combretum"}, + {"id": 19490, "synset": "hiccup_nut.n.01", "name": "hiccup_nut"}, + {"id": 19491, "synset": "bush_willow.n.02", "name": "bush_willow"}, + {"id": 19492, "synset": "bush_willow.n.01", "name": "bush_willow"}, + {"id": 19493, "synset": "button_tree.n.01", "name": "button_tree"}, + {"id": 19494, "synset": "white_mangrove.n.02", "name": "white_mangrove"}, + {"id": 19495, "synset": "oleaster.n.01", "name": "oleaster"}, + {"id": 19496, "synset": "water_milfoil.n.01", "name": "water_milfoil"}, + {"id": 19497, "synset": "anchovy_pear.n.01", "name": "anchovy_pear"}, + {"id": 19498, "synset": "brazil_nut.n.01", "name": "brazil_nut"}, + {"id": 19499, "synset": "loosestrife.n.01", "name": "loosestrife"}, + {"id": 19500, "synset": "purple_loosestrife.n.01", "name": "purple_loosestrife"}, + {"id": 19501, "synset": "grass_poly.n.01", "name": "grass_poly"}, + {"id": 19502, "synset": "crape_myrtle.n.01", "name": "crape_myrtle"}, + {"id": 19503, "synset": "queen's_crape_myrtle.n.01", "name": "Queen's_crape_myrtle"}, + {"id": 19504, "synset": "myrtaceous_tree.n.01", "name": "myrtaceous_tree"}, + {"id": 19505, "synset": "myrtle.n.02", "name": "myrtle"}, + {"id": 19506, "synset": "common_myrtle.n.01", "name": "common_myrtle"}, + {"id": 19507, "synset": "bayberry.n.01", "name": "bayberry"}, + {"id": 19508, "synset": "allspice.n.01", "name": "allspice"}, + {"id": 19509, "synset": "allspice_tree.n.01", "name": "allspice_tree"}, + {"id": 19510, "synset": "sour_cherry.n.02", "name": "sour_cherry"}, + {"id": 19511, "synset": "nakedwood.n.02", "name": "nakedwood"}, + {"id": 19512, "synset": "surinam_cherry.n.02", "name": "Surinam_cherry"}, + {"id": 19513, "synset": "rose_apple.n.01", "name": "rose_apple"}, + {"id": 19514, "synset": "feijoa.n.01", "name": "feijoa"}, + {"id": 19515, "synset": "jaboticaba.n.01", "name": "jaboticaba"}, + {"id": 19516, "synset": "guava.n.02", "name": "guava"}, + {"id": 19517, "synset": "guava.n.01", "name": "guava"}, + {"id": 19518, "synset": "cattley_guava.n.01", "name": "cattley_guava"}, + {"id": 19519, "synset": "brazilian_guava.n.01", "name": "Brazilian_guava"}, + {"id": 19520, "synset": "gum_tree.n.01", "name": "gum_tree"}, + {"id": 19521, "synset": "eucalyptus.n.02", "name": "eucalyptus"}, + {"id": 19522, "synset": "flooded_gum.n.01", "name": "flooded_gum"}, + {"id": 19523, "synset": "mallee.n.01", "name": "mallee"}, + {"id": 19524, "synset": "stringybark.n.01", "name": "stringybark"}, + {"id": 19525, "synset": "smoothbark.n.01", "name": "smoothbark"}, + {"id": 19526, "synset": "red_gum.n.03", "name": "red_gum"}, + {"id": 19527, "synset": "red_gum.n.02", "name": "red_gum"}, + {"id": 19528, "synset": "river_red_gum.n.01", "name": "river_red_gum"}, + {"id": 19529, "synset": "mountain_swamp_gum.n.01", "name": "mountain_swamp_gum"}, + {"id": 19530, "synset": "snow_gum.n.01", "name": "snow_gum"}, + {"id": 19531, "synset": "alpine_ash.n.01", "name": "alpine_ash"}, + {"id": 19532, "synset": "white_mallee.n.01", "name": "white_mallee"}, + {"id": 19533, "synset": "white_stringybark.n.01", "name": "white_stringybark"}, + {"id": 19534, "synset": "white_mountain_ash.n.01", "name": "white_mountain_ash"}, + {"id": 19535, "synset": "blue_gum.n.01", "name": "blue_gum"}, + {"id": 19536, "synset": "rose_gum.n.01", "name": "rose_gum"}, + {"id": 19537, "synset": "cider_gum.n.01", "name": "cider_gum"}, + {"id": 19538, "synset": "swamp_gum.n.01", "name": "swamp_gum"}, + {"id": 19539, "synset": "spotted_gum.n.01", "name": "spotted_gum"}, + {"id": 19540, "synset": "lemon-scented_gum.n.01", "name": "lemon-scented_gum"}, + {"id": 19541, "synset": "black_mallee.n.01", "name": "black_mallee"}, + {"id": 19542, "synset": "forest_red_gum.n.01", "name": "forest_red_gum"}, + {"id": 19543, "synset": "mountain_ash.n.02", "name": "mountain_ash"}, + {"id": 19544, "synset": "manna_gum.n.01", "name": "manna_gum"}, + {"id": 19545, "synset": "clove.n.02", "name": "clove"}, + {"id": 19546, "synset": "clove.n.01", "name": "clove"}, + {"id": 19547, "synset": "tupelo.n.02", "name": "tupelo"}, + {"id": 19548, "synset": "water_gum.n.01", "name": "water_gum"}, + {"id": 19549, "synset": "sour_gum.n.01", "name": "sour_gum"}, + {"id": 19550, "synset": "enchanter's_nightshade.n.01", "name": "enchanter's_nightshade"}, + {"id": 19551, "synset": "circaea_lutetiana.n.01", "name": "Circaea_lutetiana"}, + {"id": 19552, "synset": "willowherb.n.01", "name": "willowherb"}, + {"id": 19553, "synset": "fireweed.n.01", "name": "fireweed"}, + {"id": 19554, "synset": "california_fuchsia.n.01", "name": "California_fuchsia"}, + {"id": 19555, "synset": "fuchsia.n.01", "name": "fuchsia"}, + {"id": 19556, "synset": "lady's-eardrop.n.01", "name": "lady's-eardrop"}, + {"id": 19557, "synset": "evening_primrose.n.01", "name": "evening_primrose"}, + {"id": 19558, "synset": "common_evening_primrose.n.01", "name": "common_evening_primrose"}, + {"id": 19559, "synset": "sundrops.n.01", "name": "sundrops"}, + {"id": 19560, "synset": "missouri_primrose.n.01", "name": "Missouri_primrose"}, + {"id": 19561, "synset": "pomegranate.n.01", "name": "pomegranate"}, + {"id": 19562, "synset": "mangrove.n.01", "name": "mangrove"}, + {"id": 19563, "synset": "daphne.n.01", "name": "daphne"}, + {"id": 19564, "synset": "garland_flower.n.01", "name": "garland_flower"}, + {"id": 19565, "synset": "spurge_laurel.n.01", "name": "spurge_laurel"}, + {"id": 19566, "synset": "mezereon.n.01", "name": "mezereon"}, + {"id": 19567, "synset": "indian_rhododendron.n.01", "name": "Indian_rhododendron"}, + {"id": 19568, "synset": "medinilla_magnifica.n.01", "name": "Medinilla_magnifica"}, + {"id": 19569, "synset": "deer_grass.n.01", "name": "deer_grass"}, + {"id": 19570, "synset": "canna.n.01", "name": "canna"}, + {"id": 19571, "synset": "achira.n.01", "name": "achira"}, + {"id": 19572, "synset": "arrowroot.n.02", "name": "arrowroot"}, + {"id": 19573, "synset": "banana.n.01", "name": "banana"}, + {"id": 19574, "synset": "dwarf_banana.n.01", "name": "dwarf_banana"}, + {"id": 19575, "synset": "japanese_banana.n.01", "name": "Japanese_banana"}, + {"id": 19576, "synset": "plantain.n.02", "name": "plantain"}, + {"id": 19577, "synset": "edible_banana.n.01", "name": "edible_banana"}, + {"id": 19578, "synset": "abaca.n.02", "name": "abaca"}, + {"id": 19579, "synset": "abyssinian_banana.n.01", "name": "Abyssinian_banana"}, + {"id": 19580, "synset": "ginger.n.01", "name": "ginger"}, + {"id": 19581, "synset": "common_ginger.n.01", "name": "common_ginger"}, + {"id": 19582, "synset": "turmeric.n.01", "name": "turmeric"}, + {"id": 19583, "synset": "galangal.n.01", "name": "galangal"}, + {"id": 19584, "synset": "shellflower.n.02", "name": "shellflower"}, + {"id": 19585, "synset": "grains_of_paradise.n.01", "name": "grains_of_paradise"}, + {"id": 19586, "synset": "cardamom.n.01", "name": "cardamom"}, + {"id": 19587, "synset": "begonia.n.01", "name": "begonia"}, + {"id": 19588, "synset": "fibrous-rooted_begonia.n.01", "name": "fibrous-rooted_begonia"}, + {"id": 19589, "synset": "tuberous_begonia.n.01", "name": "tuberous_begonia"}, + {"id": 19590, "synset": "rhizomatous_begonia.n.01", "name": "rhizomatous_begonia"}, + {"id": 19591, "synset": "christmas_begonia.n.01", "name": "Christmas_begonia"}, + {"id": 19592, "synset": "angel-wing_begonia.n.01", "name": "angel-wing_begonia"}, + {"id": 19593, "synset": "beefsteak_begonia.n.01", "name": "beefsteak_begonia"}, + {"id": 19594, "synset": "star_begonia.n.01", "name": "star_begonia"}, + {"id": 19595, "synset": "rex_begonia.n.01", "name": "rex_begonia"}, + {"id": 19596, "synset": "wax_begonia.n.01", "name": "wax_begonia"}, + {"id": 19597, "synset": "socotra_begonia.n.01", "name": "Socotra_begonia"}, + {"id": 19598, "synset": "hybrid_tuberous_begonia.n.01", "name": "hybrid_tuberous_begonia"}, + {"id": 19599, "synset": "dillenia.n.01", "name": "dillenia"}, + {"id": 19600, "synset": "guinea_gold_vine.n.01", "name": "guinea_gold_vine"}, + {"id": 19601, "synset": "poon.n.02", "name": "poon"}, + {"id": 19602, "synset": "calaba.n.01", "name": "calaba"}, + {"id": 19603, "synset": "maria.n.02", "name": "Maria"}, + {"id": 19604, "synset": "laurelwood.n.01", "name": "laurelwood"}, + {"id": 19605, "synset": "alexandrian_laurel.n.01", "name": "Alexandrian_laurel"}, + {"id": 19606, "synset": "clusia.n.01", "name": "clusia"}, + {"id": 19607, "synset": "wild_fig.n.02", "name": "wild_fig"}, + {"id": 19608, "synset": "waxflower.n.02", "name": "waxflower"}, + {"id": 19609, "synset": "pitch_apple.n.01", "name": "pitch_apple"}, + {"id": 19610, "synset": "mangosteen.n.01", "name": "mangosteen"}, + {"id": 19611, "synset": "gamboge_tree.n.01", "name": "gamboge_tree"}, + {"id": 19612, "synset": "st_john's_wort.n.01", "name": "St_John's_wort"}, + {"id": 19613, "synset": "common_st_john's_wort.n.01", "name": "common_St_John's_wort"}, + {"id": 19614, "synset": "great_st_john's_wort.n.01", "name": "great_St_John's_wort"}, + {"id": 19615, "synset": "creeping_st_john's_wort.n.01", "name": "creeping_St_John's_wort"}, + {"id": 19616, "synset": "low_st_andrew's_cross.n.01", "name": "low_St_Andrew's_cross"}, + {"id": 19617, "synset": "klammath_weed.n.01", "name": "klammath_weed"}, + {"id": 19618, "synset": "shrubby_st_john's_wort.n.01", "name": "shrubby_St_John's_wort"}, + {"id": 19619, "synset": "st_peter's_wort.n.01", "name": "St_Peter's_wort"}, + {"id": 19620, "synset": "marsh_st-john's_wort.n.01", "name": "marsh_St-John's_wort"}, + {"id": 19621, "synset": "mammee_apple.n.01", "name": "mammee_apple"}, + {"id": 19622, "synset": "rose_chestnut.n.01", "name": "rose_chestnut"}, + {"id": 19623, "synset": "bower_actinidia.n.01", "name": "bower_actinidia"}, + {"id": 19624, "synset": "chinese_gooseberry.n.01", "name": "Chinese_gooseberry"}, + {"id": 19625, "synset": "silvervine.n.01", "name": "silvervine"}, + {"id": 19626, "synset": "wild_cinnamon.n.01", "name": "wild_cinnamon"}, + {"id": 19627, "synset": "papaya.n.01", "name": "papaya"}, + {"id": 19628, "synset": "souari.n.01", "name": "souari"}, + {"id": 19629, "synset": "rockrose.n.02", "name": "rockrose"}, + {"id": 19630, "synset": "white-leaved_rockrose.n.01", "name": "white-leaved_rockrose"}, + {"id": 19631, "synset": "common_gum_cistus.n.01", "name": "common_gum_cistus"}, + {"id": 19632, "synset": "frostweed.n.01", "name": "frostweed"}, + {"id": 19633, "synset": "dipterocarp.n.01", "name": "dipterocarp"}, + {"id": 19634, "synset": "red_lauan.n.02", "name": "red_lauan"}, + {"id": 19635, "synset": "governor's_plum.n.01", "name": "governor's_plum"}, + {"id": 19636, "synset": "kei_apple.n.01", "name": "kei_apple"}, + {"id": 19637, "synset": "ketembilla.n.01", "name": "ketembilla"}, + {"id": 19638, "synset": "chaulmoogra.n.01", "name": "chaulmoogra"}, + {"id": 19639, "synset": "wild_peach.n.01", "name": "wild_peach"}, + {"id": 19640, "synset": "candlewood.n.01", "name": "candlewood"}, + {"id": 19641, "synset": "boojum_tree.n.01", "name": "boojum_tree"}, + {"id": 19642, "synset": "bird's-eye_bush.n.01", "name": "bird's-eye_bush"}, + {"id": 19643, "synset": "granadilla.n.03", "name": "granadilla"}, + {"id": 19644, "synset": "granadilla.n.02", "name": "granadilla"}, + {"id": 19645, "synset": "granadilla.n.01", "name": "granadilla"}, + {"id": 19646, "synset": "maypop.n.01", "name": "maypop"}, + {"id": 19647, "synset": "jamaica_honeysuckle.n.01", "name": "Jamaica_honeysuckle"}, + {"id": 19648, "synset": "banana_passion_fruit.n.01", "name": "banana_passion_fruit"}, + {"id": 19649, "synset": "sweet_calabash.n.01", "name": "sweet_calabash"}, + {"id": 19650, "synset": "love-in-a-mist.n.01", "name": "love-in-a-mist"}, + {"id": 19651, "synset": "reseda.n.01", "name": "reseda"}, + {"id": 19652, "synset": "mignonette.n.01", "name": "mignonette"}, + {"id": 19653, "synset": "dyer's_rocket.n.01", "name": "dyer's_rocket"}, + {"id": 19654, "synset": "false_tamarisk.n.01", "name": "false_tamarisk"}, + {"id": 19655, "synset": "halophyte.n.01", "name": "halophyte"}, + {"id": 19656, "synset": "viola.n.01", "name": "viola"}, + {"id": 19657, "synset": "violet.n.01", "name": "violet"}, + {"id": 19658, "synset": "field_pansy.n.01", "name": "field_pansy"}, + {"id": 19659, "synset": "american_dog_violet.n.01", "name": "American_dog_violet"}, + {"id": 19660, "synset": "dog_violet.n.01", "name": "dog_violet"}, + {"id": 19661, "synset": "horned_violet.n.01", "name": "horned_violet"}, + {"id": 19662, "synset": "two-eyed_violet.n.01", "name": "two-eyed_violet"}, + {"id": 19663, "synset": "bird's-foot_violet.n.01", "name": "bird's-foot_violet"}, + {"id": 19664, "synset": "downy_yellow_violet.n.01", "name": "downy_yellow_violet"}, + {"id": 19665, "synset": "long-spurred_violet.n.01", "name": "long-spurred_violet"}, + {"id": 19666, "synset": "pale_violet.n.01", "name": "pale_violet"}, + {"id": 19667, "synset": "hedge_violet.n.01", "name": "hedge_violet"}, + {"id": 19668, "synset": "nettle.n.01", "name": "nettle"}, + {"id": 19669, "synset": "stinging_nettle.n.01", "name": "stinging_nettle"}, + {"id": 19670, "synset": "roman_nettle.n.01", "name": "Roman_nettle"}, + {"id": 19671, "synset": "ramie.n.01", "name": "ramie"}, + {"id": 19672, "synset": "wood_nettle.n.01", "name": "wood_nettle"}, + {"id": 19673, "synset": "australian_nettle.n.01", "name": "Australian_nettle"}, + {"id": 19674, "synset": "pellitory-of-the-wall.n.01", "name": "pellitory-of-the-wall"}, + {"id": 19675, "synset": "richweed.n.02", "name": "richweed"}, + {"id": 19676, "synset": "artillery_plant.n.01", "name": "artillery_plant"}, + {"id": 19677, "synset": "friendship_plant.n.01", "name": "friendship_plant"}, + { + "id": 19678, + "synset": "queensland_grass-cloth_plant.n.01", + "name": "Queensland_grass-cloth_plant", + }, + {"id": 19679, "synset": "pipturus_albidus.n.01", "name": "Pipturus_albidus"}, + {"id": 19680, "synset": "cannabis.n.01", "name": "cannabis"}, + {"id": 19681, "synset": "indian_hemp.n.01", "name": "Indian_hemp"}, + {"id": 19682, "synset": "mulberry.n.01", "name": "mulberry"}, + {"id": 19683, "synset": "white_mulberry.n.01", "name": "white_mulberry"}, + {"id": 19684, "synset": "black_mulberry.n.01", "name": "black_mulberry"}, + {"id": 19685, "synset": "red_mulberry.n.01", "name": "red_mulberry"}, + {"id": 19686, "synset": "osage_orange.n.01", "name": "osage_orange"}, + {"id": 19687, "synset": "breadfruit.n.01", "name": "breadfruit"}, + {"id": 19688, "synset": "jackfruit.n.01", "name": "jackfruit"}, + {"id": 19689, "synset": "marang.n.01", "name": "marang"}, + {"id": 19690, "synset": "fig_tree.n.01", "name": "fig_tree"}, + {"id": 19691, "synset": "fig.n.02", "name": "fig"}, + {"id": 19692, "synset": "caprifig.n.01", "name": "caprifig"}, + {"id": 19693, "synset": "golden_fig.n.01", "name": "golden_fig"}, + {"id": 19694, "synset": "banyan.n.01", "name": "banyan"}, + {"id": 19695, "synset": "pipal.n.01", "name": "pipal"}, + {"id": 19696, "synset": "india-rubber_tree.n.01", "name": "India-rubber_tree"}, + {"id": 19697, "synset": "mistletoe_fig.n.01", "name": "mistletoe_fig"}, + {"id": 19698, "synset": "port_jackson_fig.n.01", "name": "Port_Jackson_fig"}, + {"id": 19699, "synset": "sycamore.n.04", "name": "sycamore"}, + {"id": 19700, "synset": "paper_mulberry.n.01", "name": "paper_mulberry"}, + {"id": 19701, "synset": "trumpetwood.n.01", "name": "trumpetwood"}, + {"id": 19702, "synset": "elm.n.01", "name": "elm"}, + {"id": 19703, "synset": "winged_elm.n.01", "name": "winged_elm"}, + {"id": 19704, "synset": "american_elm.n.01", "name": "American_elm"}, + {"id": 19705, "synset": "smooth-leaved_elm.n.01", "name": "smooth-leaved_elm"}, + {"id": 19706, "synset": "cedar_elm.n.01", "name": "cedar_elm"}, + {"id": 19707, "synset": "witch_elm.n.01", "name": "witch_elm"}, + {"id": 19708, "synset": "dutch_elm.n.01", "name": "Dutch_elm"}, + {"id": 19709, "synset": "huntingdon_elm.n.01", "name": "Huntingdon_elm"}, + {"id": 19710, "synset": "water_elm.n.01", "name": "water_elm"}, + {"id": 19711, "synset": "chinese_elm.n.02", "name": "Chinese_elm"}, + {"id": 19712, "synset": "english_elm.n.01", "name": "English_elm"}, + {"id": 19713, "synset": "siberian_elm.n.01", "name": "Siberian_elm"}, + {"id": 19714, "synset": "slippery_elm.n.01", "name": "slippery_elm"}, + {"id": 19715, "synset": "jersey_elm.n.01", "name": "Jersey_elm"}, + {"id": 19716, "synset": "september_elm.n.01", "name": "September_elm"}, + {"id": 19717, "synset": "rock_elm.n.01", "name": "rock_elm"}, + {"id": 19718, "synset": "hackberry.n.01", "name": "hackberry"}, + {"id": 19719, "synset": "european_hackberry.n.01", "name": "European_hackberry"}, + {"id": 19720, "synset": "american_hackberry.n.01", "name": "American_hackberry"}, + {"id": 19721, "synset": "sugarberry.n.01", "name": "sugarberry"}, + {"id": 19722, "synset": "iridaceous_plant.n.01", "name": "iridaceous_plant"}, + {"id": 19723, "synset": "bearded_iris.n.01", "name": "bearded_iris"}, + {"id": 19724, "synset": "beardless_iris.n.01", "name": "beardless_iris"}, + {"id": 19725, "synset": "orrisroot.n.01", "name": "orrisroot"}, + {"id": 19726, "synset": "dwarf_iris.n.02", "name": "dwarf_iris"}, + {"id": 19727, "synset": "dutch_iris.n.02", "name": "Dutch_iris"}, + {"id": 19728, "synset": "florentine_iris.n.01", "name": "Florentine_iris"}, + {"id": 19729, "synset": "stinking_iris.n.01", "name": "stinking_iris"}, + {"id": 19730, "synset": "german_iris.n.02", "name": "German_iris"}, + {"id": 19731, "synset": "japanese_iris.n.01", "name": "Japanese_iris"}, + {"id": 19732, "synset": "german_iris.n.01", "name": "German_iris"}, + {"id": 19733, "synset": "dalmatian_iris.n.01", "name": "Dalmatian_iris"}, + {"id": 19734, "synset": "persian_iris.n.01", "name": "Persian_iris"}, + {"id": 19735, "synset": "dutch_iris.n.01", "name": "Dutch_iris"}, + {"id": 19736, "synset": "dwarf_iris.n.01", "name": "dwarf_iris"}, + {"id": 19737, "synset": "spanish_iris.n.01", "name": "Spanish_iris"}, + {"id": 19738, "synset": "blackberry-lily.n.01", "name": "blackberry-lily"}, + {"id": 19739, "synset": "crocus.n.01", "name": "crocus"}, + {"id": 19740, "synset": "saffron.n.01", "name": "saffron"}, + {"id": 19741, "synset": "corn_lily.n.01", "name": "corn_lily"}, + {"id": 19742, "synset": "blue-eyed_grass.n.01", "name": "blue-eyed_grass"}, + {"id": 19743, "synset": "wandflower.n.01", "name": "wandflower"}, + {"id": 19744, "synset": "amaryllis.n.01", "name": "amaryllis"}, + {"id": 19745, "synset": "salsilla.n.02", "name": "salsilla"}, + {"id": 19746, "synset": "salsilla.n.01", "name": "salsilla"}, + {"id": 19747, "synset": "blood_lily.n.01", "name": "blood_lily"}, + {"id": 19748, "synset": "cape_tulip.n.01", "name": "Cape_tulip"}, + {"id": 19749, "synset": "hippeastrum.n.01", "name": "hippeastrum"}, + {"id": 19750, "synset": "narcissus.n.01", "name": "narcissus"}, + {"id": 19751, "synset": "daffodil.n.01", "name": "daffodil"}, + {"id": 19752, "synset": "jonquil.n.01", "name": "jonquil"}, + {"id": 19753, "synset": "jonquil.n.02", "name": "jonquil"}, + {"id": 19754, "synset": "jacobean_lily.n.01", "name": "Jacobean_lily"}, + {"id": 19755, "synset": "liliaceous_plant.n.01", "name": "liliaceous_plant"}, + {"id": 19756, "synset": "mountain_lily.n.01", "name": "mountain_lily"}, + {"id": 19757, "synset": "canada_lily.n.01", "name": "Canada_lily"}, + {"id": 19758, "synset": "tiger_lily.n.02", "name": "tiger_lily"}, + {"id": 19759, "synset": "columbia_tiger_lily.n.01", "name": "Columbia_tiger_lily"}, + {"id": 19760, "synset": "tiger_lily.n.01", "name": "tiger_lily"}, + {"id": 19761, "synset": "easter_lily.n.01", "name": "Easter_lily"}, + {"id": 19762, "synset": "coast_lily.n.01", "name": "coast_lily"}, + {"id": 19763, "synset": "turk's-cap.n.02", "name": "Turk's-cap"}, + {"id": 19764, "synset": "michigan_lily.n.01", "name": "Michigan_lily"}, + {"id": 19765, "synset": "leopard_lily.n.01", "name": "leopard_lily"}, + {"id": 19766, "synset": "turk's-cap.n.01", "name": "Turk's-cap"}, + {"id": 19767, "synset": "african_lily.n.01", "name": "African_lily"}, + {"id": 19768, "synset": "colicroot.n.01", "name": "colicroot"}, + {"id": 19769, "synset": "ague_root.n.01", "name": "ague_root"}, + {"id": 19770, "synset": "yellow_colicroot.n.01", "name": "yellow_colicroot"}, + {"id": 19771, "synset": "alliaceous_plant.n.01", "name": "alliaceous_plant"}, + {"id": 19772, "synset": "hooker's_onion.n.01", "name": "Hooker's_onion"}, + {"id": 19773, "synset": "wild_leek.n.02", "name": "wild_leek"}, + {"id": 19774, "synset": "canada_garlic.n.01", "name": "Canada_garlic"}, + {"id": 19775, "synset": "keeled_garlic.n.01", "name": "keeled_garlic"}, + {"id": 19776, "synset": "shallot.n.02", "name": "shallot"}, + {"id": 19777, "synset": "nodding_onion.n.01", "name": "nodding_onion"}, + {"id": 19778, "synset": "welsh_onion.n.01", "name": "Welsh_onion"}, + {"id": 19779, "synset": "red-skinned_onion.n.01", "name": "red-skinned_onion"}, + {"id": 19780, "synset": "daffodil_garlic.n.01", "name": "daffodil_garlic"}, + {"id": 19781, "synset": "few-flowered_leek.n.01", "name": "few-flowered_leek"}, + {"id": 19782, "synset": "garlic.n.01", "name": "garlic"}, + {"id": 19783, "synset": "sand_leek.n.01", "name": "sand_leek"}, + {"id": 19784, "synset": "chives.n.01", "name": "chives"}, + {"id": 19785, "synset": "crow_garlic.n.01", "name": "crow_garlic"}, + {"id": 19786, "synset": "wild_garlic.n.01", "name": "wild_garlic"}, + {"id": 19787, "synset": "garlic_chive.n.01", "name": "garlic_chive"}, + {"id": 19788, "synset": "round-headed_leek.n.01", "name": "round-headed_leek"}, + {"id": 19789, "synset": "three-cornered_leek.n.01", "name": "three-cornered_leek"}, + {"id": 19790, "synset": "cape_aloe.n.01", "name": "cape_aloe"}, + {"id": 19791, "synset": "kniphofia.n.01", "name": "kniphofia"}, + {"id": 19792, "synset": "poker_plant.n.01", "name": "poker_plant"}, + {"id": 19793, "synset": "red-hot_poker.n.01", "name": "red-hot_poker"}, + {"id": 19794, "synset": "fly_poison.n.01", "name": "fly_poison"}, + {"id": 19795, "synset": "amber_lily.n.01", "name": "amber_lily"}, + {"id": 19796, "synset": "asparagus.n.01", "name": "asparagus"}, + {"id": 19797, "synset": "asparagus_fern.n.01", "name": "asparagus_fern"}, + {"id": 19798, "synset": "smilax.n.02", "name": "smilax"}, + {"id": 19799, "synset": "asphodel.n.01", "name": "asphodel"}, + {"id": 19800, "synset": "jacob's_rod.n.01", "name": "Jacob's_rod"}, + {"id": 19801, "synset": "aspidistra.n.01", "name": "aspidistra"}, + {"id": 19802, "synset": "coral_drops.n.01", "name": "coral_drops"}, + {"id": 19803, "synset": "christmas_bells.n.01", "name": "Christmas_bells"}, + {"id": 19804, "synset": "climbing_onion.n.01", "name": "climbing_onion"}, + {"id": 19805, "synset": "mariposa.n.01", "name": "mariposa"}, + {"id": 19806, "synset": "globe_lily.n.01", "name": "globe_lily"}, + {"id": 19807, "synset": "cat's-ear.n.01", "name": "cat's-ear"}, + {"id": 19808, "synset": "white_globe_lily.n.01", "name": "white_globe_lily"}, + {"id": 19809, "synset": "yellow_globe_lily.n.01", "name": "yellow_globe_lily"}, + {"id": 19810, "synset": "rose_globe_lily.n.01", "name": "rose_globe_lily"}, + {"id": 19811, "synset": "star_tulip.n.01", "name": "star_tulip"}, + {"id": 19812, "synset": "desert_mariposa_tulip.n.01", "name": "desert_mariposa_tulip"}, + {"id": 19813, "synset": "yellow_mariposa_tulip.n.01", "name": "yellow_mariposa_tulip"}, + {"id": 19814, "synset": "sagebrush_mariposa_tulip.n.01", "name": "sagebrush_mariposa_tulip"}, + {"id": 19815, "synset": "sego_lily.n.01", "name": "sego_lily"}, + {"id": 19816, "synset": "camas.n.01", "name": "camas"}, + {"id": 19817, "synset": "common_camas.n.01", "name": "common_camas"}, + {"id": 19818, "synset": "leichtlin's_camas.n.01", "name": "Leichtlin's_camas"}, + {"id": 19819, "synset": "wild_hyacinth.n.02", "name": "wild_hyacinth"}, + {"id": 19820, "synset": "dogtooth_violet.n.01", "name": "dogtooth_violet"}, + {"id": 19821, "synset": "white_dogtooth_violet.n.01", "name": "white_dogtooth_violet"}, + {"id": 19822, "synset": "yellow_adder's_tongue.n.01", "name": "yellow_adder's_tongue"}, + {"id": 19823, "synset": "european_dogtooth.n.01", "name": "European_dogtooth"}, + {"id": 19824, "synset": "fawn_lily.n.01", "name": "fawn_lily"}, + {"id": 19825, "synset": "glacier_lily.n.01", "name": "glacier_lily"}, + {"id": 19826, "synset": "avalanche_lily.n.01", "name": "avalanche_lily"}, + {"id": 19827, "synset": "fritillary.n.01", "name": "fritillary"}, + {"id": 19828, "synset": "mission_bells.n.02", "name": "mission_bells"}, + {"id": 19829, "synset": "mission_bells.n.01", "name": "mission_bells"}, + {"id": 19830, "synset": "stink_bell.n.01", "name": "stink_bell"}, + {"id": 19831, "synset": "crown_imperial.n.01", "name": "crown_imperial"}, + {"id": 19832, "synset": "white_fritillary.n.01", "name": "white_fritillary"}, + {"id": 19833, "synset": "snake's_head_fritillary.n.01", "name": "snake's_head_fritillary"}, + {"id": 19834, "synset": "adobe_lily.n.01", "name": "adobe_lily"}, + {"id": 19835, "synset": "scarlet_fritillary.n.01", "name": "scarlet_fritillary"}, + {"id": 19836, "synset": "tulip.n.01", "name": "tulip"}, + {"id": 19837, "synset": "dwarf_tulip.n.01", "name": "dwarf_tulip"}, + {"id": 19838, "synset": "lady_tulip.n.01", "name": "lady_tulip"}, + {"id": 19839, "synset": "tulipa_gesneriana.n.01", "name": "Tulipa_gesneriana"}, + {"id": 19840, "synset": "cottage_tulip.n.01", "name": "cottage_tulip"}, + {"id": 19841, "synset": "darwin_tulip.n.01", "name": "Darwin_tulip"}, + {"id": 19842, "synset": "gloriosa.n.01", "name": "gloriosa"}, + {"id": 19843, "synset": "lemon_lily.n.01", "name": "lemon_lily"}, + {"id": 19844, "synset": "common_hyacinth.n.01", "name": "common_hyacinth"}, + {"id": 19845, "synset": "roman_hyacinth.n.01", "name": "Roman_hyacinth"}, + {"id": 19846, "synset": "summer_hyacinth.n.01", "name": "summer_hyacinth"}, + {"id": 19847, "synset": "star-of-bethlehem.n.01", "name": "star-of-Bethlehem"}, + {"id": 19848, "synset": "bath_asparagus.n.01", "name": "bath_asparagus"}, + {"id": 19849, "synset": "grape_hyacinth.n.01", "name": "grape_hyacinth"}, + {"id": 19850, "synset": "common_grape_hyacinth.n.01", "name": "common_grape_hyacinth"}, + {"id": 19851, "synset": "tassel_hyacinth.n.01", "name": "tassel_hyacinth"}, + {"id": 19852, "synset": "scilla.n.01", "name": "scilla"}, + {"id": 19853, "synset": "spring_squill.n.01", "name": "spring_squill"}, + {"id": 19854, "synset": "false_asphodel.n.01", "name": "false_asphodel"}, + {"id": 19855, "synset": "scotch_asphodel.n.01", "name": "Scotch_asphodel"}, + {"id": 19856, "synset": "sea_squill.n.01", "name": "sea_squill"}, + {"id": 19857, "synset": "squill.n.01", "name": "squill"}, + {"id": 19858, "synset": "butcher's_broom.n.01", "name": "butcher's_broom"}, + {"id": 19859, "synset": "bog_asphodel.n.01", "name": "bog_asphodel"}, + {"id": 19860, "synset": "european_bog_asphodel.n.01", "name": "European_bog_asphodel"}, + {"id": 19861, "synset": "american_bog_asphodel.n.01", "name": "American_bog_asphodel"}, + {"id": 19862, "synset": "hellebore.n.01", "name": "hellebore"}, + {"id": 19863, "synset": "white_hellebore.n.01", "name": "white_hellebore"}, + {"id": 19864, "synset": "squaw_grass.n.01", "name": "squaw_grass"}, + {"id": 19865, "synset": "death_camas.n.01", "name": "death_camas"}, + {"id": 19866, "synset": "alkali_grass.n.01", "name": "alkali_grass"}, + {"id": 19867, "synset": "white_camas.n.01", "name": "white_camas"}, + {"id": 19868, "synset": "poison_camas.n.01", "name": "poison_camas"}, + {"id": 19869, "synset": "grassy_death_camas.n.01", "name": "grassy_death_camas"}, + {"id": 19870, "synset": "prairie_wake-robin.n.01", "name": "prairie_wake-robin"}, + {"id": 19871, "synset": "dwarf-white_trillium.n.01", "name": "dwarf-white_trillium"}, + {"id": 19872, "synset": "herb_paris.n.01", "name": "herb_Paris"}, + {"id": 19873, "synset": "sarsaparilla.n.01", "name": "sarsaparilla"}, + {"id": 19874, "synset": "bullbrier.n.01", "name": "bullbrier"}, + {"id": 19875, "synset": "rough_bindweed.n.01", "name": "rough_bindweed"}, + {"id": 19876, "synset": "clintonia.n.01", "name": "clintonia"}, + {"id": 19877, "synset": "false_lily_of_the_valley.n.02", "name": "false_lily_of_the_valley"}, + {"id": 19878, "synset": "false_lily_of_the_valley.n.01", "name": "false_lily_of_the_valley"}, + {"id": 19879, "synset": "solomon's-seal.n.01", "name": "Solomon's-seal"}, + {"id": 19880, "synset": "great_solomon's-seal.n.01", "name": "great_Solomon's-seal"}, + {"id": 19881, "synset": "bellwort.n.01", "name": "bellwort"}, + {"id": 19882, "synset": "strawflower.n.01", "name": "strawflower"}, + {"id": 19883, "synset": "pia.n.01", "name": "pia"}, + {"id": 19884, "synset": "agave.n.01", "name": "agave"}, + {"id": 19885, "synset": "american_agave.n.01", "name": "American_agave"}, + {"id": 19886, "synset": "sisal.n.02", "name": "sisal"}, + {"id": 19887, "synset": "maguey.n.02", "name": "maguey"}, + {"id": 19888, "synset": "maguey.n.01", "name": "maguey"}, + {"id": 19889, "synset": "agave_tequilana.n.01", "name": "Agave_tequilana"}, + {"id": 19890, "synset": "cabbage_tree.n.03", "name": "cabbage_tree"}, + {"id": 19891, "synset": "dracaena.n.01", "name": "dracaena"}, + {"id": 19892, "synset": "tuberose.n.01", "name": "tuberose"}, + {"id": 19893, "synset": "sansevieria.n.01", "name": "sansevieria"}, + {"id": 19894, "synset": "african_bowstring_hemp.n.01", "name": "African_bowstring_hemp"}, + {"id": 19895, "synset": "ceylon_bowstring_hemp.n.01", "name": "Ceylon_bowstring_hemp"}, + {"id": 19896, "synset": "mother-in-law's_tongue.n.01", "name": "mother-in-law's_tongue"}, + {"id": 19897, "synset": "spanish_bayonet.n.02", "name": "Spanish_bayonet"}, + {"id": 19898, "synset": "spanish_bayonet.n.01", "name": "Spanish_bayonet"}, + {"id": 19899, "synset": "joshua_tree.n.01", "name": "Joshua_tree"}, + {"id": 19900, "synset": "soapweed.n.01", "name": "soapweed"}, + {"id": 19901, "synset": "adam's_needle.n.01", "name": "Adam's_needle"}, + {"id": 19902, "synset": "bear_grass.n.02", "name": "bear_grass"}, + {"id": 19903, "synset": "spanish_dagger.n.01", "name": "Spanish_dagger"}, + {"id": 19904, "synset": "our_lord's_candle.n.01", "name": "Our_Lord's_candle"}, + {"id": 19905, "synset": "water_shamrock.n.01", "name": "water_shamrock"}, + {"id": 19906, "synset": "butterfly_bush.n.01", "name": "butterfly_bush"}, + {"id": 19907, "synset": "yellow_jasmine.n.01", "name": "yellow_jasmine"}, + {"id": 19908, "synset": "flax.n.02", "name": "flax"}, + {"id": 19909, "synset": "calabar_bean.n.01", "name": "calabar_bean"}, + {"id": 19910, "synset": "bonduc.n.02", "name": "bonduc"}, + {"id": 19911, "synset": "divi-divi.n.02", "name": "divi-divi"}, + {"id": 19912, "synset": "mysore_thorn.n.01", "name": "Mysore_thorn"}, + {"id": 19913, "synset": "brazilian_ironwood.n.01", "name": "brazilian_ironwood"}, + {"id": 19914, "synset": "bird_of_paradise.n.01", "name": "bird_of_paradise"}, + {"id": 19915, "synset": "shingle_tree.n.01", "name": "shingle_tree"}, + {"id": 19916, "synset": "mountain_ebony.n.01", "name": "mountain_ebony"}, + {"id": 19917, "synset": "msasa.n.01", "name": "msasa"}, + {"id": 19918, "synset": "cassia.n.01", "name": "cassia"}, + {"id": 19919, "synset": "golden_shower_tree.n.01", "name": "golden_shower_tree"}, + {"id": 19920, "synset": "pink_shower.n.01", "name": "pink_shower"}, + {"id": 19921, "synset": "rainbow_shower.n.01", "name": "rainbow_shower"}, + {"id": 19922, "synset": "horse_cassia.n.01", "name": "horse_cassia"}, + {"id": 19923, "synset": "carob.n.02", "name": "carob"}, + {"id": 19924, "synset": "carob.n.01", "name": "carob"}, + {"id": 19925, "synset": "paloverde.n.01", "name": "paloverde"}, + {"id": 19926, "synset": "royal_poinciana.n.01", "name": "royal_poinciana"}, + {"id": 19927, "synset": "locust_tree.n.01", "name": "locust_tree"}, + {"id": 19928, "synset": "water_locust.n.01", "name": "water_locust"}, + {"id": 19929, "synset": "honey_locust.n.01", "name": "honey_locust"}, + {"id": 19930, "synset": "kentucky_coffee_tree.n.01", "name": "Kentucky_coffee_tree"}, + {"id": 19931, "synset": "logwood.n.02", "name": "logwood"}, + {"id": 19932, "synset": "jerusalem_thorn.n.03", "name": "Jerusalem_thorn"}, + {"id": 19933, "synset": "palo_verde.n.01", "name": "palo_verde"}, + {"id": 19934, "synset": "dalmatian_laburnum.n.01", "name": "Dalmatian_laburnum"}, + {"id": 19935, "synset": "senna.n.01", "name": "senna"}, + {"id": 19936, "synset": "avaram.n.01", "name": "avaram"}, + {"id": 19937, "synset": "alexandria_senna.n.01", "name": "Alexandria_senna"}, + {"id": 19938, "synset": "wild_senna.n.01", "name": "wild_senna"}, + {"id": 19939, "synset": "sicklepod.n.01", "name": "sicklepod"}, + {"id": 19940, "synset": "coffee_senna.n.01", "name": "coffee_senna"}, + {"id": 19941, "synset": "tamarind.n.01", "name": "tamarind"}, + {"id": 19942, "synset": "false_indigo.n.03", "name": "false_indigo"}, + {"id": 19943, "synset": "false_indigo.n.02", "name": "false_indigo"}, + {"id": 19944, "synset": "hog_peanut.n.01", "name": "hog_peanut"}, + {"id": 19945, "synset": "angelim.n.01", "name": "angelim"}, + {"id": 19946, "synset": "cabbage_bark.n.01", "name": "cabbage_bark"}, + {"id": 19947, "synset": "kidney_vetch.n.01", "name": "kidney_vetch"}, + {"id": 19948, "synset": "groundnut.n.01", "name": "groundnut"}, + {"id": 19949, "synset": "rooibos.n.01", "name": "rooibos"}, + {"id": 19950, "synset": "milk_vetch.n.01", "name": "milk_vetch"}, + {"id": 19951, "synset": "alpine_milk_vetch.n.01", "name": "alpine_milk_vetch"}, + {"id": 19952, "synset": "purple_milk_vetch.n.01", "name": "purple_milk_vetch"}, + {"id": 19953, "synset": "camwood.n.01", "name": "camwood"}, + {"id": 19954, "synset": "wild_indigo.n.01", "name": "wild_indigo"}, + {"id": 19955, "synset": "blue_false_indigo.n.01", "name": "blue_false_indigo"}, + {"id": 19956, "synset": "white_false_indigo.n.01", "name": "white_false_indigo"}, + {"id": 19957, "synset": "indigo_broom.n.01", "name": "indigo_broom"}, + {"id": 19958, "synset": "dhak.n.01", "name": "dhak"}, + {"id": 19959, "synset": "pigeon_pea.n.01", "name": "pigeon_pea"}, + {"id": 19960, "synset": "sword_bean.n.01", "name": "sword_bean"}, + {"id": 19961, "synset": "pea_tree.n.01", "name": "pea_tree"}, + {"id": 19962, "synset": "siberian_pea_tree.n.01", "name": "Siberian_pea_tree"}, + {"id": 19963, "synset": "chinese_pea_tree.n.01", "name": "Chinese_pea_tree"}, + {"id": 19964, "synset": "moreton_bay_chestnut.n.01", "name": "Moreton_Bay_chestnut"}, + {"id": 19965, "synset": "butterfly_pea.n.03", "name": "butterfly_pea"}, + {"id": 19966, "synset": "judas_tree.n.01", "name": "Judas_tree"}, + {"id": 19967, "synset": "redbud.n.01", "name": "redbud"}, + {"id": 19968, "synset": "western_redbud.n.01", "name": "western_redbud"}, + {"id": 19969, "synset": "tagasaste.n.01", "name": "tagasaste"}, + {"id": 19970, "synset": "weeping_tree_broom.n.01", "name": "weeping_tree_broom"}, + {"id": 19971, "synset": "flame_pea.n.01", "name": "flame_pea"}, + {"id": 19972, "synset": "chickpea.n.02", "name": "chickpea"}, + {"id": 19973, "synset": "kentucky_yellowwood.n.01", "name": "Kentucky_yellowwood"}, + {"id": 19974, "synset": "glory_pea.n.01", "name": "glory_pea"}, + {"id": 19975, "synset": "desert_pea.n.01", "name": "desert_pea"}, + {"id": 19976, "synset": "parrot's_beak.n.01", "name": "parrot's_beak"}, + {"id": 19977, "synset": "butterfly_pea.n.02", "name": "butterfly_pea"}, + {"id": 19978, "synset": "blue_pea.n.01", "name": "blue_pea"}, + {"id": 19979, "synset": "telegraph_plant.n.01", "name": "telegraph_plant"}, + {"id": 19980, "synset": "bladder_senna.n.01", "name": "bladder_senna"}, + {"id": 19981, "synset": "axseed.n.01", "name": "axseed"}, + {"id": 19982, "synset": "crotalaria.n.01", "name": "crotalaria"}, + {"id": 19983, "synset": "guar.n.01", "name": "guar"}, + {"id": 19984, "synset": "white_broom.n.01", "name": "white_broom"}, + {"id": 19985, "synset": "common_broom.n.01", "name": "common_broom"}, + {"id": 19986, "synset": "rosewood.n.02", "name": "rosewood"}, + {"id": 19987, "synset": "indian_blackwood.n.01", "name": "Indian_blackwood"}, + {"id": 19988, "synset": "sissoo.n.01", "name": "sissoo"}, + {"id": 19989, "synset": "kingwood.n.02", "name": "kingwood"}, + {"id": 19990, "synset": "brazilian_rosewood.n.01", "name": "Brazilian_rosewood"}, + {"id": 19991, "synset": "cocobolo.n.01", "name": "cocobolo"}, + {"id": 19992, "synset": "blackwood.n.02", "name": "blackwood"}, + {"id": 19993, "synset": "bitter_pea.n.01", "name": "bitter_pea"}, + {"id": 19994, "synset": "derris.n.01", "name": "derris"}, + {"id": 19995, "synset": "derris_root.n.01", "name": "derris_root"}, + {"id": 19996, "synset": "prairie_mimosa.n.01", "name": "prairie_mimosa"}, + {"id": 19997, "synset": "tick_trefoil.n.01", "name": "tick_trefoil"}, + {"id": 19998, "synset": "beggarweed.n.01", "name": "beggarweed"}, + {"id": 19999, "synset": "australian_pea.n.01", "name": "Australian_pea"}, + {"id": 20000, "synset": "coral_tree.n.01", "name": "coral_tree"}, + {"id": 20001, "synset": "kaffir_boom.n.02", "name": "kaffir_boom"}, + {"id": 20002, "synset": "coral_bean_tree.n.01", "name": "coral_bean_tree"}, + {"id": 20003, "synset": "ceibo.n.01", "name": "ceibo"}, + {"id": 20004, "synset": "kaffir_boom.n.01", "name": "kaffir_boom"}, + {"id": 20005, "synset": "indian_coral_tree.n.01", "name": "Indian_coral_tree"}, + {"id": 20006, "synset": "cork_tree.n.02", "name": "cork_tree"}, + {"id": 20007, "synset": "goat's_rue.n.02", "name": "goat's_rue"}, + {"id": 20008, "synset": "poison_bush.n.01", "name": "poison_bush"}, + {"id": 20009, "synset": "spanish_broom.n.02", "name": "Spanish_broom"}, + {"id": 20010, "synset": "woodwaxen.n.01", "name": "woodwaxen"}, + {"id": 20011, "synset": "chanar.n.01", "name": "chanar"}, + {"id": 20012, "synset": "gliricidia.n.01", "name": "gliricidia"}, + {"id": 20013, "synset": "soy.n.01", "name": "soy"}, + {"id": 20014, "synset": "licorice.n.01", "name": "licorice"}, + {"id": 20015, "synset": "wild_licorice.n.02", "name": "wild_licorice"}, + {"id": 20016, "synset": "licorice_root.n.01", "name": "licorice_root"}, + { + "id": 20017, + "synset": "western_australia_coral_pea.n.01", + "name": "Western_Australia_coral_pea", + }, + {"id": 20018, "synset": "sweet_vetch.n.01", "name": "sweet_vetch"}, + {"id": 20019, "synset": "french_honeysuckle.n.02", "name": "French_honeysuckle"}, + {"id": 20020, "synset": "anil.n.02", "name": "anil"}, + {"id": 20021, "synset": "scarlet_runner.n.02", "name": "scarlet_runner"}, + {"id": 20022, "synset": "hyacinth_bean.n.01", "name": "hyacinth_bean"}, + {"id": 20023, "synset": "scotch_laburnum.n.01", "name": "Scotch_laburnum"}, + {"id": 20024, "synset": "vetchling.n.01", "name": "vetchling"}, + {"id": 20025, "synset": "wild_pea.n.01", "name": "wild_pea"}, + {"id": 20026, "synset": "everlasting_pea.n.01", "name": "everlasting_pea"}, + {"id": 20027, "synset": "beach_pea.n.01", "name": "beach_pea"}, + {"id": 20028, "synset": "grass_vetch.n.01", "name": "grass_vetch"}, + {"id": 20029, "synset": "marsh_pea.n.01", "name": "marsh_pea"}, + {"id": 20030, "synset": "common_vetchling.n.01", "name": "common_vetchling"}, + {"id": 20031, "synset": "grass_pea.n.01", "name": "grass_pea"}, + {"id": 20032, "synset": "tangier_pea.n.01", "name": "Tangier_pea"}, + {"id": 20033, "synset": "heath_pea.n.01", "name": "heath_pea"}, + {"id": 20034, "synset": "bicolor_lespediza.n.01", "name": "bicolor_lespediza"}, + {"id": 20035, "synset": "japanese_clover.n.01", "name": "japanese_clover"}, + {"id": 20036, "synset": "korean_lespedeza.n.01", "name": "Korean_lespedeza"}, + {"id": 20037, "synset": "sericea_lespedeza.n.01", "name": "sericea_lespedeza"}, + {"id": 20038, "synset": "lentil.n.03", "name": "lentil"}, + {"id": 20039, "synset": "lentil.n.02", "name": "lentil"}, + { + "id": 20040, + "synset": "prairie_bird's-foot_trefoil.n.01", + "name": "prairie_bird's-foot_trefoil", + }, + {"id": 20041, "synset": "bird's_foot_trefoil.n.02", "name": "bird's_foot_trefoil"}, + {"id": 20042, "synset": "winged_pea.n.02", "name": "winged_pea"}, + {"id": 20043, "synset": "lupine.n.01", "name": "lupine"}, + {"id": 20044, "synset": "white_lupine.n.01", "name": "white_lupine"}, + {"id": 20045, "synset": "tree_lupine.n.01", "name": "tree_lupine"}, + {"id": 20046, "synset": "wild_lupine.n.01", "name": "wild_lupine"}, + {"id": 20047, "synset": "bluebonnet.n.01", "name": "bluebonnet"}, + {"id": 20048, "synset": "texas_bluebonnet.n.01", "name": "Texas_bluebonnet"}, + {"id": 20049, "synset": "medic.n.01", "name": "medic"}, + {"id": 20050, "synset": "moon_trefoil.n.01", "name": "moon_trefoil"}, + {"id": 20051, "synset": "sickle_alfalfa.n.01", "name": "sickle_alfalfa"}, + {"id": 20052, "synset": "calvary_clover.n.01", "name": "Calvary_clover"}, + {"id": 20053, "synset": "black_medick.n.01", "name": "black_medick"}, + {"id": 20054, "synset": "alfalfa.n.01", "name": "alfalfa"}, + {"id": 20055, "synset": "millettia.n.01", "name": "millettia"}, + {"id": 20056, "synset": "mucuna.n.01", "name": "mucuna"}, + {"id": 20057, "synset": "cowage.n.02", "name": "cowage"}, + {"id": 20058, "synset": "tolu_tree.n.01", "name": "tolu_tree"}, + {"id": 20059, "synset": "peruvian_balsam.n.01", "name": "Peruvian_balsam"}, + {"id": 20060, "synset": "sainfoin.n.01", "name": "sainfoin"}, + {"id": 20061, "synset": "restharrow.n.02", "name": "restharrow"}, + {"id": 20062, "synset": "bead_tree.n.01", "name": "bead_tree"}, + {"id": 20063, "synset": "jumby_bead.n.01", "name": "jumby_bead"}, + {"id": 20064, "synset": "locoweed.n.01", "name": "locoweed"}, + {"id": 20065, "synset": "purple_locoweed.n.01", "name": "purple_locoweed"}, + {"id": 20066, "synset": "tumbleweed.n.01", "name": "tumbleweed"}, + {"id": 20067, "synset": "yam_bean.n.02", "name": "yam_bean"}, + {"id": 20068, "synset": "shamrock_pea.n.01", "name": "shamrock_pea"}, + {"id": 20069, "synset": "pole_bean.n.01", "name": "pole_bean"}, + {"id": 20070, "synset": "kidney_bean.n.01", "name": "kidney_bean"}, + {"id": 20071, "synset": "haricot.n.01", "name": "haricot"}, + {"id": 20072, "synset": "wax_bean.n.01", "name": "wax_bean"}, + {"id": 20073, "synset": "scarlet_runner.n.01", "name": "scarlet_runner"}, + {"id": 20074, "synset": "lima_bean.n.02", "name": "lima_bean"}, + {"id": 20075, "synset": "sieva_bean.n.01", "name": "sieva_bean"}, + {"id": 20076, "synset": "tepary_bean.n.01", "name": "tepary_bean"}, + {"id": 20077, "synset": "chaparral_pea.n.01", "name": "chaparral_pea"}, + {"id": 20078, "synset": "jamaica_dogwood.n.01", "name": "Jamaica_dogwood"}, + {"id": 20079, "synset": "pea.n.02", "name": "pea"}, + {"id": 20080, "synset": "garden_pea.n.01", "name": "garden_pea"}, + {"id": 20081, "synset": "edible-pod_pea.n.01", "name": "edible-pod_pea"}, + {"id": 20082, "synset": "sugar_snap_pea.n.01", "name": "sugar_snap_pea"}, + {"id": 20083, "synset": "field_pea.n.02", "name": "field_pea"}, + {"id": 20084, "synset": "field_pea.n.01", "name": "field_pea"}, + {"id": 20085, "synset": "common_flat_pea.n.01", "name": "common_flat_pea"}, + {"id": 20086, "synset": "quira.n.02", "name": "quira"}, + {"id": 20087, "synset": "roble.n.01", "name": "roble"}, + {"id": 20088, "synset": "panama_redwood_tree.n.01", "name": "Panama_redwood_tree"}, + {"id": 20089, "synset": "indian_beech.n.01", "name": "Indian_beech"}, + {"id": 20090, "synset": "winged_bean.n.01", "name": "winged_bean"}, + {"id": 20091, "synset": "breadroot.n.01", "name": "breadroot"}, + {"id": 20092, "synset": "bloodwood_tree.n.01", "name": "bloodwood_tree"}, + {"id": 20093, "synset": "kino.n.02", "name": "kino"}, + {"id": 20094, "synset": "red_sandalwood.n.02", "name": "red_sandalwood"}, + {"id": 20095, "synset": "kudzu.n.01", "name": "kudzu"}, + {"id": 20096, "synset": "bristly_locust.n.01", "name": "bristly_locust"}, + {"id": 20097, "synset": "black_locust.n.02", "name": "black_locust"}, + {"id": 20098, "synset": "clammy_locust.n.01", "name": "clammy_locust"}, + {"id": 20099, "synset": "carib_wood.n.01", "name": "carib_wood"}, + {"id": 20100, "synset": "colorado_river_hemp.n.01", "name": "Colorado_River_hemp"}, + {"id": 20101, "synset": "scarlet_wisteria_tree.n.01", "name": "scarlet_wisteria_tree"}, + {"id": 20102, "synset": "japanese_pagoda_tree.n.01", "name": "Japanese_pagoda_tree"}, + {"id": 20103, "synset": "mescal_bean.n.01", "name": "mescal_bean"}, + {"id": 20104, "synset": "kowhai.n.01", "name": "kowhai"}, + {"id": 20105, "synset": "jade_vine.n.01", "name": "jade_vine"}, + {"id": 20106, "synset": "hoary_pea.n.01", "name": "hoary_pea"}, + {"id": 20107, "synset": "bastard_indigo.n.01", "name": "bastard_indigo"}, + {"id": 20108, "synset": "catgut.n.01", "name": "catgut"}, + {"id": 20109, "synset": "bush_pea.n.01", "name": "bush_pea"}, + {"id": 20110, "synset": "false_lupine.n.01", "name": "false_lupine"}, + {"id": 20111, "synset": "carolina_lupine.n.01", "name": "Carolina_lupine"}, + {"id": 20112, "synset": "tipu.n.01", "name": "tipu"}, + {"id": 20113, "synset": "bird's_foot_trefoil.n.01", "name": "bird's_foot_trefoil"}, + {"id": 20114, "synset": "fenugreek.n.01", "name": "fenugreek"}, + {"id": 20115, "synset": "gorse.n.01", "name": "gorse"}, + {"id": 20116, "synset": "vetch.n.01", "name": "vetch"}, + {"id": 20117, "synset": "tufted_vetch.n.01", "name": "tufted_vetch"}, + {"id": 20118, "synset": "broad_bean.n.01", "name": "broad_bean"}, + {"id": 20119, "synset": "bitter_betch.n.01", "name": "bitter_betch"}, + {"id": 20120, "synset": "bush_vetch.n.01", "name": "bush_vetch"}, + {"id": 20121, "synset": "moth_bean.n.01", "name": "moth_bean"}, + {"id": 20122, "synset": "snailflower.n.01", "name": "snailflower"}, + {"id": 20123, "synset": "mung.n.01", "name": "mung"}, + {"id": 20124, "synset": "cowpea.n.02", "name": "cowpea"}, + {"id": 20125, "synset": "cowpea.n.01", "name": "cowpea"}, + {"id": 20126, "synset": "asparagus_bean.n.01", "name": "asparagus_bean"}, + {"id": 20127, "synset": "swamp_oak.n.01", "name": "swamp_oak"}, + {"id": 20128, "synset": "keurboom.n.02", "name": "keurboom"}, + {"id": 20129, "synset": "keurboom.n.01", "name": "keurboom"}, + {"id": 20130, "synset": "japanese_wistaria.n.01", "name": "Japanese_wistaria"}, + {"id": 20131, "synset": "chinese_wistaria.n.01", "name": "Chinese_wistaria"}, + {"id": 20132, "synset": "american_wistaria.n.01", "name": "American_wistaria"}, + {"id": 20133, "synset": "silky_wisteria.n.01", "name": "silky_wisteria"}, + {"id": 20134, "synset": "palm.n.03", "name": "palm"}, + {"id": 20135, "synset": "sago_palm.n.01", "name": "sago_palm"}, + {"id": 20136, "synset": "feather_palm.n.01", "name": "feather_palm"}, + {"id": 20137, "synset": "fan_palm.n.01", "name": "fan_palm"}, + {"id": 20138, "synset": "palmetto.n.01", "name": "palmetto"}, + {"id": 20139, "synset": "coyol.n.01", "name": "coyol"}, + {"id": 20140, "synset": "grugru.n.01", "name": "grugru"}, + {"id": 20141, "synset": "areca.n.01", "name": "areca"}, + {"id": 20142, "synset": "betel_palm.n.01", "name": "betel_palm"}, + {"id": 20143, "synset": "sugar_palm.n.01", "name": "sugar_palm"}, + {"id": 20144, "synset": "piassava_palm.n.01", "name": "piassava_palm"}, + {"id": 20145, "synset": "coquilla_nut.n.01", "name": "coquilla_nut"}, + {"id": 20146, "synset": "palmyra.n.01", "name": "palmyra"}, + {"id": 20147, "synset": "calamus.n.01", "name": "calamus"}, + {"id": 20148, "synset": "rattan.n.01", "name": "rattan"}, + {"id": 20149, "synset": "lawyer_cane.n.01", "name": "lawyer_cane"}, + {"id": 20150, "synset": "fishtail_palm.n.01", "name": "fishtail_palm"}, + {"id": 20151, "synset": "wine_palm.n.01", "name": "wine_palm"}, + {"id": 20152, "synset": "wax_palm.n.03", "name": "wax_palm"}, + {"id": 20153, "synset": "coconut.n.03", "name": "coconut"}, + {"id": 20154, "synset": "carnauba.n.02", "name": "carnauba"}, + {"id": 20155, "synset": "caranday.n.01", "name": "caranday"}, + {"id": 20156, "synset": "corozo.n.01", "name": "corozo"}, + {"id": 20157, "synset": "gebang_palm.n.01", "name": "gebang_palm"}, + {"id": 20158, "synset": "latanier.n.01", "name": "latanier"}, + {"id": 20159, "synset": "talipot.n.01", "name": "talipot"}, + {"id": 20160, "synset": "oil_palm.n.01", "name": "oil_palm"}, + {"id": 20161, "synset": "african_oil_palm.n.01", "name": "African_oil_palm"}, + {"id": 20162, "synset": "american_oil_palm.n.01", "name": "American_oil_palm"}, + {"id": 20163, "synset": "palm_nut.n.01", "name": "palm_nut"}, + {"id": 20164, "synset": "cabbage_palm.n.04", "name": "cabbage_palm"}, + {"id": 20165, "synset": "cabbage_palm.n.03", "name": "cabbage_palm"}, + {"id": 20166, "synset": "true_sago_palm.n.01", "name": "true_sago_palm"}, + {"id": 20167, "synset": "nipa_palm.n.01", "name": "nipa_palm"}, + {"id": 20168, "synset": "babassu.n.01", "name": "babassu"}, + {"id": 20169, "synset": "babassu_nut.n.01", "name": "babassu_nut"}, + {"id": 20170, "synset": "cohune_palm.n.01", "name": "cohune_palm"}, + {"id": 20171, "synset": "cohune_nut.n.01", "name": "cohune_nut"}, + {"id": 20172, "synset": "date_palm.n.01", "name": "date_palm"}, + {"id": 20173, "synset": "ivory_palm.n.01", "name": "ivory_palm"}, + {"id": 20174, "synset": "raffia_palm.n.01", "name": "raffia_palm"}, + {"id": 20175, "synset": "bamboo_palm.n.02", "name": "bamboo_palm"}, + {"id": 20176, "synset": "lady_palm.n.01", "name": "lady_palm"}, + {"id": 20177, "synset": "miniature_fan_palm.n.01", "name": "miniature_fan_palm"}, + {"id": 20178, "synset": "reed_rhapis.n.01", "name": "reed_rhapis"}, + {"id": 20179, "synset": "royal_palm.n.01", "name": "royal_palm"}, + {"id": 20180, "synset": "cabbage_palm.n.02", "name": "cabbage_palm"}, + {"id": 20181, "synset": "cabbage_palmetto.n.01", "name": "cabbage_palmetto"}, + {"id": 20182, "synset": "saw_palmetto.n.01", "name": "saw_palmetto"}, + {"id": 20183, "synset": "thatch_palm.n.01", "name": "thatch_palm"}, + {"id": 20184, "synset": "key_palm.n.01", "name": "key_palm"}, + {"id": 20185, "synset": "english_plantain.n.01", "name": "English_plantain"}, + {"id": 20186, "synset": "broad-leaved_plantain.n.02", "name": "broad-leaved_plantain"}, + {"id": 20187, "synset": "hoary_plantain.n.02", "name": "hoary_plantain"}, + {"id": 20188, "synset": "fleawort.n.01", "name": "fleawort"}, + {"id": 20189, "synset": "rugel's_plantain.n.01", "name": "rugel's_plantain"}, + {"id": 20190, "synset": "hoary_plantain.n.01", "name": "hoary_plantain"}, + {"id": 20191, "synset": "buckwheat.n.01", "name": "buckwheat"}, + {"id": 20192, "synset": "prince's-feather.n.01", "name": "prince's-feather"}, + {"id": 20193, "synset": "eriogonum.n.01", "name": "eriogonum"}, + {"id": 20194, "synset": "umbrella_plant.n.02", "name": "umbrella_plant"}, + {"id": 20195, "synset": "wild_buckwheat.n.01", "name": "wild_buckwheat"}, + {"id": 20196, "synset": "rhubarb.n.02", "name": "rhubarb"}, + {"id": 20197, "synset": "himalayan_rhubarb.n.01", "name": "Himalayan_rhubarb"}, + {"id": 20198, "synset": "pie_plant.n.01", "name": "pie_plant"}, + {"id": 20199, "synset": "chinese_rhubarb.n.01", "name": "Chinese_rhubarb"}, + {"id": 20200, "synset": "sour_dock.n.01", "name": "sour_dock"}, + {"id": 20201, "synset": "sheep_sorrel.n.01", "name": "sheep_sorrel"}, + {"id": 20202, "synset": "bitter_dock.n.01", "name": "bitter_dock"}, + {"id": 20203, "synset": "french_sorrel.n.01", "name": "French_sorrel"}, + {"id": 20204, "synset": "yellow-eyed_grass.n.01", "name": "yellow-eyed_grass"}, + {"id": 20205, "synset": "commelina.n.01", "name": "commelina"}, + {"id": 20206, "synset": "spiderwort.n.01", "name": "spiderwort"}, + {"id": 20207, "synset": "pineapple.n.01", "name": "pineapple"}, + {"id": 20208, "synset": "pipewort.n.01", "name": "pipewort"}, + {"id": 20209, "synset": "water_hyacinth.n.01", "name": "water_hyacinth"}, + {"id": 20210, "synset": "water_star_grass.n.01", "name": "water_star_grass"}, + {"id": 20211, "synset": "naiad.n.01", "name": "naiad"}, + {"id": 20212, "synset": "water_plantain.n.01", "name": "water_plantain"}, + { + "id": 20213, + "synset": "narrow-leaved_water_plantain.n.01", + "name": "narrow-leaved_water_plantain", + }, + {"id": 20214, "synset": "hydrilla.n.01", "name": "hydrilla"}, + {"id": 20215, "synset": "american_frogbit.n.01", "name": "American_frogbit"}, + {"id": 20216, "synset": "waterweed.n.01", "name": "waterweed"}, + {"id": 20217, "synset": "canadian_pondweed.n.01", "name": "Canadian_pondweed"}, + {"id": 20218, "synset": "tape_grass.n.01", "name": "tape_grass"}, + {"id": 20219, "synset": "pondweed.n.01", "name": "pondweed"}, + {"id": 20220, "synset": "curled_leaf_pondweed.n.01", "name": "curled_leaf_pondweed"}, + {"id": 20221, "synset": "loddon_pondweed.n.01", "name": "loddon_pondweed"}, + {"id": 20222, "synset": "frog's_lettuce.n.01", "name": "frog's_lettuce"}, + {"id": 20223, "synset": "arrow_grass.n.01", "name": "arrow_grass"}, + {"id": 20224, "synset": "horned_pondweed.n.01", "name": "horned_pondweed"}, + {"id": 20225, "synset": "eelgrass.n.01", "name": "eelgrass"}, + {"id": 20226, "synset": "rose.n.01", "name": "rose"}, + {"id": 20227, "synset": "hip.n.05", "name": "hip"}, + {"id": 20228, "synset": "banksia_rose.n.01", "name": "banksia_rose"}, + {"id": 20229, "synset": "damask_rose.n.01", "name": "damask_rose"}, + {"id": 20230, "synset": "sweetbrier.n.01", "name": "sweetbrier"}, + {"id": 20231, "synset": "cherokee_rose.n.01", "name": "Cherokee_rose"}, + {"id": 20232, "synset": "musk_rose.n.01", "name": "musk_rose"}, + {"id": 20233, "synset": "agrimonia.n.01", "name": "agrimonia"}, + {"id": 20234, "synset": "harvest-lice.n.01", "name": "harvest-lice"}, + {"id": 20235, "synset": "fragrant_agrimony.n.01", "name": "fragrant_agrimony"}, + {"id": 20236, "synset": "alderleaf_juneberry.n.01", "name": "alderleaf_Juneberry"}, + {"id": 20237, "synset": "flowering_quince.n.01", "name": "flowering_quince"}, + {"id": 20238, "synset": "japonica.n.02", "name": "japonica"}, + {"id": 20239, "synset": "coco_plum.n.01", "name": "coco_plum"}, + {"id": 20240, "synset": "cotoneaster.n.01", "name": "cotoneaster"}, + {"id": 20241, "synset": "cotoneaster_dammeri.n.01", "name": "Cotoneaster_dammeri"}, + {"id": 20242, "synset": "cotoneaster_horizontalis.n.01", "name": "Cotoneaster_horizontalis"}, + {"id": 20243, "synset": "parsley_haw.n.01", "name": "parsley_haw"}, + {"id": 20244, "synset": "scarlet_haw.n.01", "name": "scarlet_haw"}, + {"id": 20245, "synset": "blackthorn.n.02", "name": "blackthorn"}, + {"id": 20246, "synset": "cockspur_thorn.n.01", "name": "cockspur_thorn"}, + {"id": 20247, "synset": "mayhaw.n.01", "name": "mayhaw"}, + {"id": 20248, "synset": "red_haw.n.02", "name": "red_haw"}, + {"id": 20249, "synset": "red_haw.n.01", "name": "red_haw"}, + {"id": 20250, "synset": "quince.n.01", "name": "quince"}, + {"id": 20251, "synset": "mountain_avens.n.01", "name": "mountain_avens"}, + {"id": 20252, "synset": "loquat.n.01", "name": "loquat"}, + {"id": 20253, "synset": "beach_strawberry.n.01", "name": "beach_strawberry"}, + {"id": 20254, "synset": "virginia_strawberry.n.01", "name": "Virginia_strawberry"}, + {"id": 20255, "synset": "avens.n.01", "name": "avens"}, + {"id": 20256, "synset": "yellow_avens.n.02", "name": "yellow_avens"}, + {"id": 20257, "synset": "yellow_avens.n.01", "name": "yellow_avens"}, + {"id": 20258, "synset": "prairie_smoke.n.01", "name": "prairie_smoke"}, + {"id": 20259, "synset": "bennet.n.01", "name": "bennet"}, + {"id": 20260, "synset": "toyon.n.01", "name": "toyon"}, + {"id": 20261, "synset": "apple_tree.n.01", "name": "apple_tree"}, + {"id": 20262, "synset": "apple.n.02", "name": "apple"}, + {"id": 20263, "synset": "wild_apple.n.01", "name": "wild_apple"}, + {"id": 20264, "synset": "crab_apple.n.01", "name": "crab_apple"}, + {"id": 20265, "synset": "siberian_crab.n.01", "name": "Siberian_crab"}, + {"id": 20266, "synset": "wild_crab.n.01", "name": "wild_crab"}, + {"id": 20267, "synset": "american_crab_apple.n.01", "name": "American_crab_apple"}, + {"id": 20268, "synset": "oregon_crab_apple.n.01", "name": "Oregon_crab_apple"}, + {"id": 20269, "synset": "southern_crab_apple.n.01", "name": "Southern_crab_apple"}, + {"id": 20270, "synset": "iowa_crab.n.01", "name": "Iowa_crab"}, + {"id": 20271, "synset": "bechtel_crab.n.01", "name": "Bechtel_crab"}, + {"id": 20272, "synset": "medlar.n.02", "name": "medlar"}, + {"id": 20273, "synset": "cinquefoil.n.01", "name": "cinquefoil"}, + {"id": 20274, "synset": "silverweed.n.02", "name": "silverweed"}, + {"id": 20275, "synset": "salad_burnet.n.01", "name": "salad_burnet"}, + {"id": 20276, "synset": "plum.n.01", "name": "plum"}, + {"id": 20277, "synset": "wild_plum.n.01", "name": "wild_plum"}, + {"id": 20278, "synset": "allegheny_plum.n.01", "name": "Allegheny_plum"}, + {"id": 20279, "synset": "american_red_plum.n.01", "name": "American_red_plum"}, + {"id": 20280, "synset": "chickasaw_plum.n.01", "name": "chickasaw_plum"}, + {"id": 20281, "synset": "beach_plum.n.01", "name": "beach_plum"}, + {"id": 20282, "synset": "common_plum.n.01", "name": "common_plum"}, + {"id": 20283, "synset": "bullace.n.01", "name": "bullace"}, + {"id": 20284, "synset": "damson_plum.n.02", "name": "damson_plum"}, + {"id": 20285, "synset": "big-tree_plum.n.01", "name": "big-tree_plum"}, + {"id": 20286, "synset": "canada_plum.n.01", "name": "Canada_plum"}, + {"id": 20287, "synset": "plumcot.n.01", "name": "plumcot"}, + {"id": 20288, "synset": "apricot.n.01", "name": "apricot"}, + {"id": 20289, "synset": "japanese_apricot.n.01", "name": "Japanese_apricot"}, + {"id": 20290, "synset": "common_apricot.n.01", "name": "common_apricot"}, + {"id": 20291, "synset": "purple_apricot.n.01", "name": "purple_apricot"}, + {"id": 20292, "synset": "cherry.n.02", "name": "cherry"}, + {"id": 20293, "synset": "wild_cherry.n.02", "name": "wild_cherry"}, + {"id": 20294, "synset": "wild_cherry.n.01", "name": "wild_cherry"}, + {"id": 20295, "synset": "sweet_cherry.n.01", "name": "sweet_cherry"}, + {"id": 20296, "synset": "heart_cherry.n.01", "name": "heart_cherry"}, + {"id": 20297, "synset": "gean.n.01", "name": "gean"}, + {"id": 20298, "synset": "capulin.n.01", "name": "capulin"}, + {"id": 20299, "synset": "cherry_laurel.n.02", "name": "cherry_laurel"}, + {"id": 20300, "synset": "cherry_plum.n.01", "name": "cherry_plum"}, + {"id": 20301, "synset": "sour_cherry.n.01", "name": "sour_cherry"}, + {"id": 20302, "synset": "amarelle.n.01", "name": "amarelle"}, + {"id": 20303, "synset": "morello.n.01", "name": "morello"}, + {"id": 20304, "synset": "marasca.n.01", "name": "marasca"}, + {"id": 20305, "synset": "almond_tree.n.01", "name": "almond_tree"}, + {"id": 20306, "synset": "almond.n.01", "name": "almond"}, + {"id": 20307, "synset": "bitter_almond.n.01", "name": "bitter_almond"}, + {"id": 20308, "synset": "jordan_almond.n.01", "name": "jordan_almond"}, + {"id": 20309, "synset": "dwarf_flowering_almond.n.01", "name": "dwarf_flowering_almond"}, + {"id": 20310, "synset": "holly-leaved_cherry.n.01", "name": "holly-leaved_cherry"}, + {"id": 20311, "synset": "fuji.n.01", "name": "fuji"}, + {"id": 20312, "synset": "flowering_almond.n.02", "name": "flowering_almond"}, + {"id": 20313, "synset": "cherry_laurel.n.01", "name": "cherry_laurel"}, + {"id": 20314, "synset": "catalina_cherry.n.01", "name": "Catalina_cherry"}, + {"id": 20315, "synset": "bird_cherry.n.01", "name": "bird_cherry"}, + {"id": 20316, "synset": "hagberry_tree.n.01", "name": "hagberry_tree"}, + {"id": 20317, "synset": "hagberry.n.01", "name": "hagberry"}, + {"id": 20318, "synset": "pin_cherry.n.01", "name": "pin_cherry"}, + {"id": 20319, "synset": "peach.n.01", "name": "peach"}, + {"id": 20320, "synset": "nectarine.n.01", "name": "nectarine"}, + {"id": 20321, "synset": "sand_cherry.n.01", "name": "sand_cherry"}, + {"id": 20322, "synset": "japanese_plum.n.01", "name": "Japanese_plum"}, + {"id": 20323, "synset": "black_cherry.n.01", "name": "black_cherry"}, + {"id": 20324, "synset": "flowering_cherry.n.01", "name": "flowering_cherry"}, + {"id": 20325, "synset": "oriental_cherry.n.01", "name": "oriental_cherry"}, + {"id": 20326, "synset": "japanese_flowering_cherry.n.01", "name": "Japanese_flowering_cherry"}, + {"id": 20327, "synset": "sierra_plum.n.01", "name": "Sierra_plum"}, + {"id": 20328, "synset": "rosebud_cherry.n.01", "name": "rosebud_cherry"}, + {"id": 20329, "synset": "russian_almond.n.01", "name": "Russian_almond"}, + {"id": 20330, "synset": "flowering_almond.n.01", "name": "flowering_almond"}, + {"id": 20331, "synset": "chokecherry.n.02", "name": "chokecherry"}, + {"id": 20332, "synset": "chokecherry.n.01", "name": "chokecherry"}, + {"id": 20333, "synset": "western_chokecherry.n.01", "name": "western_chokecherry"}, + {"id": 20334, "synset": "pyracantha.n.01", "name": "Pyracantha"}, + {"id": 20335, "synset": "pear.n.02", "name": "pear"}, + {"id": 20336, "synset": "fruit_tree.n.01", "name": "fruit_tree"}, + {"id": 20337, "synset": "bramble_bush.n.01", "name": "bramble_bush"}, + {"id": 20338, "synset": "lawyerbush.n.01", "name": "lawyerbush"}, + {"id": 20339, "synset": "stone_bramble.n.01", "name": "stone_bramble"}, + {"id": 20340, "synset": "sand_blackberry.n.01", "name": "sand_blackberry"}, + {"id": 20341, "synset": "boysenberry.n.01", "name": "boysenberry"}, + {"id": 20342, "synset": "loganberry.n.01", "name": "loganberry"}, + {"id": 20343, "synset": "american_dewberry.n.02", "name": "American_dewberry"}, + {"id": 20344, "synset": "northern_dewberry.n.01", "name": "Northern_dewberry"}, + {"id": 20345, "synset": "southern_dewberry.n.01", "name": "Southern_dewberry"}, + {"id": 20346, "synset": "swamp_dewberry.n.01", "name": "swamp_dewberry"}, + {"id": 20347, "synset": "european_dewberry.n.01", "name": "European_dewberry"}, + {"id": 20348, "synset": "raspberry.n.01", "name": "raspberry"}, + {"id": 20349, "synset": "wild_raspberry.n.01", "name": "wild_raspberry"}, + {"id": 20350, "synset": "american_raspberry.n.01", "name": "American_raspberry"}, + {"id": 20351, "synset": "black_raspberry.n.01", "name": "black_raspberry"}, + {"id": 20352, "synset": "salmonberry.n.03", "name": "salmonberry"}, + {"id": 20353, "synset": "salmonberry.n.02", "name": "salmonberry"}, + {"id": 20354, "synset": "wineberry.n.01", "name": "wineberry"}, + {"id": 20355, "synset": "mountain_ash.n.01", "name": "mountain_ash"}, + {"id": 20356, "synset": "rowan.n.01", "name": "rowan"}, + {"id": 20357, "synset": "rowanberry.n.01", "name": "rowanberry"}, + {"id": 20358, "synset": "american_mountain_ash.n.01", "name": "American_mountain_ash"}, + {"id": 20359, "synset": "western_mountain_ash.n.01", "name": "Western_mountain_ash"}, + {"id": 20360, "synset": "service_tree.n.01", "name": "service_tree"}, + {"id": 20361, "synset": "wild_service_tree.n.01", "name": "wild_service_tree"}, + {"id": 20362, "synset": "spirea.n.02", "name": "spirea"}, + {"id": 20363, "synset": "bridal_wreath.n.02", "name": "bridal_wreath"}, + {"id": 20364, "synset": "madderwort.n.01", "name": "madderwort"}, + {"id": 20365, "synset": "indian_madder.n.01", "name": "Indian_madder"}, + {"id": 20366, "synset": "madder.n.01", "name": "madder"}, + {"id": 20367, "synset": "woodruff.n.02", "name": "woodruff"}, + {"id": 20368, "synset": "dagame.n.01", "name": "dagame"}, + {"id": 20369, "synset": "blolly.n.01", "name": "blolly"}, + {"id": 20370, "synset": "coffee.n.02", "name": "coffee"}, + {"id": 20371, "synset": "arabian_coffee.n.01", "name": "Arabian_coffee"}, + {"id": 20372, "synset": "liberian_coffee.n.01", "name": "Liberian_coffee"}, + {"id": 20373, "synset": "robusta_coffee.n.01", "name": "robusta_coffee"}, + {"id": 20374, "synset": "cinchona.n.02", "name": "cinchona"}, + {"id": 20375, "synset": "cartagena_bark.n.01", "name": "Cartagena_bark"}, + {"id": 20376, "synset": "calisaya.n.01", "name": "calisaya"}, + {"id": 20377, "synset": "cinchona_tree.n.01", "name": "cinchona_tree"}, + {"id": 20378, "synset": "cinchona.n.01", "name": "cinchona"}, + {"id": 20379, "synset": "bedstraw.n.01", "name": "bedstraw"}, + {"id": 20380, "synset": "sweet_woodruff.n.01", "name": "sweet_woodruff"}, + {"id": 20381, "synset": "northern_bedstraw.n.01", "name": "Northern_bedstraw"}, + {"id": 20382, "synset": "yellow_bedstraw.n.01", "name": "yellow_bedstraw"}, + {"id": 20383, "synset": "wild_licorice.n.01", "name": "wild_licorice"}, + {"id": 20384, "synset": "cleavers.n.01", "name": "cleavers"}, + {"id": 20385, "synset": "wild_madder.n.01", "name": "wild_madder"}, + {"id": 20386, "synset": "cape_jasmine.n.01", "name": "cape_jasmine"}, + {"id": 20387, "synset": "genipa.n.01", "name": "genipa"}, + {"id": 20388, "synset": "genipap_fruit.n.01", "name": "genipap_fruit"}, + {"id": 20389, "synset": "hamelia.n.01", "name": "hamelia"}, + {"id": 20390, "synset": "scarlet_bush.n.01", "name": "scarlet_bush"}, + {"id": 20391, "synset": "lemonwood.n.02", "name": "lemonwood"}, + {"id": 20392, "synset": "negro_peach.n.01", "name": "negro_peach"}, + {"id": 20393, "synset": "wild_medlar.n.01", "name": "wild_medlar"}, + {"id": 20394, "synset": "spanish_tamarind.n.01", "name": "Spanish_tamarind"}, + {"id": 20395, "synset": "abelia.n.01", "name": "abelia"}, + {"id": 20396, "synset": "bush_honeysuckle.n.02", "name": "bush_honeysuckle"}, + {"id": 20397, "synset": "american_twinflower.n.01", "name": "American_twinflower"}, + {"id": 20398, "synset": "honeysuckle.n.01", "name": "honeysuckle"}, + {"id": 20399, "synset": "american_fly_honeysuckle.n.01", "name": "American_fly_honeysuckle"}, + {"id": 20400, "synset": "italian_honeysuckle.n.01", "name": "Italian_honeysuckle"}, + {"id": 20401, "synset": "yellow_honeysuckle.n.01", "name": "yellow_honeysuckle"}, + {"id": 20402, "synset": "hairy_honeysuckle.n.01", "name": "hairy_honeysuckle"}, + {"id": 20403, "synset": "japanese_honeysuckle.n.01", "name": "Japanese_honeysuckle"}, + {"id": 20404, "synset": "hall's_honeysuckle.n.01", "name": "Hall's_honeysuckle"}, + {"id": 20405, "synset": "morrow's_honeysuckle.n.01", "name": "Morrow's_honeysuckle"}, + {"id": 20406, "synset": "woodbine.n.02", "name": "woodbine"}, + {"id": 20407, "synset": "trumpet_honeysuckle.n.01", "name": "trumpet_honeysuckle"}, + {"id": 20408, "synset": "european_fly_honeysuckle.n.01", "name": "European_fly_honeysuckle"}, + {"id": 20409, "synset": "swamp_fly_honeysuckle.n.01", "name": "swamp_fly_honeysuckle"}, + {"id": 20410, "synset": "snowberry.n.01", "name": "snowberry"}, + {"id": 20411, "synset": "coralberry.n.01", "name": "coralberry"}, + {"id": 20412, "synset": "blue_elder.n.01", "name": "blue_elder"}, + {"id": 20413, "synset": "dwarf_elder.n.01", "name": "dwarf_elder"}, + {"id": 20414, "synset": "american_red_elder.n.01", "name": "American_red_elder"}, + {"id": 20415, "synset": "european_red_elder.n.01", "name": "European_red_elder"}, + {"id": 20416, "synset": "feverroot.n.01", "name": "feverroot"}, + {"id": 20417, "synset": "cranberry_bush.n.01", "name": "cranberry_bush"}, + {"id": 20418, "synset": "wayfaring_tree.n.01", "name": "wayfaring_tree"}, + {"id": 20419, "synset": "guelder_rose.n.01", "name": "guelder_rose"}, + {"id": 20420, "synset": "arrow_wood.n.01", "name": "arrow_wood"}, + {"id": 20421, "synset": "black_haw.n.02", "name": "black_haw"}, + {"id": 20422, "synset": "weigela.n.01", "name": "weigela"}, + {"id": 20423, "synset": "teasel.n.01", "name": "teasel"}, + {"id": 20424, "synset": "common_teasel.n.01", "name": "common_teasel"}, + {"id": 20425, "synset": "fuller's_teasel.n.01", "name": "fuller's_teasel"}, + {"id": 20426, "synset": "wild_teasel.n.01", "name": "wild_teasel"}, + {"id": 20427, "synset": "scabious.n.01", "name": "scabious"}, + {"id": 20428, "synset": "sweet_scabious.n.01", "name": "sweet_scabious"}, + {"id": 20429, "synset": "field_scabious.n.01", "name": "field_scabious"}, + {"id": 20430, "synset": "jewelweed.n.01", "name": "jewelweed"}, + {"id": 20431, "synset": "geranium.n.01", "name": "geranium"}, + {"id": 20432, "synset": "cranesbill.n.01", "name": "cranesbill"}, + {"id": 20433, "synset": "wild_geranium.n.01", "name": "wild_geranium"}, + {"id": 20434, "synset": "meadow_cranesbill.n.01", "name": "meadow_cranesbill"}, + {"id": 20435, "synset": "richardson's_geranium.n.01", "name": "Richardson's_geranium"}, + {"id": 20436, "synset": "herb_robert.n.01", "name": "herb_robert"}, + {"id": 20437, "synset": "sticky_geranium.n.01", "name": "sticky_geranium"}, + {"id": 20438, "synset": "dove's_foot_geranium.n.01", "name": "dove's_foot_geranium"}, + {"id": 20439, "synset": "rose_geranium.n.01", "name": "rose_geranium"}, + {"id": 20440, "synset": "fish_geranium.n.01", "name": "fish_geranium"}, + {"id": 20441, "synset": "ivy_geranium.n.01", "name": "ivy_geranium"}, + {"id": 20442, "synset": "apple_geranium.n.01", "name": "apple_geranium"}, + {"id": 20443, "synset": "lemon_geranium.n.01", "name": "lemon_geranium"}, + {"id": 20444, "synset": "storksbill.n.01", "name": "storksbill"}, + {"id": 20445, "synset": "musk_clover.n.01", "name": "musk_clover"}, + {"id": 20446, "synset": "incense_tree.n.01", "name": "incense_tree"}, + {"id": 20447, "synset": "elephant_tree.n.01", "name": "elephant_tree"}, + {"id": 20448, "synset": "gumbo-limbo.n.01", "name": "gumbo-limbo"}, + {"id": 20449, "synset": "boswellia_carteri.n.01", "name": "Boswellia_carteri"}, + {"id": 20450, "synset": "salai.n.01", "name": "salai"}, + {"id": 20451, "synset": "balm_of_gilead.n.03", "name": "balm_of_gilead"}, + {"id": 20452, "synset": "myrrh_tree.n.01", "name": "myrrh_tree"}, + {"id": 20453, "synset": "protium_heptaphyllum.n.01", "name": "Protium_heptaphyllum"}, + {"id": 20454, "synset": "protium_guianense.n.01", "name": "Protium_guianense"}, + {"id": 20455, "synset": "water_starwort.n.01", "name": "water_starwort"}, + {"id": 20456, "synset": "barbados_cherry.n.01", "name": "barbados_cherry"}, + {"id": 20457, "synset": "mahogany.n.02", "name": "mahogany"}, + {"id": 20458, "synset": "chinaberry.n.02", "name": "chinaberry"}, + {"id": 20459, "synset": "neem.n.01", "name": "neem"}, + {"id": 20460, "synset": "neem_seed.n.01", "name": "neem_seed"}, + {"id": 20461, "synset": "spanish_cedar.n.01", "name": "Spanish_cedar"}, + {"id": 20462, "synset": "satinwood.n.03", "name": "satinwood"}, + {"id": 20463, "synset": "african_scented_mahogany.n.01", "name": "African_scented_mahogany"}, + {"id": 20464, "synset": "silver_ash.n.01", "name": "silver_ash"}, + {"id": 20465, "synset": "native_beech.n.01", "name": "native_beech"}, + {"id": 20466, "synset": "bunji-bunji.n.01", "name": "bunji-bunji"}, + {"id": 20467, "synset": "african_mahogany.n.01", "name": "African_mahogany"}, + {"id": 20468, "synset": "lanseh_tree.n.01", "name": "lanseh_tree"}, + {"id": 20469, "synset": "true_mahogany.n.01", "name": "true_mahogany"}, + {"id": 20470, "synset": "honduras_mahogany.n.01", "name": "Honduras_mahogany"}, + {"id": 20471, "synset": "philippine_mahogany.n.02", "name": "Philippine_mahogany"}, + {"id": 20472, "synset": "caracolito.n.01", "name": "caracolito"}, + {"id": 20473, "synset": "common_wood_sorrel.n.01", "name": "common_wood_sorrel"}, + {"id": 20474, "synset": "bermuda_buttercup.n.01", "name": "Bermuda_buttercup"}, + {"id": 20475, "synset": "creeping_oxalis.n.01", "name": "creeping_oxalis"}, + {"id": 20476, "synset": "goatsfoot.n.01", "name": "goatsfoot"}, + {"id": 20477, "synset": "violet_wood_sorrel.n.01", "name": "violet_wood_sorrel"}, + {"id": 20478, "synset": "oca.n.01", "name": "oca"}, + {"id": 20479, "synset": "carambola.n.01", "name": "carambola"}, + {"id": 20480, "synset": "bilimbi.n.01", "name": "bilimbi"}, + {"id": 20481, "synset": "milkwort.n.01", "name": "milkwort"}, + {"id": 20482, "synset": "senega.n.02", "name": "senega"}, + {"id": 20483, "synset": "orange_milkwort.n.01", "name": "orange_milkwort"}, + {"id": 20484, "synset": "flowering_wintergreen.n.01", "name": "flowering_wintergreen"}, + {"id": 20485, "synset": "seneca_snakeroot.n.01", "name": "Seneca_snakeroot"}, + {"id": 20486, "synset": "common_milkwort.n.01", "name": "common_milkwort"}, + {"id": 20487, "synset": "rue.n.01", "name": "rue"}, + {"id": 20488, "synset": "citrus.n.02", "name": "citrus"}, + {"id": 20489, "synset": "orange.n.03", "name": "orange"}, + {"id": 20490, "synset": "sour_orange.n.01", "name": "sour_orange"}, + {"id": 20491, "synset": "bergamot.n.01", "name": "bergamot"}, + {"id": 20492, "synset": "pomelo.n.01", "name": "pomelo"}, + {"id": 20493, "synset": "citron.n.02", "name": "citron"}, + {"id": 20494, "synset": "grapefruit.n.01", "name": "grapefruit"}, + {"id": 20495, "synset": "mandarin.n.01", "name": "mandarin"}, + {"id": 20496, "synset": "tangerine.n.01", "name": "tangerine"}, + {"id": 20497, "synset": "satsuma.n.01", "name": "satsuma"}, + {"id": 20498, "synset": "sweet_orange.n.02", "name": "sweet_orange"}, + {"id": 20499, "synset": "temple_orange.n.01", "name": "temple_orange"}, + {"id": 20500, "synset": "tangelo.n.01", "name": "tangelo"}, + {"id": 20501, "synset": "rangpur.n.01", "name": "rangpur"}, + {"id": 20502, "synset": "lemon.n.03", "name": "lemon"}, + {"id": 20503, "synset": "sweet_lemon.n.01", "name": "sweet_lemon"}, + {"id": 20504, "synset": "lime.n.04", "name": "lime"}, + {"id": 20505, "synset": "citrange.n.01", "name": "citrange"}, + {"id": 20506, "synset": "fraxinella.n.01", "name": "fraxinella"}, + {"id": 20507, "synset": "kumquat.n.01", "name": "kumquat"}, + {"id": 20508, "synset": "marumi.n.01", "name": "marumi"}, + {"id": 20509, "synset": "nagami.n.01", "name": "nagami"}, + {"id": 20510, "synset": "cork_tree.n.01", "name": "cork_tree"}, + {"id": 20511, "synset": "trifoliate_orange.n.01", "name": "trifoliate_orange"}, + {"id": 20512, "synset": "prickly_ash.n.01", "name": "prickly_ash"}, + {"id": 20513, "synset": "toothache_tree.n.01", "name": "toothache_tree"}, + {"id": 20514, "synset": "hercules'-club.n.01", "name": "Hercules'-club"}, + {"id": 20515, "synset": "bitterwood_tree.n.01", "name": "bitterwood_tree"}, + {"id": 20516, "synset": "marupa.n.01", "name": "marupa"}, + {"id": 20517, "synset": "paradise_tree.n.01", "name": "paradise_tree"}, + {"id": 20518, "synset": "ailanthus.n.01", "name": "ailanthus"}, + {"id": 20519, "synset": "tree_of_heaven.n.01", "name": "tree_of_heaven"}, + {"id": 20520, "synset": "wild_mango.n.01", "name": "wild_mango"}, + {"id": 20521, "synset": "pepper_tree.n.02", "name": "pepper_tree"}, + {"id": 20522, "synset": "jamaica_quassia.n.02", "name": "Jamaica_quassia"}, + {"id": 20523, "synset": "quassia.n.02", "name": "quassia"}, + {"id": 20524, "synset": "nasturtium.n.01", "name": "nasturtium"}, + {"id": 20525, "synset": "garden_nasturtium.n.01", "name": "garden_nasturtium"}, + {"id": 20526, "synset": "bush_nasturtium.n.01", "name": "bush_nasturtium"}, + {"id": 20527, "synset": "canarybird_flower.n.01", "name": "canarybird_flower"}, + {"id": 20528, "synset": "bean_caper.n.01", "name": "bean_caper"}, + {"id": 20529, "synset": "palo_santo.n.01", "name": "palo_santo"}, + {"id": 20530, "synset": "lignum_vitae.n.02", "name": "lignum_vitae"}, + {"id": 20531, "synset": "creosote_bush.n.01", "name": "creosote_bush"}, + {"id": 20532, "synset": "caltrop.n.01", "name": "caltrop"}, + {"id": 20533, "synset": "willow.n.01", "name": "willow"}, + {"id": 20534, "synset": "osier.n.02", "name": "osier"}, + {"id": 20535, "synset": "white_willow.n.01", "name": "white_willow"}, + {"id": 20536, "synset": "silver_willow.n.01", "name": "silver_willow"}, + {"id": 20537, "synset": "golden_willow.n.01", "name": "golden_willow"}, + {"id": 20538, "synset": "cricket-bat_willow.n.01", "name": "cricket-bat_willow"}, + {"id": 20539, "synset": "arctic_willow.n.01", "name": "arctic_willow"}, + {"id": 20540, "synset": "weeping_willow.n.01", "name": "weeping_willow"}, + {"id": 20541, "synset": "wisconsin_weeping_willow.n.01", "name": "Wisconsin_weeping_willow"}, + {"id": 20542, "synset": "pussy_willow.n.01", "name": "pussy_willow"}, + {"id": 20543, "synset": "sallow.n.01", "name": "sallow"}, + {"id": 20544, "synset": "goat_willow.n.01", "name": "goat_willow"}, + {"id": 20545, "synset": "peachleaf_willow.n.01", "name": "peachleaf_willow"}, + {"id": 20546, "synset": "almond_willow.n.01", "name": "almond_willow"}, + {"id": 20547, "synset": "hoary_willow.n.01", "name": "hoary_willow"}, + {"id": 20548, "synset": "crack_willow.n.01", "name": "crack_willow"}, + {"id": 20549, "synset": "prairie_willow.n.01", "name": "prairie_willow"}, + {"id": 20550, "synset": "dwarf_willow.n.01", "name": "dwarf_willow"}, + {"id": 20551, "synset": "grey_willow.n.01", "name": "grey_willow"}, + {"id": 20552, "synset": "arroyo_willow.n.01", "name": "arroyo_willow"}, + {"id": 20553, "synset": "shining_willow.n.01", "name": "shining_willow"}, + {"id": 20554, "synset": "swamp_willow.n.01", "name": "swamp_willow"}, + {"id": 20555, "synset": "bay_willow.n.01", "name": "bay_willow"}, + {"id": 20556, "synset": "purple_willow.n.01", "name": "purple_willow"}, + {"id": 20557, "synset": "balsam_willow.n.01", "name": "balsam_willow"}, + {"id": 20558, "synset": "creeping_willow.n.01", "name": "creeping_willow"}, + {"id": 20559, "synset": "sitka_willow.n.01", "name": "Sitka_willow"}, + {"id": 20560, "synset": "dwarf_grey_willow.n.01", "name": "dwarf_grey_willow"}, + {"id": 20561, "synset": "bearberry_willow.n.01", "name": "bearberry_willow"}, + {"id": 20562, "synset": "common_osier.n.01", "name": "common_osier"}, + {"id": 20563, "synset": "poplar.n.02", "name": "poplar"}, + {"id": 20564, "synset": "balsam_poplar.n.01", "name": "balsam_poplar"}, + {"id": 20565, "synset": "white_poplar.n.01", "name": "white_poplar"}, + {"id": 20566, "synset": "grey_poplar.n.01", "name": "grey_poplar"}, + {"id": 20567, "synset": "black_poplar.n.01", "name": "black_poplar"}, + {"id": 20568, "synset": "lombardy_poplar.n.01", "name": "Lombardy_poplar"}, + {"id": 20569, "synset": "cottonwood.n.01", "name": "cottonwood"}, + {"id": 20570, "synset": "eastern_cottonwood.n.01", "name": "Eastern_cottonwood"}, + {"id": 20571, "synset": "black_cottonwood.n.02", "name": "black_cottonwood"}, + {"id": 20572, "synset": "swamp_cottonwood.n.01", "name": "swamp_cottonwood"}, + {"id": 20573, "synset": "aspen.n.01", "name": "aspen"}, + {"id": 20574, "synset": "quaking_aspen.n.01", "name": "quaking_aspen"}, + {"id": 20575, "synset": "american_quaking_aspen.n.01", "name": "American_quaking_aspen"}, + {"id": 20576, "synset": "canadian_aspen.n.01", "name": "Canadian_aspen"}, + {"id": 20577, "synset": "sandalwood_tree.n.01", "name": "sandalwood_tree"}, + {"id": 20578, "synset": "quandong.n.01", "name": "quandong"}, + {"id": 20579, "synset": "rabbitwood.n.01", "name": "rabbitwood"}, + {"id": 20580, "synset": "loranthaceae.n.01", "name": "Loranthaceae"}, + {"id": 20581, "synset": "mistletoe.n.03", "name": "mistletoe"}, + {"id": 20582, "synset": "american_mistletoe.n.02", "name": "American_mistletoe"}, + {"id": 20583, "synset": "mistletoe.n.02", "name": "mistletoe"}, + {"id": 20584, "synset": "american_mistletoe.n.01", "name": "American_mistletoe"}, + {"id": 20585, "synset": "aalii.n.01", "name": "aalii"}, + {"id": 20586, "synset": "soapberry.n.01", "name": "soapberry"}, + {"id": 20587, "synset": "wild_china_tree.n.01", "name": "wild_China_tree"}, + {"id": 20588, "synset": "china_tree.n.01", "name": "China_tree"}, + {"id": 20589, "synset": "akee.n.01", "name": "akee"}, + {"id": 20590, "synset": "soapberry_vine.n.01", "name": "soapberry_vine"}, + {"id": 20591, "synset": "heartseed.n.01", "name": "heartseed"}, + {"id": 20592, "synset": "balloon_vine.n.01", "name": "balloon_vine"}, + {"id": 20593, "synset": "longan.n.01", "name": "longan"}, + {"id": 20594, "synset": "harpullia.n.01", "name": "harpullia"}, + {"id": 20595, "synset": "harpulla.n.01", "name": "harpulla"}, + {"id": 20596, "synset": "moreton_bay_tulipwood.n.01", "name": "Moreton_Bay_tulipwood"}, + {"id": 20597, "synset": "litchi.n.01", "name": "litchi"}, + {"id": 20598, "synset": "spanish_lime.n.01", "name": "Spanish_lime"}, + {"id": 20599, "synset": "rambutan.n.01", "name": "rambutan"}, + {"id": 20600, "synset": "pulasan.n.01", "name": "pulasan"}, + {"id": 20601, "synset": "pachysandra.n.01", "name": "pachysandra"}, + {"id": 20602, "synset": "allegheny_spurge.n.01", "name": "Allegheny_spurge"}, + {"id": 20603, "synset": "bittersweet.n.02", "name": "bittersweet"}, + {"id": 20604, "synset": "spindle_tree.n.01", "name": "spindle_tree"}, + {"id": 20605, "synset": "winged_spindle_tree.n.01", "name": "winged_spindle_tree"}, + {"id": 20606, "synset": "wahoo.n.02", "name": "wahoo"}, + {"id": 20607, "synset": "strawberry_bush.n.01", "name": "strawberry_bush"}, + {"id": 20608, "synset": "evergreen_bittersweet.n.01", "name": "evergreen_bittersweet"}, + {"id": 20609, "synset": "cyrilla.n.01", "name": "cyrilla"}, + {"id": 20610, "synset": "titi.n.01", "name": "titi"}, + {"id": 20611, "synset": "crowberry.n.01", "name": "crowberry"}, + {"id": 20612, "synset": "maple.n.02", "name": "maple"}, + {"id": 20613, "synset": "silver_maple.n.01", "name": "silver_maple"}, + {"id": 20614, "synset": "sugar_maple.n.01", "name": "sugar_maple"}, + {"id": 20615, "synset": "red_maple.n.01", "name": "red_maple"}, + {"id": 20616, "synset": "moosewood.n.01", "name": "moosewood"}, + {"id": 20617, "synset": "oregon_maple.n.01", "name": "Oregon_maple"}, + {"id": 20618, "synset": "dwarf_maple.n.01", "name": "dwarf_maple"}, + {"id": 20619, "synset": "mountain_maple.n.01", "name": "mountain_maple"}, + {"id": 20620, "synset": "vine_maple.n.01", "name": "vine_maple"}, + {"id": 20621, "synset": "hedge_maple.n.01", "name": "hedge_maple"}, + {"id": 20622, "synset": "norway_maple.n.01", "name": "Norway_maple"}, + {"id": 20623, "synset": "sycamore.n.03", "name": "sycamore"}, + {"id": 20624, "synset": "box_elder.n.01", "name": "box_elder"}, + {"id": 20625, "synset": "california_box_elder.n.01", "name": "California_box_elder"}, + {"id": 20626, "synset": "pointed-leaf_maple.n.01", "name": "pointed-leaf_maple"}, + {"id": 20627, "synset": "japanese_maple.n.02", "name": "Japanese_maple"}, + {"id": 20628, "synset": "japanese_maple.n.01", "name": "Japanese_maple"}, + {"id": 20629, "synset": "holly.n.01", "name": "holly"}, + {"id": 20630, "synset": "chinese_holly.n.01", "name": "Chinese_holly"}, + {"id": 20631, "synset": "bearberry.n.02", "name": "bearberry"}, + {"id": 20632, "synset": "inkberry.n.01", "name": "inkberry"}, + {"id": 20633, "synset": "mate.n.07", "name": "mate"}, + {"id": 20634, "synset": "american_holly.n.01", "name": "American_holly"}, + {"id": 20635, "synset": "low_gallberry_holly.n.01", "name": "low_gallberry_holly"}, + {"id": 20636, "synset": "tall_gallberry_holly.n.01", "name": "tall_gallberry_holly"}, + {"id": 20637, "synset": "yaupon_holly.n.01", "name": "yaupon_holly"}, + {"id": 20638, "synset": "deciduous_holly.n.01", "name": "deciduous_holly"}, + {"id": 20639, "synset": "juneberry_holly.n.01", "name": "juneberry_holly"}, + {"id": 20640, "synset": "largeleaf_holly.n.01", "name": "largeleaf_holly"}, + {"id": 20641, "synset": "geogia_holly.n.01", "name": "Geogia_holly"}, + {"id": 20642, "synset": "common_winterberry_holly.n.01", "name": "common_winterberry_holly"}, + {"id": 20643, "synset": "smooth_winterberry_holly.n.01", "name": "smooth_winterberry_holly"}, + {"id": 20644, "synset": "cashew.n.01", "name": "cashew"}, + {"id": 20645, "synset": "goncalo_alves.n.01", "name": "goncalo_alves"}, + {"id": 20646, "synset": "venetian_sumac.n.01", "name": "Venetian_sumac"}, + {"id": 20647, "synset": "laurel_sumac.n.01", "name": "laurel_sumac"}, + {"id": 20648, "synset": "mango.n.01", "name": "mango"}, + {"id": 20649, "synset": "pistachio.n.01", "name": "pistachio"}, + {"id": 20650, "synset": "terebinth.n.01", "name": "terebinth"}, + {"id": 20651, "synset": "mastic.n.03", "name": "mastic"}, + {"id": 20652, "synset": "australian_sumac.n.01", "name": "Australian_sumac"}, + {"id": 20653, "synset": "sumac.n.02", "name": "sumac"}, + {"id": 20654, "synset": "smooth_sumac.n.01", "name": "smooth_sumac"}, + {"id": 20655, "synset": "sugar-bush.n.01", "name": "sugar-bush"}, + {"id": 20656, "synset": "staghorn_sumac.n.01", "name": "staghorn_sumac"}, + {"id": 20657, "synset": "squawbush.n.01", "name": "squawbush"}, + {"id": 20658, "synset": "aroeira_blanca.n.01", "name": "aroeira_blanca"}, + {"id": 20659, "synset": "pepper_tree.n.01", "name": "pepper_tree"}, + {"id": 20660, "synset": "brazilian_pepper_tree.n.01", "name": "Brazilian_pepper_tree"}, + {"id": 20661, "synset": "hog_plum.n.01", "name": "hog_plum"}, + {"id": 20662, "synset": "mombin.n.01", "name": "mombin"}, + {"id": 20663, "synset": "poison_ash.n.01", "name": "poison_ash"}, + {"id": 20664, "synset": "poison_ivy.n.02", "name": "poison_ivy"}, + {"id": 20665, "synset": "western_poison_oak.n.01", "name": "western_poison_oak"}, + {"id": 20666, "synset": "eastern_poison_oak.n.01", "name": "eastern_poison_oak"}, + {"id": 20667, "synset": "varnish_tree.n.02", "name": "varnish_tree"}, + {"id": 20668, "synset": "horse_chestnut.n.01", "name": "horse_chestnut"}, + {"id": 20669, "synset": "buckeye.n.01", "name": "buckeye"}, + {"id": 20670, "synset": "sweet_buckeye.n.01", "name": "sweet_buckeye"}, + {"id": 20671, "synset": "ohio_buckeye.n.01", "name": "Ohio_buckeye"}, + {"id": 20672, "synset": "dwarf_buckeye.n.01", "name": "dwarf_buckeye"}, + {"id": 20673, "synset": "red_buckeye.n.01", "name": "red_buckeye"}, + {"id": 20674, "synset": "particolored_buckeye.n.01", "name": "particolored_buckeye"}, + {"id": 20675, "synset": "ebony.n.03", "name": "ebony"}, + {"id": 20676, "synset": "marblewood.n.02", "name": "marblewood"}, + {"id": 20677, "synset": "marblewood.n.01", "name": "marblewood"}, + {"id": 20678, "synset": "persimmon.n.01", "name": "persimmon"}, + {"id": 20679, "synset": "japanese_persimmon.n.01", "name": "Japanese_persimmon"}, + {"id": 20680, "synset": "american_persimmon.n.01", "name": "American_persimmon"}, + {"id": 20681, "synset": "date_plum.n.01", "name": "date_plum"}, + {"id": 20682, "synset": "buckthorn.n.02", "name": "buckthorn"}, + {"id": 20683, "synset": "southern_buckthorn.n.01", "name": "southern_buckthorn"}, + {"id": 20684, "synset": "false_buckthorn.n.01", "name": "false_buckthorn"}, + {"id": 20685, "synset": "star_apple.n.01", "name": "star_apple"}, + {"id": 20686, "synset": "satinleaf.n.01", "name": "satinleaf"}, + {"id": 20687, "synset": "balata.n.02", "name": "balata"}, + {"id": 20688, "synset": "sapodilla.n.01", "name": "sapodilla"}, + {"id": 20689, "synset": "gutta-percha_tree.n.02", "name": "gutta-percha_tree"}, + {"id": 20690, "synset": "gutta-percha_tree.n.01", "name": "gutta-percha_tree"}, + {"id": 20691, "synset": "canistel.n.01", "name": "canistel"}, + {"id": 20692, "synset": "marmalade_tree.n.01", "name": "marmalade_tree"}, + {"id": 20693, "synset": "sweetleaf.n.01", "name": "sweetleaf"}, + {"id": 20694, "synset": "asiatic_sweetleaf.n.01", "name": "Asiatic_sweetleaf"}, + {"id": 20695, "synset": "styrax.n.01", "name": "styrax"}, + {"id": 20696, "synset": "snowbell.n.01", "name": "snowbell"}, + {"id": 20697, "synset": "japanese_snowbell.n.01", "name": "Japanese_snowbell"}, + {"id": 20698, "synset": "texas_snowbell.n.01", "name": "Texas_snowbell"}, + {"id": 20699, "synset": "silver-bell_tree.n.01", "name": "silver-bell_tree"}, + {"id": 20700, "synset": "carnivorous_plant.n.01", "name": "carnivorous_plant"}, + {"id": 20701, "synset": "pitcher_plant.n.01", "name": "pitcher_plant"}, + {"id": 20702, "synset": "common_pitcher_plant.n.01", "name": "common_pitcher_plant"}, + {"id": 20703, "synset": "hooded_pitcher_plant.n.01", "name": "hooded_pitcher_plant"}, + {"id": 20704, "synset": "huntsman's_horn.n.01", "name": "huntsman's_horn"}, + {"id": 20705, "synset": "tropical_pitcher_plant.n.01", "name": "tropical_pitcher_plant"}, + {"id": 20706, "synset": "sundew.n.01", "name": "sundew"}, + {"id": 20707, "synset": "venus's_flytrap.n.01", "name": "Venus's_flytrap"}, + {"id": 20708, "synset": "waterwheel_plant.n.01", "name": "waterwheel_plant"}, + {"id": 20709, "synset": "drosophyllum_lusitanicum.n.01", "name": "Drosophyllum_lusitanicum"}, + {"id": 20710, "synset": "roridula.n.01", "name": "roridula"}, + {"id": 20711, "synset": "australian_pitcher_plant.n.01", "name": "Australian_pitcher_plant"}, + {"id": 20712, "synset": "sedum.n.01", "name": "sedum"}, + {"id": 20713, "synset": "stonecrop.n.01", "name": "stonecrop"}, + {"id": 20714, "synset": "rose-root.n.01", "name": "rose-root"}, + {"id": 20715, "synset": "orpine.n.01", "name": "orpine"}, + {"id": 20716, "synset": "pinwheel.n.01", "name": "pinwheel"}, + {"id": 20717, "synset": "christmas_bush.n.01", "name": "Christmas_bush"}, + {"id": 20718, "synset": "hortensia.n.01", "name": "hortensia"}, + {"id": 20719, "synset": "fall-blooming_hydrangea.n.01", "name": "fall-blooming_hydrangea"}, + {"id": 20720, "synset": "carpenteria.n.01", "name": "carpenteria"}, + {"id": 20721, "synset": "decumary.n.01", "name": "decumary"}, + {"id": 20722, "synset": "deutzia.n.01", "name": "deutzia"}, + {"id": 20723, "synset": "philadelphus.n.01", "name": "philadelphus"}, + {"id": 20724, "synset": "mock_orange.n.01", "name": "mock_orange"}, + {"id": 20725, "synset": "saxifrage.n.01", "name": "saxifrage"}, + {"id": 20726, "synset": "yellow_mountain_saxifrage.n.01", "name": "yellow_mountain_saxifrage"}, + {"id": 20727, "synset": "meadow_saxifrage.n.01", "name": "meadow_saxifrage"}, + {"id": 20728, "synset": "mossy_saxifrage.n.01", "name": "mossy_saxifrage"}, + {"id": 20729, "synset": "western_saxifrage.n.01", "name": "western_saxifrage"}, + {"id": 20730, "synset": "purple_saxifrage.n.01", "name": "purple_saxifrage"}, + {"id": 20731, "synset": "star_saxifrage.n.01", "name": "star_saxifrage"}, + {"id": 20732, "synset": "strawberry_geranium.n.01", "name": "strawberry_geranium"}, + {"id": 20733, "synset": "astilbe.n.01", "name": "astilbe"}, + {"id": 20734, "synset": "false_goatsbeard.n.01", "name": "false_goatsbeard"}, + {"id": 20735, "synset": "dwarf_astilbe.n.01", "name": "dwarf_astilbe"}, + {"id": 20736, "synset": "spirea.n.01", "name": "spirea"}, + {"id": 20737, "synset": "bergenia.n.01", "name": "bergenia"}, + {"id": 20738, "synset": "coast_boykinia.n.01", "name": "coast_boykinia"}, + {"id": 20739, "synset": "golden_saxifrage.n.01", "name": "golden_saxifrage"}, + {"id": 20740, "synset": "umbrella_plant.n.01", "name": "umbrella_plant"}, + {"id": 20741, "synset": "bridal_wreath.n.01", "name": "bridal_wreath"}, + {"id": 20742, "synset": "alumroot.n.01", "name": "alumroot"}, + {"id": 20743, "synset": "coralbells.n.01", "name": "coralbells"}, + {"id": 20744, "synset": "leatherleaf_saxifrage.n.01", "name": "leatherleaf_saxifrage"}, + {"id": 20745, "synset": "woodland_star.n.01", "name": "woodland_star"}, + {"id": 20746, "synset": "prairie_star.n.01", "name": "prairie_star"}, + {"id": 20747, "synset": "miterwort.n.01", "name": "miterwort"}, + {"id": 20748, "synset": "five-point_bishop's_cap.n.01", "name": "five-point_bishop's_cap"}, + {"id": 20749, "synset": "parnassia.n.01", "name": "parnassia"}, + {"id": 20750, "synset": "bog_star.n.01", "name": "bog_star"}, + { + "id": 20751, + "synset": "fringed_grass_of_parnassus.n.01", + "name": "fringed_grass_of_Parnassus", + }, + {"id": 20752, "synset": "false_alumroot.n.01", "name": "false_alumroot"}, + {"id": 20753, "synset": "foamflower.n.01", "name": "foamflower"}, + {"id": 20754, "synset": "false_miterwort.n.01", "name": "false_miterwort"}, + {"id": 20755, "synset": "pickaback_plant.n.01", "name": "pickaback_plant"}, + {"id": 20756, "synset": "currant.n.02", "name": "currant"}, + {"id": 20757, "synset": "black_currant.n.01", "name": "black_currant"}, + {"id": 20758, "synset": "white_currant.n.01", "name": "white_currant"}, + {"id": 20759, "synset": "gooseberry.n.01", "name": "gooseberry"}, + {"id": 20760, "synset": "plane_tree.n.01", "name": "plane_tree"}, + {"id": 20761, "synset": "london_plane.n.01", "name": "London_plane"}, + {"id": 20762, "synset": "american_sycamore.n.01", "name": "American_sycamore"}, + {"id": 20763, "synset": "oriental_plane.n.01", "name": "oriental_plane"}, + {"id": 20764, "synset": "california_sycamore.n.01", "name": "California_sycamore"}, + {"id": 20765, "synset": "arizona_sycamore.n.01", "name": "Arizona_sycamore"}, + {"id": 20766, "synset": "greek_valerian.n.01", "name": "Greek_valerian"}, + {"id": 20767, "synset": "northern_jacob's_ladder.n.01", "name": "northern_Jacob's_ladder"}, + {"id": 20768, "synset": "skunkweed.n.01", "name": "skunkweed"}, + {"id": 20769, "synset": "phlox.n.01", "name": "phlox"}, + {"id": 20770, "synset": "moss_pink.n.02", "name": "moss_pink"}, + {"id": 20771, "synset": "evening-snow.n.01", "name": "evening-snow"}, + {"id": 20772, "synset": "acanthus.n.01", "name": "acanthus"}, + {"id": 20773, "synset": "bear's_breech.n.01", "name": "bear's_breech"}, + {"id": 20774, "synset": "caricature_plant.n.01", "name": "caricature_plant"}, + {"id": 20775, "synset": "black-eyed_susan.n.01", "name": "black-eyed_Susan"}, + {"id": 20776, "synset": "catalpa.n.01", "name": "catalpa"}, + {"id": 20777, "synset": "catalpa_bignioides.n.01", "name": "Catalpa_bignioides"}, + {"id": 20778, "synset": "catalpa_speciosa.n.01", "name": "Catalpa_speciosa"}, + {"id": 20779, "synset": "desert_willow.n.01", "name": "desert_willow"}, + {"id": 20780, "synset": "calabash.n.02", "name": "calabash"}, + {"id": 20781, "synset": "calabash.n.01", "name": "calabash"}, + {"id": 20782, "synset": "borage.n.01", "name": "borage"}, + {"id": 20783, "synset": "common_amsinckia.n.01", "name": "common_amsinckia"}, + {"id": 20784, "synset": "anchusa.n.01", "name": "anchusa"}, + {"id": 20785, "synset": "bugloss.n.01", "name": "bugloss"}, + {"id": 20786, "synset": "cape_forget-me-not.n.02", "name": "cape_forget-me-not"}, + {"id": 20787, "synset": "cape_forget-me-not.n.01", "name": "cape_forget-me-not"}, + {"id": 20788, "synset": "spanish_elm.n.02", "name": "Spanish_elm"}, + {"id": 20789, "synset": "princewood.n.01", "name": "princewood"}, + {"id": 20790, "synset": "chinese_forget-me-not.n.01", "name": "Chinese_forget-me-not"}, + {"id": 20791, "synset": "hound's-tongue.n.02", "name": "hound's-tongue"}, + {"id": 20792, "synset": "hound's-tongue.n.01", "name": "hound's-tongue"}, + {"id": 20793, "synset": "blueweed.n.01", "name": "blueweed"}, + {"id": 20794, "synset": "beggar's_lice.n.01", "name": "beggar's_lice"}, + {"id": 20795, "synset": "gromwell.n.01", "name": "gromwell"}, + {"id": 20796, "synset": "puccoon.n.01", "name": "puccoon"}, + {"id": 20797, "synset": "virginia_bluebell.n.01", "name": "Virginia_bluebell"}, + {"id": 20798, "synset": "garden_forget-me-not.n.01", "name": "garden_forget-me-not"}, + {"id": 20799, "synset": "forget-me-not.n.01", "name": "forget-me-not"}, + {"id": 20800, "synset": "false_gromwell.n.01", "name": "false_gromwell"}, + {"id": 20801, "synset": "comfrey.n.01", "name": "comfrey"}, + {"id": 20802, "synset": "common_comfrey.n.01", "name": "common_comfrey"}, + {"id": 20803, "synset": "convolvulus.n.01", "name": "convolvulus"}, + {"id": 20804, "synset": "bindweed.n.01", "name": "bindweed"}, + {"id": 20805, "synset": "field_bindweed.n.01", "name": "field_bindweed"}, + {"id": 20806, "synset": "scammony.n.03", "name": "scammony"}, + {"id": 20807, "synset": "silverweed.n.01", "name": "silverweed"}, + {"id": 20808, "synset": "dodder.n.01", "name": "dodder"}, + {"id": 20809, "synset": "dichondra.n.01", "name": "dichondra"}, + {"id": 20810, "synset": "cypress_vine.n.01", "name": "cypress_vine"}, + {"id": 20811, "synset": "moonflower.n.01", "name": "moonflower"}, + {"id": 20812, "synset": "wild_potato_vine.n.01", "name": "wild_potato_vine"}, + {"id": 20813, "synset": "red_morning-glory.n.01", "name": "red_morning-glory"}, + {"id": 20814, "synset": "man-of-the-earth.n.01", "name": "man-of-the-earth"}, + {"id": 20815, "synset": "scammony.n.01", "name": "scammony"}, + {"id": 20816, "synset": "japanese_morning_glory.n.01", "name": "Japanese_morning_glory"}, + { + "id": 20817, + "synset": "imperial_japanese_morning_glory.n.01", + "name": "imperial_Japanese_morning_glory", + }, + {"id": 20818, "synset": "gesneriad.n.01", "name": "gesneriad"}, + {"id": 20819, "synset": "gesneria.n.01", "name": "gesneria"}, + {"id": 20820, "synset": "achimenes.n.01", "name": "achimenes"}, + {"id": 20821, "synset": "aeschynanthus.n.01", "name": "aeschynanthus"}, + {"id": 20822, "synset": "lace-flower_vine.n.01", "name": "lace-flower_vine"}, + {"id": 20823, "synset": "columnea.n.01", "name": "columnea"}, + {"id": 20824, "synset": "episcia.n.01", "name": "episcia"}, + {"id": 20825, "synset": "gloxinia.n.01", "name": "gloxinia"}, + {"id": 20826, "synset": "canterbury_bell.n.01", "name": "Canterbury_bell"}, + {"id": 20827, "synset": "kohleria.n.01", "name": "kohleria"}, + {"id": 20828, "synset": "african_violet.n.01", "name": "African_violet"}, + {"id": 20829, "synset": "streptocarpus.n.01", "name": "streptocarpus"}, + {"id": 20830, "synset": "cape_primrose.n.01", "name": "Cape_primrose"}, + {"id": 20831, "synset": "waterleaf.n.01", "name": "waterleaf"}, + {"id": 20832, "synset": "virginia_waterleaf.n.01", "name": "Virginia_waterleaf"}, + {"id": 20833, "synset": "yellow_bells.n.01", "name": "yellow_bells"}, + {"id": 20834, "synset": "yerba_santa.n.01", "name": "yerba_santa"}, + {"id": 20835, "synset": "nemophila.n.01", "name": "nemophila"}, + {"id": 20836, "synset": "baby_blue-eyes.n.01", "name": "baby_blue-eyes"}, + {"id": 20837, "synset": "five-spot.n.02", "name": "five-spot"}, + {"id": 20838, "synset": "scorpionweed.n.01", "name": "scorpionweed"}, + {"id": 20839, "synset": "california_bluebell.n.02", "name": "California_bluebell"}, + {"id": 20840, "synset": "california_bluebell.n.01", "name": "California_bluebell"}, + {"id": 20841, "synset": "fiddleneck.n.01", "name": "fiddleneck"}, + {"id": 20842, "synset": "fiesta_flower.n.01", "name": "fiesta_flower"}, + {"id": 20843, "synset": "basil_thyme.n.01", "name": "basil_thyme"}, + {"id": 20844, "synset": "giant_hyssop.n.01", "name": "giant_hyssop"}, + {"id": 20845, "synset": "yellow_giant_hyssop.n.01", "name": "yellow_giant_hyssop"}, + {"id": 20846, "synset": "anise_hyssop.n.01", "name": "anise_hyssop"}, + {"id": 20847, "synset": "mexican_hyssop.n.01", "name": "Mexican_hyssop"}, + {"id": 20848, "synset": "bugle.n.02", "name": "bugle"}, + {"id": 20849, "synset": "creeping_bugle.n.01", "name": "creeping_bugle"}, + {"id": 20850, "synset": "erect_bugle.n.01", "name": "erect_bugle"}, + {"id": 20851, "synset": "pyramid_bugle.n.01", "name": "pyramid_bugle"}, + {"id": 20852, "synset": "wood_mint.n.01", "name": "wood_mint"}, + {"id": 20853, "synset": "hairy_wood_mint.n.01", "name": "hairy_wood_mint"}, + {"id": 20854, "synset": "downy_wood_mint.n.01", "name": "downy_wood_mint"}, + {"id": 20855, "synset": "calamint.n.01", "name": "calamint"}, + {"id": 20856, "synset": "common_calamint.n.01", "name": "common_calamint"}, + {"id": 20857, "synset": "large-flowered_calamint.n.01", "name": "large-flowered_calamint"}, + {"id": 20858, "synset": "lesser_calamint.n.01", "name": "lesser_calamint"}, + {"id": 20859, "synset": "wild_basil.n.01", "name": "wild_basil"}, + {"id": 20860, "synset": "horse_balm.n.01", "name": "horse_balm"}, + {"id": 20861, "synset": "coleus.n.01", "name": "coleus"}, + {"id": 20862, "synset": "country_borage.n.01", "name": "country_borage"}, + {"id": 20863, "synset": "painted_nettle.n.01", "name": "painted_nettle"}, + {"id": 20864, "synset": "apalachicola_rosemary.n.01", "name": "Apalachicola_rosemary"}, + {"id": 20865, "synset": "dragonhead.n.01", "name": "dragonhead"}, + {"id": 20866, "synset": "elsholtzia.n.01", "name": "elsholtzia"}, + {"id": 20867, "synset": "hemp_nettle.n.01", "name": "hemp_nettle"}, + {"id": 20868, "synset": "ground_ivy.n.01", "name": "ground_ivy"}, + {"id": 20869, "synset": "pennyroyal.n.02", "name": "pennyroyal"}, + {"id": 20870, "synset": "hyssop.n.01", "name": "hyssop"}, + {"id": 20871, "synset": "dead_nettle.n.02", "name": "dead_nettle"}, + {"id": 20872, "synset": "white_dead_nettle.n.01", "name": "white_dead_nettle"}, + {"id": 20873, "synset": "henbit.n.01", "name": "henbit"}, + {"id": 20874, "synset": "english_lavender.n.01", "name": "English_lavender"}, + {"id": 20875, "synset": "french_lavender.n.02", "name": "French_lavender"}, + {"id": 20876, "synset": "spike_lavender.n.01", "name": "spike_lavender"}, + {"id": 20877, "synset": "dagga.n.01", "name": "dagga"}, + {"id": 20878, "synset": "lion's-ear.n.01", "name": "lion's-ear"}, + {"id": 20879, "synset": "motherwort.n.01", "name": "motherwort"}, + {"id": 20880, "synset": "pitcher_sage.n.02", "name": "pitcher_sage"}, + {"id": 20881, "synset": "bugleweed.n.01", "name": "bugleweed"}, + {"id": 20882, "synset": "water_horehound.n.01", "name": "water_horehound"}, + {"id": 20883, "synset": "gipsywort.n.01", "name": "gipsywort"}, + {"id": 20884, "synset": "origanum.n.01", "name": "origanum"}, + {"id": 20885, "synset": "oregano.n.01", "name": "oregano"}, + {"id": 20886, "synset": "sweet_marjoram.n.01", "name": "sweet_marjoram"}, + {"id": 20887, "synset": "horehound.n.01", "name": "horehound"}, + {"id": 20888, "synset": "common_horehound.n.01", "name": "common_horehound"}, + {"id": 20889, "synset": "lemon_balm.n.01", "name": "lemon_balm"}, + {"id": 20890, "synset": "corn_mint.n.01", "name": "corn_mint"}, + {"id": 20891, "synset": "water-mint.n.01", "name": "water-mint"}, + {"id": 20892, "synset": "bergamot_mint.n.02", "name": "bergamot_mint"}, + {"id": 20893, "synset": "horsemint.n.03", "name": "horsemint"}, + {"id": 20894, "synset": "peppermint.n.01", "name": "peppermint"}, + {"id": 20895, "synset": "spearmint.n.01", "name": "spearmint"}, + {"id": 20896, "synset": "apple_mint.n.01", "name": "apple_mint"}, + {"id": 20897, "synset": "pennyroyal.n.01", "name": "pennyroyal"}, + {"id": 20898, "synset": "yerba_buena.n.01", "name": "yerba_buena"}, + {"id": 20899, "synset": "molucca_balm.n.01", "name": "molucca_balm"}, + {"id": 20900, "synset": "monarda.n.01", "name": "monarda"}, + {"id": 20901, "synset": "bee_balm.n.02", "name": "bee_balm"}, + {"id": 20902, "synset": "horsemint.n.02", "name": "horsemint"}, + {"id": 20903, "synset": "bee_balm.n.01", "name": "bee_balm"}, + {"id": 20904, "synset": "lemon_mint.n.01", "name": "lemon_mint"}, + {"id": 20905, "synset": "plains_lemon_monarda.n.01", "name": "plains_lemon_monarda"}, + {"id": 20906, "synset": "basil_balm.n.01", "name": "basil_balm"}, + {"id": 20907, "synset": "mustang_mint.n.01", "name": "mustang_mint"}, + {"id": 20908, "synset": "catmint.n.01", "name": "catmint"}, + {"id": 20909, "synset": "basil.n.01", "name": "basil"}, + {"id": 20910, "synset": "beefsteak_plant.n.01", "name": "beefsteak_plant"}, + {"id": 20911, "synset": "phlomis.n.01", "name": "phlomis"}, + {"id": 20912, "synset": "jerusalem_sage.n.01", "name": "Jerusalem_sage"}, + {"id": 20913, "synset": "physostegia.n.01", "name": "physostegia"}, + {"id": 20914, "synset": "plectranthus.n.01", "name": "plectranthus"}, + {"id": 20915, "synset": "patchouli.n.01", "name": "patchouli"}, + {"id": 20916, "synset": "self-heal.n.01", "name": "self-heal"}, + {"id": 20917, "synset": "mountain_mint.n.01", "name": "mountain_mint"}, + {"id": 20918, "synset": "rosemary.n.01", "name": "rosemary"}, + {"id": 20919, "synset": "clary_sage.n.01", "name": "clary_sage"}, + {"id": 20920, "synset": "purple_sage.n.01", "name": "purple_sage"}, + {"id": 20921, "synset": "cancerweed.n.01", "name": "cancerweed"}, + {"id": 20922, "synset": "common_sage.n.01", "name": "common_sage"}, + {"id": 20923, "synset": "meadow_clary.n.01", "name": "meadow_clary"}, + {"id": 20924, "synset": "clary.n.01", "name": "clary"}, + {"id": 20925, "synset": "pitcher_sage.n.01", "name": "pitcher_sage"}, + {"id": 20926, "synset": "mexican_mint.n.01", "name": "Mexican_mint"}, + {"id": 20927, "synset": "wild_sage.n.01", "name": "wild_sage"}, + {"id": 20928, "synset": "savory.n.01", "name": "savory"}, + {"id": 20929, "synset": "summer_savory.n.01", "name": "summer_savory"}, + {"id": 20930, "synset": "winter_savory.n.01", "name": "winter_savory"}, + {"id": 20931, "synset": "skullcap.n.02", "name": "skullcap"}, + {"id": 20932, "synset": "blue_pimpernel.n.01", "name": "blue_pimpernel"}, + {"id": 20933, "synset": "hedge_nettle.n.02", "name": "hedge_nettle"}, + {"id": 20934, "synset": "hedge_nettle.n.01", "name": "hedge_nettle"}, + {"id": 20935, "synset": "germander.n.01", "name": "germander"}, + {"id": 20936, "synset": "american_germander.n.01", "name": "American_germander"}, + {"id": 20937, "synset": "cat_thyme.n.01", "name": "cat_thyme"}, + {"id": 20938, "synset": "wood_sage.n.01", "name": "wood_sage"}, + {"id": 20939, "synset": "thyme.n.01", "name": "thyme"}, + {"id": 20940, "synset": "common_thyme.n.01", "name": "common_thyme"}, + {"id": 20941, "synset": "wild_thyme.n.01", "name": "wild_thyme"}, + {"id": 20942, "synset": "blue_curls.n.01", "name": "blue_curls"}, + {"id": 20943, "synset": "turpentine_camphor_weed.n.01", "name": "turpentine_camphor_weed"}, + {"id": 20944, "synset": "bastard_pennyroyal.n.01", "name": "bastard_pennyroyal"}, + {"id": 20945, "synset": "bladderwort.n.01", "name": "bladderwort"}, + {"id": 20946, "synset": "butterwort.n.01", "name": "butterwort"}, + {"id": 20947, "synset": "genlisea.n.01", "name": "genlisea"}, + {"id": 20948, "synset": "martynia.n.01", "name": "martynia"}, + {"id": 20949, "synset": "common_unicorn_plant.n.01", "name": "common_unicorn_plant"}, + {"id": 20950, "synset": "sand_devil's_claw.n.01", "name": "sand_devil's_claw"}, + {"id": 20951, "synset": "sweet_unicorn_plant.n.01", "name": "sweet_unicorn_plant"}, + {"id": 20952, "synset": "figwort.n.01", "name": "figwort"}, + {"id": 20953, "synset": "snapdragon.n.01", "name": "snapdragon"}, + {"id": 20954, "synset": "white_snapdragon.n.01", "name": "white_snapdragon"}, + {"id": 20955, "synset": "yellow_twining_snapdragon.n.01", "name": "yellow_twining_snapdragon"}, + {"id": 20956, "synset": "mediterranean_snapdragon.n.01", "name": "Mediterranean_snapdragon"}, + {"id": 20957, "synset": "kitten-tails.n.01", "name": "kitten-tails"}, + {"id": 20958, "synset": "alpine_besseya.n.01", "name": "Alpine_besseya"}, + {"id": 20959, "synset": "false_foxglove.n.02", "name": "false_foxglove"}, + {"id": 20960, "synset": "false_foxglove.n.01", "name": "false_foxglove"}, + {"id": 20961, "synset": "calceolaria.n.01", "name": "calceolaria"}, + {"id": 20962, "synset": "indian_paintbrush.n.02", "name": "Indian_paintbrush"}, + {"id": 20963, "synset": "desert_paintbrush.n.01", "name": "desert_paintbrush"}, + {"id": 20964, "synset": "giant_red_paintbrush.n.01", "name": "giant_red_paintbrush"}, + {"id": 20965, "synset": "great_plains_paintbrush.n.01", "name": "great_plains_paintbrush"}, + {"id": 20966, "synset": "sulfur_paintbrush.n.01", "name": "sulfur_paintbrush"}, + {"id": 20967, "synset": "shellflower.n.01", "name": "shellflower"}, + {"id": 20968, "synset": "maiden_blue-eyed_mary.n.01", "name": "maiden_blue-eyed_Mary"}, + {"id": 20969, "synset": "blue-eyed_mary.n.01", "name": "blue-eyed_Mary"}, + {"id": 20970, "synset": "foxglove.n.01", "name": "foxglove"}, + {"id": 20971, "synset": "common_foxglove.n.01", "name": "common_foxglove"}, + {"id": 20972, "synset": "yellow_foxglove.n.01", "name": "yellow_foxglove"}, + {"id": 20973, "synset": "gerardia.n.01", "name": "gerardia"}, + {"id": 20974, "synset": "blue_toadflax.n.01", "name": "blue_toadflax"}, + {"id": 20975, "synset": "toadflax.n.01", "name": "toadflax"}, + {"id": 20976, "synset": "golden-beard_penstemon.n.01", "name": "golden-beard_penstemon"}, + {"id": 20977, "synset": "scarlet_bugler.n.01", "name": "scarlet_bugler"}, + {"id": 20978, "synset": "red_shrubby_penstemon.n.01", "name": "red_shrubby_penstemon"}, + {"id": 20979, "synset": "platte_river_penstemon.n.01", "name": "Platte_River_penstemon"}, + {"id": 20980, "synset": "hot-rock_penstemon.n.01", "name": "hot-rock_penstemon"}, + {"id": 20981, "synset": "jones'_penstemon.n.01", "name": "Jones'_penstemon"}, + {"id": 20982, "synset": "shrubby_penstemon.n.01", "name": "shrubby_penstemon"}, + {"id": 20983, "synset": "narrow-leaf_penstemon.n.01", "name": "narrow-leaf_penstemon"}, + {"id": 20984, "synset": "balloon_flower.n.01", "name": "balloon_flower"}, + {"id": 20985, "synset": "parry's_penstemon.n.01", "name": "Parry's_penstemon"}, + {"id": 20986, "synset": "rock_penstemon.n.01", "name": "rock_penstemon"}, + {"id": 20987, "synset": "rydberg's_penstemon.n.01", "name": "Rydberg's_penstemon"}, + {"id": 20988, "synset": "cascade_penstemon.n.01", "name": "cascade_penstemon"}, + {"id": 20989, "synset": "whipple's_penstemon.n.01", "name": "Whipple's_penstemon"}, + {"id": 20990, "synset": "moth_mullein.n.01", "name": "moth_mullein"}, + {"id": 20991, "synset": "white_mullein.n.01", "name": "white_mullein"}, + {"id": 20992, "synset": "purple_mullein.n.01", "name": "purple_mullein"}, + {"id": 20993, "synset": "common_mullein.n.01", "name": "common_mullein"}, + {"id": 20994, "synset": "veronica.n.01", "name": "veronica"}, + {"id": 20995, "synset": "field_speedwell.n.01", "name": "field_speedwell"}, + {"id": 20996, "synset": "brooklime.n.02", "name": "brooklime"}, + {"id": 20997, "synset": "corn_speedwell.n.01", "name": "corn_speedwell"}, + {"id": 20998, "synset": "brooklime.n.01", "name": "brooklime"}, + {"id": 20999, "synset": "germander_speedwell.n.01", "name": "germander_speedwell"}, + {"id": 21000, "synset": "water_speedwell.n.01", "name": "water_speedwell"}, + {"id": 21001, "synset": "common_speedwell.n.01", "name": "common_speedwell"}, + {"id": 21002, "synset": "purslane_speedwell.n.01", "name": "purslane_speedwell"}, + {"id": 21003, "synset": "thyme-leaved_speedwell.n.01", "name": "thyme-leaved_speedwell"}, + {"id": 21004, "synset": "nightshade.n.01", "name": "nightshade"}, + {"id": 21005, "synset": "horse_nettle.n.01", "name": "horse_nettle"}, + {"id": 21006, "synset": "african_holly.n.01", "name": "African_holly"}, + {"id": 21007, "synset": "potato_vine.n.02", "name": "potato_vine"}, + {"id": 21008, "synset": "garden_huckleberry.n.01", "name": "garden_huckleberry"}, + {"id": 21009, "synset": "naranjilla.n.01", "name": "naranjilla"}, + {"id": 21010, "synset": "potato_vine.n.01", "name": "potato_vine"}, + {"id": 21011, "synset": "potato_tree.n.01", "name": "potato_tree"}, + {"id": 21012, "synset": "belladonna.n.01", "name": "belladonna"}, + {"id": 21013, "synset": "bush_violet.n.01", "name": "bush_violet"}, + {"id": 21014, "synset": "lady-of-the-night.n.01", "name": "lady-of-the-night"}, + {"id": 21015, "synset": "angel's_trumpet.n.02", "name": "angel's_trumpet"}, + {"id": 21016, "synset": "angel's_trumpet.n.01", "name": "angel's_trumpet"}, + {"id": 21017, "synset": "red_angel's_trumpet.n.01", "name": "red_angel's_trumpet"}, + {"id": 21018, "synset": "cone_pepper.n.01", "name": "cone_pepper"}, + {"id": 21019, "synset": "bird_pepper.n.01", "name": "bird_pepper"}, + {"id": 21020, "synset": "day_jessamine.n.01", "name": "day_jessamine"}, + {"id": 21021, "synset": "night_jasmine.n.01", "name": "night_jasmine"}, + {"id": 21022, "synset": "tree_tomato.n.01", "name": "tree_tomato"}, + {"id": 21023, "synset": "thorn_apple.n.01", "name": "thorn_apple"}, + {"id": 21024, "synset": "jimsonweed.n.01", "name": "jimsonweed"}, + {"id": 21025, "synset": "pichi.n.01", "name": "pichi"}, + {"id": 21026, "synset": "henbane.n.01", "name": "henbane"}, + {"id": 21027, "synset": "egyptian_henbane.n.01", "name": "Egyptian_henbane"}, + {"id": 21028, "synset": "matrimony_vine.n.01", "name": "matrimony_vine"}, + {"id": 21029, "synset": "common_matrimony_vine.n.01", "name": "common_matrimony_vine"}, + {"id": 21030, "synset": "christmasberry.n.01", "name": "Christmasberry"}, + {"id": 21031, "synset": "plum_tomato.n.01", "name": "plum_tomato"}, + {"id": 21032, "synset": "mandrake.n.02", "name": "mandrake"}, + {"id": 21033, "synset": "mandrake_root.n.01", "name": "mandrake_root"}, + {"id": 21034, "synset": "apple_of_peru.n.01", "name": "apple_of_Peru"}, + {"id": 21035, "synset": "flowering_tobacco.n.01", "name": "flowering_tobacco"}, + {"id": 21036, "synset": "common_tobacco.n.01", "name": "common_tobacco"}, + {"id": 21037, "synset": "wild_tobacco.n.01", "name": "wild_tobacco"}, + {"id": 21038, "synset": "cupflower.n.02", "name": "cupflower"}, + {"id": 21039, "synset": "whitecup.n.01", "name": "whitecup"}, + {"id": 21040, "synset": "petunia.n.01", "name": "petunia"}, + {"id": 21041, "synset": "large_white_petunia.n.01", "name": "large_white_petunia"}, + {"id": 21042, "synset": "violet-flowered_petunia.n.01", "name": "violet-flowered_petunia"}, + {"id": 21043, "synset": "hybrid_petunia.n.01", "name": "hybrid_petunia"}, + {"id": 21044, "synset": "cape_gooseberry.n.01", "name": "cape_gooseberry"}, + {"id": 21045, "synset": "strawberry_tomato.n.01", "name": "strawberry_tomato"}, + {"id": 21046, "synset": "tomatillo.n.02", "name": "tomatillo"}, + {"id": 21047, "synset": "tomatillo.n.01", "name": "tomatillo"}, + {"id": 21048, "synset": "yellow_henbane.n.01", "name": "yellow_henbane"}, + {"id": 21049, "synset": "cock's_eggs.n.01", "name": "cock's_eggs"}, + {"id": 21050, "synset": "salpiglossis.n.01", "name": "salpiglossis"}, + {"id": 21051, "synset": "painted_tongue.n.01", "name": "painted_tongue"}, + {"id": 21052, "synset": "butterfly_flower.n.01", "name": "butterfly_flower"}, + {"id": 21053, "synset": "scopolia_carniolica.n.01", "name": "Scopolia_carniolica"}, + {"id": 21054, "synset": "chalice_vine.n.01", "name": "chalice_vine"}, + {"id": 21055, "synset": "verbena.n.01", "name": "verbena"}, + {"id": 21056, "synset": "lantana.n.01", "name": "lantana"}, + {"id": 21057, "synset": "black_mangrove.n.02", "name": "black_mangrove"}, + {"id": 21058, "synset": "white_mangrove.n.01", "name": "white_mangrove"}, + {"id": 21059, "synset": "black_mangrove.n.01", "name": "black_mangrove"}, + {"id": 21060, "synset": "teak.n.02", "name": "teak"}, + {"id": 21061, "synset": "spurge.n.01", "name": "spurge"}, + {"id": 21062, "synset": "sun_spurge.n.01", "name": "sun_spurge"}, + {"id": 21063, "synset": "petty_spurge.n.01", "name": "petty_spurge"}, + {"id": 21064, "synset": "medusa's_head.n.01", "name": "medusa's_head"}, + {"id": 21065, "synset": "wild_spurge.n.01", "name": "wild_spurge"}, + {"id": 21066, "synset": "snow-on-the-mountain.n.01", "name": "snow-on-the-mountain"}, + {"id": 21067, "synset": "cypress_spurge.n.01", "name": "cypress_spurge"}, + {"id": 21068, "synset": "leafy_spurge.n.01", "name": "leafy_spurge"}, + {"id": 21069, "synset": "hairy_spurge.n.01", "name": "hairy_spurge"}, + {"id": 21070, "synset": "poinsettia.n.01", "name": "poinsettia"}, + {"id": 21071, "synset": "japanese_poinsettia.n.01", "name": "Japanese_poinsettia"}, + {"id": 21072, "synset": "fire-on-the-mountain.n.01", "name": "fire-on-the-mountain"}, + {"id": 21073, "synset": "wood_spurge.n.01", "name": "wood_spurge"}, + {"id": 21074, "synset": "dwarf_spurge.n.01", "name": "dwarf_spurge"}, + {"id": 21075, "synset": "scarlet_plume.n.01", "name": "scarlet_plume"}, + {"id": 21076, "synset": "naboom.n.01", "name": "naboom"}, + {"id": 21077, "synset": "crown_of_thorns.n.02", "name": "crown_of_thorns"}, + {"id": 21078, "synset": "toothed_spurge.n.01", "name": "toothed_spurge"}, + {"id": 21079, "synset": "three-seeded_mercury.n.01", "name": "three-seeded_mercury"}, + {"id": 21080, "synset": "croton.n.02", "name": "croton"}, + {"id": 21081, "synset": "cascarilla.n.01", "name": "cascarilla"}, + {"id": 21082, "synset": "cascarilla_bark.n.01", "name": "cascarilla_bark"}, + {"id": 21083, "synset": "castor-oil_plant.n.01", "name": "castor-oil_plant"}, + {"id": 21084, "synset": "spurge_nettle.n.01", "name": "spurge_nettle"}, + {"id": 21085, "synset": "physic_nut.n.01", "name": "physic_nut"}, + {"id": 21086, "synset": "para_rubber_tree.n.01", "name": "Para_rubber_tree"}, + {"id": 21087, "synset": "cassava.n.03", "name": "cassava"}, + {"id": 21088, "synset": "bitter_cassava.n.01", "name": "bitter_cassava"}, + {"id": 21089, "synset": "cassava.n.02", "name": "cassava"}, + {"id": 21090, "synset": "sweet_cassava.n.01", "name": "sweet_cassava"}, + {"id": 21091, "synset": "candlenut.n.01", "name": "candlenut"}, + {"id": 21092, "synset": "tung_tree.n.01", "name": "tung_tree"}, + {"id": 21093, "synset": "slipper_spurge.n.01", "name": "slipper_spurge"}, + {"id": 21094, "synset": "candelilla.n.01", "name": "candelilla"}, + {"id": 21095, "synset": "jewbush.n.01", "name": "Jewbush"}, + {"id": 21096, "synset": "jumping_bean.n.01", "name": "jumping_bean"}, + {"id": 21097, "synset": "camellia.n.01", "name": "camellia"}, + {"id": 21098, "synset": "japonica.n.01", "name": "japonica"}, + {"id": 21099, "synset": "umbellifer.n.01", "name": "umbellifer"}, + {"id": 21100, "synset": "wild_parsley.n.01", "name": "wild_parsley"}, + {"id": 21101, "synset": "fool's_parsley.n.01", "name": "fool's_parsley"}, + {"id": 21102, "synset": "dill.n.01", "name": "dill"}, + {"id": 21103, "synset": "angelica.n.01", "name": "angelica"}, + {"id": 21104, "synset": "garden_angelica.n.01", "name": "garden_angelica"}, + {"id": 21105, "synset": "wild_angelica.n.01", "name": "wild_angelica"}, + {"id": 21106, "synset": "chervil.n.01", "name": "chervil"}, + {"id": 21107, "synset": "cow_parsley.n.01", "name": "cow_parsley"}, + {"id": 21108, "synset": "wild_celery.n.01", "name": "wild_celery"}, + {"id": 21109, "synset": "astrantia.n.01", "name": "astrantia"}, + {"id": 21110, "synset": "greater_masterwort.n.01", "name": "greater_masterwort"}, + {"id": 21111, "synset": "caraway.n.01", "name": "caraway"}, + {"id": 21112, "synset": "whorled_caraway.n.01", "name": "whorled_caraway"}, + {"id": 21113, "synset": "water_hemlock.n.01", "name": "water_hemlock"}, + {"id": 21114, "synset": "spotted_cowbane.n.01", "name": "spotted_cowbane"}, + {"id": 21115, "synset": "hemlock.n.02", "name": "hemlock"}, + {"id": 21116, "synset": "earthnut.n.02", "name": "earthnut"}, + {"id": 21117, "synset": "cumin.n.01", "name": "cumin"}, + {"id": 21118, "synset": "wild_carrot.n.01", "name": "wild_carrot"}, + {"id": 21119, "synset": "eryngo.n.01", "name": "eryngo"}, + {"id": 21120, "synset": "sea_holly.n.01", "name": "sea_holly"}, + {"id": 21121, "synset": "button_snakeroot.n.02", "name": "button_snakeroot"}, + {"id": 21122, "synset": "rattlesnake_master.n.01", "name": "rattlesnake_master"}, + {"id": 21123, "synset": "fennel.n.01", "name": "fennel"}, + {"id": 21124, "synset": "common_fennel.n.01", "name": "common_fennel"}, + {"id": 21125, "synset": "florence_fennel.n.01", "name": "Florence_fennel"}, + {"id": 21126, "synset": "cow_parsnip.n.01", "name": "cow_parsnip"}, + {"id": 21127, "synset": "lovage.n.01", "name": "lovage"}, + {"id": 21128, "synset": "sweet_cicely.n.01", "name": "sweet_cicely"}, + {"id": 21129, "synset": "water_fennel.n.01", "name": "water_fennel"}, + {"id": 21130, "synset": "parsnip.n.02", "name": "parsnip"}, + {"id": 21131, "synset": "cultivated_parsnip.n.01", "name": "cultivated_parsnip"}, + {"id": 21132, "synset": "wild_parsnip.n.01", "name": "wild_parsnip"}, + {"id": 21133, "synset": "parsley.n.01", "name": "parsley"}, + {"id": 21134, "synset": "italian_parsley.n.01", "name": "Italian_parsley"}, + {"id": 21135, "synset": "hamburg_parsley.n.01", "name": "Hamburg_parsley"}, + {"id": 21136, "synset": "anise.n.01", "name": "anise"}, + {"id": 21137, "synset": "sanicle.n.01", "name": "sanicle"}, + {"id": 21138, "synset": "purple_sanicle.n.01", "name": "purple_sanicle"}, + {"id": 21139, "synset": "european_sanicle.n.01", "name": "European_sanicle"}, + {"id": 21140, "synset": "water_parsnip.n.01", "name": "water_parsnip"}, + {"id": 21141, "synset": "greater_water_parsnip.n.01", "name": "greater_water_parsnip"}, + {"id": 21142, "synset": "skirret.n.01", "name": "skirret"}, + {"id": 21143, "synset": "dogwood.n.01", "name": "dogwood"}, + {"id": 21144, "synset": "common_white_dogwood.n.01", "name": "common_white_dogwood"}, + {"id": 21145, "synset": "red_osier.n.01", "name": "red_osier"}, + {"id": 21146, "synset": "silky_dogwood.n.02", "name": "silky_dogwood"}, + {"id": 21147, "synset": "silky_cornel.n.01", "name": "silky_cornel"}, + {"id": 21148, "synset": "common_european_dogwood.n.01", "name": "common_European_dogwood"}, + {"id": 21149, "synset": "bunchberry.n.01", "name": "bunchberry"}, + {"id": 21150, "synset": "cornelian_cherry.n.01", "name": "cornelian_cherry"}, + {"id": 21151, "synset": "puka.n.01", "name": "puka"}, + {"id": 21152, "synset": "kapuka.n.01", "name": "kapuka"}, + {"id": 21153, "synset": "valerian.n.01", "name": "valerian"}, + {"id": 21154, "synset": "common_valerian.n.01", "name": "common_valerian"}, + {"id": 21155, "synset": "common_corn_salad.n.01", "name": "common_corn_salad"}, + {"id": 21156, "synset": "red_valerian.n.01", "name": "red_valerian"}, + {"id": 21157, "synset": "filmy_fern.n.02", "name": "filmy_fern"}, + {"id": 21158, "synset": "bristle_fern.n.01", "name": "bristle_fern"}, + {"id": 21159, "synset": "hare's-foot_bristle_fern.n.01", "name": "hare's-foot_bristle_fern"}, + {"id": 21160, "synset": "killarney_fern.n.01", "name": "Killarney_fern"}, + {"id": 21161, "synset": "kidney_fern.n.01", "name": "kidney_fern"}, + {"id": 21162, "synset": "flowering_fern.n.02", "name": "flowering_fern"}, + {"id": 21163, "synset": "royal_fern.n.01", "name": "royal_fern"}, + {"id": 21164, "synset": "interrupted_fern.n.01", "name": "interrupted_fern"}, + {"id": 21165, "synset": "crape_fern.n.01", "name": "crape_fern"}, + {"id": 21166, "synset": "crepe_fern.n.01", "name": "crepe_fern"}, + {"id": 21167, "synset": "curly_grass.n.01", "name": "curly_grass"}, + {"id": 21168, "synset": "pine_fern.n.01", "name": "pine_fern"}, + {"id": 21169, "synset": "climbing_fern.n.01", "name": "climbing_fern"}, + {"id": 21170, "synset": "creeping_fern.n.01", "name": "creeping_fern"}, + {"id": 21171, "synset": "climbing_maidenhair.n.01", "name": "climbing_maidenhair"}, + {"id": 21172, "synset": "scented_fern.n.02", "name": "scented_fern"}, + {"id": 21173, "synset": "clover_fern.n.01", "name": "clover_fern"}, + {"id": 21174, "synset": "nardoo.n.01", "name": "nardoo"}, + {"id": 21175, "synset": "water_clover.n.01", "name": "water_clover"}, + {"id": 21176, "synset": "pillwort.n.01", "name": "pillwort"}, + {"id": 21177, "synset": "regnellidium.n.01", "name": "regnellidium"}, + {"id": 21178, "synset": "floating-moss.n.01", "name": "floating-moss"}, + {"id": 21179, "synset": "mosquito_fern.n.01", "name": "mosquito_fern"}, + {"id": 21180, "synset": "adder's_tongue.n.01", "name": "adder's_tongue"}, + {"id": 21181, "synset": "ribbon_fern.n.03", "name": "ribbon_fern"}, + {"id": 21182, "synset": "grape_fern.n.01", "name": "grape_fern"}, + {"id": 21183, "synset": "daisyleaf_grape_fern.n.01", "name": "daisyleaf_grape_fern"}, + {"id": 21184, "synset": "leathery_grape_fern.n.01", "name": "leathery_grape_fern"}, + {"id": 21185, "synset": "rattlesnake_fern.n.01", "name": "rattlesnake_fern"}, + {"id": 21186, "synset": "flowering_fern.n.01", "name": "flowering_fern"}, + {"id": 21187, "synset": "powdery_mildew.n.01", "name": "powdery_mildew"}, + {"id": 21188, "synset": "dutch_elm_fungus.n.01", "name": "Dutch_elm_fungus"}, + {"id": 21189, "synset": "ergot.n.02", "name": "ergot"}, + {"id": 21190, "synset": "rye_ergot.n.01", "name": "rye_ergot"}, + {"id": 21191, "synset": "black_root_rot_fungus.n.01", "name": "black_root_rot_fungus"}, + {"id": 21192, "synset": "dead-man's-fingers.n.01", "name": "dead-man's-fingers"}, + {"id": 21193, "synset": "sclerotinia.n.01", "name": "sclerotinia"}, + {"id": 21194, "synset": "brown_cup.n.01", "name": "brown_cup"}, + {"id": 21195, "synset": "earthball.n.01", "name": "earthball"}, + {"id": 21196, "synset": "scleroderma_citrinum.n.01", "name": "Scleroderma_citrinum"}, + {"id": 21197, "synset": "scleroderma_flavidium.n.01", "name": "Scleroderma_flavidium"}, + {"id": 21198, "synset": "scleroderma_bovista.n.01", "name": "Scleroderma_bovista"}, + {"id": 21199, "synset": "podaxaceae.n.01", "name": "Podaxaceae"}, + {"id": 21200, "synset": "stalked_puffball.n.02", "name": "stalked_puffball"}, + {"id": 21201, "synset": "stalked_puffball.n.01", "name": "stalked_puffball"}, + {"id": 21202, "synset": "false_truffle.n.01", "name": "false_truffle"}, + {"id": 21203, "synset": "rhizopogon_idahoensis.n.01", "name": "Rhizopogon_idahoensis"}, + {"id": 21204, "synset": "truncocolumella_citrina.n.01", "name": "Truncocolumella_citrina"}, + {"id": 21205, "synset": "mucor.n.01", "name": "mucor"}, + {"id": 21206, "synset": "rhizopus.n.01", "name": "rhizopus"}, + {"id": 21207, "synset": "bread_mold.n.01", "name": "bread_mold"}, + {"id": 21208, "synset": "slime_mold.n.01", "name": "slime_mold"}, + {"id": 21209, "synset": "true_slime_mold.n.01", "name": "true_slime_mold"}, + {"id": 21210, "synset": "cellular_slime_mold.n.01", "name": "cellular_slime_mold"}, + {"id": 21211, "synset": "dictostylium.n.01", "name": "dictostylium"}, + {"id": 21212, "synset": "pond-scum_parasite.n.01", "name": "pond-scum_parasite"}, + {"id": 21213, "synset": "potato_wart_fungus.n.01", "name": "potato_wart_fungus"}, + {"id": 21214, "synset": "white_fungus.n.01", "name": "white_fungus"}, + {"id": 21215, "synset": "water_mold.n.01", "name": "water_mold"}, + {"id": 21216, "synset": "downy_mildew.n.01", "name": "downy_mildew"}, + {"id": 21217, "synset": "blue_mold_fungus.n.01", "name": "blue_mold_fungus"}, + {"id": 21218, "synset": "onion_mildew.n.01", "name": "onion_mildew"}, + {"id": 21219, "synset": "tobacco_mildew.n.01", "name": "tobacco_mildew"}, + {"id": 21220, "synset": "white_rust.n.01", "name": "white_rust"}, + {"id": 21221, "synset": "pythium.n.01", "name": "pythium"}, + {"id": 21222, "synset": "damping_off_fungus.n.01", "name": "damping_off_fungus"}, + {"id": 21223, "synset": "phytophthora_citrophthora.n.01", "name": "Phytophthora_citrophthora"}, + {"id": 21224, "synset": "phytophthora_infestans.n.01", "name": "Phytophthora_infestans"}, + {"id": 21225, "synset": "clubroot_fungus.n.01", "name": "clubroot_fungus"}, + {"id": 21226, "synset": "geglossaceae.n.01", "name": "Geglossaceae"}, + {"id": 21227, "synset": "sarcosomataceae.n.01", "name": "Sarcosomataceae"}, + {"id": 21228, "synset": "rufous_rubber_cup.n.01", "name": "Rufous_rubber_cup"}, + {"id": 21229, "synset": "devil's_cigar.n.01", "name": "devil's_cigar"}, + {"id": 21230, "synset": "devil's_urn.n.01", "name": "devil's_urn"}, + {"id": 21231, "synset": "truffle.n.01", "name": "truffle"}, + {"id": 21232, "synset": "club_fungus.n.01", "name": "club_fungus"}, + {"id": 21233, "synset": "coral_fungus.n.01", "name": "coral_fungus"}, + {"id": 21234, "synset": "tooth_fungus.n.01", "name": "tooth_fungus"}, + {"id": 21235, "synset": "lichen.n.02", "name": "lichen"}, + {"id": 21236, "synset": "ascolichen.n.01", "name": "ascolichen"}, + {"id": 21237, "synset": "basidiolichen.n.01", "name": "basidiolichen"}, + {"id": 21238, "synset": "lecanora.n.01", "name": "lecanora"}, + {"id": 21239, "synset": "manna_lichen.n.01", "name": "manna_lichen"}, + {"id": 21240, "synset": "archil.n.02", "name": "archil"}, + {"id": 21241, "synset": "roccella.n.01", "name": "roccella"}, + {"id": 21242, "synset": "beard_lichen.n.01", "name": "beard_lichen"}, + {"id": 21243, "synset": "horsehair_lichen.n.01", "name": "horsehair_lichen"}, + {"id": 21244, "synset": "reindeer_moss.n.01", "name": "reindeer_moss"}, + {"id": 21245, "synset": "crottle.n.01", "name": "crottle"}, + {"id": 21246, "synset": "iceland_moss.n.01", "name": "Iceland_moss"}, + {"id": 21247, "synset": "fungus.n.01", "name": "fungus"}, + {"id": 21248, "synset": "promycelium.n.01", "name": "promycelium"}, + {"id": 21249, "synset": "true_fungus.n.01", "name": "true_fungus"}, + {"id": 21250, "synset": "basidiomycete.n.01", "name": "basidiomycete"}, + {"id": 21251, "synset": "mushroom.n.03", "name": "mushroom"}, + {"id": 21252, "synset": "agaric.n.02", "name": "agaric"}, + {"id": 21253, "synset": "mushroom.n.01", "name": "mushroom"}, + {"id": 21254, "synset": "toadstool.n.01", "name": "toadstool"}, + {"id": 21255, "synset": "horse_mushroom.n.01", "name": "horse_mushroom"}, + {"id": 21256, "synset": "meadow_mushroom.n.01", "name": "meadow_mushroom"}, + {"id": 21257, "synset": "shiitake.n.01", "name": "shiitake"}, + {"id": 21258, "synset": "scaly_lentinus.n.01", "name": "scaly_lentinus"}, + {"id": 21259, "synset": "royal_agaric.n.01", "name": "royal_agaric"}, + {"id": 21260, "synset": "false_deathcap.n.01", "name": "false_deathcap"}, + {"id": 21261, "synset": "fly_agaric.n.01", "name": "fly_agaric"}, + {"id": 21262, "synset": "death_cap.n.01", "name": "death_cap"}, + {"id": 21263, "synset": "blushing_mushroom.n.01", "name": "blushing_mushroom"}, + {"id": 21264, "synset": "destroying_angel.n.01", "name": "destroying_angel"}, + {"id": 21265, "synset": "chanterelle.n.01", "name": "chanterelle"}, + {"id": 21266, "synset": "floccose_chanterelle.n.01", "name": "floccose_chanterelle"}, + {"id": 21267, "synset": "pig's_ears.n.01", "name": "pig's_ears"}, + {"id": 21268, "synset": "cinnabar_chanterelle.n.01", "name": "cinnabar_chanterelle"}, + {"id": 21269, "synset": "jack-o-lantern_fungus.n.01", "name": "jack-o-lantern_fungus"}, + {"id": 21270, "synset": "inky_cap.n.01", "name": "inky_cap"}, + {"id": 21271, "synset": "shaggymane.n.01", "name": "shaggymane"}, + {"id": 21272, "synset": "milkcap.n.01", "name": "milkcap"}, + {"id": 21273, "synset": "fairy-ring_mushroom.n.01", "name": "fairy-ring_mushroom"}, + {"id": 21274, "synset": "fairy_ring.n.01", "name": "fairy_ring"}, + {"id": 21275, "synset": "oyster_mushroom.n.01", "name": "oyster_mushroom"}, + {"id": 21276, "synset": "olive-tree_agaric.n.01", "name": "olive-tree_agaric"}, + {"id": 21277, "synset": "pholiota_astragalina.n.01", "name": "Pholiota_astragalina"}, + {"id": 21278, "synset": "pholiota_aurea.n.01", "name": "Pholiota_aurea"}, + {"id": 21279, "synset": "pholiota_destruens.n.01", "name": "Pholiota_destruens"}, + {"id": 21280, "synset": "pholiota_flammans.n.01", "name": "Pholiota_flammans"}, + {"id": 21281, "synset": "pholiota_flavida.n.01", "name": "Pholiota_flavida"}, + {"id": 21282, "synset": "nameko.n.01", "name": "nameko"}, + { + "id": 21283, + "synset": "pholiota_squarrosa-adiposa.n.01", + "name": "Pholiota_squarrosa-adiposa", + }, + {"id": 21284, "synset": "pholiota_squarrosa.n.01", "name": "Pholiota_squarrosa"}, + {"id": 21285, "synset": "pholiota_squarrosoides.n.01", "name": "Pholiota_squarrosoides"}, + {"id": 21286, "synset": "stropharia_ambigua.n.01", "name": "Stropharia_ambigua"}, + {"id": 21287, "synset": "stropharia_hornemannii.n.01", "name": "Stropharia_hornemannii"}, + { + "id": 21288, + "synset": "stropharia_rugoso-annulata.n.01", + "name": "Stropharia_rugoso-annulata", + }, + {"id": 21289, "synset": "gill_fungus.n.01", "name": "gill_fungus"}, + {"id": 21290, "synset": "entoloma_lividum.n.01", "name": "Entoloma_lividum"}, + {"id": 21291, "synset": "entoloma_aprile.n.01", "name": "Entoloma_aprile"}, + {"id": 21292, "synset": "chlorophyllum_molybdites.n.01", "name": "Chlorophyllum_molybdites"}, + {"id": 21293, "synset": "lepiota.n.01", "name": "lepiota"}, + {"id": 21294, "synset": "parasol_mushroom.n.01", "name": "parasol_mushroom"}, + {"id": 21295, "synset": "poisonous_parasol.n.01", "name": "poisonous_parasol"}, + {"id": 21296, "synset": "lepiota_naucina.n.01", "name": "Lepiota_naucina"}, + {"id": 21297, "synset": "lepiota_rhacodes.n.01", "name": "Lepiota_rhacodes"}, + {"id": 21298, "synset": "american_parasol.n.01", "name": "American_parasol"}, + {"id": 21299, "synset": "lepiota_rubrotincta.n.01", "name": "Lepiota_rubrotincta"}, + {"id": 21300, "synset": "lepiota_clypeolaria.n.01", "name": "Lepiota_clypeolaria"}, + {"id": 21301, "synset": "onion_stem.n.01", "name": "onion_stem"}, + {"id": 21302, "synset": "pink_disease_fungus.n.01", "name": "pink_disease_fungus"}, + {"id": 21303, "synset": "bottom_rot_fungus.n.01", "name": "bottom_rot_fungus"}, + {"id": 21304, "synset": "potato_fungus.n.01", "name": "potato_fungus"}, + {"id": 21305, "synset": "coffee_fungus.n.01", "name": "coffee_fungus"}, + {"id": 21306, "synset": "blewits.n.01", "name": "blewits"}, + {"id": 21307, "synset": "sandy_mushroom.n.01", "name": "sandy_mushroom"}, + {"id": 21308, "synset": "tricholoma_pessundatum.n.01", "name": "Tricholoma_pessundatum"}, + {"id": 21309, "synset": "tricholoma_sejunctum.n.01", "name": "Tricholoma_sejunctum"}, + {"id": 21310, "synset": "man-on-a-horse.n.01", "name": "man-on-a-horse"}, + {"id": 21311, "synset": "tricholoma_venenata.n.01", "name": "Tricholoma_venenata"}, + {"id": 21312, "synset": "tricholoma_pardinum.n.01", "name": "Tricholoma_pardinum"}, + {"id": 21313, "synset": "tricholoma_vaccinum.n.01", "name": "Tricholoma_vaccinum"}, + {"id": 21314, "synset": "tricholoma_aurantium.n.01", "name": "Tricholoma_aurantium"}, + {"id": 21315, "synset": "volvaria_bombycina.n.01", "name": "Volvaria_bombycina"}, + {"id": 21316, "synset": "pluteus_aurantiorugosus.n.01", "name": "Pluteus_aurantiorugosus"}, + {"id": 21317, "synset": "pluteus_magnus.n.01", "name": "Pluteus_magnus"}, + {"id": 21318, "synset": "deer_mushroom.n.01", "name": "deer_mushroom"}, + {"id": 21319, "synset": "straw_mushroom.n.01", "name": "straw_mushroom"}, + {"id": 21320, "synset": "volvariella_bombycina.n.01", "name": "Volvariella_bombycina"}, + {"id": 21321, "synset": "clitocybe_clavipes.n.01", "name": "Clitocybe_clavipes"}, + {"id": 21322, "synset": "clitocybe_dealbata.n.01", "name": "Clitocybe_dealbata"}, + {"id": 21323, "synset": "clitocybe_inornata.n.01", "name": "Clitocybe_inornata"}, + {"id": 21324, "synset": "clitocybe_robusta.n.01", "name": "Clitocybe_robusta"}, + {"id": 21325, "synset": "clitocybe_irina.n.01", "name": "Clitocybe_irina"}, + {"id": 21326, "synset": "clitocybe_subconnexa.n.01", "name": "Clitocybe_subconnexa"}, + {"id": 21327, "synset": "winter_mushroom.n.01", "name": "winter_mushroom"}, + {"id": 21328, "synset": "mycelium.n.01", "name": "mycelium"}, + {"id": 21329, "synset": "sclerotium.n.02", "name": "sclerotium"}, + {"id": 21330, "synset": "sac_fungus.n.01", "name": "sac_fungus"}, + {"id": 21331, "synset": "ascomycete.n.01", "name": "ascomycete"}, + {"id": 21332, "synset": "clavicipitaceae.n.01", "name": "Clavicipitaceae"}, + {"id": 21333, "synset": "grainy_club.n.01", "name": "grainy_club"}, + {"id": 21334, "synset": "yeast.n.02", "name": "yeast"}, + {"id": 21335, "synset": "baker's_yeast.n.01", "name": "baker's_yeast"}, + {"id": 21336, "synset": "wine-maker's_yeast.n.01", "name": "wine-maker's_yeast"}, + {"id": 21337, "synset": "aspergillus_fumigatus.n.01", "name": "Aspergillus_fumigatus"}, + {"id": 21338, "synset": "brown_root_rot_fungus.n.01", "name": "brown_root_rot_fungus"}, + {"id": 21339, "synset": "discomycete.n.01", "name": "discomycete"}, + {"id": 21340, "synset": "leotia_lubrica.n.01", "name": "Leotia_lubrica"}, + {"id": 21341, "synset": "mitrula_elegans.n.01", "name": "Mitrula_elegans"}, + {"id": 21342, "synset": "sarcoscypha_coccinea.n.01", "name": "Sarcoscypha_coccinea"}, + {"id": 21343, "synset": "caloscypha_fulgens.n.01", "name": "Caloscypha_fulgens"}, + {"id": 21344, "synset": "aleuria_aurantia.n.01", "name": "Aleuria_aurantia"}, + {"id": 21345, "synset": "elf_cup.n.01", "name": "elf_cup"}, + {"id": 21346, "synset": "peziza_domicilina.n.01", "name": "Peziza_domicilina"}, + {"id": 21347, "synset": "blood_cup.n.01", "name": "blood_cup"}, + {"id": 21348, "synset": "urnula_craterium.n.01", "name": "Urnula_craterium"}, + {"id": 21349, "synset": "galiella_rufa.n.01", "name": "Galiella_rufa"}, + {"id": 21350, "synset": "jafnea_semitosta.n.01", "name": "Jafnea_semitosta"}, + {"id": 21351, "synset": "morel.n.01", "name": "morel"}, + {"id": 21352, "synset": "common_morel.n.01", "name": "common_morel"}, + {"id": 21353, "synset": "disciotis_venosa.n.01", "name": "Disciotis_venosa"}, + {"id": 21354, "synset": "verpa.n.01", "name": "Verpa"}, + {"id": 21355, "synset": "verpa_bohemica.n.01", "name": "Verpa_bohemica"}, + {"id": 21356, "synset": "verpa_conica.n.01", "name": "Verpa_conica"}, + {"id": 21357, "synset": "black_morel.n.01", "name": "black_morel"}, + {"id": 21358, "synset": "morchella_crassipes.n.01", "name": "Morchella_crassipes"}, + {"id": 21359, "synset": "morchella_semilibera.n.01", "name": "Morchella_semilibera"}, + {"id": 21360, "synset": "wynnea_americana.n.01", "name": "Wynnea_americana"}, + {"id": 21361, "synset": "wynnea_sparassoides.n.01", "name": "Wynnea_sparassoides"}, + {"id": 21362, "synset": "false_morel.n.01", "name": "false_morel"}, + {"id": 21363, "synset": "lorchel.n.01", "name": "lorchel"}, + {"id": 21364, "synset": "helvella.n.01", "name": "helvella"}, + {"id": 21365, "synset": "helvella_crispa.n.01", "name": "Helvella_crispa"}, + {"id": 21366, "synset": "helvella_acetabulum.n.01", "name": "Helvella_acetabulum"}, + {"id": 21367, "synset": "helvella_sulcata.n.01", "name": "Helvella_sulcata"}, + {"id": 21368, "synset": "discina.n.01", "name": "discina"}, + {"id": 21369, "synset": "gyromitra.n.01", "name": "gyromitra"}, + {"id": 21370, "synset": "gyromitra_californica.n.01", "name": "Gyromitra_californica"}, + {"id": 21371, "synset": "gyromitra_sphaerospora.n.01", "name": "Gyromitra_sphaerospora"}, + {"id": 21372, "synset": "gyromitra_esculenta.n.01", "name": "Gyromitra_esculenta"}, + {"id": 21373, "synset": "gyromitra_infula.n.01", "name": "Gyromitra_infula"}, + {"id": 21374, "synset": "gyromitra_fastigiata.n.01", "name": "Gyromitra_fastigiata"}, + {"id": 21375, "synset": "gyromitra_gigas.n.01", "name": "Gyromitra_gigas"}, + {"id": 21376, "synset": "gasteromycete.n.01", "name": "gasteromycete"}, + {"id": 21377, "synset": "stinkhorn.n.01", "name": "stinkhorn"}, + {"id": 21378, "synset": "common_stinkhorn.n.01", "name": "common_stinkhorn"}, + {"id": 21379, "synset": "phallus_ravenelii.n.01", "name": "Phallus_ravenelii"}, + {"id": 21380, "synset": "dog_stinkhorn.n.01", "name": "dog_stinkhorn"}, + {"id": 21381, "synset": "calostoma_lutescens.n.01", "name": "Calostoma_lutescens"}, + {"id": 21382, "synset": "calostoma_cinnabarina.n.01", "name": "Calostoma_cinnabarina"}, + {"id": 21383, "synset": "calostoma_ravenelii.n.01", "name": "Calostoma_ravenelii"}, + {"id": 21384, "synset": "stinky_squid.n.01", "name": "stinky_squid"}, + {"id": 21385, "synset": "puffball.n.01", "name": "puffball"}, + {"id": 21386, "synset": "giant_puffball.n.01", "name": "giant_puffball"}, + {"id": 21387, "synset": "earthstar.n.01", "name": "earthstar"}, + {"id": 21388, "synset": "geastrum_coronatum.n.01", "name": "Geastrum_coronatum"}, + {"id": 21389, "synset": "radiigera_fuscogleba.n.01", "name": "Radiigera_fuscogleba"}, + {"id": 21390, "synset": "astreus_pteridis.n.01", "name": "Astreus_pteridis"}, + {"id": 21391, "synset": "astreus_hygrometricus.n.01", "name": "Astreus_hygrometricus"}, + {"id": 21392, "synset": "bird's-nest_fungus.n.01", "name": "bird's-nest_fungus"}, + {"id": 21393, "synset": "gastrocybe_lateritia.n.01", "name": "Gastrocybe_lateritia"}, + {"id": 21394, "synset": "macowanites_americanus.n.01", "name": "Macowanites_americanus"}, + {"id": 21395, "synset": "polypore.n.01", "name": "polypore"}, + {"id": 21396, "synset": "bracket_fungus.n.01", "name": "bracket_fungus"}, + {"id": 21397, "synset": "albatrellus_dispansus.n.01", "name": "Albatrellus_dispansus"}, + {"id": 21398, "synset": "albatrellus_ovinus.n.01", "name": "Albatrellus_ovinus"}, + {"id": 21399, "synset": "neolentinus_ponderosus.n.01", "name": "Neolentinus_ponderosus"}, + {"id": 21400, "synset": "oligoporus_leucospongia.n.01", "name": "Oligoporus_leucospongia"}, + {"id": 21401, "synset": "polyporus_tenuiculus.n.01", "name": "Polyporus_tenuiculus"}, + {"id": 21402, "synset": "hen-of-the-woods.n.01", "name": "hen-of-the-woods"}, + {"id": 21403, "synset": "polyporus_squamosus.n.01", "name": "Polyporus_squamosus"}, + {"id": 21404, "synset": "beefsteak_fungus.n.01", "name": "beefsteak_fungus"}, + {"id": 21405, "synset": "agaric.n.01", "name": "agaric"}, + {"id": 21406, "synset": "bolete.n.01", "name": "bolete"}, + {"id": 21407, "synset": "boletus_chrysenteron.n.01", "name": "Boletus_chrysenteron"}, + {"id": 21408, "synset": "boletus_edulis.n.01", "name": "Boletus_edulis"}, + {"id": 21409, "synset": "frost's_bolete.n.01", "name": "Frost's_bolete"}, + {"id": 21410, "synset": "boletus_luridus.n.01", "name": "Boletus_luridus"}, + {"id": 21411, "synset": "boletus_mirabilis.n.01", "name": "Boletus_mirabilis"}, + {"id": 21412, "synset": "boletus_pallidus.n.01", "name": "Boletus_pallidus"}, + {"id": 21413, "synset": "boletus_pulcherrimus.n.01", "name": "Boletus_pulcherrimus"}, + {"id": 21414, "synset": "boletus_pulverulentus.n.01", "name": "Boletus_pulverulentus"}, + {"id": 21415, "synset": "boletus_roxanae.n.01", "name": "Boletus_roxanae"}, + {"id": 21416, "synset": "boletus_subvelutipes.n.01", "name": "Boletus_subvelutipes"}, + {"id": 21417, "synset": "boletus_variipes.n.01", "name": "Boletus_variipes"}, + {"id": 21418, "synset": "boletus_zelleri.n.01", "name": "Boletus_zelleri"}, + {"id": 21419, "synset": "fuscoboletinus_paluster.n.01", "name": "Fuscoboletinus_paluster"}, + {"id": 21420, "synset": "fuscoboletinus_serotinus.n.01", "name": "Fuscoboletinus_serotinus"}, + {"id": 21421, "synset": "leccinum_fibrillosum.n.01", "name": "Leccinum_fibrillosum"}, + {"id": 21422, "synset": "suillus_albivelatus.n.01", "name": "Suillus_albivelatus"}, + {"id": 21423, "synset": "old-man-of-the-woods.n.01", "name": "old-man-of-the-woods"}, + {"id": 21424, "synset": "boletellus_russellii.n.01", "name": "Boletellus_russellii"}, + {"id": 21425, "synset": "jelly_fungus.n.01", "name": "jelly_fungus"}, + {"id": 21426, "synset": "snow_mushroom.n.01", "name": "snow_mushroom"}, + {"id": 21427, "synset": "witches'_butter.n.01", "name": "witches'_butter"}, + {"id": 21428, "synset": "tremella_foliacea.n.01", "name": "Tremella_foliacea"}, + {"id": 21429, "synset": "tremella_reticulata.n.01", "name": "Tremella_reticulata"}, + {"id": 21430, "synset": "jew's-ear.n.01", "name": "Jew's-ear"}, + {"id": 21431, "synset": "rust.n.04", "name": "rust"}, + {"id": 21432, "synset": "aecium.n.01", "name": "aecium"}, + {"id": 21433, "synset": "flax_rust.n.01", "name": "flax_rust"}, + {"id": 21434, "synset": "blister_rust.n.02", "name": "blister_rust"}, + {"id": 21435, "synset": "wheat_rust.n.01", "name": "wheat_rust"}, + {"id": 21436, "synset": "apple_rust.n.01", "name": "apple_rust"}, + {"id": 21437, "synset": "smut.n.03", "name": "smut"}, + {"id": 21438, "synset": "covered_smut.n.01", "name": "covered_smut"}, + {"id": 21439, "synset": "loose_smut.n.02", "name": "loose_smut"}, + {"id": 21440, "synset": "cornsmut.n.01", "name": "cornsmut"}, + {"id": 21441, "synset": "boil_smut.n.01", "name": "boil_smut"}, + {"id": 21442, "synset": "sphacelotheca.n.01", "name": "Sphacelotheca"}, + {"id": 21443, "synset": "head_smut.n.01", "name": "head_smut"}, + {"id": 21444, "synset": "bunt.n.04", "name": "bunt"}, + {"id": 21445, "synset": "bunt.n.03", "name": "bunt"}, + {"id": 21446, "synset": "onion_smut.n.01", "name": "onion_smut"}, + {"id": 21447, "synset": "flag_smut_fungus.n.01", "name": "flag_smut_fungus"}, + {"id": 21448, "synset": "wheat_flag_smut.n.01", "name": "wheat_flag_smut"}, + {"id": 21449, "synset": "felt_fungus.n.01", "name": "felt_fungus"}, + {"id": 21450, "synset": "waxycap.n.01", "name": "waxycap"}, + {"id": 21451, "synset": "hygrocybe_acutoconica.n.01", "name": "Hygrocybe_acutoconica"}, + {"id": 21452, "synset": "hygrophorus_borealis.n.01", "name": "Hygrophorus_borealis"}, + {"id": 21453, "synset": "hygrophorus_caeruleus.n.01", "name": "Hygrophorus_caeruleus"}, + {"id": 21454, "synset": "hygrophorus_inocybiformis.n.01", "name": "Hygrophorus_inocybiformis"}, + {"id": 21455, "synset": "hygrophorus_kauffmanii.n.01", "name": "Hygrophorus_kauffmanii"}, + {"id": 21456, "synset": "hygrophorus_marzuolus.n.01", "name": "Hygrophorus_marzuolus"}, + {"id": 21457, "synset": "hygrophorus_purpurascens.n.01", "name": "Hygrophorus_purpurascens"}, + {"id": 21458, "synset": "hygrophorus_russula.n.01", "name": "Hygrophorus_russula"}, + {"id": 21459, "synset": "hygrophorus_sordidus.n.01", "name": "Hygrophorus_sordidus"}, + {"id": 21460, "synset": "hygrophorus_tennesseensis.n.01", "name": "Hygrophorus_tennesseensis"}, + {"id": 21461, "synset": "hygrophorus_turundus.n.01", "name": "Hygrophorus_turundus"}, + { + "id": 21462, + "synset": "neohygrophorus_angelesianus.n.01", + "name": "Neohygrophorus_angelesianus", + }, + {"id": 21463, "synset": "cortinarius_armillatus.n.01", "name": "Cortinarius_armillatus"}, + {"id": 21464, "synset": "cortinarius_atkinsonianus.n.01", "name": "Cortinarius_atkinsonianus"}, + {"id": 21465, "synset": "cortinarius_corrugatus.n.01", "name": "Cortinarius_corrugatus"}, + {"id": 21466, "synset": "cortinarius_gentilis.n.01", "name": "Cortinarius_gentilis"}, + {"id": 21467, "synset": "cortinarius_mutabilis.n.01", "name": "Cortinarius_mutabilis"}, + { + "id": 21468, + "synset": "cortinarius_semisanguineus.n.01", + "name": "Cortinarius_semisanguineus", + }, + {"id": 21469, "synset": "cortinarius_subfoetidus.n.01", "name": "Cortinarius_subfoetidus"}, + {"id": 21470, "synset": "cortinarius_violaceus.n.01", "name": "Cortinarius_violaceus"}, + {"id": 21471, "synset": "gymnopilus_spectabilis.n.01", "name": "Gymnopilus_spectabilis"}, + {"id": 21472, "synset": "gymnopilus_validipes.n.01", "name": "Gymnopilus_validipes"}, + {"id": 21473, "synset": "gymnopilus_ventricosus.n.01", "name": "Gymnopilus_ventricosus"}, + {"id": 21474, "synset": "mold.n.05", "name": "mold"}, + {"id": 21475, "synset": "mildew.n.02", "name": "mildew"}, + {"id": 21476, "synset": "verticillium.n.01", "name": "verticillium"}, + {"id": 21477, "synset": "monilia.n.01", "name": "monilia"}, + {"id": 21478, "synset": "candida.n.01", "name": "candida"}, + {"id": 21479, "synset": "candida_albicans.n.01", "name": "Candida_albicans"}, + {"id": 21480, "synset": "blastomycete.n.01", "name": "blastomycete"}, + {"id": 21481, "synset": "yellow_spot_fungus.n.01", "name": "yellow_spot_fungus"}, + {"id": 21482, "synset": "green_smut_fungus.n.01", "name": "green_smut_fungus"}, + {"id": 21483, "synset": "dry_rot.n.02", "name": "dry_rot"}, + {"id": 21484, "synset": "rhizoctinia.n.01", "name": "rhizoctinia"}, + {"id": 21485, "synset": "houseplant.n.01", "name": "houseplant"}, + {"id": 21486, "synset": "bedder.n.01", "name": "bedder"}, + {"id": 21487, "synset": "succulent.n.01", "name": "succulent"}, + {"id": 21488, "synset": "cultivar.n.01", "name": "cultivar"}, + {"id": 21489, "synset": "weed.n.01", "name": "weed"}, + {"id": 21490, "synset": "wort.n.01", "name": "wort"}, + {"id": 21491, "synset": "brier.n.02", "name": "brier"}, + {"id": 21492, "synset": "aril.n.01", "name": "aril"}, + {"id": 21493, "synset": "sporophyll.n.01", "name": "sporophyll"}, + {"id": 21494, "synset": "sporangium.n.01", "name": "sporangium"}, + {"id": 21495, "synset": "sporangiophore.n.01", "name": "sporangiophore"}, + {"id": 21496, "synset": "ascus.n.01", "name": "ascus"}, + {"id": 21497, "synset": "ascospore.n.01", "name": "ascospore"}, + {"id": 21498, "synset": "arthrospore.n.02", "name": "arthrospore"}, + {"id": 21499, "synset": "eusporangium.n.01", "name": "eusporangium"}, + {"id": 21500, "synset": "tetrasporangium.n.01", "name": "tetrasporangium"}, + {"id": 21501, "synset": "gametangium.n.01", "name": "gametangium"}, + {"id": 21502, "synset": "sorus.n.02", "name": "sorus"}, + {"id": 21503, "synset": "sorus.n.01", "name": "sorus"}, + {"id": 21504, "synset": "partial_veil.n.01", "name": "partial_veil"}, + {"id": 21505, "synset": "lignum.n.01", "name": "lignum"}, + {"id": 21506, "synset": "vascular_ray.n.01", "name": "vascular_ray"}, + {"id": 21507, "synset": "phloem.n.01", "name": "phloem"}, + {"id": 21508, "synset": "evergreen.n.01", "name": "evergreen"}, + {"id": 21509, "synset": "deciduous_plant.n.01", "name": "deciduous_plant"}, + {"id": 21510, "synset": "poisonous_plant.n.01", "name": "poisonous_plant"}, + {"id": 21511, "synset": "vine.n.01", "name": "vine"}, + {"id": 21512, "synset": "creeper.n.01", "name": "creeper"}, + {"id": 21513, "synset": "tendril.n.01", "name": "tendril"}, + {"id": 21514, "synset": "root_climber.n.01", "name": "root_climber"}, + {"id": 21515, "synset": "lignosae.n.01", "name": "lignosae"}, + {"id": 21516, "synset": "arborescent_plant.n.01", "name": "arborescent_plant"}, + {"id": 21517, "synset": "snag.n.02", "name": "snag"}, + {"id": 21518, "synset": "tree.n.01", "name": "tree"}, + {"id": 21519, "synset": "timber_tree.n.01", "name": "timber_tree"}, + {"id": 21520, "synset": "treelet.n.01", "name": "treelet"}, + {"id": 21521, "synset": "arbor.n.01", "name": "arbor"}, + {"id": 21522, "synset": "bean_tree.n.01", "name": "bean_tree"}, + {"id": 21523, "synset": "pollard.n.01", "name": "pollard"}, + {"id": 21524, "synset": "sapling.n.01", "name": "sapling"}, + {"id": 21525, "synset": "shade_tree.n.01", "name": "shade_tree"}, + {"id": 21526, "synset": "gymnospermous_tree.n.01", "name": "gymnospermous_tree"}, + {"id": 21527, "synset": "conifer.n.01", "name": "conifer"}, + {"id": 21528, "synset": "angiospermous_tree.n.01", "name": "angiospermous_tree"}, + {"id": 21529, "synset": "nut_tree.n.01", "name": "nut_tree"}, + {"id": 21530, "synset": "spice_tree.n.01", "name": "spice_tree"}, + {"id": 21531, "synset": "fever_tree.n.01", "name": "fever_tree"}, + {"id": 21532, "synset": "stump.n.01", "name": "stump"}, + {"id": 21533, "synset": "bonsai.n.01", "name": "bonsai"}, + {"id": 21534, "synset": "ming_tree.n.02", "name": "ming_tree"}, + {"id": 21535, "synset": "ming_tree.n.01", "name": "ming_tree"}, + {"id": 21536, "synset": "undershrub.n.01", "name": "undershrub"}, + {"id": 21537, "synset": "subshrub.n.01", "name": "subshrub"}, + {"id": 21538, "synset": "bramble.n.01", "name": "bramble"}, + {"id": 21539, "synset": "liana.n.01", "name": "liana"}, + {"id": 21540, "synset": "geophyte.n.01", "name": "geophyte"}, + {"id": 21541, "synset": "desert_plant.n.01", "name": "desert_plant"}, + {"id": 21542, "synset": "mesophyte.n.01", "name": "mesophyte"}, + {"id": 21543, "synset": "marsh_plant.n.01", "name": "marsh_plant"}, + {"id": 21544, "synset": "hemiepiphyte.n.01", "name": "hemiepiphyte"}, + {"id": 21545, "synset": "strangler.n.01", "name": "strangler"}, + {"id": 21546, "synset": "lithophyte.n.01", "name": "lithophyte"}, + {"id": 21547, "synset": "saprobe.n.01", "name": "saprobe"}, + {"id": 21548, "synset": "autophyte.n.01", "name": "autophyte"}, + {"id": 21549, "synset": "root.n.01", "name": "root"}, + {"id": 21550, "synset": "taproot.n.01", "name": "taproot"}, + {"id": 21551, "synset": "prop_root.n.01", "name": "prop_root"}, + {"id": 21552, "synset": "prophyll.n.01", "name": "prophyll"}, + {"id": 21553, "synset": "rootstock.n.02", "name": "rootstock"}, + {"id": 21554, "synset": "quickset.n.01", "name": "quickset"}, + {"id": 21555, "synset": "stolon.n.01", "name": "stolon"}, + {"id": 21556, "synset": "tuberous_plant.n.01", "name": "tuberous_plant"}, + {"id": 21557, "synset": "rhizome.n.01", "name": "rhizome"}, + {"id": 21558, "synset": "rachis.n.01", "name": "rachis"}, + {"id": 21559, "synset": "caudex.n.02", "name": "caudex"}, + {"id": 21560, "synset": "cladode.n.01", "name": "cladode"}, + {"id": 21561, "synset": "receptacle.n.02", "name": "receptacle"}, + {"id": 21562, "synset": "scape.n.01", "name": "scape"}, + {"id": 21563, "synset": "umbel.n.01", "name": "umbel"}, + {"id": 21564, "synset": "petiole.n.01", "name": "petiole"}, + {"id": 21565, "synset": "peduncle.n.02", "name": "peduncle"}, + {"id": 21566, "synset": "pedicel.n.01", "name": "pedicel"}, + {"id": 21567, "synset": "flower_cluster.n.01", "name": "flower_cluster"}, + {"id": 21568, "synset": "raceme.n.01", "name": "raceme"}, + {"id": 21569, "synset": "panicle.n.01", "name": "panicle"}, + {"id": 21570, "synset": "thyrse.n.01", "name": "thyrse"}, + {"id": 21571, "synset": "cyme.n.01", "name": "cyme"}, + {"id": 21572, "synset": "cymule.n.01", "name": "cymule"}, + {"id": 21573, "synset": "glomerule.n.01", "name": "glomerule"}, + {"id": 21574, "synset": "scorpioid_cyme.n.01", "name": "scorpioid_cyme"}, + {"id": 21575, "synset": "ear.n.05", "name": "ear"}, + {"id": 21576, "synset": "spadix.n.01", "name": "spadix"}, + {"id": 21577, "synset": "bulbous_plant.n.01", "name": "bulbous_plant"}, + {"id": 21578, "synset": "bulbil.n.01", "name": "bulbil"}, + {"id": 21579, "synset": "cormous_plant.n.01", "name": "cormous_plant"}, + {"id": 21580, "synset": "fruit.n.01", "name": "fruit"}, + {"id": 21581, "synset": "fruitlet.n.01", "name": "fruitlet"}, + {"id": 21582, "synset": "seed.n.01", "name": "seed"}, + {"id": 21583, "synset": "bean.n.02", "name": "bean"}, + {"id": 21584, "synset": "nut.n.01", "name": "nut"}, + {"id": 21585, "synset": "nutlet.n.01", "name": "nutlet"}, + {"id": 21586, "synset": "kernel.n.01", "name": "kernel"}, + {"id": 21587, "synset": "syconium.n.01", "name": "syconium"}, + {"id": 21588, "synset": "berry.n.02", "name": "berry"}, + {"id": 21589, "synset": "aggregate_fruit.n.01", "name": "aggregate_fruit"}, + {"id": 21590, "synset": "simple_fruit.n.01", "name": "simple_fruit"}, + {"id": 21591, "synset": "acinus.n.01", "name": "acinus"}, + {"id": 21592, "synset": "drupe.n.01", "name": "drupe"}, + {"id": 21593, "synset": "drupelet.n.01", "name": "drupelet"}, + {"id": 21594, "synset": "pome.n.01", "name": "pome"}, + {"id": 21595, "synset": "pod.n.02", "name": "pod"}, + {"id": 21596, "synset": "loment.n.01", "name": "loment"}, + {"id": 21597, "synset": "pyxidium.n.01", "name": "pyxidium"}, + {"id": 21598, "synset": "husk.n.02", "name": "husk"}, + {"id": 21599, "synset": "cornhusk.n.01", "name": "cornhusk"}, + {"id": 21600, "synset": "pod.n.01", "name": "pod"}, + {"id": 21601, "synset": "accessory_fruit.n.01", "name": "accessory_fruit"}, + {"id": 21602, "synset": "buckthorn.n.01", "name": "buckthorn"}, + {"id": 21603, "synset": "buckthorn_berry.n.01", "name": "buckthorn_berry"}, + {"id": 21604, "synset": "cascara_buckthorn.n.01", "name": "cascara_buckthorn"}, + {"id": 21605, "synset": "cascara.n.01", "name": "cascara"}, + {"id": 21606, "synset": "carolina_buckthorn.n.01", "name": "Carolina_buckthorn"}, + {"id": 21607, "synset": "coffeeberry.n.01", "name": "coffeeberry"}, + {"id": 21608, "synset": "redberry.n.01", "name": "redberry"}, + {"id": 21609, "synset": "nakedwood.n.01", "name": "nakedwood"}, + {"id": 21610, "synset": "jujube.n.01", "name": "jujube"}, + {"id": 21611, "synset": "christ's-thorn.n.01", "name": "Christ's-thorn"}, + {"id": 21612, "synset": "hazel.n.01", "name": "hazel"}, + {"id": 21613, "synset": "fox_grape.n.01", "name": "fox_grape"}, + {"id": 21614, "synset": "muscadine.n.01", "name": "muscadine"}, + {"id": 21615, "synset": "vinifera.n.01", "name": "vinifera"}, + {"id": 21616, "synset": "pinot_blanc.n.01", "name": "Pinot_blanc"}, + {"id": 21617, "synset": "sauvignon_grape.n.01", "name": "Sauvignon_grape"}, + {"id": 21618, "synset": "sauvignon_blanc.n.01", "name": "Sauvignon_blanc"}, + {"id": 21619, "synset": "muscadet.n.01", "name": "Muscadet"}, + {"id": 21620, "synset": "riesling.n.01", "name": "Riesling"}, + {"id": 21621, "synset": "zinfandel.n.01", "name": "Zinfandel"}, + {"id": 21622, "synset": "chenin_blanc.n.01", "name": "Chenin_blanc"}, + {"id": 21623, "synset": "malvasia.n.01", "name": "malvasia"}, + {"id": 21624, "synset": "verdicchio.n.01", "name": "Verdicchio"}, + {"id": 21625, "synset": "boston_ivy.n.01", "name": "Boston_ivy"}, + {"id": 21626, "synset": "virginia_creeper.n.01", "name": "Virginia_creeper"}, + {"id": 21627, "synset": "true_pepper.n.01", "name": "true_pepper"}, + {"id": 21628, "synset": "betel.n.01", "name": "betel"}, + {"id": 21629, "synset": "cubeb.n.01", "name": "cubeb"}, + {"id": 21630, "synset": "schizocarp.n.01", "name": "schizocarp"}, + {"id": 21631, "synset": "peperomia.n.01", "name": "peperomia"}, + {"id": 21632, "synset": "watermelon_begonia.n.01", "name": "watermelon_begonia"}, + {"id": 21633, "synset": "yerba_mansa.n.01", "name": "yerba_mansa"}, + {"id": 21634, "synset": "pinna.n.01", "name": "pinna"}, + {"id": 21635, "synset": "frond.n.01", "name": "frond"}, + {"id": 21636, "synset": "bract.n.01", "name": "bract"}, + {"id": 21637, "synset": "bracteole.n.01", "name": "bracteole"}, + {"id": 21638, "synset": "involucre.n.01", "name": "involucre"}, + {"id": 21639, "synset": "glume.n.01", "name": "glume"}, + {"id": 21640, "synset": "palmate_leaf.n.01", "name": "palmate_leaf"}, + {"id": 21641, "synset": "pinnate_leaf.n.01", "name": "pinnate_leaf"}, + {"id": 21642, "synset": "bijugate_leaf.n.01", "name": "bijugate_leaf"}, + {"id": 21643, "synset": "decompound_leaf.n.01", "name": "decompound_leaf"}, + {"id": 21644, "synset": "acuminate_leaf.n.01", "name": "acuminate_leaf"}, + {"id": 21645, "synset": "deltoid_leaf.n.01", "name": "deltoid_leaf"}, + {"id": 21646, "synset": "ensiform_leaf.n.01", "name": "ensiform_leaf"}, + {"id": 21647, "synset": "linear_leaf.n.01", "name": "linear_leaf"}, + {"id": 21648, "synset": "lyrate_leaf.n.01", "name": "lyrate_leaf"}, + {"id": 21649, "synset": "obtuse_leaf.n.01", "name": "obtuse_leaf"}, + {"id": 21650, "synset": "oblanceolate_leaf.n.01", "name": "oblanceolate_leaf"}, + {"id": 21651, "synset": "pandurate_leaf.n.01", "name": "pandurate_leaf"}, + {"id": 21652, "synset": "reniform_leaf.n.01", "name": "reniform_leaf"}, + {"id": 21653, "synset": "spatulate_leaf.n.01", "name": "spatulate_leaf"}, + {"id": 21654, "synset": "even-pinnate_leaf.n.01", "name": "even-pinnate_leaf"}, + {"id": 21655, "synset": "odd-pinnate_leaf.n.01", "name": "odd-pinnate_leaf"}, + {"id": 21656, "synset": "pedate_leaf.n.01", "name": "pedate_leaf"}, + {"id": 21657, "synset": "crenate_leaf.n.01", "name": "crenate_leaf"}, + {"id": 21658, "synset": "dentate_leaf.n.01", "name": "dentate_leaf"}, + {"id": 21659, "synset": "denticulate_leaf.n.01", "name": "denticulate_leaf"}, + {"id": 21660, "synset": "erose_leaf.n.01", "name": "erose_leaf"}, + {"id": 21661, "synset": "runcinate_leaf.n.01", "name": "runcinate_leaf"}, + {"id": 21662, "synset": "prickly-edged_leaf.n.01", "name": "prickly-edged_leaf"}, + {"id": 21663, "synset": "deadwood.n.01", "name": "deadwood"}, + {"id": 21664, "synset": "haulm.n.01", "name": "haulm"}, + {"id": 21665, "synset": "branchlet.n.01", "name": "branchlet"}, + {"id": 21666, "synset": "osier.n.01", "name": "osier"}, + {"id": 21667, "synset": "giant_scrambling_fern.n.01", "name": "giant_scrambling_fern"}, + {"id": 21668, "synset": "umbrella_fern.n.01", "name": "umbrella_fern"}, + {"id": 21669, "synset": "floating_fern.n.02", "name": "floating_fern"}, + {"id": 21670, "synset": "polypody.n.01", "name": "polypody"}, + {"id": 21671, "synset": "licorice_fern.n.01", "name": "licorice_fern"}, + {"id": 21672, "synset": "grey_polypody.n.01", "name": "grey_polypody"}, + {"id": 21673, "synset": "leatherleaf.n.01", "name": "leatherleaf"}, + {"id": 21674, "synset": "rock_polypody.n.01", "name": "rock_polypody"}, + {"id": 21675, "synset": "common_polypody.n.01", "name": "common_polypody"}, + {"id": 21676, "synset": "bear's-paw_fern.n.01", "name": "bear's-paw_fern"}, + {"id": 21677, "synset": "strap_fern.n.01", "name": "strap_fern"}, + {"id": 21678, "synset": "florida_strap_fern.n.01", "name": "Florida_strap_fern"}, + {"id": 21679, "synset": "basket_fern.n.02", "name": "basket_fern"}, + {"id": 21680, "synset": "snake_polypody.n.01", "name": "snake_polypody"}, + {"id": 21681, "synset": "climbing_bird's_nest_fern.n.01", "name": "climbing_bird's_nest_fern"}, + {"id": 21682, "synset": "golden_polypody.n.01", "name": "golden_polypody"}, + {"id": 21683, "synset": "staghorn_fern.n.01", "name": "staghorn_fern"}, + {"id": 21684, "synset": "south_american_staghorn.n.01", "name": "South_American_staghorn"}, + {"id": 21685, "synset": "common_staghorn_fern.n.01", "name": "common_staghorn_fern"}, + {"id": 21686, "synset": "felt_fern.n.01", "name": "felt_fern"}, + {"id": 21687, "synset": "potato_fern.n.02", "name": "potato_fern"}, + {"id": 21688, "synset": "myrmecophyte.n.01", "name": "myrmecophyte"}, + {"id": 21689, "synset": "grass_fern.n.01", "name": "grass_fern"}, + {"id": 21690, "synset": "spleenwort.n.01", "name": "spleenwort"}, + {"id": 21691, "synset": "black_spleenwort.n.01", "name": "black_spleenwort"}, + {"id": 21692, "synset": "bird's_nest_fern.n.01", "name": "bird's_nest_fern"}, + {"id": 21693, "synset": "ebony_spleenwort.n.01", "name": "ebony_spleenwort"}, + {"id": 21694, "synset": "black-stem_spleenwort.n.01", "name": "black-stem_spleenwort"}, + {"id": 21695, "synset": "walking_fern.n.01", "name": "walking_fern"}, + {"id": 21696, "synset": "green_spleenwort.n.01", "name": "green_spleenwort"}, + {"id": 21697, "synset": "mountain_spleenwort.n.01", "name": "mountain_spleenwort"}, + {"id": 21698, "synset": "lobed_spleenwort.n.01", "name": "lobed_spleenwort"}, + {"id": 21699, "synset": "lanceolate_spleenwort.n.01", "name": "lanceolate_spleenwort"}, + {"id": 21700, "synset": "hart's-tongue.n.02", "name": "hart's-tongue"}, + {"id": 21701, "synset": "scale_fern.n.01", "name": "scale_fern"}, + {"id": 21702, "synset": "scolopendrium.n.01", "name": "scolopendrium"}, + {"id": 21703, "synset": "deer_fern.n.01", "name": "deer_fern"}, + {"id": 21704, "synset": "doodia.n.01", "name": "doodia"}, + {"id": 21705, "synset": "chain_fern.n.01", "name": "chain_fern"}, + {"id": 21706, "synset": "virginia_chain_fern.n.01", "name": "Virginia_chain_fern"}, + {"id": 21707, "synset": "silver_tree_fern.n.01", "name": "silver_tree_fern"}, + {"id": 21708, "synset": "davallia.n.01", "name": "davallia"}, + {"id": 21709, "synset": "hare's-foot_fern.n.01", "name": "hare's-foot_fern"}, + { + "id": 21710, + "synset": "canary_island_hare's_foot_fern.n.01", + "name": "Canary_Island_hare's_foot_fern", + }, + {"id": 21711, "synset": "squirrel's-foot_fern.n.01", "name": "squirrel's-foot_fern"}, + {"id": 21712, "synset": "bracken.n.01", "name": "bracken"}, + {"id": 21713, "synset": "soft_tree_fern.n.01", "name": "soft_tree_fern"}, + {"id": 21714, "synset": "scythian_lamb.n.01", "name": "Scythian_lamb"}, + {"id": 21715, "synset": "false_bracken.n.01", "name": "false_bracken"}, + {"id": 21716, "synset": "thyrsopteris.n.01", "name": "thyrsopteris"}, + {"id": 21717, "synset": "shield_fern.n.01", "name": "shield_fern"}, + {"id": 21718, "synset": "broad_buckler-fern.n.01", "name": "broad_buckler-fern"}, + {"id": 21719, "synset": "fragrant_cliff_fern.n.01", "name": "fragrant_cliff_fern"}, + {"id": 21720, "synset": "goldie's_fern.n.01", "name": "Goldie's_fern"}, + {"id": 21721, "synset": "wood_fern.n.01", "name": "wood_fern"}, + {"id": 21722, "synset": "male_fern.n.01", "name": "male_fern"}, + {"id": 21723, "synset": "marginal_wood_fern.n.01", "name": "marginal_wood_fern"}, + {"id": 21724, "synset": "mountain_male_fern.n.01", "name": "mountain_male_fern"}, + {"id": 21725, "synset": "lady_fern.n.01", "name": "lady_fern"}, + {"id": 21726, "synset": "alpine_lady_fern.n.01", "name": "Alpine_lady_fern"}, + {"id": 21727, "synset": "silvery_spleenwort.n.02", "name": "silvery_spleenwort"}, + {"id": 21728, "synset": "holly_fern.n.02", "name": "holly_fern"}, + {"id": 21729, "synset": "bladder_fern.n.01", "name": "bladder_fern"}, + {"id": 21730, "synset": "brittle_bladder_fern.n.01", "name": "brittle_bladder_fern"}, + {"id": 21731, "synset": "mountain_bladder_fern.n.01", "name": "mountain_bladder_fern"}, + {"id": 21732, "synset": "bulblet_fern.n.01", "name": "bulblet_fern"}, + {"id": 21733, "synset": "silvery_spleenwort.n.01", "name": "silvery_spleenwort"}, + {"id": 21734, "synset": "oak_fern.n.01", "name": "oak_fern"}, + {"id": 21735, "synset": "limestone_fern.n.01", "name": "limestone_fern"}, + {"id": 21736, "synset": "ostrich_fern.n.01", "name": "ostrich_fern"}, + {"id": 21737, "synset": "hart's-tongue.n.01", "name": "hart's-tongue"}, + {"id": 21738, "synset": "sensitive_fern.n.01", "name": "sensitive_fern"}, + {"id": 21739, "synset": "christmas_fern.n.01", "name": "Christmas_fern"}, + {"id": 21740, "synset": "holly_fern.n.01", "name": "holly_fern"}, + {"id": 21741, "synset": "braun's_holly_fern.n.01", "name": "Braun's_holly_fern"}, + {"id": 21742, "synset": "western_holly_fern.n.01", "name": "western_holly_fern"}, + {"id": 21743, "synset": "soft_shield_fern.n.01", "name": "soft_shield_fern"}, + {"id": 21744, "synset": "leather_fern.n.02", "name": "leather_fern"}, + {"id": 21745, "synset": "button_fern.n.02", "name": "button_fern"}, + {"id": 21746, "synset": "indian_button_fern.n.01", "name": "Indian_button_fern"}, + {"id": 21747, "synset": "woodsia.n.01", "name": "woodsia"}, + {"id": 21748, "synset": "rusty_woodsia.n.01", "name": "rusty_woodsia"}, + {"id": 21749, "synset": "alpine_woodsia.n.01", "name": "Alpine_woodsia"}, + {"id": 21750, "synset": "smooth_woodsia.n.01", "name": "smooth_woodsia"}, + {"id": 21751, "synset": "boston_fern.n.01", "name": "Boston_fern"}, + {"id": 21752, "synset": "basket_fern.n.01", "name": "basket_fern"}, + {"id": 21753, "synset": "golden_fern.n.02", "name": "golden_fern"}, + {"id": 21754, "synset": "maidenhair.n.01", "name": "maidenhair"}, + {"id": 21755, "synset": "common_maidenhair.n.01", "name": "common_maidenhair"}, + {"id": 21756, "synset": "american_maidenhair_fern.n.01", "name": "American_maidenhair_fern"}, + {"id": 21757, "synset": "bermuda_maidenhair.n.01", "name": "Bermuda_maidenhair"}, + {"id": 21758, "synset": "brittle_maidenhair.n.01", "name": "brittle_maidenhair"}, + {"id": 21759, "synset": "farley_maidenhair.n.01", "name": "Farley_maidenhair"}, + {"id": 21760, "synset": "annual_fern.n.01", "name": "annual_fern"}, + {"id": 21761, "synset": "lip_fern.n.01", "name": "lip_fern"}, + {"id": 21762, "synset": "smooth_lip_fern.n.01", "name": "smooth_lip_fern"}, + {"id": 21763, "synset": "lace_fern.n.01", "name": "lace_fern"}, + {"id": 21764, "synset": "wooly_lip_fern.n.01", "name": "wooly_lip_fern"}, + {"id": 21765, "synset": "southwestern_lip_fern.n.01", "name": "southwestern_lip_fern"}, + {"id": 21766, "synset": "bamboo_fern.n.01", "name": "bamboo_fern"}, + {"id": 21767, "synset": "american_rock_brake.n.01", "name": "American_rock_brake"}, + {"id": 21768, "synset": "european_parsley_fern.n.01", "name": "European_parsley_fern"}, + {"id": 21769, "synset": "hand_fern.n.01", "name": "hand_fern"}, + {"id": 21770, "synset": "cliff_brake.n.01", "name": "cliff_brake"}, + {"id": 21771, "synset": "coffee_fern.n.01", "name": "coffee_fern"}, + {"id": 21772, "synset": "purple_rock_brake.n.01", "name": "purple_rock_brake"}, + {"id": 21773, "synset": "bird's-foot_fern.n.01", "name": "bird's-foot_fern"}, + {"id": 21774, "synset": "button_fern.n.01", "name": "button_fern"}, + {"id": 21775, "synset": "silver_fern.n.02", "name": "silver_fern"}, + {"id": 21776, "synset": "golden_fern.n.01", "name": "golden_fern"}, + {"id": 21777, "synset": "gold_fern.n.01", "name": "gold_fern"}, + {"id": 21778, "synset": "pteris_cretica.n.01", "name": "Pteris_cretica"}, + {"id": 21779, "synset": "spider_brake.n.01", "name": "spider_brake"}, + {"id": 21780, "synset": "ribbon_fern.n.01", "name": "ribbon_fern"}, + {"id": 21781, "synset": "potato_fern.n.01", "name": "potato_fern"}, + {"id": 21782, "synset": "angiopteris.n.01", "name": "angiopteris"}, + {"id": 21783, "synset": "skeleton_fork_fern.n.01", "name": "skeleton_fork_fern"}, + {"id": 21784, "synset": "horsetail.n.01", "name": "horsetail"}, + {"id": 21785, "synset": "common_horsetail.n.01", "name": "common_horsetail"}, + {"id": 21786, "synset": "swamp_horsetail.n.01", "name": "swamp_horsetail"}, + {"id": 21787, "synset": "scouring_rush.n.01", "name": "scouring_rush"}, + {"id": 21788, "synset": "marsh_horsetail.n.01", "name": "marsh_horsetail"}, + {"id": 21789, "synset": "wood_horsetail.n.01", "name": "wood_horsetail"}, + {"id": 21790, "synset": "variegated_horsetail.n.01", "name": "variegated_horsetail"}, + {"id": 21791, "synset": "club_moss.n.01", "name": "club_moss"}, + {"id": 21792, "synset": "shining_clubmoss.n.01", "name": "shining_clubmoss"}, + {"id": 21793, "synset": "alpine_clubmoss.n.01", "name": "alpine_clubmoss"}, + {"id": 21794, "synset": "fir_clubmoss.n.01", "name": "fir_clubmoss"}, + {"id": 21795, "synset": "ground_cedar.n.01", "name": "ground_cedar"}, + {"id": 21796, "synset": "ground_fir.n.01", "name": "ground_fir"}, + {"id": 21797, "synset": "foxtail_grass.n.01", "name": "foxtail_grass"}, + {"id": 21798, "synset": "spikemoss.n.01", "name": "spikemoss"}, + {"id": 21799, "synset": "meadow_spikemoss.n.01", "name": "meadow_spikemoss"}, + {"id": 21800, "synset": "desert_selaginella.n.01", "name": "desert_selaginella"}, + {"id": 21801, "synset": "resurrection_plant.n.01", "name": "resurrection_plant"}, + {"id": 21802, "synset": "florida_selaginella.n.01", "name": "florida_selaginella"}, + {"id": 21803, "synset": "quillwort.n.01", "name": "quillwort"}, + {"id": 21804, "synset": "earthtongue.n.01", "name": "earthtongue"}, + {"id": 21805, "synset": "snuffbox_fern.n.01", "name": "snuffbox_fern"}, + {"id": 21806, "synset": "christella.n.01", "name": "christella"}, + {"id": 21807, "synset": "mountain_fern.n.01", "name": "mountain_fern"}, + {"id": 21808, "synset": "new_york_fern.n.01", "name": "New_York_fern"}, + {"id": 21809, "synset": "massachusetts_fern.n.01", "name": "Massachusetts_fern"}, + {"id": 21810, "synset": "beech_fern.n.01", "name": "beech_fern"}, + {"id": 21811, "synset": "broad_beech_fern.n.01", "name": "broad_beech_fern"}, + {"id": 21812, "synset": "long_beech_fern.n.01", "name": "long_beech_fern"}, + {"id": 21813, "synset": "shoestring_fungus.n.01", "name": "shoestring_fungus"}, + {"id": 21814, "synset": "armillaria_caligata.n.01", "name": "Armillaria_caligata"}, + {"id": 21815, "synset": "armillaria_ponderosa.n.01", "name": "Armillaria_ponderosa"}, + {"id": 21816, "synset": "armillaria_zelleri.n.01", "name": "Armillaria_zelleri"}, + {"id": 21817, "synset": "honey_mushroom.n.01", "name": "honey_mushroom"}, + {"id": 21818, "synset": "milkweed.n.01", "name": "milkweed"}, + {"id": 21819, "synset": "white_milkweed.n.01", "name": "white_milkweed"}, + {"id": 21820, "synset": "poke_milkweed.n.01", "name": "poke_milkweed"}, + {"id": 21821, "synset": "swamp_milkweed.n.01", "name": "swamp_milkweed"}, + {"id": 21822, "synset": "mead's_milkweed.n.01", "name": "Mead's_milkweed"}, + {"id": 21823, "synset": "purple_silkweed.n.01", "name": "purple_silkweed"}, + {"id": 21824, "synset": "showy_milkweed.n.01", "name": "showy_milkweed"}, + {"id": 21825, "synset": "poison_milkweed.n.01", "name": "poison_milkweed"}, + {"id": 21826, "synset": "butterfly_weed.n.01", "name": "butterfly_weed"}, + {"id": 21827, "synset": "whorled_milkweed.n.01", "name": "whorled_milkweed"}, + {"id": 21828, "synset": "cruel_plant.n.01", "name": "cruel_plant"}, + {"id": 21829, "synset": "wax_plant.n.01", "name": "wax_plant"}, + {"id": 21830, "synset": "silk_vine.n.01", "name": "silk_vine"}, + {"id": 21831, "synset": "stapelia.n.01", "name": "stapelia"}, + {"id": 21832, "synset": "stapelias_asterias.n.01", "name": "Stapelias_asterias"}, + {"id": 21833, "synset": "stephanotis.n.01", "name": "stephanotis"}, + {"id": 21834, "synset": "madagascar_jasmine.n.01", "name": "Madagascar_jasmine"}, + {"id": 21835, "synset": "negro_vine.n.01", "name": "negro_vine"}, + {"id": 21836, "synset": "zygospore.n.01", "name": "zygospore"}, + {"id": 21837, "synset": "tree_of_knowledge.n.01", "name": "tree_of_knowledge"}, + {"id": 21838, "synset": "orangery.n.01", "name": "orangery"}, + {"id": 21839, "synset": "pocketbook.n.01", "name": "pocketbook"}, + {"id": 21840, "synset": "shit.n.04", "name": "shit"}, + {"id": 21841, "synset": "cordage.n.01", "name": "cordage"}, + {"id": 21842, "synset": "yard.n.01", "name": "yard"}, + {"id": 21843, "synset": "extremum.n.02", "name": "extremum"}, + {"id": 21844, "synset": "leaf_shape.n.01", "name": "leaf_shape"}, + {"id": 21845, "synset": "equilateral.n.01", "name": "equilateral"}, + {"id": 21846, "synset": "figure.n.06", "name": "figure"}, + {"id": 21847, "synset": "pencil.n.03", "name": "pencil"}, + {"id": 21848, "synset": "plane_figure.n.01", "name": "plane_figure"}, + {"id": 21849, "synset": "solid_figure.n.01", "name": "solid_figure"}, + {"id": 21850, "synset": "line.n.04", "name": "line"}, + {"id": 21851, "synset": "bulb.n.04", "name": "bulb"}, + {"id": 21852, "synset": "convex_shape.n.01", "name": "convex_shape"}, + {"id": 21853, "synset": "concave_shape.n.01", "name": "concave_shape"}, + {"id": 21854, "synset": "cylinder.n.01", "name": "cylinder"}, + {"id": 21855, "synset": "round_shape.n.01", "name": "round_shape"}, + {"id": 21856, "synset": "heart.n.07", "name": "heart"}, + {"id": 21857, "synset": "polygon.n.01", "name": "polygon"}, + {"id": 21858, "synset": "convex_polygon.n.01", "name": "convex_polygon"}, + {"id": 21859, "synset": "concave_polygon.n.01", "name": "concave_polygon"}, + {"id": 21860, "synset": "reentrant_polygon.n.01", "name": "reentrant_polygon"}, + {"id": 21861, "synset": "amorphous_shape.n.01", "name": "amorphous_shape"}, + {"id": 21862, "synset": "closed_curve.n.01", "name": "closed_curve"}, + {"id": 21863, "synset": "simple_closed_curve.n.01", "name": "simple_closed_curve"}, + {"id": 21864, "synset": "s-shape.n.01", "name": "S-shape"}, + {"id": 21865, "synset": "wave.n.07", "name": "wave"}, + {"id": 21866, "synset": "extrados.n.01", "name": "extrados"}, + {"id": 21867, "synset": "hook.n.02", "name": "hook"}, + {"id": 21868, "synset": "envelope.n.03", "name": "envelope"}, + {"id": 21869, "synset": "bight.n.02", "name": "bight"}, + {"id": 21870, "synset": "diameter.n.02", "name": "diameter"}, + {"id": 21871, "synset": "cone.n.02", "name": "cone"}, + {"id": 21872, "synset": "funnel.n.01", "name": "funnel"}, + {"id": 21873, "synset": "oblong.n.01", "name": "oblong"}, + {"id": 21874, "synset": "circle.n.01", "name": "circle"}, + {"id": 21875, "synset": "circle.n.03", "name": "circle"}, + {"id": 21876, "synset": "equator.n.02", "name": "equator"}, + {"id": 21877, "synset": "scallop.n.01", "name": "scallop"}, + {"id": 21878, "synset": "ring.n.02", "name": "ring"}, + {"id": 21879, "synset": "loop.n.02", "name": "loop"}, + {"id": 21880, "synset": "bight.n.01", "name": "bight"}, + {"id": 21881, "synset": "helix.n.01", "name": "helix"}, + {"id": 21882, "synset": "element_of_a_cone.n.01", "name": "element_of_a_cone"}, + {"id": 21883, "synset": "element_of_a_cylinder.n.01", "name": "element_of_a_cylinder"}, + {"id": 21884, "synset": "ellipse.n.01", "name": "ellipse"}, + {"id": 21885, "synset": "quadrate.n.02", "name": "quadrate"}, + {"id": 21886, "synset": "triangle.n.01", "name": "triangle"}, + {"id": 21887, "synset": "acute_triangle.n.01", "name": "acute_triangle"}, + {"id": 21888, "synset": "isosceles_triangle.n.01", "name": "isosceles_triangle"}, + {"id": 21889, "synset": "obtuse_triangle.n.01", "name": "obtuse_triangle"}, + {"id": 21890, "synset": "right_triangle.n.01", "name": "right_triangle"}, + {"id": 21891, "synset": "scalene_triangle.n.01", "name": "scalene_triangle"}, + {"id": 21892, "synset": "parallel.n.03", "name": "parallel"}, + {"id": 21893, "synset": "trapezoid.n.01", "name": "trapezoid"}, + {"id": 21894, "synset": "star.n.05", "name": "star"}, + {"id": 21895, "synset": "pentagon.n.03", "name": "pentagon"}, + {"id": 21896, "synset": "hexagon.n.01", "name": "hexagon"}, + {"id": 21897, "synset": "heptagon.n.01", "name": "heptagon"}, + {"id": 21898, "synset": "octagon.n.01", "name": "octagon"}, + {"id": 21899, "synset": "nonagon.n.01", "name": "nonagon"}, + {"id": 21900, "synset": "decagon.n.01", "name": "decagon"}, + {"id": 21901, "synset": "rhombus.n.01", "name": "rhombus"}, + {"id": 21902, "synset": "spherical_polygon.n.01", "name": "spherical_polygon"}, + {"id": 21903, "synset": "spherical_triangle.n.01", "name": "spherical_triangle"}, + {"id": 21904, "synset": "convex_polyhedron.n.01", "name": "convex_polyhedron"}, + {"id": 21905, "synset": "concave_polyhedron.n.01", "name": "concave_polyhedron"}, + {"id": 21906, "synset": "cuboid.n.01", "name": "cuboid"}, + {"id": 21907, "synset": "quadrangular_prism.n.01", "name": "quadrangular_prism"}, + {"id": 21908, "synset": "bell.n.05", "name": "bell"}, + {"id": 21909, "synset": "angular_distance.n.01", "name": "angular_distance"}, + {"id": 21910, "synset": "true_anomaly.n.01", "name": "true_anomaly"}, + {"id": 21911, "synset": "spherical_angle.n.01", "name": "spherical_angle"}, + {"id": 21912, "synset": "angle_of_refraction.n.01", "name": "angle_of_refraction"}, + {"id": 21913, "synset": "acute_angle.n.01", "name": "acute_angle"}, + {"id": 21914, "synset": "groove.n.01", "name": "groove"}, + {"id": 21915, "synset": "rut.n.01", "name": "rut"}, + {"id": 21916, "synset": "bulge.n.01", "name": "bulge"}, + {"id": 21917, "synset": "belly.n.03", "name": "belly"}, + {"id": 21918, "synset": "bow.n.05", "name": "bow"}, + {"id": 21919, "synset": "crescent.n.01", "name": "crescent"}, + {"id": 21920, "synset": "ellipsoid.n.01", "name": "ellipsoid"}, + {"id": 21921, "synset": "hypotenuse.n.01", "name": "hypotenuse"}, + {"id": 21922, "synset": "balance.n.04", "name": "balance"}, + {"id": 21923, "synset": "conformation.n.01", "name": "conformation"}, + {"id": 21924, "synset": "symmetry.n.02", "name": "symmetry"}, + {"id": 21925, "synset": "spheroid.n.01", "name": "spheroid"}, + {"id": 21926, "synset": "spherule.n.01", "name": "spherule"}, + {"id": 21927, "synset": "toroid.n.01", "name": "toroid"}, + {"id": 21928, "synset": "column.n.04", "name": "column"}, + {"id": 21929, "synset": "barrel.n.03", "name": "barrel"}, + {"id": 21930, "synset": "pipe.n.03", "name": "pipe"}, + {"id": 21931, "synset": "pellet.n.01", "name": "pellet"}, + {"id": 21932, "synset": "bolus.n.01", "name": "bolus"}, + {"id": 21933, "synset": "dewdrop.n.01", "name": "dewdrop"}, + {"id": 21934, "synset": "ridge.n.02", "name": "ridge"}, + {"id": 21935, "synset": "rim.n.01", "name": "rim"}, + {"id": 21936, "synset": "taper.n.01", "name": "taper"}, + {"id": 21937, "synset": "boundary.n.02", "name": "boundary"}, + {"id": 21938, "synset": "incisure.n.01", "name": "incisure"}, + {"id": 21939, "synset": "notch.n.01", "name": "notch"}, + {"id": 21940, "synset": "wrinkle.n.01", "name": "wrinkle"}, + {"id": 21941, "synset": "dermatoglyphic.n.01", "name": "dermatoglyphic"}, + {"id": 21942, "synset": "frown_line.n.01", "name": "frown_line"}, + {"id": 21943, "synset": "line_of_life.n.01", "name": "line_of_life"}, + {"id": 21944, "synset": "line_of_heart.n.01", "name": "line_of_heart"}, + {"id": 21945, "synset": "crevice.n.01", "name": "crevice"}, + {"id": 21946, "synset": "cleft.n.01", "name": "cleft"}, + {"id": 21947, "synset": "roulette.n.01", "name": "roulette"}, + {"id": 21948, "synset": "node.n.01", "name": "node"}, + {"id": 21949, "synset": "tree.n.02", "name": "tree"}, + {"id": 21950, "synset": "stemma.n.01", "name": "stemma"}, + {"id": 21951, "synset": "brachium.n.01", "name": "brachium"}, + {"id": 21952, "synset": "fork.n.03", "name": "fork"}, + {"id": 21953, "synset": "block.n.03", "name": "block"}, + {"id": 21954, "synset": "ovoid.n.01", "name": "ovoid"}, + {"id": 21955, "synset": "tetrahedron.n.01", "name": "tetrahedron"}, + {"id": 21956, "synset": "pentahedron.n.01", "name": "pentahedron"}, + {"id": 21957, "synset": "hexahedron.n.01", "name": "hexahedron"}, + {"id": 21958, "synset": "regular_polyhedron.n.01", "name": "regular_polyhedron"}, + {"id": 21959, "synset": "polyhedral_angle.n.01", "name": "polyhedral_angle"}, + {"id": 21960, "synset": "cube.n.01", "name": "cube"}, + {"id": 21961, "synset": "truncated_pyramid.n.01", "name": "truncated_pyramid"}, + {"id": 21962, "synset": "truncated_cone.n.01", "name": "truncated_cone"}, + {"id": 21963, "synset": "tail.n.03", "name": "tail"}, + {"id": 21964, "synset": "tongue.n.03", "name": "tongue"}, + {"id": 21965, "synset": "trapezohedron.n.01", "name": "trapezohedron"}, + {"id": 21966, "synset": "wedge.n.01", "name": "wedge"}, + {"id": 21967, "synset": "keel.n.01", "name": "keel"}, + {"id": 21968, "synset": "place.n.06", "name": "place"}, + {"id": 21969, "synset": "herpes.n.01", "name": "herpes"}, + {"id": 21970, "synset": "chlamydia.n.01", "name": "chlamydia"}, + {"id": 21971, "synset": "wall.n.04", "name": "wall"}, + {"id": 21972, "synset": "micronutrient.n.01", "name": "micronutrient"}, + {"id": 21973, "synset": "chyme.n.01", "name": "chyme"}, + {"id": 21974, "synset": "ragweed_pollen.n.01", "name": "ragweed_pollen"}, + {"id": 21975, "synset": "pina_cloth.n.01", "name": "pina_cloth"}, + { + "id": 21976, + "synset": "chlorobenzylidenemalononitrile.n.01", + "name": "chlorobenzylidenemalononitrile", + }, + {"id": 21977, "synset": "carbon.n.01", "name": "carbon"}, + {"id": 21978, "synset": "charcoal.n.01", "name": "charcoal"}, + {"id": 21979, "synset": "rock.n.02", "name": "rock"}, + {"id": 21980, "synset": "gravel.n.01", "name": "gravel"}, + {"id": 21981, "synset": "aflatoxin.n.01", "name": "aflatoxin"}, + {"id": 21982, "synset": "alpha-tocopheral.n.01", "name": "alpha-tocopheral"}, + {"id": 21983, "synset": "leopard.n.01", "name": "leopard"}, + {"id": 21984, "synset": "bricks_and_mortar.n.01", "name": "bricks_and_mortar"}, + {"id": 21985, "synset": "lagging.n.01", "name": "lagging"}, + {"id": 21986, "synset": "hydraulic_cement.n.01", "name": "hydraulic_cement"}, + {"id": 21987, "synset": "choline.n.01", "name": "choline"}, + {"id": 21988, "synset": "concrete.n.01", "name": "concrete"}, + {"id": 21989, "synset": "glass_wool.n.01", "name": "glass_wool"}, + {"id": 21990, "synset": "soil.n.02", "name": "soil"}, + {"id": 21991, "synset": "high_explosive.n.01", "name": "high_explosive"}, + {"id": 21992, "synset": "litter.n.02", "name": "litter"}, + {"id": 21993, "synset": "fish_meal.n.01", "name": "fish_meal"}, + {"id": 21994, "synset": "greek_fire.n.01", "name": "Greek_fire"}, + {"id": 21995, "synset": "culture_medium.n.01", "name": "culture_medium"}, + {"id": 21996, "synset": "agar.n.01", "name": "agar"}, + {"id": 21997, "synset": "blood_agar.n.01", "name": "blood_agar"}, + {"id": 21998, "synset": "hip_tile.n.01", "name": "hip_tile"}, + {"id": 21999, "synset": "hyacinth.n.01", "name": "hyacinth"}, + {"id": 22000, "synset": "hydroxide_ion.n.01", "name": "hydroxide_ion"}, + {"id": 22001, "synset": "ice.n.01", "name": "ice"}, + {"id": 22002, "synset": "inositol.n.01", "name": "inositol"}, + {"id": 22003, "synset": "linoleum.n.01", "name": "linoleum"}, + {"id": 22004, "synset": "lithia_water.n.01", "name": "lithia_water"}, + {"id": 22005, "synset": "lodestone.n.01", "name": "lodestone"}, + {"id": 22006, "synset": "pantothenic_acid.n.01", "name": "pantothenic_acid"}, + {"id": 22007, "synset": "paper.n.01", "name": "paper"}, + {"id": 22008, "synset": "papyrus.n.01", "name": "papyrus"}, + {"id": 22009, "synset": "pantile.n.01", "name": "pantile"}, + {"id": 22010, "synset": "blacktop.n.01", "name": "blacktop"}, + {"id": 22011, "synset": "tarmacadam.n.01", "name": "tarmacadam"}, + {"id": 22012, "synset": "paving.n.01", "name": "paving"}, + {"id": 22013, "synset": "plaster.n.01", "name": "plaster"}, + {"id": 22014, "synset": "poison_gas.n.01", "name": "poison_gas"}, + {"id": 22015, "synset": "ridge_tile.n.01", "name": "ridge_tile"}, + {"id": 22016, "synset": "roughcast.n.01", "name": "roughcast"}, + {"id": 22017, "synset": "sand.n.01", "name": "sand"}, + {"id": 22018, "synset": "spackle.n.01", "name": "spackle"}, + {"id": 22019, "synset": "render.n.01", "name": "render"}, + {"id": 22020, "synset": "wattle_and_daub.n.01", "name": "wattle_and_daub"}, + {"id": 22021, "synset": "stucco.n.01", "name": "stucco"}, + {"id": 22022, "synset": "tear_gas.n.01", "name": "tear_gas"}, + {"id": 22023, "synset": "linseed.n.01", "name": "linseed"}, + {"id": 22024, "synset": "vitamin.n.01", "name": "vitamin"}, + {"id": 22025, "synset": "fat-soluble_vitamin.n.01", "name": "fat-soluble_vitamin"}, + {"id": 22026, "synset": "water-soluble_vitamin.n.01", "name": "water-soluble_vitamin"}, + {"id": 22027, "synset": "vitamin_a.n.01", "name": "vitamin_A"}, + {"id": 22028, "synset": "vitamin_a1.n.01", "name": "vitamin_A1"}, + {"id": 22029, "synset": "vitamin_a2.n.01", "name": "vitamin_A2"}, + {"id": 22030, "synset": "b-complex_vitamin.n.01", "name": "B-complex_vitamin"}, + {"id": 22031, "synset": "vitamin_b1.n.01", "name": "vitamin_B1"}, + {"id": 22032, "synset": "vitamin_b12.n.01", "name": "vitamin_B12"}, + {"id": 22033, "synset": "vitamin_b2.n.01", "name": "vitamin_B2"}, + {"id": 22034, "synset": "vitamin_b6.n.01", "name": "vitamin_B6"}, + {"id": 22035, "synset": "vitamin_bc.n.01", "name": "vitamin_Bc"}, + {"id": 22036, "synset": "niacin.n.01", "name": "niacin"}, + {"id": 22037, "synset": "vitamin_d.n.01", "name": "vitamin_D"}, + {"id": 22038, "synset": "vitamin_e.n.01", "name": "vitamin_E"}, + {"id": 22039, "synset": "biotin.n.01", "name": "biotin"}, + {"id": 22040, "synset": "vitamin_k.n.01", "name": "vitamin_K"}, + {"id": 22041, "synset": "vitamin_k1.n.01", "name": "vitamin_K1"}, + {"id": 22042, "synset": "vitamin_k3.n.01", "name": "vitamin_K3"}, + {"id": 22043, "synset": "vitamin_p.n.01", "name": "vitamin_P"}, + {"id": 22044, "synset": "vitamin_c.n.01", "name": "vitamin_C"}, + {"id": 22045, "synset": "planking.n.01", "name": "planking"}, + {"id": 22046, "synset": "chipboard.n.01", "name": "chipboard"}, + {"id": 22047, "synset": "knothole.n.01", "name": "knothole"}, +] diff --git a/dimos/models/Detic/detic/data/datasets/lvis_v1.py b/dimos/models/Detic/detic/data/datasets/lvis_v1.py index 4b9b279f17..659a5fbbc0 100644 --- a/dimos/models/Detic/detic/data/datasets/lvis_v1.py +++ b/dimos/models/Detic/detic/data/datasets/lvis_v1.py @@ -2,35 +2,33 @@ import logging import os -from fvcore.common.timer import Timer -from detectron2.structures import BoxMode -from fvcore.common.file_io import PathManager from detectron2.data import DatasetCatalog, MetadataCatalog from detectron2.data.datasets.lvis import get_lvis_instances_meta +from detectron2.structures import BoxMode +from fvcore.common.file_io import PathManager +from fvcore.common.timer import Timer +from typing import Optional logger = logging.getLogger(__name__) __all__ = ["custom_load_lvis_json", "custom_register_lvis_instances"] -def custom_register_lvis_instances(name, metadata, json_file, image_root): - """ - """ - DatasetCatalog.register(name, lambda: custom_load_lvis_json( - json_file, image_root, name)) +def custom_register_lvis_instances(name: str, metadata, json_file, image_root) -> None: + """ """ + DatasetCatalog.register(name, lambda: custom_load_lvis_json(json_file, image_root, name)) MetadataCatalog.get(name).set( - json_file=json_file, image_root=image_root, - evaluator_type="lvis", **metadata + json_file=json_file, image_root=image_root, evaluator_type="lvis", **metadata ) -def custom_load_lvis_json(json_file, image_root, dataset_name=None): - ''' +def custom_load_lvis_json(json_file, image_root, dataset_name: Optional[str]=None): + """ Modifications: use `file_name` convert neg_category_ids add pos_category_ids - ''' + """ from lvis import LVIS json_file = PathManager.get_local_path(json_file) @@ -38,75 +36,72 @@ def custom_load_lvis_json(json_file, image_root, dataset_name=None): timer = Timer() lvis_api = LVIS(json_file) if timer.seconds() > 1: - logger.info("Loading {} takes {:.2f} seconds.".format( - json_file, timer.seconds())) - - catid2contid = {x['id']: i for i, x in enumerate( - sorted(lvis_api.dataset['categories'], key=lambda x: x['id']))} - if len(lvis_api.dataset['categories']) == 1203: - for x in lvis_api.dataset['categories']: - assert catid2contid[x['id']] == x['id'] - 1 + logger.info(f"Loading {json_file} takes {timer.seconds():.2f} seconds.") + + catid2contid = { + x["id"]: i + for i, x in enumerate(sorted(lvis_api.dataset["categories"], key=lambda x: x["id"])) + } + if len(lvis_api.dataset["categories"]) == 1203: + for x in lvis_api.dataset["categories"]: + assert catid2contid[x["id"]] == x["id"] - 1 img_ids = sorted(lvis_api.imgs.keys()) imgs = lvis_api.load_imgs(img_ids) anns = [lvis_api.img_ann_map[img_id] for img_id in img_ids] ann_ids = [ann["id"] for anns_per_image in anns for ann in anns_per_image] - assert len(set(ann_ids)) == len(ann_ids), \ - "Annotation ids in '{}' are not unique".format(json_file) + assert len(set(ann_ids)) == len(ann_ids), f"Annotation ids in '{json_file}' are not unique" - imgs_anns = list(zip(imgs, anns)) - logger.info("Loaded {} images in the LVIS v1 format from {}".format( - len(imgs_anns), json_file)) + imgs_anns = list(zip(imgs, anns, strict=False)) + logger.info(f"Loaded {len(imgs_anns)} images in the LVIS v1 format from {json_file}") dataset_dicts = [] - for (img_dict, anno_dict_list) in imgs_anns: + for img_dict, anno_dict_list in imgs_anns: record = {} if "file_name" in img_dict: file_name = img_dict["file_name"] if img_dict["file_name"].startswith("COCO"): file_name = file_name[-16:] record["file_name"] = os.path.join(image_root, file_name) - elif 'coco_url' in img_dict: + elif "coco_url" in img_dict: # e.g., http://images.cocodataset.org/train2017/000000391895.jpg file_name = img_dict["coco_url"][30:] record["file_name"] = os.path.join(image_root, file_name) - elif 'tar_index' in img_dict: - record['tar_index'] = img_dict['tar_index'] - + elif "tar_index" in img_dict: + record["tar_index"] = img_dict["tar_index"] + record["height"] = img_dict["height"] record["width"] = img_dict["width"] - record["not_exhaustive_category_ids"] = img_dict.get( - "not_exhaustive_category_ids", []) + record["not_exhaustive_category_ids"] = img_dict.get("not_exhaustive_category_ids", []) record["neg_category_ids"] = img_dict.get("neg_category_ids", []) # NOTE: modified by Xingyi: convert to 0-based - record["neg_category_ids"] = [ - catid2contid[x] for x in record["neg_category_ids"]] - if 'pos_category_ids' in img_dict: - record['pos_category_ids'] = [ - catid2contid[x] for x in img_dict.get("pos_category_ids", [])] - if 'captions' in img_dict: - record['captions'] = img_dict['captions'] - if 'caption_features' in img_dict: - record['caption_features'] = img_dict['caption_features'] + record["neg_category_ids"] = [catid2contid[x] for x in record["neg_category_ids"]] + if "pos_category_ids" in img_dict: + record["pos_category_ids"] = [ + catid2contid[x] for x in img_dict.get("pos_category_ids", []) + ] + if "captions" in img_dict: + record["captions"] = img_dict["captions"] + if "caption_features" in img_dict: + record["caption_features"] = img_dict["caption_features"] image_id = record["image_id"] = img_dict["id"] objs = [] for anno in anno_dict_list: assert anno["image_id"] == image_id - if anno.get('iscrowd', 0) > 0: + if anno.get("iscrowd", 0) > 0: continue obj = {"bbox": anno["bbox"], "bbox_mode": BoxMode.XYWH_ABS} - obj["category_id"] = catid2contid[anno['category_id']] - if 'segmentation' in anno: + obj["category_id"] = catid2contid[anno["category_id"]] + if "segmentation" in anno: segm = anno["segmentation"] - valid_segm = [poly for poly in segm \ - if len(poly) % 2 == 0 and len(poly) >= 6] + valid_segm = [poly for poly in segm if len(poly) % 2 == 0 and len(poly) >= 6] # assert len(segm) == len( # valid_segm # ), "Annotation contains an invalid polygon with < 3 points" if not len(segm) == len(valid_segm): - print('Annotation contains an invalid polygon with < 3 points') + print("Annotation contains an invalid polygon with < 3 points") assert len(segm) > 0 obj["segmentation"] = segm objs.append(obj) @@ -115,6 +110,7 @@ def custom_load_lvis_json(json_file, image_root, dataset_name=None): return dataset_dicts + _CUSTOM_SPLITS_LVIS = { "lvis_v1_train+coco": ("coco/", "lvis/lvis_v1_train+coco_mask.json"), "lvis_v1_train_norare": ("coco/", "lvis/lvis_v1_train_norare.json"), @@ -132,16 +128,18 @@ def custom_load_lvis_json(json_file, image_root, dataset_name=None): def get_lvis_22k_meta(): from .lvis_22k_categories import CATEGORIES + cat_ids = [k["id"] for k in CATEGORIES] - assert min(cat_ids) == 1 and max(cat_ids) == len( - cat_ids - ), "Category ids are not in [1, #categories], as expected" + assert min(cat_ids) == 1 and max(cat_ids) == len(cat_ids), ( + "Category ids are not in [1, #categories], as expected" + ) # Ensure that the category list is sorted by id lvis_categories = sorted(CATEGORIES, key=lambda x: x["id"]) thing_classes = [k["name"] for k in lvis_categories] meta = {"thing_classes": thing_classes} return meta + _CUSTOM_SPLITS_LVIS_22K = { "lvis_v1_train_22k": ("coco/", "lvis/lvis_v1_train_lvis-22k.json"), } @@ -152,4 +150,4 @@ def get_lvis_22k_meta(): get_lvis_22k_meta(), os.path.join("datasets", json_file) if "://" not in json_file else json_file, os.path.join("datasets", image_root), - ) \ No newline at end of file + ) diff --git a/dimos/models/Detic/detic/data/datasets/objects365.py b/dimos/models/Detic/detic/data/datasets/objects365.py index b98128738b..236e609287 100644 --- a/dimos/models/Detic/detic/data/datasets/objects365.py +++ b/dimos/models/Detic/detic/data/datasets/objects365.py @@ -1,7 +1,8 @@ # Copyright (c) Facebook, Inc. and its affiliates. -from detectron2.data.datasets.register_coco import register_coco_instances import os +from detectron2.data.datasets.register_coco import register_coco_instances + # categories_v2 = [ # {'id': 1, 'name': 'Person'}, # {'id': 2, 'name': 'Sneakers'}, @@ -370,395 +371,405 @@ # {'id': 365, 'name': 'Table Tennis '}, # ] -''' +""" The official Objects365 category names contains typos. Below is a manual fix. -''' +""" categories_v2_fix = [ - {'id': 1, 'name': 'Person'}, - {'id': 2, 'name': 'Sneakers'}, - {'id': 3, 'name': 'Chair'}, - {'id': 4, 'name': 'Other Shoes'}, - {'id': 5, 'name': 'Hat'}, - {'id': 6, 'name': 'Car'}, - {'id': 7, 'name': 'Lamp'}, - {'id': 8, 'name': 'Glasses'}, - {'id': 9, 'name': 'Bottle'}, - {'id': 10, 'name': 'Desk'}, - {'id': 11, 'name': 'Cup'}, - {'id': 12, 'name': 'Street Lights'}, - {'id': 13, 'name': 'Cabinet/shelf'}, - {'id': 14, 'name': 'Handbag/Satchel'}, - {'id': 15, 'name': 'Bracelet'}, - {'id': 16, 'name': 'Plate'}, - {'id': 17, 'name': 'Picture/Frame'}, - {'id': 18, 'name': 'Helmet'}, - {'id': 19, 'name': 'Book'}, - {'id': 20, 'name': 'Gloves'}, - {'id': 21, 'name': 'Storage box'}, - {'id': 22, 'name': 'Boat'}, - {'id': 23, 'name': 'Leather Shoes'}, - {'id': 24, 'name': 'Flower'}, - {'id': 25, 'name': 'Bench'}, - {'id': 26, 'name': 'Potted Plant'}, - {'id': 27, 'name': 'Bowl/Basin'}, - {'id': 28, 'name': 'Flag'}, - {'id': 29, 'name': 'Pillow'}, - {'id': 30, 'name': 'Boots'}, - {'id': 31, 'name': 'Vase'}, - {'id': 32, 'name': 'Microphone'}, - {'id': 33, 'name': 'Necklace'}, - {'id': 34, 'name': 'Ring'}, - {'id': 35, 'name': 'SUV'}, - {'id': 36, 'name': 'Wine Glass'}, - {'id': 37, 'name': 'Belt'}, - {'id': 38, 'name': 'Monitor/TV'}, - {'id': 39, 'name': 'Backpack'}, - {'id': 40, 'name': 'Umbrella'}, - {'id': 41, 'name': 'Traffic Light'}, - {'id': 42, 'name': 'Speaker'}, - {'id': 43, 'name': 'Watch'}, - {'id': 44, 'name': 'Tie'}, - {'id': 45, 'name': 'Trash bin Can'}, - {'id': 46, 'name': 'Slippers'}, - {'id': 47, 'name': 'Bicycle'}, - {'id': 48, 'name': 'Stool'}, - {'id': 49, 'name': 'Barrel/bucket'}, - {'id': 50, 'name': 'Van'}, - {'id': 51, 'name': 'Couch'}, - {'id': 52, 'name': 'Sandals'}, - {'id': 53, 'name': 'Basket'}, - {'id': 54, 'name': 'Drum'}, - {'id': 55, 'name': 'Pen/Pencil'}, - {'id': 56, 'name': 'Bus'}, - {'id': 57, 'name': 'Wild Bird'}, - {'id': 58, 'name': 'High Heels'}, - {'id': 59, 'name': 'Motorcycle'}, - {'id': 60, 'name': 'Guitar'}, - {'id': 61, 'name': 'Carpet'}, - {'id': 62, 'name': 'Cell Phone'}, - {'id': 63, 'name': 'Bread'}, - {'id': 64, 'name': 'Camera'}, - {'id': 65, 'name': 'Canned'}, - {'id': 66, 'name': 'Truck'}, - {'id': 67, 'name': 'Traffic cone'}, - {'id': 68, 'name': 'Cymbal'}, - {'id': 69, 'name': 'Lifesaver'}, - {'id': 70, 'name': 'Towel'}, - {'id': 71, 'name': 'Stuffed Toy'}, - {'id': 72, 'name': 'Candle'}, - {'id': 73, 'name': 'Sailboat'}, - {'id': 74, 'name': 'Laptop'}, - {'id': 75, 'name': 'Awning'}, - {'id': 76, 'name': 'Bed'}, - {'id': 77, 'name': 'Faucet'}, - {'id': 78, 'name': 'Tent'}, - {'id': 79, 'name': 'Horse'}, - {'id': 80, 'name': 'Mirror'}, - {'id': 81, 'name': 'Power outlet'}, - {'id': 82, 'name': 'Sink'}, - {'id': 83, 'name': 'Apple'}, - {'id': 84, 'name': 'Air Conditioner'}, - {'id': 85, 'name': 'Knife'}, - {'id': 86, 'name': 'Hockey Stick'}, - {'id': 87, 'name': 'Paddle'}, - {'id': 88, 'name': 'Pickup Truck'}, - {'id': 89, 'name': 'Fork'}, - {'id': 90, 'name': 'Traffic Sign'}, - {'id': 91, 'name': 'Ballon'}, - {'id': 92, 'name': 'Tripod'}, - {'id': 93, 'name': 'Dog'}, - {'id': 94, 'name': 'Spoon'}, - {'id': 95, 'name': 'Clock'}, - {'id': 96, 'name': 'Pot'}, - {'id': 97, 'name': 'Cow'}, - {'id': 98, 'name': 'Cake'}, - {'id': 99, 'name': 'Dining Table'}, - {'id': 100, 'name': 'Sheep'}, - {'id': 101, 'name': 'Hanger'}, - {'id': 102, 'name': 'Blackboard/Whiteboard'}, - {'id': 103, 'name': 'Napkin'}, - {'id': 104, 'name': 'Other Fish'}, - {'id': 105, 'name': 'Orange/Tangerine'}, - {'id': 106, 'name': 'Toiletry'}, - {'id': 107, 'name': 'Keyboard'}, - {'id': 108, 'name': 'Tomato'}, - {'id': 109, 'name': 'Lantern'}, - {'id': 110, 'name': 'Machinery Vehicle'}, - {'id': 111, 'name': 'Fan'}, - {'id': 112, 'name': 'Green Vegetables'}, - {'id': 113, 'name': 'Banana'}, - {'id': 114, 'name': 'Baseball Glove'}, - {'id': 115, 'name': 'Airplane'}, - {'id': 116, 'name': 'Mouse'}, - {'id': 117, 'name': 'Train'}, - {'id': 118, 'name': 'Pumpkin'}, - {'id': 119, 'name': 'Soccer'}, - {'id': 120, 'name': 'Skiboard'}, - {'id': 121, 'name': 'Luggage'}, - {'id': 122, 'name': 'Nightstand'}, - {'id': 123, 'name': 'Teapot'}, - {'id': 124, 'name': 'Telephone'}, - {'id': 125, 'name': 'Trolley'}, - {'id': 126, 'name': 'Head Phone'}, - {'id': 127, 'name': 'Sports Car'}, - {'id': 128, 'name': 'Stop Sign'}, - {'id': 129, 'name': 'Dessert'}, - {'id': 130, 'name': 'Scooter'}, - {'id': 131, 'name': 'Stroller'}, - {'id': 132, 'name': 'Crane'}, - {'id': 133, 'name': 'Remote'}, - {'id': 134, 'name': 'Refrigerator'}, - {'id': 135, 'name': 'Oven'}, - {'id': 136, 'name': 'Lemon'}, - {'id': 137, 'name': 'Duck'}, - {'id': 138, 'name': 'Baseball Bat'}, - {'id': 139, 'name': 'Surveillance Camera'}, - {'id': 140, 'name': 'Cat'}, - {'id': 141, 'name': 'Jug'}, - {'id': 142, 'name': 'Broccoli'}, - {'id': 143, 'name': 'Piano'}, - {'id': 144, 'name': 'Pizza'}, - {'id': 145, 'name': 'Elephant'}, - {'id': 146, 'name': 'Skateboard'}, - {'id': 147, 'name': 'Surfboard'}, - {'id': 148, 'name': 'Gun'}, - {'id': 149, 'name': 'Skating and Skiing shoes'}, - {'id': 150, 'name': 'Gas stove'}, - {'id': 151, 'name': 'Donut'}, - {'id': 152, 'name': 'Bow Tie'}, - {'id': 153, 'name': 'Carrot'}, - {'id': 154, 'name': 'Toilet'}, - {'id': 155, 'name': 'Kite'}, - {'id': 156, 'name': 'Strawberry'}, - {'id': 157, 'name': 'Other Balls'}, - {'id': 158, 'name': 'Shovel'}, - {'id': 159, 'name': 'Pepper'}, - {'id': 160, 'name': 'Computer Box'}, - {'id': 161, 'name': 'Toilet Paper'}, - {'id': 162, 'name': 'Cleaning Products'}, - {'id': 163, 'name': 'Chopsticks'}, - {'id': 164, 'name': 'Microwave'}, - {'id': 165, 'name': 'Pigeon'}, - {'id': 166, 'name': 'Baseball'}, - {'id': 167, 'name': 'Cutting/chopping Board'}, - {'id': 168, 'name': 'Coffee Table'}, - {'id': 169, 'name': 'Side Table'}, - {'id': 170, 'name': 'Scissors'}, - {'id': 171, 'name': 'Marker'}, - {'id': 172, 'name': 'Pie'}, - {'id': 173, 'name': 'Ladder'}, - {'id': 174, 'name': 'Snowboard'}, - {'id': 175, 'name': 'Cookies'}, - {'id': 176, 'name': 'Radiator'}, - {'id': 177, 'name': 'Fire Hydrant'}, - {'id': 178, 'name': 'Basketball'}, - {'id': 179, 'name': 'Zebra'}, - {'id': 180, 'name': 'Grape'}, - {'id': 181, 'name': 'Giraffe'}, - {'id': 182, 'name': 'Potato'}, - {'id': 183, 'name': 'Sausage'}, - {'id': 184, 'name': 'Tricycle'}, - {'id': 185, 'name': 'Violin'}, - {'id': 186, 'name': 'Egg'}, - {'id': 187, 'name': 'Fire Extinguisher'}, - {'id': 188, 'name': 'Candy'}, - {'id': 189, 'name': 'Fire Truck'}, - {'id': 190, 'name': 'Billards'}, - {'id': 191, 'name': 'Converter'}, - {'id': 192, 'name': 'Bathtub'}, - {'id': 193, 'name': 'Wheelchair'}, - {'id': 194, 'name': 'Golf Club'}, - {'id': 195, 'name': 'Briefcase'}, - {'id': 196, 'name': 'Cucumber'}, - {'id': 197, 'name': 'Cigar/Cigarette '}, - {'id': 198, 'name': 'Paint Brush'}, - {'id': 199, 'name': 'Pear'}, - {'id': 200, 'name': 'Heavy Truck'}, - {'id': 201, 'name': 'Hamburger'}, - {'id': 202, 'name': 'Extractor'}, - {'id': 203, 'name': 'Extension Cord'}, - {'id': 204, 'name': 'Tong'}, - {'id': 205, 'name': 'Tennis Racket'}, - {'id': 206, 'name': 'Folder'}, - {'id': 207, 'name': 'American Football'}, - {'id': 208, 'name': 'earphone'}, - {'id': 209, 'name': 'Mask'}, - {'id': 210, 'name': 'Kettle'}, - {'id': 211, 'name': 'Tennis'}, - {'id': 212, 'name': 'Ship'}, - {'id': 213, 'name': 'Swing'}, - {'id': 214, 'name': 'Coffee Machine'}, - {'id': 215, 'name': 'Slide'}, - {'id': 216, 'name': 'Carriage'}, - {'id': 217, 'name': 'Onion'}, - {'id': 218, 'name': 'Green beans'}, - {'id': 219, 'name': 'Projector'}, - {'id': 220, 'name': 'Frisbee'}, - {'id': 221, 'name': 'Washing Machine/Drying Machine'}, - {'id': 222, 'name': 'Chicken'}, - {'id': 223, 'name': 'Printer'}, - {'id': 224, 'name': 'Watermelon'}, - {'id': 225, 'name': 'Saxophone'}, - {'id': 226, 'name': 'Tissue'}, - {'id': 227, 'name': 'Toothbrush'}, - {'id': 228, 'name': 'Ice cream'}, - {'id': 229, 'name': 'Hot air balloon'}, - {'id': 230, 'name': 'Cello'}, - {'id': 231, 'name': 'French Fries'}, - {'id': 232, 'name': 'Scale'}, - {'id': 233, 'name': 'Trophy'}, - {'id': 234, 'name': 'Cabbage'}, - {'id': 235, 'name': 'Hot dog'}, - {'id': 236, 'name': 'Blender'}, - {'id': 237, 'name': 'Peach'}, - {'id': 238, 'name': 'Rice'}, - {'id': 239, 'name': 'Wallet/Purse'}, - {'id': 240, 'name': 'Volleyball'}, - {'id': 241, 'name': 'Deer'}, - {'id': 242, 'name': 'Goose'}, - {'id': 243, 'name': 'Tape'}, - {'id': 244, 'name': 'Tablet'}, - {'id': 245, 'name': 'Cosmetics'}, - {'id': 246, 'name': 'Trumpet'}, - {'id': 247, 'name': 'Pineapple'}, - {'id': 248, 'name': 'Golf Ball'}, - {'id': 249, 'name': 'Ambulance'}, - {'id': 250, 'name': 'Parking meter'}, - {'id': 251, 'name': 'Mango'}, - {'id': 252, 'name': 'Key'}, - {'id': 253, 'name': 'Hurdle'}, - {'id': 254, 'name': 'Fishing Rod'}, - {'id': 255, 'name': 'Medal'}, - {'id': 256, 'name': 'Flute'}, - {'id': 257, 'name': 'Brush'}, - {'id': 258, 'name': 'Penguin'}, - {'id': 259, 'name': 'Megaphone'}, - {'id': 260, 'name': 'Corn'}, - {'id': 261, 'name': 'Lettuce'}, - {'id': 262, 'name': 'Garlic'}, - {'id': 263, 'name': 'Swan'}, - {'id': 264, 'name': 'Helicopter'}, - {'id': 265, 'name': 'Green Onion'}, - {'id': 266, 'name': 'Sandwich'}, - {'id': 267, 'name': 'Nuts'}, - {'id': 268, 'name': 'Speed Limit Sign'}, - {'id': 269, 'name': 'Induction Cooker'}, - {'id': 270, 'name': 'Broom'}, - {'id': 271, 'name': 'Trombone'}, - {'id': 272, 'name': 'Plum'}, - {'id': 273, 'name': 'Rickshaw'}, - {'id': 274, 'name': 'Goldfish'}, - {'id': 275, 'name': 'Kiwi fruit'}, - {'id': 276, 'name': 'Router/modem'}, - {'id': 277, 'name': 'Poker Card'}, - {'id': 278, 'name': 'Toaster'}, - {'id': 279, 'name': 'Shrimp'}, - {'id': 280, 'name': 'Sushi'}, - {'id': 281, 'name': 'Cheese'}, - {'id': 282, 'name': 'Notepaper'}, - {'id': 283, 'name': 'Cherry'}, - {'id': 284, 'name': 'Pliers'}, - {'id': 285, 'name': 'CD'}, - {'id': 286, 'name': 'Pasta'}, - {'id': 287, 'name': 'Hammer'}, - {'id': 288, 'name': 'Cue'}, - {'id': 289, 'name': 'Avocado'}, - {'id': 290, 'name': 'Hami melon'}, - {'id': 291, 'name': 'Flask'}, - {'id': 292, 'name': 'Mushroom'}, - {'id': 293, 'name': 'Screwdriver'}, - {'id': 294, 'name': 'Soap'}, - {'id': 295, 'name': 'Recorder'}, - {'id': 296, 'name': 'Bear'}, - {'id': 297, 'name': 'Eggplant'}, - {'id': 298, 'name': 'Board Eraser'}, - {'id': 299, 'name': 'Coconut'}, - {'id': 300, 'name': 'Tape Measure/ Ruler'}, - {'id': 301, 'name': 'Pig'}, - {'id': 302, 'name': 'Showerhead'}, - {'id': 303, 'name': 'Globe'}, - {'id': 304, 'name': 'Chips'}, - {'id': 305, 'name': 'Steak'}, - {'id': 306, 'name': 'Crosswalk Sign'}, - {'id': 307, 'name': 'Stapler'}, - {'id': 308, 'name': 'Camel'}, - {'id': 309, 'name': 'Formula 1 '}, - {'id': 310, 'name': 'Pomegranate'}, - {'id': 311, 'name': 'Dishwasher'}, - {'id': 312, 'name': 'Crab'}, - {'id': 313, 'name': 'Hoverboard'}, - {'id': 314, 'name': 'Meatball'}, - {'id': 315, 'name': 'Rice Cooker'}, - {'id': 316, 'name': 'Tuba'}, - {'id': 317, 'name': 'Calculator'}, - {'id': 318, 'name': 'Papaya'}, - {'id': 319, 'name': 'Antelope'}, - {'id': 320, 'name': 'Parrot'}, - {'id': 321, 'name': 'Seal'}, - {'id': 322, 'name': 'Butterfly'}, - {'id': 323, 'name': 'Dumbbell'}, - {'id': 324, 'name': 'Donkey'}, - {'id': 325, 'name': 'Lion'}, - {'id': 326, 'name': 'Urinal'}, - {'id': 327, 'name': 'Dolphin'}, - {'id': 328, 'name': 'Electric Drill'}, - {'id': 329, 'name': 'Hair Dryer'}, - {'id': 330, 'name': 'Egg tart'}, - {'id': 331, 'name': 'Jellyfish'}, - {'id': 332, 'name': 'Treadmill'}, - {'id': 333, 'name': 'Lighter'}, - {'id': 334, 'name': 'Grapefruit'}, - {'id': 335, 'name': 'Game board'}, - {'id': 336, 'name': 'Mop'}, - {'id': 337, 'name': 'Radish'}, - {'id': 338, 'name': 'Baozi'}, - {'id': 339, 'name': 'Target'}, - {'id': 340, 'name': 'French'}, - {'id': 341, 'name': 'Spring Rolls'}, - {'id': 342, 'name': 'Monkey'}, - {'id': 343, 'name': 'Rabbit'}, - {'id': 344, 'name': 'Pencil Case'}, - {'id': 345, 'name': 'Yak'}, - {'id': 346, 'name': 'Red Cabbage'}, - {'id': 347, 'name': 'Binoculars'}, - {'id': 348, 'name': 'Asparagus'}, - {'id': 349, 'name': 'Barbell'}, - {'id': 350, 'name': 'Scallop'}, - {'id': 351, 'name': 'Noddles'}, - {'id': 352, 'name': 'Comb'}, - {'id': 353, 'name': 'Dumpling'}, - {'id': 354, 'name': 'Oyster'}, - {'id': 355, 'name': 'Table Tennis paddle'}, - {'id': 356, 'name': 'Cosmetics Brush/Eyeliner Pencil'}, - {'id': 357, 'name': 'Chainsaw'}, - {'id': 358, 'name': 'Eraser'}, - {'id': 359, 'name': 'Lobster'}, - {'id': 360, 'name': 'Durian'}, - {'id': 361, 'name': 'Okra'}, - {'id': 362, 'name': 'Lipstick'}, - {'id': 363, 'name': 'Cosmetics Mirror'}, - {'id': 364, 'name': 'Curling'}, - {'id': 365, 'name': 'Table Tennis '}, + {"id": 1, "name": "Person"}, + {"id": 2, "name": "Sneakers"}, + {"id": 3, "name": "Chair"}, + {"id": 4, "name": "Other Shoes"}, + {"id": 5, "name": "Hat"}, + {"id": 6, "name": "Car"}, + {"id": 7, "name": "Lamp"}, + {"id": 8, "name": "Glasses"}, + {"id": 9, "name": "Bottle"}, + {"id": 10, "name": "Desk"}, + {"id": 11, "name": "Cup"}, + {"id": 12, "name": "Street Lights"}, + {"id": 13, "name": "Cabinet/shelf"}, + {"id": 14, "name": "Handbag/Satchel"}, + {"id": 15, "name": "Bracelet"}, + {"id": 16, "name": "Plate"}, + {"id": 17, "name": "Picture/Frame"}, + {"id": 18, "name": "Helmet"}, + {"id": 19, "name": "Book"}, + {"id": 20, "name": "Gloves"}, + {"id": 21, "name": "Storage box"}, + {"id": 22, "name": "Boat"}, + {"id": 23, "name": "Leather Shoes"}, + {"id": 24, "name": "Flower"}, + {"id": 25, "name": "Bench"}, + {"id": 26, "name": "Potted Plant"}, + {"id": 27, "name": "Bowl/Basin"}, + {"id": 28, "name": "Flag"}, + {"id": 29, "name": "Pillow"}, + {"id": 30, "name": "Boots"}, + {"id": 31, "name": "Vase"}, + {"id": 32, "name": "Microphone"}, + {"id": 33, "name": "Necklace"}, + {"id": 34, "name": "Ring"}, + {"id": 35, "name": "SUV"}, + {"id": 36, "name": "Wine Glass"}, + {"id": 37, "name": "Belt"}, + {"id": 38, "name": "Monitor/TV"}, + {"id": 39, "name": "Backpack"}, + {"id": 40, "name": "Umbrella"}, + {"id": 41, "name": "Traffic Light"}, + {"id": 42, "name": "Speaker"}, + {"id": 43, "name": "Watch"}, + {"id": 44, "name": "Tie"}, + {"id": 45, "name": "Trash bin Can"}, + {"id": 46, "name": "Slippers"}, + {"id": 47, "name": "Bicycle"}, + {"id": 48, "name": "Stool"}, + {"id": 49, "name": "Barrel/bucket"}, + {"id": 50, "name": "Van"}, + {"id": 51, "name": "Couch"}, + {"id": 52, "name": "Sandals"}, + {"id": 53, "name": "Basket"}, + {"id": 54, "name": "Drum"}, + {"id": 55, "name": "Pen/Pencil"}, + {"id": 56, "name": "Bus"}, + {"id": 57, "name": "Wild Bird"}, + {"id": 58, "name": "High Heels"}, + {"id": 59, "name": "Motorcycle"}, + {"id": 60, "name": "Guitar"}, + {"id": 61, "name": "Carpet"}, + {"id": 62, "name": "Cell Phone"}, + {"id": 63, "name": "Bread"}, + {"id": 64, "name": "Camera"}, + {"id": 65, "name": "Canned"}, + {"id": 66, "name": "Truck"}, + {"id": 67, "name": "Traffic cone"}, + {"id": 68, "name": "Cymbal"}, + {"id": 69, "name": "Lifesaver"}, + {"id": 70, "name": "Towel"}, + {"id": 71, "name": "Stuffed Toy"}, + {"id": 72, "name": "Candle"}, + {"id": 73, "name": "Sailboat"}, + {"id": 74, "name": "Laptop"}, + {"id": 75, "name": "Awning"}, + {"id": 76, "name": "Bed"}, + {"id": 77, "name": "Faucet"}, + {"id": 78, "name": "Tent"}, + {"id": 79, "name": "Horse"}, + {"id": 80, "name": "Mirror"}, + {"id": 81, "name": "Power outlet"}, + {"id": 82, "name": "Sink"}, + {"id": 83, "name": "Apple"}, + {"id": 84, "name": "Air Conditioner"}, + {"id": 85, "name": "Knife"}, + {"id": 86, "name": "Hockey Stick"}, + {"id": 87, "name": "Paddle"}, + {"id": 88, "name": "Pickup Truck"}, + {"id": 89, "name": "Fork"}, + {"id": 90, "name": "Traffic Sign"}, + {"id": 91, "name": "Ballon"}, + {"id": 92, "name": "Tripod"}, + {"id": 93, "name": "Dog"}, + {"id": 94, "name": "Spoon"}, + {"id": 95, "name": "Clock"}, + {"id": 96, "name": "Pot"}, + {"id": 97, "name": "Cow"}, + {"id": 98, "name": "Cake"}, + {"id": 99, "name": "Dining Table"}, + {"id": 100, "name": "Sheep"}, + {"id": 101, "name": "Hanger"}, + {"id": 102, "name": "Blackboard/Whiteboard"}, + {"id": 103, "name": "Napkin"}, + {"id": 104, "name": "Other Fish"}, + {"id": 105, "name": "Orange/Tangerine"}, + {"id": 106, "name": "Toiletry"}, + {"id": 107, "name": "Keyboard"}, + {"id": 108, "name": "Tomato"}, + {"id": 109, "name": "Lantern"}, + {"id": 110, "name": "Machinery Vehicle"}, + {"id": 111, "name": "Fan"}, + {"id": 112, "name": "Green Vegetables"}, + {"id": 113, "name": "Banana"}, + {"id": 114, "name": "Baseball Glove"}, + {"id": 115, "name": "Airplane"}, + {"id": 116, "name": "Mouse"}, + {"id": 117, "name": "Train"}, + {"id": 118, "name": "Pumpkin"}, + {"id": 119, "name": "Soccer"}, + {"id": 120, "name": "Skiboard"}, + {"id": 121, "name": "Luggage"}, + {"id": 122, "name": "Nightstand"}, + {"id": 123, "name": "Teapot"}, + {"id": 124, "name": "Telephone"}, + {"id": 125, "name": "Trolley"}, + {"id": 126, "name": "Head Phone"}, + {"id": 127, "name": "Sports Car"}, + {"id": 128, "name": "Stop Sign"}, + {"id": 129, "name": "Dessert"}, + {"id": 130, "name": "Scooter"}, + {"id": 131, "name": "Stroller"}, + {"id": 132, "name": "Crane"}, + {"id": 133, "name": "Remote"}, + {"id": 134, "name": "Refrigerator"}, + {"id": 135, "name": "Oven"}, + {"id": 136, "name": "Lemon"}, + {"id": 137, "name": "Duck"}, + {"id": 138, "name": "Baseball Bat"}, + {"id": 139, "name": "Surveillance Camera"}, + {"id": 140, "name": "Cat"}, + {"id": 141, "name": "Jug"}, + {"id": 142, "name": "Broccoli"}, + {"id": 143, "name": "Piano"}, + {"id": 144, "name": "Pizza"}, + {"id": 145, "name": "Elephant"}, + {"id": 146, "name": "Skateboard"}, + {"id": 147, "name": "Surfboard"}, + {"id": 148, "name": "Gun"}, + {"id": 149, "name": "Skating and Skiing shoes"}, + {"id": 150, "name": "Gas stove"}, + {"id": 151, "name": "Donut"}, + {"id": 152, "name": "Bow Tie"}, + {"id": 153, "name": "Carrot"}, + {"id": 154, "name": "Toilet"}, + {"id": 155, "name": "Kite"}, + {"id": 156, "name": "Strawberry"}, + {"id": 157, "name": "Other Balls"}, + {"id": 158, "name": "Shovel"}, + {"id": 159, "name": "Pepper"}, + {"id": 160, "name": "Computer Box"}, + {"id": 161, "name": "Toilet Paper"}, + {"id": 162, "name": "Cleaning Products"}, + {"id": 163, "name": "Chopsticks"}, + {"id": 164, "name": "Microwave"}, + {"id": 165, "name": "Pigeon"}, + {"id": 166, "name": "Baseball"}, + {"id": 167, "name": "Cutting/chopping Board"}, + {"id": 168, "name": "Coffee Table"}, + {"id": 169, "name": "Side Table"}, + {"id": 170, "name": "Scissors"}, + {"id": 171, "name": "Marker"}, + {"id": 172, "name": "Pie"}, + {"id": 173, "name": "Ladder"}, + {"id": 174, "name": "Snowboard"}, + {"id": 175, "name": "Cookies"}, + {"id": 176, "name": "Radiator"}, + {"id": 177, "name": "Fire Hydrant"}, + {"id": 178, "name": "Basketball"}, + {"id": 179, "name": "Zebra"}, + {"id": 180, "name": "Grape"}, + {"id": 181, "name": "Giraffe"}, + {"id": 182, "name": "Potato"}, + {"id": 183, "name": "Sausage"}, + {"id": 184, "name": "Tricycle"}, + {"id": 185, "name": "Violin"}, + {"id": 186, "name": "Egg"}, + {"id": 187, "name": "Fire Extinguisher"}, + {"id": 188, "name": "Candy"}, + {"id": 189, "name": "Fire Truck"}, + {"id": 190, "name": "Billards"}, + {"id": 191, "name": "Converter"}, + {"id": 192, "name": "Bathtub"}, + {"id": 193, "name": "Wheelchair"}, + {"id": 194, "name": "Golf Club"}, + {"id": 195, "name": "Briefcase"}, + {"id": 196, "name": "Cucumber"}, + {"id": 197, "name": "Cigar/Cigarette "}, + {"id": 198, "name": "Paint Brush"}, + {"id": 199, "name": "Pear"}, + {"id": 200, "name": "Heavy Truck"}, + {"id": 201, "name": "Hamburger"}, + {"id": 202, "name": "Extractor"}, + {"id": 203, "name": "Extension Cord"}, + {"id": 204, "name": "Tong"}, + {"id": 205, "name": "Tennis Racket"}, + {"id": 206, "name": "Folder"}, + {"id": 207, "name": "American Football"}, + {"id": 208, "name": "earphone"}, + {"id": 209, "name": "Mask"}, + {"id": 210, "name": "Kettle"}, + {"id": 211, "name": "Tennis"}, + {"id": 212, "name": "Ship"}, + {"id": 213, "name": "Swing"}, + {"id": 214, "name": "Coffee Machine"}, + {"id": 215, "name": "Slide"}, + {"id": 216, "name": "Carriage"}, + {"id": 217, "name": "Onion"}, + {"id": 218, "name": "Green beans"}, + {"id": 219, "name": "Projector"}, + {"id": 220, "name": "Frisbee"}, + {"id": 221, "name": "Washing Machine/Drying Machine"}, + {"id": 222, "name": "Chicken"}, + {"id": 223, "name": "Printer"}, + {"id": 224, "name": "Watermelon"}, + {"id": 225, "name": "Saxophone"}, + {"id": 226, "name": "Tissue"}, + {"id": 227, "name": "Toothbrush"}, + {"id": 228, "name": "Ice cream"}, + {"id": 229, "name": "Hot air balloon"}, + {"id": 230, "name": "Cello"}, + {"id": 231, "name": "French Fries"}, + {"id": 232, "name": "Scale"}, + {"id": 233, "name": "Trophy"}, + {"id": 234, "name": "Cabbage"}, + {"id": 235, "name": "Hot dog"}, + {"id": 236, "name": "Blender"}, + {"id": 237, "name": "Peach"}, + {"id": 238, "name": "Rice"}, + {"id": 239, "name": "Wallet/Purse"}, + {"id": 240, "name": "Volleyball"}, + {"id": 241, "name": "Deer"}, + {"id": 242, "name": "Goose"}, + {"id": 243, "name": "Tape"}, + {"id": 244, "name": "Tablet"}, + {"id": 245, "name": "Cosmetics"}, + {"id": 246, "name": "Trumpet"}, + {"id": 247, "name": "Pineapple"}, + {"id": 248, "name": "Golf Ball"}, + {"id": 249, "name": "Ambulance"}, + {"id": 250, "name": "Parking meter"}, + {"id": 251, "name": "Mango"}, + {"id": 252, "name": "Key"}, + {"id": 253, "name": "Hurdle"}, + {"id": 254, "name": "Fishing Rod"}, + {"id": 255, "name": "Medal"}, + {"id": 256, "name": "Flute"}, + {"id": 257, "name": "Brush"}, + {"id": 258, "name": "Penguin"}, + {"id": 259, "name": "Megaphone"}, + {"id": 260, "name": "Corn"}, + {"id": 261, "name": "Lettuce"}, + {"id": 262, "name": "Garlic"}, + {"id": 263, "name": "Swan"}, + {"id": 264, "name": "Helicopter"}, + {"id": 265, "name": "Green Onion"}, + {"id": 266, "name": "Sandwich"}, + {"id": 267, "name": "Nuts"}, + {"id": 268, "name": "Speed Limit Sign"}, + {"id": 269, "name": "Induction Cooker"}, + {"id": 270, "name": "Broom"}, + {"id": 271, "name": "Trombone"}, + {"id": 272, "name": "Plum"}, + {"id": 273, "name": "Rickshaw"}, + {"id": 274, "name": "Goldfish"}, + {"id": 275, "name": "Kiwi fruit"}, + {"id": 276, "name": "Router/modem"}, + {"id": 277, "name": "Poker Card"}, + {"id": 278, "name": "Toaster"}, + {"id": 279, "name": "Shrimp"}, + {"id": 280, "name": "Sushi"}, + {"id": 281, "name": "Cheese"}, + {"id": 282, "name": "Notepaper"}, + {"id": 283, "name": "Cherry"}, + {"id": 284, "name": "Pliers"}, + {"id": 285, "name": "CD"}, + {"id": 286, "name": "Pasta"}, + {"id": 287, "name": "Hammer"}, + {"id": 288, "name": "Cue"}, + {"id": 289, "name": "Avocado"}, + {"id": 290, "name": "Hami melon"}, + {"id": 291, "name": "Flask"}, + {"id": 292, "name": "Mushroom"}, + {"id": 293, "name": "Screwdriver"}, + {"id": 294, "name": "Soap"}, + {"id": 295, "name": "Recorder"}, + {"id": 296, "name": "Bear"}, + {"id": 297, "name": "Eggplant"}, + {"id": 298, "name": "Board Eraser"}, + {"id": 299, "name": "Coconut"}, + {"id": 300, "name": "Tape Measure/ Ruler"}, + {"id": 301, "name": "Pig"}, + {"id": 302, "name": "Showerhead"}, + {"id": 303, "name": "Globe"}, + {"id": 304, "name": "Chips"}, + {"id": 305, "name": "Steak"}, + {"id": 306, "name": "Crosswalk Sign"}, + {"id": 307, "name": "Stapler"}, + {"id": 308, "name": "Camel"}, + {"id": 309, "name": "Formula 1 "}, + {"id": 310, "name": "Pomegranate"}, + {"id": 311, "name": "Dishwasher"}, + {"id": 312, "name": "Crab"}, + {"id": 313, "name": "Hoverboard"}, + {"id": 314, "name": "Meatball"}, + {"id": 315, "name": "Rice Cooker"}, + {"id": 316, "name": "Tuba"}, + {"id": 317, "name": "Calculator"}, + {"id": 318, "name": "Papaya"}, + {"id": 319, "name": "Antelope"}, + {"id": 320, "name": "Parrot"}, + {"id": 321, "name": "Seal"}, + {"id": 322, "name": "Butterfly"}, + {"id": 323, "name": "Dumbbell"}, + {"id": 324, "name": "Donkey"}, + {"id": 325, "name": "Lion"}, + {"id": 326, "name": "Urinal"}, + {"id": 327, "name": "Dolphin"}, + {"id": 328, "name": "Electric Drill"}, + {"id": 329, "name": "Hair Dryer"}, + {"id": 330, "name": "Egg tart"}, + {"id": 331, "name": "Jellyfish"}, + {"id": 332, "name": "Treadmill"}, + {"id": 333, "name": "Lighter"}, + {"id": 334, "name": "Grapefruit"}, + {"id": 335, "name": "Game board"}, + {"id": 336, "name": "Mop"}, + {"id": 337, "name": "Radish"}, + {"id": 338, "name": "Baozi"}, + {"id": 339, "name": "Target"}, + {"id": 340, "name": "French"}, + {"id": 341, "name": "Spring Rolls"}, + {"id": 342, "name": "Monkey"}, + {"id": 343, "name": "Rabbit"}, + {"id": 344, "name": "Pencil Case"}, + {"id": 345, "name": "Yak"}, + {"id": 346, "name": "Red Cabbage"}, + {"id": 347, "name": "Binoculars"}, + {"id": 348, "name": "Asparagus"}, + {"id": 349, "name": "Barbell"}, + {"id": 350, "name": "Scallop"}, + {"id": 351, "name": "Noddles"}, + {"id": 352, "name": "Comb"}, + {"id": 353, "name": "Dumpling"}, + {"id": 354, "name": "Oyster"}, + {"id": 355, "name": "Table Tennis paddle"}, + {"id": 356, "name": "Cosmetics Brush/Eyeliner Pencil"}, + {"id": 357, "name": "Chainsaw"}, + {"id": 358, "name": "Eraser"}, + {"id": 359, "name": "Lobster"}, + {"id": 360, "name": "Durian"}, + {"id": 361, "name": "Okra"}, + {"id": 362, "name": "Lipstick"}, + {"id": 363, "name": "Cosmetics Mirror"}, + {"id": 364, "name": "Curling"}, + {"id": 365, "name": "Table Tennis "}, ] def _get_builtin_metadata(): - id_to_name = {x['id']: x['name'] for x in categories_v2_fix} + id_to_name = {x["id"]: x["name"] for x in categories_v2_fix} thing_dataset_id_to_contiguous_id = { - x['id']: i for i, x in enumerate( - sorted(categories_v2_fix, key=lambda x: x['id']))} + x["id"]: i for i, x in enumerate(sorted(categories_v2_fix, key=lambda x: x["id"])) + } thing_classes = [id_to_name[k] for k in sorted(id_to_name)] return { "thing_dataset_id_to_contiguous_id": thing_dataset_id_to_contiguous_id, - "thing_classes": thing_classes} + "thing_classes": thing_classes, + } _PREDEFINED_SPLITS_OBJECTS365 = { - "objects365_v2_train": ("objects365/train", "objects365/annotations/zhiyuan_objv2_train_fixname_fixmiss.json"), + "objects365_v2_train": ( + "objects365/train", + "objects365/annotations/zhiyuan_objv2_train_fixname_fixmiss.json", + ), # 80,000 images, 1,240,587 annotations - "objects365_v2_val": ("objects365/val", "objects365/annotations/zhiyuan_objv2_val_fixname.json"), - "objects365_v2_val_rare": ("objects365/val", "objects365/annotations/zhiyuan_objv2_val_fixname_rare.json"), + "objects365_v2_val": ( + "objects365/val", + "objects365/annotations/zhiyuan_objv2_val_fixname.json", + ), + "objects365_v2_val_rare": ( + "objects365/val", + "objects365/annotations/zhiyuan_objv2_val_fixname_rare.json", + ), } for key, (image_root, json_file) in _PREDEFINED_SPLITS_OBJECTS365.items(): @@ -767,4 +778,4 @@ def _get_builtin_metadata(): _get_builtin_metadata(), os.path.join("datasets", json_file) if "://" not in json_file else json_file, os.path.join("datasets", image_root), - ) \ No newline at end of file + ) diff --git a/dimos/models/Detic/detic/data/datasets/oid.py b/dimos/models/Detic/detic/data/datasets/oid.py index 90d7f8613e..0308a8da1d 100644 --- a/dimos/models/Detic/detic/data/datasets/oid.py +++ b/dimos/models/Detic/detic/data/datasets/oid.py @@ -1,519 +1,522 @@ # Part of the code is from https://github.com/xingyizhou/UniDet/blob/master/projects/UniDet/unidet/data/datasets/oid.py # Copyright (c) Facebook, Inc. and its affiliates. -from .register_oid import register_oid_instances import os +from .register_oid import register_oid_instances + categories = [ - {'id': 1, 'name': 'Infant bed', 'freebase_id': '/m/061hd_'}, - {'id': 2, 'name': 'Rose', 'freebase_id': '/m/06m11'}, - {'id': 3, 'name': 'Flag', 'freebase_id': '/m/03120'}, - {'id': 4, 'name': 'Flashlight', 'freebase_id': '/m/01kb5b'}, - {'id': 5, 'name': 'Sea turtle', 'freebase_id': '/m/0120dh'}, - {'id': 6, 'name': 'Camera', 'freebase_id': '/m/0dv5r'}, - {'id': 7, 'name': 'Animal', 'freebase_id': '/m/0jbk'}, - {'id': 8, 'name': 'Glove', 'freebase_id': '/m/0174n1'}, - {'id': 9, 'name': 'Crocodile', 'freebase_id': '/m/09f_2'}, - {'id': 10, 'name': 'Cattle', 'freebase_id': '/m/01xq0k1'}, - {'id': 11, 'name': 'House', 'freebase_id': '/m/03jm5'}, - {'id': 12, 'name': 'Guacamole', 'freebase_id': '/m/02g30s'}, - {'id': 13, 'name': 'Penguin', 'freebase_id': '/m/05z6w'}, - {'id': 14, 'name': 'Vehicle registration plate', 'freebase_id': '/m/01jfm_'}, - {'id': 15, 'name': 'Bench', 'freebase_id': '/m/076lb9'}, - {'id': 16, 'name': 'Ladybug', 'freebase_id': '/m/0gj37'}, - {'id': 17, 'name': 'Human nose', 'freebase_id': '/m/0k0pj'}, - {'id': 18, 'name': 'Watermelon', 'freebase_id': '/m/0kpqd'}, - {'id': 19, 'name': 'Flute', 'freebase_id': '/m/0l14j_'}, - {'id': 20, 'name': 'Butterfly', 'freebase_id': '/m/0cyf8'}, - {'id': 21, 'name': 'Washing machine', 'freebase_id': '/m/0174k2'}, - {'id': 22, 'name': 'Raccoon', 'freebase_id': '/m/0dq75'}, - {'id': 23, 'name': 'Segway', 'freebase_id': '/m/076bq'}, - {'id': 24, 'name': 'Taco', 'freebase_id': '/m/07crc'}, - {'id': 25, 'name': 'Jellyfish', 'freebase_id': '/m/0d8zb'}, - {'id': 26, 'name': 'Cake', 'freebase_id': '/m/0fszt'}, - {'id': 27, 'name': 'Pen', 'freebase_id': '/m/0k1tl'}, - {'id': 28, 'name': 'Cannon', 'freebase_id': '/m/020kz'}, - {'id': 29, 'name': 'Bread', 'freebase_id': '/m/09728'}, - {'id': 30, 'name': 'Tree', 'freebase_id': '/m/07j7r'}, - {'id': 31, 'name': 'Shellfish', 'freebase_id': '/m/0fbdv'}, - {'id': 32, 'name': 'Bed', 'freebase_id': '/m/03ssj5'}, - {'id': 33, 'name': 'Hamster', 'freebase_id': '/m/03qrc'}, - {'id': 34, 'name': 'Hat', 'freebase_id': '/m/02dl1y'}, - {'id': 35, 'name': 'Toaster', 'freebase_id': '/m/01k6s3'}, - {'id': 36, 'name': 'Sombrero', 'freebase_id': '/m/02jfl0'}, - {'id': 37, 'name': 'Tiara', 'freebase_id': '/m/01krhy'}, - {'id': 38, 'name': 'Bowl', 'freebase_id': '/m/04kkgm'}, - {'id': 39, 'name': 'Dragonfly', 'freebase_id': '/m/0ft9s'}, - {'id': 40, 'name': 'Moths and butterflies', 'freebase_id': '/m/0d_2m'}, - {'id': 41, 'name': 'Antelope', 'freebase_id': '/m/0czz2'}, - {'id': 42, 'name': 'Vegetable', 'freebase_id': '/m/0f4s2w'}, - {'id': 43, 'name': 'Torch', 'freebase_id': '/m/07dd4'}, - {'id': 44, 'name': 'Building', 'freebase_id': '/m/0cgh4'}, - {'id': 45, 'name': 'Power plugs and sockets', 'freebase_id': '/m/03bbps'}, - {'id': 46, 'name': 'Blender', 'freebase_id': '/m/02pjr4'}, - {'id': 47, 'name': 'Billiard table', 'freebase_id': '/m/04p0qw'}, - {'id': 48, 'name': 'Cutting board', 'freebase_id': '/m/02pdsw'}, - {'id': 49, 'name': 'Bronze sculpture', 'freebase_id': '/m/01yx86'}, - {'id': 50, 'name': 'Turtle', 'freebase_id': '/m/09dzg'}, - {'id': 51, 'name': 'Broccoli', 'freebase_id': '/m/0hkxq'}, - {'id': 52, 'name': 'Tiger', 'freebase_id': '/m/07dm6'}, - {'id': 53, 'name': 'Mirror', 'freebase_id': '/m/054_l'}, - {'id': 54, 'name': 'Bear', 'freebase_id': '/m/01dws'}, - {'id': 55, 'name': 'Zucchini', 'freebase_id': '/m/027pcv'}, - {'id': 56, 'name': 'Dress', 'freebase_id': '/m/01d40f'}, - {'id': 57, 'name': 'Volleyball', 'freebase_id': '/m/02rgn06'}, - {'id': 58, 'name': 'Guitar', 'freebase_id': '/m/0342h'}, - {'id': 59, 'name': 'Reptile', 'freebase_id': '/m/06bt6'}, - {'id': 60, 'name': 'Golf cart', 'freebase_id': '/m/0323sq'}, - {'id': 61, 'name': 'Tart', 'freebase_id': '/m/02zvsm'}, - {'id': 62, 'name': 'Fedora', 'freebase_id': '/m/02fq_6'}, - {'id': 63, 'name': 'Carnivore', 'freebase_id': '/m/01lrl'}, - {'id': 64, 'name': 'Car', 'freebase_id': '/m/0k4j'}, - {'id': 65, 'name': 'Lighthouse', 'freebase_id': '/m/04h7h'}, - {'id': 66, 'name': 'Coffeemaker', 'freebase_id': '/m/07xyvk'}, - {'id': 67, 'name': 'Food processor', 'freebase_id': '/m/03y6mg'}, - {'id': 68, 'name': 'Truck', 'freebase_id': '/m/07r04'}, - {'id': 69, 'name': 'Bookcase', 'freebase_id': '/m/03__z0'}, - {'id': 70, 'name': 'Surfboard', 'freebase_id': '/m/019w40'}, - {'id': 71, 'name': 'Footwear', 'freebase_id': '/m/09j5n'}, - {'id': 72, 'name': 'Bench', 'freebase_id': '/m/0cvnqh'}, - {'id': 73, 'name': 'Necklace', 'freebase_id': '/m/01llwg'}, - {'id': 74, 'name': 'Flower', 'freebase_id': '/m/0c9ph5'}, - {'id': 75, 'name': 'Radish', 'freebase_id': '/m/015x5n'}, - {'id': 76, 'name': 'Marine mammal', 'freebase_id': '/m/0gd2v'}, - {'id': 77, 'name': 'Frying pan', 'freebase_id': '/m/04v6l4'}, - {'id': 78, 'name': 'Tap', 'freebase_id': '/m/02jz0l'}, - {'id': 79, 'name': 'Peach', 'freebase_id': '/m/0dj6p'}, - {'id': 80, 'name': 'Knife', 'freebase_id': '/m/04ctx'}, - {'id': 81, 'name': 'Handbag', 'freebase_id': '/m/080hkjn'}, - {'id': 82, 'name': 'Laptop', 'freebase_id': '/m/01c648'}, - {'id': 83, 'name': 'Tent', 'freebase_id': '/m/01j61q'}, - {'id': 84, 'name': 'Ambulance', 'freebase_id': '/m/012n7d'}, - {'id': 85, 'name': 'Christmas tree', 'freebase_id': '/m/025nd'}, - {'id': 86, 'name': 'Eagle', 'freebase_id': '/m/09csl'}, - {'id': 87, 'name': 'Limousine', 'freebase_id': '/m/01lcw4'}, - {'id': 88, 'name': 'Kitchen & dining room table', 'freebase_id': '/m/0h8n5zk'}, - {'id': 89, 'name': 'Polar bear', 'freebase_id': '/m/0633h'}, - {'id': 90, 'name': 'Tower', 'freebase_id': '/m/01fdzj'}, - {'id': 91, 'name': 'Football', 'freebase_id': '/m/01226z'}, - {'id': 92, 'name': 'Willow', 'freebase_id': '/m/0mw_6'}, - {'id': 93, 'name': 'Human head', 'freebase_id': '/m/04hgtk'}, - {'id': 94, 'name': 'Stop sign', 'freebase_id': '/m/02pv19'}, - {'id': 95, 'name': 'Banana', 'freebase_id': '/m/09qck'}, - {'id': 96, 'name': 'Mixer', 'freebase_id': '/m/063rgb'}, - {'id': 97, 'name': 'Binoculars', 'freebase_id': '/m/0lt4_'}, - {'id': 98, 'name': 'Dessert', 'freebase_id': '/m/0270h'}, - {'id': 99, 'name': 'Bee', 'freebase_id': '/m/01h3n'}, - {'id': 100, 'name': 'Chair', 'freebase_id': '/m/01mzpv'}, - {'id': 101, 'name': 'Wood-burning stove', 'freebase_id': '/m/04169hn'}, - {'id': 102, 'name': 'Flowerpot', 'freebase_id': '/m/0fm3zh'}, - {'id': 103, 'name': 'Beaker', 'freebase_id': '/m/0d20w4'}, - {'id': 104, 'name': 'Oyster', 'freebase_id': '/m/0_cp5'}, - {'id': 105, 'name': 'Woodpecker', 'freebase_id': '/m/01dy8n'}, - {'id': 106, 'name': 'Harp', 'freebase_id': '/m/03m5k'}, - {'id': 107, 'name': 'Bathtub', 'freebase_id': '/m/03dnzn'}, - {'id': 108, 'name': 'Wall clock', 'freebase_id': '/m/0h8mzrc'}, - {'id': 109, 'name': 'Sports uniform', 'freebase_id': '/m/0h8mhzd'}, - {'id': 110, 'name': 'Rhinoceros', 'freebase_id': '/m/03d443'}, - {'id': 111, 'name': 'Beehive', 'freebase_id': '/m/01gllr'}, - {'id': 112, 'name': 'Cupboard', 'freebase_id': '/m/0642b4'}, - {'id': 113, 'name': 'Chicken', 'freebase_id': '/m/09b5t'}, - {'id': 114, 'name': 'Man', 'freebase_id': '/m/04yx4'}, - {'id': 115, 'name': 'Blue jay', 'freebase_id': '/m/01f8m5'}, - {'id': 116, 'name': 'Cucumber', 'freebase_id': '/m/015x4r'}, - {'id': 117, 'name': 'Balloon', 'freebase_id': '/m/01j51'}, - {'id': 118, 'name': 'Kite', 'freebase_id': '/m/02zt3'}, - {'id': 119, 'name': 'Fireplace', 'freebase_id': '/m/03tw93'}, - {'id': 120, 'name': 'Lantern', 'freebase_id': '/m/01jfsr'}, - {'id': 121, 'name': 'Missile', 'freebase_id': '/m/04ylt'}, - {'id': 122, 'name': 'Book', 'freebase_id': '/m/0bt_c3'}, - {'id': 123, 'name': 'Spoon', 'freebase_id': '/m/0cmx8'}, - {'id': 124, 'name': 'Grapefruit', 'freebase_id': '/m/0hqkz'}, - {'id': 125, 'name': 'Squirrel', 'freebase_id': '/m/071qp'}, - {'id': 126, 'name': 'Orange', 'freebase_id': '/m/0cyhj_'}, - {'id': 127, 'name': 'Coat', 'freebase_id': '/m/01xygc'}, - {'id': 128, 'name': 'Punching bag', 'freebase_id': '/m/0420v5'}, - {'id': 129, 'name': 'Zebra', 'freebase_id': '/m/0898b'}, - {'id': 130, 'name': 'Billboard', 'freebase_id': '/m/01knjb'}, - {'id': 131, 'name': 'Bicycle', 'freebase_id': '/m/0199g'}, - {'id': 132, 'name': 'Door handle', 'freebase_id': '/m/03c7gz'}, - {'id': 133, 'name': 'Mechanical fan', 'freebase_id': '/m/02x984l'}, - {'id': 134, 'name': 'Ring binder', 'freebase_id': '/m/04zwwv'}, - {'id': 135, 'name': 'Table', 'freebase_id': '/m/04bcr3'}, - {'id': 136, 'name': 'Parrot', 'freebase_id': '/m/0gv1x'}, - {'id': 137, 'name': 'Sock', 'freebase_id': '/m/01nq26'}, - {'id': 138, 'name': 'Vase', 'freebase_id': '/m/02s195'}, - {'id': 139, 'name': 'Weapon', 'freebase_id': '/m/083kb'}, - {'id': 140, 'name': 'Shotgun', 'freebase_id': '/m/06nrc'}, - {'id': 141, 'name': 'Glasses', 'freebase_id': '/m/0jyfg'}, - {'id': 142, 'name': 'Seahorse', 'freebase_id': '/m/0nybt'}, - {'id': 143, 'name': 'Belt', 'freebase_id': '/m/0176mf'}, - {'id': 144, 'name': 'Watercraft', 'freebase_id': '/m/01rzcn'}, - {'id': 145, 'name': 'Window', 'freebase_id': '/m/0d4v4'}, - {'id': 146, 'name': 'Giraffe', 'freebase_id': '/m/03bk1'}, - {'id': 147, 'name': 'Lion', 'freebase_id': '/m/096mb'}, - {'id': 148, 'name': 'Tire', 'freebase_id': '/m/0h9mv'}, - {'id': 149, 'name': 'Vehicle', 'freebase_id': '/m/07yv9'}, - {'id': 150, 'name': 'Canoe', 'freebase_id': '/m/0ph39'}, - {'id': 151, 'name': 'Tie', 'freebase_id': '/m/01rkbr'}, - {'id': 152, 'name': 'Shelf', 'freebase_id': '/m/0gjbg72'}, - {'id': 153, 'name': 'Picture frame', 'freebase_id': '/m/06z37_'}, - {'id': 154, 'name': 'Printer', 'freebase_id': '/m/01m4t'}, - {'id': 155, 'name': 'Human leg', 'freebase_id': '/m/035r7c'}, - {'id': 156, 'name': 'Boat', 'freebase_id': '/m/019jd'}, - {'id': 157, 'name': 'Slow cooker', 'freebase_id': '/m/02tsc9'}, - {'id': 158, 'name': 'Croissant', 'freebase_id': '/m/015wgc'}, - {'id': 159, 'name': 'Candle', 'freebase_id': '/m/0c06p'}, - {'id': 160, 'name': 'Pancake', 'freebase_id': '/m/01dwwc'}, - {'id': 161, 'name': 'Pillow', 'freebase_id': '/m/034c16'}, - {'id': 162, 'name': 'Coin', 'freebase_id': '/m/0242l'}, - {'id': 163, 'name': 'Stretcher', 'freebase_id': '/m/02lbcq'}, - {'id': 164, 'name': 'Sandal', 'freebase_id': '/m/03nfch'}, - {'id': 165, 'name': 'Woman', 'freebase_id': '/m/03bt1vf'}, - {'id': 166, 'name': 'Stairs', 'freebase_id': '/m/01lynh'}, - {'id': 167, 'name': 'Harpsichord', 'freebase_id': '/m/03q5t'}, - {'id': 168, 'name': 'Stool', 'freebase_id': '/m/0fqt361'}, - {'id': 169, 'name': 'Bus', 'freebase_id': '/m/01bjv'}, - {'id': 170, 'name': 'Suitcase', 'freebase_id': '/m/01s55n'}, - {'id': 171, 'name': 'Human mouth', 'freebase_id': '/m/0283dt1'}, - {'id': 172, 'name': 'Juice', 'freebase_id': '/m/01z1kdw'}, - {'id': 173, 'name': 'Skull', 'freebase_id': '/m/016m2d'}, - {'id': 174, 'name': 'Door', 'freebase_id': '/m/02dgv'}, - {'id': 175, 'name': 'Violin', 'freebase_id': '/m/07y_7'}, - {'id': 176, 'name': 'Chopsticks', 'freebase_id': '/m/01_5g'}, - {'id': 177, 'name': 'Digital clock', 'freebase_id': '/m/06_72j'}, - {'id': 178, 'name': 'Sunflower', 'freebase_id': '/m/0ftb8'}, - {'id': 179, 'name': 'Leopard', 'freebase_id': '/m/0c29q'}, - {'id': 180, 'name': 'Bell pepper', 'freebase_id': '/m/0jg57'}, - {'id': 181, 'name': 'Harbor seal', 'freebase_id': '/m/02l8p9'}, - {'id': 182, 'name': 'Snake', 'freebase_id': '/m/078jl'}, - {'id': 183, 'name': 'Sewing machine', 'freebase_id': '/m/0llzx'}, - {'id': 184, 'name': 'Goose', 'freebase_id': '/m/0dbvp'}, - {'id': 185, 'name': 'Helicopter', 'freebase_id': '/m/09ct_'}, - {'id': 186, 'name': 'Seat belt', 'freebase_id': '/m/0dkzw'}, - {'id': 187, 'name': 'Coffee cup', 'freebase_id': '/m/02p5f1q'}, - {'id': 188, 'name': 'Microwave oven', 'freebase_id': '/m/0fx9l'}, - {'id': 189, 'name': 'Hot dog', 'freebase_id': '/m/01b9xk'}, - {'id': 190, 'name': 'Countertop', 'freebase_id': '/m/0b3fp9'}, - {'id': 191, 'name': 'Serving tray', 'freebase_id': '/m/0h8n27j'}, - {'id': 192, 'name': 'Dog bed', 'freebase_id': '/m/0h8n6f9'}, - {'id': 193, 'name': 'Beer', 'freebase_id': '/m/01599'}, - {'id': 194, 'name': 'Sunglasses', 'freebase_id': '/m/017ftj'}, - {'id': 195, 'name': 'Golf ball', 'freebase_id': '/m/044r5d'}, - {'id': 196, 'name': 'Waffle', 'freebase_id': '/m/01dwsz'}, - {'id': 197, 'name': 'Palm tree', 'freebase_id': '/m/0cdl1'}, - {'id': 198, 'name': 'Trumpet', 'freebase_id': '/m/07gql'}, - {'id': 199, 'name': 'Ruler', 'freebase_id': '/m/0hdln'}, - {'id': 200, 'name': 'Helmet', 'freebase_id': '/m/0zvk5'}, - {'id': 201, 'name': 'Ladder', 'freebase_id': '/m/012w5l'}, - {'id': 202, 'name': 'Office building', 'freebase_id': '/m/021sj1'}, - {'id': 203, 'name': 'Tablet computer', 'freebase_id': '/m/0bh9flk'}, - {'id': 204, 'name': 'Toilet paper', 'freebase_id': '/m/09gtd'}, - {'id': 205, 'name': 'Pomegranate', 'freebase_id': '/m/0jwn_'}, - {'id': 206, 'name': 'Skirt', 'freebase_id': '/m/02wv6h6'}, - {'id': 207, 'name': 'Gas stove', 'freebase_id': '/m/02wv84t'}, - {'id': 208, 'name': 'Cookie', 'freebase_id': '/m/021mn'}, - {'id': 209, 'name': 'Cart', 'freebase_id': '/m/018p4k'}, - {'id': 210, 'name': 'Raven', 'freebase_id': '/m/06j2d'}, - {'id': 211, 'name': 'Egg', 'freebase_id': '/m/033cnk'}, - {'id': 212, 'name': 'Burrito', 'freebase_id': '/m/01j3zr'}, - {'id': 213, 'name': 'Goat', 'freebase_id': '/m/03fwl'}, - {'id': 214, 'name': 'Kitchen knife', 'freebase_id': '/m/058qzx'}, - {'id': 215, 'name': 'Skateboard', 'freebase_id': '/m/06_fw'}, - {'id': 216, 'name': 'Salt and pepper shakers', 'freebase_id': '/m/02x8cch'}, - {'id': 217, 'name': 'Lynx', 'freebase_id': '/m/04g2r'}, - {'id': 218, 'name': 'Boot', 'freebase_id': '/m/01b638'}, - {'id': 219, 'name': 'Platter', 'freebase_id': '/m/099ssp'}, - {'id': 220, 'name': 'Ski', 'freebase_id': '/m/071p9'}, - {'id': 221, 'name': 'Swimwear', 'freebase_id': '/m/01gkx_'}, - {'id': 222, 'name': 'Swimming pool', 'freebase_id': '/m/0b_rs'}, - {'id': 223, 'name': 'Drinking straw', 'freebase_id': '/m/03v5tg'}, - {'id': 224, 'name': 'Wrench', 'freebase_id': '/m/01j5ks'}, - {'id': 225, 'name': 'Drum', 'freebase_id': '/m/026t6'}, - {'id': 226, 'name': 'Ant', 'freebase_id': '/m/0_k2'}, - {'id': 227, 'name': 'Human ear', 'freebase_id': '/m/039xj_'}, - {'id': 228, 'name': 'Headphones', 'freebase_id': '/m/01b7fy'}, - {'id': 229, 'name': 'Fountain', 'freebase_id': '/m/0220r2'}, - {'id': 230, 'name': 'Bird', 'freebase_id': '/m/015p6'}, - {'id': 231, 'name': 'Jeans', 'freebase_id': '/m/0fly7'}, - {'id': 232, 'name': 'Television', 'freebase_id': '/m/07c52'}, - {'id': 233, 'name': 'Crab', 'freebase_id': '/m/0n28_'}, - {'id': 234, 'name': 'Microphone', 'freebase_id': '/m/0hg7b'}, - {'id': 235, 'name': 'Home appliance', 'freebase_id': '/m/019dx1'}, - {'id': 236, 'name': 'Snowplow', 'freebase_id': '/m/04vv5k'}, - {'id': 237, 'name': 'Beetle', 'freebase_id': '/m/020jm'}, - {'id': 238, 'name': 'Artichoke', 'freebase_id': '/m/047v4b'}, - {'id': 239, 'name': 'Jet ski', 'freebase_id': '/m/01xs3r'}, - {'id': 240, 'name': 'Stationary bicycle', 'freebase_id': '/m/03kt2w'}, - {'id': 241, 'name': 'Human hair', 'freebase_id': '/m/03q69'}, - {'id': 242, 'name': 'Brown bear', 'freebase_id': '/m/01dxs'}, - {'id': 243, 'name': 'Starfish', 'freebase_id': '/m/01h8tj'}, - {'id': 244, 'name': 'Fork', 'freebase_id': '/m/0dt3t'}, - {'id': 245, 'name': 'Lobster', 'freebase_id': '/m/0cjq5'}, - {'id': 246, 'name': 'Corded phone', 'freebase_id': '/m/0h8lkj8'}, - {'id': 247, 'name': 'Drink', 'freebase_id': '/m/0271t'}, - {'id': 248, 'name': 'Saucer', 'freebase_id': '/m/03q5c7'}, - {'id': 249, 'name': 'Carrot', 'freebase_id': '/m/0fj52s'}, - {'id': 250, 'name': 'Insect', 'freebase_id': '/m/03vt0'}, - {'id': 251, 'name': 'Clock', 'freebase_id': '/m/01x3z'}, - {'id': 252, 'name': 'Castle', 'freebase_id': '/m/0d5gx'}, - {'id': 253, 'name': 'Tennis racket', 'freebase_id': '/m/0h8my_4'}, - {'id': 254, 'name': 'Ceiling fan', 'freebase_id': '/m/03ldnb'}, - {'id': 255, 'name': 'Asparagus', 'freebase_id': '/m/0cjs7'}, - {'id': 256, 'name': 'Jaguar', 'freebase_id': '/m/0449p'}, - {'id': 257, 'name': 'Musical instrument', 'freebase_id': '/m/04szw'}, - {'id': 258, 'name': 'Train', 'freebase_id': '/m/07jdr'}, - {'id': 259, 'name': 'Cat', 'freebase_id': '/m/01yrx'}, - {'id': 260, 'name': 'Rifle', 'freebase_id': '/m/06c54'}, - {'id': 261, 'name': 'Dumbbell', 'freebase_id': '/m/04h8sr'}, - {'id': 262, 'name': 'Mobile phone', 'freebase_id': '/m/050k8'}, - {'id': 263, 'name': 'Taxi', 'freebase_id': '/m/0pg52'}, - {'id': 264, 'name': 'Shower', 'freebase_id': '/m/02f9f_'}, - {'id': 265, 'name': 'Pitcher', 'freebase_id': '/m/054fyh'}, - {'id': 266, 'name': 'Lemon', 'freebase_id': '/m/09k_b'}, - {'id': 267, 'name': 'Invertebrate', 'freebase_id': '/m/03xxp'}, - {'id': 268, 'name': 'Turkey', 'freebase_id': '/m/0jly1'}, - {'id': 269, 'name': 'High heels', 'freebase_id': '/m/06k2mb'}, - {'id': 270, 'name': 'Bust', 'freebase_id': '/m/04yqq2'}, - {'id': 271, 'name': 'Elephant', 'freebase_id': '/m/0bwd_0j'}, - {'id': 272, 'name': 'Scarf', 'freebase_id': '/m/02h19r'}, - {'id': 273, 'name': 'Barrel', 'freebase_id': '/m/02zn6n'}, - {'id': 274, 'name': 'Trombone', 'freebase_id': '/m/07c6l'}, - {'id': 275, 'name': 'Pumpkin', 'freebase_id': '/m/05zsy'}, - {'id': 276, 'name': 'Box', 'freebase_id': '/m/025dyy'}, - {'id': 277, 'name': 'Tomato', 'freebase_id': '/m/07j87'}, - {'id': 278, 'name': 'Frog', 'freebase_id': '/m/09ld4'}, - {'id': 279, 'name': 'Bidet', 'freebase_id': '/m/01vbnl'}, - {'id': 280, 'name': 'Human face', 'freebase_id': '/m/0dzct'}, - {'id': 281, 'name': 'Houseplant', 'freebase_id': '/m/03fp41'}, - {'id': 282, 'name': 'Van', 'freebase_id': '/m/0h2r6'}, - {'id': 283, 'name': 'Shark', 'freebase_id': '/m/0by6g'}, - {'id': 284, 'name': 'Ice cream', 'freebase_id': '/m/0cxn2'}, - {'id': 285, 'name': 'Swim cap', 'freebase_id': '/m/04tn4x'}, - {'id': 286, 'name': 'Falcon', 'freebase_id': '/m/0f6wt'}, - {'id': 287, 'name': 'Ostrich', 'freebase_id': '/m/05n4y'}, - {'id': 288, 'name': 'Handgun', 'freebase_id': '/m/0gxl3'}, - {'id': 289, 'name': 'Whiteboard', 'freebase_id': '/m/02d9qx'}, - {'id': 290, 'name': 'Lizard', 'freebase_id': '/m/04m9y'}, - {'id': 291, 'name': 'Pasta', 'freebase_id': '/m/05z55'}, - {'id': 292, 'name': 'Snowmobile', 'freebase_id': '/m/01x3jk'}, - {'id': 293, 'name': 'Light bulb', 'freebase_id': '/m/0h8l4fh'}, - {'id': 294, 'name': 'Window blind', 'freebase_id': '/m/031b6r'}, - {'id': 295, 'name': 'Muffin', 'freebase_id': '/m/01tcjp'}, - {'id': 296, 'name': 'Pretzel', 'freebase_id': '/m/01f91_'}, - {'id': 297, 'name': 'Computer monitor', 'freebase_id': '/m/02522'}, - {'id': 298, 'name': 'Horn', 'freebase_id': '/m/0319l'}, - {'id': 299, 'name': 'Furniture', 'freebase_id': '/m/0c_jw'}, - {'id': 300, 'name': 'Sandwich', 'freebase_id': '/m/0l515'}, - {'id': 301, 'name': 'Fox', 'freebase_id': '/m/0306r'}, - {'id': 302, 'name': 'Convenience store', 'freebase_id': '/m/0crjs'}, - {'id': 303, 'name': 'Fish', 'freebase_id': '/m/0ch_cf'}, - {'id': 304, 'name': 'Fruit', 'freebase_id': '/m/02xwb'}, - {'id': 305, 'name': 'Earrings', 'freebase_id': '/m/01r546'}, - {'id': 306, 'name': 'Curtain', 'freebase_id': '/m/03rszm'}, - {'id': 307, 'name': 'Grape', 'freebase_id': '/m/0388q'}, - {'id': 308, 'name': 'Sofa bed', 'freebase_id': '/m/03m3pdh'}, - {'id': 309, 'name': 'Horse', 'freebase_id': '/m/03k3r'}, - {'id': 310, 'name': 'Luggage and bags', 'freebase_id': '/m/0hf58v5'}, - {'id': 311, 'name': 'Desk', 'freebase_id': '/m/01y9k5'}, - {'id': 312, 'name': 'Crutch', 'freebase_id': '/m/05441v'}, - {'id': 313, 'name': 'Bicycle helmet', 'freebase_id': '/m/03p3bw'}, - {'id': 314, 'name': 'Tick', 'freebase_id': '/m/0175cv'}, - {'id': 315, 'name': 'Airplane', 'freebase_id': '/m/0cmf2'}, - {'id': 316, 'name': 'Canary', 'freebase_id': '/m/0ccs93'}, - {'id': 317, 'name': 'Spatula', 'freebase_id': '/m/02d1br'}, - {'id': 318, 'name': 'Watch', 'freebase_id': '/m/0gjkl'}, - {'id': 319, 'name': 'Lily', 'freebase_id': '/m/0jqgx'}, - {'id': 320, 'name': 'Kitchen appliance', 'freebase_id': '/m/0h99cwc'}, - {'id': 321, 'name': 'Filing cabinet', 'freebase_id': '/m/047j0r'}, - {'id': 322, 'name': 'Aircraft', 'freebase_id': '/m/0k5j'}, - {'id': 323, 'name': 'Cake stand', 'freebase_id': '/m/0h8n6ft'}, - {'id': 324, 'name': 'Candy', 'freebase_id': '/m/0gm28'}, - {'id': 325, 'name': 'Sink', 'freebase_id': '/m/0130jx'}, - {'id': 326, 'name': 'Mouse', 'freebase_id': '/m/04rmv'}, - {'id': 327, 'name': 'Wine', 'freebase_id': '/m/081qc'}, - {'id': 328, 'name': 'Wheelchair', 'freebase_id': '/m/0qmmr'}, - {'id': 329, 'name': 'Goldfish', 'freebase_id': '/m/03fj2'}, - {'id': 330, 'name': 'Refrigerator', 'freebase_id': '/m/040b_t'}, - {'id': 331, 'name': 'French fries', 'freebase_id': '/m/02y6n'}, - {'id': 332, 'name': 'Drawer', 'freebase_id': '/m/0fqfqc'}, - {'id': 333, 'name': 'Treadmill', 'freebase_id': '/m/030610'}, - {'id': 334, 'name': 'Picnic basket', 'freebase_id': '/m/07kng9'}, - {'id': 335, 'name': 'Dice', 'freebase_id': '/m/029b3'}, - {'id': 336, 'name': 'Cabbage', 'freebase_id': '/m/0fbw6'}, - {'id': 337, 'name': 'Football helmet', 'freebase_id': '/m/07qxg_'}, - {'id': 338, 'name': 'Pig', 'freebase_id': '/m/068zj'}, - {'id': 339, 'name': 'Person', 'freebase_id': '/m/01g317'}, - {'id': 340, 'name': 'Shorts', 'freebase_id': '/m/01bfm9'}, - {'id': 341, 'name': 'Gondola', 'freebase_id': '/m/02068x'}, - {'id': 342, 'name': 'Honeycomb', 'freebase_id': '/m/0fz0h'}, - {'id': 343, 'name': 'Doughnut', 'freebase_id': '/m/0jy4k'}, - {'id': 344, 'name': 'Chest of drawers', 'freebase_id': '/m/05kyg_'}, - {'id': 345, 'name': 'Land vehicle', 'freebase_id': '/m/01prls'}, - {'id': 346, 'name': 'Bat', 'freebase_id': '/m/01h44'}, - {'id': 347, 'name': 'Monkey', 'freebase_id': '/m/08pbxl'}, - {'id': 348, 'name': 'Dagger', 'freebase_id': '/m/02gzp'}, - {'id': 349, 'name': 'Tableware', 'freebase_id': '/m/04brg2'}, - {'id': 350, 'name': 'Human foot', 'freebase_id': '/m/031n1'}, - {'id': 351, 'name': 'Mug', 'freebase_id': '/m/02jvh9'}, - {'id': 352, 'name': 'Alarm clock', 'freebase_id': '/m/046dlr'}, - {'id': 353, 'name': 'Pressure cooker', 'freebase_id': '/m/0h8ntjv'}, - {'id': 354, 'name': 'Human hand', 'freebase_id': '/m/0k65p'}, - {'id': 355, 'name': 'Tortoise', 'freebase_id': '/m/011k07'}, - {'id': 356, 'name': 'Baseball glove', 'freebase_id': '/m/03grzl'}, - {'id': 357, 'name': 'Sword', 'freebase_id': '/m/06y5r'}, - {'id': 358, 'name': 'Pear', 'freebase_id': '/m/061_f'}, - {'id': 359, 'name': 'Miniskirt', 'freebase_id': '/m/01cmb2'}, - {'id': 360, 'name': 'Traffic sign', 'freebase_id': '/m/01mqdt'}, - {'id': 361, 'name': 'Girl', 'freebase_id': '/m/05r655'}, - {'id': 362, 'name': 'Roller skates', 'freebase_id': '/m/02p3w7d'}, - {'id': 363, 'name': 'Dinosaur', 'freebase_id': '/m/029tx'}, - {'id': 364, 'name': 'Porch', 'freebase_id': '/m/04m6gz'}, - {'id': 365, 'name': 'Human beard', 'freebase_id': '/m/015h_t'}, - {'id': 366, 'name': 'Submarine sandwich', 'freebase_id': '/m/06pcq'}, - {'id': 367, 'name': 'Screwdriver', 'freebase_id': '/m/01bms0'}, - {'id': 368, 'name': 'Strawberry', 'freebase_id': '/m/07fbm7'}, - {'id': 369, 'name': 'Wine glass', 'freebase_id': '/m/09tvcd'}, - {'id': 370, 'name': 'Seafood', 'freebase_id': '/m/06nwz'}, - {'id': 371, 'name': 'Racket', 'freebase_id': '/m/0dv9c'}, - {'id': 372, 'name': 'Wheel', 'freebase_id': '/m/083wq'}, - {'id': 373, 'name': 'Sea lion', 'freebase_id': '/m/0gd36'}, - {'id': 374, 'name': 'Toy', 'freebase_id': '/m/0138tl'}, - {'id': 375, 'name': 'Tea', 'freebase_id': '/m/07clx'}, - {'id': 376, 'name': 'Tennis ball', 'freebase_id': '/m/05ctyq'}, - {'id': 377, 'name': 'Waste container', 'freebase_id': '/m/0bjyj5'}, - {'id': 378, 'name': 'Mule', 'freebase_id': '/m/0dbzx'}, - {'id': 379, 'name': 'Cricket ball', 'freebase_id': '/m/02ctlc'}, - {'id': 380, 'name': 'Pineapple', 'freebase_id': '/m/0fp6w'}, - {'id': 381, 'name': 'Coconut', 'freebase_id': '/m/0djtd'}, - {'id': 382, 'name': 'Doll', 'freebase_id': '/m/0167gd'}, - {'id': 383, 'name': 'Coffee table', 'freebase_id': '/m/078n6m'}, - {'id': 384, 'name': 'Snowman', 'freebase_id': '/m/0152hh'}, - {'id': 385, 'name': 'Lavender', 'freebase_id': '/m/04gth'}, - {'id': 386, 'name': 'Shrimp', 'freebase_id': '/m/0ll1f78'}, - {'id': 387, 'name': 'Maple', 'freebase_id': '/m/0cffdh'}, - {'id': 388, 'name': 'Cowboy hat', 'freebase_id': '/m/025rp__'}, - {'id': 389, 'name': 'Goggles', 'freebase_id': '/m/02_n6y'}, - {'id': 390, 'name': 'Rugby ball', 'freebase_id': '/m/0wdt60w'}, - {'id': 391, 'name': 'Caterpillar', 'freebase_id': '/m/0cydv'}, - {'id': 392, 'name': 'Poster', 'freebase_id': '/m/01n5jq'}, - {'id': 393, 'name': 'Rocket', 'freebase_id': '/m/09rvcxw'}, - {'id': 394, 'name': 'Organ', 'freebase_id': '/m/013y1f'}, - {'id': 395, 'name': 'Saxophone', 'freebase_id': '/m/06ncr'}, - {'id': 396, 'name': 'Traffic light', 'freebase_id': '/m/015qff'}, - {'id': 397, 'name': 'Cocktail', 'freebase_id': '/m/024g6'}, - {'id': 398, 'name': 'Plastic bag', 'freebase_id': '/m/05gqfk'}, - {'id': 399, 'name': 'Squash', 'freebase_id': '/m/0dv77'}, - {'id': 400, 'name': 'Mushroom', 'freebase_id': '/m/052sf'}, - {'id': 401, 'name': 'Hamburger', 'freebase_id': '/m/0cdn1'}, - {'id': 402, 'name': 'Light switch', 'freebase_id': '/m/03jbxj'}, - {'id': 403, 'name': 'Parachute', 'freebase_id': '/m/0cyfs'}, - {'id': 404, 'name': 'Teddy bear', 'freebase_id': '/m/0kmg4'}, - {'id': 405, 'name': 'Winter melon', 'freebase_id': '/m/02cvgx'}, - {'id': 406, 'name': 'Deer', 'freebase_id': '/m/09kx5'}, - {'id': 407, 'name': 'Musical keyboard', 'freebase_id': '/m/057cc'}, - {'id': 408, 'name': 'Plumbing fixture', 'freebase_id': '/m/02pkr5'}, - {'id': 409, 'name': 'Scoreboard', 'freebase_id': '/m/057p5t'}, - {'id': 410, 'name': 'Baseball bat', 'freebase_id': '/m/03g8mr'}, - {'id': 411, 'name': 'Envelope', 'freebase_id': '/m/0frqm'}, - {'id': 412, 'name': 'Adhesive tape', 'freebase_id': '/m/03m3vtv'}, - {'id': 413, 'name': 'Briefcase', 'freebase_id': '/m/0584n8'}, - {'id': 414, 'name': 'Paddle', 'freebase_id': '/m/014y4n'}, - {'id': 415, 'name': 'Bow and arrow', 'freebase_id': '/m/01g3x7'}, - {'id': 416, 'name': 'Telephone', 'freebase_id': '/m/07cx4'}, - {'id': 417, 'name': 'Sheep', 'freebase_id': '/m/07bgp'}, - {'id': 418, 'name': 'Jacket', 'freebase_id': '/m/032b3c'}, - {'id': 419, 'name': 'Boy', 'freebase_id': '/m/01bl7v'}, - {'id': 420, 'name': 'Pizza', 'freebase_id': '/m/0663v'}, - {'id': 421, 'name': 'Otter', 'freebase_id': '/m/0cn6p'}, - {'id': 422, 'name': 'Office supplies', 'freebase_id': '/m/02rdsp'}, - {'id': 423, 'name': 'Couch', 'freebase_id': '/m/02crq1'}, - {'id': 424, 'name': 'Cello', 'freebase_id': '/m/01xqw'}, - {'id': 425, 'name': 'Bull', 'freebase_id': '/m/0cnyhnx'}, - {'id': 426, 'name': 'Camel', 'freebase_id': '/m/01x_v'}, - {'id': 427, 'name': 'Ball', 'freebase_id': '/m/018xm'}, - {'id': 428, 'name': 'Duck', 'freebase_id': '/m/09ddx'}, - {'id': 429, 'name': 'Whale', 'freebase_id': '/m/084zz'}, - {'id': 430, 'name': 'Shirt', 'freebase_id': '/m/01n4qj'}, - {'id': 431, 'name': 'Tank', 'freebase_id': '/m/07cmd'}, - {'id': 432, 'name': 'Motorcycle', 'freebase_id': '/m/04_sv'}, - {'id': 433, 'name': 'Accordion', 'freebase_id': '/m/0mkg'}, - {'id': 434, 'name': 'Owl', 'freebase_id': '/m/09d5_'}, - {'id': 435, 'name': 'Porcupine', 'freebase_id': '/m/0c568'}, - {'id': 436, 'name': 'Sun hat', 'freebase_id': '/m/02wbtzl'}, - {'id': 437, 'name': 'Nail', 'freebase_id': '/m/05bm6'}, - {'id': 438, 'name': 'Scissors', 'freebase_id': '/m/01lsmm'}, - {'id': 439, 'name': 'Swan', 'freebase_id': '/m/0dftk'}, - {'id': 440, 'name': 'Lamp', 'freebase_id': '/m/0dtln'}, - {'id': 441, 'name': 'Crown', 'freebase_id': '/m/0nl46'}, - {'id': 442, 'name': 'Piano', 'freebase_id': '/m/05r5c'}, - {'id': 443, 'name': 'Sculpture', 'freebase_id': '/m/06msq'}, - {'id': 444, 'name': 'Cheetah', 'freebase_id': '/m/0cd4d'}, - {'id': 445, 'name': 'Oboe', 'freebase_id': '/m/05kms'}, - {'id': 446, 'name': 'Tin can', 'freebase_id': '/m/02jnhm'}, - {'id': 447, 'name': 'Mango', 'freebase_id': '/m/0fldg'}, - {'id': 448, 'name': 'Tripod', 'freebase_id': '/m/073bxn'}, - {'id': 449, 'name': 'Oven', 'freebase_id': '/m/029bxz'}, - {'id': 450, 'name': 'Mouse', 'freebase_id': '/m/020lf'}, - {'id': 451, 'name': 'Barge', 'freebase_id': '/m/01btn'}, - {'id': 452, 'name': 'Coffee', 'freebase_id': '/m/02vqfm'}, - {'id': 453, 'name': 'Snowboard', 'freebase_id': '/m/06__v'}, - {'id': 454, 'name': 'Common fig', 'freebase_id': '/m/043nyj'}, - {'id': 455, 'name': 'Salad', 'freebase_id': '/m/0grw1'}, - {'id': 456, 'name': 'Marine invertebrates', 'freebase_id': '/m/03hl4l9'}, - {'id': 457, 'name': 'Umbrella', 'freebase_id': '/m/0hnnb'}, - {'id': 458, 'name': 'Kangaroo', 'freebase_id': '/m/04c0y'}, - {'id': 459, 'name': 'Human arm', 'freebase_id': '/m/0dzf4'}, - {'id': 460, 'name': 'Measuring cup', 'freebase_id': '/m/07v9_z'}, - {'id': 461, 'name': 'Snail', 'freebase_id': '/m/0f9_l'}, - {'id': 462, 'name': 'Loveseat', 'freebase_id': '/m/0703r8'}, - {'id': 463, 'name': 'Suit', 'freebase_id': '/m/01xyhv'}, - {'id': 464, 'name': 'Teapot', 'freebase_id': '/m/01fh4r'}, - {'id': 465, 'name': 'Bottle', 'freebase_id': '/m/04dr76w'}, - {'id': 466, 'name': 'Alpaca', 'freebase_id': '/m/0pcr'}, - {'id': 467, 'name': 'Kettle', 'freebase_id': '/m/03s_tn'}, - {'id': 468, 'name': 'Trousers', 'freebase_id': '/m/07mhn'}, - {'id': 469, 'name': 'Popcorn', 'freebase_id': '/m/01hrv5'}, - {'id': 470, 'name': 'Centipede', 'freebase_id': '/m/019h78'}, - {'id': 471, 'name': 'Spider', 'freebase_id': '/m/09kmb'}, - {'id': 472, 'name': 'Sparrow', 'freebase_id': '/m/0h23m'}, - {'id': 473, 'name': 'Plate', 'freebase_id': '/m/050gv4'}, - {'id': 474, 'name': 'Bagel', 'freebase_id': '/m/01fb_0'}, - {'id': 475, 'name': 'Personal care', 'freebase_id': '/m/02w3_ws'}, - {'id': 476, 'name': 'Apple', 'freebase_id': '/m/014j1m'}, - {'id': 477, 'name': 'Brassiere', 'freebase_id': '/m/01gmv2'}, - {'id': 478, 'name': 'Bathroom cabinet', 'freebase_id': '/m/04y4h8h'}, - {'id': 479, 'name': 'studio couch', 'freebase_id': '/m/026qbn5'}, - {'id': 480, 'name': 'Computer keyboard', 'freebase_id': '/m/01m2v'}, - {'id': 481, 'name': 'Table tennis racket', 'freebase_id': '/m/05_5p_0'}, - {'id': 482, 'name': 'Sushi', 'freebase_id': '/m/07030'}, - {'id': 483, 'name': 'Cabinetry', 'freebase_id': '/m/01s105'}, - {'id': 484, 'name': 'Street light', 'freebase_id': '/m/033rq4'}, - {'id': 485, 'name': 'Towel', 'freebase_id': '/m/0162_1'}, - {'id': 486, 'name': 'Nightstand', 'freebase_id': '/m/02z51p'}, - {'id': 487, 'name': 'Rabbit', 'freebase_id': '/m/06mf6'}, - {'id': 488, 'name': 'Dolphin', 'freebase_id': '/m/02hj4'}, - {'id': 489, 'name': 'Dog', 'freebase_id': '/m/0bt9lr'}, - {'id': 490, 'name': 'Jug', 'freebase_id': '/m/08hvt4'}, - {'id': 491, 'name': 'Wok', 'freebase_id': '/m/084rd'}, - {'id': 492, 'name': 'Fire hydrant', 'freebase_id': '/m/01pns0'}, - {'id': 493, 'name': 'Human eye', 'freebase_id': '/m/014sv8'}, - {'id': 494, 'name': 'Skyscraper', 'freebase_id': '/m/079cl'}, - {'id': 495, 'name': 'Backpack', 'freebase_id': '/m/01940j'}, - {'id': 496, 'name': 'Potato', 'freebase_id': '/m/05vtc'}, - {'id': 497, 'name': 'Paper towel', 'freebase_id': '/m/02w3r3'}, - {'id': 498, 'name': 'Lifejacket', 'freebase_id': '/m/054xkw'}, - {'id': 499, 'name': 'Bicycle wheel', 'freebase_id': '/m/01bqk0'}, - {'id': 500, 'name': 'Toilet', 'freebase_id': '/m/09g1w'}, + {"id": 1, "name": "Infant bed", "freebase_id": "/m/061hd_"}, + {"id": 2, "name": "Rose", "freebase_id": "/m/06m11"}, + {"id": 3, "name": "Flag", "freebase_id": "/m/03120"}, + {"id": 4, "name": "Flashlight", "freebase_id": "/m/01kb5b"}, + {"id": 5, "name": "Sea turtle", "freebase_id": "/m/0120dh"}, + {"id": 6, "name": "Camera", "freebase_id": "/m/0dv5r"}, + {"id": 7, "name": "Animal", "freebase_id": "/m/0jbk"}, + {"id": 8, "name": "Glove", "freebase_id": "/m/0174n1"}, + {"id": 9, "name": "Crocodile", "freebase_id": "/m/09f_2"}, + {"id": 10, "name": "Cattle", "freebase_id": "/m/01xq0k1"}, + {"id": 11, "name": "House", "freebase_id": "/m/03jm5"}, + {"id": 12, "name": "Guacamole", "freebase_id": "/m/02g30s"}, + {"id": 13, "name": "Penguin", "freebase_id": "/m/05z6w"}, + {"id": 14, "name": "Vehicle registration plate", "freebase_id": "/m/01jfm_"}, + {"id": 15, "name": "Bench", "freebase_id": "/m/076lb9"}, + {"id": 16, "name": "Ladybug", "freebase_id": "/m/0gj37"}, + {"id": 17, "name": "Human nose", "freebase_id": "/m/0k0pj"}, + {"id": 18, "name": "Watermelon", "freebase_id": "/m/0kpqd"}, + {"id": 19, "name": "Flute", "freebase_id": "/m/0l14j_"}, + {"id": 20, "name": "Butterfly", "freebase_id": "/m/0cyf8"}, + {"id": 21, "name": "Washing machine", "freebase_id": "/m/0174k2"}, + {"id": 22, "name": "Raccoon", "freebase_id": "/m/0dq75"}, + {"id": 23, "name": "Segway", "freebase_id": "/m/076bq"}, + {"id": 24, "name": "Taco", "freebase_id": "/m/07crc"}, + {"id": 25, "name": "Jellyfish", "freebase_id": "/m/0d8zb"}, + {"id": 26, "name": "Cake", "freebase_id": "/m/0fszt"}, + {"id": 27, "name": "Pen", "freebase_id": "/m/0k1tl"}, + {"id": 28, "name": "Cannon", "freebase_id": "/m/020kz"}, + {"id": 29, "name": "Bread", "freebase_id": "/m/09728"}, + {"id": 30, "name": "Tree", "freebase_id": "/m/07j7r"}, + {"id": 31, "name": "Shellfish", "freebase_id": "/m/0fbdv"}, + {"id": 32, "name": "Bed", "freebase_id": "/m/03ssj5"}, + {"id": 33, "name": "Hamster", "freebase_id": "/m/03qrc"}, + {"id": 34, "name": "Hat", "freebase_id": "/m/02dl1y"}, + {"id": 35, "name": "Toaster", "freebase_id": "/m/01k6s3"}, + {"id": 36, "name": "Sombrero", "freebase_id": "/m/02jfl0"}, + {"id": 37, "name": "Tiara", "freebase_id": "/m/01krhy"}, + {"id": 38, "name": "Bowl", "freebase_id": "/m/04kkgm"}, + {"id": 39, "name": "Dragonfly", "freebase_id": "/m/0ft9s"}, + {"id": 40, "name": "Moths and butterflies", "freebase_id": "/m/0d_2m"}, + {"id": 41, "name": "Antelope", "freebase_id": "/m/0czz2"}, + {"id": 42, "name": "Vegetable", "freebase_id": "/m/0f4s2w"}, + {"id": 43, "name": "Torch", "freebase_id": "/m/07dd4"}, + {"id": 44, "name": "Building", "freebase_id": "/m/0cgh4"}, + {"id": 45, "name": "Power plugs and sockets", "freebase_id": "/m/03bbps"}, + {"id": 46, "name": "Blender", "freebase_id": "/m/02pjr4"}, + {"id": 47, "name": "Billiard table", "freebase_id": "/m/04p0qw"}, + {"id": 48, "name": "Cutting board", "freebase_id": "/m/02pdsw"}, + {"id": 49, "name": "Bronze sculpture", "freebase_id": "/m/01yx86"}, + {"id": 50, "name": "Turtle", "freebase_id": "/m/09dzg"}, + {"id": 51, "name": "Broccoli", "freebase_id": "/m/0hkxq"}, + {"id": 52, "name": "Tiger", "freebase_id": "/m/07dm6"}, + {"id": 53, "name": "Mirror", "freebase_id": "/m/054_l"}, + {"id": 54, "name": "Bear", "freebase_id": "/m/01dws"}, + {"id": 55, "name": "Zucchini", "freebase_id": "/m/027pcv"}, + {"id": 56, "name": "Dress", "freebase_id": "/m/01d40f"}, + {"id": 57, "name": "Volleyball", "freebase_id": "/m/02rgn06"}, + {"id": 58, "name": "Guitar", "freebase_id": "/m/0342h"}, + {"id": 59, "name": "Reptile", "freebase_id": "/m/06bt6"}, + {"id": 60, "name": "Golf cart", "freebase_id": "/m/0323sq"}, + {"id": 61, "name": "Tart", "freebase_id": "/m/02zvsm"}, + {"id": 62, "name": "Fedora", "freebase_id": "/m/02fq_6"}, + {"id": 63, "name": "Carnivore", "freebase_id": "/m/01lrl"}, + {"id": 64, "name": "Car", "freebase_id": "/m/0k4j"}, + {"id": 65, "name": "Lighthouse", "freebase_id": "/m/04h7h"}, + {"id": 66, "name": "Coffeemaker", "freebase_id": "/m/07xyvk"}, + {"id": 67, "name": "Food processor", "freebase_id": "/m/03y6mg"}, + {"id": 68, "name": "Truck", "freebase_id": "/m/07r04"}, + {"id": 69, "name": "Bookcase", "freebase_id": "/m/03__z0"}, + {"id": 70, "name": "Surfboard", "freebase_id": "/m/019w40"}, + {"id": 71, "name": "Footwear", "freebase_id": "/m/09j5n"}, + {"id": 72, "name": "Bench", "freebase_id": "/m/0cvnqh"}, + {"id": 73, "name": "Necklace", "freebase_id": "/m/01llwg"}, + {"id": 74, "name": "Flower", "freebase_id": "/m/0c9ph5"}, + {"id": 75, "name": "Radish", "freebase_id": "/m/015x5n"}, + {"id": 76, "name": "Marine mammal", "freebase_id": "/m/0gd2v"}, + {"id": 77, "name": "Frying pan", "freebase_id": "/m/04v6l4"}, + {"id": 78, "name": "Tap", "freebase_id": "/m/02jz0l"}, + {"id": 79, "name": "Peach", "freebase_id": "/m/0dj6p"}, + {"id": 80, "name": "Knife", "freebase_id": "/m/04ctx"}, + {"id": 81, "name": "Handbag", "freebase_id": "/m/080hkjn"}, + {"id": 82, "name": "Laptop", "freebase_id": "/m/01c648"}, + {"id": 83, "name": "Tent", "freebase_id": "/m/01j61q"}, + {"id": 84, "name": "Ambulance", "freebase_id": "/m/012n7d"}, + {"id": 85, "name": "Christmas tree", "freebase_id": "/m/025nd"}, + {"id": 86, "name": "Eagle", "freebase_id": "/m/09csl"}, + {"id": 87, "name": "Limousine", "freebase_id": "/m/01lcw4"}, + {"id": 88, "name": "Kitchen & dining room table", "freebase_id": "/m/0h8n5zk"}, + {"id": 89, "name": "Polar bear", "freebase_id": "/m/0633h"}, + {"id": 90, "name": "Tower", "freebase_id": "/m/01fdzj"}, + {"id": 91, "name": "Football", "freebase_id": "/m/01226z"}, + {"id": 92, "name": "Willow", "freebase_id": "/m/0mw_6"}, + {"id": 93, "name": "Human head", "freebase_id": "/m/04hgtk"}, + {"id": 94, "name": "Stop sign", "freebase_id": "/m/02pv19"}, + {"id": 95, "name": "Banana", "freebase_id": "/m/09qck"}, + {"id": 96, "name": "Mixer", "freebase_id": "/m/063rgb"}, + {"id": 97, "name": "Binoculars", "freebase_id": "/m/0lt4_"}, + {"id": 98, "name": "Dessert", "freebase_id": "/m/0270h"}, + {"id": 99, "name": "Bee", "freebase_id": "/m/01h3n"}, + {"id": 100, "name": "Chair", "freebase_id": "/m/01mzpv"}, + {"id": 101, "name": "Wood-burning stove", "freebase_id": "/m/04169hn"}, + {"id": 102, "name": "Flowerpot", "freebase_id": "/m/0fm3zh"}, + {"id": 103, "name": "Beaker", "freebase_id": "/m/0d20w4"}, + {"id": 104, "name": "Oyster", "freebase_id": "/m/0_cp5"}, + {"id": 105, "name": "Woodpecker", "freebase_id": "/m/01dy8n"}, + {"id": 106, "name": "Harp", "freebase_id": "/m/03m5k"}, + {"id": 107, "name": "Bathtub", "freebase_id": "/m/03dnzn"}, + {"id": 108, "name": "Wall clock", "freebase_id": "/m/0h8mzrc"}, + {"id": 109, "name": "Sports uniform", "freebase_id": "/m/0h8mhzd"}, + {"id": 110, "name": "Rhinoceros", "freebase_id": "/m/03d443"}, + {"id": 111, "name": "Beehive", "freebase_id": "/m/01gllr"}, + {"id": 112, "name": "Cupboard", "freebase_id": "/m/0642b4"}, + {"id": 113, "name": "Chicken", "freebase_id": "/m/09b5t"}, + {"id": 114, "name": "Man", "freebase_id": "/m/04yx4"}, + {"id": 115, "name": "Blue jay", "freebase_id": "/m/01f8m5"}, + {"id": 116, "name": "Cucumber", "freebase_id": "/m/015x4r"}, + {"id": 117, "name": "Balloon", "freebase_id": "/m/01j51"}, + {"id": 118, "name": "Kite", "freebase_id": "/m/02zt3"}, + {"id": 119, "name": "Fireplace", "freebase_id": "/m/03tw93"}, + {"id": 120, "name": "Lantern", "freebase_id": "/m/01jfsr"}, + {"id": 121, "name": "Missile", "freebase_id": "/m/04ylt"}, + {"id": 122, "name": "Book", "freebase_id": "/m/0bt_c3"}, + {"id": 123, "name": "Spoon", "freebase_id": "/m/0cmx8"}, + {"id": 124, "name": "Grapefruit", "freebase_id": "/m/0hqkz"}, + {"id": 125, "name": "Squirrel", "freebase_id": "/m/071qp"}, + {"id": 126, "name": "Orange", "freebase_id": "/m/0cyhj_"}, + {"id": 127, "name": "Coat", "freebase_id": "/m/01xygc"}, + {"id": 128, "name": "Punching bag", "freebase_id": "/m/0420v5"}, + {"id": 129, "name": "Zebra", "freebase_id": "/m/0898b"}, + {"id": 130, "name": "Billboard", "freebase_id": "/m/01knjb"}, + {"id": 131, "name": "Bicycle", "freebase_id": "/m/0199g"}, + {"id": 132, "name": "Door handle", "freebase_id": "/m/03c7gz"}, + {"id": 133, "name": "Mechanical fan", "freebase_id": "/m/02x984l"}, + {"id": 134, "name": "Ring binder", "freebase_id": "/m/04zwwv"}, + {"id": 135, "name": "Table", "freebase_id": "/m/04bcr3"}, + {"id": 136, "name": "Parrot", "freebase_id": "/m/0gv1x"}, + {"id": 137, "name": "Sock", "freebase_id": "/m/01nq26"}, + {"id": 138, "name": "Vase", "freebase_id": "/m/02s195"}, + {"id": 139, "name": "Weapon", "freebase_id": "/m/083kb"}, + {"id": 140, "name": "Shotgun", "freebase_id": "/m/06nrc"}, + {"id": 141, "name": "Glasses", "freebase_id": "/m/0jyfg"}, + {"id": 142, "name": "Seahorse", "freebase_id": "/m/0nybt"}, + {"id": 143, "name": "Belt", "freebase_id": "/m/0176mf"}, + {"id": 144, "name": "Watercraft", "freebase_id": "/m/01rzcn"}, + {"id": 145, "name": "Window", "freebase_id": "/m/0d4v4"}, + {"id": 146, "name": "Giraffe", "freebase_id": "/m/03bk1"}, + {"id": 147, "name": "Lion", "freebase_id": "/m/096mb"}, + {"id": 148, "name": "Tire", "freebase_id": "/m/0h9mv"}, + {"id": 149, "name": "Vehicle", "freebase_id": "/m/07yv9"}, + {"id": 150, "name": "Canoe", "freebase_id": "/m/0ph39"}, + {"id": 151, "name": "Tie", "freebase_id": "/m/01rkbr"}, + {"id": 152, "name": "Shelf", "freebase_id": "/m/0gjbg72"}, + {"id": 153, "name": "Picture frame", "freebase_id": "/m/06z37_"}, + {"id": 154, "name": "Printer", "freebase_id": "/m/01m4t"}, + {"id": 155, "name": "Human leg", "freebase_id": "/m/035r7c"}, + {"id": 156, "name": "Boat", "freebase_id": "/m/019jd"}, + {"id": 157, "name": "Slow cooker", "freebase_id": "/m/02tsc9"}, + {"id": 158, "name": "Croissant", "freebase_id": "/m/015wgc"}, + {"id": 159, "name": "Candle", "freebase_id": "/m/0c06p"}, + {"id": 160, "name": "Pancake", "freebase_id": "/m/01dwwc"}, + {"id": 161, "name": "Pillow", "freebase_id": "/m/034c16"}, + {"id": 162, "name": "Coin", "freebase_id": "/m/0242l"}, + {"id": 163, "name": "Stretcher", "freebase_id": "/m/02lbcq"}, + {"id": 164, "name": "Sandal", "freebase_id": "/m/03nfch"}, + {"id": 165, "name": "Woman", "freebase_id": "/m/03bt1vf"}, + {"id": 166, "name": "Stairs", "freebase_id": "/m/01lynh"}, + {"id": 167, "name": "Harpsichord", "freebase_id": "/m/03q5t"}, + {"id": 168, "name": "Stool", "freebase_id": "/m/0fqt361"}, + {"id": 169, "name": "Bus", "freebase_id": "/m/01bjv"}, + {"id": 170, "name": "Suitcase", "freebase_id": "/m/01s55n"}, + {"id": 171, "name": "Human mouth", "freebase_id": "/m/0283dt1"}, + {"id": 172, "name": "Juice", "freebase_id": "/m/01z1kdw"}, + {"id": 173, "name": "Skull", "freebase_id": "/m/016m2d"}, + {"id": 174, "name": "Door", "freebase_id": "/m/02dgv"}, + {"id": 175, "name": "Violin", "freebase_id": "/m/07y_7"}, + {"id": 176, "name": "Chopsticks", "freebase_id": "/m/01_5g"}, + {"id": 177, "name": "Digital clock", "freebase_id": "/m/06_72j"}, + {"id": 178, "name": "Sunflower", "freebase_id": "/m/0ftb8"}, + {"id": 179, "name": "Leopard", "freebase_id": "/m/0c29q"}, + {"id": 180, "name": "Bell pepper", "freebase_id": "/m/0jg57"}, + {"id": 181, "name": "Harbor seal", "freebase_id": "/m/02l8p9"}, + {"id": 182, "name": "Snake", "freebase_id": "/m/078jl"}, + {"id": 183, "name": "Sewing machine", "freebase_id": "/m/0llzx"}, + {"id": 184, "name": "Goose", "freebase_id": "/m/0dbvp"}, + {"id": 185, "name": "Helicopter", "freebase_id": "/m/09ct_"}, + {"id": 186, "name": "Seat belt", "freebase_id": "/m/0dkzw"}, + {"id": 187, "name": "Coffee cup", "freebase_id": "/m/02p5f1q"}, + {"id": 188, "name": "Microwave oven", "freebase_id": "/m/0fx9l"}, + {"id": 189, "name": "Hot dog", "freebase_id": "/m/01b9xk"}, + {"id": 190, "name": "Countertop", "freebase_id": "/m/0b3fp9"}, + {"id": 191, "name": "Serving tray", "freebase_id": "/m/0h8n27j"}, + {"id": 192, "name": "Dog bed", "freebase_id": "/m/0h8n6f9"}, + {"id": 193, "name": "Beer", "freebase_id": "/m/01599"}, + {"id": 194, "name": "Sunglasses", "freebase_id": "/m/017ftj"}, + {"id": 195, "name": "Golf ball", "freebase_id": "/m/044r5d"}, + {"id": 196, "name": "Waffle", "freebase_id": "/m/01dwsz"}, + {"id": 197, "name": "Palm tree", "freebase_id": "/m/0cdl1"}, + {"id": 198, "name": "Trumpet", "freebase_id": "/m/07gql"}, + {"id": 199, "name": "Ruler", "freebase_id": "/m/0hdln"}, + {"id": 200, "name": "Helmet", "freebase_id": "/m/0zvk5"}, + {"id": 201, "name": "Ladder", "freebase_id": "/m/012w5l"}, + {"id": 202, "name": "Office building", "freebase_id": "/m/021sj1"}, + {"id": 203, "name": "Tablet computer", "freebase_id": "/m/0bh9flk"}, + {"id": 204, "name": "Toilet paper", "freebase_id": "/m/09gtd"}, + {"id": 205, "name": "Pomegranate", "freebase_id": "/m/0jwn_"}, + {"id": 206, "name": "Skirt", "freebase_id": "/m/02wv6h6"}, + {"id": 207, "name": "Gas stove", "freebase_id": "/m/02wv84t"}, + {"id": 208, "name": "Cookie", "freebase_id": "/m/021mn"}, + {"id": 209, "name": "Cart", "freebase_id": "/m/018p4k"}, + {"id": 210, "name": "Raven", "freebase_id": "/m/06j2d"}, + {"id": 211, "name": "Egg", "freebase_id": "/m/033cnk"}, + {"id": 212, "name": "Burrito", "freebase_id": "/m/01j3zr"}, + {"id": 213, "name": "Goat", "freebase_id": "/m/03fwl"}, + {"id": 214, "name": "Kitchen knife", "freebase_id": "/m/058qzx"}, + {"id": 215, "name": "Skateboard", "freebase_id": "/m/06_fw"}, + {"id": 216, "name": "Salt and pepper shakers", "freebase_id": "/m/02x8cch"}, + {"id": 217, "name": "Lynx", "freebase_id": "/m/04g2r"}, + {"id": 218, "name": "Boot", "freebase_id": "/m/01b638"}, + {"id": 219, "name": "Platter", "freebase_id": "/m/099ssp"}, + {"id": 220, "name": "Ski", "freebase_id": "/m/071p9"}, + {"id": 221, "name": "Swimwear", "freebase_id": "/m/01gkx_"}, + {"id": 222, "name": "Swimming pool", "freebase_id": "/m/0b_rs"}, + {"id": 223, "name": "Drinking straw", "freebase_id": "/m/03v5tg"}, + {"id": 224, "name": "Wrench", "freebase_id": "/m/01j5ks"}, + {"id": 225, "name": "Drum", "freebase_id": "/m/026t6"}, + {"id": 226, "name": "Ant", "freebase_id": "/m/0_k2"}, + {"id": 227, "name": "Human ear", "freebase_id": "/m/039xj_"}, + {"id": 228, "name": "Headphones", "freebase_id": "/m/01b7fy"}, + {"id": 229, "name": "Fountain", "freebase_id": "/m/0220r2"}, + {"id": 230, "name": "Bird", "freebase_id": "/m/015p6"}, + {"id": 231, "name": "Jeans", "freebase_id": "/m/0fly7"}, + {"id": 232, "name": "Television", "freebase_id": "/m/07c52"}, + {"id": 233, "name": "Crab", "freebase_id": "/m/0n28_"}, + {"id": 234, "name": "Microphone", "freebase_id": "/m/0hg7b"}, + {"id": 235, "name": "Home appliance", "freebase_id": "/m/019dx1"}, + {"id": 236, "name": "Snowplow", "freebase_id": "/m/04vv5k"}, + {"id": 237, "name": "Beetle", "freebase_id": "/m/020jm"}, + {"id": 238, "name": "Artichoke", "freebase_id": "/m/047v4b"}, + {"id": 239, "name": "Jet ski", "freebase_id": "/m/01xs3r"}, + {"id": 240, "name": "Stationary bicycle", "freebase_id": "/m/03kt2w"}, + {"id": 241, "name": "Human hair", "freebase_id": "/m/03q69"}, + {"id": 242, "name": "Brown bear", "freebase_id": "/m/01dxs"}, + {"id": 243, "name": "Starfish", "freebase_id": "/m/01h8tj"}, + {"id": 244, "name": "Fork", "freebase_id": "/m/0dt3t"}, + {"id": 245, "name": "Lobster", "freebase_id": "/m/0cjq5"}, + {"id": 246, "name": "Corded phone", "freebase_id": "/m/0h8lkj8"}, + {"id": 247, "name": "Drink", "freebase_id": "/m/0271t"}, + {"id": 248, "name": "Saucer", "freebase_id": "/m/03q5c7"}, + {"id": 249, "name": "Carrot", "freebase_id": "/m/0fj52s"}, + {"id": 250, "name": "Insect", "freebase_id": "/m/03vt0"}, + {"id": 251, "name": "Clock", "freebase_id": "/m/01x3z"}, + {"id": 252, "name": "Castle", "freebase_id": "/m/0d5gx"}, + {"id": 253, "name": "Tennis racket", "freebase_id": "/m/0h8my_4"}, + {"id": 254, "name": "Ceiling fan", "freebase_id": "/m/03ldnb"}, + {"id": 255, "name": "Asparagus", "freebase_id": "/m/0cjs7"}, + {"id": 256, "name": "Jaguar", "freebase_id": "/m/0449p"}, + {"id": 257, "name": "Musical instrument", "freebase_id": "/m/04szw"}, + {"id": 258, "name": "Train", "freebase_id": "/m/07jdr"}, + {"id": 259, "name": "Cat", "freebase_id": "/m/01yrx"}, + {"id": 260, "name": "Rifle", "freebase_id": "/m/06c54"}, + {"id": 261, "name": "Dumbbell", "freebase_id": "/m/04h8sr"}, + {"id": 262, "name": "Mobile phone", "freebase_id": "/m/050k8"}, + {"id": 263, "name": "Taxi", "freebase_id": "/m/0pg52"}, + {"id": 264, "name": "Shower", "freebase_id": "/m/02f9f_"}, + {"id": 265, "name": "Pitcher", "freebase_id": "/m/054fyh"}, + {"id": 266, "name": "Lemon", "freebase_id": "/m/09k_b"}, + {"id": 267, "name": "Invertebrate", "freebase_id": "/m/03xxp"}, + {"id": 268, "name": "Turkey", "freebase_id": "/m/0jly1"}, + {"id": 269, "name": "High heels", "freebase_id": "/m/06k2mb"}, + {"id": 270, "name": "Bust", "freebase_id": "/m/04yqq2"}, + {"id": 271, "name": "Elephant", "freebase_id": "/m/0bwd_0j"}, + {"id": 272, "name": "Scarf", "freebase_id": "/m/02h19r"}, + {"id": 273, "name": "Barrel", "freebase_id": "/m/02zn6n"}, + {"id": 274, "name": "Trombone", "freebase_id": "/m/07c6l"}, + {"id": 275, "name": "Pumpkin", "freebase_id": "/m/05zsy"}, + {"id": 276, "name": "Box", "freebase_id": "/m/025dyy"}, + {"id": 277, "name": "Tomato", "freebase_id": "/m/07j87"}, + {"id": 278, "name": "Frog", "freebase_id": "/m/09ld4"}, + {"id": 279, "name": "Bidet", "freebase_id": "/m/01vbnl"}, + {"id": 280, "name": "Human face", "freebase_id": "/m/0dzct"}, + {"id": 281, "name": "Houseplant", "freebase_id": "/m/03fp41"}, + {"id": 282, "name": "Van", "freebase_id": "/m/0h2r6"}, + {"id": 283, "name": "Shark", "freebase_id": "/m/0by6g"}, + {"id": 284, "name": "Ice cream", "freebase_id": "/m/0cxn2"}, + {"id": 285, "name": "Swim cap", "freebase_id": "/m/04tn4x"}, + {"id": 286, "name": "Falcon", "freebase_id": "/m/0f6wt"}, + {"id": 287, "name": "Ostrich", "freebase_id": "/m/05n4y"}, + {"id": 288, "name": "Handgun", "freebase_id": "/m/0gxl3"}, + {"id": 289, "name": "Whiteboard", "freebase_id": "/m/02d9qx"}, + {"id": 290, "name": "Lizard", "freebase_id": "/m/04m9y"}, + {"id": 291, "name": "Pasta", "freebase_id": "/m/05z55"}, + {"id": 292, "name": "Snowmobile", "freebase_id": "/m/01x3jk"}, + {"id": 293, "name": "Light bulb", "freebase_id": "/m/0h8l4fh"}, + {"id": 294, "name": "Window blind", "freebase_id": "/m/031b6r"}, + {"id": 295, "name": "Muffin", "freebase_id": "/m/01tcjp"}, + {"id": 296, "name": "Pretzel", "freebase_id": "/m/01f91_"}, + {"id": 297, "name": "Computer monitor", "freebase_id": "/m/02522"}, + {"id": 298, "name": "Horn", "freebase_id": "/m/0319l"}, + {"id": 299, "name": "Furniture", "freebase_id": "/m/0c_jw"}, + {"id": 300, "name": "Sandwich", "freebase_id": "/m/0l515"}, + {"id": 301, "name": "Fox", "freebase_id": "/m/0306r"}, + {"id": 302, "name": "Convenience store", "freebase_id": "/m/0crjs"}, + {"id": 303, "name": "Fish", "freebase_id": "/m/0ch_cf"}, + {"id": 304, "name": "Fruit", "freebase_id": "/m/02xwb"}, + {"id": 305, "name": "Earrings", "freebase_id": "/m/01r546"}, + {"id": 306, "name": "Curtain", "freebase_id": "/m/03rszm"}, + {"id": 307, "name": "Grape", "freebase_id": "/m/0388q"}, + {"id": 308, "name": "Sofa bed", "freebase_id": "/m/03m3pdh"}, + {"id": 309, "name": "Horse", "freebase_id": "/m/03k3r"}, + {"id": 310, "name": "Luggage and bags", "freebase_id": "/m/0hf58v5"}, + {"id": 311, "name": "Desk", "freebase_id": "/m/01y9k5"}, + {"id": 312, "name": "Crutch", "freebase_id": "/m/05441v"}, + {"id": 313, "name": "Bicycle helmet", "freebase_id": "/m/03p3bw"}, + {"id": 314, "name": "Tick", "freebase_id": "/m/0175cv"}, + {"id": 315, "name": "Airplane", "freebase_id": "/m/0cmf2"}, + {"id": 316, "name": "Canary", "freebase_id": "/m/0ccs93"}, + {"id": 317, "name": "Spatula", "freebase_id": "/m/02d1br"}, + {"id": 318, "name": "Watch", "freebase_id": "/m/0gjkl"}, + {"id": 319, "name": "Lily", "freebase_id": "/m/0jqgx"}, + {"id": 320, "name": "Kitchen appliance", "freebase_id": "/m/0h99cwc"}, + {"id": 321, "name": "Filing cabinet", "freebase_id": "/m/047j0r"}, + {"id": 322, "name": "Aircraft", "freebase_id": "/m/0k5j"}, + {"id": 323, "name": "Cake stand", "freebase_id": "/m/0h8n6ft"}, + {"id": 324, "name": "Candy", "freebase_id": "/m/0gm28"}, + {"id": 325, "name": "Sink", "freebase_id": "/m/0130jx"}, + {"id": 326, "name": "Mouse", "freebase_id": "/m/04rmv"}, + {"id": 327, "name": "Wine", "freebase_id": "/m/081qc"}, + {"id": 328, "name": "Wheelchair", "freebase_id": "/m/0qmmr"}, + {"id": 329, "name": "Goldfish", "freebase_id": "/m/03fj2"}, + {"id": 330, "name": "Refrigerator", "freebase_id": "/m/040b_t"}, + {"id": 331, "name": "French fries", "freebase_id": "/m/02y6n"}, + {"id": 332, "name": "Drawer", "freebase_id": "/m/0fqfqc"}, + {"id": 333, "name": "Treadmill", "freebase_id": "/m/030610"}, + {"id": 334, "name": "Picnic basket", "freebase_id": "/m/07kng9"}, + {"id": 335, "name": "Dice", "freebase_id": "/m/029b3"}, + {"id": 336, "name": "Cabbage", "freebase_id": "/m/0fbw6"}, + {"id": 337, "name": "Football helmet", "freebase_id": "/m/07qxg_"}, + {"id": 338, "name": "Pig", "freebase_id": "/m/068zj"}, + {"id": 339, "name": "Person", "freebase_id": "/m/01g317"}, + {"id": 340, "name": "Shorts", "freebase_id": "/m/01bfm9"}, + {"id": 341, "name": "Gondola", "freebase_id": "/m/02068x"}, + {"id": 342, "name": "Honeycomb", "freebase_id": "/m/0fz0h"}, + {"id": 343, "name": "Doughnut", "freebase_id": "/m/0jy4k"}, + {"id": 344, "name": "Chest of drawers", "freebase_id": "/m/05kyg_"}, + {"id": 345, "name": "Land vehicle", "freebase_id": "/m/01prls"}, + {"id": 346, "name": "Bat", "freebase_id": "/m/01h44"}, + {"id": 347, "name": "Monkey", "freebase_id": "/m/08pbxl"}, + {"id": 348, "name": "Dagger", "freebase_id": "/m/02gzp"}, + {"id": 349, "name": "Tableware", "freebase_id": "/m/04brg2"}, + {"id": 350, "name": "Human foot", "freebase_id": "/m/031n1"}, + {"id": 351, "name": "Mug", "freebase_id": "/m/02jvh9"}, + {"id": 352, "name": "Alarm clock", "freebase_id": "/m/046dlr"}, + {"id": 353, "name": "Pressure cooker", "freebase_id": "/m/0h8ntjv"}, + {"id": 354, "name": "Human hand", "freebase_id": "/m/0k65p"}, + {"id": 355, "name": "Tortoise", "freebase_id": "/m/011k07"}, + {"id": 356, "name": "Baseball glove", "freebase_id": "/m/03grzl"}, + {"id": 357, "name": "Sword", "freebase_id": "/m/06y5r"}, + {"id": 358, "name": "Pear", "freebase_id": "/m/061_f"}, + {"id": 359, "name": "Miniskirt", "freebase_id": "/m/01cmb2"}, + {"id": 360, "name": "Traffic sign", "freebase_id": "/m/01mqdt"}, + {"id": 361, "name": "Girl", "freebase_id": "/m/05r655"}, + {"id": 362, "name": "Roller skates", "freebase_id": "/m/02p3w7d"}, + {"id": 363, "name": "Dinosaur", "freebase_id": "/m/029tx"}, + {"id": 364, "name": "Porch", "freebase_id": "/m/04m6gz"}, + {"id": 365, "name": "Human beard", "freebase_id": "/m/015h_t"}, + {"id": 366, "name": "Submarine sandwich", "freebase_id": "/m/06pcq"}, + {"id": 367, "name": "Screwdriver", "freebase_id": "/m/01bms0"}, + {"id": 368, "name": "Strawberry", "freebase_id": "/m/07fbm7"}, + {"id": 369, "name": "Wine glass", "freebase_id": "/m/09tvcd"}, + {"id": 370, "name": "Seafood", "freebase_id": "/m/06nwz"}, + {"id": 371, "name": "Racket", "freebase_id": "/m/0dv9c"}, + {"id": 372, "name": "Wheel", "freebase_id": "/m/083wq"}, + {"id": 373, "name": "Sea lion", "freebase_id": "/m/0gd36"}, + {"id": 374, "name": "Toy", "freebase_id": "/m/0138tl"}, + {"id": 375, "name": "Tea", "freebase_id": "/m/07clx"}, + {"id": 376, "name": "Tennis ball", "freebase_id": "/m/05ctyq"}, + {"id": 377, "name": "Waste container", "freebase_id": "/m/0bjyj5"}, + {"id": 378, "name": "Mule", "freebase_id": "/m/0dbzx"}, + {"id": 379, "name": "Cricket ball", "freebase_id": "/m/02ctlc"}, + {"id": 380, "name": "Pineapple", "freebase_id": "/m/0fp6w"}, + {"id": 381, "name": "Coconut", "freebase_id": "/m/0djtd"}, + {"id": 382, "name": "Doll", "freebase_id": "/m/0167gd"}, + {"id": 383, "name": "Coffee table", "freebase_id": "/m/078n6m"}, + {"id": 384, "name": "Snowman", "freebase_id": "/m/0152hh"}, + {"id": 385, "name": "Lavender", "freebase_id": "/m/04gth"}, + {"id": 386, "name": "Shrimp", "freebase_id": "/m/0ll1f78"}, + {"id": 387, "name": "Maple", "freebase_id": "/m/0cffdh"}, + {"id": 388, "name": "Cowboy hat", "freebase_id": "/m/025rp__"}, + {"id": 389, "name": "Goggles", "freebase_id": "/m/02_n6y"}, + {"id": 390, "name": "Rugby ball", "freebase_id": "/m/0wdt60w"}, + {"id": 391, "name": "Caterpillar", "freebase_id": "/m/0cydv"}, + {"id": 392, "name": "Poster", "freebase_id": "/m/01n5jq"}, + {"id": 393, "name": "Rocket", "freebase_id": "/m/09rvcxw"}, + {"id": 394, "name": "Organ", "freebase_id": "/m/013y1f"}, + {"id": 395, "name": "Saxophone", "freebase_id": "/m/06ncr"}, + {"id": 396, "name": "Traffic light", "freebase_id": "/m/015qff"}, + {"id": 397, "name": "Cocktail", "freebase_id": "/m/024g6"}, + {"id": 398, "name": "Plastic bag", "freebase_id": "/m/05gqfk"}, + {"id": 399, "name": "Squash", "freebase_id": "/m/0dv77"}, + {"id": 400, "name": "Mushroom", "freebase_id": "/m/052sf"}, + {"id": 401, "name": "Hamburger", "freebase_id": "/m/0cdn1"}, + {"id": 402, "name": "Light switch", "freebase_id": "/m/03jbxj"}, + {"id": 403, "name": "Parachute", "freebase_id": "/m/0cyfs"}, + {"id": 404, "name": "Teddy bear", "freebase_id": "/m/0kmg4"}, + {"id": 405, "name": "Winter melon", "freebase_id": "/m/02cvgx"}, + {"id": 406, "name": "Deer", "freebase_id": "/m/09kx5"}, + {"id": 407, "name": "Musical keyboard", "freebase_id": "/m/057cc"}, + {"id": 408, "name": "Plumbing fixture", "freebase_id": "/m/02pkr5"}, + {"id": 409, "name": "Scoreboard", "freebase_id": "/m/057p5t"}, + {"id": 410, "name": "Baseball bat", "freebase_id": "/m/03g8mr"}, + {"id": 411, "name": "Envelope", "freebase_id": "/m/0frqm"}, + {"id": 412, "name": "Adhesive tape", "freebase_id": "/m/03m3vtv"}, + {"id": 413, "name": "Briefcase", "freebase_id": "/m/0584n8"}, + {"id": 414, "name": "Paddle", "freebase_id": "/m/014y4n"}, + {"id": 415, "name": "Bow and arrow", "freebase_id": "/m/01g3x7"}, + {"id": 416, "name": "Telephone", "freebase_id": "/m/07cx4"}, + {"id": 417, "name": "Sheep", "freebase_id": "/m/07bgp"}, + {"id": 418, "name": "Jacket", "freebase_id": "/m/032b3c"}, + {"id": 419, "name": "Boy", "freebase_id": "/m/01bl7v"}, + {"id": 420, "name": "Pizza", "freebase_id": "/m/0663v"}, + {"id": 421, "name": "Otter", "freebase_id": "/m/0cn6p"}, + {"id": 422, "name": "Office supplies", "freebase_id": "/m/02rdsp"}, + {"id": 423, "name": "Couch", "freebase_id": "/m/02crq1"}, + {"id": 424, "name": "Cello", "freebase_id": "/m/01xqw"}, + {"id": 425, "name": "Bull", "freebase_id": "/m/0cnyhnx"}, + {"id": 426, "name": "Camel", "freebase_id": "/m/01x_v"}, + {"id": 427, "name": "Ball", "freebase_id": "/m/018xm"}, + {"id": 428, "name": "Duck", "freebase_id": "/m/09ddx"}, + {"id": 429, "name": "Whale", "freebase_id": "/m/084zz"}, + {"id": 430, "name": "Shirt", "freebase_id": "/m/01n4qj"}, + {"id": 431, "name": "Tank", "freebase_id": "/m/07cmd"}, + {"id": 432, "name": "Motorcycle", "freebase_id": "/m/04_sv"}, + {"id": 433, "name": "Accordion", "freebase_id": "/m/0mkg"}, + {"id": 434, "name": "Owl", "freebase_id": "/m/09d5_"}, + {"id": 435, "name": "Porcupine", "freebase_id": "/m/0c568"}, + {"id": 436, "name": "Sun hat", "freebase_id": "/m/02wbtzl"}, + {"id": 437, "name": "Nail", "freebase_id": "/m/05bm6"}, + {"id": 438, "name": "Scissors", "freebase_id": "/m/01lsmm"}, + {"id": 439, "name": "Swan", "freebase_id": "/m/0dftk"}, + {"id": 440, "name": "Lamp", "freebase_id": "/m/0dtln"}, + {"id": 441, "name": "Crown", "freebase_id": "/m/0nl46"}, + {"id": 442, "name": "Piano", "freebase_id": "/m/05r5c"}, + {"id": 443, "name": "Sculpture", "freebase_id": "/m/06msq"}, + {"id": 444, "name": "Cheetah", "freebase_id": "/m/0cd4d"}, + {"id": 445, "name": "Oboe", "freebase_id": "/m/05kms"}, + {"id": 446, "name": "Tin can", "freebase_id": "/m/02jnhm"}, + {"id": 447, "name": "Mango", "freebase_id": "/m/0fldg"}, + {"id": 448, "name": "Tripod", "freebase_id": "/m/073bxn"}, + {"id": 449, "name": "Oven", "freebase_id": "/m/029bxz"}, + {"id": 450, "name": "Mouse", "freebase_id": "/m/020lf"}, + {"id": 451, "name": "Barge", "freebase_id": "/m/01btn"}, + {"id": 452, "name": "Coffee", "freebase_id": "/m/02vqfm"}, + {"id": 453, "name": "Snowboard", "freebase_id": "/m/06__v"}, + {"id": 454, "name": "Common fig", "freebase_id": "/m/043nyj"}, + {"id": 455, "name": "Salad", "freebase_id": "/m/0grw1"}, + {"id": 456, "name": "Marine invertebrates", "freebase_id": "/m/03hl4l9"}, + {"id": 457, "name": "Umbrella", "freebase_id": "/m/0hnnb"}, + {"id": 458, "name": "Kangaroo", "freebase_id": "/m/04c0y"}, + {"id": 459, "name": "Human arm", "freebase_id": "/m/0dzf4"}, + {"id": 460, "name": "Measuring cup", "freebase_id": "/m/07v9_z"}, + {"id": 461, "name": "Snail", "freebase_id": "/m/0f9_l"}, + {"id": 462, "name": "Loveseat", "freebase_id": "/m/0703r8"}, + {"id": 463, "name": "Suit", "freebase_id": "/m/01xyhv"}, + {"id": 464, "name": "Teapot", "freebase_id": "/m/01fh4r"}, + {"id": 465, "name": "Bottle", "freebase_id": "/m/04dr76w"}, + {"id": 466, "name": "Alpaca", "freebase_id": "/m/0pcr"}, + {"id": 467, "name": "Kettle", "freebase_id": "/m/03s_tn"}, + {"id": 468, "name": "Trousers", "freebase_id": "/m/07mhn"}, + {"id": 469, "name": "Popcorn", "freebase_id": "/m/01hrv5"}, + {"id": 470, "name": "Centipede", "freebase_id": "/m/019h78"}, + {"id": 471, "name": "Spider", "freebase_id": "/m/09kmb"}, + {"id": 472, "name": "Sparrow", "freebase_id": "/m/0h23m"}, + {"id": 473, "name": "Plate", "freebase_id": "/m/050gv4"}, + {"id": 474, "name": "Bagel", "freebase_id": "/m/01fb_0"}, + {"id": 475, "name": "Personal care", "freebase_id": "/m/02w3_ws"}, + {"id": 476, "name": "Apple", "freebase_id": "/m/014j1m"}, + {"id": 477, "name": "Brassiere", "freebase_id": "/m/01gmv2"}, + {"id": 478, "name": "Bathroom cabinet", "freebase_id": "/m/04y4h8h"}, + {"id": 479, "name": "studio couch", "freebase_id": "/m/026qbn5"}, + {"id": 480, "name": "Computer keyboard", "freebase_id": "/m/01m2v"}, + {"id": 481, "name": "Table tennis racket", "freebase_id": "/m/05_5p_0"}, + {"id": 482, "name": "Sushi", "freebase_id": "/m/07030"}, + {"id": 483, "name": "Cabinetry", "freebase_id": "/m/01s105"}, + {"id": 484, "name": "Street light", "freebase_id": "/m/033rq4"}, + {"id": 485, "name": "Towel", "freebase_id": "/m/0162_1"}, + {"id": 486, "name": "Nightstand", "freebase_id": "/m/02z51p"}, + {"id": 487, "name": "Rabbit", "freebase_id": "/m/06mf6"}, + {"id": 488, "name": "Dolphin", "freebase_id": "/m/02hj4"}, + {"id": 489, "name": "Dog", "freebase_id": "/m/0bt9lr"}, + {"id": 490, "name": "Jug", "freebase_id": "/m/08hvt4"}, + {"id": 491, "name": "Wok", "freebase_id": "/m/084rd"}, + {"id": 492, "name": "Fire hydrant", "freebase_id": "/m/01pns0"}, + {"id": 493, "name": "Human eye", "freebase_id": "/m/014sv8"}, + {"id": 494, "name": "Skyscraper", "freebase_id": "/m/079cl"}, + {"id": 495, "name": "Backpack", "freebase_id": "/m/01940j"}, + {"id": 496, "name": "Potato", "freebase_id": "/m/05vtc"}, + {"id": 497, "name": "Paper towel", "freebase_id": "/m/02w3r3"}, + {"id": 498, "name": "Lifejacket", "freebase_id": "/m/054xkw"}, + {"id": 499, "name": "Bicycle wheel", "freebase_id": "/m/01bqk0"}, + {"id": 500, "name": "Toilet", "freebase_id": "/m/09g1w"}, ] def _get_builtin_metadata(cats): - id_to_name = {x['id']: x['name'] for x in cats} + {x["id"]: x["name"] for x in cats} thing_dataset_id_to_contiguous_id = {i + 1: i for i in range(len(cats))} - thing_classes = [x['name'] for x in sorted(cats, key=lambda x: x['id'])] + thing_classes = [x["name"] for x in sorted(cats, key=lambda x: x["id"])] return { "thing_dataset_id_to_contiguous_id": thing_dataset_id_to_contiguous_id, - "thing_classes": thing_classes} + "thing_classes": thing_classes, + } + _PREDEFINED_SPLITS_OID = { # cat threshold: 500, 1500: r 170, c 151, f 179 @@ -521,8 +524,14 @@ def _get_builtin_metadata(cats): # "expanded" duplicates annotations to their father classes based on the official # hierarchy. This is used in the official evaulation protocol. # https://storage.googleapis.com/openimages/web/evaluation.html - "oid_val_expanded": ("oid/images/validation/", "oid/annotations/oid_challenge_2019_val_expanded.json"), - "oid_val_expanded_rare": ("oid/images/validation/", "oid/annotations/oid_challenge_2019_val_expanded_rare.json"), + "oid_val_expanded": ( + "oid/images/validation/", + "oid/annotations/oid_challenge_2019_val_expanded.json", + ), + "oid_val_expanded_rare": ( + "oid/images/validation/", + "oid/annotations/oid_challenge_2019_val_expanded_rare.json", + ), } @@ -532,4 +541,4 @@ def _get_builtin_metadata(cats): _get_builtin_metadata(categories), os.path.join("datasets", json_file) if "://" not in json_file else json_file, os.path.join("datasets", image_root), - ) \ No newline at end of file + ) diff --git a/dimos/models/Detic/detic/data/datasets/register_oid.py b/dimos/models/Detic/detic/data/datasets/register_oid.py index bd281f53f0..0739556041 100644 --- a/dimos/models/Detic/detic/data/datasets/register_oid.py +++ b/dimos/models/Detic/detic/data/datasets/register_oid.py @@ -1,20 +1,15 @@ # Copyright (c) Facebook, Inc. and its affiliates. # Modified by Xingyi Zhou from https://github.com/facebookresearch/detectron2/blob/master/detectron2/data/datasets/coco.py -import copy +import contextlib import io import logging -import contextlib import os -import datetime -import json -import numpy as np - -from PIL import Image -from fvcore.common.timer import Timer -from fvcore.common.file_io import PathManager, file_lock -from detectron2.structures import BoxMode, PolygonMasks, Boxes from detectron2.data import DatasetCatalog, MetadataCatalog +from detectron2.structures import BoxMode +from fvcore.common.file_io import PathManager +from fvcore.common.timer import Timer +from typing import Optional logger = logging.getLogger(__name__) @@ -25,13 +20,10 @@ __all__ = ["register_coco_instances", "register_coco_panoptic_separated"] - -def register_oid_instances(name, metadata, json_file, image_root): - """ - """ +def register_oid_instances(name: str, metadata, json_file, image_root) -> None: + """ """ # 1. register a function which returns dicts - DatasetCatalog.register(name, lambda: load_coco_json_mem_efficient( - json_file, image_root, name)) + DatasetCatalog.register(name, lambda: load_coco_json_mem_efficient(json_file, image_root, name)) # 2. Optionally, add metadata about this dataset, # since they might be useful in evaluation, visualization or logging @@ -40,7 +32,9 @@ def register_oid_instances(name, metadata, json_file, image_root): ) -def load_coco_json_mem_efficient(json_file, image_root, dataset_name=None, extra_annotation_keys=None): +def load_coco_json_mem_efficient( + json_file, image_root, dataset_name: Optional[str]=None, extra_annotation_keys=None +): """ Actually not mem efficient """ @@ -51,7 +45,7 @@ def load_coco_json_mem_efficient(json_file, image_root, dataset_name=None, extra with contextlib.redirect_stdout(io.StringIO()): coco_api = COCO(json_file) if timer.seconds() > 1: - logger.info("Loading {} takes {:.2f} seconds.".format(json_file, timer.seconds())) + logger.info(f"Loading {json_file} takes {timer.seconds():.2f} seconds.") id_map = None if dataset_name is not None: @@ -75,7 +69,7 @@ def load_coco_json_mem_efficient(json_file, image_root, dataset_name=None, extra # sort indices for reproducible results img_ids = sorted(coco_api.imgs.keys()) imgs = coco_api.loadImgs(img_ids) - logger.info("Loaded {} images in COCO format from {}".format(len(imgs), json_file)) + logger.info(f"Loaded {len(imgs)} images in COCO format from {json_file}") dataset_dicts = [] @@ -88,9 +82,8 @@ def load_coco_json_mem_efficient(json_file, image_root, dataset_name=None, extra record["width"] = img_dict["width"] image_id = record["image_id"] = img_dict["id"] anno_dict_list = coco_api.imgToAnns[image_id] - if 'neg_category_ids' in img_dict: - record['neg_category_ids'] = \ - [id_map[x] for x in img_dict['neg_category_ids']] + if "neg_category_ids" in img_dict: + record["neg_category_ids"] = [id_map[x] for x in img_dict["neg_category_ids"]] objs = [] for anno in anno_dict_list: @@ -117,6 +110,6 @@ def load_coco_json_mem_efficient(json_file, image_root, dataset_name=None, extra objs.append(obj) record["annotations"] = objs dataset_dicts.append(record) - + del coco_api - return dataset_dicts \ No newline at end of file + return dataset_dicts diff --git a/dimos/models/Detic/detic/data/tar_dataset.py b/dimos/models/Detic/detic/data/tar_dataset.py index 0605ba3a96..8c87a056d1 100644 --- a/dimos/models/Detic/detic/data/tar_dataset.py +++ b/dimos/models/Detic/detic/data/tar_dataset.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. -import os import gzip -import numpy as np import io +import os + +import numpy as np from PIL import Image from torch.utils.data import Dataset @@ -15,12 +16,15 @@ # UnidentifiedImageError isn't available in older versions of PIL unidentified_error_available = False + class DiskTarDataset(Dataset): - def __init__(self, - tarfile_path='dataset/imagenet/ImageNet-21k/metadata/tar_files.npy', - tar_index_dir='dataset/imagenet/ImageNet-21k/metadata/tarindex_npy', - preload=False, - num_synsets="all"): + def __init__( + self, + tarfile_path: str="dataset/imagenet/ImageNet-21k/metadata/tar_files.npy", + tar_index_dir: str="dataset/imagenet/ImageNet-21k/metadata/tarindex_npy", + preload: bool=False, + num_synsets: str="all", + ) -> None: """ - preload (bool): Recommend to set preload to False when using - num_synsets (integer or string "all"): set to small number for debugging @@ -45,12 +49,14 @@ def __init__(self, labels = np.zeros(self.dataset_lens.sum(), dtype=np.int64) sI = 0 for k in range(len(self.dataset_lens)): - assert (sI+self.dataset_lens[k]) <= len(labels), f"{k} {sI+self.dataset_lens[k]} vs. {len(labels)}" - labels[sI:(sI+self.dataset_lens[k])] = k + assert (sI + self.dataset_lens[k]) <= len(labels), ( + f"{k} {sI + self.dataset_lens[k]} vs. {len(labels)}" + ) + labels[sI : (sI + self.dataset_lens[k])] = k sI += self.dataset_lens[k] self.labels = labels - def __len__(self): + def __len__(self) -> int: return self.num_samples def __getitem__(self, index): @@ -62,7 +68,9 @@ def __getitem__(self, index): if index in self.dataset_cumsums: d_index += 1 - assert d_index == self.labels[index], f"{d_index} vs. {self.labels[index]} mismatch for {index}" + assert d_index == self.labels[index], ( + f"{d_index} vs. {self.labels[index]} mismatch for {index}" + ) # change index to local dataset index if d_index == 0: @@ -74,19 +82,19 @@ def __getitem__(self, index): try: image = Image.open(data_bytes).convert("RGB") except exception_to_catch: - image = Image.fromarray(np.ones((224,224,3), dtype=np.uint8)*128) + image = Image.fromarray(np.ones((224, 224, 3), dtype=np.uint8) * 128) d_index = -1 # label is the dataset (synset) we indexed into return image, d_index, index - def __repr__(self): + def __repr__(self) -> str: st = f"DiskTarDataset(subdatasets={len(self.dataset_lens)},samples={self.num_samples})" return st -class _TarDataset(object): - def __init__(self, filename, npy_index_dir, preload=False): +class _TarDataset: + def __init__(self, filename, npy_index_dir, preload: bool=False) -> None: # translated from # fbcode/experimental/deeplearning/matthijs/comp_descs/tardataset.lua self.filename = filename @@ -97,13 +105,12 @@ def __init__(self, filename, npy_index_dir, preload=False): self.num_samples = len(names) if preload: - self.data = np.memmap(filename, mode='r', dtype='uint8') + self.data = np.memmap(filename, mode="r", dtype="uint8") self.offsets = offsets else: self.data = None - - def __len__(self): + def __len__(self) -> int: return self.num_samples def load_index(self): @@ -113,26 +120,26 @@ def load_index(self): offsets = np.load(os.path.join(self.npy_index_dir, f"{basename}_offsets.npy")) return names, offsets - def __getitem__(self, idx): + def __getitem__(self, idx: int): if self.data is None: - self.data = np.memmap(self.filename, mode='r', dtype='uint8') + self.data = np.memmap(self.filename, mode="r", dtype="uint8") _, self.offsets = self.load_index() ofs = self.offsets[idx] * 512 fsize = 512 * (self.offsets[idx + 1] - self.offsets[idx]) - data = self.data[ofs:ofs + fsize] + data = self.data[ofs : ofs + fsize] - if data[:13].tostring() == '././@LongLink': - data = data[3 * 512:] + if data[:13].tostring() == "././@LongLink": + data = data[3 * 512 :] else: data = data[512:] # just to make it more fun a few JPEGs are GZIP compressed... # catch this case - if tuple(data[:2]) == (0x1f, 0x8b): + if tuple(data[:2]) == (0x1F, 0x8B): s = io.BytesIO(data.tostring()) - g = gzip.GzipFile(None, 'r', 0, s) + g = gzip.GzipFile(None, "r", 0, s) sdata = g.read() else: sdata = data.tostring() - return io.BytesIO(sdata) \ No newline at end of file + return io.BytesIO(sdata) diff --git a/dimos/models/Detic/detic/data/transforms/custom_augmentation_impl.py b/dimos/models/Detic/detic/data/transforms/custom_augmentation_impl.py index 47bef39566..7cabc91e0f 100644 --- a/dimos/models/Detic/detic/data/transforms/custom_augmentation_impl.py +++ b/dimos/models/Detic/detic/data/transforms/custom_augmentation_impl.py @@ -1,38 +1,26 @@ -# -*- coding: utf-8 -*- # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# Part of the code is from https://github.com/rwightman/efficientdet-pytorch/blob/master/effdet/data/transforms.py +# Part of the code is from https://github.com/rwightman/efficientdet-pytorch/blob/master/effdet/data/transforms.py # Modified by Xingyi Zhou # The original code is under Apache-2.0 License +from detectron2.data.transforms.augmentation import Augmentation import numpy as np -import sys -from fvcore.transforms.transform import ( - BlendTransform, - CropTransform, - HFlipTransform, - NoOpTransform, - Transform, - VFlipTransform, -) from PIL import Image -from detectron2.data.transforms.augmentation import Augmentation from .custom_transform import EfficientDetResizeCropTransform __all__ = [ "EfficientDetResizeCrop", ] + class EfficientDetResizeCrop(Augmentation): """ Scale the shorter edge to the given size, with a limit of `max_size` on the longer edge. If `max_size` is reached, then downscale so that the longer edge does not exceed max_size. """ - def __init__( - self, size, scale, interp=Image.BILINEAR - ): - """ - """ + def __init__(self, size: int, scale, interp=Image.BILINEAR) -> None: + """ """ super().__init__() self.target_size = (size, size) self.scale = scale @@ -57,4 +45,5 @@ def get_transform(self, img): offset_y = int(max(0.0, float(offset_y)) * np.random.uniform(0, 1)) offset_x = int(max(0.0, float(offset_x)) * np.random.uniform(0, 1)) return EfficientDetResizeCropTransform( - scaled_h, scaled_w, offset_y, offset_x, img_scale, self.target_size, self.interp) + scaled_h, scaled_w, offset_y, offset_x, img_scale, self.target_size, self.interp + ) diff --git a/dimos/models/Detic/detic/data/transforms/custom_transform.py b/dimos/models/Detic/detic/data/transforms/custom_transform.py index 3cc28b6b31..2017c27a5f 100644 --- a/dimos/models/Detic/detic/data/transforms/custom_transform.py +++ b/dimos/models/Detic/detic/data/transforms/custom_transform.py @@ -1,22 +1,17 @@ -# -*- coding: utf-8 -*- # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -# Part of the code is from https://github.com/rwightman/efficientdet-pytorch/blob/master/effdet/data/transforms.py +# Part of the code is from https://github.com/rwightman/efficientdet-pytorch/blob/master/effdet/data/transforms.py # Modified by Xingyi Zhou # The original code is under Apache-2.0 License -import numpy as np -import torch -import torch.nn.functional as F from fvcore.transforms.transform import ( - CropTransform, - HFlipTransform, - NoOpTransform, Transform, - TransformList, ) +import numpy as np from PIL import Image +import torch +import torch.nn.functional as F try: - import cv2 # noqa + import cv2 except ImportError: # OpenCV is an optional dependency at the moment pass @@ -25,12 +20,11 @@ "EfficientDetResizeCropTransform", ] + class EfficientDetResizeCropTransform(Transform): - """ - """ + """ """ - def __init__(self, scaled_h, scaled_w, offset_y, offset_x, img_scale, \ - target_size, interp=None): + def __init__(self, scaled_h, scaled_w, offset_y, offset_x, img_scale, target_size: int, interp=None) -> None: """ Args: h, w (int): original image size @@ -54,9 +48,9 @@ def apply_image(self, img, interp=None): right = min(self.scaled_w, self.offset_x + self.target_size[1]) lower = min(self.scaled_h, self.offset_y + self.target_size[0]) if len(ret.shape) <= 3: - ret = ret[self.offset_y: lower, self.offset_x: right] + ret = ret[self.offset_y : lower, self.offset_x : right] else: - ret = ret[..., self.offset_y: lower, self.offset_x: right, :] + ret = ret[..., self.offset_y : lower, self.offset_x : right, :] else: # PIL only supports uint8 img = torch.from_numpy(img) @@ -71,12 +65,11 @@ def apply_image(self, img, interp=None): right = min(self.scaled_w, self.offset_x + self.target_size[1]) lower = min(self.scaled_h, self.offset_y + self.target_size[0]) if len(ret.shape) <= 3: - ret = ret[self.offset_y: lower, self.offset_x: right] + ret = ret[self.offset_y : lower, self.offset_x : right] else: - ret = ret[..., self.offset_y: lower, self.offset_x: right, :] + ret = ret[..., self.offset_y : lower, self.offset_x : right, :] return ret - def apply_coords(self, coords): coords[:, 0] = coords[:, 0] * self.img_scale coords[:, 1] = coords[:, 1] * self.img_scale @@ -84,16 +77,13 @@ def apply_coords(self, coords): coords[:, 1] -= self.offset_y return coords - def apply_segmentation(self, segmentation): segmentation = self.apply_image(segmentation, interp=Image.NEAREST) return segmentation - def inverse(self): raise NotImplementedError - def inverse_apply_coords(self, coords): coords[:, 0] += self.offset_x coords[:, 1] += self.offset_y @@ -101,14 +91,12 @@ def inverse_apply_coords(self, coords): coords[:, 1] = coords[:, 1] / self.img_scale return coords - def inverse_apply_box(self, box: np.ndarray) -> np.ndarray: - """ - """ + """ """ idxs = np.array([(0, 1), (2, 1), (0, 3), (2, 3)]).flatten() coords = np.asarray(box).reshape(-1, 4)[:, idxs].reshape(-1, 2) coords = self.inverse_apply_coords(coords).reshape((-1, 4, 2)) minxy = coords.min(axis=1) maxxy = coords.max(axis=1) trans_boxes = np.concatenate((minxy, maxxy), axis=1) - return trans_boxes \ No newline at end of file + return trans_boxes diff --git a/dimos/models/Detic/detic/evaluation/custom_coco_eval.py b/dimos/models/Detic/detic/evaluation/custom_coco_eval.py index 2ea1d5e570..759d885f00 100644 --- a/dimos/models/Detic/detic/evaluation/custom_coco_eval.py +++ b/dimos/models/Detic/detic/evaluation/custom_coco_eval.py @@ -1,36 +1,21 @@ # Copyright (c) Facebook, Inc. and its affiliates. -import contextlib -import copy -import io import itertools -import json -import logging -import numpy as np -import os -import pickle -from collections import OrderedDict -import pycocotools.mask as mask_util -import torch -from pycocotools.coco import COCO -from pycocotools.cocoeval import COCOeval -from tabulate import tabulate -import detectron2.utils.comm as comm -from detectron2.config import CfgNode -from detectron2.data import MetadataCatalog -from detectron2.data.datasets.coco import convert_to_coco_json from detectron2.evaluation.coco_evaluation import COCOEvaluator -from detectron2.structures import Boxes, BoxMode, pairwise_iou -from detectron2.utils.file_io import PathManager from detectron2.utils.logger import create_small_table +import numpy as np +from tabulate import tabulate + from ..data.datasets.coco_zeroshot import categories_seen, categories_unseen +from typing import Optional, Sequence + class CustomCOCOEvaluator(COCOEvaluator): - def _derive_coco_results(self, coco_eval, iou_type, class_names=None): + def _derive_coco_results(self, coco_eval, iou_type, class_names: Optional[Sequence[str]]=None): """ Additionally plot mAP for 'seen classes' and 'unseen classes' """ - + metrics = { "bbox": ["AP", "AP50", "AP75", "APs", "APm", "APl"], "segm": ["AP", "AP50", "AP75", "APs", "APm", "APl"], @@ -47,7 +32,7 @@ def _derive_coco_results(self, coco_eval, iou_type, class_names=None): for idx, metric in enumerate(metrics) } self._logger.info( - "Evaluation results for {}: \n".format(iou_type) + create_small_table(results) + f"Evaluation results for {iou_type}: \n" + create_small_table(results) ) if not np.isfinite(sum(results.values())): self._logger.info("Some metrics cannot be computed and is shown as NaN.") @@ -55,13 +40,13 @@ def _derive_coco_results(self, coco_eval, iou_type, class_names=None): if class_names is None or len(class_names) <= 1: return results # Compute per-category AP - # from https://github.com/facebookresearch/Detectron/blob/a6a835f5b8208c45d0dce217ce9bbda915f44df7/detectron/datasets/json_dataset_evaluator.py#L222-L252 # noqa + # from https://github.com/facebookresearch/Detectron/blob/a6a835f5b8208c45d0dce217ce9bbda915f44df7/detectron/datasets/json_dataset_evaluator.py#L222-L252 precisions = coco_eval.eval["precision"] # precision has dims (iou, recall, cls, area range, max dets) assert len(class_names) == precisions.shape[2] - seen_names = set([x['name'] for x in categories_seen]) - unseen_names = set([x['name'] for x in categories_unseen]) + seen_names = set([x["name"] for x in categories_seen]) + unseen_names = set([x["name"] for x in categories_unseen]) results_per_category = [] results_per_category50 = [] results_per_category50_seen = [] @@ -72,11 +57,11 @@ def _derive_coco_results(self, coco_eval, iou_type, class_names=None): precision = precisions[:, :, idx, 0, -1] precision = precision[precision > -1] ap = np.mean(precision) if precision.size else float("nan") - results_per_category.append(("{}".format(name), float(ap * 100))) + results_per_category.append((f"{name}", float(ap * 100))) precision50 = precisions[0, :, idx, 0, -1] precision50 = precision50[precision50 > -1] ap50 = np.mean(precision50) if precision50.size else float("nan") - results_per_category50.append(("{}".format(name), float(ap50 * 100))) + results_per_category50.append((f"{name}", float(ap50 * 100))) if name in seen_names: results_per_category50_seen.append(float(ap50 * 100)) if name in unseen_names: @@ -93,8 +78,7 @@ def _derive_coco_results(self, coco_eval, iou_type, class_names=None): headers=["category", "AP"] * (N_COLS // 2), numalign="left", ) - self._logger.info("Per-category {} AP: \n".format(iou_type) + table) - + self._logger.info(f"Per-category {iou_type} AP: \n" + table) N_COLS = min(6, len(results_per_category50) * 2) results_flatten = list(itertools.chain(*results_per_category50)) @@ -106,19 +90,17 @@ def _derive_coco_results(self, coco_eval, iou_type, class_names=None): headers=["category", "AP50"] * (N_COLS // 2), numalign="left", ) - self._logger.info("Per-category {} AP50: \n".format(iou_type) + table) + self._logger.info(f"Per-category {iou_type} AP50: \n" + table) self._logger.info( - "Seen {} AP50: {}".format( - iou_type, - sum(results_per_category50_seen) / len(results_per_category50_seen), - )) + f"Seen {iou_type} AP50: {sum(results_per_category50_seen) / len(results_per_category50_seen)}" + ) self._logger.info( - "Unseen {} AP50: {}".format( - iou_type, - sum(results_per_category50_unseen) / len(results_per_category50_unseen), - )) + f"Unseen {iou_type} AP50: {sum(results_per_category50_unseen) / len(results_per_category50_unseen)}" + ) results.update({"AP-" + name: ap for name, ap in results_per_category}) results["AP50-seen"] = sum(results_per_category50_seen) / len(results_per_category50_seen) - results["AP50-unseen"] = sum(results_per_category50_unseen) / len(results_per_category50_unseen) - return results \ No newline at end of file + results["AP50-unseen"] = sum(results_per_category50_unseen) / len( + results_per_category50_unseen + ) + return results diff --git a/dimos/models/Detic/detic/evaluation/oideval.py b/dimos/models/Detic/detic/evaluation/oideval.py index e60125aec2..aa5a954aef 100644 --- a/dimos/models/Detic/detic/evaluation/oideval.py +++ b/dimos/models/Detic/detic/evaluation/oideval.py @@ -8,78 +8,81 @@ # The code is from https://github.com/xingyizhou/UniDet/blob/master/projects/UniDet/unidet/evaluation/oideval.py # The original code is under Apache-2.0 License # Copyright (c) Facebook, Inc. and its affiliates. -import os +from collections import OrderedDict, defaultdict +import copy import datetime -import logging import itertools -from collections import OrderedDict -from collections import defaultdict -import copy import json -import numpy as np -import torch -from tabulate import tabulate +import logging +import os +from detectron2.data import MetadataCatalog +from detectron2.evaluation import DatasetEvaluator +from detectron2.evaluation.coco_evaluation import instances_to_coco_json +import detectron2.utils.comm as comm +from detectron2.utils.logger import create_small_table +from fvcore.common.file_io import PathManager from lvis.lvis import LVIS from lvis.results import LVISResults - +import numpy as np import pycocotools.mask as mask_utils +from tabulate import tabulate +import torch +from typing import Optional, Sequence -from fvcore.common.file_io import PathManager -import detectron2.utils.comm as comm -from detectron2.data import MetadataCatalog -from detectron2.evaluation.coco_evaluation import instances_to_coco_json -from detectron2.utils.logger import create_small_table -from detectron2.evaluation import DatasetEvaluator def compute_average_precision(precision, recall): - """Compute Average Precision according to the definition in VOCdevkit. - Precision is modified to ensure that it does not decrease as recall - decrease. - Args: - precision: A float [N, 1] numpy array of precisions - recall: A float [N, 1] numpy array of recalls - Raises: - ValueError: if the input is not of the correct format - Returns: - average_precison: The area under the precision recall curve. NaN if - precision and recall are None. - """ - if precision is None: - if recall is not None: - raise ValueError("If precision is None, recall must also be None") - return np.NAN - - if not isinstance(precision, np.ndarray) or not isinstance( - recall, np.ndarray): - raise ValueError("precision and recall must be numpy array") - if precision.dtype != np.float or recall.dtype != np.float: - raise ValueError("input must be float numpy array.") - if len(precision) != len(recall): - raise ValueError("precision and recall must be of the same size.") - if not precision.size: - return 0.0 - if np.amin(precision) < 0 or np.amax(precision) > 1: - raise ValueError("Precision must be in the range of [0, 1].") - if np.amin(recall) < 0 or np.amax(recall) > 1: - raise ValueError("recall must be in the range of [0, 1].") - if not all(recall[i] <= recall[i + 1] for i in range(len(recall) - 1)): - raise ValueError("recall must be a non-decreasing array") - - recall = np.concatenate([[0], recall, [1]]) - precision = np.concatenate([[0], precision, [0]]) - - for i in range(len(precision) - 2, -1, -1): - precision[i] = np.maximum(precision[i], precision[i + 1]) - indices = np.where(recall[1:] != recall[:-1])[0] + 1 - average_precision = np.sum( - (recall[indices] - recall[indices - 1]) * precision[indices]) - return average_precision + """Compute Average Precision according to the definition in VOCdevkit. + Precision is modified to ensure that it does not decrease as recall + decrease. + Args: + precision: A float [N, 1] numpy array of precisions + recall: A float [N, 1] numpy array of recalls + Raises: + ValueError: if the input is not of the correct format + Returns: + average_precison: The area under the precision recall curve. NaN if + precision and recall are None. + """ + if precision is None: + if recall is not None: + raise ValueError("If precision is None, recall must also be None") + return np.NAN + + if not isinstance(precision, np.ndarray) or not isinstance(recall, np.ndarray): + raise ValueError("precision and recall must be numpy array") + if precision.dtype != np.float or recall.dtype != np.float: + raise ValueError("input must be float numpy array.") + if len(precision) != len(recall): + raise ValueError("precision and recall must be of the same size.") + if not precision.size: + return 0.0 + if np.amin(precision) < 0 or np.amax(precision) > 1: + raise ValueError("Precision must be in the range of [0, 1].") + if np.amin(recall) < 0 or np.amax(recall) > 1: + raise ValueError("recall must be in the range of [0, 1].") + if not all(recall[i] <= recall[i + 1] for i in range(len(recall) - 1)): + raise ValueError("recall must be a non-decreasing array") + + recall = np.concatenate([[0], recall, [1]]) + precision = np.concatenate([[0], precision, [0]]) + + for i in range(len(precision) - 2, -1, -1): + precision[i] = np.maximum(precision[i], precision[i + 1]) + indices = np.where(recall[1:] != recall[:-1])[0] + 1 + average_precision = np.sum((recall[indices] - recall[indices - 1]) * precision[indices]) + return average_precision + class OIDEval: def __init__( - self, lvis_gt, lvis_dt, iou_type="bbox", expand_pred_label=False, - oid_hierarchy_path='./datasets/oid/annotations/challenge-2019-label500-hierarchy.json'): + self, + lvis_gt, + lvis_dt, + iou_type: str="bbox", + expand_pred_label: bool=False, + oid_hierarchy_path: str="./datasets/oid/annotations/challenge-2019-label500-hierarchy.json", + ) -> None: """Constructor for OIDEval. Args: lvis_gt (LVIS class instance, or str containing path of annotation file) @@ -90,64 +93,66 @@ def __init__( self.logger = logging.getLogger(__name__) if iou_type not in ["bbox", "segm"]: - raise ValueError("iou_type: {} is not supported.".format(iou_type)) + raise ValueError(f"iou_type: {iou_type} is not supported.") if isinstance(lvis_gt, LVIS): self.lvis_gt = lvis_gt elif isinstance(lvis_gt, str): self.lvis_gt = LVIS(lvis_gt) else: - raise TypeError("Unsupported type {} of lvis_gt.".format(lvis_gt)) + raise TypeError(f"Unsupported type {lvis_gt} of lvis_gt.") if isinstance(lvis_dt, LVISResults): self.lvis_dt = lvis_dt - elif isinstance(lvis_dt, (str, list)): + elif isinstance(lvis_dt, str | list): # self.lvis_dt = LVISResults(self.lvis_gt, lvis_dt, max_dets=-1) self.lvis_dt = LVISResults(self.lvis_gt, lvis_dt) else: - raise TypeError("Unsupported type {} of lvis_dt.".format(lvis_dt)) + raise TypeError(f"Unsupported type {lvis_dt} of lvis_dt.") if expand_pred_label: - oid_hierarchy = json.load(open(oid_hierarchy_path, 'r')) - cat_info = self.lvis_gt.dataset['categories'] - freebase2id = {x['freebase_id']: x['id'] for x in cat_info} - id2freebase = {x['id']: x['freebase_id'] for x in cat_info} - id2name = {x['id']: x['name'] for x in cat_info} - + oid_hierarchy = json.load(open(oid_hierarchy_path)) + cat_info = self.lvis_gt.dataset["categories"] + freebase2id = {x["freebase_id"]: x["id"] for x in cat_info} + {x["id"]: x["freebase_id"] for x in cat_info} + {x["id"]: x["name"] for x in cat_info} + fas = defaultdict(set) + def dfs(hierarchy, cur_id): all_childs = set() - all_keyed_child = {} - if 'Subcategory' in hierarchy: - for x in hierarchy['Subcategory']: - childs = dfs(x, freebase2id[x['LabelName']]) + if "Subcategory" in hierarchy: + for x in hierarchy["Subcategory"]: + childs = dfs(x, freebase2id[x["LabelName"]]) all_childs.update(childs) if cur_id != -1: for c in all_childs: fas[c].add(cur_id) all_childs.add(cur_id) return all_childs + dfs(oid_hierarchy, -1) - + expanded_pred = [] id_count = 0 - for d in self.lvis_dt.dataset['annotations']: - cur_id = d['category_id'] + for d in self.lvis_dt.dataset["annotations"]: + cur_id = d["category_id"] ids = [cur_id] + [x for x in fas[cur_id]] for cat_id in ids: new_box = copy.deepcopy(d) id_count = id_count + 1 - new_box['id'] = id_count - new_box['category_id'] = cat_id + new_box["id"] = id_count + new_box["category_id"] = cat_id expanded_pred.append(new_box) - print('Expanding original {} preds to {} preds'.format( - len(self.lvis_dt.dataset['annotations']), - len(expanded_pred) - )) - self.lvis_dt.dataset['annotations'] = expanded_pred + print( + "Expanding original {} preds to {} preds".format( + len(self.lvis_dt.dataset["annotations"]), len(expanded_pred) + ) + ) + self.lvis_dt.dataset["annotations"] = expanded_pred self.lvis_dt._create_index() - + # per-image per-category evaluation results self.eval_imgs = defaultdict(list) self.eval = {} # accumulated evaluation results @@ -160,12 +165,12 @@ def dfs(hierarchy, cur_id): self.params.img_ids = sorted(self.lvis_gt.get_img_ids()) self.params.cat_ids = sorted(self.lvis_gt.get_cat_ids()) - def _to_mask(self, anns, lvis): + def _to_mask(self, anns, lvis) -> None: for ann in anns: rle = lvis.ann_to_rle(ann) ann["segmentation"] = rle - def _prepare(self): + def _prepare(self) -> None: """Prepare self._gts and self._dts for evaluation based on params.""" cat_ids = self.params.cat_ids if self.params.cat_ids else None @@ -199,20 +204,20 @@ def _prepare(self): # img_pl[ann["image_id"]].add(ann["category_id"]) assert ann["category_id"] in img_pl[ann["image_id"]] # print('check pos ids OK.') - + for dt in dts: img_id, cat_id = dt["image_id"], dt["category_id"] if cat_id not in img_nl[img_id] and cat_id not in img_pl[img_id]: continue self._dts[img_id, cat_id].append(dt) - def evaluate(self): + def evaluate(self) -> None: """ Run per image evaluation on given images and store results (a list of dict) in self.eval_imgs. """ self.logger.info("Running per image evaluation.") - self.logger.info("Evaluate annotation type *{}*".format(self.params.iou_type)) + self.logger.info(f"Evaluate annotation type *{self.params.iou_type}*") self.params.img_ids = list(np.unique(self.params.img_ids)) @@ -230,7 +235,7 @@ def evaluate(self): } # loop through images, area range, max detection number - print('Evaluating ...') + print("Evaluating ...") self.eval_imgs = [ self.evaluate_img_google(img_id, cat_id, area_rng) for cat_id in cat_ids @@ -247,16 +252,8 @@ def _get_gt_dt(self, img_id, cat_id): gt = self._gts[img_id, cat_id] dt = self._dts[img_id, cat_id] else: - gt = [ - _ann - for _cat_id in self.params.cat_ids - for _ann in self._gts[img_id, cat_id] - ] - dt = [ - _ann - for _cat_id in self.params.cat_ids - for _ann in self._dts[img_id, cat_id] - ] + gt = [_ann for _cat_id in self.params.cat_ids for _ann in self._gts[img_id, cat_id]] + dt = [_ann for _cat_id in self.params.cat_ids for _ann in self._dts[img_id, cat_id]] return gt, dt def compute_iou(self, img_id, cat_id): @@ -270,7 +267,7 @@ def compute_iou(self, img_id, cat_id): dt = [dt[i] for i in idx] # iscrowd = [int(False)] * len(gt) - iscrowd = [int('iscrowd' in g and g['iscrowd'] > 0) for g in gt] + iscrowd = [int("iscrowd" in g and g["iscrowd"] > 0) for g in gt] if self.params.iou_type == "segm": ann_type = "segmentation" @@ -290,7 +287,7 @@ def evaluate_img_google(self, img_id, cat_id, area_rng): gt, dt = self._get_gt_dt(img_id, cat_id) if len(gt) == 0 and len(dt) == 0: return None - + if len(dt) == 0: return { "image_id": img_id, @@ -300,13 +297,11 @@ def evaluate_img_google(self, img_id, cat_id, area_rng): "dt_matches": np.array([], dtype=np.int32).reshape(1, -1), "dt_scores": [], "dt_ignore": np.array([], dtype=np.int32).reshape(1, -1), - 'num_gt': len(gt) + "num_gt": len(gt), } - no_crowd_inds = [i for i, g in enumerate(gt) \ - if ('iscrowd' not in g) or g['iscrowd'] == 0] - crowd_inds = [i for i, g in enumerate(gt) \ - if 'iscrowd' in g and g['iscrowd'] == 1] + no_crowd_inds = [i for i, g in enumerate(gt) if ("iscrowd" not in g) or g["iscrowd"] == 0] + crowd_inds = [i for i, g in enumerate(gt) if "iscrowd" in g and g["iscrowd"] == 1] dt_idx = np.argsort([-d["score"] for d in dt], kind="mergesort") if len(self.ious[img_id, cat_id]) > 0: @@ -318,20 +313,20 @@ def evaluate_img_google(self, img_id, cat_id, area_rng): else: iou = np.zeros((len(dt_idx), 0)) ioa = np.zeros((len(dt_idx), 0)) - scores = np.array([dt[i]['score'] for i in dt_idx]) + scores = np.array([dt[i]["score"] for i in dt_idx]) num_detected_boxes = len(dt) tp_fp_labels = np.zeros(num_detected_boxes, dtype=bool) is_matched_to_group_of = np.zeros(num_detected_boxes, dtype=bool) - def compute_match_iou(iou): + def compute_match_iou(iou) -> None: max_overlap_gt_ids = np.argmax(iou, axis=1) is_gt_detected = np.zeros(iou.shape[1], dtype=bool) for i in range(num_detected_boxes): gt_id = max_overlap_gt_ids[i] - is_evaluatable = (not tp_fp_labels[i] and - iou[i, gt_id] >= 0.5 and - not is_matched_to_group_of[i]) + is_evaluatable = ( + not tp_fp_labels[i] and iou[i, gt_id] >= 0.5 and not is_matched_to_group_of[i] + ) if is_evaluatable: if not is_gt_detected[gt_id]: tp_fp_labels[i] = True @@ -339,14 +334,13 @@ def compute_match_iou(iou): def compute_match_ioa(ioa): scores_group_of = np.zeros(ioa.shape[1], dtype=float) - tp_fp_labels_group_of = np.ones( - ioa.shape[1], dtype=float) + tp_fp_labels_group_of = np.ones(ioa.shape[1], dtype=float) max_overlap_group_of_gt_ids = np.argmax(ioa, axis=1) for i in range(num_detected_boxes): gt_id = max_overlap_group_of_gt_ids[i] - is_evaluatable = (not tp_fp_labels[i] and - ioa[i, gt_id] >= 0.5 and - not is_matched_to_group_of[i]) + is_evaluatable = ( + not tp_fp_labels[i] and ioa[i, gt_id] >= 0.5 and not is_matched_to_group_of[i] + ) if is_evaluatable: is_matched_to_group_of[i] = True scores_group_of[gt_id] = max(scores_group_of[gt_id], scores[i]) @@ -365,25 +359,26 @@ def compute_match_ioa(ioa): if ioa.shape[1] > 0: scores_box_group_of, tp_fp_labels_box_group_of = compute_match_ioa(ioa) - valid_entries = (~is_matched_to_group_of) + valid_entries = ~is_matched_to_group_of - scores = np.concatenate( - (scores[valid_entries], scores_box_group_of)) + scores = np.concatenate((scores[valid_entries], scores_box_group_of)) tp_fps = np.concatenate( - (tp_fp_labels[valid_entries].astype(float), - tp_fp_labels_box_group_of)) - + (tp_fp_labels[valid_entries].astype(float), tp_fp_labels_box_group_of) + ) + return { "image_id": img_id, "category_id": cat_id, "area_rng": area_rng, - "dt_matches": np.array([1 if x > 0 else 0 for x in tp_fps], dtype=np.int32).reshape(1, -1), + "dt_matches": np.array([1 if x > 0 else 0 for x in tp_fps], dtype=np.int32).reshape( + 1, -1 + ), "dt_scores": [x for x in scores], - "dt_ignore": np.array([0 for x in scores], dtype=np.int32).reshape(1, -1), - 'num_gt': len(gt) + "dt_ignore": np.array([0 for x in scores], dtype=np.int32).reshape(1, -1), + "num_gt": len(gt), } - def accumulate(self): + def accumulate(self) -> None: """Accumulate per image evaluation results and store the result in self.eval. """ @@ -405,9 +400,7 @@ def accumulate(self): num_imgs = len(self.params.img_ids) # -1 for absent categories - precision = -np.ones( - (num_thrs, num_recalls, num_cats, num_area_rngs) - ) + precision = -np.ones((num_thrs, num_recalls, num_cats, num_area_rngs)) recall = -np.ones((num_thrs, num_cats, num_area_rngs)) # Initialize dt_pointers @@ -422,12 +415,9 @@ def accumulate(self): Nk = cat_idx * num_area_rngs * num_imgs for area_idx in range(num_area_rngs): Na = area_idx * num_imgs - E = [ - self.eval_imgs[Nk + Na + img_idx] - for img_idx in range(num_imgs) - ] + E = [self.eval_imgs[Nk + Na + img_idx] for img_idx in range(num_imgs)] # Remove elements which are None - E = [e for e in E if not e is None] + E = [e for e in E if e is not None] if len(E) == 0: continue @@ -437,7 +427,7 @@ def accumulate(self): dt_m = np.concatenate([e["dt_matches"] for e in E], axis=1)[:, dt_idx] dt_ig = np.concatenate([e["dt_ignore"] for e in E], axis=1)[:, dt_idx] - num_gt = sum([e['num_gt'] for e in E]) + num_gt = sum([e["num_gt"] for e in E]) if num_gt == 0: continue @@ -451,16 +441,14 @@ def accumulate(self): "fps": fps, } - for iou_thr_idx, (tp, fp) in enumerate(zip(tp_sum, fp_sum)): + for iou_thr_idx, (tp, fp) in enumerate(zip(tp_sum, fp_sum, strict=False)): tp = np.array(tp) fp = np.array(fp) num_tp = len(tp) rc = tp / num_gt - + if num_tp: - recall[iou_thr_idx, cat_idx, area_idx] = rc[ - -1 - ] + recall[iou_thr_idx, cat_idx, area_idx] = rc[-1] else: recall[iou_thr_idx, cat_idx, area_idx] = 0 @@ -473,8 +461,8 @@ def accumulate(self): pr[i - 1] = pr[i] mAP = compute_average_precision( - np.array(pr, np.float).reshape(-1), - np.array(rc, np.float).reshape(-1)) + np.array(pr, np.float).reshape(-1), np.array(rc, np.float).reshape(-1) + ) precision[iou_thr_idx, :, cat_idx, area_idx] = mAP self.eval = { @@ -500,16 +488,15 @@ def summarize(self): if not self.eval: raise RuntimeError("Please run accumulate() first.") - max_dets = self.params.max_dets - self.results["AP50"] = self._summarize('ap') + self.results["AP50"] = self._summarize("ap") - def run(self): + def run(self) -> None: """Wrapper function which calculates the results.""" self.evaluate() self.accumulate() self.summarize() - def print_results(self): + def print_results(self) -> None: template = " {:<18} {} @[ IoU={:<9} | area={:>6s} | maxDets={:>3d} catIds={:>3s}] = {:0.3f}" for key, value in self.results.items(): @@ -522,12 +509,10 @@ def print_results(self): _type = "(AR)" if len(key) > 2 and key[2].isdigit(): - iou_thr = (float(key[2:]) / 100) - iou = "{:0.2f}".format(iou_thr) + iou_thr = float(key[2:]) / 100 + iou = f"{iou_thr:0.2f}" else: - iou = "{:0.2f}:{:0.2f}".format( - self.params.iou_thrs[0], self.params.iou_thrs[-1] - ) + iou = f"{self.params.iou_thrs[0]:0.2f}:{self.params.iou_thrs[-1]:0.2f}" cat_group_name = "all" area_rng = "all" @@ -541,7 +526,7 @@ def get_results(self): class Params: - def __init__(self, iou_type): + def __init__(self, iou_type) -> None: self.img_ids = [] self.cat_ids = [] # np.arange causes trouble. the data point on arange is slightly @@ -555,7 +540,7 @@ def __init__(self, iou_type): self.max_dets = 1000 self.area_rng = [ - [0 ** 2, 1e5 ** 2], + [0**2, 1e5**2], ] self.area_rng_lbl = ["all"] self.use_cats = 1 @@ -563,7 +548,7 @@ def __init__(self, iou_type): class OIDEvaluator(DatasetEvaluator): - def __init__(self, dataset_name, cfg, distributed, output_dir=None): + def __init__(self, dataset_name: str, cfg, distributed, output_dir=None) -> None: self._distributed = distributed self._output_dir = output_dir @@ -578,16 +563,15 @@ def __init__(self, dataset_name, cfg, distributed, output_dir=None): self._do_evaluation = len(self._oid_api.get_ann_ids()) > 0 self._mask_on = cfg.MODEL.MASK_ON - def reset(self): + def reset(self) -> None: self._predictions = [] self._oid_results = [] - def process(self, inputs, outputs): - for input, output in zip(inputs, outputs): + def process(self, inputs, outputs) -> None: + for input, output in zip(inputs, outputs, strict=False): prediction = {"image_id": input["image_id"]} instances = output["instances"].to(self._cpu_device) - prediction["instances"] = instances_to_coco_json( - instances, input["image_id"]) + prediction["instances"] = instances_to_coco_json(instances, input["image_id"]) self._predictions.append(prediction) def evaluate(self): @@ -604,17 +588,15 @@ def evaluate(self): return {} self._logger.info("Preparing results in the OID format ...") - self._oid_results = list( - itertools.chain(*[x["instances"] for x in self._predictions])) + self._oid_results = list(itertools.chain(*[x["instances"] for x in self._predictions])) # unmap the category ids for LVIS (from 0-indexed to 1-indexed) for result in self._oid_results: result["category_id"] += 1 PathManager.mkdirs(self._output_dir) - file_path = os.path.join( - self._output_dir, "oid_instances_results.json") - self._logger.info("Saving results to {}".format(file_path)) + file_path = os.path.join(self._output_dir, "oid_instances_results.json") + self._logger.info(f"Saving results to {file_path}") with PathManager.open(file_path, "w") as f: f.write(json.dumps(self._oid_results)) f.flush() @@ -631,37 +613,35 @@ def evaluate(self): eval_seg=self._mask_on, class_names=self._metadata.get("thing_classes"), ) - self._results['bbox'] = res + self._results["bbox"] = res mAP_out_path = os.path.join(self._output_dir, "oid_mAP.npy") - self._logger.info('Saving mAP to' + mAP_out_path) + self._logger.info("Saving mAP to" + mAP_out_path) np.save(mAP_out_path, mAP) return copy.deepcopy(self._results) -def _evaluate_predictions_on_oid( - oid_gt, oid_results_path, eval_seg=False, - class_names=None): + +def _evaluate_predictions_on_oid(oid_gt, oid_results_path, eval_seg: bool=False, class_names: Optional[Sequence[str]]=None): logger = logging.getLogger(__name__) - metrics = ["AP50", "AP50_expand"] results = {} - oid_eval = OIDEval(oid_gt, oid_results_path, 'bbox', expand_pred_label=False) + oid_eval = OIDEval(oid_gt, oid_results_path, "bbox", expand_pred_label=False) oid_eval.run() oid_eval.print_results() results["AP50"] = oid_eval.get_results()["AP50"] if eval_seg: - oid_eval = OIDEval(oid_gt, oid_results_path, 'segm', expand_pred_label=False) + oid_eval = OIDEval(oid_gt, oid_results_path, "segm", expand_pred_label=False) oid_eval.run() oid_eval.print_results() results["AP50_segm"] = oid_eval.get_results()["AP50"] else: - oid_eval = OIDEval(oid_gt, oid_results_path, 'bbox', expand_pred_label=True) + oid_eval = OIDEval(oid_gt, oid_results_path, "bbox", expand_pred_label=True) oid_eval.run() oid_eval.print_results() results["AP50_expand"] = oid_eval.get_results()["AP50"] mAP = np.zeros(len(class_names)) - 1 - precisions = oid_eval.eval['precision'] + precisions = oid_eval.eval["precision"] assert len(class_names) == precisions.shape[2] results_per_category = [] id2apiid = sorted(oid_gt.get_cat_ids()) @@ -672,10 +652,15 @@ def _evaluate_predictions_on_oid( ap = np.mean(precision) if precision.size else float("nan") inst_num = len(oid_gt.get_ann_ids(cat_ids=[id2apiid[idx]])) if inst_num > 0: - results_per_category.append(("{} {}".format( - name.replace(' ', '_'), - inst_num if inst_num < 1000 else '{:.1f}k'.format(inst_num / 1000)), - float(ap * 100))) + results_per_category.append( + ( + "{} {}".format( + name.replace(" ", "_"), + inst_num if inst_num < 1000 else f"{inst_num / 1000:.1f}k", + ), + float(ap * 100), + ) + ) inst_aware_ap += inst_num * ap inst_count += inst_num mAP[idx] = ap @@ -691,9 +676,8 @@ def _evaluate_predictions_on_oid( headers=["category", "AP"] * (N_COLS // 2), numalign="left", ) - logger.info("Per-category {} AP: \n".format('bbox') + table) - logger.info("Instance-aware {} AP: {:.4f}".format('bbox', inst_aware_ap)) + logger.info("Per-category {} AP: \n".format("bbox") + table) + logger.info("Instance-aware {} AP: {:.4f}".format("bbox", inst_aware_ap)) - logger.info("Evaluation results for bbox: \n" + \ - create_small_table(results)) - return results, mAP \ No newline at end of file + logger.info("Evaluation results for bbox: \n" + create_small_table(results)) + return results, mAP diff --git a/dimos/models/Detic/detic/modeling/backbone/swintransformer.py b/dimos/models/Detic/detic/modeling/backbone/swintransformer.py index 21cabb37dd..b7da6328e3 100644 --- a/dimos/models/Detic/detic/modeling/backbone/swintransformer.py +++ b/dimos/models/Detic/detic/modeling/backbone/swintransformer.py @@ -9,26 +9,29 @@ # Modified by Xingyi Zhou from https://github.com/SwinTransformer/Swin-Transformer-Object-Detection/blob/master/mmdet/models/backbones/swin_transformer.py -import torch -import torch.nn as nn -import torch.nn.functional as F -import torch.utils.checkpoint as checkpoint -import numpy as np -from timm.models.layers import DropPath, to_2tuple, trunc_normal_ - +from centernet.modeling.backbone.bifpn import BiFPN +from centernet.modeling.backbone.fpn_p5 import LastLevelP6P7_P5 from detectron2.layers import ShapeSpec from detectron2.modeling.backbone.backbone import Backbone from detectron2.modeling.backbone.build import BACKBONE_REGISTRY from detectron2.modeling.backbone.fpn import FPN +import numpy as np +from timm.models.layers import DropPath, to_2tuple, trunc_normal_ +import torch +import torch.nn as nn +import torch.nn.functional as F +import torch.utils.checkpoint as checkpoint +from typing import Optional, Sequence -from centernet.modeling.backbone.fpn_p5 import LastLevelP6P7_P5 -from centernet.modeling.backbone.bifpn import BiFPN # from .checkpoint import load_checkpoint + class Mlp(nn.Module): - """ Multilayer perceptron.""" + """Multilayer perceptron.""" - def __init__(self, in_features, hidden_features=None, out_features=None, act_layer=nn.GELU, drop=0.): + def __init__( + self, in_features, hidden_features=None, out_features=None, act_layer=nn.GELU, drop: float=0.0 + ) -> None: super().__init__() out_features = out_features or in_features hidden_features = hidden_features or in_features @@ -46,7 +49,7 @@ def forward(self, x): return x -def window_partition(x, window_size): +def window_partition(x, window_size: int): """ Args: x: (B, H, W, C) @@ -60,7 +63,7 @@ def window_partition(x, window_size): return windows -def window_reverse(windows, window_size, H, W): +def window_reverse(windows, window_size: int, H, W): """ Args: windows: (num_windows*B, window_size, window_size, C) @@ -77,7 +80,7 @@ def window_reverse(windows, window_size, H, W): class WindowAttention(nn.Module): - """ Window based multi-head self attention (W-MSA) module with relative position bias. + """Window based multi-head self attention (W-MSA) module with relative position bias. It supports both of shifted and non-shifted window. Args: dim (int): Number of input channels. @@ -89,18 +92,27 @@ class WindowAttention(nn.Module): proj_drop (float, optional): Dropout ratio of output. Default: 0.0 """ - def __init__(self, dim, window_size, num_heads, qkv_bias=True, qk_scale=None, attn_drop=0., proj_drop=0.): - + def __init__( + self, + dim: int, + window_size: int, + num_heads: int, + qkv_bias: bool=True, + qk_scale=None, + attn_drop: float=0.0, + proj_drop: float=0.0, + ) -> None: super().__init__() self.dim = dim self.window_size = window_size # Wh, Ww self.num_heads = num_heads head_dim = dim // num_heads - self.scale = qk_scale or head_dim ** -0.5 + self.scale = qk_scale or head_dim**-0.5 # define a parameter table of relative position bias self.relative_position_bias_table = nn.Parameter( - torch.zeros((2 * window_size[0] - 1) * (2 * window_size[1] - 1), num_heads)) # 2*Wh-1 * 2*Ww-1, nH + torch.zeros((2 * window_size[0] - 1) * (2 * window_size[1] - 1), num_heads) + ) # 2*Wh-1 * 2*Ww-1, nH # get pair-wise relative position index for each token inside the window coords_h = torch.arange(self.window_size[0]) @@ -120,25 +132,34 @@ def __init__(self, dim, window_size, num_heads, qkv_bias=True, qk_scale=None, at self.proj = nn.Linear(dim, dim) self.proj_drop = nn.Dropout(proj_drop) - trunc_normal_(self.relative_position_bias_table, std=.02) + trunc_normal_(self.relative_position_bias_table, std=0.02) self.softmax = nn.Softmax(dim=-1) def forward(self, x, mask=None): - """ Forward function. + """Forward function. Args: x: input features with shape of (num_windows*B, N, C) mask: (0/-inf) mask with shape of (num_windows, Wh*Ww, Wh*Ww) or None """ B_, N, C = x.shape - qkv = self.qkv(x).reshape(B_, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4) + qkv = ( + self.qkv(x) + .reshape(B_, N, 3, self.num_heads, C // self.num_heads) + .permute(2, 0, 3, 1, 4) + ) q, k, v = qkv[0], qkv[1], qkv[2] # make torchscript happy (cannot use tensor as tuple) q = q * self.scale - attn = (q @ k.transpose(-2, -1)) - - relative_position_bias = self.relative_position_bias_table[self.relative_position_index.view(-1)].view( - self.window_size[0] * self.window_size[1], self.window_size[0] * self.window_size[1], -1) # Wh*Ww,Wh*Ww,nH - relative_position_bias = relative_position_bias.permute(2, 0, 1).contiguous() # nH, Wh*Ww, Wh*Ww + attn = q @ k.transpose(-2, -1) + + relative_position_bias = self.relative_position_bias_table[ + self.relative_position_index.view(-1) + ].view( + self.window_size[0] * self.window_size[1], self.window_size[0] * self.window_size[1], -1 + ) # Wh*Ww,Wh*Ww,nH + relative_position_bias = relative_position_bias.permute( + 2, 0, 1 + ).contiguous() # nH, Wh*Ww, Wh*Ww attn = attn + relative_position_bias.unsqueeze(0) if mask is not None: @@ -158,7 +179,7 @@ def forward(self, x, mask=None): class SwinTransformerBlock(nn.Module): - """ Swin Transformer Block. + """Swin Transformer Block. Args: dim (int): Number of input channels. num_heads (int): Number of attention heads. @@ -174,9 +195,21 @@ class SwinTransformerBlock(nn.Module): norm_layer (nn.Module, optional): Normalization layer. Default: nn.LayerNorm """ - def __init__(self, dim, num_heads, window_size=7, shift_size=0, - mlp_ratio=4., qkv_bias=True, qk_scale=None, drop=0., attn_drop=0., drop_path=0., - act_layer=nn.GELU, norm_layer=nn.LayerNorm): + def __init__( + self, + dim: int, + num_heads: int, + window_size: int=7, + shift_size: int=0, + mlp_ratio: float=4.0, + qkv_bias: bool=True, + qk_scale=None, + drop: float=0.0, + attn_drop: float=0.0, + drop_path: float=0.0, + act_layer=nn.GELU, + norm_layer=nn.LayerNorm, + ) -> None: super().__init__() self.dim = dim self.num_heads = num_heads @@ -187,19 +220,27 @@ def __init__(self, dim, num_heads, window_size=7, shift_size=0, self.norm1 = norm_layer(dim) self.attn = WindowAttention( - dim, window_size=to_2tuple(self.window_size), num_heads=num_heads, - qkv_bias=qkv_bias, qk_scale=qk_scale, attn_drop=attn_drop, proj_drop=drop) - - self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity() + dim, + window_size=to_2tuple(self.window_size), + num_heads=num_heads, + qkv_bias=qkv_bias, + qk_scale=qk_scale, + attn_drop=attn_drop, + proj_drop=drop, + ) + + self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity() self.norm2 = norm_layer(dim) mlp_hidden_dim = int(dim * mlp_ratio) - self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim, act_layer=act_layer, drop=drop) + self.mlp = Mlp( + in_features=dim, hidden_features=mlp_hidden_dim, act_layer=act_layer, drop=drop + ) self.H = None self.W = None def forward(self, x, mask_matrix): - """ Forward function. + """Forward function. Args: x: Input feature, tensor size (B, H*W, C). H, W: Spatial resolution of the input feature. @@ -229,8 +270,12 @@ def forward(self, x, mask_matrix): attn_mask = None # partition windows - x_windows = window_partition(shifted_x, self.window_size) # nW*B, window_size, window_size, C - x_windows = x_windows.view(-1, self.window_size * self.window_size, C) # nW*B, window_size*window_size, C + x_windows = window_partition( + shifted_x, self.window_size + ) # nW*B, window_size, window_size, C + x_windows = x_windows.view( + -1, self.window_size * self.window_size, C + ) # nW*B, window_size*window_size, C # W-MSA/SW-MSA attn_windows = self.attn(x_windows, mask=attn_mask) # nW*B, window_size*window_size, C @@ -258,19 +303,20 @@ def forward(self, x, mask_matrix): class PatchMerging(nn.Module): - """ Patch Merging Layer + """Patch Merging Layer Args: dim (int): Number of input channels. norm_layer (nn.Module, optional): Normalization layer. Default: nn.LayerNorm """ - def __init__(self, dim, norm_layer=nn.LayerNorm): + + def __init__(self, dim: int, norm_layer=nn.LayerNorm) -> None: super().__init__() self.dim = dim self.reduction = nn.Linear(4 * dim, 2 * dim, bias=False) self.norm = norm_layer(4 * dim) def forward(self, x, H, W): - """ Forward function. + """Forward function. Args: x: Input feature, tensor size (B, H*W, C). H, W: Spatial resolution of the input feature. @@ -299,7 +345,7 @@ def forward(self, x, H, W): class BasicLayer(nn.Module): - """ A basic Swin Transformer layer for one stage. + """A basic Swin Transformer layer for one stage. Args: dim (int): Number of feature channels depth (int): Depths of this stage. @@ -316,20 +362,22 @@ class BasicLayer(nn.Module): use_checkpoint (bool): Whether to use checkpointing to save memory. Default: False. """ - def __init__(self, - dim, - depth, - num_heads, - window_size=7, - mlp_ratio=4., - qkv_bias=True, - qk_scale=None, - drop=0., - attn_drop=0., - drop_path=0., - norm_layer=nn.LayerNorm, - downsample=None, - use_checkpoint=False): + def __init__( + self, + dim: int, + depth: int, + num_heads: int, + window_size: int=7, + mlp_ratio: float=4.0, + qkv_bias: bool=True, + qk_scale=None, + drop: float=0.0, + attn_drop: float=0.0, + drop_path: float=0.0, + norm_layer=nn.LayerNorm, + downsample=None, + use_checkpoint: bool=False, + ) -> None: super().__init__() self.window_size = window_size self.shift_size = window_size // 2 @@ -337,20 +385,24 @@ def __init__(self, self.use_checkpoint = use_checkpoint # build blocks - self.blocks = nn.ModuleList([ - SwinTransformerBlock( - dim=dim, - num_heads=num_heads, - window_size=window_size, - shift_size=0 if (i % 2 == 0) else window_size // 2, - mlp_ratio=mlp_ratio, - qkv_bias=qkv_bias, - qk_scale=qk_scale, - drop=drop, - attn_drop=attn_drop, - drop_path=drop_path[i] if isinstance(drop_path, list) else drop_path, - norm_layer=norm_layer) - for i in range(depth)]) + self.blocks = nn.ModuleList( + [ + SwinTransformerBlock( + dim=dim, + num_heads=num_heads, + window_size=window_size, + shift_size=0 if (i % 2 == 0) else window_size // 2, + mlp_ratio=mlp_ratio, + qkv_bias=qkv_bias, + qk_scale=qk_scale, + drop=drop, + attn_drop=attn_drop, + drop_path=drop_path[i] if isinstance(drop_path, list) else drop_path, + norm_layer=norm_layer, + ) + for i in range(depth) + ] + ) # patch merging layer if downsample is not None: @@ -359,7 +411,7 @@ def __init__(self, self.downsample = None def forward(self, x, H, W): - """ Forward function. + """Forward function. Args: x: Input feature, tensor size (B, H*W, C). H, W: Spatial resolution of the input feature. @@ -369,22 +421,30 @@ def forward(self, x, H, W): Hp = int(np.ceil(H / self.window_size)) * self.window_size Wp = int(np.ceil(W / self.window_size)) * self.window_size img_mask = torch.zeros((1, Hp, Wp, 1), device=x.device) # 1 Hp Wp 1 - h_slices = (slice(0, -self.window_size), - slice(-self.window_size, -self.shift_size), - slice(-self.shift_size, None)) - w_slices = (slice(0, -self.window_size), - slice(-self.window_size, -self.shift_size), - slice(-self.shift_size, None)) + h_slices = ( + slice(0, -self.window_size), + slice(-self.window_size, -self.shift_size), + slice(-self.shift_size, None), + ) + w_slices = ( + slice(0, -self.window_size), + slice(-self.window_size, -self.shift_size), + slice(-self.shift_size, None), + ) cnt = 0 for h in h_slices: for w in w_slices: img_mask[:, h, w, :] = cnt cnt += 1 - mask_windows = window_partition(img_mask, self.window_size) # nW, window_size, window_size, 1 + mask_windows = window_partition( + img_mask, self.window_size + ) # nW, window_size, window_size, 1 mask_windows = mask_windows.view(-1, self.window_size * self.window_size) attn_mask = mask_windows.unsqueeze(1) - mask_windows.unsqueeze(2) - attn_mask = attn_mask.masked_fill(attn_mask != 0, float(-100.0)).masked_fill(attn_mask == 0, float(0.0)) + attn_mask = attn_mask.masked_fill(attn_mask != 0, (-100.0)).masked_fill( + attn_mask == 0, 0.0 + ) for blk in self.blocks: blk.H, blk.W = H, W @@ -401,7 +461,7 @@ def forward(self, x, H, W): class PatchEmbed(nn.Module): - """ Image to Patch Embedding + """Image to Patch Embedding Args: patch_size (int): Patch token size. Default: 4. in_chans (int): Number of input image channels. Default: 3. @@ -409,7 +469,7 @@ class PatchEmbed(nn.Module): norm_layer (nn.Module, optional): Normalization layer. Default: None """ - def __init__(self, patch_size=4, in_chans=3, embed_dim=96, norm_layer=None): + def __init__(self, patch_size: int=4, in_chans: int=3, embed_dim: int=96, norm_layer=None) -> None: super().__init__() patch_size = to_2tuple(patch_size) self.patch_size = patch_size @@ -443,7 +503,7 @@ def forward(self, x): class SwinTransformer(Backbone): - """ Swin Transformer backbone. + """Swin Transformer backbone. A PyTorch impl of : `Swin Transformer: Hierarchical Vision Transformer using Shifted Windows` - https://arxiv.org/pdf/2103.14030 Args: @@ -470,26 +530,32 @@ class SwinTransformer(Backbone): use_checkpoint (bool): Whether to use checkpointing to save memory. Default: False. """ - def __init__(self, - pretrain_img_size=224, - patch_size=4, - in_chans=3, - embed_dim=96, - depths=[2, 2, 6, 2], - num_heads=[3, 6, 12, 24], - window_size=7, - mlp_ratio=4., - qkv_bias=True, - qk_scale=None, - drop_rate=0., - attn_drop_rate=0., - drop_path_rate=0.2, - norm_layer=nn.LayerNorm, - ape=False, - patch_norm=True, - out_indices=(0, 1, 2, 3), - frozen_stages=-1, - use_checkpoint=False): + def __init__( + self, + pretrain_img_size: int=224, + patch_size: int=4, + in_chans: int=3, + embed_dim: int=96, + depths: Optional[Sequence[int]]=None, + num_heads: Optional[int]=None, + window_size: int=7, + mlp_ratio: float=4.0, + qkv_bias: bool=True, + qk_scale=None, + drop_rate: float=0.0, + attn_drop_rate: float=0.0, + drop_path_rate: float=0.2, + norm_layer=nn.LayerNorm, + ape: bool=False, + patch_norm: bool=True, + out_indices=(0, 1, 2, 3), + frozen_stages=-1, + use_checkpoint: bool=False, + ) -> None: + if num_heads is None: + num_heads = [3, 6, 12, 24] + if depths is None: + depths = [2, 2, 6, 2] super().__init__() self.pretrain_img_size = pretrain_img_size @@ -502,28 +568,38 @@ def __init__(self, # split image into non-overlapping patches self.patch_embed = PatchEmbed( - patch_size=patch_size, in_chans=in_chans, embed_dim=embed_dim, - norm_layer=norm_layer if self.patch_norm else None) + patch_size=patch_size, + in_chans=in_chans, + embed_dim=embed_dim, + norm_layer=norm_layer if self.patch_norm else None, + ) # absolute position embedding if self.ape: pretrain_img_size = to_2tuple(pretrain_img_size) patch_size = to_2tuple(patch_size) - patches_resolution = [pretrain_img_size[0] // patch_size[0], pretrain_img_size[1] // patch_size[1]] + patches_resolution = [ + pretrain_img_size[0] // patch_size[0], + pretrain_img_size[1] // patch_size[1], + ] - self.absolute_pos_embed = nn.Parameter(torch.zeros(1, embed_dim, patches_resolution[0], patches_resolution[1])) - trunc_normal_(self.absolute_pos_embed, std=.02) + self.absolute_pos_embed = nn.Parameter( + torch.zeros(1, embed_dim, patches_resolution[0], patches_resolution[1]) + ) + trunc_normal_(self.absolute_pos_embed, std=0.02) self.pos_drop = nn.Dropout(p=drop_rate) # stochastic depth - dpr = [x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))] # stochastic depth decay rule + dpr = [ + x.item() for x in torch.linspace(0, drop_path_rate, sum(depths)) + ] # stochastic depth decay rule # build layers self.layers = nn.ModuleList() for i_layer in range(self.num_layers): layer = BasicLayer( - dim=int(embed_dim * 2 ** i_layer), + dim=int(embed_dim * 2**i_layer), depth=depths[i_layer], num_heads=num_heads[i_layer], window_size=window_size, @@ -532,33 +608,31 @@ def __init__(self, qk_scale=qk_scale, drop=drop_rate, attn_drop=attn_drop_rate, - drop_path=dpr[sum(depths[:i_layer]):sum(depths[:i_layer + 1])], + drop_path=dpr[sum(depths[:i_layer]) : sum(depths[: i_layer + 1])], norm_layer=norm_layer, downsample=PatchMerging if (i_layer < self.num_layers - 1) else None, - use_checkpoint=use_checkpoint) + use_checkpoint=use_checkpoint, + ) self.layers.append(layer) - num_features = [int(embed_dim * 2 ** i) for i in range(self.num_layers)] + num_features = [int(embed_dim * 2**i) for i in range(self.num_layers)] self.num_features = num_features # add a norm layer for each output for i_layer in out_indices: layer = norm_layer(num_features[i_layer]) - layer_name = f'norm{i_layer}' + layer_name = f"norm{i_layer}" self.add_module(layer_name, layer) self._freeze_stages() - self._out_features = ['swin{}'.format(i) for i in self.out_indices] + self._out_features = [f"swin{i}" for i in self.out_indices] self._out_feature_channels = { - 'swin{}'.format(i): self.embed_dim * 2 ** i for i in self.out_indices - } - self._out_feature_strides = { - 'swin{}'.format(i): 2 ** (i + 2) for i in self.out_indices + f"swin{i}": self.embed_dim * 2**i for i in self.out_indices } + self._out_feature_strides = {f"swin{i}": 2 ** (i + 2) for i in self.out_indices} self._size_devisibility = 32 - - def _freeze_stages(self): + def _freeze_stages(self) -> None: if self.frozen_stages >= 0: self.patch_embed.eval() for param in self.patch_embed.parameters(): @@ -575,16 +649,16 @@ def _freeze_stages(self): for param in m.parameters(): param.requires_grad = False - def init_weights(self, pretrained=None): + def init_weights(self, pretrained: Optional[bool]=None): """Initialize the weights in backbone. Args: pretrained (str, optional): Path to pre-trained weights. Defaults to None. """ - def _init_weights(m): + def _init_weights(m) -> None: if isinstance(m, nn.Linear): - trunc_normal_(m.weight, std=.02) + trunc_normal_(m.weight, std=0.02) if isinstance(m, nn.Linear) and m.bias is not None: nn.init.constant_(m.bias, 0) elif isinstance(m, nn.LayerNorm): @@ -597,7 +671,7 @@ def _init_weights(m): elif pretrained is None: self.apply(_init_weights) else: - raise TypeError('pretrained must be a str or None') + raise TypeError("pretrained must be a str or None") def forward(self, x): """Forward function.""" @@ -606,7 +680,9 @@ def forward(self, x): Wh, Ww = x.size(2), x.size(3) if self.ape: # interpolate the position embedding to the corresponding size - absolute_pos_embed = F.interpolate(self.absolute_pos_embed, size=(Wh, Ww), mode='bicubic') + absolute_pos_embed = F.interpolate( + self.absolute_pos_embed, size=(Wh, Ww), mode="bicubic" + ) x = (x + absolute_pos_embed).flatten(2).transpose(1, 2) # B Wh*Ww C else: x = x.flatten(2).transpose(1, 2) @@ -619,104 +695,104 @@ def forward(self, x): x_out, H, W, x, Wh, Ww = layer(x, Wh, Ww) if i in self.out_indices: - norm_layer = getattr(self, f'norm{i}') + norm_layer = getattr(self, f"norm{i}") x_out = norm_layer(x_out) out = x_out.view(-1, H, W, self.num_features[i]).permute(0, 3, 1, 2).contiguous() # outs.append(out) - outs['swin{}'.format(i)] = out + outs[f"swin{i}"] = out return outs - def train(self, mode=True): + def train(self, mode: bool=True) -> None: """Convert the model into training mode while keep layers freezed.""" - super(SwinTransformer, self).train(mode) + super().train(mode) self._freeze_stages() + size2config = { - 'T': { - 'window_size': 7, - 'embed_dim': 96, - 'depth': [2, 2, 6, 2], - 'num_heads': [3, 6, 12, 24], - 'drop_path_rate': 0.2, - 'pretrained': 'models/swin_tiny_patch4_window7_224.pth' + "T": { + "window_size": 7, + "embed_dim": 96, + "depth": [2, 2, 6, 2], + "num_heads": [3, 6, 12, 24], + "drop_path_rate": 0.2, + "pretrained": "models/swin_tiny_patch4_window7_224.pth", }, - 'S': { - 'window_size': 7, - 'embed_dim': 96, - 'depth': [2, 2, 18, 2], - 'num_heads': [3, 6, 12, 24], - 'drop_path_rate': 0.2, - 'pretrained': 'models/swin_small_patch4_window7_224.pth' + "S": { + "window_size": 7, + "embed_dim": 96, + "depth": [2, 2, 18, 2], + "num_heads": [3, 6, 12, 24], + "drop_path_rate": 0.2, + "pretrained": "models/swin_small_patch4_window7_224.pth", }, - 'B': { - 'window_size': 7, - 'embed_dim': 128, - 'depth': [2, 2, 18, 2], - 'num_heads': [4, 8, 16, 32], - 'drop_path_rate': 0.3, - 'pretrained': 'models/swin_base_patch4_window7_224.pth' + "B": { + "window_size": 7, + "embed_dim": 128, + "depth": [2, 2, 18, 2], + "num_heads": [4, 8, 16, 32], + "drop_path_rate": 0.3, + "pretrained": "models/swin_base_patch4_window7_224.pth", }, - 'B-22k': { - 'window_size': 7, - 'embed_dim': 128, - 'depth': [2, 2, 18, 2], - 'num_heads': [4, 8, 16, 32], - 'drop_path_rate': 0.3, - 'pretrained': 'models/swin_base_patch4_window7_224_22k.pth' + "B-22k": { + "window_size": 7, + "embed_dim": 128, + "depth": [2, 2, 18, 2], + "num_heads": [4, 8, 16, 32], + "drop_path_rate": 0.3, + "pretrained": "models/swin_base_patch4_window7_224_22k.pth", }, - 'B-22k-384': { - 'window_size': 12, - 'embed_dim': 128, - 'depth': [2, 2, 18, 2], - 'num_heads': [4, 8, 16, 32], - 'drop_path_rate': 0.3, - 'pretrained': 'models/swin_base_patch4_window12_384_22k.pth' + "B-22k-384": { + "window_size": 12, + "embed_dim": 128, + "depth": [2, 2, 18, 2], + "num_heads": [4, 8, 16, 32], + "drop_path_rate": 0.3, + "pretrained": "models/swin_base_patch4_window12_384_22k.pth", }, - 'L-22k': { - 'window_size': 7, - 'embed_dim': 192, - 'depth': [2, 2, 18, 2], - 'num_heads': [6, 12, 24, 48], - 'drop_path_rate': 0.3, # TODO (xingyi): this is unclear - 'pretrained': 'models/swin_large_patch4_window7_224_22k.pth' + "L-22k": { + "window_size": 7, + "embed_dim": 192, + "depth": [2, 2, 18, 2], + "num_heads": [6, 12, 24, 48], + "drop_path_rate": 0.3, # TODO (xingyi): this is unclear + "pretrained": "models/swin_large_patch4_window7_224_22k.pth", + }, + "L-22k-384": { + "window_size": 12, + "embed_dim": 192, + "depth": [2, 2, 18, 2], + "num_heads": [6, 12, 24, 48], + "drop_path_rate": 0.3, # TODO (xingyi): this is unclear + "pretrained": "models/swin_large_patch4_window12_384_22k.pth", }, - 'L-22k-384': { - 'window_size': 12, - 'embed_dim': 192, - 'depth': [2, 2, 18, 2], - 'num_heads': [6, 12, 24, 48], - 'drop_path_rate': 0.3, # TODO (xingyi): this is unclear - 'pretrained': 'models/swin_large_patch4_window12_384_22k.pth' - } } + @BACKBONE_REGISTRY.register() def build_swintransformer_backbone(cfg, input_shape): - """ - """ + """ """ config = size2config[cfg.MODEL.SWIN.SIZE] out_indices = cfg.MODEL.SWIN.OUT_FEATURES model = SwinTransformer( - embed_dim=config['embed_dim'], - window_size=config['window_size'], - depths=config['depth'], - num_heads=config['num_heads'], - drop_path_rate=config['drop_path_rate'], + embed_dim=config["embed_dim"], + window_size=config["window_size"], + depths=config["depth"], + num_heads=config["num_heads"], + drop_path_rate=config["drop_path_rate"], out_indices=out_indices, frozen_stages=-1, - use_checkpoint=cfg.MODEL.SWIN.USE_CHECKPOINT + use_checkpoint=cfg.MODEL.SWIN.USE_CHECKPOINT, ) # print('Initializing', config['pretrained']) - model.init_weights(config['pretrained']) + model.init_weights(config["pretrained"]) return model @BACKBONE_REGISTRY.register() def build_swintransformer_fpn_backbone(cfg, input_shape: ShapeSpec): - """ - """ + """ """ bottom_up = build_swintransformer_backbone(cfg, input_shape) in_features = cfg.MODEL.FPN.IN_FEATURES out_channels = cfg.MODEL.FPN.OUT_CHANNELS @@ -733,8 +809,7 @@ def build_swintransformer_fpn_backbone(cfg, input_shape: ShapeSpec): @BACKBONE_REGISTRY.register() def build_swintransformer_bifpn_backbone(cfg, input_shape: ShapeSpec): - """ - """ + """ """ bottom_up = build_swintransformer_backbone(cfg, input_shape) in_features = cfg.MODEL.FPN.IN_FEATURES backbone = BiFPN( @@ -747,4 +822,4 @@ def build_swintransformer_bifpn_backbone(cfg, input_shape: ShapeSpec): num_bifpn=cfg.MODEL.BIFPN.NUM_BIFPN, separable_conv=cfg.MODEL.BIFPN.SEPARABLE_CONV, ) - return backbone \ No newline at end of file + return backbone diff --git a/dimos/models/Detic/detic/modeling/backbone/timm.py b/dimos/models/Detic/detic/modeling/backbone/timm.py index 1ac62cb5d9..a15e03f875 100644 --- a/dimos/models/Detic/detic/modeling/backbone/timm.py +++ b/dimos/models/Detic/detic/modeling/backbone/timm.py @@ -1,50 +1,43 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright (c) Facebook, Inc. and its affiliates. -import math -from os.path import join -import numpy as np import copy -from functools import partial -import torch -from torch import nn -import torch.utils.model_zoo as model_zoo -import torch.nn.functional as F -import fvcore.nn.weight_init as weight_init - -from detectron2.modeling.backbone import FPN +from detectron2.layers.batch_norm import FrozenBatchNorm2d +from detectron2.modeling.backbone import FPN, Backbone from detectron2.modeling.backbone.build import BACKBONE_REGISTRY -from detectron2.layers.batch_norm import get_norm, FrozenBatchNorm2d -from detectron2.modeling.backbone import Backbone - +import fvcore.nn.weight_init as weight_init from timm import create_model +from timm.models.convnext import ConvNeXt, checkpoint_filter_fn, default_cfgs from timm.models.helpers import build_model_with_cfg from timm.models.registry import register_model -from timm.models.resnet import ResNet, Bottleneck -from timm.models.resnet import default_cfgs as default_cfgs_resnet -from timm.models.convnext import ConvNeXt, default_cfgs, checkpoint_filter_fn +from timm.models.resnet import Bottleneck, ResNet, default_cfgs as default_cfgs_resnet +import torch +from torch import nn +import torch.nn.functional as F @register_model -def convnext_tiny_21k(pretrained=False, **kwargs): +def convnext_tiny_21k(pretrained: bool=False, **kwargs): model_args = dict(depths=(3, 3, 9, 3), dims=(96, 192, 384, 768), **kwargs) - cfg = default_cfgs['convnext_tiny'] - cfg['url'] = 'https://dl.fbaipublicfiles.com/convnext/convnext_tiny_22k_224.pth' + cfg = default_cfgs["convnext_tiny"] + cfg["url"] = "https://dl.fbaipublicfiles.com/convnext/convnext_tiny_22k_224.pth" model = build_model_with_cfg( - ConvNeXt, 'convnext_tiny', pretrained, + ConvNeXt, + "convnext_tiny", + pretrained, default_cfg=cfg, pretrained_filter_fn=checkpoint_filter_fn, feature_cfg=dict(out_indices=(0, 1, 2, 3), flatten_sequential=True), - **model_args) + **model_args, + ) return model + class CustomResNet(ResNet): - def __init__(self, **kwargs): - self.out_indices = kwargs.pop('out_indices') + def __init__(self, **kwargs) -> None: + self.out_indices = kwargs.pop("out_indices") super().__init__(**kwargs) - def forward(self, x): x = self.conv1(x) x = self.bn1(x) @@ -61,41 +54,43 @@ def forward(self, x): ret.append(x) return [ret[i] for i in self.out_indices] - - def load_pretrained(self, cached_file): - data = torch.load(cached_file, map_location='cpu') - if 'state_dict' in data: - self.load_state_dict(data['state_dict']) + def load_pretrained(self, cached_file) -> None: + data = torch.load(cached_file, map_location="cpu") + if "state_dict" in data: + self.load_state_dict(data["state_dict"]) else: self.load_state_dict(data) model_params = { - 'resnet50_in21k': dict(block=Bottleneck, layers=[3, 4, 6, 3]), + "resnet50_in21k": dict(block=Bottleneck, layers=[3, 4, 6, 3]), } -def create_timm_resnet(variant, out_indices, pretrained=False, **kwargs): +def create_timm_resnet(variant, out_indices, pretrained: bool=False, **kwargs): params = model_params[variant] - default_cfgs_resnet['resnet50_in21k'] = \ - copy.deepcopy(default_cfgs_resnet['resnet50']) - default_cfgs_resnet['resnet50_in21k']['url'] = \ - 'https://miil-public-eu.oss-eu-central-1.aliyuncs.com/model-zoo/ImageNet_21K_P/models/resnet50_miil_21k.pth' - default_cfgs_resnet['resnet50_in21k']['num_classes'] = 11221 + default_cfgs_resnet["resnet50_in21k"] = copy.deepcopy(default_cfgs_resnet["resnet50"]) + default_cfgs_resnet["resnet50_in21k"]["url"] = ( + "https://miil-public-eu.oss-eu-central-1.aliyuncs.com/model-zoo/ImageNet_21K_P/models/resnet50_miil_21k.pth" + ) + default_cfgs_resnet["resnet50_in21k"]["num_classes"] = 11221 return build_model_with_cfg( - CustomResNet, variant, pretrained, + CustomResNet, + variant, + pretrained, default_cfg=default_cfgs_resnet[variant], out_indices=out_indices, pretrained_custom_load=True, **params, - **kwargs) + **kwargs, + ) class LastLevelP6P7_P5(nn.Module): - """ - """ - def __init__(self, in_channels, out_channels): + """ """ + + def __init__(self, in_channels, out_channels) -> None: super().__init__() self.num_levels = 2 self.in_feature = "p5" @@ -111,8 +106,7 @@ def forward(self, c5): def freeze_module(x): - """ - """ + """ """ for p in x.parameters(): p.requires_grad = False FrozenBatchNorm2d.convert_frozen_batchnorm(x) @@ -120,54 +114,57 @@ def freeze_module(x): class TIMM(Backbone): - def __init__(self, base_name, out_levels, freeze_at=0, norm='FrozenBN', pretrained=False): + def __init__(self, base_name: str, out_levels, freeze_at: int=0, norm: str="FrozenBN", pretrained: bool=False) -> None: super().__init__() out_indices = [x - 1 for x in out_levels] if base_name in model_params: - self.base = create_timm_resnet( - base_name, out_indices=out_indices, - pretrained=False) - elif 'eff' in base_name or 'resnet' in base_name or 'regnet' in base_name: + self.base = create_timm_resnet(base_name, out_indices=out_indices, pretrained=False) + elif "eff" in base_name or "resnet" in base_name or "regnet" in base_name: self.base = create_model( - base_name, features_only=True, - out_indices=out_indices, pretrained=pretrained) - elif 'convnext' in base_name: - drop_path_rate = 0.2 \ - if ('tiny' in base_name or 'small' in base_name) else 0.3 + base_name, features_only=True, out_indices=out_indices, pretrained=pretrained + ) + elif "convnext" in base_name: + drop_path_rate = 0.2 if ("tiny" in base_name or "small" in base_name) else 0.3 self.base = create_model( - base_name, features_only=True, - out_indices=out_indices, pretrained=pretrained, - drop_path_rate=drop_path_rate) + base_name, + features_only=True, + out_indices=out_indices, + pretrained=pretrained, + drop_path_rate=drop_path_rate, + ) else: assert 0, base_name - feature_info = [dict(num_chs=f['num_chs'], reduction=f['reduction']) \ - for i, f in enumerate(self.base.feature_info)] - self._out_features = ['layer{}'.format(x) for x in out_levels] + feature_info = [ + dict(num_chs=f["num_chs"], reduction=f["reduction"]) + for i, f in enumerate(self.base.feature_info) + ] + self._out_features = [f"layer{x}" for x in out_levels] self._out_feature_channels = { - 'layer{}'.format(l): feature_info[l - 1]['num_chs'] for l in out_levels} + f"layer{l}": feature_info[l - 1]["num_chs"] for l in out_levels + } self._out_feature_strides = { - 'layer{}'.format(l): feature_info[l - 1]['reduction'] for l in out_levels} + f"layer{l}": feature_info[l - 1]["reduction"] for l in out_levels + } self._size_divisibility = max(self._out_feature_strides.values()) - if 'resnet' in base_name: + if "resnet" in base_name: self.freeze(freeze_at) - if norm == 'FrozenBN': + if norm == "FrozenBN": self = FrozenBatchNorm2d.convert_frozen_batchnorm(self) - def freeze(self, freeze_at=0): - """ - """ + def freeze(self, freeze_at: int=0) -> None: + """ """ if freeze_at >= 1: - print('Frezing', self.base.conv1) + print("Frezing", self.base.conv1) self.base.conv1 = freeze_module(self.base.conv1) if freeze_at >= 2: - print('Frezing', self.base.layer1) + print("Frezing", self.base.layer1) self.base.layer1 = freeze_module(self.base.layer1) def forward(self, x): features = self.base(x) - ret = {k: v for k, v in zip(self._out_features, features)} + ret = {k: v for k, v in zip(self._out_features, features, strict=False)} return ret - + @property def size_divisibility(self): return self._size_divisibility @@ -176,7 +173,7 @@ def size_divisibility(self): @BACKBONE_REGISTRY.register() def build_timm_backbone(cfg, input_shape): model = TIMM( - cfg.MODEL.TIMM.BASE_NAME, + cfg.MODEL.TIMM.BASE_NAME, cfg.MODEL.TIMM.OUT_LEVELS, freeze_at=cfg.MODEL.TIMM.FREEZE_AT, norm=cfg.MODEL.TIMM.NORM, @@ -187,8 +184,7 @@ def build_timm_backbone(cfg, input_shape): @BACKBONE_REGISTRY.register() def build_p67_timm_fpn_backbone(cfg, input_shape): - """ - """ + """ """ bottom_up = build_timm_backbone(cfg, input_shape) in_features = cfg.MODEL.FPN.IN_FEATURES out_channels = cfg.MODEL.FPN.OUT_CHANNELS @@ -202,12 +198,12 @@ def build_p67_timm_fpn_backbone(cfg, input_shape): ) return backbone + @BACKBONE_REGISTRY.register() def build_p35_timm_fpn_backbone(cfg, input_shape): - """ - """ + """ """ bottom_up = build_timm_backbone(cfg, input_shape) - + in_features = cfg.MODEL.FPN.IN_FEATURES out_channels = cfg.MODEL.FPN.OUT_CHANNELS backbone = FPN( @@ -218,4 +214,4 @@ def build_p35_timm_fpn_backbone(cfg, input_shape): top_block=None, fuse_type=cfg.MODEL.FPN.FUSE_TYPE, ) - return backbone \ No newline at end of file + return backbone diff --git a/dimos/models/Detic/detic/modeling/debug.py b/dimos/models/Detic/detic/modeling/debug.py index 9c7c442eb8..f37849019e 100644 --- a/dimos/models/Detic/detic/modeling/debug.py +++ b/dimos/models/Detic/detic/modeling/debug.py @@ -1,30 +1,34 @@ # Copyright (c) Facebook, Inc. and its affiliates. +import os + import cv2 import numpy as np import torch import torch.nn.functional as F -import os +from typing import Optional, Sequence + +COLORS = ((np.random.rand(1300, 3) * 0.4 + 0.6) * 255).astype(np.uint8).reshape(1300, 1, 1, 3) -COLORS = ((np.random.rand(1300, 3) * 0.4 + 0.6) * 255).astype( - np.uint8).reshape(1300, 1, 1, 3) def _get_color_image(heatmap): - heatmap = heatmap.reshape( - heatmap.shape[0], heatmap.shape[1], heatmap.shape[2], 1) - if heatmap.shape[0] == 1: - color_map = (heatmap * np.ones((1, 1, 1, 3), np.uint8) * 255).max( - axis=0).astype(np.uint8) # H, W, 3 - else: - color_map = (heatmap * COLORS[:heatmap.shape[0]]).max(axis=0).astype(np.uint8) # H, W, 3 - - return color_map - -def _blend_image(image, color_map, a=0.7): - color_map = cv2.resize(color_map, (image.shape[1], image.shape[0])) - ret = np.clip(image * (1 - a) + color_map * a, 0, 255).astype(np.uint8) - return ret - -def _blend_image_heatmaps(image, color_maps, a=0.7): + heatmap = heatmap.reshape(heatmap.shape[0], heatmap.shape[1], heatmap.shape[2], 1) + if heatmap.shape[0] == 1: + color_map = ( + (heatmap * np.ones((1, 1, 1, 3), np.uint8) * 255).max(axis=0).astype(np.uint8) + ) # H, W, 3 + else: + color_map = (heatmap * COLORS[: heatmap.shape[0]]).max(axis=0).astype(np.uint8) # H, W, 3 + + return color_map + + +def _blend_image(image, color_map, a: float=0.7): + color_map = cv2.resize(color_map, (image.shape[1], image.shape[0])) + ret = np.clip(image * (1 - a) + color_map * a, 0, 255).astype(np.uint8) + return ret + + +def _blend_image_heatmaps(image, color_maps, a: float=0.7): merges = np.zeros((image.shape[0], image.shape[1], 3), np.float32) for color_map in color_maps: color_map = cv2.resize(color_map, (image.shape[1], image.shape[0])) @@ -32,10 +36,11 @@ def _blend_image_heatmaps(image, color_maps, a=0.7): ret = np.clip(image * (1 - a) + merges * a, 0, 255).astype(np.uint8) return ret + def _decompose_level(x, shapes_per_level, N): - ''' + """ x: LNHiWi x C - ''' + """ x = x.view(x.shape[0], -1) ret = [] st = 0 @@ -44,11 +49,11 @@ def _decompose_level(x, shapes_per_level, N): h = shapes_per_level[l][0].int().item() w = shapes_per_level[l][1].int().item() for i in range(N): - ret[l].append(x[st + h * w * i:st + h * w * (i + 1)].view( - h, w, -1).permute(2, 0, 1)) + ret[l].append(x[st + h * w * i : st + h * w * (i + 1)].view(h, w, -1).permute(2, 0, 1)) st += h * w * N return ret + def _imagelist_to_tensor(images): images = [x for x in images] image_sizes = [x.shape[-2:] for x in images] @@ -56,8 +61,7 @@ def _imagelist_to_tensor(images): w = max([size[1] for size in image_sizes]) S = 32 h, w = ((h - 1) // S + 1) * S, ((w - 1) // S + 1) * S - images = [F.pad(x, (0, w - x.shape[2], 0, h - x.shape[1], 0, 0)) \ - for x in images] + images = [F.pad(x, (0, w - x.shape[2], 0, h - x.shape[1], 0, 0)) for x in images] images = torch.stack(images) return images @@ -72,21 +76,28 @@ def _ind2il(ind, shapes_per_level, N): i = (r - S) // (shapes_per_level[l][0] * shapes_per_level[l][1]) return i, l + def debug_train( - images, gt_instances, flattened_hms, reg_targets, labels, pos_inds, - shapes_per_level, locations, strides): - ''' + images, + gt_instances, + flattened_hms, + reg_targets, + labels: Sequence[str], + pos_inds, + shapes_per_level, + locations, + strides: Sequence[int], +) -> None: + """ images: N x 3 x H x W flattened_hms: LNHiWi x C shapes_per_level: L x 2 [(H_i, W_i)] locations: LNHiWi x 2 - ''' - reg_inds = torch.nonzero( - reg_targets.max(dim=1)[0] > 0).squeeze(1) + """ + reg_inds = torch.nonzero(reg_targets.max(dim=1)[0] > 0).squeeze(1) N = len(images) images = _imagelist_to_tensor(images) - repeated_locations = [torch.cat([loc] * N, dim=0) \ - for loc in locations] + repeated_locations = [torch.cat([loc] * N, dim=0) for loc in locations] locations = torch.cat(repeated_locations, dim=0) gt_hms = _decompose_level(flattened_hms, shapes_per_level, N) masks = flattened_hms.new_zeros((flattened_hms.shape[0], 1)) @@ -96,30 +107,32 @@ def debug_train( image = images[i].detach().cpu().numpy().transpose(1, 2, 0) color_maps = [] for l in range(len(gt_hms)): - color_map = _get_color_image( - gt_hms[l][i].detach().cpu().numpy()) + color_map = _get_color_image(gt_hms[l][i].detach().cpu().numpy()) color_maps.append(color_map) - cv2.imshow('gthm_{}'.format(l), color_map) + cv2.imshow(f"gthm_{l}", color_map) blend = _blend_image_heatmaps(image.copy(), color_maps) if gt_instances is not None: bboxes = gt_instances[i].gt_boxes.tensor for j in range(len(bboxes)): bbox = bboxes[j] cv2.rectangle( - blend, + blend, (int(bbox[0]), int(bbox[1])), (int(bbox[2]), int(bbox[3])), - (0, 0, 255), 3, cv2.LINE_AA) - + (0, 0, 255), + 3, + cv2.LINE_AA, + ) + for j in range(len(pos_inds)): image_id, l = _ind2il(pos_inds[j], shapes_per_level, N) if image_id != i: continue loc = locations[pos_inds[j]] cv2.drawMarker( - blend, (int(loc[0]), int(loc[1])), (0, 255, 255), - markerSize=(l + 1) * 16) - + blend, (int(loc[0]), int(loc[1])), (0, 255, 255), markerSize=(l + 1) * 16 + ) + for j in range(len(reg_inds)): image_id, l = _ind2il(reg_inds[j], shapes_per_level, N) if image_id != i: @@ -127,113 +140,152 @@ def debug_train( ltrb = reg_targets[reg_inds[j]] ltrb *= strides[l] loc = locations[reg_inds[j]] - bbox = [(loc[0] - ltrb[0]), (loc[1] - ltrb[1]), - (loc[0] + ltrb[2]), (loc[1] + ltrb[3])] + bbox = [(loc[0] - ltrb[0]), (loc[1] - ltrb[1]), (loc[0] + ltrb[2]), (loc[1] + ltrb[3])] cv2.rectangle( - blend, + blend, (int(bbox[0]), int(bbox[1])), (int(bbox[2]), int(bbox[3])), - (255, 0, 0), 1, cv2.LINE_AA) + (255, 0, 0), + 1, + cv2.LINE_AA, + ) cv2.circle(blend, (int(loc[0]), int(loc[1])), 2, (255, 0, 0), -1) - cv2.imshow('blend', blend) + cv2.imshow("blend", blend) cv2.waitKey() def debug_test( - images, logits_pred, reg_pred, agn_hm_pred=[], preds=[], - vis_thresh=0.3, debug_show_name=False, mult_agn=False): - ''' + images, + logits_pred, + reg_pred, + agn_hm_pred=None, + preds=None, + vis_thresh: float=0.3, + debug_show_name: bool=False, + mult_agn: bool=False, +) -> None: + """ images: N x 3 x H x W class_target: LNHiWi x C cat_agn_heatmap: LNHiWi shapes_per_level: L x 2 [(H_i, W_i)] - ''' - N = len(images) + """ + if preds is None: + preds = [] + if agn_hm_pred is None: + agn_hm_pred = [] + len(images) for i in range(len(images)): image = images[i].detach().cpu().numpy().transpose(1, 2, 0) - result = image.copy().astype(np.uint8) + image.copy().astype(np.uint8) pred_image = image.copy().astype(np.uint8) color_maps = [] L = len(logits_pred) for l in range(L): if logits_pred[0] is not None: stride = min(image.shape[0], image.shape[1]) / min( - logits_pred[l][i].shape[1], logits_pred[l][i].shape[2]) + logits_pred[l][i].shape[1], logits_pred[l][i].shape[2] + ) else: stride = min(image.shape[0], image.shape[1]) / min( - agn_hm_pred[l][i].shape[1], agn_hm_pred[l][i].shape[2]) + agn_hm_pred[l][i].shape[1], agn_hm_pred[l][i].shape[2] + ) stride = stride if stride < 60 else 64 if stride < 100 else 128 if logits_pred[0] is not None: if mult_agn: logits_pred[l][i] = logits_pred[l][i] * agn_hm_pred[l][i] - color_map = _get_color_image( - logits_pred[l][i].detach().cpu().numpy()) + color_map = _get_color_image(logits_pred[l][i].detach().cpu().numpy()) color_maps.append(color_map) - cv2.imshow('predhm_{}'.format(l), color_map) + cv2.imshow(f"predhm_{l}", color_map) if debug_show_name: - from detectron2.data.datasets.lvis_v1_categories import LVIS_CATEGORIES - cat2name = [x['name'] for x in LVIS_CATEGORIES] + from detectron2.data.datasets.lvis_v1_categories import LVIS_CATEGORIES + + cat2name = [x["name"] for x in LVIS_CATEGORIES] for j in range(len(preds[i].scores) if preds is not None else 0): if preds[i].scores[j] > vis_thresh: - bbox = preds[i].proposal_boxes[j] \ - if preds[i].has('proposal_boxes') else \ - preds[i].pred_boxes[j] + bbox = ( + preds[i].proposal_boxes[j] + if preds[i].has("proposal_boxes") + else preds[i].pred_boxes[j] + ) bbox = bbox.tensor[0].detach().cpu().numpy().astype(np.int32) - cat = int(preds[i].pred_classes[j]) \ - if preds[i].has('pred_classes') else 0 + cat = int(preds[i].pred_classes[j]) if preds[i].has("pred_classes") else 0 cl = COLORS[cat, 0, 0] cv2.rectangle( - pred_image, (int(bbox[0]), int(bbox[1])), - (int(bbox[2]), int(bbox[3])), - (int(cl[0]), int(cl[1]), int(cl[2])), 2, cv2.LINE_AA) + pred_image, + (int(bbox[0]), int(bbox[1])), + (int(bbox[2]), int(bbox[3])), + (int(cl[0]), int(cl[1]), int(cl[2])), + 2, + cv2.LINE_AA, + ) if debug_show_name: - txt = '{}{:.1f}'.format( - cat2name[cat] if cat > 0 else '', - preds[i].scores[j]) + txt = "{}{:.1f}".format( + cat2name[cat] if cat > 0 else "", preds[i].scores[j] + ) font = cv2.FONT_HERSHEY_SIMPLEX cat_size = cv2.getTextSize(txt, font, 0.5, 2)[0] cv2.rectangle( pred_image, (int(bbox[0]), int(bbox[1] - cat_size[1] - 2)), - (int(bbox[0] + cat_size[0]), int(bbox[1] - 2)), - (int(cl[0]), int(cl[1]), int(cl[2])), -1) + (int(bbox[0] + cat_size[0]), int(bbox[1] - 2)), + (int(cl[0]), int(cl[1]), int(cl[2])), + -1, + ) cv2.putText( - pred_image, txt, (int(bbox[0]), int(bbox[1] - 2)), - font, 0.5, (0, 0, 0), thickness=1, lineType=cv2.LINE_AA) - + pred_image, + txt, + (int(bbox[0]), int(bbox[1] - 2)), + font, + 0.5, + (0, 0, 0), + thickness=1, + lineType=cv2.LINE_AA, + ) if agn_hm_pred[l] is not None: agn_hm_ = agn_hm_pred[l][i, 0, :, :, None].detach().cpu().numpy() - agn_hm_ = (agn_hm_ * np.array([255, 255, 255]).reshape( - 1, 1, 3)).astype(np.uint8) - cv2.imshow('agn_hm_{}'.format(l), agn_hm_) + agn_hm_ = (agn_hm_ * np.array([255, 255, 255]).reshape(1, 1, 3)).astype(np.uint8) + cv2.imshow(f"agn_hm_{l}", agn_hm_) blend = _blend_image_heatmaps(image.copy(), color_maps) - cv2.imshow('blend', blend) - cv2.imshow('preds', pred_image) + cv2.imshow("blend", blend) + cv2.imshow("preds", pred_image) cv2.waitKey() + global cnt cnt = 0 -def debug_second_stage(images, instances, proposals=None, vis_thresh=0.3, - save_debug=False, debug_show_name=False, image_labels=[], - save_debug_path='output/save_debug/', - bgr=False): + +def debug_second_stage( + images, + instances, + proposals=None, + vis_thresh: float=0.3, + save_debug: bool=False, + debug_show_name: bool=False, + image_labels: Optional[Sequence[str]]=None, + save_debug_path: str="output/save_debug/", + bgr: bool=False, +) -> None: + if image_labels is None: + image_labels = [] images = _imagelist_to_tensor(images) - if 'COCO' in save_debug_path: + if "COCO" in save_debug_path: from detectron2.data.datasets.builtin_meta import COCO_CATEGORIES - cat2name = [x['name'] for x in COCO_CATEGORIES] + + cat2name = [x["name"] for x in COCO_CATEGORIES] else: from detectron2.data.datasets.lvis_v1_categories import LVIS_CATEGORIES - cat2name = ['({}){}'.format(x['frequency'], x['name']) \ - for x in LVIS_CATEGORIES] + + cat2name = ["({}){}".format(x["frequency"], x["name"]) for x in LVIS_CATEGORIES] for i in range(len(images)): image = images[i].detach().cpu().numpy().transpose(1, 2, 0).astype(np.uint8).copy() if bgr: image = image[:, :, ::-1].copy() - if instances[i].has('gt_boxes'): + if instances[i].has("gt_boxes"): bboxes = instances[i].gt_boxes.tensor.cpu().numpy() scores = np.ones(bboxes.shape[0]) cats = instances[i].gt_classes.cpu().numpy() @@ -247,40 +299,52 @@ def debug_second_stage(images, instances, proposals=None, vis_thresh=0.3, cl = COLORS[cats[j], 0, 0] cl = (int(cl[0]), int(cl[1]), int(cl[2])) cv2.rectangle( - image, + image, (int(bbox[0]), int(bbox[1])), (int(bbox[2]), int(bbox[3])), - cl, 2, cv2.LINE_AA) + cl, + 2, + cv2.LINE_AA, + ) if debug_show_name: cat = cats[j] - txt = '{}{:.1f}'.format( - cat2name[cat] if cat > 0 else '', - scores[j]) + txt = "{}{:.1f}".format(cat2name[cat] if cat > 0 else "", scores[j]) font = cv2.FONT_HERSHEY_SIMPLEX cat_size = cv2.getTextSize(txt, font, 0.5, 2)[0] cv2.rectangle( image, (int(bbox[0]), int(bbox[1] - cat_size[1] - 2)), - (int(bbox[0] + cat_size[0]), int(bbox[1] - 2)), - (int(cl[0]), int(cl[1]), int(cl[2])), -1) + (int(bbox[0] + cat_size[0]), int(bbox[1] - 2)), + (int(cl[0]), int(cl[1]), int(cl[2])), + -1, + ) cv2.putText( - image, txt, (int(bbox[0]), int(bbox[1] - 2)), - font, 0.5, (0, 0, 0), thickness=1, lineType=cv2.LINE_AA) + image, + txt, + (int(bbox[0]), int(bbox[1] - 2)), + font, + 0.5, + (0, 0, 0), + thickness=1, + lineType=cv2.LINE_AA, + ) if proposals is not None: - proposal_image = images[i].detach().cpu().numpy().transpose(1, 2, 0).astype(np.uint8).copy() + proposal_image = ( + images[i].detach().cpu().numpy().transpose(1, 2, 0).astype(np.uint8).copy() + ) if bgr: proposal_image = proposal_image.copy() else: proposal_image = proposal_image[:, :, ::-1].copy() bboxes = proposals[i].proposal_boxes.tensor.cpu().numpy() - if proposals[i].has('scores'): + if proposals[i].has("scores"): scores = proposals[i].scores.detach().cpu().numpy() else: scores = proposals[i].objectness_logits.detach().cpu().numpy() # selected = -1 # if proposals[i].has('image_loss'): # selected = proposals[i].image_loss.argmin() - if proposals[i].has('selected'): + if proposals[i].has("selected"): selected = proposals[i].selected else: selected = [-1 for _ in range(len(bboxes))] @@ -290,45 +354,55 @@ def debug_second_stage(images, instances, proposals=None, vis_thresh=0.3, cl = (209, 159, 83) th = 2 if selected[j] >= 0: - cl = (0, 0, 0xa4) + cl = (0, 0, 0xA4) th = 4 cv2.rectangle( - proposal_image, + proposal_image, (int(bbox[0]), int(bbox[1])), (int(bbox[2]), int(bbox[3])), - cl, th, cv2.LINE_AA) + cl, + th, + cv2.LINE_AA, + ) if selected[j] >= 0 and debug_show_name: cat = selected[j].item() - txt = '{}'.format(cat2name[cat]) + txt = f"{cat2name[cat]}" font = cv2.FONT_HERSHEY_SIMPLEX cat_size = cv2.getTextSize(txt, font, 0.5, 2)[0] cv2.rectangle( proposal_image, (int(bbox[0]), int(bbox[1] - cat_size[1] - 2)), - (int(bbox[0] + cat_size[0]), int(bbox[1] - 2)), - (int(cl[0]), int(cl[1]), int(cl[2])), -1) + (int(bbox[0] + cat_size[0]), int(bbox[1] - 2)), + (int(cl[0]), int(cl[1]), int(cl[2])), + -1, + ) cv2.putText( - proposal_image, txt, - (int(bbox[0]), int(bbox[1] - 2)), - font, 0.5, (0, 0, 0), thickness=1, - lineType=cv2.LINE_AA) + proposal_image, + txt, + (int(bbox[0]), int(bbox[1] - 2)), + font, + 0.5, + (0, 0, 0), + thickness=1, + lineType=cv2.LINE_AA, + ) if save_debug: global cnt cnt = (cnt + 1) % 5000 if not os.path.exists(save_debug_path): os.mkdir(save_debug_path) - save_name = '{}/{:05d}.jpg'.format(save_debug_path, cnt) + save_name = f"{save_debug_path}/{cnt:05d}.jpg" if i < len(image_labels): image_label = image_labels[i] - save_name = '{}/{:05d}'.format(save_debug_path, cnt) + save_name = f"{save_debug_path}/{cnt:05d}" for x in image_label: class_name = cat2name[x] - save_name = save_name + '|{}'.format(class_name) - save_name = save_name + '.jpg' + save_name = save_name + f"|{class_name}" + save_name = save_name + ".jpg" cv2.imwrite(save_name, proposal_image) else: - cv2.imshow('image', image) + cv2.imshow("image", image) if proposals is not None: - cv2.imshow('proposals', proposal_image) - cv2.waitKey() \ No newline at end of file + cv2.imshow("proposals", proposal_image) + cv2.waitKey() diff --git a/dimos/models/Detic/detic/modeling/meta_arch/custom_rcnn.py b/dimos/models/Detic/detic/modeling/meta_arch/custom_rcnn.py index 9a5ac721d4..872084f7cb 100644 --- a/dimos/models/Detic/detic/modeling/meta_arch/custom_rcnn.py +++ b/dimos/models/Detic/detic/modeling/meta_arch/custom_rcnn.py @@ -1,45 +1,41 @@ # Copyright (c) Facebook, Inc. and its affiliates. -import copy -import logging -import numpy as np from typing import Dict, List, Optional, Tuple -import torch -from torch import nn -import json -from detectron2.utils.events import get_event_storage -from detectron2.config import configurable -from detectron2.structures import ImageList, Instances, Boxes -import detectron2.utils.comm as comm +from detectron2.config import configurable from detectron2.modeling.meta_arch.build import META_ARCH_REGISTRY from detectron2.modeling.meta_arch.rcnn import GeneralizedRCNN -from detectron2.modeling.postprocessing import detector_postprocess -from detectron2.utils.visualizer import Visualizer, _create_text_labels -from detectron2.data.detection_utils import convert_image_to_rgb - +from detectron2.structures import Instances +import detectron2.utils.comm as comm +from detectron2.utils.events import get_event_storage +import torch from torch.cuda.amp import autocast + from ..text.text_encoder import build_text_encoder -from ..utils import load_class_freq, get_fed_loss_inds +from ..utils import get_fed_loss_inds, load_class_freq + @META_ARCH_REGISTRY.register() class CustomRCNN(GeneralizedRCNN): - ''' + """ Add image labels - ''' + """ + @configurable def __init__( - self, - with_image_labels = False, - dataset_loss_weight = [], - fp16 = False, - sync_caption_batch = False, - roi_head_name = '', - cap_batch_ratio = 4, - with_caption = False, - dynamic_classifier = False, - **kwargs): - """ - """ + self, + with_image_labels: bool=False, + dataset_loss_weight=None, + fp16: bool=False, + sync_caption_batch: bool=False, + roi_head_name: str="", + cap_batch_ratio: int=4, + with_caption: bool=False, + dynamic_classifier: bool=False, + **kwargs, + ) -> None: + """ """ + if dataset_loss_weight is None: + dataset_loss_weight = [] self.with_image_labels = with_image_labels self.dataset_loss_weight = dataset_loss_weight self.fp16 = fp16 @@ -50,9 +46,9 @@ def __init__( self.dynamic_classifier = dynamic_classifier self.return_proposal = False if self.dynamic_classifier: - self.freq_weight = kwargs.pop('freq_weight') - self.num_classes = kwargs.pop('num_classes') - self.num_sample_cats = kwargs.pop('num_sample_cats') + self.freq_weight = kwargs.pop("freq_weight") + self.num_classes = kwargs.pop("num_classes") + self.num_sample_cats = kwargs.pop("num_sample_cats") super().__init__(**kwargs) assert self.proposal_generator is not None if self.with_caption: @@ -61,33 +57,33 @@ def __init__( for v in self.text_encoder.parameters(): v.requires_grad = False - @classmethod def from_config(cls, cfg): ret = super().from_config(cfg) - ret.update({ - 'with_image_labels': cfg.WITH_IMAGE_LABELS, - 'dataset_loss_weight': cfg.MODEL.DATASET_LOSS_WEIGHT, - 'fp16': cfg.FP16, - 'with_caption': cfg.MODEL.WITH_CAPTION, - 'sync_caption_batch': cfg.MODEL.SYNC_CAPTION_BATCH, - 'dynamic_classifier': cfg.MODEL.DYNAMIC_CLASSIFIER, - 'roi_head_name': cfg.MODEL.ROI_HEADS.NAME, - 'cap_batch_ratio': cfg.MODEL.CAP_BATCH_RATIO, - }) - if ret['dynamic_classifier']: - ret['freq_weight'] = load_class_freq( - cfg.MODEL.ROI_BOX_HEAD.CAT_FREQ_PATH, - cfg.MODEL.ROI_BOX_HEAD.FED_LOSS_FREQ_WEIGHT) - ret['num_classes'] = cfg.MODEL.ROI_HEADS.NUM_CLASSES - ret['num_sample_cats'] = cfg.MODEL.NUM_SAMPLE_CATS + ret.update( + { + "with_image_labels": cfg.WITH_IMAGE_LABELS, + "dataset_loss_weight": cfg.MODEL.DATASET_LOSS_WEIGHT, + "fp16": cfg.FP16, + "with_caption": cfg.MODEL.WITH_CAPTION, + "sync_caption_batch": cfg.MODEL.SYNC_CAPTION_BATCH, + "dynamic_classifier": cfg.MODEL.DYNAMIC_CLASSIFIER, + "roi_head_name": cfg.MODEL.ROI_HEADS.NAME, + "cap_batch_ratio": cfg.MODEL.CAP_BATCH_RATIO, + } + ) + if ret["dynamic_classifier"]: + ret["freq_weight"] = load_class_freq( + cfg.MODEL.ROI_BOX_HEAD.CAT_FREQ_PATH, cfg.MODEL.ROI_BOX_HEAD.FED_LOSS_FREQ_WEIGHT + ) + ret["num_classes"] = cfg.MODEL.ROI_HEADS.NUM_CLASSES + ret["num_sample_cats"] = cfg.MODEL.NUM_SAMPLE_CATS return ret - def inference( self, - batched_inputs: Tuple[Dict[str, torch.Tensor]], - detected_instances: Optional[List[Instances]] = None, + batched_inputs: tuple[dict[str, torch.Tensor]], + detected_instances: list[Instances] | None = None, do_postprocess: bool = True, ): assert not self.training @@ -98,15 +94,12 @@ def inference( proposals, _ = self.proposal_generator(images, features, None) results, _ = self.roi_heads(images, features, proposals) if do_postprocess: - assert not torch.jit.is_scripting(), \ - "Scripting is not supported for postprocess." - return CustomRCNN._postprocess( - results, batched_inputs, images.image_sizes) + assert not torch.jit.is_scripting(), "Scripting is not supported for postprocess." + return CustomRCNN._postprocess(results, batched_inputs, images.image_sizes) else: return results - - def forward(self, batched_inputs: List[Dict[str, torch.Tensor]]): + def forward(self, batched_inputs: list[dict[str, torch.Tensor]]): """ Add ann_type Ignore proposal loss when training with image labels @@ -116,20 +109,20 @@ def forward(self, batched_inputs: List[Dict[str, torch.Tensor]]): images = self.preprocess_image(batched_inputs) - ann_type = 'box' + ann_type = "box" gt_instances = [x["instances"].to(self.device) for x in batched_inputs] if self.with_image_labels: - for inst, x in zip(gt_instances, batched_inputs): - inst._ann_type = x['ann_type'] - inst._pos_category_ids = x['pos_category_ids'] - ann_types = [x['ann_type'] for x in batched_inputs] + for inst, x in zip(gt_instances, batched_inputs, strict=False): + inst._ann_type = x["ann_type"] + inst._pos_category_ids = x["pos_category_ids"] + ann_types = [x["ann_type"] for x in batched_inputs] assert len(set(ann_types)) == 1 ann_type = ann_types[0] - if ann_type in ['prop', 'proptag']: + if ann_type in ["prop", "proptag"]: for t in gt_instances: t.gt_classes *= 0 - - if self.fp16: # TODO (zhouxy): improve + + if self.fp16: # TODO (zhouxy): improve with autocast(): features = self.backbone(images.tensor.half()) features = {k: v.float() for k, v in features.items()} @@ -138,33 +131,40 @@ def forward(self, batched_inputs: List[Dict[str, torch.Tensor]]): cls_features, cls_inds, caption_features = None, None, None - if self.with_caption and 'caption' in ann_type: - inds = [torch.randint(len(x['captions']), (1,))[0].item() \ - for x in batched_inputs] - caps = [x['captions'][ind] for ind, x in zip(inds, batched_inputs)] + if self.with_caption and "caption" in ann_type: + inds = [torch.randint(len(x["captions"]), (1,))[0].item() for x in batched_inputs] + caps = [x["captions"][ind] for ind, x in zip(inds, batched_inputs, strict=False)] caption_features = self.text_encoder(caps).float() if self.sync_caption_batch: caption_features = self._sync_caption_features( - caption_features, ann_type, len(batched_inputs)) - - if self.dynamic_classifier and ann_type != 'caption': - cls_inds = self._sample_cls_inds(gt_instances, ann_type) # inds, inv_inds - ind_with_bg = cls_inds[0].tolist() + [-1] - cls_features = self.roi_heads.box_predictor[ - 0].cls_score.zs_weight[:, ind_with_bg].permute(1, 0).contiguous() + caption_features, ann_type, len(batched_inputs) + ) + + if self.dynamic_classifier and ann_type != "caption": + cls_inds = self._sample_cls_inds(gt_instances, ann_type) # inds, inv_inds + ind_with_bg = [*cls_inds[0].tolist(), -1] + cls_features = ( + self.roi_heads.box_predictor[0] + .cls_score.zs_weight[:, ind_with_bg] + .permute(1, 0) + .contiguous() + ) classifier_info = cls_features, cls_inds, caption_features - proposals, proposal_losses = self.proposal_generator( - images, features, gt_instances) + proposals, proposal_losses = self.proposal_generator(images, features, gt_instances) - if self.roi_head_name in ['StandardROIHeads', 'CascadeROIHeads']: - proposals, detector_losses = self.roi_heads( - images, features, proposals, gt_instances) + if self.roi_head_name in ["StandardROIHeads", "CascadeROIHeads"]: + proposals, detector_losses = self.roi_heads(images, features, proposals, gt_instances) else: proposals, detector_losses = self.roi_heads( - images, features, proposals, gt_instances, - ann_type=ann_type, classifier_info=classifier_info) - + images, + features, + proposals, + gt_instances, + ann_type=ann_type, + classifier_info=classifier_info, + ) + if self.vis_period > 0: storage = get_event_storage() if storage.iter % self.vis_period == 0: @@ -173,60 +173,55 @@ def forward(self, batched_inputs: List[Dict[str, torch.Tensor]]): losses = {} losses.update(detector_losses) if self.with_image_labels: - if ann_type in ['box', 'prop', 'proptag']: + if ann_type in ["box", "prop", "proptag"]: losses.update(proposal_losses) - else: # ignore proposal loss for non-bbox data + else: # ignore proposal loss for non-bbox data losses.update({k: v * 0 for k, v in proposal_losses.items()}) else: losses.update(proposal_losses) if len(self.dataset_loss_weight) > 0: - dataset_sources = [x['dataset_source'] for x in batched_inputs] + dataset_sources = [x["dataset_source"] for x in batched_inputs] assert len(set(dataset_sources)) == 1 dataset_source = dataset_sources[0] for k in losses: losses[k] *= self.dataset_loss_weight[dataset_source] - + if self.return_proposal: return proposals, losses else: return losses - def _sync_caption_features(self, caption_features, ann_type, BS): - has_caption_feature = (caption_features is not None) - BS = (BS * self.cap_batch_ratio) if (ann_type == 'box') else BS - rank = torch.full( - (BS, 1), comm.get_rank(), dtype=torch.float32, - device=self.device) + has_caption_feature = caption_features is not None + BS = (BS * self.cap_batch_ratio) if (ann_type == "box") else BS + rank = torch.full((BS, 1), comm.get_rank(), dtype=torch.float32, device=self.device) if not has_caption_feature: caption_features = rank.new_zeros((BS, 512)) caption_features = torch.cat([caption_features, rank], dim=1) global_caption_features = comm.all_gather(caption_features) - caption_features = torch.cat( - [x.to(self.device) for x in global_caption_features], dim=0) \ - if has_caption_feature else None # (NB) x (D + 1) + caption_features = ( + torch.cat([x.to(self.device) for x in global_caption_features], dim=0) + if has_caption_feature + else None + ) # (NB) x (D + 1) return caption_features - - def _sample_cls_inds(self, gt_instances, ann_type='box'): - if ann_type == 'box': - gt_classes = torch.cat( - [x.gt_classes for x in gt_instances]) + def _sample_cls_inds(self, gt_instances, ann_type: str="box"): + if ann_type == "box": + gt_classes = torch.cat([x.gt_classes for x in gt_instances]) C = len(self.freq_weight) freq_weight = self.freq_weight else: gt_classes = torch.cat( - [torch.tensor( - x._pos_category_ids, - dtype=torch.long, device=x.gt_classes.device) \ - for x in gt_instances]) + [ + torch.tensor(x._pos_category_ids, dtype=torch.long, device=x.gt_classes.device) + for x in gt_instances + ] + ) C = self.num_classes freq_weight = None - assert gt_classes.max() < C, '{} {}'.format(gt_classes.max(), C) - inds = get_fed_loss_inds( - gt_classes, self.num_sample_cats, C, - weight=freq_weight) - cls_id_map = gt_classes.new_full( - (self.num_classes + 1,), len(inds)) + assert gt_classes.max() < C, f"{gt_classes.max()} {C}" + inds = get_fed_loss_inds(gt_classes, self.num_sample_cats, C, weight=freq_weight) + cls_id_map = gt_classes.new_full((self.num_classes + 1,), len(inds)) cls_id_map[inds] = torch.arange(len(inds), device=cls_id_map.device) - return inds, cls_id_map \ No newline at end of file + return inds, cls_id_map diff --git a/dimos/models/Detic/detic/modeling/meta_arch/d2_deformable_detr.py b/dimos/models/Detic/detic/modeling/meta_arch/d2_deformable_detr.py index 47ff220fc3..9c2ec8e81e 100644 --- a/dimos/models/Detic/detic/modeling/meta_arch/d2_deformable_detr.py +++ b/dimos/models/Detic/detic/modeling/meta_arch/d2_deformable_detr.py @@ -1,84 +1,92 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -import torch -import torch.nn.functional as F -from torch import nn -import math - +# Copyright (c) Facebook, Inc. and its affiliates. from detectron2.modeling import META_ARCH_REGISTRY, build_backbone from detectron2.structures import Boxes, Instances -from ..utils import load_class_freq, get_fed_loss_inds - from models.backbone import Joiner -from models.deformable_detr import DeformableDETR, SetCriterion, MLP -from models.deformable_detr import _get_clones +from models.deformable_detr import DeformableDETR, SetCriterion +from models.deformable_transformer import DeformableTransformer from models.matcher import HungarianMatcher from models.position_encoding import PositionEmbeddingSine -from models.deformable_transformer import DeformableTransformer from models.segmentation import sigmoid_focal_loss +import torch +from torch import nn +import torch.nn.functional as F from util.box_ops import box_cxcywh_to_xyxy, box_xyxy_to_cxcywh from util.misc import NestedTensor, accuracy +from ..utils import get_fed_loss_inds, load_class_freq +from typing import Sequence __all__ = ["DeformableDetr"] + class CustomSetCriterion(SetCriterion): - def __init__(self, num_classes, matcher, weight_dict, losses, \ - focal_alpha=0.25, use_fed_loss=False): + def __init__( + self, num_classes: int, matcher, weight_dict, losses, focal_alpha: float=0.25, use_fed_loss: bool=False + ) -> None: super().__init__(num_classes, matcher, weight_dict, losses, focal_alpha) self.use_fed_loss = use_fed_loss if self.use_fed_loss: - self.register_buffer( - 'fed_loss_weight', load_class_freq(freq_weight=0.5)) + self.register_buffer("fed_loss_weight", load_class_freq(freq_weight=0.5)) - def loss_labels(self, outputs, targets, indices, num_boxes, log=True): + def loss_labels(self, outputs, targets, indices, num_boxes: int, log: bool=True): """Classification loss (NLL) targets dicts must contain the key "labels" containing a tensor of dim [nb_target_boxes] """ - assert 'pred_logits' in outputs - src_logits = outputs['pred_logits'] + assert "pred_logits" in outputs + src_logits = outputs["pred_logits"] idx = self._get_src_permutation_idx(indices) - target_classes_o = torch.cat([t["labels"][J] for t, (_, J) in zip(targets, indices)]) - target_classes = torch.full(src_logits.shape[:2], self.num_classes, - dtype=torch.int64, device=src_logits.device) + target_classes_o = torch.cat([t["labels"][J] for t, (_, J) in zip(targets, indices, strict=False)]) + target_classes = torch.full( + src_logits.shape[:2], self.num_classes, dtype=torch.int64, device=src_logits.device + ) target_classes[idx] = target_classes_o target_classes_onehot = torch.zeros( [src_logits.shape[0], src_logits.shape[1], src_logits.shape[2] + 1], - dtype=src_logits.dtype, layout=src_logits.layout, - device=src_logits.device) + dtype=src_logits.dtype, + layout=src_logits.layout, + device=src_logits.device, + ) target_classes_onehot.scatter_(2, target_classes.unsqueeze(-1), 1) - target_classes_onehot = target_classes_onehot[:,:,:-1] # B x N x C + target_classes_onehot = target_classes_onehot[:, :, :-1] # B x N x C if self.use_fed_loss: inds = get_fed_loss_inds( gt_classes=target_classes_o, num_sample_cats=50, weight=self.fed_loss_weight, - C=target_classes_onehot.shape[2]) - loss_ce = sigmoid_focal_loss( - src_logits[:, :, inds], - target_classes_onehot[:, :, inds], - num_boxes, - alpha=self.focal_alpha, - gamma=2) * src_logits.shape[1] + C=target_classes_onehot.shape[2], + ) + loss_ce = ( + sigmoid_focal_loss( + src_logits[:, :, inds], + target_classes_onehot[:, :, inds], + num_boxes, + alpha=self.focal_alpha, + gamma=2, + ) + * src_logits.shape[1] + ) else: - loss_ce = sigmoid_focal_loss( - src_logits, target_classes_onehot, num_boxes, - alpha=self.focal_alpha, - gamma=2) * src_logits.shape[1] - losses = {'loss_ce': loss_ce} + loss_ce = ( + sigmoid_focal_loss( + src_logits, target_classes_onehot, num_boxes, alpha=self.focal_alpha, gamma=2 + ) + * src_logits.shape[1] + ) + losses = {"loss_ce": loss_ce} if log: # TODO this should probably be a separate loss, not hacked in this one here - losses['class_error'] = 100 - accuracy(src_logits[idx], target_classes_o)[0] + losses["class_error"] = 100 - accuracy(src_logits[idx], target_classes_o)[0] return losses class MaskedBackbone(nn.Module): - """ This is a thin wrapper around D2's backbone to provide padding masking""" + """This is a thin wrapper around D2's backbone to provide padding masking""" - def __init__(self, cfg): + def __init__(self, cfg) -> None: super().__init__() self.backbone = build_backbone(cfg) backbone_shape = self.backbone.output_shape() @@ -96,13 +104,14 @@ def forward(self, tensor_list: NestedTensor): out[name] = NestedTensor(x, mask) return out + @META_ARCH_REGISTRY.register() class DeformableDetr(nn.Module): """ Implement Deformable Detr """ - def __init__(self, cfg): + def __init__(self, cfg) -> None: super().__init__() self.with_image_labels = cfg.WITH_IMAGE_LABELS self.weak_weight = cfg.MODEL.DETR.WEAK_WEIGHT @@ -148,10 +157,13 @@ def __init__(self, cfg): dec_n_points=4, enc_n_points=4, two_stage=two_stage, - two_stage_num_proposals=num_queries) + two_stage_num_proposals=num_queries, + ) self.detr = DeformableDETR( - backbone, transformer, num_classes=self.num_classes, + backbone, + transformer, + num_classes=self.num_classes, num_queries=num_queries, num_feature_levels=num_feature_levels, aux_loss=deep_supervision, @@ -160,10 +172,11 @@ def __init__(self, cfg): ) if self.mask_on: - assert 0, 'Mask is not supported yet :(' + assert 0, "Mask is not supported yet :(" matcher = HungarianMatcher( - cost_class=cls_weight, cost_bbox=l1_weight, cost_giou=giou_weight) + cost_class=cls_weight, cost_bbox=l1_weight, cost_giou=giou_weight + ) weight_dict = {"loss_ce": cls_weight, "loss_bbox": l1_weight} weight_dict["loss_giou"] = giou_weight if deep_supervision: @@ -171,21 +184,22 @@ def __init__(self, cfg): for i in range(dec_layers - 1): aux_weight_dict.update({k + f"_{i}": v for k, v in weight_dict.items()}) weight_dict.update(aux_weight_dict) - print('weight_dict', weight_dict) + print("weight_dict", weight_dict) losses = ["labels", "boxes", "cardinality"] if self.mask_on: losses += ["masks"] self.criterion = CustomSetCriterion( - self.num_classes, matcher=matcher, weight_dict=weight_dict, - focal_alpha=focal_alpha, + self.num_classes, + matcher=matcher, + weight_dict=weight_dict, + focal_alpha=focal_alpha, losses=losses, - use_fed_loss=cfg.MODEL.DETR.USE_FED_LOSS + use_fed_loss=cfg.MODEL.DETR.USE_FED_LOSS, ) pixel_mean = torch.Tensor(cfg.MODEL.PIXEL_MEAN).to(self.device).view(3, 1, 1) pixel_std = torch.Tensor(cfg.MODEL.PIXEL_STD).to(self.device).view(3, 1, 1) self.normalizer = lambda x: (x - pixel_mean) / pixel_std - def forward(self, batched_inputs): """ Args: @@ -204,21 +218,21 @@ def forward(self, batched_inputs): if k in weight_dict: loss_dict[k] *= weight_dict[k] if self.with_image_labels: - if batched_inputs[0]['ann_type'] in ['image', 'captiontag']: - loss_dict['loss_image'] = self.weak_weight * self._weak_loss( - output, batched_inputs) + if batched_inputs[0]["ann_type"] in ["image", "captiontag"]: + loss_dict["loss_image"] = self.weak_weight * self._weak_loss( + output, batched_inputs + ) else: - loss_dict['loss_image'] = images[0].new_zeros( - [1], dtype=torch.float32)[0] + loss_dict["loss_image"] = images[0].new_zeros([1], dtype=torch.float32)[0] # import pdb; pdb.set_trace() return loss_dict else: image_sizes = output["pred_boxes"].new_tensor( - [(t["height"], t["width"]) for t in batched_inputs]) + [(t["height"], t["width"]) for t in batched_inputs] + ) results = self.post_process(output, image_sizes) return results - def prepare_targets(self, targets): new_targets = [] for targets_per_image in targets: @@ -228,29 +242,28 @@ def prepare_targets(self, targets): gt_boxes = targets_per_image.gt_boxes.tensor / image_size_xyxy gt_boxes = box_xyxy_to_cxcywh(gt_boxes) new_targets.append({"labels": gt_classes, "boxes": gt_boxes}) - if self.mask_on and hasattr(targets_per_image, 'gt_masks'): - assert 0, 'Mask is not supported yet :(' + if self.mask_on and hasattr(targets_per_image, "gt_masks"): + assert 0, "Mask is not supported yet :(" gt_masks = targets_per_image.gt_masks gt_masks = convert_coco_poly_to_mask(gt_masks.polygons, h, w) - new_targets[-1].update({'masks': gt_masks}) + new_targets[-1].update({"masks": gt_masks}) return new_targets - - def post_process(self, outputs, target_sizes): - """ - """ - out_logits, out_bbox = outputs['pred_logits'], outputs['pred_boxes'] + def post_process(self, outputs, target_sizes: Sequence[int]): + """ """ + out_logits, out_bbox = outputs["pred_logits"], outputs["pred_boxes"] assert len(out_logits) == len(target_sizes) assert target_sizes.shape[1] == 2 prob = out_logits.sigmoid() topk_values, topk_indexes = torch.topk( - prob.view(out_logits.shape[0], -1), self.test_topk, dim=1) + prob.view(out_logits.shape[0], -1), self.test_topk, dim=1 + ) scores = topk_values topk_boxes = topk_indexes // out_logits.shape[2] labels = topk_indexes % out_logits.shape[2] boxes = box_cxcywh_to_xyxy(out_bbox) - boxes = torch.gather(boxes, 1, topk_boxes.unsqueeze(-1).repeat(1,1,4)) + boxes = torch.gather(boxes, 1, topk_boxes.unsqueeze(-1).repeat(1, 1, 4)) # and from relative [0, 1] to absolute [0, height] coordinates img_h, img_w = target_sizes.unbind(1) @@ -258,15 +271,14 @@ def post_process(self, outputs, target_sizes): boxes = boxes * scale_fct[:, None, :] results = [] - for s, l, b, size in zip(scores, labels, boxes, target_sizes): + for s, l, b, size in zip(scores, labels, boxes, target_sizes, strict=False): r = Instances((size[0], size[1])) r.pred_boxes = Boxes(b) r.scores = s r.pred_classes = l - results.append({'instances': r}) + results.append({"instances": r}) return results - def preprocess_image(self, batched_inputs): """ Normalize, pad and batch the input images. @@ -274,35 +286,33 @@ def preprocess_image(self, batched_inputs): images = [self.normalizer(x["image"].to(self.device)) for x in batched_inputs] return images - def _weak_loss(self, outputs, batched_inputs): loss = 0 for b, x in enumerate(batched_inputs): - labels = x['pos_category_ids'] - pred_logits = [outputs['pred_logits'][b]] - pred_boxes = [outputs['pred_boxes'][b]] - for xx in outputs['aux_outputs']: - pred_logits.append(xx['pred_logits'][b]) - pred_boxes.append(xx['pred_boxes'][b]) - pred_logits = torch.stack(pred_logits, dim=0) # L x N x C - pred_boxes = torch.stack(pred_boxes, dim=0) # L x N x 4 + labels = x["pos_category_ids"] + pred_logits = [outputs["pred_logits"][b]] + pred_boxes = [outputs["pred_boxes"][b]] + for xx in outputs["aux_outputs"]: + pred_logits.append(xx["pred_logits"][b]) + pred_boxes.append(xx["pred_boxes"][b]) + pred_logits = torch.stack(pred_logits, dim=0) # L x N x C + pred_boxes = torch.stack(pred_boxes, dim=0) # L x N x 4 for label in labels: - loss += self._max_size_loss( - pred_logits, pred_boxes, label) / len(labels) + loss += self._max_size_loss(pred_logits, pred_boxes, label) / len(labels) loss = loss / len(batched_inputs) return loss - - def _max_size_loss(self, logits, boxes, label): - ''' + def _max_size_loss(self, logits, boxes, label: str): + """ Inputs: logits: L x N x C boxes: L x N x 4 - ''' + """ target = logits.new_zeros((logits.shape[0], logits.shape[2])) - target[:, label] = 1. - sizes = boxes[..., 2] * boxes[..., 3] # L x N - ind = sizes.argmax(dim=1) # L + target[:, label] = 1.0 + sizes = boxes[..., 2] * boxes[..., 3] # L x N + ind = sizes.argmax(dim=1) # L loss = F.binary_cross_entropy_with_logits( - logits[range(len(ind)), ind], target, reduction='sum') - return loss \ No newline at end of file + logits[range(len(ind)), ind], target, reduction="sum" + ) + return loss diff --git a/dimos/models/Detic/detic/modeling/roi_heads/detic_fast_rcnn.py b/dimos/models/Detic/detic/modeling/roi_heads/detic_fast_rcnn.py index 186822dd8f..aaa7ca233e 100644 --- a/dimos/models/Detic/detic/modeling/roi_heads/detic_fast_rcnn.py +++ b/dimos/models/Detic/detic/modeling/roi_heads/detic_fast_rcnn.py @@ -1,27 +1,24 @@ # Copyright (c) Facebook, Inc. and its affiliates. -import logging import math -import json -import numpy as np -from typing import Dict, Union -import torch + +from detectron2.config import configurable +from detectron2.layers import ShapeSpec, cat, nonzero_tuple +from detectron2.modeling.roi_heads.fast_rcnn import ( + FastRCNNOutputLayers, + _log_classification_stats, + fast_rcnn_inference, +) +import detectron2.utils.comm as comm +from detectron2.utils.events import get_event_storage from fvcore.nn import giou_loss, smooth_l1_loss +import fvcore.nn.weight_init as weight_init +import torch from torch import nn from torch.nn import functional as F -import fvcore.nn.weight_init as weight_init -import detectron2.utils.comm as comm -from detectron2.config import configurable -from detectron2.layers import ShapeSpec, batched_nms, cat, cross_entropy, nonzero_tuple -from detectron2.structures import Boxes, Instances -from detectron2.utils.events import get_event_storage -from detectron2.modeling.box_regression import Box2BoxTransform -from detectron2.modeling.roi_heads.fast_rcnn import FastRCNNOutputLayers -from detectron2.modeling.roi_heads.fast_rcnn import fast_rcnn_inference -from detectron2.modeling.roi_heads.fast_rcnn import _log_classification_stats -from torch.cuda.amp import autocast -from ..utils import load_class_freq, get_fed_loss_inds +from ..utils import get_fed_loss_inds, load_class_freq from .zero_shot_classifier import ZeroShotClassifier +from typing import Sequence __all__ = ["DeticFastRCNNOutputLayers"] @@ -29,33 +26,33 @@ class DeticFastRCNNOutputLayers(FastRCNNOutputLayers): @configurable def __init__( - self, + self, input_shape: ShapeSpec, *, - mult_proposal_score=False, + mult_proposal_score: bool=False, cls_score=None, - sync_caption_batch = False, - use_sigmoid_ce = False, - use_fed_loss = False, - ignore_zero_cats = False, - fed_loss_num_cat = 50, - dynamic_classifier = False, - image_label_loss = '', - use_zeroshot_cls = False, - image_loss_weight = 0.1, - with_softmax_prop = False, - caption_weight = 1.0, - neg_cap_weight = 1.0, - add_image_box = False, - debug = False, - prior_prob = 0.01, - cat_freq_path = '', - fed_loss_freq_weight = 0.5, - softmax_weak_loss = False, + sync_caption_batch: bool=False, + use_sigmoid_ce: bool=False, + use_fed_loss: bool=False, + ignore_zero_cats: bool=False, + fed_loss_num_cat: int=50, + dynamic_classifier: bool=False, + image_label_loss: str="", + use_zeroshot_cls: bool=False, + image_loss_weight: float=0.1, + with_softmax_prop: bool=False, + caption_weight: float=1.0, + neg_cap_weight: float=1.0, + add_image_box: bool=False, + debug: bool=False, + prior_prob: float=0.01, + cat_freq_path: str="", + fed_loss_freq_weight: float=0.5, + softmax_weak_loss: bool=False, **kwargs, - ): + ) -> None: super().__init__( - input_shape=input_shape, + input_shape=input_shape, **kwargs, ) self.mult_proposal_score = mult_proposal_score @@ -76,40 +73,38 @@ def __init__( self.debug = debug if softmax_weak_loss: - assert image_label_loss in ['max_size'] + assert image_label_loss in ["max_size"] if self.use_sigmoid_ce: bias_value = -math.log((1 - prior_prob) / prior_prob) nn.init.constant_(self.cls_score.bias, bias_value) - + if self.use_fed_loss or self.ignore_zero_cats: freq_weight = load_class_freq(cat_freq_path, fed_loss_freq_weight) - self.register_buffer('freq_weight', freq_weight) + self.register_buffer("freq_weight", freq_weight) else: self.freq_weight = None if self.use_fed_loss and len(self.freq_weight) < self.num_classes: # assert self.num_classes == 11493 - print('Extending federated loss weight') + print("Extending federated loss weight") self.freq_weight = torch.cat( - [self.freq_weight, - self.freq_weight.new_zeros( - self.num_classes - len(self.freq_weight))] + [ + self.freq_weight, + self.freq_weight.new_zeros(self.num_classes - len(self.freq_weight)), + ] ) assert (not self.dynamic_classifier) or (not self.use_fed_loss) - input_size = input_shape.channels * \ - (input_shape.width or 1) * (input_shape.height or 1) - + input_size = input_shape.channels * (input_shape.width or 1) * (input_shape.height or 1) + if self.use_zeroshot_cls: del self.cls_score del self.bbox_pred assert cls_score is not None self.cls_score = cls_score self.bbox_pred = nn.Sequential( - nn.Linear(input_size, input_size), - nn.ReLU(inplace=True), - nn.Linear(input_size, 4) + nn.Linear(input_size, input_size), nn.ReLU(inplace=True), nn.Linear(input_size, 4) ) weight_init.c2_xavier_fill(self.bbox_pred[0]) nn.init.normal_(self.bbox_pred[-1].weight, std=0.001) @@ -125,38 +120,39 @@ def __init__( nn.init.normal_(self.prop_score[-1].weight, mean=0, std=0.001) nn.init.constant_(self.prop_score[-1].bias, 0) - @classmethod def from_config(cls, cfg, input_shape): ret = super().from_config(cfg, input_shape) - ret.update({ - 'mult_proposal_score': cfg.MODEL.ROI_BOX_HEAD.MULT_PROPOSAL_SCORE, - 'sync_caption_batch': cfg.MODEL.SYNC_CAPTION_BATCH, - 'use_sigmoid_ce': cfg.MODEL.ROI_BOX_HEAD.USE_SIGMOID_CE, - 'use_fed_loss': cfg.MODEL.ROI_BOX_HEAD.USE_FED_LOSS, - 'ignore_zero_cats': cfg.MODEL.ROI_BOX_HEAD.IGNORE_ZERO_CATS, - 'fed_loss_num_cat': cfg.MODEL.ROI_BOX_HEAD.FED_LOSS_NUM_CAT, - 'dynamic_classifier': cfg.MODEL.DYNAMIC_CLASSIFIER, - 'image_label_loss': cfg.MODEL.ROI_BOX_HEAD.IMAGE_LABEL_LOSS, - 'use_zeroshot_cls': cfg.MODEL.ROI_BOX_HEAD.USE_ZEROSHOT_CLS, - 'image_loss_weight': cfg.MODEL.ROI_BOX_HEAD.IMAGE_LOSS_WEIGHT, - 'with_softmax_prop': cfg.MODEL.ROI_BOX_HEAD.WITH_SOFTMAX_PROP, - 'caption_weight': cfg.MODEL.ROI_BOX_HEAD.CAPTION_WEIGHT, - 'neg_cap_weight': cfg.MODEL.ROI_BOX_HEAD.NEG_CAP_WEIGHT, - 'add_image_box': cfg.MODEL.ROI_BOX_HEAD.ADD_IMAGE_BOX, - 'debug': cfg.DEBUG or cfg.SAVE_DEBUG or cfg.IS_DEBUG, - 'prior_prob': cfg.MODEL.ROI_BOX_HEAD.PRIOR_PROB, - 'cat_freq_path': cfg.MODEL.ROI_BOX_HEAD.CAT_FREQ_PATH, - 'fed_loss_freq_weight': cfg.MODEL.ROI_BOX_HEAD.FED_LOSS_FREQ_WEIGHT, - 'softmax_weak_loss': cfg.MODEL.ROI_BOX_HEAD.SOFTMAX_WEAK_LOSS, - }) - if ret['use_zeroshot_cls']: - ret['cls_score'] = ZeroShotClassifier(cfg, input_shape) + ret.update( + { + "mult_proposal_score": cfg.MODEL.ROI_BOX_HEAD.MULT_PROPOSAL_SCORE, + "sync_caption_batch": cfg.MODEL.SYNC_CAPTION_BATCH, + "use_sigmoid_ce": cfg.MODEL.ROI_BOX_HEAD.USE_SIGMOID_CE, + "use_fed_loss": cfg.MODEL.ROI_BOX_HEAD.USE_FED_LOSS, + "ignore_zero_cats": cfg.MODEL.ROI_BOX_HEAD.IGNORE_ZERO_CATS, + "fed_loss_num_cat": cfg.MODEL.ROI_BOX_HEAD.FED_LOSS_NUM_CAT, + "dynamic_classifier": cfg.MODEL.DYNAMIC_CLASSIFIER, + "image_label_loss": cfg.MODEL.ROI_BOX_HEAD.IMAGE_LABEL_LOSS, + "use_zeroshot_cls": cfg.MODEL.ROI_BOX_HEAD.USE_ZEROSHOT_CLS, + "image_loss_weight": cfg.MODEL.ROI_BOX_HEAD.IMAGE_LOSS_WEIGHT, + "with_softmax_prop": cfg.MODEL.ROI_BOX_HEAD.WITH_SOFTMAX_PROP, + "caption_weight": cfg.MODEL.ROI_BOX_HEAD.CAPTION_WEIGHT, + "neg_cap_weight": cfg.MODEL.ROI_BOX_HEAD.NEG_CAP_WEIGHT, + "add_image_box": cfg.MODEL.ROI_BOX_HEAD.ADD_IMAGE_BOX, + "debug": cfg.DEBUG or cfg.SAVE_DEBUG or cfg.IS_DEBUG, + "prior_prob": cfg.MODEL.ROI_BOX_HEAD.PRIOR_PROB, + "cat_freq_path": cfg.MODEL.ROI_BOX_HEAD.CAT_FREQ_PATH, + "fed_loss_freq_weight": cfg.MODEL.ROI_BOX_HEAD.FED_LOSS_FREQ_WEIGHT, + "softmax_weak_loss": cfg.MODEL.ROI_BOX_HEAD.SOFTMAX_WEAK_LOSS, + } + ) + if ret["use_zeroshot_cls"]: + ret["cls_score"] = ZeroShotClassifier(cfg, input_shape) return ret - def losses(self, predictions, proposals, \ - use_advanced_loss=True, - classifier_info=(None,None,None)): + def losses( + self, predictions, proposals, use_advanced_loss: bool=True, classifier_info=(None, None, None) + ): """ enable advanced loss """ @@ -187,34 +183,31 @@ def losses(self, predictions, proposals, \ else: loss_cls = self.softmax_cross_entropy_loss(scores, gt_classes) return { - "loss_cls": loss_cls, + "loss_cls": loss_cls, "loss_box_reg": self.box_reg_loss( - proposal_boxes, gt_boxes, proposal_deltas, gt_classes, - num_classes=num_classes) + proposal_boxes, gt_boxes, proposal_deltas, gt_classes, num_classes=num_classes + ), } - def sigmoid_cross_entropy_loss(self, pred_class_logits, gt_classes): if pred_class_logits.numel() == 0: - return pred_class_logits.new_zeros([1])[0] # This is more robust than .sum() * 0. + return pred_class_logits.new_zeros([1])[0] # This is more robust than .sum() * 0. B = pred_class_logits.shape[0] C = pred_class_logits.shape[1] - 1 target = pred_class_logits.new_zeros(B, C + 1) - target[range(len(gt_classes)), gt_classes] = 1 # B x (C + 1) - target = target[:, :C] # B x C + target[range(len(gt_classes)), gt_classes] = 1 # B x (C + 1) + target = target[:, :C] # B x C weight = 1 - - if self.use_fed_loss and (self.freq_weight is not None): # fedloss + + if self.use_fed_loss and (self.freq_weight is not None): # fedloss appeared = get_fed_loss_inds( - gt_classes, - num_sample_cats=self.fed_loss_num_cat, - C=C, - weight=self.freq_weight) + gt_classes, num_sample_cats=self.fed_loss_num_cat, C=C, weight=self.freq_weight + ) appeared_mask = appeared.new_zeros(C + 1) - appeared_mask[appeared] = 1 # C + 1 + appeared_mask[appeared] = 1 # C + 1 appeared_mask = appeared_mask[:C] fed_w = appeared_mask.view(1, C).expand(B, C) weight = weight * fed_w.float() @@ -224,11 +217,11 @@ def sigmoid_cross_entropy_loss(self, pred_class_logits, gt_classes): # import pdb; pdb.set_trace() cls_loss = F.binary_cross_entropy_with_logits( - pred_class_logits[:, :-1], target, reduction='none') # B x C - loss = torch.sum(cls_loss * weight) / B + pred_class_logits[:, :-1], target, reduction="none" + ) # B x C + loss = torch.sum(cls_loss * weight) / B return loss - - + def softmax_cross_entropy_loss(self, pred_class_logits, gt_classes): """ change _no_instance handling @@ -237,34 +230,28 @@ def softmax_cross_entropy_loss(self, pred_class_logits, gt_classes): return pred_class_logits.new_zeros([1])[0] if self.ignore_zero_cats and (self.freq_weight is not None): - zero_weight = torch.cat([ - (self.freq_weight.view(-1) > 1e-4).float(), - self.freq_weight.new_ones(1)]) # C + 1 + zero_weight = torch.cat( + [(self.freq_weight.view(-1) > 1e-4).float(), self.freq_weight.new_ones(1)] + ) # C + 1 loss = F.cross_entropy( - pred_class_logits, gt_classes, - weight=zero_weight, reduction="mean") - elif self.use_fed_loss and (self.freq_weight is not None): # fedloss + pred_class_logits, gt_classes, weight=zero_weight, reduction="mean" + ) + elif self.use_fed_loss and (self.freq_weight is not None): # fedloss C = pred_class_logits.shape[1] - 1 appeared = get_fed_loss_inds( - gt_classes, - num_sample_cats=self.fed_loss_num_cat, - C=C, - weight=self.freq_weight) + gt_classes, num_sample_cats=self.fed_loss_num_cat, C=C, weight=self.freq_weight + ) appeared_mask = appeared.new_zeros(C + 1).float() - appeared_mask[appeared] = 1. # C + 1 - appeared_mask[C] = 1. + appeared_mask[appeared] = 1.0 # C + 1 + appeared_mask[C] = 1.0 loss = F.cross_entropy( - pred_class_logits, gt_classes, - weight=appeared_mask, reduction="mean") + pred_class_logits, gt_classes, weight=appeared_mask, reduction="mean" + ) else: - loss = F.cross_entropy( - pred_class_logits, gt_classes, reduction="mean") + loss = F.cross_entropy(pred_class_logits, gt_classes, reduction="mean") return loss - - def box_reg_loss( - self, proposal_boxes, gt_boxes, pred_deltas, gt_classes, - num_classes=-1): + def box_reg_loss(self, proposal_boxes, gt_boxes, pred_deltas, gt_classes, num_classes: int=-1): """ Allow custom background index """ @@ -303,9 +290,8 @@ def inference(self, predictions, proposals): boxes = self.predict_boxes(predictions, proposals) scores = self.predict_probs(predictions, proposals) if self.mult_proposal_score: - proposal_scores = [p.get('objectness_logits') for p in proposals] - scores = [(s * ps[:, None]) ** 0.5 \ - for s, ps in zip(scores, proposal_scores)] + proposal_scores = [p.get("objectness_logits") for p in proposals] + scores = [(s * ps[:, None]) ** 0.5 for s, ps in zip(scores, proposal_scores, strict=False)] image_shapes = [x.image_size for x in proposals] return fast_rcnn_inference( boxes, @@ -316,7 +302,6 @@ def inference(self, predictions, proposals): self.test_topk_per_image, ) - def predict_probs(self, predictions, proposals): """ support sigmoid @@ -330,17 +315,22 @@ def predict_probs(self, predictions, proposals): probs = F.softmax(scores, dim=-1) return probs.split(num_inst_per_image, dim=0) - - def image_label_losses(self, predictions, proposals, image_labels, \ - classifier_info=(None,None,None), ann_type='image'): - ''' + def image_label_losses( + self, + predictions, + proposals, + image_labels: Sequence[str], + classifier_info=(None, None, None), + ann_type: str="image", + ): + """ Inputs: scores: N x (C + 1) image_labels B x 1 - ''' + """ num_inst_per_image = [len(p) for p in proposals] scores = predictions[0] - scores = scores.split(num_inst_per_image, dim=0) # B x n x (C + 1) + scores = scores.split(num_inst_per_image, dim=0) # B x n x (C + 1) if self.with_softmax_prop: prop_scores = predictions[2].split(num_inst_per_image, dim=0) else: @@ -354,38 +344,37 @@ def image_label_losses(self, predictions, proposals, image_labels, \ storage = get_event_storage() loss = scores[0].new_zeros([1])[0] caption_loss = scores[0].new_zeros([1])[0] - for idx, (score, labels, prop_score, p) in enumerate(zip( - scores, image_labels, prop_scores, proposals)): + for idx, (score, labels, prop_score, p) in enumerate( + zip(scores, image_labels, prop_scores, proposals, strict=False) + ): if score.shape[0] == 0: loss += score.new_zeros([1])[0] continue - if 'caption' in ann_type: - score, caption_loss_img = self._caption_loss( - score, classifier_info, idx, B) + if "caption" in ann_type: + score, caption_loss_img = self._caption_loss(score, classifier_info, idx, B) caption_loss += self.caption_weight * caption_loss_img - if ann_type == 'caption': + if ann_type == "caption": continue if self.debug: - p.selected = score.new_zeros( - (len(p),), dtype=torch.long) - 1 + p.selected = score.new_zeros((len(p),), dtype=torch.long) - 1 for i_l, label in enumerate(labels): if self.dynamic_classifier: if idx == 0 and i_l == 0 and comm.is_main_process(): - storage.put_scalar('stats_label', label) + storage.put_scalar("stats_label", label) label = classifier_info[1][1][label] assert label < score.shape[1] - if self.image_label_loss in ['wsod', 'wsddn']: + if self.image_label_loss in ["wsod", "wsddn"]: loss_i, ind = self._wsddn_loss(score, prop_score, label) - elif self.image_label_loss == 'max_score': + elif self.image_label_loss == "max_score": loss_i, ind = self._max_score_loss(score, label) - elif self.image_label_loss == 'max_size': + elif self.image_label_loss == "max_size": loss_i, ind = self._max_size_loss(score, label, p) - elif self.image_label_loss == 'first': + elif self.image_label_loss == "first": loss_i, ind = self._first_loss(score, label) - elif self.image_label_loss == 'image': + elif self.image_label_loss == "image": loss_i, ind = self._image_loss(score, label) - elif self.image_label_loss == 'min_loss': + elif self.image_label_loss == "min_loss": loss_i, ind = self._min_loss_loss(score, label) else: assert 0 @@ -397,43 +386,50 @@ def image_label_losses(self, predictions, proposals, image_labels, \ p.selected[ind_i] = label else: img_box_count = ind - select_size_count = p[ind].proposal_boxes.area() / \ - (p.image_size[0] * p.image_size[1]) + select_size_count = p[ind].proposal_boxes.area() / ( + p.image_size[0] * p.image_size[1] + ) max_score_count = score[ind, label].sigmoid() - select_x_count = (p.proposal_boxes.tensor[ind, 0] + \ - p.proposal_boxes.tensor[ind, 2]) / 2 / p.image_size[1] - select_y_count = (p.proposal_boxes.tensor[ind, 1] + \ - p.proposal_boxes.tensor[ind, 3]) / 2 / p.image_size[0] + select_x_count = ( + (p.proposal_boxes.tensor[ind, 0] + p.proposal_boxes.tensor[ind, 2]) + / 2 + / p.image_size[1] + ) + select_y_count = ( + (p.proposal_boxes.tensor[ind, 1] + p.proposal_boxes.tensor[ind, 3]) + / 2 + / p.image_size[0] + ) if self.debug: p.selected[ind] = label loss = loss / B - storage.put_scalar('stats_l_image', loss.item()) - if 'caption' in ann_type: + storage.put_scalar("stats_l_image", loss.item()) + if "caption" in ann_type: caption_loss = caption_loss / B loss = loss + caption_loss - storage.put_scalar('stats_l_caption', caption_loss.item()) + storage.put_scalar("stats_l_caption", caption_loss.item()) if comm.is_main_process(): - storage.put_scalar('pool_stats', img_box_count) - storage.put_scalar('stats_select_size', select_size_count) - storage.put_scalar('stats_select_x', select_x_count) - storage.put_scalar('stats_select_y', select_y_count) - storage.put_scalar('stats_max_label_score', max_score_count) + storage.put_scalar("pool_stats", img_box_count) + storage.put_scalar("stats_select_size", select_size_count) + storage.put_scalar("stats_select_x", select_x_count) + storage.put_scalar("stats_select_y", select_y_count) + storage.put_scalar("stats_max_label_score", max_score_count) return { - 'image_loss': loss * self.image_loss_weight, - 'loss_cls': score.new_zeros([1])[0], - 'loss_box_reg': score.new_zeros([1])[0]} - + "image_loss": loss * self.image_loss_weight, + "loss_cls": score.new_zeros([1])[0], + "loss_box_reg": score.new_zeros([1])[0], + } - def forward(self, x, classifier_info=(None,None,None)): + def forward(self, x, classifier_info=(None, None, None)): """ enable classifier_info """ if x.dim() > 2: x = torch.flatten(x, start_dim=1) scores = [] - + if classifier_info[0] is not None: cls_scores = self.cls_score(x, classifier=classifier_info[0]) scores.append(cls_scores) @@ -444,11 +440,11 @@ def forward(self, x, classifier_info=(None,None,None)): if classifier_info[2] is not None: cap_cls = classifier_info[2] if self.sync_caption_batch: - caption_scores = self.cls_score(x, classifier=cap_cls[:, :-1]) + caption_scores = self.cls_score(x, classifier=cap_cls[:, :-1]) else: caption_scores = self.cls_score(x, classifier=cap_cls) scores.append(caption_scores) - scores = torch.cat(scores, dim=1) # B x C' or B x N or B x (C'+N) + scores = torch.cat(scores, dim=1) # B x C' or B x N or B x (C'+N) proposal_deltas = self.bbox_pred(x) if self.with_softmax_prop: @@ -457,129 +453,107 @@ def forward(self, x, classifier_info=(None,None,None)): else: return scores, proposal_deltas - - def _caption_loss(self, score, classifier_info, idx, B): - assert (classifier_info[2] is not None) + def _caption_loss(self, score, classifier_info, idx: int, B): + assert classifier_info[2] is not None assert self.add_image_box cls_and_cap_num = score.shape[1] cap_num = classifier_info[2].shape[0] - score, caption_score = score.split( - [cls_and_cap_num - cap_num, cap_num], dim=1) + score, caption_score = score.split([cls_and_cap_num - cap_num, cap_num], dim=1) # n x (C + 1), n x B - caption_score = caption_score[-1:] # 1 x B # -1: image level box + caption_score = caption_score[-1:] # 1 x B # -1: image level box caption_target = caption_score.new_zeros( - caption_score.shape) # 1 x B or 1 x MB, M: num machines + caption_score.shape + ) # 1 x B or 1 x MB, M: num machines if self.sync_caption_batch: # caption_target: 1 x MB rank = comm.get_rank() global_idx = B * rank + idx - assert (classifier_info[2][ - global_idx, -1] - rank) ** 2 < 1e-8, \ - '{} {} {} {} {}'.format( - rank, global_idx, - classifier_info[2][global_idx, -1], - classifier_info[2].shape, - classifier_info[2][:, -1]) - caption_target[:, global_idx] = 1. + assert (classifier_info[2][global_idx, -1] - rank) ** 2 < 1e-8, f"{rank} {global_idx} {classifier_info[2][global_idx, -1]} {classifier_info[2].shape} {classifier_info[2][:, -1]}" + caption_target[:, global_idx] = 1.0 else: assert caption_score.shape[1] == B - caption_target[:, idx] = 1. + caption_target[:, idx] = 1.0 caption_loss_img = F.binary_cross_entropy_with_logits( - caption_score, caption_target, reduction='none') + caption_score, caption_target, reduction="none" + ) if self.sync_caption_batch: fg_mask = (caption_target > 0.5).float() - assert (fg_mask.sum().item() - 1.) ** 2 < 1e-8, '{} {}'.format( - fg_mask.shape, fg_mask) + assert (fg_mask.sum().item() - 1.0) ** 2 < 1e-8, f"{fg_mask.shape} {fg_mask}" pos_loss = (caption_loss_img * fg_mask).sum() - neg_loss = (caption_loss_img * (1. - fg_mask)).sum() + neg_loss = (caption_loss_img * (1.0 - fg_mask)).sum() caption_loss_img = pos_loss + self.neg_cap_weight * neg_loss else: caption_loss_img = caption_loss_img.sum() return score, caption_loss_img - - def _wsddn_loss(self, score, prop_score, label): + def _wsddn_loss(self, score, prop_score, label: str): assert prop_score is not None loss = 0 - final_score = score.sigmoid() * \ - F.softmax(prop_score, dim=0) # B x (C + 1) - img_score = torch.clamp( - torch.sum(final_score, dim=0), - min=1e-10, max=1-1e-10) # (C + 1) - target = img_score.new_zeros(img_score.shape) # (C + 1) - target[label] = 1. + final_score = score.sigmoid() * F.softmax(prop_score, dim=0) # B x (C + 1) + img_score = torch.clamp(torch.sum(final_score, dim=0), min=1e-10, max=1 - 1e-10) # (C + 1) + target = img_score.new_zeros(img_score.shape) # (C + 1) + target[label] = 1.0 loss += F.binary_cross_entropy(img_score, target) ind = final_score[:, label].argmax() return loss, ind - - def _max_score_loss(self, score, label): + def _max_score_loss(self, score, label: str): loss = 0 target = score.new_zeros(score.shape[1]) - target[label] = 1. + target[label] = 1.0 ind = score[:, label].argmax().item() - loss += F.binary_cross_entropy_with_logits( - score[ind], target, reduction='sum') + loss += F.binary_cross_entropy_with_logits(score[ind], target, reduction="sum") return loss, ind - - def _min_loss_loss(self, score, label): + def _min_loss_loss(self, score, label: str): loss = 0 target = score.new_zeros(score.shape) - target[:, label] = 1. + target[:, label] = 1.0 with torch.no_grad(): - x = F.binary_cross_entropy_with_logits( - score, target, reduction='none').sum(dim=1) # n + x = F.binary_cross_entropy_with_logits(score, target, reduction="none").sum(dim=1) # n ind = x.argmin().item() - loss += F.binary_cross_entropy_with_logits( - score[ind], target[0], reduction='sum') + loss += F.binary_cross_entropy_with_logits(score[ind], target[0], reduction="sum") return loss, ind - - def _first_loss(self, score, label): + def _first_loss(self, score, label: str): loss = 0 target = score.new_zeros(score.shape[1]) - target[label] = 1. + target[label] = 1.0 ind = 0 - loss += F.binary_cross_entropy_with_logits( - score[ind], target, reduction='sum') + loss += F.binary_cross_entropy_with_logits(score[ind], target, reduction="sum") return loss, ind - - def _image_loss(self, score, label): + def _image_loss(self, score, label: str): assert self.add_image_box target = score.new_zeros(score.shape[1]) - target[label] = 1. + target[label] = 1.0 ind = score.shape[0] - 1 - loss = F.binary_cross_entropy_with_logits( - score[ind], target, reduction='sum') + loss = F.binary_cross_entropy_with_logits(score[ind], target, reduction="sum") return loss, ind - - def _max_size_loss(self, score, label, p): + def _max_size_loss(self, score, label: str, p): loss = 0 target = score.new_zeros(score.shape[1]) - target[label] = 1. + target[label] = 1.0 sizes = p.proposal_boxes.area() ind = sizes[:-1].argmax().item() if len(sizes) > 1 else 0 if self.softmax_weak_loss: loss += F.cross_entropy( - score[ind:ind+1], - score.new_tensor(label, dtype=torch.long).view(1), - reduction='sum') + score[ind : ind + 1], + score.new_tensor(label, dtype=torch.long).view(1), + reduction="sum", + ) else: - loss += F.binary_cross_entropy_with_logits( - score[ind], target, reduction='sum') + loss += F.binary_cross_entropy_with_logits(score[ind], target, reduction="sum") return loss, ind - -def put_label_distribution(storage, hist_name, hist_counts, num_classes): - """ - """ +def put_label_distribution(storage, hist_name: str, hist_counts, num_classes: int) -> None: + """ """ ht_min, ht_max = 0, num_classes hist_edges = torch.linspace( - start=ht_min, end=ht_max, steps=num_classes + 1, dtype=torch.float32) + start=ht_min, end=ht_max, steps=num_classes + 1, dtype=torch.float32 + ) hist_params = dict( tag=hist_name, @@ -592,4 +566,4 @@ def put_label_distribution(storage, hist_name, hist_counts, num_classes): bucket_counts=hist_counts.tolist(), global_step=storage._iter, ) - storage._histograms.append(hist_params) \ No newline at end of file + storage._histograms.append(hist_params) diff --git a/dimos/models/Detic/detic/modeling/roi_heads/detic_roi_heads.py b/dimos/models/Detic/detic/modeling/roi_heads/detic_roi_heads.py index c87559359e..a8f1f4efe2 100644 --- a/dimos/models/Detic/detic/modeling/roi_heads/detic_roi_heads.py +++ b/dimos/models/Detic/detic/modeling/roi_heads/detic_roi_heads.py @@ -1,29 +1,16 @@ # Copyright (c) Facebook, Inc. and its affiliates. -import copy -import numpy as np -import json -import math -import torch -from torch import nn -from torch.autograd.function import Function -from typing import Dict, List, Optional, Tuple, Union -from torch.nn import functional as F - from detectron2.config import configurable -from detectron2.layers import ShapeSpec -from detectron2.layers import batched_nms -from detectron2.structures import Boxes, Instances, pairwise_iou -from detectron2.utils.events import get_event_storage - from detectron2.modeling.box_regression import Box2BoxTransform -from detectron2.modeling.roi_heads.fast_rcnn import fast_rcnn_inference -from detectron2.modeling.roi_heads.roi_heads import ROI_HEADS_REGISTRY, StandardROIHeads from detectron2.modeling.roi_heads.cascade_rcnn import CascadeROIHeads, _ScaleGradient -from detectron2.modeling.roi_heads.box_head import build_box_head +from detectron2.modeling.roi_heads.fast_rcnn import fast_rcnn_inference +from detectron2.modeling.roi_heads.roi_heads import ROI_HEADS_REGISTRY +from detectron2.structures import Boxes, Instances +from detectron2.utils.events import get_event_storage +import torch + from .detic_fast_rcnn import DeticFastRCNNOutputLayers -from ..debug import debug_second_stage +from typing import Sequence -from torch.cuda.amp import autocast @ROI_HEADS_REGISTRY.register() class DeticCascadeROIHeads(CascadeROIHeads): @@ -40,7 +27,7 @@ def __init__( mask_weight: float = 1.0, one_class_per_proposal: bool = False, **kwargs, - ): + ) -> None: super().__init__(**kwargs) self.mult_proposal_score = mult_proposal_score self.with_image_labels = with_image_labels @@ -54,48 +41,50 @@ def __init__( @classmethod def from_config(cls, cfg, input_shape): ret = super().from_config(cfg, input_shape) - ret.update({ - 'mult_proposal_score': cfg.MODEL.ROI_BOX_HEAD.MULT_PROPOSAL_SCORE, - 'with_image_labels': cfg.WITH_IMAGE_LABELS, - 'add_image_box': cfg.MODEL.ROI_BOX_HEAD.ADD_IMAGE_BOX, - 'image_box_size': cfg.MODEL.ROI_BOX_HEAD.IMAGE_BOX_SIZE, - 'ws_num_props': cfg.MODEL.ROI_BOX_HEAD.WS_NUM_PROPS, - 'add_feature_to_prop': cfg.MODEL.ROI_BOX_HEAD.ADD_FEATURE_TO_PROP, - 'mask_weight': cfg.MODEL.ROI_HEADS.MASK_WEIGHT, - 'one_class_per_proposal': cfg.MODEL.ROI_HEADS.ONE_CLASS_PER_PROPOSAL, - }) + ret.update( + { + "mult_proposal_score": cfg.MODEL.ROI_BOX_HEAD.MULT_PROPOSAL_SCORE, + "with_image_labels": cfg.WITH_IMAGE_LABELS, + "add_image_box": cfg.MODEL.ROI_BOX_HEAD.ADD_IMAGE_BOX, + "image_box_size": cfg.MODEL.ROI_BOX_HEAD.IMAGE_BOX_SIZE, + "ws_num_props": cfg.MODEL.ROI_BOX_HEAD.WS_NUM_PROPS, + "add_feature_to_prop": cfg.MODEL.ROI_BOX_HEAD.ADD_FEATURE_TO_PROP, + "mask_weight": cfg.MODEL.ROI_HEADS.MASK_WEIGHT, + "one_class_per_proposal": cfg.MODEL.ROI_HEADS.ONE_CLASS_PER_PROPOSAL, + } + ) return ret - @classmethod - def _init_box_head(self, cfg, input_shape): + def _init_box_head(cls, cfg, input_shape): ret = super()._init_box_head(cfg, input_shape) - del ret['box_predictors'] + del ret["box_predictors"] cascade_bbox_reg_weights = cfg.MODEL.ROI_BOX_CASCADE_HEAD.BBOX_REG_WEIGHTS box_predictors = [] - for box_head, bbox_reg_weights in zip(ret['box_heads'], \ - cascade_bbox_reg_weights): + for box_head, bbox_reg_weights in zip(ret["box_heads"], cascade_bbox_reg_weights, strict=False): box_predictors.append( DeticFastRCNNOutputLayers( - cfg, box_head.output_shape, - box2box_transform=Box2BoxTransform(weights=bbox_reg_weights) - )) - ret['box_predictors'] = box_predictors + cfg, + box_head.output_shape, + box2box_transform=Box2BoxTransform(weights=bbox_reg_weights), + ) + ) + ret["box_predictors"] = box_predictors return ret - - def _forward_box(self, features, proposals, targets=None, - ann_type='box', classifier_info=(None,None,None)): + def _forward_box( + self, features, proposals, targets=None, ann_type: str="box", classifier_info=(None, None, None) + ): """ Add mult proposal scores at testing Add ann_type """ if (not self.training) and self.mult_proposal_score: - if len(proposals) > 0 and proposals[0].has('scores'): - proposal_scores = [p.get('scores') for p in proposals] + if len(proposals) > 0 and proposals[0].has("scores"): + proposal_scores = [p.get("scores") for p in proposals] else: - proposal_scores = [p.get('objectness_logits') for p in proposals] - + proposal_scores = [p.get("objectness_logits") for p in proposals] + features = [features[f] for f in self.box_in_features] head_outputs = [] # (predictor, predictions, proposals) prev_pred_boxes = None @@ -104,56 +93,56 @@ def _forward_box(self, features, proposals, targets=None, for k in range(self.num_cascade_stages): if k > 0: proposals = self._create_proposals_from_boxes( - prev_pred_boxes, image_sizes, - logits=[p.objectness_logits for p in proposals]) - if self.training and ann_type in ['box']: - proposals = self._match_and_label_boxes( - proposals, k, targets) - predictions = self._run_stage(features, proposals, k, - classifier_info=classifier_info) + prev_pred_boxes, image_sizes, logits=[p.objectness_logits for p in proposals] + ) + if self.training and ann_type in ["box"]: + proposals = self._match_and_label_boxes(proposals, k, targets) + predictions = self._run_stage(features, proposals, k, classifier_info=classifier_info) prev_pred_boxes = self.box_predictor[k].predict_boxes( - (predictions[0], predictions[1]), proposals) + (predictions[0], predictions[1]), proposals + ) head_outputs.append((self.box_predictor[k], predictions, proposals)) - + if self.training: losses = {} storage = get_event_storage() for stage, (predictor, predictions, proposals) in enumerate(head_outputs): - with storage.name_scope("stage{}".format(stage)): - if ann_type != 'box': + with storage.name_scope(f"stage{stage}"): + if ann_type != "box": stage_losses = {} - if ann_type in ['image', 'caption', 'captiontag']: + if ann_type in ["image", "caption", "captiontag"]: image_labels = [x._pos_category_ids for x in targets] weak_losses = predictor.image_label_losses( - predictions, proposals, image_labels, + predictions, + proposals, + image_labels, classifier_info=classifier_info, - ann_type=ann_type) + ann_type=ann_type, + ) stage_losses.update(weak_losses) - else: # supervised + else: # supervised stage_losses = predictor.losses( - (predictions[0], predictions[1]), proposals, - classifier_info=classifier_info) + (predictions[0], predictions[1]), + proposals, + classifier_info=classifier_info, + ) if self.with_image_labels: - stage_losses['image_loss'] = \ - predictions[0].new_zeros([1])[0] - losses.update({k + "_stage{}".format(stage): v \ - for k, v in stage_losses.items()}) + stage_losses["image_loss"] = predictions[0].new_zeros([1])[0] + losses.update({k + f"_stage{stage}": v for k, v in stage_losses.items()}) return losses else: # Each is a list[Tensor] of length #image. Each tensor is Ri x (K+1) scores_per_stage = [h[0].predict_probs(h[1], h[2]) for h in head_outputs] scores = [ sum(list(scores_per_image)) * (1.0 / self.num_cascade_stages) - for scores_per_image in zip(*scores_per_stage) + for scores_per_image in zip(*scores_per_stage, strict=False) ] if self.mult_proposal_score: - scores = [(s * ps[:, None]) ** 0.5 \ - for s, ps in zip(scores, proposal_scores)] + scores = [(s * ps[:, None]) ** 0.5 for s, ps in zip(scores, proposal_scores, strict=False)] if self.one_class_per_proposal: scores = [s * (s == s[:, :-1].max(dim=1)[0][:, None]).float() for s in scores] predictor, predictions, proposals = head_outputs[-1] - boxes = predictor.predict_boxes( - (predictions[0], predictions[1]), proposals) + boxes = predictor.predict_boxes((predictions[0], predictions[1]), proposals) pred_instances, _ = fast_rcnn_inference( boxes, scores, @@ -164,50 +153,54 @@ def _forward_box(self, features, proposals, targets=None, ) return pred_instances - - def forward(self, images, features, proposals, targets=None, - ann_type='box', classifier_info=(None,None,None)): - ''' + def forward( + self, + images, + features, + proposals, + targets=None, + ann_type: str="box", + classifier_info=(None, None, None), + ): + """ enable debug and image labels classifier_info is shared across the batch - ''' + """ if self.training: - if ann_type in ['box', 'prop', 'proptag']: - proposals = self.label_and_sample_proposals( - proposals, targets) + if ann_type in ["box", "prop", "proptag"]: + proposals = self.label_and_sample_proposals(proposals, targets) else: proposals = self.get_top_proposals(proposals) - - losses = self._forward_box(features, proposals, targets, \ - ann_type=ann_type, classifier_info=classifier_info) - if ann_type == 'box' and targets[0].has('gt_masks'): + + losses = self._forward_box( + features, proposals, targets, ann_type=ann_type, classifier_info=classifier_info + ) + if ann_type == "box" and targets[0].has("gt_masks"): mask_losses = self._forward_mask(features, proposals) - losses.update({k: v * self.mask_weight \ - for k, v in mask_losses.items()}) + losses.update({k: v * self.mask_weight for k, v in mask_losses.items()}) losses.update(self._forward_keypoint(features, proposals)) else: - losses.update(self._get_empty_mask_loss( - features, proposals, - device=proposals[0].objectness_logits.device)) + losses.update( + self._get_empty_mask_loss( + features, proposals, device=proposals[0].objectness_logits.device + ) + ) return proposals, losses else: - pred_instances = self._forward_box( - features, proposals, classifier_info=classifier_info) + pred_instances = self._forward_box(features, proposals, classifier_info=classifier_info) pred_instances = self.forward_with_given_boxes(features, pred_instances) return pred_instances, {} - def get_top_proposals(self, proposals): for i in range(len(proposals)): proposals[i].proposal_boxes.clip(proposals[i].image_size) - proposals = [p[:self.ws_num_props] for p in proposals] + proposals = [p[: self.ws_num_props] for p in proposals] for i, p in enumerate(proposals): p.proposal_boxes.tensor = p.proposal_boxes.tensor.detach() if self.add_image_box: proposals[i] = self._add_image_box(p) return proposals - def _add_image_box(self, p): image_box = Instances(p.image_size) n = 1 @@ -215,31 +208,30 @@ def _add_image_box(self, p): f = self.image_box_size image_box.proposal_boxes = Boxes( p.proposal_boxes.tensor.new_tensor( - [w * (1. - f) / 2., - h * (1. - f) / 2., - w * (1. - (1. - f) / 2.), - h * (1. - (1. - f) / 2.)] - ).view(n, 4)) + [ + w * (1.0 - f) / 2.0, + h * (1.0 - f) / 2.0, + w * (1.0 - (1.0 - f) / 2.0), + h * (1.0 - (1.0 - f) / 2.0), + ] + ).view(n, 4) + ) image_box.objectness_logits = p.objectness_logits.new_ones(n) return Instances.cat([p, image_box]) - def _get_empty_mask_loss(self, features, proposals, device): if self.mask_on: - return {'loss_mask': torch.zeros( - (1, ), device=device, dtype=torch.float32)[0]} + return {"loss_mask": torch.zeros((1,), device=device, dtype=torch.float32)[0]} else: return {} - - def _create_proposals_from_boxes(self, boxes, image_sizes, logits): + def _create_proposals_from_boxes(self, boxes, image_sizes: Sequence[int], logits): """ Add objectness_logits """ boxes = [Boxes(b.detach()) for b in boxes] proposals = [] - for boxes_per_image, image_size, logit in zip( - boxes, image_sizes, logits): + for boxes_per_image, image_size, logit in zip(boxes, image_sizes, logits, strict=False): boxes_per_image.clip(image_size) if self.training: inds = boxes_per_image.nonempty() @@ -251,9 +243,7 @@ def _create_proposals_from_boxes(self, boxes, image_sizes, logits): proposals.append(prop) return proposals - - def _run_stage(self, features, proposals, stage, \ - classifier_info=(None,None,None)): + def _run_stage(self, features, proposals, stage, classifier_info=(None, None, None)): """ Support classifier_info and add_feature_to_prop """ @@ -262,10 +252,7 @@ def _run_stage(self, features, proposals, stage, \ box_features = _ScaleGradient.apply(box_features, 1.0 / self.num_cascade_stages) box_features = self.box_head[stage](box_features) if self.add_feature_to_prop: - feats_per_image = box_features.split( - [len(p) for p in proposals], dim=0) - for feat, p in zip(feats_per_image, proposals): + feats_per_image = box_features.split([len(p) for p in proposals], dim=0) + for feat, p in zip(feats_per_image, proposals, strict=False): p.feat = feat - return self.box_predictor[stage]( - box_features, - classifier_info=classifier_info) + return self.box_predictor[stage](box_features, classifier_info=classifier_info) diff --git a/dimos/models/Detic/detic/modeling/roi_heads/res5_roi_heads.py b/dimos/models/Detic/detic/modeling/roi_heads/res5_roi_heads.py index bab706999a..642f889b5d 100644 --- a/dimos/models/Detic/detic/modeling/roi_heads/res5_roi_heads.py +++ b/dimos/models/Detic/detic/modeling/roi_heads/res5_roi_heads.py @@ -1,35 +1,21 @@ # Copyright (c) Facebook, Inc. and its affiliates. -import inspect -import logging -import numpy as np -from typing import Dict, List, Optional, Tuple -import torch -from torch import nn - from detectron2.config import configurable -from detectron2.layers import ShapeSpec, nonzero_tuple -from detectron2.structures import Boxes, ImageList, Instances, pairwise_iou -from detectron2.utils.events import get_event_storage -from detectron2.utils.registry import Registry - -from detectron2.modeling.box_regression import Box2BoxTransform -from detectron2.modeling.roi_heads.fast_rcnn import fast_rcnn_inference +from detectron2.layers import ShapeSpec from detectron2.modeling.roi_heads.roi_heads import ROI_HEADS_REGISTRY, Res5ROIHeads -from detectron2.modeling.roi_heads.cascade_rcnn import CascadeROIHeads, _ScaleGradient -from detectron2.modeling.roi_heads.box_head import build_box_head +from detectron2.structures import Boxes, Instances +import torch -from .detic_fast_rcnn import DeticFastRCNNOutputLayers from ..debug import debug_second_stage +from .detic_fast_rcnn import DeticFastRCNNOutputLayers -from torch.cuda.amp import autocast @ROI_HEADS_REGISTRY.register() class CustomRes5ROIHeads(Res5ROIHeads): @configurable - def __init__(self, **kwargs): - cfg = kwargs.pop('cfg') + def __init__(self, **kwargs) -> None: + cfg = kwargs.pop("cfg") super().__init__(**kwargs) - stage_channel_factor = 2 ** 3 + stage_channel_factor = 2**3 out_channels = cfg.MODEL.RESNETS.RES2_OUT_CHANNELS * stage_channel_factor self.with_image_labels = cfg.WITH_IMAGE_LABELS @@ -46,31 +32,39 @@ def __init__(self, **kwargs): if self.save_debug: self.debug_show_name = cfg.DEBUG_SHOW_NAME self.vis_thresh = cfg.VIS_THRESH - self.pixel_mean = torch.Tensor(cfg.MODEL.PIXEL_MEAN).to( - torch.device(cfg.MODEL.DEVICE)).view(3, 1, 1) - self.pixel_std = torch.Tensor(cfg.MODEL.PIXEL_STD).to( - torch.device(cfg.MODEL.DEVICE)).view(3, 1, 1) - self.bgr = (cfg.INPUT.FORMAT == 'BGR') + self.pixel_mean = ( + torch.Tensor(cfg.MODEL.PIXEL_MEAN).to(torch.device(cfg.MODEL.DEVICE)).view(3, 1, 1) + ) + self.pixel_std = ( + torch.Tensor(cfg.MODEL.PIXEL_STD).to(torch.device(cfg.MODEL.DEVICE)).view(3, 1, 1) + ) + self.bgr = cfg.INPUT.FORMAT == "BGR" @classmethod def from_config(cls, cfg, input_shape): ret = super().from_config(cfg, input_shape) - ret['cfg'] = cfg + ret["cfg"] = cfg return ret - def forward(self, images, features, proposals, targets=None, - ann_type='box', classifier_info=(None,None,None)): - ''' + def forward( + self, + images, + features, + proposals, + targets=None, + ann_type: str="box", + classifier_info=(None, None, None), + ): + """ enable debug and image labels classifier_info is shared across the batch - ''' + """ if not self.save_debug: del images - + if self.training: - if ann_type in ['box']: - proposals = self.label_and_sample_proposals( - proposals, targets) + if ann_type in ["box"]: + proposals = self.label_and_sample_proposals(proposals, targets) else: proposals = self.get_top_proposals(proposals) @@ -79,71 +73,80 @@ def forward(self, images, features, proposals, targets=None, [features[f] for f in self.in_features], proposal_boxes ) predictions = self.box_predictor( - box_features.mean(dim=[2, 3]), - classifier_info=classifier_info) - + box_features.mean(dim=[2, 3]), classifier_info=classifier_info + ) + if self.add_feature_to_prop: feats_per_image = box_features.mean(dim=[2, 3]).split( - [len(p) for p in proposals], dim=0) - for feat, p in zip(feats_per_image, proposals): + [len(p) for p in proposals], dim=0 + ) + for feat, p in zip(feats_per_image, proposals, strict=False): p.feat = feat if self.training: del features - if (ann_type != 'box'): + if ann_type != "box": image_labels = [x._pos_category_ids for x in targets] losses = self.box_predictor.image_label_losses( - predictions, proposals, image_labels, + predictions, + proposals, + image_labels, classifier_info=classifier_info, - ann_type=ann_type) + ann_type=ann_type, + ) else: - losses = self.box_predictor.losses( - (predictions[0], predictions[1]), proposals) + losses = self.box_predictor.losses((predictions[0], predictions[1]), proposals) if self.with_image_labels: - assert 'image_loss' not in losses - losses['image_loss'] = predictions[0].new_zeros([1])[0] + assert "image_loss" not in losses + losses["image_loss"] = predictions[0].new_zeros([1])[0] if self.save_debug: - denormalizer = lambda x: x * self.pixel_std + self.pixel_mean - if ann_type != 'box': + def denormalizer(x): + return x * self.pixel_std + self.pixel_mean + if ann_type != "box": image_labels = [x._pos_category_ids for x in targets] else: image_labels = [[] for x in targets] debug_second_stage( [denormalizer(x.clone()) for x in images], - targets, proposals=proposals, + targets, + proposals=proposals, save_debug=self.save_debug, debug_show_name=self.debug_show_name, vis_thresh=self.vis_thresh, image_labels=image_labels, save_debug_path=self.save_debug_path, - bgr=self.bgr) + bgr=self.bgr, + ) return proposals, losses else: pred_instances, _ = self.box_predictor.inference(predictions, proposals) pred_instances = self.forward_with_given_boxes(features, pred_instances) if self.save_debug: - denormalizer = lambda x: x * self.pixel_std + self.pixel_mean + def denormalizer(x): + return x * self.pixel_std + self.pixel_mean debug_second_stage( [denormalizer(x.clone()) for x in images], - pred_instances, proposals=proposals, + pred_instances, + proposals=proposals, save_debug=self.save_debug, debug_show_name=self.debug_show_name, vis_thresh=self.vis_thresh, save_debug_path=self.save_debug_path, - bgr=self.bgr) + bgr=self.bgr, + ) return pred_instances, {} def get_top_proposals(self, proposals): for i in range(len(proposals)): proposals[i].proposal_boxes.clip(proposals[i].image_size) - proposals = [p[:self.ws_num_props] for p in proposals] + proposals = [p[: self.ws_num_props] for p in proposals] for i, p in enumerate(proposals): p.proposal_boxes.tensor = p.proposal_boxes.tensor.detach() if self.add_image_box: proposals[i] = self._add_image_box(p) return proposals - def _add_image_box(self, p, use_score=False): + def _add_image_box(self, p, use_score: bool=False): image_box = Instances(p.image_size) n = 1 h, w = p.image_size @@ -151,23 +154,22 @@ def _add_image_box(self, p, use_score=False): f = self.image_box_size image_box.proposal_boxes = Boxes( p.proposal_boxes.tensor.new_tensor( - [w * (1. - f) / 2., - h * (1. - f) / 2., - w * (1. - (1. - f) / 2.), - h * (1. - (1. - f) / 2.)] - ).view(n, 4)) + [ + w * (1.0 - f) / 2.0, + h * (1.0 - f) / 2.0, + w * (1.0 - (1.0 - f) / 2.0), + h * (1.0 - (1.0 - f) / 2.0), + ] + ).view(n, 4) + ) else: image_box.proposal_boxes = Boxes( - p.proposal_boxes.tensor.new_tensor( - [0, 0, w, h]).view(n, 4)) + p.proposal_boxes.tensor.new_tensor([0, 0, w, h]).view(n, 4) + ) if use_score: - image_box.scores = \ - p.objectness_logits.new_ones(n) - image_box.pred_classes = \ - p.objectness_logits.new_zeros(n, dtype=torch.long) - image_box.objectness_logits = \ - p.objectness_logits.new_ones(n) + image_box.scores = p.objectness_logits.new_ones(n) + image_box.pred_classes = p.objectness_logits.new_zeros(n, dtype=torch.long) + image_box.objectness_logits = p.objectness_logits.new_ones(n) else: - image_box.objectness_logits = \ - p.objectness_logits.new_ones(n) - return Instances.cat([p, image_box]) \ No newline at end of file + image_box.objectness_logits = p.objectness_logits.new_ones(n) + return Instances.cat([p, image_box]) diff --git a/dimos/models/Detic/detic/modeling/roi_heads/zero_shot_classifier.py b/dimos/models/Detic/detic/modeling/roi_heads/zero_shot_classifier.py index edf217c6db..d436e6be34 100644 --- a/dimos/models/Detic/detic/modeling/roi_heads/zero_shot_classifier.py +++ b/dimos/models/Detic/detic/modeling/roi_heads/zero_shot_classifier.py @@ -1,10 +1,11 @@ # Copyright (c) Facebook, Inc. and its affiliates. +from detectron2.config import configurable +from detectron2.layers import ShapeSpec import numpy as np import torch from torch import nn from torch.nn import functional as F -from detectron2.config import configurable -from detectron2.layers import Linear, ShapeSpec + class ZeroShotClassifier(nn.Module): @configurable @@ -15,10 +16,10 @@ def __init__( num_classes: int, zs_weight_path: str, zs_weight_dim: int = 512, - use_bias: float = 0.0, + use_bias: float = 0.0, norm_weight: bool = True, norm_temperature: float = 50.0, - ): + ) -> None: super().__init__() if isinstance(input_shape, int): # some backward compatibility input_shape = ShapeSpec(channels=input_shape) @@ -31,52 +32,52 @@ def __init__( self.cls_bias = nn.Parameter(torch.ones(1) * use_bias) self.linear = nn.Linear(input_size, zs_weight_dim) - - if zs_weight_path == 'rand': + + if zs_weight_path == "rand": zs_weight = torch.randn((zs_weight_dim, num_classes)) nn.init.normal_(zs_weight, std=0.01) else: - zs_weight = torch.tensor( - np.load(zs_weight_path), - dtype=torch.float32).permute(1, 0).contiguous() # D x C + zs_weight = ( + torch.tensor(np.load(zs_weight_path), dtype=torch.float32) + .permute(1, 0) + .contiguous() + ) # D x C zs_weight = torch.cat( - [zs_weight, zs_weight.new_zeros((zs_weight_dim, 1))], - dim=1) # D x (C + 1) - + [zs_weight, zs_weight.new_zeros((zs_weight_dim, 1))], dim=1 + ) # D x (C + 1) + if self.norm_weight: zs_weight = F.normalize(zs_weight, p=2, dim=0) - - if zs_weight_path == 'rand': + + if zs_weight_path == "rand": self.zs_weight = nn.Parameter(zs_weight) else: - self.register_buffer('zs_weight', zs_weight) + self.register_buffer("zs_weight", zs_weight) assert self.zs_weight.shape[1] == num_classes + 1, self.zs_weight.shape - @classmethod def from_config(cls, cfg, input_shape): return { - 'input_shape': input_shape, - 'num_classes': cfg.MODEL.ROI_HEADS.NUM_CLASSES, - 'zs_weight_path': cfg.MODEL.ROI_BOX_HEAD.ZEROSHOT_WEIGHT_PATH, - 'zs_weight_dim': cfg.MODEL.ROI_BOX_HEAD.ZEROSHOT_WEIGHT_DIM, - 'use_bias': cfg.MODEL.ROI_BOX_HEAD.USE_BIAS, - 'norm_weight': cfg.MODEL.ROI_BOX_HEAD.NORM_WEIGHT, - 'norm_temperature': cfg.MODEL.ROI_BOX_HEAD.NORM_TEMP, + "input_shape": input_shape, + "num_classes": cfg.MODEL.ROI_HEADS.NUM_CLASSES, + "zs_weight_path": cfg.MODEL.ROI_BOX_HEAD.ZEROSHOT_WEIGHT_PATH, + "zs_weight_dim": cfg.MODEL.ROI_BOX_HEAD.ZEROSHOT_WEIGHT_DIM, + "use_bias": cfg.MODEL.ROI_BOX_HEAD.USE_BIAS, + "norm_weight": cfg.MODEL.ROI_BOX_HEAD.NORM_WEIGHT, + "norm_temperature": cfg.MODEL.ROI_BOX_HEAD.NORM_TEMP, } def forward(self, x, classifier=None): - ''' + """ Inputs: x: B x D' classifier_info: (C', C' x D) - ''' + """ x = self.linear(x) if classifier is not None: - zs_weight = classifier.permute(1, 0).contiguous() # D x C' - zs_weight = F.normalize(zs_weight, p=2, dim=0) \ - if self.norm_weight else zs_weight + zs_weight = classifier.permute(1, 0).contiguous() # D x C' + zs_weight = F.normalize(zs_weight, p=2, dim=0) if self.norm_weight else zs_weight else: zs_weight = self.zs_weight if self.norm_weight: @@ -84,4 +85,4 @@ def forward(self, x, classifier=None): x = torch.mm(x, zs_weight) if self.use_bias: x = x + self.cls_bias - return x \ No newline at end of file + return x diff --git a/dimos/models/Detic/detic/modeling/text/text_encoder.py b/dimos/models/Detic/detic/modeling/text/text_encoder.py index 3ec5090c29..7c9b15bdf5 100644 --- a/dimos/models/Detic/detic/modeling/text/text_encoder.py +++ b/dimos/models/Detic/detic/modeling/text/text_encoder.py @@ -2,18 +2,18 @@ # Modified by Xingyi Zhou # The original code is under MIT license # Copyright (c) Facebook, Inc. and its affiliates. -from typing import Union, List from collections import OrderedDict -import torch -from torch import nn -import torch +from typing import List, Union from clip.simple_tokenizer import SimpleTokenizer as _Tokenizer +import torch +from torch import nn __all__ = ["tokenize"] count = 0 + class LayerNorm(nn.LayerNorm): """Subclass torch's LayerNorm to handle fp16.""" @@ -29,21 +29,29 @@ def forward(self, x: torch.Tensor): class ResidualAttentionBlock(nn.Module): - def __init__(self, d_model: int, n_head: int, attn_mask: torch.Tensor = None): + def __init__(self, d_model: int, n_head: int, attn_mask: torch.Tensor = None) -> None: super().__init__() self.attn = nn.MultiheadAttention(d_model, n_head) self.ln_1 = LayerNorm(d_model) - self.mlp = nn.Sequential(OrderedDict([ - ("c_fc", nn.Linear(d_model, d_model * 4)), - ("gelu", QuickGELU()), - ("c_proj", nn.Linear(d_model * 4, d_model)) - ])) + self.mlp = nn.Sequential( + OrderedDict( + [ + ("c_fc", nn.Linear(d_model, d_model * 4)), + ("gelu", QuickGELU()), + ("c_proj", nn.Linear(d_model * 4, d_model)), + ] + ) + ) self.ln_2 = LayerNorm(d_model) self.attn_mask = attn_mask def attention(self, x: torch.Tensor): - self.attn_mask = self.attn_mask.to(dtype=x.dtype, device=x.device) if self.attn_mask is not None else None + self.attn_mask = ( + self.attn_mask.to(dtype=x.dtype, device=x.device) + if self.attn_mask is not None + else None + ) return self.attn(x, x, x, need_weights=False, attn_mask=self.attn_mask)[0] def forward(self, x: torch.Tensor): @@ -53,29 +61,31 @@ def forward(self, x: torch.Tensor): class Transformer(nn.Module): - def __init__(self, width: int, layers: int, heads: int, attn_mask: torch.Tensor = None): + def __init__(self, width: int, layers: int, heads: int, attn_mask: torch.Tensor = None) -> None: super().__init__() self.width = width self.layers = layers self.resblocks = nn.Sequential( - *[ResidualAttentionBlock(width, heads, attn_mask) \ - for _ in range(layers)]) + *[ResidualAttentionBlock(width, heads, attn_mask) for _ in range(layers)] + ) def forward(self, x: torch.Tensor): return self.resblocks(x) + class CLIPTEXT(nn.Module): - def __init__(self, - embed_dim=512, - # text - context_length=77, - vocab_size=49408, - transformer_width=512, - transformer_heads=8, - transformer_layers=12 - ): + def __init__( + self, + embed_dim: int=512, + # text + context_length: int=77, + vocab_size: int=49408, + transformer_width: int=512, + transformer_heads: int=8, + transformer_layers: int=12, + ) -> None: super().__init__() - + self._tokenizer = _Tokenizer() self.context_length = context_length @@ -83,12 +93,14 @@ def __init__(self, width=transformer_width, layers=transformer_layers, heads=transformer_heads, - attn_mask=self.build_attention_mask() + attn_mask=self.build_attention_mask(), ) self.vocab_size = vocab_size self.token_embedding = nn.Embedding(vocab_size, transformer_width) - self.positional_embedding = nn.Parameter(torch.empty(self.context_length, transformer_width)) + self.positional_embedding = nn.Parameter( + torch.empty(self.context_length, transformer_width) + ) self.ln_final = LayerNorm(transformer_width) self.text_projection = nn.Parameter(torch.empty(transformer_width, embed_dim)) @@ -96,12 +108,12 @@ def __init__(self, self.initialize_parameters() - def initialize_parameters(self): + def initialize_parameters(self) -> None: nn.init.normal_(self.token_embedding.weight, std=0.02) nn.init.normal_(self.positional_embedding, std=0.01) - proj_std = (self.transformer.width ** -0.5) * ((2 * self.transformer.layers) ** -0.5) - attn_std = self.transformer.width ** -0.5 + proj_std = (self.transformer.width**-0.5) * ((2 * self.transformer.layers) ** -0.5) + attn_std = self.transformer.width**-0.5 fc_std = (2 * self.transformer.width) ** -0.5 for block in self.transformer.resblocks: nn.init.normal_(block.attn.in_proj_weight, std=attn_std) @@ -110,7 +122,7 @@ def initialize_parameters(self): nn.init.normal_(block.mlp.c_proj.weight, std=proj_std) if self.text_projection is not None: - nn.init.normal_(self.text_projection, std=self.transformer.width ** -0.5) + nn.init.normal_(self.text_projection, std=self.transformer.width**-0.5) def build_attention_mask(self): # lazily create causal attention mask, with full attention between the vision tokens @@ -128,30 +140,26 @@ def device(self): def dtype(self): return self.text_projection.dtype - def tokenize(self, - texts: Union[str, List[str]], \ - context_length: int = 77) -> torch.LongTensor: - """ - """ + def tokenize(self, texts: Union[str, list[str]], context_length: int = 77) -> torch.LongTensor: + """ """ if isinstance(texts, str): texts = [texts] sot_token = self._tokenizer.encoder["<|startoftext|>"] eot_token = self._tokenizer.encoder["<|endoftext|>"] - all_tokens = [[sot_token] + self._tokenizer.encode(text) + [eot_token] for text in texts] + all_tokens = [[sot_token, *self._tokenizer.encode(text), eot_token] for text in texts] result = torch.zeros(len(all_tokens), context_length, dtype=torch.long) for i, tokens in enumerate(all_tokens): if len(tokens) > context_length: - st = torch.randint( - len(tokens) - context_length + 1, (1,))[0].item() - tokens = tokens[st: st + context_length] + st = torch.randint(len(tokens) - context_length + 1, (1,))[0].item() + tokens = tokens[st : st + context_length] # raise RuntimeError(f"Input {texts[i]} is too long for context length {context_length}") - result[i, :len(tokens)] = torch.tensor(tokens) + result[i, : len(tokens)] = torch.tensor(tokens) return result - def encode_text(self, text): + def encode_text(self, text: str): x = self.token_embedding(text).type(self.dtype) # [batch_size, n_ctx, d_model] x = x + self.positional_embedding.type(self.dtype) x = x.permute(1, 0, 2) # NLD -> LND @@ -163,27 +171,28 @@ def encode_text(self, text): return x def forward(self, captions): - ''' + """ captions: list of strings - ''' - text = self.tokenize(captions).to(self.device) # B x L x D - features = self.encode_text(text) # B x D + """ + text = self.tokenize(captions).to(self.device) # B x L x D + features = self.encode_text(text) # B x D return features -def build_text_encoder(pretrain=True): +def build_text_encoder(pretrain: bool=True): text_encoder = CLIPTEXT() if pretrain: import clip - pretrained_model, _ = clip.load("ViT-B/32", device='cpu') + + pretrained_model, _ = clip.load("ViT-B/32", device="cpu") state_dict = pretrained_model.state_dict() - to_delete_keys = ["logit_scale", "input_resolution", \ - "context_length", "vocab_size"] + \ - [k for k in state_dict.keys() if k.startswith('visual.')] + to_delete_keys = ["logit_scale", "input_resolution", "context_length", "vocab_size"] + [ + k for k in state_dict.keys() if k.startswith("visual.") + ] for k in to_delete_keys: if k in state_dict: del state_dict[k] - print('Loading pretrained CLIP') + print("Loading pretrained CLIP") text_encoder.load_state_dict(state_dict) # import pdb; pdb.set_trace() - return text_encoder \ No newline at end of file + return text_encoder diff --git a/dimos/models/Detic/detic/modeling/utils.py b/dimos/models/Detic/detic/modeling/utils.py index 297fb469a0..f24a0699a1 100644 --- a/dimos/models/Detic/detic/modeling/utils.py +++ b/dimos/models/Detic/detic/modeling/utils.py @@ -1,49 +1,46 @@ # Copyright (c) Facebook, Inc. and its affiliates. -import torch import json + import numpy as np +import torch from torch.nn import functional as F -def load_class_freq( - path='datasets/metadata/lvis_v1_train_cat_info.json', freq_weight=1.0): - cat_info = json.load(open(path, 'r')) - cat_info = torch.tensor( - [c['image_count'] for c in sorted(cat_info, key=lambda x: x['id'])]) + +def load_class_freq(path: str="datasets/metadata/lvis_v1_train_cat_info.json", freq_weight: float=1.0): + cat_info = json.load(open(path)) + cat_info = torch.tensor([c["image_count"] for c in sorted(cat_info, key=lambda x: x["id"])]) freq_weight = cat_info.float() ** freq_weight return freq_weight -def get_fed_loss_inds(gt_classes, num_sample_cats, C, weight=None): - appeared = torch.unique(gt_classes) # C' +def get_fed_loss_inds(gt_classes, num_sample_cats: int, C, weight=None): + appeared = torch.unique(gt_classes) # C' prob = appeared.new_ones(C + 1).float() prob[-1] = 0 if len(appeared) < num_sample_cats: if weight is not None: prob[:C] = weight.float().clone() prob[appeared] = 0 - more_appeared = torch.multinomial( - prob, num_sample_cats - len(appeared), - replacement=False) + more_appeared = torch.multinomial(prob, num_sample_cats - len(appeared), replacement=False) appeared = torch.cat([appeared, more_appeared]) return appeared - -def reset_cls_test(model, cls_path, num_classes): +def reset_cls_test(model, cls_path, num_classes: int) -> None: model.roi_heads.num_classes = num_classes if type(cls_path) == str: - print('Resetting zs_weight', cls_path) - zs_weight = torch.tensor( - np.load(cls_path), - dtype=torch.float32).permute(1, 0).contiguous() # D x C + print("Resetting zs_weight", cls_path) + zs_weight = ( + torch.tensor(np.load(cls_path), dtype=torch.float32).permute(1, 0).contiguous() + ) # D x C else: zs_weight = cls_path zs_weight = torch.cat( - [zs_weight, zs_weight.new_zeros((zs_weight.shape[0], 1))], - dim=1) # D x (C + 1) + [zs_weight, zs_weight.new_zeros((zs_weight.shape[0], 1))], dim=1 + ) # D x (C + 1) if model.roi_heads.box_predictor[0].cls_score.norm_weight: zs_weight = F.normalize(zs_weight, p=2, dim=0) zs_weight = zs_weight.to(model.device) for k in range(len(model.roi_heads.box_predictor)): del model.roi_heads.box_predictor[k].cls_score.zs_weight - model.roi_heads.box_predictor[k].cls_score.zs_weight = zs_weight \ No newline at end of file + model.roi_heads.box_predictor[k].cls_score.zs_weight = zs_weight diff --git a/dimos/models/Detic/detic/predictor.py b/dimos/models/Detic/detic/predictor.py index 318205acb9..a85941e25a 100644 --- a/dimos/models/Detic/detic/predictor.py +++ b/dimos/models/Detic/detic/predictor.py @@ -1,44 +1,46 @@ # Copyright (c) Facebook, Inc. and its affiliates. import atexit import bisect -import multiprocessing as mp from collections import deque -import cv2 -import torch +import multiprocessing as mp +import cv2 from detectron2.data import MetadataCatalog from detectron2.engine.defaults import DefaultPredictor from detectron2.utils.video_visualizer import VideoVisualizer from detectron2.utils.visualizer import ColorMode, Visualizer +import torch from .modeling.utils import reset_cls_test -def get_clip_embeddings(vocabulary, prompt='a '): +def get_clip_embeddings(vocabulary, prompt: str="a "): from detic.modeling.text.text_encoder import build_text_encoder + text_encoder = build_text_encoder(pretrain=True) text_encoder.eval() texts = [prompt + x for x in vocabulary] emb = text_encoder(texts).detach().permute(1, 0).contiguous().cpu() return emb + BUILDIN_CLASSIFIER = { - 'lvis': 'datasets/metadata/lvis_v1_clip_a+cname.npy', - 'objects365': 'datasets/metadata/o365_clip_a+cnamefix.npy', - 'openimages': 'datasets/metadata/oid_clip_a+cname.npy', - 'coco': 'datasets/metadata/coco_clip_a+cname.npy', + "lvis": "datasets/metadata/lvis_v1_clip_a+cname.npy", + "objects365": "datasets/metadata/o365_clip_a+cnamefix.npy", + "openimages": "datasets/metadata/oid_clip_a+cname.npy", + "coco": "datasets/metadata/coco_clip_a+cname.npy", } BUILDIN_METADATA_PATH = { - 'lvis': 'lvis_v1_val', - 'objects365': 'objects365_v2_val', - 'openimages': 'oid_val_expanded', - 'coco': 'coco_2017_val', + "lvis": "lvis_v1_val", + "objects365": "objects365_v2_val", + "openimages": "oid_val_expanded", + "coco": "coco_2017_val", } -class VisualizationDemo(object): - def __init__(self, cfg, args, - instance_mode=ColorMode.IMAGE, parallel=False): + +class VisualizationDemo: + def __init__(self, cfg, args, instance_mode=ColorMode.IMAGE, parallel: bool=False) -> None: """ Args: cfg (CfgNode): @@ -46,13 +48,12 @@ def __init__(self, cfg, args, parallel (bool): whether to run the model in different processes from visualization. Useful since the visualization logic can be slow. """ - if args.vocabulary == 'custom': + if args.vocabulary == "custom": self.metadata = MetadataCatalog.get("__unused") - self.metadata.thing_classes = args.custom_vocabulary.split(',') + self.metadata.thing_classes = args.custom_vocabulary.split(",") classifier = get_clip_embeddings(self.metadata.thing_classes) else: - self.metadata = MetadataCatalog.get( - BUILDIN_METADATA_PATH[args.vocabulary]) + self.metadata = MetadataCatalog.get(BUILDIN_METADATA_PATH[args.vocabulary]) classifier = BUILDIN_CLASSIFIER[args.vocabulary] num_classes = len(self.metadata.thing_classes) @@ -173,13 +174,13 @@ class _StopToken: pass class _PredictWorker(mp.Process): - def __init__(self, cfg, task_queue, result_queue): + def __init__(self, cfg, task_queue, result_queue) -> None: self.cfg = cfg self.task_queue = task_queue self.result_queue = result_queue super().__init__() - def run(self): + def run(self) -> None: predictor = DefaultPredictor(self.cfg) while True: @@ -190,7 +191,7 @@ def run(self): result = predictor(data) self.result_queue.put((idx, result)) - def __init__(self, cfg, num_gpus: int = 1): + def __init__(self, cfg, num_gpus: int = 1) -> None: """ Args: cfg (CfgNode): @@ -203,7 +204,7 @@ def __init__(self, cfg, num_gpus: int = 1): for gpuid in range(max(num_gpus, 1)): cfg = cfg.clone() cfg.defrost() - cfg.MODEL.DEVICE = "cuda:{}".format(gpuid) if num_gpus > 0 else "cpu" + cfg.MODEL.DEVICE = f"cuda:{gpuid}" if num_gpus > 0 else "cpu" self.procs.append( AsyncPredictor._PredictWorker(cfg, self.task_queue, self.result_queue) ) @@ -217,7 +218,7 @@ def __init__(self, cfg, num_gpus: int = 1): p.start() atexit.register(self.shutdown) - def put(self, image): + def put(self, image) -> None: self.put_idx += 1 self.task_queue.put((self.put_idx, image)) @@ -237,14 +238,14 @@ def get(self): self.result_rank.insert(insert, idx) self.result_data.insert(insert, res) - def __len__(self): + def __len__(self) -> int: return self.put_idx - self.get_idx def __call__(self, image): self.put(image) return self.get() - def shutdown(self): + def shutdown(self) -> None: for _ in self.procs: self.task_queue.put(AsyncPredictor._StopToken()) diff --git a/dimos/models/Detic/lazy_train_net.py b/dimos/models/Detic/lazy_train_net.py index 2734befb0d..3525a1f63a 100644 --- a/dimos/models/Detic/lazy_train_net.py +++ b/dimos/models/Detic/lazy_train_net.py @@ -9,6 +9,7 @@ To add more complicated training logic, you can easily add other configs in the config file and implement a new train_net.py to handle them. """ + import logging import sys @@ -26,10 +27,12 @@ from detectron2.engine.defaults import create_ddp_model from detectron2.evaluation import inference_on_dataset, print_csv_format from detectron2.utils import comm -sys.path.insert(0, 'third_party/CenterNet2/') -sys.path.insert(0, 'third_party/Deformable-DETR') + +sys.path.insert(0, "third_party/CenterNet2/") +sys.path.insert(0, "third_party/Deformable-DETR") logger = logging.getLogger("detectron2") + def do_test(cfg, model): if "evaluator" in cfg.dataloader: ret = inference_on_dataset( @@ -39,7 +42,7 @@ def do_test(cfg, model): return ret -def do_train(args, cfg): +def do_train(args, cfg) -> None: """ Args: cfg: an object with the following attributes: @@ -60,7 +63,7 @@ def do_train(args, cfg): """ model = instantiate(cfg.model) logger = logging.getLogger("detectron2") - logger.info("Model:\n{}".format(model)) + logger.info(f"Model:\n{model}") model.to(cfg.train.device) cfg.optimizer.params.model = model @@ -102,7 +105,7 @@ def do_train(args, cfg): trainer.train(start_iter, cfg.train.max_iter) -def main(args): +def main(args) -> None: cfg = LazyConfig.load(args.config_file) cfg = LazyConfig.apply_overrides(cfg, args.opts) default_setup(cfg, args) @@ -126,4 +129,4 @@ def main(args): machine_rank=args.machine_rank, dist_url=args.dist_url, args=(args,), - ) \ No newline at end of file + ) diff --git a/dimos/models/Detic/predict.py b/dimos/models/Detic/predict.py index a0fa53bb15..bf71d007a1 100644 --- a/dimos/models/Detic/predict.py +++ b/dimos/models/Detic/predict.py @@ -1,45 +1,47 @@ +from pathlib import Path import sys -import cv2 import tempfile -from pathlib import Path -import cog import time +import cog +import cv2 +from detectron2.config import get_cfg +from detectron2.data import MetadataCatalog + # import some common detectron2 utilities from detectron2.engine import DefaultPredictor -from detectron2.config import get_cfg from detectron2.utils.visualizer import Visualizer -from detectron2.data import MetadataCatalog # Detic libraries -sys.path.insert(0, 'third_party/CenterNet2/') +sys.path.insert(0, "third_party/CenterNet2/") from centernet.config import add_centernet_config from detic.config import add_detic_config -from detic.modeling.utils import reset_cls_test from detic.modeling.text.text_encoder import build_text_encoder +from detic.modeling.utils import reset_cls_test + class Predictor(cog.Predictor): - def setup(self): + def setup(self) -> None: cfg = get_cfg() add_centernet_config(cfg) add_detic_config(cfg) cfg.merge_from_file("configs/Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml") - cfg.MODEL.WEIGHTS = 'Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.pth' + cfg.MODEL.WEIGHTS = "Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.pth" cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = 0.5 # set threshold for this model - cfg.MODEL.ROI_BOX_HEAD.ZEROSHOT_WEIGHT_PATH = 'rand' + cfg.MODEL.ROI_BOX_HEAD.ZEROSHOT_WEIGHT_PATH = "rand" cfg.MODEL.ROI_HEADS.ONE_CLASS_PER_PROPOSAL = True self.predictor = DefaultPredictor(cfg) self.BUILDIN_CLASSIFIER = { - 'lvis': 'datasets/metadata/lvis_v1_clip_a+cname.npy', - 'objects365': 'datasets/metadata/o365_clip_a+cnamefix.npy', - 'openimages': 'datasets/metadata/oid_clip_a+cname.npy', - 'coco': 'datasets/metadata/coco_clip_a+cname.npy', + "lvis": "datasets/metadata/lvis_v1_clip_a+cname.npy", + "objects365": "datasets/metadata/o365_clip_a+cnamefix.npy", + "openimages": "datasets/metadata/oid_clip_a+cname.npy", + "coco": "datasets/metadata/coco_clip_a+cname.npy", } self.BUILDIN_METADATA_PATH = { - 'lvis': 'lvis_v1_val', - 'objects365': 'objects365_v2_val', - 'openimages': 'oid_val_expanded', - 'coco': 'coco_2017_val', + "lvis": "lvis_v1_val", + "objects365": "objects365_v2_val", + "openimages": "oid_val_expanded", + "coco": "coco_2017_val", } @cog.input( @@ -50,8 +52,8 @@ def setup(self): @cog.input( "vocabulary", type=str, - default='lvis', - options=['lvis', 'objects365', 'openimages', 'coco', 'custom'], + default="lvis", + options=["lvis", "objects365", "openimages", "coco", "custom"], help="Choose vocabulary", ) @cog.input( @@ -62,24 +64,27 @@ def setup(self): ) def predict(self, image, vocabulary, custom_vocabulary): image = cv2.imread(str(image)) - if not vocabulary == 'custom': + if not vocabulary == "custom": metadata = MetadataCatalog.get(self.BUILDIN_METADATA_PATH[vocabulary]) classifier = self.BUILDIN_CLASSIFIER[vocabulary] num_classes = len(metadata.thing_classes) reset_cls_test(self.predictor.model, classifier, num_classes) else: - assert custom_vocabulary is not None and len(custom_vocabulary.split(',')) > 0, \ + assert custom_vocabulary is not None and len(custom_vocabulary.split(",")) > 0, ( "Please provide your own vocabularies when vocabulary is set to 'custom'." + ) metadata = MetadataCatalog.get(str(time.time())) - metadata.thing_classes = custom_vocabulary.split(',') + metadata.thing_classes = custom_vocabulary.split(",") classifier = get_clip_embeddings(metadata.thing_classes) num_classes = len(metadata.thing_classes) reset_cls_test(self.predictor.model, classifier, num_classes) # Reset visualization threshold output_score_threshold = 0.3 for cascade_stages in range(len(self.predictor.model.roi_heads.box_predictor)): - self.predictor.model.roi_heads.box_predictor[cascade_stages].test_score_thresh = output_score_threshold + self.predictor.model.roi_heads.box_predictor[ + cascade_stages + ].test_score_thresh = output_score_threshold outputs = self.predictor(image) v = Visualizer(image[:, :, ::-1], metadata) @@ -89,7 +94,7 @@ def predict(self, image, vocabulary, custom_vocabulary): return out_path -def get_clip_embeddings(vocabulary, prompt='a '): +def get_clip_embeddings(vocabulary, prompt: str="a "): text_encoder = build_text_encoder(pretrain=True) text_encoder.eval() texts = [prompt + x for x in vocabulary] diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/__init__.py b/dimos/models/Detic/third_party/CenterNet2/centernet/__init__.py index e17db317d9..5e2e7afac6 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/__init__.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/__init__.py @@ -1,14 +1,12 @@ -from .modeling.meta_arch.centernet_detector import CenterNetDetector -from .modeling.dense_heads.centernet import CenterNet -from .modeling.roi_heads.custom_roi_heads import CustomROIHeads, CustomCascadeROIHeads - -from .modeling.backbone.fpn_p5 import build_p67_resnet_fpn_backbone -from .modeling.backbone.dla import build_dla_backbone -from .modeling.backbone.dlafpn import build_dla_fpn3_backbone +from .data.datasets import nuimages +from .data.datasets.coco import _PREDEFINED_SPLITS_COCO +from .data.datasets.objects365 import categories_v1 from .modeling.backbone.bifpn import build_resnet_bifpn_backbone from .modeling.backbone.bifpn_fcos import build_fcos_resnet_bifpn_backbone +from .modeling.backbone.dla import build_dla_backbone +from .modeling.backbone.dlafpn import build_dla_fpn3_backbone +from .modeling.backbone.fpn_p5 import build_p67_resnet_fpn_backbone from .modeling.backbone.res2net import build_p67_res2net_fpn_backbone - -from .data.datasets.objects365 import categories_v1 -from .data.datasets.coco import _PREDEFINED_SPLITS_COCO -from .data.datasets import nuimages +from .modeling.dense_heads.centernet import CenterNet +from .modeling.meta_arch.centernet_detector import CenterNetDetector +from .modeling.roi_heads.custom_roi_heads import CustomCascadeROIHeads, CustomROIHeads diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/config.py b/dimos/models/Detic/third_party/CenterNet2/centernet/config.py index 82c44fa640..255eb36340 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/config.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/config.py @@ -1,6 +1,7 @@ from detectron2.config import CfgNode as CN -def add_centernet_config(cfg): + +def add_centernet_config(cfg) -> None: _C = cfg _C.MODEL.CENTERNET = CN() @@ -21,21 +22,21 @@ def add_centernet_config(cfg): _C.MODEL.CENTERNET.NUM_CLS_CONVS = 4 _C.MODEL.CENTERNET.NUM_BOX_CONVS = 4 _C.MODEL.CENTERNET.NUM_SHARE_CONVS = 0 - _C.MODEL.CENTERNET.LOC_LOSS_TYPE = 'giou' + _C.MODEL.CENTERNET.LOC_LOSS_TYPE = "giou" _C.MODEL.CENTERNET.SIGMOID_CLAMP = 1e-4 _C.MODEL.CENTERNET.HM_MIN_OVERLAP = 0.8 _C.MODEL.CENTERNET.MIN_RADIUS = 4 _C.MODEL.CENTERNET.SOI = [[0, 80], [64, 160], [128, 320], [256, 640], [512, 10000000]] - _C.MODEL.CENTERNET.POS_WEIGHT = 1. - _C.MODEL.CENTERNET.NEG_WEIGHT = 1. - _C.MODEL.CENTERNET.REG_WEIGHT = 2. + _C.MODEL.CENTERNET.POS_WEIGHT = 1.0 + _C.MODEL.CENTERNET.NEG_WEIGHT = 1.0 + _C.MODEL.CENTERNET.REG_WEIGHT = 2.0 _C.MODEL.CENTERNET.HM_FOCAL_BETA = 4 _C.MODEL.CENTERNET.HM_FOCAL_ALPHA = 0.25 _C.MODEL.CENTERNET.LOSS_GAMMA = 2.0 _C.MODEL.CENTERNET.WITH_AGN_HM = False _C.MODEL.CENTERNET.ONLY_PROPOSAL = False _C.MODEL.CENTERNET.AS_PROPOSAL = False - _C.MODEL.CENTERNET.IGNORE_HIGH_FP = -1. + _C.MODEL.CENTERNET.IGNORE_HIGH_FP = -1.0 _C.MODEL.CENTERNET.MORE_POS = False _C.MODEL.CENTERNET.MORE_POS_THRESH = 0.2 _C.MODEL.CENTERNET.MORE_POS_TOPK = 9 @@ -46,8 +47,7 @@ def add_centernet_config(cfg): _C.MODEL.ROI_BOX_HEAD.USE_SIGMOID_CE = False _C.MODEL.ROI_BOX_HEAD.PRIOR_PROB = 0.01 _C.MODEL.ROI_BOX_HEAD.USE_EQL_LOSS = False - _C.MODEL.ROI_BOX_HEAD.CAT_FREQ_PATH = \ - 'datasets/lvis/lvis_v1_train_cat_info.json' + _C.MODEL.ROI_BOX_HEAD.CAT_FREQ_PATH = "datasets/lvis/lvis_v1_train_cat_info.json" _C.MODEL.ROI_BOX_HEAD.EQL_FREQ_CAT = 200 _C.MODEL.ROI_BOX_HEAD.USE_FED_LOSS = False _C.MODEL.ROI_BOX_HEAD.FED_LOSS_NUM_CAT = 50 @@ -57,30 +57,30 @@ def add_centernet_config(cfg): _C.MODEL.BIFPN = CN() _C.MODEL.BIFPN.NUM_LEVELS = 5 _C.MODEL.BIFPN.NUM_BIFPN = 6 - _C.MODEL.BIFPN.NORM = 'GN' + _C.MODEL.BIFPN.NORM = "GN" _C.MODEL.BIFPN.OUT_CHANNELS = 160 _C.MODEL.BIFPN.SEPARABLE_CONV = False _C.MODEL.DLA = CN() - _C.MODEL.DLA.OUT_FEATURES = ['dla2'] + _C.MODEL.DLA.OUT_FEATURES = ["dla2"] _C.MODEL.DLA.USE_DLA_UP = True _C.MODEL.DLA.NUM_LAYERS = 34 _C.MODEL.DLA.MS_OUTPUT = False - _C.MODEL.DLA.NORM = 'BN' - _C.MODEL.DLA.DLAUP_IN_FEATURES = ['dla3', 'dla4', 'dla5'] - _C.MODEL.DLA.DLAUP_NODE = 'conv' + _C.MODEL.DLA.NORM = "BN" + _C.MODEL.DLA.DLAUP_IN_FEATURES = ["dla3", "dla4", "dla5"] + _C.MODEL.DLA.DLAUP_NODE = "conv" _C.SOLVER.RESET_ITER = False _C.SOLVER.TRAIN_ITER = -1 - _C.INPUT.CUSTOM_AUG = '' + _C.INPUT.CUSTOM_AUG = "" _C.INPUT.TRAIN_SIZE = 640 _C.INPUT.TEST_SIZE = 640 - _C.INPUT.SCALE_RANGE = (0.1, 2.) + _C.INPUT.SCALE_RANGE = (0.1, 2.0) # 'default' for fixed short/ long edge, 'square' for max size=INPUT.SIZE - _C.INPUT.TEST_INPUT_TYPE = 'default' + _C.INPUT.TEST_INPUT_TYPE = "default" _C.INPUT.NOT_CLAMP_BOX = False - + _C.DEBUG = False _C.SAVE_DEBUG = False _C.SAVE_PTH = False diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/data/custom_build_augmentation.py b/dimos/models/Detic/third_party/CenterNet2/centernet/data/custom_build_augmentation.py index 7d91f21edb..1bcb7cee66 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/data/custom_build_augmentation.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/data/custom_build_augmentation.py @@ -1,25 +1,9 @@ -import logging -import numpy as np -import pycocotools.mask as mask_util -import torch -from fvcore.common.file_io import PathManager -from PIL import Image - -from detectron2.structures import ( - BitMasks, - Boxes, - BoxMode, - Instances, - Keypoints, - PolygonMasks, - RotatedBoxes, - polygons_to_bitmask, -) - from detectron2.data import transforms as T + from .transforms.custom_augmentation_impl import EfficientDetResizeCrop -def build_custom_augmentation(cfg, is_train): + +def build_custom_augmentation(cfg, is_train: bool): """ Create a list of default :class:`Augmentation` from config. Now it includes resizing and flipping. @@ -27,7 +11,7 @@ def build_custom_augmentation(cfg, is_train): Returns: list[Augmentation] """ - if cfg.INPUT.CUSTOM_AUG == 'ResizeShortestEdge': + if cfg.INPUT.CUSTOM_AUG == "ResizeShortestEdge": if is_train: min_size = cfg.INPUT.MIN_SIZE_TRAIN max_size = cfg.INPUT.MAX_SIZE_TRAIN @@ -37,7 +21,7 @@ def build_custom_augmentation(cfg, is_train): max_size = cfg.INPUT.MAX_SIZE_TEST sample_style = "choice" augmentation = [T.ResizeShortestEdge(min_size, max_size, sample_style)] - elif cfg.INPUT.CUSTOM_AUG == 'EfficientDetResizeCrop': + elif cfg.INPUT.CUSTOM_AUG == "EfficientDetResizeCrop": if is_train: scale = cfg.INPUT.SCALE_RANGE size = cfg.INPUT.TRAIN_SIZE @@ -56,4 +40,4 @@ def build_custom_augmentation(cfg, is_train): build_custom_transform_gen = build_custom_augmentation """ Alias for backward-compatibility. -""" \ No newline at end of file +""" diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/data/custom_dataset_dataloader.py b/dimos/models/Detic/third_party/CenterNet2/centernet/data/custom_dataset_dataloader.py index 4e9844c99b..a7cfdd523d 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/data/custom_dataset_dataloader.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/data/custom_dataset_dataloader.py @@ -1,32 +1,28 @@ # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -import copy +from collections import defaultdict +import itertools import logging -import numpy as np -import operator -import torch -import torch.utils.data -import json -from detectron2.utils.comm import get_world_size - -from detectron2.data import samplers -from torch.utils.data.sampler import BatchSampler, Sampler +from typing import Iterator, Sequence, Optional + +from detectron2.data.build import ( + build_batch_data_loader, + check_metadata_consistency, + filter_images_with_few_keypoints, + filter_images_with_only_crowd_annotations, + get_detection_dataset_dicts, + print_instances_class_histogram, +) +from detectron2.data.catalog import DatasetCatalog, MetadataCatalog from detectron2.data.common import DatasetFromList, MapDataset -from detectron2.data.dataset_mapper import DatasetMapper -from detectron2.data.build import get_detection_dataset_dicts, build_batch_data_loader -from detectron2.data.samplers import TrainingSampler, RepeatFactorTrainingSampler -from detectron2.data.build import worker_init_reset_seed, print_instances_class_histogram -from detectron2.data.build import filter_images_with_only_crowd_annotations -from detectron2.data.build import filter_images_with_few_keypoints -from detectron2.data.build import check_metadata_consistency -from detectron2.data.catalog import MetadataCatalog, DatasetCatalog +from detectron2.data.samplers import RepeatFactorTrainingSampler, TrainingSampler from detectron2.utils import comm -import itertools -import math -from collections import defaultdict -from typing import Optional +import torch +import torch.utils.data +from torch.utils.data.sampler import Sampler # from .custom_build_augmentation import build_custom_augmentation + def build_custom_train_loader(cfg, mapper=None): """ Modified from detectron2.data.build.build_custom_train_loader, but supports @@ -44,8 +40,8 @@ def build_custom_train_loader(cfg, mapper=None): ) sizes = [0 for _ in range(len(cfg.DATASETS.TRAIN))] for d in dataset_dicts: - sizes[d['dataset_source']] += 1 - print('dataset sizes', sizes) + sizes[d["dataset_source"]] += 1 + print("dataset sizes", sizes) else: dataset_dicts = get_detection_dataset_dicts( cfg.DATASETS.TRAIN, @@ -64,7 +60,7 @@ def build_custom_train_loader(cfg, mapper=None): sampler_name = cfg.DATALOADER.SAMPLER_TRAIN logger = logging.getLogger(__name__) - logger.info("Using training sampler {}".format(sampler_name)) + logger.info(f"Using training sampler {sampler_name}") # TODO avoid if-else? if sampler_name == "TrainingSampler": sampler = TrainingSampler(len(dataset)) @@ -79,7 +75,7 @@ def build_custom_train_loader(cfg, mapper=None): elif sampler_name == "ClassAwareSampler": sampler = ClassAwareSampler(dataset_dicts) else: - raise ValueError("Unknown training sampler: {}".format(sampler_name)) + raise ValueError(f"Unknown training sampler: {sampler_name}") return build_batch_data_loader( dataset, @@ -91,7 +87,7 @@ def build_custom_train_loader(cfg, mapper=None): class ClassAwareSampler(Sampler): - def __init__(self, dataset_dicts, seed: Optional[int] = None): + def __init__(self, dataset_dicts, seed: int | None = None) -> None: """ Args: size (int): the total number of data of the underlying dataset to sample from @@ -104,29 +100,23 @@ def __init__(self, dataset_dicts, seed: Optional[int] = None): if seed is None: seed = comm.shared_random_seed() self._seed = int(seed) - + self._rank = comm.get_rank() self._world_size = comm.get_world_size() self.weights = self._get_class_balance_factor(dataset_dicts) - - def __iter__(self): + def __iter__(self) -> Iterator: start = self._rank - yield from itertools.islice( - self._infinite_indices(), start, None, self._world_size) - + yield from itertools.islice(self._infinite_indices(), start, None, self._world_size) def _infinite_indices(self): g = torch.Generator() g.manual_seed(self._seed) while True: - ids = torch.multinomial( - self.weights, self._size, generator=g, - replacement=True) + ids = torch.multinomial(self.weights, self._size, generator=g, replacement=True) yield from ids - - def _get_class_balance_factor(self, dataset_dicts, l=1.): + def _get_class_balance_factor(self, dataset_dicts, l: float=1.0): # 1. For each category c, compute the fraction of images that contain it: f(c) ret = [] category_freq = defaultdict(int) @@ -134,26 +124,24 @@ def _get_class_balance_factor(self, dataset_dicts, l=1.): cat_ids = {ann["category_id"] for ann in dataset_dict["annotations"]} for cat_id in cat_ids: category_freq[cat_id] += 1 - for i, dataset_dict in enumerate(dataset_dicts): + for _i, dataset_dict in enumerate(dataset_dicts): cat_ids = {ann["category_id"] for ann in dataset_dict["annotations"]} - ret.append(sum( - [1. / (category_freq[cat_id] ** l) for cat_id in cat_ids])) + ret.append(sum([1.0 / (category_freq[cat_id] ** l) for cat_id in cat_ids])) return torch.tensor(ret).float() def get_detection_dataset_dicts_with_source( - dataset_names, filter_empty=True, min_keypoints=0, proposal_files=None + dataset_names: Sequence[str], filter_empty: bool=True, min_keypoints: int=0, proposal_files=None ): assert len(dataset_names) dataset_dicts = [DatasetCatalog.get(dataset_name) for dataset_name in dataset_names] - for dataset_name, dicts in zip(dataset_names, dataset_dicts): - assert len(dicts), "Dataset '{}' is empty!".format(dataset_name) - - for source_id, (dataset_name, dicts) in \ - enumerate(zip(dataset_names, dataset_dicts)): - assert len(dicts), "Dataset '{}' is empty!".format(dataset_name) + for dataset_name, dicts in zip(dataset_names, dataset_dicts, strict=False): + assert len(dicts), f"Dataset '{dataset_name}' is empty!" + + for source_id, (dataset_name, dicts) in enumerate(zip(dataset_names, dataset_dicts, strict=False)): + assert len(dicts), f"Dataset '{dataset_name}' is empty!" for d in dicts: - d['dataset_source'] = source_id + d["dataset_source"] = source_id if "annotations" in dicts[0]: try: @@ -175,8 +163,9 @@ def get_detection_dataset_dicts_with_source( return dataset_dicts + class MultiDatasetSampler(Sampler): - def __init__(self, cfg, sizes, dataset_dicts, seed: Optional[int] = None): + def __init__(self, cfg, sizes: Sequence[int], dataset_dicts, seed: int | None = None) -> None: """ Args: size (int): the total number of data of the underlying dataset to sample from @@ -187,43 +176,42 @@ def __init__(self, cfg, sizes, dataset_dicts, seed: Optional[int] = None): self.sizes = sizes dataset_ratio = cfg.DATALOADER.DATASET_RATIO self._batch_size = cfg.SOLVER.IMS_PER_BATCH - assert len(dataset_ratio) == len(sizes), \ - 'length of dataset ratio {} should be equal to number if dataset {}'.format( - len(dataset_ratio), len(sizes) - ) + assert len(dataset_ratio) == len(sizes), ( + f"length of dataset ratio {len(dataset_ratio)} should be equal to number if dataset {len(sizes)}" + ) if seed is None: seed = comm.shared_random_seed() self._seed = int(seed) self._rank = comm.get_rank() self._world_size = comm.get_world_size() - + self._ims_per_gpu = self._batch_size // self._world_size - self.dataset_ids = torch.tensor( - [d['dataset_source'] for d in dataset_dicts], dtype=torch.long) + self.dataset_ids = torch.tensor( + [d["dataset_source"] for d in dataset_dicts], dtype=torch.long + ) - dataset_weight = [torch.ones(s) * max(sizes) / s * r / sum(dataset_ratio) \ - for i, (r, s) in enumerate(zip(dataset_ratio, sizes))] + dataset_weight = [ + torch.ones(s) * max(sizes) / s * r / sum(dataset_ratio) + for i, (r, s) in enumerate(zip(dataset_ratio, sizes, strict=False)) + ] dataset_weight = torch.cat(dataset_weight) self.weights = dataset_weight self.sample_epoch_size = len(self.weights) - def __iter__(self): + def __iter__(self) -> Iterator: start = self._rank - yield from itertools.islice( - self._infinite_indices(), start, None, self._world_size) - + yield from itertools.islice(self._infinite_indices(), start, None, self._world_size) def _infinite_indices(self): g = torch.Generator() g.manual_seed(self._seed) while True: ids = torch.multinomial( - self.weights, self.sample_epoch_size, generator=g, - replacement=True) - nums = [(self.dataset_ids[ids] == i).sum().int().item() \ - for i in range(len(self.sizes))] - print('_rank, len, nums', self._rank, len(ids), nums, flush=True) - # print('_rank, len, nums, self.dataset_ids[ids[:10]], ', - # self._rank, len(ids), nums, self.dataset_ids[ids[:10]], + self.weights, self.sample_epoch_size, generator=g, replacement=True + ) + nums = [(self.dataset_ids[ids] == i).sum().int().item() for i in range(len(self.sizes))] + print("_rank, len, nums", self._rank, len(ids), nums, flush=True) + # print('_rank, len, nums, self.dataset_ids[ids[:10]], ', + # self._rank, len(ids), nums, self.dataset_ids[ids[:10]], # flush=True) - yield from ids \ No newline at end of file + yield from ids diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/data/datasets/coco.py b/dimos/models/Detic/third_party/CenterNet2/centernet/data/datasets/coco.py index f8496aacf2..33ff5a6980 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/data/datasets/coco.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/data/datasets/coco.py @@ -1,21 +1,22 @@ import os -from detectron2.data.datasets.register_coco import register_coco_instances -from detectron2.data.datasets.coco import load_coco_json -from detectron2.data.datasets.builtin_meta import _get_builtin_metadata from detectron2.data import DatasetCatalog, MetadataCatalog +from detectron2.data.datasets.builtin_meta import _get_builtin_metadata +from detectron2.data.datasets.coco import load_coco_json +from detectron2.data.datasets.register_coco import register_coco_instances -def register_distill_coco_instances(name, metadata, json_file, image_root): +def register_distill_coco_instances(name: str, metadata, json_file, image_root) -> None: """ add extra_annotation_keys """ assert isinstance(name, str), name - assert isinstance(json_file, (str, os.PathLike)), json_file - assert isinstance(image_root, (str, os.PathLike)), image_root + assert isinstance(json_file, str | os.PathLike), json_file + assert isinstance(image_root, str | os.PathLike), image_root # 1. register a function which returns dicts - DatasetCatalog.register(name, lambda: load_coco_json( - json_file, image_root, name, extra_annotation_keys=['score'])) + DatasetCatalog.register( + name, lambda: load_coco_json(json_file, image_root, name, extra_annotation_keys=["score"]) + ) # 2. Optionally, add metadata about this dataset, # since they might be useful in evaluation, visualization or logging @@ -31,19 +32,22 @@ def register_distill_coco_instances(name, metadata, json_file, image_root): for key, (image_root, json_file) in _PREDEFINED_SPLITS_COCO.items(): register_coco_instances( key, - _get_builtin_metadata('coco'), + _get_builtin_metadata("coco"), os.path.join("datasets", json_file) if "://" not in json_file else json_file, os.path.join("datasets", image_root), ) _PREDEFINED_SPLITS_DISTILL_COCO = { - "coco_un_yolov4_55_0.5": ("coco/unlabeled2017", "coco/annotations/yolov4_cocounlabeled_55_ann0.5.json"), + "coco_un_yolov4_55_0.5": ( + "coco/unlabeled2017", + "coco/annotations/yolov4_cocounlabeled_55_ann0.5.json", + ), } for key, (image_root, json_file) in _PREDEFINED_SPLITS_DISTILL_COCO.items(): register_distill_coco_instances( key, - _get_builtin_metadata('coco'), + _get_builtin_metadata("coco"), os.path.join("datasets", json_file) if "://" not in json_file else json_file, os.path.join("datasets", image_root), - ) \ No newline at end of file + ) diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/data/datasets/nuimages.py b/dimos/models/Detic/third_party/CenterNet2/centernet/data/datasets/nuimages.py index 52736e331c..fdcd40242f 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/data/datasets/nuimages.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/data/datasets/nuimages.py @@ -1,26 +1,30 @@ -from detectron2.data.datasets.register_coco import register_coco_instances import os +from detectron2.data.datasets.register_coco import register_coco_instances + categories = [ - {'id': 0, 'name': 'car'}, - {'id': 1, 'name': 'truck'}, - {'id': 2, 'name': 'trailer'}, - {'id': 3, 'name': 'bus'}, - {'id': 4, 'name': 'construction_vehicle'}, - {'id': 5, 'name': 'bicycle'}, - {'id': 6, 'name': 'motorcycle'}, - {'id': 7, 'name': 'pedestrian'}, - {'id': 8, 'name': 'traffic_cone'}, - {'id': 9, 'name': 'barrier'}, + {"id": 0, "name": "car"}, + {"id": 1, "name": "truck"}, + {"id": 2, "name": "trailer"}, + {"id": 3, "name": "bus"}, + {"id": 4, "name": "construction_vehicle"}, + {"id": 5, "name": "bicycle"}, + {"id": 6, "name": "motorcycle"}, + {"id": 7, "name": "pedestrian"}, + {"id": 8, "name": "traffic_cone"}, + {"id": 9, "name": "barrier"}, ] + def _get_builtin_metadata(): - id_to_name = {x['id']: x['name'] for x in categories} + id_to_name = {x["id"]: x["name"] for x in categories} thing_dataset_id_to_contiguous_id = {i: i for i in range(len(categories))} thing_classes = [id_to_name[k] for k in sorted(id_to_name)] return { "thing_dataset_id_to_contiguous_id": thing_dataset_id_to_contiguous_id, - "thing_classes": thing_classes} + "thing_classes": thing_classes, + } + _PREDEFINED_SPLITS = { "nuimages_train": ("nuimages", "nuimages/annotations/nuimages_v1.0-train.json"), diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/data/datasets/objects365.py b/dimos/models/Detic/third_party/CenterNet2/centernet/data/datasets/objects365.py index 41395bdd53..e3e8383a91 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/data/datasets/objects365.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/data/datasets/objects365.py @@ -1,384 +1,388 @@ -from detectron2.data.datasets.register_coco import register_coco_instances import os +from detectron2.data.datasets.register_coco import register_coco_instances + categories_v1 = [ -{'id': 164, 'name': 'cutting/chopping board'} , -{'id': 49, 'name': 'tie'} , -{'id': 306, 'name': 'crosswalk sign'} , -{'id': 145, 'name': 'gun'} , -{'id': 14, 'name': 'street lights'} , -{'id': 223, 'name': 'bar soap'} , -{'id': 74, 'name': 'wild bird'} , -{'id': 219, 'name': 'ice cream'} , -{'id': 37, 'name': 'stool'} , -{'id': 25, 'name': 'storage box'} , -{'id': 153, 'name': 'giraffe'} , -{'id': 52, 'name': 'pen/pencil'} , -{'id': 61, 'name': 'high heels'} , -{'id': 340, 'name': 'mangosteen'} , -{'id': 22, 'name': 'bracelet'} , -{'id': 155, 'name': 'piano'} , -{'id': 162, 'name': 'vent'} , -{'id': 75, 'name': 'laptop'} , -{'id': 236, 'name': 'toaster'} , -{'id': 231, 'name': 'fire truck'} , -{'id': 42, 'name': 'basket'} , -{'id': 150, 'name': 'zebra'} , -{'id': 124, 'name': 'head phone'} , -{'id': 90, 'name': 'sheep'} , -{'id': 322, 'name': 'steak'} , -{'id': 39, 'name': 'couch'} , -{'id': 209, 'name': 'toothbrush'} , -{'id': 59, 'name': 'bicycle'} , -{'id': 336, 'name': 'red cabbage'} , -{'id': 228, 'name': 'golf ball'} , -{'id': 120, 'name': 'tomato'} , -{'id': 132, 'name': 'computer box'} , -{'id': 8, 'name': 'cup'} , -{'id': 183, 'name': 'basketball'} , -{'id': 298, 'name': 'butterfly'} , -{'id': 250, 'name': 'garlic'} , -{'id': 12, 'name': 'desk'} , -{'id': 141, 'name': 'microwave'} , -{'id': 171, 'name': 'strawberry'} , -{'id': 200, 'name': 'kettle'} , -{'id': 63, 'name': 'van'} , -{'id': 300, 'name': 'cheese'} , -{'id': 215, 'name': 'marker'} , -{'id': 100, 'name': 'blackboard/whiteboard'} , -{'id': 186, 'name': 'printer'} , -{'id': 333, 'name': 'bread/bun'} , -{'id': 243, 'name': 'penguin'} , -{'id': 364, 'name': 'iron'} , -{'id': 180, 'name': 'ladder'} , -{'id': 34, 'name': 'flag'} , -{'id': 78, 'name': 'cell phone'} , -{'id': 97, 'name': 'fan'} , -{'id': 224, 'name': 'scale'} , -{'id': 151, 'name': 'duck'} , -{'id': 319, 'name': 'flute'} , -{'id': 156, 'name': 'stop sign'} , -{'id': 290, 'name': 'rickshaw'} , -{'id': 128, 'name': 'sailboat'} , -{'id': 165, 'name': 'tennis racket'} , -{'id': 241, 'name': 'cigar'} , -{'id': 101, 'name': 'balloon'} , -{'id': 308, 'name': 'hair drier'} , -{'id': 167, 'name': 'skating and skiing shoes'} , -{'id': 237, 'name': 'helicopter'} , -{'id': 65, 'name': 'sink'} , -{'id': 129, 'name': 'tangerine'} , -{'id': 330, 'name': 'crab'} , -{'id': 320, 'name': 'measuring cup'} , -{'id': 260, 'name': 'fishing rod'} , -{'id': 346, 'name': 'saw'} , -{'id': 216, 'name': 'ship'} , -{'id': 46, 'name': 'coffee table'} , -{'id': 194, 'name': 'facial mask'} , -{'id': 281, 'name': 'stapler'} , -{'id': 118, 'name': 'refrigerator'} , -{'id': 40, 'name': 'belt'} , -{'id': 349, 'name': 'starfish'} , -{'id': 87, 'name': 'hanger'} , -{'id': 116, 'name': 'baseball glove'} , -{'id': 261, 'name': 'cherry'} , -{'id': 334, 'name': 'baozi'} , -{'id': 267, 'name': 'screwdriver'} , -{'id': 158, 'name': 'converter'} , -{'id': 335, 'name': 'lion'} , -{'id': 170, 'name': 'baseball'} , -{'id': 111, 'name': 'skis'} , -{'id': 136, 'name': 'broccoli'} , -{'id': 342, 'name': 'eraser'} , -{'id': 337, 'name': 'polar bear'} , -{'id': 139, 'name': 'shovel'} , -{'id': 193, 'name': 'extension cord'} , -{'id': 284, 'name': 'goldfish'} , -{'id': 174, 'name': 'pepper'} , -{'id': 138, 'name': 'stroller'} , -{'id': 328, 'name': 'yak'} , -{'id': 83, 'name': 'clock'} , -{'id': 235, 'name': 'tricycle'} , -{'id': 248, 'name': 'parking meter'} , -{'id': 274, 'name': 'trophy'} , -{'id': 324, 'name': 'binoculars'} , -{'id': 51, 'name': 'traffic light'} , -{'id': 314, 'name': 'donkey'} , -{'id': 45, 'name': 'barrel/bucket'} , -{'id': 292, 'name': 'pomegranate'} , -{'id': 13, 'name': 'handbag'} , -{'id': 262, 'name': 'tablet'} , -{'id': 68, 'name': 'apple'} , -{'id': 226, 'name': 'cabbage'} , -{'id': 23, 'name': 'flower'} , -{'id': 58, 'name': 'faucet'} , -{'id': 206, 'name': 'tong'} , -{'id': 291, 'name': 'trombone'} , -{'id': 160, 'name': 'carrot'} , -{'id': 172, 'name': 'bow tie'} , -{'id': 122, 'name': 'tent'} , -{'id': 163, 'name': 'cookies'} , -{'id': 115, 'name': 'remote'} , -{'id': 175, 'name': 'coffee machine'} , -{'id': 238, 'name': 'green beans'} , -{'id': 233, 'name': 'cello'} , -{'id': 28, 'name': 'wine glass'} , -{'id': 295, 'name': 'mushroom'} , -{'id': 344, 'name': 'scallop'} , -{'id': 125, 'name': 'lantern'} , -{'id': 123, 'name': 'shampoo/shower gel'} , -{'id': 285, 'name': 'meat balls'} , -{'id': 266, 'name': 'key'} , -{'id': 296, 'name': 'calculator'} , -{'id': 168, 'name': 'scissors'} , -{'id': 103, 'name': 'cymbal'} , -{'id': 6, 'name': 'bottle'} , -{'id': 264, 'name': 'nuts'} , -{'id': 234, 'name': 'notepaper'} , -{'id': 211, 'name': 'mango'} , -{'id': 287, 'name': 'toothpaste'} , -{'id': 196, 'name': 'chopsticks'} , -{'id': 140, 'name': 'baseball bat'} , -{'id': 244, 'name': 'hurdle'} , -{'id': 195, 'name': 'tennis ball'} , -{'id': 144, 'name': 'surveillance camera'} , -{'id': 271, 'name': 'volleyball'} , -{'id': 94, 'name': 'keyboard'} , -{'id': 339, 'name': 'seal'} , -{'id': 11, 'name': 'picture/frame'} , -{'id': 348, 'name': 'okra'} , -{'id': 191, 'name': 'sausage'} , -{'id': 166, 'name': 'candy'} , -{'id': 62, 'name': 'ring'} , -{'id': 311, 'name': 'dolphin'} , -{'id': 273, 'name': 'eggplant'} , -{'id': 84, 'name': 'drum'} , -{'id': 143, 'name': 'surfboard'} , -{'id': 288, 'name': 'antelope'} , -{'id': 204, 'name': 'clutch'} , -{'id': 207, 'name': 'slide'} , -{'id': 43, 'name': 'towel/napkin'} , -{'id': 352, 'name': 'durian'} , -{'id': 276, 'name': 'board eraser'} , -{'id': 315, 'name': 'electric drill'} , -{'id': 312, 'name': 'sushi'} , -{'id': 198, 'name': 'pie'} , -{'id': 106, 'name': 'pickup truck'} , -{'id': 176, 'name': 'bathtub'} , -{'id': 26, 'name': 'vase'} , -{'id': 133, 'name': 'elephant'} , -{'id': 256, 'name': 'sandwich'} , -{'id': 327, 'name': 'noodles'} , -{'id': 10, 'name': 'glasses'} , -{'id': 109, 'name': 'airplane'} , -{'id': 95, 'name': 'tripod'} , -{'id': 247, 'name': 'CD'} , -{'id': 121, 'name': 'machinery vehicle'} , -{'id': 365, 'name': 'flashlight'} , -{'id': 53, 'name': 'microphone'} , -{'id': 270, 'name': 'pliers'} , -{'id': 362, 'name': 'chainsaw'} , -{'id': 259, 'name': 'bear'} , -{'id': 197, 'name': 'electronic stove and gas stove'} , -{'id': 89, 'name': 'pot/pan'} , -{'id': 220, 'name': 'tape'} , -{'id': 338, 'name': 'lighter'} , -{'id': 177, 'name': 'snowboard'} , -{'id': 214, 'name': 'violin'} , -{'id': 217, 'name': 'chicken'} , -{'id': 2, 'name': 'sneakers'} , -{'id': 161, 'name': 'washing machine'} , -{'id': 131, 'name': 'kite'} , -{'id': 354, 'name': 'rabbit'} , -{'id': 86, 'name': 'bus'} , -{'id': 275, 'name': 'dates'} , -{'id': 282, 'name': 'camel'} , -{'id': 88, 'name': 'nightstand'} , -{'id': 179, 'name': 'grapes'} , -{'id': 229, 'name': 'pine apple'} , -{'id': 56, 'name': 'necklace'} , -{'id': 18, 'name': 'leather shoes'} , -{'id': 358, 'name': 'hoverboard'} , -{'id': 345, 'name': 'pencil case'} , -{'id': 359, 'name': 'pasta'} , -{'id': 157, 'name': 'radiator'} , -{'id': 201, 'name': 'hamburger'} , -{'id': 268, 'name': 'globe'} , -{'id': 332, 'name': 'barbell'} , -{'id': 329, 'name': 'mop'} , -{'id': 252, 'name': 'horn'} , -{'id': 350, 'name': 'eagle'} , -{'id': 169, 'name': 'folder'} , -{'id': 137, 'name': 'toilet'} , -{'id': 5, 'name': 'lamp'} , -{'id': 27, 'name': 'bench'} , -{'id': 249, 'name': 'swan'} , -{'id': 76, 'name': 'knife'} , -{'id': 341, 'name': 'comb'} , -{'id': 64, 'name': 'watch'} , -{'id': 105, 'name': 'telephone'} , -{'id': 3, 'name': 'chair'} , -{'id': 33, 'name': 'boat'} , -{'id': 107, 'name': 'orange'} , -{'id': 60, 'name': 'bread'} , -{'id': 147, 'name': 'cat'} , -{'id': 135, 'name': 'gas stove'} , -{'id': 307, 'name': 'papaya'} , -{'id': 227, 'name': 'router/modem'} , -{'id': 357, 'name': 'asparagus'} , -{'id': 73, 'name': 'motorcycle'} , -{'id': 77, 'name': 'traffic sign'} , -{'id': 67, 'name': 'fish'} , -{'id': 326, 'name': 'radish'} , -{'id': 213, 'name': 'egg'} , -{'id': 203, 'name': 'cucumber'} , -{'id': 17, 'name': 'helmet'} , -{'id': 110, 'name': 'luggage'} , -{'id': 80, 'name': 'truck'} , -{'id': 199, 'name': 'frisbee'} , -{'id': 232, 'name': 'peach'} , -{'id': 1, 'name': 'person'} , -{'id': 29, 'name': 'boots'} , -{'id': 310, 'name': 'chips'} , -{'id': 142, 'name': 'skateboard'} , -{'id': 44, 'name': 'slippers'} , -{'id': 4, 'name': 'hat'} , -{'id': 178, 'name': 'suitcase'} , -{'id': 24, 'name': 'tv'} , -{'id': 119, 'name': 'train'} , -{'id': 82, 'name': 'power outlet'} , -{'id': 245, 'name': 'swing'} , -{'id': 15, 'name': 'book'} , -{'id': 294, 'name': 'jellyfish'} , -{'id': 192, 'name': 'fire extinguisher'} , -{'id': 212, 'name': 'deer'} , -{'id': 181, 'name': 'pear'} , -{'id': 347, 'name': 'table tennis paddle'} , -{'id': 113, 'name': 'trolley'} , -{'id': 91, 'name': 'guitar'} , -{'id': 202, 'name': 'golf club'} , -{'id': 221, 'name': 'wheelchair'} , -{'id': 254, 'name': 'saxophone'} , -{'id': 117, 'name': 'paper towel'} , -{'id': 303, 'name': 'race car'} , -{'id': 240, 'name': 'carriage'} , -{'id': 246, 'name': 'radio'} , -{'id': 318, 'name': 'parrot'} , -{'id': 251, 'name': 'french fries'} , -{'id': 98, 'name': 'dog'} , -{'id': 112, 'name': 'soccer'} , -{'id': 355, 'name': 'french horn'} , -{'id': 79, 'name': 'paddle'} , -{'id': 283, 'name': 'lettuce'} , -{'id': 9, 'name': 'car'} , -{'id': 258, 'name': 'kiwi fruit'} , -{'id': 325, 'name': 'llama'} , -{'id': 187, 'name': 'billiards'} , -{'id': 210, 'name': 'facial cleanser'} , -{'id': 81, 'name': 'cow'} , -{'id': 331, 'name': 'microscope'} , -{'id': 148, 'name': 'lemon'} , -{'id': 302, 'name': 'pomelo'} , -{'id': 85, 'name': 'fork'} , -{'id': 154, 'name': 'pumpkin'} , -{'id': 289, 'name': 'shrimp'} , -{'id': 71, 'name': 'teddy bear'} , -{'id': 184, 'name': 'potato'} , -{'id': 102, 'name': 'air conditioner'} , -{'id': 208, 'name': 'hot dog'} , -{'id': 222, 'name': 'plum'} , -{'id': 316, 'name': 'spring rolls'} , -{'id': 230, 'name': 'crane'} , -{'id': 149, 'name': 'liquid soap'} , -{'id': 55, 'name': 'canned'} , -{'id': 35, 'name': 'speaker'} , -{'id': 108, 'name': 'banana'} , -{'id': 297, 'name': 'treadmill'} , -{'id': 99, 'name': 'spoon'} , -{'id': 104, 'name': 'mouse'} , -{'id': 182, 'name': 'american football'} , -{'id': 299, 'name': 'egg tart'} , -{'id': 127, 'name': 'cleaning products'} , -{'id': 313, 'name': 'urinal'} , -{'id': 286, 'name': 'medal'} , -{'id': 239, 'name': 'brush'} , -{'id': 96, 'name': 'hockey'} , -{'id': 279, 'name': 'dumbbell'} , -{'id': 32, 'name': 'umbrella'} , -{'id': 272, 'name': 'hammer'} , -{'id': 16, 'name': 'plate'} , -{'id': 21, 'name': 'potted plant'} , -{'id': 242, 'name': 'earphone'} , -{'id': 70, 'name': 'candle'} , -{'id': 185, 'name': 'paint brush'} , -{'id': 48, 'name': 'toy'} , -{'id': 130, 'name': 'pizza'} , -{'id': 255, 'name': 'trumpet'} , -{'id': 361, 'name': 'hotair balloon'} , -{'id': 188, 'name': 'fire hydrant'} , -{'id': 50, 'name': 'bed'} , -{'id': 253, 'name': 'avocado'} , -{'id': 293, 'name': 'coconut'} , -{'id': 257, 'name': 'cue'} , -{'id': 280, 'name': 'hamimelon'} , -{'id': 66, 'name': 'horse'} , -{'id': 173, 'name': 'pigeon'} , -{'id': 190, 'name': 'projector'} , -{'id': 69, 'name': 'camera'} , -{'id': 30, 'name': 'bowl'} , -{'id': 269, 'name': 'broom'} , -{'id': 343, 'name': 'pitaya'} , -{'id': 305, 'name': 'tuba'} , -{'id': 309, 'name': 'green onion'} , -{'id': 363, 'name': 'lobster'} , -{'id': 225, 'name': 'watermelon'} , -{'id': 47, 'name': 'suv'} , -{'id': 31, 'name': 'dining table'} , -{'id': 54, 'name': 'sandals'} , -{'id': 351, 'name': 'monkey'} , -{'id': 218, 'name': 'onion'} , -{'id': 36, 'name': 'trash bin/can'} , -{'id': 20, 'name': 'glove'} , -{'id': 277, 'name': 'rice'} , -{'id': 152, 'name': 'sports car'} , -{'id': 360, 'name': 'target'} , -{'id': 205, 'name': 'blender'} , -{'id': 19, 'name': 'pillow'} , -{'id': 72, 'name': 'cake'} , -{'id': 93, 'name': 'tea pot'} , -{'id': 353, 'name': 'game board'} , -{'id': 38, 'name': 'backpack'} , -{'id': 356, 'name': 'ambulance'} , -{'id': 146, 'name': 'life saver'} , -{'id': 189, 'name': 'goose'} , -{'id': 278, 'name': 'tape measure/ruler'} , -{'id': 92, 'name': 'traffic cone'} , -{'id': 134, 'name': 'toiletries'} , -{'id': 114, 'name': 'oven'} , -{'id': 317, 'name': 'tortoise/turtle'} , -{'id': 265, 'name': 'corn'} , -{'id': 126, 'name': 'donut'} , -{'id': 57, 'name': 'mirror'} , -{'id': 7, 'name': 'cabinet/shelf'} , -{'id': 263, 'name': 'green vegetables'} , -{'id': 159, 'name': 'tissue '} , -{'id': 321, 'name': 'shark'} , -{'id': 301, 'name': 'pig'} , -{'id': 41, 'name': 'carpet'} , -{'id': 304, 'name': 'rice cooker'} , -{'id': 323, 'name': 'poker card'} , + {"id": 164, "name": "cutting/chopping board"}, + {"id": 49, "name": "tie"}, + {"id": 306, "name": "crosswalk sign"}, + {"id": 145, "name": "gun"}, + {"id": 14, "name": "street lights"}, + {"id": 223, "name": "bar soap"}, + {"id": 74, "name": "wild bird"}, + {"id": 219, "name": "ice cream"}, + {"id": 37, "name": "stool"}, + {"id": 25, "name": "storage box"}, + {"id": 153, "name": "giraffe"}, + {"id": 52, "name": "pen/pencil"}, + {"id": 61, "name": "high heels"}, + {"id": 340, "name": "mangosteen"}, + {"id": 22, "name": "bracelet"}, + {"id": 155, "name": "piano"}, + {"id": 162, "name": "vent"}, + {"id": 75, "name": "laptop"}, + {"id": 236, "name": "toaster"}, + {"id": 231, "name": "fire truck"}, + {"id": 42, "name": "basket"}, + {"id": 150, "name": "zebra"}, + {"id": 124, "name": "head phone"}, + {"id": 90, "name": "sheep"}, + {"id": 322, "name": "steak"}, + {"id": 39, "name": "couch"}, + {"id": 209, "name": "toothbrush"}, + {"id": 59, "name": "bicycle"}, + {"id": 336, "name": "red cabbage"}, + {"id": 228, "name": "golf ball"}, + {"id": 120, "name": "tomato"}, + {"id": 132, "name": "computer box"}, + {"id": 8, "name": "cup"}, + {"id": 183, "name": "basketball"}, + {"id": 298, "name": "butterfly"}, + {"id": 250, "name": "garlic"}, + {"id": 12, "name": "desk"}, + {"id": 141, "name": "microwave"}, + {"id": 171, "name": "strawberry"}, + {"id": 200, "name": "kettle"}, + {"id": 63, "name": "van"}, + {"id": 300, "name": "cheese"}, + {"id": 215, "name": "marker"}, + {"id": 100, "name": "blackboard/whiteboard"}, + {"id": 186, "name": "printer"}, + {"id": 333, "name": "bread/bun"}, + {"id": 243, "name": "penguin"}, + {"id": 364, "name": "iron"}, + {"id": 180, "name": "ladder"}, + {"id": 34, "name": "flag"}, + {"id": 78, "name": "cell phone"}, + {"id": 97, "name": "fan"}, + {"id": 224, "name": "scale"}, + {"id": 151, "name": "duck"}, + {"id": 319, "name": "flute"}, + {"id": 156, "name": "stop sign"}, + {"id": 290, "name": "rickshaw"}, + {"id": 128, "name": "sailboat"}, + {"id": 165, "name": "tennis racket"}, + {"id": 241, "name": "cigar"}, + {"id": 101, "name": "balloon"}, + {"id": 308, "name": "hair drier"}, + {"id": 167, "name": "skating and skiing shoes"}, + {"id": 237, "name": "helicopter"}, + {"id": 65, "name": "sink"}, + {"id": 129, "name": "tangerine"}, + {"id": 330, "name": "crab"}, + {"id": 320, "name": "measuring cup"}, + {"id": 260, "name": "fishing rod"}, + {"id": 346, "name": "saw"}, + {"id": 216, "name": "ship"}, + {"id": 46, "name": "coffee table"}, + {"id": 194, "name": "facial mask"}, + {"id": 281, "name": "stapler"}, + {"id": 118, "name": "refrigerator"}, + {"id": 40, "name": "belt"}, + {"id": 349, "name": "starfish"}, + {"id": 87, "name": "hanger"}, + {"id": 116, "name": "baseball glove"}, + {"id": 261, "name": "cherry"}, + {"id": 334, "name": "baozi"}, + {"id": 267, "name": "screwdriver"}, + {"id": 158, "name": "converter"}, + {"id": 335, "name": "lion"}, + {"id": 170, "name": "baseball"}, + {"id": 111, "name": "skis"}, + {"id": 136, "name": "broccoli"}, + {"id": 342, "name": "eraser"}, + {"id": 337, "name": "polar bear"}, + {"id": 139, "name": "shovel"}, + {"id": 193, "name": "extension cord"}, + {"id": 284, "name": "goldfish"}, + {"id": 174, "name": "pepper"}, + {"id": 138, "name": "stroller"}, + {"id": 328, "name": "yak"}, + {"id": 83, "name": "clock"}, + {"id": 235, "name": "tricycle"}, + {"id": 248, "name": "parking meter"}, + {"id": 274, "name": "trophy"}, + {"id": 324, "name": "binoculars"}, + {"id": 51, "name": "traffic light"}, + {"id": 314, "name": "donkey"}, + {"id": 45, "name": "barrel/bucket"}, + {"id": 292, "name": "pomegranate"}, + {"id": 13, "name": "handbag"}, + {"id": 262, "name": "tablet"}, + {"id": 68, "name": "apple"}, + {"id": 226, "name": "cabbage"}, + {"id": 23, "name": "flower"}, + {"id": 58, "name": "faucet"}, + {"id": 206, "name": "tong"}, + {"id": 291, "name": "trombone"}, + {"id": 160, "name": "carrot"}, + {"id": 172, "name": "bow tie"}, + {"id": 122, "name": "tent"}, + {"id": 163, "name": "cookies"}, + {"id": 115, "name": "remote"}, + {"id": 175, "name": "coffee machine"}, + {"id": 238, "name": "green beans"}, + {"id": 233, "name": "cello"}, + {"id": 28, "name": "wine glass"}, + {"id": 295, "name": "mushroom"}, + {"id": 344, "name": "scallop"}, + {"id": 125, "name": "lantern"}, + {"id": 123, "name": "shampoo/shower gel"}, + {"id": 285, "name": "meat balls"}, + {"id": 266, "name": "key"}, + {"id": 296, "name": "calculator"}, + {"id": 168, "name": "scissors"}, + {"id": 103, "name": "cymbal"}, + {"id": 6, "name": "bottle"}, + {"id": 264, "name": "nuts"}, + {"id": 234, "name": "notepaper"}, + {"id": 211, "name": "mango"}, + {"id": 287, "name": "toothpaste"}, + {"id": 196, "name": "chopsticks"}, + {"id": 140, "name": "baseball bat"}, + {"id": 244, "name": "hurdle"}, + {"id": 195, "name": "tennis ball"}, + {"id": 144, "name": "surveillance camera"}, + {"id": 271, "name": "volleyball"}, + {"id": 94, "name": "keyboard"}, + {"id": 339, "name": "seal"}, + {"id": 11, "name": "picture/frame"}, + {"id": 348, "name": "okra"}, + {"id": 191, "name": "sausage"}, + {"id": 166, "name": "candy"}, + {"id": 62, "name": "ring"}, + {"id": 311, "name": "dolphin"}, + {"id": 273, "name": "eggplant"}, + {"id": 84, "name": "drum"}, + {"id": 143, "name": "surfboard"}, + {"id": 288, "name": "antelope"}, + {"id": 204, "name": "clutch"}, + {"id": 207, "name": "slide"}, + {"id": 43, "name": "towel/napkin"}, + {"id": 352, "name": "durian"}, + {"id": 276, "name": "board eraser"}, + {"id": 315, "name": "electric drill"}, + {"id": 312, "name": "sushi"}, + {"id": 198, "name": "pie"}, + {"id": 106, "name": "pickup truck"}, + {"id": 176, "name": "bathtub"}, + {"id": 26, "name": "vase"}, + {"id": 133, "name": "elephant"}, + {"id": 256, "name": "sandwich"}, + {"id": 327, "name": "noodles"}, + {"id": 10, "name": "glasses"}, + {"id": 109, "name": "airplane"}, + {"id": 95, "name": "tripod"}, + {"id": 247, "name": "CD"}, + {"id": 121, "name": "machinery vehicle"}, + {"id": 365, "name": "flashlight"}, + {"id": 53, "name": "microphone"}, + {"id": 270, "name": "pliers"}, + {"id": 362, "name": "chainsaw"}, + {"id": 259, "name": "bear"}, + {"id": 197, "name": "electronic stove and gas stove"}, + {"id": 89, "name": "pot/pan"}, + {"id": 220, "name": "tape"}, + {"id": 338, "name": "lighter"}, + {"id": 177, "name": "snowboard"}, + {"id": 214, "name": "violin"}, + {"id": 217, "name": "chicken"}, + {"id": 2, "name": "sneakers"}, + {"id": 161, "name": "washing machine"}, + {"id": 131, "name": "kite"}, + {"id": 354, "name": "rabbit"}, + {"id": 86, "name": "bus"}, + {"id": 275, "name": "dates"}, + {"id": 282, "name": "camel"}, + {"id": 88, "name": "nightstand"}, + {"id": 179, "name": "grapes"}, + {"id": 229, "name": "pine apple"}, + {"id": 56, "name": "necklace"}, + {"id": 18, "name": "leather shoes"}, + {"id": 358, "name": "hoverboard"}, + {"id": 345, "name": "pencil case"}, + {"id": 359, "name": "pasta"}, + {"id": 157, "name": "radiator"}, + {"id": 201, "name": "hamburger"}, + {"id": 268, "name": "globe"}, + {"id": 332, "name": "barbell"}, + {"id": 329, "name": "mop"}, + {"id": 252, "name": "horn"}, + {"id": 350, "name": "eagle"}, + {"id": 169, "name": "folder"}, + {"id": 137, "name": "toilet"}, + {"id": 5, "name": "lamp"}, + {"id": 27, "name": "bench"}, + {"id": 249, "name": "swan"}, + {"id": 76, "name": "knife"}, + {"id": 341, "name": "comb"}, + {"id": 64, "name": "watch"}, + {"id": 105, "name": "telephone"}, + {"id": 3, "name": "chair"}, + {"id": 33, "name": "boat"}, + {"id": 107, "name": "orange"}, + {"id": 60, "name": "bread"}, + {"id": 147, "name": "cat"}, + {"id": 135, "name": "gas stove"}, + {"id": 307, "name": "papaya"}, + {"id": 227, "name": "router/modem"}, + {"id": 357, "name": "asparagus"}, + {"id": 73, "name": "motorcycle"}, + {"id": 77, "name": "traffic sign"}, + {"id": 67, "name": "fish"}, + {"id": 326, "name": "radish"}, + {"id": 213, "name": "egg"}, + {"id": 203, "name": "cucumber"}, + {"id": 17, "name": "helmet"}, + {"id": 110, "name": "luggage"}, + {"id": 80, "name": "truck"}, + {"id": 199, "name": "frisbee"}, + {"id": 232, "name": "peach"}, + {"id": 1, "name": "person"}, + {"id": 29, "name": "boots"}, + {"id": 310, "name": "chips"}, + {"id": 142, "name": "skateboard"}, + {"id": 44, "name": "slippers"}, + {"id": 4, "name": "hat"}, + {"id": 178, "name": "suitcase"}, + {"id": 24, "name": "tv"}, + {"id": 119, "name": "train"}, + {"id": 82, "name": "power outlet"}, + {"id": 245, "name": "swing"}, + {"id": 15, "name": "book"}, + {"id": 294, "name": "jellyfish"}, + {"id": 192, "name": "fire extinguisher"}, + {"id": 212, "name": "deer"}, + {"id": 181, "name": "pear"}, + {"id": 347, "name": "table tennis paddle"}, + {"id": 113, "name": "trolley"}, + {"id": 91, "name": "guitar"}, + {"id": 202, "name": "golf club"}, + {"id": 221, "name": "wheelchair"}, + {"id": 254, "name": "saxophone"}, + {"id": 117, "name": "paper towel"}, + {"id": 303, "name": "race car"}, + {"id": 240, "name": "carriage"}, + {"id": 246, "name": "radio"}, + {"id": 318, "name": "parrot"}, + {"id": 251, "name": "french fries"}, + {"id": 98, "name": "dog"}, + {"id": 112, "name": "soccer"}, + {"id": 355, "name": "french horn"}, + {"id": 79, "name": "paddle"}, + {"id": 283, "name": "lettuce"}, + {"id": 9, "name": "car"}, + {"id": 258, "name": "kiwi fruit"}, + {"id": 325, "name": "llama"}, + {"id": 187, "name": "billiards"}, + {"id": 210, "name": "facial cleanser"}, + {"id": 81, "name": "cow"}, + {"id": 331, "name": "microscope"}, + {"id": 148, "name": "lemon"}, + {"id": 302, "name": "pomelo"}, + {"id": 85, "name": "fork"}, + {"id": 154, "name": "pumpkin"}, + {"id": 289, "name": "shrimp"}, + {"id": 71, "name": "teddy bear"}, + {"id": 184, "name": "potato"}, + {"id": 102, "name": "air conditioner"}, + {"id": 208, "name": "hot dog"}, + {"id": 222, "name": "plum"}, + {"id": 316, "name": "spring rolls"}, + {"id": 230, "name": "crane"}, + {"id": 149, "name": "liquid soap"}, + {"id": 55, "name": "canned"}, + {"id": 35, "name": "speaker"}, + {"id": 108, "name": "banana"}, + {"id": 297, "name": "treadmill"}, + {"id": 99, "name": "spoon"}, + {"id": 104, "name": "mouse"}, + {"id": 182, "name": "american football"}, + {"id": 299, "name": "egg tart"}, + {"id": 127, "name": "cleaning products"}, + {"id": 313, "name": "urinal"}, + {"id": 286, "name": "medal"}, + {"id": 239, "name": "brush"}, + {"id": 96, "name": "hockey"}, + {"id": 279, "name": "dumbbell"}, + {"id": 32, "name": "umbrella"}, + {"id": 272, "name": "hammer"}, + {"id": 16, "name": "plate"}, + {"id": 21, "name": "potted plant"}, + {"id": 242, "name": "earphone"}, + {"id": 70, "name": "candle"}, + {"id": 185, "name": "paint brush"}, + {"id": 48, "name": "toy"}, + {"id": 130, "name": "pizza"}, + {"id": 255, "name": "trumpet"}, + {"id": 361, "name": "hotair balloon"}, + {"id": 188, "name": "fire hydrant"}, + {"id": 50, "name": "bed"}, + {"id": 253, "name": "avocado"}, + {"id": 293, "name": "coconut"}, + {"id": 257, "name": "cue"}, + {"id": 280, "name": "hamimelon"}, + {"id": 66, "name": "horse"}, + {"id": 173, "name": "pigeon"}, + {"id": 190, "name": "projector"}, + {"id": 69, "name": "camera"}, + {"id": 30, "name": "bowl"}, + {"id": 269, "name": "broom"}, + {"id": 343, "name": "pitaya"}, + {"id": 305, "name": "tuba"}, + {"id": 309, "name": "green onion"}, + {"id": 363, "name": "lobster"}, + {"id": 225, "name": "watermelon"}, + {"id": 47, "name": "suv"}, + {"id": 31, "name": "dining table"}, + {"id": 54, "name": "sandals"}, + {"id": 351, "name": "monkey"}, + {"id": 218, "name": "onion"}, + {"id": 36, "name": "trash bin/can"}, + {"id": 20, "name": "glove"}, + {"id": 277, "name": "rice"}, + {"id": 152, "name": "sports car"}, + {"id": 360, "name": "target"}, + {"id": 205, "name": "blender"}, + {"id": 19, "name": "pillow"}, + {"id": 72, "name": "cake"}, + {"id": 93, "name": "tea pot"}, + {"id": 353, "name": "game board"}, + {"id": 38, "name": "backpack"}, + {"id": 356, "name": "ambulance"}, + {"id": 146, "name": "life saver"}, + {"id": 189, "name": "goose"}, + {"id": 278, "name": "tape measure/ruler"}, + {"id": 92, "name": "traffic cone"}, + {"id": 134, "name": "toiletries"}, + {"id": 114, "name": "oven"}, + {"id": 317, "name": "tortoise/turtle"}, + {"id": 265, "name": "corn"}, + {"id": 126, "name": "donut"}, + {"id": 57, "name": "mirror"}, + {"id": 7, "name": "cabinet/shelf"}, + {"id": 263, "name": "green vegetables"}, + {"id": 159, "name": "tissue "}, + {"id": 321, "name": "shark"}, + {"id": 301, "name": "pig"}, + {"id": 41, "name": "carpet"}, + {"id": 304, "name": "rice cooker"}, + {"id": 323, "name": "poker card"}, ] + def _get_builtin_metadata(version): - if version == 'v1': - id_to_name = {x['id']: x['name'] for x in categories_v1} + if version == "v1": + id_to_name = {x["id"]: x["name"] for x in categories_v1} else: assert 0, version thing_dataset_id_to_contiguous_id = {i + 1: i for i in range(365)} thing_classes = [id_to_name[k] for k in sorted(id_to_name)] return { "thing_dataset_id_to_contiguous_id": thing_dataset_id_to_contiguous_id, - "thing_classes": thing_classes} + "thing_classes": thing_classes, + } + _PREDEFINED_SPLITS_OBJECTS365 = { "objects365_train": ("objects365/train", "objects365/annotations/objects365_train.json"), @@ -388,7 +392,7 @@ def _get_builtin_metadata(version): for key, (image_root, json_file) in _PREDEFINED_SPLITS_OBJECTS365.items(): register_coco_instances( key, - _get_builtin_metadata('v1'), + _get_builtin_metadata("v1"), os.path.join("datasets", json_file) if "://" not in json_file else json_file, os.path.join("datasets", image_root), ) diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/data/transforms/custom_augmentation_impl.py b/dimos/models/Detic/third_party/CenterNet2/centernet/data/transforms/custom_augmentation_impl.py index 5a69e178a5..f4ec0ad07f 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/data/transforms/custom_augmentation_impl.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/data/transforms/custom_augmentation_impl.py @@ -1,22 +1,13 @@ -# -*- coding: utf-8 -*- # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved # Modified by Xingyi Zhou """ Implement many useful :class:`Augmentation`. """ + +from detectron2.data.transforms.augmentation import Augmentation import numpy as np -import sys -from fvcore.transforms.transform import ( - BlendTransform, - CropTransform, - HFlipTransform, - NoOpTransform, - Transform, - VFlipTransform, -) from PIL import Image -from detectron2.data.transforms.augmentation import Augmentation from .custom_transform import EfficientDetResizeCropTransform __all__ = [ @@ -30,9 +21,7 @@ class EfficientDetResizeCrop(Augmentation): If `max_size` is reached, then downscale so that the longer edge does not exceed max_size. """ - def __init__( - self, size, scale, interp=Image.BILINEAR - ): + def __init__(self, size: int, scale, interp=Image.BILINEAR) -> None: """ Args: """ @@ -60,4 +49,5 @@ def get_transform(self, img): offset_y = int(max(0.0, float(offset_y)) * np.random.uniform(0, 1)) offset_x = int(max(0.0, float(offset_x)) * np.random.uniform(0, 1)) return EfficientDetResizeCropTransform( - scaled_h, scaled_w, offset_y, offset_x, img_scale, self.target_size, self.interp) + scaled_h, scaled_w, offset_y, offset_x, img_scale, self.target_size, self.interp + ) diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/data/transforms/custom_transform.py b/dimos/models/Detic/third_party/CenterNet2/centernet/data/transforms/custom_transform.py index 654d65d97d..6635a5999b 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/data/transforms/custom_transform.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/data/transforms/custom_transform.py @@ -1,22 +1,17 @@ -# -*- coding: utf-8 -*- # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved # Modified by Xingyi Zhou # File: transform.py -import numpy as np -import torch -import torch.nn.functional as F from fvcore.transforms.transform import ( - CropTransform, - HFlipTransform, - NoOpTransform, Transform, - TransformList, ) +import numpy as np from PIL import Image +import torch +import torch.nn.functional as F try: - import cv2 # noqa + import cv2 except ImportError: # OpenCV is an optional dependency at the moment pass @@ -27,10 +22,9 @@ class EfficientDetResizeCropTransform(Transform): - """ - """ + """ """ - def __init__(self, scaled_h, scaled_w, offset_y, offset_x, img_scale, target_size, interp=None): + def __init__(self, scaled_h, scaled_w, offset_y, offset_x, img_scale, target_size: int, interp=None) -> None: """ Args: h, w (int): original image size @@ -56,9 +50,9 @@ def apply_image(self, img, interp=None): lower = min(self.scaled_h, self.offset_y + self.target_size[0]) # img = img.crop((self.offset_x, self.offset_y, right, lower)) if len(ret.shape) <= 3: - ret = ret[self.offset_y: lower, self.offset_x: right] + ret = ret[self.offset_y : lower, self.offset_x : right] else: - ret = ret[..., self.offset_y: lower, self.offset_x: right, :] + ret = ret[..., self.offset_y : lower, self.offset_x : right, :] else: # PIL only supports uint8 img = torch.from_numpy(img) @@ -73,9 +67,9 @@ def apply_image(self, img, interp=None): right = min(self.scaled_w, self.offset_x + self.target_size[1]) lower = min(self.scaled_h, self.offset_y + self.target_size[0]) if len(ret.shape) <= 3: - ret = ret[self.offset_y: lower, self.offset_x: right] + ret = ret[self.offset_y : lower, self.offset_x : right] else: - ret = ret[..., self.offset_y: lower, self.offset_x: right, :] + ret = ret[..., self.offset_y : lower, self.offset_x : right, :] return ret def apply_coords(self, coords): @@ -91,4 +85,4 @@ def apply_segmentation(self, segmentation): def inverse(self): raise NotImplementedError - # return ResizeTransform(self.new_h, self.new_w, self.h, self.w, self.interp) \ No newline at end of file + # return ResizeTransform(self.new_h, self.new_w, self.h, self.w, self.interp) diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/bifpn.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/bifpn.py index 565e2940ad..733b502da4 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/bifpn.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/bifpn.py @@ -1,51 +1,45 @@ # Modified from https://github.com/rwightman/efficientdet-pytorch/blob/master/effdet/efficientdet.py # The original file is under Apache-2.0 License -import math -from os.path import join -import numpy as np from collections import OrderedDict -from typing import List +import math +from detectron2.layers import Conv2d, ShapeSpec +from detectron2.layers.batch_norm import get_norm +from detectron2.modeling.backbone import Backbone +from detectron2.modeling.backbone.build import BACKBONE_REGISTRY +from detectron2.modeling.backbone.resnet import build_resnet_backbone import torch from torch import nn -import torch.utils.model_zoo as model_zoo -import torch.nn.functional as F -import fvcore.nn.weight_init as weight_init -from detectron2.layers import ShapeSpec, Conv2d -from detectron2.modeling.backbone.resnet import build_resnet_backbone -from detectron2.modeling.backbone.build import BACKBONE_REGISTRY -from detectron2.layers.batch_norm import get_norm -from detectron2.modeling.backbone import Backbone from .dlafpn import dla34 -def get_fpn_config(base_reduction=8): + +def get_fpn_config(base_reduction: int=8): """BiFPN config with sum.""" p = { - 'nodes': [ - {'reduction': base_reduction << 3, 'inputs_offsets': [3, 4]}, - {'reduction': base_reduction << 2, 'inputs_offsets': [2, 5]}, - {'reduction': base_reduction << 1, 'inputs_offsets': [1, 6]}, - {'reduction': base_reduction, 'inputs_offsets': [0, 7]}, - {'reduction': base_reduction << 1, 'inputs_offsets': [1, 7, 8]}, - {'reduction': base_reduction << 2, 'inputs_offsets': [2, 6, 9]}, - {'reduction': base_reduction << 3, 'inputs_offsets': [3, 5, 10]}, - {'reduction': base_reduction << 4, 'inputs_offsets': [4, 11]}, + "nodes": [ + {"reduction": base_reduction << 3, "inputs_offsets": [3, 4]}, + {"reduction": base_reduction << 2, "inputs_offsets": [2, 5]}, + {"reduction": base_reduction << 1, "inputs_offsets": [1, 6]}, + {"reduction": base_reduction, "inputs_offsets": [0, 7]}, + {"reduction": base_reduction << 1, "inputs_offsets": [1, 7, 8]}, + {"reduction": base_reduction << 2, "inputs_offsets": [2, 6, 9]}, + {"reduction": base_reduction << 3, "inputs_offsets": [3, 5, 10]}, + {"reduction": base_reduction << 4, "inputs_offsets": [4, 11]}, ], - 'weight_method': 'fastattn', + "weight_method": "fastattn", } return p def swish(x, inplace: bool = False): - """Swish - Described in: https://arxiv.org/abs/1710.05941 - """ + """Swish - Described in: https://arxiv.org/abs/1710.05941""" return x.mul_(x.sigmoid()) if inplace else x.mul(x.sigmoid()) class Swish(nn.Module): - def __init__(self, inplace: bool = False): - super(Swish, self).__init__() + def __init__(self, inplace: bool = False) -> None: + super().__init__() self.inplace = inplace def forward(self, x): @@ -53,8 +47,8 @@ def forward(self, x): class SequentialAppend(nn.Sequential): - def __init__(self, *args): - super(SequentialAppend, self).__init__(*args) + def __init__(self, *args) -> None: + super().__init__(*args) def forward(self, x): for module in self: @@ -63,8 +57,8 @@ def forward(self, x): class SequentialAppendLast(nn.Sequential): - def __init__(self, *args): - super(SequentialAppendLast, self).__init__(*args) + def __init__(self, *args) -> None: + super().__init__(*args) # def forward(self, x: List[torch.Tensor]): def forward(self, x): @@ -74,14 +68,29 @@ def forward(self, x): class ConvBnAct2d(nn.Module): - def __init__(self, in_channels, out_channels, kernel_size, stride=1, dilation=1, padding='', bias=False, - norm='', act_layer=Swish): - super(ConvBnAct2d, self).__init__() + def __init__( + self, + in_channels, + out_channels, + kernel_size: int, + stride: int=1, + dilation: int=1, + padding: str="", + bias: bool=False, + norm: str="", + act_layer=Swish, + ) -> None: + super().__init__() # self.conv = create_conv2d( # in_channels, out_channels, kernel_size, stride=stride, dilation=dilation, padding=padding, bias=bias) self.conv = Conv2d( - in_channels, out_channels, kernel_size=kernel_size, stride=stride, - padding=kernel_size // 2, bias=(norm == '')) + in_channels, + out_channels, + kernel_size=kernel_size, + stride=stride, + padding=kernel_size // 2, + bias=(norm == ""), + ) self.bn = get_norm(norm, out_channels) self.act = None if act_layer is None else act_layer(inplace=True) @@ -95,29 +104,49 @@ def forward(self, x): class SeparableConv2d(nn.Module): - """ Separable Conv - """ - def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, dilation=1, padding='', bias=False, - channel_multiplier=1.0, pw_kernel_size=1, act_layer=Swish, - norm=''): - super(SeparableConv2d, self).__init__() + """Separable Conv""" + + def __init__( + self, + in_channels, + out_channels, + kernel_size: int=3, + stride: int=1, + dilation: int=1, + padding: str="", + bias: bool=False, + channel_multiplier: float=1.0, + pw_kernel_size: int=1, + act_layer=Swish, + norm: str="", + ) -> None: + super().__init__() # self.conv_dw = create_conv2d( # in_channels, int(in_channels * channel_multiplier), kernel_size, # stride=stride, dilation=dilation, padding=padding, depthwise=True) self.conv_dw = Conv2d( - in_channels, int(in_channels * channel_multiplier), - kernel_size=kernel_size, stride=stride, padding=kernel_size // 2, bias=bias, - groups=out_channels) - # print('conv_dw', kernel_size, stride) + in_channels, + int(in_channels * channel_multiplier), + kernel_size=kernel_size, + stride=stride, + padding=kernel_size // 2, + bias=bias, + groups=out_channels, + ) + # print('conv_dw', kernel_size, stride) # self.conv_pw = create_conv2d( # int(in_channels * channel_multiplier), out_channels, pw_kernel_size, padding=padding, bias=bias) - + self.conv_pw = Conv2d( - int(in_channels * channel_multiplier), out_channels, - kernel_size=pw_kernel_size, padding=pw_kernel_size // 2, bias=(norm=='')) - # print('conv_pw', pw_kernel_size) + int(in_channels * channel_multiplier), + out_channels, + kernel_size=pw_kernel_size, + padding=pw_kernel_size // 2, + bias=(norm == ""), + ) + # print('conv_pw', pw_kernel_size) self.bn = get_norm(norm, out_channels) self.act = None if act_layer is None else act_layer(inplace=True) @@ -133,11 +162,20 @@ def forward(self, x): class ResampleFeatureMap(nn.Sequential): - def __init__(self, in_channels, out_channels, reduction_ratio=1., pad_type='', pooling_type='max', - norm='', apply_bn=False, conv_after_downsample=False, - redundant_bias=False): - super(ResampleFeatureMap, self).__init__() - pooling_type = pooling_type or 'max' + def __init__( + self, + in_channels, + out_channels, + reduction_ratio: float=1.0, + pad_type: str="", + pooling_type: str="max", + norm: str="", + apply_bn: bool=False, + conv_after_downsample: bool=False, + redundant_bias: bool=False, + ) -> None: + super().__init__() + pooling_type = pooling_type or "max" self.in_channels = in_channels self.out_channels = out_channels self.reduction_ratio = reduction_ratio @@ -146,57 +184,80 @@ def __init__(self, in_channels, out_channels, reduction_ratio=1., pad_type='', p conv = None if in_channels != out_channels: conv = ConvBnAct2d( - in_channels, out_channels, kernel_size=1, padding=pad_type, - norm=norm if apply_bn else '', - bias=not apply_bn or redundant_bias, act_layer=None) + in_channels, + out_channels, + kernel_size=1, + padding=pad_type, + norm=norm if apply_bn else "", + bias=not apply_bn or redundant_bias, + act_layer=None, + ) if reduction_ratio > 1: stride_size = int(reduction_ratio) if conv is not None and not self.conv_after_downsample: - self.add_module('conv', conv) + self.add_module("conv", conv) self.add_module( - 'downsample', + "downsample", # create_pool2d( # pooling_type, kernel_size=stride_size + 1, stride=stride_size, padding=pad_type) # nn.MaxPool2d(kernel_size=stride_size + 1, stride=stride_size, padding=pad_type) - nn.MaxPool2d(kernel_size=stride_size, stride=stride_size) - ) + nn.MaxPool2d(kernel_size=stride_size, stride=stride_size), + ) if conv is not None and self.conv_after_downsample: - self.add_module('conv', conv) + self.add_module("conv", conv) else: if conv is not None: - self.add_module('conv', conv) + self.add_module("conv", conv) if reduction_ratio < 1: scale = int(1 // reduction_ratio) - self.add_module('upsample', nn.UpsamplingNearest2d(scale_factor=scale)) + self.add_module("upsample", nn.UpsamplingNearest2d(scale_factor=scale)) class FpnCombine(nn.Module): - def __init__(self, feature_info, fpn_config, fpn_channels, inputs_offsets, target_reduction, pad_type='', - pooling_type='max', norm='', apply_bn_for_resampling=False, - conv_after_downsample=False, redundant_bias=False, weight_method='attn'): - super(FpnCombine, self).__init__() + def __init__( + self, + feature_info, + fpn_config, + fpn_channels, + inputs_offsets, + target_reduction, + pad_type: str="", + pooling_type: str="max", + norm: str="", + apply_bn_for_resampling: bool=False, + conv_after_downsample: bool=False, + redundant_bias: bool=False, + weight_method: str="attn", + ) -> None: + super().__init__() self.inputs_offsets = inputs_offsets self.weight_method = weight_method self.resample = nn.ModuleDict() - for idx, offset in enumerate(inputs_offsets): + for _idx, offset in enumerate(inputs_offsets): in_channels = fpn_channels if offset < len(feature_info): - in_channels = feature_info[offset]['num_chs'] - input_reduction = feature_info[offset]['reduction'] + in_channels = feature_info[offset]["num_chs"] + input_reduction = feature_info[offset]["reduction"] else: node_idx = offset - len(feature_info) # print('node_idx, len', node_idx, len(fpn_config['nodes'])) - input_reduction = fpn_config['nodes'][node_idx]['reduction'] + input_reduction = fpn_config["nodes"][node_idx]["reduction"] reduction_ratio = target_reduction / input_reduction self.resample[str(offset)] = ResampleFeatureMap( - in_channels, fpn_channels, reduction_ratio=reduction_ratio, pad_type=pad_type, - pooling_type=pooling_type, norm=norm, - apply_bn=apply_bn_for_resampling, conv_after_downsample=conv_after_downsample, - redundant_bias=redundant_bias) + in_channels, + fpn_channels, + reduction_ratio=reduction_ratio, + pad_type=pad_type, + pooling_type=pooling_type, + norm=norm, + apply_bn=apply_bn_for_resampling, + conv_after_downsample=conv_after_downsample, + redundant_bias=redundant_bias, + ) - if weight_method == 'attn' or weight_method == 'fastattn': + if weight_method == "attn" or weight_method == "fastattn": # WSM self.edge_weights = nn.Parameter(torch.ones(len(inputs_offsets)), requires_grad=True) else: @@ -210,62 +271,93 @@ def forward(self, x): input_node = self.resample[str(offset)](input_node) nodes.append(input_node) - if self.weight_method == 'attn': + if self.weight_method == "attn": normalized_weights = torch.softmax(self.edge_weights.type(dtype), dim=0) x = torch.stack(nodes, dim=-1) * normalized_weights - elif self.weight_method == 'fastattn': + elif self.weight_method == "fastattn": edge_weights = nn.functional.relu(self.edge_weights.type(dtype)) weights_sum = torch.sum(edge_weights) x = torch.stack( - [(nodes[i] * edge_weights[i]) / (weights_sum + 0.0001) for i in range(len(nodes))], dim=-1) - elif self.weight_method == 'sum': + [(nodes[i] * edge_weights[i]) / (weights_sum + 0.0001) for i in range(len(nodes))], + dim=-1, + ) + elif self.weight_method == "sum": x = torch.stack(nodes, dim=-1) else: - raise ValueError('unknown weight_method {}'.format(self.weight_method)) + raise ValueError(f"unknown weight_method {self.weight_method}") x = torch.sum(x, dim=-1) return x class BiFpnLayer(nn.Module): - def __init__(self, feature_info, fpn_config, fpn_channels, num_levels=5, pad_type='', - pooling_type='max', norm='', act_layer=Swish, - apply_bn_for_resampling=False, conv_after_downsample=True, conv_bn_relu_pattern=False, - separable_conv=True, redundant_bias=False): - super(BiFpnLayer, self).__init__() + def __init__( + self, + feature_info, + fpn_config, + fpn_channels, + num_levels: int=5, + pad_type: str="", + pooling_type: str="max", + norm: str="", + act_layer=Swish, + apply_bn_for_resampling: bool=False, + conv_after_downsample: bool=True, + conv_bn_relu_pattern: bool=False, + separable_conv: bool=True, + redundant_bias: bool=False, + ) -> None: + super().__init__() self.fpn_config = fpn_config self.num_levels = num_levels self.conv_bn_relu_pattern = False self.feature_info = [] self.fnode = SequentialAppend() - for i, fnode_cfg in enumerate(fpn_config['nodes']): + for i, fnode_cfg in enumerate(fpn_config["nodes"]): # logging.debug('fnode {} : {}'.format(i, fnode_cfg)) # print('fnode {} : {}'.format(i, fnode_cfg)) fnode_layers = OrderedDict() # combine features - reduction = fnode_cfg['reduction'] - fnode_layers['combine'] = FpnCombine( - feature_info, fpn_config, fpn_channels, fnode_cfg['inputs_offsets'], target_reduction=reduction, - pad_type=pad_type, pooling_type=pooling_type, norm=norm, - apply_bn_for_resampling=apply_bn_for_resampling, conv_after_downsample=conv_after_downsample, - redundant_bias=redundant_bias, weight_method=fpn_config['weight_method']) + reduction = fnode_cfg["reduction"] + fnode_layers["combine"] = FpnCombine( + feature_info, + fpn_config, + fpn_channels, + fnode_cfg["inputs_offsets"], + target_reduction=reduction, + pad_type=pad_type, + pooling_type=pooling_type, + norm=norm, + apply_bn_for_resampling=apply_bn_for_resampling, + conv_after_downsample=conv_after_downsample, + redundant_bias=redundant_bias, + weight_method=fpn_config["weight_method"], + ) self.feature_info.append(dict(num_chs=fpn_channels, reduction=reduction)) # after combine ops after_combine = OrderedDict() if not conv_bn_relu_pattern: - after_combine['act'] = act_layer(inplace=True) + after_combine["act"] = act_layer(inplace=True) conv_bias = redundant_bias conv_act = None else: conv_bias = False conv_act = act_layer conv_kwargs = dict( - in_channels=fpn_channels, out_channels=fpn_channels, kernel_size=3, padding=pad_type, - bias=conv_bias, norm=norm, act_layer=conv_act) - after_combine['conv'] = SeparableConv2d(**conv_kwargs) if separable_conv else ConvBnAct2d(**conv_kwargs) - fnode_layers['after_combine'] = nn.Sequential(after_combine) + in_channels=fpn_channels, + out_channels=fpn_channels, + kernel_size=3, + padding=pad_type, + bias=conv_bias, + norm=norm, + act_layer=conv_act, + ) + after_combine["conv"] = ( + SeparableConv2d(**conv_kwargs) if separable_conv else ConvBnAct2d(**conv_kwargs) + ) + fnode_layers["after_combine"] = nn.Sequential(after_combine) self.fnode.add_module(str(i), nn.Sequential(fnode_layers)) @@ -273,17 +365,24 @@ def __init__(self, feature_info, fpn_config, fpn_channels, num_levels=5, pad_typ def forward(self, x): x = self.fnode(x) - return x[-self.num_levels::] + return x[-self.num_levels : :] class BiFPN(Backbone): def __init__( - self, cfg, bottom_up, in_features, out_channels, norm='', - num_levels=5, num_bifpn=4, separable_conv=False, - ): - super(BiFPN, self).__init__() + self, + cfg, + bottom_up, + in_features, + out_channels, + norm: str="", + num_levels: int=5, + num_bifpn: int=4, + separable_conv: bool=False, + ) -> None: + super().__init__() assert isinstance(bottom_up, Backbone) - + # Feature map strides and channels from the bottom up network (e.g. ResNet) input_shapes = bottom_up.output_shape() in_strides = [input_shapes[f].stride for f in in_features] @@ -295,20 +394,19 @@ def __init__( self.in_features = in_features self._size_divisibility = 128 levels = [int(math.log2(s)) for s in in_strides] - self._out_feature_strides = { - "p{}".format(int(math.log2(s))): s for s in in_strides} + self._out_feature_strides = {f"p{int(math.log2(s))}": s for s in in_strides} if len(in_features) < num_levels: for l in range(num_levels - len(in_features)): s = l + levels[-1] - self._out_feature_strides["p{}".format(s + 1)] = 2 ** (s + 1) + self._out_feature_strides[f"p{s + 1}"] = 2 ** (s + 1) self._out_features = list(sorted(self._out_feature_strides.keys())) self._out_feature_channels = {k: out_channels for k in self._out_features} - + # print('self._out_feature_strides', self._out_feature_strides) # print('self._out_feature_channels', self._out_feature_channels) - + feature_info = [ - {'num_chs': in_channels[level], 'reduction': in_strides[level]} \ + {"num_chs": in_channels[level], "reduction": in_strides[level]} for level in range(len(self.in_features)) ] # self.config = config @@ -316,22 +414,25 @@ def __init__( self.resample = SequentialAppendLast() for level in range(num_levels): if level < len(feature_info): - in_chs = in_channels[level] # feature_info[level]['num_chs'] - reduction = in_strides[level] # feature_info[level]['reduction'] + in_chs = in_channels[level] # feature_info[level]['num_chs'] + reduction = in_strides[level] # feature_info[level]['reduction'] else: # Adds a coarser level by downsampling the last feature map reduction_ratio = 2 - self.resample.add_module(str(level), ResampleFeatureMap( - in_channels=in_chs, - out_channels=out_channels, - pad_type='same', - pooling_type=None, - norm=norm, - reduction_ratio=reduction_ratio, - apply_bn=True, - conv_after_downsample=False, - redundant_bias=False, - )) + self.resample.add_module( + str(level), + ResampleFeatureMap( + in_channels=in_chs, + out_channels=out_channels, + pad_type="same", + pooling_type=None, + norm=norm, + reduction_ratio=reduction_ratio, + apply_bn=True, + conv_after_downsample=False, + redundant_bias=False, + ), + ) in_chs = out_channels reduction = int(reduction * reduction_ratio) feature_info.append(dict(num_chs=in_chs, reduction=reduction)) @@ -345,7 +446,7 @@ def __init__( fpn_config=fpn_config, fpn_channels=out_channels, num_levels=self.num_levels, - pad_type='same', + pad_type="same", pooling_type=None, norm=norm, act_layer=Swish, @@ -369,10 +470,10 @@ def forward(self, x): x = [bottom_up_features[f] for f in self.in_features] assert len(self.resample) == self.num_levels - len(x) x = self.resample(x) - shapes = [xx.shape for xx in x] + [xx.shape for xx in x] # print('resample shapes', shapes) x = self.cell(x) - out = {f: xx for f, xx in zip(self._out_features, x)} + out = {f: xx for f, xx in zip(self._out_features, x, strict=False)} # import pdb; pdb.set_trace() return out @@ -400,6 +501,7 @@ def build_resnet_bifpn_backbone(cfg, input_shape: ShapeSpec): ) return backbone + @BACKBONE_REGISTRY.register() def build_p37_dla_bifpn_backbone(cfg, input_shape: ShapeSpec): """ diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/bifpn_fcos.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/bifpn_fcos.py index 17f2904cca..27ad4e62fc 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/bifpn_fcos.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/bifpn_fcos.py @@ -1,14 +1,14 @@ # This file is modified from https://github.com/aim-uofa/AdelaiDet/blob/master/adet/modeling/backbone/bifpn.py # The original file is under 2-clause BSD License for academic use, and *non-commercial use*. +from detectron2.layers import Conv2d, ShapeSpec, get_norm +from detectron2.modeling import BACKBONE_REGISTRY +from detectron2.modeling.backbone import Backbone, build_resnet_backbone import torch -import torch.nn.functional as F from torch import nn +import torch.nn.functional as F -from detectron2.layers import Conv2d, ShapeSpec, get_norm - -from detectron2.modeling.backbone import Backbone, build_resnet_backbone -from detectron2.modeling import BACKBONE_REGISTRY from .dlafpn import dla34 +from typing import Sequence __all__ = [] @@ -17,7 +17,7 @@ def swish(x): return x * x.sigmoid() -def split_name(name): +def split_name(name: str): for i, c in enumerate(name): if not c.isalpha(): return name[:i], int(name[i:]) @@ -25,14 +25,16 @@ def split_name(name): class FeatureMapResampler(nn.Module): - def __init__(self, in_channels, out_channels, stride, norm=""): - super(FeatureMapResampler, self).__init__() + def __init__(self, in_channels, out_channels, stride: int, norm: str="") -> None: + super().__init__() if in_channels != out_channels: self.reduction = Conv2d( - in_channels, out_channels, kernel_size=1, + in_channels, + out_channels, + kernel_size=1, bias=(norm == ""), norm=get_norm(norm, out_channels), - activation=None + activation=None, ) else: self.reduction = None @@ -45,10 +47,7 @@ def forward(self, x): x = self.reduction(x) if self.stride == 2: - x = F.max_pool2d( - x, kernel_size=self.stride + 1, - stride=self.stride, padding=1 - ) + x = F.max_pool2d(x, kernel_size=self.stride + 1, stride=self.stride, padding=1) elif self.stride == 1: pass else: @@ -57,13 +56,17 @@ def forward(self, x): class BackboneWithTopLevels(Backbone): - def __init__(self, backbone, out_channels, num_top_levels, norm=""): - super(BackboneWithTopLevels, self).__init__() + def __init__(self, backbone, out_channels, num_top_levels: int, norm: str="") -> None: + super().__init__() self.backbone = backbone backbone_output_shape = backbone.output_shape() - self._out_feature_channels = {name: shape.channels for name, shape in backbone_output_shape.items()} - self._out_feature_strides = {name: shape.stride for name, shape in backbone_output_shape.items()} + self._out_feature_channels = { + name: shape.channels for name, shape in backbone_output_shape.items() + } + self._out_feature_strides = { + name: shape.stride for name, shape in backbone_output_shape.items() + } self._out_features = list(self._out_feature_strides.keys()) last_feature_name = max(self._out_feature_strides.keys(), key=lambda x: split_name(x)[1]) @@ -77,9 +80,7 @@ def __init__(self, backbone, out_channels, num_top_levels, norm=""): prev_channels = last_channels for i in range(num_top_levels): name = prefix + str(suffix + i + 1) - self.add_module(name, FeatureMapResampler( - prev_channels, out_channels, 2, norm - )) + self.add_module(name, FeatureMapResampler(prev_channels, out_channels, 2, norm)) prev_channels = out_channels self._out_feature_channels[name] = out_channels @@ -106,9 +107,7 @@ class SingleBiFPN(Backbone): It creates pyramid features built on top of some input feature maps. """ - def __init__( - self, in_channels_list, out_channels, norm="" - ): + def __init__(self, in_channels_list, out_channels, norm: str="") -> None: """ Args: bottom_up (Backbone): module representing the bottom up subnetwork. @@ -122,27 +121,27 @@ def __init__( out_channels (int): number of channels in the output feature maps. norm (str): the normalization to use. """ - super(SingleBiFPN, self).__init__() + super().__init__() self.out_channels = out_channels # build 5-levels bifpn if len(in_channels_list) == 5: self.nodes = [ - {'feat_level': 3, 'inputs_offsets': [3, 4]}, - {'feat_level': 2, 'inputs_offsets': [2, 5]}, - {'feat_level': 1, 'inputs_offsets': [1, 6]}, - {'feat_level': 0, 'inputs_offsets': [0, 7]}, - {'feat_level': 1, 'inputs_offsets': [1, 7, 8]}, - {'feat_level': 2, 'inputs_offsets': [2, 6, 9]}, - {'feat_level': 3, 'inputs_offsets': [3, 5, 10]}, - {'feat_level': 4, 'inputs_offsets': [4, 11]}, + {"feat_level": 3, "inputs_offsets": [3, 4]}, + {"feat_level": 2, "inputs_offsets": [2, 5]}, + {"feat_level": 1, "inputs_offsets": [1, 6]}, + {"feat_level": 0, "inputs_offsets": [0, 7]}, + {"feat_level": 1, "inputs_offsets": [1, 7, 8]}, + {"feat_level": 2, "inputs_offsets": [2, 6, 9]}, + {"feat_level": 3, "inputs_offsets": [3, 5, 10]}, + {"feat_level": 4, "inputs_offsets": [4, 11]}, ] elif len(in_channels_list) == 3: self.nodes = [ - {'feat_level': 1, 'inputs_offsets': [1, 2]}, - {'feat_level': 0, 'inputs_offsets': [0, 3]}, - {'feat_level': 1, 'inputs_offsets': [1, 3, 4]}, - {'feat_level': 2, 'inputs_offsets': [2, 5]}, + {"feat_level": 1, "inputs_offsets": [1, 2]}, + {"feat_level": 0, "inputs_offsets": [0, 3]}, + {"feat_level": 1, "inputs_offsets": [1, 3, 4]}, + {"feat_level": 2, "inputs_offsets": [2, 5]}, ] else: raise NotImplementedError @@ -160,34 +159,34 @@ def __init__( in_channels = node_info[input_offset] if in_channels != out_channels: lateral_conv = Conv2d( - in_channels, - out_channels, - kernel_size=1, - norm=get_norm(norm, out_channels) - ) - self.add_module( - "lateral_{}_f{}".format(input_offset, feat_level), lateral_conv + in_channels, out_channels, kernel_size=1, norm=get_norm(norm, out_channels) ) + self.add_module(f"lateral_{input_offset}_f{feat_level}", lateral_conv) node_info.append(out_channels) num_output_connections.append(0) # generate attention weights - name = "weights_f{}_{}".format(feat_level, inputs_offsets_str) - self.__setattr__(name, nn.Parameter( - torch.ones(len(inputs_offsets), dtype=torch.float32), - requires_grad=True - )) + name = f"weights_f{feat_level}_{inputs_offsets_str}" + self.__setattr__( + name, + nn.Parameter( + torch.ones(len(inputs_offsets), dtype=torch.float32), requires_grad=True + ), + ) # generate convolutions after combination - name = "outputs_f{}_{}".format(feat_level, inputs_offsets_str) - self.add_module(name, Conv2d( - out_channels, - out_channels, - kernel_size=3, - padding=1, - norm=get_norm(norm, out_channels), - bias=(norm == "") - )) + name = f"outputs_f{feat_level}_{inputs_offsets_str}" + self.add_module( + name, + Conv2d( + out_channels, + out_channels, + kernel_size=3, + padding=1, + norm=get_norm(norm, out_channels), + bias=(norm == ""), + ), + ) def forward(self, feats): """ @@ -216,7 +215,7 @@ def forward(self, feats): # reduction if input_node.size(1) != self.out_channels: - name = "lateral_{}_f{}".format(input_offset, feat_level) + name = f"lateral_{input_offset}_f{feat_level}" input_node = self.__getattr__(name)(input_node) # maybe downsample @@ -226,22 +225,22 @@ def forward(self, feats): width_stride_size = int((w - 1) // target_w + 1) assert height_stride_size == width_stride_size == 2 input_node = F.max_pool2d( - input_node, kernel_size=(height_stride_size + 1, width_stride_size + 1), - stride=(height_stride_size, width_stride_size), padding=1 + input_node, + kernel_size=(height_stride_size + 1, width_stride_size + 1), + stride=(height_stride_size, width_stride_size), + padding=1, ) elif h <= target_h and w <= target_w: if h < target_h or w < target_w: input_node = F.interpolate( - input_node, - size=(target_h, target_w), - mode="nearest" + input_node, size=(target_h, target_w), mode="nearest" ) else: raise NotImplementedError() input_nodes.append(input_node) # attention - name = "weights_f{}_{}".format(feat_level, inputs_offsets_str) + name = f"weights_f{feat_level}_{inputs_offsets_str}" weights = F.relu(self.__getattr__(name)) norm_weights = weights / (weights.sum() + 0.0001) @@ -249,7 +248,7 @@ def forward(self, feats): new_node = (norm_weights * new_node).sum(dim=-1) new_node = swish(new_node) - name = "outputs_f{}_{}".format(feat_level, inputs_offsets_str) + name = f"outputs_f{feat_level}_{inputs_offsets_str}" feats.append(self.__getattr__(name)(new_node)) num_output_connections.append(0) @@ -257,7 +256,7 @@ def forward(self, feats): output_feats = [] for idx in range(num_levels): for i, fnode in enumerate(reversed(self.nodes)): - if fnode['feat_level'] == idx: + if fnode["feat_level"] == idx: output_feats.append(feats[-1 - i]) break else: @@ -271,9 +270,7 @@ class BiFPN(Backbone): It creates pyramid features built on top of some input feature maps. """ - def __init__( - self, bottom_up, in_features, out_channels, num_top_levels, num_repeats, norm="" - ): + def __init__(self, bottom_up, in_features, out_channels, num_top_levels: int, num_repeats: int, norm: str="") -> None: """ Args: bottom_up (Backbone): module representing the bottom up subnetwork. @@ -289,18 +286,15 @@ def __init__( num_repeats (int): the number of repeats of BiFPN. norm (str): the normalization to use. """ - super(BiFPN, self).__init__() + super().__init__() assert isinstance(bottom_up, Backbone) # add extra feature levels (i.e., 6 and 7) - self.bottom_up = BackboneWithTopLevels( - bottom_up, out_channels, - num_top_levels, norm - ) + self.bottom_up = BackboneWithTopLevels(bottom_up, out_channels, num_top_levels, norm) bottom_up_output_shapes = self.bottom_up.output_shape() in_features = sorted(in_features, key=lambda x: split_name(x)[1]) - self._size_divisibility = 128 #bottom_up_output_shapes[in_features[-1]].stride + self._size_divisibility = 128 # bottom_up_output_shapes[in_features[-1]].stride self.out_channels = out_channels self.min_level = split_name(in_features[0])[1] @@ -311,10 +305,10 @@ def __init__( self.in_features = in_features # generate output features - self._out_features = ["p{}".format(split_name(name)[1]) for name in in_features] + self._out_features = [f"p{split_name(name)[1]}" for name in in_features] self._out_feature_strides = { out_name: bottom_up_output_shapes[in_name].stride - for out_name, in_name in zip(self._out_features, in_features) + for out_name, in_name in zip(self._out_features, in_features, strict=False) } self._out_feature_channels = {k: out_channels for k in self._out_features} @@ -322,16 +316,10 @@ def __init__( self.repeated_bifpn = nn.ModuleList() for i in range(num_repeats): if i == 0: - in_channels_list = [ - bottom_up_output_shapes[name].channels for name in in_features - ] + in_channels_list = [bottom_up_output_shapes[name].channels for name in in_features] else: - in_channels_list = [ - self._out_feature_channels[name] for name in self._out_features - ] - self.repeated_bifpn.append(SingleBiFPN( - in_channels_list, out_channels, norm - )) + in_channels_list = [self._out_feature_channels[name] for name in self._out_features] + self.repeated_bifpn.append(SingleBiFPN(in_channels_list, out_channels, norm)) @property def size_divisibility(self): @@ -353,19 +341,17 @@ def forward(self, x): feats = [bottom_up_features[f] for f in self.in_features] for bifpn in self.repeated_bifpn: - feats = bifpn(feats) + feats = bifpn(feats) - return dict(zip(self._out_features, feats)) + return dict(zip(self._out_features, feats, strict=False)) -def _assert_strides_are_log2_contiguous(strides): +def _assert_strides_are_log2_contiguous(strides: Sequence[int]) -> None: """ Assert that each stride is 2x times its preceding stride, i.e. "contiguous in log2". """ for i, stride in enumerate(strides[1:], 1): - assert stride == 2 * strides[i - 1], "Strides {} {} are not log2 contiguous".format( - stride, strides[i - 1] - ) + assert stride == 2 * strides[i - 1], f"Strides {stride} {strides[i - 1]} are not log2 contiguous" @BACKBONE_REGISTRY.register() @@ -388,12 +374,11 @@ def build_fcos_resnet_bifpn_backbone(cfg, input_shape: ShapeSpec): out_channels=out_channels, num_top_levels=top_levels, num_repeats=num_repeats, - norm=cfg.MODEL.BIFPN.NORM + norm=cfg.MODEL.BIFPN.NORM, ) return backbone - @BACKBONE_REGISTRY.register() def build_p35_fcos_resnet_bifpn_backbone(cfg, input_shape: ShapeSpec): """ @@ -414,7 +399,7 @@ def build_p35_fcos_resnet_bifpn_backbone(cfg, input_shape: ShapeSpec): out_channels=out_channels, num_top_levels=top_levels, num_repeats=num_repeats, - norm=cfg.MODEL.BIFPN.NORM + norm=cfg.MODEL.BIFPN.NORM, ) return backbone @@ -439,10 +424,11 @@ def build_p35_fcos_dla_bifpn_backbone(cfg, input_shape: ShapeSpec): out_channels=out_channels, num_top_levels=top_levels, num_repeats=num_repeats, - norm=cfg.MODEL.BIFPN.NORM + norm=cfg.MODEL.BIFPN.NORM, ) return backbone + @BACKBONE_REGISTRY.register() def build_p37_fcos_dla_bifpn_backbone(cfg, input_shape: ShapeSpec): """ @@ -464,6 +450,6 @@ def build_p37_fcos_dla_bifpn_backbone(cfg, input_shape: ShapeSpec): out_channels=out_channels, num_top_levels=top_levels, num_repeats=num_repeats, - norm=cfg.MODEL.BIFPN.NORM + norm=cfg.MODEL.BIFPN.NORM, ) - return backbone \ No newline at end of file + return backbone diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/dla.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/dla.py index 9f15f84035..8b6464153b 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/dla.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/dla.py @@ -1,54 +1,59 @@ -import numpy as np import math from os.path import join -import fvcore.nn.weight_init as weight_init -import torch -import torch.nn.functional as F -from torch import nn -import torch.utils.model_zoo as model_zoo -from detectron2.modeling.backbone.resnet import ( - BasicStem, BottleneckBlock, DeformBottleneckBlock) from detectron2.layers import ( Conv2d, DeformConv, - FrozenBatchNorm2d, ModulatedDeformConv, ShapeSpec, get_norm, ) - from detectron2.modeling.backbone.backbone import Backbone from detectron2.modeling.backbone.build import BACKBONE_REGISTRY from detectron2.modeling.backbone.fpn import FPN +from detectron2.modeling.backbone.resnet import BasicStem, BottleneckBlock, DeformBottleneckBlock +import fvcore.nn.weight_init as weight_init +import numpy as np +import torch +from torch import nn +import torch.nn.functional as F +import torch.utils.model_zoo as model_zoo __all__ = [ + "BasicStem", "BottleneckBlock", "DeformBottleneckBlock", - "BasicStem", ] DCNV1 = False HASH = { - 34: 'ba72cf86', - 60: '24839fc4', + 34: "ba72cf86", + 60: "24839fc4", } -def get_model_url(data, name, hash): - return join('http://dl.yf.io/dla/models', data, '{}-{}.pth'.format(name, hash)) + +def get_model_url(data, name: str, hash): + return join("http://dl.yf.io/dla/models", data, f"{name}-{hash}.pth") + class BasicBlock(nn.Module): - def __init__(self, inplanes, planes, stride=1, dilation=1, norm='BN'): - super(BasicBlock, self).__init__() - self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=3, - stride=stride, padding=dilation, - bias=False, dilation=dilation) + def __init__(self, inplanes, planes, stride: int=1, dilation: int=1, norm: str="BN") -> None: + super().__init__() + self.conv1 = nn.Conv2d( + inplanes, + planes, + kernel_size=3, + stride=stride, + padding=dilation, + bias=False, + dilation=dilation, + ) self.bn1 = get_norm(norm, planes) self.relu = nn.ReLU(inplace=True) - self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, - stride=1, padding=dilation, - bias=False, dilation=dilation) + self.conv2 = nn.Conv2d( + planes, planes, kernel_size=3, stride=1, padding=dilation, bias=False, dilation=dilation + ) self.bn2 = get_norm(norm, planes) self.stride = stride @@ -68,22 +73,27 @@ def forward(self, x, residual=None): return out + class Bottleneck(nn.Module): expansion = 2 - def __init__(self, inplanes, planes, stride=1, dilation=1, norm='BN'): - super(Bottleneck, self).__init__() + def __init__(self, inplanes, planes, stride: int=1, dilation: int=1, norm: str="BN") -> None: + super().__init__() expansion = Bottleneck.expansion bottle_planes = planes // expansion - self.conv1 = nn.Conv2d(inplanes, bottle_planes, - kernel_size=1, bias=False) + self.conv1 = nn.Conv2d(inplanes, bottle_planes, kernel_size=1, bias=False) self.bn1 = get_norm(norm, bottle_planes) - self.conv2 = nn.Conv2d(bottle_planes, bottle_planes, kernel_size=3, - stride=stride, padding=dilation, - bias=False, dilation=dilation) + self.conv2 = nn.Conv2d( + bottle_planes, + bottle_planes, + kernel_size=3, + stride=stride, + padding=dilation, + bias=False, + dilation=dilation, + ) self.bn2 = get_norm(norm, bottle_planes) - self.conv3 = nn.Conv2d(bottle_planes, planes, - kernel_size=1, bias=False) + self.conv3 = nn.Conv2d(bottle_planes, planes, kernel_size=1, bias=False) self.bn3 = get_norm(norm, planes) self.relu = nn.ReLU(inplace=True) self.stride = stride @@ -108,12 +118,13 @@ def forward(self, x, residual=None): return out + class Root(nn.Module): - def __init__(self, in_channels, out_channels, kernel_size, residual, norm='BN'): - super(Root, self).__init__() + def __init__(self, in_channels, out_channels, kernel_size: int, residual, norm: str="BN") -> None: + super().__init__() self.conv = nn.Conv2d( - in_channels, out_channels, 1, - stride=1, bias=False, padding=(kernel_size - 1) // 2) + in_channels, out_channels, 1, stride=1, bias=False, padding=(kernel_size - 1) // 2 + ) self.bn = get_norm(norm, out_channels) self.relu = nn.ReLU(inplace=True) self.residual = residual @@ -130,33 +141,54 @@ def forward(self, *x): class Tree(nn.Module): - def __init__(self, levels, block, in_channels, out_channels, stride=1, - level_root=False, root_dim=0, root_kernel_size=1, - dilation=1, root_residual=False, norm='BN'): - super(Tree, self).__init__() + def __init__( + self, + levels, + block, + in_channels, + out_channels, + stride: int=1, + level_root: bool=False, + root_dim: int=0, + root_kernel_size: int=1, + dilation: int=1, + root_residual: bool=False, + norm: str="BN", + ) -> None: + super().__init__() if root_dim == 0: root_dim = 2 * out_channels if level_root: root_dim += in_channels if levels == 1: - self.tree1 = block(in_channels, out_channels, stride, - dilation=dilation, norm=norm) - self.tree2 = block(out_channels, out_channels, 1, - dilation=dilation, norm=norm) + self.tree1 = block(in_channels, out_channels, stride, dilation=dilation, norm=norm) + self.tree2 = block(out_channels, out_channels, 1, dilation=dilation, norm=norm) else: - self.tree1 = Tree(levels - 1, block, in_channels, out_channels, - stride, root_dim=0, - root_kernel_size=root_kernel_size, - dilation=dilation, root_residual=root_residual, - norm=norm) - self.tree2 = Tree(levels - 1, block, out_channels, out_channels, - root_dim=root_dim + out_channels, - root_kernel_size=root_kernel_size, - dilation=dilation, root_residual=root_residual, - norm=norm) + self.tree1 = Tree( + levels - 1, + block, + in_channels, + out_channels, + stride, + root_dim=0, + root_kernel_size=root_kernel_size, + dilation=dilation, + root_residual=root_residual, + norm=norm, + ) + self.tree2 = Tree( + levels - 1, + block, + out_channels, + out_channels, + root_dim=root_dim + out_channels, + root_kernel_size=root_kernel_size, + dilation=dilation, + root_residual=root_residual, + norm=norm, + ) if levels == 1: - self.root = Root(root_dim, out_channels, root_kernel_size, - root_residual, norm=norm) + self.root = Root(root_dim, out_channels, root_kernel_size, root_residual, norm=norm) self.level_root = level_root self.root_dim = root_dim self.downsample = None @@ -166,9 +198,8 @@ def __init__(self, levels, block, in_channels, out_channels, stride=1, self.downsample = nn.MaxPool2d(stride, stride=stride) if in_channels != out_channels: self.project = nn.Sequential( - nn.Conv2d(in_channels, out_channels, - kernel_size=1, stride=1, bias=False), - get_norm(norm, out_channels) + nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, bias=False), + get_norm(norm, out_channels), ) def forward(self, x, residual=None, children=None): @@ -186,59 +217,96 @@ def forward(self, x, residual=None, children=None): x = self.tree2(x1, children=children) return x + class DLA(nn.Module): - def __init__(self, num_layers, levels, channels, - block=BasicBlock, residual_root=False, norm='BN'): + def __init__( + self, num_layers: int, levels, channels, block=BasicBlock, residual_root: bool=False, norm: str="BN" + ) -> None: """ Args: """ - super(DLA, self).__init__() + super().__init__() self.norm = norm self.channels = channels self.base_layer = nn.Sequential( - nn.Conv2d(3, channels[0], kernel_size=7, stride=1, - padding=3, bias=False), + nn.Conv2d(3, channels[0], kernel_size=7, stride=1, padding=3, bias=False), get_norm(self.norm, channels[0]), - nn.ReLU(inplace=True)) - self.level0 = self._make_conv_level( - channels[0], channels[0], levels[0]) - self.level1 = self._make_conv_level( - channels[0], channels[1], levels[1], stride=2) - self.level2 = Tree(levels[2], block, channels[1], channels[2], 2, - level_root=False, - root_residual=residual_root, norm=norm) - self.level3 = Tree(levels[3], block, channels[2], channels[3], 2, - level_root=True, root_residual=residual_root, - norm=norm) - self.level4 = Tree(levels[4], block, channels[3], channels[4], 2, - level_root=True, root_residual=residual_root, - norm=norm) - self.level5 = Tree(levels[5], block, channels[4], channels[5], 2, - level_root=True, root_residual=residual_root, - norm=norm) + nn.ReLU(inplace=True), + ) + self.level0 = self._make_conv_level(channels[0], channels[0], levels[0]) + self.level1 = self._make_conv_level(channels[0], channels[1], levels[1], stride=2) + self.level2 = Tree( + levels[2], + block, + channels[1], + channels[2], + 2, + level_root=False, + root_residual=residual_root, + norm=norm, + ) + self.level3 = Tree( + levels[3], + block, + channels[2], + channels[3], + 2, + level_root=True, + root_residual=residual_root, + norm=norm, + ) + self.level4 = Tree( + levels[4], + block, + channels[3], + channels[4], + 2, + level_root=True, + root_residual=residual_root, + norm=norm, + ) + self.level5 = Tree( + levels[5], + block, + channels[4], + channels[5], + 2, + level_root=True, + root_residual=residual_root, + norm=norm, + ) self.load_pretrained_model( - data='imagenet', name='dla{}'.format(num_layers), - hash=HASH[num_layers]) + data="imagenet", name=f"dla{num_layers}", hash=HASH[num_layers] + ) - def load_pretrained_model(self, data, name, hash): + def load_pretrained_model(self, data, name: str, hash) -> None: model_url = get_model_url(data, name, hash) model_weights = model_zoo.load_url(model_url) num_classes = len(model_weights[list(model_weights.keys())[-1]]) self.fc = nn.Conv2d( - self.channels[-1], num_classes, - kernel_size=1, stride=1, padding=0, bias=True) - print('Loading pretrained') + self.channels[-1], num_classes, kernel_size=1, stride=1, padding=0, bias=True + ) + print("Loading pretrained") self.load_state_dict(model_weights, strict=False) - def _make_conv_level(self, inplanes, planes, convs, stride=1, dilation=1): + def _make_conv_level(self, inplanes, planes, convs, stride: int=1, dilation: int=1): modules = [] for i in range(convs): - modules.extend([ - nn.Conv2d(inplanes, planes, kernel_size=3, - stride=stride if i == 0 else 1, - padding=dilation, bias=False, dilation=dilation), - get_norm(self.norm, planes), - nn.ReLU(inplace=True)]) + modules.extend( + [ + nn.Conv2d( + inplanes, + planes, + kernel_size=3, + stride=stride if i == 0 else 1, + padding=dilation, + bias=False, + dilation=dilation, + ), + get_norm(self.norm, planes), + nn.ReLU(inplace=True), + ] + ) inplanes = planes return nn.Sequential(*modules) @@ -246,47 +314,39 @@ def forward(self, x): y = [] x = self.base_layer(x) for i in range(6): - x = getattr(self, 'level{}'.format(i))(x) + x = getattr(self, f"level{i}")(x) y.append(x) return y -def fill_up_weights(up): +def fill_up_weights(up) -> None: w = up.weight.data f = math.ceil(w.size(2) / 2) - c = (2 * f - 1 - f % 2) / (2. * f) + c = (2 * f - 1 - f % 2) / (2.0 * f) for i in range(w.size(2)): for j in range(w.size(3)): - w[0, 0, i, j] = \ - (1 - math.fabs(i / f - c)) * (1 - math.fabs(j / f - c)) + w[0, 0, i, j] = (1 - math.fabs(i / f - c)) * (1 - math.fabs(j / f - c)) for c in range(1, w.size(0)): w[c, 0, :, :] = w[0, 0, :, :] class _DeformConv(nn.Module): - def __init__(self, chi, cho, norm='BN'): - super(_DeformConv, self).__init__() - self.actf = nn.Sequential( - get_norm(norm, cho), - nn.ReLU(inplace=True) - ) + def __init__(self, chi, cho, norm: str="BN") -> None: + super().__init__() + self.actf = nn.Sequential(get_norm(norm, cho), nn.ReLU(inplace=True)) if DCNV1: - self.offset = Conv2d( - chi, 18, kernel_size=3, stride=1, - padding=1, dilation=1) + self.offset = Conv2d(chi, 18, kernel_size=3, stride=1, padding=1, dilation=1) self.conv = DeformConv( - chi, cho, kernel_size=(3,3), stride=1, padding=1, - dilation=1, deformable_groups=1) + chi, cho, kernel_size=(3, 3), stride=1, padding=1, dilation=1, deformable_groups=1 + ) else: - self.offset = Conv2d( - chi, 27, kernel_size=3, stride=1, - padding=1, dilation=1) + self.offset = Conv2d(chi, 27, kernel_size=3, stride=1, padding=1, dilation=1) self.conv = ModulatedDeformConv( - chi, cho, kernel_size=3, stride=1, padding=1, - dilation=1, deformable_groups=1) + chi, cho, kernel_size=3, stride=1, padding=1, dilation=1, deformable_groups=1 + ) nn.init.constant_(self.offset.weight, 0) nn.init.constant_(self.offset.bias, 0) - + def forward(self, x): if DCNV1: offset = self.offset(x) @@ -302,36 +362,35 @@ def forward(self, x): class IDAUp(nn.Module): - def __init__(self, o, channels, up_f, norm='BN'): - super(IDAUp, self).__init__() + def __init__(self, o, channels, up_f, norm: str="BN") -> None: + super().__init__() for i in range(1, len(channels)): c = channels[i] - f = int(up_f[i]) + f = int(up_f[i]) proj = _DeformConv(c, o, norm=norm) node = _DeformConv(o, o, norm=norm) - - up = nn.ConvTranspose2d(o, o, f * 2, stride=f, - padding=f // 2, output_padding=0, - groups=o, bias=False) + + up = nn.ConvTranspose2d( + o, o, f * 2, stride=f, padding=f // 2, output_padding=0, groups=o, bias=False + ) fill_up_weights(up) - setattr(self, 'proj_' + str(i), proj) - setattr(self, 'up_' + str(i), up) - setattr(self, 'node_' + str(i), node) - - - def forward(self, layers, startp, endp): + setattr(self, "proj_" + str(i), proj) + setattr(self, "up_" + str(i), up) + setattr(self, "node_" + str(i), node) + + def forward(self, layers, startp, endp) -> None: for i in range(startp + 1, endp): - upsample = getattr(self, 'up_' + str(i - startp)) - project = getattr(self, 'proj_' + str(i - startp)) + upsample = getattr(self, "up_" + str(i - startp)) + project = getattr(self, "proj_" + str(i - startp)) layers[i] = upsample(project(layers[i])) - node = getattr(self, 'node_' + str(i - startp)) + node = getattr(self, "node_" + str(i - startp)) layers[i] = node(layers[i] + layers[i - 1]) class DLAUp(nn.Module): - def __init__(self, startp, channels, scales, in_channels=None, norm='BN'): - super(DLAUp, self).__init__() + def __init__(self, startp, channels, scales, in_channels=None, norm: str="BN") -> None: + super().__init__() self.startp = startp if in_channels is None: in_channels = channels @@ -340,56 +399,57 @@ def __init__(self, startp, channels, scales, in_channels=None, norm='BN'): scales = np.array(scales, dtype=int) for i in range(len(channels) - 1): j = -i - 2 - setattr(self, 'ida_{}'.format(i), - IDAUp(channels[j], in_channels[j:], - scales[j:] // scales[j], norm=norm)) - scales[j + 1:] = scales[j] - in_channels[j + 1:] = [channels[j] for _ in channels[j + 1:]] + setattr( + self, + f"ida_{i}", + IDAUp(channels[j], in_channels[j:], scales[j:] // scales[j], norm=norm), + ) + scales[j + 1 :] = scales[j] + in_channels[j + 1 :] = [channels[j] for _ in channels[j + 1 :]] def forward(self, layers): - out = [layers[-1]] # start with 32 + out = [layers[-1]] # start with 32 for i in range(len(layers) - self.startp - 1): - ida = getattr(self, 'ida_{}'.format(i)) - ida(layers, len(layers) -i - 2, len(layers)) + ida = getattr(self, f"ida_{i}") + ida(layers, len(layers) - i - 2, len(layers)) out.insert(0, layers[-1]) return out + DLA_CONFIGS = { 34: ([1, 1, 1, 2, 2, 1], [16, 32, 64, 128, 256, 512], BasicBlock), - 60: ([1, 1, 1, 2, 3, 1], [16, 32, 128, 256, 512, 1024], Bottleneck) + 60: ([1, 1, 1, 2, 3, 1], [16, 32, 128, 256, 512, 1024], Bottleneck), } class DLASeg(Backbone): - def __init__(self, num_layers, out_features, use_dla_up=True, - ms_output=False, norm='BN'): - super(DLASeg, self).__init__() + def __init__(self, num_layers: int, out_features, use_dla_up: bool=True, ms_output: bool=False, norm: str="BN") -> None: + super().__init__() # depth = 34 levels, channels, Block = DLA_CONFIGS[num_layers] - self.base = DLA(num_layers=num_layers, - levels=levels, channels=channels, block=Block, norm=norm) + self.base = DLA( + num_layers=num_layers, levels=levels, channels=channels, block=Block, norm=norm + ) down_ratio = 4 self.first_level = int(np.log2(down_ratio)) self.ms_output = ms_output self.last_level = 5 if not self.ms_output else 6 channels = self.base.channels - scales = [2 ** i for i in range(len(channels[self.first_level:]))] + scales = [2**i for i in range(len(channels[self.first_level :]))] self.use_dla_up = use_dla_up if self.use_dla_up: - self.dla_up = DLAUp( - self.first_level, channels[self.first_level:], scales, - norm=norm) + self.dla_up = DLAUp(self.first_level, channels[self.first_level :], scales, norm=norm) out_channel = channels[self.first_level] - if not self.ms_output: # stride 4 DLA + if not self.ms_output: # stride 4 DLA self.ida_up = IDAUp( - out_channel, channels[self.first_level:self.last_level], - [2 ** i for i in range(self.last_level - self.first_level)], - norm=norm) + out_channel, + channels[self.first_level : self.last_level], + [2**i for i in range(self.last_level - self.first_level)], + norm=norm, + ) self._out_features = out_features - self._out_feature_channels = { - 'dla{}'.format(i): channels[i] for i in range(6)} - self._out_feature_strides = { - 'dla{}'.format(i): 2 ** i for i in range(6)} + self._out_feature_channels = {f"dla{i}": channels[i] for i in range(6)} + self._out_feature_strides = {f"dla{i}": 2**i for i in range(6)} self._size_divisibility = 32 @property @@ -400,24 +460,24 @@ def forward(self, x): x = self.base(x) if self.use_dla_up: x = self.dla_up(x) - if not self.ms_output: # stride 4 dla + if not self.ms_output: # stride 4 dla y = [] for i in range(self.last_level - self.first_level): y.append(x[i].clone()) self.ida_up(y, 0, len(y)) ret = {} for i in range(self.last_level - self.first_level): - out_feature = 'dla{}'.format(i) + out_feature = f"dla{i}" if out_feature in self._out_features: ret[out_feature] = y[i] else: ret = {} st = self.first_level if self.use_dla_up else 0 for i in range(self.last_level - st): - out_feature = 'dla{}'.format(i + st) + out_feature = f"dla{i + st}" if out_feature in self._out_features: ret[out_feature] = x[i] - + return ret @@ -430,11 +490,13 @@ def build_dla_backbone(cfg, input_shape): ResNet: a :class:`ResNet` instance. """ return DLASeg( - out_features=cfg.MODEL.DLA.OUT_FEATURES, + out_features=cfg.MODEL.DLA.OUT_FEATURES, num_layers=cfg.MODEL.DLA.NUM_LAYERS, use_dla_up=cfg.MODEL.DLA.USE_DLA_UP, ms_output=cfg.MODEL.DLA.MS_OUTPUT, - norm=cfg.MODEL.DLA.NORM) + norm=cfg.MODEL.DLA.NORM, + ) + class LastLevelP6P7(nn.Module): """ @@ -442,7 +504,7 @@ class LastLevelP6P7(nn.Module): C5 feature. """ - def __init__(self, in_channels, out_channels): + def __init__(self, in_channels, out_channels) -> None: super().__init__() self.num_levels = 2 self.in_feature = "dla5" @@ -456,6 +518,7 @@ def forward(self, c5): p7 = self.p7(F.relu(p6)) return [p6, p7] + @BACKBONE_REGISTRY.register() def build_retinanet_dla_fpn_backbone(cfg, input_shape: ShapeSpec): """ @@ -467,7 +530,7 @@ def build_retinanet_dla_fpn_backbone(cfg, input_shape: ShapeSpec): bottom_up = build_dla_backbone(cfg, input_shape) in_features = cfg.MODEL.FPN.IN_FEATURES out_channels = cfg.MODEL.FPN.OUT_CHANNELS - in_channels_p6p7 = bottom_up.output_shape()['dla5'].channels + in_channels_p6p7 = bottom_up.output_shape()["dla5"].channels backbone = FPN( bottom_up=bottom_up, in_features=in_features, diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/dlafpn.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/dlafpn.py index 2a33c66bf3..54f05bf719 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/dlafpn.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/dlafpn.py @@ -1,49 +1,51 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # this file is from https://github.com/ucbdrive/dla/blob/master/dla.py. import math from os.path import join -import numpy as np +from detectron2.layers import Conv2d, ModulatedDeformConv, ShapeSpec +from detectron2.layers.batch_norm import get_norm +from detectron2.modeling.backbone import FPN, Backbone +from detectron2.modeling.backbone.build import BACKBONE_REGISTRY +import fvcore.nn.weight_init as weight_init +import numpy as np import torch from torch import nn -import torch.utils.model_zoo as model_zoo import torch.nn.functional as F -import fvcore.nn.weight_init as weight_init - -from detectron2.modeling.backbone import FPN -from detectron2.layers import ShapeSpec, ModulatedDeformConv, Conv2d -from detectron2.modeling.backbone.build import BACKBONE_REGISTRY -from detectron2.layers.batch_norm import get_norm -from detectron2.modeling.backbone import Backbone +import torch.utils.model_zoo as model_zoo +from typing import Optional -WEB_ROOT = 'http://dl.yf.io/dla/models' +WEB_ROOT = "http://dl.yf.io/dla/models" -def get_model_url(data, name, hash): - return join( - 'http://dl.yf.io/dla/models', data, '{}-{}.pth'.format(name, hash)) +def get_model_url(data, name: str, hash): + return join("http://dl.yf.io/dla/models", data, f"{name}-{hash}.pth") -def conv3x3(in_planes, out_planes, stride=1): +def conv3x3(in_planes, out_planes, stride: int=1): "3x3 convolution with padding" - return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride, - padding=1, bias=False) + return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride, padding=1, bias=False) class BasicBlock(nn.Module): - def __init__(self, cfg, inplanes, planes, stride=1, dilation=1): - super(BasicBlock, self).__init__() - self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=3, - stride=stride, padding=dilation, - bias=False, dilation=dilation) + def __init__(self, cfg, inplanes, planes, stride: int=1, dilation: int=1) -> None: + super().__init__() + self.conv1 = nn.Conv2d( + inplanes, + planes, + kernel_size=3, + stride=stride, + padding=dilation, + bias=False, + dilation=dilation, + ) self.bn1 = get_norm(cfg.MODEL.DLA.NORM, planes) self.relu = nn.ReLU(inplace=True) - self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, - stride=1, padding=dilation, - bias=False, dilation=dilation) + self.conv2 = nn.Conv2d( + planes, planes, kernel_size=3, stride=1, padding=dilation, bias=False, dilation=dilation + ) self.bn2 = get_norm(cfg.MODEL.DLA.NORM, planes) self.stride = stride @@ -67,19 +69,23 @@ def forward(self, x, residual=None): class Bottleneck(nn.Module): expansion = 2 - def __init__(self, cfg, inplanes, planes, stride=1, dilation=1): - super(Bottleneck, self).__init__() + def __init__(self, cfg, inplanes, planes, stride: int=1, dilation: int=1) -> None: + super().__init__() expansion = Bottleneck.expansion bottle_planes = planes // expansion - self.conv1 = nn.Conv2d(inplanes, bottle_planes, - kernel_size=1, bias=False) + self.conv1 = nn.Conv2d(inplanes, bottle_planes, kernel_size=1, bias=False) self.bn1 = get_norm(cfg.MODEL.DLA.NORM, bottle_planes) - self.conv2 = nn.Conv2d(bottle_planes, bottle_planes, kernel_size=3, - stride=stride, padding=dilation, - bias=False, dilation=dilation) + self.conv2 = nn.Conv2d( + bottle_planes, + bottle_planes, + kernel_size=3, + stride=stride, + padding=dilation, + bias=False, + dilation=dilation, + ) self.bn2 = get_norm(cfg.MODEL.DLA.NORM, bottle_planes) - self.conv3 = nn.Conv2d(bottle_planes, planes, - kernel_size=1, bias=False) + self.conv3 = nn.Conv2d(bottle_planes, planes, kernel_size=1, bias=False) self.bn3 = get_norm(cfg.MODEL.DLA.NORM, planes) self.relu = nn.ReLU(inplace=True) self.stride = stride @@ -106,11 +112,16 @@ def forward(self, x, residual=None): class Root(nn.Module): - def __init__(self, cfg, in_channels, out_channels, kernel_size, residual): - super(Root, self).__init__() + def __init__(self, cfg, in_channels, out_channels, kernel_size: int, residual) -> None: + super().__init__() self.conv = nn.Conv2d( - in_channels, out_channels, kernel_size, - stride=1, bias=False, padding=(kernel_size - 1) // 2) + in_channels, + out_channels, + kernel_size, + stride=1, + bias=False, + padding=(kernel_size - 1) // 2, + ) self.bn = get_norm(cfg.MODEL.DLA.NORM, out_channels) self.relu = nn.ReLU(inplace=True) self.residual = residual @@ -127,31 +138,54 @@ def forward(self, *x): class Tree(nn.Module): - def __init__(self, cfg, levels, block, in_channels, out_channels, stride=1, - level_root=False, root_dim=0, root_kernel_size=1, - dilation=1, root_residual=False): - super(Tree, self).__init__() + def __init__( + self, + cfg, + levels, + block, + in_channels, + out_channels, + stride: int=1, + level_root: bool=False, + root_dim: int=0, + root_kernel_size: int=1, + dilation: int=1, + root_residual: bool=False, + ) -> None: + super().__init__() if root_dim == 0: root_dim = 2 * out_channels if level_root: root_dim += in_channels if levels == 1: - self.tree1 = block(cfg, in_channels, out_channels, stride, - dilation=dilation) - self.tree2 = block(cfg, out_channels, out_channels, 1, - dilation=dilation) + self.tree1 = block(cfg, in_channels, out_channels, stride, dilation=dilation) + self.tree2 = block(cfg, out_channels, out_channels, 1, dilation=dilation) else: - self.tree1 = Tree(cfg, levels - 1, block, in_channels, out_channels, - stride, root_dim=0, - root_kernel_size=root_kernel_size, - dilation=dilation, root_residual=root_residual) - self.tree2 = Tree(cfg, levels - 1, block, out_channels, out_channels, - root_dim=root_dim + out_channels, - root_kernel_size=root_kernel_size, - dilation=dilation, root_residual=root_residual) + self.tree1 = Tree( + cfg, + levels - 1, + block, + in_channels, + out_channels, + stride, + root_dim=0, + root_kernel_size=root_kernel_size, + dilation=dilation, + root_residual=root_residual, + ) + self.tree2 = Tree( + cfg, + levels - 1, + block, + out_channels, + out_channels, + root_dim=root_dim + out_channels, + root_kernel_size=root_kernel_size, + dilation=dilation, + root_residual=root_residual, + ) if levels == 1: - self.root = Root(cfg, root_dim, out_channels, root_kernel_size, - root_residual) + self.root = Root(cfg, root_dim, out_channels, root_kernel_size, root_residual) self.level_root = level_root self.root_dim = root_dim self.downsample = None @@ -161,9 +195,8 @@ def __init__(self, cfg, levels, block, in_channels, out_channels, stride=1, self.downsample = nn.MaxPool2d(stride, stride=stride) if in_channels != out_channels: self.project = nn.Sequential( - nn.Conv2d(in_channels, out_channels, - kernel_size=1, stride=1, bias=False), - get_norm(cfg.MODEL.DLA.NORM, out_channels) + nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, bias=False), + get_norm(cfg.MODEL.DLA.NORM, out_channels), ) def forward(self, x, residual=None, children=None): @@ -185,59 +218,96 @@ def forward(self, x, residual=None, children=None): class DLA(Backbone): - def __init__(self, cfg, levels, channels, block=BasicBlock, residual_root=False): - super(DLA, self).__init__() + def __init__(self, cfg, levels, channels, block=BasicBlock, residual_root: bool=False) -> None: + super().__init__() self.cfg = cfg self.channels = channels - self._out_features = ["dla{}".format(i) for i in range(6)] + self._out_features = [f"dla{i}" for i in range(6)] self._out_feature_channels = {k: channels[i] for i, k in enumerate(self._out_features)} - self._out_feature_strides = {k: 2 ** i for i, k in enumerate(self._out_features)} + self._out_feature_strides = {k: 2**i for i, k in enumerate(self._out_features)} self.base_layer = nn.Sequential( - nn.Conv2d(3, channels[0], kernel_size=7, stride=1, - padding=3, bias=False), + nn.Conv2d(3, channels[0], kernel_size=7, stride=1, padding=3, bias=False), get_norm(cfg.MODEL.DLA.NORM, channels[0]), - nn.ReLU(inplace=True)) - self.level0 = self._make_conv_level( - channels[0], channels[0], levels[0]) - self.level1 = self._make_conv_level( - channels[0], channels[1], levels[1], stride=2) - self.level2 = Tree(cfg, levels[2], block, channels[1], channels[2], 2, - level_root=False, - root_residual=residual_root) - self.level3 = Tree(cfg, levels[3], block, channels[2], channels[3], 2, - level_root=True, root_residual=residual_root) - self.level4 = Tree(cfg, levels[4], block, channels[3], channels[4], 2, - level_root=True, root_residual=residual_root) - self.level5 = Tree(cfg, levels[5], block, channels[4], channels[5], 2, - level_root=True, root_residual=residual_root) + nn.ReLU(inplace=True), + ) + self.level0 = self._make_conv_level(channels[0], channels[0], levels[0]) + self.level1 = self._make_conv_level(channels[0], channels[1], levels[1], stride=2) + self.level2 = Tree( + cfg, + levels[2], + block, + channels[1], + channels[2], + 2, + level_root=False, + root_residual=residual_root, + ) + self.level3 = Tree( + cfg, + levels[3], + block, + channels[2], + channels[3], + 2, + level_root=True, + root_residual=residual_root, + ) + self.level4 = Tree( + cfg, + levels[4], + block, + channels[3], + channels[4], + 2, + level_root=True, + root_residual=residual_root, + ) + self.level5 = Tree( + cfg, + levels[5], + block, + channels[4], + channels[5], + 2, + level_root=True, + root_residual=residual_root, + ) for m in self.modules(): if isinstance(m, nn.Conv2d): n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels - m.weight.data.normal_(0, math.sqrt(2. / n)) + m.weight.data.normal_(0, math.sqrt(2.0 / n)) - self.load_pretrained_model( - data='imagenet', name='dla34', hash='ba72cf86') + self.load_pretrained_model(data="imagenet", name="dla34", hash="ba72cf86") - def load_pretrained_model(self, data, name, hash): + def load_pretrained_model(self, data, name: str, hash) -> None: model_url = get_model_url(data, name, hash) model_weights = model_zoo.load_url(model_url) - del model_weights['fc.weight'] - del model_weights['fc.bias'] - print('Loading pretrained DLA!') + del model_weights["fc.weight"] + del model_weights["fc.bias"] + print("Loading pretrained DLA!") self.load_state_dict(model_weights, strict=True) - def _make_conv_level(self, inplanes, planes, convs, stride=1, dilation=1): + def _make_conv_level(self, inplanes, planes, convs, stride: int=1, dilation: int=1): modules = [] for i in range(convs): - modules.extend([ - nn.Conv2d(inplanes, planes, kernel_size=3, - stride=stride if i == 0 else 1, - padding=dilation, bias=False, dilation=dilation), - get_norm(self.cfg.MODEL.DLA.NORM, planes), - nn.ReLU(inplace=True)]) + modules.extend( + [ + nn.Conv2d( + inplanes, + planes, + kernel_size=3, + stride=stride if i == 0 else 1, + padding=dilation, + bias=False, + dilation=dilation, + ), + get_norm(self.cfg.MODEL.DLA.NORM, planes), + nn.ReLU(inplace=True), + ] + ) inplanes = planes return nn.Sequential(*modules) @@ -245,49 +315,44 @@ def forward(self, x): y = {} x = self.base_layer(x) for i in range(6): - name = 'level{}'.format(i) + name = f"level{i}" x = getattr(self, name)(x) - y['dla{}'.format(i)] = x + y[f"dla{i}"] = x return y -def fill_up_weights(up): +def fill_up_weights(up) -> None: w = up.weight.data f = math.ceil(w.size(2) / 2) - c = (2 * f - 1 - f % 2) / (2. * f) + c = (2 * f - 1 - f % 2) / (2.0 * f) for i in range(w.size(2)): for j in range(w.size(3)): - w[0, 0, i, j] = \ - (1 - math.fabs(i / f - c)) * (1 - math.fabs(j / f - c)) + w[0, 0, i, j] = (1 - math.fabs(i / f - c)) * (1 - math.fabs(j / f - c)) for c in range(1, w.size(0)): w[c, 0, :, :] = w[0, 0, :, :] class Conv(nn.Module): - def __init__(self, chi, cho, norm): - super(Conv, self).__init__() + def __init__(self, chi, cho, norm) -> None: + super().__init__() self.conv = nn.Sequential( nn.Conv2d(chi, cho, kernel_size=1, stride=1, bias=False), get_norm(norm, cho), - nn.ReLU(inplace=True)) - + nn.ReLU(inplace=True), + ) + def forward(self, x): return self.conv(x) class DeformConv(nn.Module): - def __init__(self, chi, cho, norm): - super(DeformConv, self).__init__() - self.actf = nn.Sequential( - get_norm(norm, cho), - nn.ReLU(inplace=True) - ) - self.offset = Conv2d( - chi, 27, kernel_size=3, stride=1, - padding=1, dilation=1) + def __init__(self, chi, cho, norm) -> None: + super().__init__() + self.actf = nn.Sequential(get_norm(norm, cho), nn.ReLU(inplace=True)) + self.offset = Conv2d(chi, 27, kernel_size=3, stride=1, padding=1, dilation=1) self.conv = ModulatedDeformConv( - chi, cho, kernel_size=3, stride=1, padding=1, - dilation=1, deformable_groups=1) + chi, cho, kernel_size=3, stride=1, padding=1, dilation=1, deformable_groups=1 + ) nn.init.constant_(self.offset.weight, 0) nn.init.constant_(self.offset.bias, 0) @@ -302,58 +367,58 @@ def forward(self, x): class IDAUp(nn.Module): - def __init__(self, o, channels, up_f, norm='FrozenBN', node_type=Conv): - super(IDAUp, self).__init__() + def __init__(self, o, channels, up_f, norm: str="FrozenBN", node_type=Conv) -> None: + super().__init__() for i in range(1, len(channels)): c = channels[i] - f = int(up_f[i]) + f = int(up_f[i]) proj = node_type(c, o, norm) node = node_type(o, o, norm) - - up = nn.ConvTranspose2d(o, o, f * 2, stride=f, - padding=f // 2, output_padding=0, - groups=o, bias=False) + + up = nn.ConvTranspose2d( + o, o, f * 2, stride=f, padding=f // 2, output_padding=0, groups=o, bias=False + ) fill_up_weights(up) - setattr(self, 'proj_' + str(i), proj) - setattr(self, 'up_' + str(i), up) - setattr(self, 'node_' + str(i), node) - - - def forward(self, layers, startp, endp): + setattr(self, "proj_" + str(i), proj) + setattr(self, "up_" + str(i), up) + setattr(self, "node_" + str(i), node) + + def forward(self, layers, startp, endp) -> None: for i in range(startp + 1, endp): - upsample = getattr(self, 'up_' + str(i - startp)) - project = getattr(self, 'proj_' + str(i - startp)) + upsample = getattr(self, "up_" + str(i - startp)) + project = getattr(self, "proj_" + str(i - startp)) layers[i] = upsample(project(layers[i])) - node = getattr(self, 'node_' + str(i - startp)) + node = getattr(self, "node_" + str(i - startp)) layers[i] = node(layers[i] + layers[i - 1]) DLAUP_NODE_MAP = { - 'conv': Conv, - 'dcn': DeformConv, + "conv": Conv, + "dcn": DeformConv, } + class DLAUP(Backbone): - def __init__(self, bottom_up, in_features, norm, dlaup_node='conv'): - super(DLAUP, self).__init__() + def __init__(self, bottom_up, in_features, norm, dlaup_node: str="conv") -> None: + super().__init__() assert isinstance(bottom_up, Backbone) self.bottom_up = bottom_up input_shapes = bottom_up.output_shape() in_strides = [input_shapes[f].stride for f in in_features] - in_channels = [input_shapes[f].channels for f in in_features] + in_channels = [input_shapes[f].channels for f in in_features] in_levels = [int(math.log2(input_shapes[f].stride)) for f in in_features] self.in_features = in_features - out_features = ['dlaup{}'.format(l) for l in in_levels] + out_features = [f"dlaup{l}" for l in in_levels] self._out_features = out_features self._out_feature_channels = { - 'dlaup{}'.format(l): in_channels[i] for i, l in enumerate(in_levels)} - self._out_feature_strides = { - 'dlaup{}'.format(l): 2 ** l for l in in_levels} + f"dlaup{l}": in_channels[i] for i, l in enumerate(in_levels) + } + self._out_feature_strides = {f"dlaup{l}": 2**l for l in in_levels} - print('self._out_features', self._out_features) - print('self._out_feature_channels', self._out_feature_channels) - print('self._out_feature_strides', self._out_feature_strides) + print("self._out_features", self._out_features) + print("self._out_feature_channels", self._out_feature_channels) + print("self._out_feature_strides", self._out_feature_strides) self._size_divisibility = 32 node_type = DLAUP_NODE_MAP[dlaup_node] @@ -361,16 +426,22 @@ def __init__(self, bottom_up, in_features, norm, dlaup_node='conv'): self.startp = int(math.log2(in_strides[0])) self.channels = in_channels channels = list(in_channels) - scales = np.array([2 ** i for i in range(len(out_features))], dtype=int) + scales = np.array([2**i for i in range(len(out_features))], dtype=int) for i in range(len(channels) - 1): j = -i - 2 - setattr(self, 'ida_{}'.format(i), - IDAUp(channels[j], in_channels[j:], - scales[j:] // scales[j], - norm=norm, - node_type=node_type)) - scales[j + 1:] = scales[j] - in_channels[j + 1:] = [channels[j] for _ in channels[j + 1:]] + setattr( + self, + f"ida_{i}", + IDAUp( + channels[j], + in_channels[j:], + scales[j:] // scales[j], + norm=norm, + node_type=node_type, + ), + ) + scales[j + 1 :] = scales[j] + in_channels[j + 1 :] = [channels[j] for _ in channels[j + 1 :]] @property def size_divisibility(self): @@ -379,22 +450,20 @@ def size_divisibility(self): def forward(self, x): bottom_up_features = self.bottom_up(x) layers = [bottom_up_features[f] for f in self.in_features] - out = [layers[-1]] # start with 32 + out = [layers[-1]] # start with 32 for i in range(len(layers) - 1): - ida = getattr(self, 'ida_{}'.format(i)) + ida = getattr(self, f"ida_{i}") ida(layers, len(layers) - i - 2, len(layers)) out.insert(0, layers[-1]) ret = {} - for k, v in zip(self._out_features, out): + for k, v in zip(self._out_features, out, strict=False): ret[k] = v # import pdb; pdb.set_trace() return ret -def dla34(cfg, pretrained=None): # DLA-34 - model = DLA(cfg, [1, 1, 1, 2, 2, 1], - [16, 32, 64, 128, 256, 512], - block=BasicBlock) +def dla34(cfg, pretrained: Optional[bool]=None): # DLA-34 + model = DLA(cfg, [1, 1, 1, 2, 2, 1], [16, 32, 64, 128, 256, 512], block=BasicBlock) return model @@ -404,7 +473,7 @@ class LastLevelP6P7(nn.Module): C5 feature. """ - def __init__(self, in_channels, out_channels): + def __init__(self, in_channels, out_channels) -> None: super().__init__() self.num_levels = 2 self.in_feature = "dla5" @@ -429,7 +498,7 @@ def build_dla_fpn3_backbone(cfg, input_shape: ShapeSpec): """ depth_to_creator = {"dla34": dla34} - bottom_up = depth_to_creator['dla{}'.format(cfg.MODEL.DLA.NUM_LAYERS)](cfg) + bottom_up = depth_to_creator[f"dla{cfg.MODEL.DLA.NUM_LAYERS}"](cfg) in_features = cfg.MODEL.FPN.IN_FEATURES out_channels = cfg.MODEL.FPN.OUT_CHANNELS @@ -444,6 +513,7 @@ def build_dla_fpn3_backbone(cfg, input_shape: ShapeSpec): return backbone + @BACKBONE_REGISTRY.register() def build_dla_fpn5_backbone(cfg, input_shape: ShapeSpec): """ @@ -454,10 +524,10 @@ def build_dla_fpn5_backbone(cfg, input_shape: ShapeSpec): """ depth_to_creator = {"dla34": dla34} - bottom_up = depth_to_creator['dla{}'.format(cfg.MODEL.DLA.NUM_LAYERS)](cfg) + bottom_up = depth_to_creator[f"dla{cfg.MODEL.DLA.NUM_LAYERS}"](cfg) in_features = cfg.MODEL.FPN.IN_FEATURES out_channels = cfg.MODEL.FPN.OUT_CHANNELS - in_channels_top = bottom_up.output_shape()['dla5'].channels + in_channels_top = bottom_up.output_shape()["dla5"].channels backbone = FPN( bottom_up=bottom_up, @@ -481,7 +551,7 @@ def build_dlaup_backbone(cfg, input_shape: ShapeSpec): """ depth_to_creator = {"dla34": dla34} - bottom_up = depth_to_creator['dla{}'.format(cfg.MODEL.DLA.NUM_LAYERS)](cfg) + bottom_up = depth_to_creator[f"dla{cfg.MODEL.DLA.NUM_LAYERS}"](cfg) backbone = DLAUP( bottom_up=bottom_up, diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/fpn_p5.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/fpn_p5.py index e991f9c7be..4ce285b6c6 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/fpn_p5.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/fpn_p5.py @@ -1,15 +1,11 @@ # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -import math -import fvcore.nn.weight_init as weight_init -import torch.nn.functional as F -from torch import nn - -from detectron2.layers import Conv2d, ShapeSpec, get_norm - -from detectron2.modeling.backbone import Backbone -from detectron2.modeling.backbone.fpn import FPN +from detectron2.layers import ShapeSpec from detectron2.modeling.backbone.build import BACKBONE_REGISTRY +from detectron2.modeling.backbone.fpn import FPN from detectron2.modeling.backbone.resnet import build_resnet_backbone +import fvcore.nn.weight_init as weight_init +from torch import nn +import torch.nn.functional as F class LastLevelP6P7_P5(nn.Module): @@ -18,7 +14,7 @@ class LastLevelP6P7_P5(nn.Module): C5 feature. """ - def __init__(self, in_channels, out_channels): + def __init__(self, in_channels, out_channels) -> None: super().__init__() self.num_levels = 2 self.in_feature = "p5" @@ -55,6 +51,7 @@ def build_p67_resnet_fpn_backbone(cfg, input_shape: ShapeSpec): ) return backbone + @BACKBONE_REGISTRY.register() def build_p35_resnet_fpn_backbone(cfg, input_shape: ShapeSpec): """ @@ -75,4 +72,4 @@ def build_p35_resnet_fpn_backbone(cfg, input_shape: ShapeSpec): top_block=None, fuse_type=cfg.MODEL.FPN.FUSE_TYPE, ) - return backbone \ No newline at end of file + return backbone diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/res2net.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/res2net.py index 1d0d40adb4..e04400032e 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/res2net.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/backbone/res2net.py @@ -1,12 +1,6 @@ # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved # This file is modified from https://github.com/Res2Net/Res2Net-detectron2/blob/master/detectron2/modeling/backbone/resnet.py # The original file is under Apache-2.0 License -import numpy as np -import fvcore.nn.weight_init as weight_init -import torch -import torch.nn.functional as F -from torch import nn - from detectron2.layers import ( CNNBlockBase, Conv2d, @@ -15,22 +9,28 @@ ShapeSpec, get_norm, ) - from detectron2.modeling.backbone import Backbone -from detectron2.modeling.backbone.fpn import FPN from detectron2.modeling.backbone.build import BACKBONE_REGISTRY -from .fpn_p5 import LastLevelP6P7_P5 +from detectron2.modeling.backbone.fpn import FPN +import fvcore.nn.weight_init as weight_init +import numpy as np +import torch +from torch import nn +import torch.nn.functional as F + from .bifpn import BiFPN +from .fpn_p5 import LastLevelP6P7_P5 +from typing import Optional __all__ = [ - "ResNetBlockBase", "BasicBlock", + "BasicStem", "BottleneckBlock", "DeformBottleneckBlock", - "BasicStem", "ResNet", - "make_stage", + "ResNetBlockBase", "build_res2net_backbone", + "make_stage", ] @@ -46,7 +46,7 @@ class BasicBlock(CNNBlockBase): and a projection shortcut if needed. """ - def __init__(self, in_channels, out_channels, *, stride=1, norm="BN"): + def __init__(self, in_channels, out_channels, *, stride: int=1, norm: str="BN") -> None: """ Args: in_channels (int): Number of input channels. @@ -119,14 +119,14 @@ def __init__( out_channels, *, bottleneck_channels, - stride=1, - num_groups=1, - norm="BN", - stride_in_1x1=False, - dilation=1, - basewidth=26, - scale=4, - ): + stride: int=1, + num_groups: int=1, + norm: str="BN", + stride_in_1x1: bool=False, + dilation: int=1, + basewidth: int=26, + scale: int=4, + ) -> None: """ Args: bottleneck_channels (int): number of output channels for the 3x3 @@ -142,8 +142,9 @@ def __init__( if in_channels != out_channels: self.shortcut = nn.Sequential( - nn.AvgPool2d(kernel_size=stride, stride=stride, - ceil_mode=True, count_include_pad=False), + nn.AvgPool2d( + kernel_size=stride, stride=stride, ceil_mode=True, count_include_pad=False + ), Conv2d( in_channels, out_channels, @@ -151,7 +152,7 @@ def __init__( stride=1, bias=False, norm=get_norm(norm, out_channels), - ) + ), ) else: self.shortcut = None @@ -160,7 +161,7 @@ def __init__( # The subsequent fb.torch.resnet and Caffe2 ResNe[X]t implementations have # stride in the 3x3 conv stride_1x1, stride_3x3 = (stride, 1) if stride_in_1x1 else (1, stride) - width = bottleneck_channels//scale + width = bottleneck_channels // scale self.conv1 = Conv2d( in_channels, @@ -171,25 +172,27 @@ def __init__( norm=get_norm(norm, bottleneck_channels), ) if scale == 1: - self.nums = 1 + self.nums = 1 else: - self.nums = scale -1 - if self.in_channels!=self.out_channels and stride_3x3!=2: - self.pool = nn.AvgPool2d(kernel_size=3, stride = stride_3x3, padding=1) + self.nums = scale - 1 + if self.in_channels != self.out_channels and stride_3x3 != 2: + self.pool = nn.AvgPool2d(kernel_size=3, stride=stride_3x3, padding=1) convs = [] bns = [] - for i in range(self.nums): - convs.append(nn.Conv2d( - width, - width, - kernel_size=3, - stride=stride_3x3, - padding=1 * dilation, - bias=False, - groups=num_groups, - dilation=dilation, - )) + for _i in range(self.nums): + convs.append( + nn.Conv2d( + width, + width, + kernel_size=3, + stride=stride_3x3, + padding=1 * dilation, + bias=False, + groups=num_groups, + dilation=dilation, + ) + ) bns.append(get_norm(norm, width)) self.convs = nn.ModuleList(convs) self.bns = nn.ModuleList(bns) @@ -213,7 +216,7 @@ def __init__( for layer in self.shortcut.modules(): if isinstance(layer, Conv2d): weight_init.c2_msra_fill(layer) - + for layer in self.convs: if layer is not None: # shortcut can be None weight_init.c2_msra_fill(layer) @@ -236,19 +239,19 @@ def forward(self, x): spx = torch.split(out, self.width, 1) for i in range(self.nums): - if i==0 or self.in_channels!=self.out_channels: + if i == 0 or self.in_channels != self.out_channels: sp = spx[i] else: sp = sp + spx[i] sp = self.convs[i](sp) sp = F.relu_(self.bns[i](sp)) - if i==0: + if i == 0: out = sp else: out = torch.cat((out, sp), 1) - if self.scale!=1 and self.stride_3x3==1: + if self.scale != 1 and self.stride_3x3 == 1: out = torch.cat((out, spx[self.nums]), 1) - elif self.scale != 1 and self.stride_3x3==2: + elif self.scale != 1 and self.stride_3x3 == 2: out = torch.cat((out, self.pool(spx[self.nums])), 1) out = self.conv3(out) @@ -275,16 +278,16 @@ def __init__( out_channels, *, bottleneck_channels, - stride=1, - num_groups=1, - norm="BN", - stride_in_1x1=False, - dilation=1, - deform_modulated=False, - deform_num_groups=1, - basewidth=26, - scale=4, - ): + stride: int=1, + num_groups: int=1, + norm: str="BN", + stride_in_1x1: bool=False, + dilation: int=1, + deform_modulated: bool=False, + deform_num_groups: int=1, + basewidth: int=26, + scale: int=4, + ) -> None: super().__init__(in_channels, out_channels, stride) self.deform_modulated = deform_modulated @@ -298,8 +301,9 @@ def __init__( # norm=get_norm(norm, out_channels), # ) self.shortcut = nn.Sequential( - nn.AvgPool2d(kernel_size=stride, stride=stride, - ceil_mode=True, count_include_pad=False), + nn.AvgPool2d( + kernel_size=stride, stride=stride, ceil_mode=True, count_include_pad=False + ), Conv2d( in_channels, out_channels, @@ -307,13 +311,13 @@ def __init__( stride=1, bias=False, norm=get_norm(norm, out_channels), - ) + ), ) else: self.shortcut = None stride_1x1, stride_3x3 = (stride, 1) if stride_in_1x1 else (1, stride) - width = bottleneck_channels//scale + width = bottleneck_channels // scale self.conv1 = Conv2d( in_channels, @@ -325,11 +329,11 @@ def __init__( ) if scale == 1: - self.nums = 1 + self.nums = 1 else: - self.nums = scale -1 - if self.in_channels!=self.out_channels and stride_3x3!=2: - self.pool = nn.AvgPool2d(kernel_size=3, stride = stride_3x3, padding=1) + self.nums = scale - 1 + if self.in_channels != self.out_channels and stride_3x3 != 2: + self.pool = nn.AvgPool2d(kernel_size=3, stride=stride_3x3, padding=1) if deform_modulated: deform_conv_op = ModulatedDeformConv @@ -363,28 +367,32 @@ def __init__( conv2_offsets = [] convs = [] bns = [] - for i in range(self.nums): - conv2_offsets.append(Conv2d( - width, - offset_channels * deform_num_groups, - kernel_size=3, - stride=stride_3x3, - padding=1 * dilation, - bias=False, - groups=num_groups, - dilation=dilation, - )) - convs.append(deform_conv_op( - width, - width, - kernel_size=3, - stride=stride_3x3, - padding=1 * dilation, - bias=False, - groups=num_groups, - dilation=dilation, - deformable_groups=deform_num_groups, - )) + for _i in range(self.nums): + conv2_offsets.append( + Conv2d( + width, + offset_channels * deform_num_groups, + kernel_size=3, + stride=stride_3x3, + padding=1 * dilation, + bias=False, + groups=num_groups, + dilation=dilation, + ) + ) + convs.append( + deform_conv_op( + width, + width, + kernel_size=3, + stride=stride_3x3, + padding=1 * dilation, + bias=False, + groups=num_groups, + dilation=dilation, + deformable_groups=deform_num_groups, + ) + ) bns.append(get_norm(norm, width)) self.conv2_offsets = nn.ModuleList(conv2_offsets) self.convs = nn.ModuleList(convs) @@ -415,7 +423,7 @@ def __init__( for layer in self.shortcut.modules(): if isinstance(layer, Conv2d): weight_init.c2_msra_fill(layer) - + for layer in self.convs: if layer is not None: # shortcut can be None weight_init.c2_msra_fill(layer) @@ -443,11 +451,11 @@ def forward(self, x): spx = torch.split(out, self.width, 1) for i in range(self.nums): - if i==0 or self.in_channels!=self.out_channels: + if i == 0 or self.in_channels != self.out_channels: sp = spx[i].contiguous() else: sp = sp + spx[i].contiguous() - + # sp = self.convs[i](sp) if self.deform_modulated: offset_mask = self.conv2_offsets[i](sp) @@ -459,13 +467,13 @@ def forward(self, x): offset = self.conv2_offsets[i](sp) sp = self.convs[i](sp, offset) sp = F.relu_(self.bns[i](sp)) - if i==0: + if i == 0: out = sp else: out = torch.cat((out, sp), 1) - if self.scale!=1 and self.stride_3x3==1: + if self.scale != 1 and self.stride_3x3 == 1: out = torch.cat((out, spx[self.nums]), 1) - elif self.scale != 1 and self.stride_3x3==2: + elif self.scale != 1 and self.stride_3x3 == 2: out = torch.cat((out, self.pool(spx[self.nums])), 1) out = self.conv3(out) @@ -480,7 +488,7 @@ def forward(self, x): return out -def make_stage(block_class, num_blocks, first_stride, *, in_channels, out_channels, **kwargs): +def make_stage(block_class, num_blocks: int, first_stride, *, in_channels, out_channels, **kwargs): """ Create a list of blocks just like those in a ResNet stage. Args: @@ -513,7 +521,7 @@ class BasicStem(CNNBlockBase): The standard ResNet stem (layers before the first residual block). """ - def __init__(self, in_channels=3, out_channels=64, norm="BN"): + def __init__(self, in_channels: int=3, out_channels: int=64, norm: str="BN") -> None: """ Args: norm (str or callable): norm after the first conv layer. @@ -529,7 +537,7 @@ def __init__(self, in_channels=3, out_channels=64, norm="BN"): stride=2, padding=1, bias=False, - ), + ), get_norm(norm, 32), nn.ReLU(inplace=True), Conv2d( @@ -539,7 +547,7 @@ def __init__(self, in_channels=3, out_channels=64, norm="BN"): stride=1, padding=1, bias=False, - ), + ), get_norm(norm, 32), nn.ReLU(inplace=True), Conv2d( @@ -549,7 +557,7 @@ def __init__(self, in_channels=3, out_channels=64, norm="BN"): stride=1, padding=1, bias=False, - ), + ), ) self.bn1 = get_norm(norm, out_channels) @@ -566,7 +574,7 @@ def forward(self, x): class ResNet(Backbone): - def __init__(self, stem, stages, num_classes=None, out_features=None): + def __init__(self, stem, stages, num_classes: Optional[int]=None, out_features=None) -> None: """ Args: stem (nn.Module): a stem module @@ -578,7 +586,7 @@ def __init__(self, stem, stages, num_classes=None, out_features=None): be returned in forward. Can be anything in "stem", "linear", or "res2" ... If None, will return the output of the last layer. """ - super(ResNet, self).__init__() + super().__init__() self.stem = stem self.num_classes = num_classes @@ -646,7 +654,7 @@ def output_shape(self): for name in self._out_features } - def freeze(self, freeze_at=0): + def freeze(self, freeze_at: int=0): """ Freeze the first several stages of the ResNet. Commonly used in fine-tuning. @@ -697,7 +705,7 @@ def build_res2net_backbone(cfg, input_shape): deform_modulated = cfg.MODEL.RESNETS.DEFORM_MODULATED deform_num_groups = cfg.MODEL.RESNETS.DEFORM_NUM_GROUPS # fmt: on - assert res5_dilation in {1, 2}, "res5_dilation cannot be {}.".format(res5_dilation) + assert res5_dilation in {1, 2}, f"res5_dilation cannot be {res5_dilation}." num_blocks_per_stage = { 18: [2, 2, 2, 2], @@ -709,9 +717,9 @@ def build_res2net_backbone(cfg, input_shape): if depth in [18, 34]: assert out_channels == 64, "Must set MODEL.RESNETS.RES2_OUT_CHANNELS = 64 for R18/R34" - assert not any( - deform_on_per_stage - ), "MODEL.RESNETS.DEFORM_ON_PER_STAGE unsupported for R18/R34" + assert not any(deform_on_per_stage), ( + "MODEL.RESNETS.DEFORM_ON_PER_STAGE unsupported for R18/R34" + ) assert res5_dilation == 1, "Must set MODEL.RESNETS.RES5_DILATION = 1 for R18/R34" assert num_groups == 1, "Must set MODEL.RESNETS.NUM_GROUPS = 1 for R18/R34" @@ -799,4 +807,4 @@ def build_res2net_bifpn_backbone(cfg, input_shape: ShapeSpec): num_bifpn=cfg.MODEL.BIFPN.NUM_BIFPN, separable_conv=cfg.MODEL.BIFPN.SEPARABLE_CONV, ) - return backbone \ No newline at end of file + return backbone diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/debug.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/debug.py index 0a4437fb5a..63186b05c5 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/debug.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/debug.py @@ -2,27 +2,30 @@ import numpy as np import torch import torch.nn.functional as F +from typing import Sequence + +COLORS = ((np.random.rand(1300, 3) * 0.4 + 0.6) * 255).astype(np.uint8).reshape(1300, 1, 1, 3) -COLORS = ((np.random.rand(1300, 3) * 0.4 + 0.6) * 255).astype( - np.uint8).reshape(1300, 1, 1, 3) def _get_color_image(heatmap): - heatmap = heatmap.reshape( - heatmap.shape[0], heatmap.shape[1], heatmap.shape[2], 1) - if heatmap.shape[0] == 1: - color_map = (heatmap * np.ones((1, 1, 1, 3), np.uint8) * 255).max( - axis=0).astype(np.uint8) # H, W, 3 - else: - color_map = (heatmap * COLORS[:heatmap.shape[0]]).max(axis=0).astype(np.uint8) # H, W, 3 + heatmap = heatmap.reshape(heatmap.shape[0], heatmap.shape[1], heatmap.shape[2], 1) + if heatmap.shape[0] == 1: + color_map = ( + (heatmap * np.ones((1, 1, 1, 3), np.uint8) * 255).max(axis=0).astype(np.uint8) + ) # H, W, 3 + else: + color_map = (heatmap * COLORS[: heatmap.shape[0]]).max(axis=0).astype(np.uint8) # H, W, 3 + + return color_map + - return color_map +def _blend_image(image, color_map, a: float=0.7): + color_map = cv2.resize(color_map, (image.shape[1], image.shape[0])) + ret = np.clip(image * (1 - a) + color_map * a, 0, 255).astype(np.uint8) + return ret -def _blend_image(image, color_map, a=0.7): - color_map = cv2.resize(color_map, (image.shape[1], image.shape[0])) - ret = np.clip(image * (1 - a) + color_map * a, 0, 255).astype(np.uint8) - return ret -def _blend_image_heatmaps(image, color_maps, a=0.7): +def _blend_image_heatmaps(image, color_maps, a: float=0.7): merges = np.zeros((image.shape[0], image.shape[1], 3), np.float32) for color_map in color_maps: color_map = cv2.resize(color_map, (image.shape[1], image.shape[0])) @@ -30,10 +33,11 @@ def _blend_image_heatmaps(image, color_maps, a=0.7): ret = np.clip(image * (1 - a) + merges * a, 0, 255).astype(np.uint8) return ret + def _decompose_level(x, shapes_per_level, N): - ''' + """ x: LNHiWi x C - ''' + """ x = x.view(x.shape[0], -1) ret = [] st = 0 @@ -42,11 +46,11 @@ def _decompose_level(x, shapes_per_level, N): h = shapes_per_level[l][0].int().item() w = shapes_per_level[l][1].int().item() for i in range(N): - ret[l].append(x[st + h * w * i:st + h * w * (i + 1)].view( - h, w, -1).permute(2, 0, 1)) + ret[l].append(x[st + h * w * i : st + h * w * (i + 1)].view(h, w, -1).permute(2, 0, 1)) st += h * w * N return ret + def _imagelist_to_tensor(images): images = [x for x in images] image_sizes = [x.shape[-2:] for x in images] @@ -54,8 +58,7 @@ def _imagelist_to_tensor(images): w = max([size[1] for size in image_sizes]) S = 32 h, w = ((h - 1) // S + 1) * S, ((w - 1) // S + 1) * S - images = [F.pad(x, (0, w - x.shape[2], 0, h - x.shape[1], 0, 0)) \ - for x in images] + images = [F.pad(x, (0, w - x.shape[2], 0, h - x.shape[1], 0, 0)) for x in images] images = torch.stack(images) return images @@ -70,21 +73,28 @@ def _ind2il(ind, shapes_per_level, N): i = (r - S) // (shapes_per_level[l][0] * shapes_per_level[l][1]) return i, l + def debug_train( - images, gt_instances, flattened_hms, reg_targets, labels, pos_inds, - shapes_per_level, locations, strides): - ''' + images, + gt_instances, + flattened_hms, + reg_targets, + labels: Sequence[str], + pos_inds, + shapes_per_level, + locations, + strides: Sequence[int], +) -> None: + """ images: N x 3 x H x W flattened_hms: LNHiWi x C shapes_per_level: L x 2 [(H_i, W_i)] locations: LNHiWi x 2 - ''' - reg_inds = torch.nonzero( - reg_targets.max(dim=1)[0] > 0).squeeze(1) + """ + reg_inds = torch.nonzero(reg_targets.max(dim=1)[0] > 0).squeeze(1) N = len(images) images = _imagelist_to_tensor(images) - repeated_locations = [torch.cat([loc] * N, dim=0) \ - for loc in locations] + repeated_locations = [torch.cat([loc] * N, dim=0) for loc in locations] locations = torch.cat(repeated_locations, dim=0) gt_hms = _decompose_level(flattened_hms, shapes_per_level, N) masks = flattened_hms.new_zeros((flattened_hms.shape[0], 1)) @@ -94,30 +104,32 @@ def debug_train( image = images[i].detach().cpu().numpy().transpose(1, 2, 0) color_maps = [] for l in range(len(gt_hms)): - color_map = _get_color_image( - gt_hms[l][i].detach().cpu().numpy()) + color_map = _get_color_image(gt_hms[l][i].detach().cpu().numpy()) color_maps.append(color_map) - cv2.imshow('gthm_{}'.format(l), color_map) + cv2.imshow(f"gthm_{l}", color_map) blend = _blend_image_heatmaps(image.copy(), color_maps) if gt_instances is not None: bboxes = gt_instances[i].gt_boxes.tensor for j in range(len(bboxes)): bbox = bboxes[j] cv2.rectangle( - blend, + blend, (int(bbox[0]), int(bbox[1])), (int(bbox[2]), int(bbox[3])), - (0, 0, 255), 3, cv2.LINE_AA) - + (0, 0, 255), + 3, + cv2.LINE_AA, + ) + for j in range(len(pos_inds)): image_id, l = _ind2il(pos_inds[j], shapes_per_level, N) if image_id != i: continue loc = locations[pos_inds[j]] cv2.drawMarker( - blend, (int(loc[0]), int(loc[1])), (0, 255, 255), - markerSize=(l + 1) * 16) - + blend, (int(loc[0]), int(loc[1])), (0, 255, 255), markerSize=(l + 1) * 16 + ) + for j in range(len(reg_inds)): image_id, l = _ind2il(reg_inds[j], shapes_per_level, N) if image_id != i: @@ -125,105 +137,136 @@ def debug_train( ltrb = reg_targets[reg_inds[j]] ltrb *= strides[l] loc = locations[reg_inds[j]] - bbox = [(loc[0] - ltrb[0]), (loc[1] - ltrb[1]), - (loc[0] + ltrb[2]), (loc[1] + ltrb[3])] + bbox = [(loc[0] - ltrb[0]), (loc[1] - ltrb[1]), (loc[0] + ltrb[2]), (loc[1] + ltrb[3])] cv2.rectangle( - blend, + blend, (int(bbox[0]), int(bbox[1])), (int(bbox[2]), int(bbox[3])), - (255, 0, 0), 1, cv2.LINE_AA) + (255, 0, 0), + 1, + cv2.LINE_AA, + ) cv2.circle(blend, (int(loc[0]), int(loc[1])), 2, (255, 0, 0), -1) - cv2.imshow('blend', blend) + cv2.imshow("blend", blend) cv2.waitKey() def debug_test( - images, logits_pred, reg_pred, agn_hm_pred=[], preds=[], - vis_thresh=0.3, debug_show_name=False, mult_agn=False): - ''' + images, + logits_pred, + reg_pred, + agn_hm_pred=None, + preds=None, + vis_thresh: float=0.3, + debug_show_name: bool=False, + mult_agn: bool=False, +) -> None: + """ images: N x 3 x H x W class_target: LNHiWi x C cat_agn_heatmap: LNHiWi shapes_per_level: L x 2 [(H_i, W_i)] - ''' - N = len(images) + """ + if preds is None: + preds = [] + if agn_hm_pred is None: + agn_hm_pred = [] + len(images) for i in range(len(images)): image = images[i].detach().cpu().numpy().transpose(1, 2, 0) - result = image.copy().astype(np.uint8) + image.copy().astype(np.uint8) pred_image = image.copy().astype(np.uint8) color_maps = [] L = len(logits_pred) for l in range(L): if logits_pred[0] is not None: stride = min(image.shape[0], image.shape[1]) / min( - logits_pred[l][i].shape[1], logits_pred[l][i].shape[2]) + logits_pred[l][i].shape[1], logits_pred[l][i].shape[2] + ) else: stride = min(image.shape[0], image.shape[1]) / min( - agn_hm_pred[l][i].shape[1], agn_hm_pred[l][i].shape[2]) + agn_hm_pred[l][i].shape[1], agn_hm_pred[l][i].shape[2] + ) stride = stride if stride < 60 else 64 if stride < 100 else 128 if logits_pred[0] is not None: if mult_agn: logits_pred[l][i] = logits_pred[l][i] * agn_hm_pred[l][i] - color_map = _get_color_image( - logits_pred[l][i].detach().cpu().numpy()) + color_map = _get_color_image(logits_pred[l][i].detach().cpu().numpy()) color_maps.append(color_map) - cv2.imshow('predhm_{}'.format(l), color_map) + cv2.imshow(f"predhm_{l}", color_map) if debug_show_name: - from detectron2.data.datasets.lvis_v1_categories import LVIS_CATEGORIES - cat2name = [x['name'] for x in LVIS_CATEGORIES] + from detectron2.data.datasets.lvis_v1_categories import LVIS_CATEGORIES + + cat2name = [x["name"] for x in LVIS_CATEGORIES] for j in range(len(preds[i].scores) if preds is not None else 0): if preds[i].scores[j] > vis_thresh: - bbox = preds[i].proposal_boxes[j] \ - if preds[i].has('proposal_boxes') else \ - preds[i].pred_boxes[j] + bbox = ( + preds[i].proposal_boxes[j] + if preds[i].has("proposal_boxes") + else preds[i].pred_boxes[j] + ) bbox = bbox.tensor[0].detach().cpu().numpy().astype(np.int32) - cat = int(preds[i].pred_classes[j]) \ - if preds[i].has('pred_classes') else 0 + cat = int(preds[i].pred_classes[j]) if preds[i].has("pred_classes") else 0 cl = COLORS[cat, 0, 0] cv2.rectangle( - pred_image, (int(bbox[0]), int(bbox[1])), - (int(bbox[2]), int(bbox[3])), - (int(cl[0]), int(cl[1]), int(cl[2])), 2, cv2.LINE_AA) + pred_image, + (int(bbox[0]), int(bbox[1])), + (int(bbox[2]), int(bbox[3])), + (int(cl[0]), int(cl[1]), int(cl[2])), + 2, + cv2.LINE_AA, + ) if debug_show_name: - txt = '{}{:.1f}'.format( - cat2name[cat] if cat > 0 else '', - preds[i].scores[j]) + txt = "{}{:.1f}".format( + cat2name[cat] if cat > 0 else "", preds[i].scores[j] + ) font = cv2.FONT_HERSHEY_SIMPLEX cat_size = cv2.getTextSize(txt, font, 0.5, 2)[0] cv2.rectangle( pred_image, (int(bbox[0]), int(bbox[1] - cat_size[1] - 2)), - (int(bbox[0] + cat_size[0]), int(bbox[1] - 2)), - (int(cl[0]), int(cl[1]), int(cl[2])), -1) + (int(bbox[0] + cat_size[0]), int(bbox[1] - 2)), + (int(cl[0]), int(cl[1]), int(cl[2])), + -1, + ) cv2.putText( - pred_image, txt, (int(bbox[0]), int(bbox[1] - 2)), - font, 0.5, (0, 0, 0), thickness=1, lineType=cv2.LINE_AA) - + pred_image, + txt, + (int(bbox[0]), int(bbox[1] - 2)), + font, + 0.5, + (0, 0, 0), + thickness=1, + lineType=cv2.LINE_AA, + ) if agn_hm_pred[l] is not None: agn_hm_ = agn_hm_pred[l][i, 0, :, :, None].detach().cpu().numpy() - agn_hm_ = (agn_hm_ * np.array([255, 255, 255]).reshape( - 1, 1, 3)).astype(np.uint8) - cv2.imshow('agn_hm_{}'.format(l), agn_hm_) + agn_hm_ = (agn_hm_ * np.array([255, 255, 255]).reshape(1, 1, 3)).astype(np.uint8) + cv2.imshow(f"agn_hm_{l}", agn_hm_) blend = _blend_image_heatmaps(image.copy(), color_maps) - cv2.imshow('blend', blend) - cv2.imshow('preds', pred_image) + cv2.imshow("blend", blend) + cv2.imshow("preds", pred_image) cv2.waitKey() + global cnt cnt = 0 -def debug_second_stage(images, instances, proposals=None, vis_thresh=0.3, - save_debug=False, debug_show_name=False): + +def debug_second_stage( + images, instances, proposals=None, vis_thresh: float=0.3, save_debug: bool=False, debug_show_name: bool=False +) -> None: images = _imagelist_to_tensor(images) if debug_show_name: from detectron2.data.datasets.lvis_v1_categories import LVIS_CATEGORIES - cat2name = [x['name'] for x in LVIS_CATEGORIES] + + cat2name = [x["name"] for x in LVIS_CATEGORIES] for i in range(len(images)): image = images[i].detach().cpu().numpy().transpose(1, 2, 0).astype(np.uint8).copy() - if instances[i].has('gt_boxes'): + if instances[i].has("gt_boxes"): bboxes = instances[i].gt_boxes.tensor.cpu().numpy() scores = np.ones(bboxes.shape[0]) cats = instances[i].gt_classes.cpu().numpy() @@ -237,29 +280,41 @@ def debug_second_stage(images, instances, proposals=None, vis_thresh=0.3, cl = COLORS[cats[j], 0, 0] cl = (int(cl[0]), int(cl[1]), int(cl[2])) cv2.rectangle( - image, + image, (int(bbox[0]), int(bbox[1])), (int(bbox[2]), int(bbox[3])), - cl, 2, cv2.LINE_AA) + cl, + 2, + cv2.LINE_AA, + ) if debug_show_name: cat = cats[j] - txt = '{}{:.1f}'.format( - cat2name[cat] if cat > 0 else '', - scores[j]) + txt = "{}{:.1f}".format(cat2name[cat] if cat > 0 else "", scores[j]) font = cv2.FONT_HERSHEY_SIMPLEX cat_size = cv2.getTextSize(txt, font, 0.5, 2)[0] cv2.rectangle( image, (int(bbox[0]), int(bbox[1] - cat_size[1] - 2)), - (int(bbox[0] + cat_size[0]), int(bbox[1] - 2)), - (int(cl[0]), int(cl[1]), int(cl[2])), -1) + (int(bbox[0] + cat_size[0]), int(bbox[1] - 2)), + (int(cl[0]), int(cl[1]), int(cl[2])), + -1, + ) cv2.putText( - image, txt, (int(bbox[0]), int(bbox[1] - 2)), - font, 0.5, (0, 0, 0), thickness=1, lineType=cv2.LINE_AA) + image, + txt, + (int(bbox[0]), int(bbox[1] - 2)), + font, + 0.5, + (0, 0, 0), + thickness=1, + lineType=cv2.LINE_AA, + ) if proposals is not None: - proposal_image = images[i].detach().cpu().numpy().transpose(1, 2, 0).astype(np.uint8).copy() + proposal_image = ( + images[i].detach().cpu().numpy().transpose(1, 2, 0).astype(np.uint8).copy() + ) bboxes = proposals[i].proposal_boxes.tensor.cpu().numpy() - if proposals[i].has('scores'): + if proposals[i].has("scores"): scores = proposals[i].scores.cpu().numpy() else: scores = proposals[i].objectness_logits.sigmoid().cpu().numpy() @@ -268,16 +323,19 @@ def debug_second_stage(images, instances, proposals=None, vis_thresh=0.3, bbox = bboxes[j] cl = (209, 159, 83) cv2.rectangle( - proposal_image, + proposal_image, (int(bbox[0]), int(bbox[1])), (int(bbox[2]), int(bbox[3])), - cl, 2, cv2.LINE_AA) - - cv2.imshow('image', image) + cl, + 2, + cv2.LINE_AA, + ) + + cv2.imshow("image", image) if proposals is not None: - cv2.imshow('proposals', proposal_image) + cv2.imshow("proposals", proposal_image) if save_debug: global cnt cnt += 1 - cv2.imwrite('output/save_debug/{}.jpg'.format(cnt), proposal_image) - cv2.waitKey() \ No newline at end of file + cv2.imwrite(f"output/save_debug/{cnt}.jpg", proposal_image) + cv2.waitKey() diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/dense_heads/centernet.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/dense_heads/centernet.py index aef02942ab..cd68ed3f40 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/dense_heads/centernet.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/dense_heads/centernet.py @@ -1,79 +1,78 @@ - -import math -import json -import copy -from typing import List, Dict -import numpy as np -import torch -from torch import nn -from torch.nn import functional as F - +from detectron2.config import configurable +from detectron2.layers import cat from detectron2.modeling.proposal_generator.build import PROPOSAL_GENERATOR_REGISTRY -from detectron2.layers import ShapeSpec, cat -from detectron2.structures import Instances, Boxes -from detectron2.modeling import detector_postprocess +from detectron2.structures import Boxes, Instances from detectron2.utils.comm import get_world_size -from detectron2.config import configurable +import torch +from torch import nn -from ..layers.heatmap_focal_loss import heatmap_focal_loss_jit -from ..layers.heatmap_focal_loss import binary_heatmap_focal_loss_jit +from ..debug import debug_test, debug_train +from ..layers.heatmap_focal_loss import binary_heatmap_focal_loss_jit, heatmap_focal_loss_jit from ..layers.iou_loss import IOULoss from ..layers.ml_nms import ml_nms -from ..debug import debug_train, debug_test -from .utils import reduce_sum, _transpose from .centernet_head import CenterNetHead +from .utils import _transpose, reduce_sum +from typing import Sequence __all__ = ["CenterNet"] INF = 100000000 + @PROPOSAL_GENERATOR_REGISTRY.register() class CenterNet(nn.Module): @configurable - def __init__(self, + def __init__( + self, # input_shape: Dict[str, ShapeSpec], - in_channels=256, + in_channels: int=256, *, - num_classes=80, + num_classes: int=80, in_features=("p3", "p4", "p5", "p6", "p7"), - strides=(8, 16, 32, 64, 128), - score_thresh=0.05, - hm_min_overlap=0.8, - loc_loss_type='giou', - min_radius=4, - hm_focal_alpha=0.25, - hm_focal_beta=4, - loss_gamma=2.0, - reg_weight=2.0, - not_norm_reg=True, - with_agn_hm=False, - only_proposal=False, - as_proposal=False, - not_nms=False, - pos_weight=1., - neg_weight=1., - sigmoid_clamp=1e-4, - ignore_high_fp=-1., - center_nms=False, - sizes_of_interest=[[0,80],[64,160],[128,320],[256,640],[512,10000000]], - more_pos=False, - more_pos_thresh=0.2, - more_pos_topk=9, - pre_nms_topk_train=1000, - pre_nms_topk_test=1000, - post_nms_topk_train=100, - post_nms_topk_test=100, - nms_thresh_train=0.6, - nms_thresh_test=0.6, - no_reduce=False, - not_clamp_box=False, - debug=False, - vis_thresh=0.5, - pixel_mean=[103.530,116.280,123.675], - pixel_std=[1.0,1.0,1.0], - device='cuda', + strides: Sequence[int]=(8, 16, 32, 64, 128), + score_thresh: float=0.05, + hm_min_overlap: float=0.8, + loc_loss_type: str="giou", + min_radius: int=4, + hm_focal_alpha: float=0.25, + hm_focal_beta: int=4, + loss_gamma: float=2.0, + reg_weight: float=2.0, + not_norm_reg: bool=True, + with_agn_hm: bool=False, + only_proposal: bool=False, + as_proposal: bool=False, + not_nms: bool=False, + pos_weight: float=1.0, + neg_weight: float=1.0, + sigmoid_clamp: float=1e-4, + ignore_high_fp=-1.0, + center_nms: bool=False, + sizes_of_interest=None, + more_pos: bool=False, + more_pos_thresh: float=0.2, + more_pos_topk: int=9, + pre_nms_topk_train: int=1000, + pre_nms_topk_test: int=1000, + post_nms_topk_train: int=100, + post_nms_topk_test: int=100, + nms_thresh_train: float=0.6, + nms_thresh_test: float=0.6, + no_reduce: bool=False, + not_clamp_box: bool=False, + debug: bool=False, + vis_thresh: float=0.5, + pixel_mean=None, + pixel_std=None, + device: str="cuda", centernet_head=None, - ): + ) -> None: + if pixel_std is None: + pixel_std = [1.0, 1.0, 1.0] + if pixel_mean is None: + pixel_mean = [103.53, 116.28, 123.675] + if sizes_of_interest is None: + sizes_of_interest = [[0, 80], [64, 160], [128, 320], [256, 640], [512, 10000000]] super().__init__() self.num_classes = num_classes self.in_features = in_features @@ -106,7 +105,7 @@ def __init__(self, self.nms_thresh_test = nms_thresh_test self.no_reduce = no_reduce self.not_clamp_box = not_clamp_box - + self.debug = debug self.vis_thresh = vis_thresh if self.center_nms: @@ -120,128 +119,140 @@ def __init__(self, in_channels=in_channels, num_levels=len(in_features), with_agn_hm=with_agn_hm, - only_proposal=only_proposal) + only_proposal=only_proposal, + ) else: self.centernet_head = centernet_head if self.debug: - pixel_mean = torch.Tensor(pixel_mean).to( - torch.device(device)).view(3, 1, 1) - pixel_std = torch.Tensor(pixel_std).to( - torch.device(device)).view(3, 1, 1) + pixel_mean = torch.Tensor(pixel_mean).to(torch.device(device)).view(3, 1, 1) + pixel_std = torch.Tensor(pixel_std).to(torch.device(device)).view(3, 1, 1) self.denormalizer = lambda x: x * pixel_std + pixel_mean @classmethod def from_config(cls, cfg, input_shape): ret = { # 'input_shape': input_shape, - 'in_channels': input_shape[ - cfg.MODEL.CENTERNET.IN_FEATURES[0]].channels, - 'num_classes': cfg.MODEL.CENTERNET.NUM_CLASSES, - 'in_features': cfg.MODEL.CENTERNET.IN_FEATURES, - 'strides': cfg.MODEL.CENTERNET.FPN_STRIDES, - 'score_thresh': cfg.MODEL.CENTERNET.INFERENCE_TH, - 'loc_loss_type': cfg.MODEL.CENTERNET.LOC_LOSS_TYPE, - 'hm_min_overlap': cfg.MODEL.CENTERNET.HM_MIN_OVERLAP, - 'min_radius': cfg.MODEL.CENTERNET.MIN_RADIUS, - 'hm_focal_alpha': cfg.MODEL.CENTERNET.HM_FOCAL_ALPHA, - 'hm_focal_beta': cfg.MODEL.CENTERNET.HM_FOCAL_BETA, - 'loss_gamma': cfg.MODEL.CENTERNET.LOSS_GAMMA, - 'reg_weight': cfg.MODEL.CENTERNET.REG_WEIGHT, - 'not_norm_reg': cfg.MODEL.CENTERNET.NOT_NORM_REG, - 'with_agn_hm': cfg.MODEL.CENTERNET.WITH_AGN_HM, - 'only_proposal': cfg.MODEL.CENTERNET.ONLY_PROPOSAL, - 'as_proposal': cfg.MODEL.CENTERNET.AS_PROPOSAL, - 'not_nms': cfg.MODEL.CENTERNET.NOT_NMS, - 'pos_weight': cfg.MODEL.CENTERNET.POS_WEIGHT, - 'neg_weight': cfg.MODEL.CENTERNET.NEG_WEIGHT, - 'sigmoid_clamp': cfg.MODEL.CENTERNET.SIGMOID_CLAMP, - 'ignore_high_fp': cfg.MODEL.CENTERNET.IGNORE_HIGH_FP, - 'center_nms': cfg.MODEL.CENTERNET.CENTER_NMS, - 'sizes_of_interest': cfg.MODEL.CENTERNET.SOI, - 'more_pos': cfg.MODEL.CENTERNET.MORE_POS, - 'more_pos_thresh': cfg.MODEL.CENTERNET.MORE_POS_THRESH, - 'more_pos_topk': cfg.MODEL.CENTERNET.MORE_POS_TOPK, - 'pre_nms_topk_train': cfg.MODEL.CENTERNET.PRE_NMS_TOPK_TRAIN, - 'pre_nms_topk_test': cfg.MODEL.CENTERNET.PRE_NMS_TOPK_TEST, - 'post_nms_topk_train': cfg.MODEL.CENTERNET.POST_NMS_TOPK_TRAIN, - 'post_nms_topk_test': cfg.MODEL.CENTERNET.POST_NMS_TOPK_TEST, - 'nms_thresh_train': cfg.MODEL.CENTERNET.NMS_TH_TRAIN, - 'nms_thresh_test': cfg.MODEL.CENTERNET.NMS_TH_TEST, - 'no_reduce': cfg.MODEL.CENTERNET.NO_REDUCE, - 'not_clamp_box': cfg.INPUT.NOT_CLAMP_BOX, - 'debug': cfg.DEBUG, - 'vis_thresh': cfg.VIS_THRESH, - 'pixel_mean': cfg.MODEL.PIXEL_MEAN, - 'pixel_std': cfg.MODEL.PIXEL_STD, - 'device': cfg.MODEL.DEVICE, - 'centernet_head': CenterNetHead( - cfg, [input_shape[f] for f in cfg.MODEL.CENTERNET.IN_FEATURES]), + "in_channels": input_shape[cfg.MODEL.CENTERNET.IN_FEATURES[0]].channels, + "num_classes": cfg.MODEL.CENTERNET.NUM_CLASSES, + "in_features": cfg.MODEL.CENTERNET.IN_FEATURES, + "strides": cfg.MODEL.CENTERNET.FPN_STRIDES, + "score_thresh": cfg.MODEL.CENTERNET.INFERENCE_TH, + "loc_loss_type": cfg.MODEL.CENTERNET.LOC_LOSS_TYPE, + "hm_min_overlap": cfg.MODEL.CENTERNET.HM_MIN_OVERLAP, + "min_radius": cfg.MODEL.CENTERNET.MIN_RADIUS, + "hm_focal_alpha": cfg.MODEL.CENTERNET.HM_FOCAL_ALPHA, + "hm_focal_beta": cfg.MODEL.CENTERNET.HM_FOCAL_BETA, + "loss_gamma": cfg.MODEL.CENTERNET.LOSS_GAMMA, + "reg_weight": cfg.MODEL.CENTERNET.REG_WEIGHT, + "not_norm_reg": cfg.MODEL.CENTERNET.NOT_NORM_REG, + "with_agn_hm": cfg.MODEL.CENTERNET.WITH_AGN_HM, + "only_proposal": cfg.MODEL.CENTERNET.ONLY_PROPOSAL, + "as_proposal": cfg.MODEL.CENTERNET.AS_PROPOSAL, + "not_nms": cfg.MODEL.CENTERNET.NOT_NMS, + "pos_weight": cfg.MODEL.CENTERNET.POS_WEIGHT, + "neg_weight": cfg.MODEL.CENTERNET.NEG_WEIGHT, + "sigmoid_clamp": cfg.MODEL.CENTERNET.SIGMOID_CLAMP, + "ignore_high_fp": cfg.MODEL.CENTERNET.IGNORE_HIGH_FP, + "center_nms": cfg.MODEL.CENTERNET.CENTER_NMS, + "sizes_of_interest": cfg.MODEL.CENTERNET.SOI, + "more_pos": cfg.MODEL.CENTERNET.MORE_POS, + "more_pos_thresh": cfg.MODEL.CENTERNET.MORE_POS_THRESH, + "more_pos_topk": cfg.MODEL.CENTERNET.MORE_POS_TOPK, + "pre_nms_topk_train": cfg.MODEL.CENTERNET.PRE_NMS_TOPK_TRAIN, + "pre_nms_topk_test": cfg.MODEL.CENTERNET.PRE_NMS_TOPK_TEST, + "post_nms_topk_train": cfg.MODEL.CENTERNET.POST_NMS_TOPK_TRAIN, + "post_nms_topk_test": cfg.MODEL.CENTERNET.POST_NMS_TOPK_TEST, + "nms_thresh_train": cfg.MODEL.CENTERNET.NMS_TH_TRAIN, + "nms_thresh_test": cfg.MODEL.CENTERNET.NMS_TH_TEST, + "no_reduce": cfg.MODEL.CENTERNET.NO_REDUCE, + "not_clamp_box": cfg.INPUT.NOT_CLAMP_BOX, + "debug": cfg.DEBUG, + "vis_thresh": cfg.VIS_THRESH, + "pixel_mean": cfg.MODEL.PIXEL_MEAN, + "pixel_std": cfg.MODEL.PIXEL_STD, + "device": cfg.MODEL.DEVICE, + "centernet_head": CenterNetHead( + cfg, [input_shape[f] for f in cfg.MODEL.CENTERNET.IN_FEATURES] + ), } return ret - def forward(self, images, features_dict, gt_instances): features = [features_dict[f] for f in self.in_features] - clss_per_level, reg_pred_per_level, agn_hm_pred_per_level = \ - self.centernet_head(features) + clss_per_level, reg_pred_per_level, agn_hm_pred_per_level = self.centernet_head(features) grids = self.compute_grids(features) shapes_per_level = grids[0].new_tensor( - [(x.shape[2], x.shape[3]) for x in reg_pred_per_level]) - + [(x.shape[2], x.shape[3]) for x in reg_pred_per_level] + ) + if not self.training: return self.inference( - images, clss_per_level, reg_pred_per_level, - agn_hm_pred_per_level, grids) + images, clss_per_level, reg_pred_per_level, agn_hm_pred_per_level, grids + ) else: - pos_inds, labels, reg_targets, flattened_hms = \ - self._get_ground_truth( - grids, shapes_per_level, gt_instances) + pos_inds, labels, reg_targets, flattened_hms = self._get_ground_truth( + grids, shapes_per_level, gt_instances + ) # logits_pred: M x F, reg_pred: M x 4, agn_hm_pred: M logits_pred, reg_pred, agn_hm_pred = self._flatten_outputs( - clss_per_level, reg_pred_per_level, agn_hm_pred_per_level) + clss_per_level, reg_pred_per_level, agn_hm_pred_per_level + ) if self.more_pos: # add more pixels as positive if \ # 1. they are within the center3x3 region of an object # 2. their regression losses are small (= 0).squeeze(1) reg_pred = reg_pred[reg_inds] reg_targets_pos = reg_targets[reg_inds] reg_weight_map = flattened_hms.max(dim=1)[0] reg_weight_map = reg_weight_map[reg_inds] - reg_weight_map = reg_weight_map * 0 + 1 \ - if self.not_norm_reg else reg_weight_map + reg_weight_map = reg_weight_map * 0 + 1 if self.not_norm_reg else reg_weight_map if self.no_reduce: reg_norm = max(reg_weight_map.sum(), 1) else: reg_norm = max(reduce_sum(reg_weight_map.sum()).item() / num_gpus, 1) - - reg_loss = self.reg_weight * self.iou_loss( - reg_pred, reg_targets_pos, reg_weight_map, - reduction='sum') / reg_norm - losses['loss_centernet_loc'] = reg_loss + + reg_loss = ( + self.reg_weight + * self.iou_loss(reg_pred, reg_targets_pos, reg_weight_map, reduction="sum") + / reg_norm + ) + losses["loss_centernet_loc"] = reg_loss if self.with_agn_hm: - cat_agn_heatmap = flattened_hms.max(dim=1)[0] # M + cat_agn_heatmap = flattened_hms.max(dim=1)[0] # M agn_pos_loss, agn_neg_loss = binary_heatmap_focal_loss_jit( - agn_hm_pred.float(), cat_agn_heatmap.float(), pos_inds, - alpha=self.hm_focal_alpha, - beta=self.hm_focal_beta, + agn_hm_pred.float(), + cat_agn_heatmap.float(), + pos_inds, + alpha=self.hm_focal_alpha, + beta=self.hm_focal_beta, gamma=self.loss_gamma, sigmoid_clamp=self.sigmoid_clamp, ignore_high_fp=self.ignore_high_fp, ) agn_pos_loss = self.pos_weight * agn_pos_loss / num_pos_avg agn_neg_loss = self.neg_weight * agn_neg_loss / num_pos_avg - losses['loss_centernet_agn_pos'] = agn_pos_loss - losses['loss_centernet_agn_neg'] = agn_neg_loss - + losses["loss_centernet_agn_pos"] = agn_pos_loss + losses["loss_centernet_agn_neg"] = agn_neg_loss + if self.debug: - print('losses', losses) - print('total_num_pos', total_num_pos) + print("losses", losses) + print("total_num_pos", total_num_pos) return losses - def compute_grids(self, features): grids = [] for level, feature in enumerate(features): h, w = feature.size()[-2:] shifts_x = torch.arange( - 0, w * self.strides[level], + 0, + w * self.strides[level], step=self.strides[level], - dtype=torch.float32, device=feature.device) + dtype=torch.float32, + device=feature.device, + ) shifts_y = torch.arange( - 0, h * self.strides[level], + 0, + h * self.strides[level], step=self.strides[level], - dtype=torch.float32, device=feature.device) + dtype=torch.float32, + device=feature.device, + ) shift_y, shift_x = torch.meshgrid(shifts_y, shifts_x) shift_x = shift_x.reshape(-1) shift_y = shift_y.reshape(-1) - grids_per_level = torch.stack((shift_x, shift_y), dim=1) + \ - self.strides[level] // 2 + grids_per_level = torch.stack((shift_x, shift_y), dim=1) + self.strides[level] // 2 grids.append(grids_per_level) return grids - def _get_ground_truth(self, grids, shapes_per_level, gt_instances): - ''' + """ Input: grids: list of tensors [(hl x wl, 2)]_l shapes_per_level: list of tuples L x 2: @@ -352,202 +371,206 @@ def _get_ground_truth(self, grids, shapes_per_level, gt_instances): flattened_hms: M x C or M x 1 N: number of objects in all images M: number of pixels from all FPN levels - ''' + """ # get positive pixel index if not self.more_pos: - pos_inds, labels = self._get_label_inds( - gt_instances, shapes_per_level) + pos_inds, labels = self._get_label_inds(gt_instances, shapes_per_level) else: pos_inds, labels = None, None heatmap_channels = self.num_classes L = len(grids) num_loc_list = [len(loc) for loc in grids] - strides = torch.cat([ - shapes_per_level.new_ones(num_loc_list[l]) * self.strides[l] \ - for l in range(L)]).float() # M - reg_size_ranges = torch.cat([ - shapes_per_level.new_tensor(self.sizes_of_interest[l]).float().view( - 1, 2).expand(num_loc_list[l], 2) for l in range(L)]) # M x 2 - grids = torch.cat(grids, dim=0) # M x 2 + strides = torch.cat( + [shapes_per_level.new_ones(num_loc_list[l]) * self.strides[l] for l in range(L)] + ).float() # M + reg_size_ranges = torch.cat( + [ + shapes_per_level.new_tensor(self.sizes_of_interest[l]) + .float() + .view(1, 2) + .expand(num_loc_list[l], 2) + for l in range(L) + ] + ) # M x 2 + grids = torch.cat(grids, dim=0) # M x 2 M = grids.shape[0] reg_targets = [] flattened_hms = [] - for i in range(len(gt_instances)): # images - boxes = gt_instances[i].gt_boxes.tensor # N x 4 - area = gt_instances[i].gt_boxes.area() # N - gt_classes = gt_instances[i].gt_classes # N in [0, self.num_classes] + for i in range(len(gt_instances)): # images + boxes = gt_instances[i].gt_boxes.tensor # N x 4 + area = gt_instances[i].gt_boxes.area() # N + gt_classes = gt_instances[i].gt_classes # N in [0, self.num_classes] N = boxes.shape[0] if N == 0: reg_targets.append(grids.new_zeros((M, 4)) - INF) flattened_hms.append( - grids.new_zeros(( - M, 1 if self.only_proposal else heatmap_channels))) + grids.new_zeros((M, 1 if self.only_proposal else heatmap_channels)) + ) continue - - l = grids[:, 0].view(M, 1) - boxes[:, 0].view(1, N) # M x N - t = grids[:, 1].view(M, 1) - boxes[:, 1].view(1, N) # M x N - r = boxes[:, 2].view(1, N) - grids[:, 0].view(M, 1) # M x N - b = boxes[:, 3].view(1, N) - grids[:, 1].view(M, 1) # M x N - reg_target = torch.stack([l, t, r, b], dim=2) # M x N x 4 - - centers = ((boxes[:, [0, 1]] + boxes[:, [2, 3]]) / 2) # N x 2 - centers_expanded = centers.view(1, N, 2).expand(M, N, 2) # M x N x 2 + + l = grids[:, 0].view(M, 1) - boxes[:, 0].view(1, N) # M x N + t = grids[:, 1].view(M, 1) - boxes[:, 1].view(1, N) # M x N + r = boxes[:, 2].view(1, N) - grids[:, 0].view(M, 1) # M x N + b = boxes[:, 3].view(1, N) - grids[:, 1].view(M, 1) # M x N + reg_target = torch.stack([l, t, r, b], dim=2) # M x N x 4 + + centers = (boxes[:, [0, 1]] + boxes[:, [2, 3]]) / 2 # N x 2 + centers_expanded = centers.view(1, N, 2).expand(M, N, 2) # M x N x 2 strides_expanded = strides.view(M, 1, 1).expand(M, N, 2) - centers_discret = ((centers_expanded / strides_expanded).int() * \ - strides_expanded).float() + strides_expanded / 2 # M x N x 2 - - is_peak = (((grids.view(M, 1, 2).expand(M, N, 2) - \ - centers_discret) ** 2).sum(dim=2) == 0) # M x N - is_in_boxes = reg_target.min(dim=2)[0] > 0 # M x N - is_center3x3 = self.get_center3x3( - grids, centers, strides) & is_in_boxes # M x N - is_cared_in_the_level = self.assign_reg_fpn( - reg_target, reg_size_ranges) # M x N - reg_mask = is_center3x3 & is_cared_in_the_level # M x N - - dist2 = ((grids.view(M, 1, 2).expand(M, N, 2) - \ - centers_expanded) ** 2).sum(dim=2) # M x N + centers_discret = ( + (centers_expanded / strides_expanded).int() * strides_expanded + ).float() + strides_expanded / 2 # M x N x 2 + + is_peak = ((grids.view(M, 1, 2).expand(M, N, 2) - centers_discret) ** 2).sum( + dim=2 + ) == 0 # M x N + is_in_boxes = reg_target.min(dim=2)[0] > 0 # M x N + is_center3x3 = self.get_center3x3(grids, centers, strides) & is_in_boxes # M x N + is_cared_in_the_level = self.assign_reg_fpn(reg_target, reg_size_ranges) # M x N + reg_mask = is_center3x3 & is_cared_in_the_level # M x N + + dist2 = ((grids.view(M, 1, 2).expand(M, N, 2) - centers_expanded) ** 2).sum( + dim=2 + ) # M x N dist2[is_peak] = 0 - radius2 = self.delta ** 2 * 2 * area # N - radius2 = torch.clamp( - radius2, min=self.min_radius ** 2) - weighted_dist2 = dist2 / radius2.view(1, N).expand(M, N) # M x N + radius2 = self.delta**2 * 2 * area # N + radius2 = torch.clamp(radius2, min=self.min_radius**2) + weighted_dist2 = dist2 / radius2.view(1, N).expand(M, N) # M x N reg_target = self._get_reg_targets( - reg_target, weighted_dist2.clone(), reg_mask, area) # M x 4 + reg_target, weighted_dist2.clone(), reg_mask, area + ) # M x 4 if self.only_proposal: - flattened_hm = self._create_agn_heatmaps_from_dist( - weighted_dist2.clone()) # M x 1 + flattened_hm = self._create_agn_heatmaps_from_dist(weighted_dist2.clone()) # M x 1 else: flattened_hm = self._create_heatmaps_from_dist( - weighted_dist2.clone(), gt_classes, - channels=heatmap_channels) # M x C + weighted_dist2.clone(), gt_classes, channels=heatmap_channels + ) # M x C reg_targets.append(reg_target) flattened_hms.append(flattened_hm) - + # transpose im first training_targets to level first ones reg_targets = _transpose(reg_targets, num_loc_list) flattened_hms = _transpose(flattened_hms, num_loc_list) for l in range(len(reg_targets)): reg_targets[l] = reg_targets[l] / float(self.strides[l]) - reg_targets = cat([x for x in reg_targets], dim=0) # MB x 4 - flattened_hms = cat([x for x in flattened_hms], dim=0) # MB x C - - return pos_inds, labels, reg_targets, flattened_hms + reg_targets = cat([x for x in reg_targets], dim=0) # MB x 4 + flattened_hms = cat([x for x in flattened_hms], dim=0) # MB x C + return pos_inds, labels, reg_targets, flattened_hms def _get_label_inds(self, gt_instances, shapes_per_level): - ''' + """ Inputs: gt_instances: [n_i], sum n_i = N shapes_per_level: L x 2 [(h_l, w_l)]_L Returns: pos_inds: N' labels: N' - ''' + """ pos_inds = [] labels = [] L = len(self.strides) B = len(gt_instances) shapes_per_level = shapes_per_level.long() - loc_per_level = (shapes_per_level[:, 0] * shapes_per_level[:, 1]).long() # L + loc_per_level = (shapes_per_level[:, 0] * shapes_per_level[:, 1]).long() # L level_bases = [] s = 0 for l in range(L): level_bases.append(s) s = s + B * loc_per_level[l] - level_bases = shapes_per_level.new_tensor(level_bases).long() # L - strides_default = shapes_per_level.new_tensor(self.strides).float() # L + level_bases = shapes_per_level.new_tensor(level_bases).long() # L + strides_default = shapes_per_level.new_tensor(self.strides).float() # L for im_i in range(B): targets_per_im = gt_instances[im_i] - bboxes = targets_per_im.gt_boxes.tensor # n x 4 + bboxes = targets_per_im.gt_boxes.tensor # n x 4 n = bboxes.shape[0] - centers = ((bboxes[:, [0, 1]] + bboxes[:, [2, 3]]) / 2) # n x 2 + centers = (bboxes[:, [0, 1]] + bboxes[:, [2, 3]]) / 2 # n x 2 centers = centers.view(n, 1, 2).expand(n, L, 2).contiguous() if self.not_clamp_box: h, w = gt_instances[im_i]._image_size - centers[:, :, 0].clamp_(min=0).clamp_(max=w-1) - centers[:, :, 1].clamp_(min=0).clamp_(max=h-1) + centers[:, :, 0].clamp_(min=0).clamp_(max=w - 1) + centers[:, :, 1].clamp_(min=0).clamp_(max=h - 1) strides = strides_default.view(1, L, 1).expand(n, L, 2) - centers_inds = (centers / strides).long() # n x L x 2 + centers_inds = (centers / strides).long() # n x L x 2 Ws = shapes_per_level[:, 1].view(1, L).expand(n, L) - pos_ind = level_bases.view(1, L).expand(n, L) + \ - im_i * loc_per_level.view(1, L).expand(n, L) + \ - centers_inds[:, :, 1] * Ws + \ - centers_inds[:, :, 0] # n x L + pos_ind = ( + level_bases.view(1, L).expand(n, L) + + im_i * loc_per_level.view(1, L).expand(n, L) + + centers_inds[:, :, 1] * Ws + + centers_inds[:, :, 0] + ) # n x L is_cared_in_the_level = self.assign_fpn_level(bboxes) pos_ind = pos_ind[is_cared_in_the_level].view(-1) - label = targets_per_im.gt_classes.view( - n, 1).expand(n, L)[is_cared_in_the_level].view(-1) + label = ( + targets_per_im.gt_classes.view(n, 1).expand(n, L)[is_cared_in_the_level].view(-1) + ) - pos_inds.append(pos_ind) # n' - labels.append(label) # n' + pos_inds.append(pos_ind) # n' + labels.append(label) # n' pos_inds = torch.cat(pos_inds, dim=0).long() labels = torch.cat(labels, dim=0) - return pos_inds, labels # N, N - + return pos_inds, labels # N, N def assign_fpn_level(self, boxes): - ''' + """ Inputs: boxes: n x 4 size_ranges: L x 2 Return: is_cared_in_the_level: n x L - ''' - size_ranges = boxes.new_tensor( - self.sizes_of_interest).view(len(self.sizes_of_interest), 2) # L x 2 - crit = ((boxes[:, 2:] - boxes[:, :2]) **2).sum(dim=1) ** 0.5 / 2 # n + """ + size_ranges = boxes.new_tensor(self.sizes_of_interest).view( + len(self.sizes_of_interest), 2 + ) # L x 2 + crit = ((boxes[:, 2:] - boxes[:, :2]) ** 2).sum(dim=1) ** 0.5 / 2 # n n, L = crit.shape[0], size_ranges.shape[0] crit = crit.view(n, 1).expand(n, L) size_ranges_expand = size_ranges.view(1, L, 2).expand(n, L, 2) - is_cared_in_the_level = (crit >= size_ranges_expand[:, :, 0]) & \ - (crit <= size_ranges_expand[:, :, 1]) + is_cared_in_the_level = (crit >= size_ranges_expand[:, :, 0]) & ( + crit <= size_ranges_expand[:, :, 1] + ) return is_cared_in_the_level - def assign_reg_fpn(self, reg_targets_per_im, size_ranges): - ''' + """ TODO (Xingyi): merge it with assign_fpn_level Inputs: reg_targets_per_im: M x N x 4 size_ranges: M x 2 - ''' - crit = ((reg_targets_per_im[:, :, :2] + \ - reg_targets_per_im[:, :, 2:])**2).sum(dim=2) ** 0.5 / 2 # M x N - is_cared_in_the_level = (crit >= size_ranges[:, [0]]) & \ - (crit <= size_ranges[:, [1]]) + """ + crit = ((reg_targets_per_im[:, :, :2] + reg_targets_per_im[:, :, 2:]) ** 2).sum( + dim=2 + ) ** 0.5 / 2 # M x N + is_cared_in_the_level = (crit >= size_ranges[:, [0]]) & (crit <= size_ranges[:, [1]]) return is_cared_in_the_level - def _get_reg_targets(self, reg_targets, dist, mask, area): - ''' - reg_targets (M x N x 4): long tensor - dist (M x N) - is_*: M x N - ''' + """ + reg_targets (M x N x 4): long tensor + dist (M x N) + is_*: M x N + """ dist[mask == 0] = INF * 1.0 - min_dist, min_inds = dist.min(dim=1) # M - reg_targets_per_im = reg_targets[ - range(len(reg_targets)), min_inds] # M x N x 4 --> M x 4 - reg_targets_per_im[min_dist == INF] = - INF + min_dist, min_inds = dist.min(dim=1) # M + reg_targets_per_im = reg_targets[range(len(reg_targets)), min_inds] # M x N x 4 --> M x 4 + reg_targets_per_im[min_dist == INF] = -INF return reg_targets_per_im - - def _create_heatmaps_from_dist(self, dist, labels, channels): - ''' + def _create_heatmaps_from_dist(self, dist, labels: Sequence[str], channels): + """ dist: M x N labels: N return: heatmaps: M x C - ''' + """ heatmaps = dist.new_zeros((dist.shape[0], channels)) for c in range(channels): - inds = (labels == c) # N + inds = labels == c # N if inds.int().sum() == 0: continue heatmaps[:, c] = torch.exp(-dist[:, inds].min(dim=1)[0]) @@ -555,118 +578,129 @@ def _create_heatmaps_from_dist(self, dist, labels, channels): heatmaps[zeros, c] = 0 return heatmaps - def _create_agn_heatmaps_from_dist(self, dist): - ''' + """ TODO (Xingyi): merge it with _create_heatmaps_from_dist dist: M x N return: heatmaps: M x 1 - ''' + """ heatmaps = dist.new_zeros((dist.shape[0], 1)) heatmaps[:, 0] = torch.exp(-dist.min(dim=1)[0]) zeros = heatmaps < 1e-4 heatmaps[zeros] = 0 return heatmaps - def _flatten_outputs(self, clss, reg_pred, agn_hm_pred): # Reshape: (N, F, Hl, Wl) -> (N, Hl, Wl, F) -> (sum_l N*Hl*Wl, F) - clss = cat([x.permute(0, 2, 3, 1).reshape(-1, x.shape[1]) \ - for x in clss], dim=0) if clss[0] is not None else None - reg_pred = cat( - [x.permute(0, 2, 3, 1).reshape(-1, 4) for x in reg_pred], dim=0) - agn_hm_pred = cat([x.permute(0, 2, 3, 1).reshape(-1) \ - for x in agn_hm_pred], dim=0) if self.with_agn_hm else None + clss = ( + cat([x.permute(0, 2, 3, 1).reshape(-1, x.shape[1]) for x in clss], dim=0) + if clss[0] is not None + else None + ) + reg_pred = cat([x.permute(0, 2, 3, 1).reshape(-1, 4) for x in reg_pred], dim=0) + agn_hm_pred = ( + cat([x.permute(0, 2, 3, 1).reshape(-1) for x in agn_hm_pred], dim=0) + if self.with_agn_hm + else None + ) return clss, reg_pred, agn_hm_pred - - def get_center3x3(self, locations, centers, strides): - ''' + def get_center3x3(self, locations, centers, strides: Sequence[int]): + """ Inputs: locations: M x 2 centers: N x 2 strides: M - ''' + """ M, N = locations.shape[0], centers.shape[0] - locations_expanded = locations.view(M, 1, 2).expand(M, N, 2) # M x N x 2 - centers_expanded = centers.view(1, N, 2).expand(M, N, 2) # M x N x 2 - strides_expanded = strides.view(M, 1, 1).expand(M, N, 2) # M x N - centers_discret = ((centers_expanded / strides_expanded).int() * \ - strides_expanded).float() + strides_expanded / 2 # M x N x 2 + locations_expanded = locations.view(M, 1, 2).expand(M, N, 2) # M x N x 2 + centers_expanded = centers.view(1, N, 2).expand(M, N, 2) # M x N x 2 + strides_expanded = strides.view(M, 1, 1).expand(M, N, 2) # M x N + centers_discret = ( + (centers_expanded / strides_expanded).int() * strides_expanded + ).float() + strides_expanded / 2 # M x N x 2 dist_x = (locations_expanded[:, :, 0] - centers_discret[:, :, 0]).abs() dist_y = (locations_expanded[:, :, 1] - centers_discret[:, :, 1]).abs() - return (dist_x <= strides_expanded[:, :, 0]) & \ - (dist_y <= strides_expanded[:, :, 0]) - + return (dist_x <= strides_expanded[:, :, 0]) & (dist_y <= strides_expanded[:, :, 0]) @torch.no_grad() - def inference(self, images, clss_per_level, reg_pred_per_level, - agn_hm_pred_per_level, grids): - logits_pred = [x.sigmoid() if x is not None else None \ - for x in clss_per_level] - agn_hm_pred_per_level = [x.sigmoid() if x is not None else None \ - for x in agn_hm_pred_per_level] + def inference(self, images, clss_per_level, reg_pred_per_level, agn_hm_pred_per_level, grids): + logits_pred = [x.sigmoid() if x is not None else None for x in clss_per_level] + agn_hm_pred_per_level = [ + x.sigmoid() if x is not None else None for x in agn_hm_pred_per_level + ] if self.only_proposal: proposals = self.predict_instances( - grids, agn_hm_pred_per_level, reg_pred_per_level, - images.image_sizes, [None for _ in agn_hm_pred_per_level]) + grids, + agn_hm_pred_per_level, + reg_pred_per_level, + images.image_sizes, + [None for _ in agn_hm_pred_per_level], + ) else: proposals = self.predict_instances( - grids, logits_pred, reg_pred_per_level, - images.image_sizes, agn_hm_pred_per_level) + grids, logits_pred, reg_pred_per_level, images.image_sizes, agn_hm_pred_per_level + ) if self.as_proposal or self.only_proposal: for p in range(len(proposals)): - proposals[p].proposal_boxes = proposals[p].get('pred_boxes') - proposals[p].objectness_logits = proposals[p].get('scores') - proposals[p].remove('pred_boxes') + proposals[p].proposal_boxes = proposals[p].get("pred_boxes") + proposals[p].objectness_logits = proposals[p].get("scores") + proposals[p].remove("pred_boxes") if self.debug: debug_test( - [self.denormalizer(x) for x in images], - logits_pred, reg_pred_per_level, - agn_hm_pred_per_level, preds=proposals, - vis_thresh=self.vis_thresh, - debug_show_name=False) + [self.denormalizer(x) for x in images], + logits_pred, + reg_pred_per_level, + agn_hm_pred_per_level, + preds=proposals, + vis_thresh=self.vis_thresh, + debug_show_name=False, + ) return proposals, {} - @torch.no_grad() def predict_instances( - self, grids, logits_pred, reg_pred, image_sizes, agn_hm_pred, - is_proposal=False): + self, grids, logits_pred, reg_pred, image_sizes: Sequence[int], agn_hm_pred, is_proposal: bool=False + ): sampled_boxes = [] for l in range(len(grids)): - sampled_boxes.append(self.predict_single_level( - grids[l], logits_pred[l], reg_pred[l] * self.strides[l], - image_sizes, agn_hm_pred[l], l, is_proposal=is_proposal)) - boxlists = list(zip(*sampled_boxes)) + sampled_boxes.append( + self.predict_single_level( + grids[l], + logits_pred[l], + reg_pred[l] * self.strides[l], + image_sizes, + agn_hm_pred[l], + l, + is_proposal=is_proposal, + ) + ) + boxlists = list(zip(*sampled_boxes, strict=False)) boxlists = [Instances.cat(boxlist) for boxlist in boxlists] - boxlists = self.nms_and_topK( - boxlists, nms=not self.not_nms) + boxlists = self.nms_and_topK(boxlists, nms=not self.not_nms) return boxlists - @torch.no_grad() def predict_single_level( - self, grids, heatmap, reg_pred, image_sizes, agn_hm, level, - is_proposal=False): + self, grids, heatmap, reg_pred, image_sizes: Sequence[int], agn_hm, level, is_proposal: bool=False + ): N, C, H, W = heatmap.shape # put in the same format as grids if self.center_nms: - heatmap_nms = nn.functional.max_pool2d( - heatmap, (3, 3), stride=1, padding=1) + heatmap_nms = nn.functional.max_pool2d(heatmap, (3, 3), stride=1, padding=1) heatmap = heatmap * (heatmap_nms == heatmap).float() - heatmap = heatmap.permute(0, 2, 3, 1) # N x H x W x C - heatmap = heatmap.reshape(N, -1, C) # N x HW x C - box_regression = reg_pred.view(N, 4, H, W).permute(0, 2, 3, 1) # N x H x W x 4 + heatmap = heatmap.permute(0, 2, 3, 1) # N x H x W x C + heatmap = heatmap.reshape(N, -1, C) # N x HW x C + box_regression = reg_pred.view(N, 4, H, W).permute(0, 2, 3, 1) # N x H x W x 4 box_regression = box_regression.reshape(N, -1, 4) - candidate_inds = heatmap > self.score_thresh # 0.05 - pre_nms_top_n = candidate_inds.view(N, -1).sum(1) # N + candidate_inds = heatmap > self.score_thresh # 0.05 + pre_nms_top_n = candidate_inds.view(N, -1).sum(1) # N pre_nms_topk = self.pre_nms_topk_train if self.training else self.pre_nms_topk_test - pre_nms_top_n = pre_nms_top_n.clamp(max=pre_nms_topk) # N + pre_nms_top_n = pre_nms_top_n.clamp(max=pre_nms_topk) # N if agn_hm is not None: agn_hm = agn_hm.view(N, 1, H, W).permute(0, 2, 3, 1) @@ -675,118 +709,115 @@ def predict_single_level( results = [] for i in range(N): - per_box_cls = heatmap[i] # HW x C - per_candidate_inds = candidate_inds[i] # n - per_box_cls = per_box_cls[per_candidate_inds] # n + per_box_cls = heatmap[i] # HW x C + per_candidate_inds = candidate_inds[i] # n + per_box_cls = per_box_cls[per_candidate_inds] # n - per_candidate_nonzeros = per_candidate_inds.nonzero() # n - per_box_loc = per_candidate_nonzeros[:, 0] # n - per_class = per_candidate_nonzeros[:, 1] # n + per_candidate_nonzeros = per_candidate_inds.nonzero() # n + per_box_loc = per_candidate_nonzeros[:, 0] # n + per_class = per_candidate_nonzeros[:, 1] # n - per_box_regression = box_regression[i] # HW x 4 - per_box_regression = per_box_regression[per_box_loc] # n x 4 - per_grids = grids[per_box_loc] # n x 2 + per_box_regression = box_regression[i] # HW x 4 + per_box_regression = per_box_regression[per_box_loc] # n x 4 + per_grids = grids[per_box_loc] # n x 2 - per_pre_nms_top_n = pre_nms_top_n[i] # 1 + per_pre_nms_top_n = pre_nms_top_n[i] # 1 if per_candidate_inds.sum().item() > per_pre_nms_top_n.item(): - per_box_cls, top_k_indices = \ - per_box_cls.topk(per_pre_nms_top_n, sorted=False) + per_box_cls, top_k_indices = per_box_cls.topk(per_pre_nms_top_n, sorted=False) per_class = per_class[top_k_indices] per_box_regression = per_box_regression[top_k_indices] per_grids = per_grids[top_k_indices] - - detections = torch.stack([ - per_grids[:, 0] - per_box_regression[:, 0], - per_grids[:, 1] - per_box_regression[:, 1], - per_grids[:, 0] + per_box_regression[:, 2], - per_grids[:, 1] + per_box_regression[:, 3], - ], dim=1) # n x 4 + + detections = torch.stack( + [ + per_grids[:, 0] - per_box_regression[:, 0], + per_grids[:, 1] - per_box_regression[:, 1], + per_grids[:, 0] + per_box_regression[:, 2], + per_grids[:, 1] + per_box_regression[:, 3], + ], + dim=1, + ) # n x 4 # avoid invalid boxes in RoI heads detections[:, 2] = torch.max(detections[:, 2], detections[:, 0] + 0.01) detections[:, 3] = torch.max(detections[:, 3], detections[:, 1] + 0.01) boxlist = Instances(image_sizes[i]) - boxlist.scores = torch.sqrt(per_box_cls) \ - if self.with_agn_hm else per_box_cls # n + boxlist.scores = torch.sqrt(per_box_cls) if self.with_agn_hm else per_box_cls # n # import pdb; pdb.set_trace() boxlist.pred_boxes = Boxes(detections) boxlist.pred_classes = per_class results.append(boxlist) return results - @torch.no_grad() - def nms_and_topK(self, boxlists, nms=True): + def nms_and_topK(self, boxlists, nms: bool=True): num_images = len(boxlists) results = [] for i in range(num_images): - nms_thresh = self.nms_thresh_train if self.training else \ - self.nms_thresh_test + nms_thresh = self.nms_thresh_train if self.training else self.nms_thresh_test result = ml_nms(boxlists[i], nms_thresh) if nms else boxlists[i] if self.debug: - print('#proposals before nms', len(boxlists[i])) - print('#proposals after nms', len(result)) + print("#proposals before nms", len(boxlists[i])) + print("#proposals after nms", len(result)) num_dets = len(result) - post_nms_topk = self.post_nms_topk_train if self.training else \ - self.post_nms_topk_test + post_nms_topk = self.post_nms_topk_train if self.training else self.post_nms_topk_test if num_dets > post_nms_topk: cls_scores = result.scores image_thresh, _ = torch.kthvalue( - cls_scores.float().cpu(), - num_dets - post_nms_topk + 1 + cls_scores.float().cpu(), num_dets - post_nms_topk + 1 ) keep = cls_scores >= image_thresh.item() keep = torch.nonzero(keep).squeeze(1) result = result[keep] if self.debug: - print('#proposals after filter', len(result)) + print("#proposals after filter", len(result)) results.append(result) return results - @torch.no_grad() def _add_more_pos(self, reg_pred, gt_instances, shapes_per_level): - labels, level_masks, c33_inds, c33_masks, c33_regs = \ - self._get_c33_inds(gt_instances, shapes_per_level) + labels, level_masks, c33_inds, c33_masks, c33_regs = self._get_c33_inds( + gt_instances, shapes_per_level + ) N, L, K = labels.shape[0], len(self.strides), 9 c33_inds[c33_masks == 0] = 0 - reg_pred_c33 = reg_pred[c33_inds].detach() # N x L x K + reg_pred_c33 = reg_pred[c33_inds].detach() # N x L x K invalid_reg = c33_masks == 0 c33_regs_expand = c33_regs.view(N * L * K, 4).clamp(min=0) if N > 0: with torch.no_grad(): - c33_reg_loss = self.iou_loss( - reg_pred_c33.view(N * L * K, 4), - c33_regs_expand, None, - reduction='none').view(N, L, K).detach() # N x L x K + c33_reg_loss = ( + self.iou_loss( + reg_pred_c33.view(N * L * K, 4), c33_regs_expand, None, reduction="none" + ) + .view(N, L, K) + .detach() + ) # N x L x K else: c33_reg_loss = reg_pred_c33.new_zeros((N, L, K)).detach() - c33_reg_loss[invalid_reg] = INF # N x L x K - c33_reg_loss.view(N * L, K)[level_masks.view(N * L), 4] = 0 # real center + c33_reg_loss[invalid_reg] = INF # N x L x K + c33_reg_loss.view(N * L, K)[level_masks.view(N * L), 4] = 0 # real center c33_reg_loss = c33_reg_loss.view(N, L * K) if N == 0: - loss_thresh = c33_reg_loss.new_ones((N)).float() + loss_thresh = c33_reg_loss.new_ones(N).float() else: - loss_thresh = torch.kthvalue( - c33_reg_loss, self.more_pos_topk, dim=1)[0] # N - loss_thresh[loss_thresh > self.more_pos_thresh] = self.more_pos_thresh # N - new_pos = c33_reg_loss.view(N, L, K) < \ - loss_thresh.view(N, 1, 1).expand(N, L, K) - pos_inds = c33_inds[new_pos].view(-1) # P + loss_thresh = torch.kthvalue(c33_reg_loss, self.more_pos_topk, dim=1)[0] # N + loss_thresh[loss_thresh > self.more_pos_thresh] = self.more_pos_thresh # N + new_pos = c33_reg_loss.view(N, L, K) < loss_thresh.view(N, 1, 1).expand(N, L, K) + pos_inds = c33_inds[new_pos].view(-1) # P labels = labels.view(N, 1, 1).expand(N, L, K)[new_pos].view(-1) return pos_inds, labels - - + @torch.no_grad() def _get_c33_inds(self, gt_instances, shapes_per_level): - ''' + """ TODO (Xingyi): The current implementation is ugly. Refactor. Get the center (and the 3x3 region near center) locations of each objects Inputs: gt_instances: [n_i], sum n_i = N shapes_per_level: L x 2 [(h_l, w_l)]_L - ''' + """ labels = [] level_masks = [] c33_inds = [] @@ -795,58 +826,61 @@ def _get_c33_inds(self, gt_instances, shapes_per_level): L = len(self.strides) B = len(gt_instances) shapes_per_level = shapes_per_level.long() - loc_per_level = (shapes_per_level[:, 0] * shapes_per_level[:, 1]).long() # L + loc_per_level = (shapes_per_level[:, 0] * shapes_per_level[:, 1]).long() # L level_bases = [] s = 0 for l in range(L): level_bases.append(s) s = s + B * loc_per_level[l] - level_bases = shapes_per_level.new_tensor(level_bases).long() # L - strides_default = shapes_per_level.new_tensor(self.strides).float() # L + level_bases = shapes_per_level.new_tensor(level_bases).long() # L + strides_default = shapes_per_level.new_tensor(self.strides).float() # L K = 9 dx = shapes_per_level.new_tensor([-1, 0, 1, -1, 0, 1, -1, 0, 1]).long() dy = shapes_per_level.new_tensor([-1, -1, -1, 0, 0, 0, 1, 1, 1]).long() for im_i in range(B): targets_per_im = gt_instances[im_i] - bboxes = targets_per_im.gt_boxes.tensor # n x 4 + bboxes = targets_per_im.gt_boxes.tensor # n x 4 n = bboxes.shape[0] if n == 0: continue - centers = ((bboxes[:, [0, 1]] + bboxes[:, [2, 3]]) / 2) # n x 2 + centers = (bboxes[:, [0, 1]] + bboxes[:, [2, 3]]) / 2 # n x 2 centers = centers.view(n, 1, 2).expand(n, L, 2) - strides = strides_default.view(1, L, 1).expand(n, L, 2) # - centers_inds = (centers / strides).long() # n x L x 2 - center_grids = centers_inds * strides + strides // 2# n x L x 2 + strides = strides_default.view(1, L, 1).expand(n, L, 2) # + centers_inds = (centers / strides).long() # n x L x 2 + center_grids = centers_inds * strides + strides // 2 # n x L x 2 l = center_grids[:, :, 0] - bboxes[:, 0].view(n, 1).expand(n, L) t = center_grids[:, :, 1] - bboxes[:, 1].view(n, 1).expand(n, L) r = bboxes[:, 2].view(n, 1).expand(n, L) - center_grids[:, :, 0] - b = bboxes[:, 3].view(n, 1).expand(n, L) - center_grids[:, :, 1] # n x L - reg = torch.stack([l, t, r, b], dim=2) # n x L x 4 + b = bboxes[:, 3].view(n, 1).expand(n, L) - center_grids[:, :, 1] # n x L + reg = torch.stack([l, t, r, b], dim=2) # n x L x 4 reg = reg / strides_default.view(1, L, 1).expand(n, L, 4).float() - + Ws = shapes_per_level[:, 1].view(1, L).expand(n, L) Hs = shapes_per_level[:, 0].view(1, L).expand(n, L) expand_Ws = Ws.view(n, L, 1).expand(n, L, K) expand_Hs = Hs.view(n, L, 1).expand(n, L, K) label = targets_per_im.gt_classes.view(n).clone() - mask = reg.min(dim=2)[0] >= 0 # n x L + mask = reg.min(dim=2)[0] >= 0 # n x L mask = mask & self.assign_fpn_level(bboxes) - labels.append(label) # n - level_masks.append(mask) # n x L + labels.append(label) # n + level_masks.append(mask) # n x L Dy = dy.view(1, 1, K).expand(n, L, K) Dx = dx.view(1, 1, K).expand(n, L, K) - c33_ind = level_bases.view(1, L, 1).expand(n, L, K) + \ - im_i * loc_per_level.view(1, L, 1).expand(n, L, K) + \ - (centers_inds[:, :, 1:2].expand(n, L, K) + Dy) * expand_Ws + \ - (centers_inds[:, :, 0:1].expand(n, L, K) + Dx) # n x L x K - - c33_mask = \ - ((centers_inds[:, :, 1:2].expand(n, L, K) + dy) < expand_Hs) & \ - ((centers_inds[:, :, 1:2].expand(n, L, K) + dy) >= 0) & \ - ((centers_inds[:, :, 0:1].expand(n, L, K) + dx) < expand_Ws) & \ - ((centers_inds[:, :, 0:1].expand(n, L, K) + dx) >= 0) + c33_ind = ( + level_bases.view(1, L, 1).expand(n, L, K) + + im_i * loc_per_level.view(1, L, 1).expand(n, L, K) + + (centers_inds[:, :, 1:2].expand(n, L, K) + Dy) * expand_Ws + + (centers_inds[:, :, 0:1].expand(n, L, K) + Dx) + ) # n x L x K + + c33_mask = ( + ((centers_inds[:, :, 1:2].expand(n, L, K) + dy) < expand_Hs) + & ((centers_inds[:, :, 1:2].expand(n, L, K) + dy) >= 0) + & ((centers_inds[:, :, 0:1].expand(n, L, K) + dx) < expand_Ws) + & ((centers_inds[:, :, 0:1].expand(n, L, K) + dx) >= 0) + ) # TODO (Xingyi): think about better way to implement this # Currently it hard codes the 3x3 region c33_reg = reg.view(n, L, 1, 4).expand(n, L, K, 4).clone() @@ -858,11 +892,11 @@ def _get_c33_inds(self, gt_instances, shapes_per_level): c33_reg[:, :, [0, 1, 2], 3] += 1 c33_reg[:, :, [6, 7, 8], 1] += 1 c33_reg[:, :, [6, 7, 8], 3] -= 1 - c33_mask = c33_mask & (c33_reg.min(dim=3)[0] >= 0) # n x L x K + c33_mask = c33_mask & (c33_reg.min(dim=3)[0] >= 0) # n x L x K c33_inds.append(c33_ind) c33_masks.append(c33_mask) c33_regs.append(c33_reg) - + if len(level_masks) > 0: labels = torch.cat(labels, dim=0) level_masks = torch.cat(level_masks, dim=0) @@ -870,9 +904,9 @@ def _get_c33_inds(self, gt_instances, shapes_per_level): c33_regs = torch.cat(c33_regs, dim=0) c33_masks = torch.cat(c33_masks, dim=0) else: - labels = shapes_per_level.new_zeros((0)).long() + labels = shapes_per_level.new_zeros(0).long() level_masks = shapes_per_level.new_zeros((0, L)).bool() c33_inds = shapes_per_level.new_zeros((0, L, K)).long() c33_regs = shapes_per_level.new_zeros((0, L, K, 4)).float() c33_masks = shapes_per_level.new_zeros((0, L, K)).bool() - return labels, level_masks, c33_inds, c33_masks, c33_regs # N x L, N x L x K + return labels, level_masks, c33_inds, c33_masks, c33_regs # N x L, N x L x K diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/dense_heads/centernet_head.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/dense_heads/centernet_head.py index 57e0960a57..e2e1852e27 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/dense_heads/centernet_head.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/dense_heads/centernet_head.py @@ -1,39 +1,43 @@ import math -from typing import List + +from detectron2.config import configurable +from detectron2.layers import get_norm import torch from torch import nn from torch.nn import functional as F -from detectron2.layers import ShapeSpec, get_norm -from detectron2.config import configurable from ..layers.deform_conv import DFConv2d __all__ = ["CenterNetHead"] + class Scale(nn.Module): - def __init__(self, init_value=1.0): - super(Scale, self).__init__() + def __init__(self, init_value: float=1.0) -> None: + super().__init__() self.scale = nn.Parameter(torch.FloatTensor([init_value])) def forward(self, input): return input * self.scale + class CenterNetHead(nn.Module): @configurable - def __init__(self, + def __init__( + self, # input_shape: List[ShapeSpec], in_channels, - num_levels, + num_levels: int, *, - num_classes=80, - with_agn_hm=False, - only_proposal=False, - norm='GN', - num_cls_convs=4, - num_box_convs=4, - num_share_convs=0, - use_deformable=False, - prior_prob=0.01): + num_classes: int=80, + with_agn_hm: bool=False, + only_proposal: bool=False, + norm: str="GN", + num_cls_convs: int=4, + num_box_convs: int=4, + num_share_convs: int=0, + use_deformable: bool=False, + prior_prob: float=0.01, + ) -> None: super().__init__() self.num_classes = num_classes self.with_agn_hm = with_agn_hm @@ -41,19 +45,19 @@ def __init__(self, self.out_kernel = 3 head_configs = { - "cls": (num_cls_convs if not self.only_proposal else 0, \ - use_deformable), + "cls": (num_cls_convs if not self.only_proposal else 0, use_deformable), "bbox": (num_box_convs, use_deformable), - "share": (num_share_convs, use_deformable)} + "share": (num_share_convs, use_deformable), + } # in_channels = [s.channels for s in input_shape] # assert len(set(in_channels)) == 1, \ # "Each level must have the same channel!" # in_channels = in_channels[0] channels = { - 'cls': in_channels, - 'bbox': in_channels, - 'share': in_channels, + "cls": in_channels, + "bbox": in_channels, + "share": in_channels, } for head in head_configs: tower = [] @@ -64,30 +68,32 @@ def __init__(self, conv_func = DFConv2d else: conv_func = nn.Conv2d - tower.append(conv_func( + tower.append( + conv_func( in_channels if i == 0 else channel, - channel, - kernel_size=3, stride=1, - padding=1, bias=True - )) - if norm == 'GN' and channel % 32 != 0: + channel, + kernel_size=3, + stride=1, + padding=1, + bias=True, + ) + ) + if norm == "GN" and channel % 32 != 0: tower.append(nn.GroupNorm(25, channel)) - elif norm != '': + elif norm != "": tower.append(get_norm(norm, channel)) tower.append(nn.ReLU()) - self.add_module('{}_tower'.format(head), - nn.Sequential(*tower)) + self.add_module(f"{head}_tower", nn.Sequential(*tower)) self.bbox_pred = nn.Conv2d( - in_channels, 4, kernel_size=self.out_kernel, - stride=1, padding=self.out_kernel // 2 + in_channels, 4, kernel_size=self.out_kernel, stride=1, padding=self.out_kernel // 2 ) - self.scales = nn.ModuleList( - [Scale(init_value=1.0) for _ in range(num_levels)]) + self.scales = nn.ModuleList([Scale(init_value=1.0) for _ in range(num_levels)]) for modules in [ - self.cls_tower, self.bbox_tower, + self.cls_tower, + self.bbox_tower, self.share_tower, self.bbox_pred, ]: @@ -95,15 +101,14 @@ def __init__(self, if isinstance(l, nn.Conv2d): torch.nn.init.normal_(l.weight, std=0.01) torch.nn.init.constant_(l.bias, 0) - - torch.nn.init.constant_(self.bbox_pred.bias, 8.) + + torch.nn.init.constant_(self.bbox_pred.bias, 8.0) prior_prob = prior_prob bias_value = -math.log((1 - prior_prob) / prior_prob) if self.with_agn_hm: self.agn_hm = nn.Conv2d( - in_channels, 1, kernel_size=self.out_kernel, - stride=1, padding=self.out_kernel // 2 + in_channels, 1, kernel_size=self.out_kernel, stride=1, padding=self.out_kernel // 2 ) torch.nn.init.constant_(self.agn_hm.bias, bias_value) torch.nn.init.normal_(self.agn_hm.weight, std=0.01) @@ -111,8 +116,9 @@ def __init__(self, if not self.only_proposal: cls_kernel_size = self.out_kernel self.cls_logits = nn.Conv2d( - in_channels, self.num_classes, - kernel_size=cls_kernel_size, + in_channels, + self.num_classes, + kernel_size=cls_kernel_size, stride=1, padding=cls_kernel_size // 2, ) @@ -124,17 +130,17 @@ def __init__(self, def from_config(cls, cfg, input_shape): ret = { # 'input_shape': input_shape, - 'in_channels': [s.channels for s in input_shape][0], - 'num_levels': len(input_shape), - 'num_classes': cfg.MODEL.CENTERNET.NUM_CLASSES, - 'with_agn_hm': cfg.MODEL.CENTERNET.WITH_AGN_HM, - 'only_proposal': cfg.MODEL.CENTERNET.ONLY_PROPOSAL, - 'norm': cfg.MODEL.CENTERNET.NORM, - 'num_cls_convs': cfg.MODEL.CENTERNET.NUM_CLS_CONVS, - 'num_box_convs': cfg.MODEL.CENTERNET.NUM_BOX_CONVS, - 'num_share_convs': cfg.MODEL.CENTERNET.NUM_SHARE_CONVS, - 'use_deformable': cfg.MODEL.CENTERNET.USE_DEFORMABLE, - 'prior_prob': cfg.MODEL.CENTERNET.PRIOR_PROB, + "in_channels": next(s.channels for s in input_shape), + "num_levels": len(input_shape), + "num_classes": cfg.MODEL.CENTERNET.NUM_CLASSES, + "with_agn_hm": cfg.MODEL.CENTERNET.WITH_AGN_HM, + "only_proposal": cfg.MODEL.CENTERNET.ONLY_PROPOSAL, + "norm": cfg.MODEL.CENTERNET.NORM, + "num_cls_convs": cfg.MODEL.CENTERNET.NUM_CLS_CONVS, + "num_box_convs": cfg.MODEL.CENTERNET.NUM_BOX_CONVS, + "num_share_convs": cfg.MODEL.CENTERNET.NUM_SHARE_CONVS, + "use_deformable": cfg.MODEL.CENTERNET.USE_DEFORMABLE, + "prior_prob": cfg.MODEL.CENTERNET.PRIOR_PROB, } return ret @@ -158,5 +164,5 @@ def forward(self, x): reg = self.bbox_pred(bbox_tower) reg = self.scales[l](reg) bbox_reg.append(F.relu(reg)) - - return clss, bbox_reg, agn_hms \ No newline at end of file + + return clss, bbox_reg, agn_hms diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/dense_heads/utils.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/dense_heads/utils.py index c9efa287fc..ea962943ca 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/dense_heads/utils.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/dense_heads/utils.py @@ -1,31 +1,25 @@ -import cv2 -import torch -from torch import nn from detectron2.utils.comm import get_world_size -from detectron2.structures import pairwise_iou, Boxes +import torch + # from .data import CenterNetCrop -import torch.nn.functional as F -import numpy as np -from detectron2.structures import Boxes, ImageList, Instances -__all__ = ['reduce_sum', '_transpose'] +__all__ = ["_transpose", "reduce_sum"] INF = 1000000000 + def _transpose(training_targets, num_loc_list): - ''' - This function is used to transpose image first training targets to + """ + This function is used to transpose image first training targets to level first ones :return: level first training targets - ''' + """ for im_i in range(len(training_targets)): - training_targets[im_i] = torch.split( - training_targets[im_i], num_loc_list, dim=0) + training_targets[im_i] = torch.split(training_targets[im_i], num_loc_list, dim=0) targets_level_first = [] - for targets_per_level in zip(*training_targets): - targets_level_first.append( - torch.cat(targets_per_level, dim=0)) + for targets_per_level in zip(*training_targets, strict=False): + targets_level_first.append(torch.cat(targets_per_level, dim=0)) return targets_level_first @@ -35,4 +29,4 @@ def reduce_sum(tensor): return tensor tensor = tensor.clone() torch.distributed.all_reduce(tensor, op=torch.distributed.ReduceOp.SUM) - return tensor \ No newline at end of file + return tensor diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/deform_conv.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/deform_conv.py index e5650c4067..643660c6bc 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/deform_conv.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/deform_conv.py @@ -1,8 +1,7 @@ +from detectron2.layers import Conv2d import torch from torch import nn -from detectron2.layers import Conv2d - class _NewEmptyTensorOp(torch.autograd.Function): @staticmethod @@ -18,29 +17,30 @@ def backward(ctx, grad): class DFConv2d(nn.Module): """Deformable convolutional layer""" + def __init__( - self, - in_channels, - out_channels, - with_modulated_dcn=True, - kernel_size=3, - stride=1, - groups=1, - dilation=1, - deformable_groups=1, - bias=False, - padding=None - ): - super(DFConv2d, self).__init__() - if isinstance(kernel_size, (list, tuple)): - assert isinstance(stride, (list, tuple)) - assert isinstance(dilation, (list, tuple)) + self, + in_channels, + out_channels, + with_modulated_dcn: bool=True, + kernel_size: int=3, + stride: int=1, + groups: int=1, + dilation: int=1, + deformable_groups: int=1, + bias: bool=False, + padding=None, + ) -> None: + super().__init__() + if isinstance(kernel_size, list | tuple): + assert isinstance(stride, list | tuple) + assert isinstance(dilation, list | tuple) assert len(kernel_size) == 2 assert len(stride) == 2 assert len(dilation) == 2 padding = ( dilation[0] * (kernel_size[0] - 1) // 2, - dilation[1] * (kernel_size[1] - 1) // 2 + dilation[1] * (kernel_size[1] - 1) // 2, ) offset_base_channels = kernel_size[0] * kernel_size[1] else: @@ -48,10 +48,12 @@ def __init__( offset_base_channels = kernel_size * kernel_size if with_modulated_dcn: from detectron2.layers.deform_conv import ModulatedDeformConv + offset_channels = offset_base_channels * 3 # default: 27 conv_block = ModulatedDeformConv else: from detectron2.layers.deform_conv import DeformConv + offset_channels = offset_base_channels * 2 # default: 18 conv_block = DeformConv self.offset = Conv2d( @@ -61,15 +63,15 @@ def __init__( stride=stride, padding=padding, groups=1, - dilation=dilation + dilation=dilation, ) nn.init.constant_(self.offset.weight, 0) nn.init.constant_(self.offset.bias, 0) - ''' + """ for l in [self.offset, ]: nn.init.kaiming_uniform_(l.weight, a=1) torch.nn.init.constant_(l.bias, 0.) - ''' + """ self.conv = conv_block( in_channels, out_channels, @@ -79,7 +81,7 @@ def __init__( dilation=dilation, groups=groups, deformable_groups=deformable_groups, - bias=bias + bias=bias, ) self.with_modulated_dcn = with_modulated_dcn self.kernel_size = kernel_size @@ -88,15 +90,15 @@ def __init__( self.dilation = dilation self.offset_split = offset_base_channels * deformable_groups * 2 - def forward(self, x, return_offset=False): + def forward(self, x, return_offset: bool=False): if x.numel() > 0: if not self.with_modulated_dcn: offset_mask = self.offset(x) x = self.conv(x, offset_mask) else: offset_mask = self.offset(x) - offset = offset_mask[:, :self.offset_split, :, :] - mask = offset_mask[:, self.offset_split:, :, :].sigmoid() + offset = offset_mask[:, : self.offset_split, :, :] + mask = offset_mask[:, self.offset_split :, :, :].sigmoid() x = self.conv(x, offset, mask) if return_offset: return x, offset_mask @@ -105,12 +107,8 @@ def forward(self, x, return_offset=False): output_shape = [ (i + 2 * p - (di * (k - 1) + 1)) // d + 1 for i, p, di, k, d in zip( - x.shape[-2:], - self.padding, - self.dilation, - self.kernel_size, - self.stride + x.shape[-2:], self.padding, self.dilation, self.kernel_size, self.stride, strict=False ) ] - output_shape = [x.shape[0], self.conv.weight.shape[0]] + output_shape - return _NewEmptyTensorOp.apply(x, output_shape) \ No newline at end of file + output_shape = [x.shape[0], self.conv.weight.shape[0], *output_shape] + return _NewEmptyTensorOp.apply(x, output_shape) diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/heatmap_focal_loss.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/heatmap_focal_loss.py index 8c0c1be13d..50ccf371c9 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/heatmap_focal_loss.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/heatmap_focal_loss.py @@ -1,18 +1,19 @@ import torch -from torch.nn import functional as F +from typing import Sequence + # TODO: merge these two function def heatmap_focal_loss( inputs, targets, pos_inds, - labels, + labels: Sequence[str], alpha: float = -1, beta: float = 4, gamma: float = 2, - reduction: str = 'sum', + reduction: str = "sum", sigmoid_clamp: float = 1e-4, - ignore_high_fp: float = -1., + ignore_high_fp: float = -1.0, ): """ Loss used in RetinaNet for dense detection: https://arxiv.org/abs/1708.02002. @@ -24,9 +25,9 @@ def heatmap_focal_loss( Returns: Loss tensor with the reduction option applied. """ - pred = torch.clamp(inputs.sigmoid_(), min=sigmoid_clamp, max=1-sigmoid_clamp) + pred = torch.clamp(inputs.sigmoid_(), min=sigmoid_clamp, max=1 - sigmoid_clamp) neg_weights = torch.pow(1 - targets, beta) - pos_pred_pix = pred[pos_inds] # N x C + pos_pred_pix = pred[pos_inds] # N x C pos_pred = pos_pred_pix.gather(1, labels.unsqueeze(1)) pos_loss = torch.log(pos_pred) * torch.pow(1 - pos_pred, gamma) neg_loss = torch.log(1 - pred) * torch.pow(pred, gamma) * neg_weights @@ -43,11 +44,13 @@ def heatmap_focal_loss( pos_loss = alpha * pos_loss neg_loss = (1 - alpha) * neg_loss - return - pos_loss, - neg_loss + return -pos_loss, -neg_loss + heatmap_focal_loss_jit = torch.jit.script(heatmap_focal_loss) # heatmap_focal_loss_jit = heatmap_focal_loss + def binary_heatmap_focal_loss( inputs, targets, @@ -56,7 +59,7 @@ def binary_heatmap_focal_loss( beta: float = 4, gamma: float = 2, sigmoid_clamp: float = 1e-4, - ignore_high_fp: float = -1., + ignore_high_fp: float = -1.0, ): """ Args: @@ -66,17 +69,17 @@ def binary_heatmap_focal_loss( Returns: Loss tensor with the reduction option applied. """ - pred = torch.clamp(inputs.sigmoid_(), min=sigmoid_clamp, max=1-sigmoid_clamp) + pred = torch.clamp(inputs.sigmoid_(), min=sigmoid_clamp, max=1 - sigmoid_clamp) neg_weights = torch.pow(1 - targets, beta) - pos_pred = pred[pos_inds] # N + pos_pred = pred[pos_inds] # N pos_loss = torch.log(pos_pred) * torch.pow(1 - pos_pred, gamma) neg_loss = torch.log(1 - pred) * torch.pow(pred, gamma) * neg_weights if ignore_high_fp > 0: not_high_fp = (pred < ignore_high_fp).float() neg_loss = not_high_fp * neg_loss - pos_loss = - pos_loss.sum() - neg_loss = - neg_loss.sum() + pos_loss = -pos_loss.sum() + neg_loss = -neg_loss.sum() if alpha >= 0: pos_loss = alpha * pos_loss @@ -84,4 +87,5 @@ def binary_heatmap_focal_loss( return pos_loss, neg_loss -binary_heatmap_focal_loss_jit = torch.jit.script(binary_heatmap_focal_loss) \ No newline at end of file + +binary_heatmap_focal_loss_jit = torch.jit.script(binary_heatmap_focal_loss) diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/iou_loss.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/iou_loss.py index 6a02464651..55fa2a186d 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/iou_loss.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/iou_loss.py @@ -3,11 +3,11 @@ class IOULoss(nn.Module): - def __init__(self, loc_loss_type='iou'): - super(IOULoss, self).__init__() + def __init__(self, loc_loss_type: str="iou") -> None: + super().__init__() self.loc_loss_type = loc_loss_type - def forward(self, pred, target, weight=None, reduction='sum'): + def forward(self, pred, target, weight=None, reduction: str="sum"): pred_left = pred[:, 0] pred_top = pred[:, 1] pred_right = pred[:, 2] @@ -18,20 +18,14 @@ def forward(self, pred, target, weight=None, reduction='sum'): target_right = target[:, 2] target_bottom = target[:, 3] - target_aera = (target_left + target_right) * \ - (target_top + target_bottom) - pred_aera = (pred_left + pred_right) * \ - (pred_top + pred_bottom) + target_aera = (target_left + target_right) * (target_top + target_bottom) + pred_aera = (pred_left + pred_right) * (pred_top + pred_bottom) - w_intersect = torch.min(pred_left, target_left) + \ - torch.min(pred_right, target_right) - h_intersect = torch.min(pred_bottom, target_bottom) + \ - torch.min(pred_top, target_top) + w_intersect = torch.min(pred_left, target_left) + torch.min(pred_right, target_right) + h_intersect = torch.min(pred_bottom, target_bottom) + torch.min(pred_top, target_top) - g_w_intersect = torch.max(pred_left, target_left) + \ - torch.max(pred_right, target_right) - g_h_intersect = torch.max(pred_bottom, target_bottom) + \ - torch.max(pred_top, target_top) + g_w_intersect = torch.max(pred_left, target_left) + torch.max(pred_right, target_right) + g_h_intersect = torch.max(pred_bottom, target_bottom) + torch.max(pred_top, target_top) ac_uion = g_w_intersect * g_h_intersect area_intersect = w_intersect * h_intersect @@ -39,11 +33,11 @@ def forward(self, pred, target, weight=None, reduction='sum'): ious = (area_intersect + 1.0) / (area_union + 1.0) gious = ious - (ac_uion - area_union) / ac_uion - if self.loc_loss_type == 'iou': + if self.loc_loss_type == "iou": losses = -torch.log(ious) - elif self.loc_loss_type == 'linear_iou': + elif self.loc_loss_type == "linear_iou": losses = 1 - ious - elif self.loc_loss_type == 'giou': + elif self.loc_loss_type == "giou": losses = 1 - gious else: raise NotImplementedError @@ -53,11 +47,11 @@ def forward(self, pred, target, weight=None, reduction='sum'): else: losses = losses - if reduction == 'sum': + if reduction == "sum": return losses.sum() - elif reduction == 'batch': + elif reduction == "batch": return losses.sum(dim=[1]) - elif reduction == 'none': + elif reduction == "none": return losses else: raise NotImplementedError @@ -118,4 +112,4 @@ def giou_loss( elif reduction == "sum": loss = loss.sum() - return loss \ No newline at end of file + return loss diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/ml_nms.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/ml_nms.py index 325d709a98..429c986cfe 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/ml_nms.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/layers/ml_nms.py @@ -1,8 +1,7 @@ from detectron2.layers import batched_nms -def ml_nms(boxlist, nms_thresh, max_proposals=-1, - score_field="scores", label_field="labels"): +def ml_nms(boxlist, nms_thresh, max_proposals=-1, score_field: str="scores", label_field: str="labels"): """ Performs non-maximum suppression on a boxlist, with scores specified in a boxlist field via score_field. @@ -15,17 +14,16 @@ def ml_nms(boxlist, nms_thresh, max_proposals=-1, """ if nms_thresh <= 0: return boxlist - if boxlist.has('pred_boxes'): + if boxlist.has("pred_boxes"): boxes = boxlist.pred_boxes.tensor labels = boxlist.pred_classes else: boxes = boxlist.proposal_boxes.tensor - labels = boxlist.proposal_boxes.tensor.new_zeros( - len(boxlist.proposal_boxes.tensor)) + labels = boxlist.proposal_boxes.tensor.new_zeros(len(boxlist.proposal_boxes.tensor)) scores = boxlist.scores - + keep = batched_nms(boxes, scores, labels, nms_thresh) if max_proposals > 0: - keep = keep[: max_proposals] + keep = keep[:max_proposals] boxlist = boxlist[keep] return boxlist diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/meta_arch/centernet_detector.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/meta_arch/centernet_detector.py index b7525c7b31..02cd3da416 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/meta_arch/centernet_detector.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/meta_arch/centernet_detector.py @@ -1,27 +1,23 @@ -import math -import json -import numpy as np +from detectron2.modeling import build_backbone, build_proposal_generator, detector_postprocess +from detectron2.modeling.meta_arch.build import META_ARCH_REGISTRY +from detectron2.structures import ImageList import torch from torch import nn -from detectron2.modeling.meta_arch.build import META_ARCH_REGISTRY -from detectron2.modeling import build_backbone, build_proposal_generator -from detectron2.modeling import detector_postprocess -from detectron2.structures import ImageList @META_ARCH_REGISTRY.register() class CenterNetDetector(nn.Module): - def __init__(self, cfg): + def __init__(self, cfg) -> None: super().__init__() self.mean, self.std = cfg.MODEL.PIXEL_MEAN, cfg.MODEL.PIXEL_STD self.register_buffer("pixel_mean", torch.Tensor(cfg.MODEL.PIXEL_MEAN).view(-1, 1, 1)) self.register_buffer("pixel_std", torch.Tensor(cfg.MODEL.PIXEL_STD).view(-1, 1, 1)) - + self.backbone = build_backbone(cfg) self.proposal_generator = build_proposal_generator( - cfg, self.backbone.output_shape()) # TODO: change to a more precise name - - + cfg, self.backbone.output_shape() + ) # TODO: change to a more precise name + def forward(self, batched_inputs): if not self.training: return self.inference(batched_inputs) @@ -29,18 +25,15 @@ def forward(self, batched_inputs): features = self.backbone(images.tensor) gt_instances = [x["instances"].to(self.device) for x in batched_inputs] - _, proposal_losses = self.proposal_generator( - images, features, gt_instances) + _, proposal_losses = self.proposal_generator(images, features, gt_instances) return proposal_losses - @property def device(self): return self.pixel_mean.device - @torch.no_grad() - def inference(self, batched_inputs, do_postprocess=True): + def inference(self, batched_inputs, do_postprocess: bool=True): images = self.preprocess_image(batched_inputs) inp = images.tensor features = self.backbone(inp) @@ -48,7 +41,8 @@ def inference(self, batched_inputs, do_postprocess=True): processed_results = [] for results_per_image, input_per_image, image_size in zip( - proposals, batched_inputs, images.image_sizes): + proposals, batched_inputs, images.image_sizes, strict=False + ): if do_postprocess: height = input_per_image.get("height", image_size[0]) width = input_per_image.get("width", image_size[1]) diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/roi_heads/custom_fast_rcnn.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/roi_heads/custom_fast_rcnn.py index 1f0f430d0f..b48b5447ac 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/roi_heads/custom_fast_rcnn.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/roi_heads/custom_fast_rcnn.py @@ -1,51 +1,41 @@ # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved # Part of the code is from https://github.com/tztztztztz/eql.detectron2/blob/master/projects/EQL/eql/fast_rcnn.py -import logging import math -import json -from typing import Dict, Union + +from detectron2.layers import ShapeSpec, cat +from detectron2.modeling.roi_heads.fast_rcnn import ( + FastRCNNOutputLayers, + _log_classification_stats, + fast_rcnn_inference, +) import torch -from fvcore.nn import giou_loss, smooth_l1_loss from torch import nn from torch.nn import functional as F -from detectron2.config import configurable -from detectron2.layers import Linear, ShapeSpec, batched_nms, cat, nonzero_tuple -from detectron2.modeling.box_regression import Box2BoxTransform -from detectron2.structures import Boxes, Instances -from detectron2.utils.events import get_event_storage -from detectron2.modeling.roi_heads.fast_rcnn import FastRCNNOutputLayers -from detectron2.modeling.roi_heads.fast_rcnn import fast_rcnn_inference -from detectron2.modeling.roi_heads.fast_rcnn import _log_classification_stats -from detectron2.utils.comm import get_world_size -from .fed_loss import load_class_freq, get_fed_loss_inds +from .fed_loss import get_fed_loss_inds, load_class_freq __all__ = ["CustomFastRCNNOutputLayers"] + class CustomFastRCNNOutputLayers(FastRCNNOutputLayers): - def __init__( - self, - cfg, - input_shape: ShapeSpec, - **kwargs - ): + def __init__(self, cfg, input_shape: ShapeSpec, **kwargs) -> None: super().__init__(cfg, input_shape, **kwargs) self.use_sigmoid_ce = cfg.MODEL.ROI_BOX_HEAD.USE_SIGMOID_CE if self.use_sigmoid_ce: prior_prob = cfg.MODEL.ROI_BOX_HEAD.PRIOR_PROB bias_value = -math.log((1 - prior_prob) / prior_prob) nn.init.constant_(self.cls_score.bias, bias_value) - + self.cfg = cfg self.use_fed_loss = cfg.MODEL.ROI_BOX_HEAD.USE_FED_LOSS if self.use_fed_loss: self.fed_loss_num_cat = cfg.MODEL.ROI_BOX_HEAD.FED_LOSS_NUM_CAT self.register_buffer( - 'freq_weight', + "freq_weight", load_class_freq( - cfg.MODEL.ROI_BOX_HEAD.CAT_FREQ_PATH, + cfg.MODEL.ROI_BOX_HEAD.CAT_FREQ_PATH, cfg.MODEL.ROI_BOX_HEAD.FED_LOSS_FREQ_WEIGHT, - ) + ), ) def losses(self, predictions, proposals): @@ -56,7 +46,6 @@ def losses(self, predictions, proposals): gt_classes = ( cat([p.gt_classes for p in proposals], dim=0) if len(proposals) else torch.empty(0) ) - num_classes = self.num_classes _log_classification_stats(scores, gt_classes) if len(proposals): @@ -74,42 +63,40 @@ def losses(self, predictions, proposals): else: loss_cls = self.softmax_cross_entropy_loss(scores, gt_classes) return { - "loss_cls": loss_cls, + "loss_cls": loss_cls, "loss_box_reg": self.box_reg_loss( - proposal_boxes, gt_boxes, proposal_deltas, gt_classes) + proposal_boxes, gt_boxes, proposal_deltas, gt_classes + ), } - def sigmoid_cross_entropy_loss(self, pred_class_logits, gt_classes): if pred_class_logits.numel() == 0: - return pred_class_logits.new_zeros([1])[0] # This is more robust than .sum() * 0. + return pred_class_logits.new_zeros([1])[0] # This is more robust than .sum() * 0. B = pred_class_logits.shape[0] C = pred_class_logits.shape[1] - 1 target = pred_class_logits.new_zeros(B, C + 1) - target[range(len(gt_classes)), gt_classes] = 1 # B x (C + 1) - target = target[:, :C] # B x C + target[range(len(gt_classes)), gt_classes] = 1 # B x (C + 1) + target = target[:, :C] # B x C weight = 1 - if self.use_fed_loss and (self.freq_weight is not None): # fedloss + if self.use_fed_loss and (self.freq_weight is not None): # fedloss appeared = get_fed_loss_inds( - gt_classes, - num_sample_cats=self.fed_loss_num_cat, - C=C, - weight=self.freq_weight) + gt_classes, num_sample_cats=self.fed_loss_num_cat, C=C, weight=self.freq_weight + ) appeared_mask = appeared.new_zeros(C + 1) - appeared_mask[appeared] = 1 # C + 1 + appeared_mask[appeared] = 1 # C + 1 appeared_mask = appeared_mask[:C] fed_w = appeared_mask.view(1, C).expand(B, C) weight = weight * fed_w.float() cls_loss = F.binary_cross_entropy_with_logits( - pred_class_logits[:, :-1], target, reduction='none') # B x C - loss = torch.sum(cls_loss * weight) / B + pred_class_logits[:, :-1], target, reduction="none" + ) # B x C + loss = torch.sum(cls_loss * weight) / B return loss - - + def softmax_cross_entropy_loss(self, pred_class_logits, gt_classes): """ change _no_instance handling @@ -120,22 +107,18 @@ def softmax_cross_entropy_loss(self, pred_class_logits, gt_classes): if self.use_fed_loss and (self.freq_weight is not None): C = pred_class_logits.shape[1] - 1 appeared = get_fed_loss_inds( - gt_classes, - num_sample_cats=self.fed_loss_num_cat, - C=C, - weight=self.freq_weight) + gt_classes, num_sample_cats=self.fed_loss_num_cat, C=C, weight=self.freq_weight + ) appeared_mask = appeared.new_zeros(C + 1).float() - appeared_mask[appeared] = 1. # C + 1 - appeared_mask[C] = 1. + appeared_mask[appeared] = 1.0 # C + 1 + appeared_mask[C] = 1.0 loss = F.cross_entropy( - pred_class_logits, gt_classes, - weight=appeared_mask, reduction="mean") + pred_class_logits, gt_classes, weight=appeared_mask, reduction="mean" + ) else: - loss = F.cross_entropy( - pred_class_logits, gt_classes, reduction="mean") + loss = F.cross_entropy(pred_class_logits, gt_classes, reduction="mean") return loss - def inference(self, predictions, proposals): """ enable use proposal boxes @@ -143,9 +126,8 @@ def inference(self, predictions, proposals): boxes = self.predict_boxes(predictions, proposals) scores = self.predict_probs(predictions, proposals) if self.cfg.MODEL.ROI_BOX_HEAD.MULT_PROPOSAL_SCORE: - proposal_scores = [p.get('objectness_logits') for p in proposals] - scores = [(s * ps[:, None]) ** 0.5 \ - for s, ps in zip(scores, proposal_scores)] + proposal_scores = [p.get("objectness_logits") for p in proposals] + scores = [(s * ps[:, None]) ** 0.5 for s, ps in zip(scores, proposal_scores, strict=False)] image_shapes = [x.image_size for x in proposals] return fast_rcnn_inference( boxes, @@ -156,7 +138,6 @@ def inference(self, predictions, proposals): self.test_topk_per_image, ) - def predict_probs(self, predictions, proposals): """ support sigmoid diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/roi_heads/custom_roi_heads.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/roi_heads/custom_roi_heads.py index 90fadf1a96..d0478de2f3 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/roi_heads/custom_roi_heads.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/roi_heads/custom_roi_heads.py @@ -1,41 +1,32 @@ # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved -import numpy as np -import json -import math -import torch -from torch import nn -from torch.autograd.function import Function -from typing import Dict, List, Optional, Tuple, Union - -from detectron2.layers import ShapeSpec -from detectron2.structures import Boxes, Instances, pairwise_iou -from detectron2.utils.events import get_event_storage - from detectron2.modeling.box_regression import Box2BoxTransform +from detectron2.modeling.roi_heads.cascade_rcnn import CascadeROIHeads from detectron2.modeling.roi_heads.fast_rcnn import fast_rcnn_inference from detectron2.modeling.roi_heads.roi_heads import ROI_HEADS_REGISTRY, StandardROIHeads -from detectron2.modeling.roi_heads.cascade_rcnn import CascadeROIHeads -from detectron2.modeling.roi_heads.box_head import build_box_head +from detectron2.utils.events import get_event_storage +import torch + from .custom_fast_rcnn import CustomFastRCNNOutputLayers @ROI_HEADS_REGISTRY.register() class CustomROIHeads(StandardROIHeads): @classmethod - def _init_box_head(self, cfg, input_shape): + def _init_box_head(cls, cfg, input_shape): ret = super()._init_box_head(cfg, input_shape) - del ret['box_predictor'] - ret['box_predictor'] = CustomFastRCNNOutputLayers( - cfg, ret['box_head'].output_shape) - self.debug = cfg.DEBUG - if self.debug: - self.debug_show_name = cfg.DEBUG_SHOW_NAME - self.save_debug = cfg.SAVE_DEBUG - self.vis_thresh = cfg.VIS_THRESH - self.pixel_mean = torch.Tensor(cfg.MODEL.PIXEL_MEAN).to( - torch.device(cfg.MODEL.DEVICE)).view(3, 1, 1) - self.pixel_std = torch.Tensor(cfg.MODEL.PIXEL_STD).to( - torch.device(cfg.MODEL.DEVICE)).view(3, 1, 1) + del ret["box_predictor"] + ret["box_predictor"] = CustomFastRCNNOutputLayers(cfg, ret["box_head"].output_shape) + cls.debug = cfg.DEBUG + if cls.debug: + cls.debug_show_name = cfg.DEBUG_SHOW_NAME + cls.save_debug = cfg.SAVE_DEBUG + cls.vis_thresh = cfg.VIS_THRESH + cls.pixel_mean = ( + torch.Tensor(cfg.MODEL.PIXEL_MEAN).to(torch.device(cfg.MODEL.DEVICE)).view(3, 1, 1) + ) + cls.pixel_std = ( + torch.Tensor(cfg.MODEL.PIXEL_STD).to(torch.device(cfg.MODEL.DEVICE)).view(3, 1, 1) + ) return ret def forward(self, images, features, proposals, targets=None): @@ -59,54 +50,59 @@ def forward(self, images, features, proposals, targets=None): pred_instances = self.forward_with_given_boxes(features, pred_instances) if self.debug: from ..debug import debug_second_stage - denormalizer = lambda x: x * self.pixel_std + self.pixel_mean + + def denormalizer(x): + return x * self.pixel_std + self.pixel_mean debug_second_stage( [denormalizer(images[0].clone())], - pred_instances, proposals=proposals, - debug_show_name=self.debug_show_name) + pred_instances, + proposals=proposals, + debug_show_name=self.debug_show_name, + ) return pred_instances, {} @ROI_HEADS_REGISTRY.register() class CustomCascadeROIHeads(CascadeROIHeads): @classmethod - def _init_box_head(self, cfg, input_shape): - self.mult_proposal_score = cfg.MODEL.ROI_BOX_HEAD.MULT_PROPOSAL_SCORE + def _init_box_head(cls, cfg, input_shape): + cls.mult_proposal_score = cfg.MODEL.ROI_BOX_HEAD.MULT_PROPOSAL_SCORE ret = super()._init_box_head(cfg, input_shape) - del ret['box_predictors'] + del ret["box_predictors"] cascade_bbox_reg_weights = cfg.MODEL.ROI_BOX_CASCADE_HEAD.BBOX_REG_WEIGHTS box_predictors = [] - for box_head, bbox_reg_weights in zip(ret['box_heads'], cascade_bbox_reg_weights): + for box_head, bbox_reg_weights in zip(ret["box_heads"], cascade_bbox_reg_weights, strict=False): box_predictors.append( CustomFastRCNNOutputLayers( - cfg, box_head.output_shape, - box2box_transform=Box2BoxTransform(weights=bbox_reg_weights) - )) - ret['box_predictors'] = box_predictors - self.debug = cfg.DEBUG - if self.debug: - self.debug_show_name = cfg.DEBUG_SHOW_NAME - self.save_debug = cfg.SAVE_DEBUG - self.vis_thresh = cfg.VIS_THRESH - self.pixel_mean = torch.Tensor(cfg.MODEL.PIXEL_MEAN).to( - torch.device(cfg.MODEL.DEVICE)).view(3, 1, 1) - self.pixel_std = torch.Tensor(cfg.MODEL.PIXEL_STD).to( - torch.device(cfg.MODEL.DEVICE)).view(3, 1, 1) + cfg, + box_head.output_shape, + box2box_transform=Box2BoxTransform(weights=bbox_reg_weights), + ) + ) + ret["box_predictors"] = box_predictors + cls.debug = cfg.DEBUG + if cls.debug: + cls.debug_show_name = cfg.DEBUG_SHOW_NAME + cls.save_debug = cfg.SAVE_DEBUG + cls.vis_thresh = cfg.VIS_THRESH + cls.pixel_mean = ( + torch.Tensor(cfg.MODEL.PIXEL_MEAN).to(torch.device(cfg.MODEL.DEVICE)).view(3, 1, 1) + ) + cls.pixel_std = ( + torch.Tensor(cfg.MODEL.PIXEL_STD).to(torch.device(cfg.MODEL.DEVICE)).view(3, 1, 1) + ) return ret - def _forward_box(self, features, proposals, targets=None): """ Add mult proposal scores at testing """ if (not self.training) and self.mult_proposal_score: - if len(proposals) > 0 and proposals[0].has('scores'): - proposal_scores = [ - p.get('scores') for p in proposals] + if len(proposals) > 0 and proposals[0].has("scores"): + proposal_scores = [p.get("scores") for p in proposals] else: - proposal_scores = [ - p.get('objectness_logits') for p in proposals] - + proposal_scores = [p.get("objectness_logits") for p in proposals] + features = [features[f] for f in self.box_in_features] head_outputs = [] # (predictor, predictions, proposals) prev_pred_boxes = None @@ -124,21 +120,20 @@ def _forward_box(self, features, proposals, targets=None): losses = {} storage = get_event_storage() for stage, (predictor, predictions, proposals) in enumerate(head_outputs): - with storage.name_scope("stage{}".format(stage)): + with storage.name_scope(f"stage{stage}"): stage_losses = predictor.losses(predictions, proposals) - losses.update({k + "_stage{}".format(stage): v for k, v in stage_losses.items()}) + losses.update({k + f"_stage{stage}": v for k, v in stage_losses.items()}) return losses else: # Each is a list[Tensor] of length #image. Each tensor is Ri x (K+1) scores_per_stage = [h[0].predict_probs(h[1], h[2]) for h in head_outputs] scores = [ sum(list(scores_per_image)) * (1.0 / self.num_cascade_stages) - for scores_per_image in zip(*scores_per_stage) + for scores_per_image in zip(*scores_per_stage, strict=False) ] - + if self.mult_proposal_score: - scores = [(s * ps[:, None]) ** 0.5 \ - for s, ps in zip(scores, proposal_scores)] + scores = [(s * ps[:, None]) ** 0.5 for s, ps in zip(scores, proposal_scores, strict=False)] predictor, predictions, proposals = head_outputs[-1] boxes = predictor.predict_boxes(predictions, proposals) @@ -150,13 +145,13 @@ def _forward_box(self, features, proposals, targets=None): predictor.test_nms_thresh, predictor.test_topk_per_image, ) - + return pred_instances def forward(self, images, features, proposals, targets=None): - ''' + """ enable debug - ''' + """ if not self.debug: del images if self.training: @@ -173,13 +168,15 @@ def forward(self, images, features, proposals, targets=None): pred_instances = self.forward_with_given_boxes(features, pred_instances) if self.debug: from ..debug import debug_second_stage - denormalizer = lambda x: x * self.pixel_std + self.pixel_mean + + def denormalizer(x): + return x * self.pixel_std + self.pixel_mean debug_second_stage( [denormalizer(x.clone()) for x in images], - pred_instances, proposals=proposals, + pred_instances, + proposals=proposals, save_debug=self.save_debug, debug_show_name=self.debug_show_name, - vis_thresh=self.vis_thresh) + vis_thresh=self.vis_thresh, + ) return pred_instances, {} - - diff --git a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/roi_heads/fed_loss.py b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/roi_heads/fed_loss.py index 290f0f0720..8a41607ea9 100644 --- a/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/roi_heads/fed_loss.py +++ b/dimos/models/Detic/third_party/CenterNet2/centernet/modeling/roi_heads/fed_loss.py @@ -1,21 +1,17 @@ -import torch import json -import numpy as np -from torch.nn import functional as F -def load_class_freq( - path='datasets/lvis/lvis_v1_train_cat_info.json', - freq_weight=0.5): - cat_info = json.load(open(path, 'r')) - cat_info = torch.tensor( - [c['image_count'] for c in sorted(cat_info, key=lambda x: x['id'])]) +import torch + + +def load_class_freq(path: str="datasets/lvis/lvis_v1_train_cat_info.json", freq_weight: float=0.5): + cat_info = json.load(open(path)) + cat_info = torch.tensor([c["image_count"] for c in sorted(cat_info, key=lambda x: x["id"])]) freq_weight = cat_info.float() ** freq_weight return freq_weight -def get_fed_loss_inds( - gt_classes, num_sample_cats=50, C=1203, \ - weight=None, fed_cls_inds=-1): - appeared = torch.unique(gt_classes) # C' + +def get_fed_loss_inds(gt_classes, num_sample_cats: int=50, C: int=1203, weight=None, fed_cls_inds=-1): + appeared = torch.unique(gt_classes) # C' prob = appeared.new_ones(C + 1).float() prob[-1] = 0 if len(appeared) < num_sample_cats: @@ -24,8 +20,6 @@ def get_fed_loss_inds( prob[appeared] = 0 if fed_cls_inds > 0: prob[fed_cls_inds:] = 0 - more_appeared = torch.multinomial( - prob, num_sample_cats - len(appeared), - replacement=False) + more_appeared = torch.multinomial(prob, num_sample_cats - len(appeared), replacement=False) appeared = torch.cat([appeared, more_appeared]) - return appeared \ No newline at end of file + return appeared diff --git a/dimos/models/Detic/third_party/CenterNet2/demo.py b/dimos/models/Detic/third_party/CenterNet2/demo.py index 5213faf4d8..3177d838ac 100644 --- a/dimos/models/Detic/third_party/CenterNet2/demo.py +++ b/dimos/models/Detic/third_party/CenterNet2/demo.py @@ -4,21 +4,22 @@ import multiprocessing as mp import os import time -import cv2 -import tqdm +from centernet.config import add_centernet_config +import cv2 from detectron2.config import get_cfg from detectron2.data.detection_utils import read_image from detectron2.utils.logger import setup_logger - from predictor import VisualizationDemo -from centernet.config import add_centernet_config +import tqdm + # constants WINDOW_NAME = "CenterNet2 detections" -from detectron2.utils.video_visualizer import VideoVisualizer -from detectron2.utils.visualizer import ColorMode, Visualizer from detectron2.data import MetadataCatalog +from detectron2.utils.video_visualizer import VideoVisualizer +from detectron2.utils.visualizer import ColorMode + def setup_cfg(args): # load config from file and command-line arguments @@ -29,7 +30,7 @@ def setup_cfg(args): # Set score_threshold for builtin models cfg.MODEL.RETINANET.SCORE_THRESH_TEST = args.confidence_threshold cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = args.confidence_threshold - if cfg.MODEL.META_ARCHITECTURE in ['ProposalNetwork', 'CenterNetDetector']: + if cfg.MODEL.META_ARCHITECTURE in ["ProposalNetwork", "CenterNetDetector"]: cfg.MODEL.CENTERNET.INFERENCE_TH = args.confidence_threshold cfg.MODEL.CENTERNET.NMS_TH = cfg.MODEL.ROI_HEADS.NMS_THRESH_TEST cfg.MODEL.PANOPTIC_FPN.COMBINE.INSTANCES_CONFIDENCE_THRESH = args.confidence_threshold @@ -50,8 +51,7 @@ def get_parser(): parser.add_argument("--input", nargs="+", help="A list of space separated input images") parser.add_argument( "--output", - help="A file or directory to save output visualizations. " - "If not given, will show output in an OpenCV window.", + help="A file or directory to save output visualizations. If not given, will show output in an OpenCV window.", ) parser.add_argument( @@ -86,17 +86,15 @@ def get_parser(): args.input = [args.input[0] + x for x in files] assert args.input, "The input path(s) was not found" visualizer = VideoVisualizer( - MetadataCatalog.get( - cfg.DATASETS.TEST[0] if len(cfg.DATASETS.TEST) else "__unused" - ), - instance_mode=ColorMode.IMAGE) + MetadataCatalog.get(cfg.DATASETS.TEST[0] if len(cfg.DATASETS.TEST) else "__unused"), + instance_mode=ColorMode.IMAGE, + ) for path in tqdm.tqdm(args.input, disable=not args.output): # use PIL, to be consistent with evaluation img = read_image(path, format="BGR") start_time = time.time() - predictions, visualized_output = demo.run_on_image( - img, visualizer=visualizer) - if 'instances' in predictions: + predictions, visualized_output = demo.run_on_image(img, visualizer=visualizer) + if "instances" in predictions: logger.info( "{}: detected {} instances in {:.2f}s".format( path, len(predictions["instances"]), time.time() - start_time @@ -134,7 +132,7 @@ def get_parser(): else: # cv2.namedWindow(WINDOW_NAME, cv2.WINDOW_NORMAL) cv2.imshow(WINDOW_NAME, visualized_output.get_image()[:, :, ::-1]) - if cv2.waitKey(1 ) == 27: + if cv2.waitKey(1) == 27: break # esc to quit elif args.webcam: assert args.input is None, "Cannot have both --input and --webcam!" @@ -149,7 +147,7 @@ def get_parser(): video = cv2.VideoCapture(args.video_input) width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT)) - frames_per_second = 15 # video.get(cv2.CAP_PROP_FPS) + frames_per_second = 15 # video.get(cv2.CAP_PROP_FPS) num_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) basename = os.path.basename(args.video_input) diff --git a/dimos/models/Detic/third_party/CenterNet2/predictor.py b/dimos/models/Detic/third_party/CenterNet2/predictor.py index 8a036bde3f..0bdee56264 100644 --- a/dimos/models/Detic/third_party/CenterNet2/predictor.py +++ b/dimos/models/Detic/third_party/CenterNet2/predictor.py @@ -1,19 +1,19 @@ # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved import atexit import bisect -import multiprocessing as mp from collections import deque -import cv2 -import torch +import multiprocessing as mp +import cv2 from detectron2.data import MetadataCatalog from detectron2.engine.defaults import DefaultPredictor from detectron2.utils.video_visualizer import VideoVisualizer from detectron2.utils.visualizer import ColorMode, Visualizer +import torch -class VisualizationDemo(object): - def __init__(self, cfg, instance_mode=ColorMode.IMAGE, parallel=False): +class VisualizationDemo: + def __init__(self, cfg, instance_mode=ColorMode.IMAGE, parallel: bool=False) -> None: """ Args: cfg (CfgNode): @@ -65,8 +65,7 @@ def run_on_image(self, image, visualizer=None): if "instances" in predictions: instances = predictions["instances"].to(self.cpu_device) if use_video_vis: - vis_output = visualizer.draw_instance_predictions( - image, predictions=instances) + vis_output = visualizer.draw_instance_predictions(image, predictions=instances) else: vis_output = visualizer.draw_instance_predictions(predictions=instances) elif "proposals" in predictions: @@ -75,8 +74,7 @@ def run_on_image(self, image, visualizer=None): instances.scores = instances.objectness_logits instances.pred_classes[:] = -1 if use_video_vis: - vis_output = visualizer.draw_instance_predictions( - image, predictions=instances) + vis_output = visualizer.draw_instance_predictions(image, predictions=instances) else: vis_output = visualizer.draw_instance_predictions(predictions=instances) @@ -163,13 +161,13 @@ class _StopToken: pass class _PredictWorker(mp.Process): - def __init__(self, cfg, task_queue, result_queue): + def __init__(self, cfg, task_queue, result_queue) -> None: self.cfg = cfg self.task_queue = task_queue self.result_queue = result_queue super().__init__() - def run(self): + def run(self) -> None: predictor = DefaultPredictor(self.cfg) while True: @@ -180,7 +178,7 @@ def run(self): result = predictor(data) self.result_queue.put((idx, result)) - def __init__(self, cfg, num_gpus: int = 1): + def __init__(self, cfg, num_gpus: int = 1) -> None: """ Args: cfg (CfgNode): @@ -193,7 +191,7 @@ def __init__(self, cfg, num_gpus: int = 1): for gpuid in range(max(num_gpus, 1)): cfg = cfg.clone() cfg.defrost() - cfg.MODEL.DEVICE = "cuda:{}".format(gpuid) if num_gpus > 0 else "cpu" + cfg.MODEL.DEVICE = f"cuda:{gpuid}" if num_gpus > 0 else "cpu" self.procs.append( AsyncPredictor._PredictWorker(cfg, self.task_queue, self.result_queue) ) @@ -207,7 +205,7 @@ def __init__(self, cfg, num_gpus: int = 1): p.start() atexit.register(self.shutdown) - def put(self, image): + def put(self, image) -> None: self.put_idx += 1 self.task_queue.put((self.put_idx, image)) @@ -227,14 +225,14 @@ def get(self): self.result_rank.insert(insert, idx) self.result_data.insert(insert, res) - def __len__(self): + def __len__(self) -> int: return self.put_idx - self.get_idx def __call__(self, image): self.put(image) return self.get() - def shutdown(self): + def shutdown(self) -> None: for _ in self.procs: self.task_queue.put(AsyncPredictor._StopToken()) diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/analyze_model.py b/dimos/models/Detic/third_party/CenterNet2/tools/analyze_model.py index 8e38f8b71e..7b7b9e3432 100755 --- a/dimos/models/Detic/third_party/CenterNet2/tools/analyze_model.py +++ b/dimos/models/Detic/third_party/CenterNet2/tools/analyze_model.py @@ -1,11 +1,7 @@ -# -*- coding: utf-8 -*- # Copyright (c) Facebook, Inc. and its affiliates. -import logging -import numpy as np from collections import Counter -import tqdm -from fvcore.nn import flop_count_table # can also try flop_count_str +import logging from detectron2.checkpoint import DetectionCheckpointer from detectron2.config import CfgNode, LazyConfig, get_cfg, instantiate @@ -18,6 +14,9 @@ parameter_count_table, ) from detectron2.utils.logger import setup_logger +from fvcore.nn import flop_count_table # can also try flop_count_str +import numpy as np +import tqdm logger = logging.getLogger("detectron2") @@ -37,7 +36,7 @@ def setup(args): return cfg -def do_flop(cfg): +def do_flop(cfg) -> None: if isinstance(cfg, CfgNode): data_loader = build_detection_test_loader(cfg, cfg.DATASETS.TEST[0]) model = build_model(cfg) @@ -64,11 +63,11 @@ def do_flop(cfg): + str([(k, v / (idx + 1) / 1e9) for k, v in counts.items()]) ) logger.info( - "Total GFlops: {:.1f}±{:.1f}".format(np.mean(total_flops) / 1e9, np.std(total_flops) / 1e9) + f"Total GFlops: {np.mean(total_flops) / 1e9:.1f}±{np.std(total_flops) / 1e9:.1f}" ) -def do_activation(cfg): +def do_activation(cfg) -> None: if isinstance(cfg, CfgNode): data_loader = build_detection_test_loader(cfg, cfg.DATASETS.TEST[0]) model = build_model(cfg) @@ -91,13 +90,11 @@ def do_activation(cfg): + str([(k, v / idx) for k, v in counts.items()]) ) logger.info( - "Total (Million) Activations: {}±{}".format( - np.mean(total_activations), np.std(total_activations) - ) + f"Total (Million) Activations: {np.mean(total_activations)}±{np.std(total_activations)}" ) -def do_parameter(cfg): +def do_parameter(cfg) -> None: if isinstance(cfg, CfgNode): model = build_model(cfg) else: @@ -105,7 +102,7 @@ def do_parameter(cfg): logger.info("Parameter Count:\n" + parameter_count_table(model, max_depth=5)) -def do_structure(cfg): +def do_structure(cfg) -> None: if isinstance(cfg, CfgNode): model = build_model(cfg) else: @@ -141,8 +138,7 @@ def do_structure(cfg): "--num-inputs", default=100, type=int, - help="number of inputs used to compute statistics for flops/activations, " - "both are data dependent.", + help="number of inputs used to compute statistics for flops/activations, both are data dependent.", ) args = parser.parse_args() assert not args.eval_only diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/benchmark.py b/dimos/models/Detic/third_party/CenterNet2/tools/benchmark.py index aaac564001..48f398d83d 100755 --- a/dimos/models/Detic/third_party/CenterNet2/tools/benchmark.py +++ b/dimos/models/Detic/third_party/CenterNet2/tools/benchmark.py @@ -8,11 +8,6 @@ import itertools import logging -import psutil -import torch -import tqdm -from fvcore.common.timer import Timer -from torch.nn.parallel import DistributedDataParallel from detectron2.checkpoint import DetectionCheckpointer from detectron2.config import LazyConfig, get_cfg, instantiate @@ -29,6 +24,11 @@ from detectron2.utils.collect_env import collect_env_info from detectron2.utils.events import CommonMetricPrinter from detectron2.utils.logger import setup_logger +from fvcore.common.timer import Timer +import psutil +import torch +from torch.nn.parallel import DistributedDataParallel +import tqdm logger = logging.getLogger("detectron2") @@ -59,14 +59,12 @@ def create_data_benchmark(cfg, args): return instantiate(kwargs) -def RAM_msg(): +def RAM_msg() -> str: vram = psutil.virtual_memory() - return "RAM Usage: {:.2f}/{:.2f} GB".format( - (vram.total - vram.available) / 1024 ** 3, vram.total / 1024 ** 3 - ) + return f"RAM Usage: {(vram.total - vram.available) / 1024**3:.2f}/{vram.total / 1024**3:.2f} GB" -def benchmark_data(args): +def benchmark_data(args) -> None: cfg = setup(args) logger.info("After spawning " + RAM_msg()) @@ -78,7 +76,7 @@ def benchmark_data(args): benchmark.benchmark_distributed(250, 1) -def benchmark_data_advanced(args): +def benchmark_data_advanced(args) -> None: # benchmark dataloader with more details to help analyze performance bottleneck cfg = setup(args) benchmark = create_data_benchmark(cfg, args) @@ -94,10 +92,10 @@ def benchmark_data_advanced(args): benchmark.benchmark_distributed(100) -def benchmark_train(args): +def benchmark_train(args) -> None: cfg = setup(args) model = build_model(cfg) - logger.info("Model:\n{}".format(model)) + logger.info(f"Model:\n{model}") if comm.get_world_size() > 1: model = DistributedDataParallel( model, device_ids=[comm.get_local_rank()], broadcast_buffers=False @@ -131,7 +129,7 @@ def f(): @torch.no_grad() -def benchmark_eval(args): +def benchmark_eval(args) -> None: cfg = setup(args) if args.config_file.endswith(".yaml"): model = build_model(cfg) @@ -149,7 +147,7 @@ def benchmark_eval(args): data_loader = instantiate(cfg.dataloader.test) model.eval() - logger.info("Model:\n{}".format(model)) + logger.info(f"Model:\n{model}") dummy_data = DatasetFromList(list(itertools.islice(data_loader, 100)), copy=False) def f(): @@ -167,7 +165,7 @@ def f(): break model(d) pbar.update() - logger.info("{} iters in {} seconds.".format(max_iter, timer.seconds())) + logger.info(f"{max_iter} iters in {timer.seconds()} seconds.") if __name__ == "__main__": diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/convert-torchvision-to-d2.py b/dimos/models/Detic/third_party/CenterNet2/tools/convert-torchvision-to-d2.py index 4b827d960c..8bf0565d5e 100755 --- a/dimos/models/Detic/third_party/CenterNet2/tools/convert-torchvision-to-d2.py +++ b/dimos/models/Detic/third_party/CenterNet2/tools/convert-torchvision-to-d2.py @@ -3,6 +3,7 @@ import pickle as pkl import sys + import torch """ @@ -40,9 +41,9 @@ if "layer" not in k: k = "stem." + k for t in [1, 2, 3, 4]: - k = k.replace("layer{}".format(t), "res{}".format(t + 1)) + k = k.replace(f"layer{t}", f"res{t + 1}") for t in [1, 2, 3]: - k = k.replace("bn{}".format(t), "conv{}.norm".format(t)) + k = k.replace(f"bn{t}", f"conv{t}.norm") k = k.replace("downsample.0", "shortcut") k = k.replace("downsample.1", "shortcut.norm") print(old_k, "->", k) diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/deploy/export_model.py b/dimos/models/Detic/third_party/CenterNet2/tools/deploy/export_model.py index bb1bcee632..6b9d2d60be 100755 --- a/dimos/models/Detic/third_party/CenterNet2/tools/deploy/export_model.py +++ b/dimos/models/Detic/third_party/CenterNet2/tools/deploy/export_model.py @@ -3,13 +3,11 @@ import argparse import os from typing import Dict, List, Tuple -import torch -from torch import Tensor, nn -import detectron2.data.transforms as T from detectron2.checkpoint import DetectionCheckpointer from detectron2.config import get_cfg from detectron2.data import build_detection_test_loader, detection_utils +import detectron2.data.transforms as T from detectron2.evaluation import COCOEvaluator, inference_on_dataset, print_csv_format from detectron2.export import TracingAdapter, dump_torchscript_IR, scripting_with_instances from detectron2.modeling import GeneralizedRCNN, RetinaNet, build_model @@ -19,6 +17,8 @@ from detectron2.utils.env import TORCH_VERSION from detectron2.utils.file_io import PathManager from detectron2.utils.logger import setup_logger +import torch +from torch import Tensor, nn def setup_cfg(args): @@ -72,7 +72,7 @@ def export_scripting(torch_model): class ScriptableAdapterBase(nn.Module): # Use this adapter to workaround https://github.com/pytorch/pytorch/issues/46944 # by not retuning instances but dicts. Otherwise the exported model is not deployable - def __init__(self): + def __init__(self) -> None: super().__init__() self.model = torch_model self.eval() @@ -80,14 +80,14 @@ def __init__(self): if isinstance(torch_model, GeneralizedRCNN): class ScriptableAdapter(ScriptableAdapterBase): - def forward(self, inputs: Tuple[Dict[str, torch.Tensor]]) -> List[Dict[str, Tensor]]: + def forward(self, inputs: tuple[dict[str, torch.Tensor]]) -> list[dict[str, Tensor]]: instances = self.model.inference(inputs, do_postprocess=False) return [i.get_fields() for i in instances] else: class ScriptableAdapter(ScriptableAdapterBase): - def forward(self, inputs: Tuple[Dict[str, torch.Tensor]]) -> List[Dict[str, Tensor]]: + def forward(self, inputs: tuple[dict[str, torch.Tensor]]) -> list[dict[str, Tensor]]: instances = self.model(inputs) return [i.get_fields() for i in instances] @@ -130,7 +130,7 @@ def inference(model, inputs): if args.format != "torchscript": return None - if not isinstance(torch_model, (GeneralizedRCNN, RetinaNet)): + if not isinstance(torch_model, GeneralizedRCNN | RetinaNet): return None def eval_wrapper(inputs): @@ -147,7 +147,6 @@ def eval_wrapper(inputs): def get_sample_inputs(args): - if args.sample_image is None: # get a first batch from dataset data_loader = build_detection_test_loader(cfg, cfg.DATASETS.TEST[0]) @@ -223,8 +222,7 @@ def get_sample_inputs(args): # run evaluation with the converted model if args.run_eval: assert exported_model is not None, ( - "Python inference is not yet implemented for " - f"export_method={args.export_method}, format={args.format}." + f"Python inference is not yet implemented for export_method={args.export_method}, format={args.format}." ) logger.info("Running evaluation ... this takes a long time if you export to CPU.") dataset = cfg.DATASETS.TEST[0] diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/lazyconfig_train_net.py b/dimos/models/Detic/third_party/CenterNet2/tools/lazyconfig_train_net.py index bb62d36c0c..8f40a40c39 100755 --- a/dimos/models/Detic/third_party/CenterNet2/tools/lazyconfig_train_net.py +++ b/dimos/models/Detic/third_party/CenterNet2/tools/lazyconfig_train_net.py @@ -12,6 +12,7 @@ To add more complicated training logic, you can easily add other configs in the config file and implement a new train_net.py to handle them. """ + import logging from detectron2.checkpoint import DetectionCheckpointer @@ -41,7 +42,7 @@ def do_test(cfg, model): return ret -def do_train(args, cfg): +def do_train(args, cfg) -> None: """ Args: cfg: an object with the following attributes: @@ -62,7 +63,7 @@ def do_train(args, cfg): """ model = instantiate(cfg.model) logger = logging.getLogger("detectron2") - logger.info("Model:\n{}".format(model)) + logger.info(f"Model:\n{model}") model.to(cfg.train.device) cfg.optimizer.params.model = model @@ -104,7 +105,7 @@ def do_train(args, cfg): trainer.train(start_iter, cfg.train.max_iter) -def main(args): +def main(args) -> None: cfg = LazyConfig.load(args.config_file) cfg = LazyConfig.apply_overrides(cfg, args.opts) default_setup(cfg, args) diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/lightning_train_net.py b/dimos/models/Detic/third_party/CenterNet2/tools/lightning_train_net.py index f6734b566b..dbb6cb6e43 100644 --- a/dimos/models/Detic/third_party/CenterNet2/tools/lightning_train_net.py +++ b/dimos/models/Detic/third_party/CenterNet2/tools/lightning_train_net.py @@ -5,14 +5,13 @@ # Depending on how you launch the trainer, there are issues with processes terminating correctly # This module is still dependent on D2 logging, but could be transferred to use Lightning logging +from collections import OrderedDict import logging import os import time -import weakref -from collections import OrderedDict from typing import Any, Dict, List +import weakref -import detectron2.utils.comm as comm from detectron2.checkpoint import DetectionCheckpointer from detectron2.config import get_cfg from detectron2.data import build_detection_test_loader, build_detection_train_loader @@ -28,9 +27,9 @@ from detectron2.evaluation.testing import flatten_results_dict from detectron2.modeling import build_model from detectron2.solver import build_lr_scheduler, build_optimizer +import detectron2.utils.comm as comm from detectron2.utils.events import EventStorage from detectron2.utils.logger import setup_logger - import pytorch_lightning as pl # type: ignore from pytorch_lightning import LightningDataModule, LightningModule from train_net import build_evaluator @@ -40,7 +39,7 @@ class TrainingModule(LightningModule): - def __init__(self, cfg): + def __init__(self, cfg) -> None: super().__init__() if not logger.isEnabledFor(logging.INFO): # setup_logger is not called for d2 setup_logger() @@ -51,14 +50,14 @@ def __init__(self, cfg): self.start_iter = 0 self.max_iter = cfg.SOLVER.MAX_ITER - def on_save_checkpoint(self, checkpoint: Dict[str, Any]) -> None: + def on_save_checkpoint(self, checkpoint: dict[str, Any]) -> None: checkpoint["iteration"] = self.storage.iter - def on_load_checkpoint(self, checkpointed_state: Dict[str, Any]) -> None: + def on_load_checkpoint(self, checkpointed_state: dict[str, Any]) -> None: self.start_iter = checkpointed_state["iteration"] self.storage.iter = self.start_iter - def setup(self, stage: str): + def setup(self, stage: str) -> None: if self.cfg.MODEL.WEIGHTS: self.checkpointer = DetectionCheckpointer( # Assume you want to save checkpoints together with logs/statistics @@ -110,7 +109,7 @@ def training_step_end(self, training_step_outpus): self.data_start = time.perf_counter() return training_step_outpus - def training_epoch_end(self, training_step_outputs): + def training_epoch_end(self, training_step_outputs) -> None: self.iteration_timer.after_train() if comm.is_main_process(): self.checkpointer.save("model_final") @@ -127,17 +126,17 @@ def _process_dataset_evaluation_results(self) -> OrderedDict: print_csv_format(results[dataset_name]) if len(results) == 1: - results = list(results.values())[0] + results = next(iter(results.values())) return results - def _reset_dataset_evaluators(self): + def _reset_dataset_evaluators(self) -> None: self._evaluators = [] for dataset_name in self.cfg.DATASETS.TEST: evaluator = build_evaluator(self.cfg, dataset_name) evaluator.reset() self._evaluators.append(evaluator) - def on_validation_epoch_start(self, _outputs): + def on_validation_epoch_start(self, _outputs) -> None: self._reset_dataset_evaluators() def validation_epoch_end(self, _outputs): @@ -149,13 +148,12 @@ def validation_epoch_end(self, _outputs): v = float(v) except Exception as e: raise ValueError( - "[EvalHook] eval_function should return a nested dict of float. " - "Got '{}: {}' instead.".format(k, v) + f"[EvalHook] eval_function should return a nested dict of float. Got '{k}: {v}' instead." ) from e self.storage.put_scalars(**flattened_results, smoothing_hint=False) def validation_step(self, batch, batch_idx: int, dataloader_idx: int = 0) -> None: - if not isinstance(batch, List): + if not isinstance(batch, list): batch = [batch] outputs = self.model(batch) self._evaluators[dataloader_idx].process(batch, outputs) @@ -168,7 +166,7 @@ def configure_optimizers(self): class DataModule(LightningDataModule): - def __init__(self, cfg): + def __init__(self, cfg) -> None: super().__init__() self.cfg = DefaultTrainer.auto_scale_workers(cfg, comm.get_world_size()) @@ -182,18 +180,18 @@ def val_dataloader(self): return dataloaders -def main(args): +def main(args) -> None: cfg = setup(args) train(cfg, args) -def train(cfg, args): +def train(cfg, args) -> None: trainer_params = { # training loop is bounded by max steps, use a large max_epochs to make # sure max_steps is met first - "max_epochs": 10 ** 8, + "max_epochs": 10**8, "max_steps": cfg.SOLVER.MAX_ITER, - "val_check_interval": cfg.TEST.EVAL_PERIOD if cfg.TEST.EVAL_PERIOD > 0 else 10 ** 8, + "val_check_interval": cfg.TEST.EVAL_PERIOD if cfg.TEST.EVAL_PERIOD > 0 else 10**8, "num_nodes": args.num_machines, "gpus": args.num_gpus, "num_sanity_val_steps": 0, diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/plain_train_net.py b/dimos/models/Detic/third_party/CenterNet2/tools/plain_train_net.py index 4851a8398e..a06d19aff2 100755 --- a/dimos/models/Detic/third_party/CenterNet2/tools/plain_train_net.py +++ b/dimos/models/Detic/third_party/CenterNet2/tools/plain_train_net.py @@ -19,13 +19,10 @@ It also includes fewer abstraction, therefore is easier to add custom logic. """ +from collections import OrderedDict import logging import os -from collections import OrderedDict -import torch -from torch.nn.parallel import DistributedDataParallel -import detectron2.utils.comm as comm from detectron2.checkpoint import DetectionCheckpointer, PeriodicCheckpointer from detectron2.config import get_cfg from detectron2.data import ( @@ -48,12 +45,15 @@ ) from detectron2.modeling import build_model from detectron2.solver import build_lr_scheduler, build_optimizer +import detectron2.utils.comm as comm from detectron2.utils.events import EventStorage +import torch +from torch.nn.parallel import DistributedDataParallel logger = logging.getLogger("detectron2") -def get_evaluator(cfg, dataset_name, output_folder=None): +def get_evaluator(cfg, dataset_name: str, output_folder=None): """ Create evaluator(s) for a given dataset. This uses the special metadata "evaluator_type" associated with each builtin dataset. @@ -77,14 +77,14 @@ def get_evaluator(cfg, dataset_name, output_folder=None): if evaluator_type == "coco_panoptic_seg": evaluator_list.append(COCOPanopticEvaluator(dataset_name, output_folder)) if evaluator_type == "cityscapes_instance": - assert ( - torch.cuda.device_count() > comm.get_rank() - ), "CityscapesEvaluator currently do not work with multiple machines." + assert torch.cuda.device_count() > comm.get_rank(), ( + "CityscapesEvaluator currently do not work with multiple machines." + ) return CityscapesInstanceEvaluator(dataset_name) if evaluator_type == "cityscapes_sem_seg": - assert ( - torch.cuda.device_count() > comm.get_rank() - ), "CityscapesEvaluator currently do not work with multiple machines." + assert torch.cuda.device_count() > comm.get_rank(), ( + "CityscapesEvaluator currently do not work with multiple machines." + ) return CityscapesSemSegEvaluator(dataset_name) if evaluator_type == "pascal_voc": return PascalVOCDetectionEvaluator(dataset_name) @@ -92,7 +92,7 @@ def get_evaluator(cfg, dataset_name, output_folder=None): return LVISEvaluator(dataset_name, cfg, True, output_folder) if len(evaluator_list) == 0: raise NotImplementedError( - "no Evaluator for the dataset {} with the type {}".format(dataset_name, evaluator_type) + f"no Evaluator for the dataset {dataset_name} with the type {evaluator_type}" ) if len(evaluator_list) == 1: return evaluator_list[0] @@ -109,14 +109,14 @@ def do_test(cfg, model): results_i = inference_on_dataset(model, data_loader, evaluator) results[dataset_name] = results_i if comm.is_main_process(): - logger.info("Evaluation results for {} in csv format:".format(dataset_name)) + logger.info(f"Evaluation results for {dataset_name} in csv format:") print_csv_format(results_i) if len(results) == 1: - results = list(results.values())[0] + results = next(iter(results.values())) return results -def do_train(cfg, model, resume=False): +def do_train(cfg, model, resume: bool=False) -> None: model.train() optimizer = build_optimizer(cfg, model) scheduler = build_lr_scheduler(cfg, optimizer) @@ -138,9 +138,9 @@ def do_train(cfg, model, resume=False): # compared to "train_net.py", we do not support accurate timing and # precise BN here, because they are not trivial to implement in a small training loop data_loader = build_detection_train_loader(cfg) - logger.info("Starting training from iteration {}".format(start_iter)) + logger.info(f"Starting training from iteration {start_iter}") with EventStorage(start_iter) as storage: - for data, iteration in zip(data_loader, range(start_iter, max_iter)): + for data, iteration in zip(data_loader, range(start_iter, max_iter), strict=False): storage.iter = iteration loss_dict = model(data) @@ -193,7 +193,7 @@ def main(args): cfg = setup(args) model = build_model(cfg) - logger.info("Model:\n{}".format(model)) + logger.info(f"Model:\n{model}") if args.eval_only: DetectionCheckpointer(model, save_dir=cfg.OUTPUT_DIR).resume_or_load( cfg.MODEL.WEIGHTS, resume=args.resume diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/train_net.py b/dimos/models/Detic/third_party/CenterNet2/tools/train_net.py index 6ebf5f60a2..deb2ca6db8 100755 --- a/dimos/models/Detic/third_party/CenterNet2/tools/train_net.py +++ b/dimos/models/Detic/third_party/CenterNet2/tools/train_net.py @@ -16,12 +16,10 @@ You may want to write your own script with your datasets and other customizations. """ +from collections import OrderedDict import logging import os -from collections import OrderedDict -import torch -import detectron2.utils.comm as comm from detectron2.checkpoint import DetectionCheckpointer from detectron2.config import get_cfg from detectron2.data import MetadataCatalog @@ -38,9 +36,11 @@ verify_results, ) from detectron2.modeling import GeneralizedRCNNWithTTA +import detectron2.utils.comm as comm +import torch -def build_evaluator(cfg, dataset_name, output_folder=None): +def build_evaluator(cfg, dataset_name: str, output_folder=None): """ Create evaluator(s) for a given dataset. This uses the special metadata "evaluator_type" associated with each builtin dataset. @@ -64,14 +64,14 @@ def build_evaluator(cfg, dataset_name, output_folder=None): if evaluator_type == "coco_panoptic_seg": evaluator_list.append(COCOPanopticEvaluator(dataset_name, output_folder)) if evaluator_type == "cityscapes_instance": - assert ( - torch.cuda.device_count() > comm.get_rank() - ), "CityscapesEvaluator currently do not work with multiple machines." + assert torch.cuda.device_count() > comm.get_rank(), ( + "CityscapesEvaluator currently do not work with multiple machines." + ) return CityscapesInstanceEvaluator(dataset_name) if evaluator_type == "cityscapes_sem_seg": - assert ( - torch.cuda.device_count() > comm.get_rank() - ), "CityscapesEvaluator currently do not work with multiple machines." + assert torch.cuda.device_count() > comm.get_rank(), ( + "CityscapesEvaluator currently do not work with multiple machines." + ) return CityscapesSemSegEvaluator(dataset_name) elif evaluator_type == "pascal_voc": return PascalVOCDetectionEvaluator(dataset_name) @@ -79,7 +79,7 @@ def build_evaluator(cfg, dataset_name, output_folder=None): return LVISEvaluator(dataset_name, output_dir=output_folder) if len(evaluator_list) == 0: raise NotImplementedError( - "no Evaluator for the dataset {} with the type {}".format(dataset_name, evaluator_type) + f"no Evaluator for the dataset {dataset_name} with the type {evaluator_type}" ) elif len(evaluator_list) == 1: return evaluator_list[0] @@ -95,7 +95,7 @@ class Trainer(DefaultTrainer): """ @classmethod - def build_evaluator(cls, cfg, dataset_name, output_folder=None): + def build_evaluator(cls, cfg, dataset_name: str, output_folder=None): return build_evaluator(cfg, dataset_name, output_folder) @classmethod diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/visualize_data.py b/dimos/models/Detic/third_party/CenterNet2/tools/visualize_data.py index fd0ba8347b..99abfdff4e 100755 --- a/dimos/models/Detic/third_party/CenterNet2/tools/visualize_data.py +++ b/dimos/models/Detic/third_party/CenterNet2/tools/visualize_data.py @@ -1,17 +1,21 @@ #!/usr/bin/env python # Copyright (c) Facebook, Inc. and its affiliates. import argparse -import os from itertools import chain -import cv2 -import tqdm +import os +import cv2 from detectron2.config import get_cfg -from detectron2.data import DatasetCatalog, MetadataCatalog, build_detection_train_loader -from detectron2.data import detection_utils as utils +from detectron2.data import ( + DatasetCatalog, + MetadataCatalog, + build_detection_train_loader, + detection_utils as utils, +) from detectron2.data.build import filter_images_with_few_keypoints from detectron2.utils.logger import setup_logger from detectron2.utils.visualizer import Visualizer +import tqdm def setup(args): @@ -54,14 +58,14 @@ def parse_args(in_args=None): os.makedirs(dirname, exist_ok=True) metadata = MetadataCatalog.get(cfg.DATASETS.TRAIN[0]) - def output(vis, fname): + def output(vis, fname) -> None: if args.show: print(fname) cv2.imshow("window", vis.get_image()[:, :, ::-1]) cv2.waitKey() else: filepath = os.path.join(dirname, fname) - print("Saving to {} ...".format(filepath)) + print(f"Saving to {filepath} ...") vis.save(filepath) scale = 1.0 diff --git a/dimos/models/Detic/third_party/CenterNet2/tools/visualize_json_results.py b/dimos/models/Detic/third_party/CenterNet2/tools/visualize_json_results.py index 472190e0b3..04dea72446 100755 --- a/dimos/models/Detic/third_party/CenterNet2/tools/visualize_json_results.py +++ b/dimos/models/Detic/third_party/CenterNet2/tools/visualize_json_results.py @@ -2,21 +2,21 @@ # Copyright (c) Facebook, Inc. and its affiliates. import argparse +from collections import defaultdict import json -import numpy as np import os -from collections import defaultdict -import cv2 -import tqdm +import cv2 from detectron2.data import DatasetCatalog, MetadataCatalog from detectron2.structures import Boxes, BoxMode, Instances from detectron2.utils.file_io import PathManager from detectron2.utils.logger import setup_logger from detectron2.utils.visualizer import Visualizer +import numpy as np +import tqdm -def create_instances(predictions, image_size): +def create_instances(predictions, image_size: int): ret = Instances(image_size) score = np.asarray([x["score"] for x in predictions]) @@ -71,7 +71,7 @@ def dataset_id_map(ds_id): return ds_id - 1 else: - raise ValueError("Unsupported dataset: {}".format(args.dataset)) + raise ValueError(f"Unsupported dataset: {args.dataset}") os.makedirs(args.output, exist_ok=True) diff --git a/dimos/models/Detic/third_party/CenterNet2/train_net.py b/dimos/models/Detic/third_party/CenterNet2/train_net.py index d903efde07..92859d7586 100644 --- a/dimos/models/Detic/third_party/CenterNet2/train_net.py +++ b/dimos/models/Detic/third_party/CenterNet2/train_net.py @@ -1,22 +1,20 @@ +from collections import OrderedDict +import datetime import logging import os -from collections import OrderedDict -import torch -from torch.nn.parallel import DistributedDataParallel import time -import datetime -import json -from fvcore.common.timer import Timer -import detectron2.utils.comm as comm +from centernet.config import add_centernet_config +from centernet.data.custom_build_augmentation import build_custom_augmentation from detectron2.checkpoint import DetectionCheckpointer, PeriodicCheckpointer from detectron2.config import get_cfg from detectron2.data import ( MetadataCatalog, build_detection_test_loader, ) +from detectron2.data.build import build_detection_train_loader +from detectron2.data.dataset_mapper import DatasetMapper from detectron2.engine import default_argument_parser, default_setup, launch - from detectron2.evaluation import ( COCOEvaluator, LVISEvaluator, @@ -24,51 +22,51 @@ print_csv_format, ) from detectron2.modeling import build_model +from detectron2.modeling.test_time_augmentation import GeneralizedRCNNWithTTA from detectron2.solver import build_lr_scheduler, build_optimizer +import detectron2.utils.comm as comm from detectron2.utils.events import ( CommonMetricPrinter, EventStorage, JSONWriter, TensorboardXWriter, ) -from detectron2.modeling.test_time_augmentation import GeneralizedRCNNWithTTA -from detectron2.data.dataset_mapper import DatasetMapper -from detectron2.data.build import build_detection_train_loader - -from centernet.config import add_centernet_config -from centernet.data.custom_build_augmentation import build_custom_augmentation +from fvcore.common.timer import Timer +import torch +from torch.nn.parallel import DistributedDataParallel logger = logging.getLogger("detectron2") + def do_test(cfg, model): results = OrderedDict() for dataset_name in cfg.DATASETS.TEST: - mapper = None if cfg.INPUT.TEST_INPUT_TYPE == 'default' else \ - DatasetMapper( - cfg, False, augmentations=build_custom_augmentation(cfg, False)) + mapper = ( + None + if cfg.INPUT.TEST_INPUT_TYPE == "default" + else DatasetMapper(cfg, False, augmentations=build_custom_augmentation(cfg, False)) + ) data_loader = build_detection_test_loader(cfg, dataset_name, mapper=mapper) - output_folder = os.path.join( - cfg.OUTPUT_DIR, "inference_{}".format(dataset_name)) + output_folder = os.path.join(cfg.OUTPUT_DIR, f"inference_{dataset_name}") evaluator_type = MetadataCatalog.get(dataset_name).evaluator_type if evaluator_type == "lvis": evaluator = LVISEvaluator(dataset_name, cfg, True, output_folder) - elif evaluator_type == 'coco': + elif evaluator_type == "coco": evaluator = COCOEvaluator(dataset_name, cfg, True, output_folder) else: assert 0, evaluator_type - - results[dataset_name] = inference_on_dataset( - model, data_loader, evaluator) + + results[dataset_name] = inference_on_dataset(model, data_loader, evaluator) if comm.is_main_process(): - logger.info("Evaluation results for {} in csv format:".format( - dataset_name)) + logger.info(f"Evaluation results for {dataset_name} in csv format:") print_csv_format(results[dataset_name]) if len(results) == 1: - results = list(results.values())[0] + results = next(iter(results.values())) return results -def do_train(cfg, model, resume=False): + +def do_train(cfg, model, resume: bool=False) -> None: model.train() optimizer = build_optimizer(cfg, model) scheduler = build_lr_scheduler(cfg, optimizer) @@ -79,11 +77,13 @@ def do_train(cfg, model, resume=False): start_iter = ( checkpointer.resume_or_load( - cfg.MODEL.WEIGHTS, resume=resume, - ).get("iteration", -1) + 1 + cfg.MODEL.WEIGHTS, + resume=resume, + ).get("iteration", -1) + + 1 ) if cfg.SOLVER.RESET_ITER: - logger.info('Reset loaded iteration. Start training from iteration 0.') + logger.info("Reset loaded iteration. Start training from iteration 0.") start_iter = 0 max_iter = cfg.SOLVER.MAX_ITER if cfg.SOLVER.TRAIN_ITER < 0 else cfg.SOLVER.TRAIN_ITER @@ -101,22 +101,24 @@ def do_train(cfg, model, resume=False): else [] ) - - mapper = DatasetMapper(cfg, True) if cfg.INPUT.CUSTOM_AUG == '' else \ - DatasetMapper(cfg, True, augmentations=build_custom_augmentation(cfg, True)) - if cfg.DATALOADER.SAMPLER_TRAIN in ['TrainingSampler', 'RepeatFactorTrainingSampler']: + mapper = ( + DatasetMapper(cfg, True) + if cfg.INPUT.CUSTOM_AUG == "" + else DatasetMapper(cfg, True, augmentations=build_custom_augmentation(cfg, True)) + ) + if cfg.DATALOADER.SAMPLER_TRAIN in ["TrainingSampler", "RepeatFactorTrainingSampler"]: data_loader = build_detection_train_loader(cfg, mapper=mapper) else: - from centernet.data.custom_dataset_dataloader import build_custom_train_loader - data_loader = build_custom_train_loader(cfg, mapper=mapper) + from centernet.data.custom_dataset_dataloader import build_custom_train_loader + data_loader = build_custom_train_loader(cfg, mapper=mapper) - logger.info("Starting training from iteration {}".format(start_iter)) + logger.info(f"Starting training from iteration {start_iter}") with EventStorage(start_iter) as storage: step_timer = Timer() data_timer = Timer() start_time = time.perf_counter() - for data, iteration in zip(data_loader, range(start_iter, max_iter)): + for data, iteration in zip(data_loader, range(start_iter, max_iter), strict=False): data_time = data_timer.seconds() storage.put_scalars(data_time=data_time) step_timer.reset() @@ -124,23 +126,19 @@ def do_train(cfg, model, resume=False): storage.step() loss_dict = model(data) - losses = sum( - loss for k, loss in loss_dict.items()) + losses = sum(loss for k, loss in loss_dict.items()) assert torch.isfinite(losses).all(), loss_dict - loss_dict_reduced = {k: v.item() \ - for k, v in comm.reduce_dict(loss_dict).items()} + loss_dict_reduced = {k: v.item() for k, v in comm.reduce_dict(loss_dict).items()} losses_reduced = sum(loss for loss in loss_dict_reduced.values()) if comm.is_main_process(): - storage.put_scalars( - total_loss=losses_reduced, **loss_dict_reduced) + storage.put_scalars(total_loss=losses_reduced, **loss_dict_reduced) optimizer.zero_grad() losses.backward() optimizer.step() - storage.put_scalar( - "lr", optimizer.param_groups[0]["lr"], smoothing_hint=False) + storage.put_scalar("lr", optimizer.param_groups[0]["lr"], smoothing_hint=False) step_time = step_timer.seconds() storage.put_scalars(time=step_time) @@ -155,16 +153,16 @@ def do_train(cfg, model, resume=False): do_test(cfg, model) comm.synchronize() - if iteration - start_iter > 5 and \ - (iteration % 20 == 0 or iteration == max_iter): + if iteration - start_iter > 5 and (iteration % 20 == 0 or iteration == max_iter): for writer in writers: writer.write() periodic_checkpointer.step(iteration) total_time = time.perf_counter() - start_time logger.info( - "Total training time: {}".format( - str(datetime.timedelta(seconds=int(total_time))))) + f"Total training time: {datetime.timedelta(seconds=int(total_time))!s}" + ) + def setup(args): """ @@ -174,10 +172,10 @@ def setup(args): add_centernet_config(cfg) cfg.merge_from_file(args.config_file) cfg.merge_from_list(args.opts) - if '/auto' in cfg.OUTPUT_DIR: + if "/auto" in cfg.OUTPUT_DIR: file_name = os.path.basename(args.config_file)[:-5] - cfg.OUTPUT_DIR = cfg.OUTPUT_DIR.replace('/auto', '/{}'.format(file_name)) - logger.info('OUTPUT_DIR: {}'.format(cfg.OUTPUT_DIR)) + cfg.OUTPUT_DIR = cfg.OUTPUT_DIR.replace("/auto", f"/{file_name}") + logger.info(f"OUTPUT_DIR: {cfg.OUTPUT_DIR}") cfg.freeze() default_setup(cfg, args) return cfg @@ -187,7 +185,7 @@ def main(args): cfg = setup(args) model = build_model(cfg) - logger.info("Model:\n{}".format(model)) + logger.info(f"Model:\n{model}") if args.eval_only: DetectionCheckpointer(model, save_dir=cfg.OUTPUT_DIR).resume_or_load( cfg.MODEL.WEIGHTS, resume=args.resume @@ -201,8 +199,10 @@ def main(args): distributed = comm.get_world_size() > 1 if distributed: model = DistributedDataParallel( - model, device_ids=[comm.get_local_rank()], broadcast_buffers=False, - find_unused_parameters=True + model, + device_ids=[comm.get_local_rank()], + broadcast_buffers=False, + find_unused_parameters=True, ) do_train(cfg, model, resume=args.resume) @@ -211,12 +211,11 @@ def main(args): if __name__ == "__main__": args = default_argument_parser() - args.add_argument('--manual_device', default='') + args.add_argument("--manual_device", default="") args = args.parse_args() - if args.manual_device != '': - os.environ['CUDA_VISIBLE_DEVICES'] = args.manual_device - args.dist_url = 'tcp://127.0.0.1:{}'.format( - torch.randint(11111, 60000, (1,))[0].item()) + if args.manual_device != "": + os.environ["CUDA_VISIBLE_DEVICES"] = args.manual_device + args.dist_url = f"tcp://127.0.0.1:{torch.randint(11111, 60000, (1,))[0].item()}" print("Command Line Args:", args) launch( main, diff --git a/dimos/models/Detic/third_party/Deformable-DETR/benchmark.py b/dimos/models/Detic/third_party/Deformable-DETR/benchmark.py index 5919477899..3a4fcbd4e6 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/benchmark.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/benchmark.py @@ -7,29 +7,31 @@ """ Benchmark inference speed of Deformable DETR. """ + +import argparse import os import time -import argparse - -import torch +from datasets import build_dataset from main import get_args_parser as get_main_args_parser from models import build_model -from datasets import build_dataset +import torch from util.misc import nested_tensor_from_tensor_list def get_benckmark_arg_parser(): - parser = argparse.ArgumentParser('Benchmark inference speed of Deformable DETR.') - parser.add_argument('--num_iters', type=int, default=300, help='total iters to benchmark speed') - parser.add_argument('--warm_iters', type=int, default=5, help='ignore first several iters that are very slow') - parser.add_argument('--batch_size', type=int, default=1, help='batch size in inference') - parser.add_argument('--resume', type=str, help='load the pre-trained checkpoint') + parser = argparse.ArgumentParser("Benchmark inference speed of Deformable DETR.") + parser.add_argument("--num_iters", type=int, default=300, help="total iters to benchmark speed") + parser.add_argument( + "--warm_iters", type=int, default=5, help="ignore first several iters that are very slow" + ) + parser.add_argument("--batch_size", type=int, default=1, help="batch size in inference") + parser.add_argument("--resume", type=str, help="load the pre-trained checkpoint") return parser @torch.no_grad() -def measure_average_inference_time(model, inputs, num_iters=100, warm_iters=5): +def measure_average_inference_time(model, inputs, num_iters: int=100, warm_iters: int=5): ts = [] for iter_ in range(num_iters): torch.cuda.synchronize() @@ -38,7 +40,7 @@ def measure_average_inference_time(model, inputs, num_iters=100, warm_iters=5): torch.cuda.synchronize() t = time.perf_counter() - t_ if iter_ >= warm_iters: - ts.append(t) + ts.append(t) print(ts) return sum(ts) / len(ts) @@ -49,19 +51,20 @@ def benchmark(): assert args.warm_iters < args.num_iters and args.num_iters > 0 and args.warm_iters >= 0 assert args.batch_size > 0 assert args.resume is None or os.path.exists(args.resume) - dataset = build_dataset('val', main_args) + dataset = build_dataset("val", main_args) model, _, _ = build_model(main_args) model.cuda() model.eval() if args.resume is not None: ckpt = torch.load(args.resume, map_location=lambda storage, loc: storage) - model.load_state_dict(ckpt['model']) - inputs = nested_tensor_from_tensor_list([dataset.__getitem__(0)[0].cuda() for _ in range(args.batch_size)]) + model.load_state_dict(ckpt["model"]) + inputs = nested_tensor_from_tensor_list( + [dataset.__getitem__(0)[0].cuda() for _ in range(args.batch_size)] + ) t = measure_average_inference_time(model, inputs, args.num_iters, args.warm_iters) return 1.0 / t * args.batch_size -if __name__ == '__main__': +if __name__ == "__main__": fps = benchmark() - print(f'Inference Speed: {fps:.1f} FPS') - + print(f"Inference Speed: {fps:.1f} FPS") diff --git a/dimos/models/Detic/third_party/Deformable-DETR/datasets/__init__.py b/dimos/models/Detic/third_party/Deformable-DETR/datasets/__init__.py index f5bd856992..870166e145 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/datasets/__init__.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/datasets/__init__.py @@ -8,9 +8,9 @@ # ------------------------------------------------------------------------ import torch.utils.data -from .torchvision_datasets import CocoDetection from .coco import build as build_coco +from .torchvision_datasets import CocoDetection def get_coco_api_from_dataset(dataset): @@ -24,10 +24,11 @@ def get_coco_api_from_dataset(dataset): def build_dataset(image_set, args): - if args.dataset_file == 'coco': + if args.dataset_file == "coco": return build_coco(image_set, args) - if args.dataset_file == 'coco_panoptic': + if args.dataset_file == "coco_panoptic": # to avoid making panopticapi required for coco from .coco_panoptic import build as build_coco_panoptic + return build_coco_panoptic(image_set, args) - raise ValueError(f'dataset {args.dataset_file} not supported') + raise ValueError(f"dataset {args.dataset_file} not supported") diff --git a/dimos/models/Detic/third_party/Deformable-DETR/datasets/coco.py b/dimos/models/Detic/third_party/Deformable-DETR/datasets/coco.py index 1be8308c84..aa00ce49e3 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/datasets/coco.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/datasets/coco.py @@ -12,35 +12,51 @@ Mostly copy-paste from https://github.com/pytorch/vision/blob/13b35ff/references/detection/coco_utils.py """ + from pathlib import Path +from pycocotools import mask as coco_mask import torch import torch.utils.data -from pycocotools import mask as coco_mask - -from .torchvision_datasets import CocoDetection as TvCocoDetection from util.misc import get_local_rank, get_local_size + import datasets.transforms as T +from .torchvision_datasets import CocoDetection as TvCocoDetection + class CocoDetection(TvCocoDetection): - def __init__(self, img_folder, ann_file, transforms, return_masks, cache_mode=False, local_rank=0, local_size=1): - super(CocoDetection, self).__init__(img_folder, ann_file, - cache_mode=cache_mode, local_rank=local_rank, local_size=local_size) + def __init__( + self, + img_folder, + ann_file, + transforms, + return_masks, + cache_mode: bool=False, + local_rank: int=0, + local_size: int=1, + ) -> None: + super().__init__( + img_folder, + ann_file, + cache_mode=cache_mode, + local_rank=local_rank, + local_size=local_size, + ) self._transforms = transforms self.prepare = ConvertCocoPolysToMask(return_masks) - def __getitem__(self, idx): - img, target = super(CocoDetection, self).__getitem__(idx) + def __getitem__(self, idx: int): + img, target = super().__getitem__(idx) image_id = self.ids[idx] - target = {'image_id': image_id, 'annotations': target} + target = {"image_id": image_id, "annotations": target} img, target = self.prepare(img, target) if self._transforms is not None: img, target = self._transforms(img, target) return img, target -def convert_coco_poly_to_mask(segmentations, height, width): +def convert_coco_poly_to_mask(segmentations, height, width: int): masks = [] for polygons in segmentations: rles = coco_mask.frPyObjects(polygons, height, width) @@ -57,8 +73,8 @@ def convert_coco_poly_to_mask(segmentations, height, width): return masks -class ConvertCocoPolysToMask(object): - def __init__(self, return_masks=False): +class ConvertCocoPolysToMask: + def __init__(self, return_masks: bool=False) -> None: self.return_masks = return_masks def __call__(self, image, target): @@ -69,7 +85,7 @@ def __call__(self, image, target): anno = target["annotations"] - anno = [obj for obj in anno if 'iscrowd' not in obj or obj['iscrowd'] == 0] + anno = [obj for obj in anno if "iscrowd" not in obj or obj["iscrowd"] == 0] boxes = [obj["bbox"] for obj in anno] # guard against no boxes via resizing @@ -123,47 +139,56 @@ def __call__(self, image, target): def make_coco_transforms(image_set): - - normalize = T.Compose([ - T.ToTensor(), - T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) - ]) + normalize = T.Compose([T.ToTensor(), T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]) scales = [480, 512, 544, 576, 608, 640, 672, 704, 736, 768, 800] - if image_set == 'train': - return T.Compose([ - T.RandomHorizontalFlip(), - T.RandomSelect( - T.RandomResize(scales, max_size=1333), - T.Compose([ - T.RandomResize([400, 500, 600]), - T.RandomSizeCrop(384, 600), + if image_set == "train": + return T.Compose( + [ + T.RandomHorizontalFlip(), + T.RandomSelect( T.RandomResize(scales, max_size=1333), - ]) - ), - normalize, - ]) - - if image_set == 'val': - return T.Compose([ - T.RandomResize([800], max_size=1333), - normalize, - ]) - - raise ValueError(f'unknown {image_set}') + T.Compose( + [ + T.RandomResize([400, 500, 600]), + T.RandomSizeCrop(384, 600), + T.RandomResize(scales, max_size=1333), + ] + ), + ), + normalize, + ] + ) + + if image_set == "val": + return T.Compose( + [ + T.RandomResize([800], max_size=1333), + normalize, + ] + ) + + raise ValueError(f"unknown {image_set}") def build(image_set, args): root = Path(args.coco_path) - assert root.exists(), f'provided COCO path {root} does not exist' - mode = 'instances' + assert root.exists(), f"provided COCO path {root} does not exist" + mode = "instances" PATHS = { - "train": (root / "train2017", root / "annotations" / f'{mode}_train2017.json'), - "val": (root / "val2017", root / "annotations" / f'{mode}_val2017.json'), + "train": (root / "train2017", root / "annotations" / f"{mode}_train2017.json"), + "val": (root / "val2017", root / "annotations" / f"{mode}_val2017.json"), } img_folder, ann_file = PATHS[image_set] - dataset = CocoDetection(img_folder, ann_file, transforms=make_coco_transforms(image_set), return_masks=args.masks, - cache_mode=args.cache_mode, local_rank=get_local_rank(), local_size=get_local_size()) + dataset = CocoDetection( + img_folder, + ann_file, + transforms=make_coco_transforms(image_set), + return_masks=args.masks, + cache_mode=args.cache_mode, + local_rank=get_local_rank(), + local_size=get_local_size(), + ) return dataset diff --git a/dimos/models/Detic/third_party/Deformable-DETR/datasets/coco_eval.py b/dimos/models/Detic/third_party/Deformable-DETR/datasets/coco_eval.py index 9a3ebe7e7d..1a0e7962bd 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/datasets/coco_eval.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/datasets/coco_eval.py @@ -14,22 +14,22 @@ The difference is that there is less copy-pasting from pycocotools in the end of the file, as python3 can suppress prints with contextlib """ -import os + import contextlib import copy -import numpy as np -import torch +import os -from pycocotools.cocoeval import COCOeval +import numpy as np from pycocotools.coco import COCO +from pycocotools.cocoeval import COCOeval import pycocotools.mask as mask_util - +import torch from util.misc import all_gather -class CocoEvaluator(object): - def __init__(self, coco_gt, iou_types): - assert isinstance(iou_types, (list, tuple)) +class CocoEvaluator: + def __init__(self, coco_gt, iou_types) -> None: + assert isinstance(iou_types, list | tuple) coco_gt = copy.deepcopy(coco_gt) self.coco_gt = coco_gt @@ -41,7 +41,7 @@ def __init__(self, coco_gt, iou_types): self.img_ids = [] self.eval_imgs = {k: [] for k in iou_types} - def update(self, predictions): + def update(self, predictions) -> None: img_ids = list(np.unique(list(predictions.keys()))) self.img_ids.extend(img_ids) @@ -49,7 +49,7 @@ def update(self, predictions): results = self.prepare(predictions, iou_type) # suppress pycocotools prints - with open(os.devnull, 'w') as devnull: + with open(os.devnull, "w") as devnull: with contextlib.redirect_stdout(devnull): coco_dt = COCO.loadRes(self.coco_gt, results) if results else COCO() coco_eval = self.coco_eval[iou_type] @@ -60,18 +60,20 @@ def update(self, predictions): self.eval_imgs[iou_type].append(eval_imgs) - def synchronize_between_processes(self): + def synchronize_between_processes(self) -> None: for iou_type in self.iou_types: self.eval_imgs[iou_type] = np.concatenate(self.eval_imgs[iou_type], 2) - create_common_coco_eval(self.coco_eval[iou_type], self.img_ids, self.eval_imgs[iou_type]) + create_common_coco_eval( + self.coco_eval[iou_type], self.img_ids, self.eval_imgs[iou_type] + ) - def accumulate(self): + def accumulate(self) -> None: for coco_eval in self.coco_eval.values(): coco_eval.accumulate() - def summarize(self): + def summarize(self) -> None: for iou_type, coco_eval in self.coco_eval.items(): - print("IoU metric: {}".format(iou_type)) + print(f"IoU metric: {iou_type}") coco_eval.summarize() def prepare(self, predictions, iou_type): @@ -82,7 +84,7 @@ def prepare(self, predictions, iou_type): elif iou_type == "keypoints": return self.prepare_for_coco_keypoint(predictions) else: - raise ValueError("Unknown iou type {}".format(iou_type)) + raise ValueError(f"Unknown iou type {iou_type}") def prepare_for_coco_detection(self, predictions): coco_results = [] @@ -161,7 +163,7 @@ def prepare_for_coco_keypoint(self, predictions): { "image_id": original_id, "category_id": labels[k], - 'keypoints': keypoint, + "keypoints": keypoint, "score": scores[k], } for k, keypoint in enumerate(keypoints) @@ -197,7 +199,7 @@ def merge(img_ids, eval_imgs): return merged_img_ids, merged_eval_imgs -def create_common_coco_eval(coco_eval, img_ids, eval_imgs): +def create_common_coco_eval(coco_eval, img_ids, eval_imgs) -> None: img_ids, eval_imgs = merge(img_ids, eval_imgs) img_ids = list(img_ids) eval_imgs = list(eval_imgs.flatten()) @@ -214,17 +216,17 @@ def create_common_coco_eval(coco_eval, img_ids, eval_imgs): def evaluate(self): - ''' + """ Run per image evaluation on given images and store results (a list of dict) in self.evalImgs :return: None - ''' + """ # tic = time.time() # print('Running per image evaluation...') p = self.params # add backward compatibility if useSegm is specified in params if p.useSegm is not None: - p.iouType = 'segm' if p.useSegm == 1 else 'bbox' - print('useSegm (deprecated) is not None. Running {} evaluation'.format(p.iouType)) + p.iouType = "segm" if p.useSegm == 1 else "bbox" + print(f"useSegm (deprecated) is not None. Running {p.iouType} evaluation") # print('Evaluate annotation type *{}*'.format(p.iouType)) p.imgIds = list(np.unique(p.imgIds)) if p.useCats: @@ -236,14 +238,11 @@ def evaluate(self): # loop through images, area range, max detection number catIds = p.catIds if p.useCats else [-1] - if p.iouType == 'segm' or p.iouType == 'bbox': + if p.iouType == "segm" or p.iouType == "bbox": computeIoU = self.computeIoU - elif p.iouType == 'keypoints': + elif p.iouType == "keypoints": computeIoU = self.computeOks - self.ious = { - (imgId, catId): computeIoU(imgId, catId) - for imgId in p.imgIds - for catId in catIds} + self.ious = {(imgId, catId): computeIoU(imgId, catId) for imgId in p.imgIds for catId in catIds} evaluateImg = self.evaluateImg maxDet = p.maxDets[-1] @@ -260,6 +259,7 @@ def evaluate(self): # print('DONE (t={:0.2f}s).'.format(toc-tic)) return p.imgIds, evalImgs + ################################################################# # end of straight copy from pycocotools, just removing the prints ################################################################# diff --git a/dimos/models/Detic/third_party/Deformable-DETR/datasets/coco_panoptic.py b/dimos/models/Detic/third_party/Deformable-DETR/datasets/coco_panoptic.py index e856e49d84..d1dd9bda59 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/datasets/coco_panoptic.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/datasets/coco_panoptic.py @@ -11,27 +11,26 @@ from pathlib import Path import numpy as np -import torch -from PIL import Image - from panopticapi.utils import rgb2id +from PIL import Image +import torch from util.box_ops import masks_to_boxes from .coco import make_coco_transforms class CocoPanoptic: - def __init__(self, img_folder, ann_folder, ann_file, transforms=None, return_masks=True): - with open(ann_file, 'r') as f: + def __init__(self, img_folder, ann_folder, ann_file, transforms=None, return_masks: bool=True) -> None: + with open(ann_file) as f: self.coco = json.load(f) # sort 'images' field so that they are aligned with 'annotations' # i.e., in alphabetical order - self.coco['images'] = sorted(self.coco['images'], key=lambda x: x['id']) + self.coco["images"] = sorted(self.coco["images"], key=lambda x: x["id"]) # sanity check if "annotations" in self.coco: - for img, ann in zip(self.coco['images'], self.coco['annotations']): - assert img['file_name'][:-4] == ann['file_name'][:-4] + for img, ann in zip(self.coco["images"], self.coco["annotations"], strict=False): + assert img["file_name"][:-4] == ann["file_name"][:-4] self.img_folder = img_folder self.ann_folder = ann_folder @@ -39,69 +38,82 @@ def __init__(self, img_folder, ann_folder, ann_file, transforms=None, return_mas self.transforms = transforms self.return_masks = return_masks - def __getitem__(self, idx): - ann_info = self.coco['annotations'][idx] if "annotations" in self.coco else self.coco['images'][idx] - img_path = Path(self.img_folder) / ann_info['file_name'].replace('.png', '.jpg') - ann_path = Path(self.ann_folder) / ann_info['file_name'] + def __getitem__(self, idx: int): + ann_info = ( + self.coco["annotations"][idx] + if "annotations" in self.coco + else self.coco["images"][idx] + ) + img_path = Path(self.img_folder) / ann_info["file_name"].replace(".png", ".jpg") + ann_path = Path(self.ann_folder) / ann_info["file_name"] - img = Image.open(img_path).convert('RGB') + img = Image.open(img_path).convert("RGB") w, h = img.size if "segments_info" in ann_info: masks = np.asarray(Image.open(ann_path), dtype=np.uint32) masks = rgb2id(masks) - ids = np.array([ann['id'] for ann in ann_info['segments_info']]) + ids = np.array([ann["id"] for ann in ann_info["segments_info"]]) masks = masks == ids[:, None, None] masks = torch.as_tensor(masks, dtype=torch.uint8) - labels = torch.tensor([ann['category_id'] for ann in ann_info['segments_info']], dtype=torch.int64) + labels = torch.tensor( + [ann["category_id"] for ann in ann_info["segments_info"]], dtype=torch.int64 + ) target = {} - target['image_id'] = torch.tensor([ann_info['image_id'] if "image_id" in ann_info else ann_info["id"]]) + target["image_id"] = torch.tensor( + [ann_info["image_id"] if "image_id" in ann_info else ann_info["id"]] + ) if self.return_masks: - target['masks'] = masks - target['labels'] = labels + target["masks"] = masks + target["labels"] = labels target["boxes"] = masks_to_boxes(masks) - target['size'] = torch.as_tensor([int(h), int(w)]) - target['orig_size'] = torch.as_tensor([int(h), int(w)]) + target["size"] = torch.as_tensor([int(h), int(w)]) + target["orig_size"] = torch.as_tensor([int(h), int(w)]) if "segments_info" in ann_info: - for name in ['iscrowd', 'area']: - target[name] = torch.tensor([ann[name] for ann in ann_info['segments_info']]) + for name in ["iscrowd", "area"]: + target[name] = torch.tensor([ann[name] for ann in ann_info["segments_info"]]) if self.transforms is not None: img, target = self.transforms(img, target) return img, target - def __len__(self): - return len(self.coco['images']) + def __len__(self) -> int: + return len(self.coco["images"]) - def get_height_and_width(self, idx): - img_info = self.coco['images'][idx] - height = img_info['height'] - width = img_info['width'] + def get_height_and_width(self, idx: int): + img_info = self.coco["images"][idx] + height = img_info["height"] + width = img_info["width"] return height, width def build(image_set, args): img_folder_root = Path(args.coco_path) ann_folder_root = Path(args.coco_panoptic_path) - assert img_folder_root.exists(), f'provided COCO path {img_folder_root} does not exist' - assert ann_folder_root.exists(), f'provided COCO path {ann_folder_root} does not exist' - mode = 'panoptic' + assert img_folder_root.exists(), f"provided COCO path {img_folder_root} does not exist" + assert ann_folder_root.exists(), f"provided COCO path {ann_folder_root} does not exist" + mode = "panoptic" PATHS = { - "train": ("train2017", Path("annotations") / f'{mode}_train2017.json'), - "val": ("val2017", Path("annotations") / f'{mode}_val2017.json'), + "train": ("train2017", Path("annotations") / f"{mode}_train2017.json"), + "val": ("val2017", Path("annotations") / f"{mode}_val2017.json"), } img_folder, ann_file = PATHS[image_set] img_folder_path = img_folder_root / img_folder - ann_folder = ann_folder_root / f'{mode}_{img_folder}' + ann_folder = ann_folder_root / f"{mode}_{img_folder}" ann_file = ann_folder_root / ann_file - dataset = CocoPanoptic(img_folder_path, ann_folder, ann_file, - transforms=make_coco_transforms(image_set), return_masks=args.masks) + dataset = CocoPanoptic( + img_folder_path, + ann_folder, + ann_file, + transforms=make_coco_transforms(image_set), + return_masks=args.masks, + ) return dataset diff --git a/dimos/models/Detic/third_party/Deformable-DETR/datasets/data_prefetcher.py b/dimos/models/Detic/third_party/Deformable-DETR/datasets/data_prefetcher.py index 7d28d9fdd7..4942500801 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/datasets/data_prefetcher.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/datasets/data_prefetcher.py @@ -6,13 +6,15 @@ import torch + def to_cuda(samples, targets, device): samples = samples.to(device, non_blocking=True) targets = [{k: v.to(device, non_blocking=True) for k, v in t.items()} for t in targets] return samples, targets -class data_prefetcher(): - def __init__(self, loader, device, prefetch=True): + +class data_prefetcher: + def __init__(self, loader, device, prefetch: bool=True) -> None: self.loader = iter(loader) self.prefetch = prefetch self.device = device @@ -20,7 +22,7 @@ def __init__(self, loader, device, prefetch=True): self.stream = torch.cuda.Stream() self.preload() - def preload(self): + def preload(self) -> None: try: self.next_samples, self.next_targets = next(self.loader) except StopIteration: @@ -35,7 +37,9 @@ def preload(self): # at the time we start copying to next_*: # self.stream.wait_stream(torch.cuda.current_stream()) with torch.cuda.stream(self.stream): - self.next_samples, self.next_targets = to_cuda(self.next_samples, self.next_targets, self.device) + self.next_samples, self.next_targets = to_cuda( + self.next_samples, self.next_targets, self.device + ) # more code for the alternative if record_stream() doesn't work: # copy_ will record the use of the pinned source tensor in this side stream. # self.next_input_gpu.copy_(self.next_input, non_blocking=True) @@ -57,7 +61,7 @@ def next(self): samples.record_stream(torch.cuda.current_stream()) if targets is not None: for t in targets: - for k, v in t.items(): + for _k, v in t.items(): v.record_stream(torch.cuda.current_stream()) self.preload() else: diff --git a/dimos/models/Detic/third_party/Deformable-DETR/datasets/panoptic_eval.py b/dimos/models/Detic/third_party/Deformable-DETR/datasets/panoptic_eval.py index 0dabffdb58..1a8ed7a82f 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/datasets/panoptic_eval.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/datasets/panoptic_eval.py @@ -18,8 +18,8 @@ pass -class PanopticEvaluator(object): - def __init__(self, ann_file, ann_folder, output_dir="panoptic_eval"): +class PanopticEvaluator: + def __init__(self, ann_file, ann_folder, output_dir: str="panoptic_eval") -> None: self.gt_json = ann_file self.gt_folder = ann_folder if utils.is_main_process(): @@ -28,14 +28,14 @@ def __init__(self, ann_file, ann_folder, output_dir="panoptic_eval"): self.output_dir = output_dir self.predictions = [] - def update(self, predictions): + def update(self, predictions) -> None: for p in predictions: with open(os.path.join(self.output_dir, p["file_name"]), "wb") as f: f.write(p.pop("png_string")) self.predictions += predictions - def synchronize_between_processes(self): + def synchronize_between_processes(self) -> None: all_predictions = utils.all_gather(self.predictions) merged_predictions = [] for p in all_predictions: @@ -48,5 +48,10 @@ def summarize(self): predictions_json = os.path.join(self.output_dir, "predictions.json") with open(predictions_json, "w") as f: f.write(json.dumps(json_data)) - return pq_compute(self.gt_json, predictions_json, gt_folder=self.gt_folder, pred_folder=self.output_dir) + return pq_compute( + self.gt_json, + predictions_json, + gt_folder=self.gt_folder, + pred_folder=self.output_dir, + ) return None diff --git a/dimos/models/Detic/third_party/Deformable-DETR/datasets/samplers.py b/dimos/models/Detic/third_party/Deformable-DETR/datasets/samplers.py index 14c0af2f98..5c2fff2d46 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/datasets/samplers.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/datasets/samplers.py @@ -6,11 +6,13 @@ # Modified from codes in torch.utils.data.distributed # ------------------------------------------------------------------------ -import os import math +import os + import torch import torch.distributed as dist from torch.utils.data.sampler import Sampler +from typing import Iterator, Optional class DistributedSampler(Sampler): @@ -28,7 +30,9 @@ class DistributedSampler(Sampler): rank (optional): Rank of the current process within num_replicas. """ - def __init__(self, dataset, num_replicas=None, rank=None, local_rank=None, local_size=None, shuffle=True): + def __init__( + self, dataset, num_replicas: Optional[int]=None, rank=None, local_rank=None, local_size: Optional[int]=None, shuffle: bool=True + ) -> None: if num_replicas is None: if not dist.is_available(): raise RuntimeError("Requires distributed package to be available") @@ -41,11 +45,11 @@ def __init__(self, dataset, num_replicas=None, rank=None, local_rank=None, local self.num_replicas = num_replicas self.rank = rank self.epoch = 0 - self.num_samples = int(math.ceil(len(self.dataset) * 1.0 / self.num_replicas)) + self.num_samples = math.ceil(len(self.dataset) * 1.0 / self.num_replicas) self.total_size = self.num_samples * self.num_replicas self.shuffle = shuffle - def __iter__(self): + def __iter__(self) -> Iterator: if self.shuffle: # deterministically shuffle based on epoch g = torch.Generator() @@ -65,10 +69,10 @@ def __iter__(self): return iter(indices) - def __len__(self): + def __len__(self) -> int: return self.num_samples - def set_epoch(self, epoch): + def set_epoch(self, epoch: int) -> None: self.epoch = epoch @@ -87,7 +91,9 @@ class NodeDistributedSampler(Sampler): rank (optional): Rank of the current process within num_replicas. """ - def __init__(self, dataset, num_replicas=None, rank=None, local_rank=None, local_size=None, shuffle=True): + def __init__( + self, dataset, num_replicas: Optional[int]=None, rank=None, local_rank=None, local_size: Optional[int]=None, shuffle: bool=True + ) -> None: if num_replicas is None: if not dist.is_available(): raise RuntimeError("Requires distributed package to be available") @@ -97,9 +103,9 @@ def __init__(self, dataset, num_replicas=None, rank=None, local_rank=None, local raise RuntimeError("Requires distributed package to be available") rank = dist.get_rank() if local_rank is None: - local_rank = int(os.environ.get('LOCAL_RANK', 0)) + local_rank = int(os.environ.get("LOCAL_RANK", 0)) if local_size is None: - local_size = int(os.environ.get('LOCAL_SIZE', 1)) + local_size = int(os.environ.get("LOCAL_SIZE", 1)) self.dataset = dataset self.shuffle = shuffle self.num_replicas = num_replicas @@ -107,12 +113,12 @@ def __init__(self, dataset, num_replicas=None, rank=None, local_rank=None, local self.rank = rank self.local_rank = local_rank self.epoch = 0 - self.num_samples = int(math.ceil(len(self.dataset) * 1.0 / self.num_replicas)) + self.num_samples = math.ceil(len(self.dataset) * 1.0 / self.num_replicas) self.total_size = self.num_samples * self.num_replicas self.total_size_parts = self.num_samples * self.num_replicas // self.num_parts - def __iter__(self): + def __iter__(self) -> Iterator: if self.shuffle: # deterministically shuffle based on epoch g = torch.Generator() @@ -123,17 +129,20 @@ def __iter__(self): indices = [i for i in indices if i % self.num_parts == self.local_rank] # add extra samples to make it evenly divisible - indices += indices[:(self.total_size_parts - len(indices))] + indices += indices[: (self.total_size_parts - len(indices))] assert len(indices) == self.total_size_parts # subsample - indices = indices[self.rank // self.num_parts:self.total_size_parts:self.num_replicas // self.num_parts] + indices = indices[ + self.rank // self.num_parts : self.total_size_parts : self.num_replicas + // self.num_parts + ] assert len(indices) == self.num_samples return iter(indices) - def __len__(self): + def __len__(self) -> int: return self.num_samples - def set_epoch(self, epoch): + def set_epoch(self, epoch: int) -> None: self.epoch = epoch diff --git a/dimos/models/Detic/third_party/Deformable-DETR/datasets/torchvision_datasets/coco.py b/dimos/models/Detic/third_party/Deformable-DETR/datasets/torchvision_datasets/coco.py index 45b5f52fa9..65eb674294 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/datasets/torchvision_datasets/coco.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/datasets/torchvision_datasets/coco.py @@ -9,12 +9,14 @@ """ Copy-Paste from torchvision, but add utility of caching images on memory """ -from torchvision.datasets.vision import VisionDataset -from PIL import Image + +from io import BytesIO import os import os.path + +from PIL import Image +from torchvision.datasets.vision import VisionDataset import tqdm -from io import BytesIO class CocoDetection(VisionDataset): @@ -30,10 +32,20 @@ class CocoDetection(VisionDataset): and returns a transformed version. """ - def __init__(self, root, annFile, transform=None, target_transform=None, transforms=None, - cache_mode=False, local_rank=0, local_size=1): - super(CocoDetection, self).__init__(root, transforms, transform, target_transform) + def __init__( + self, + root, + annFile, + transform=None, + target_transform=None, + transforms=None, + cache_mode: bool=False, + local_rank: int=0, + local_size: int=1, + ) -> None: + super().__init__(root, transforms, transform, target_transform) from pycocotools.coco import COCO + self.coco = COCO(annFile) self.ids = list(sorted(self.coco.imgs.keys())) self.cache_mode = cache_mode @@ -43,22 +55,22 @@ def __init__(self, root, annFile, transform=None, target_transform=None, transfo self.cache = {} self.cache_images() - def cache_images(self): + def cache_images(self) -> None: self.cache = {} - for index, img_id in zip(tqdm.trange(len(self.ids)), self.ids): + for index, img_id in zip(tqdm.trange(len(self.ids)), self.ids, strict=False): if index % self.local_size != self.local_rank: continue - path = self.coco.loadImgs(img_id)[0]['file_name'] - with open(os.path.join(self.root, path), 'rb') as f: + path = self.coco.loadImgs(img_id)[0]["file_name"] + with open(os.path.join(self.root, path), "rb") as f: self.cache[path] = f.read() def get_image(self, path): if self.cache_mode: if path not in self.cache.keys(): - with open(os.path.join(self.root, path), 'rb') as f: + with open(os.path.join(self.root, path), "rb") as f: self.cache[path] = f.read() - return Image.open(BytesIO(self.cache[path])).convert('RGB') - return Image.open(os.path.join(self.root, path)).convert('RGB') + return Image.open(BytesIO(self.cache[path])).convert("RGB") + return Image.open(os.path.join(self.root, path)).convert("RGB") def __getitem__(self, index): """ @@ -72,7 +84,7 @@ def __getitem__(self, index): ann_ids = coco.getAnnIds(imgIds=img_id) target = coco.loadAnns(ann_ids) - path = coco.loadImgs(img_id)[0]['file_name'] + path = coco.loadImgs(img_id)[0]["file_name"] img = self.get_image(path) if self.transforms is not None: @@ -80,5 +92,5 @@ def __getitem__(self, index): return img, target - def __len__(self): + def __len__(self) -> int: return len(self.ids) diff --git a/dimos/models/Detic/third_party/Deformable-DETR/datasets/transforms.py b/dimos/models/Detic/third_party/Deformable-DETR/datasets/transforms.py index 8f4baeb51c..3c2947ee36 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/datasets/transforms.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/datasets/transforms.py @@ -10,15 +10,16 @@ """ Transforms and data augmentation for both image + bbox. """ + import random import PIL import torch import torchvision.transforms as T import torchvision.transforms.functional as F - from util.box_ops import box_xyxy_to_cxcywh from util.misc import interpolate +from typing import Optional, Sequence def crop(image, target, region): @@ -45,7 +46,7 @@ def crop(image, target, region): if "masks" in target: # FIXME should we update the area here if there are no boxes? - target['masks'] = target['masks'][:, i:i + h, j:j + w] + target["masks"] = target["masks"][:, i : i + h, j : j + w] fields.append("masks") # remove elements for which the boxes or masks that have zero area @@ -53,10 +54,10 @@ def crop(image, target, region): # favor boxes selection when defining which elements to keep # this is compatible with previous implementation if "boxes" in target: - cropped_boxes = target['boxes'].reshape(-1, 2, 2) + cropped_boxes = target["boxes"].reshape(-1, 2, 2) keep = torch.all(cropped_boxes[:, 1, :] > cropped_boxes[:, 0, :], dim=1) else: - keep = target['masks'].flatten(1).any(1) + keep = target["masks"].flatten(1).any(1) for field in fields: target[field] = target[field][keep] @@ -72,25 +73,27 @@ def hflip(image, target): target = target.copy() if "boxes" in target: boxes = target["boxes"] - boxes = boxes[:, [2, 1, 0, 3]] * torch.as_tensor([-1, 1, -1, 1]) + torch.as_tensor([w, 0, w, 0]) + boxes = boxes[:, [2, 1, 0, 3]] * torch.as_tensor([-1, 1, -1, 1]) + torch.as_tensor( + [w, 0, w, 0] + ) target["boxes"] = boxes if "masks" in target: - target['masks'] = target['masks'].flip(-1) + target["masks"] = target["masks"].flip(-1) return flipped_image, target -def resize(image, target, size, max_size=None): +def resize(image, target, size: int, max_size: Optional[int]=None): # size can be min_size (scalar) or (w, h) tuple - def get_size_with_aspect_ratio(image_size, size, max_size=None): + def get_size_with_aspect_ratio(image_size: int, size: int, max_size: Optional[int]=None): w, h = image_size if max_size is not None: min_original_size = float(min((w, h))) max_original_size = float(max((w, h))) if max_original_size / min_original_size * size > max_size: - size = int(round(max_size * min_original_size / max_original_size)) + size = round(max_size * min_original_size / max_original_size) if (w <= h and w == size) or (h <= w and h == size): return (h, w) @@ -104,8 +107,8 @@ def get_size_with_aspect_ratio(image_size, size, max_size=None): return (oh, ow) - def get_size(image_size, size, max_size=None): - if isinstance(size, (list, tuple)): + def get_size(image_size: int, size: int, max_size: Optional[int]=None): + if isinstance(size, list | tuple): return size[::-1] else: return get_size_with_aspect_ratio(image_size, size, max_size) @@ -116,13 +119,15 @@ def get_size(image_size, size, max_size=None): if target is None: return rescaled_image, None - ratios = tuple(float(s) / float(s_orig) for s, s_orig in zip(rescaled_image.size, image.size)) + ratios = tuple(float(s) / float(s_orig) for s, s_orig in zip(rescaled_image.size, image.size, strict=False)) ratio_width, ratio_height = ratios target = target.copy() if "boxes" in target: boxes = target["boxes"] - scaled_boxes = boxes * torch.as_tensor([ratio_width, ratio_height, ratio_width, ratio_height]) + scaled_boxes = boxes * torch.as_tensor( + [ratio_width, ratio_height, ratio_width, ratio_height] + ) target["boxes"] = scaled_boxes if "area" in target: @@ -134,8 +139,9 @@ def get_size(image_size, size, max_size=None): target["size"] = torch.tensor([h, w]) if "masks" in target: - target['masks'] = interpolate( - target['masks'][:, None].float(), size, mode="nearest")[:, 0] > 0.5 + target["masks"] = ( + interpolate(target["masks"][:, None].float(), size, mode="nearest")[:, 0] > 0.5 + ) return rescaled_image, target @@ -149,12 +155,12 @@ def pad(image, target, padding): # should we do something wrt the original size? target["size"] = torch.tensor(padded_image[::-1]) if "masks" in target: - target['masks'] = torch.nn.functional.pad(target['masks'], (0, padding[0], 0, padding[1])) + target["masks"] = torch.nn.functional.pad(target["masks"], (0, padding[0], 0, padding[1])) return padded_image, target -class RandomCrop(object): - def __init__(self, size): +class RandomCrop: + def __init__(self, size: int) -> None: self.size = size def __call__(self, img, target): @@ -162,8 +168,8 @@ def __call__(self, img, target): return crop(img, target, region) -class RandomSizeCrop(object): - def __init__(self, min_size: int, max_size: int): +class RandomSizeCrop: + def __init__(self, min_size: int, max_size: int) -> None: self.min_size = min_size self.max_size = max_size @@ -174,20 +180,20 @@ def __call__(self, img: PIL.Image.Image, target: dict): return crop(img, target, region) -class CenterCrop(object): - def __init__(self, size): +class CenterCrop: + def __init__(self, size: int) -> None: self.size = size def __call__(self, img, target): image_width, image_height = img.size crop_height, crop_width = self.size - crop_top = int(round((image_height - crop_height) / 2.)) - crop_left = int(round((image_width - crop_width) / 2.)) + crop_top = round((image_height - crop_height) / 2.0) + crop_left = round((image_width - crop_width) / 2.0) return crop(img, target, (crop_top, crop_left, crop_height, crop_width)) -class RandomHorizontalFlip(object): - def __init__(self, p=0.5): +class RandomHorizontalFlip: + def __init__(self, p: float=0.5) -> None: self.p = p def __call__(self, img, target): @@ -196,9 +202,9 @@ def __call__(self, img, target): return img, target -class RandomResize(object): - def __init__(self, sizes, max_size=None): - assert isinstance(sizes, (list, tuple)) +class RandomResize: + def __init__(self, sizes: Sequence[int], max_size: Optional[int]=None) -> None: + assert isinstance(sizes, list | tuple) self.sizes = sizes self.max_size = max_size @@ -207,8 +213,8 @@ def __call__(self, img, target=None): return resize(img, target, size, self.max_size) -class RandomPad(object): - def __init__(self, max_pad): +class RandomPad: + def __init__(self, max_pad) -> None: self.max_pad = max_pad def __call__(self, img, target): @@ -217,12 +223,13 @@ def __call__(self, img, target): return pad(img, target, (pad_x, pad_y)) -class RandomSelect(object): +class RandomSelect: """ Randomly selects between transforms1 and transforms2, with probability p for transforms1 and (1 - p) for transforms2 """ - def __init__(self, transforms1, transforms2, p=0.5): + + def __init__(self, transforms1, transforms2, p: float=0.5) -> None: self.transforms1 = transforms1 self.transforms2 = transforms2 self.p = p @@ -233,22 +240,21 @@ def __call__(self, img, target): return self.transforms2(img, target) -class ToTensor(object): +class ToTensor: def __call__(self, img, target): return F.to_tensor(img), target -class RandomErasing(object): - - def __init__(self, *args, **kwargs): +class RandomErasing: + def __init__(self, *args, **kwargs) -> None: self.eraser = T.RandomErasing(*args, **kwargs) def __call__(self, img, target): return self.eraser(img), target -class Normalize(object): - def __init__(self, mean, std): +class Normalize: + def __init__(self, mean, std) -> None: self.mean = mean self.std = std @@ -266,8 +272,8 @@ def __call__(self, image, target=None): return image, target -class Compose(object): - def __init__(self, transforms): +class Compose: + def __init__(self, transforms) -> None: self.transforms = transforms def __call__(self, image, target): @@ -275,10 +281,10 @@ def __call__(self, image, target): image, target = t(image, target) return image, target - def __repr__(self): + def __repr__(self) -> str: format_string = self.__class__.__name__ + "(" for t in self.transforms: format_string += "\n" - format_string += " {0}".format(t) + format_string += f" {t}" format_string += "\n)" return format_string diff --git a/dimos/models/Detic/third_party/Deformable-DETR/engine.py b/dimos/models/Detic/third_party/Deformable-DETR/engine.py index 1ae2ae9591..7e6e7c2c20 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/engine.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/engine.py @@ -10,28 +10,35 @@ """ Train and eval functions used in main.py """ + import math import os import sys from typing import Iterable -import torch -import util.misc as utils from datasets.coco_eval import CocoEvaluator -from datasets.panoptic_eval import PanopticEvaluator from datasets.data_prefetcher import data_prefetcher +from datasets.panoptic_eval import PanopticEvaluator +import torch +import util.misc as utils -def train_one_epoch(model: torch.nn.Module, criterion: torch.nn.Module, - data_loader: Iterable, optimizer: torch.optim.Optimizer, - device: torch.device, epoch: int, max_norm: float = 0): +def train_one_epoch( + model: torch.nn.Module, + criterion: torch.nn.Module, + data_loader: Iterable, + optimizer: torch.optim.Optimizer, + device: torch.device, + epoch: int, + max_norm: float = 0, +): model.train() criterion.train() metric_logger = utils.MetricLogger(delimiter=" ") - metric_logger.add_meter('lr', utils.SmoothedValue(window_size=1, fmt='{value:.6f}')) - metric_logger.add_meter('class_error', utils.SmoothedValue(window_size=1, fmt='{value:.2f}')) - metric_logger.add_meter('grad_norm', utils.SmoothedValue(window_size=1, fmt='{value:.2f}')) - header = 'Epoch: [{}]'.format(epoch) + metric_logger.add_meter("lr", utils.SmoothedValue(window_size=1, fmt="{value:.6f}")) + metric_logger.add_meter("class_error", utils.SmoothedValue(window_size=1, fmt="{value:.2f}")) + metric_logger.add_meter("grad_norm", utils.SmoothedValue(window_size=1, fmt="{value:.2f}")) + header = f"Epoch: [{epoch}]" print_freq = 10 prefetcher = data_prefetcher(data_loader, device, prefetch=True) @@ -46,16 +53,16 @@ def train_one_epoch(model: torch.nn.Module, criterion: torch.nn.Module, # reduce losses over all GPUs for logging purposes loss_dict_reduced = utils.reduce_dict(loss_dict) - loss_dict_reduced_unscaled = {f'{k}_unscaled': v - for k, v in loss_dict_reduced.items()} - loss_dict_reduced_scaled = {k: v * weight_dict[k] - for k, v in loss_dict_reduced.items() if k in weight_dict} + loss_dict_reduced_unscaled = {f"{k}_unscaled": v for k, v in loss_dict_reduced.items()} + loss_dict_reduced_scaled = { + k: v * weight_dict[k] for k, v in loss_dict_reduced.items() if k in weight_dict + } losses_reduced_scaled = sum(loss_dict_reduced_scaled.values()) loss_value = losses_reduced_scaled.item() if not math.isfinite(loss_value): - print("Loss is {}, stopping training".format(loss_value)) + print(f"Loss is {loss_value}, stopping training") print(loss_dict_reduced) sys.exit(1) @@ -67,8 +74,10 @@ def train_one_epoch(model: torch.nn.Module, criterion: torch.nn.Module, grad_total_norm = utils.get_total_grad_norm(model.parameters(), max_norm) optimizer.step() - metric_logger.update(loss=loss_value, **loss_dict_reduced_scaled, **loss_dict_reduced_unscaled) - metric_logger.update(class_error=loss_dict_reduced['class_error']) + metric_logger.update( + loss=loss_value, **loss_dict_reduced_scaled, **loss_dict_reduced_unscaled + ) + metric_logger.update(class_error=loss_dict_reduced["class_error"]) metric_logger.update(lr=optimizer.param_groups[0]["lr"]) metric_logger.update(grad_norm=grad_total_norm) @@ -85,15 +94,15 @@ def evaluate(model, criterion, postprocessors, data_loader, base_ds, device, out criterion.eval() metric_logger = utils.MetricLogger(delimiter=" ") - metric_logger.add_meter('class_error', utils.SmoothedValue(window_size=1, fmt='{value:.2f}')) - header = 'Test:' + metric_logger.add_meter("class_error", utils.SmoothedValue(window_size=1, fmt="{value:.2f}")) + header = "Test:" - iou_types = tuple(k for k in ('segm', 'bbox') if k in postprocessors.keys()) + iou_types = tuple(k for k in ("segm", "bbox") if k in postprocessors.keys()) coco_evaluator = CocoEvaluator(base_ds, iou_types) # coco_evaluator.coco_eval[iou_types[0]].params.iouThrs = [0, 0.1, 0.5, 0.75] panoptic_evaluator = None - if 'panoptic' in postprocessors.keys(): + if "panoptic" in postprocessors.keys(): panoptic_evaluator = PanopticEvaluator( data_loader.dataset.ann_file, data_loader.dataset.ann_folder, @@ -110,21 +119,23 @@ def evaluate(model, criterion, postprocessors, data_loader, base_ds, device, out # reduce losses over all GPUs for logging purposes loss_dict_reduced = utils.reduce_dict(loss_dict) - loss_dict_reduced_scaled = {k: v * weight_dict[k] - for k, v in loss_dict_reduced.items() if k in weight_dict} - loss_dict_reduced_unscaled = {f'{k}_unscaled': v - for k, v in loss_dict_reduced.items()} - metric_logger.update(loss=sum(loss_dict_reduced_scaled.values()), - **loss_dict_reduced_scaled, - **loss_dict_reduced_unscaled) - metric_logger.update(class_error=loss_dict_reduced['class_error']) + loss_dict_reduced_scaled = { + k: v * weight_dict[k] for k, v in loss_dict_reduced.items() if k in weight_dict + } + loss_dict_reduced_unscaled = {f"{k}_unscaled": v for k, v in loss_dict_reduced.items()} + metric_logger.update( + loss=sum(loss_dict_reduced_scaled.values()), + **loss_dict_reduced_scaled, + **loss_dict_reduced_unscaled, + ) + metric_logger.update(class_error=loss_dict_reduced["class_error"]) orig_target_sizes = torch.stack([t["orig_size"] for t in targets], dim=0) - results = postprocessors['bbox'](outputs, orig_target_sizes) - if 'segm' in postprocessors.keys(): + results = postprocessors["bbox"](outputs, orig_target_sizes) + if "segm" in postprocessors.keys(): target_sizes = torch.stack([t["size"] for t in targets], dim=0) - results = postprocessors['segm'](results, outputs, orig_target_sizes, target_sizes) - res = {target['image_id'].item(): output for target, output in zip(targets, results)} + results = postprocessors["segm"](results, outputs, orig_target_sizes, target_sizes) + res = {target["image_id"].item(): output for target, output in zip(targets, results, strict=False)} if coco_evaluator is not None: coco_evaluator.update(res) @@ -155,12 +166,12 @@ def evaluate(model, criterion, postprocessors, data_loader, base_ds, device, out panoptic_res = panoptic_evaluator.summarize() stats = {k: meter.global_avg for k, meter in metric_logger.meters.items()} if coco_evaluator is not None: - if 'bbox' in postprocessors.keys(): - stats['coco_eval_bbox'] = coco_evaluator.coco_eval['bbox'].stats.tolist() - if 'segm' in postprocessors.keys(): - stats['coco_eval_masks'] = coco_evaluator.coco_eval['segm'].stats.tolist() + if "bbox" in postprocessors.keys(): + stats["coco_eval_bbox"] = coco_evaluator.coco_eval["bbox"].stats.tolist() + if "segm" in postprocessors.keys(): + stats["coco_eval_masks"] = coco_evaluator.coco_eval["segm"].stats.tolist() if panoptic_res is not None: - stats['PQ_all'] = panoptic_res["All"] - stats['PQ_th'] = panoptic_res["Things"] - stats['PQ_st'] = panoptic_res["Stuff"] + stats["PQ_all"] = panoptic_res["All"] + stats["PQ_th"] = panoptic_res["Things"] + stats["PQ_st"] = panoptic_res["Stuff"] return stats, coco_evaluator diff --git a/dimos/models/Detic/third_party/Deformable-DETR/main.py b/dimos/models/Detic/third_party/Deformable-DETR/main.py index fc6ccfac28..187b93a868 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/main.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/main.py @@ -11,124 +11,166 @@ import argparse import datetime import json +from pathlib import Path import random import time -from pathlib import Path -import numpy as np -import torch -from torch.utils.data import DataLoader import datasets -import util.misc as utils -import datasets.samplers as samplers from datasets import build_dataset, get_coco_api_from_dataset +import datasets.samplers as samplers from engine import evaluate, train_one_epoch from models import build_model +import numpy as np +import torch +from torch.utils.data import DataLoader +import util.misc as utils def get_args_parser(): - parser = argparse.ArgumentParser('Deformable DETR Detector', add_help=False) - parser.add_argument('--lr', default=2e-4, type=float) - parser.add_argument('--lr_backbone_names', default=["backbone.0"], type=str, nargs='+') - parser.add_argument('--lr_backbone', default=2e-5, type=float) - parser.add_argument('--lr_linear_proj_names', default=['reference_points', 'sampling_offsets'], type=str, nargs='+') - parser.add_argument('--lr_linear_proj_mult', default=0.1, type=float) - parser.add_argument('--batch_size', default=2, type=int) - parser.add_argument('--weight_decay', default=1e-4, type=float) - parser.add_argument('--epochs', default=50, type=int) - parser.add_argument('--lr_drop', default=40, type=int) - parser.add_argument('--lr_drop_epochs', default=None, type=int, nargs='+') - parser.add_argument('--clip_max_norm', default=0.1, type=float, - help='gradient clipping max norm') - - - parser.add_argument('--sgd', action='store_true') + parser = argparse.ArgumentParser("Deformable DETR Detector", add_help=False) + parser.add_argument("--lr", default=2e-4, type=float) + parser.add_argument("--lr_backbone_names", default=["backbone.0"], type=str, nargs="+") + parser.add_argument("--lr_backbone", default=2e-5, type=float) + parser.add_argument( + "--lr_linear_proj_names", + default=["reference_points", "sampling_offsets"], + type=str, + nargs="+", + ) + parser.add_argument("--lr_linear_proj_mult", default=0.1, type=float) + parser.add_argument("--batch_size", default=2, type=int) + parser.add_argument("--weight_decay", default=1e-4, type=float) + parser.add_argument("--epochs", default=50, type=int) + parser.add_argument("--lr_drop", default=40, type=int) + parser.add_argument("--lr_drop_epochs", default=None, type=int, nargs="+") + parser.add_argument( + "--clip_max_norm", default=0.1, type=float, help="gradient clipping max norm" + ) + + parser.add_argument("--sgd", action="store_true") # Variants of Deformable DETR - parser.add_argument('--with_box_refine', default=False, action='store_true') - parser.add_argument('--two_stage', default=False, action='store_true') + parser.add_argument("--with_box_refine", default=False, action="store_true") + parser.add_argument("--two_stage", default=False, action="store_true") # Model parameters - parser.add_argument('--frozen_weights', type=str, default=None, - help="Path to the pretrained model. If set, only the mask head will be trained") + parser.add_argument( + "--frozen_weights", + type=str, + default=None, + help="Path to the pretrained model. If set, only the mask head will be trained", + ) # * Backbone - parser.add_argument('--backbone', default='resnet50', type=str, - help="Name of the convolutional backbone to use") - parser.add_argument('--dilation', action='store_true', - help="If true, we replace stride with dilation in the last convolutional block (DC5)") - parser.add_argument('--position_embedding', default='sine', type=str, choices=('sine', 'learned'), - help="Type of positional embedding to use on top of the image features") - parser.add_argument('--position_embedding_scale', default=2 * np.pi, type=float, - help="position / size * scale") - parser.add_argument('--num_feature_levels', default=4, type=int, help='number of feature levels') + parser.add_argument( + "--backbone", default="resnet50", type=str, help="Name of the convolutional backbone to use" + ) + parser.add_argument( + "--dilation", + action="store_true", + help="If true, we replace stride with dilation in the last convolutional block (DC5)", + ) + parser.add_argument( + "--position_embedding", + default="sine", + type=str, + choices=("sine", "learned"), + help="Type of positional embedding to use on top of the image features", + ) + parser.add_argument( + "--position_embedding_scale", default=2 * np.pi, type=float, help="position / size * scale" + ) + parser.add_argument( + "--num_feature_levels", default=4, type=int, help="number of feature levels" + ) # * Transformer - parser.add_argument('--enc_layers', default=6, type=int, - help="Number of encoding layers in the transformer") - parser.add_argument('--dec_layers', default=6, type=int, - help="Number of decoding layers in the transformer") - parser.add_argument('--dim_feedforward', default=1024, type=int, - help="Intermediate size of the feedforward layers in the transformer blocks") - parser.add_argument('--hidden_dim', default=256, type=int, - help="Size of the embeddings (dimension of the transformer)") - parser.add_argument('--dropout', default=0.1, type=float, - help="Dropout applied in the transformer") - parser.add_argument('--nheads', default=8, type=int, - help="Number of attention heads inside the transformer's attentions") - parser.add_argument('--num_queries', default=300, type=int, - help="Number of query slots") - parser.add_argument('--dec_n_points', default=4, type=int) - parser.add_argument('--enc_n_points', default=4, type=int) + parser.add_argument( + "--enc_layers", default=6, type=int, help="Number of encoding layers in the transformer" + ) + parser.add_argument( + "--dec_layers", default=6, type=int, help="Number of decoding layers in the transformer" + ) + parser.add_argument( + "--dim_feedforward", + default=1024, + type=int, + help="Intermediate size of the feedforward layers in the transformer blocks", + ) + parser.add_argument( + "--hidden_dim", + default=256, + type=int, + help="Size of the embeddings (dimension of the transformer)", + ) + parser.add_argument( + "--dropout", default=0.1, type=float, help="Dropout applied in the transformer" + ) + parser.add_argument( + "--nheads", + default=8, + type=int, + help="Number of attention heads inside the transformer's attentions", + ) + parser.add_argument("--num_queries", default=300, type=int, help="Number of query slots") + parser.add_argument("--dec_n_points", default=4, type=int) + parser.add_argument("--enc_n_points", default=4, type=int) # * Segmentation - parser.add_argument('--masks', action='store_true', - help="Train segmentation head if the flag is provided") + parser.add_argument( + "--masks", action="store_true", help="Train segmentation head if the flag is provided" + ) # Loss - parser.add_argument('--no_aux_loss', dest='aux_loss', action='store_false', - help="Disables auxiliary decoding losses (loss at each layer)") + parser.add_argument( + "--no_aux_loss", + dest="aux_loss", + action="store_false", + help="Disables auxiliary decoding losses (loss at each layer)", + ) # * Matcher - parser.add_argument('--set_cost_class', default=2, type=float, - help="Class coefficient in the matching cost") - parser.add_argument('--set_cost_bbox', default=5, type=float, - help="L1 box coefficient in the matching cost") - parser.add_argument('--set_cost_giou', default=2, type=float, - help="giou box coefficient in the matching cost") + parser.add_argument( + "--set_cost_class", default=2, type=float, help="Class coefficient in the matching cost" + ) + parser.add_argument( + "--set_cost_bbox", default=5, type=float, help="L1 box coefficient in the matching cost" + ) + parser.add_argument( + "--set_cost_giou", default=2, type=float, help="giou box coefficient in the matching cost" + ) # * Loss coefficients - parser.add_argument('--mask_loss_coef', default=1, type=float) - parser.add_argument('--dice_loss_coef', default=1, type=float) - parser.add_argument('--cls_loss_coef', default=2, type=float) - parser.add_argument('--bbox_loss_coef', default=5, type=float) - parser.add_argument('--giou_loss_coef', default=2, type=float) - parser.add_argument('--focal_alpha', default=0.25, type=float) + parser.add_argument("--mask_loss_coef", default=1, type=float) + parser.add_argument("--dice_loss_coef", default=1, type=float) + parser.add_argument("--cls_loss_coef", default=2, type=float) + parser.add_argument("--bbox_loss_coef", default=5, type=float) + parser.add_argument("--giou_loss_coef", default=2, type=float) + parser.add_argument("--focal_alpha", default=0.25, type=float) # dataset parameters - parser.add_argument('--dataset_file', default='coco') - parser.add_argument('--coco_path', default='./data/coco', type=str) - parser.add_argument('--coco_panoptic_path', type=str) - parser.add_argument('--remove_difficult', action='store_true') - - parser.add_argument('--output_dir', default='', - help='path where to save, empty for no saving') - parser.add_argument('--device', default='cuda', - help='device to use for training / testing') - parser.add_argument('--seed', default=42, type=int) - parser.add_argument('--resume', default='', help='resume from checkpoint') - parser.add_argument('--start_epoch', default=0, type=int, metavar='N', - help='start epoch') - parser.add_argument('--eval', action='store_true') - parser.add_argument('--num_workers', default=2, type=int) - parser.add_argument('--cache_mode', default=False, action='store_true', help='whether to cache images on memory') + parser.add_argument("--dataset_file", default="coco") + parser.add_argument("--coco_path", default="./data/coco", type=str) + parser.add_argument("--coco_panoptic_path", type=str) + parser.add_argument("--remove_difficult", action="store_true") + + parser.add_argument("--output_dir", default="", help="path where to save, empty for no saving") + parser.add_argument("--device", default="cuda", help="device to use for training / testing") + parser.add_argument("--seed", default=42, type=int) + parser.add_argument("--resume", default="", help="resume from checkpoint") + parser.add_argument("--start_epoch", default=0, type=int, metavar="N", help="start epoch") + parser.add_argument("--eval", action="store_true") + parser.add_argument("--num_workers", default=2, type=int) + parser.add_argument( + "--cache_mode", default=False, action="store_true", help="whether to cache images on memory" + ) return parser -def main(args): +def main(args) -> None: utils.init_distributed_mode(args) - print("git:\n {}\n".format(utils.get_sha())) + print(f"git:\n {utils.get_sha()}\n") if args.frozen_weights is not None: assert args.masks, "Frozen training is meant for segmentation only" @@ -147,10 +189,10 @@ def main(args): model_without_ddp = model n_parameters = sum(p.numel() for p in model.parameters() if p.requires_grad) - print('number of params:', n_parameters) + print("number of params:", n_parameters) - dataset_train = build_dataset(image_set='train', args=args) - dataset_val = build_dataset(image_set='val', args=args) + dataset_train = build_dataset(image_set="train", args=args) + dataset_val = build_dataset(image_set="val", args=args) if args.distributed: if args.cache_mode: @@ -164,14 +206,25 @@ def main(args): sampler_val = torch.utils.data.SequentialSampler(dataset_val) batch_sampler_train = torch.utils.data.BatchSampler( - sampler_train, args.batch_size, drop_last=True) - - data_loader_train = DataLoader(dataset_train, batch_sampler=batch_sampler_train, - collate_fn=utils.collate_fn, num_workers=args.num_workers, - pin_memory=True) - data_loader_val = DataLoader(dataset_val, args.batch_size, sampler=sampler_val, - drop_last=False, collate_fn=utils.collate_fn, num_workers=args.num_workers, - pin_memory=True) + sampler_train, args.batch_size, drop_last=True + ) + + data_loader_train = DataLoader( + dataset_train, + batch_sampler=batch_sampler_train, + collate_fn=utils.collate_fn, + num_workers=args.num_workers, + pin_memory=True, + ) + data_loader_val = DataLoader( + dataset_val, + args.batch_size, + sampler=sampler_val, + drop_last=False, + collate_fn=utils.collate_fn, + num_workers=args.num_workers, + pin_memory=True, + ) # lr_backbone_names = ["backbone.0", "backbone.neck", "input_proj", "transformer.encoder"] def match_name_keywords(n, name_keywords): @@ -182,31 +235,43 @@ def match_name_keywords(n, name_keywords): break return out - for n, p in model_without_ddp.named_parameters(): + for n, _p in model_without_ddp.named_parameters(): print(n) param_dicts = [ { - "params": - [p for n, p in model_without_ddp.named_parameters() - if not match_name_keywords(n, args.lr_backbone_names) and not match_name_keywords(n, args.lr_linear_proj_names) and p.requires_grad], + "params": [ + p + for n, p in model_without_ddp.named_parameters() + if not match_name_keywords(n, args.lr_backbone_names) + and not match_name_keywords(n, args.lr_linear_proj_names) + and p.requires_grad + ], "lr": args.lr, }, { - "params": [p for n, p in model_without_ddp.named_parameters() if match_name_keywords(n, args.lr_backbone_names) and p.requires_grad], + "params": [ + p + for n, p in model_without_ddp.named_parameters() + if match_name_keywords(n, args.lr_backbone_names) and p.requires_grad + ], "lr": args.lr_backbone, }, { - "params": [p for n, p in model_without_ddp.named_parameters() if match_name_keywords(n, args.lr_linear_proj_names) and p.requires_grad], + "params": [ + p + for n, p in model_without_ddp.named_parameters() + if match_name_keywords(n, args.lr_linear_proj_names) and p.requires_grad + ], "lr": args.lr * args.lr_linear_proj_mult, - } + }, ] if args.sgd: - optimizer = torch.optim.SGD(param_dicts, lr=args.lr, momentum=0.9, - weight_decay=args.weight_decay) + optimizer = torch.optim.SGD( + param_dicts, lr=args.lr, momentum=0.9, weight_decay=args.weight_decay + ) else: - optimizer = torch.optim.AdamW(param_dicts, lr=args.lr, - weight_decay=args.weight_decay) + optimizer = torch.optim.AdamW(param_dicts, lr=args.lr, weight_decay=args.weight_decay) lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, args.lr_drop) if args.distributed: @@ -221,48 +286,66 @@ def match_name_keywords(n, name_keywords): base_ds = get_coco_api_from_dataset(dataset_val) if args.frozen_weights is not None: - checkpoint = torch.load(args.frozen_weights, map_location='cpu') - model_without_ddp.detr.load_state_dict(checkpoint['model']) + checkpoint = torch.load(args.frozen_weights, map_location="cpu") + model_without_ddp.detr.load_state_dict(checkpoint["model"]) output_dir = Path(args.output_dir) if args.resume: - if args.resume.startswith('https'): + if args.resume.startswith("https"): checkpoint = torch.hub.load_state_dict_from_url( - args.resume, map_location='cpu', check_hash=True) + args.resume, map_location="cpu", check_hash=True + ) else: - checkpoint = torch.load(args.resume, map_location='cpu') - missing_keys, unexpected_keys = model_without_ddp.load_state_dict(checkpoint['model'], strict=False) - unexpected_keys = [k for k in unexpected_keys if not (k.endswith('total_params') or k.endswith('total_ops'))] + checkpoint = torch.load(args.resume, map_location="cpu") + missing_keys, unexpected_keys = model_without_ddp.load_state_dict( + checkpoint["model"], strict=False + ) + unexpected_keys = [ + k + for k in unexpected_keys + if not (k.endswith("total_params") or k.endswith("total_ops")) + ] if len(missing_keys) > 0: - print('Missing Keys: {}'.format(missing_keys)) + print(f"Missing Keys: {missing_keys}") if len(unexpected_keys) > 0: - print('Unexpected Keys: {}'.format(unexpected_keys)) - if not args.eval and 'optimizer' in checkpoint and 'lr_scheduler' in checkpoint and 'epoch' in checkpoint: + print(f"Unexpected Keys: {unexpected_keys}") + if ( + not args.eval + and "optimizer" in checkpoint + and "lr_scheduler" in checkpoint + and "epoch" in checkpoint + ): import copy + p_groups = copy.deepcopy(optimizer.param_groups) - optimizer.load_state_dict(checkpoint['optimizer']) - for pg, pg_old in zip(optimizer.param_groups, p_groups): - pg['lr'] = pg_old['lr'] - pg['initial_lr'] = pg_old['initial_lr'] + optimizer.load_state_dict(checkpoint["optimizer"]) + for pg, pg_old in zip(optimizer.param_groups, p_groups, strict=False): + pg["lr"] = pg_old["lr"] + pg["initial_lr"] = pg_old["initial_lr"] print(optimizer.param_groups) - lr_scheduler.load_state_dict(checkpoint['lr_scheduler']) + lr_scheduler.load_state_dict(checkpoint["lr_scheduler"]) # todo: this is a hack for doing experiment that resume from checkpoint and also modify lr scheduler (e.g., decrease lr in advance). args.override_resumed_lr_drop = True if args.override_resumed_lr_drop: - print('Warning: (hack) args.override_resumed_lr_drop is set to True, so args.lr_drop would override lr_drop in resumed lr_scheduler.') + print( + "Warning: (hack) args.override_resumed_lr_drop is set to True, so args.lr_drop would override lr_drop in resumed lr_scheduler." + ) lr_scheduler.step_size = args.lr_drop - lr_scheduler.base_lrs = list(map(lambda group: group['initial_lr'], optimizer.param_groups)) + lr_scheduler.base_lrs = list( + map(lambda group: group["initial_lr"], optimizer.param_groups) + ) lr_scheduler.step(lr_scheduler.last_epoch) - args.start_epoch = checkpoint['epoch'] + 1 + args.start_epoch = checkpoint["epoch"] + 1 # check the resumed model if not args.eval: test_stats, coco_evaluator = evaluate( model, criterion, postprocessors, data_loader_val, base_ds, device, args.output_dir ) - + if args.eval: - test_stats, coco_evaluator = evaluate(model, criterion, postprocessors, - data_loader_val, base_ds, device, args.output_dir) + test_stats, coco_evaluator = evaluate( + model, criterion, postprocessors, data_loader_val, base_ds, device, args.output_dir + ) if args.output_dir: utils.save_on_master(coco_evaluator.coco_eval["bbox"].eval, output_dir / "eval.pth") return @@ -273,30 +356,36 @@ def match_name_keywords(n, name_keywords): if args.distributed: sampler_train.set_epoch(epoch) train_stats = train_one_epoch( - model, criterion, data_loader_train, optimizer, device, epoch, args.clip_max_norm) + model, criterion, data_loader_train, optimizer, device, epoch, args.clip_max_norm + ) lr_scheduler.step() if args.output_dir: - checkpoint_paths = [output_dir / 'checkpoint.pth'] + checkpoint_paths = [output_dir / "checkpoint.pth"] # extra checkpoint before LR drop and every 5 epochs if (epoch + 1) % args.lr_drop == 0 or (epoch + 1) % 5 == 0: - checkpoint_paths.append(output_dir / f'checkpoint{epoch:04}.pth') + checkpoint_paths.append(output_dir / f"checkpoint{epoch:04}.pth") for checkpoint_path in checkpoint_paths: - utils.save_on_master({ - 'model': model_without_ddp.state_dict(), - 'optimizer': optimizer.state_dict(), - 'lr_scheduler': lr_scheduler.state_dict(), - 'epoch': epoch, - 'args': args, - }, checkpoint_path) + utils.save_on_master( + { + "model": model_without_ddp.state_dict(), + "optimizer": optimizer.state_dict(), + "lr_scheduler": lr_scheduler.state_dict(), + "epoch": epoch, + "args": args, + }, + checkpoint_path, + ) test_stats, coco_evaluator = evaluate( model, criterion, postprocessors, data_loader_val, base_ds, device, args.output_dir ) - log_stats = {**{f'train_{k}': v for k, v in train_stats.items()}, - **{f'test_{k}': v for k, v in test_stats.items()}, - 'epoch': epoch, - 'n_parameters': n_parameters} + log_stats = { + **{f"train_{k}": v for k, v in train_stats.items()}, + **{f"test_{k}": v for k, v in test_stats.items()}, + "epoch": epoch, + "n_parameters": n_parameters, + } if args.output_dir and utils.is_main_process(): with (output_dir / "log.txt").open("a") as f: @@ -304,22 +393,25 @@ def match_name_keywords(n, name_keywords): # for evaluation logs if coco_evaluator is not None: - (output_dir / 'eval').mkdir(exist_ok=True) + (output_dir / "eval").mkdir(exist_ok=True) if "bbox" in coco_evaluator.coco_eval: - filenames = ['latest.pth'] + filenames = ["latest.pth"] if epoch % 50 == 0: - filenames.append(f'{epoch:03}.pth') + filenames.append(f"{epoch:03}.pth") for name in filenames: - torch.save(coco_evaluator.coco_eval["bbox"].eval, - output_dir / "eval" / name) + torch.save( + coco_evaluator.coco_eval["bbox"].eval, output_dir / "eval" / name + ) total_time = time.time() - start_time total_time_str = str(datetime.timedelta(seconds=int(total_time))) - print('Training time {}'.format(total_time_str)) + print(f"Training time {total_time_str}") -if __name__ == '__main__': - parser = argparse.ArgumentParser('Deformable DETR training and evaluation script', parents=[get_args_parser()]) +if __name__ == "__main__": + parser = argparse.ArgumentParser( + "Deformable DETR training and evaluation script", parents=[get_args_parser()] + ) args = parser.parse_args() if args.output_dir: Path(args.output_dir).mkdir(parents=True, exist_ok=True) diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/__init__.py b/dimos/models/Detic/third_party/Deformable-DETR/models/__init__.py index 9a59c33484..46b898b988 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/__init__.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/models/__init__.py @@ -12,4 +12,3 @@ def build_model(args): return build(args) - diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/backbone.py b/dimos/models/Detic/third_party/Deformable-DETR/models/backbone.py index 4bfe7053e9..cd973fa891 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/backbone.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/models/backbone.py @@ -10,15 +10,14 @@ """ Backbone modules. """ -from collections import OrderedDict + +from typing import Dict, List import torch +from torch import nn import torch.nn.functional as F import torchvision -from torch import nn from torchvision.models._utils import IntermediateLayerGetter -from typing import Dict, List - from util.misc import NestedTensor, is_main_process from .position_encoding import build_position_encoding @@ -33,23 +32,24 @@ class FrozenBatchNorm2d(torch.nn.Module): produce nans. """ - def __init__(self, n, eps=1e-5): - super(FrozenBatchNorm2d, self).__init__() + def __init__(self, n, eps: float=1e-5) -> None: + super().__init__() self.register_buffer("weight", torch.ones(n)) self.register_buffer("bias", torch.zeros(n)) self.register_buffer("running_mean", torch.zeros(n)) self.register_buffer("running_var", torch.ones(n)) self.eps = eps - def _load_from_state_dict(self, state_dict, prefix, local_metadata, strict, - missing_keys, unexpected_keys, error_msgs): - num_batches_tracked_key = prefix + 'num_batches_tracked' + def _load_from_state_dict( + self, state_dict, prefix: str, local_metadata, strict: bool, missing_keys, unexpected_keys, error_msgs + ) -> None: + num_batches_tracked_key = prefix + "num_batches_tracked" if num_batches_tracked_key in state_dict: del state_dict[num_batches_tracked_key] - super(FrozenBatchNorm2d, self)._load_from_state_dict( - state_dict, prefix, local_metadata, strict, - missing_keys, unexpected_keys, error_msgs) + super()._load_from_state_dict( + state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs + ) def forward(self, x): # move reshapes to the beginning @@ -65,11 +65,15 @@ def forward(self, x): class BackboneBase(nn.Module): - - def __init__(self, backbone: nn.Module, train_backbone: bool, return_interm_layers: bool): + def __init__(self, backbone: nn.Module, train_backbone: bool, return_interm_layers: bool) -> None: super().__init__() for name, parameter in backbone.named_parameters(): - if not train_backbone or 'layer2' not in name and 'layer3' not in name and 'layer4' not in name: + if ( + not train_backbone + or ("layer2" not in name + and "layer3" not in name + and "layer4" not in name) + ): parameter.requires_grad_(False) if return_interm_layers: # return_layers = {"layer1": "0", "layer2": "1", "layer3": "2", "layer4": "3"} @@ -77,14 +81,14 @@ def __init__(self, backbone: nn.Module, train_backbone: bool, return_interm_laye self.strides = [8, 16, 32] self.num_channels = [512, 1024, 2048] else: - return_layers = {'layer4': "0"} + return_layers = {"layer4": "0"} self.strides = [32] self.num_channels = [2048] self.body = IntermediateLayerGetter(backbone, return_layers=return_layers) def forward(self, tensor_list: NestedTensor): xs = self.body(tensor_list.tensors) - out: Dict[str, NestedTensor] = {} + out: dict[str, NestedTensor] = {} for name, x in xs.items(): m = tensor_list.mask assert m is not None @@ -95,31 +99,31 @@ def forward(self, tensor_list: NestedTensor): class Backbone(BackboneBase): """ResNet backbone with frozen BatchNorm.""" - def __init__(self, name: str, - train_backbone: bool, - return_interm_layers: bool, - dilation: bool): + + def __init__(self, name: str, train_backbone: bool, return_interm_layers: bool, dilation: bool) -> None: norm_layer = FrozenBatchNorm2d backbone = getattr(torchvision.models, name)( replace_stride_with_dilation=[False, False, dilation], - pretrained=is_main_process(), norm_layer=norm_layer) - assert name not in ('resnet18', 'resnet34'), "number of channels are hard coded" + pretrained=is_main_process(), + norm_layer=norm_layer, + ) + assert name not in ("resnet18", "resnet34"), "number of channels are hard coded" super().__init__(backbone, train_backbone, return_interm_layers) if dilation: self.strides[-1] = self.strides[-1] // 2 class Joiner(nn.Sequential): - def __init__(self, backbone, position_embedding): + def __init__(self, backbone, position_embedding) -> None: super().__init__(backbone, position_embedding) self.strides = backbone.strides self.num_channels = backbone.num_channels def forward(self, tensor_list: NestedTensor): xs = self[0](tensor_list) - out: List[NestedTensor] = [] + out: list[NestedTensor] = [] pos = [] - for name, x in sorted(xs.items()): + for _name, x in sorted(xs.items()): out.append(x) # position encoding diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/deformable_detr.py b/dimos/models/Detic/third_party/Deformable-DETR/models/deformable_detr.py index f1415e8562..661c6b3d98 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/deformable_detr.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/models/deformable_detr.py @@ -10,22 +10,35 @@ """ Deformable DETR model and criterion classes. """ -import torch -import torch.nn.functional as F -from torch import nn + +import copy import math +import torch +from torch import nn +import torch.nn.functional as F from util import box_ops -from util.misc import (NestedTensor, nested_tensor_from_tensor_list, - accuracy, get_world_size, interpolate, - is_dist_avail_and_initialized, inverse_sigmoid) +from util.misc import ( + NestedTensor, + accuracy, + get_world_size, + interpolate, + inverse_sigmoid, + is_dist_avail_and_initialized, + nested_tensor_from_tensor_list, +) from .backbone import build_backbone -from .matcher import build_matcher -from .segmentation import (DETRsegm, PostProcessPanoptic, PostProcessSegm, - dice_loss, sigmoid_focal_loss) from .deformable_transformer import build_deforamble_transformer -import copy +from .matcher import build_matcher +from .segmentation import ( + DETRsegm, + PostProcessPanoptic, + PostProcessSegm, + dice_loss, + sigmoid_focal_loss, +) +from typing import Sequence def _get_clones(module, N): @@ -33,10 +46,20 @@ def _get_clones(module, N): class DeformableDETR(nn.Module): - """ This is the Deformable DETR module that performs object detection """ - def __init__(self, backbone, transformer, num_classes, num_queries, num_feature_levels, - aux_loss=True, with_box_refine=False, two_stage=False): - """ Initializes the model. + """This is the Deformable DETR module that performs object detection""" + + def __init__( + self, + backbone, + transformer, + num_classes: int, + num_queries: int, + num_feature_levels: int, + aux_loss: bool=True, + with_box_refine: bool=False, + two_stage: bool=False, + ) -> None: + """Initializes the model. Parameters: backbone: torch module of the backbone to be used. See backbone.py transformer: torch module of the transformer architecture. See transformer.py @@ -55,29 +78,36 @@ def __init__(self, backbone, transformer, num_classes, num_queries, num_feature_ self.bbox_embed = MLP(hidden_dim, hidden_dim, 4, 3) self.num_feature_levels = num_feature_levels if not two_stage: - self.query_embed = nn.Embedding(num_queries, hidden_dim*2) + self.query_embed = nn.Embedding(num_queries, hidden_dim * 2) if num_feature_levels > 1: num_backbone_outs = len(backbone.strides) input_proj_list = [] for _ in range(num_backbone_outs): in_channels = backbone.num_channels[_] - input_proj_list.append(nn.Sequential( - nn.Conv2d(in_channels, hidden_dim, kernel_size=1), - nn.GroupNorm(32, hidden_dim), - )) + input_proj_list.append( + nn.Sequential( + nn.Conv2d(in_channels, hidden_dim, kernel_size=1), + nn.GroupNorm(32, hidden_dim), + ) + ) for _ in range(num_feature_levels - num_backbone_outs): - input_proj_list.append(nn.Sequential( - nn.Conv2d(in_channels, hidden_dim, kernel_size=3, stride=2, padding=1), - nn.GroupNorm(32, hidden_dim), - )) + input_proj_list.append( + nn.Sequential( + nn.Conv2d(in_channels, hidden_dim, kernel_size=3, stride=2, padding=1), + nn.GroupNorm(32, hidden_dim), + ) + ) in_channels = hidden_dim self.input_proj = nn.ModuleList(input_proj_list) else: - self.input_proj = nn.ModuleList([ - nn.Sequential( - nn.Conv2d(backbone.num_channels[0], hidden_dim, kernel_size=1), - nn.GroupNorm(32, hidden_dim), - )]) + self.input_proj = nn.ModuleList( + [ + nn.Sequential( + nn.Conv2d(backbone.num_channels[0], hidden_dim, kernel_size=1), + nn.GroupNorm(32, hidden_dim), + ) + ] + ) self.backbone = backbone self.aux_loss = aux_loss self.with_box_refine = with_box_refine @@ -93,7 +123,9 @@ def __init__(self, backbone, transformer, num_classes, num_queries, num_feature_ nn.init.constant_(proj[0].bias, 0) # if two-stage, the last class_embed and bbox_embed is for region proposal generation - num_pred = (transformer.decoder.num_layers + 1) if two_stage else transformer.decoder.num_layers + num_pred = ( + (transformer.decoder.num_layers + 1) if two_stage else transformer.decoder.num_layers + ) if with_box_refine: self.class_embed = _get_clones(self.class_embed, num_pred) self.bbox_embed = _get_clones(self.bbox_embed, num_pred) @@ -112,19 +144,19 @@ def __init__(self, backbone, transformer, num_classes, num_queries, num_feature_ nn.init.constant_(box_embed.layers[-1].bias.data[2:], 0.0) def forward(self, samples: NestedTensor): - """ The forward expects a NestedTensor, which consists of: - - samples.tensor: batched images, of shape [batch_size x 3 x H x W] - - samples.mask: a binary mask of shape [batch_size x H x W], containing 1 on padded pixels - - It returns a dict with the following elements: - - "pred_logits": the classification logits (including no-object) for all queries. - Shape= [batch_size x num_queries x (num_classes + 1)] - - "pred_boxes": The normalized boxes coordinates for all queries, represented as - (center_x, center_y, height, width). These values are normalized in [0, 1], - relative to the size of each individual image (disregarding possible padding). - See PostProcess for information on how to retrieve the unnormalized bounding box. - - "aux_outputs": Optional, only returned when auxilary losses are activated. It is a list of - dictionnaries containing the two above keys for each decoder layer. + """The forward expects a NestedTensor, which consists of: + - samples.tensor: batched images, of shape [batch_size x 3 x H x W] + - samples.mask: a binary mask of shape [batch_size x H x W], containing 1 on padded pixels + + It returns a dict with the following elements: + - "pred_logits": the classification logits (including no-object) for all queries. + Shape= [batch_size x num_queries x (num_classes + 1)] + - "pred_boxes": The normalized boxes coordinates for all queries, represented as + (center_x, center_y, height, width). These values are normalized in [0, 1], + relative to the size of each individual image (disregarding possible padding). + See PostProcess for information on how to retrieve the unnormalized bounding box. + - "aux_outputs": Optional, only returned when auxilary losses are activated. It is a list of + dictionnaries containing the two above keys for each decoder layer. """ if not isinstance(samples, NestedTensor): samples = nested_tensor_from_tensor_list(samples) @@ -154,7 +186,9 @@ def forward(self, samples: NestedTensor): query_embeds = None if not self.two_stage: query_embeds = self.query_embed.weight - hs, init_reference, inter_references, enc_outputs_class, enc_outputs_coord_unact = self.transformer(srcs, masks, pos, query_embeds) + hs, init_reference, inter_references, enc_outputs_class, enc_outputs_coord_unact = ( + self.transformer(srcs, masks, pos, query_embeds) + ) outputs_classes = [] outputs_coords = [] @@ -177,13 +211,13 @@ def forward(self, samples: NestedTensor): outputs_class = torch.stack(outputs_classes) outputs_coord = torch.stack(outputs_coords) - out = {'pred_logits': outputs_class[-1], 'pred_boxes': outputs_coord[-1]} + out = {"pred_logits": outputs_class[-1], "pred_boxes": outputs_coord[-1]} if self.aux_loss: - out['aux_outputs'] = self._set_aux_loss(outputs_class, outputs_coord) + out["aux_outputs"] = self._set_aux_loss(outputs_class, outputs_coord) if self.two_stage: enc_outputs_coord = enc_outputs_coord_unact.sigmoid() - out['enc_outputs'] = {'pred_logits': enc_outputs_class, 'pred_boxes': enc_outputs_coord} + out["enc_outputs"] = {"pred_logits": enc_outputs_class, "pred_boxes": enc_outputs_coord} return out @torch.jit.unused @@ -191,18 +225,21 @@ def _set_aux_loss(self, outputs_class, outputs_coord): # this is a workaround to make torchscript happy, as torchscript # doesn't support dictionary with non-homogeneous values, such # as a dict having both a Tensor and a list. - return [{'pred_logits': a, 'pred_boxes': b} - for a, b in zip(outputs_class[:-1], outputs_coord[:-1])] + return [ + {"pred_logits": a, "pred_boxes": b} + for a, b in zip(outputs_class[:-1], outputs_coord[:-1], strict=False) + ] class SetCriterion(nn.Module): - """ This class computes the loss for DETR. + """This class computes the loss for DETR. The process happens in two steps: 1) we compute hungarian assignment between ground truth boxes and the outputs of the model 2) we supervise each pair of matched ground-truth / prediction (supervise class and box) """ - def __init__(self, num_classes, matcher, weight_dict, losses, focal_alpha=0.25): - """ Create the criterion. + + def __init__(self, num_classes: int, matcher, weight_dict, losses, focal_alpha: float=0.25) -> None: + """Create the criterion. Parameters: num_classes: number of object categories, omitting the special no-object category matcher: module able to compute a matching between targets and proposals @@ -217,70 +254,82 @@ def __init__(self, num_classes, matcher, weight_dict, losses, focal_alpha=0.25): self.losses = losses self.focal_alpha = focal_alpha - def loss_labels(self, outputs, targets, indices, num_boxes, log=True): + def loss_labels(self, outputs, targets, indices, num_boxes: int, log: bool=True): """Classification loss (NLL) targets dicts must contain the key "labels" containing a tensor of dim [nb_target_boxes] """ - assert 'pred_logits' in outputs - src_logits = outputs['pred_logits'] + assert "pred_logits" in outputs + src_logits = outputs["pred_logits"] idx = self._get_src_permutation_idx(indices) - target_classes_o = torch.cat([t["labels"][J] for t, (_, J) in zip(targets, indices)]) - target_classes = torch.full(src_logits.shape[:2], self.num_classes, - dtype=torch.int64, device=src_logits.device) + target_classes_o = torch.cat([t["labels"][J] for t, (_, J) in zip(targets, indices, strict=False)]) + target_classes = torch.full( + src_logits.shape[:2], self.num_classes, dtype=torch.int64, device=src_logits.device + ) target_classes[idx] = target_classes_o - target_classes_onehot = torch.zeros([src_logits.shape[0], src_logits.shape[1], src_logits.shape[2] + 1], - dtype=src_logits.dtype, layout=src_logits.layout, device=src_logits.device) + target_classes_onehot = torch.zeros( + [src_logits.shape[0], src_logits.shape[1], src_logits.shape[2] + 1], + dtype=src_logits.dtype, + layout=src_logits.layout, + device=src_logits.device, + ) target_classes_onehot.scatter_(2, target_classes.unsqueeze(-1), 1) - target_classes_onehot = target_classes_onehot[:,:,:-1] - loss_ce = sigmoid_focal_loss(src_logits, target_classes_onehot, num_boxes, alpha=self.focal_alpha, gamma=2) * src_logits.shape[1] - losses = {'loss_ce': loss_ce} + target_classes_onehot = target_classes_onehot[:, :, :-1] + loss_ce = ( + sigmoid_focal_loss( + src_logits, target_classes_onehot, num_boxes, alpha=self.focal_alpha, gamma=2 + ) + * src_logits.shape[1] + ) + losses = {"loss_ce": loss_ce} if log: # TODO this should probably be a separate loss, not hacked in this one here - losses['class_error'] = 100 - accuracy(src_logits[idx], target_classes_o)[0] + losses["class_error"] = 100 - accuracy(src_logits[idx], target_classes_o)[0] return losses @torch.no_grad() - def loss_cardinality(self, outputs, targets, indices, num_boxes): - """ Compute the cardinality error, ie the absolute error in the number of predicted non-empty boxes + def loss_cardinality(self, outputs, targets, indices, num_boxes: int): + """Compute the cardinality error, ie the absolute error in the number of predicted non-empty boxes This is not really a loss, it is intended for logging purposes only. It doesn't propagate gradients """ - pred_logits = outputs['pred_logits'] + pred_logits = outputs["pred_logits"] device = pred_logits.device tgt_lengths = torch.as_tensor([len(v["labels"]) for v in targets], device=device) # Count the number of predictions that are NOT "no-object" (which is the last class) card_pred = (pred_logits.argmax(-1) != pred_logits.shape[-1] - 1).sum(1) card_err = F.l1_loss(card_pred.float(), tgt_lengths.float()) - losses = {'cardinality_error': card_err} + losses = {"cardinality_error": card_err} return losses - def loss_boxes(self, outputs, targets, indices, num_boxes): + def loss_boxes(self, outputs, targets, indices, num_boxes: int): """Compute the losses related to the bounding boxes, the L1 regression loss and the GIoU loss - targets dicts must contain the key "boxes" containing a tensor of dim [nb_target_boxes, 4] - The target boxes are expected in format (center_x, center_y, h, w), normalized by the image size. + targets dicts must contain the key "boxes" containing a tensor of dim [nb_target_boxes, 4] + The target boxes are expected in format (center_x, center_y, h, w), normalized by the image size. """ - assert 'pred_boxes' in outputs + assert "pred_boxes" in outputs idx = self._get_src_permutation_idx(indices) - src_boxes = outputs['pred_boxes'][idx] - target_boxes = torch.cat([t['boxes'][i] for t, (_, i) in zip(targets, indices)], dim=0) + src_boxes = outputs["pred_boxes"][idx] + target_boxes = torch.cat([t["boxes"][i] for t, (_, i) in zip(targets, indices, strict=False)], dim=0) - loss_bbox = F.l1_loss(src_boxes, target_boxes, reduction='none') + loss_bbox = F.l1_loss(src_boxes, target_boxes, reduction="none") losses = {} - losses['loss_bbox'] = loss_bbox.sum() / num_boxes - - loss_giou = 1 - torch.diag(box_ops.generalized_box_iou( - box_ops.box_cxcywh_to_xyxy(src_boxes), - box_ops.box_cxcywh_to_xyxy(target_boxes))) - losses['loss_giou'] = loss_giou.sum() / num_boxes + losses["loss_bbox"] = loss_bbox.sum() / num_boxes + + loss_giou = 1 - torch.diag( + box_ops.generalized_box_iou( + box_ops.box_cxcywh_to_xyxy(src_boxes), box_ops.box_cxcywh_to_xyxy(target_boxes) + ) + ) + losses["loss_giou"] = loss_giou.sum() / num_boxes return losses - def loss_masks(self, outputs, targets, indices, num_boxes): + def loss_masks(self, outputs, targets, indices, num_boxes: int): """Compute the losses related to the masks: the focal loss and the dice loss. - targets dicts must contain the key "masks" containing a tensor of dim [nb_target_boxes, h, w] + targets dicts must contain the key "masks" containing a tensor of dim [nb_target_boxes, h, w] """ assert "pred_masks" in outputs @@ -290,13 +339,16 @@ def loss_masks(self, outputs, targets, indices, num_boxes): src_masks = outputs["pred_masks"] # TODO use valid to mask invalid areas due to padding in loss - target_masks, valid = nested_tensor_from_tensor_list([t["masks"] for t in targets]).decompose() + target_masks, valid = nested_tensor_from_tensor_list( + [t["masks"] for t in targets] + ).decompose() target_masks = target_masks.to(src_masks) src_masks = src_masks[src_idx] # upsample predictions to the target size - src_masks = interpolate(src_masks[:, None], size=target_masks.shape[-2:], - mode="bilinear", align_corners=False) + src_masks = interpolate( + src_masks[:, None], size=target_masks.shape[-2:], mode="bilinear", align_corners=False + ) src_masks = src_masks[:, 0].flatten(1) target_masks = target_masks[tgt_idx].flatten(1) @@ -319,31 +371,35 @@ def _get_tgt_permutation_idx(self, indices): tgt_idx = torch.cat([tgt for (_, tgt) in indices]) return batch_idx, tgt_idx - def get_loss(self, loss, outputs, targets, indices, num_boxes, **kwargs): + def get_loss(self, loss, outputs, targets, indices, num_boxes: int, **kwargs): loss_map = { - 'labels': self.loss_labels, - 'cardinality': self.loss_cardinality, - 'boxes': self.loss_boxes, - 'masks': self.loss_masks + "labels": self.loss_labels, + "cardinality": self.loss_cardinality, + "boxes": self.loss_boxes, + "masks": self.loss_masks, } - assert loss in loss_map, f'do you really want to compute {loss} loss?' + assert loss in loss_map, f"do you really want to compute {loss} loss?" return loss_map[loss](outputs, targets, indices, num_boxes, **kwargs) def forward(self, outputs, targets): - """ This performs the loss computation. + """This performs the loss computation. Parameters: outputs: dict of tensors, see the output specification of the model for the format targets: list of dicts, such that len(targets) == batch_size. The expected keys in each dict depends on the losses applied, see each loss' doc """ - outputs_without_aux = {k: v for k, v in outputs.items() if k != 'aux_outputs' and k != 'enc_outputs'} + outputs_without_aux = { + k: v for k, v in outputs.items() if k != "aux_outputs" and k != "enc_outputs" + } # Retrieve the matching between the outputs of the last layer and the targets indices = self.matcher(outputs_without_aux, targets) # Compute the average number of target boxes accross all nodes, for normalization purposes num_boxes = sum(len(t["labels"]) for t in targets) - num_boxes = torch.as_tensor([num_boxes], dtype=torch.float, device=next(iter(outputs.values())).device) + num_boxes = torch.as_tensor( + [num_boxes], dtype=torch.float, device=next(iter(outputs.values())).device + ) if is_dist_avail_and_initialized(): torch.distributed.all_reduce(num_boxes) num_boxes = torch.clamp(num_boxes / get_world_size(), min=1).item() @@ -355,55 +411,55 @@ def forward(self, outputs, targets): losses.update(self.get_loss(loss, outputs, targets, indices, num_boxes, **kwargs)) # In case of auxiliary losses, we repeat this process with the output of each intermediate layer. - if 'aux_outputs' in outputs: - for i, aux_outputs in enumerate(outputs['aux_outputs']): + if "aux_outputs" in outputs: + for i, aux_outputs in enumerate(outputs["aux_outputs"]): indices = self.matcher(aux_outputs, targets) for loss in self.losses: - if loss == 'masks': + if loss == "masks": # Intermediate masks losses are too costly to compute, we ignore them. continue kwargs = {} - if loss == 'labels': + if loss == "labels": # Logging is enabled only for the last layer - kwargs['log'] = False + kwargs["log"] = False l_dict = self.get_loss(loss, aux_outputs, targets, indices, num_boxes, **kwargs) - l_dict = {k + f'_{i}': v for k, v in l_dict.items()} + l_dict = {k + f"_{i}": v for k, v in l_dict.items()} losses.update(l_dict) - if 'enc_outputs' in outputs: - enc_outputs = outputs['enc_outputs'] + if "enc_outputs" in outputs: + enc_outputs = outputs["enc_outputs"] bin_targets = copy.deepcopy(targets) for bt in bin_targets: - bt['labels'] = torch.zeros_like(bt['labels']) + bt["labels"] = torch.zeros_like(bt["labels"]) indices = self.matcher(enc_outputs, bin_targets) for loss in self.losses: - if loss == 'masks': + if loss == "masks": # Intermediate masks losses are too costly to compute, we ignore them. continue kwargs = {} - if loss == 'labels': + if loss == "labels": # Logging is enabled only for the last layer - kwargs['log'] = False + kwargs["log"] = False l_dict = self.get_loss(loss, enc_outputs, bin_targets, indices, num_boxes, **kwargs) - l_dict = {k + f'_enc': v for k, v in l_dict.items()} + l_dict = {k + "_enc": v for k, v in l_dict.items()} losses.update(l_dict) return losses class PostProcess(nn.Module): - """ This module converts the model's output into the format expected by the coco api""" + """This module converts the model's output into the format expected by the coco api""" @torch.no_grad() - def forward(self, outputs, target_sizes): - """ Perform the computation + def forward(self, outputs, target_sizes: Sequence[int]): + """Perform the computation Parameters: outputs: raw outputs of the model target_sizes: tensor of dimension [batch_size x 2] containing the size of each images of the batch For evaluation, this must be the original image size (before any data augmentation) For visualization, this should be the image size after data augment, but before padding """ - out_logits, out_bbox = outputs['pred_logits'], outputs['pred_boxes'] + out_logits, out_bbox = outputs["pred_logits"], outputs["pred_boxes"] assert len(out_logits) == len(target_sizes) assert target_sizes.shape[1] == 2 @@ -414,26 +470,28 @@ def forward(self, outputs, target_sizes): topk_boxes = topk_indexes // out_logits.shape[2] labels = topk_indexes % out_logits.shape[2] boxes = box_ops.box_cxcywh_to_xyxy(out_bbox) - boxes = torch.gather(boxes, 1, topk_boxes.unsqueeze(-1).repeat(1,1,4)) + boxes = torch.gather(boxes, 1, topk_boxes.unsqueeze(-1).repeat(1, 1, 4)) # and from relative [0, 1] to absolute [0, height] coordinates img_h, img_w = target_sizes.unbind(1) scale_fct = torch.stack([img_w, img_h, img_w, img_h], dim=1) boxes = boxes * scale_fct[:, None, :] - results = [{'scores': s, 'labels': l, 'boxes': b} for s, l, b in zip(scores, labels, boxes)] + results = [{"scores": s, "labels": l, "boxes": b} for s, l, b in zip(scores, labels, boxes, strict=False)] return results class MLP(nn.Module): - """ Very simple multi-layer perceptron (also called FFN)""" + """Very simple multi-layer perceptron (also called FFN)""" - def __init__(self, input_dim, hidden_dim, output_dim, num_layers): + def __init__(self, input_dim, hidden_dim, output_dim, num_layers: int) -> None: super().__init__() self.num_layers = num_layers h = [hidden_dim] * (num_layers - 1) - self.layers = nn.ModuleList(nn.Linear(n, k) for n, k in zip([input_dim] + h, h + [output_dim])) + self.layers = nn.ModuleList( + nn.Linear(n, k) for n, k in zip([input_dim, *h], [*h, output_dim], strict=False) + ) def forward(self, x): for i, layer in enumerate(self.layers): @@ -442,7 +500,7 @@ def forward(self, x): def build(args): - num_classes = 20 if args.dataset_file != 'coco' else 91 + num_classes = 20 if args.dataset_file != "coco" else 91 if args.dataset_file == "coco_panoptic": num_classes = 250 device = torch.device(args.device) @@ -463,8 +521,8 @@ def build(args): if args.masks: model = DETRsegm(model, freeze_detr=(args.frozen_weights is not None)) matcher = build_matcher(args) - weight_dict = {'loss_ce': args.cls_loss_coef, 'loss_bbox': args.bbox_loss_coef} - weight_dict['loss_giou'] = args.giou_loss_coef + weight_dict = {"loss_ce": args.cls_loss_coef, "loss_bbox": args.bbox_loss_coef} + weight_dict["loss_giou"] = args.giou_loss_coef if args.masks: weight_dict["loss_mask"] = args.mask_loss_coef weight_dict["loss_dice"] = args.dice_loss_coef @@ -472,19 +530,21 @@ def build(args): if args.aux_loss: aux_weight_dict = {} for i in range(args.dec_layers - 1): - aux_weight_dict.update({k + f'_{i}': v for k, v in weight_dict.items()}) - aux_weight_dict.update({k + f'_enc': v for k, v in weight_dict.items()}) + aux_weight_dict.update({k + f"_{i}": v for k, v in weight_dict.items()}) + aux_weight_dict.update({k + "_enc": v for k, v in weight_dict.items()}) weight_dict.update(aux_weight_dict) - losses = ['labels', 'boxes', 'cardinality'] + losses = ["labels", "boxes", "cardinality"] if args.masks: losses += ["masks"] # num_classes, matcher, weight_dict, losses, focal_alpha=0.25 - criterion = SetCriterion(num_classes, matcher, weight_dict, losses, focal_alpha=args.focal_alpha) + criterion = SetCriterion( + num_classes, matcher, weight_dict, losses, focal_alpha=args.focal_alpha + ) criterion.to(device) - postprocessors = {'bbox': PostProcess()} + postprocessors = {"bbox": PostProcess()} if args.masks: - postprocessors['segm'] = PostProcessSegm() + postprocessors["segm"] = PostProcessSegm() if args.dataset_file == "coco_panoptic": is_thing_map = {i: i <= 90 for i in range(201)} postprocessors["panoptic"] = PostProcessPanoptic(is_thing_map, threshold=0.85) diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/deformable_transformer.py b/dimos/models/Detic/third_party/Deformable-DETR/models/deformable_transformer.py index 08ca377798..f3cde19e1b 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/deformable_transformer.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/models/deformable_transformer.py @@ -8,24 +8,34 @@ # ------------------------------------------------------------------------ import copy -from typing import Optional, List import math import torch +from torch import nn import torch.nn.functional as F -from torch import nn, Tensor -from torch.nn.init import xavier_uniform_, constant_, uniform_, normal_ - +from torch.nn.init import constant_, normal_, xavier_uniform_ from util.misc import inverse_sigmoid + from models.ops.modules import MSDeformAttn class DeformableTransformer(nn.Module): - def __init__(self, d_model=256, nhead=8, - num_encoder_layers=6, num_decoder_layers=6, dim_feedforward=1024, dropout=0.1, - activation="relu", return_intermediate_dec=False, - num_feature_levels=4, dec_n_points=4, enc_n_points=4, - two_stage=False, two_stage_num_proposals=300): + def __init__( + self, + d_model: int=256, + nhead: int=8, + num_encoder_layers: int=6, + num_decoder_layers: int=6, + dim_feedforward: int=1024, + dropout: float=0.1, + activation: str="relu", + return_intermediate_dec: bool=False, + num_feature_levels: int=4, + dec_n_points: int=4, + enc_n_points: int=4, + two_stage: bool=False, + two_stage_num_proposals: int=300, + ) -> None: super().__init__() self.d_model = d_model @@ -33,15 +43,17 @@ def __init__(self, d_model=256, nhead=8, self.two_stage = two_stage self.two_stage_num_proposals = two_stage_num_proposals - encoder_layer = DeformableTransformerEncoderLayer(d_model, dim_feedforward, - dropout, activation, - num_feature_levels, nhead, enc_n_points) + encoder_layer = DeformableTransformerEncoderLayer( + d_model, dim_feedforward, dropout, activation, num_feature_levels, nhead, enc_n_points + ) self.encoder = DeformableTransformerEncoder(encoder_layer, num_encoder_layers) - decoder_layer = DeformableTransformerDecoderLayer(d_model, dim_feedforward, - dropout, activation, - num_feature_levels, nhead, dec_n_points) - self.decoder = DeformableTransformerDecoder(decoder_layer, num_decoder_layers, return_intermediate_dec) + decoder_layer = DeformableTransformerDecoderLayer( + d_model, dim_feedforward, dropout, activation, num_feature_levels, nhead, dec_n_points + ) + self.decoder = DeformableTransformerDecoder( + decoder_layer, num_decoder_layers, return_intermediate_dec + ) self.level_embed = nn.Parameter(torch.Tensor(num_feature_levels, d_model)) @@ -55,7 +67,7 @@ def __init__(self, d_model=256, nhead=8, self._reset_parameters() - def _reset_parameters(self): + def _reset_parameters(self) -> None: for p in self.parameters(): if p.dim() > 1: nn.init.xavier_uniform_(p) @@ -64,7 +76,7 @@ def _reset_parameters(self): m._reset_parameters() if not self.two_stage: xavier_uniform_(self.reference_points.weight.data, gain=1.0) - constant_(self.reference_points.bias.data, 0.) + constant_(self.reference_points.bias.data, 0.0) normal_(self.level_embed) def get_proposal_pos_embed(self, proposals): @@ -84,29 +96,34 @@ def get_proposal_pos_embed(self, proposals): def gen_encoder_output_proposals(self, memory, memory_padding_mask, spatial_shapes): N_, S_, C_ = memory.shape - base_scale = 4.0 proposals = [] _cur = 0 for lvl, (H_, W_) in enumerate(spatial_shapes): - mask_flatten_ = memory_padding_mask[:, _cur:(_cur + H_ * W_)].view(N_, H_, W_, 1) + mask_flatten_ = memory_padding_mask[:, _cur : (_cur + H_ * W_)].view(N_, H_, W_, 1) valid_H = torch.sum(~mask_flatten_[:, :, 0, 0], 1) valid_W = torch.sum(~mask_flatten_[:, 0, :, 0], 1) - grid_y, grid_x = torch.meshgrid(torch.linspace(0, H_ - 1, H_, dtype=torch.float32, device=memory.device), - torch.linspace(0, W_ - 1, W_, dtype=torch.float32, device=memory.device)) + grid_y, grid_x = torch.meshgrid( + torch.linspace(0, H_ - 1, H_, dtype=torch.float32, device=memory.device), + torch.linspace(0, W_ - 1, W_, dtype=torch.float32, device=memory.device), + ) grid = torch.cat([grid_x.unsqueeze(-1), grid_y.unsqueeze(-1)], -1) scale = torch.cat([valid_W.unsqueeze(-1), valid_H.unsqueeze(-1)], 1).view(N_, 1, 1, 2) grid = (grid.unsqueeze(0).expand(N_, -1, -1, -1) + 0.5) / scale - wh = torch.ones_like(grid) * 0.05 * (2.0 ** lvl) + wh = torch.ones_like(grid) * 0.05 * (2.0**lvl) proposal = torch.cat((grid, wh), -1).view(N_, -1, 4) proposals.append(proposal) - _cur += (H_ * W_) + _cur += H_ * W_ output_proposals = torch.cat(proposals, 1) - output_proposals_valid = ((output_proposals > 0.01) & (output_proposals < 0.99)).all(-1, keepdim=True) + output_proposals_valid = ((output_proposals > 0.01) & (output_proposals < 0.99)).all( + -1, keepdim=True + ) output_proposals = torch.log(output_proposals / (1 - output_proposals)) - output_proposals = output_proposals.masked_fill(memory_padding_mask.unsqueeze(-1), float('inf')) - output_proposals = output_proposals.masked_fill(~output_proposals_valid, float('inf')) + output_proposals = output_proposals.masked_fill( + memory_padding_mask.unsqueeze(-1), float("inf") + ) + output_proposals = output_proposals.masked_fill(~output_proposals_valid, float("inf")) output_memory = memory output_memory = output_memory.masked_fill(memory_padding_mask.unsqueeze(-1), float(0)) @@ -131,7 +148,7 @@ def forward(self, srcs, masks, pos_embeds, query_embed=None): mask_flatten = [] lvl_pos_embed_flatten = [] spatial_shapes = [] - for lvl, (src, mask, pos_embed) in enumerate(zip(srcs, masks, pos_embeds)): + for lvl, (src, mask, pos_embed) in enumerate(zip(srcs, masks, pos_embeds, strict=False)): bs, c, h, w = src.shape spatial_shape = (h, w) spatial_shapes.append(spatial_shape) @@ -145,29 +162,48 @@ def forward(self, srcs, masks, pos_embeds, query_embed=None): src_flatten = torch.cat(src_flatten, 1) mask_flatten = torch.cat(mask_flatten, 1) lvl_pos_embed_flatten = torch.cat(lvl_pos_embed_flatten, 1) - spatial_shapes = torch.as_tensor(spatial_shapes, dtype=torch.long, device=src_flatten.device) - level_start_index = torch.cat((spatial_shapes.new_zeros((1, )), spatial_shapes.prod(1).cumsum(0)[:-1])) + spatial_shapes = torch.as_tensor( + spatial_shapes, dtype=torch.long, device=src_flatten.device + ) + level_start_index = torch.cat( + (spatial_shapes.new_zeros((1,)), spatial_shapes.prod(1).cumsum(0)[:-1]) + ) valid_ratios = torch.stack([self.get_valid_ratio(m) for m in masks], 1) # encoder - memory = self.encoder(src_flatten, spatial_shapes, level_start_index, valid_ratios, lvl_pos_embed_flatten, mask_flatten) + memory = self.encoder( + src_flatten, + spatial_shapes, + level_start_index, + valid_ratios, + lvl_pos_embed_flatten, + mask_flatten, + ) # prepare input for decoder bs, _, c = memory.shape if self.two_stage: - output_memory, output_proposals = self.gen_encoder_output_proposals(memory, mask_flatten, spatial_shapes) + output_memory, output_proposals = self.gen_encoder_output_proposals( + memory, mask_flatten, spatial_shapes + ) # hack implementation for two-stage Deformable DETR enc_outputs_class = self.decoder.class_embed[self.decoder.num_layers](output_memory) - enc_outputs_coord_unact = self.decoder.bbox_embed[self.decoder.num_layers](output_memory) + output_proposals + enc_outputs_coord_unact = ( + self.decoder.bbox_embed[self.decoder.num_layers](output_memory) + output_proposals + ) topk = self.two_stage_num_proposals topk_proposals = torch.topk(enc_outputs_class[..., 0], topk, dim=1)[1] - topk_coords_unact = torch.gather(enc_outputs_coord_unact, 1, topk_proposals.unsqueeze(-1).repeat(1, 1, 4)) + topk_coords_unact = torch.gather( + enc_outputs_coord_unact, 1, topk_proposals.unsqueeze(-1).repeat(1, 1, 4) + ) topk_coords_unact = topk_coords_unact.detach() reference_points = topk_coords_unact.sigmoid() init_reference_out = reference_points - pos_trans_out = self.pos_trans_norm(self.pos_trans(self.get_proposal_pos_embed(topk_coords_unact))) + pos_trans_out = self.pos_trans_norm( + self.pos_trans(self.get_proposal_pos_embed(topk_coords_unact)) + ) query_embed, tgt = torch.split(pos_trans_out, c, dim=2) else: query_embed, tgt = torch.split(query_embed, c, dim=1) @@ -177,20 +213,40 @@ def forward(self, srcs, masks, pos_embeds, query_embed=None): init_reference_out = reference_points # decoder - hs, inter_references = self.decoder(tgt, reference_points, memory, - spatial_shapes, level_start_index, valid_ratios, query_embed, mask_flatten) + hs, inter_references = self.decoder( + tgt, + reference_points, + memory, + spatial_shapes, + level_start_index, + valid_ratios, + query_embed, + mask_flatten, + ) inter_references_out = inter_references if self.two_stage: - return hs, init_reference_out, inter_references_out, enc_outputs_class, enc_outputs_coord_unact + return ( + hs, + init_reference_out, + inter_references_out, + enc_outputs_class, + enc_outputs_coord_unact, + ) return hs, init_reference_out, inter_references_out, None, None class DeformableTransformerEncoderLayer(nn.Module): - def __init__(self, - d_model=256, d_ffn=1024, - dropout=0.1, activation="relu", - n_levels=4, n_heads=8, n_points=4): + def __init__( + self, + d_model: int=256, + d_ffn: int=1024, + dropout: float=0.1, + activation: str="relu", + n_levels: int=4, + n_heads: int=8, + n_points: int=4, + ) -> None: super().__init__() # self attention @@ -216,9 +272,18 @@ def forward_ffn(self, src): src = self.norm2(src) return src - def forward(self, src, pos, reference_points, spatial_shapes, level_start_index, padding_mask=None): + def forward( + self, src, pos, reference_points, spatial_shapes, level_start_index, padding_mask=None + ): # self attention - src2 = self.self_attn(self.with_pos_embed(src, pos), reference_points, src, spatial_shapes, level_start_index, padding_mask) + src2 = self.self_attn( + self.with_pos_embed(src, pos), + reference_points, + src, + spatial_shapes, + level_start_index, + padding_mask, + ) src = src + self.dropout1(src2) src = self.norm1(src) @@ -229,7 +294,7 @@ def forward(self, src, pos, reference_points, spatial_shapes, level_start_index, class DeformableTransformerEncoder(nn.Module): - def __init__(self, encoder_layer, num_layers): + def __init__(self, encoder_layer, num_layers: int) -> None: super().__init__() self.layers = _get_clones(encoder_layer, num_layers) self.num_layers = num_layers @@ -238,9 +303,10 @@ def __init__(self, encoder_layer, num_layers): def get_reference_points(spatial_shapes, valid_ratios, device): reference_points_list = [] for lvl, (H_, W_) in enumerate(spatial_shapes): - - ref_y, ref_x = torch.meshgrid(torch.linspace(0.5, H_ - 0.5, H_, dtype=torch.float32, device=device), - torch.linspace(0.5, W_ - 0.5, W_, dtype=torch.float32, device=device)) + ref_y, ref_x = torch.meshgrid( + torch.linspace(0.5, H_ - 0.5, H_, dtype=torch.float32, device=device), + torch.linspace(0.5, W_ - 0.5, W_, dtype=torch.float32, device=device), + ) ref_y = ref_y.reshape(-1)[None] / (valid_ratios[:, None, lvl, 1] * H_) ref_x = ref_x.reshape(-1)[None] / (valid_ratios[:, None, lvl, 0] * W_) ref = torch.stack((ref_x, ref_y), -1) @@ -249,19 +315,32 @@ def get_reference_points(spatial_shapes, valid_ratios, device): reference_points = reference_points[:, :, None] * valid_ratios[:, None] return reference_points - def forward(self, src, spatial_shapes, level_start_index, valid_ratios, pos=None, padding_mask=None): + def forward( + self, src, spatial_shapes, level_start_index, valid_ratios, pos=None, padding_mask=None + ): output = src - reference_points = self.get_reference_points(spatial_shapes, valid_ratios, device=src.device) + reference_points = self.get_reference_points( + spatial_shapes, valid_ratios, device=src.device + ) for _, layer in enumerate(self.layers): - output = layer(output, pos, reference_points, spatial_shapes, level_start_index, padding_mask) + output = layer( + output, pos, reference_points, spatial_shapes, level_start_index, padding_mask + ) return output class DeformableTransformerDecoderLayer(nn.Module): - def __init__(self, d_model=256, d_ffn=1024, - dropout=0.1, activation="relu", - n_levels=4, n_heads=8, n_points=4): + def __init__( + self, + d_model: int=256, + d_ffn: int=1024, + dropout: float=0.1, + activation: str="relu", + n_levels: int=4, + n_heads: int=8, + n_points: int=4, + ) -> None: super().__init__() # cross attention @@ -292,17 +371,33 @@ def forward_ffn(self, tgt): tgt = self.norm3(tgt) return tgt - def forward(self, tgt, query_pos, reference_points, src, src_spatial_shapes, level_start_index, src_padding_mask=None): + def forward( + self, + tgt, + query_pos, + reference_points, + src, + src_spatial_shapes, + level_start_index, + src_padding_mask=None, + ): # self attention q = k = self.with_pos_embed(tgt, query_pos) - tgt2 = self.self_attn(q.transpose(0, 1), k.transpose(0, 1), tgt.transpose(0, 1))[0].transpose(0, 1) + tgt2 = self.self_attn(q.transpose(0, 1), k.transpose(0, 1), tgt.transpose(0, 1))[ + 0 + ].transpose(0, 1) tgt = tgt + self.dropout2(tgt2) tgt = self.norm2(tgt) # cross attention - tgt2 = self.cross_attn(self.with_pos_embed(tgt, query_pos), - reference_points, - src, src_spatial_shapes, level_start_index, src_padding_mask) + tgt2 = self.cross_attn( + self.with_pos_embed(tgt, query_pos), + reference_points, + src, + src_spatial_shapes, + level_start_index, + src_padding_mask, + ) tgt = tgt + self.dropout1(tgt2) tgt = self.norm1(tgt) @@ -313,7 +408,7 @@ def forward(self, tgt, query_pos, reference_points, src, src_spatial_shapes, lev class DeformableTransformerDecoder(nn.Module): - def __init__(self, decoder_layer, num_layers, return_intermediate=False): + def __init__(self, decoder_layer, num_layers: int, return_intermediate: bool=False) -> None: super().__init__() self.layers = _get_clones(decoder_layer, num_layers) self.num_layers = num_layers @@ -322,20 +417,39 @@ def __init__(self, decoder_layer, num_layers, return_intermediate=False): self.bbox_embed = None self.class_embed = None - def forward(self, tgt, reference_points, src, src_spatial_shapes, src_level_start_index, src_valid_ratios, - query_pos=None, src_padding_mask=None): + def forward( + self, + tgt, + reference_points, + src, + src_spatial_shapes, + src_level_start_index, + src_valid_ratios, + query_pos=None, + src_padding_mask=None, + ): output = tgt intermediate = [] intermediate_reference_points = [] for lid, layer in enumerate(self.layers): if reference_points.shape[-1] == 4: - reference_points_input = reference_points[:, :, None] \ - * torch.cat([src_valid_ratios, src_valid_ratios], -1)[:, None] + reference_points_input = ( + reference_points[:, :, None] + * torch.cat([src_valid_ratios, src_valid_ratios], -1)[:, None] + ) else: assert reference_points.shape[-1] == 2 reference_points_input = reference_points[:, :, None] * src_valid_ratios[:, None] - output = layer(output, query_pos, reference_points_input, src, src_spatial_shapes, src_level_start_index, src_padding_mask) + output = layer( + output, + query_pos, + reference_points_input, + src, + src_spatial_shapes, + src_level_start_index, + src_padding_mask, + ) # hack implementation for iterative bounding box refinement if self.bbox_embed is not None: @@ -372,7 +486,7 @@ def _get_activation_fn(activation): return F.gelu if activation == "glu": return F.glu - raise RuntimeError(F"activation should be relu/gelu, not {activation}.") + raise RuntimeError(f"activation should be relu/gelu, not {activation}.") def build_deforamble_transformer(args): @@ -389,6 +503,5 @@ def build_deforamble_transformer(args): dec_n_points=args.dec_n_points, enc_n_points=args.enc_n_points, two_stage=args.two_stage, - two_stage_num_proposals=args.num_queries) - - + two_stage_num_proposals=args.num_queries, + ) diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/matcher.py b/dimos/models/Detic/third_party/Deformable-DETR/models/matcher.py index 63ef029425..7cbcf4a82e 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/matcher.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/models/matcher.py @@ -10,10 +10,10 @@ """ Modules to compute the matching cost and solve the corresponding LSAP. """ -import torch + from scipy.optimize import linear_sum_assignment +import torch from torch import nn - from util.box_ops import box_cxcywh_to_xyxy, generalized_box_iou @@ -25,10 +25,7 @@ class HungarianMatcher(nn.Module): while the others are un-matched (and thus treated as non-objects). """ - def __init__(self, - cost_class: float = 1, - cost_bbox: float = 1, - cost_giou: float = 1): + def __init__(self, cost_class: float = 1, cost_bbox: float = 1, cost_giou: float = 1) -> None: """Creates the matcher Params: @@ -43,7 +40,7 @@ def __init__(self, assert cost_class != 0 or cost_bbox != 0 or cost_giou != 0, "all costs cant be 0" def forward(self, outputs, targets): - """ Performs the matching + """Performs the matching Params: outputs: This is a dict that contains at least these entries: @@ -76,7 +73,7 @@ def forward(self, outputs, targets): # Compute the classification cost. alpha = 0.25 gamma = 2.0 - neg_cost_class = (1 - alpha) * (out_prob ** gamma) * (-(1 - out_prob + 1e-8).log()) + neg_cost_class = (1 - alpha) * (out_prob**gamma) * (-(1 - out_prob + 1e-8).log()) pos_cost_class = alpha * ((1 - out_prob) ** gamma) * (-(out_prob + 1e-8).log()) cost_class = pos_cost_class[:, tgt_ids] - neg_cost_class[:, tgt_ids] @@ -84,19 +81,27 @@ def forward(self, outputs, targets): cost_bbox = torch.cdist(out_bbox, tgt_bbox, p=1) # Compute the giou cost betwen boxes - cost_giou = -generalized_box_iou(box_cxcywh_to_xyxy(out_bbox), - box_cxcywh_to_xyxy(tgt_bbox)) + cost_giou = -generalized_box_iou( + box_cxcywh_to_xyxy(out_bbox), box_cxcywh_to_xyxy(tgt_bbox) + ) # Final cost matrix - C = self.cost_bbox * cost_bbox + self.cost_class * cost_class + self.cost_giou * cost_giou + C = ( + self.cost_bbox * cost_bbox + + self.cost_class * cost_class + + self.cost_giou * cost_giou + ) C = C.view(bs, num_queries, -1).cpu() sizes = [len(v["boxes"]) for v in targets] indices = [linear_sum_assignment(c[i]) for i, c in enumerate(C.split(sizes, -1))] - return [(torch.as_tensor(i, dtype=torch.int64), torch.as_tensor(j, dtype=torch.int64)) for i, j in indices] + return [ + (torch.as_tensor(i, dtype=torch.int64), torch.as_tensor(j, dtype=torch.int64)) + for i, j in indices + ] def build_matcher(args): - return HungarianMatcher(cost_class=args.set_cost_class, - cost_bbox=args.set_cost_bbox, - cost_giou=args.set_cost_giou) + return HungarianMatcher( + cost_class=args.set_cost_class, cost_bbox=args.set_cost_bbox, cost_giou=args.set_cost_giou + ) diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/functions/__init__.py b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/functions/__init__.py index 8a2197bda3..c528f3c6cf 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/functions/__init__.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/functions/__init__.py @@ -7,4 +7,3 @@ # ------------------------------------------------------------------------------------------------ from .ms_deform_attn_func import MSDeformAttnFunction - diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/functions/ms_deform_attn_func.py b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/functions/ms_deform_attn_func.py index 8c5df8cf5d..965811ed7f 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/functions/ms_deform_attn_func.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/functions/ms_deform_attn_func.py @@ -6,34 +6,62 @@ # Modified from https://github.com/chengdazhi/Deformable-Convolution-V2-PyTorch/tree/pytorch_1.0.0 # ------------------------------------------------------------------------------------------------ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import division +import MultiScaleDeformableAttention as MSDA import torch -import torch.nn.functional as F from torch.autograd import Function from torch.autograd.function import once_differentiable - -import MultiScaleDeformableAttention as MSDA +import torch.nn.functional as F class MSDeformAttnFunction(Function): @staticmethod - def forward(ctx, value, value_spatial_shapes, value_level_start_index, sampling_locations, attention_weights, im2col_step): + def forward( + ctx, + value, + value_spatial_shapes, + value_level_start_index, + sampling_locations, + attention_weights, + im2col_step, + ): ctx.im2col_step = im2col_step output = MSDA.ms_deform_attn_forward( - value, value_spatial_shapes, value_level_start_index, sampling_locations, attention_weights, ctx.im2col_step) - ctx.save_for_backward(value, value_spatial_shapes, value_level_start_index, sampling_locations, attention_weights) + value, + value_spatial_shapes, + value_level_start_index, + sampling_locations, + attention_weights, + ctx.im2col_step, + ) + ctx.save_for_backward( + value, + value_spatial_shapes, + value_level_start_index, + sampling_locations, + attention_weights, + ) return output @staticmethod @once_differentiable def backward(ctx, grad_output): - value, value_spatial_shapes, value_level_start_index, sampling_locations, attention_weights = ctx.saved_tensors - grad_value, grad_sampling_loc, grad_attn_weight = \ - MSDA.ms_deform_attn_backward( - value, value_spatial_shapes, value_level_start_index, sampling_locations, attention_weights, grad_output, ctx.im2col_step) + ( + value, + value_spatial_shapes, + value_level_start_index, + sampling_locations, + attention_weights, + ) = ctx.saved_tensors + grad_value, grad_sampling_loc, grad_attn_weight = MSDA.ms_deform_attn_backward( + value, + value_spatial_shapes, + value_level_start_index, + sampling_locations, + attention_weights, + grad_output, + ctx.im2col_step, + ) return grad_value, None, None, grad_sampling_loc, grad_attn_weight, None @@ -48,14 +76,19 @@ def ms_deform_attn_core_pytorch(value, value_spatial_shapes, sampling_locations, sampling_value_list = [] for lid_, (H_, W_) in enumerate(value_spatial_shapes): # N_, H_*W_, M_, D_ -> N_, H_*W_, M_*D_ -> N_, M_*D_, H_*W_ -> N_*M_, D_, H_, W_ - value_l_ = value_list[lid_].flatten(2).transpose(1, 2).reshape(N_*M_, D_, H_, W_) + value_l_ = value_list[lid_].flatten(2).transpose(1, 2).reshape(N_ * M_, D_, H_, W_) # N_, Lq_, M_, P_, 2 -> N_, M_, Lq_, P_, 2 -> N_*M_, Lq_, P_, 2 sampling_grid_l_ = sampling_grids[:, :, :, lid_].transpose(1, 2).flatten(0, 1) # N_*M_, D_, Lq_, P_ - sampling_value_l_ = F.grid_sample(value_l_, sampling_grid_l_, - mode='bilinear', padding_mode='zeros', align_corners=False) + sampling_value_l_ = F.grid_sample( + value_l_, sampling_grid_l_, mode="bilinear", padding_mode="zeros", align_corners=False + ) sampling_value_list.append(sampling_value_l_) # (N_, Lq_, M_, L_, P_) -> (N_, M_, Lq_, L_, P_) -> (N_, M_, 1, Lq_, L_*P_) - attention_weights = attention_weights.transpose(1, 2).reshape(N_*M_, 1, Lq_, L_*P_) - output = (torch.stack(sampling_value_list, dim=-2).flatten(-2) * attention_weights).sum(-1).view(N_, M_*D_, Lq_) + attention_weights = attention_weights.transpose(1, 2).reshape(N_ * M_, 1, Lq_, L_ * P_) + output = ( + (torch.stack(sampling_value_list, dim=-2).flatten(-2) * attention_weights) + .sum(-1) + .view(N_, M_ * D_, Lq_) + ) return output.transpose(1, 2).contiguous() diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/modules/ms_deform_attn.py b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/modules/ms_deform_attn.py index 663d64a3d2..1d70af7cc4 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/modules/ms_deform_attn.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/modules/ms_deform_attn.py @@ -6,29 +6,26 @@ # Modified from https://github.com/chengdazhi/Deformable-Convolution-V2-PyTorch/tree/pytorch_1.0.0 # ------------------------------------------------------------------------------------------------ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import division -import warnings import math +import warnings import torch from torch import nn import torch.nn.functional as F -from torch.nn.init import xavier_uniform_, constant_ +from torch.nn.init import constant_, xavier_uniform_ from ..functions import MSDeformAttnFunction def _is_power_of_2(n): if (not isinstance(n, int)) or (n < 0): - raise ValueError("invalid input for _is_power_of_2: {} (type: {})".format(n, type(n))) - return (n & (n-1) == 0) and n != 0 + raise ValueError(f"invalid input for _is_power_of_2: {n} (type: {type(n)})") + return (n & (n - 1) == 0) and n != 0 class MSDeformAttn(nn.Module): - def __init__(self, d_model=256, n_levels=4, n_heads=8, n_points=4): + def __init__(self, d_model: int=256, n_levels: int=4, n_heads: int=8, n_points: int=4) -> None: """ Multi-Scale Deformable Attention Module :param d_model hidden dimension @@ -38,12 +35,16 @@ def __init__(self, d_model=256, n_levels=4, n_heads=8, n_points=4): """ super().__init__() if d_model % n_heads != 0: - raise ValueError('d_model must be divisible by n_heads, but got {} and {}'.format(d_model, n_heads)) + raise ValueError( + f"d_model must be divisible by n_heads, but got {d_model} and {n_heads}" + ) _d_per_head = d_model // n_heads # you'd better set _d_per_head to a power of 2 which is more efficient in our CUDA implementation if not _is_power_of_2(_d_per_head): - warnings.warn("You'd better set d_model in MSDeformAttn to make the dimension of each attention head a power of 2 " - "which is more efficient in our CUDA implementation.") + warnings.warn( + "You'd better set d_model in MSDeformAttn to make the dimension of each attention head a power of 2 " + "which is more efficient in our CUDA implementation.", stacklevel=2 + ) self.im2col_step = 64 @@ -59,24 +60,36 @@ def __init__(self, d_model=256, n_levels=4, n_heads=8, n_points=4): self._reset_parameters() - def _reset_parameters(self): - constant_(self.sampling_offsets.weight.data, 0.) + def _reset_parameters(self) -> None: + constant_(self.sampling_offsets.weight.data, 0.0) thetas = torch.arange(self.n_heads, dtype=torch.float32) * (2.0 * math.pi / self.n_heads) grid_init = torch.stack([thetas.cos(), thetas.sin()], -1) - grid_init = (grid_init / grid_init.abs().max(-1, keepdim=True)[0]).view(self.n_heads, 1, 1, 2).repeat(1, self.n_levels, self.n_points, 1) + grid_init = ( + (grid_init / grid_init.abs().max(-1, keepdim=True)[0]) + .view(self.n_heads, 1, 1, 2) + .repeat(1, self.n_levels, self.n_points, 1) + ) for i in range(self.n_points): grid_init[:, :, i, :] *= i + 1 with torch.no_grad(): self.sampling_offsets.bias = nn.Parameter(grid_init.view(-1)) - constant_(self.attention_weights.weight.data, 0.) - constant_(self.attention_weights.bias.data, 0.) + constant_(self.attention_weights.weight.data, 0.0) + constant_(self.attention_weights.bias.data, 0.0) xavier_uniform_(self.value_proj.weight.data) - constant_(self.value_proj.bias.data, 0.) + constant_(self.value_proj.bias.data, 0.0) xavier_uniform_(self.output_proj.weight.data) - constant_(self.output_proj.bias.data, 0.) - - def forward(self, query, reference_points, input_flatten, input_spatial_shapes, input_level_start_index, input_padding_mask=None): - """ + constant_(self.output_proj.bias.data, 0.0) + + def forward( + self, + query, + reference_points, + input_flatten, + input_spatial_shapes, + input_level_start_index, + input_padding_mask=None, + ): + r""" :param query (N, Length_{query}, C) :param reference_points (N, Length_{query}, n_levels, 2), range in [0, 1], top-left (0,0), bottom-right (1, 1), including padding area or (N, Length_{query}, n_levels, 4), add additional (w, h) to form reference boxes @@ -95,21 +108,40 @@ def forward(self, query, reference_points, input_flatten, input_spatial_shapes, if input_padding_mask is not None: value = value.masked_fill(input_padding_mask[..., None], float(0)) value = value.view(N, Len_in, self.n_heads, self.d_model // self.n_heads) - sampling_offsets = self.sampling_offsets(query).view(N, Len_q, self.n_heads, self.n_levels, self.n_points, 2) - attention_weights = self.attention_weights(query).view(N, Len_q, self.n_heads, self.n_levels * self.n_points) - attention_weights = F.softmax(attention_weights, -1).view(N, Len_q, self.n_heads, self.n_levels, self.n_points) + sampling_offsets = self.sampling_offsets(query).view( + N, Len_q, self.n_heads, self.n_levels, self.n_points, 2 + ) + attention_weights = self.attention_weights(query).view( + N, Len_q, self.n_heads, self.n_levels * self.n_points + ) + attention_weights = F.softmax(attention_weights, -1).view( + N, Len_q, self.n_heads, self.n_levels, self.n_points + ) # N, Len_q, n_heads, n_levels, n_points, 2 if reference_points.shape[-1] == 2: - offset_normalizer = torch.stack([input_spatial_shapes[..., 1], input_spatial_shapes[..., 0]], -1) - sampling_locations = reference_points[:, :, None, :, None, :] \ - + sampling_offsets / offset_normalizer[None, None, None, :, None, :] + offset_normalizer = torch.stack( + [input_spatial_shapes[..., 1], input_spatial_shapes[..., 0]], -1 + ) + sampling_locations = ( + reference_points[:, :, None, :, None, :] + + sampling_offsets / offset_normalizer[None, None, None, :, None, :] + ) elif reference_points.shape[-1] == 4: - sampling_locations = reference_points[:, :, None, :, None, :2] \ - + sampling_offsets / self.n_points * reference_points[:, :, None, :, None, 2:] * 0.5 + sampling_locations = ( + reference_points[:, :, None, :, None, :2] + + sampling_offsets / self.n_points * reference_points[:, :, None, :, None, 2:] * 0.5 + ) else: raise ValueError( - 'Last dim of reference_points must be 2 or 4, but get {} instead.'.format(reference_points.shape[-1])) + f"Last dim of reference_points must be 2 or 4, but get {reference_points.shape[-1]} instead." + ) output = MSDeformAttnFunction.apply( - value, input_spatial_shapes, input_level_start_index, sampling_locations, attention_weights, self.im2col_step) + value, + input_spatial_shapes, + input_level_start_index, + sampling_locations, + attention_weights, + self.im2col_step, + ) output = self.output_proj(output) return output diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/setup.py b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/setup.py index a0131bc21c..7a5560a83f 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/setup.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/setup.py @@ -6,20 +6,16 @@ # Modified from https://github.com/chengdazhi/Deformable-Convolution-V2-PyTorch/tree/pytorch_1.0.0 # ------------------------------------------------------------------------------------------------ -import os import glob +import os +from setuptools import find_packages, setup import torch - -from torch.utils.cpp_extension import CUDA_HOME -from torch.utils.cpp_extension import CppExtension -from torch.utils.cpp_extension import CUDAExtension - -from setuptools import find_packages -from setuptools import setup +from torch.utils.cpp_extension import CUDA_HOME, CppExtension, CUDAExtension requirements = ["torch", "torchvision"] + def get_extensions(): this_dir = os.path.dirname(os.path.abspath(__file__)) extensions_dir = os.path.join(this_dir, "src") @@ -44,7 +40,7 @@ def get_extensions(): "-D__CUDA_NO_HALF2_OPERATORS__", ] else: - raise NotImplementedError('Cuda is not availabel') + raise NotImplementedError("Cuda is not availabel") sources = [os.path.join(extensions_dir, s) for s in sources] include_dirs = [extensions_dir] @@ -59,13 +55,19 @@ def get_extensions(): ] return ext_modules + setup( name="MultiScaleDeformableAttention", version="1.0", author="Weijie Su", url="https://github.com/fundamentalvision/Deformable-DETR", description="PyTorch Wrapper for CUDA Functions of Multi-Scale Deformable Attention", - packages=find_packages(exclude=("configs", "tests",)), + packages=find_packages( + exclude=( + "configs", + "tests", + ) + ), ext_modules=get_extensions(), cmdclass={"build_ext": torch.utils.cpp_extension.BuildExtension}, ) diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/test.py b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/test.py index 8dbf6d5547..720d6473b2 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/ops/test.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/models/ops/test.py @@ -6,62 +6,87 @@ # Modified from https://github.com/chengdazhi/Deformable-Convolution-V2-PyTorch/tree/pytorch_1.0.0 # ------------------------------------------------------------------------------------------------ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import division -import time +from functions.ms_deform_attn_func import MSDeformAttnFunction, ms_deform_attn_core_pytorch import torch -import torch.nn as nn from torch.autograd import gradcheck -from functions.ms_deform_attn_func import MSDeformAttnFunction, ms_deform_attn_core_pytorch - - N, M, D = 1, 2, 2 Lq, L, P = 2, 2, 2 shapes = torch.as_tensor([(6, 4), (3, 2)], dtype=torch.long).cuda() -level_start_index = torch.cat((shapes.new_zeros((1, )), shapes.prod(1).cumsum(0)[:-1])) -S = sum([(H*W).item() for H, W in shapes]) +level_start_index = torch.cat((shapes.new_zeros((1,)), shapes.prod(1).cumsum(0)[:-1])) +S = sum([(H * W).item() for H, W in shapes]) torch.manual_seed(3) @torch.no_grad() -def check_forward_equal_with_pytorch_double(): +def check_forward_equal_with_pytorch_double() -> None: value = torch.rand(N, S, M, D).cuda() * 0.01 sampling_locations = torch.rand(N, Lq, M, L, P, 2).cuda() attention_weights = torch.rand(N, Lq, M, L, P).cuda() + 1e-5 attention_weights /= attention_weights.sum(-1, keepdim=True).sum(-2, keepdim=True) im2col_step = 2 - output_pytorch = ms_deform_attn_core_pytorch(value.double(), shapes, sampling_locations.double(), attention_weights.double()).detach().cpu() - output_cuda = MSDeformAttnFunction.apply(value.double(), shapes, level_start_index, sampling_locations.double(), attention_weights.double(), im2col_step).detach().cpu() + output_pytorch = ( + ms_deform_attn_core_pytorch( + value.double(), shapes, sampling_locations.double(), attention_weights.double() + ) + .detach() + .cpu() + ) + output_cuda = ( + MSDeformAttnFunction.apply( + value.double(), + shapes, + level_start_index, + sampling_locations.double(), + attention_weights.double(), + im2col_step, + ) + .detach() + .cpu() + ) fwdok = torch.allclose(output_cuda, output_pytorch) max_abs_err = (output_cuda - output_pytorch).abs().max() max_rel_err = ((output_cuda - output_pytorch).abs() / output_pytorch.abs()).max() - print(f'* {fwdok} check_forward_equal_with_pytorch_double: max_abs_err {max_abs_err:.2e} max_rel_err {max_rel_err:.2e}') + print( + f"* {fwdok} check_forward_equal_with_pytorch_double: max_abs_err {max_abs_err:.2e} max_rel_err {max_rel_err:.2e}" + ) @torch.no_grad() -def check_forward_equal_with_pytorch_float(): +def check_forward_equal_with_pytorch_float() -> None: value = torch.rand(N, S, M, D).cuda() * 0.01 sampling_locations = torch.rand(N, Lq, M, L, P, 2).cuda() attention_weights = torch.rand(N, Lq, M, L, P).cuda() + 1e-5 attention_weights /= attention_weights.sum(-1, keepdim=True).sum(-2, keepdim=True) im2col_step = 2 - output_pytorch = ms_deform_attn_core_pytorch(value, shapes, sampling_locations, attention_weights).detach().cpu() - output_cuda = MSDeformAttnFunction.apply(value, shapes, level_start_index, sampling_locations, attention_weights, im2col_step).detach().cpu() + output_pytorch = ( + ms_deform_attn_core_pytorch(value, shapes, sampling_locations, attention_weights) + .detach() + .cpu() + ) + output_cuda = ( + MSDeformAttnFunction.apply( + value, shapes, level_start_index, sampling_locations, attention_weights, im2col_step + ) + .detach() + .cpu() + ) fwdok = torch.allclose(output_cuda, output_pytorch, rtol=1e-2, atol=1e-3) max_abs_err = (output_cuda - output_pytorch).abs().max() max_rel_err = ((output_cuda - output_pytorch).abs() / output_pytorch.abs()).max() - print(f'* {fwdok} check_forward_equal_with_pytorch_float: max_abs_err {max_abs_err:.2e} max_rel_err {max_rel_err:.2e}') + print( + f"* {fwdok} check_forward_equal_with_pytorch_float: max_abs_err {max_abs_err:.2e} max_rel_err {max_rel_err:.2e}" + ) -def check_gradient_numerical(channels=4, grad_value=True, grad_sampling_loc=True, grad_attn_weight=True): - +def check_gradient_numerical( + channels: int=4, grad_value: bool=True, grad_sampling_loc: bool=True, grad_attn_weight: bool=True +) -> None: value = torch.rand(N, S, M, channels).cuda() * 0.01 sampling_locations = torch.rand(N, Lq, M, L, P, 2).cuda() attention_weights = torch.rand(N, Lq, M, L, P).cuda() + 1e-5 @@ -73,17 +98,24 @@ def check_gradient_numerical(channels=4, grad_value=True, grad_sampling_loc=True sampling_locations.requires_grad = grad_sampling_loc attention_weights.requires_grad = grad_attn_weight - gradok = gradcheck(func, (value.double(), shapes, level_start_index, sampling_locations.double(), attention_weights.double(), im2col_step)) + gradok = gradcheck( + func, + ( + value.double(), + shapes, + level_start_index, + sampling_locations.double(), + attention_weights.double(), + im2col_step, + ), + ) - print(f'* {gradok} check_gradient_numerical(D={channels})') + print(f"* {gradok} check_gradient_numerical(D={channels})") -if __name__ == '__main__': +if __name__ == "__main__": check_forward_equal_with_pytorch_double() check_forward_equal_with_pytorch_float() for channels in [30, 32, 64, 71, 1025, 2048, 3096]: check_gradient_numerical(channels, True, True, True) - - - diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/position_encoding.py b/dimos/models/Detic/third_party/Deformable-DETR/models/position_encoding.py index a92f0d36ae..2ce5038e5e 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/position_encoding.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/models/position_encoding.py @@ -10,10 +10,11 @@ """ Various positional encodings for the transformer. """ + import math + import torch from torch import nn - from util.misc import NestedTensor @@ -22,7 +23,8 @@ class PositionEmbeddingSine(nn.Module): This is a more standard version of the position embedding, very similar to the one used by the Attention is all you need paper, generalized to work on images. """ - def __init__(self, num_pos_feats=64, temperature=10000, normalize=False, scale=None): + + def __init__(self, num_pos_feats: int=64, temperature: int=10000, normalize: bool=False, scale=None) -> None: super().__init__() self.num_pos_feats = num_pos_feats self.temperature = temperature @@ -50,8 +52,12 @@ def forward(self, tensor_list: NestedTensor): pos_x = x_embed[:, :, :, None] / dim_t pos_y = y_embed[:, :, :, None] / dim_t - pos_x = torch.stack((pos_x[:, :, :, 0::2].sin(), pos_x[:, :, :, 1::2].cos()), dim=4).flatten(3) - pos_y = torch.stack((pos_y[:, :, :, 0::2].sin(), pos_y[:, :, :, 1::2].cos()), dim=4).flatten(3) + pos_x = torch.stack( + (pos_x[:, :, :, 0::2].sin(), pos_x[:, :, :, 1::2].cos()), dim=4 + ).flatten(3) + pos_y = torch.stack( + (pos_y[:, :, :, 0::2].sin(), pos_y[:, :, :, 1::2].cos()), dim=4 + ).flatten(3) pos = torch.cat((pos_y, pos_x), dim=3).permute(0, 3, 1, 2) return pos @@ -60,13 +66,14 @@ class PositionEmbeddingLearned(nn.Module): """ Absolute pos embedding, learned. """ - def __init__(self, num_pos_feats=256): + + def __init__(self, num_pos_feats: int=256) -> None: super().__init__() self.row_embed = nn.Embedding(50, num_pos_feats) self.col_embed = nn.Embedding(50, num_pos_feats) self.reset_parameters() - def reset_parameters(self): + def reset_parameters(self) -> None: nn.init.uniform_(self.row_embed.weight) nn.init.uniform_(self.col_embed.weight) @@ -77,19 +84,27 @@ def forward(self, tensor_list: NestedTensor): j = torch.arange(h, device=x.device) x_emb = self.col_embed(i) y_emb = self.row_embed(j) - pos = torch.cat([ - x_emb.unsqueeze(0).repeat(h, 1, 1), - y_emb.unsqueeze(1).repeat(1, w, 1), - ], dim=-1).permute(2, 0, 1).unsqueeze(0).repeat(x.shape[0], 1, 1, 1) + pos = ( + torch.cat( + [ + x_emb.unsqueeze(0).repeat(h, 1, 1), + y_emb.unsqueeze(1).repeat(1, w, 1), + ], + dim=-1, + ) + .permute(2, 0, 1) + .unsqueeze(0) + .repeat(x.shape[0], 1, 1, 1) + ) return pos def build_position_encoding(args): N_steps = args.hidden_dim // 2 - if args.position_embedding in ('v2', 'sine'): + if args.position_embedding in ("v2", "sine"): # TODO find a better way of exposing other arguments position_embedding = PositionEmbeddingSine(N_steps, normalize=True) - elif args.position_embedding in ('v3', 'learned'): + elif args.position_embedding in ("v3", "learned"): position_embedding = PositionEmbeddingLearned(N_steps) else: raise ValueError(f"not supported {args.position_embedding}") diff --git a/dimos/models/Detic/third_party/Deformable-DETR/models/segmentation.py b/dimos/models/Detic/third_party/Deformable-DETR/models/segmentation.py index c801c0eaad..2450a5c447 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/models/segmentation.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/models/segmentation.py @@ -10,16 +10,17 @@ """ This file provides the definition of the convolutional heads used to predict masks, as well as the losses """ -import io + from collections import defaultdict +import io +from PIL import Image import torch import torch.nn as nn import torch.nn.functional as F -from PIL import Image - import util.box_ops as box_ops from util.misc import NestedTensor, interpolate, nested_tensor_from_tensor_list +from typing import Optional, Sequence try: from panopticapi.utils import id2rgb, rgb2id @@ -28,7 +29,7 @@ class DETRsegm(nn.Module): - def __init__(self, detr, freeze_detr=False): + def __init__(self, detr, freeze_detr: bool=False) -> None: super().__init__() self.detr = detr @@ -56,14 +57,19 @@ def forward(self, samples: NestedTensor): out = {"pred_logits": outputs_class[-1], "pred_boxes": outputs_coord[-1]} if self.detr.aux_loss: out["aux_outputs"] = [ - {"pred_logits": a, "pred_boxes": b} for a, b in zip(outputs_class[:-1], outputs_coord[:-1]) + {"pred_logits": a, "pred_boxes": b} + for a, b in zip(outputs_class[:-1], outputs_coord[:-1], strict=False) ] # FIXME h_boxes takes the last one computed, keep this in mind bbox_mask = self.bbox_attention(hs[-1], memory, mask=mask) - seg_masks = self.mask_head(src_proj, bbox_mask, [features[2].tensors, features[1].tensors, features[0].tensors]) - outputs_seg_masks = seg_masks.view(bs, self.detr.num_queries, seg_masks.shape[-2], seg_masks.shape[-1]) + seg_masks = self.mask_head( + src_proj, bbox_mask, [features[2].tensors, features[1].tensors, features[0].tensors] + ) + outputs_seg_masks = seg_masks.view( + bs, self.detr.num_queries, seg_masks.shape[-2], seg_masks.shape[-1] + ) out["pred_masks"] = outputs_seg_masks return out @@ -75,10 +81,17 @@ class MaskHeadSmallConv(nn.Module): Upsampling is done using a FPN approach """ - def __init__(self, dim, fpn_dims, context_dim): + def __init__(self, dim: int, fpn_dims, context_dim) -> None: super().__init__() - inter_dims = [dim, context_dim // 2, context_dim // 4, context_dim // 8, context_dim // 16, context_dim // 64] + inter_dims = [ + dim, + context_dim // 2, + context_dim // 4, + context_dim // 8, + context_dim // 16, + context_dim // 64, + ] self.lay1 = torch.nn.Conv2d(dim, dim, 3, padding=1) self.gn1 = torch.nn.GroupNorm(8, dim) self.lay2 = torch.nn.Conv2d(dim, inter_dims[1], 3, padding=1) @@ -103,7 +116,7 @@ def __init__(self, dim, fpn_dims, context_dim): nn.init.constant_(m.bias, 0) def forward(self, x, bbox_mask, fpns): - def expand(tensor, length): + def expand(tensor, length: int): return tensor.unsqueeze(1).repeat(1, int(length), 1, 1, 1).flatten(0, 1) x = torch.cat([expand(x, bbox_mask.shape[1]), bbox_mask.flatten(0, 1)], 1) @@ -146,7 +159,7 @@ def expand(tensor, length): class MHAttentionMap(nn.Module): """This is a 2D attention module, which only returns the attention softmax (no multiplication by value)""" - def __init__(self, query_dim, hidden_dim, num_heads, dropout=0, bias=True): + def __init__(self, query_dim, hidden_dim, num_heads: int, dropout: int=0, bias: bool=True) -> None: super().__init__() self.num_heads = num_heads self.hidden_dim = hidden_dim @@ -165,7 +178,9 @@ def forward(self, q, k, mask=None): q = self.q_linear(q) k = F.conv2d(k, self.k_linear.weight.unsqueeze(-1).unsqueeze(-1), self.k_linear.bias) qh = q.view(q.shape[0], q.shape[1], self.num_heads, self.hidden_dim // self.num_heads) - kh = k.view(k.shape[0], self.num_heads, self.hidden_dim // self.num_heads, k.shape[-2], k.shape[-1]) + kh = k.view( + k.shape[0], self.num_heads, self.hidden_dim // self.num_heads, k.shape[-2], k.shape[-1] + ) weights = torch.einsum("bqnc,bnchw->bqnhw", qh * self.normalize_fact, kh) if mask is not None: @@ -175,7 +190,7 @@ def forward(self, q, k, mask=None): return weights -def dice_loss(inputs, targets, num_boxes): +def dice_loss(inputs, targets, num_boxes: int): """ Compute the DICE loss, similar to generalized IOU for masks Args: @@ -193,7 +208,7 @@ def dice_loss(inputs, targets, num_boxes): return loss.sum() / num_boxes -def sigmoid_focal_loss(inputs, targets, num_boxes, alpha: float = 0.25, gamma: float = 2): +def sigmoid_focal_loss(inputs, targets, num_boxes: int, alpha: float = 0.25, gamma: float = 2): """ Loss used in RetinaNet for dense detection: https://arxiv.org/abs/1708.02002. Args: @@ -222,19 +237,23 @@ def sigmoid_focal_loss(inputs, targets, num_boxes, alpha: float = 0.25, gamma: f class PostProcessSegm(nn.Module): - def __init__(self, threshold=0.5): + def __init__(self, threshold: float=0.5) -> None: super().__init__() self.threshold = threshold @torch.no_grad() - def forward(self, results, outputs, orig_target_sizes, max_target_sizes): + def forward(self, results, outputs, orig_target_sizes: Sequence[int], max_target_sizes: Sequence[int]): assert len(orig_target_sizes) == len(max_target_sizes) max_h, max_w = max_target_sizes.max(0)[0].tolist() outputs_masks = outputs["pred_masks"].squeeze(2) - outputs_masks = F.interpolate(outputs_masks, size=(max_h, max_w), mode="bilinear", align_corners=False) + outputs_masks = F.interpolate( + outputs_masks, size=(max_h, max_w), mode="bilinear", align_corners=False + ) outputs_masks = (outputs_masks.sigmoid() > self.threshold).cpu() - for i, (cur_mask, t, tt) in enumerate(zip(outputs_masks, max_target_sizes, orig_target_sizes)): + for i, (cur_mask, t, tt) in enumerate( + zip(outputs_masks, max_target_sizes, orig_target_sizes, strict=False) + ): img_h, img_w = t[0], t[1] results[i]["masks"] = cur_mask[:, :img_h, :img_w].unsqueeze(1) results[i]["masks"] = F.interpolate( @@ -246,9 +265,9 @@ def forward(self, results, outputs, orig_target_sizes, max_target_sizes): class PostProcessPanoptic(nn.Module): """This class converts the output of the model to the final panoptic result, in the format expected by the - coco panoptic API """ + coco panoptic API""" - def __init__(self, is_thing_map, threshold=0.85): + def __init__(self, is_thing_map: bool, threshold: float=0.85) -> None: """ Parameters: is_thing_map: This is a whose keys are the class ids, and the values a boolean indicating whether @@ -259,19 +278,23 @@ def __init__(self, is_thing_map, threshold=0.85): self.threshold = threshold self.is_thing_map = is_thing_map - def forward(self, outputs, processed_sizes, target_sizes=None): - """ This function computes the panoptic prediction from the model's predictions. + def forward(self, outputs, processed_sizes: Sequence[int], target_sizes: Optional[Sequence[int]]=None): + """This function computes the panoptic prediction from the model's predictions. Parameters: outputs: This is a dict coming directly from the model. See the model doc for the content. processed_sizes: This is a list of tuples (or torch tensors) of sizes of the images that were passed to the model, ie the size after data augmentation but before batching. target_sizes: This is a list of tuples (or torch tensors) corresponding to the requested final size of each prediction. If left to None, it will default to the processed_sizes - """ + """ if target_sizes is None: target_sizes = processed_sizes assert len(processed_sizes) == len(target_sizes) - out_logits, raw_masks, raw_boxes = outputs["pred_logits"], outputs["pred_masks"], outputs["pred_boxes"] + out_logits, raw_masks, raw_boxes = ( + outputs["pred_logits"], + outputs["pred_masks"], + outputs["pred_boxes"], + ) assert len(out_logits) == len(raw_masks) == len(target_sizes) preds = [] @@ -281,7 +304,7 @@ def to_tuple(tup): return tuple(tup.cpu().tolist()) for cur_logits, cur_masks, cur_boxes, size, target_size in zip( - out_logits, raw_masks, raw_boxes, processed_sizes, target_sizes + out_logits, raw_masks, raw_boxes, processed_sizes, target_sizes, strict=False ): # we filter empty queries and detection below threshold scores, labels = cur_logits.softmax(-1).max(-1) @@ -304,7 +327,7 @@ def to_tuple(tup): if not self.is_thing_map[label.item()]: stuff_equiv_classes[label.item()].append(k) - def get_ids_area(masks, scores, dedup=False): + def get_ids_area(masks, scores, dedup: bool=False): # This helper function creates the final panoptic segmentation image # It also returns the area of the masks that appears on the image @@ -329,7 +352,9 @@ def get_ids_area(masks, scores, dedup=False): seg_img = seg_img.resize(size=(final_w, final_h), resample=Image.NEAREST) np_seg_img = ( - torch.ByteTensor(torch.ByteStorage.from_buffer(seg_img.tobytes())).view(final_h, final_w, 3).numpy() + torch.ByteTensor(torch.ByteStorage.from_buffer(seg_img.tobytes())) + .view(final_h, final_w, 3) + .numpy() ) m_id = torch.from_numpy(rgb2id(np_seg_img)) @@ -343,7 +368,9 @@ def get_ids_area(masks, scores, dedup=False): # We know filter empty masks as long as we find some while True: filtered_small = torch.as_tensor( - [area[i] <= 4 for i, c in enumerate(cur_classes)], dtype=torch.bool, device=keep.device + [area[i] <= 4 for i, c in enumerate(cur_classes)], + dtype=torch.bool, + device=keep.device, ) if filtered_small.any().item(): cur_scores = cur_scores[~filtered_small] @@ -359,7 +386,9 @@ def get_ids_area(masks, scores, dedup=False): segments_info = [] for i, a in enumerate(area): cat = cur_classes[i].item() - segments_info.append({"id": i, "isthing": self.is_thing_map[cat], "category_id": cat, "area": a}) + segments_info.append( + {"id": i, "isthing": self.is_thing_map[cat], "category_id": cat, "area": a} + ) del cur_classes with io.BytesIO() as out: diff --git a/dimos/models/Detic/third_party/Deformable-DETR/tools/launch.py b/dimos/models/Detic/third_party/Deformable-DETR/tools/launch.py index 2b3ceaa7bc..1d60ae4994 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/tools/launch.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/tools/launch.py @@ -103,14 +103,9 @@ how things can go wrong if you don't do this correctly. """ - -import sys -import subprocess +from argparse import REMAINDER, ArgumentParser import os -import socket -from argparse import ArgumentParser, REMAINDER - -import torch +import subprocess def parse_args(): @@ -118,41 +113,59 @@ def parse_args(): Helper function parsing the command line options @retval ArgumentParser """ - parser = ArgumentParser(description="PyTorch distributed training launch " - "helper utilty that will spawn up " - "multiple distributed processes") + parser = ArgumentParser( + description="PyTorch distributed training launch " + "helper utilty that will spawn up " + "multiple distributed processes" + ) # Optional arguments for the launch helper - parser.add_argument("--nnodes", type=int, default=1, - help="The number of nodes to use for distributed " - "training") - parser.add_argument("--node_rank", type=int, default=0, - help="The rank of the node for multi-node distributed " - "training") - parser.add_argument("--nproc_per_node", type=int, default=1, - help="The number of processes to launch on each node, " - "for GPU training, this is recommended to be set " - "to the number of GPUs in your system so that " - "each process can be bound to a single GPU.") - parser.add_argument("--master_addr", default="127.0.0.1", type=str, - help="Master node (rank 0)'s address, should be either " - "the IP address or the hostname of node 0, for " - "single node multi-proc training, the " - "--master_addr can simply be 127.0.0.1") - parser.add_argument("--master_port", default=29500, type=int, - help="Master node (rank 0)'s free port that needs to " - "be used for communciation during distributed " - "training") + parser.add_argument( + "--nnodes", type=int, default=1, help="The number of nodes to use for distributed training" + ) + parser.add_argument( + "--node_rank", + type=int, + default=0, + help="The rank of the node for multi-node distributed training", + ) + parser.add_argument( + "--nproc_per_node", + type=int, + default=1, + help="The number of processes to launch on each node, " + "for GPU training, this is recommended to be set " + "to the number of GPUs in your system so that " + "each process can be bound to a single GPU.", + ) + parser.add_argument( + "--master_addr", + default="127.0.0.1", + type=str, + help="Master node (rank 0)'s address, should be either " + "the IP address or the hostname of node 0, for " + "single node multi-proc training, the " + "--master_addr can simply be 127.0.0.1", + ) + parser.add_argument( + "--master_port", + default=29500, + type=int, + help="Master node (rank 0)'s free port that needs to be used for communciation during distributed training", + ) # positional - parser.add_argument("training_script", type=str, - help="The full path to the single GPU training " - "program/script to be launched in parallel, " - "followed by all the arguments for the " - "training script") + parser.add_argument( + "training_script", + type=str, + help="The full path to the single GPU training " + "program/script to be launched in parallel, " + "followed by all the arguments for the " + "training script", + ) # rest from the training program - parser.add_argument('training_script_args', nargs=REMAINDER) + parser.add_argument("training_script_args", nargs=REMAINDER) return parser.parse_args() @@ -176,7 +189,7 @@ def main(): current_env["RANK"] = str(dist_rank) current_env["LOCAL_RANK"] = str(local_rank) - cmd = [args.training_script] + args.training_script_args + cmd = [args.training_script, *args.training_script_args] process = subprocess.Popen(cmd, env=current_env) processes.append(process) @@ -184,9 +197,8 @@ def main(): for process in processes: process.wait() if process.returncode != 0: - raise subprocess.CalledProcessError(returncode=process.returncode, - cmd=process.args) + raise subprocess.CalledProcessError(returncode=process.returncode, cmd=process.args) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/dimos/models/Detic/third_party/Deformable-DETR/util/box_ops.py b/dimos/models/Detic/third_party/Deformable-DETR/util/box_ops.py index ca29592f80..5864b68d3b 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/util/box_ops.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/util/box_ops.py @@ -10,21 +10,20 @@ """ Utilities for bounding box manipulation and GIoU. """ + import torch from torchvision.ops.boxes import box_area def box_cxcywh_to_xyxy(x): x_c, y_c, w, h = x.unbind(-1) - b = [(x_c - 0.5 * w), (y_c - 0.5 * h), - (x_c + 0.5 * w), (y_c + 0.5 * h)] + b = [(x_c - 0.5 * w), (y_c - 0.5 * h), (x_c + 0.5 * w), (y_c + 0.5 * h)] return torch.stack(b, dim=-1) def box_xyxy_to_cxcywh(x): x0, y0, x1, y1 = x.unbind(-1) - b = [(x0 + x1) / 2, (y0 + y1) / 2, - (x1 - x0), (y1 - y0)] + b = [(x0 + x1) / 2, (y0 + y1) / 2, (x1 - x0), (y1 - y0)] return torch.stack(b, dim=-1) @@ -85,11 +84,11 @@ def masks_to_boxes(masks): x = torch.arange(0, w, dtype=torch.float) y, x = torch.meshgrid(y, x) - x_mask = (masks * x.unsqueeze(0)) + x_mask = masks * x.unsqueeze(0) x_max = x_mask.flatten(1).max(-1)[0] x_min = x_mask.masked_fill(~(masks.bool()), 1e8).flatten(1).min(-1)[0] - y_mask = (masks * y.unsqueeze(0)) + y_mask = masks * y.unsqueeze(0) y_max = y_mask.flatten(1).max(-1)[0] y_min = y_mask.masked_fill(~(masks.bool()), 1e8).flatten(1).min(-1)[0] diff --git a/dimos/models/Detic/third_party/Deformable-DETR/util/misc.py b/dimos/models/Detic/third_party/Deformable-DETR/util/misc.py index 6d4d076720..0615de5b5f 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/util/misc.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/util/misc.py @@ -12,25 +12,28 @@ Mostly copy-paste from torchvision references. """ -import os -import subprocess -import time + from collections import defaultdict, deque import datetime +import os import pickle -from typing import Optional, List +import subprocess +import time +from typing import List, Optional import torch -import torch.nn as nn -import torch.distributed as dist from torch import Tensor +import torch.distributed as dist # needed due to empty tensor bug in pytorch and torchvision 0.5 import torchvision + if float(torchvision.__version__[:3]) < 0.5: import math + from torchvision.ops.misc import _NewEmptyTensorOp - def _check_size_scale_factor(dim, size, scale_factor): + + def _check_size_scale_factor(dim: int, size: int, scale_factor): # type: (int, Optional[List[int]], Optional[float]) -> None if size is None and scale_factor is None: raise ValueError("either size or scale_factor should be defined") @@ -38,33 +41,31 @@ def _check_size_scale_factor(dim, size, scale_factor): raise ValueError("only one of size or scale_factor should be defined") if not (scale_factor is not None and len(scale_factor) != dim): raise ValueError( - "scale_factor shape must match input shape. " - "Input is {}D, scale_factor size is {}".format(dim, len(scale_factor)) + f"scale_factor shape must match input shape. Input is {dim}D, scale_factor size is {len(scale_factor)}" ) - def _output_size(dim, input, size, scale_factor): + + def _output_size(dim: int, input, size: int, scale_factor): # type: (int, Tensor, Optional[List[int]], Optional[float]) -> List[int] assert dim == 2 _check_size_scale_factor(dim, size, scale_factor) if size is not None: return size # if dim is not 2 or scale_factor is iterable use _ntuple instead of concat - assert scale_factor is not None and isinstance(scale_factor, (int, float)) + assert scale_factor is not None and isinstance(scale_factor, int | float) scale_factors = [scale_factor, scale_factor] # math.floor might return float in py2.7 - return [ - int(math.floor(input.size(i + 2) * scale_factors[i])) for i in range(dim) - ] + return [math.floor(input.size(i + 2) * scale_factors[i]) for i in range(dim)] elif float(torchvision.__version__[:3]) < 0.7: from torchvision.ops import _new_empty_tensor from torchvision.ops.misc import _output_size -class SmoothedValue(object): +class SmoothedValue: """Track a series of values and provide access to smoothed values over a window or the global series average. """ - def __init__(self, window_size=20, fmt=None): + def __init__(self, window_size: int=20, fmt=None) -> None: if fmt is None: fmt = "{median:.4f} ({global_avg:.4f})" self.deque = deque(maxlen=window_size) @@ -72,18 +73,18 @@ def __init__(self, window_size=20, fmt=None): self.count = 0 self.fmt = fmt - def update(self, value, n=1): + def update(self, value, n: int=1) -> None: self.deque.append(value) self.count += n self.total += value * n - def synchronize_between_processes(self): + def synchronize_between_processes(self) -> None: """ Warning: does not synchronize the deque! """ if not is_dist_avail_and_initialized(): return - t = torch.tensor([self.count, self.total], dtype=torch.float64, device='cuda') + t = torch.tensor([self.count, self.total], dtype=torch.float64, device="cuda") dist.barrier() dist.all_reduce(t) t = t.tolist() @@ -112,13 +113,14 @@ def max(self): def value(self): return self.deque[-1] - def __str__(self): + def __str__(self) -> str: return self.fmt.format( median=self.median, avg=self.avg, global_avg=self.global_avg, max=self.max, - value=self.value) + value=self.value, + ) def all_gather(data): @@ -157,14 +159,14 @@ def all_gather(data): dist.all_gather(tensor_list, tensor) data_list = [] - for size, tensor in zip(size_list, tensor_list): + for size, tensor in zip(size_list, tensor_list, strict=False): buffer = tensor.cpu().numpy().tobytes()[:size] data_list.append(pickle.loads(buffer)) return data_list -def reduce_dict(input_dict, average=True): +def reduce_dict(input_dict, average: bool=True): """ Args: input_dict (dict): all the values will be reduced @@ -187,20 +189,20 @@ def reduce_dict(input_dict, average=True): dist.all_reduce(values) if average: values /= world_size - reduced_dict = {k: v for k, v in zip(names, values)} + reduced_dict = {k: v for k, v in zip(names, values, strict=False)} return reduced_dict -class MetricLogger(object): - def __init__(self, delimiter="\t"): +class MetricLogger: + def __init__(self, delimiter: str="\t") -> None: self.meters = defaultdict(SmoothedValue) self.delimiter = delimiter - def update(self, **kwargs): + def update(self, **kwargs) -> None: for k, v in kwargs.items(): if isinstance(v, torch.Tensor): v = v.item() - assert isinstance(v, (float, int)) + assert isinstance(v, float | int) self.meters[k].update(v) def __getattr__(self, attr): @@ -208,52 +210,53 @@ def __getattr__(self, attr): return self.meters[attr] if attr in self.__dict__: return self.__dict__[attr] - raise AttributeError("'{}' object has no attribute '{}'".format( - type(self).__name__, attr)) + raise AttributeError(f"'{type(self).__name__}' object has no attribute '{attr}'") - def __str__(self): + def __str__(self) -> str: loss_str = [] for name, meter in self.meters.items(): - loss_str.append( - "{}: {}".format(name, str(meter)) - ) + loss_str.append(f"{name}: {meter!s}") return self.delimiter.join(loss_str) - def synchronize_between_processes(self): + def synchronize_between_processes(self) -> None: for meter in self.meters.values(): meter.synchronize_between_processes() - def add_meter(self, name, meter): + def add_meter(self, name: str, meter) -> None: self.meters[name] = meter def log_every(self, iterable, print_freq, header=None): i = 0 if not header: - header = '' + header = "" start_time = time.time() end = time.time() - iter_time = SmoothedValue(fmt='{avg:.4f}') - data_time = SmoothedValue(fmt='{avg:.4f}') - space_fmt = ':' + str(len(str(len(iterable)))) + 'd' + iter_time = SmoothedValue(fmt="{avg:.4f}") + data_time = SmoothedValue(fmt="{avg:.4f}") + space_fmt = ":" + str(len(str(len(iterable)))) + "d" if torch.cuda.is_available(): - log_msg = self.delimiter.join([ - header, - '[{0' + space_fmt + '}/{1}]', - 'eta: {eta}', - '{meters}', - 'time: {time}', - 'data: {data}', - 'max mem: {memory:.0f}' - ]) + log_msg = self.delimiter.join( + [ + header, + "[{0" + space_fmt + "}/{1}]", + "eta: {eta}", + "{meters}", + "time: {time}", + "data: {data}", + "max mem: {memory:.0f}", + ] + ) else: - log_msg = self.delimiter.join([ - header, - '[{0' + space_fmt + '}/{1}]', - 'eta: {eta}', - '{meters}', - 'time: {time}', - 'data: {data}' - ]) + log_msg = self.delimiter.join( + [ + header, + "[{0" + space_fmt + "}/{1}]", + "eta: {eta}", + "{meters}", + "time: {time}", + "data: {data}", + ] + ) MB = 1024.0 * 1024.0 for obj in iterable: data_time.update(time.time() - end) @@ -263,38 +266,52 @@ def log_every(self, iterable, print_freq, header=None): eta_seconds = iter_time.global_avg * (len(iterable) - i) eta_string = str(datetime.timedelta(seconds=int(eta_seconds))) if torch.cuda.is_available(): - print(log_msg.format( - i, len(iterable), eta=eta_string, - meters=str(self), - time=str(iter_time), data=str(data_time), - memory=torch.cuda.max_memory_allocated() / MB)) + print( + log_msg.format( + i, + len(iterable), + eta=eta_string, + meters=str(self), + time=str(iter_time), + data=str(data_time), + memory=torch.cuda.max_memory_allocated() / MB, + ) + ) else: - print(log_msg.format( - i, len(iterable), eta=eta_string, - meters=str(self), - time=str(iter_time), data=str(data_time))) + print( + log_msg.format( + i, + len(iterable), + eta=eta_string, + meters=str(self), + time=str(iter_time), + data=str(data_time), + ) + ) i += 1 end = time.time() total_time = time.time() - start_time total_time_str = str(datetime.timedelta(seconds=int(total_time))) - print('{} Total time: {} ({:.4f} s / it)'.format( - header, total_time_str, total_time / len(iterable))) + print( + f"{header} Total time: {total_time_str} ({total_time / len(iterable):.4f} s / it)" + ) def get_sha(): cwd = os.path.dirname(os.path.abspath(__file__)) def _run(command): - return subprocess.check_output(command, cwd=cwd).decode('ascii').strip() - sha = 'N/A' + return subprocess.check_output(command, cwd=cwd).decode("ascii").strip() + + sha = "N/A" diff = "clean" - branch = 'N/A' + branch = "N/A" try: - sha = _run(['git', 'rev-parse', 'HEAD']) - subprocess.check_output(['git', 'diff'], cwd=cwd) - diff = _run(['git', 'diff-index', 'HEAD']) + sha = _run(["git", "rev-parse", "HEAD"]) + subprocess.check_output(["git", "diff"], cwd=cwd) + diff = _run(["git", "diff-index", "HEAD"]) diff = "has uncommited changes" if diff else "clean" - branch = _run(['git', 'rev-parse', '--abbrev-ref', 'HEAD']) + branch = _run(["git", "rev-parse", "--abbrev-ref", "HEAD"]) except Exception: pass message = f"sha: {sha}, status: {diff}, branch: {branch}" @@ -302,7 +319,7 @@ def _run(command): def collate_fn(batch): - batch = list(zip(*batch)) + batch = list(zip(*batch, strict=False)) batch[0] = nested_tensor_from_tensor_list(batch[0]) return tuple(batch) @@ -316,33 +333,33 @@ def _max_by_axis(the_list): return maxes -def nested_tensor_from_tensor_list(tensor_list: List[Tensor]): +def nested_tensor_from_tensor_list(tensor_list: list[Tensor]): # TODO make this more general if tensor_list[0].ndim == 3: # TODO make it support different-sized images max_size = _max_by_axis([list(img.shape) for img in tensor_list]) # min_size = tuple(min(s) for s in zip(*[img.shape for img in tensor_list])) - batch_shape = [len(tensor_list)] + max_size + batch_shape = [len(tensor_list), *max_size] b, c, h, w = batch_shape dtype = tensor_list[0].dtype device = tensor_list[0].device tensor = torch.zeros(batch_shape, dtype=dtype, device=device) mask = torch.ones((b, h, w), dtype=torch.bool, device=device) - for img, pad_img, m in zip(tensor_list, tensor, mask): + for img, pad_img, m in zip(tensor_list, tensor, mask, strict=False): pad_img[: img.shape[0], : img.shape[1], : img.shape[2]].copy_(img) - m[: img.shape[1], :img.shape[2]] = False + m[: img.shape[1], : img.shape[2]] = False else: - raise ValueError('not supported') + raise ValueError("not supported") return NestedTensor(tensor, mask) -class NestedTensor(object): - def __init__(self, tensors, mask: Optional[Tensor]): +class NestedTensor: + def __init__(self, tensors, mask: Tensor | None) -> None: self.tensors = tensors self.mask = mask - def to(self, device, non_blocking=False): - # type: (Device) -> NestedTensor # noqa + def to(self, device, non_blocking: bool=False): + # type: (Device) -> NestedTensor cast_tensor = self.tensors.to(device, non_blocking=non_blocking) mask = self.mask if mask is not None: @@ -352,7 +369,7 @@ def to(self, device, non_blocking=False): cast_mask = None return NestedTensor(cast_tensor, cast_mask) - def record_stream(self, *args, **kwargs): + def record_stream(self, *args, **kwargs) -> None: self.tensors.record_stream(*args, **kwargs) if self.mask is not None: self.mask.record_stream(*args, **kwargs) @@ -360,26 +377,27 @@ def record_stream(self, *args, **kwargs): def decompose(self): return self.tensors, self.mask - def __repr__(self): + def __repr__(self) -> str: return str(self.tensors) -def setup_for_distributed(is_master): +def setup_for_distributed(is_master: bool) -> None: """ This function disables printing when not in master process """ import builtins as __builtin__ + builtin_print = __builtin__.print - def print(*args, **kwargs): - force = kwargs.pop('force', False) + def print(*args, **kwargs) -> None: + force = kwargs.pop("force", False) if is_master or force: builtin_print(*args, **kwargs) __builtin__.print = print -def is_dist_avail_and_initialized(): +def is_dist_avail_and_initialized() -> bool: if not dist.is_available(): return False if not dist.is_initialized(): @@ -402,61 +420,63 @@ def get_rank(): def get_local_size(): if not is_dist_avail_and_initialized(): return 1 - return int(os.environ['LOCAL_SIZE']) + return int(os.environ["LOCAL_SIZE"]) def get_local_rank(): if not is_dist_avail_and_initialized(): return 0 - return int(os.environ['LOCAL_RANK']) + return int(os.environ["LOCAL_RANK"]) def is_main_process(): return get_rank() == 0 -def save_on_master(*args, **kwargs): +def save_on_master(*args, **kwargs) -> None: if is_main_process(): torch.save(*args, **kwargs) -def init_distributed_mode(args): - if 'RANK' in os.environ and 'WORLD_SIZE' in os.environ: +def init_distributed_mode(args) -> None: + if "RANK" in os.environ and "WORLD_SIZE" in os.environ: args.rank = int(os.environ["RANK"]) - args.world_size = int(os.environ['WORLD_SIZE']) - args.gpu = int(os.environ['LOCAL_RANK']) - args.dist_url = 'env://' - os.environ['LOCAL_SIZE'] = str(torch.cuda.device_count()) - elif 'SLURM_PROCID' in os.environ: - proc_id = int(os.environ['SLURM_PROCID']) - ntasks = int(os.environ['SLURM_NTASKS']) - node_list = os.environ['SLURM_NODELIST'] + args.world_size = int(os.environ["WORLD_SIZE"]) + args.gpu = int(os.environ["LOCAL_RANK"]) + args.dist_url = "env://" + os.environ["LOCAL_SIZE"] = str(torch.cuda.device_count()) + elif "SLURM_PROCID" in os.environ: + proc_id = int(os.environ["SLURM_PROCID"]) + ntasks = int(os.environ["SLURM_NTASKS"]) + node_list = os.environ["SLURM_NODELIST"] num_gpus = torch.cuda.device_count() - addr = subprocess.getoutput( - 'scontrol show hostname {} | head -n1'.format(node_list)) - os.environ['MASTER_PORT'] = os.environ.get('MASTER_PORT', '29500') - os.environ['MASTER_ADDR'] = addr - os.environ['WORLD_SIZE'] = str(ntasks) - os.environ['RANK'] = str(proc_id) - os.environ['LOCAL_RANK'] = str(proc_id % num_gpus) - os.environ['LOCAL_SIZE'] = str(num_gpus) - args.dist_url = 'env://' + addr = subprocess.getoutput(f"scontrol show hostname {node_list} | head -n1") + os.environ["MASTER_PORT"] = os.environ.get("MASTER_PORT", "29500") + os.environ["MASTER_ADDR"] = addr + os.environ["WORLD_SIZE"] = str(ntasks) + os.environ["RANK"] = str(proc_id) + os.environ["LOCAL_RANK"] = str(proc_id % num_gpus) + os.environ["LOCAL_SIZE"] = str(num_gpus) + args.dist_url = "env://" args.world_size = ntasks args.rank = proc_id args.gpu = proc_id % num_gpus else: - print('Not using distributed mode') + print("Not using distributed mode") args.distributed = False return args.distributed = True torch.cuda.set_device(args.gpu) - args.dist_backend = 'nccl' - print('| distributed init (rank {}): {}'.format( - args.rank, args.dist_url), flush=True) - torch.distributed.init_process_group(backend=args.dist_backend, init_method=args.dist_url, - world_size=args.world_size, rank=args.rank) + args.dist_backend = "nccl" + print(f"| distributed init (rank {args.rank}): {args.dist_url}", flush=True) + torch.distributed.init_process_group( + backend=args.dist_backend, + init_method=args.dist_url, + world_size=args.world_size, + rank=args.rank, + ) torch.distributed.barrier() setup_for_distributed(args.rank == 0) @@ -480,7 +500,7 @@ def accuracy(output, target, topk=(1,)): return res -def interpolate(input, size=None, scale_factor=None, mode="nearest", align_corners=None): +def interpolate(input, size: Optional[int]=None, scale_factor=None, mode: str="nearest", align_corners=None): # type: (Tensor, Optional[List[int]], Optional[float], str, Optional[bool]) -> Tensor """ Equivalent to nn.functional.interpolate, but with support for empty batch sizes. @@ -489,9 +509,7 @@ class can go away. """ if float(torchvision.__version__[:3]) < 0.7: if input.numel() > 0: - return torch.nn.functional.interpolate( - input, size, scale_factor, mode, align_corners - ) + return torch.nn.functional.interpolate(input, size, scale_factor, mode, align_corners) output_shape = _output_size(2, input, size, scale_factor) output_shape = list(input.shape[:-2]) + list(output_shape) @@ -502,17 +520,19 @@ class can go away. return torchvision.ops.misc.interpolate(input, size, scale_factor, mode, align_corners) -def get_total_grad_norm(parameters, norm_type=2): +def get_total_grad_norm(parameters, norm_type: int=2): parameters = list(filter(lambda p: p.grad is not None, parameters)) norm_type = float(norm_type) device = parameters[0].grad.device - total_norm = torch.norm(torch.stack([torch.norm(p.grad.detach(), norm_type).to(device) for p in parameters]), - norm_type) + total_norm = torch.norm( + torch.stack([torch.norm(p.grad.detach(), norm_type).to(device) for p in parameters]), + norm_type, + ) return total_norm -def inverse_sigmoid(x, eps=1e-5): + +def inverse_sigmoid(x, eps: float=1e-5): x = x.clamp(min=0, max=1) x1 = x.clamp(min=eps) x2 = (1 - x).clamp(min=eps) - return torch.log(x1/x2) - + return torch.log(x1 / x2) diff --git a/dimos/models/Detic/third_party/Deformable-DETR/util/plot_utils.py b/dimos/models/Detic/third_party/Deformable-DETR/util/plot_utils.py index 759f34d252..0af3b9e5e6 100644 --- a/dimos/models/Detic/third_party/Deformable-DETR/util/plot_utils.py +++ b/dimos/models/Detic/third_party/Deformable-DETR/util/plot_utils.py @@ -10,16 +10,19 @@ """ Plotting utilities to visualize training logs. """ -import torch -import pandas as pd -import seaborn as sns -import matplotlib.pyplot as plt from pathlib import Path, PurePath +import matplotlib.pyplot as plt +import pandas as pd +import seaborn as sns +import torch + -def plot_logs(logs, fields=('class_error', 'loss_bbox_unscaled', 'mAP'), ewm_col=0, log_name='log.txt'): - ''' +def plot_logs( + logs, fields=("class_error", "loss_bbox_unscaled", "mAP"), ewm_col: int=0, log_name: str="log.txt" +): + """ Function to plot specific fields from training log(s). Plots both training and test results. :: Inputs - logs = list containing Path objects, each pointing to individual dir with a log file @@ -30,7 +33,7 @@ def plot_logs(logs, fields=('class_error', 'loss_bbox_unscaled', 'mAP'), ewm_col :: Outputs - matplotlib plots of results in fields, color coded for each log file. - solid lines are training results, dashed lines are test results. - ''' + """ func_name = "plot_utils.py::plot_logs" # verify logs is a list of Paths (list[Paths]) or single Pathlib object Path, @@ -41,13 +44,17 @@ def plot_logs(logs, fields=('class_error', 'loss_bbox_unscaled', 'mAP'), ewm_col logs = [logs] print(f"{func_name} info: logs param expects a list argument, converted to list[Path].") else: - raise ValueError(f"{func_name} - invalid argument for logs parameter.\n \ - Expect list[Path] or single Path obj, received {type(logs)}") + raise ValueError( + f"{func_name} - invalid argument for logs parameter.\n \ + Expect list[Path] or single Path obj, received {type(logs)}" + ) # verify valid dir(s) and that every item in list is Path object - for i, dir in enumerate(logs): + for _i, dir in enumerate(logs): if not isinstance(dir, PurePath): - raise ValueError(f"{func_name} - non-Path object in logs argument of {type(dir)}: \n{dir}") + raise ValueError( + f"{func_name} - non-Path object in logs argument of {type(dir)}: \n{dir}" + ) if dir.exists(): continue raise ValueError(f"{func_name} - invalid directory in logs argument:\n{dir}") @@ -57,55 +64,57 @@ def plot_logs(logs, fields=('class_error', 'loss_bbox_unscaled', 'mAP'), ewm_col fig, axs = plt.subplots(ncols=len(fields), figsize=(16, 5)) - for df, color in zip(dfs, sns.color_palette(n_colors=len(logs))): + for df, color in zip(dfs, sns.color_palette(n_colors=len(logs)), strict=False): for j, field in enumerate(fields): - if field == 'mAP': - coco_eval = pd.DataFrame(pd.np.stack(df.test_coco_eval.dropna().values)[:, 1]).ewm(com=ewm_col).mean() + if field == "mAP": + coco_eval = ( + pd.DataFrame(pd.np.stack(df.test_coco_eval.dropna().values)[:, 1]) + .ewm(com=ewm_col) + .mean() + ) axs[j].plot(coco_eval, c=color) else: df.interpolate().ewm(com=ewm_col).mean().plot( - y=[f'train_{field}', f'test_{field}'], + y=[f"train_{field}", f"test_{field}"], ax=axs[j], color=[color] * 2, - style=['-', '--'] + style=["-", "--"], ) - for ax, field in zip(axs, fields): + for ax, field in zip(axs, fields, strict=False): ax.legend([Path(p).name for p in logs]) ax.set_title(field) -def plot_precision_recall(files, naming_scheme='iter'): - if naming_scheme == 'exp_id': +def plot_precision_recall(files, naming_scheme: str="iter"): + if naming_scheme == "exp_id": # name becomes exp_id names = [f.parts[-3] for f in files] - elif naming_scheme == 'iter': + elif naming_scheme == "iter": names = [f.stem for f in files] else: - raise ValueError(f'not supported {naming_scheme}') + raise ValueError(f"not supported {naming_scheme}") fig, axs = plt.subplots(ncols=2, figsize=(16, 5)) - for f, color, name in zip(files, sns.color_palette("Blues", n_colors=len(files)), names): + for f, color, name in zip(files, sns.color_palette("Blues", n_colors=len(files)), names, strict=False): data = torch.load(f) # precision is n_iou, n_points, n_cat, n_area, max_det - precision = data['precision'] - recall = data['params'].recThrs - scores = data['scores'] + precision = data["precision"] + recall = data["params"].recThrs + scores = data["scores"] # take precision for all classes, all areas and 100 detections precision = precision[0, :, :, 0, -1].mean(1) scores = scores[0, :, :, 0, -1].mean(1) prec = precision.mean() - rec = data['recall'][0, :, 0, -1].mean() - print(f'{naming_scheme} {name}: mAP@50={prec * 100: 05.1f}, ' + - f'score={scores.mean():0.3f}, ' + - f'f1={2 * prec * rec / (prec + rec + 1e-8):0.3f}' - ) + rec = data["recall"][0, :, 0, -1].mean() + print( + f"{naming_scheme} {name}: mAP@50={prec * 100: 05.1f}, " + + f"score={scores.mean():0.3f}, " + + f"f1={2 * prec * rec / (prec + rec + 1e-8):0.3f}" + ) axs[0].plot(recall, precision, c=color) axs[1].plot(recall, scores, c=color) - axs[0].set_title('Precision / Recall') + axs[0].set_title("Precision / Recall") axs[0].legend(names) - axs[1].set_title('Scores / Recall') + axs[1].set_title("Scores / Recall") axs[1].legend(names) return fig, axs - - - diff --git a/dimos/models/Detic/tools/convert-thirdparty-pretrained-model-to-d2.py b/dimos/models/Detic/tools/convert-thirdparty-pretrained-model-to-d2.py index ec042b8ce4..567e71f7c4 100644 --- a/dimos/models/Detic/tools/convert-thirdparty-pretrained-model-to-d2.py +++ b/dimos/models/Detic/tools/convert-thirdparty-pretrained-model-to-d2.py @@ -1,7 +1,8 @@ #!/usr/bin/env python # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved import argparse -import pickle +import pickle + import torch """ @@ -19,21 +20,17 @@ if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument('--path', default='') + parser.add_argument("--path", default="") args = parser.parse_args() - print('Loading', args.path) + print("Loading", args.path) model = torch.load(args.path, map_location="cpu") # import pdb; pdb.set_trace() - if 'model' in model: - model = model['model'] - if 'state_dict' in model: - model = model['state_dict'] - ret = { - "model": model, - "__author__": "third_party", - "matching_heuristics": True - } - out_path = args.path.replace('.pth', '.pkl') - print('Saving to', out_path) + if "model" in model: + model = model["model"] + if "state_dict" in model: + model = model["state_dict"] + ret = {"model": model, "__author__": "third_party", "matching_heuristics": True} + out_path = args.path.replace(".pth", ".pkl") + print("Saving to", out_path) pickle.dump(ret, open(out_path, "wb")) diff --git a/dimos/models/Detic/tools/create_imagenetlvis_json.py b/dimos/models/Detic/tools/create_imagenetlvis_json.py index 8f44f6221f..4f53874421 100644 --- a/dimos/models/Detic/tools/create_imagenetlvis_json.py +++ b/dimos/models/Detic/tools/create_imagenetlvis_json.py @@ -2,21 +2,23 @@ import argparse import json import os -import cv2 -from nltk.corpus import wordnet + from detectron2.data.detection_utils import read_image +from nltk.corpus import wordnet -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument('--imagenet_path', default='datasets/imagenet/ImageNet-LVIS') - parser.add_argument('--lvis_meta_path', default='datasets/lvis/lvis_v1_val.json') - parser.add_argument('--out_path', default='datasets/imagenet/annotations/imagenet_lvis_image_info.json') + parser.add_argument("--imagenet_path", default="datasets/imagenet/ImageNet-LVIS") + parser.add_argument("--lvis_meta_path", default="datasets/lvis/lvis_v1_val.json") + parser.add_argument( + "--out_path", default="datasets/imagenet/annotations/imagenet_lvis_image_info.json" + ) args = parser.parse_args() - print('Loading LVIS meta') - data = json.load(open(args.lvis_meta_path, 'r')) - print('Done') - synset2cat = {x['synset']: x for x in data['categories']} + print("Loading LVIS meta") + data = json.load(open(args.lvis_meta_path)) + print("Done") + synset2cat = {x["synset"]: x for x in data["categories"]} count = 0 images = [] image_counts = {} @@ -24,31 +26,31 @@ for i, folder in enumerate(folders): class_path = args.imagenet_path + folder files = sorted(os.listdir(class_path)) - synset = wordnet.synset_from_pos_and_offset('n', int(folder[1:])).name() + synset = wordnet.synset_from_pos_and_offset("n", int(folder[1:])).name() cat = synset2cat[synset] - cat_id = cat['id'] - cat_name = cat['name'] + cat_id = cat["id"] + cat_name = cat["name"] cat_images = [] for file in files: count = count + 1 - file_name = '{}/{}'.format(folder, file) + file_name = f"{folder}/{file}" # img = cv2.imread('{}/{}'.format(args.imagenet_path, file_name)) - img = read_image('{}/{}'.format(args.imagenet_path, file_name)) + img = read_image(f"{args.imagenet_path}/{file_name}") h, w = img.shape[:2] image = { - 'id': count, - 'file_name': file_name, - 'pos_category_ids': [cat_id], - 'width': w, - 'height': h + "id": count, + "file_name": file_name, + "pos_category_ids": [cat_id], + "width": w, + "height": h, } cat_images.append(image) images.extend(cat_images) image_counts[cat_id] = len(cat_images) print(i, cat_name, len(cat_images)) - print('# Images', len(images)) - for x in data['categories']: - x['image_count'] = image_counts[x['id']] if x['id'] in image_counts else 0 - out = {'categories': data['categories'], 'images': images, 'annotations': []} - print('Writing to', args.out_path) - json.dump(out, open(args.out_path, 'w')) + print("# Images", len(images)) + for x in data["categories"]: + x["image_count"] = image_counts[x["id"]] if x["id"] in image_counts else 0 + out = {"categories": data["categories"], "images": images, "annotations": []} + print("Writing to", args.out_path) + json.dump(out, open(args.out_path, "w")) diff --git a/dimos/models/Detic/tools/create_lvis_21k.py b/dimos/models/Detic/tools/create_lvis_21k.py index 3e6fe60a2d..a1f24446ac 100644 --- a/dimos/models/Detic/tools/create_lvis_21k.py +++ b/dimos/models/Detic/tools/create_lvis_21k.py @@ -3,72 +3,73 @@ import copy import json -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument('--imagenet_path', default='datasets/imagenet/annotations/imagenet-21k_image_info.json') - parser.add_argument('--lvis_path', default='datasets/lvis/lvis_v1_train.json') - parser.add_argument('--save_categories', default='') - parser.add_argument('--not_save_imagenet', action='store_true') - parser.add_argument('--not_save_lvis', action='store_true') - parser.add_argument('--mark', default='lvis-21k') + parser.add_argument( + "--imagenet_path", default="datasets/imagenet/annotations/imagenet-21k_image_info.json" + ) + parser.add_argument("--lvis_path", default="datasets/lvis/lvis_v1_train.json") + parser.add_argument("--save_categories", default="") + parser.add_argument("--not_save_imagenet", action="store_true") + parser.add_argument("--not_save_lvis", action="store_true") + parser.add_argument("--mark", default="lvis-21k") args = parser.parse_args() - print('Loading', args.imagenet_path) - in_data = json.load(open(args.imagenet_path, 'r')) - print('Loading', args.lvis_path) - lvis_data = json.load(open(args.lvis_path, 'r')) + print("Loading", args.imagenet_path) + in_data = json.load(open(args.imagenet_path)) + print("Loading", args.lvis_path) + lvis_data = json.load(open(args.lvis_path)) - categories = copy.deepcopy(lvis_data['categories']) - cat_count = max(x['id'] for x in categories) - synset2id = {x['synset']: x['id'] for x in categories} - name2id = {x['name']: x['id'] for x in categories} + categories = copy.deepcopy(lvis_data["categories"]) + cat_count = max(x["id"] for x in categories) + synset2id = {x["synset"]: x["id"] for x in categories} + name2id = {x["name"]: x["id"] for x in categories} in_id_map = {} - for x in in_data['categories']: - if x['synset'] in synset2id: - in_id_map[x['id']] = synset2id[x['synset']] - elif x['name'] in name2id: - in_id_map[x['id']] = name2id[x['name']] - x['id'] = name2id[x['name']] + for x in in_data["categories"]: + if x["synset"] in synset2id: + in_id_map[x["id"]] = synset2id[x["synset"]] + elif x["name"] in name2id: + in_id_map[x["id"]] = name2id[x["name"]] + x["id"] = name2id[x["name"]] else: cat_count = cat_count + 1 - name2id[x['name']] = cat_count - in_id_map[x['id']] = cat_count - x['id'] = cat_count + name2id[x["name"]] = cat_count + in_id_map[x["id"]] = cat_count + x["id"] = cat_count categories.append(x) - - print('lvis cats', len(lvis_data['categories'])) - print('imagenet cats', len(in_data['categories'])) - print('merge cats', len(categories)) + + print("lvis cats", len(lvis_data["categories"])) + print("imagenet cats", len(in_data["categories"])) + print("merge cats", len(categories)) filtered_images = [] - for x in in_data['images']: - x['pos_category_ids'] = [in_id_map[xx] for xx in x['pos_category_ids']] - x['pos_category_ids'] = [xx for xx in \ - sorted(set(x['pos_category_ids'])) if xx >= 0] - if len(x['pos_category_ids']) > 0: + for x in in_data["images"]: + x["pos_category_ids"] = [in_id_map[xx] for xx in x["pos_category_ids"]] + x["pos_category_ids"] = [xx for xx in sorted(set(x["pos_category_ids"])) if xx >= 0] + if len(x["pos_category_ids"]) > 0: filtered_images.append(x) - in_data['categories'] = categories - lvis_data['categories'] = categories + in_data["categories"] = categories + lvis_data["categories"] = categories if not args.not_save_imagenet: - in_out_path = args.imagenet_path[:-5] + '_{}.json'.format(args.mark) + in_out_path = args.imagenet_path[:-5] + f"_{args.mark}.json" for k, v in in_data.items(): - print('imagenet', k, len(v)) - print('Saving Imagenet to', in_out_path) - json.dump(in_data, open(in_out_path, 'w')) - + print("imagenet", k, len(v)) + print("Saving Imagenet to", in_out_path) + json.dump(in_data, open(in_out_path, "w")) + if not args.not_save_lvis: - lvis_out_path = args.lvis_path[:-5] + '_{}.json'.format(args.mark) + lvis_out_path = args.lvis_path[:-5] + f"_{args.mark}.json" for k, v in lvis_data.items(): - print('lvis', k, len(v)) - print('Saving LVIS to', lvis_out_path) - json.dump(lvis_data, open(lvis_out_path, 'w')) + print("lvis", k, len(v)) + print("Saving LVIS to", lvis_out_path) + json.dump(lvis_data, open(lvis_out_path, "w")) - if args.save_categories != '': + if args.save_categories != "": for x in categories: - for k in ['image_count', 'instance_count', 'synonyms', 'def']: + for k in ["image_count", "instance_count", "synonyms", "def"]: if k in x: del x[k] CATEGORIES = repr(categories) + " # noqa" - open(args.save_categories, 'wt').write(f"CATEGORIES = {CATEGORIES}") + open(args.save_categories, "w").write(f"CATEGORIES = {CATEGORIES}") diff --git a/dimos/models/Detic/tools/download_cc.py b/dimos/models/Detic/tools/download_cc.py index 3c43690a3c..ef7b4b0f7d 100644 --- a/dimos/models/Detic/tools/download_cc.py +++ b/dimos/models/Detic/tools/download_cc.py @@ -1,47 +1,45 @@ # Copyright (c) Facebook, Inc. and its affiliates. -import os -import json import argparse -from PIL import Image +import json +import os + import numpy as np +from PIL import Image -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument('--ann', default='datasets/cc3m/Train_GCC-training.tsv') - parser.add_argument('--save_image_path', default='datasets/cc3m/training/') - parser.add_argument('--cat_info', default='datasets/lvis/lvis_v1_val.json') - parser.add_argument('--out_path', default='datasets/cc3m/train_image_info.json') - parser.add_argument('--not_download_image', action='store_true') + parser.add_argument("--ann", default="datasets/cc3m/Train_GCC-training.tsv") + parser.add_argument("--save_image_path", default="datasets/cc3m/training/") + parser.add_argument("--cat_info", default="datasets/lvis/lvis_v1_val.json") + parser.add_argument("--out_path", default="datasets/cc3m/train_image_info.json") + parser.add_argument("--not_download_image", action="store_true") args = parser.parse_args() - categories = json.load(open(args.cat_info, 'r'))['categories'] + categories = json.load(open(args.cat_info))["categories"] images = [] if not os.path.exists(args.save_image_path): os.makedirs(args.save_image_path) f = open(args.ann) for i, line in enumerate(f): - cap, path = line[:-1].split('\t') + cap, path = line[:-1].split("\t") print(i, cap, path) if not args.not_download_image: - os.system( - 'wget {} -O {}/{}.jpg'.format( - path, args.save_image_path, i + 1)) + os.system(f"wget {path} -O {args.save_image_path}/{i + 1}.jpg") try: - img = Image.open( - open('{}/{}.jpg'.format(args.save_image_path, i + 1), "rb")) + img = Image.open(open(f"{args.save_image_path}/{i + 1}.jpg", "rb")) img = np.asarray(img.convert("RGB")) h, w = img.shape[:2] except: continue image_info = { - 'id': i + 1, - 'file_name': '{}.jpg'.format(i + 1), - 'height': h, - 'width': w, - 'captions': [cap], + "id": i + 1, + "file_name": f"{i + 1}.jpg", + "height": h, + "width": w, + "captions": [cap], } images.append(image_info) - data = {'categories': categories, 'images': images, 'annotations': []} + data = {"categories": categories, "images": images, "annotations": []} for k, v in data.items(): print(k, len(v)) - print('Saving to', args.out_path) - json.dump(data, open(args.out_path, 'w')) + print("Saving to", args.out_path) + json.dump(data, open(args.out_path, "w")) diff --git a/dimos/models/Detic/tools/dump_clip_features.py b/dimos/models/Detic/tools/dump_clip_features.py index 127f8c2a86..31be161f6d 100644 --- a/dimos/models/Detic/tools/dump_clip_features.py +++ b/dimos/models/Detic/tools/dump_clip_features.py @@ -1,100 +1,104 @@ # Copyright (c) Facebook, Inc. and its affiliates. import argparse -import json -import torch -import numpy as np import itertools +import json + from nltk.corpus import wordnet -import sys +import numpy as np +import torch -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument('--ann', default='datasets/lvis/lvis_v1_val.json') - parser.add_argument('--out_path', default='') - parser.add_argument('--prompt', default='a') - parser.add_argument('--model', default='clip') - parser.add_argument('--clip_model', default="ViT-B/32") - parser.add_argument('--fix_space', action='store_true') - parser.add_argument('--use_underscore', action='store_true') - parser.add_argument('--avg_synonyms', action='store_true') - parser.add_argument('--use_wn_name', action='store_true') + parser.add_argument("--ann", default="datasets/lvis/lvis_v1_val.json") + parser.add_argument("--out_path", default="") + parser.add_argument("--prompt", default="a") + parser.add_argument("--model", default="clip") + parser.add_argument("--clip_model", default="ViT-B/32") + parser.add_argument("--fix_space", action="store_true") + parser.add_argument("--use_underscore", action="store_true") + parser.add_argument("--avg_synonyms", action="store_true") + parser.add_argument("--use_wn_name", action="store_true") args = parser.parse_args() - print('Loading', args.ann) - data = json.load(open(args.ann, 'r')) - cat_names = [x['name'] for x in \ - sorted(data['categories'], key=lambda x: x['id'])] - if 'synonyms' in data['categories'][0]: + print("Loading", args.ann) + data = json.load(open(args.ann)) + cat_names = [x["name"] for x in sorted(data["categories"], key=lambda x: x["id"])] + if "synonyms" in data["categories"][0]: if args.use_wn_name: synonyms = [ - [xx.name() for xx in wordnet.synset(x['synset']).lemmas()] \ - if x['synset'] != 'stop_sign.n.01' else ['stop_sign'] \ - for x in sorted(data['categories'], key=lambda x: x['id'])] + [xx.name() for xx in wordnet.synset(x["synset"]).lemmas()] + if x["synset"] != "stop_sign.n.01" + else ["stop_sign"] + for x in sorted(data["categories"], key=lambda x: x["id"]) + ] else: - synonyms = [x['synonyms'] for x in \ - sorted(data['categories'], key=lambda x: x['id'])] + synonyms = [x["synonyms"] for x in sorted(data["categories"], key=lambda x: x["id"])] else: synonyms = [] if args.fix_space: - cat_names = [x.replace('_', ' ') for x in cat_names] + cat_names = [x.replace("_", " ") for x in cat_names] if args.use_underscore: - cat_names = [x.strip().replace('/ ', '/').replace(' ', '_') for x in cat_names] - print('cat_names', cat_names) + cat_names = [x.strip().replace("/ ", "/").replace(" ", "_") for x in cat_names] + print("cat_names", cat_names) device = "cuda" if torch.cuda.is_available() else "cpu" - - if args.prompt == 'a': - sentences = ['a ' + x for x in cat_names] - sentences_synonyms = [['a ' + xx for xx in x] for x in synonyms] - if args.prompt == 'none': + + if args.prompt == "a": + sentences = ["a " + x for x in cat_names] + sentences_synonyms = [["a " + xx for xx in x] for x in synonyms] + if args.prompt == "none": sentences = [x for x in cat_names] sentences_synonyms = [[xx for xx in x] for x in synonyms] - elif args.prompt == 'photo': - sentences = ['a photo of a {}'.format(x) for x in cat_names] - sentences_synonyms = [['a photo of a {}'.format(xx) for xx in x] \ - for x in synonyms] - elif args.prompt == 'scene': - sentences = ['a photo of a {} in the scene'.format(x) for x in cat_names] - sentences_synonyms = [['a photo of a {} in the scene'.format(xx) for xx in x] \ - for x in synonyms] + elif args.prompt == "photo": + sentences = [f"a photo of a {x}" for x in cat_names] + sentences_synonyms = [[f"a photo of a {xx}" for xx in x] for x in synonyms] + elif args.prompt == "scene": + sentences = [f"a photo of a {x} in the scene" for x in cat_names] + sentences_synonyms = [ + [f"a photo of a {xx} in the scene" for xx in x] for x in synonyms + ] - print('sentences_synonyms', len(sentences_synonyms), \ - sum(len(x) for x in sentences_synonyms)) - if args.model == 'clip': + print("sentences_synonyms", len(sentences_synonyms), sum(len(x) for x in sentences_synonyms)) + if args.model == "clip": import clip - print('Loading CLIP') + + print("Loading CLIP") model, preprocess = clip.load(args.clip_model, device=device) if args.avg_synonyms: sentences = list(itertools.chain.from_iterable(sentences_synonyms)) - print('flattened_sentences', len(sentences)) + print("flattened_sentences", len(sentences)) text = clip.tokenize(sentences).to(device) with torch.no_grad(): if len(text) > 10000: - text_features = torch.cat([ - model.encode_text(text[:len(text) // 2]), - model.encode_text(text[len(text) // 2:])], - dim=0) + text_features = torch.cat( + [ + model.encode_text(text[: len(text) // 2]), + model.encode_text(text[len(text) // 2 :]), + ], + dim=0, + ) else: text_features = model.encode_text(text) - print('text_features.shape', text_features.shape) + print("text_features.shape", text_features.shape) if args.avg_synonyms: synonyms_per_cat = [len(x) for x in sentences_synonyms] text_features = text_features.split(synonyms_per_cat, dim=0) text_features = [x.mean(dim=0) for x in text_features] text_features = torch.stack(text_features, dim=0) - print('after stack', text_features.shape) + print("after stack", text_features.shape) text_features = text_features.cpu().numpy() - elif args.model in ['bert', 'roberta']: - from transformers import AutoTokenizer, AutoModel - if args.model == 'bert': - model_name = 'bert-large-uncased' - if args.model == 'roberta': - model_name = 'roberta-large' + elif args.model in ["bert", "roberta"]: + from transformers import AutoModel, AutoTokenizer + + if args.model == "bert": + model_name = "bert-large-uncased" + if args.model == "roberta": + model_name = "roberta-large" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModel.from_pretrained(model_name) model.eval() if args.avg_synonyms: sentences = list(itertools.chain.from_iterable(sentences_synonyms)) - print('flattened_sentences', len(sentences)) + print("flattened_sentences", len(sentences)) inputs = tokenizer(sentences, padding=True, return_tensors="pt") with torch.no_grad(): model_outputs = model(**inputs) @@ -105,12 +109,14 @@ text_features = text_features.split(synonyms_per_cat, dim=0) text_features = [x.mean(dim=0) for x in text_features] text_features = torch.stack(text_features, dim=0) - print('after stack', text_features.shape) + print("after stack", text_features.shape) text_features = text_features.numpy() - print('text_features.shape', text_features.shape) + print("text_features.shape", text_features.shape) else: assert 0, args.model - if args.out_path != '': - print('saveing to', args.out_path) - np.save(open(args.out_path, 'wb'), text_features) - import pdb; pdb.set_trace() + if args.out_path != "": + print("saveing to", args.out_path) + np.save(open(args.out_path, "wb"), text_features) + import pdb + + pdb.set_trace() diff --git a/dimos/models/Detic/tools/fix_o365_names.py b/dimos/models/Detic/tools/fix_o365_names.py index c6730eacec..5aee27a14f 100644 --- a/dimos/models/Detic/tools/fix_o365_names.py +++ b/dimos/models/Detic/tools/fix_o365_names.py @@ -1,34 +1,36 @@ # Copyright (c) Facebook, Inc. and its affiliates. import argparse -import json import copy +import json -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument("--ann", default='datasets/objects365/annotations/zhiyuan_objv2_val.json') - parser.add_argument("--fix_name_map", default='datasets/metadata/Objects365_names_fix.csv') + parser.add_argument("--ann", default="datasets/objects365/annotations/zhiyuan_objv2_val.json") + parser.add_argument("--fix_name_map", default="datasets/metadata/Objects365_names_fix.csv") args = parser.parse_args() new_names = {} old_names = {} - with open(args.fix_name_map, 'r') as f: + with open(args.fix_name_map) as f: for line in f: - tmp = line.strip().split(',') + tmp = line.strip().split(",") old_names[int(tmp[0])] = tmp[1] new_names[int(tmp[0])] = tmp[2] - data = json.load(open(args.ann, 'r')) + data = json.load(open(args.ann)) + + cat_info = copy.deepcopy(data["categories"]) - cat_info = copy.deepcopy(data['categories']) - for x in cat_info: - if old_names[x['id']].strip() != x['name'].strip(): - print('{} {} {}'.format(x, old_names[x['id']], new_names[x['id']])) - import pdb; pdb.set_trace() - if old_names[x['id']] != new_names[x['id']]: - print('Renaming', x['id'], x['name'], new_names[x['id']]) - x['name'] = new_names[x['id']] - - data['categories'] = cat_info - out_name = args.ann[:-5] + '_fixname.json' - print('Saving to', out_name) - json.dump(data, open(out_name, 'w')) + if old_names[x["id"]].strip() != x["name"].strip(): + print("{} {} {}".format(x, old_names[x["id"]], new_names[x["id"]])) + import pdb + + pdb.set_trace() + if old_names[x["id"]] != new_names[x["id"]]: + print("Renaming", x["id"], x["name"], new_names[x["id"]]) + x["name"] = new_names[x["id"]] + + data["categories"] = cat_info + out_name = args.ann[:-5] + "_fixname.json" + print("Saving to", out_name) + json.dump(data, open(out_name, "w")) diff --git a/dimos/models/Detic/tools/fix_o365_path.py b/dimos/models/Detic/tools/fix_o365_path.py index 38716e56c4..c43358fff0 100644 --- a/dimos/models/Detic/tools/fix_o365_path.py +++ b/dimos/models/Detic/tools/fix_o365_path.py @@ -1,28 +1,31 @@ # Copyright (c) Facebook, Inc. and its affiliates. import argparse import json -import path import os -if __name__ == '__main__': +import path + +if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument("--ann", default='datasets/objects365/annotations/zhiyuan_objv2_train_fixname.json') - parser.add_argument("--img_dir", default='datasets/objects365/train/') + parser.add_argument( + "--ann", default="datasets/objects365/annotations/zhiyuan_objv2_train_fixname.json" + ) + parser.add_argument("--img_dir", default="datasets/objects365/train/") args = parser.parse_args() - print('Loading', args.ann) - data = json.load(open(args.ann, 'r')) + print("Loading", args.ann) + data = json.load(open(args.ann)) images = [] count = 0 - for x in data['images']: - path = '{}/{}'.format(args.img_dir, x['file_name']) + for x in data["images"]: + path = "{}/{}".format(args.img_dir, x["file_name"]) if os.path.exists(path): images.append(x) else: print(path) count = count + 1 - print('Missing', count, 'images') - data['images'] = images - out_name = args.ann[:-5] + '_fixmiss.json' - print('Saving to', out_name) - json.dump(data, open(out_name, 'w')) + print("Missing", count, "images") + data["images"] = images + out_name = args.ann[:-5] + "_fixmiss.json" + print("Saving to", out_name) + json.dump(data, open(out_name, "w")) diff --git a/dimos/models/Detic/tools/get_cc_tags.py b/dimos/models/Detic/tools/get_cc_tags.py index 570d236faa..0a5cdab8ec 100644 --- a/dimos/models/Detic/tools/get_cc_tags.py +++ b/dimos/models/Detic/tools/get_cc_tags.py @@ -1,7 +1,8 @@ # Copyright (c) Facebook, Inc. and its affiliates. import argparse -import json from collections import defaultdict +import json + from detectron2.data.datasets.lvis_v1_categories import LVIS_CATEGORIES # This mapping is extracted from the official LVIS mapping: @@ -90,106 +91,108 @@ {"synset": "toothbrush.n.01", "coco_cat_id": 90}, ] + def map_name(x): - x = x.replace('_', ' ') - if '(' in x: - x = x[:x.find('(')] + x = x.replace("_", " ") + if "(" in x: + x = x[: x.find("(")] return x.lower().strip() -if __name__ == '__main__': + +if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument('--cc_ann', default='datasets/cc3m/train_image_info.json') - parser.add_argument('--out_path', default='datasets/cc3m/train_image_info_tags.json') - parser.add_argument('--keep_images', action='store_true') - parser.add_argument('--allcaps', action='store_true') - parser.add_argument('--cat_path', default='') - parser.add_argument('--convert_caption', action='store_true') + parser.add_argument("--cc_ann", default="datasets/cc3m/train_image_info.json") + parser.add_argument("--out_path", default="datasets/cc3m/train_image_info_tags.json") + parser.add_argument("--keep_images", action="store_true") + parser.add_argument("--allcaps", action="store_true") + parser.add_argument("--cat_path", default="") + parser.add_argument("--convert_caption", action="store_true") # parser.add_argument('--lvis_ann', default='datasets/lvis/lvis_v1_val.json') args = parser.parse_args() # lvis_data = json.load(open(args.lvis_ann, 'r')) - cc_data = json.load(open(args.cc_ann, 'r')) + cc_data = json.load(open(args.cc_ann)) if args.convert_caption: num_caps = 0 caps = defaultdict(list) - for x in cc_data['annotations']: - caps[x['image_id']].append(x['caption']) - for x in cc_data['images']: - x['captions'] = caps[x['id']] - num_caps += len(x['captions']) - print('# captions', num_caps) + for x in cc_data["annotations"]: + caps[x["image_id"]].append(x["caption"]) + for x in cc_data["images"]: + x["captions"] = caps[x["id"]] + num_caps += len(x["captions"]) + print("# captions", num_caps) - if args.cat_path != '': - print('Loading', args.cat_path) - cats = json.load(open(args.cat_path))['categories'] - if 'synonyms' not in cats[0]: - cocoid2synset = {x['coco_cat_id']: x['synset'] \ - for x in COCO_SYNSET_CATEGORIES} - synset2synonyms = {x['synset']: x['synonyms'] \ - for x in LVIS_CATEGORIES} + if args.cat_path != "": + print("Loading", args.cat_path) + cats = json.load(open(args.cat_path))["categories"] + if "synonyms" not in cats[0]: + cocoid2synset = {x["coco_cat_id"]: x["synset"] for x in COCO_SYNSET_CATEGORIES} + synset2synonyms = {x["synset"]: x["synonyms"] for x in LVIS_CATEGORIES} for x in cats: - synonyms = synset2synonyms[cocoid2synset[x['id']]] - x['synonyms'] = synonyms - x['frequency'] = 'f' - cc_data['categories'] = cats + synonyms = synset2synonyms[cocoid2synset[x["id"]]] + x["synonyms"] = synonyms + x["frequency"] = "f" + cc_data["categories"] = cats - id2cat = {x['id']: x for x in cc_data['categories']} - class_count = {x['id']: 0 for x in cc_data['categories']} - class_data = {x['id']: [' ' + map_name(xx) + ' ' for xx in x['synonyms']] \ - for x in cc_data['categories']} + id2cat = {x["id"]: x for x in cc_data["categories"]} + class_count = {x["id"]: 0 for x in cc_data["categories"]} + class_data = { + x["id"]: [" " + map_name(xx) + " " for xx in x["synonyms"]] for x in cc_data["categories"] + } num_examples = 5 - examples = {x['id']: [] for x in cc_data['categories']} + examples = {x["id"]: [] for x in cc_data["categories"]} - print('class_data', class_data) + print("class_data", class_data) images = [] - for i, x in enumerate(cc_data['images']): + for i, x in enumerate(cc_data["images"]): if i % 10000 == 0: - print(i, len(cc_data['images'])) + print(i, len(cc_data["images"])) if args.allcaps: - caption = (' '.join(x['captions'])).lower() + caption = (" ".join(x["captions"])).lower() else: - caption = x['captions'][0].lower() - x['pos_category_ids'] = [] + caption = x["captions"][0].lower() + x["pos_category_ids"] = [] for cat_id, cat_names in class_data.items(): find = False for c in cat_names: - if c in caption or caption.startswith(c[1:]) \ - or caption.endswith(c[:-1]): + if c in caption or caption.startswith(c[1:]) or caption.endswith(c[:-1]): find = True break if find: - x['pos_category_ids'].append(cat_id) + x["pos_category_ids"].append(cat_id) class_count[cat_id] += 1 if len(examples[cat_id]) < num_examples: examples[cat_id].append(caption) - if len(x['pos_category_ids']) > 0 or args.keep_images: + if len(x["pos_category_ids"]) > 0 or args.keep_images: images.append(x) zero_class = [] for cat_id, count in class_count.items(): - print(id2cat[cat_id]['name'], count, end=', ') + print(id2cat[cat_id]["name"], count, end=", ") if count == 0: zero_class.append(id2cat[cat_id]) - print('==') - print('zero class', zero_class) + print("==") + print("zero class", zero_class) # for freq in ['r', 'c', 'f']: # print('#cats', freq, len([x for x in cc_data['categories'] \ # if x['frequency'] == freq] and class_count[x['id']] > 0)) - for freq in ['r', 'c', 'f']: - print('#Images', freq, sum([v for k, v in class_count.items() \ - if id2cat[k]['frequency'] == freq])) + for freq in ["r", "c", "f"]: + print( + "#Images", + freq, + sum([v for k, v in class_count.items() if id2cat[k]["frequency"] == freq]), + ) try: - out_data = {'images': images, 'categories': cc_data['categories'], \ - 'annotations': []} + out_data = {"images": images, "categories": cc_data["categories"], "annotations": []} for k, v in out_data.items(): print(k, len(v)) - if args.keep_images and not args.out_path.endswith('_full.json'): - args.out_path = args.out_path[:-5] + '_full.json' - print('Writing to', args.out_path) - json.dump(out_data, open(args.out_path, 'w')) + if args.keep_images and not args.out_path.endswith("_full.json"): + args.out_path = args.out_path[:-5] + "_full.json" + print("Writing to", args.out_path) + json.dump(out_data, open(args.out_path, "w")) except: pass diff --git a/dimos/models/Detic/tools/get_coco_zeroshot_oriorder.py b/dimos/models/Detic/tools/get_coco_zeroshot_oriorder.py index ed6748be1f..688b0a92e5 100644 --- a/dimos/models/Detic/tools/get_coco_zeroshot_oriorder.py +++ b/dimos/models/Detic/tools/get_coco_zeroshot_oriorder.py @@ -2,17 +2,19 @@ import argparse import json -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument('--data_path', default='datasets/coco/annotations/instances_val2017_unseen_2.json') - parser.add_argument('--cat_path', default='datasets/coco/annotations/instances_val2017.json') + parser.add_argument( + "--data_path", default="datasets/coco/annotations/instances_val2017_unseen_2.json" + ) + parser.add_argument("--cat_path", default="datasets/coco/annotations/instances_val2017.json") args = parser.parse_args() - print('Loading', args.cat_path) - cat = json.load(open(args.cat_path, 'r'))['categories'] + print("Loading", args.cat_path) + cat = json.load(open(args.cat_path))["categories"] - print('Loading', args.data_path) - data = json.load(open(args.data_path, 'r')) - data['categories'] = cat - out_path = args.data_path[:-5] + '_oriorder.json' - print('Saving to', out_path) - json.dump(data, open(out_path, 'w')) + print("Loading", args.data_path) + data = json.load(open(args.data_path)) + data["categories"] = cat + out_path = args.data_path[:-5] + "_oriorder.json" + print("Saving to", out_path) + json.dump(data, open(out_path, "w")) diff --git a/dimos/models/Detic/tools/get_imagenet_21k_full_tar_json.py b/dimos/models/Detic/tools/get_imagenet_21k_full_tar_json.py index a5fc0a8ee3..00502db11f 100644 --- a/dimos/models/Detic/tools/get_imagenet_21k_full_tar_json.py +++ b/dimos/models/Detic/tools/get_imagenet_21k_full_tar_json.py @@ -1,58 +1,58 @@ # Copyright (c) Facebook, Inc. and its affiliates. import argparse import json -import numpy as np -import pickle -import io -import gzip +import operator import sys import time + from nltk.corpus import wordnet -from tqdm import tqdm -import operator +import numpy as np import torch +from tqdm import tqdm -sys.path.insert(0, 'third_party/CenterNet2/') -sys.path.insert(0, 'third_party/Deformable-DETR') -from detic.data.tar_dataset import DiskTarDataset, _TarDataset +sys.path.insert(0, "third_party/CenterNet2/") +sys.path.insert(0, "third_party/Deformable-DETR") +from detic.data.tar_dataset import DiskTarDataset -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument("--imagenet_dir", default='datasets/imagenet/ImageNet-21k/') - parser.add_argument("--tarfile_path", default='datasets/imagenet/metadata-22k/tar_files.npy') - parser.add_argument("--tar_index_dir", default='datasets/imagenet/metadata-22k/tarindex_npy') - parser.add_argument("--out_path", default='datasets/imagenet/annotations/imagenet-22k_image_info.json') + parser.add_argument("--imagenet_dir", default="datasets/imagenet/ImageNet-21k/") + parser.add_argument("--tarfile_path", default="datasets/imagenet/metadata-22k/tar_files.npy") + parser.add_argument("--tar_index_dir", default="datasets/imagenet/metadata-22k/tarindex_npy") + parser.add_argument( + "--out_path", default="datasets/imagenet/annotations/imagenet-22k_image_info.json" + ) parser.add_argument("--workers", default=16, type=int) args = parser.parse_args() - start_time = time.time() - print('Building dataset') + print("Building dataset") dataset = DiskTarDataset(args.tarfile_path, args.tar_index_dir) end_time = time.time() - print(f"Took {end_time-start_time} seconds to make the dataset.") + print(f"Took {end_time - start_time} seconds to make the dataset.") print(f"Have {len(dataset)} samples.") - print('dataset', dataset) - + print("dataset", dataset) tar_files = np.load(args.tarfile_path) categories = [] for i, tar_file in enumerate(tar_files): wnid = tar_file[-13:-4] - synset = wordnet.synset_from_pos_and_offset('n', int(wnid[1:])) + synset = wordnet.synset_from_pos_and_offset("n", int(wnid[1:])) synonyms = [x.name() for x in synset.lemmas()] category = { - 'id': i + 1, - 'synset': synset.name(), - 'name': synonyms[0], - 'def': synset.definition(), - 'synonyms': synonyms, + "id": i + 1, + "synset": synset.name(), + "name": synonyms[0], + "def": synset.definition(), + "synonyms": synonyms, } categories.append(category) - print('categories', len(categories)) + print("categories", len(categories)) data_loader = torch.utils.data.DataLoader( - dataset, batch_size=1, shuffle=False, + dataset, + batch_size=1, + shuffle=False, num_workers=args.workers, collate_fn=operator.itemgetter(0), ) @@ -61,21 +61,22 @@ if label == -1: continue image = { - 'id': int(index) + 1, - 'pos_category_ids': [int(label) + 1], - 'height': int(img.height), - 'width': int(img.width), - 'tar_index': int(index), + "id": int(index) + 1, + "pos_category_ids": [int(label) + 1], + "height": int(img.height), + "width": int(img.width), + "tar_index": int(index), } images.append(image) - - data = {'categories': categories, 'images': images, 'annotations': []} + + data = {"categories": categories, "images": images, "annotations": []} try: for k, v in data.items(): print(k, len(v)) - print('Saving to ', args.out_path) - json.dump(data, open(args.out_path, 'w')) + print("Saving to ", args.out_path) + json.dump(data, open(args.out_path, "w")) except: pass - import pdb; pdb.set_trace() - + import pdb + + pdb.set_trace() diff --git a/dimos/models/Detic/tools/get_lvis_cat_info.py b/dimos/models/Detic/tools/get_lvis_cat_info.py index 83f286983c..414a615b8a 100644 --- a/dimos/models/Detic/tools/get_lvis_cat_info.py +++ b/dimos/models/Detic/tools/get_lvis_cat_info.py @@ -2,43 +2,42 @@ import argparse import json -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument("--ann", default='datasets/lvis/lvis_v1_train.json') - parser.add_argument("--add_freq", action='store_true') + parser.add_argument("--ann", default="datasets/lvis/lvis_v1_train.json") + parser.add_argument("--add_freq", action="store_true") parser.add_argument("--r_thresh", type=int, default=10) parser.add_argument("--c_thresh", type=int, default=100) args = parser.parse_args() - print('Loading', args.ann) - data = json.load(open(args.ann, 'r')) - cats = data['categories'] - image_count = {x['id']: set() for x in cats} - ann_count = {x['id']: 0 for x in cats} - for x in data['annotations']: - image_count[x['category_id']].add(x['image_id']) - ann_count[x['category_id']] += 1 - num_freqs = {x: 0 for x in ['r', 'f', 'c']} + print("Loading", args.ann) + data = json.load(open(args.ann)) + cats = data["categories"] + image_count = {x["id"]: set() for x in cats} + ann_count = {x["id"]: 0 for x in cats} + for x in data["annotations"]: + image_count[x["category_id"]].add(x["image_id"]) + ann_count[x["category_id"]] += 1 + num_freqs = {x: 0 for x in ["r", "f", "c"]} for x in cats: - x['image_count'] = len(image_count[x['id']]) - x['instance_count'] = ann_count[x['id']] + x["image_count"] = len(image_count[x["id"]]) + x["instance_count"] = ann_count[x["id"]] if args.add_freq: - freq = 'f' - if x['image_count'] < args.c_thresh: - freq = 'c' - if x['image_count'] < args.r_thresh: - freq = 'r' - x['frequency'] = freq + freq = "f" + if x["image_count"] < args.c_thresh: + freq = "c" + if x["image_count"] < args.r_thresh: + freq = "r" + x["frequency"] = freq num_freqs[freq] += 1 print(cats) - image_counts = sorted([x['image_count'] for x in cats]) + image_counts = sorted([x["image_count"] for x in cats]) # print('image count', image_counts) # import pdb; pdb.set_trace() if args.add_freq: - for x in ['r', 'c', 'f']: + for x in ["r", "c", "f"]: print(x, num_freqs[x]) - out = cats # {'categories': cats} - out_path = args.ann[:-5] + '_cat_info.json' - print('Saving to', out_path) - json.dump(out, open(out_path, 'w')) - + out = cats # {'categories': cats} + out_path = args.ann[:-5] + "_cat_info.json" + print("Saving to", out_path) + json.dump(out, open(out_path, "w")) diff --git a/dimos/models/Detic/tools/merge_lvis_coco.py b/dimos/models/Detic/tools/merge_lvis_coco.py index abc2b673a3..1a76a02f0b 100644 --- a/dimos/models/Detic/tools/merge_lvis_coco.py +++ b/dimos/models/Detic/tools/merge_lvis_coco.py @@ -1,19 +1,18 @@ # Copyright (c) Facebook, Inc. and its affiliates. from collections import defaultdict -import torch -import sys import json -import numpy as np from detectron2.structures import Boxes, pairwise_iou -COCO_PATH = 'datasets/coco/annotations/instances_train2017.json' -IMG_PATH = 'datasets/coco/train2017/' -LVIS_PATH = 'datasets/lvis/lvis_v1_train.json' +import torch + +COCO_PATH = "datasets/coco/annotations/instances_train2017.json" +IMG_PATH = "datasets/coco/train2017/" +LVIS_PATH = "datasets/lvis/lvis_v1_train.json" NO_SEG = False if NO_SEG: - SAVE_PATH = 'datasets/lvis/lvis_v1_train+coco_box.json' + SAVE_PATH = "datasets/lvis/lvis_v1_train+coco_box.json" else: - SAVE_PATH = 'datasets/lvis/lvis_v1_train+coco_mask.json' + SAVE_PATH = "datasets/lvis/lvis_v1_train+coco_mask.json" THRESH = 0.7 DEBUG = False @@ -105,60 +104,63 @@ def get_bbox(ann): - bbox = ann['bbox'] + bbox = ann["bbox"] return [bbox[0], bbox[1], bbox[0] + bbox[2], bbox[1] + bbox[3]] -if __name__ == '__main__': - file_name_key = 'file_name' if 'v0.5' in LVIS_PATH else 'coco_url' - coco_data = json.load(open(COCO_PATH, 'r')) - lvis_data = json.load(open(LVIS_PATH, 'r')) +if __name__ == "__main__": + file_name_key = "file_name" if "v0.5" in LVIS_PATH else "coco_url" + coco_data = json.load(open(COCO_PATH)) + lvis_data = json.load(open(LVIS_PATH)) - coco_cats = coco_data['categories'] - lvis_cats = lvis_data['categories'] + coco_cats = coco_data["categories"] + lvis_cats = lvis_data["categories"] num_find = 0 num_not_find = 0 num_twice = 0 coco2lviscats = {} - synset2lvisid = {x['synset']: x['id'] for x in lvis_cats} + synset2lvisid = {x["synset"]: x["id"] for x in lvis_cats} # cocoid2synset = {x['coco_cat_id']: x['synset'] for x in COCO_SYNSET_CATEGORIES} - coco2lviscats = {x['coco_cat_id']: synset2lvisid[x['synset']] \ - for x in COCO_SYNSET_CATEGORIES if x['synset'] in synset2lvisid} + coco2lviscats = { + x["coco_cat_id"]: synset2lvisid[x["synset"]] + for x in COCO_SYNSET_CATEGORIES + if x["synset"] in synset2lvisid + } print(len(coco2lviscats)) - - lvis_file2id = {x[file_name_key][-16:]: x['id'] for x in lvis_data['images']} - lvis_id2img = {x['id']: x for x in lvis_data['images']} - lvis_catid2name = {x['id']: x['name'] for x in lvis_data['categories']} + + lvis_file2id = {x[file_name_key][-16:]: x["id"] for x in lvis_data["images"]} + lvis_id2img = {x["id"]: x for x in lvis_data["images"]} + lvis_catid2name = {x["id"]: x["name"] for x in lvis_data["categories"]} coco_file2anns = {} - coco_id2img = {x['id']: x for x in coco_data['images']} + coco_id2img = {x["id"]: x for x in coco_data["images"]} coco_img2anns = defaultdict(list) - for ann in coco_data['annotations']: - coco_img = coco_id2img[ann['image_id']] - file_name = coco_img['file_name'][-16:] - if ann['category_id'] in coco2lviscats and \ - file_name in lvis_file2id: + for ann in coco_data["annotations"]: + coco_img = coco_id2img[ann["image_id"]] + file_name = coco_img["file_name"][-16:] + if ann["category_id"] in coco2lviscats and file_name in lvis_file2id: lvis_image_id = lvis_file2id[file_name] lvis_image = lvis_id2img[lvis_image_id] - lvis_cat_id = coco2lviscats[ann['category_id']] - if lvis_cat_id in lvis_image['neg_category_ids']: + lvis_cat_id = coco2lviscats[ann["category_id"]] + if lvis_cat_id in lvis_image["neg_category_ids"]: continue if DEBUG: import cv2 + img_path = IMG_PATH + file_name img = cv2.imread(img_path) print(lvis_catid2name[lvis_cat_id]) - print('neg', [lvis_catid2name[x] for x in lvis_image['neg_category_ids']]) - cv2.imshow('img', img) + print("neg", [lvis_catid2name[x] for x in lvis_image["neg_category_ids"]]) + cv2.imshow("img", img) cv2.waitKey() - ann['category_id'] = lvis_cat_id - ann['image_id'] = lvis_image_id + ann["category_id"] = lvis_cat_id + ann["image_id"] = lvis_image_id coco_img2anns[file_name].append(ann) - + lvis_img2anns = defaultdict(list) - for ann in lvis_data['annotations']: - lvis_img = lvis_id2img[ann['image_id']] + for ann in lvis_data["annotations"]: + lvis_img = lvis_id2img[ann["image_id"]] file_name = lvis_img[file_name_key][-16:] lvis_img2anns[file_name].append(ann) @@ -168,35 +170,37 @@ def get_bbox(ann): coco_anns = coco_img2anns[file_name] lvis_anns = lvis_img2anns[file_name] ious = pairwise_iou( - Boxes(torch.tensor([get_bbox(x) for x in coco_anns])), - Boxes(torch.tensor([get_bbox(x) for x in lvis_anns])) + Boxes(torch.tensor([get_bbox(x) for x in coco_anns])), + Boxes(torch.tensor([get_bbox(x) for x in lvis_anns])), ) for ann in lvis_anns: ann_id_count = ann_id_count + 1 - ann['id'] = ann_id_count + ann["id"] = ann_id_count anns.append(ann) for i, ann in enumerate(coco_anns): if len(ious[i]) == 0 or ious[i].max() < THRESH: ann_id_count = ann_id_count + 1 - ann['id'] = ann_id_count + ann["id"] = ann_id_count anns.append(ann) else: duplicated = False for j in range(len(ious[i])): - if ious[i, j] >= THRESH and \ - coco_anns[i]['category_id'] == lvis_anns[j]['category_id']: + if ( + ious[i, j] >= THRESH + and coco_anns[i]["category_id"] == lvis_anns[j]["category_id"] + ): duplicated = True if not duplicated: ann_id_count = ann_id_count + 1 - ann['id'] = ann_id_count + ann["id"] = ann_id_count anns.append(ann) if NO_SEG: for ann in anns: - del ann['segmentation'] - lvis_data['annotations'] = anns - - print('# Images', len(lvis_data['images'])) - print('# Anns', len(lvis_data['annotations'])) - json.dump(lvis_data, open(SAVE_PATH, 'w')) + del ann["segmentation"] + lvis_data["annotations"] = anns + + print("# Images", len(lvis_data["images"])) + print("# Anns", len(lvis_data["annotations"])) + json.dump(lvis_data, open(SAVE_PATH, "w")) diff --git a/dimos/models/Detic/tools/preprocess_imagenet22k.py b/dimos/models/Detic/tools/preprocess_imagenet22k.py index 67795c209b..edf2d2bbf7 100644 --- a/dimos/models/Detic/tools/preprocess_imagenet22k.py +++ b/dimos/models/Detic/tools/preprocess_imagenet22k.py @@ -2,21 +2,21 @@ # Copyright (c) Facebook, Inc. and its affiliates. import os -import numpy as np import sys -sys.path.insert(0, 'third_party/CenterNet2/') -sys.path.insert(0, 'third_party/Deformable-DETR') -from detic.data.tar_dataset import _TarDataset, DiskTarDataset -import pickle -import io +import numpy as np + +sys.path.insert(0, "third_party/CenterNet2/") +sys.path.insert(0, "third_party/Deformable-DETR") import gzip +import io import time +from detic.data.tar_dataset import _TarDataset -class _RawTarDataset(object): - def __init__(self, filename, indexname, preload=False): +class _RawTarDataset: + def __init__(self, filename, indexname: str, preload: bool=False) -> None: self.filename = filename self.names = [] self.offsets = [] @@ -25,75 +25,75 @@ def __init__(self, filename, indexname, preload=False): ll = l.split() a, b, c = ll[:3] offset = int(b[:-1]) - if l.endswith('** Block of NULs **\n'): + if l.endswith("** Block of NULs **\n"): self.offsets.append(offset) break else: - if c.endswith('JPEG'): + if c.endswith("JPEG"): self.names.append(c) self.offsets.append(offset) else: # ignore directories pass if preload: - self.data = np.memmap(filename, mode='r', dtype='uint8') + self.data = np.memmap(filename, mode="r", dtype="uint8") else: self.data = None - def __len__(self): + def __len__(self) -> int: return len(self.names) - def __getitem__(self, idx): + def __getitem__(self, idx: int): if self.data is None: - self.data = np.memmap(self.filename, mode='r', dtype='uint8') + self.data = np.memmap(self.filename, mode="r", dtype="uint8") ofs = self.offsets[idx] * 512 fsize = 512 * (self.offsets[idx + 1] - self.offsets[idx]) - data = self.data[ofs:ofs + fsize] + data = self.data[ofs : ofs + fsize] - if data[:13].tostring() == '././@LongLink': - data = data[3 * 512:] + if data[:13].tostring() == "././@LongLink": + data = data[3 * 512 :] else: data = data[512:] # just to make it more fun a few JPEGs are GZIP compressed... # catch this case - if tuple(data[:2]) == (0x1f, 0x8b): + if tuple(data[:2]) == (0x1F, 0x8B): s = io.StringIO(data.tostring()) - g = gzip.GzipFile(None, 'r', 0, s) + g = gzip.GzipFile(None, "r", 0, s) sdata = g.read() else: sdata = data.tostring() return sdata - -def preprocess(): +def preprocess() -> None: # Follow https://github.com/Alibaba-MIIL/ImageNet21K/blob/main/dataset_preprocessing/processing_script.sh # Expect 12358684 samples with 11221 classes # ImageNet folder has 21841 classes (synsets) - i22kdir = '/datasets01/imagenet-22k/062717/' - i22ktarlogs = '/checkpoint/imisra/datasets/imagenet-22k/tarindex' - class_names_file = '/checkpoint/imisra/datasets/imagenet-22k/words.txt' + i22kdir = "/datasets01/imagenet-22k/062717/" + i22ktarlogs = "/checkpoint/imisra/datasets/imagenet-22k/tarindex" + class_names_file = "/checkpoint/imisra/datasets/imagenet-22k/words.txt" - output_dir = '/checkpoint/zhouxy/Datasets/ImageNet/metadata-22k/' - i22knpytarlogs = '/checkpoint/zhouxy/Datasets/ImageNet/metadata-22k/tarindex_npy' - print('Listing dir') + output_dir = "/checkpoint/zhouxy/Datasets/ImageNet/metadata-22k/" + i22knpytarlogs = "/checkpoint/zhouxy/Datasets/ImageNet/metadata-22k/tarindex_npy" + print("Listing dir") log_files = os.listdir(i22ktarlogs) log_files = [x for x in log_files if x.endswith(".tarlog")] log_files.sort() - chunk_datasets = [] dataset_lens = [] min_count = 0 create_npy_tarlogs = True - print('Creating folders') + print("Creating folders") if create_npy_tarlogs: os.makedirs(i22knpytarlogs, exist_ok=True) for log_file in log_files: syn = log_file.replace(".tarlog", "") - dataset = _RawTarDataset(os.path.join(i22kdir, syn + ".tar"), - os.path.join(i22ktarlogs, syn + ".tarlog"), - preload=False) + dataset = _RawTarDataset( + os.path.join(i22kdir, syn + ".tar"), + os.path.join(i22ktarlogs, syn + ".tarlog"), + preload=False, + ) names = np.array(dataset.names) offsets = np.array(dataset.offsets, dtype=np.int64) np.save(os.path.join(i22knpytarlogs, f"{syn}_names.npy"), names) @@ -112,7 +112,6 @@ def preprocess(): end_time = time.time() print(f"Time {end_time - start_time}") - dataset_lens = np.array(dataset_lens) dataset_valid = dataset_lens > min_count diff --git a/dimos/models/Detic/tools/remove_lvis_rare.py b/dimos/models/Detic/tools/remove_lvis_rare.py index 06e4e881bf..423dd6e6e2 100644 --- a/dimos/models/Detic/tools/remove_lvis_rare.py +++ b/dimos/models/Detic/tools/remove_lvis_rare.py @@ -2,19 +2,20 @@ import argparse import json -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument('--ann', default='datasets/lvis/lvis_v1_train.json') + parser.add_argument("--ann", default="datasets/lvis/lvis_v1_train.json") args = parser.parse_args() - print('Loading', args.ann) - data = json.load(open(args.ann, 'r')) - catid2freq = {x['id']: x['frequency'] for x in data['categories']} - print('ori #anns', len(data['annotations'])) - exclude = ['r'] - data['annotations'] = [x for x in data['annotations'] \ - if catid2freq[x['category_id']] not in exclude] - print('filtered #anns', len(data['annotations'])) - out_path = args.ann[:-5] + '_norare.json' - print('Saving to', out_path) - json.dump(data, open(out_path, 'w')) + print("Loading", args.ann) + data = json.load(open(args.ann)) + catid2freq = {x["id"]: x["frequency"] for x in data["categories"]} + print("ori #anns", len(data["annotations"])) + exclude = ["r"] + data["annotations"] = [ + x for x in data["annotations"] if catid2freq[x["category_id"]] not in exclude + ] + print("filtered #anns", len(data["annotations"])) + out_path = args.ann[:-5] + "_norare.json" + print("Saving to", out_path) + json.dump(data, open(out_path, "w")) diff --git a/dimos/models/Detic/tools/unzip_imagenet_lvis.py b/dimos/models/Detic/tools/unzip_imagenet_lvis.py index 56ccad1a90..fd969c28bb 100644 --- a/dimos/models/Detic/tools/unzip_imagenet_lvis.py +++ b/dimos/models/Detic/tools/unzip_imagenet_lvis.py @@ -1,19 +1,18 @@ # Copyright (c) Facebook, Inc. and its affiliates. -import os import argparse +import os -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument('--src_path', default='datasets/imagenet/ImageNet-21K/') - parser.add_argument('--dst_path', default='datasets/imagenet/ImageNet-LVIS/') - parser.add_argument('--data_path', default='datasets/imagenet_lvis_wnid.txt') + parser.add_argument("--src_path", default="datasets/imagenet/ImageNet-21K/") + parser.add_argument("--dst_path", default="datasets/imagenet/ImageNet-LVIS/") + parser.add_argument("--data_path", default="datasets/imagenet_lvis_wnid.txt") args = parser.parse_args() f = open(args.data_path) for i, line in enumerate(f): - cmd = 'mkdir {x} && tar -xf {src}/{l}.tar -C {x}'.format( - src=args.src_path, - l=line.strip(), - x=args.dst_path + '/' + line.strip()) - print(i, cmd) - os.system(cmd) + cmd = "mkdir {x} && tar -xf {src}/{l}.tar -C {x}".format( + src=args.src_path, l=line.strip(), x=args.dst_path + "/" + line.strip() + ) + print(i, cmd) + os.system(cmd) diff --git a/dimos/models/Detic/train_net.py b/dimos/models/Detic/train_net.py index ff1f570dd8..54ab6136f4 100644 --- a/dimos/models/Detic/train_net.py +++ b/dimos/models/Detic/train_net.py @@ -1,105 +1,101 @@ # Copyright (c) Facebook, Inc. and its affiliates. +from collections import OrderedDict +import datetime import logging import os import sys -from collections import OrderedDict -import torch -from torch.nn.parallel import DistributedDataParallel import time -import datetime -from fvcore.common.timer import Timer -import detectron2.utils.comm as comm from detectron2.checkpoint import DetectionCheckpointer, PeriodicCheckpointer from detectron2.config import get_cfg from detectron2.data import ( MetadataCatalog, build_detection_test_loader, ) +from detectron2.data.build import build_detection_train_loader +from detectron2.data.dataset_mapper import DatasetMapper from detectron2.engine import default_argument_parser, default_setup, launch - from detectron2.evaluation import ( + COCOEvaluator, + LVISEvaluator, inference_on_dataset, print_csv_format, - LVISEvaluator, - COCOEvaluator, ) from detectron2.modeling import build_model from detectron2.solver import build_lr_scheduler, build_optimizer +import detectron2.utils.comm as comm from detectron2.utils.events import ( CommonMetricPrinter, EventStorage, JSONWriter, TensorboardXWriter, ) -from detectron2.data.dataset_mapper import DatasetMapper -from detectron2.data.build import build_detection_train_loader from detectron2.utils.logger import setup_logger +from fvcore.common.timer import Timer +import torch from torch.cuda.amp import GradScaler +from torch.nn.parallel import DistributedDataParallel -sys.path.insert(0, 'third_party/CenterNet2/') +sys.path.insert(0, "third_party/CenterNet2/") from centernet.config import add_centernet_config -sys.path.insert(0, 'third_party/Deformable-DETR') +sys.path.insert(0, "third_party/Deformable-DETR") from detic.config import add_detic_config +from detic.custom_solver import build_custom_optimizer from detic.data.custom_build_augmentation import build_custom_augmentation -from detic.data.custom_dataset_dataloader import build_custom_train_loader +from detic.data.custom_dataset_dataloader import build_custom_train_loader from detic.data.custom_dataset_mapper import CustomDatasetMapper, DetrDatasetMapper -from detic.custom_solver import build_custom_optimizer -from detic.evaluation.oideval import OIDEvaluator from detic.evaluation.custom_coco_eval import CustomCOCOEvaluator +from detic.evaluation.oideval import OIDEvaluator from detic.modeling.utils import reset_cls_test - logger = logging.getLogger("detectron2") + def do_test(cfg, model): results = OrderedDict() for d, dataset_name in enumerate(cfg.DATASETS.TEST): if cfg.MODEL.RESET_CLS_TESTS: - reset_cls_test( - model, - cfg.MODEL.TEST_CLASSIFIERS[d], - cfg.MODEL.TEST_NUM_CLASSES[d]) - mapper = None if cfg.INPUT.TEST_INPUT_TYPE == 'default' \ - else DatasetMapper( - cfg, False, augmentations=build_custom_augmentation(cfg, False)) + reset_cls_test(model, cfg.MODEL.TEST_CLASSIFIERS[d], cfg.MODEL.TEST_NUM_CLASSES[d]) + mapper = ( + None + if cfg.INPUT.TEST_INPUT_TYPE == "default" + else DatasetMapper(cfg, False, augmentations=build_custom_augmentation(cfg, False)) + ) data_loader = build_detection_test_loader(cfg, dataset_name, mapper=mapper) - output_folder = os.path.join( - cfg.OUTPUT_DIR, "inference_{}".format(dataset_name)) + output_folder = os.path.join(cfg.OUTPUT_DIR, f"inference_{dataset_name}") evaluator_type = MetadataCatalog.get(dataset_name).evaluator_type if evaluator_type == "lvis" or cfg.GEN_PSEDO_LABELS: evaluator = LVISEvaluator(dataset_name, cfg, True, output_folder) - elif evaluator_type == 'coco': - if dataset_name == 'coco_generalized_zeroshot_val': + elif evaluator_type == "coco": + if dataset_name == "coco_generalized_zeroshot_val": # Additionally plot mAP for 'seen classes' and 'unseen classes' evaluator = CustomCOCOEvaluator(dataset_name, cfg, True, output_folder) else: evaluator = COCOEvaluator(dataset_name, cfg, True, output_folder) - elif evaluator_type == 'oid': + elif evaluator_type == "oid": evaluator = OIDEvaluator(dataset_name, cfg, True, output_folder) else: assert 0, evaluator_type - - results[dataset_name] = inference_on_dataset( - model, data_loader, evaluator) + + results[dataset_name] = inference_on_dataset(model, data_loader, evaluator) if comm.is_main_process(): - logger.info("Evaluation results for {} in csv format:".format( - dataset_name)) + logger.info(f"Evaluation results for {dataset_name} in csv format:") print_csv_format(results[dataset_name]) if len(results) == 1: - results = list(results.values())[0] + results = next(iter(results.values())) return results -def do_train(cfg, model, resume=False): + +def do_train(cfg, model, resume: bool=False) -> None: model.train() if cfg.SOLVER.USE_CUSTOM_SOLVER: optimizer = build_custom_optimizer(cfg, model) else: - assert cfg.SOLVER.OPTIMIZER == 'SGD' - assert cfg.SOLVER.CLIP_GRADIENTS.CLIP_TYPE != 'full_model' - assert cfg.SOLVER.BACKBONE_MULTIPLIER == 1. + assert cfg.SOLVER.OPTIMIZER == "SGD" + assert cfg.SOLVER.CLIP_GRADIENTS.CLIP_TYPE != "full_model" + assert cfg.SOLVER.BACKBONE_MULTIPLIER == 1.0 optimizer = build_optimizer(cfg, model) scheduler = build_lr_scheduler(cfg, optimizer) @@ -107,8 +103,9 @@ def do_train(cfg, model, resume=False): model, cfg.OUTPUT_DIR, optimizer=optimizer, scheduler=scheduler ) - start_iter = checkpointer.resume_or_load( - cfg.MODEL.WEIGHTS, resume=resume).get("iteration", -1) + 1 + start_iter = ( + checkpointer.resume_or_load(cfg.MODEL.WEIGHTS, resume=resume).get("iteration", -1) + 1 + ) if not resume: start_iter = 0 max_iter = cfg.SOLVER.MAX_ITER if cfg.SOLVER.TRAIN_ITER < 0 else cfg.SOLVER.TRAIN_ITER @@ -129,10 +126,14 @@ def do_train(cfg, model, resume=False): use_custom_mapper = cfg.WITH_IMAGE_LABELS MapperClass = CustomDatasetMapper if use_custom_mapper else DatasetMapper - mapper = MapperClass(cfg, True) if cfg.INPUT.CUSTOM_AUG == '' else \ - DetrDatasetMapper(cfg, True) if cfg.INPUT.CUSTOM_AUG == 'DETR' else \ - MapperClass(cfg, True, augmentations=build_custom_augmentation(cfg, True)) - if cfg.DATALOADER.SAMPLER_TRAIN in ['TrainingSampler', 'RepeatFactorTrainingSampler']: + mapper = ( + MapperClass(cfg, True) + if cfg.INPUT.CUSTOM_AUG == "" + else DetrDatasetMapper(cfg, True) + if cfg.INPUT.CUSTOM_AUG == "DETR" + else MapperClass(cfg, True, augmentations=build_custom_augmentation(cfg, True)) + ) + if cfg.DATALOADER.SAMPLER_TRAIN in ["TrainingSampler", "RepeatFactorTrainingSampler"]: data_loader = build_detection_train_loader(cfg, mapper=mapper) else: data_loader = build_custom_train_loader(cfg, mapper=mapper) @@ -140,12 +141,12 @@ def do_train(cfg, model, resume=False): if cfg.FP16: scaler = GradScaler() - logger.info("Starting training from iteration {}".format(start_iter)) + logger.info(f"Starting training from iteration {start_iter}") with EventStorage(start_iter) as storage: step_timer = Timer() data_timer = Timer() start_time = time.perf_counter() - for data, iteration in zip(data_loader, range(start_iter, max_iter)): + for data, iteration in zip(data_loader, range(start_iter, max_iter), strict=False): data_time = data_timer.seconds() storage.put_scalars(data_time=data_time) step_timer.reset() @@ -153,16 +154,13 @@ def do_train(cfg, model, resume=False): storage.step() loss_dict = model(data) - losses = sum( - loss for k, loss in loss_dict.items()) + losses = sum(loss for k, loss in loss_dict.items()) assert torch.isfinite(losses).all(), loss_dict - loss_dict_reduced = {k: v.item() \ - for k, v in comm.reduce_dict(loss_dict).items()} + loss_dict_reduced = {k: v.item() for k, v in comm.reduce_dict(loss_dict).items()} losses_reduced = sum(loss for loss in loss_dict_reduced.values()) if comm.is_main_process(): - storage.put_scalars( - total_loss=losses_reduced, **loss_dict_reduced) + storage.put_scalars(total_loss=losses_reduced, **loss_dict_reduced) optimizer.zero_grad() if cfg.FP16: @@ -173,30 +171,31 @@ def do_train(cfg, model, resume=False): losses.backward() optimizer.step() - storage.put_scalar( - "lr", optimizer.param_groups[0]["lr"], smoothing_hint=False) + storage.put_scalar("lr", optimizer.param_groups[0]["lr"], smoothing_hint=False) step_time = step_timer.seconds() storage.put_scalars(time=step_time) data_timer.reset() scheduler.step() - if (cfg.TEST.EVAL_PERIOD > 0 + if ( + cfg.TEST.EVAL_PERIOD > 0 and iteration % cfg.TEST.EVAL_PERIOD == 0 - and iteration != max_iter): + and iteration != max_iter + ): do_test(cfg, model) comm.synchronize() - if iteration - start_iter > 5 and \ - (iteration % 20 == 0 or iteration == max_iter): + if iteration - start_iter > 5 and (iteration % 20 == 0 or iteration == max_iter): for writer in writers: writer.write() periodic_checkpointer.step(iteration) total_time = time.perf_counter() - start_time logger.info( - "Total training time: {}".format( - str(datetime.timedelta(seconds=int(total_time))))) + f"Total training time: {datetime.timedelta(seconds=int(total_time))!s}" + ) + def setup(args): """ @@ -207,14 +206,13 @@ def setup(args): add_detic_config(cfg) cfg.merge_from_file(args.config_file) cfg.merge_from_list(args.opts) - if '/auto' in cfg.OUTPUT_DIR: + if "/auto" in cfg.OUTPUT_DIR: file_name = os.path.basename(args.config_file)[:-5] - cfg.OUTPUT_DIR = cfg.OUTPUT_DIR.replace('/auto', '/{}'.format(file_name)) - logger.info('OUTPUT_DIR: {}'.format(cfg.OUTPUT_DIR)) + cfg.OUTPUT_DIR = cfg.OUTPUT_DIR.replace("/auto", f"/{file_name}") + logger.info(f"OUTPUT_DIR: {cfg.OUTPUT_DIR}") cfg.freeze() default_setup(cfg, args) - setup_logger(output=cfg.OUTPUT_DIR, \ - distributed_rank=comm.get_rank(), name="detic") + setup_logger(output=cfg.OUTPUT_DIR, distributed_rank=comm.get_rank(), name="detic") return cfg @@ -222,7 +220,7 @@ def main(args): cfg = setup(args) model = build_model(cfg) - logger.info("Model:\n{}".format(model)) + logger.info(f"Model:\n{model}") if args.eval_only: DetectionCheckpointer(model, save_dir=cfg.OUTPUT_DIR).resume_or_load( cfg.MODEL.WEIGHTS, resume=args.resume @@ -233,8 +231,10 @@ def main(args): distributed = comm.get_world_size() > 1 if distributed: model = DistributedDataParallel( - model, device_ids=[comm.get_local_rank()], broadcast_buffers=False, - find_unused_parameters=cfg.FIND_UNUSED_PARAM + model, + device_ids=[comm.get_local_rank()], + broadcast_buffers=False, + find_unused_parameters=cfg.FIND_UNUSED_PARAM, ) do_train(cfg, model, resume=args.resume) @@ -245,19 +245,16 @@ def main(args): args = default_argument_parser() args = args.parse_args() if args.num_machines == 1: - args.dist_url = 'tcp://127.0.0.1:{}'.format( - torch.randint(11111, 60000, (1,))[0].item()) + args.dist_url = f"tcp://127.0.0.1:{torch.randint(11111, 60000, (1,))[0].item()}" else: - if args.dist_url == 'host': - args.dist_url = 'tcp://{}:12345'.format( - os.environ['SLURM_JOB_NODELIST']) - elif not args.dist_url.startswith('tcp'): + if args.dist_url == "host": + args.dist_url = "tcp://{}:12345".format(os.environ["SLURM_JOB_NODELIST"]) + elif not args.dist_url.startswith("tcp"): tmp = os.popen( - 'echo $(scontrol show job {} | grep BatchHost)'.format( - args.dist_url) - ).read() - tmp = tmp[tmp.find('=') + 1: -1] - args.dist_url = 'tcp://{}:12345'.format(tmp) + f"echo $(scontrol show job {args.dist_url} | grep BatchHost)" + ).read() + tmp = tmp[tmp.find("=") + 1 : -1] + args.dist_url = f"tcp://{tmp}:12345" print("Command Line Args:", args) launch( main, diff --git a/dimos/models/depth/metric3d.py b/dimos/models/depth/metric3d.py index 3e5dd7cf2f..e22c546dc3 100644 --- a/dimos/models/depth/metric3d.py +++ b/dimos/models/depth/metric3d.py @@ -12,12 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os -import sys -import torch -from PIL import Image import cv2 import numpy as np +from PIL import Image +import torch # May need to add this back for import to work # external_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'external', 'Metric3D')) @@ -26,24 +24,27 @@ class Metric3D: - def __init__(self, gt_depth_scale=256.0): - #self.conf = get_config("zoedepth", "infer") - #self.depth_model = build_model(self.conf) - self.depth_model = torch.hub.load('yvanyin/metric3d', 'metric3d_vit_small', pretrain=True).cuda() + def __init__(self, camera_intrinsics=None, gt_depth_scale: float=256.0) -> None: + # self.conf = get_config("zoedepth", "infer") + # self.depth_model = build_model(self.conf) + self.depth_model = torch.hub.load( + "yvanyin/metric3d", "metric3d_vit_small", pretrain=True + ).cuda() if torch.cuda.device_count() > 1: print(f"Using {torch.cuda.device_count()} GPUs!") - #self.depth_model = torch.nn.DataParallel(self.depth_model) + # self.depth_model = torch.nn.DataParallel(self.depth_model) self.depth_model.eval() - self.intrinsic = [707.0493, 707.0493, 604.0814, 180.5066] + self.intrinsic = camera_intrinsics self.intrinsic_scaled = None - self.gt_depth_scale = gt_depth_scale # And this + self.gt_depth_scale = gt_depth_scale # And this self.pad_info = None self.rgb_origin = None - ''' + + """ Input: Single image in RGB format Output: Depth map - ''' + """ def update_intrinsic(self, intrinsic): """ @@ -55,7 +56,7 @@ def update_intrinsic(self, intrinsic): self.intrinsic = intrinsic print(f"Intrinsics updated to: {self.intrinsic}") - def infer_depth(self, img, debug=False): + def infer_depth(self, img, debug: bool=False): if debug: print(f"Input image: {img}") try: @@ -71,18 +72,17 @@ def infer_depth(self, img, debug=False): img = self.rescale_input(img, self.rgb_origin) with torch.no_grad(): - pred_depth, confidence, output_dict = self.depth_model.inference({'input': img}) + pred_depth, confidence, output_dict = self.depth_model.inference({"input": img}) # Convert to PIL format depth_image = self.unpad_transform_depth(pred_depth) - out_16bit_numpy = (depth_image.squeeze().cpu().numpy() * self.gt_depth_scale).astype(np.uint16) - depth_map_pil = Image.fromarray(out_16bit_numpy) - return depth_map_pil - def save_depth(self, pred_depth): + return depth_image.cpu().numpy() + + def save_depth(self, pred_depth) -> None: # Save the depth map to a file pred_depth_np = pred_depth.cpu().numpy() - output_depth_file = 'output_depth_map.png' + output_depth_file = "output_depth_map.png" cv2.imwrite(output_depth_file, pred_depth_np) print(f"Depth map saved to {output_depth_file}") @@ -94,9 +94,16 @@ def rescale_input(self, rgb, rgb_origin): # input_size = (544, 1216) # for convnext model h, w = rgb_origin.shape[:2] scale = min(input_size[0] / h, input_size[1] / w) - rgb = cv2.resize(rgb_origin, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_LINEAR) + rgb = cv2.resize( + rgb_origin, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_LINEAR + ) # remember to scale intrinsic, hold depth - self.intrinsic_scaled = [self.intrinsic[0] * scale, self.intrinsic[1] * scale, self.intrinsic[2] * scale, self.intrinsic[3] * scale] + self.intrinsic_scaled = [ + self.intrinsic[0] * scale, + self.intrinsic[1] * scale, + self.intrinsic[2] * scale, + self.intrinsic[3] * scale, + ] # padding to input_size padding = [123.675, 116.28, 103.53] h, w = rgb.shape[:2] @@ -104,8 +111,15 @@ def rescale_input(self, rgb, rgb_origin): pad_w = input_size[1] - w pad_h_half = pad_h // 2 pad_w_half = pad_w // 2 - rgb = cv2.copyMakeBorder(rgb, pad_h_half, pad_h - pad_h_half, pad_w_half, pad_w - pad_w_half, - cv2.BORDER_CONSTANT, value=padding) + rgb = cv2.copyMakeBorder( + rgb, + pad_h_half, + pad_h - pad_h_half, + pad_w_half, + pad_w - pad_w_half, + cv2.BORDER_CONSTANT, + value=padding, + ) self.pad_info = [pad_h_half, pad_h - pad_h_half, pad_w_half, pad_w - pad_w_half] #### normalize @@ -115,35 +129,41 @@ def rescale_input(self, rgb, rgb_origin): rgb = torch.div((rgb - mean), std) rgb = rgb[None, :, :, :].cuda() return rgb + def unpad_transform_depth(self, pred_depth): # un pad pred_depth = pred_depth.squeeze() - pred_depth = pred_depth[self.pad_info[0]: pred_depth.shape[0] - self.pad_info[1], - self.pad_info[2]: pred_depth.shape[1] - self.pad_info[3]] + pred_depth = pred_depth[ + self.pad_info[0] : pred_depth.shape[0] - self.pad_info[1], + self.pad_info[2] : pred_depth.shape[1] - self.pad_info[3], + ] # upsample to original size - pred_depth = torch.nn.functional.interpolate(pred_depth[None, None, :, :], self.rgb_origin.shape[:2], - mode='bilinear').squeeze() + pred_depth = torch.nn.functional.interpolate( + pred_depth[None, None, :, :], self.rgb_origin.shape[:2], mode="bilinear" + ).squeeze() ###################### canonical camera space ###################### #### de-canonical transform - canonical_to_real_scale = self.intrinsic_scaled[0] / 1000.0 # 1000.0 is the focal length of canonical camera + canonical_to_real_scale = ( + self.intrinsic_scaled[0] / 1000.0 + ) # 1000.0 is the focal length of canonical camera pred_depth = pred_depth * canonical_to_real_scale # now the depth is metric pred_depth = torch.clamp(pred_depth, 0, 1000) return pred_depth - """Set new intrinsic value.""" - def update_intrinsic(self, intrinsic): + + def update_intrinsic(self, intrinsic) -> None: self.intrinsic = intrinsic - def eval_predicted_depth(self, depth_file, pred_depth): + def eval_predicted_depth(self, depth_file, pred_depth) -> None: if depth_file is not None: gt_depth = cv2.imread(depth_file, -1) gt_depth = gt_depth / self.gt_depth_scale gt_depth = torch.from_numpy(gt_depth).float().cuda() assert gt_depth.shape == pred_depth.shape - mask = (gt_depth > 1e-8) + mask = gt_depth > 1e-8 abs_rel_err = (torch.abs(pred_depth[mask] - gt_depth[mask]) / gt_depth[mask]).mean() - print('abs_rel_err:', abs_rel_err.item()) \ No newline at end of file + print("abs_rel_err:", abs_rel_err.item()) diff --git a/dimos/models/embedding/__init__.py b/dimos/models/embedding/__init__.py new file mode 100644 index 0000000000..981e25e5c2 --- /dev/null +++ b/dimos/models/embedding/__init__.py @@ -0,0 +1,30 @@ +from dimos.models.embedding.base import Embedding, EmbeddingModel + +__all__ = [ + "Embedding", + "EmbeddingModel", +] + +# Optional: CLIP support +try: + from dimos.models.embedding.clip import CLIPEmbedding, CLIPModel + + __all__.extend(["CLIPEmbedding", "CLIPModel"]) +except ImportError: + pass + +# Optional: MobileCLIP support +try: + from dimos.models.embedding.mobileclip import MobileCLIPEmbedding, MobileCLIPModel + + __all__.extend(["MobileCLIPEmbedding", "MobileCLIPModel"]) +except ImportError: + pass + +# Optional: TorchReID support +try: + from dimos.models.embedding.treid import TorchReIDEmbedding, TorchReIDModel + + __all__.extend(["TorchReIDEmbedding", "TorchReIDModel"]) +except ImportError: + pass diff --git a/dimos/models/embedding/base.py b/dimos/models/embedding/base.py new file mode 100644 index 0000000000..99a8d8fd15 --- /dev/null +++ b/dimos/models/embedding/base.py @@ -0,0 +1,150 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from abc import ABC, abstractmethod +import time +from typing import TYPE_CHECKING, Generic, Optional, TypeVar + +import numpy as np +import torch + +from dimos.types.timestamped import Timestamped + +if TYPE_CHECKING: + from dimos.msgs.sensor_msgs import Image + + +class Embedding(Timestamped): + """Base class for embeddings with vector data. + + Supports both torch.Tensor (for GPU-accelerated comparisons) and np.ndarray. + Embeddings are kept as torch.Tensor on device by default for efficiency. + """ + + vector: torch.Tensor | np.ndarray + + def __init__(self, vector: torch.Tensor | np.ndarray, timestamp: float | None = None) -> None: + self.vector = vector + if timestamp: + self.timestamp = timestamp + else: + self.timestamp = time.time() + + def __matmul__(self, other: Embedding) -> float: + """Compute cosine similarity via @ operator.""" + if isinstance(self.vector, torch.Tensor): + other_tensor = other.to_torch(self.vector.device) + result = self.vector @ other_tensor + return result.item() + return float(self.vector @ other.to_numpy()) + + def to_numpy(self) -> np.ndarray: + """Convert to numpy array (moves to CPU if needed).""" + if isinstance(self.vector, torch.Tensor): + return self.vector.detach().cpu().numpy() + return self.vector + + def to_torch(self, device: str | torch.device | None = None) -> torch.Tensor: + """Convert to torch tensor on specified device.""" + if isinstance(self.vector, np.ndarray): + tensor = torch.from_numpy(self.vector) + return tensor.to(device) if device else tensor + + if device is not None and self.vector.device != torch.device(device): + return self.vector.to(device) + return self.vector + + def to_cpu(self) -> Embedding: + """Move embedding to CPU, returning self for chaining.""" + if isinstance(self.vector, torch.Tensor): + self.vector = self.vector.cpu() + return self + + +E = TypeVar("E", bound="Embedding") + + +class EmbeddingModel(ABC, Generic[E]): + """Abstract base class for embedding models supporting vision and language.""" + + device: str + normalize: bool = True + + @abstractmethod + def embed(self, *images: Image) -> E | list[E]: + """ + Embed one or more images. + Returns single Embedding if one image, list if multiple. + """ + pass + + @abstractmethod + def embed_text(self, *texts: str) -> E | list[E]: + """ + Embed one or more text strings. + Returns single Embedding if one text, list if multiple. + """ + pass + + def compare_one_to_many(self, query: E, candidates: list[E]) -> torch.Tensor: + """ + Efficiently compare one query against many candidates on GPU. + + Args: + query: Query embedding + candidates: List of candidate embeddings + + Returns: + torch.Tensor of similarities (N,) + """ + query_tensor = query.to_torch(self.device) + candidate_tensors = torch.stack([c.to_torch(self.device) for c in candidates]) + return query_tensor @ candidate_tensors.T + + def compare_many_to_many(self, queries: list[E], candidates: list[E]) -> torch.Tensor: + """ + Efficiently compare all queries against all candidates on GPU. + + Args: + queries: List of query embeddings + candidates: List of candidate embeddings + + Returns: + torch.Tensor of similarities (M, N) where M=len(queries), N=len(candidates) + """ + query_tensors = torch.stack([q.to_torch(self.device) for q in queries]) + candidate_tensors = torch.stack([c.to_torch(self.device) for c in candidates]) + return query_tensors @ candidate_tensors.T + + def query(self, query_emb: E, candidates: list[E], top_k: int = 5) -> list[tuple[int, float]]: + """ + Find top-k most similar candidates to query (GPU accelerated). + + Args: + query_emb: Query embedding + candidates: List of candidate embeddings + top_k: Number of top results to return + + Returns: + List of (index, similarity) tuples sorted by similarity (descending) + """ + similarities = self.compare_one_to_many(query_emb, candidates) + top_values, top_indices = similarities.topk(k=min(top_k, len(candidates))) + return [(idx.item(), val.item()) for idx, val in zip(top_indices, top_values, strict=False)] + + def warmup(self) -> None: + """Optional warmup method to pre-load model.""" + pass diff --git a/dimos/models/embedding/clip.py b/dimos/models/embedding/clip.py new file mode 100644 index 0000000000..23ab5e94f2 --- /dev/null +++ b/dimos/models/embedding/clip.py @@ -0,0 +1,122 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from PIL import Image as PILImage +import torch +import torch.nn.functional as F +from transformers import CLIPModel as HFCLIPModel, CLIPProcessor + +from dimos.models.embedding.base import Embedding, EmbeddingModel +from dimos.msgs.sensor_msgs import Image + +_CUDA_INITIALIZED = False + + +class CLIPEmbedding(Embedding): ... + + +class CLIPModel(EmbeddingModel[CLIPEmbedding]): + """CLIP embedding model for vision-language re-identification.""" + + def __init__( + self, + model_name: str = "openai/clip-vit-base-patch32", + device: str | None = None, + normalize: bool = False, + ) -> None: + """ + Initialize CLIP model. + + Args: + model_name: HuggingFace model name (e.g., "openai/clip-vit-base-patch32") + device: Device to run on (cuda/cpu), auto-detects if None + normalize: Whether to L2 normalize embeddings + """ + self.device = device or ("cuda" if torch.cuda.is_available() else "cpu") + self.normalize = normalize + + # Load model and processor + self.model = HFCLIPModel.from_pretrained(model_name).eval().to(self.device) + self.processor = CLIPProcessor.from_pretrained(model_name) + + def embed(self, *images: Image) -> CLIPEmbedding | list[CLIPEmbedding]: + """Embed one or more images. + + Returns embeddings as torch.Tensor on device for efficient GPU comparisons. + """ + # Convert to PIL images + pil_images = [PILImage.fromarray(img.to_opencv()) for img in images] + + # Process images + with torch.inference_mode(): + inputs = self.processor(images=pil_images, return_tensors="pt").to(self.device) + image_features = self.model.get_image_features(**inputs) + + if self.normalize: + image_features = F.normalize(image_features, dim=-1) + + # Create embeddings (keep as torch.Tensor on device) + embeddings = [] + for i, feat in enumerate(image_features): + timestamp = images[i].ts + embeddings.append(CLIPEmbedding(vector=feat, timestamp=timestamp)) + + return embeddings[0] if len(images) == 1 else embeddings + + def embed_text(self, *texts: str) -> CLIPEmbedding | list[CLIPEmbedding]: + """Embed one or more text strings. + + Returns embeddings as torch.Tensor on device for efficient GPU comparisons. + """ + with torch.inference_mode(): + inputs = self.processor(text=list(texts), return_tensors="pt", padding=True).to( + self.device + ) + text_features = self.model.get_text_features(**inputs) + + if self.normalize: + text_features = F.normalize(text_features, dim=-1) + + # Create embeddings (keep as torch.Tensor on device) + embeddings = [] + for feat in text_features: + embeddings.append(CLIPEmbedding(vector=feat)) + + return embeddings[0] if len(texts) == 1 else embeddings + + def warmup(self) -> None: + """Warmup the model with a dummy forward pass.""" + # WORKAROUND: HuggingFace CLIP fails with CUBLAS_STATUS_ALLOC_FAILED when it's + # the first model to use CUDA. Initialize CUDA context with a dummy operation. + # This only needs to happen once per process. + global _CUDA_INITIALIZED + if self.device == "cuda" and not _CUDA_INITIALIZED: + try: + # Initialize CUDA with a small matmul operation to setup cuBLAS properly + _ = torch.zeros(1, 1, device="cuda") @ torch.zeros(1, 1, device="cuda") + torch.cuda.synchronize() + _CUDA_INITIALIZED = True + except Exception: + # If initialization fails, continue anyway - the warmup might still work + pass + + dummy_image = torch.randn(1, 3, 224, 224).to(self.device) + dummy_text_inputs = self.processor(text=["warmup"], return_tensors="pt", padding=True).to( + self.device + ) + + with torch.inference_mode(): + # Use pixel_values directly for image warmup + self.model.get_image_features(pixel_values=dummy_image) + self.model.get_text_features(**dummy_text_inputs) diff --git a/dimos/models/embedding/embedding_models_disabled_tests.py b/dimos/models/embedding/embedding_models_disabled_tests.py new file mode 100644 index 0000000000..bb1f038410 --- /dev/null +++ b/dimos/models/embedding/embedding_models_disabled_tests.py @@ -0,0 +1,404 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pytest + +from dimos.msgs.sensor_msgs import Image +from dimos.utils.data import get_data + + +@pytest.fixture(scope="session", params=["clip", "mobileclip", "treid"]) +def embedding_model(request): + """Load embedding model once for all tests. Parametrized for different models.""" + if request.param == "mobileclip": + from dimos.models.embedding.mobileclip import MobileCLIPModel + + model_path = get_data("models_mobileclip") / "mobileclip2_s0.pt" + model = MobileCLIPModel(model_name="MobileCLIP2-S0", model_path=model_path) + elif request.param == "clip": + from dimos.models.embedding.clip import CLIPModel + + model = CLIPModel(model_name="openai/clip-vit-base-patch32") + elif request.param == "treid": + from dimos.models.embedding.treid import TorchReIDModel + + model = TorchReIDModel(model_name="osnet_x1_0") + else: + raise ValueError(f"Unknown model: {request.param}") + + model.warmup() + return model + + +@pytest.fixture(scope="session") +def test_image(): + """Load test image.""" + return Image.from_file(get_data("cafe.jpg")).to_rgb() + + +@pytest.mark.heavy +def test_single_image_embedding(embedding_model, test_image) -> None: + """Test embedding a single image.""" + embedding = embedding_model.embed(test_image) + + # Embedding should be torch.Tensor on device + import torch + + assert isinstance(embedding.vector, torch.Tensor), "Embedding should be torch.Tensor" + assert embedding.vector.device.type in ["cuda", "cpu"], "Should be on valid device" + + # Test conversion to numpy + vector_np = embedding.to_numpy() + print(f"\nEmbedding shape: {vector_np.shape}") + print(f"Embedding dtype: {vector_np.dtype}") + print(f"Embedding norm: {np.linalg.norm(vector_np):.4f}") + + assert vector_np.shape[0] > 0, "Embedding should have features" + assert np.isfinite(vector_np).all(), "Embedding should contain finite values" + + # Check L2 normalization + norm = np.linalg.norm(vector_np) + assert abs(norm - 1.0) < 0.01, f"Embedding should be L2 normalized, got norm={norm}" + + +@pytest.mark.heavy +def test_batch_image_embedding(embedding_model, test_image) -> None: + """Test embedding multiple images at once.""" + embeddings = embedding_model.embed(test_image, test_image, test_image) + + assert isinstance(embeddings, list), "Batch embedding should return list" + assert len(embeddings) == 3, "Should return 3 embeddings" + + # Check all embeddings are similar (same image) + sim_01 = embeddings[0] @ embeddings[1] + sim_02 = embeddings[0] @ embeddings[2] + + print(f"\nSimilarity between same images: {sim_01:.6f}, {sim_02:.6f}") + + assert sim_01 > 0.99, f"Same image embeddings should be very similar, got {sim_01}" + assert sim_02 > 0.99, f"Same image embeddings should be very similar, got {sim_02}" + + +@pytest.mark.heavy +def test_single_text_embedding(embedding_model) -> None: + """Test embedding a single text string.""" + import torch + + if not hasattr(embedding_model, "embed_text"): + pytest.skip("Model does not support text embeddings") + + embedding = embedding_model.embed_text("a cafe") + + # Should be torch.Tensor + assert isinstance(embedding.vector, torch.Tensor), "Text embedding should be torch.Tensor" + + vector_np = embedding.to_numpy() + print(f"\nText embedding shape: {vector_np.shape}") + print(f"Text embedding norm: {np.linalg.norm(vector_np):.4f}") + + assert vector_np.shape[0] > 0, "Text embedding should have features" + assert np.isfinite(vector_np).all(), "Text embedding should contain finite values" + + # Check L2 normalization + norm = np.linalg.norm(vector_np) + assert abs(norm - 1.0) < 0.01, f"Text embedding should be L2 normalized, got norm={norm}" + + +@pytest.mark.heavy +def test_batch_text_embedding(embedding_model) -> None: + """Test embedding multiple text strings at once.""" + import torch + + if not hasattr(embedding_model, "embed_text"): + pytest.skip("Model does not support text embeddings") + + embeddings = embedding_model.embed_text("a cafe", "a person", "a dog") + + assert isinstance(embeddings, list), "Batch text embedding should return list" + assert len(embeddings) == 3, "Should return 3 text embeddings" + + # All should be torch.Tensor and normalized + for i, emb in enumerate(embeddings): + assert isinstance(emb.vector, torch.Tensor), f"Embedding {i} should be torch.Tensor" + norm = np.linalg.norm(emb.to_numpy()) + assert abs(norm - 1.0) < 0.01, f"Text embedding {i} should be L2 normalized" + + +@pytest.mark.heavy +def test_text_image_similarity(embedding_model, test_image) -> None: + """Test cross-modal text-image similarity using @ operator.""" + if not hasattr(embedding_model, "embed_text"): + pytest.skip("Model does not support text embeddings") + + img_embedding = embedding_model.embed(test_image) + + # Embed text queries + queries = ["a cafe", "a person", "a car", "a dog", "potato", "food"] + text_embeddings = embedding_model.embed_text(*queries) + + # Compute similarities using @ operator + similarities = {} + for query, text_emb in zip(queries, text_embeddings, strict=False): + similarity = img_embedding @ text_emb + similarities[query] = similarity + print(f"\n'{query}': {similarity:.4f}") + + # Cafe image should match "a cafe" better than "a dog" + assert similarities["a cafe"] > similarities["a dog"], "Should recognize cafe scene" + assert similarities["a person"] > similarities["a car"], "Should detect people in cafe" + + +@pytest.mark.heavy +def test_cosine_distance(embedding_model, test_image) -> None: + """Test cosine distance computation (1 - similarity).""" + emb1 = embedding_model.embed(test_image) + emb2 = embedding_model.embed(test_image) + + # Similarity using @ operator + similarity = emb1 @ emb2 + + # Distance is 1 - similarity + distance = 1.0 - similarity + + print(f"\nSimilarity (same image): {similarity:.6f}") + print(f"Distance (same image): {distance:.6f}") + + assert similarity > 0.99, f"Same image should have high similarity, got {similarity}" + assert distance < 0.01, f"Same image should have low distance, got {distance}" + + +@pytest.mark.heavy +def test_query_functionality(embedding_model, test_image) -> None: + """Test query method for top-k retrieval.""" + if not hasattr(embedding_model, "embed_text"): + pytest.skip("Model does not support text embeddings") + + # Create a query and some candidates + query_text = embedding_model.embed_text("a cafe") + + # Create candidate embeddings + candidate_texts = ["a cafe", "a restaurant", "a person", "a dog", "a car"] + candidates = embedding_model.embed_text(*candidate_texts) + + # Query for top-3 + results = embedding_model.query(query_text, candidates, top_k=3) + + print("\nTop-3 results:") + for idx, sim in results: + print(f" {candidate_texts[idx]}: {sim:.4f}") + + assert len(results) == 3, "Should return top-3 results" + assert results[0][0] == 0, "Top match should be 'a cafe' itself" + assert results[0][1] > results[1][1], "Results should be sorted by similarity" + assert results[1][1] > results[2][1], "Results should be sorted by similarity" + + +@pytest.mark.heavy +def test_embedding_operator(embedding_model, test_image) -> None: + """Test that @ operator works on embeddings.""" + emb1 = embedding_model.embed(test_image) + emb2 = embedding_model.embed(test_image) + + # Use @ operator + similarity = emb1 @ emb2 + + assert isinstance(similarity, float), "@ operator should return float" + assert 0.0 <= similarity <= 1.0, "Cosine similarity should be in [0, 1]" + assert similarity > 0.99, "Same image should have similarity near 1.0" + + +@pytest.mark.heavy +def test_warmup(embedding_model) -> None: + """Test that warmup runs without error.""" + # Warmup is already called in fixture, but test it explicitly + embedding_model.warmup() + # Just verify no exceptions raised + assert True + + +@pytest.mark.heavy +def test_compare_one_to_many(embedding_model, test_image) -> None: + """Test GPU-accelerated one-to-many comparison.""" + import torch + + # Create query and gallery + query_emb = embedding_model.embed(test_image) + gallery_embs = embedding_model.embed(test_image, test_image, test_image) + + # Compare on GPU + similarities = embedding_model.compare_one_to_many(query_emb, gallery_embs) + + print(f"\nOne-to-many similarities: {similarities}") + + # Should return torch.Tensor + assert isinstance(similarities, torch.Tensor), "Should return torch.Tensor" + assert similarities.shape == (3,), "Should have 3 similarities" + assert similarities.device.type in ["cuda", "cpu"], "Should be on device" + + # All should be ~1.0 (same image) + similarities_np = similarities.cpu().numpy() + assert np.all(similarities_np > 0.99), "Same images should have similarity ~1.0" + + +@pytest.mark.heavy +def test_compare_many_to_many(embedding_model) -> None: + """Test GPU-accelerated many-to-many comparison.""" + import torch + + if not hasattr(embedding_model, "embed_text"): + pytest.skip("Model does not support text embeddings") + + # Create queries and candidates + queries = embedding_model.embed_text("a cafe", "a person") + candidates = embedding_model.embed_text("a cafe", "a restaurant", "a dog") + + # Compare on GPU + similarities = embedding_model.compare_many_to_many(queries, candidates) + + print(f"\nMany-to-many similarities:\n{similarities}") + + # Should return torch.Tensor + assert isinstance(similarities, torch.Tensor), "Should return torch.Tensor" + assert similarities.shape == (2, 3), "Should be (2, 3) similarity matrix" + assert similarities.device.type in ["cuda", "cpu"], "Should be on device" + + # First query should match first candidate best + similarities_np = similarities.cpu().numpy() + assert similarities_np[0, 0] > similarities_np[0, 2], "Cafe should match cafe better than dog" + + +@pytest.mark.heavy +def test_gpu_query_performance(embedding_model, test_image) -> None: + """Test that query method uses GPU acceleration.""" + # Create a larger gallery + gallery_size = 20 + gallery_images = [test_image] * gallery_size + gallery_embs = embedding_model.embed(*gallery_images) + + query_emb = embedding_model.embed(test_image) + + # Query should use GPU-accelerated comparison + results = embedding_model.query(query_emb, gallery_embs, top_k=5) + + print(f"\nTop-5 results from gallery of {gallery_size}") + for idx, sim in results: + print(f" Index {idx}: {sim:.4f}") + + assert len(results) == 5, "Should return top-5 results" + # All should be high similarity (same image, allow some variation for image preprocessing) + for idx, sim in results: + assert sim > 0.90, f"Same images should have high similarity, got {sim}" + + +@pytest.mark.heavy +def test_embedding_performance(embedding_model) -> None: + """Measure embedding performance over multiple real video frames.""" + import time + + from dimos.utils.testing import TimedSensorReplay + + # Load actual video frames + data_dir = "unitree_go2_lidar_corrected" + get_data(data_dir) + + video_replay = TimedSensorReplay(f"{data_dir}/video") + + # Collect 10 real frames from the video + test_images = [] + for _ts, frame in video_replay.iterate_ts(duration=1.0): + test_images.append(frame.to_rgb()) + if len(test_images) >= 10: + break + + if len(test_images) < 10: + pytest.skip(f"Not enough video frames found (got {len(test_images)})") + + # Measure single image embedding time + times = [] + for img in test_images: + start = time.perf_counter() + _ = embedding_model.embed(img) + end = time.perf_counter() + elapsed_ms = (end - start) * 1000 + times.append(elapsed_ms) + + # Calculate statistics + avg_time = sum(times) / len(times) + min_time = min(times) + max_time = max(times) + std_time = (sum((t - avg_time) ** 2 for t in times) / len(times)) ** 0.5 + + print("\n" + "=" * 60) + print("Embedding Performance Statistics:") + print("=" * 60) + print(f"Number of images: {len(test_images)}") + print(f"Average time: {avg_time:.2f} ms") + print(f"Min time: {min_time:.2f} ms") + print(f"Max time: {max_time:.2f} ms") + print(f"Std dev: {std_time:.2f} ms") + print(f"Throughput: {1000 / avg_time:.1f} images/sec") + print("=" * 60) + + # Also test batch embedding performance + start = time.perf_counter() + batch_embeddings = embedding_model.embed(*test_images) + end = time.perf_counter() + batch_time = (end - start) * 1000 + batch_per_image = batch_time / len(test_images) + + print("\nBatch Embedding Performance:") + print(f"Total batch time: {batch_time:.2f} ms") + print(f"Time per image (batched): {batch_per_image:.2f} ms") + print(f"Batch throughput: {1000 / batch_per_image:.1f} images/sec") + print(f"Speedup vs single: {avg_time / batch_per_image:.2f}x") + print("=" * 60) + + # Verify embeddings are valid + assert len(batch_embeddings) == len(test_images) + assert all(e.vector is not None for e in batch_embeddings) + + # Sanity check: verify embeddings are meaningful by testing text-image similarity + # Skip for models that don't support text embeddings + if hasattr(embedding_model, "embed_text"): + print("\n" + "=" * 60) + print("Sanity Check: Text-Image Similarity on First Frame") + print("=" * 60) + first_frame_emb = batch_embeddings[0] + + # Test common object/scene queries + test_queries = [ + "indoor scene", + "outdoor scene", + "a person", + "a dog", + "a robot", + "grass and trees", + "furniture", + "a car", + ] + + text_embeddings = embedding_model.embed_text(*test_queries) + similarities = [] + for query, text_emb in zip(test_queries, text_embeddings, strict=False): + sim = first_frame_emb @ text_emb + similarities.append((query, sim)) + + # Sort by similarity + similarities.sort(key=lambda x: x[1], reverse=True) + + print("Top matching concepts:") + for query, sim in similarities[:5]: + print(f" '{query}': {sim:.4f}") + print("=" * 60) diff --git a/dimos/models/embedding/mobileclip.py b/dimos/models/embedding/mobileclip.py new file mode 100644 index 0000000000..8ddefd3c87 --- /dev/null +++ b/dimos/models/embedding/mobileclip.py @@ -0,0 +1,112 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + +import open_clip +from PIL import Image as PILImage +import torch +import torch.nn.functional as F + +from dimos.models.embedding.base import Embedding, EmbeddingModel +from dimos.msgs.sensor_msgs import Image + + +class MobileCLIPEmbedding(Embedding): ... + + +class MobileCLIPModel(EmbeddingModel[MobileCLIPEmbedding]): + """MobileCLIP embedding model for vision-language re-identification.""" + + def __init__( + self, + model_name: str = "MobileCLIP2-S4", + model_path: Path | str | None = None, + device: str | None = None, + normalize: bool = True, + ) -> None: + """ + Initialize MobileCLIP model. + + Args: + model_name: Name of the model architecture + model_path: Path to pretrained weights + device: Device to run on (cuda/cpu), auto-detects if None + normalize: Whether to L2 normalize embeddings + """ + if not OPEN_CLIP_AVAILABLE: + raise ImportError( + "open_clip is required for MobileCLIPModel. " + "Install it with: pip install open-clip-torch" + ) + + self.device = device or ("cuda" if torch.cuda.is_available() else "cpu") + self.normalize = normalize + + # Load model + pretrained = str(model_path) if model_path else None + self.model, _, self.preprocess = open_clip.create_model_and_transforms( + model_name, pretrained=pretrained + ) + self.tokenizer = open_clip.get_tokenizer(model_name) + self.model = self.model.eval().to(self.device) + + def embed(self, *images: Image) -> MobileCLIPEmbedding | list[MobileCLIPEmbedding]: + """Embed one or more images. + + Returns embeddings as torch.Tensor on device for efficient GPU comparisons. + """ + # Convert to PIL images + pil_images = [PILImage.fromarray(img.to_opencv()) for img in images] + + # Preprocess and batch + with torch.inference_mode(): + batch = torch.stack([self.preprocess(img) for img in pil_images]).to(self.device) + feats = self.model.encode_image(batch) + if self.normalize: + feats = F.normalize(feats, dim=-1) + + # Create embeddings (keep as torch.Tensor on device) + embeddings = [] + for i, feat in enumerate(feats): + timestamp = images[i].ts + embeddings.append(MobileCLIPEmbedding(vector=feat, timestamp=timestamp)) + + return embeddings[0] if len(images) == 1 else embeddings + + def embed_text(self, *texts: str) -> MobileCLIPEmbedding | list[MobileCLIPEmbedding]: + """Embed one or more text strings. + + Returns embeddings as torch.Tensor on device for efficient GPU comparisons. + """ + with torch.inference_mode(): + text_tokens = self.tokenizer(list(texts)).to(self.device) + feats = self.model.encode_text(text_tokens) + if self.normalize: + feats = F.normalize(feats, dim=-1) + + # Create embeddings (keep as torch.Tensor on device) + embeddings = [] + for feat in feats: + embeddings.append(MobileCLIPEmbedding(vector=feat)) + + return embeddings[0] if len(texts) == 1 else embeddings + + def warmup(self) -> None: + """Warmup the model with a dummy forward pass.""" + dummy_image = torch.randn(1, 3, 224, 224).to(self.device) + dummy_text = self.tokenizer(["warmup"]).to(self.device) + with torch.inference_mode(): + self.model.encode_image(dummy_image) + self.model.encode_text(dummy_text) diff --git a/dimos/models/embedding/treid.py b/dimos/models/embedding/treid.py new file mode 100644 index 0000000000..b00ad11250 --- /dev/null +++ b/dimos/models/embedding/treid.py @@ -0,0 +1,125 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + +import torch +import torch.nn.functional as F +from torchreid import utils as torchreid_utils + +from dimos.models.embedding.base import Embedding, EmbeddingModel +from dimos.msgs.sensor_msgs import Image + +_CUDA_INITIALIZED = False + + +class TorchReIDEmbedding(Embedding): ... + + +class TorchReIDModel(EmbeddingModel[TorchReIDEmbedding]): + """TorchReID embedding model for person re-identification.""" + + def __init__( + self, + model_name: str = "se_resnext101_32x4d", + model_path: Path | str | None = None, + device: str | None = None, + normalize: bool = False, + ) -> None: + """ + Initialize TorchReID model. + + Args: + model_name: Name of the model architecture (e.g., "osnet_x1_0", "osnet_x0_75") + model_path: Path to pretrained weights (.pth.tar file) + device: Device to run on (cuda/cpu), auto-detects if None + normalize: Whether to L2 normalize embeddings + """ + if not TORCHREID_AVAILABLE: + raise ImportError( + "torchreid is required for TorchReIDModel. Install it with: pip install torchreid" + ) + + self.device = device or ("cuda" if torch.cuda.is_available() else "cpu") + self.normalize = normalize + + # Load model using torchreid's FeatureExtractor + model_path_str = str(model_path) if model_path else "" + self.extractor = torchreid_utils.FeatureExtractor( + model_name=model_name, + model_path=model_path_str, + device=self.device, + ) + + def embed(self, *images: Image) -> TorchReIDEmbedding | list[TorchReIDEmbedding]: + """Embed one or more images. + + Returns embeddings as torch.Tensor on device for efficient GPU comparisons. + """ + # Convert to numpy arrays - torchreid expects numpy arrays or file paths + np_images = [img.to_opencv() for img in images] + + # Extract features + with torch.inference_mode(): + features = self.extractor(np_images) + + # torchreid may return either numpy array or torch tensor depending on configuration + if isinstance(features, torch.Tensor): + features_tensor = features.to(self.device) + else: + features_tensor = torch.from_numpy(features).to(self.device) + + if self.normalize: + features_tensor = F.normalize(features_tensor, dim=-1) + + # Create embeddings (keep as torch.Tensor on device) + embeddings = [] + for i, feat in enumerate(features_tensor): + timestamp = images[i].ts + embeddings.append(TorchReIDEmbedding(vector=feat, timestamp=timestamp)) + + return embeddings[0] if len(images) == 1 else embeddings + + def embed_text(self, *texts: str) -> TorchReIDEmbedding | list[TorchReIDEmbedding]: + """Text embedding not supported for ReID models. + + TorchReID models are vision-only person re-identification models + and do not support text embeddings. + """ + raise NotImplementedError( + "TorchReID models are vision-only and do not support text embeddings. " + "Use CLIP or MobileCLIP for text-image similarity." + ) + + def warmup(self) -> None: + """Warmup the model with a dummy forward pass.""" + # WORKAROUND: TorchReID can fail with CUBLAS errors when it's the first model to use CUDA. + # Initialize CUDA context with a dummy operation. This only needs to happen once per process. + global _CUDA_INITIALIZED + if self.device == "cuda" and not _CUDA_INITIALIZED: + try: + # Initialize CUDA with a small matmul operation to setup cuBLAS properly + _ = torch.zeros(1, 1, device="cuda") @ torch.zeros(1, 1, device="cuda") + torch.cuda.synchronize() + _CUDA_INITIALIZED = True + except Exception: + # If initialization fails, continue anyway - the warmup might still work + pass + + # Create a dummy 256x128 image (typical person ReID input size) as numpy array + import numpy as np + + dummy_image = np.random.randint(0, 256, (256, 128, 3), dtype=np.uint8) + with torch.inference_mode(): + _ = self.extractor([dummy_image]) diff --git a/dimos/models/labels/llava-34b.py b/dimos/models/labels/llava-34b.py index 3290409950..52e28ac24e 100644 --- a/dimos/models/labels/llava-34b.py +++ b/dimos/models/labels/llava-34b.py @@ -18,34 +18,53 @@ # llava v1.6 from llama_cpp import Llama from llama_cpp.llama_chat_format import Llava15ChatHandler - from vqasynth.datasets.utils import image_to_base64_data_uri + class Llava: - def __init__(self, mmproj=f"{os.getcwd()}/models/mmproj-model-f16.gguf", model_path=f"{os.getcwd()}/models/llava-v1.6-34b.Q4_K_M.gguf", gpu=True): + def __init__( + self, + mmproj: str=f"{os.getcwd()}/models/mmproj-model-f16.gguf", + model_path: str=f"{os.getcwd()}/models/llava-v1.6-34b.Q4_K_M.gguf", + gpu: bool=True, + ) -> None: chat_handler = Llava15ChatHandler(clip_model_path=mmproj, verbose=True) n_gpu_layers = 0 if gpu: - n_gpu_layers = -1 - self.llm = Llama(model_path=model_path, chat_handler=chat_handler, n_ctx=2048, logits_all=True, n_gpu_layers=n_gpu_layers) + n_gpu_layers = -1 + self.llm = Llama( + model_path=model_path, + chat_handler=chat_handler, + n_ctx=2048, + logits_all=True, + n_gpu_layers=n_gpu_layers, + ) - def run_inference(self, image, prompt, return_json=True): + def run_inference(self, image, prompt: str, return_json: bool=True): data_uri = image_to_base64_data_uri(image) res = self.llm.create_chat_completion( - messages = [ - {"role": "system", "content": "You are an assistant who perfectly describes images."}, - { - "role": "user", - "content": [ - {"type": "image_url", "image_url": {"url": data_uri}}, - {"type" : "text", "text": prompt} - ] - } - ] - ) + messages=[ + { + "role": "system", + "content": "You are an assistant who perfectly describes images.", + }, + { + "role": "user", + "content": [ + {"type": "image_url", "image_url": {"url": data_uri}}, + {"type": "text", "text": prompt}, + ], + }, + ] + ) if return_json: - - return list(set(self.extract_descriptions_from_incomplete_json(res["choices"][0]["message"]["content"]))) + return list( + set( + self.extract_descriptions_from_incomplete_json( + res["choices"][0]["message"]["content"] + ) + ) + ) return res["choices"][0]["message"]["content"] @@ -53,15 +72,19 @@ def extract_descriptions_from_incomplete_json(self, json_like_str): last_object_idx = json_like_str.rfind(',"object') if last_object_idx != -1: - json_str = json_like_str[:last_object_idx] + '}' + json_str = json_like_str[:last_object_idx] + "}" else: json_str = json_like_str.strip() - if not json_str.endswith('}'): - json_str += '}' + if not json_str.endswith("}"): + json_str += "}" try: json_obj = json.loads(json_str) - descriptions = [details['description'].replace(".","") for key, details in json_obj.items() if 'description' in details] + descriptions = [ + details["description"].replace(".", "") + for key, details in json_obj.items() + if "description" in details + ] return descriptions except json.JSONDecodeError as e: diff --git a/dimos/models/manipulation/__init__.py b/dimos/models/manipulation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/dimos/models/manipulation/contact_graspnet_pytorch/README.md b/dimos/models/manipulation/contact_graspnet_pytorch/README.md new file mode 100644 index 0000000000..bf95fa39cd --- /dev/null +++ b/dimos/models/manipulation/contact_graspnet_pytorch/README.md @@ -0,0 +1,52 @@ +# ContactGraspNet PyTorch Module + +This module provides a PyTorch implementation of ContactGraspNet for robotic grasping on dimOS. + +## Setup Instructions + +### 1. Install Required Dependencies + +Install the manipulation extras from the main repository: + +```bash +# From the root directory of the dimos repository +pip install -e ".[manipulation]" +``` + +This will install all the necessary dependencies for using the contact_graspnet_pytorch module, including: +- PyTorch +- Open3D +- Other manipulation-specific dependencies + +### 2. Testing the Module + +To test that the module is properly installed and functioning: + +```bash +# From the root directory of the dimos repository +pytest -s dimos/models/manipulation/contact_graspnet_pytorch/test_contact_graspnet.py +``` + +The test will verify that: +- The model can be loaded +- Inference runs correctly +- Grasping outputs are generated as expected + +### 3. Using in Your Code + +Reference ```inference.py``` for usage example. + +### Troubleshooting + +If you encounter issues with imports or missing dependencies: + +1. Verify that the manipulation extras are properly installed: + ```python + import contact_graspnet_pytorch + print("Module loaded successfully!") + ``` + +2. If LFS data files are missing, ensure Git LFS is installed and initialized: + ```bash + git lfs pull + ``` \ No newline at end of file diff --git a/dimos/models/manipulation/contact_graspnet_pytorch/inference.py b/dimos/models/manipulation/contact_graspnet_pytorch/inference.py new file mode 100644 index 0000000000..4241392d8e --- /dev/null +++ b/dimos/models/manipulation/contact_graspnet_pytorch/inference.py @@ -0,0 +1,118 @@ +import argparse +import glob +import os + +from contact_graspnet_pytorch import config_utils +from contact_graspnet_pytorch.checkpoints import CheckpointIO +from contact_graspnet_pytorch.contact_grasp_estimator import GraspEstimator +from contact_graspnet_pytorch.data import load_available_input_data +from contact_graspnet_pytorch.visualization_utils_o3d import show_image, visualize_grasps +import numpy as np +import torch + +from dimos.utils.data import get_data + + +def inference(global_config, + ckpt_dir, + input_paths, + local_regions: bool=True, + filter_grasps: bool=True, + skip_border_objects: bool=False, + z_range = None, + forward_passes: int=1, + K=None,): + """ + Predict 6-DoF grasp distribution for given model and input data + + :param global_config: config.yaml from checkpoint directory + :param checkpoint_dir: checkpoint directory + :param input_paths: .png/.npz/.npy file paths that contain depth/pointcloud and optionally intrinsics/segmentation/rgb + :param K: Camera Matrix with intrinsics to convert depth to point cloud + :param local_regions: Crop 3D local regions around given segments. + :param skip_border_objects: When extracting local_regions, ignore segments at depth map boundary. + :param filter_grasps: Filter and assign grasp contacts according to segmap. + :param segmap_id: only return grasps from specified segmap_id. + :param z_range: crop point cloud at a minimum/maximum z distance from camera to filter out outlier points. Default: [0.2, 1.8] m + :param forward_passes: Number of forward passes to run on each point cloud. Default: 1 + """ + # Build the model + if z_range is None: + z_range = [0.2, 1.8] + grasp_estimator = GraspEstimator(global_config) + + # Load the weights + model_checkpoint_dir = get_data(ckpt_dir) + checkpoint_io = CheckpointIO(checkpoint_dir=model_checkpoint_dir, model=grasp_estimator.model) + try: + checkpoint_io.load('model.pt') + except FileExistsError: + print('No model checkpoint found') + + + os.makedirs('results', exist_ok=True) + + # Process example test scenes + for p in glob.glob(input_paths): + print('Loading ', p) + + pc_segments = {} + segmap, rgb, depth, cam_K, pc_full, pc_colors = load_available_input_data(p, K=K) + + if segmap is None and (local_regions or filter_grasps): + raise ValueError('Need segmentation map to extract local regions or filter grasps') + + if pc_full is None: + print('Converting depth to point cloud(s)...') + pc_full, pc_segments, pc_colors = grasp_estimator.extract_point_clouds(depth, cam_K, segmap=segmap, rgb=rgb, + skip_border_objects=skip_border_objects, + z_range=z_range) + + print(pc_full.shape) + + print('Generating Grasps...') + pred_grasps_cam, scores, contact_pts, _ = grasp_estimator.predict_scene_grasps(pc_full, + pc_segments=pc_segments, + local_regions=local_regions, + filter_grasps=filter_grasps, + forward_passes=forward_passes) + + # Save results + np.savez('results/predictions_{}'.format(os.path.basename(p.replace('png','npz').replace('npy','npz'))), + pc_full=pc_full, pred_grasps_cam=pred_grasps_cam, scores=scores, contact_pts=contact_pts, pc_colors=pc_colors) + + # Visualize results + # show_image(rgb, segmap) + # visualize_grasps(pc_full, pred_grasps_cam, scores, plot_opencv_cam=True, pc_colors=pc_colors) + + if not glob.glob(input_paths): + print('No files found: ', input_paths) + +if __name__ == "__main__": + + parser = argparse.ArgumentParser() + parser.add_argument('--ckpt_dir', default='models_contact_graspnet', help='Log dir') + parser.add_argument('--np_path', default='test_data/7.npy', help='Input data: npz/npy file with keys either "depth" & camera matrix "K" or just point cloud "pc" in meters. Optionally, a 2D "segmap"') + parser.add_argument('--K', default=None, help='Flat Camera Matrix, pass as "[fx, 0, cx, 0, fy, cy, 0, 0 ,1]"') + parser.add_argument('--z_range', default=[0.2,1.8], help='Z value threshold to crop the input point cloud') + parser.add_argument('--local_regions', action='store_true', default=True, help='Crop 3D local regions around given segments.') + parser.add_argument('--filter_grasps', action='store_true', default=True, help='Filter grasp contacts according to segmap.') + parser.add_argument('--skip_border_objects', action='store_true', default=False, help='When extracting local_regions, ignore segments at depth map boundary.') + parser.add_argument('--forward_passes', type=int, default=1, help='Run multiple parallel forward passes to mesh_utils more potential contact points.') + parser.add_argument('--arg_configs', nargs="*", type=str, default=[], help='overwrite config parameters') + FLAGS = parser.parse_args() + + global_config = config_utils.load_config(FLAGS.ckpt_dir, batch_size=FLAGS.forward_passes, arg_configs=FLAGS.arg_configs) + + print(str(global_config)) + print(f'pid: {os.getpid()!s}') + + inference(global_config, + FLAGS.ckpt_dir, + FLAGS.np_path, + local_regions=FLAGS.local_regions, + filter_grasps=FLAGS.filter_grasps, + skip_border_objects=FLAGS.skip_border_objects, + z_range=eval(str(FLAGS.z_range)), + forward_passes=FLAGS.forward_passes, + K=eval(str(FLAGS.K))) diff --git a/dimos/models/manipulation/contact_graspnet_pytorch/test_contact_graspnet.py b/dimos/models/manipulation/contact_graspnet_pytorch/test_contact_graspnet.py new file mode 100644 index 0000000000..b006c98603 --- /dev/null +++ b/dimos/models/manipulation/contact_graspnet_pytorch/test_contact_graspnet.py @@ -0,0 +1,73 @@ +import glob +import importlib.util +import os +import sys + +import numpy as np +import pytest + + +def is_manipulation_installed() -> bool: + """Check if the manipulation extras are installed.""" + try: + import contact_graspnet_pytorch + return True + except ImportError: + return False + +@pytest.mark.skipif(not is_manipulation_installed(), + reason="This test requires 'pip install .[manipulation]' to be run") +def test_contact_graspnet_inference() -> None: + """Test contact graspnet inference with local regions and filter grasps.""" + # Skip test if manipulation dependencies not installed + if not is_manipulation_installed(): + pytest.skip("contact_graspnet_pytorch not installed. Run 'pip install .[manipulation]' first.") + return + + try: + from contact_graspnet_pytorch import config_utils + + from dimos.models.manipulation.contact_graspnet_pytorch.inference import inference + from dimos.utils.data import get_data + except ImportError: + pytest.skip("Required modules could not be imported. Make sure you have run 'pip install .[manipulation]'.") + return + + # Test data path - use the default test data path + test_data_path = os.path.join(get_data("models_contact_graspnet"), "test_data/0.npy") + + # Check if test data exists + test_files = glob.glob(test_data_path) + if not test_files: + pytest.fail(f"No test data found at {test_data_path}") + + # Load config with default values + ckpt_dir = 'models_contact_graspnet' + global_config = config_utils.load_config(ckpt_dir, batch_size=1) + + # Run inference function with the same params as the command line + result_files_before = glob.glob('results/predictions_*.npz') + + inference( + global_config=global_config, + ckpt_dir=ckpt_dir, + input_paths=test_data_path, + local_regions=True, + filter_grasps=True, + skip_border_objects=False, + z_range=[0.2, 1.8], + forward_passes=1, + K=None + ) + + # Verify results were created + result_files_after = glob.glob('results/predictions_*.npz') + assert len(result_files_after) >= len(result_files_before), "No result files were generated" + + # Load at least one result file and verify it contains expected data + if result_files_after: + latest_result = sorted(result_files_after)[-1] + result_data = np.load(latest_result, allow_pickle=True) + expected_keys = ['pc_full', 'pred_grasps_cam', 'scores', 'contact_pts', 'pc_colors'] + for key in expected_keys: + assert key in result_data.files, f"Expected key '{key}' not found in results" diff --git a/dimos/models/pointcloud/pointcloud_utils.py b/dimos/models/pointcloud/pointcloud_utils.py index 54cbd5f319..33b4b59607 100644 --- a/dimos/models/pointcloud/pointcloud_utils.py +++ b/dimos/models/pointcloud/pointcloud_utils.py @@ -12,17 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pickle +import random + import numpy as np import open3d as o3d -import random -def save_pointcloud(pcd, file_path): + +def save_pointcloud(pcd, file_path) -> None: """ Save a point cloud to a file using Open3D. """ o3d.io.write_point_cloud(file_path, pcd) + def restore_pointclouds(pointcloud_paths): restored_pointclouds = [] for path in pointcloud_paths: @@ -34,20 +36,28 @@ def create_point_cloud_from_rgbd(rgb_image, depth_image, intrinsic_parameters): rgbd_image = o3d.geometry.RGBDImage.create_from_color_and_depth( o3d.geometry.Image(rgb_image), o3d.geometry.Image(depth_image), - depth_scale=0.125, #1000.0, - depth_trunc=10.0, #10.0, - convert_rgb_to_intensity=False + depth_scale=0.125, # 1000.0, + depth_trunc=10.0, # 10.0, + convert_rgb_to_intensity=False, ) intrinsic = o3d.camera.PinholeCameraIntrinsic() - intrinsic.set_intrinsics(intrinsic_parameters['width'], intrinsic_parameters['height'], - intrinsic_parameters['fx'], intrinsic_parameters['fy'], - intrinsic_parameters['cx'], intrinsic_parameters['cy']) + intrinsic.set_intrinsics( + intrinsic_parameters["width"], + intrinsic_parameters["height"], + intrinsic_parameters["fx"], + intrinsic_parameters["fy"], + intrinsic_parameters["cx"], + intrinsic_parameters["cy"], + ) pcd = o3d.geometry.PointCloud.create_from_rgbd_image(rgbd_image, intrinsic) return pcd -def canonicalize_point_cloud(pcd, canonicalize_threshold=0.3): + +def canonicalize_point_cloud(pcd, canonicalize_threshold: float=0.3): # Segment the largest plane, assumed to be the floor - plane_model, inliers = pcd.segment_plane(distance_threshold=0.01, ransac_n=3, num_iterations=1000) + plane_model, inliers = pcd.segment_plane( + distance_threshold=0.01, ransac_n=3, num_iterations=1000 + ) canonicalized = False if len(inliers) / len(pcd.points) > canonicalize_threshold: @@ -75,9 +85,9 @@ def canonicalize_point_cloud(pcd, canonicalize_threshold=0.3): pcd.transform(transformation) # Additional 180-degree rotation around the Z-axis - rotation_z_180 = np.array([[np.cos(np.pi), -np.sin(np.pi), 0], - [np.sin(np.pi), np.cos(np.pi), 0], - [0, 0, 1]]) + rotation_z_180 = np.array( + [[np.cos(np.pi), -np.sin(np.pi), 0], [np.sin(np.pi), np.cos(np.pi), 0], [0, 0, 1]] + ) pcd.rotate(rotation_z_180, center=(0, 0, 0)) return pcd, canonicalized, transformation @@ -86,7 +96,7 @@ def canonicalize_point_cloud(pcd, canonicalize_threshold=0.3): # Distance calculations -def human_like_distance(distance_meters): +def human_like_distance(distance_meters) -> str: # Define the choices with units included, focusing on the 0.1 to 10 meters range if distance_meters < 1: # For distances less than 1 meter choices = [ @@ -141,6 +151,7 @@ def human_like_distance(distance_meters): # Fallback to the last choice if something goes wrong return f"{choices[-1][0]} {choices[-1][1]}" + def calculate_distances_between_point_clouds(A, B): dist_pcd1_to_pcd2 = np.asarray(A.compute_point_cloud_distance(B)) dist_pcd2_to_pcd1 = np.asarray(B.compute_point_cloud_distance(A)) @@ -148,12 +159,14 @@ def calculate_distances_between_point_clouds(A, B): avg_dist = np.mean(combined_distances) return human_like_distance(avg_dist) + def calculate_centroid(pcd): """Calculate the centroid of a point cloud.""" points = np.asarray(pcd.points) centroid = np.mean(points, axis=0) return centroid + def calculate_relative_positions(centroids): """Calculate the relative positions between centroids of point clouds.""" num_centroids = len(centroids) @@ -164,14 +177,13 @@ def calculate_relative_positions(centroids): relative_vector = centroids[j] - centroids[i] distance = np.linalg.norm(relative_vector) - relative_positions_info.append({ - 'pcd_pair': (i, j), - 'relative_vector': relative_vector, - 'distance': distance - }) + relative_positions_info.append( + {"pcd_pair": (i, j), "relative_vector": relative_vector, "distance": distance} + ) return relative_positions_info + def get_bounding_box_height(pcd): """ Compute the height of the bounding box for a given point cloud. @@ -185,6 +197,7 @@ def get_bounding_box_height(pcd): aabb = pcd.get_axis_aligned_bounding_box() return aabb.get_extent()[1] # Assuming the Y-axis is the up-direction + def compare_bounding_box_height(pcd_i, pcd_j): """ Compare the bounding box heights of two point clouds. diff --git a/dimos/models/qwen/video_query.py b/dimos/models/qwen/video_query.py index 9ba73dbece..0f8a3b8f9c 100644 --- a/dimos/models/qwen/video_query.py +++ b/dimos/models/qwen/video_query.py @@ -1,9 +1,10 @@ """Utility functions for one-off video frame queries using Qwen model.""" +import json import os -import threading +from typing import Optional, Tuple + import numpy as np -from typing import Optional from openai import OpenAI from reactivex import Observable, operators as ops from reactivex.subject import Subject @@ -11,26 +12,27 @@ from dimos.agents.agent import OpenAIAgent from dimos.agents.tokenizer.huggingface_tokenizer import HuggingFaceTokenizer from dimos.utils.threadpool import get_scheduler -import json + +BBox = tuple[float, float, float, float] # (x1, y1, x2, y2) def query_single_frame_observable( video_observable: Observable, query: str, - api_key: Optional[str] = None, - model_name: str = "qwen2.5-vl-72b-instruct" + api_key: str | None = None, + model_name: str = "qwen2.5-vl-72b-instruct", ) -> Observable: """Process a single frame from a video observable with Qwen model. - + Args: video_observable: An observable that emits video frames query: The query to ask about the frame api_key: Alibaba API key. If None, will try to get from ALIBABA_API_KEY env var model_name: The Qwen model to use. Defaults to qwen2.5-vl-72b-instruct - + Returns: Observable: An observable that emits a single response string - + Example: ```python video_obs = video_provider.capture_video_as_observable() @@ -40,19 +42,21 @@ def query_single_frame_observable( ``` """ # Get API key from env if not provided - api_key = api_key or os.getenv('ALIBABA_API_KEY') + api_key = api_key or os.getenv("ALIBABA_API_KEY") if not api_key: - raise ValueError("Alibaba API key must be provided or set in ALIBABA_API_KEY environment variable") + raise ValueError( + "Alibaba API key must be provided or set in ALIBABA_API_KEY environment variable" + ) # Create Qwen client qwen_client = OpenAI( - base_url='https://dashscope-intl.aliyuncs.com/compatible-mode/v1', + base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1", api_key=api_key, ) # Create response subject response_subject = Subject() - + # Create temporary agent for processing agent = OpenAIAgent( dev_name="QwenSingleFrameAgent", @@ -65,61 +69,60 @@ def query_single_frame_observable( ) # Take only first frame - single_frame = video_observable.pipe( - ops.take(1) - ) + single_frame = video_observable.pipe(ops.take(1)) # Subscribe to frame processing and forward response to our subject agent.subscribe_to_image_processing(single_frame) - + # Forward agent responses to our response subject agent.get_response_observable().subscribe( on_next=lambda x: response_subject.on_next(x), on_error=lambda e: response_subject.on_error(e), - on_completed=lambda: response_subject.on_completed() + on_completed=lambda: response_subject.on_completed(), ) # Clean up agent when response subject completes - response_subject.subscribe( - on_completed=lambda: agent.dispose_all() - ) + response_subject.subscribe(on_completed=lambda: agent.dispose_all()) - return response_subject + return response_subject def query_single_frame( - image: 'PIL.Image', - query: str = 'Return the center coordinates of the fridge handle as a tuple (x,y)', - api_key: Optional[str] = None, - model_name: str = "qwen2.5-vl-72b-instruct" + image: np.ndarray, + query: str = "Return the center coordinates of the fridge handle as a tuple (x,y)", + api_key: str | None = None, + model_name: str = "qwen2.5-vl-72b-instruct", ) -> str: - """Process a single PIL image with Qwen model. - + """Process a single numpy image array with Qwen model. + Args: - image: A PIL Image to process + image: A numpy array image to process (H, W, 3) in RGB format query: The query to ask about the image api_key: Alibaba API key. If None, will try to get from ALIBABA_API_KEY env var model_name: The Qwen model to use. Defaults to qwen2.5-vl-72b-instruct - + Returns: str: The model's response - + Example: ```python - from PIL import Image - image = Image.open('image.jpg') + import cv2 + image = cv2.imread('image.jpg') + image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # Convert to RGB response = query_single_frame(image, "Return the center coordinates of the object _____ as a tuple (x,y)") print(response) ``` """ # Get API key from env if not provided - api_key = api_key or os.getenv('ALIBABA_API_KEY') + api_key = api_key or os.getenv("ALIBABA_API_KEY") if not api_key: - raise ValueError("Alibaba API key must be provided or set in ALIBABA_API_KEY environment variable") + raise ValueError( + "Alibaba API key must be provided or set in ALIBABA_API_KEY environment variable" + ) # Create Qwen client qwen_client = OpenAI( - base_url='https://dashscope-intl.aliyuncs.com/compatible-mode/v1', + base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1", api_key=api_key, ) @@ -129,49 +132,48 @@ def query_single_frame( openai_client=qwen_client, model_name=model_name, tokenizer=HuggingFaceTokenizer(model_name=f"Qwen/{model_name}"), - max_output_tokens_per_request=100, + max_output_tokens_per_request=8192, system_query=query, pool_scheduler=get_scheduler(), ) - # Convert PIL image to numpy array - frame = np.array(image) - + # Use the numpy array directly (no conversion needed) + frame = image + # Create a Subject that will emit the image once frame_subject = Subject() - + # Subscribe to frame processing agent.subscribe_to_image_processing(frame_subject) - + # Create response observable response_observable = agent.get_response_observable() - + # Emit the image frame_subject.on_next(frame) frame_subject.on_completed() - + # Take first response and run synchronously - response = response_observable.pipe( - ops.take(1) - ).run() - + response = response_observable.pipe(ops.take(1)).run() + # Clean up agent.dispose_all() - + return response + def get_bbox_from_qwen( - video_stream: Observable, - object_name: Optional[str] = None -) -> Optional[list]: + video_stream: Observable, object_name: str | None = None +) -> tuple[BBox, float] | None: """Get bounding box coordinates from Qwen for a specific object or any object. - + Args: video_stream: Observable video stream object_name: Optional name of object to detect - + Returns: - bbox: Bounding box as [x1, y1, x2, y2] or None if no detection + Tuple of (bbox, size) where bbox is (x1, y1, x2, y2) and size is height in meters, + or None if no detection """ prompt = ( f"Look at this image and find the {object_name if object_name else 'most prominent object'}. Estimate the approximate height of the subject." @@ -183,43 +185,40 @@ def get_bbox_from_qwen( try: # Extract JSON from response - start_idx = response.find('{') - end_idx = response.rfind('}') + 1 + start_idx = response.find("{") + end_idx = response.rfind("}") + 1 if start_idx >= 0 and end_idx > start_idx: json_str = response[start_idx:end_idx] result = json.loads(json_str) - + # Extract and validate bbox - if 'bbox' in result and len(result['bbox']) == 4: - return result['bbox'], result['size'] + if "bbox" in result and len(result["bbox"]) == 4: + bbox = tuple(result["bbox"]) # Convert list to tuple + return (bbox, result["size"]) except Exception as e: print(f"Error parsing Qwen response: {e}") print(f"Raw response: {response}") return None -def get_bbox_from_qwen_frame( - frame, - object_name: Optional[str] = None -) -> Optional[tuple]: + +def get_bbox_from_qwen_frame(frame, object_name: str | None = None) -> BBox | None: """Get bounding box coordinates from Qwen for a specific object or any object using a single frame. - + Args: - frame: A single image frame (PIL Image or numpy array) + frame: A single image frame (numpy array in RGB format) object_name: Optional name of object to detect - + Returns: - tuple: (bbox, size) where bbox is [x1, y1, x2, y2] or None if no detection - and size is the estimated height in meters + BBox: Bounding box as (x1, y1, x2, y2) or None if no detection """ - # Convert numpy array to PIL Image if needed - if isinstance(frame, np.ndarray): - from PIL import Image - frame = Image.fromarray(frame) - + # Ensure frame is numpy array + if not isinstance(frame, np.ndarray): + raise ValueError("Frame must be a numpy array") + prompt = ( - f"Look at this image and find the {object_name if object_name else 'most prominent object'}. Estimate the approximate height of the subject." - "Return ONLY a JSON object with format: {'name': 'object_name', 'bbox': [x1, y1, x2, y2], 'size': height_in_meters} " + f"Look at this image and find the {object_name if object_name else 'most prominent object'}. " + "Return ONLY a JSON object with format: {'name': 'object_name', 'bbox': [x1, y1, x2, y2]} " "where x1,y1 is the top-left and x2,y2 is the bottom-right corner of the bounding box. If not found, return None." ) @@ -227,17 +226,17 @@ def get_bbox_from_qwen_frame( try: # Extract JSON from response - start_idx = response.find('{') - end_idx = response.rfind('}') + 1 + start_idx = response.find("{") + end_idx = response.rfind("}") + 1 if start_idx >= 0 and end_idx > start_idx: json_str = response[start_idx:end_idx] result = json.loads(json_str) - + # Extract and validate bbox - if 'bbox' in result and len(result['bbox']) == 4: - return result['bbox'], result['size'] + if "bbox" in result and len(result["bbox"]) == 4: + return tuple(result["bbox"]) # Convert list to tuple except Exception as e: print(f"Error parsing Qwen response: {e}") print(f"Raw response: {response}") - return None \ No newline at end of file + return None diff --git a/dimos/models/segmentation/clipseg.py b/dimos/models/segmentation/clipseg.py index 34100dff52..ca8fbeb6fc 100644 --- a/dimos/models/segmentation/clipseg.py +++ b/dimos/models/segmentation/clipseg.py @@ -13,16 +13,20 @@ # limitations under the License. from transformers import AutoProcessor, CLIPSegForImageSegmentation -import torch -import numpy as np + class CLIPSeg: - def __init__(self, model_name="CIDAS/clipseg-rd64-refined"): + def __init__(self, model_name: str="CIDAS/clipseg-rd64-refined") -> None: self.clipseg_processor = AutoProcessor.from_pretrained(model_name) self.clipseg_model = CLIPSegForImageSegmentation.from_pretrained(model_name) def run_inference(self, image, text_descriptions): - inputs = self.clipseg_processor(text=text_descriptions, images=[image] * len(text_descriptions), padding=True, return_tensors="pt") + inputs = self.clipseg_processor( + text=text_descriptions, + images=[image] * len(text_descriptions), + padding=True, + return_tensors="pt", + ) outputs = self.clipseg_model(**inputs) logits = outputs.logits - return logits.detach().unsqueeze(1) \ No newline at end of file + return logits.detach().unsqueeze(1) diff --git a/dimos/models/segmentation/sam.py b/dimos/models/segmentation/sam.py index b1c41915c2..96b23bf984 100644 --- a/dimos/models/segmentation/sam.py +++ b/dimos/models/segmentation/sam.py @@ -12,18 +12,24 @@ # See the License for the specific language governing permissions and # limitations under the License. -from transformers import SamModel, SamProcessor import torch -import numpy as np +from transformers import SamModel, SamProcessor + class SAM: - def __init__(self, model_name="facebook/sam-vit-huge", device="cuda"): + def __init__(self, model_name: str="facebook/sam-vit-huge", device: str="cuda") -> None: self.device = device self.sam_model = SamModel.from_pretrained(model_name).to(self.device) self.sam_processor = SamProcessor.from_pretrained(model_name) def run_inference_from_points(self, image, points): - sam_inputs = self.sam_processor(image, input_points=points, return_tensors="pt").to(self.device) + sam_inputs = self.sam_processor(image, input_points=points, return_tensors="pt").to( + self.device + ) with torch.no_grad(): sam_outputs = self.sam_model(**sam_inputs) - return self.sam_processor.image_processor.post_process_masks(sam_outputs.pred_masks.cpu(), sam_inputs["original_sizes"].cpu(), sam_inputs["reshaped_input_sizes"].cpu()) + return self.sam_processor.image_processor.post_process_masks( + sam_outputs.pred_masks.cpu(), + sam_inputs["original_sizes"].cpu(), + sam_inputs["reshaped_input_sizes"].cpu(), + ) diff --git a/dimos/models/segmentation/segment_utils.py b/dimos/models/segmentation/segment_utils.py index 04f18f22a1..9b15f353e4 100644 --- a/dimos/models/segmentation/segment_utils.py +++ b/dimos/models/segmentation/segment_utils.py @@ -12,10 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import torch import numpy as np +import torch -def find_medoid_and_closest_points(points, num_closest=5): + +def find_medoid_and_closest_points(points, num_closest: int=5): """ Find the medoid from a collection of points and the closest points to the medoid. @@ -32,10 +33,11 @@ def find_medoid_and_closest_points(points, num_closest=5): medoid_idx = np.argmin(distance_sums) medoid = points[medoid_idx] sorted_indices = np.argsort(distances[medoid_idx]) - closest_indices = sorted_indices[1:num_closest + 1] + closest_indices = sorted_indices[1 : num_closest + 1] return medoid, points[closest_indices] -def sample_points_from_heatmap(heatmap, original_size, num_points=5, percentile=0.95): + +def sample_points_from_heatmap(heatmap, original_size: int, num_points: int=5, percentile: float=0.95): """ Sample points from the given heatmap, focusing on areas with higher values. """ @@ -46,7 +48,9 @@ def sample_points_from_heatmap(heatmap, original_size, num_points=5, percentile= attn = torch.sigmoid(heatmap) w = attn.shape[0] - sampled_indices = torch.multinomial(torch.tensor(probabilities.ravel()), num_points, replacement=True) + sampled_indices = torch.multinomial( + torch.tensor(probabilities.ravel()), num_points, replacement=True + ) sampled_coords = np.array(np.unravel_index(sampled_indices, attn.shape)).T medoid, sampled_coords = find_medoid_and_closest_points(sampled_coords) @@ -66,4 +70,4 @@ def apply_mask_to_image(image, mask): masked_image = image.copy() for c in range(masked_image.shape[2]): masked_image[:, :, c] = masked_image[:, :, c] * mask - return masked_image \ No newline at end of file + return masked_image diff --git a/dimos/models/vl/README.md b/dimos/models/vl/README.md new file mode 100644 index 0000000000..3a8353c69a --- /dev/null +++ b/dimos/models/vl/README.md @@ -0,0 +1,22 @@ +# Vision Language Models + +This provides vision language model implementations for processing images and text queries. + +## QwenVL Model + +The `QwenVlModel` class provides access to Alibaba's Qwen2.5-VL model for vision-language tasks. + +### Example Usage + +```python +from dimos.models.vl.qwen import QwenVlModel +from dimos.msgs.sensor_msgs.Image import Image + +# Initialize the model (requires ALIBABA_API_KEY environment variable) +model = QwenVlModel() + +image = Image.from_file("path/to/your/image.jpg") + +response = model.query(image.data, "What do you see in this image?") +print(response) +``` diff --git a/dimos/models/vl/__init__.py b/dimos/models/vl/__init__.py new file mode 100644 index 0000000000..8cb0a7944b --- /dev/null +++ b/dimos/models/vl/__init__.py @@ -0,0 +1,2 @@ +from dimos.models.vl.base import VlModel +from dimos.models.vl.qwen import QwenVlModel diff --git a/dimos/models/vl/base.py b/dimos/models/vl/base.py new file mode 100644 index 0000000000..7e162b3ccf --- /dev/null +++ b/dimos/models/vl/base.py @@ -0,0 +1,106 @@ +from abc import ABC, abstractmethod +import json +import logging + +from dimos.msgs.sensor_msgs import Image +from dimos.perception.detection.type import Detection2DBBox, ImageDetections2D +from dimos.utils.data import get_data +from dimos.utils.decorators import retry +from dimos.utils.llm_utils import extract_json + +logger = logging.getLogger(__name__) + + +def vlm_detection_to_detection2d( + vlm_detection: list, track_id: int, image: Image +) -> Detection2DBBox | None: + """Convert a single VLM detection [label, x1, y1, x2, y2] to Detection2DBBox. + + Args: + vlm_detection: Single detection list containing [label, x1, y1, x2, y2] + track_id: Track ID to assign to this detection + image: Source image for the detection + + Returns: + Detection2DBBox instance or None if invalid + """ + # Validate list structure + if not isinstance(vlm_detection, list): + logger.debug(f"VLM detection is not a list: {type(vlm_detection)}") + return None + + if len(vlm_detection) != 5: + logger.debug( + f"Invalid VLM detection length: {len(vlm_detection)}, expected 5. Got: {vlm_detection}" + ) + return None + + # Extract label + name = str(vlm_detection[0]) + + # Validate and convert coordinates + try: + coords = [float(x) for x in vlm_detection[1:]] + except (ValueError, TypeError) as e: + logger.debug(f"Invalid VLM detection coordinates: {vlm_detection[1:]}. Error: {e}") + return None + + bbox = tuple(coords) + + # Use -1 for class_id since VLM doesn't provide it + # confidence defaults to 1.0 for VLM + return Detection2DBBox( + bbox=bbox, + track_id=track_id, + class_id=-1, + confidence=1.0, + name=name, + ts=image.ts, + image=image, + ) + + +class VlModel(ABC): + @abstractmethod + def query(self, image: Image, query: str, **kwargs) -> str: ... + + def warmup(self) -> None: + try: + image = Image.from_file(get_data("cafe-smol.jpg")).to_rgb() + self._model.detect(image, "person", settings={"max_objects": 1}) + except Exception: + pass + + # requery once if JSON parsing fails + @retry(max_retries=2, on_exception=json.JSONDecodeError, delay=0.0) + def query_json(self, image: Image, query: str) -> dict: + response = self.query(image, query) + return extract_json(response) + + def query_detections(self, image: Image, query: str, **kwargs) -> ImageDetections2D: + full_query = f"""show me bounding boxes in pixels for this query: `{query}` + + format should be: + `[ + [label, x1, y1, x2, y2] + ... + ]` + + (etc, multiple matches are possible) + + If there's no match return `[]`. Label is whatever you think is appropriate + Only respond with the coordinates, no other text.""" + + image_detections = ImageDetections2D(image) + + try: + detection_tuples = self.query_json(image, full_query) + except Exception: + return image_detections + + for track_id, detection_tuple in enumerate(detection_tuples): + detection2d = vlm_detection_to_detection2d(detection_tuple, track_id, image) + if detection2d is not None and detection2d.is_valid(): + image_detections.detections.append(detection2d) + + return image_detections diff --git a/dimos/models/vl/moondream.py b/dimos/models/vl/moondream.py new file mode 100644 index 0000000000..ce63c70238 --- /dev/null +++ b/dimos/models/vl/moondream.py @@ -0,0 +1,114 @@ +from functools import cached_property +from typing import Optional +import warnings + +import numpy as np +from PIL import Image as PILImage +import torch +from transformers import AutoModelForCausalLM + +from dimos.models.vl.base import VlModel +from dimos.msgs.sensor_msgs import Image +from dimos.perception.detection.type import Detection2DBBox, ImageDetections2D + + +class MoondreamVlModel(VlModel): + _model_name: str + _device: str + _dtype: torch.dtype + + def __init__( + self, + model_name: str = "vikhyatk/moondream2", + device: str | None = None, + dtype: torch.dtype = torch.bfloat16, + ) -> None: + self._model_name = model_name + self._device = device or ("cuda" if torch.cuda.is_available() else "cpu") + self._dtype = dtype + + @cached_property + def _model(self) -> AutoModelForCausalLM: + model = AutoModelForCausalLM.from_pretrained( + self._model_name, + trust_remote_code=True, + torch_dtype=self._dtype, + ) + model = model.to(self._device) + model.compile() + + return model + + def query(self, image: Image | np.ndarray, query: str, **kwargs) -> str: + if isinstance(image, np.ndarray): + warnings.warn( + "MoondreamVlModel.query should receive standard dimos Image type, not a numpy array", + DeprecationWarning, + stacklevel=2, + ) + image = Image.from_numpy(image) + + # Convert dimos Image to PIL Image + # dimos Image stores data in RGB/BGR format, convert to RGB for PIL + rgb_image = image.to_rgb() + pil_image = PILImage.fromarray(rgb_image.data) + + # Query the model + result = self._model.query(image=pil_image, question=query, reasoning=False) + + # Handle both dict and string responses + if isinstance(result, dict): + return result.get("answer", str(result)) + + return str(result) + + def query_detections(self, image: Image, query: str, **kwargs) -> ImageDetections2D: + """Detect objects using Moondream's native detect method. + + Args: + image: Input image + query: Object query (e.g., "person", "car") + max_objects: Maximum number of objects to detect + + Returns: + ImageDetections2D containing detected bounding boxes + """ + pil_image = PILImage.fromarray(image.data) + + settings = {"max_objects": kwargs.get("max_objects", 5)} + result = self._model.detect(pil_image, query, settings=settings) + + # Convert to ImageDetections2D + image_detections = ImageDetections2D(image) + + # Get image dimensions for converting normalized coords to pixels + height, width = image.height, image.width + + for track_id, obj in enumerate(result.get("objects", [])): + # Convert normalized coordinates (0-1) to pixel coordinates + x_min_norm = obj["x_min"] + y_min_norm = obj["y_min"] + x_max_norm = obj["x_max"] + y_max_norm = obj["y_max"] + + x1 = x_min_norm * width + y1 = y_min_norm * height + x2 = x_max_norm * width + y2 = y_max_norm * height + + bbox = (x1, y1, x2, y2) + + detection = Detection2DBBox( + bbox=bbox, + track_id=track_id, + class_id=-1, # Moondream doesn't provide class IDs + confidence=1.0, # Moondream doesn't provide confidence scores + name=query, # Use the query as the object name + ts=image.ts, + image=image, + ) + + if detection.is_valid(): + image_detections.detections.append(detection) + + return image_detections diff --git a/dimos/models/vl/qwen.py b/dimos/models/vl/qwen.py new file mode 100644 index 0000000000..c302d12c22 --- /dev/null +++ b/dimos/models/vl/qwen.py @@ -0,0 +1,63 @@ +from functools import cached_property +import os +from typing import Optional + +import numpy as np +from openai import OpenAI + +from dimos.models.vl.base import VlModel +from dimos.msgs.sensor_msgs import Image + + +class QwenVlModel(VlModel): + _model_name: str + _api_key: str | None + + def __init__(self, api_key: str | None = None, model_name: str = "qwen2.5-vl-72b-instruct") -> None: + self._model_name = model_name + self._api_key = api_key + + @cached_property + def _client(self) -> OpenAI: + api_key = self._api_key or os.getenv("ALIBABA_API_KEY") + if not api_key: + raise ValueError( + "Alibaba API key must be provided or set in ALIBABA_API_KEY environment variable" + ) + + return OpenAI( + base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1", + api_key=api_key, + ) + + def query(self, image: Image | np.ndarray, query: str) -> str: + if isinstance(image, np.ndarray): + import warnings + + warnings.warn( + "QwenVlModel.query should receive standard dimos Image type, not a numpy array", + DeprecationWarning, + stacklevel=2, + ) + + image = Image.from_numpy(image) + + img_base64 = image.to_base64() + + response = self._client.chat.completions.create( + model=self._model_name, + messages=[ + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": {"url": f"data:image/png;base64,{img_base64}"}, + }, + {"type": "text", "text": query}, + ], + } + ], + ) + + return response.choices[0].message.content diff --git a/dimos/models/vl/test_base.py b/dimos/models/vl/test_base.py new file mode 100644 index 0000000000..3d8575fab3 --- /dev/null +++ b/dimos/models/vl/test_base.py @@ -0,0 +1,105 @@ +import os +from unittest.mock import MagicMock + +import pytest + +from dimos.models.vl.qwen import QwenVlModel +from dimos.msgs.sensor_msgs import Image +from dimos.perception.detection.type import ImageDetections2D +from dimos.utils.data import get_data + +# Captured actual response from Qwen API for cafe.jpg with query "humans" +# Added garbage around JSON to ensure we are robustly extracting it +MOCK_QWEN_RESPONSE = """ + Locating humans for you 😊😊 + + [ + ["humans", 76, 368, 219, 580], + ["humans", 354, 372, 512, 525], + ["humans", 409, 370, 615, 748], + ["humans", 628, 350, 762, 528], + ["humans", 785, 323, 960, 650] + ] + + Here is some trash at the end of the response :) + Let me know if you need anything else 😀😊 + """ + + +def test_query_detections_mocked() -> None: + """Test query_detections with mocked API response (no API key required).""" + # Load test image + image = Image.from_file(get_data("cafe.jpg")) + + # Create model and mock the query method + model = QwenVlModel() + model.query = MagicMock(return_value=MOCK_QWEN_RESPONSE) + + # Query for humans in the image + query = "humans" + detections = model.query_detections(image, query) + + # Verify the return type + assert isinstance(detections, ImageDetections2D) + + # Should have 5 detections based on our mock data + assert len(detections.detections) == 5, ( + f"Expected 5 detections, got {len(detections.detections)}" + ) + + # Verify each detection + img_height, img_width = image.shape[:2] + + for i, detection in enumerate(detections.detections): + # Verify attributes + assert detection.name == "humans" + assert detection.confidence == 1.0 + assert detection.class_id == -1 # VLM detections use -1 for class_id + assert detection.track_id == i + assert len(detection.bbox) == 4 + + assert detection.is_valid() + + # Verify bbox coordinates are valid (out-of-bounds detections are discarded) + x1, y1, x2, y2 = detection.bbox + assert x2 > x1, f"Detection {i}: Invalid x coordinates: x1={x1}, x2={x2}" + assert y2 > y1, f"Detection {i}: Invalid y coordinates: y1={y1}, y2={y2}" + + # Check bounds (out-of-bounds detections would have been discarded) + assert 0 <= x1 <= img_width, f"Detection {i}: x1={x1} out of bounds" + assert 0 <= x2 <= img_width, f"Detection {i}: x2={x2} out of bounds" + assert 0 <= y1 <= img_height, f"Detection {i}: y1={y1} out of bounds" + assert 0 <= y2 <= img_height, f"Detection {i}: y2={y2} out of bounds" + + print(f"✓ Successfully processed {len(detections.detections)} mocked detections") + + +@pytest.mark.tool +@pytest.mark.skipif(not os.getenv("ALIBABA_API_KEY"), reason="ALIBABA_API_KEY not set") +def test_query_detections_real() -> None: + """Test query_detections with real API calls (requires API key).""" + # Load test image + image = Image.from_file(get_data("cafe.jpg")) + + # Initialize the model (will use real API) + model = QwenVlModel() + + # Query for humans in the image + query = "humans" + detections = model.query_detections(image, query) + + assert isinstance(detections, ImageDetections2D) + print(detections) + + # Check that detections were found + if detections.detections: + for detection in detections.detections: + # Verify each detection has expected attributes + assert detection.bbox is not None + assert len(detection.bbox) == 4 + assert detection.name + assert detection.confidence == 1.0 + assert detection.class_id == -1 # VLM detections use -1 for class_id + assert detection.is_valid() + + print(f"Found {len(detections.detections)} detections for query '{query}'") diff --git a/dimos/models/vl/test_models.py b/dimos/models/vl/test_models.py new file mode 100644 index 0000000000..a30951669c --- /dev/null +++ b/dimos/models/vl/test_models.py @@ -0,0 +1,92 @@ +import time +from typing import TYPE_CHECKING + +from dimos_lcm.foxglove_msgs.ImageAnnotations import ImageAnnotations +import pytest + +from dimos.core import LCMTransport +from dimos.models.vl.moondream import MoondreamVlModel +from dimos.models.vl.qwen import QwenVlModel +from dimos.msgs.sensor_msgs import Image +from dimos.perception.detection.detectors.yolo import Yolo2DDetector +from dimos.perception.detection.type import ImageDetections2D +from dimos.utils.data import get_data + +if TYPE_CHECKING: + from dimos.models.vl.base import VlModel + + +@pytest.mark.parametrize( + "model_class,model_name", + [ + (MoondreamVlModel, "Moondream"), + (QwenVlModel, "Qwen"), + ], + ids=["moondream", "qwen"], +) +@pytest.mark.gpu +def test_vlm(model_class, model_name: str) -> None: + image = Image.from_file(get_data("cafe.jpg")).to_rgb() + + print(f"Testing {model_name}") + + # Initialize model + print(f"Loading {model_name} model...") + model: VlModel = model_class() + model.warmup() + + queries = [ + "glasses", + "blue shirt", + "bulb", + "cigarette", + "reflection of a car", + "knee", + "flowers on the left table", + "shoes", + "leftmost persons ear", + "rightmost arm", + ] + + all_detections = ImageDetections2D(image) + query_times = [] + + # # First, run YOLO detection + # print("\nRunning YOLO detection...") + # yolo_detector = Yolo2DDetector() + # yolo_detections = yolo_detector.process_image(image) + # print(f" YOLO found {len(yolo_detections.detections)} objects") + # all_detections.detections.extend(yolo_detections.detections) + # annotations_transport.publish(all_detections.to_foxglove_annotations()) + + # Publish to LCM with model-specific channel names + annotations_transport: LCMTransport[ImageAnnotations] = LCMTransport( + "/annotations", ImageAnnotations + ) + + image_transport: LCMTransport[Image] = LCMTransport("/image", Image) + + image_transport.publish(image) + + # Then run VLM queries + for query in queries: + print(f"\nQuerying for: {query}") + start_time = time.time() + detections = model.query_detections(image, query, max_objects=5) + query_time = time.time() - start_time + query_times.append(query_time) + + print(f" Found {len(detections)} detections in {query_time:.3f}s") + all_detections.detections.extend(detections.detections) + annotations_transport.publish(all_detections.to_foxglove_annotations()) + + avg_time = sum(query_times) / len(query_times) if query_times else 0 + print(f"\n{model_name} Results:") + print(f" Average query time: {avg_time:.3f}s") + print(f" Total detections: {len(all_detections)}") + print(all_detections) + + annotations_transport.publish(all_detections.to_foxglove_annotations()) + + annotations_transport.lcm.stop() + image_transport.lcm.stop() diff --git a/dimos/msgs/__init__.py b/dimos/msgs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/dimos/msgs/foxglove_msgs/Color.py b/dimos/msgs/foxglove_msgs/Color.py new file mode 100644 index 0000000000..ed19911eb7 --- /dev/null +++ b/dimos/msgs/foxglove_msgs/Color.py @@ -0,0 +1,65 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import hashlib + +from dimos_lcm.foxglove_msgs import Color as LCMColor + + +class Color(LCMColor): + """Color with convenience methods.""" + + @classmethod + def from_string(cls, name: str, alpha: float = 0.2, brightness: float = 1.0) -> Color: + """Generate a consistent color from a string using hash function. + + Args: + name: String to generate color from + alpha: Transparency value (0.0-1.0) + brightness: Brightness multiplier (0.0-2.0). Values > 1.0 lighten towards white. + + Returns: + Color instance with deterministic RGB values + """ + # Hash the string to get consistent values + hash_obj = hashlib.md5(name.encode()) + hash_bytes = hash_obj.digest() + + # Use first 3 bytes for RGB (0-255) + r = hash_bytes[0] / 255.0 + g = hash_bytes[1] / 255.0 + b = hash_bytes[2] / 255.0 + + # Apply brightness adjustment + # If brightness > 1.0, mix with white to lighten + if brightness > 1.0: + mix_factor = brightness - 1.0 # 0.0 to 1.0 + r = r + (1.0 - r) * mix_factor + g = g + (1.0 - g) * mix_factor + b = b + (1.0 - b) * mix_factor + else: + # If brightness < 1.0, darken by scaling + r *= brightness + g *= brightness + b *= brightness + + # Create and return color instance + color = cls() + color.r = min(1.0, r) + color.g = min(1.0, g) + color.b = min(1.0, b) + color.a = alpha + return color diff --git a/dimos/msgs/foxglove_msgs/ImageAnnotations.py b/dimos/msgs/foxglove_msgs/ImageAnnotations.py new file mode 100644 index 0000000000..1f58b09d73 --- /dev/null +++ b/dimos/msgs/foxglove_msgs/ImageAnnotations.py @@ -0,0 +1,33 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos_lcm.foxglove_msgs.ImageAnnotations import ImageAnnotations as FoxgloveImageAnnotations + + +class ImageAnnotations(FoxgloveImageAnnotations): + def __add__(self, other: "ImageAnnotations") -> "ImageAnnotations": + points = self.points + other.points + texts = self.texts + other.texts + + return ImageAnnotations( + texts=texts, + texts_length=len(texts), + points=points, + points_length=len(points), + ) + + def agent_encode(self) -> str: + if len(self.texts) == 0: + return None + return list(map(lambda t: t.text, self.texts)) diff --git a/dimos/msgs/foxglove_msgs/__init__.py b/dimos/msgs/foxglove_msgs/__init__.py new file mode 100644 index 0000000000..36698f5484 --- /dev/null +++ b/dimos/msgs/foxglove_msgs/__init__.py @@ -0,0 +1 @@ +from dimos.msgs.foxglove_msgs.ImageAnnotations import ImageAnnotations diff --git a/dimos/msgs/geometry_msgs/Pose.py b/dimos/msgs/geometry_msgs/Pose.py new file mode 100644 index 0000000000..0bb69d84bf --- /dev/null +++ b/dimos/msgs/geometry_msgs/Pose.py @@ -0,0 +1,268 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import TypeAlias + +from dimos_lcm.geometry_msgs import Pose as LCMPose, Transform as LCMTransform + +try: + from geometry_msgs.msg import Point as ROSPoint, Pose as ROSPose, Quaternion as ROSQuaternion +except ImportError: + ROSPose = None + ROSPoint = None + ROSQuaternion = None + +from plum import dispatch + +from dimos.msgs.geometry_msgs.Quaternion import Quaternion, QuaternionConvertable +from dimos.msgs.geometry_msgs.Transform import Transform +from dimos.msgs.geometry_msgs.Vector3 import Vector3, VectorConvertable + +# Types that can be converted to/from Pose +PoseConvertable: TypeAlias = ( + tuple[VectorConvertable, QuaternionConvertable] + | LCMPose + | Vector3 + | dict[str, VectorConvertable | QuaternionConvertable] +) + + +class Pose(LCMPose): + position: Vector3 + orientation: Quaternion + msg_name = "geometry_msgs.Pose" + + @dispatch + def __init__(self) -> None: + """Initialize a pose at origin with identity orientation.""" + self.position = Vector3(0.0, 0.0, 0.0) + self.orientation = Quaternion(0.0, 0.0, 0.0, 1.0) + + @dispatch + def __init__(self, x: int | float, y: int | float, z: int | float) -> None: + """Initialize a pose with position and identity orientation.""" + self.position = Vector3(x, y, z) + self.orientation = Quaternion(0.0, 0.0, 0.0, 1.0) + + @dispatch + def __init__( + self, + x: int | float, + y: int | float, + z: int | float, + qx: int | float, + qy: int | float, + qz: int | float, + qw: int | float, + ) -> None: + """Initialize a pose with position and orientation.""" + self.position = Vector3(x, y, z) + self.orientation = Quaternion(qx, qy, qz, qw) + + @dispatch + def __init__( + self, + position: VectorConvertable | Vector3 | None = None, + orientation: QuaternionConvertable | Quaternion | None = None, + ) -> None: + """Initialize a pose with position and orientation.""" + if orientation is None: + orientation = [0, 0, 0, 1] + if position is None: + position = [0, 0, 0] + self.position = Vector3(position) + self.orientation = Quaternion(orientation) + + @dispatch + def __init__(self, pose_tuple: tuple[VectorConvertable, QuaternionConvertable]) -> None: + """Initialize from a tuple of (position, orientation).""" + self.position = Vector3(pose_tuple[0]) + self.orientation = Quaternion(pose_tuple[1]) + + @dispatch + def __init__(self, pose_dict: dict[str, VectorConvertable | QuaternionConvertable]) -> None: + """Initialize from a dictionary with 'position' and 'orientation' keys.""" + self.position = Vector3(pose_dict["position"]) + self.orientation = Quaternion(pose_dict["orientation"]) + + @dispatch + def __init__(self, pose: Pose) -> None: + """Initialize from another Pose (copy constructor).""" + self.position = Vector3(pose.position) + self.orientation = Quaternion(pose.orientation) + + @dispatch + def __init__(self, lcm_pose: LCMPose) -> None: + """Initialize from an LCM Pose.""" + self.position = Vector3(lcm_pose.position.x, lcm_pose.position.y, lcm_pose.position.z) + self.orientation = Quaternion( + lcm_pose.orientation.x, + lcm_pose.orientation.y, + lcm_pose.orientation.z, + lcm_pose.orientation.w, + ) + + @property + def x(self) -> float: + """X coordinate of position.""" + return self.position.x + + @property + def y(self) -> float: + """Y coordinate of position.""" + return self.position.y + + @property + def z(self) -> float: + """Z coordinate of position.""" + return self.position.z + + @property + def roll(self) -> float: + """Roll angle in radians.""" + return self.orientation.to_euler().roll + + @property + def pitch(self) -> float: + """Pitch angle in radians.""" + return self.orientation.to_euler().pitch + + @property + def yaw(self) -> float: + """Yaw angle in radians.""" + return self.orientation.to_euler().yaw + + def __repr__(self) -> str: + return f"Pose(position={self.position!r}, orientation={self.orientation!r})" + + def __str__(self) -> str: + return ( + f"Pose(pos=[{self.x:.3f}, {self.y:.3f}, {self.z:.3f}], " + f"euler=[{self.roll:.3f}, {self.pitch:.3f}, {self.yaw:.3f}]), " + f"quaternion=[{self.orientation}])" + ) + + def __eq__(self, other) -> bool: + """Check if two poses are equal.""" + if not isinstance(other, Pose): + return False + return self.position == other.position and self.orientation == other.orientation + + def __matmul__(self, transform: LCMTransform | Transform) -> Pose: + return self + transform + + def __add__(self, other: Pose | PoseConvertable | LCMTransform | Transform) -> Pose: + """Compose two poses or apply a transform (transform composition). + + The operation self + other represents applying transformation 'other' + in the coordinate frame defined by 'self'. This is equivalent to: + - First apply transformation 'self' (from world to self's frame) + - Then apply transformation 'other' (from self's frame to other's frame) + + This matches ROS tf convention where: + T_world_to_other = T_world_to_self * T_self_to_other + + Args: + other: The pose or transform to compose with this one + + Returns: + A new Pose representing the composed transformation + + Example: + robot_pose = Pose(1, 0, 0) # Robot at (1,0,0) facing forward + object_in_robot = Pose(2, 0, 0) # Object 2m in front of robot + object_in_world = robot_pose + object_in_robot # Object at (3,0,0) in world + + # Or with a Transform: + transform = Transform() + transform.translation = Vector3(2, 0, 0) + transform.rotation = Quaternion(0, 0, 0, 1) + new_pose = pose + transform + """ + # Handle Transform objects + if isinstance(other, LCMTransform | Transform): + # Convert Transform to Pose using its translation and rotation + other_position = Vector3(other.translation) + other_orientation = Quaternion(other.rotation) + elif isinstance(other, Pose): + other_position = other.position + other_orientation = other.orientation + else: + # Convert to Pose if it's a convertible type + other_pose = Pose(other) + other_position = other_pose.position + other_orientation = other_pose.orientation + + # Compose orientations: self.orientation * other.orientation + new_orientation = self.orientation * other_orientation + + # Transform other's position by self's orientation, then add to self's position + rotated_position = self.orientation.rotate_vector(other_position) + new_position = self.position + rotated_position + + return Pose(new_position, new_orientation) + + @classmethod + def from_ros_msg(cls, ros_msg: ROSPose) -> Pose: + """Create a Pose from a ROS geometry_msgs/Pose message. + + Args: + ros_msg: ROS Pose message + + Returns: + Pose instance + """ + position = Vector3(ros_msg.position.x, ros_msg.position.y, ros_msg.position.z) + orientation = Quaternion( + ros_msg.orientation.x, + ros_msg.orientation.y, + ros_msg.orientation.z, + ros_msg.orientation.w, + ) + return cls(position, orientation) + + def to_ros_msg(self) -> ROSPose: + """Convert to a ROS geometry_msgs/Pose message. + + Returns: + ROS Pose message + """ + ros_msg = ROSPose() + ros_msg.position = ROSPoint( + x=float(self.position.x), y=float(self.position.y), z=float(self.position.z) + ) + ros_msg.orientation = ROSQuaternion( + x=float(self.orientation.x), + y=float(self.orientation.y), + z=float(self.orientation.z), + w=float(self.orientation.w), + ) + return ros_msg + + +@dispatch +def to_pose(value: Pose) -> Pose: + """Pass through Pose objects.""" + return value + + +@dispatch +def to_pose(value: PoseConvertable) -> Pose: + """Convert a pose-compatible value to a Pose object.""" + return Pose(value) + + +PoseLike: TypeAlias = PoseConvertable | Pose diff --git a/dimos/msgs/geometry_msgs/PoseStamped.py b/dimos/msgs/geometry_msgs/PoseStamped.py new file mode 100644 index 0000000000..770f41b641 --- /dev/null +++ b/dimos/msgs/geometry_msgs/PoseStamped.py @@ -0,0 +1,154 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import time +from typing import BinaryIO, TypeAlias + +from dimos_lcm.geometry_msgs import PoseStamped as LCMPoseStamped + +try: + from geometry_msgs.msg import PoseStamped as ROSPoseStamped +except ImportError: + ROSPoseStamped = None + +from plum import dispatch + +from dimos.msgs.geometry_msgs.Pose import Pose +from dimos.msgs.geometry_msgs.Quaternion import Quaternion, QuaternionConvertable +from dimos.msgs.geometry_msgs.Transform import Transform +from dimos.msgs.geometry_msgs.Vector3 import Vector3, VectorConvertable +from dimos.types.timestamped import Timestamped + +# Types that can be converted to/from Pose +PoseConvertable: TypeAlias = ( + tuple[VectorConvertable, QuaternionConvertable] + | LCMPoseStamped + | dict[str, VectorConvertable | QuaternionConvertable] +) + + +def sec_nsec(ts): + s = int(ts) + return [s, int((ts - s) * 1_000_000_000)] + + +class PoseStamped(Pose, Timestamped): + msg_name = "geometry_msgs.PoseStamped" + ts: float + frame_id: str + + @dispatch + def __init__(self, ts: float = 0.0, frame_id: str = "", **kwargs) -> None: + self.frame_id = frame_id + self.ts = ts if ts != 0 else time.time() + super().__init__(**kwargs) + + def lcm_encode(self) -> bytes: + lcm_mgs = LCMPoseStamped() + lcm_mgs.pose = self + [lcm_mgs.header.stamp.sec, lcm_mgs.header.stamp.nsec] = sec_nsec(self.ts) + lcm_mgs.header.frame_id = self.frame_id + return lcm_mgs.lcm_encode() + + @classmethod + def lcm_decode(cls, data: bytes | BinaryIO) -> PoseStamped: + lcm_msg = LCMPoseStamped.lcm_decode(data) + return cls( + ts=lcm_msg.header.stamp.sec + (lcm_msg.header.stamp.nsec / 1_000_000_000), + frame_id=lcm_msg.header.frame_id, + position=[lcm_msg.pose.position.x, lcm_msg.pose.position.y, lcm_msg.pose.position.z], + orientation=[ + lcm_msg.pose.orientation.x, + lcm_msg.pose.orientation.y, + lcm_msg.pose.orientation.z, + lcm_msg.pose.orientation.w, + ], + ) + + def __str__(self) -> str: + return ( + f"PoseStamped(pos=[{self.x:.3f}, {self.y:.3f}, {self.z:.3f}], " + f"euler=[{self.roll:.3f}, {self.pitch:.3f}, {self.yaw:.3f}])" + ) + + def new_transform_to(self, name: str) -> Transform: + return self.find_transform( + PoseStamped( + frame_id=name, + position=Vector3(0, 0, 0), + orientation=Quaternion(0, 0, 0, 1), # Identity quaternion + ) + ) + + def new_transform_from(self, name: str) -> Transform: + return self.new_transform_to(name).inverse() + + def find_transform(self, other: PoseStamped) -> Transform: + inv_orientation = self.orientation.conjugate() + + pos_diff = other.position - self.position + + local_translation = inv_orientation.rotate_vector(pos_diff) + + relative_rotation = inv_orientation * other.orientation + + return Transform( + child_frame_id=other.frame_id, + frame_id=self.frame_id, + translation=local_translation, + rotation=relative_rotation, + ) + + @classmethod + def from_ros_msg(cls, ros_msg: ROSPoseStamped) -> PoseStamped: + """Create a PoseStamped from a ROS geometry_msgs/PoseStamped message. + + Args: + ros_msg: ROS PoseStamped message + + Returns: + PoseStamped instance + """ + # Convert timestamp from ROS header + ts = ros_msg.header.stamp.sec + (ros_msg.header.stamp.nanosec / 1_000_000_000) + + # Convert pose + pose = Pose.from_ros_msg(ros_msg.pose) + + return cls( + ts=ts, + frame_id=ros_msg.header.frame_id, + position=pose.position, + orientation=pose.orientation, + ) + + def to_ros_msg(self) -> ROSPoseStamped: + """Convert to a ROS geometry_msgs/PoseStamped message. + + Returns: + ROS PoseStamped message + """ + ros_msg = ROSPoseStamped() + + # Set header + ros_msg.header.frame_id = self.frame_id + ros_msg.header.stamp.sec = int(self.ts) + ros_msg.header.stamp.nanosec = int((self.ts - int(self.ts)) * 1_000_000_000) + + # Set pose + ros_msg.pose = Pose.to_ros_msg(self) + + return ros_msg diff --git a/dimos/msgs/geometry_msgs/PoseWithCovariance.py b/dimos/msgs/geometry_msgs/PoseWithCovariance.py new file mode 100644 index 0000000000..ba2c360935 --- /dev/null +++ b/dimos/msgs/geometry_msgs/PoseWithCovariance.py @@ -0,0 +1,227 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import TYPE_CHECKING, TypeAlias + +from dimos_lcm.geometry_msgs import PoseWithCovariance as LCMPoseWithCovariance +import numpy as np +from plum import dispatch + +try: + from geometry_msgs.msg import PoseWithCovariance as ROSPoseWithCovariance +except ImportError: + ROSPoseWithCovariance = None + +from dimos.msgs.geometry_msgs.Pose import Pose, PoseConvertable + +if TYPE_CHECKING: + from dimos.msgs.geometry_msgs.Quaternion import Quaternion + from dimos.msgs.geometry_msgs.Vector3 import Vector3 + +# Types that can be converted to/from PoseWithCovariance +PoseWithCovarianceConvertable: TypeAlias = ( + tuple[PoseConvertable, list[float] | np.ndarray] + | LCMPoseWithCovariance + | dict[str, PoseConvertable | list[float] | np.ndarray] +) + + +class PoseWithCovariance(LCMPoseWithCovariance): + pose: Pose + msg_name = "geometry_msgs.PoseWithCovariance" + + @dispatch + def __init__(self) -> None: + """Initialize with default pose and zero covariance.""" + self.pose = Pose() + self.covariance = np.zeros(36) + + @dispatch + def __init__( + self, pose: Pose | PoseConvertable, covariance: list[float] | np.ndarray | None = None + ) -> None: + """Initialize with pose and optional covariance.""" + self.pose = Pose(pose) if not isinstance(pose, Pose) else pose + if covariance is None: + self.covariance = np.zeros(36) + else: + self.covariance = np.array(covariance, dtype=float).reshape(36) + + @dispatch + def __init__(self, pose_with_cov: PoseWithCovariance) -> None: + """Initialize from another PoseWithCovariance (copy constructor).""" + self.pose = Pose(pose_with_cov.pose) + self.covariance = np.array(pose_with_cov.covariance).copy() + + @dispatch + def __init__(self, lcm_pose_with_cov: LCMPoseWithCovariance) -> None: + """Initialize from an LCM PoseWithCovariance.""" + self.pose = Pose(lcm_pose_with_cov.pose) + self.covariance = np.array(lcm_pose_with_cov.covariance) + + @dispatch + def __init__(self, pose_dict: dict[str, PoseConvertable | list[float] | np.ndarray]) -> None: + """Initialize from a dictionary with 'pose' and 'covariance' keys.""" + self.pose = Pose(pose_dict["pose"]) + covariance = pose_dict.get("covariance") + if covariance is None: + self.covariance = np.zeros(36) + else: + self.covariance = np.array(covariance, dtype=float).reshape(36) + + @dispatch + def __init__(self, pose_tuple: tuple[PoseConvertable, list[float] | np.ndarray]) -> None: + """Initialize from a tuple of (pose, covariance).""" + self.pose = Pose(pose_tuple[0]) + self.covariance = np.array(pose_tuple[1], dtype=float).reshape(36) + + def __getattribute__(self, name: str): + """Override to ensure covariance is always returned as numpy array.""" + if name == "covariance": + cov = object.__getattribute__(self, "covariance") + if not isinstance(cov, np.ndarray): + return np.array(cov, dtype=float) + return cov + return super().__getattribute__(name) + + def __setattr__(self, name: str, value) -> None: + """Override to ensure covariance is stored as numpy array.""" + if name == "covariance": + if not isinstance(value, np.ndarray): + value = np.array(value, dtype=float).reshape(36) + super().__setattr__(name, value) + + @property + def x(self) -> float: + """X coordinate of position.""" + return self.pose.x + + @property + def y(self) -> float: + """Y coordinate of position.""" + return self.pose.y + + @property + def z(self) -> float: + """Z coordinate of position.""" + return self.pose.z + + @property + def position(self) -> Vector3: + """Position vector.""" + return self.pose.position + + @property + def orientation(self) -> Quaternion: + """Orientation quaternion.""" + return self.pose.orientation + + @property + def roll(self) -> float: + """Roll angle in radians.""" + return self.pose.roll + + @property + def pitch(self) -> float: + """Pitch angle in radians.""" + return self.pose.pitch + + @property + def yaw(self) -> float: + """Yaw angle in radians.""" + return self.pose.yaw + + @property + def covariance_matrix(self) -> np.ndarray: + """Get covariance as 6x6 matrix.""" + return self.covariance.reshape(6, 6) + + @covariance_matrix.setter + def covariance_matrix(self, value: np.ndarray) -> None: + """Set covariance from 6x6 matrix.""" + self.covariance = np.array(value).reshape(36) + + def __repr__(self) -> str: + return f"PoseWithCovariance(pose={self.pose!r}, covariance=<{self.covariance.shape[0] if isinstance(self.covariance, np.ndarray) else len(self.covariance)} elements>)" + + def __str__(self) -> str: + return ( + f"PoseWithCovariance(pos=[{self.x:.3f}, {self.y:.3f}, {self.z:.3f}], " + f"euler=[{self.roll:.3f}, {self.pitch:.3f}, {self.yaw:.3f}], " + f"cov_trace={np.trace(self.covariance_matrix):.3f})" + ) + + def __eq__(self, other) -> bool: + """Check if two PoseWithCovariance are equal.""" + if not isinstance(other, PoseWithCovariance): + return False + return self.pose == other.pose and np.allclose(self.covariance, other.covariance) + + def lcm_encode(self) -> bytes: + """Encode to LCM binary format.""" + lcm_msg = LCMPoseWithCovariance() + lcm_msg.pose = self.pose + # LCM expects list, not numpy array + if isinstance(self.covariance, np.ndarray): + lcm_msg.covariance = self.covariance.tolist() + else: + lcm_msg.covariance = list(self.covariance) + return lcm_msg.lcm_encode() + + @classmethod + def lcm_decode(cls, data: bytes) -> PoseWithCovariance: + """Decode from LCM binary format.""" + lcm_msg = LCMPoseWithCovariance.lcm_decode(data) + pose = Pose( + position=[lcm_msg.pose.position.x, lcm_msg.pose.position.y, lcm_msg.pose.position.z], + orientation=[ + lcm_msg.pose.orientation.x, + lcm_msg.pose.orientation.y, + lcm_msg.pose.orientation.z, + lcm_msg.pose.orientation.w, + ], + ) + return cls(pose, lcm_msg.covariance) + + @classmethod + def from_ros_msg(cls, ros_msg: ROSPoseWithCovariance) -> PoseWithCovariance: + """Create a PoseWithCovariance from a ROS geometry_msgs/PoseWithCovariance message. + + Args: + ros_msg: ROS PoseWithCovariance message + + Returns: + PoseWithCovariance instance + """ + + pose = Pose.from_ros_msg(ros_msg.pose) + return cls(pose, list(ros_msg.covariance)) + + def to_ros_msg(self) -> ROSPoseWithCovariance: + """Convert to a ROS geometry_msgs/PoseWithCovariance message. + + Returns: + ROS PoseWithCovariance message + """ + + ros_msg = ROSPoseWithCovariance() + ros_msg.pose = self.pose.to_ros_msg() + # ROS expects list, not numpy array + if isinstance(self.covariance, np.ndarray): + ros_msg.covariance = self.covariance.tolist() + else: + ros_msg.covariance = list(self.covariance) + return ros_msg diff --git a/dimos/msgs/geometry_msgs/PoseWithCovarianceStamped.py b/dimos/msgs/geometry_msgs/PoseWithCovarianceStamped.py new file mode 100644 index 0000000000..3683a15fbd --- /dev/null +++ b/dimos/msgs/geometry_msgs/PoseWithCovarianceStamped.py @@ -0,0 +1,161 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import time +from typing import TypeAlias + +from dimos_lcm.geometry_msgs import PoseWithCovarianceStamped as LCMPoseWithCovarianceStamped +import numpy as np +from plum import dispatch + +try: + from geometry_msgs.msg import PoseWithCovarianceStamped as ROSPoseWithCovarianceStamped +except ImportError: + ROSPoseWithCovarianceStamped = None + +from dimos.msgs.geometry_msgs.Pose import Pose, PoseConvertable +from dimos.msgs.geometry_msgs.PoseWithCovariance import PoseWithCovariance +from dimos.types.timestamped import Timestamped + +# Types that can be converted to/from PoseWithCovarianceStamped +PoseWithCovarianceStampedConvertable: TypeAlias = ( + tuple[PoseConvertable, list[float] | np.ndarray] + | LCMPoseWithCovarianceStamped + | dict[str, PoseConvertable | list[float] | np.ndarray | float | str] +) + + +def sec_nsec(ts): + s = int(ts) + return [s, int((ts - s) * 1_000_000_000)] + + +class PoseWithCovarianceStamped(PoseWithCovariance, Timestamped): + msg_name = "geometry_msgs.PoseWithCovarianceStamped" + ts: float + frame_id: str + + @dispatch + def __init__(self, ts: float = 0.0, frame_id: str = "", **kwargs) -> None: + """Initialize with timestamp and frame_id.""" + self.frame_id = frame_id + self.ts = ts if ts != 0 else time.time() + super().__init__(**kwargs) + + @dispatch + def __init__( + self, + ts: float = 0.0, + frame_id: str = "", + pose: Pose | PoseConvertable | None = None, + covariance: list[float] | np.ndarray | None = None, + ) -> None: + """Initialize with timestamp, frame_id, pose and covariance.""" + self.frame_id = frame_id + self.ts = ts if ts != 0 else time.time() + if pose is None: + super().__init__() + else: + super().__init__(pose, covariance) + + def lcm_encode(self) -> bytes: + lcm_msg = LCMPoseWithCovarianceStamped() + lcm_msg.pose.pose = self.pose + # LCM expects list, not numpy array + if isinstance(self.covariance, np.ndarray): + lcm_msg.pose.covariance = self.covariance.tolist() + else: + lcm_msg.pose.covariance = list(self.covariance) + [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) + lcm_msg.header.frame_id = self.frame_id + return lcm_msg.lcm_encode() + + @classmethod + def lcm_decode(cls, data: bytes) -> PoseWithCovarianceStamped: + lcm_msg = LCMPoseWithCovarianceStamped.lcm_decode(data) + return cls( + ts=lcm_msg.header.stamp.sec + (lcm_msg.header.stamp.nsec / 1_000_000_000), + frame_id=lcm_msg.header.frame_id, + pose=Pose( + position=[ + lcm_msg.pose.pose.position.x, + lcm_msg.pose.pose.position.y, + lcm_msg.pose.pose.position.z, + ], + orientation=[ + lcm_msg.pose.pose.orientation.x, + lcm_msg.pose.pose.orientation.y, + lcm_msg.pose.pose.orientation.z, + lcm_msg.pose.pose.orientation.w, + ], + ), + covariance=lcm_msg.pose.covariance, + ) + + def __str__(self) -> str: + return ( + f"PoseWithCovarianceStamped(pos=[{self.x:.3f}, {self.y:.3f}, {self.z:.3f}], " + f"euler=[{self.roll:.3f}, {self.pitch:.3f}, {self.yaw:.3f}], " + f"cov_trace={np.trace(self.covariance_matrix):.3f})" + ) + + @classmethod + def from_ros_msg(cls, ros_msg: ROSPoseWithCovarianceStamped) -> PoseWithCovarianceStamped: + """Create a PoseWithCovarianceStamped from a ROS geometry_msgs/PoseWithCovarianceStamped message. + + Args: + ros_msg: ROS PoseWithCovarianceStamped message + + Returns: + PoseWithCovarianceStamped instance + """ + + # Convert timestamp from ROS header + ts = ros_msg.header.stamp.sec + (ros_msg.header.stamp.nanosec / 1_000_000_000) + + # Convert pose with covariance + pose_with_cov = PoseWithCovariance.from_ros_msg(ros_msg.pose) + + return cls( + ts=ts, + frame_id=ros_msg.header.frame_id, + pose=pose_with_cov.pose, + covariance=pose_with_cov.covariance, + ) + + def to_ros_msg(self) -> ROSPoseWithCovarianceStamped: + """Convert to a ROS geometry_msgs/PoseWithCovarianceStamped message. + + Returns: + ROS PoseWithCovarianceStamped message + """ + + ros_msg = ROSPoseWithCovarianceStamped() + + # Set header + ros_msg.header.frame_id = self.frame_id + ros_msg.header.stamp.sec = int(self.ts) + ros_msg.header.stamp.nanosec = int((self.ts - int(self.ts)) * 1_000_000_000) + + # Set pose with covariance + ros_msg.pose.pose = self.pose.to_ros_msg() + # ROS expects list, not numpy array + if isinstance(self.covariance, np.ndarray): + ros_msg.pose.covariance = self.covariance.tolist() + else: + ros_msg.pose.covariance = list(self.covariance) + + return ros_msg diff --git a/dimos/msgs/geometry_msgs/Quaternion.py b/dimos/msgs/geometry_msgs/Quaternion.py new file mode 100644 index 0000000000..6ce8c3bf2d --- /dev/null +++ b/dimos/msgs/geometry_msgs/Quaternion.py @@ -0,0 +1,246 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from collections.abc import Sequence +from io import BytesIO +import struct +from typing import BinaryIO, TypeAlias + +from dimos_lcm.geometry_msgs import Quaternion as LCMQuaternion +import numpy as np +from plum import dispatch +from scipy.spatial.transform import Rotation as R + +from dimos.msgs.geometry_msgs.Vector3 import Vector3 + +# Types that can be converted to/from Quaternion +QuaternionConvertable: TypeAlias = Sequence[int | float] | LCMQuaternion | np.ndarray + + +class Quaternion(LCMQuaternion): + x: float = 0.0 + y: float = 0.0 + z: float = 0.0 + w: float = 1.0 + msg_name = "geometry_msgs.Quaternion" + + @classmethod + def lcm_decode(cls, data: bytes | BinaryIO): + if not hasattr(data, "read"): + data = BytesIO(data) + if data.read(8) != cls._get_packed_fingerprint(): + raise ValueError("Decode error") + return cls._lcm_decode_one(data) + + @classmethod + def _lcm_decode_one(cls, buf): + return cls(struct.unpack(">dddd", buf.read(32))) + + @dispatch + def __init__(self) -> None: ... + + @dispatch + def __init__(self, x: int | float, y: int | float, z: int | float, w: int | float) -> None: + self.x = float(x) + self.y = float(y) + self.z = float(z) + self.w = float(w) + + @dispatch + def __init__(self, sequence: Sequence[int | float] | np.ndarray) -> None: + if isinstance(sequence, np.ndarray): + if sequence.size != 4: + raise ValueError("Quaternion requires exactly 4 components [x, y, z, w]") + else: + if len(sequence) != 4: + raise ValueError("Quaternion requires exactly 4 components [x, y, z, w]") + + self.x = sequence[0] + self.y = sequence[1] + self.z = sequence[2] + self.w = sequence[3] + + @dispatch + def __init__(self, quaternion: Quaternion) -> None: + """Initialize from another Quaternion (copy constructor).""" + self.x, self.y, self.z, self.w = quaternion.x, quaternion.y, quaternion.z, quaternion.w + + @dispatch + def __init__(self, lcm_quaternion: LCMQuaternion) -> None: + """Initialize from an LCM Quaternion.""" + self.x, self.y, self.z, self.w = ( + lcm_quaternion.x, + lcm_quaternion.y, + lcm_quaternion.z, + lcm_quaternion.w, + ) + + def to_tuple(self) -> tuple[float, float, float, float]: + """Tuple representation of the quaternion (x, y, z, w).""" + return (self.x, self.y, self.z, self.w) + + def to_list(self) -> list[float]: + """List representation of the quaternion (x, y, z, w).""" + return [self.x, self.y, self.z, self.w] + + def to_numpy(self) -> np.ndarray: + """Numpy array representation of the quaternion (x, y, z, w).""" + return np.array([self.x, self.y, self.z, self.w]) + + @property + def euler(self) -> Vector3: + return self.to_euler() + + @property + def radians(self) -> Vector3: + return self.to_euler() + + def to_radians(self) -> Vector3: + """Radians representation of the quaternion (x, y, z, w).""" + return self.to_euler() + + @classmethod + def from_euler(cls, vector: Vector3) -> Quaternion: + """Convert Euler angles (roll, pitch, yaw) in radians to quaternion. + + Args: + vector: Vector3 containing (roll, pitch, yaw) in radians + + Returns: + Quaternion representation + """ + + # Calculate quaternion components + cy = np.cos(vector.yaw * 0.5) + sy = np.sin(vector.yaw * 0.5) + cp = np.cos(vector.pitch * 0.5) + sp = np.sin(vector.pitch * 0.5) + cr = np.cos(vector.roll * 0.5) + sr = np.sin(vector.roll * 0.5) + + w = cr * cp * cy + sr * sp * sy + x = sr * cp * cy - cr * sp * sy + y = cr * sp * cy + sr * cp * sy + z = cr * cp * sy - sr * sp * cy + + return cls(x, y, z, w) + + def to_euler(self) -> Vector3: + """Convert quaternion to Euler angles (roll, pitch, yaw) in radians. + + Returns: + Vector3: Euler angles as (roll, pitch, yaw) in radians + """ + # Use scipy for accurate quaternion to euler conversion + quat = [self.x, self.y, self.z, self.w] + rotation = R.from_quat(quat) + euler_angles = rotation.as_euler("xyz") # roll, pitch, yaw + + return Vector3(euler_angles[0], euler_angles[1], euler_angles[2]) + + def __getitem__(self, idx: int) -> float: + """Allow indexing into quaternion components: 0=x, 1=y, 2=z, 3=w.""" + if idx == 0: + return self.x + elif idx == 1: + return self.y + elif idx == 2: + return self.z + elif idx == 3: + return self.w + else: + raise IndexError(f"Quaternion index {idx} out of range [0-3]") + + def __repr__(self) -> str: + return f"Quaternion({self.x:.6f}, {self.y:.6f}, {self.z:.6f}, {self.w:.6f})" + + def __str__(self) -> str: + return self.__repr__() + + def __eq__(self, other) -> bool: + if not isinstance(other, Quaternion): + return False + return self.x == other.x and self.y == other.y and self.z == other.z and self.w == other.w + + def __mul__(self, other: Quaternion) -> Quaternion: + """Multiply two quaternions (Hamilton product). + + The result represents the composition of rotations: + q1 * q2 represents rotating by q2 first, then by q1. + """ + if not isinstance(other, Quaternion): + raise TypeError(f"Cannot multiply Quaternion with {type(other)}") + + # Hamilton product formula + w = self.w * other.w - self.x * other.x - self.y * other.y - self.z * other.z + x = self.w * other.x + self.x * other.w + self.y * other.z - self.z * other.y + y = self.w * other.y - self.x * other.z + self.y * other.w + self.z * other.x + z = self.w * other.z + self.x * other.y - self.y * other.x + self.z * other.w + + return Quaternion(x, y, z, w) + + def conjugate(self) -> Quaternion: + """Return the conjugate of the quaternion. + + For unit quaternions, the conjugate represents the inverse rotation. + """ + return Quaternion(-self.x, -self.y, -self.z, self.w) + + def inverse(self) -> Quaternion: + """Return the inverse of the quaternion. + + For unit quaternions, this is equivalent to the conjugate. + For non-unit quaternions, this is conjugate / norm^2. + """ + norm_sq = self.x**2 + self.y**2 + self.z**2 + self.w**2 + if norm_sq == 0: + raise ZeroDivisionError("Cannot invert zero quaternion") + + # For unit quaternions (norm_sq ≈ 1), this simplifies to conjugate + if np.isclose(norm_sq, 1.0): + return self.conjugate() + + # For non-unit quaternions + conj = self.conjugate() + return Quaternion(conj.x / norm_sq, conj.y / norm_sq, conj.z / norm_sq, conj.w / norm_sq) + + def normalize(self) -> Quaternion: + """Return a normalized (unit) quaternion.""" + norm = np.sqrt(self.x**2 + self.y**2 + self.z**2 + self.w**2) + if norm == 0: + raise ZeroDivisionError("Cannot normalize zero quaternion") + return Quaternion(self.x / norm, self.y / norm, self.z / norm, self.w / norm) + + def rotate_vector(self, vector: Vector3) -> Vector3: + """Rotate a 3D vector by this quaternion. + + Args: + vector: The vector to rotate + + Returns: + The rotated vector + """ + # For unit quaternions, conjugate equals inverse, so we use conjugate for efficiency + # The rotation formula is: q * v * q^* where q^* is the conjugate + + # Convert vector to pure quaternion (w=0) + v_quat = Quaternion(vector.x, vector.y, vector.z, 0) + + # Apply rotation: q * v * q^* (conjugate for unit quaternions) + rotated = self * v_quat * self.conjugate() + + # Extract vector components + return Vector3(rotated.x, rotated.y, rotated.z) diff --git a/dimos/msgs/geometry_msgs/Transform.py b/dimos/msgs/geometry_msgs/Transform.py new file mode 100644 index 0000000000..b168eceaa5 --- /dev/null +++ b/dimos/msgs/geometry_msgs/Transform.py @@ -0,0 +1,351 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import time +from typing import BinaryIO + +from dimos_lcm.geometry_msgs import ( + Transform as LCMTransform, + TransformStamped as LCMTransformStamped, +) + +try: + from geometry_msgs.msg import ( + Quaternion as ROSQuaternion, + Transform as ROSTransform, + TransformStamped as ROSTransformStamped, + Vector3 as ROSVector3, + ) +except ImportError: + ROSTransformStamped = None + ROSTransform = None + ROSVector3 = None + ROSQuaternion = None + +from dimos.msgs.geometry_msgs.Quaternion import Quaternion +from dimos.msgs.geometry_msgs.Vector3 import Vector3 +from dimos.msgs.std_msgs import Header +from dimos.types.timestamped import Timestamped + + +class Transform(Timestamped): + translation: Vector3 + rotation: Quaternion + ts: float + frame_id: str + child_frame_id: str + msg_name = "tf2_msgs.TFMessage" + + def __init__( + self, + translation: Vector3 | None = None, + rotation: Quaternion | None = None, + frame_id: str = "world", + child_frame_id: str = "unset", + ts: float = 0.0, + **kwargs, + ) -> None: + self.frame_id = frame_id + self.child_frame_id = child_frame_id + self.ts = ts if ts != 0.0 else time.time() + self.translation = translation if translation is not None else Vector3() + self.rotation = rotation if rotation is not None else Quaternion() + + def __repr__(self) -> str: + return f"Transform(translation={self.translation!r}, rotation={self.rotation!r})" + + def __str__(self) -> str: + return f"Transform:\n {self.frame_id} -> {self.child_frame_id} Translation: {self.translation}\n Rotation: {self.rotation}" + + def __eq__(self, other) -> bool: + """Check if two transforms are equal.""" + if not isinstance(other, Transform): + return False + return self.translation == other.translation and self.rotation == other.rotation + + @classmethod + def identity(cls) -> Transform: + """Create an identity transform.""" + return cls() + + def lcm_transform(self) -> LCMTransformStamped: + return LCMTransformStamped( + child_frame_id=self.child_frame_id, + header=Header(self.ts, self.frame_id), + transform=LCMTransform( + translation=self.translation, + rotation=self.rotation, + ), + ) + + def apply(self, other: Transform) -> Transform: + return self.__add__(other) + + def __add__(self, other: Transform) -> Transform: + """Compose two transforms (transform composition). + + The operation self + other represents applying transformation 'other' + in the coordinate frame defined by 'self'. This is equivalent to: + - First apply transformation 'self' (from frame A to frame B) + - Then apply transformation 'other' (from frame B to frame C) + + Args: + other: The transform to compose with this one + + Returns: + A new Transform representing the composed transformation + + Example: + t1 = Transform(Vector3(1, 0, 0), Quaternion(0, 0, 0, 1)) + t2 = Transform(Vector3(2, 0, 0), Quaternion(0, 0, 0, 1)) + t3 = t1 + t2 # Combined transform: translation (3, 0, 0) + """ + if not isinstance(other, Transform): + raise TypeError(f"Cannot add Transform and {type(other).__name__}") + + # Compose orientations: self.rotation * other.rotation + new_rotation = self.rotation * other.rotation + + # Transform other's translation by self's rotation, then add to self's translation + rotated_translation = self.rotation.rotate_vector(other.translation) + new_translation = self.translation + rotated_translation + + return Transform( + translation=new_translation, + rotation=new_rotation, + frame_id=self.frame_id, + child_frame_id=other.child_frame_id, + ts=self.ts, + ) + + def inverse(self) -> Transform: + """Compute the inverse transform. + + The inverse transform reverses the direction of the transformation. + If this transform goes from frame A to frame B, the inverse goes from B to A. + + Returns: + A new Transform representing the inverse transformation + """ + # Inverse rotation + inv_rotation = self.rotation.inverse() + + # Inverse translation: -R^(-1) * t + inv_translation = inv_rotation.rotate_vector(self.translation) + inv_translation = Vector3(-inv_translation.x, -inv_translation.y, -inv_translation.z) + + return Transform( + translation=inv_translation, + rotation=inv_rotation, + frame_id=self.child_frame_id, # Swap frame references + child_frame_id=self.frame_id, + ts=self.ts, + ) + + @classmethod + def from_ros_transform_stamped(cls, ros_msg: ROSTransformStamped) -> Transform: + """Create a Transform from a ROS geometry_msgs/TransformStamped message. + + Args: + ros_msg: ROS TransformStamped message + + Returns: + Transform instance + """ + + # Convert timestamp + ts = ros_msg.header.stamp.sec + (ros_msg.header.stamp.nanosec / 1_000_000_000) + + # Convert translation + translation = Vector3( + ros_msg.transform.translation.x, + ros_msg.transform.translation.y, + ros_msg.transform.translation.z, + ) + + # Convert rotation + rotation = Quaternion( + ros_msg.transform.rotation.x, + ros_msg.transform.rotation.y, + ros_msg.transform.rotation.z, + ros_msg.transform.rotation.w, + ) + + return cls( + translation=translation, + rotation=rotation, + frame_id=ros_msg.header.frame_id, + child_frame_id=ros_msg.child_frame_id, + ts=ts, + ) + + def to_ros_transform_stamped(self) -> ROSTransformStamped: + """Convert to a ROS geometry_msgs/TransformStamped message. + + Returns: + ROS TransformStamped message + """ + + ros_msg = ROSTransformStamped() + + # Set header + ros_msg.header.frame_id = self.frame_id + ros_msg.header.stamp.sec = int(self.ts) + ros_msg.header.stamp.nanosec = int((self.ts - int(self.ts)) * 1_000_000_000) + + # Set child frame + ros_msg.child_frame_id = self.child_frame_id + + # Set transform + ros_msg.transform.translation = ROSVector3( + x=self.translation.x, y=self.translation.y, z=self.translation.z + ) + ros_msg.transform.rotation = ROSQuaternion( + x=self.rotation.x, y=self.rotation.y, z=self.rotation.z, w=self.rotation.w + ) + + return ros_msg + + def __neg__(self) -> Transform: + """Unary minus operator returns the inverse transform.""" + return self.inverse() + + @classmethod + def from_pose(cls, frame_id: str, pose: Pose | PoseStamped) -> Transform: + """Create a Transform from a Pose or PoseStamped. + + Args: + pose: A Pose or PoseStamped object to convert + + Returns: + A Transform with the same translation and rotation as the pose + """ + # Import locally to avoid circular imports + from dimos.msgs.geometry_msgs.Pose import Pose + from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped + + # Handle both Pose and PoseStamped + if isinstance(pose, PoseStamped): + return cls( + translation=pose.position, + rotation=pose.orientation, + frame_id=pose.frame_id, + child_frame_id=frame_id, + ts=pose.ts, + ) + elif isinstance(pose, Pose): + return cls( + translation=pose.position, + rotation=pose.orientation, + child_frame_id=frame_id, + ) + else: + raise TypeError(f"Expected Pose or PoseStamped, got {type(pose).__name__}") + + def to_pose(self, **kwargs) -> PoseStamped: + """Create a Transform from a Pose or PoseStamped. + + Args: + pose: A Pose or PoseStamped object to convert + + Returns: + A Transform with the same translation and rotation as the pose + """ + # Import locally to avoid circular imports + from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped + + # Handle both Pose and PoseStamped + return PoseStamped( + **{ + "position": self.translation, + "orientation": self.rotation, + "frame_id": self.frame_id, + }, + **kwargs, + ) + + def to_matrix(self) -> np.ndarray: + """Convert Transform to a 4x4 transformation matrix. + + Returns a homogeneous transformation matrix that represents both + the rotation and translation of this transform. + + Returns: + np.ndarray: A 4x4 homogeneous transformation matrix + """ + import numpy as np + + # Extract quaternion components + x, y, z, w = self.rotation.x, self.rotation.y, self.rotation.z, self.rotation.w + + # Build rotation matrix from quaternion using standard formula + # This avoids numerical issues compared to converting to axis-angle first + rotation_matrix = np.array( + [ + [1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w)], + [2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w)], + [2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)], + ] + ) + + # Build 4x4 homogeneous transformation matrix + matrix = np.eye(4) + matrix[:3, :3] = rotation_matrix + matrix[:3, 3] = [self.translation.x, self.translation.y, self.translation.z] + + return matrix + + def lcm_encode(self) -> bytes: + # we get a circular import otherwise + from dimos.msgs.tf2_msgs.TFMessage import TFMessage + + return TFMessage(self).lcm_encode() + + @classmethod + def lcm_decode(cls, data: bytes | BinaryIO) -> Transform: + """Decode from LCM TFMessage bytes.""" + from dimos_lcm.tf2_msgs import TFMessage as LCMTFMessage + + lcm_msg = LCMTFMessage.lcm_decode(data) + + if not lcm_msg.transforms: + raise ValueError("No transforms found in LCM message") + + # Get the first transform from the message + lcm_transform_stamped = lcm_msg.transforms[0] + + # Extract timestamp from header + ts = lcm_transform_stamped.header.stamp.sec + ( + lcm_transform_stamped.header.stamp.nsec / 1_000_000_000 + ) + + # Create and return Transform instance + return cls( + translation=Vector3( + lcm_transform_stamped.transform.translation.x, + lcm_transform_stamped.transform.translation.y, + lcm_transform_stamped.transform.translation.z, + ), + rotation=Quaternion( + lcm_transform_stamped.transform.rotation.x, + lcm_transform_stamped.transform.rotation.y, + lcm_transform_stamped.transform.rotation.z, + lcm_transform_stamped.transform.rotation.w, + ), + frame_id=lcm_transform_stamped.header.frame_id, + child_frame_id=lcm_transform_stamped.child_frame_id, + ts=ts, + ) diff --git a/dimos/msgs/geometry_msgs/Twist.py b/dimos/msgs/geometry_msgs/Twist.py new file mode 100644 index 0000000000..e824e1cfbf --- /dev/null +++ b/dimos/msgs/geometry_msgs/Twist.py @@ -0,0 +1,136 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dimos_lcm.geometry_msgs import Twist as LCMTwist +from plum import dispatch + +try: + from geometry_msgs.msg import Twist as ROSTwist, Vector3 as ROSVector3 +except ImportError: + ROSTwist = None + ROSVector3 = None + +# Import Quaternion at runtime for beartype compatibility +# (beartype needs to resolve forward references at runtime) +from dimos.msgs.geometry_msgs.Quaternion import Quaternion +from dimos.msgs.geometry_msgs.Vector3 import Vector3, VectorLike + + +class Twist(LCMTwist): + linear: Vector3 + angular: Vector3 + msg_name = "geometry_msgs.Twist" + + @dispatch + def __init__(self) -> None: + """Initialize a zero twist (no linear or angular velocity).""" + self.linear = Vector3() + self.angular = Vector3() + + @dispatch + def __init__(self, linear: VectorLike, angular: VectorLike) -> None: + """Initialize a twist from linear and angular velocities.""" + + self.linear = Vector3(linear) + self.angular = Vector3(angular) + + @dispatch + def __init__(self, linear: VectorLike, angular: Quaternion) -> None: + """Initialize a twist from linear velocity and angular as quaternion (converted to euler).""" + self.linear = Vector3(linear) + self.angular = angular.to_euler() + + @dispatch + def __init__(self, twist: Twist) -> None: + """Initialize from another Twist (copy constructor).""" + self.linear = Vector3(twist.linear) + self.angular = Vector3(twist.angular) + + @dispatch + def __init__(self, lcm_twist: LCMTwist) -> None: + """Initialize from an LCM Twist.""" + self.linear = Vector3(lcm_twist.linear) + self.angular = Vector3(lcm_twist.angular) + + @dispatch + def __init__(self, **kwargs) -> None: + """Handle keyword arguments for LCM compatibility.""" + linear = kwargs.get("linear", Vector3()) + angular = kwargs.get("angular", Vector3()) + + self.__init__(linear, angular) + + def __repr__(self) -> str: + return f"Twist(linear={self.linear!r}, angular={self.angular!r})" + + def __str__(self) -> str: + return f"Twist:\n Linear: {self.linear}\n Angular: {self.angular}" + + def __eq__(self, other) -> bool: + """Check if two twists are equal.""" + if not isinstance(other, Twist): + return False + return self.linear == other.linear and self.angular == other.angular + + @classmethod + def zero(cls) -> Twist: + """Create a zero twist (no motion).""" + return cls() + + def is_zero(self) -> bool: + """Check if this is a zero twist (no linear or angular velocity).""" + return self.linear.is_zero() and self.angular.is_zero() + + def __bool__(self) -> bool: + """Boolean conversion for Twist. + + A Twist is considered False if it's a zero twist (no motion), + and True otherwise. + + Returns: + False if twist is zero, True otherwise + """ + return not self.is_zero() + + @classmethod + def from_ros_msg(cls, ros_msg: ROSTwist) -> Twist: + """Create a Twist from a ROS geometry_msgs/Twist message. + + Args: + ros_msg: ROS Twist message + + Returns: + Twist instance + """ + + linear = Vector3(ros_msg.linear.x, ros_msg.linear.y, ros_msg.linear.z) + angular = Vector3(ros_msg.angular.x, ros_msg.angular.y, ros_msg.angular.z) + return cls(linear, angular) + + def to_ros_msg(self) -> ROSTwist: + """Convert to a ROS geometry_msgs/Twist message. + + Returns: + ROS Twist message + """ + + ros_msg = ROSTwist() + ros_msg.linear = ROSVector3(x=self.linear.x, y=self.linear.y, z=self.linear.z) + ros_msg.angular = ROSVector3(x=self.angular.x, y=self.angular.y, z=self.angular.z) + return ros_msg + + +__all__ = ["Quaternion", "Twist"] diff --git a/dimos/msgs/geometry_msgs/TwistStamped.py b/dimos/msgs/geometry_msgs/TwistStamped.py new file mode 100644 index 0000000000..1a14d8cb0d --- /dev/null +++ b/dimos/msgs/geometry_msgs/TwistStamped.py @@ -0,0 +1,118 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import time +from typing import BinaryIO, TypeAlias + +from dimos_lcm.geometry_msgs import TwistStamped as LCMTwistStamped +from plum import dispatch + +try: + from geometry_msgs.msg import TwistStamped as ROSTwistStamped +except ImportError: + ROSTwistStamped = None + +from dimos.msgs.geometry_msgs.Twist import Twist +from dimos.msgs.geometry_msgs.Vector3 import VectorConvertable +from dimos.types.timestamped import Timestamped + +# Types that can be converted to/from TwistStamped +TwistConvertable: TypeAlias = ( + tuple[VectorConvertable, VectorConvertable] | LCMTwistStamped | dict[str, VectorConvertable] +) + + +def sec_nsec(ts): + s = int(ts) + return [s, int((ts - s) * 1_000_000_000)] + + +class TwistStamped(Twist, Timestamped): + msg_name = "geometry_msgs.TwistStamped" + ts: float + frame_id: str + + @dispatch + def __init__(self, ts: float = 0.0, frame_id: str = "", **kwargs) -> None: + self.frame_id = frame_id + self.ts = ts if ts != 0 else time.time() + super().__init__(**kwargs) + + def lcm_encode(self) -> bytes: + lcm_msg = LCMTwistStamped() + lcm_msg.twist = self + [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) + lcm_msg.header.frame_id = self.frame_id + return lcm_msg.lcm_encode() + + @classmethod + def lcm_decode(cls, data: bytes | BinaryIO) -> TwistStamped: + lcm_msg = LCMTwistStamped.lcm_decode(data) + return cls( + ts=lcm_msg.header.stamp.sec + (lcm_msg.header.stamp.nsec / 1_000_000_000), + frame_id=lcm_msg.header.frame_id, + linear=[lcm_msg.twist.linear.x, lcm_msg.twist.linear.y, lcm_msg.twist.linear.z], + angular=[lcm_msg.twist.angular.x, lcm_msg.twist.angular.y, lcm_msg.twist.angular.z], + ) + + def __str__(self) -> str: + return ( + f"TwistStamped(linear=[{self.linear.x:.3f}, {self.linear.y:.3f}, {self.linear.z:.3f}], " + f"angular=[{self.angular.x:.3f}, {self.angular.y:.3f}, {self.angular.z:.3f}])" + ) + + @classmethod + def from_ros_msg(cls, ros_msg: ROSTwistStamped) -> TwistStamped: + """Create a TwistStamped from a ROS geometry_msgs/TwistStamped message. + + Args: + ros_msg: ROS TwistStamped message + + Returns: + TwistStamped instance + """ + + # Convert timestamp from ROS header + ts = ros_msg.header.stamp.sec + (ros_msg.header.stamp.nanosec / 1_000_000_000) + + # Convert twist + twist = Twist.from_ros_msg(ros_msg.twist) + + return cls( + ts=ts, + frame_id=ros_msg.header.frame_id, + linear=twist.linear, + angular=twist.angular, + ) + + def to_ros_msg(self) -> ROSTwistStamped: + """Convert to a ROS geometry_msgs/TwistStamped message. + + Returns: + ROS TwistStamped message + """ + + ros_msg = ROSTwistStamped() + + # Set header + ros_msg.header.frame_id = self.frame_id + ros_msg.header.stamp.sec = int(self.ts) + ros_msg.header.stamp.nanosec = int((self.ts - int(self.ts)) * 1_000_000_000) + + # Set twist + ros_msg.twist = Twist.to_ros_msg(self) + + return ros_msg diff --git a/dimos/msgs/geometry_msgs/TwistWithCovariance.py b/dimos/msgs/geometry_msgs/TwistWithCovariance.py new file mode 100644 index 0000000000..53e77beaf7 --- /dev/null +++ b/dimos/msgs/geometry_msgs/TwistWithCovariance.py @@ -0,0 +1,225 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import TypeAlias + +from dimos_lcm.geometry_msgs import TwistWithCovariance as LCMTwistWithCovariance +import numpy as np +from plum import dispatch + +try: + from geometry_msgs.msg import TwistWithCovariance as ROSTwistWithCovariance +except ImportError: + ROSTwistWithCovariance = None + +from dimos.msgs.geometry_msgs.Twist import Twist +from dimos.msgs.geometry_msgs.Vector3 import Vector3, VectorConvertable + +# Types that can be converted to/from TwistWithCovariance +TwistWithCovarianceConvertable: TypeAlias = ( + tuple[Twist | tuple[VectorConvertable, VectorConvertable], list[float] | np.ndarray] + | LCMTwistWithCovariance + | dict[str, Twist | tuple[VectorConvertable, VectorConvertable] | list[float] | np.ndarray] +) + + +class TwistWithCovariance(LCMTwistWithCovariance): + twist: Twist + msg_name = "geometry_msgs.TwistWithCovariance" + + @dispatch + def __init__(self) -> None: + """Initialize with default twist and zero covariance.""" + self.twist = Twist() + self.covariance = np.zeros(36) + + @dispatch + def __init__( + self, + twist: Twist | tuple[VectorConvertable, VectorConvertable], + covariance: list[float] | np.ndarray | None = None, + ) -> None: + """Initialize with twist and optional covariance.""" + if isinstance(twist, Twist): + self.twist = twist + else: + # Assume it's a tuple of (linear, angular) + self.twist = Twist(twist[0], twist[1]) + + if covariance is None: + self.covariance = np.zeros(36) + else: + self.covariance = np.array(covariance, dtype=float).reshape(36) + + @dispatch + def __init__(self, twist_with_cov: TwistWithCovariance) -> None: + """Initialize from another TwistWithCovariance (copy constructor).""" + self.twist = Twist(twist_with_cov.twist) + self.covariance = np.array(twist_with_cov.covariance).copy() + + @dispatch + def __init__(self, lcm_twist_with_cov: LCMTwistWithCovariance) -> None: + """Initialize from an LCM TwistWithCovariance.""" + self.twist = Twist(lcm_twist_with_cov.twist) + self.covariance = np.array(lcm_twist_with_cov.covariance) + + @dispatch + def __init__( + self, + twist_dict: dict[ + str, Twist | tuple[VectorConvertable, VectorConvertable] | list[float] | np.ndarray + ], + ) -> None: + """Initialize from a dictionary with 'twist' and 'covariance' keys.""" + twist = twist_dict["twist"] + if isinstance(twist, Twist): + self.twist = twist + else: + # Assume it's a tuple of (linear, angular) + self.twist = Twist(twist[0], twist[1]) + + covariance = twist_dict.get("covariance") + if covariance is None: + self.covariance = np.zeros(36) + else: + self.covariance = np.array(covariance, dtype=float).reshape(36) + + @dispatch + def __init__( + self, + twist_tuple: tuple[ + Twist | tuple[VectorConvertable, VectorConvertable], list[float] | np.ndarray + ], + ) -> None: + """Initialize from a tuple of (twist, covariance).""" + twist = twist_tuple[0] + if isinstance(twist, Twist): + self.twist = twist + else: + # Assume it's a tuple of (linear, angular) + self.twist = Twist(twist[0], twist[1]) + self.covariance = np.array(twist_tuple[1], dtype=float).reshape(36) + + def __getattribute__(self, name: str): + """Override to ensure covariance is always returned as numpy array.""" + if name == "covariance": + cov = object.__getattribute__(self, "covariance") + if not isinstance(cov, np.ndarray): + return np.array(cov, dtype=float) + return cov + return super().__getattribute__(name) + + def __setattr__(self, name: str, value) -> None: + """Override to ensure covariance is stored as numpy array.""" + if name == "covariance": + if not isinstance(value, np.ndarray): + value = np.array(value, dtype=float).reshape(36) + super().__setattr__(name, value) + + @property + def linear(self) -> Vector3: + """Linear velocity vector.""" + return self.twist.linear + + @property + def angular(self) -> Vector3: + """Angular velocity vector.""" + return self.twist.angular + + @property + def covariance_matrix(self) -> np.ndarray: + """Get covariance as 6x6 matrix.""" + return self.covariance.reshape(6, 6) + + @covariance_matrix.setter + def covariance_matrix(self, value: np.ndarray) -> None: + """Set covariance from 6x6 matrix.""" + self.covariance = np.array(value).reshape(36) + + def __repr__(self) -> str: + return f"TwistWithCovariance(twist={self.twist!r}, covariance=<{self.covariance.shape[0] if isinstance(self.covariance, np.ndarray) else len(self.covariance)} elements>)" + + def __str__(self) -> str: + return ( + f"TwistWithCovariance(linear=[{self.linear.x:.3f}, {self.linear.y:.3f}, {self.linear.z:.3f}], " + f"angular=[{self.angular.x:.3f}, {self.angular.y:.3f}, {self.angular.z:.3f}], " + f"cov_trace={np.trace(self.covariance_matrix):.3f})" + ) + + def __eq__(self, other) -> bool: + """Check if two TwistWithCovariance are equal.""" + if not isinstance(other, TwistWithCovariance): + return False + return self.twist == other.twist and np.allclose(self.covariance, other.covariance) + + def is_zero(self) -> bool: + """Check if this is a zero twist (no linear or angular velocity).""" + return self.twist.is_zero() + + def __bool__(self) -> bool: + """Boolean conversion - False if zero twist, True otherwise.""" + return not self.is_zero() + + def lcm_encode(self) -> bytes: + """Encode to LCM binary format.""" + lcm_msg = LCMTwistWithCovariance() + lcm_msg.twist = self.twist + # LCM expects list, not numpy array + if isinstance(self.covariance, np.ndarray): + lcm_msg.covariance = self.covariance.tolist() + else: + lcm_msg.covariance = list(self.covariance) + return lcm_msg.lcm_encode() + + @classmethod + def lcm_decode(cls, data: bytes) -> TwistWithCovariance: + """Decode from LCM binary format.""" + lcm_msg = LCMTwistWithCovariance.lcm_decode(data) + twist = Twist( + linear=[lcm_msg.twist.linear.x, lcm_msg.twist.linear.y, lcm_msg.twist.linear.z], + angular=[lcm_msg.twist.angular.x, lcm_msg.twist.angular.y, lcm_msg.twist.angular.z], + ) + return cls(twist, lcm_msg.covariance) + + @classmethod + def from_ros_msg(cls, ros_msg: ROSTwistWithCovariance) -> TwistWithCovariance: + """Create a TwistWithCovariance from a ROS geometry_msgs/TwistWithCovariance message. + + Args: + ros_msg: ROS TwistWithCovariance message + + Returns: + TwistWithCovariance instance + """ + + twist = Twist.from_ros_msg(ros_msg.twist) + return cls(twist, list(ros_msg.covariance)) + + def to_ros_msg(self) -> ROSTwistWithCovariance: + """Convert to a ROS geometry_msgs/TwistWithCovariance message. + + Returns: + ROS TwistWithCovariance message + """ + + ros_msg = ROSTwistWithCovariance() + ros_msg.twist = self.twist.to_ros_msg() + # ROS expects list, not numpy array + if isinstance(self.covariance, np.ndarray): + ros_msg.covariance = self.covariance.tolist() + else: + ros_msg.covariance = list(self.covariance) + return ros_msg diff --git a/dimos/msgs/geometry_msgs/TwistWithCovarianceStamped.py b/dimos/msgs/geometry_msgs/TwistWithCovarianceStamped.py new file mode 100644 index 0000000000..20684d9375 --- /dev/null +++ b/dimos/msgs/geometry_msgs/TwistWithCovarianceStamped.py @@ -0,0 +1,169 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import time +from typing import TypeAlias + +from dimos_lcm.geometry_msgs import TwistWithCovarianceStamped as LCMTwistWithCovarianceStamped +import numpy as np +from plum import dispatch + +try: + from geometry_msgs.msg import TwistWithCovarianceStamped as ROSTwistWithCovarianceStamped +except ImportError: + ROSTwistWithCovarianceStamped = None + +from dimos.msgs.geometry_msgs.Twist import Twist +from dimos.msgs.geometry_msgs.TwistWithCovariance import TwistWithCovariance +from dimos.msgs.geometry_msgs.Vector3 import VectorConvertable +from dimos.types.timestamped import Timestamped + +# Types that can be converted to/from TwistWithCovarianceStamped +TwistWithCovarianceStampedConvertable: TypeAlias = ( + tuple[Twist | tuple[VectorConvertable, VectorConvertable], list[float] | np.ndarray] + | LCMTwistWithCovarianceStamped + | dict[ + str, + Twist + | tuple[VectorConvertable, VectorConvertable] + | list[float] + | np.ndarray + | float + | str, + ] +) + + +def sec_nsec(ts): + s = int(ts) + return [s, int((ts - s) * 1_000_000_000)] + + +class TwistWithCovarianceStamped(TwistWithCovariance, Timestamped): + msg_name = "geometry_msgs.TwistWithCovarianceStamped" + ts: float + frame_id: str + + @dispatch + def __init__(self, ts: float = 0.0, frame_id: str = "", **kwargs) -> None: + """Initialize with timestamp and frame_id.""" + self.frame_id = frame_id + self.ts = ts if ts != 0 else time.time() + super().__init__(**kwargs) + + @dispatch + def __init__( + self, + ts: float = 0.0, + frame_id: str = "", + twist: Twist | tuple[VectorConvertable, VectorConvertable] | None = None, + covariance: list[float] | np.ndarray | None = None, + ) -> None: + """Initialize with timestamp, frame_id, twist and covariance.""" + self.frame_id = frame_id + self.ts = ts if ts != 0 else time.time() + if twist is None: + super().__init__() + else: + super().__init__(twist, covariance) + + def lcm_encode(self) -> bytes: + lcm_msg = LCMTwistWithCovarianceStamped() + lcm_msg.twist.twist = self.twist + # LCM expects list, not numpy array + if isinstance(self.covariance, np.ndarray): + lcm_msg.twist.covariance = self.covariance.tolist() + else: + lcm_msg.twist.covariance = list(self.covariance) + [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) + lcm_msg.header.frame_id = self.frame_id + return lcm_msg.lcm_encode() + + @classmethod + def lcm_decode(cls, data: bytes) -> TwistWithCovarianceStamped: + lcm_msg = LCMTwistWithCovarianceStamped.lcm_decode(data) + return cls( + ts=lcm_msg.header.stamp.sec + (lcm_msg.header.stamp.nsec / 1_000_000_000), + frame_id=lcm_msg.header.frame_id, + twist=Twist( + linear=[ + lcm_msg.twist.twist.linear.x, + lcm_msg.twist.twist.linear.y, + lcm_msg.twist.twist.linear.z, + ], + angular=[ + lcm_msg.twist.twist.angular.x, + lcm_msg.twist.twist.angular.y, + lcm_msg.twist.twist.angular.z, + ], + ), + covariance=lcm_msg.twist.covariance, + ) + + def __str__(self) -> str: + return ( + f"TwistWithCovarianceStamped(linear=[{self.linear.x:.3f}, {self.linear.y:.3f}, {self.linear.z:.3f}], " + f"angular=[{self.angular.x:.3f}, {self.angular.y:.3f}, {self.angular.z:.3f}], " + f"cov_trace={np.trace(self.covariance_matrix):.3f})" + ) + + @classmethod + def from_ros_msg(cls, ros_msg: ROSTwistWithCovarianceStamped) -> TwistWithCovarianceStamped: + """Create a TwistWithCovarianceStamped from a ROS geometry_msgs/TwistWithCovarianceStamped message. + + Args: + ros_msg: ROS TwistWithCovarianceStamped message + + Returns: + TwistWithCovarianceStamped instance + """ + + # Convert timestamp from ROS header + ts = ros_msg.header.stamp.sec + (ros_msg.header.stamp.nanosec / 1_000_000_000) + + # Convert twist with covariance + twist_with_cov = TwistWithCovariance.from_ros_msg(ros_msg.twist) + + return cls( + ts=ts, + frame_id=ros_msg.header.frame_id, + twist=twist_with_cov.twist, + covariance=twist_with_cov.covariance, + ) + + def to_ros_msg(self) -> ROSTwistWithCovarianceStamped: + """Convert to a ROS geometry_msgs/TwistWithCovarianceStamped message. + + Returns: + ROS TwistWithCovarianceStamped message + """ + + ros_msg = ROSTwistWithCovarianceStamped() + + # Set header + ros_msg.header.frame_id = self.frame_id + ros_msg.header.stamp.sec = int(self.ts) + ros_msg.header.stamp.nanosec = int((self.ts - int(self.ts)) * 1_000_000_000) + + # Set twist with covariance + ros_msg.twist.twist = self.twist.to_ros_msg() + # ROS expects list, not numpy array + if isinstance(self.covariance, np.ndarray): + ros_msg.twist.covariance = self.covariance.tolist() + else: + ros_msg.twist.covariance = list(self.covariance) + + return ros_msg diff --git a/dimos/msgs/geometry_msgs/Vector3.py b/dimos/msgs/geometry_msgs/Vector3.py new file mode 100644 index 0000000000..05d3340a42 --- /dev/null +++ b/dimos/msgs/geometry_msgs/Vector3.py @@ -0,0 +1,462 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from collections.abc import Sequence +from typing import TypeAlias + +from dimos_lcm.geometry_msgs import Vector3 as LCMVector3 +import numpy as np +from plum import dispatch + +# Types that can be converted to/from Vector +VectorConvertable: TypeAlias = Sequence[int | float] | LCMVector3 | np.ndarray + + +def _ensure_3d(data: np.ndarray) -> np.ndarray: + """Ensure the data array is exactly 3D by padding with zeros or raising an exception if too long.""" + if len(data) == 3: + return data + elif len(data) < 3: + padded = np.zeros(3, dtype=float) + padded[: len(data)] = data + return padded + else: + raise ValueError( + f"Vector3 cannot be initialized with more than 3 components. Got {len(data)} components." + ) + + +class Vector3(LCMVector3): + x: float = 0.0 + y: float = 0.0 + z: float = 0.0 + msg_name = "geometry_msgs.Vector3" + + @dispatch + def __init__(self) -> None: + """Initialize a zero 3D vector.""" + self.x = 0.0 + self.y = 0.0 + self.z = 0.0 + + @dispatch + def __init__(self, x: int | float) -> None: + """Initialize a 3D vector from a single numeric value (x, 0, 0).""" + self.x = float(x) + self.y = 0.0 + self.z = 0.0 + + @dispatch + def __init__(self, x: int | float, y: int | float) -> None: + """Initialize a 3D vector from x, y components (z=0).""" + self.x = float(x) + self.y = float(y) + self.z = 0.0 + + @dispatch + def __init__(self, x: int | float, y: int | float, z: int | float) -> None: + """Initialize a 3D vector from x, y, z components.""" + self.x = float(x) + self.y = float(y) + self.z = float(z) + + @dispatch + def __init__(self, sequence: Sequence[int | float]) -> None: + """Initialize from a sequence (list, tuple) of numbers, ensuring 3D.""" + data = _ensure_3d(np.array(sequence, dtype=float)) + self.x = float(data[0]) + self.y = float(data[1]) + self.z = float(data[2]) + + @dispatch + def __init__(self, array: np.ndarray) -> None: + """Initialize from a numpy array, ensuring 3D.""" + data = _ensure_3d(np.array(array, dtype=float)) + self.x = float(data[0]) + self.y = float(data[1]) + self.z = float(data[2]) + + @dispatch + def __init__(self, vector: Vector3) -> None: + """Initialize from another Vector3 (copy constructor).""" + self.x = vector.x + self.y = vector.y + self.z = vector.z + + @dispatch + def __init__(self, lcm_vector: LCMVector3) -> None: + """Initialize from an LCM Vector3.""" + self.x = float(lcm_vector.x) + self.y = float(lcm_vector.y) + self.z = float(lcm_vector.z) + + @property + def as_tuple(self) -> tuple[float, float, float]: + return (self.x, self.y, self.z) + + @property + def yaw(self) -> float: + return self.z + + @property + def pitch(self) -> float: + return self.y + + @property + def roll(self) -> float: + return self.x + + @property + def data(self) -> np.ndarray: + """Get the underlying numpy array.""" + return np.array([self.x, self.y, self.z], dtype=float) + + def __getitem__(self, idx: int): + if idx == 0: + return self.x + elif idx == 1: + return self.y + elif idx == 2: + return self.z + else: + raise IndexError(f"Vector3 index {idx} out of range [0-2]") + + def __repr__(self) -> str: + return f"Vector({self.data})" + + def __str__(self) -> str: + def getArrow(): + repr = ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"] + + if self.x == 0 and self.y == 0: + return "·" + + # Calculate angle in radians and convert to directional index + angle = np.arctan2(self.y, self.x) + # Map angle to 0-7 index (8 directions) with proper orientation + dir_index = int(((angle + np.pi) * 4 / np.pi) % 8) + # Get directional arrow symbol + return repr[dir_index] + + return f"{getArrow()} Vector {self.__repr__()}" + + def agent_encode(self) -> dict: + """Encode the vector for agent communication.""" + return {"x": self.x, "y": self.y, "z": self.z} + + def serialize(self) -> dict: + """Serialize the vector to a tuple.""" + return {"type": "vector", "c": (self.x, self.y, self.z)} + + def __eq__(self, other) -> bool: + """Check if two vectors are equal using numpy's allclose for floating point comparison.""" + if not isinstance(other, Vector3): + return False + return np.allclose([self.x, self.y, self.z], [other.x, other.y, other.z]) + + def __add__(self, other: VectorConvertable | Vector3) -> Vector3: + other_vector: Vector3 = to_vector(other) + return self.__class__( + self.x + other_vector.x, self.y + other_vector.y, self.z + other_vector.z + ) + + def __sub__(self, other: VectorConvertable | Vector3) -> Vector3: + other_vector = to_vector(other) + return self.__class__( + self.x - other_vector.x, self.y - other_vector.y, self.z - other_vector.z + ) + + def __mul__(self, scalar: float) -> Vector3: + return self.__class__(self.x * scalar, self.y * scalar, self.z * scalar) + + def __rmul__(self, scalar: float) -> Vector3: + return self.__mul__(scalar) + + def __truediv__(self, scalar: float) -> Vector3: + return self.__class__(self.x / scalar, self.y / scalar, self.z / scalar) + + def __neg__(self) -> Vector3: + return self.__class__(-self.x, -self.y, -self.z) + + def dot(self, other: VectorConvertable | Vector3) -> float: + """Compute dot product.""" + other_vector = to_vector(other) + return self.x * other_vector.x + self.y * other_vector.y + self.z * other_vector.z + + def cross(self, other: VectorConvertable | Vector3) -> Vector3: + """Compute cross product (3D vectors only).""" + other_vector = to_vector(other) + return self.__class__( + self.y * other_vector.z - self.z * other_vector.y, + self.z * other_vector.x - self.x * other_vector.z, + self.x * other_vector.y - self.y * other_vector.x, + ) + + def magnitude(self) -> float: + """Alias for length().""" + return self.length() + + def length(self) -> float: + """Compute the Euclidean length (magnitude) of the vector.""" + return float(np.sqrt(self.x * self.x + self.y * self.y + self.z * self.z)) + + def length_squared(self) -> float: + """Compute the squared length of the vector (faster than length()).""" + return float(self.x * self.x + self.y * self.y + self.z * self.z) + + def normalize(self) -> Vector3: + """Return a normalized unit vector in the same direction.""" + length = self.length() + if length < 1e-10: # Avoid division by near-zero + return self.__class__(0.0, 0.0, 0.0) + return self.__class__(self.x / length, self.y / length, self.z / length) + + def to_2d(self) -> Vector3: + """Convert a vector to a 2D vector by taking only the x and y components (z=0).""" + return self.__class__(self.x, self.y, 0.0) + + def distance(self, other: VectorConvertable | Vector3) -> float: + """Compute Euclidean distance to another vector.""" + other_vector = to_vector(other) + dx = self.x - other_vector.x + dy = self.y - other_vector.y + dz = self.z - other_vector.z + return float(np.sqrt(dx * dx + dy * dy + dz * dz)) + + def distance_squared(self, other: VectorConvertable | Vector3) -> float: + """Compute squared Euclidean distance to another vector (faster than distance()).""" + other_vector = to_vector(other) + dx = self.x - other_vector.x + dy = self.y - other_vector.y + dz = self.z - other_vector.z + return float(dx * dx + dy * dy + dz * dz) + + def angle(self, other: VectorConvertable | Vector3) -> float: + """Compute the angle (in radians) between this vector and another.""" + other_vector = to_vector(other) + this_length = self.length() + other_length = other_vector.length() + + if this_length < 1e-10 or other_length < 1e-10: + return 0.0 + + cos_angle = np.clip( + self.dot(other_vector) / (this_length * other_length), + -1.0, + 1.0, + ) + return float(np.arccos(cos_angle)) + + def project(self, onto: VectorConvertable | Vector3) -> Vector3: + """Project this vector onto another vector.""" + onto_vector = to_vector(onto) + onto_length_sq = ( + onto_vector.x * onto_vector.x + + onto_vector.y * onto_vector.y + + onto_vector.z * onto_vector.z + ) + if onto_length_sq < 1e-10: + return self.__class__(0.0, 0.0, 0.0) + + scalar_projection = self.dot(onto_vector) / onto_length_sq + return self.__class__( + scalar_projection * onto_vector.x, + scalar_projection * onto_vector.y, + scalar_projection * onto_vector.z, + ) + + # this is here to test ros_observable_topic + # doesn't happen irl afaik that we want a vector from ros message + @classmethod + def from_msg(cls, msg) -> Vector3: + return cls(*msg) + + @classmethod + def zeros(cls) -> Vector3: + """Create a zero 3D vector.""" + return cls() + + @classmethod + def ones(cls) -> Vector3: + """Create a 3D vector of ones.""" + return cls(1.0, 1.0, 1.0) + + @classmethod + def unit_x(cls) -> Vector3: + """Create a unit vector in the x direction.""" + return cls(1.0, 0.0, 0.0) + + @classmethod + def unit_y(cls) -> Vector3: + """Create a unit vector in the y direction.""" + return cls(0.0, 1.0, 0.0) + + @classmethod + def unit_z(cls) -> Vector3: + """Create a unit vector in the z direction.""" + return cls(0.0, 0.0, 1.0) + + def to_list(self) -> list[float]: + """Convert the vector to a list.""" + return [self.x, self.y, self.z] + + def to_tuple(self) -> tuple[float, float, float]: + """Convert the vector to a tuple.""" + return (self.x, self.y, self.z) + + def to_numpy(self) -> np.ndarray: + """Convert the vector to a numpy array.""" + return np.array([self.x, self.y, self.z], dtype=float) + + def is_zero(self) -> bool: + """Check if this is a zero vector (all components are zero). + + Returns: + True if all components are zero, False otherwise + """ + return np.allclose([self.x, self.y, self.z], 0.0) + + @property + def quaternion(self): + return self.to_quaternion() + + def to_quaternion(self): + """Convert Vector3 representing Euler angles (roll, pitch, yaw) to a Quaternion. + + Assumes this Vector3 contains Euler angles in radians: + - x component: roll (rotation around x-axis) + - y component: pitch (rotation around y-axis) + - z component: yaw (rotation around z-axis) + + Returns: + Quaternion: The equivalent quaternion representation + """ + # Import here to avoid circular imports + from dimos.msgs.geometry_msgs.Quaternion import Quaternion + + # Extract Euler angles + roll = self.x + pitch = self.y + yaw = self.z + + # Convert Euler angles to quaternion using ZYX convention + # Source: https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles + + # Compute half angles + cy = np.cos(yaw * 0.5) + sy = np.sin(yaw * 0.5) + cp = np.cos(pitch * 0.5) + sp = np.sin(pitch * 0.5) + cr = np.cos(roll * 0.5) + sr = np.sin(roll * 0.5) + + # Compute quaternion components + w = cr * cp * cy + sr * sp * sy + x = sr * cp * cy - cr * sp * sy + y = cr * sp * cy + sr * cp * sy + z = cr * cp * sy - sr * sp * cy + + return Quaternion(x, y, z, w) + + def __bool__(self) -> bool: + """Boolean conversion for Vector. + + A Vector is considered False if it's a zero vector (all components are zero), + and True otherwise. + + Returns: + False if vector is zero, True otherwise + """ + return not self.is_zero() + + +@dispatch +def to_numpy(value: Vector3) -> np.ndarray: + """Convert a Vector3 to a numpy array.""" + return value.to_numpy() + + +@dispatch +def to_numpy(value: np.ndarray) -> np.ndarray: + """Pass through numpy arrays.""" + return value + + +@dispatch +def to_numpy(value: Sequence[int | float]) -> np.ndarray: + """Convert a sequence to a numpy array.""" + return np.array(value, dtype=float) + + +@dispatch +def to_vector(value: Vector3) -> Vector3: + """Pass through Vector3 objects.""" + return value + + +@dispatch +def to_vector(value: VectorConvertable | Vector3) -> Vector3: + """Convert a vector-compatible value to a Vector3 object.""" + return Vector3(value) + + +@dispatch +def to_tuple(value: Vector3) -> tuple[float, float, float]: + """Convert a Vector3 to a tuple.""" + return value.to_tuple() + + +@dispatch +def to_tuple(value: np.ndarray) -> tuple[float, ...]: + """Convert a numpy array to a tuple.""" + return tuple(value.tolist()) + + +@dispatch +def to_tuple(value: Sequence[int | float]) -> tuple[float, ...]: + """Convert a sequence to a tuple.""" + if isinstance(value, tuple): + return value + else: + return tuple(value) + + +@dispatch +def to_list(value: Vector3) -> list[float]: + """Convert a Vector3 to a list.""" + return value.to_list() + + +@dispatch +def to_list(value: np.ndarray) -> list[float]: + """Convert a numpy array to a list.""" + return value.tolist() + + +@dispatch +def to_list(value: Sequence[int | float]) -> list[float]: + """Convert a sequence to a list.""" + if isinstance(value, list): + return value + else: + return list(value) + + +VectorLike: TypeAlias = VectorConvertable | Vector3 + + +def make_vector3(x: float, y: float, z: float) -> Vector3: + return Vector3(x, y, z) diff --git a/dimos/msgs/geometry_msgs/__init__.py b/dimos/msgs/geometry_msgs/__init__.py new file mode 100644 index 0000000000..de46a0a079 --- /dev/null +++ b/dimos/msgs/geometry_msgs/__init__.py @@ -0,0 +1,11 @@ +from dimos.msgs.geometry_msgs.Pose import Pose, PoseLike, to_pose +from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped +from dimos.msgs.geometry_msgs.PoseWithCovariance import PoseWithCovariance +from dimos.msgs.geometry_msgs.PoseWithCovarianceStamped import PoseWithCovarianceStamped +from dimos.msgs.geometry_msgs.Quaternion import Quaternion +from dimos.msgs.geometry_msgs.Transform import Transform +from dimos.msgs.geometry_msgs.Twist import Twist +from dimos.msgs.geometry_msgs.TwistStamped import TwistStamped +from dimos.msgs.geometry_msgs.TwistWithCovariance import TwistWithCovariance +from dimos.msgs.geometry_msgs.TwistWithCovarianceStamped import TwistWithCovarianceStamped +from dimos.msgs.geometry_msgs.Vector3 import Vector3, VectorLike diff --git a/dimos/msgs/geometry_msgs/test_Pose.py b/dimos/msgs/geometry_msgs/test_Pose.py new file mode 100644 index 0000000000..e5c373e166 --- /dev/null +++ b/dimos/msgs/geometry_msgs/test_Pose.py @@ -0,0 +1,808 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pickle + +from dimos_lcm.geometry_msgs import Pose as LCMPose +import numpy as np +import pytest + +try: + from geometry_msgs.msg import Point as ROSPoint, Pose as ROSPose, Quaternion as ROSQuaternion +except ImportError: + ROSPose = None + ROSPoint = None + ROSQuaternion = None + +from dimos.msgs.geometry_msgs.Pose import Pose, to_pose +from dimos.msgs.geometry_msgs.Quaternion import Quaternion +from dimos.msgs.geometry_msgs.Vector3 import Vector3 + + +def test_pose_default_init() -> None: + """Test that default initialization creates a pose at origin with identity orientation.""" + pose = Pose() + + # Position should be at origin + assert pose.position.x == 0.0 + assert pose.position.y == 0.0 + assert pose.position.z == 0.0 + + # Orientation should be identity quaternion + assert pose.orientation.x == 0.0 + assert pose.orientation.y == 0.0 + assert pose.orientation.z == 0.0 + assert pose.orientation.w == 1.0 + + # Test convenience properties + assert pose.x == 0.0 + assert pose.y == 0.0 + assert pose.z == 0.0 + + +def test_pose_pose_init() -> None: + """Test initialization with position coordinates only (identity orientation).""" + pose_data = Pose(1.0, 2.0, 3.0) + + pose = to_pose(pose_data) + + # Position should be as specified + assert pose.position.x == 1.0 + assert pose.position.y == 2.0 + assert pose.position.z == 3.0 + + # Orientation should be identity quaternion + assert pose.orientation.x == 0.0 + assert pose.orientation.y == 0.0 + assert pose.orientation.z == 0.0 + assert pose.orientation.w == 1.0 + + # Test convenience properties + assert pose.x == 1.0 + assert pose.y == 2.0 + assert pose.z == 3.0 + + +def test_pose_position_init() -> None: + """Test initialization with position coordinates only (identity orientation).""" + pose = Pose(1.0, 2.0, 3.0) + + # Position should be as specified + assert pose.position.x == 1.0 + assert pose.position.y == 2.0 + assert pose.position.z == 3.0 + + # Orientation should be identity quaternion + assert pose.orientation.x == 0.0 + assert pose.orientation.y == 0.0 + assert pose.orientation.z == 0.0 + assert pose.orientation.w == 1.0 + + # Test convenience properties + assert pose.x == 1.0 + assert pose.y == 2.0 + assert pose.z == 3.0 + + +def test_pose_full_init() -> None: + """Test initialization with position and orientation coordinates.""" + pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) + + # Position should be as specified + assert pose.position.x == 1.0 + assert pose.position.y == 2.0 + assert pose.position.z == 3.0 + + # Orientation should be as specified + assert pose.orientation.x == 0.1 + assert pose.orientation.y == 0.2 + assert pose.orientation.z == 0.3 + assert pose.orientation.w == 0.9 + + # Test convenience properties + assert pose.x == 1.0 + assert pose.y == 2.0 + assert pose.z == 3.0 + + +def test_pose_vector_position_init() -> None: + """Test initialization with Vector3 position (identity orientation).""" + position = Vector3(4.0, 5.0, 6.0) + pose = Pose(position) + + # Position should match the vector + assert pose.position.x == 4.0 + assert pose.position.y == 5.0 + assert pose.position.z == 6.0 + + # Orientation should be identity + assert pose.orientation.x == 0.0 + assert pose.orientation.y == 0.0 + assert pose.orientation.z == 0.0 + assert pose.orientation.w == 1.0 + + +def test_pose_vector_quaternion_init() -> None: + """Test initialization with Vector3 position and Quaternion orientation.""" + position = Vector3(1.0, 2.0, 3.0) + orientation = Quaternion(0.1, 0.2, 0.3, 0.9) + pose = Pose(position, orientation) + + # Position should match the vector + assert pose.position.x == 1.0 + assert pose.position.y == 2.0 + assert pose.position.z == 3.0 + + # Orientation should match the quaternion + assert pose.orientation.x == 0.1 + assert pose.orientation.y == 0.2 + assert pose.orientation.z == 0.3 + assert pose.orientation.w == 0.9 + + +def test_pose_list_init() -> None: + """Test initialization with lists for position and orientation.""" + position_list = [1.0, 2.0, 3.0] + orientation_list = [0.1, 0.2, 0.3, 0.9] + pose = Pose(position_list, orientation_list) + + # Position should match the list + assert pose.position.x == 1.0 + assert pose.position.y == 2.0 + assert pose.position.z == 3.0 + + # Orientation should match the list + assert pose.orientation.x == 0.1 + assert pose.orientation.y == 0.2 + assert pose.orientation.z == 0.3 + assert pose.orientation.w == 0.9 + + +def test_pose_tuple_init() -> None: + """Test initialization from a tuple of (position, orientation).""" + position = [1.0, 2.0, 3.0] + orientation = [0.1, 0.2, 0.3, 0.9] + pose_tuple = (position, orientation) + pose = Pose(pose_tuple) + + # Position should match + assert pose.position.x == 1.0 + assert pose.position.y == 2.0 + assert pose.position.z == 3.0 + + # Orientation should match + assert pose.orientation.x == 0.1 + assert pose.orientation.y == 0.2 + assert pose.orientation.z == 0.3 + assert pose.orientation.w == 0.9 + + +def test_pose_dict_init() -> None: + """Test initialization from a dictionary with 'position' and 'orientation' keys.""" + pose_dict = {"position": [1.0, 2.0, 3.0], "orientation": [0.1, 0.2, 0.3, 0.9]} + pose = Pose(pose_dict) + + # Position should match + assert pose.position.x == 1.0 + assert pose.position.y == 2.0 + assert pose.position.z == 3.0 + + # Orientation should match + assert pose.orientation.x == 0.1 + assert pose.orientation.y == 0.2 + assert pose.orientation.z == 0.3 + assert pose.orientation.w == 0.9 + + +def test_pose_copy_init() -> None: + """Test initialization from another Pose (copy constructor).""" + original = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) + copy = Pose(original) + + # Position should match + assert copy.position.x == 1.0 + assert copy.position.y == 2.0 + assert copy.position.z == 3.0 + + # Orientation should match + assert copy.orientation.x == 0.1 + assert copy.orientation.y == 0.2 + assert copy.orientation.z == 0.3 + assert copy.orientation.w == 0.9 + + # Should be a copy, not the same object + assert copy is not original + assert copy == original + + +def test_pose_lcm_init() -> None: + """Test initialization from an LCM Pose.""" + # Create LCM pose + lcm_pose = LCMPose() + lcm_pose.position.x = 1.0 + lcm_pose.position.y = 2.0 + lcm_pose.position.z = 3.0 + lcm_pose.orientation.x = 0.1 + lcm_pose.orientation.y = 0.2 + lcm_pose.orientation.z = 0.3 + lcm_pose.orientation.w = 0.9 + + pose = Pose(lcm_pose) + + # Position should match + assert pose.position.x == 1.0 + assert pose.position.y == 2.0 + assert pose.position.z == 3.0 + + # Orientation should match + assert pose.orientation.x == 0.1 + assert pose.orientation.y == 0.2 + assert pose.orientation.z == 0.3 + assert pose.orientation.w == 0.9 + + +def test_pose_properties() -> None: + """Test pose property access.""" + pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) + + # Test position properties + assert pose.x == 1.0 + assert pose.y == 2.0 + assert pose.z == 3.0 + + # Test orientation properties (through quaternion's to_euler method) + euler = pose.orientation.to_euler() + assert pose.roll == euler.x + assert pose.pitch == euler.y + assert pose.yaw == euler.z + + +def test_pose_euler_properties_identity() -> None: + """Test pose Euler angle properties with identity orientation.""" + pose = Pose(1.0, 2.0, 3.0) # Identity orientation + + # Identity quaternion should give zero Euler angles + assert np.isclose(pose.roll, 0.0, atol=1e-10) + assert np.isclose(pose.pitch, 0.0, atol=1e-10) + assert np.isclose(pose.yaw, 0.0, atol=1e-10) + + # Euler property should also be zeros + assert np.isclose(pose.orientation.euler.x, 0.0, atol=1e-10) + assert np.isclose(pose.orientation.euler.y, 0.0, atol=1e-10) + assert np.isclose(pose.orientation.euler.z, 0.0, atol=1e-10) + + +def test_pose_repr() -> None: + """Test pose string representation.""" + pose = Pose(1.234, 2.567, 3.891, 0.1, 0.2, 0.3, 0.9) + + repr_str = repr(pose) + + # Should contain position and orientation info + assert "Pose" in repr_str + assert "position" in repr_str + assert "orientation" in repr_str + + # Should contain the actual values (approximately) + assert "1.234" in repr_str or "1.23" in repr_str + assert "2.567" in repr_str or "2.57" in repr_str + + +def test_pose_str() -> None: + """Test pose string formatting.""" + pose = Pose(1.234, 2.567, 3.891, 0.1, 0.2, 0.3, 0.9) + + str_repr = str(pose) + + # Should contain position coordinates + assert "1.234" in str_repr + assert "2.567" in str_repr + assert "3.891" in str_repr + + # Should contain Euler angles + assert "euler" in str_repr + + # Should be formatted with specified precision + assert str_repr.count("Pose") == 1 + + +def test_pose_equality() -> None: + """Test pose equality comparison.""" + pose1 = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) + pose2 = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) + pose3 = Pose(1.1, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) # Different position + pose4 = Pose(1.0, 2.0, 3.0, 0.11, 0.2, 0.3, 0.9) # Different orientation + + # Equal poses + assert pose1 == pose2 + assert pose2 == pose1 + + # Different poses + assert pose1 != pose3 + assert pose1 != pose4 + assert pose3 != pose4 + + # Different types + assert pose1 != "not a pose" + assert pose1 != [1.0, 2.0, 3.0] + assert pose1 is not None + + +def test_pose_with_numpy_arrays() -> None: + """Test pose initialization with numpy arrays.""" + position_array = np.array([1.0, 2.0, 3.0]) + orientation_array = np.array([0.1, 0.2, 0.3, 0.9]) + + pose = Pose(position_array, orientation_array) + + # Position should match + assert pose.position.x == 1.0 + assert pose.position.y == 2.0 + assert pose.position.z == 3.0 + + # Orientation should match + assert pose.orientation.x == 0.1 + assert pose.orientation.y == 0.2 + assert pose.orientation.z == 0.3 + assert pose.orientation.w == 0.9 + + +def test_pose_with_mixed_types() -> None: + """Test pose initialization with mixed input types.""" + # Position as tuple, orientation as list + pose1 = Pose((1.0, 2.0, 3.0), [0.1, 0.2, 0.3, 0.9]) + + # Position as numpy array, orientation as Vector3/Quaternion + position = np.array([1.0, 2.0, 3.0]) + orientation = Quaternion(0.1, 0.2, 0.3, 0.9) + pose2 = Pose(position, orientation) + + # Both should result in the same pose + assert pose1.position.x == pose2.position.x + assert pose1.position.y == pose2.position.y + assert pose1.position.z == pose2.position.z + assert pose1.orientation.x == pose2.orientation.x + assert pose1.orientation.y == pose2.orientation.y + assert pose1.orientation.z == pose2.orientation.z + assert pose1.orientation.w == pose2.orientation.w + + +def test_to_pose_passthrough() -> None: + """Test to_pose function with Pose input (passthrough).""" + original = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) + result = to_pose(original) + + # Should be the same object (passthrough) + assert result is original + + +def test_to_pose_conversion() -> None: + """Test to_pose function with convertible inputs.""" + # Note: The to_pose conversion function has type checking issues in the current implementation + # Test direct construction instead to verify the intended functionality + + # Test the intended functionality by creating poses directly + pose_tuple = ([1.0, 2.0, 3.0], [0.1, 0.2, 0.3, 0.9]) + result1 = Pose(pose_tuple) + + assert isinstance(result1, Pose) + assert result1.position.x == 1.0 + assert result1.position.y == 2.0 + assert result1.position.z == 3.0 + assert result1.orientation.x == 0.1 + assert result1.orientation.y == 0.2 + assert result1.orientation.z == 0.3 + assert result1.orientation.w == 0.9 + + # Test with dictionary + pose_dict = {"position": [1.0, 2.0, 3.0], "orientation": [0.1, 0.2, 0.3, 0.9]} + result2 = Pose(pose_dict) + + assert isinstance(result2, Pose) + assert result2.position.x == 1.0 + assert result2.position.y == 2.0 + assert result2.position.z == 3.0 + assert result2.orientation.x == 0.1 + assert result2.orientation.y == 0.2 + assert result2.orientation.z == 0.3 + assert result2.orientation.w == 0.9 + + +def test_pose_euler_roundtrip() -> None: + """Test conversion from Euler angles to quaternion and back.""" + # Start with known Euler angles (small angles to avoid gimbal lock) + roll = 0.1 + pitch = 0.2 + yaw = 0.3 + + # Create quaternion from Euler angles + euler_vector = Vector3(roll, pitch, yaw) + quaternion = euler_vector.to_quaternion() + + # Create pose with this quaternion + pose = Pose(Vector3(0, 0, 0), quaternion) + + # Convert back to Euler angles + result_euler = pose.orientation.euler + + # Should get back the original Euler angles (within tolerance) + assert np.isclose(result_euler.x, roll, atol=1e-6) + assert np.isclose(result_euler.y, pitch, atol=1e-6) + assert np.isclose(result_euler.z, yaw, atol=1e-6) + + +def test_pose_zero_position() -> None: + """Test pose with zero position vector.""" + # Use manual construction since Vector3.zeros has signature issues + pose = Pose(0.0, 0.0, 0.0) # Position at origin with identity orientation + + assert pose.x == 0.0 + assert pose.y == 0.0 + assert pose.z == 0.0 + assert np.isclose(pose.roll, 0.0, atol=1e-10) + assert np.isclose(pose.pitch, 0.0, atol=1e-10) + assert np.isclose(pose.yaw, 0.0, atol=1e-10) + + +def test_pose_unit_vectors() -> None: + """Test pose with unit vector positions.""" + # Test unit x vector position + pose_x = Pose(Vector3.unit_x()) + assert pose_x.x == 1.0 + assert pose_x.y == 0.0 + assert pose_x.z == 0.0 + + # Test unit y vector position + pose_y = Pose(Vector3.unit_y()) + assert pose_y.x == 0.0 + assert pose_y.y == 1.0 + assert pose_y.z == 0.0 + + # Test unit z vector position + pose_z = Pose(Vector3.unit_z()) + assert pose_z.x == 0.0 + assert pose_z.y == 0.0 + assert pose_z.z == 1.0 + + +def test_pose_negative_coordinates() -> None: + """Test pose with negative coordinates.""" + pose = Pose(-1.0, -2.0, -3.0, -0.1, -0.2, -0.3, 0.9) + + # Position should be negative + assert pose.x == -1.0 + assert pose.y == -2.0 + assert pose.z == -3.0 + + # Orientation should be as specified + assert pose.orientation.x == -0.1 + assert pose.orientation.y == -0.2 + assert pose.orientation.z == -0.3 + assert pose.orientation.w == 0.9 + + +def test_pose_large_coordinates() -> None: + """Test pose with large coordinate values.""" + large_value = 1000.0 + pose = Pose(large_value, large_value, large_value) + + assert pose.x == large_value + assert pose.y == large_value + assert pose.z == large_value + + # Orientation should still be identity + assert pose.orientation.x == 0.0 + assert pose.orientation.y == 0.0 + assert pose.orientation.z == 0.0 + assert pose.orientation.w == 1.0 + + +@pytest.mark.parametrize( + "x,y,z", + [(0.0, 0.0, 0.0), (1.0, 2.0, 3.0), (-1.0, -2.0, -3.0), (0.5, -0.5, 1.5), (100.0, -100.0, 0.0)], +) +def test_pose_parametrized_positions(x, y, z) -> None: + """Parametrized test for various position values.""" + pose = Pose(x, y, z) + + assert pose.x == x + assert pose.y == y + assert pose.z == z + + # Should have identity orientation + assert pose.orientation.x == 0.0 + assert pose.orientation.y == 0.0 + assert pose.orientation.z == 0.0 + assert pose.orientation.w == 1.0 + + +@pytest.mark.parametrize( + "qx,qy,qz,qw", + [ + (0.0, 0.0, 0.0, 1.0), # Identity + (1.0, 0.0, 0.0, 0.0), # 180° around x + (0.0, 1.0, 0.0, 0.0), # 180° around y + (0.0, 0.0, 1.0, 0.0), # 180° around z + (0.5, 0.5, 0.5, 0.5), # Equal components + ], +) +def test_pose_parametrized_orientations(qx, qy, qz, qw) -> None: + """Parametrized test for various orientation values.""" + pose = Pose(0.0, 0.0, 0.0, qx, qy, qz, qw) + + # Position should be at origin + assert pose.x == 0.0 + assert pose.y == 0.0 + assert pose.z == 0.0 + + # Orientation should match + assert pose.orientation.x == qx + assert pose.orientation.y == qy + assert pose.orientation.z == qz + assert pose.orientation.w == qw + + +def test_lcm_encode_decode() -> None: + """Test encoding and decoding of Pose to/from binary LCM format.""" + + def encodepass() -> None: + pose_source = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) + binary_msg = pose_source.lcm_encode() + pose_dest = Pose.lcm_decode(binary_msg) + assert isinstance(pose_dest, Pose) + assert pose_dest is not pose_source + assert pose_dest == pose_source + # Verify we get our custom types back + assert isinstance(pose_dest.position, Vector3) + assert isinstance(pose_dest.orientation, Quaternion) + + import timeit + + print(f"{timeit.timeit(encodepass, number=1000)} ms per cycle") + + +def test_pickle_encode_decode() -> None: + """Test encoding and decoding of Pose to/from binary LCM format.""" + + def encodepass() -> None: + pose_source = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) + binary_msg = pickle.dumps(pose_source) + pose_dest = pickle.loads(binary_msg) + assert isinstance(pose_dest, Pose) + assert pose_dest is not pose_source + assert pose_dest == pose_source + + import timeit + + print(f"{timeit.timeit(encodepass, number=1000)} ms per cycle") + + +def test_pose_addition_translation_only() -> None: + """Test pose addition with translation only (identity rotations).""" + # Two poses with only translations + pose1 = Pose(1.0, 2.0, 3.0) # First translation + pose2 = Pose(4.0, 5.0, 6.0) # Second translation + + # Adding should combine translations + result = pose1 + pose2 + + assert result.position.x == 5.0 # 1 + 4 + assert result.position.y == 7.0 # 2 + 5 + assert result.position.z == 9.0 # 3 + 6 + + # Orientation should remain identity + assert result.orientation.x == 0.0 + assert result.orientation.y == 0.0 + assert result.orientation.z == 0.0 + assert result.orientation.w == 1.0 + + +def test_pose_addition_with_rotation() -> None: + """Test pose addition with rotation applied to translation.""" + # First pose: at origin, rotated 90 degrees around Z (yaw) + # 90 degree rotation quaternion around Z: (0, 0, sin(pi/4), cos(pi/4)) + angle = np.pi / 2 # 90 degrees + pose1 = Pose(0.0, 0.0, 0.0, 0.0, 0.0, np.sin(angle / 2), np.cos(angle / 2)) + + # Second pose: 1 unit forward (along X in its frame) + pose2 = Pose(1.0, 0.0, 0.0) + + # After rotation, the forward direction should be along Y + result = pose1 + pose2 + + # Position should be rotated + assert np.isclose(result.position.x, 0.0, atol=1e-10) + assert np.isclose(result.position.y, 1.0, atol=1e-10) + assert np.isclose(result.position.z, 0.0, atol=1e-10) + + # Orientation should be same as pose1 (pose2 has identity rotation) + assert np.isclose(result.orientation.x, 0.0, atol=1e-10) + assert np.isclose(result.orientation.y, 0.0, atol=1e-10) + assert np.isclose(result.orientation.z, np.sin(angle / 2), atol=1e-10) + assert np.isclose(result.orientation.w, np.cos(angle / 2), atol=1e-10) + + +def test_pose_addition_rotation_composition() -> None: + """Test that rotations are properly composed.""" + # First pose: 45 degrees around Z + angle1 = np.pi / 4 # 45 degrees + pose1 = Pose(0.0, 0.0, 0.0, 0.0, 0.0, np.sin(angle1 / 2), np.cos(angle1 / 2)) + + # Second pose: another 45 degrees around Z + angle2 = np.pi / 4 # 45 degrees + pose2 = Pose(0.0, 0.0, 0.0, 0.0, 0.0, np.sin(angle2 / 2), np.cos(angle2 / 2)) + + # Result should be 90 degrees around Z + result = pose1 + pose2 + + # Check final angle is 90 degrees + expected_angle = angle1 + angle2 # 90 degrees + expected_qz = np.sin(expected_angle / 2) + expected_qw = np.cos(expected_angle / 2) + + assert np.isclose(result.orientation.z, expected_qz, atol=1e-10) + assert np.isclose(result.orientation.w, expected_qw, atol=1e-10) + + +def test_pose_addition_full_transform() -> None: + """Test full pose composition with translation and rotation.""" + # Robot pose: at (2, 1, 0), facing 90 degrees left (positive yaw) + robot_yaw = np.pi / 2 # 90 degrees + robot_pose = Pose(2.0, 1.0, 0.0, 0.0, 0.0, np.sin(robot_yaw / 2), np.cos(robot_yaw / 2)) + + # Object in robot frame: 3 units forward, 1 unit right + object_in_robot = Pose(3.0, -1.0, 0.0) + + # Compose to get object in world frame + object_in_world = robot_pose + object_in_robot + + # Robot is facing left (90 degrees), so: + # - Robot's forward (X) is world's negative Y + # - Robot's right (negative Y) is world's X + # So object should be at: robot_pos + rotated_offset + # rotated_offset: (3, -1) rotated 90° CCW = (1, 3) + assert np.isclose(object_in_world.position.x, 3.0, atol=1e-10) # 2 + 1 + assert np.isclose(object_in_world.position.y, 4.0, atol=1e-10) # 1 + 3 + assert np.isclose(object_in_world.position.z, 0.0, atol=1e-10) + + # Orientation should match robot's orientation (object has no rotation) + assert np.isclose(object_in_world.yaw, robot_yaw, atol=1e-10) + + +def test_pose_addition_chain() -> None: + """Test chaining multiple pose additions.""" + # Create a chain of transformations + pose1 = Pose(1.0, 0.0, 0.0) # Move 1 unit in X + pose2 = Pose(0.0, 1.0, 0.0) # Move 1 unit in Y (relative to pose1) + pose3 = Pose(0.0, 0.0, 1.0) # Move 1 unit in Z (relative to pose1+pose2) + + # Chain them together + result = pose1 + pose2 + pose3 + + # Should accumulate all translations + assert result.position.x == 1.0 + assert result.position.y == 1.0 + assert result.position.z == 1.0 + + +def test_pose_addition_with_convertible() -> None: + """Test pose addition with convertible types.""" + pose1 = Pose(1.0, 2.0, 3.0) + + # Add with tuple + pose_tuple = ([4.0, 5.0, 6.0], [0.0, 0.0, 0.0, 1.0]) + result1 = pose1 + pose_tuple + assert result1.position.x == 5.0 + assert result1.position.y == 7.0 + assert result1.position.z == 9.0 + + # Add with dict + pose_dict = {"position": [1.0, 0.0, 0.0], "orientation": [0.0, 0.0, 0.0, 1.0]} + result2 = pose1 + pose_dict + assert result2.position.x == 2.0 + assert result2.position.y == 2.0 + assert result2.position.z == 3.0 + + +def test_pose_identity_addition() -> None: + """Test that adding identity pose leaves pose unchanged.""" + pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) + identity = Pose() # Identity pose at origin + + result = pose + identity + + # Should be unchanged + assert result.position.x == pose.position.x + assert result.position.y == pose.position.y + assert result.position.z == pose.position.z + assert result.orientation.x == pose.orientation.x + assert result.orientation.y == pose.orientation.y + assert result.orientation.z == pose.orientation.z + assert result.orientation.w == pose.orientation.w + + +def test_pose_addition_3d_rotation() -> None: + """Test pose addition with 3D rotations.""" + # First pose: rotated around X axis (roll) + roll = np.pi / 4 # 45 degrees + pose1 = Pose(1.0, 0.0, 0.0, np.sin(roll / 2), 0.0, 0.0, np.cos(roll / 2)) + + # Second pose: movement along Y and Z in local frame + pose2 = Pose(0.0, 1.0, 1.0) + + # Compose transformations + result = pose1 + pose2 + + # The Y and Z movement should be rotated around X + # After 45° rotation around X: + # - Local Y -> world Y * cos(45°) - Z * sin(45°) + # - Local Z -> world Y * sin(45°) + Z * cos(45°) + cos45 = np.cos(roll) + sin45 = np.sin(roll) + + assert np.isclose(result.position.x, 1.0, atol=1e-10) # X unchanged + assert np.isclose(result.position.y, cos45 - sin45, atol=1e-10) + assert np.isclose(result.position.z, sin45 + cos45, atol=1e-10) + + +@pytest.mark.ros +def test_pose_from_ros_msg() -> None: + """Test creating a Pose from a ROS Pose message.""" + ros_msg = ROSPose() + ros_msg.position = ROSPoint(x=1.0, y=2.0, z=3.0) + ros_msg.orientation = ROSQuaternion(x=0.1, y=0.2, z=0.3, w=0.9) + + pose = Pose.from_ros_msg(ros_msg) + + assert pose.position.x == 1.0 + assert pose.position.y == 2.0 + assert pose.position.z == 3.0 + assert pose.orientation.x == 0.1 + assert pose.orientation.y == 0.2 + assert pose.orientation.z == 0.3 + assert pose.orientation.w == 0.9 + + +@pytest.mark.ros +def test_pose_to_ros_msg() -> None: + """Test converting a Pose to a ROS Pose message.""" + pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) + + ros_msg = pose.to_ros_msg() + + assert isinstance(ros_msg, ROSPose) + assert ros_msg.position.x == 1.0 + assert ros_msg.position.y == 2.0 + assert ros_msg.position.z == 3.0 + assert ros_msg.orientation.x == 0.1 + assert ros_msg.orientation.y == 0.2 + assert ros_msg.orientation.z == 0.3 + assert ros_msg.orientation.w == 0.9 + + +@pytest.mark.ros +def test_pose_ros_roundtrip() -> None: + """Test round-trip conversion between Pose and ROS Pose.""" + original = Pose(1.5, 2.5, 3.5, 0.15, 0.25, 0.35, 0.85) + + ros_msg = original.to_ros_msg() + restored = Pose.from_ros_msg(ros_msg) + + assert restored.position.x == original.position.x + assert restored.position.y == original.position.y + assert restored.position.z == original.position.z + assert restored.orientation.x == original.orientation.x + assert restored.orientation.y == original.orientation.y + assert restored.orientation.z == original.orientation.z + assert restored.orientation.w == original.orientation.w diff --git a/dimos/msgs/geometry_msgs/test_PoseStamped.py b/dimos/msgs/geometry_msgs/test_PoseStamped.py new file mode 100644 index 0000000000..6224b6548a --- /dev/null +++ b/dimos/msgs/geometry_msgs/test_PoseStamped.py @@ -0,0 +1,139 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pickle +import time + +import pytest + +try: + from geometry_msgs.msg import PoseStamped as ROSPoseStamped +except ImportError: + ROSPoseStamped = None + +from dimos.msgs.geometry_msgs import PoseStamped + + +def test_lcm_encode_decode() -> None: + """Test encoding and decoding of Pose to/from binary LCM format.""" + + pose_source = PoseStamped( + ts=time.time(), + position=(1.0, 2.0, 3.0), + orientation=(0.1, 0.2, 0.3, 0.9), + ) + binary_msg = pose_source.lcm_encode() + pose_dest = PoseStamped.lcm_decode(binary_msg) + + assert isinstance(pose_dest, PoseStamped) + assert pose_dest is not pose_source + + print(pose_source.position) + print(pose_source.orientation) + + print(pose_dest.position) + print(pose_dest.orientation) + assert pose_dest == pose_source + + +def test_pickle_encode_decode() -> None: + """Test encoding and decoding of PoseStamped to/from binary LCM format.""" + + pose_source = PoseStamped( + ts=time.time(), + position=(1.0, 2.0, 3.0), + orientation=(0.1, 0.2, 0.3, 0.9), + ) + binary_msg = pickle.dumps(pose_source) + pose_dest = pickle.loads(binary_msg) + assert isinstance(pose_dest, PoseStamped) + assert pose_dest is not pose_source + assert pose_dest == pose_source + + +@pytest.mark.ros +def test_pose_stamped_from_ros_msg() -> None: + """Test creating a PoseStamped from a ROS PoseStamped message.""" + ros_msg = ROSPoseStamped() + ros_msg.header.frame_id = "world" + ros_msg.header.stamp.sec = 123 + ros_msg.header.stamp.nanosec = 456000000 + ros_msg.pose.position.x = 1.0 + ros_msg.pose.position.y = 2.0 + ros_msg.pose.position.z = 3.0 + ros_msg.pose.orientation.x = 0.1 + ros_msg.pose.orientation.y = 0.2 + ros_msg.pose.orientation.z = 0.3 + ros_msg.pose.orientation.w = 0.9 + + pose_stamped = PoseStamped.from_ros_msg(ros_msg) + + assert pose_stamped.frame_id == "world" + assert pose_stamped.ts == 123.456 + assert pose_stamped.position.x == 1.0 + assert pose_stamped.position.y == 2.0 + assert pose_stamped.position.z == 3.0 + assert pose_stamped.orientation.x == 0.1 + assert pose_stamped.orientation.y == 0.2 + assert pose_stamped.orientation.z == 0.3 + assert pose_stamped.orientation.w == 0.9 + + +@pytest.mark.ros +def test_pose_stamped_to_ros_msg() -> None: + """Test converting a PoseStamped to a ROS PoseStamped message.""" + pose_stamped = PoseStamped( + ts=123.456, + frame_id="base_link", + position=(1.0, 2.0, 3.0), + orientation=(0.1, 0.2, 0.3, 0.9), + ) + + ros_msg = pose_stamped.to_ros_msg() + + assert isinstance(ros_msg, ROSPoseStamped) + assert ros_msg.header.frame_id == "base_link" + assert ros_msg.header.stamp.sec == 123 + assert ros_msg.header.stamp.nanosec == 456000000 + assert ros_msg.pose.position.x == 1.0 + assert ros_msg.pose.position.y == 2.0 + assert ros_msg.pose.position.z == 3.0 + assert ros_msg.pose.orientation.x == 0.1 + assert ros_msg.pose.orientation.y == 0.2 + assert ros_msg.pose.orientation.z == 0.3 + assert ros_msg.pose.orientation.w == 0.9 + + +@pytest.mark.ros +def test_pose_stamped_ros_roundtrip() -> None: + """Test round-trip conversion between PoseStamped and ROS PoseStamped.""" + original = PoseStamped( + ts=123.789, + frame_id="odom", + position=(1.5, 2.5, 3.5), + orientation=(0.15, 0.25, 0.35, 0.85), + ) + + ros_msg = original.to_ros_msg() + restored = PoseStamped.from_ros_msg(ros_msg) + + assert restored.frame_id == original.frame_id + assert restored.ts == original.ts + assert restored.position.x == original.position.x + assert restored.position.y == original.position.y + assert restored.position.z == original.position.z + assert restored.orientation.x == original.orientation.x + assert restored.orientation.y == original.orientation.y + assert restored.orientation.z == original.orientation.z + assert restored.orientation.w == original.orientation.w diff --git a/dimos/msgs/geometry_msgs/test_PoseWithCovariance.py b/dimos/msgs/geometry_msgs/test_PoseWithCovariance.py new file mode 100644 index 0000000000..ea455ba488 --- /dev/null +++ b/dimos/msgs/geometry_msgs/test_PoseWithCovariance.py @@ -0,0 +1,388 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos_lcm.geometry_msgs import PoseWithCovariance as LCMPoseWithCovariance +import numpy as np +import pytest + +try: + from geometry_msgs.msg import ( + Point as ROSPoint, + Pose as ROSPose, + PoseWithCovariance as ROSPoseWithCovariance, + Quaternion as ROSQuaternion, + ) +except ImportError: + ROSPoseWithCovariance = None + ROSPose = None + ROSPoint = None + ROSQuaternion = None + +from dimos.msgs.geometry_msgs.Pose import Pose +from dimos.msgs.geometry_msgs.PoseWithCovariance import PoseWithCovariance + + +def test_pose_with_covariance_default_init() -> None: + """Test that default initialization creates a pose at origin with zero covariance.""" + pose_cov = PoseWithCovariance() + + # Pose should be at origin with identity orientation + assert pose_cov.pose.position.x == 0.0 + assert pose_cov.pose.position.y == 0.0 + assert pose_cov.pose.position.z == 0.0 + assert pose_cov.pose.orientation.x == 0.0 + assert pose_cov.pose.orientation.y == 0.0 + assert pose_cov.pose.orientation.z == 0.0 + assert pose_cov.pose.orientation.w == 1.0 + + # Covariance should be all zeros + assert np.all(pose_cov.covariance == 0.0) + assert pose_cov.covariance.shape == (36,) + + +def test_pose_with_covariance_pose_init() -> None: + """Test initialization with a Pose object.""" + pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) + pose_cov = PoseWithCovariance(pose) + + # Pose should match + assert pose_cov.pose.position.x == 1.0 + assert pose_cov.pose.position.y == 2.0 + assert pose_cov.pose.position.z == 3.0 + assert pose_cov.pose.orientation.x == 0.1 + assert pose_cov.pose.orientation.y == 0.2 + assert pose_cov.pose.orientation.z == 0.3 + assert pose_cov.pose.orientation.w == 0.9 + + # Covariance should be zeros by default + assert np.all(pose_cov.covariance == 0.0) + + +def test_pose_with_covariance_pose_and_covariance_init() -> None: + """Test initialization with pose and covariance.""" + pose = Pose(1.0, 2.0, 3.0) + covariance = np.arange(36, dtype=float) + pose_cov = PoseWithCovariance(pose, covariance) + + # Pose should match + assert pose_cov.pose.position.x == 1.0 + assert pose_cov.pose.position.y == 2.0 + assert pose_cov.pose.position.z == 3.0 + + # Covariance should match + assert np.array_equal(pose_cov.covariance, covariance) + + +def test_pose_with_covariance_list_covariance() -> None: + """Test initialization with covariance as a list.""" + pose = Pose(1.0, 2.0, 3.0) + covariance_list = list(range(36)) + pose_cov = PoseWithCovariance(pose, covariance_list) + + # Covariance should be converted to numpy array + assert isinstance(pose_cov.covariance, np.ndarray) + assert np.array_equal(pose_cov.covariance, np.array(covariance_list)) + + +def test_pose_with_covariance_copy_init() -> None: + """Test copy constructor.""" + pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) + covariance = np.arange(36, dtype=float) + original = PoseWithCovariance(pose, covariance) + copy = PoseWithCovariance(original) + + # Should be equal but not the same object + assert copy == original + assert copy is not original + assert copy.pose is not original.pose + assert copy.covariance is not original.covariance + + # Modify original to ensure they're independent + original.covariance[0] = 999.0 + assert copy.covariance[0] != 999.0 + + +def test_pose_with_covariance_lcm_init() -> None: + """Test initialization from LCM message.""" + lcm_msg = LCMPoseWithCovariance() + lcm_msg.pose.position.x = 1.0 + lcm_msg.pose.position.y = 2.0 + lcm_msg.pose.position.z = 3.0 + lcm_msg.pose.orientation.x = 0.1 + lcm_msg.pose.orientation.y = 0.2 + lcm_msg.pose.orientation.z = 0.3 + lcm_msg.pose.orientation.w = 0.9 + lcm_msg.covariance = list(range(36)) + + pose_cov = PoseWithCovariance(lcm_msg) + + # Pose should match + assert pose_cov.pose.position.x == 1.0 + assert pose_cov.pose.position.y == 2.0 + assert pose_cov.pose.position.z == 3.0 + assert pose_cov.pose.orientation.x == 0.1 + assert pose_cov.pose.orientation.y == 0.2 + assert pose_cov.pose.orientation.z == 0.3 + assert pose_cov.pose.orientation.w == 0.9 + + # Covariance should match + assert np.array_equal(pose_cov.covariance, np.arange(36)) + + +def test_pose_with_covariance_dict_init() -> None: + """Test initialization from dictionary.""" + pose_dict = {"pose": Pose(1.0, 2.0, 3.0), "covariance": list(range(36))} + pose_cov = PoseWithCovariance(pose_dict) + + assert pose_cov.pose.position.x == 1.0 + assert pose_cov.pose.position.y == 2.0 + assert pose_cov.pose.position.z == 3.0 + assert np.array_equal(pose_cov.covariance, np.arange(36)) + + +def test_pose_with_covariance_dict_init_no_covariance() -> None: + """Test initialization from dictionary without covariance.""" + pose_dict = {"pose": Pose(1.0, 2.0, 3.0)} + pose_cov = PoseWithCovariance(pose_dict) + + assert pose_cov.pose.position.x == 1.0 + assert np.all(pose_cov.covariance == 0.0) + + +def test_pose_with_covariance_tuple_init() -> None: + """Test initialization from tuple.""" + pose = Pose(1.0, 2.0, 3.0) + covariance = np.arange(36, dtype=float) + pose_tuple = (pose, covariance) + pose_cov = PoseWithCovariance(pose_tuple) + + assert pose_cov.pose.position.x == 1.0 + assert pose_cov.pose.position.y == 2.0 + assert pose_cov.pose.position.z == 3.0 + assert np.array_equal(pose_cov.covariance, covariance) + + +def test_pose_with_covariance_properties() -> None: + """Test convenience properties.""" + pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) + pose_cov = PoseWithCovariance(pose) + + # Position properties + assert pose_cov.x == 1.0 + assert pose_cov.y == 2.0 + assert pose_cov.z == 3.0 + assert pose_cov.position.x == 1.0 + assert pose_cov.position.y == 2.0 + assert pose_cov.position.z == 3.0 + + # Orientation properties + assert pose_cov.orientation.x == 0.1 + assert pose_cov.orientation.y == 0.2 + assert pose_cov.orientation.z == 0.3 + assert pose_cov.orientation.w == 0.9 + + # Euler angle properties + assert pose_cov.roll == pose.roll + assert pose_cov.pitch == pose.pitch + assert pose_cov.yaw == pose.yaw + + +def test_pose_with_covariance_matrix_property() -> None: + """Test covariance matrix property.""" + pose = Pose() + covariance_array = np.arange(36, dtype=float) + pose_cov = PoseWithCovariance(pose, covariance_array) + + # Get as matrix + cov_matrix = pose_cov.covariance_matrix + assert cov_matrix.shape == (6, 6) + assert cov_matrix[0, 0] == 0.0 + assert cov_matrix[5, 5] == 35.0 + + # Set from matrix + new_matrix = np.eye(6) * 2.0 + pose_cov.covariance_matrix = new_matrix + assert np.array_equal(pose_cov.covariance[:6], [2.0, 0.0, 0.0, 0.0, 0.0, 0.0]) + + +def test_pose_with_covariance_repr() -> None: + """Test string representation.""" + pose = Pose(1.234, 2.567, 3.891) + pose_cov = PoseWithCovariance(pose) + + repr_str = repr(pose_cov) + assert "PoseWithCovariance" in repr_str + assert "pose=" in repr_str + assert "covariance=" in repr_str + assert "36 elements" in repr_str + + +def test_pose_with_covariance_str() -> None: + """Test string formatting.""" + pose = Pose(1.234, 2.567, 3.891) + covariance = np.eye(6).flatten() + pose_cov = PoseWithCovariance(pose, covariance) + + str_repr = str(pose_cov) + assert "PoseWithCovariance" in str_repr + assert "1.234" in str_repr + assert "2.567" in str_repr + assert "3.891" in str_repr + assert "cov_trace" in str_repr + assert "6.000" in str_repr # Trace of identity matrix is 6 + + +def test_pose_with_covariance_equality() -> None: + """Test equality comparison.""" + pose1 = Pose(1.0, 2.0, 3.0) + cov1 = np.arange(36, dtype=float) + pose_cov1 = PoseWithCovariance(pose1, cov1) + + pose2 = Pose(1.0, 2.0, 3.0) + cov2 = np.arange(36, dtype=float) + pose_cov2 = PoseWithCovariance(pose2, cov2) + + # Equal + assert pose_cov1 == pose_cov2 + + # Different pose + pose3 = Pose(1.1, 2.0, 3.0) + pose_cov3 = PoseWithCovariance(pose3, cov1) + assert pose_cov1 != pose_cov3 + + # Different covariance + cov3 = np.arange(36, dtype=float) + 1 + pose_cov4 = PoseWithCovariance(pose1, cov3) + assert pose_cov1 != pose_cov4 + + # Different type + assert pose_cov1 != "not a pose" + assert pose_cov1 is not None + + +def test_pose_with_covariance_lcm_encode_decode() -> None: + """Test LCM encoding and decoding.""" + pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) + covariance = np.arange(36, dtype=float) + source = PoseWithCovariance(pose, covariance) + + # Encode and decode + binary_msg = source.lcm_encode() + decoded = PoseWithCovariance.lcm_decode(binary_msg) + + # Should be equal + assert decoded == source + assert isinstance(decoded, PoseWithCovariance) + assert isinstance(decoded.pose, Pose) + assert isinstance(decoded.covariance, np.ndarray) + + +@pytest.mark.ros +def test_pose_with_covariance_from_ros_msg() -> None: + """Test creating from ROS message.""" + ros_msg = ROSPoseWithCovariance() + ros_msg.pose.position = ROSPoint(x=1.0, y=2.0, z=3.0) + ros_msg.pose.orientation = ROSQuaternion(x=0.1, y=0.2, z=0.3, w=0.9) + ros_msg.covariance = [float(i) for i in range(36)] + + pose_cov = PoseWithCovariance.from_ros_msg(ros_msg) + + assert pose_cov.pose.position.x == 1.0 + assert pose_cov.pose.position.y == 2.0 + assert pose_cov.pose.position.z == 3.0 + assert pose_cov.pose.orientation.x == 0.1 + assert pose_cov.pose.orientation.y == 0.2 + assert pose_cov.pose.orientation.z == 0.3 + assert pose_cov.pose.orientation.w == 0.9 + assert np.array_equal(pose_cov.covariance, np.arange(36)) + + +@pytest.mark.ros +def test_pose_with_covariance_to_ros_msg() -> None: + """Test converting to ROS message.""" + pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) + covariance = np.arange(36, dtype=float) + pose_cov = PoseWithCovariance(pose, covariance) + + ros_msg = pose_cov.to_ros_msg() + + assert isinstance(ros_msg, ROSPoseWithCovariance) + assert ros_msg.pose.position.x == 1.0 + assert ros_msg.pose.position.y == 2.0 + assert ros_msg.pose.position.z == 3.0 + assert ros_msg.pose.orientation.x == 0.1 + assert ros_msg.pose.orientation.y == 0.2 + assert ros_msg.pose.orientation.z == 0.3 + assert ros_msg.pose.orientation.w == 0.9 + assert list(ros_msg.covariance) == list(range(36)) + + +@pytest.mark.ros +def test_pose_with_covariance_ros_roundtrip() -> None: + """Test round-trip conversion with ROS messages.""" + pose = Pose(1.5, 2.5, 3.5, 0.15, 0.25, 0.35, 0.85) + covariance = np.random.rand(36) + original = PoseWithCovariance(pose, covariance) + + ros_msg = original.to_ros_msg() + restored = PoseWithCovariance.from_ros_msg(ros_msg) + + assert restored == original + + +def test_pose_with_covariance_zero_covariance() -> None: + """Test with zero covariance matrix.""" + pose = Pose(1.0, 2.0, 3.0) + pose_cov = PoseWithCovariance(pose) + + assert np.all(pose_cov.covariance == 0.0) + assert np.trace(pose_cov.covariance_matrix) == 0.0 + + +def test_pose_with_covariance_diagonal_covariance() -> None: + """Test with diagonal covariance matrix.""" + pose = Pose() + covariance = np.zeros(36) + # Set diagonal elements + for i in range(6): + covariance[i * 6 + i] = i + 1 + + pose_cov = PoseWithCovariance(pose, covariance) + + cov_matrix = pose_cov.covariance_matrix + assert np.trace(cov_matrix) == sum(range(1, 7)) # 1+2+3+4+5+6 = 21 + + # Check diagonal elements + for i in range(6): + assert cov_matrix[i, i] == i + 1 + + # Check off-diagonal elements are zero + for i in range(6): + for j in range(6): + if i != j: + assert cov_matrix[i, j] == 0.0 + + +@pytest.mark.parametrize( + "x,y,z", + [(0.0, 0.0, 0.0), (1.0, 2.0, 3.0), (-1.0, -2.0, -3.0), (100.0, -100.0, 0.0)], +) +def test_pose_with_covariance_parametrized_positions(x, y, z) -> None: + """Parametrized test for various position values.""" + pose = Pose(x, y, z) + pose_cov = PoseWithCovariance(pose) + + assert pose_cov.x == x + assert pose_cov.y == y + assert pose_cov.z == z diff --git a/dimos/msgs/geometry_msgs/test_PoseWithCovarianceStamped.py b/dimos/msgs/geometry_msgs/test_PoseWithCovarianceStamped.py new file mode 100644 index 0000000000..25a246495d --- /dev/null +++ b/dimos/msgs/geometry_msgs/test_PoseWithCovarianceStamped.py @@ -0,0 +1,368 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +import numpy as np +import pytest + +try: + from builtin_interfaces.msg import Time as ROSTime + from geometry_msgs.msg import ( + Point as ROSPoint, + Pose as ROSPose, + PoseWithCovariance as ROSPoseWithCovariance, + PoseWithCovarianceStamped as ROSPoseWithCovarianceStamped, + Quaternion as ROSQuaternion, + ) + from std_msgs.msg import Header as ROSHeader +except ImportError: + ROSHeader = None + ROSPoseWithCovarianceStamped = None + ROSPose = None + ROSQuaternion = None + ROSPoint = None + ROSTime = None + ROSPoseWithCovariance = None + + +from dimos.msgs.geometry_msgs.Pose import Pose +from dimos.msgs.geometry_msgs.PoseWithCovariance import PoseWithCovariance +from dimos.msgs.geometry_msgs.PoseWithCovarianceStamped import PoseWithCovarianceStamped + + +def test_pose_with_covariance_stamped_default_init() -> None: + """Test default initialization.""" + if ROSPoseWithCovariance is None: + pytest.skip("ROS not available") + if ROSTime is None: + pytest.skip("ROS not available") + if ROSPoint is None: + pytest.skip("ROS not available") + if ROSQuaternion is None: + pytest.skip("ROS not available") + if ROSPose is None: + pytest.skip("ROS not available") + if ROSPoseWithCovarianceStamped is None: + pytest.skip("ROS not available") + if ROSHeader is None: + pytest.skip("ROS not available") + pose_cov_stamped = PoseWithCovarianceStamped() + + # Should have current timestamp + assert pose_cov_stamped.ts > 0 + assert pose_cov_stamped.frame_id == "" + + # Pose should be at origin with identity orientation + assert pose_cov_stamped.pose.position.x == 0.0 + assert pose_cov_stamped.pose.position.y == 0.0 + assert pose_cov_stamped.pose.position.z == 0.0 + assert pose_cov_stamped.pose.orientation.w == 1.0 + + # Covariance should be all zeros + assert np.all(pose_cov_stamped.covariance == 0.0) + + +def test_pose_with_covariance_stamped_with_timestamp() -> None: + """Test initialization with specific timestamp.""" + ts = 1234567890.123456 + frame_id = "base_link" + pose_cov_stamped = PoseWithCovarianceStamped(ts=ts, frame_id=frame_id) + + assert pose_cov_stamped.ts == ts + assert pose_cov_stamped.frame_id == frame_id + + +def test_pose_with_covariance_stamped_with_pose() -> None: + """Test initialization with pose.""" + ts = 1234567890.123456 + frame_id = "map" + pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) + covariance = np.arange(36, dtype=float) + + pose_cov_stamped = PoseWithCovarianceStamped( + ts=ts, frame_id=frame_id, pose=pose, covariance=covariance + ) + + assert pose_cov_stamped.ts == ts + assert pose_cov_stamped.frame_id == frame_id + assert pose_cov_stamped.pose.position.x == 1.0 + assert pose_cov_stamped.pose.position.y == 2.0 + assert pose_cov_stamped.pose.position.z == 3.0 + assert np.array_equal(pose_cov_stamped.covariance, covariance) + + +def test_pose_with_covariance_stamped_properties() -> None: + """Test convenience properties.""" + pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) + covariance = np.eye(6).flatten() + pose_cov_stamped = PoseWithCovarianceStamped( + ts=1234567890.0, frame_id="odom", pose=pose, covariance=covariance + ) + + # Position properties + assert pose_cov_stamped.x == 1.0 + assert pose_cov_stamped.y == 2.0 + assert pose_cov_stamped.z == 3.0 + + # Orientation properties + assert pose_cov_stamped.orientation.x == 0.1 + assert pose_cov_stamped.orientation.y == 0.2 + assert pose_cov_stamped.orientation.z == 0.3 + assert pose_cov_stamped.orientation.w == 0.9 + + # Euler angles + assert pose_cov_stamped.roll == pose.roll + assert pose_cov_stamped.pitch == pose.pitch + assert pose_cov_stamped.yaw == pose.yaw + + # Covariance matrix + cov_matrix = pose_cov_stamped.covariance_matrix + assert cov_matrix.shape == (6, 6) + assert np.trace(cov_matrix) == 6.0 + + +def test_pose_with_covariance_stamped_str() -> None: + """Test string representation.""" + pose = Pose(1.234, 2.567, 3.891) + covariance = np.eye(6).flatten() * 2.0 + pose_cov_stamped = PoseWithCovarianceStamped( + ts=1234567890.0, frame_id="world", pose=pose, covariance=covariance + ) + + str_repr = str(pose_cov_stamped) + assert "PoseWithCovarianceStamped" in str_repr + assert "1.234" in str_repr + assert "2.567" in str_repr + assert "3.891" in str_repr + assert "cov_trace" in str_repr + assert "12.000" in str_repr # Trace of 2*identity is 12 + + +def test_pose_with_covariance_stamped_lcm_encode_decode() -> None: + """Test LCM encoding and decoding.""" + ts = 1234567890.123456 + frame_id = "camera_link" + pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) + covariance = np.arange(36, dtype=float) + + source = PoseWithCovarianceStamped(ts=ts, frame_id=frame_id, pose=pose, covariance=covariance) + + # Encode and decode + binary_msg = source.lcm_encode() + decoded = PoseWithCovarianceStamped.lcm_decode(binary_msg) + + # Check timestamp (may lose some precision) + assert abs(decoded.ts - ts) < 1e-6 + assert decoded.frame_id == frame_id + + # Check pose + assert decoded.pose.position.x == 1.0 + assert decoded.pose.position.y == 2.0 + assert decoded.pose.position.z == 3.0 + assert decoded.pose.orientation.x == 0.1 + assert decoded.pose.orientation.y == 0.2 + assert decoded.pose.orientation.z == 0.3 + assert decoded.pose.orientation.w == 0.9 + + # Check covariance + assert np.array_equal(decoded.covariance, covariance) + + +@pytest.mark.ros +def test_pose_with_covariance_stamped_from_ros_msg() -> None: + """Test creating from ROS message.""" + ros_msg = ROSPoseWithCovarianceStamped() + + # Set header + ros_msg.header = ROSHeader() + ros_msg.header.stamp = ROSTime() + ros_msg.header.stamp.sec = 1234567890 + ros_msg.header.stamp.nanosec = 123456000 + ros_msg.header.frame_id = "laser" + + # Set pose with covariance + ros_msg.pose = ROSPoseWithCovariance() + ros_msg.pose.pose = ROSPose() + ros_msg.pose.pose.position = ROSPoint(x=1.0, y=2.0, z=3.0) + ros_msg.pose.pose.orientation = ROSQuaternion(x=0.1, y=0.2, z=0.3, w=0.9) + ros_msg.pose.covariance = [float(i) for i in range(36)] + + pose_cov_stamped = PoseWithCovarianceStamped.from_ros_msg(ros_msg) + + assert pose_cov_stamped.ts == 1234567890.123456 + assert pose_cov_stamped.frame_id == "laser" + assert pose_cov_stamped.pose.position.x == 1.0 + assert pose_cov_stamped.pose.position.y == 2.0 + assert pose_cov_stamped.pose.position.z == 3.0 + assert pose_cov_stamped.pose.orientation.x == 0.1 + assert pose_cov_stamped.pose.orientation.y == 0.2 + assert pose_cov_stamped.pose.orientation.z == 0.3 + assert pose_cov_stamped.pose.orientation.w == 0.9 + assert np.array_equal(pose_cov_stamped.covariance, np.arange(36)) + + +@pytest.mark.ros +def test_pose_with_covariance_stamped_to_ros_msg() -> None: + """Test converting to ROS message.""" + ts = 1234567890.567890 + frame_id = "imu" + pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) + covariance = np.arange(36, dtype=float) + + pose_cov_stamped = PoseWithCovarianceStamped( + ts=ts, frame_id=frame_id, pose=pose, covariance=covariance + ) + + ros_msg = pose_cov_stamped.to_ros_msg() + + assert isinstance(ros_msg, ROSPoseWithCovarianceStamped) + assert ros_msg.header.frame_id == frame_id + assert ros_msg.header.stamp.sec == 1234567890 + assert abs(ros_msg.header.stamp.nanosec - 567890000) < 100 # Allow small rounding error + + assert ros_msg.pose.pose.position.x == 1.0 + assert ros_msg.pose.pose.position.y == 2.0 + assert ros_msg.pose.pose.position.z == 3.0 + assert ros_msg.pose.pose.orientation.x == 0.1 + assert ros_msg.pose.pose.orientation.y == 0.2 + assert ros_msg.pose.pose.orientation.z == 0.3 + assert ros_msg.pose.pose.orientation.w == 0.9 + assert list(ros_msg.pose.covariance) == list(range(36)) + + +@pytest.mark.ros +def test_pose_with_covariance_stamped_ros_roundtrip() -> None: + """Test round-trip conversion with ROS messages.""" + ts = 2147483647.987654 # Max int32 value for ROS Time.sec + frame_id = "robot_base" + pose = Pose(1.5, 2.5, 3.5, 0.15, 0.25, 0.35, 0.85) + covariance = np.random.rand(36) + + original = PoseWithCovarianceStamped(ts=ts, frame_id=frame_id, pose=pose, covariance=covariance) + + ros_msg = original.to_ros_msg() + restored = PoseWithCovarianceStamped.from_ros_msg(ros_msg) + + # Check timestamp (loses some precision in conversion) + assert abs(restored.ts - ts) < 1e-6 + assert restored.frame_id == frame_id + + # Check pose + assert restored.pose.position.x == original.pose.position.x + assert restored.pose.position.y == original.pose.position.y + assert restored.pose.position.z == original.pose.position.z + assert restored.pose.orientation.x == original.pose.orientation.x + assert restored.pose.orientation.y == original.pose.orientation.y + assert restored.pose.orientation.z == original.pose.orientation.z + assert restored.pose.orientation.w == original.pose.orientation.w + + # Check covariance + assert np.allclose(restored.covariance, original.covariance) + + +def test_pose_with_covariance_stamped_zero_timestamp() -> None: + """Test that zero timestamp gets replaced with current time.""" + pose_cov_stamped = PoseWithCovarianceStamped(ts=0.0) + + # Should have been replaced with current time + assert pose_cov_stamped.ts > 0 + assert pose_cov_stamped.ts <= time.time() + + +def test_pose_with_covariance_stamped_inheritance() -> None: + """Test that it properly inherits from PoseWithCovariance and Timestamped.""" + pose = Pose(1.0, 2.0, 3.0) + covariance = np.eye(6).flatten() + pose_cov_stamped = PoseWithCovarianceStamped( + ts=1234567890.0, frame_id="test", pose=pose, covariance=covariance + ) + + # Should be instance of parent classes + assert isinstance(pose_cov_stamped, PoseWithCovariance) + + # Should have Timestamped attributes + assert hasattr(pose_cov_stamped, "ts") + assert hasattr(pose_cov_stamped, "frame_id") + + # Should have PoseWithCovariance attributes + assert hasattr(pose_cov_stamped, "pose") + assert hasattr(pose_cov_stamped, "covariance") + + +def test_pose_with_covariance_stamped_sec_nsec() -> None: + """Test the sec_nsec helper function.""" + from dimos.msgs.geometry_msgs.PoseWithCovarianceStamped import sec_nsec + + # Test integer seconds + s, ns = sec_nsec(1234567890.0) + assert s == 1234567890 + assert ns == 0 + + # Test fractional seconds + s, ns = sec_nsec(1234567890.123456789) + assert s == 1234567890 + assert abs(ns - 123456789) < 100 # Allow small rounding error + + # Test small fractional seconds + s, ns = sec_nsec(0.000000001) + assert s == 0 + assert ns == 1 + + # Test large timestamp + s, ns = sec_nsec(9999999999.999999999) + # Due to floating point precision, this might round to 10000000000 + assert s in [9999999999, 10000000000] + if s == 9999999999: + assert abs(ns - 999999999) < 10 + else: + assert ns == 0 + + +@pytest.mark.ros +@pytest.mark.parametrize( + "frame_id", + ["", "map", "odom", "base_link", "camera_optical_frame", "sensor/lidar/front"], +) +def test_pose_with_covariance_stamped_frame_ids(frame_id) -> None: + """Test various frame ID values.""" + pose_cov_stamped = PoseWithCovarianceStamped(frame_id=frame_id) + assert pose_cov_stamped.frame_id == frame_id + + # Test roundtrip through ROS + ros_msg = pose_cov_stamped.to_ros_msg() + assert ros_msg.header.frame_id == frame_id + + restored = PoseWithCovarianceStamped.from_ros_msg(ros_msg) + assert restored.frame_id == frame_id + + +def test_pose_with_covariance_stamped_different_covariances() -> None: + """Test with different covariance patterns.""" + pose = Pose(1.0, 2.0, 3.0) + + # Zero covariance + zero_cov = np.zeros(36) + pose_cov1 = PoseWithCovarianceStamped(pose=pose, covariance=zero_cov) + assert np.all(pose_cov1.covariance == 0.0) + + # Identity covariance + identity_cov = np.eye(6).flatten() + pose_cov2 = PoseWithCovarianceStamped(pose=pose, covariance=identity_cov) + assert np.trace(pose_cov2.covariance_matrix) == 6.0 + + # Full covariance + full_cov = np.random.rand(36) + pose_cov3 = PoseWithCovarianceStamped(pose=pose, covariance=full_cov) + assert np.array_equal(pose_cov3.covariance, full_cov) diff --git a/dimos/msgs/geometry_msgs/test_Quaternion.py b/dimos/msgs/geometry_msgs/test_Quaternion.py new file mode 100644 index 0000000000..501f5a0271 --- /dev/null +++ b/dimos/msgs/geometry_msgs/test_Quaternion.py @@ -0,0 +1,387 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos_lcm.geometry_msgs import Quaternion as LCMQuaternion +import numpy as np +import pytest + +from dimos.msgs.geometry_msgs.Quaternion import Quaternion + + +def test_quaternion_default_init() -> None: + """Test that default initialization creates an identity quaternion (w=1, x=y=z=0).""" + q = Quaternion() + assert q.x == 0.0 + assert q.y == 0.0 + assert q.z == 0.0 + assert q.w == 1.0 + assert q.to_tuple() == (0.0, 0.0, 0.0, 1.0) + + +def test_quaternion_component_init() -> None: + """Test initialization with four float components (x, y, z, w).""" + q = Quaternion(0.5, 0.5, 0.5, 0.5) + assert q.x == 0.5 + assert q.y == 0.5 + assert q.z == 0.5 + assert q.w == 0.5 + + # Test with different values + q2 = Quaternion(1.0, 2.0, 3.0, 4.0) + assert q2.x == 1.0 + assert q2.y == 2.0 + assert q2.z == 3.0 + assert q2.w == 4.0 + + # Test with negative values + q3 = Quaternion(-1.0, -2.0, -3.0, -4.0) + assert q3.x == -1.0 + assert q3.y == -2.0 + assert q3.z == -3.0 + assert q3.w == -4.0 + + # Test with integers (should convert to float) + q4 = Quaternion(1, 2, 3, 4) + assert q4.x == 1.0 + assert q4.y == 2.0 + assert q4.z == 3.0 + assert q4.w == 4.0 + assert isinstance(q4.x, float) + + +def test_quaternion_sequence_init() -> None: + """Test initialization from sequence (list, tuple) of 4 numbers.""" + # From list + q1 = Quaternion([0.1, 0.2, 0.3, 0.4]) + assert q1.x == 0.1 + assert q1.y == 0.2 + assert q1.z == 0.3 + assert q1.w == 0.4 + + # From tuple + q2 = Quaternion((0.5, 0.6, 0.7, 0.8)) + assert q2.x == 0.5 + assert q2.y == 0.6 + assert q2.z == 0.7 + assert q2.w == 0.8 + + # Test with integers in sequence + q3 = Quaternion([1, 2, 3, 4]) + assert q3.x == 1.0 + assert q3.y == 2.0 + assert q3.z == 3.0 + assert q3.w == 4.0 + + # Test error with wrong length + with pytest.raises(ValueError, match="Quaternion requires exactly 4 components"): + Quaternion([1, 2, 3]) # Only 3 components + + with pytest.raises(ValueError, match="Quaternion requires exactly 4 components"): + Quaternion([1, 2, 3, 4, 5]) # Too many components + + +def test_quaternion_numpy_init() -> None: + """Test initialization from numpy array.""" + # From numpy array + arr = np.array([0.1, 0.2, 0.3, 0.4]) + q1 = Quaternion(arr) + assert q1.x == 0.1 + assert q1.y == 0.2 + assert q1.z == 0.3 + assert q1.w == 0.4 + + # Test with different dtypes + arr_int = np.array([1, 2, 3, 4], dtype=int) + q2 = Quaternion(arr_int) + assert q2.x == 1.0 + assert q2.y == 2.0 + assert q2.z == 3.0 + assert q2.w == 4.0 + + # Test error with wrong size + with pytest.raises(ValueError, match="Quaternion requires exactly 4 components"): + Quaternion(np.array([1, 2, 3])) # Only 3 elements + + with pytest.raises(ValueError, match="Quaternion requires exactly 4 components"): + Quaternion(np.array([1, 2, 3, 4, 5])) # Too many elements + + +def test_quaternion_copy_init() -> None: + """Test initialization from another Quaternion (copy constructor).""" + original = Quaternion(0.1, 0.2, 0.3, 0.4) + copy = Quaternion(original) + + assert copy.x == 0.1 + assert copy.y == 0.2 + assert copy.z == 0.3 + assert copy.w == 0.4 + + # Verify it's a copy, not the same object + assert copy is not original + assert copy == original + + +def test_quaternion_lcm_init() -> None: + """Test initialization from LCM Quaternion.""" + lcm_quat = LCMQuaternion() + lcm_quat.x = 0.1 + lcm_quat.y = 0.2 + lcm_quat.z = 0.3 + lcm_quat.w = 0.4 + + q = Quaternion(lcm_quat) + assert q.x == 0.1 + assert q.y == 0.2 + assert q.z == 0.3 + assert q.w == 0.4 + + +def test_quaternion_properties() -> None: + """Test quaternion component properties.""" + q = Quaternion(1.0, 2.0, 3.0, 4.0) + + # Test property access + assert q.x == 1.0 + assert q.y == 2.0 + assert q.z == 3.0 + assert q.w == 4.0 + + # Test as_tuple property + assert q.to_tuple() == (1.0, 2.0, 3.0, 4.0) + + +def test_quaternion_indexing() -> None: + """Test quaternion indexing support.""" + q = Quaternion(1.0, 2.0, 3.0, 4.0) + + # Test indexing + assert q[0] == 1.0 + assert q[1] == 2.0 + assert q[2] == 3.0 + assert q[3] == 4.0 + + +def test_quaternion_euler() -> None: + """Test quaternion to Euler angles conversion.""" + + # Test identity quaternion (should give zero angles) + q_identity = Quaternion() + angles = q_identity.to_euler() + assert np.isclose(angles.x, 0.0, atol=1e-10) # roll + assert np.isclose(angles.y, 0.0, atol=1e-10) # pitch + assert np.isclose(angles.z, 0.0, atol=1e-10) # yaw + + # Test 90 degree rotation around Z-axis (yaw) + q_z90 = Quaternion(0, 0, np.sin(np.pi / 4), np.cos(np.pi / 4)) + angles_z90 = q_z90.to_euler() + assert np.isclose(angles_z90.roll, 0.0, atol=1e-10) # roll should be 0 + assert np.isclose(angles_z90.pitch, 0.0, atol=1e-10) # pitch should be 0 + assert np.isclose(angles_z90.yaw, np.pi / 2, atol=1e-10) # yaw should be π/2 (90 degrees) + + # Test 90 degree rotation around X-axis (roll) + q_x90 = Quaternion(np.sin(np.pi / 4), 0, 0, np.cos(np.pi / 4)) + angles_x90 = q_x90.to_euler() + assert np.isclose(angles_x90.x, np.pi / 2, atol=1e-10) # roll should be π/2 + assert np.isclose(angles_x90.y, 0.0, atol=1e-10) # pitch should be 0 + assert np.isclose(angles_x90.z, 0.0, atol=1e-10) # yaw should be 0 + + +def test_lcm_encode_decode() -> None: + """Test encoding and decoding of Quaternion to/from binary LCM format.""" + q_source = Quaternion(1.0, 2.0, 3.0, 4.0) + + binary_msg = q_source.lcm_encode() + + q_dest = Quaternion.lcm_decode(binary_msg) + + assert isinstance(q_dest, Quaternion) + assert q_dest is not q_source + assert q_dest == q_source + + +def test_quaternion_multiplication() -> None: + """Test quaternion multiplication (Hamilton product).""" + # Test identity multiplication + q1 = Quaternion(0.5, 0.5, 0.5, 0.5) + identity = Quaternion(0, 0, 0, 1) + + result = q1 * identity + assert np.allclose([result.x, result.y, result.z, result.w], [q1.x, q1.y, q1.z, q1.w]) + + # Test multiplication order matters (non-commutative) + q2 = Quaternion(0.1, 0.2, 0.3, 0.4) + q3 = Quaternion(0.4, 0.3, 0.2, 0.1) + + result1 = q2 * q3 + result2 = q3 * q2 + + # Results should be different + assert not np.allclose( + [result1.x, result1.y, result1.z, result1.w], [result2.x, result2.y, result2.z, result2.w] + ) + + # Test specific multiplication case + # 90 degree rotations around Z axis + angle = np.pi / 2 + q_90z = Quaternion(0, 0, np.sin(angle / 2), np.cos(angle / 2)) + + # Two 90 degree rotations should give 180 degrees + result = q_90z * q_90z + expected_angle = np.pi + assert np.isclose(result.x, 0, atol=1e-10) + assert np.isclose(result.y, 0, atol=1e-10) + assert np.isclose(result.z, np.sin(expected_angle / 2), atol=1e-10) + assert np.isclose(result.w, np.cos(expected_angle / 2), atol=1e-10) + + +def test_quaternion_conjugate() -> None: + """Test quaternion conjugate.""" + q = Quaternion(0.1, 0.2, 0.3, 0.4) + conj = q.conjugate() + + # Conjugate should negate x, y, z but keep w + assert conj.x == -q.x + assert conj.y == -q.y + assert conj.z == -q.z + assert conj.w == q.w + + # Test that q * q^* gives a real quaternion (x=y=z=0) + result = q * conj + assert np.isclose(result.x, 0, atol=1e-10) + assert np.isclose(result.y, 0, atol=1e-10) + assert np.isclose(result.z, 0, atol=1e-10) + # w should be the squared norm + expected_w = q.x**2 + q.y**2 + q.z**2 + q.w**2 + assert np.isclose(result.w, expected_w, atol=1e-10) + + +def test_quaternion_inverse() -> None: + """Test quaternion inverse.""" + # Test with unit quaternion + q_unit = Quaternion(0, 0, 0, 1).normalize() # Already normalized but being explicit + inv = q_unit.inverse() + + # For unit quaternion, inverse equals conjugate + conj = q_unit.conjugate() + assert np.allclose([inv.x, inv.y, inv.z, inv.w], [conj.x, conj.y, conj.z, conj.w]) + + # Test that q * q^-1 = identity + q = Quaternion(0.5, 0.5, 0.5, 0.5) + inv = q.inverse() + result = q * inv + + assert np.isclose(result.x, 0, atol=1e-10) + assert np.isclose(result.y, 0, atol=1e-10) + assert np.isclose(result.z, 0, atol=1e-10) + assert np.isclose(result.w, 1, atol=1e-10) + + # Test inverse of non-unit quaternion + q_non_unit = Quaternion(2, 0, 0, 0) # Non-unit quaternion + inv = q_non_unit.inverse() + result = q_non_unit * inv + + assert np.isclose(result.x, 0, atol=1e-10) + assert np.isclose(result.y, 0, atol=1e-10) + assert np.isclose(result.z, 0, atol=1e-10) + assert np.isclose(result.w, 1, atol=1e-10) + + +def test_quaternion_normalize() -> None: + """Test quaternion normalization.""" + # Test non-unit quaternion + q = Quaternion(1, 2, 3, 4) + q_norm = q.normalize() + + # Check that magnitude is 1 + magnitude = np.sqrt(q_norm.x**2 + q_norm.y**2 + q_norm.z**2 + q_norm.w**2) + assert np.isclose(magnitude, 1.0, atol=1e-10) + + # Check that direction is preserved + scale = np.sqrt(q.x**2 + q.y**2 + q.z**2 + q.w**2) + assert np.isclose(q_norm.x, q.x / scale, atol=1e-10) + assert np.isclose(q_norm.y, q.y / scale, atol=1e-10) + assert np.isclose(q_norm.z, q.z / scale, atol=1e-10) + assert np.isclose(q_norm.w, q.w / scale, atol=1e-10) + + +def test_quaternion_rotate_vector() -> None: + """Test rotating vectors with quaternions.""" + from dimos.msgs.geometry_msgs.Vector3 import Vector3 + + # Test rotation of unit vectors + # 90 degree rotation around Z axis + angle = np.pi / 2 + q_rot = Quaternion(0, 0, np.sin(angle / 2), np.cos(angle / 2)) + + # Rotate X unit vector + v_x = Vector3(1, 0, 0) + v_rotated = q_rot.rotate_vector(v_x) + + # Should now point along Y axis + assert np.isclose(v_rotated.x, 0, atol=1e-10) + assert np.isclose(v_rotated.y, 1, atol=1e-10) + assert np.isclose(v_rotated.z, 0, atol=1e-10) + + # Rotate Y unit vector + v_y = Vector3(0, 1, 0) + v_rotated = q_rot.rotate_vector(v_y) + + # Should now point along negative X axis + assert np.isclose(v_rotated.x, -1, atol=1e-10) + assert np.isclose(v_rotated.y, 0, atol=1e-10) + assert np.isclose(v_rotated.z, 0, atol=1e-10) + + # Test that Z vector is unchanged (rotation axis) + v_z = Vector3(0, 0, 1) + v_rotated = q_rot.rotate_vector(v_z) + + assert np.isclose(v_rotated.x, 0, atol=1e-10) + assert np.isclose(v_rotated.y, 0, atol=1e-10) + assert np.isclose(v_rotated.z, 1, atol=1e-10) + + # Test identity rotation + q_identity = Quaternion(0, 0, 0, 1) + v = Vector3(1, 2, 3) + v_rotated = q_identity.rotate_vector(v) + + assert np.isclose(v_rotated.x, v.x, atol=1e-10) + assert np.isclose(v_rotated.y, v.y, atol=1e-10) + assert np.isclose(v_rotated.z, v.z, atol=1e-10) + + +def test_quaternion_inverse_zero() -> None: + """Test that inverting zero quaternion raises error.""" + q_zero = Quaternion(0, 0, 0, 0) + + with pytest.raises(ZeroDivisionError, match="Cannot invert zero quaternion"): + q_zero.inverse() + + +def test_quaternion_normalize_zero() -> None: + """Test that normalizing zero quaternion raises error.""" + q_zero = Quaternion(0, 0, 0, 0) + + with pytest.raises(ZeroDivisionError, match="Cannot normalize zero quaternion"): + q_zero.normalize() + + +def test_quaternion_multiplication_type_error() -> None: + """Test that multiplying quaternion with non-quaternion raises error.""" + q = Quaternion(1, 0, 0, 0) + + with pytest.raises(TypeError, match="Cannot multiply Quaternion with"): + q * 5.0 + + with pytest.raises(TypeError, match="Cannot multiply Quaternion with"): + q * [1, 2, 3, 4] diff --git a/dimos/msgs/geometry_msgs/test_Transform.py b/dimos/msgs/geometry_msgs/test_Transform.py new file mode 100644 index 0000000000..b61e92ae01 --- /dev/null +++ b/dimos/msgs/geometry_msgs/test_Transform.py @@ -0,0 +1,510 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +import time + +import numpy as np +import pytest + +try: + from geometry_msgs.msg import TransformStamped as ROSTransformStamped +except ImportError: + ROSTransformStamped = None + + +from dimos.msgs.geometry_msgs import Pose, PoseStamped, Quaternion, Transform, Vector3 + + +def test_transform_initialization() -> None: + # Test default initialization (identity transform) + tf = Transform() + assert tf.translation.x == 0.0 + assert tf.translation.y == 0.0 + assert tf.translation.z == 0.0 + assert tf.rotation.x == 0.0 + assert tf.rotation.y == 0.0 + assert tf.rotation.z == 0.0 + assert tf.rotation.w == 1.0 + + # Test initialization with Vector3 and Quaternion + trans = Vector3(1.0, 2.0, 3.0) + rot = Quaternion(0.0, 0.0, 0.707107, 0.707107) # 90 degrees around Z + tf2 = Transform(translation=trans, rotation=rot) + assert tf2.translation == trans + assert tf2.rotation == rot + + # Test initialization with only translation + tf5 = Transform(translation=Vector3(7.0, 8.0, 9.0)) + assert tf5.translation.x == 7.0 + assert tf5.translation.y == 8.0 + assert tf5.translation.z == 9.0 + assert tf5.rotation.w == 1.0 # Identity rotation + + # Test initialization with only rotation + tf6 = Transform(rotation=Quaternion(0.0, 0.0, 0.0, 1.0)) + assert tf6.translation.is_zero() # Zero translation + assert tf6.rotation.w == 1.0 + + # Test keyword argument initialization + tf7 = Transform(translation=Vector3(1, 2, 3), rotation=Quaternion()) + assert tf7.translation == Vector3(1, 2, 3) + assert tf7.rotation == Quaternion() + + # Test keyword with only translation + tf8 = Transform(translation=Vector3(4, 5, 6)) + assert tf8.translation == Vector3(4, 5, 6) + assert tf8.rotation.w == 1.0 + + # Test keyword with only rotation + tf9 = Transform(rotation=Quaternion(0, 0, 1, 0)) + assert tf9.translation.is_zero() + assert tf9.rotation == Quaternion(0, 0, 1, 0) + + +def test_transform_identity() -> None: + # Test identity class method + tf = Transform.identity() + assert tf.translation.is_zero() + assert tf.rotation.x == 0.0 + assert tf.rotation.y == 0.0 + assert tf.rotation.z == 0.0 + assert tf.rotation.w == 1.0 + + # Identity should equal default constructor + assert tf == Transform() + + +def test_transform_equality() -> None: + tf1 = Transform(translation=Vector3(1, 2, 3), rotation=Quaternion(0, 0, 0, 1)) + tf2 = Transform(translation=Vector3(1, 2, 3), rotation=Quaternion(0, 0, 0, 1)) + tf3 = Transform(translation=Vector3(1, 2, 4), rotation=Quaternion(0, 0, 0, 1)) # Different z + tf4 = Transform( + translation=Vector3(1, 2, 3), rotation=Quaternion(0, 0, 1, 0) + ) # Different rotation + + assert tf1 == tf2 + assert tf1 != tf3 + assert tf1 != tf4 + assert tf1 != "not a transform" + + +def test_transform_string_representations() -> None: + tf = Transform( + translation=Vector3(1.5, -2.0, 3.14), rotation=Quaternion(0, 0, 0.707107, 0.707107) + ) + + # Test repr + repr_str = repr(tf) + assert "Transform" in repr_str + assert "translation=" in repr_str + assert "rotation=" in repr_str + assert "1.5" in repr_str + + # Test str + str_str = str(tf) + assert "Transform:" in str_str + assert "Translation:" in str_str + assert "Rotation:" in str_str + + +def test_pose_add_transform() -> None: + initial_pose = Pose(1.0, 0.0, 0.0) + + # 90 degree rotation around Z axis + angle = np.pi / 2 + transform = Transform( + translation=Vector3(2.0, 1.0, 0.0), + rotation=Quaternion(0.0, 0.0, np.sin(angle / 2), np.cos(angle / 2)), + ) + + transformed_pose = initial_pose @ transform + + # - Translation (2, 1, 0) is added directly to position (1, 0, 0) + # - Result position: (3, 1, 0) + assert np.isclose(transformed_pose.position.x, 3.0, atol=1e-10) + assert np.isclose(transformed_pose.position.y, 1.0, atol=1e-10) + assert np.isclose(transformed_pose.position.z, 0.0, atol=1e-10) + + # Rotation should be 90 degrees around Z + assert np.isclose(transformed_pose.orientation.x, 0.0, atol=1e-10) + assert np.isclose(transformed_pose.orientation.y, 0.0, atol=1e-10) + assert np.isclose(transformed_pose.orientation.z, np.sin(angle / 2), atol=1e-10) + assert np.isclose(transformed_pose.orientation.w, np.cos(angle / 2), atol=1e-10) + + initial_pose_stamped = PoseStamped( + position=initial_pose.position, orientation=initial_pose.orientation + ) + transformed_pose_stamped = PoseStamped( + position=transformed_pose.position, orientation=transformed_pose.orientation + ) + + found_tf = initial_pose_stamped.find_transform(transformed_pose_stamped) + + assert found_tf.translation == transform.translation + assert found_tf.rotation == transform.rotation + assert found_tf.translation.x == transform.translation.x + assert found_tf.translation.y == transform.translation.y + assert found_tf.translation.z == transform.translation.z + + assert found_tf.rotation.x == transform.rotation.x + assert found_tf.rotation.y == transform.rotation.y + assert found_tf.rotation.z == transform.rotation.z + assert found_tf.rotation.w == transform.rotation.w + + print(found_tf.rotation, found_tf.translation) + + +def test_pose_add_transform_with_rotation() -> None: + # Create a pose at (0, 0, 0) rotated 90 degrees around Z + angle = np.pi / 2 + initial_pose = Pose(0.0, 0.0, 0.0, 0.0, 0.0, np.sin(angle / 2), np.cos(angle / 2)) + + # Add 45 degree rotation to transform1 + rotation_angle = np.pi / 4 # 45 degrees + transform1 = Transform( + translation=Vector3(1.0, 0.0, 0.0), + rotation=Quaternion( + 0.0, 0.0, np.sin(rotation_angle / 2), np.cos(rotation_angle / 2) + ), # 45� around Z + ) + + transform2 = Transform( + translation=Vector3(0.0, 1.0, 1.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), # No rotation + ) + + transformed_pose1 = initial_pose @ transform1 + transformed_pose2 = initial_pose @ transform1 @ transform2 + + # Test transformed_pose1: initial_pose + transform1 + # Since the pose is rotated 90� (facing +Y), moving forward (local X) + # means moving in the +Y direction in world frame + assert np.isclose(transformed_pose1.position.x, 0.0, atol=1e-10) + assert np.isclose(transformed_pose1.position.y, 1.0, atol=1e-10) + assert np.isclose(transformed_pose1.position.z, 0.0, atol=1e-10) + + # Orientation should be 90� + 45� = 135� around Z + total_angle1 = angle + rotation_angle # 135 degrees + assert np.isclose(transformed_pose1.orientation.x, 0.0, atol=1e-10) + assert np.isclose(transformed_pose1.orientation.y, 0.0, atol=1e-10) + assert np.isclose(transformed_pose1.orientation.z, np.sin(total_angle1 / 2), atol=1e-10) + assert np.isclose(transformed_pose1.orientation.w, np.cos(total_angle1 / 2), atol=1e-10) + + # Test transformed_pose2: initial_pose + transform1 + transform2 + # Starting from (0, 0, 0) facing 90�: + # + # - Apply transform1: move 1 forward (along +Y) � (0, 1, 0), now facing 135� + # + # - Apply transform2: move 1 in local Y and 1 up + # At 135�, local Y points at 225� (135� + 90�) + # + # x += cos(225�) = -2/2, y += sin(225�) = -2/2 + sqrt2_2 = np.sqrt(2) / 2 + expected_x = 0.0 - sqrt2_2 # 0 - 2/2 H -0.707 + expected_y = 1.0 - sqrt2_2 # 1 - 2/2 H 0.293 + expected_z = 1.0 # 0 + 1 + + assert np.isclose(transformed_pose2.position.x, expected_x, atol=1e-10) + assert np.isclose(transformed_pose2.position.y, expected_y, atol=1e-10) + assert np.isclose(transformed_pose2.position.z, expected_z, atol=1e-10) + + # Orientation should be 135� (only transform1 has rotation) + total_angle2 = total_angle1 # 135 degrees (transform2 has no rotation) + assert np.isclose(transformed_pose2.orientation.x, 0.0, atol=1e-10) + assert np.isclose(transformed_pose2.orientation.y, 0.0, atol=1e-10) + assert np.isclose(transformed_pose2.orientation.z, np.sin(total_angle2 / 2), atol=1e-10) + assert np.isclose(transformed_pose2.orientation.w, np.cos(total_angle2 / 2), atol=1e-10) + + +def test_lcm_encode_decode() -> None: + angle = np.pi / 2 + transform = Transform( + translation=Vector3(2.0, 1.0, 0.0), + rotation=Quaternion(0.0, 0.0, np.sin(angle / 2), np.cos(angle / 2)), + ) + + data = transform.lcm_encode() + + decoded_transform = Transform.lcm_decode(data) + + assert decoded_transform == transform + + +def test_transform_addition() -> None: + # Test 1: Simple translation addition (no rotation) + t1 = Transform( + translation=Vector3(1, 0, 0), + rotation=Quaternion(0, 0, 0, 1), # identity rotation + ) + t2 = Transform( + translation=Vector3(2, 0, 0), + rotation=Quaternion(0, 0, 0, 1), # identity rotation + ) + t3 = t1 + t2 + assert t3.translation == Vector3(3, 0, 0) + assert t3.rotation == Quaternion(0, 0, 0, 1) + + # Test 2: 90-degree rotation composition + # First transform: move 1 unit in X + t1 = Transform( + translation=Vector3(1, 0, 0), + rotation=Quaternion(0, 0, 0, 1), # identity + ) + # Second transform: move 1 unit in X with 90-degree rotation around Z + angle = np.pi / 2 + t2 = Transform( + translation=Vector3(1, 0, 0), + rotation=Quaternion(0, 0, np.sin(angle / 2), np.cos(angle / 2)), + ) + t3 = t1 + t2 + assert t3.translation == Vector3(2, 0, 0) + # Rotation should be 90 degrees around Z + assert np.isclose(t3.rotation.x, 0.0, atol=1e-10) + assert np.isclose(t3.rotation.y, 0.0, atol=1e-10) + assert np.isclose(t3.rotation.z, np.sin(angle / 2), atol=1e-10) + assert np.isclose(t3.rotation.w, np.cos(angle / 2), atol=1e-10) + + # Test 3: Rotation affects translation + # First transform: 90-degree rotation around Z + t1 = Transform( + translation=Vector3(0, 0, 0), + rotation=Quaternion(0, 0, np.sin(angle / 2), np.cos(angle / 2)), # 90° around Z + ) + # Second transform: move 1 unit in X + t2 = Transform( + translation=Vector3(1, 0, 0), + rotation=Quaternion(0, 0, 0, 1), # identity + ) + t3 = t1 + t2 + # X direction rotated 90° becomes Y direction + assert np.isclose(t3.translation.x, 0.0, atol=1e-10) + assert np.isclose(t3.translation.y, 1.0, atol=1e-10) + assert np.isclose(t3.translation.z, 0.0, atol=1e-10) + # Rotation remains 90° around Z + assert np.isclose(t3.rotation.z, np.sin(angle / 2), atol=1e-10) + assert np.isclose(t3.rotation.w, np.cos(angle / 2), atol=1e-10) + + # Test 4: Frame tracking + t1 = Transform( + translation=Vector3(1, 0, 0), + rotation=Quaternion(0, 0, 0, 1), + frame_id="world", + child_frame_id="robot", + ) + t2 = Transform( + translation=Vector3(2, 0, 0), + rotation=Quaternion(0, 0, 0, 1), + frame_id="robot", + child_frame_id="sensor", + ) + t3 = t1 + t2 + assert t3.frame_id == "world" + assert t3.child_frame_id == "sensor" + + # Test 5: Type error + with pytest.raises(TypeError): + t1 + "not a transform" + + +def test_transform_from_pose() -> None: + """Test converting Pose to Transform""" + # Create a Pose with position and orientation + pose = Pose( + position=Vector3(1.0, 2.0, 3.0), + orientation=Quaternion(0.0, 0.0, 0.707, 0.707), # 90 degrees around Z + ) + + # Convert to Transform + transform = Transform.from_pose("base_link", pose) + + # Check that translation and rotation match + assert transform.translation == pose.position + assert transform.rotation == pose.orientation + assert transform.frame_id == "world" # default frame_id + assert transform.child_frame_id == "base_link" # passed as first argument + + +# validating results from example @ +# https://foxglove.dev/blog/understanding-ros-transforms +def test_transform_from_ros() -> None: + """Test converting PoseStamped to Transform""" + test_time = time.time() + pose_stamped = PoseStamped( + ts=test_time, + frame_id="base_link", + position=Vector3(1, -1, 0), + orientation=Quaternion.from_euler(Vector3(0, 0, math.pi / 6)), + ) + transform_base_link_to_arm = Transform.from_pose("arm_base_link", pose_stamped) + + transform_arm_to_end = Transform.from_pose( + "end", + PoseStamped( + ts=test_time, + frame_id="arm_base_link", + position=Vector3(1, 1, 0), + orientation=Quaternion.from_euler(Vector3(0, 0, math.pi / 6)), + ), + ) + + print(transform_base_link_to_arm) + print(transform_arm_to_end) + + end_effector_global_pose = transform_base_link_to_arm + transform_arm_to_end + + assert end_effector_global_pose.translation.x == pytest.approx(1.366, abs=1e-3) + assert end_effector_global_pose.translation.y == pytest.approx(0.366, abs=1e-3) + + +def test_transform_from_pose_stamped() -> None: + """Test converting PoseStamped to Transform""" + # Create a PoseStamped with position, orientation, timestamp and frame + test_time = time.time() + pose_stamped = PoseStamped( + ts=test_time, + frame_id="map", + position=Vector3(4.0, 5.0, 6.0), + orientation=Quaternion(0.0, 0.707, 0.0, 0.707), # 90 degrees around Y + ) + + # Convert to Transform + transform = Transform.from_pose("robot_base", pose_stamped) + + # Check that all fields match + assert transform.translation == pose_stamped.position + assert transform.rotation == pose_stamped.orientation + assert transform.frame_id == pose_stamped.frame_id + assert transform.ts == pose_stamped.ts + assert transform.child_frame_id == "robot_base" # passed as first argument + + +def test_transform_from_pose_variants() -> None: + """Test from_pose with different Pose initialization methods""" + # Test with Pose created from x,y,z + pose1 = Pose(1.0, 2.0, 3.0) + transform1 = Transform.from_pose("base_link", pose1) + assert transform1.translation.x == 1.0 + assert transform1.translation.y == 2.0 + assert transform1.translation.z == 3.0 + assert transform1.rotation.w == 1.0 # Identity quaternion + + # Test with Pose created from tuple + pose2 = Pose(([7.0, 8.0, 9.0], [0.0, 0.0, 0.0, 1.0])) + transform2 = Transform.from_pose("base_link", pose2) + assert transform2.translation.x == 7.0 + assert transform2.translation.y == 8.0 + assert transform2.translation.z == 9.0 + + # Test with Pose created from dict + pose3 = Pose({"position": [10.0, 11.0, 12.0], "orientation": [0.0, 0.0, 0.0, 1.0]}) + transform3 = Transform.from_pose("base_link", pose3) + assert transform3.translation.x == 10.0 + assert transform3.translation.y == 11.0 + assert transform3.translation.z == 12.0 + + +def test_transform_from_pose_invalid_type() -> None: + """Test that from_pose raises TypeError for invalid types""" + with pytest.raises(TypeError): + Transform.from_pose("not a pose") + + with pytest.raises(TypeError): + Transform.from_pose(42) + + with pytest.raises(TypeError): + Transform.from_pose(None) + + +@pytest.mark.ros +def test_transform_from_ros_transform_stamped() -> None: + """Test creating a Transform from a ROS TransformStamped message.""" + ros_msg = ROSTransformStamped() + ros_msg.header.frame_id = "world" + ros_msg.header.stamp.sec = 123 + ros_msg.header.stamp.nanosec = 456000000 + ros_msg.child_frame_id = "robot" + ros_msg.transform.translation.x = 1.0 + ros_msg.transform.translation.y = 2.0 + ros_msg.transform.translation.z = 3.0 + ros_msg.transform.rotation.x = 0.1 + ros_msg.transform.rotation.y = 0.2 + ros_msg.transform.rotation.z = 0.3 + ros_msg.transform.rotation.w = 0.9 + + transform = Transform.from_ros_transform_stamped(ros_msg) + + assert transform.frame_id == "world" + assert transform.child_frame_id == "robot" + assert transform.ts == 123.456 + assert transform.translation.x == 1.0 + assert transform.translation.y == 2.0 + assert transform.translation.z == 3.0 + assert transform.rotation.x == 0.1 + assert transform.rotation.y == 0.2 + assert transform.rotation.z == 0.3 + assert transform.rotation.w == 0.9 + + +@pytest.mark.ros +def test_transform_to_ros_transform_stamped() -> None: + """Test converting a Transform to a ROS TransformStamped message.""" + transform = Transform( + translation=Vector3(4.0, 5.0, 6.0), + rotation=Quaternion(0.15, 0.25, 0.35, 0.85), + frame_id="base_link", + child_frame_id="sensor", + ts=124.789, + ) + + ros_msg = transform.to_ros_transform_stamped() + + assert isinstance(ros_msg, ROSTransformStamped) + assert ros_msg.header.frame_id == "base_link" + assert ros_msg.child_frame_id == "sensor" + assert ros_msg.header.stamp.sec == 124 + assert ros_msg.header.stamp.nanosec == 789000000 + assert ros_msg.transform.translation.x == 4.0 + assert ros_msg.transform.translation.y == 5.0 + assert ros_msg.transform.translation.z == 6.0 + assert ros_msg.transform.rotation.x == 0.15 + assert ros_msg.transform.rotation.y == 0.25 + assert ros_msg.transform.rotation.z == 0.35 + assert ros_msg.transform.rotation.w == 0.85 + + +@pytest.mark.ros +def test_transform_ros_roundtrip() -> None: + """Test round-trip conversion between Transform and ROS TransformStamped.""" + original = Transform( + translation=Vector3(7.5, 8.5, 9.5), + rotation=Quaternion(0.0, 0.0, 0.383, 0.924), # ~45 degrees around Z + frame_id="odom", + child_frame_id="base_footprint", + ts=99.123, + ) + + ros_msg = original.to_ros_transform_stamped() + restored = Transform.from_ros_transform_stamped(ros_msg) + + assert restored.frame_id == original.frame_id + assert restored.child_frame_id == original.child_frame_id + assert restored.ts == original.ts + assert restored.translation.x == original.translation.x + assert restored.translation.y == original.translation.y + assert restored.translation.z == original.translation.z + assert restored.rotation.x == original.rotation.x + assert restored.rotation.y == original.rotation.y + assert restored.rotation.z == original.rotation.z + assert restored.rotation.w == original.rotation.w diff --git a/dimos/msgs/geometry_msgs/test_Twist.py b/dimos/msgs/geometry_msgs/test_Twist.py new file mode 100644 index 0000000000..49631a5372 --- /dev/null +++ b/dimos/msgs/geometry_msgs/test_Twist.py @@ -0,0 +1,301 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pytest + +try: + from geometry_msgs.msg import Twist as ROSTwist, Vector3 as ROSVector3 +except ImportError: + ROSTwist = None + ROSVector3 = None + +from dimos_lcm.geometry_msgs import Twist as LCMTwist + +from dimos.msgs.geometry_msgs import Quaternion, Twist, Vector3 + + +def test_twist_initialization() -> None: + # Test default initialization (zero twist) + tw = Twist() + assert tw.linear.x == 0.0 + assert tw.linear.y == 0.0 + assert tw.linear.z == 0.0 + assert tw.angular.x == 0.0 + assert tw.angular.y == 0.0 + assert tw.angular.z == 0.0 + + # Test initialization with Vector3 linear and angular + lin = Vector3(1.0, 2.0, 3.0) + ang = Vector3(0.1, 0.2, 0.3) + tw2 = Twist(lin, ang) + assert tw2.linear == lin + assert tw2.angular == ang + + # Test copy constructor + tw3 = Twist(tw2) + assert tw3.linear == tw2.linear + assert tw3.angular == tw2.angular + assert tw3 == tw2 + # Ensure it's a deep copy + tw3.linear.x = 10.0 + assert tw2.linear.x == 1.0 + + # Test initialization from LCM Twist + lcm_tw = LCMTwist() + lcm_tw.linear = Vector3(4.0, 5.0, 6.0) + lcm_tw.angular = Vector3(0.4, 0.5, 0.6) + tw4 = Twist(lcm_tw) + assert tw4.linear.x == 4.0 + assert tw4.linear.y == 5.0 + assert tw4.linear.z == 6.0 + assert tw4.angular.x == 0.4 + assert tw4.angular.y == 0.5 + assert tw4.angular.z == 0.6 + + # Test initialization with linear and angular as quaternion + quat = Quaternion(0, 0, 0.707107, 0.707107) # 90 degrees around Z + tw5 = Twist(Vector3(1.0, 2.0, 3.0), quat) + assert tw5.linear == Vector3(1.0, 2.0, 3.0) + # Quaternion should be converted to euler angles + euler = quat.to_euler() + assert np.allclose(tw5.angular.x, euler.x) + assert np.allclose(tw5.angular.y, euler.y) + assert np.allclose(tw5.angular.z, euler.z) + + # Test keyword argument initialization + tw7 = Twist(linear=Vector3(1, 2, 3), angular=Vector3(0.1, 0.2, 0.3)) + assert tw7.linear == Vector3(1, 2, 3) + assert tw7.angular == Vector3(0.1, 0.2, 0.3) + + # Test keyword with only linear + tw8 = Twist(linear=Vector3(4, 5, 6)) + assert tw8.linear == Vector3(4, 5, 6) + assert tw8.angular.is_zero() + + # Test keyword with only angular + tw9 = Twist(angular=Vector3(0.4, 0.5, 0.6)) + assert tw9.linear.is_zero() + assert tw9.angular == Vector3(0.4, 0.5, 0.6) + + # Test keyword with angular as quaternion + tw10 = Twist(angular=Quaternion(0, 0, 0.707107, 0.707107)) + assert tw10.linear.is_zero() + euler = Quaternion(0, 0, 0.707107, 0.707107).to_euler() + assert np.allclose(tw10.angular.x, euler.x) + assert np.allclose(tw10.angular.y, euler.y) + assert np.allclose(tw10.angular.z, euler.z) + + # Test keyword with linear and angular as quaternion + tw11 = Twist(linear=Vector3(1, 0, 0), angular=Quaternion(0, 0, 0, 1)) + assert tw11.linear == Vector3(1, 0, 0) + assert tw11.angular.is_zero() # Identity quaternion -> zero euler angles + + +def test_twist_zero() -> None: + # Test zero class method + tw = Twist.zero() + assert tw.linear.is_zero() + assert tw.angular.is_zero() + assert tw.is_zero() + + # Zero should equal default constructor + assert tw == Twist() + + +def test_twist_equality() -> None: + tw1 = Twist(Vector3(1, 2, 3), Vector3(0.1, 0.2, 0.3)) + tw2 = Twist(Vector3(1, 2, 3), Vector3(0.1, 0.2, 0.3)) + tw3 = Twist(Vector3(1, 2, 4), Vector3(0.1, 0.2, 0.3)) # Different linear z + tw4 = Twist(Vector3(1, 2, 3), Vector3(0.1, 0.2, 0.4)) # Different angular z + + assert tw1 == tw2 + assert tw1 != tw3 + assert tw1 != tw4 + assert tw1 != "not a twist" + + +def test_twist_string_representations() -> None: + tw = Twist(Vector3(1.5, -2.0, 3.14), Vector3(0.1, -0.2, 0.3)) + + # Test repr + repr_str = repr(tw) + assert "Twist" in repr_str + assert "linear=" in repr_str + assert "angular=" in repr_str + assert "1.5" in repr_str + assert "0.1" in repr_str + + # Test str + str_str = str(tw) + assert "Twist:" in str_str + assert "Linear:" in str_str + assert "Angular:" in str_str + + +def test_twist_is_zero() -> None: + # Test zero twist + tw1 = Twist() + assert tw1.is_zero() + + # Test non-zero linear + tw2 = Twist(linear=Vector3(0.1, 0, 0)) + assert not tw2.is_zero() + + # Test non-zero angular + tw3 = Twist(angular=Vector3(0, 0, 0.1)) + assert not tw3.is_zero() + + # Test both non-zero + tw4 = Twist(Vector3(1, 2, 3), Vector3(0.1, 0.2, 0.3)) + assert not tw4.is_zero() + + +def test_twist_bool() -> None: + # Test zero twist is False + tw1 = Twist() + assert not tw1 + + # Test non-zero twist is True + tw2 = Twist(linear=Vector3(1, 0, 0)) + assert tw2 + + tw3 = Twist(angular=Vector3(0, 0, 0.1)) + assert tw3 + + tw4 = Twist(Vector3(1, 2, 3), Vector3(0.1, 0.2, 0.3)) + assert tw4 + + +def test_twist_lcm_encoding() -> None: + # Test encoding and decoding + tw = Twist(Vector3(1.5, 2.5, 3.5), Vector3(0.1, 0.2, 0.3)) + + # Encode + encoded = tw.lcm_encode() + assert isinstance(encoded, bytes) + + # Decode + decoded = Twist.lcm_decode(encoded) + assert decoded.linear == tw.linear + assert decoded.angular == tw.angular + + assert isinstance(decoded.linear, Vector3) + assert decoded == tw + + +def test_twist_with_lists() -> None: + # Test initialization with lists instead of Vector3 + tw1 = Twist(linear=[1, 2, 3], angular=[0.1, 0.2, 0.3]) + assert tw1.linear == Vector3(1, 2, 3) + assert tw1.angular == Vector3(0.1, 0.2, 0.3) + + # Test with numpy arrays + tw2 = Twist(linear=np.array([4, 5, 6]), angular=np.array([0.4, 0.5, 0.6])) + assert tw2.linear == Vector3(4, 5, 6) + assert tw2.angular == Vector3(0.4, 0.5, 0.6) + + +@pytest.mark.ros +def test_twist_from_ros_msg() -> None: + """Test Twist.from_ros_msg conversion.""" + # Create ROS message + ros_msg = ROSTwist() + ros_msg.linear = ROSVector3(x=10.0, y=20.0, z=30.0) + ros_msg.angular = ROSVector3(x=1.0, y=2.0, z=3.0) + + # Convert to LCM + lcm_msg = Twist.from_ros_msg(ros_msg) + + assert isinstance(lcm_msg, Twist) + assert lcm_msg.linear.x == 10.0 + assert lcm_msg.linear.y == 20.0 + assert lcm_msg.linear.z == 30.0 + assert lcm_msg.angular.x == 1.0 + assert lcm_msg.angular.y == 2.0 + assert lcm_msg.angular.z == 3.0 + + +@pytest.mark.ros +def test_twist_to_ros_msg() -> None: + """Test Twist.to_ros_msg conversion.""" + # Create LCM message + lcm_msg = Twist(linear=Vector3(40.0, 50.0, 60.0), angular=Vector3(4.0, 5.0, 6.0)) + + # Convert to ROS + ros_msg = lcm_msg.to_ros_msg() + + assert isinstance(ros_msg, ROSTwist) + assert ros_msg.linear.x == 40.0 + assert ros_msg.linear.y == 50.0 + assert ros_msg.linear.z == 60.0 + assert ros_msg.angular.x == 4.0 + assert ros_msg.angular.y == 5.0 + assert ros_msg.angular.z == 6.0 + + +@pytest.mark.ros +def test_ros_zero_twist_conversion() -> None: + """Test conversion of zero twist messages between ROS and LCM.""" + # Test ROS to LCM with zero twist + ros_zero = ROSTwist() + lcm_zero = Twist.from_ros_msg(ros_zero) + assert lcm_zero.is_zero() + + # Test LCM to ROS with zero twist + lcm_zero2 = Twist.zero() + ros_zero2 = lcm_zero2.to_ros_msg() + assert ros_zero2.linear.x == 0.0 + assert ros_zero2.linear.y == 0.0 + assert ros_zero2.linear.z == 0.0 + assert ros_zero2.angular.x == 0.0 + assert ros_zero2.angular.y == 0.0 + assert ros_zero2.angular.z == 0.0 + + +@pytest.mark.ros +def test_ros_negative_values_conversion() -> None: + """Test ROS conversion with negative values.""" + # Create ROS message with negative values + ros_msg = ROSTwist() + ros_msg.linear = ROSVector3(x=-1.5, y=-2.5, z=-3.5) + ros_msg.angular = ROSVector3(x=-0.1, y=-0.2, z=-0.3) + + # Convert to LCM and back + lcm_msg = Twist.from_ros_msg(ros_msg) + ros_msg2 = lcm_msg.to_ros_msg() + + assert ros_msg2.linear.x == -1.5 + assert ros_msg2.linear.y == -2.5 + assert ros_msg2.linear.z == -3.5 + assert ros_msg2.angular.x == -0.1 + assert ros_msg2.angular.y == -0.2 + assert ros_msg2.angular.z == -0.3 + + +@pytest.mark.ros +def test_ros_roundtrip_conversion() -> None: + """Test round-trip conversion maintains data integrity.""" + # LCM -> ROS -> LCM + original_lcm = Twist(linear=Vector3(1.234, 5.678, 9.012), angular=Vector3(0.111, 0.222, 0.333)) + ros_intermediate = original_lcm.to_ros_msg() + final_lcm = Twist.from_ros_msg(ros_intermediate) + + assert final_lcm == original_lcm + assert final_lcm.linear.x == 1.234 + assert final_lcm.linear.y == 5.678 + assert final_lcm.linear.z == 9.012 + assert final_lcm.angular.x == 0.111 + assert final_lcm.angular.y == 0.222 + assert final_lcm.angular.z == 0.333 diff --git a/dimos/msgs/geometry_msgs/test_TwistStamped.py b/dimos/msgs/geometry_msgs/test_TwistStamped.py new file mode 100644 index 0000000000..385523a284 --- /dev/null +++ b/dimos/msgs/geometry_msgs/test_TwistStamped.py @@ -0,0 +1,158 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pickle +import time + +import pytest + +try: + from geometry_msgs.msg import TwistStamped as ROSTwistStamped +except ImportError: + ROSTwistStamped = None + +from dimos.msgs.geometry_msgs.TwistStamped import TwistStamped + + +def test_lcm_encode_decode() -> None: + """Test encoding and decoding of TwistStamped to/from binary LCM format.""" + twist_source = TwistStamped( + ts=time.time(), + linear=(1.0, 2.0, 3.0), + angular=(0.1, 0.2, 0.3), + ) + binary_msg = twist_source.lcm_encode() + twist_dest = TwistStamped.lcm_decode(binary_msg) + + assert isinstance(twist_dest, TwistStamped) + assert twist_dest is not twist_source + + print(twist_source.linear) + print(twist_source.angular) + + print(twist_dest.linear) + print(twist_dest.angular) + assert twist_dest == twist_source + + +def test_pickle_encode_decode() -> None: + """Test encoding and decoding of TwistStamped to/from binary pickle format.""" + + twist_source = TwistStamped( + ts=time.time(), + linear=(1.0, 2.0, 3.0), + angular=(0.1, 0.2, 0.3), + ) + binary_msg = pickle.dumps(twist_source) + twist_dest = pickle.loads(binary_msg) + assert isinstance(twist_dest, TwistStamped) + assert twist_dest is not twist_source + assert twist_dest == twist_source + + +@pytest.mark.ros +def test_twist_stamped_from_ros_msg() -> None: + """Test creating a TwistStamped from a ROS TwistStamped message.""" + ros_msg = ROSTwistStamped() + ros_msg.header.frame_id = "world" + ros_msg.header.stamp.sec = 123 + ros_msg.header.stamp.nanosec = 456000000 + ros_msg.twist.linear.x = 1.0 + ros_msg.twist.linear.y = 2.0 + ros_msg.twist.linear.z = 3.0 + ros_msg.twist.angular.x = 0.1 + ros_msg.twist.angular.y = 0.2 + ros_msg.twist.angular.z = 0.3 + + twist_stamped = TwistStamped.from_ros_msg(ros_msg) + + assert twist_stamped.frame_id == "world" + assert twist_stamped.ts == 123.456 + assert twist_stamped.linear.x == 1.0 + assert twist_stamped.linear.y == 2.0 + assert twist_stamped.linear.z == 3.0 + assert twist_stamped.angular.x == 0.1 + assert twist_stamped.angular.y == 0.2 + assert twist_stamped.angular.z == 0.3 + + +@pytest.mark.ros +def test_twist_stamped_to_ros_msg() -> None: + """Test converting a TwistStamped to a ROS TwistStamped message.""" + twist_stamped = TwistStamped( + ts=123.456, + frame_id="base_link", + linear=(1.0, 2.0, 3.0), + angular=(0.1, 0.2, 0.3), + ) + + ros_msg = twist_stamped.to_ros_msg() + + assert isinstance(ros_msg, ROSTwistStamped) + assert ros_msg.header.frame_id == "base_link" + assert ros_msg.header.stamp.sec == 123 + assert ros_msg.header.stamp.nanosec == 456000000 + assert ros_msg.twist.linear.x == 1.0 + assert ros_msg.twist.linear.y == 2.0 + assert ros_msg.twist.linear.z == 3.0 + assert ros_msg.twist.angular.x == 0.1 + assert ros_msg.twist.angular.y == 0.2 + assert ros_msg.twist.angular.z == 0.3 + + +@pytest.mark.ros +def test_twist_stamped_ros_roundtrip() -> None: + """Test round-trip conversion between TwistStamped and ROS TwistStamped.""" + original = TwistStamped( + ts=123.789, + frame_id="odom", + linear=(1.5, 2.5, 3.5), + angular=(0.15, 0.25, 0.35), + ) + + ros_msg = original.to_ros_msg() + restored = TwistStamped.from_ros_msg(ros_msg) + + assert restored.frame_id == original.frame_id + assert restored.ts == original.ts + assert restored.linear.x == original.linear.x + assert restored.linear.y == original.linear.y + assert restored.linear.z == original.linear.z + assert restored.angular.x == original.angular.x + assert restored.angular.y == original.angular.y + assert restored.angular.z == original.angular.z + + +if __name__ == "__main__": + print("Running test_lcm_encode_decode...") + test_lcm_encode_decode() + print("✓ test_lcm_encode_decode passed") + + print("Running test_pickle_encode_decode...") + test_pickle_encode_decode() + print("✓ test_pickle_encode_decode passed") + + print("Running test_twist_stamped_from_ros_msg...") + test_twist_stamped_from_ros_msg() + print("✓ test_twist_stamped_from_ros_msg passed") + + print("Running test_twist_stamped_to_ros_msg...") + test_twist_stamped_to_ros_msg() + print("✓ test_twist_stamped_to_ros_msg passed") + + print("Running test_twist_stamped_ros_roundtrip...") + test_twist_stamped_ros_roundtrip() + print("✓ test_twist_stamped_ros_roundtrip passed") + + print("\nAll tests passed!") diff --git a/dimos/msgs/geometry_msgs/test_TwistWithCovariance.py b/dimos/msgs/geometry_msgs/test_TwistWithCovariance.py new file mode 100644 index 0000000000..19b992baf4 --- /dev/null +++ b/dimos/msgs/geometry_msgs/test_TwistWithCovariance.py @@ -0,0 +1,423 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pytest + +try: + from geometry_msgs.msg import ( + Twist as ROSTwist, + TwistWithCovariance as ROSTwistWithCovariance, + Vector3 as ROSVector3, + ) +except ImportError: + ROSTwist = None + ROSTwistWithCovariance = None + ROSVector3 = None + +from dimos_lcm.geometry_msgs import TwistWithCovariance as LCMTwistWithCovariance + +from dimos.msgs.geometry_msgs.Twist import Twist +from dimos.msgs.geometry_msgs.TwistWithCovariance import TwistWithCovariance +from dimos.msgs.geometry_msgs.Vector3 import Vector3 + + +def test_twist_with_covariance_default_init() -> None: + """Test that default initialization creates a zero twist with zero covariance.""" + if ROSVector3 is None: + pytest.skip("ROS not available") + if ROSTwistWithCovariance is None: + pytest.skip("ROS not available") + twist_cov = TwistWithCovariance() + + # Twist should be zero + assert twist_cov.twist.linear.x == 0.0 + assert twist_cov.twist.linear.y == 0.0 + assert twist_cov.twist.linear.z == 0.0 + assert twist_cov.twist.angular.x == 0.0 + assert twist_cov.twist.angular.y == 0.0 + assert twist_cov.twist.angular.z == 0.0 + + # Covariance should be all zeros + assert np.all(twist_cov.covariance == 0.0) + assert twist_cov.covariance.shape == (36,) + + +def test_twist_with_covariance_twist_init() -> None: + """Test initialization with a Twist object.""" + linear = Vector3(1.0, 2.0, 3.0) + angular = Vector3(0.1, 0.2, 0.3) + twist = Twist(linear, angular) + twist_cov = TwistWithCovariance(twist) + + # Twist should match + assert twist_cov.twist.linear.x == 1.0 + assert twist_cov.twist.linear.y == 2.0 + assert twist_cov.twist.linear.z == 3.0 + assert twist_cov.twist.angular.x == 0.1 + assert twist_cov.twist.angular.y == 0.2 + assert twist_cov.twist.angular.z == 0.3 + + # Covariance should be zeros by default + assert np.all(twist_cov.covariance == 0.0) + + +def test_twist_with_covariance_twist_and_covariance_init() -> None: + """Test initialization with twist and covariance.""" + twist = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) + covariance = np.arange(36, dtype=float) + twist_cov = TwistWithCovariance(twist, covariance) + + # Twist should match + assert twist_cov.twist.linear.x == 1.0 + assert twist_cov.twist.linear.y == 2.0 + assert twist_cov.twist.linear.z == 3.0 + + # Covariance should match + assert np.array_equal(twist_cov.covariance, covariance) + + +def test_twist_with_covariance_tuple_init() -> None: + """Test initialization with tuple of (linear, angular) velocities.""" + linear = [1.0, 2.0, 3.0] + angular = [0.1, 0.2, 0.3] + covariance = np.arange(36, dtype=float) + twist_cov = TwistWithCovariance((linear, angular), covariance) + + # Twist should match + assert twist_cov.twist.linear.x == 1.0 + assert twist_cov.twist.linear.y == 2.0 + assert twist_cov.twist.linear.z == 3.0 + assert twist_cov.twist.angular.x == 0.1 + assert twist_cov.twist.angular.y == 0.2 + assert twist_cov.twist.angular.z == 0.3 + + # Covariance should match + assert np.array_equal(twist_cov.covariance, covariance) + + +def test_twist_with_covariance_list_covariance() -> None: + """Test initialization with covariance as a list.""" + twist = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) + covariance_list = list(range(36)) + twist_cov = TwistWithCovariance(twist, covariance_list) + + # Covariance should be converted to numpy array + assert isinstance(twist_cov.covariance, np.ndarray) + assert np.array_equal(twist_cov.covariance, np.array(covariance_list)) + + +def test_twist_with_covariance_copy_init() -> None: + """Test copy constructor.""" + twist = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) + covariance = np.arange(36, dtype=float) + original = TwistWithCovariance(twist, covariance) + copy = TwistWithCovariance(original) + + # Should be equal but not the same object + assert copy == original + assert copy is not original + assert copy.twist is not original.twist + assert copy.covariance is not original.covariance + + # Modify original to ensure they're independent + original.covariance[0] = 999.0 + assert copy.covariance[0] != 999.0 + + +def test_twist_with_covariance_lcm_init() -> None: + """Test initialization from LCM message.""" + lcm_msg = LCMTwistWithCovariance() + lcm_msg.twist.linear.x = 1.0 + lcm_msg.twist.linear.y = 2.0 + lcm_msg.twist.linear.z = 3.0 + lcm_msg.twist.angular.x = 0.1 + lcm_msg.twist.angular.y = 0.2 + lcm_msg.twist.angular.z = 0.3 + lcm_msg.covariance = list(range(36)) + + twist_cov = TwistWithCovariance(lcm_msg) + + # Twist should match + assert twist_cov.twist.linear.x == 1.0 + assert twist_cov.twist.linear.y == 2.0 + assert twist_cov.twist.linear.z == 3.0 + assert twist_cov.twist.angular.x == 0.1 + assert twist_cov.twist.angular.y == 0.2 + assert twist_cov.twist.angular.z == 0.3 + + # Covariance should match + assert np.array_equal(twist_cov.covariance, np.arange(36)) + + +def test_twist_with_covariance_dict_init() -> None: + """Test initialization from dictionary.""" + twist_dict = { + "twist": Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)), + "covariance": list(range(36)), + } + twist_cov = TwistWithCovariance(twist_dict) + + assert twist_cov.twist.linear.x == 1.0 + assert twist_cov.twist.linear.y == 2.0 + assert twist_cov.twist.linear.z == 3.0 + assert np.array_equal(twist_cov.covariance, np.arange(36)) + + +def test_twist_with_covariance_dict_init_no_covariance() -> None: + """Test initialization from dictionary without covariance.""" + twist_dict = {"twist": Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3))} + twist_cov = TwistWithCovariance(twist_dict) + + assert twist_cov.twist.linear.x == 1.0 + assert np.all(twist_cov.covariance == 0.0) + + +def test_twist_with_covariance_tuple_of_tuple_init() -> None: + """Test initialization from tuple of (twist_tuple, covariance).""" + twist_tuple = ([1.0, 2.0, 3.0], [0.1, 0.2, 0.3]) + covariance = np.arange(36, dtype=float) + twist_cov = TwistWithCovariance((twist_tuple, covariance)) + + assert twist_cov.twist.linear.x == 1.0 + assert twist_cov.twist.linear.y == 2.0 + assert twist_cov.twist.linear.z == 3.0 + assert twist_cov.twist.angular.x == 0.1 + assert twist_cov.twist.angular.y == 0.2 + assert twist_cov.twist.angular.z == 0.3 + assert np.array_equal(twist_cov.covariance, covariance) + + +def test_twist_with_covariance_properties() -> None: + """Test convenience properties.""" + twist = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) + twist_cov = TwistWithCovariance(twist) + + # Linear and angular properties + assert twist_cov.linear.x == 1.0 + assert twist_cov.linear.y == 2.0 + assert twist_cov.linear.z == 3.0 + assert twist_cov.angular.x == 0.1 + assert twist_cov.angular.y == 0.2 + assert twist_cov.angular.z == 0.3 + + +def test_twist_with_covariance_matrix_property() -> None: + """Test covariance matrix property.""" + twist = Twist() + covariance_array = np.arange(36, dtype=float) + twist_cov = TwistWithCovariance(twist, covariance_array) + + # Get as matrix + cov_matrix = twist_cov.covariance_matrix + assert cov_matrix.shape == (6, 6) + assert cov_matrix[0, 0] == 0.0 + assert cov_matrix[5, 5] == 35.0 + + # Set from matrix + new_matrix = np.eye(6) * 2.0 + twist_cov.covariance_matrix = new_matrix + assert np.array_equal(twist_cov.covariance[:6], [2.0, 0.0, 0.0, 0.0, 0.0, 0.0]) + + +def test_twist_with_covariance_repr() -> None: + """Test string representation.""" + twist = Twist(Vector3(1.234, 2.567, 3.891), Vector3(0.1, 0.2, 0.3)) + twist_cov = TwistWithCovariance(twist) + + repr_str = repr(twist_cov) + assert "TwistWithCovariance" in repr_str + assert "twist=" in repr_str + assert "covariance=" in repr_str + assert "36 elements" in repr_str + + +def test_twist_with_covariance_str() -> None: + """Test string formatting.""" + twist = Twist(Vector3(1.234, 2.567, 3.891), Vector3(0.1, 0.2, 0.3)) + covariance = np.eye(6).flatten() + twist_cov = TwistWithCovariance(twist, covariance) + + str_repr = str(twist_cov) + assert "TwistWithCovariance" in str_repr + assert "1.234" in str_repr + assert "2.567" in str_repr + assert "3.891" in str_repr + assert "cov_trace" in str_repr + assert "6.000" in str_repr # Trace of identity matrix is 6 + + +def test_twist_with_covariance_equality() -> None: + """Test equality comparison.""" + twist1 = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) + cov1 = np.arange(36, dtype=float) + twist_cov1 = TwistWithCovariance(twist1, cov1) + + twist2 = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) + cov2 = np.arange(36, dtype=float) + twist_cov2 = TwistWithCovariance(twist2, cov2) + + # Equal + assert twist_cov1 == twist_cov2 + + # Different twist + twist3 = Twist(Vector3(1.1, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) + twist_cov3 = TwistWithCovariance(twist3, cov1) + assert twist_cov1 != twist_cov3 + + # Different covariance + cov3 = np.arange(36, dtype=float) + 1 + twist_cov4 = TwistWithCovariance(twist1, cov3) + assert twist_cov1 != twist_cov4 + + # Different type + assert twist_cov1 != "not a twist" + assert twist_cov1 is not None + + +def test_twist_with_covariance_is_zero() -> None: + """Test is_zero method.""" + # Zero twist + twist_cov1 = TwistWithCovariance() + assert twist_cov1.is_zero() + assert not twist_cov1 # Boolean conversion + + # Non-zero twist + twist = Twist(Vector3(1.0, 0.0, 0.0), Vector3(0.0, 0.0, 0.0)) + twist_cov2 = TwistWithCovariance(twist) + assert not twist_cov2.is_zero() + assert twist_cov2 # Boolean conversion + + +def test_twist_with_covariance_lcm_encode_decode() -> None: + """Test LCM encoding and decoding.""" + twist = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) + covariance = np.arange(36, dtype=float) + source = TwistWithCovariance(twist, covariance) + + # Encode and decode + binary_msg = source.lcm_encode() + decoded = TwistWithCovariance.lcm_decode(binary_msg) + + # Should be equal + assert decoded == source + assert isinstance(decoded, TwistWithCovariance) + assert isinstance(decoded.twist, Twist) + assert isinstance(decoded.covariance, np.ndarray) + + +@pytest.mark.ros +def test_twist_with_covariance_from_ros_msg() -> None: + """Test creating from ROS message.""" + ros_msg = ROSTwistWithCovariance() + ros_msg.twist.linear = ROSVector3(x=1.0, y=2.0, z=3.0) + ros_msg.twist.angular = ROSVector3(x=0.1, y=0.2, z=0.3) + ros_msg.covariance = [float(i) for i in range(36)] + + twist_cov = TwistWithCovariance.from_ros_msg(ros_msg) + + assert twist_cov.twist.linear.x == 1.0 + assert twist_cov.twist.linear.y == 2.0 + assert twist_cov.twist.linear.z == 3.0 + assert twist_cov.twist.angular.x == 0.1 + assert twist_cov.twist.angular.y == 0.2 + assert twist_cov.twist.angular.z == 0.3 + assert np.array_equal(twist_cov.covariance, np.arange(36)) + + +@pytest.mark.ros +def test_twist_with_covariance_to_ros_msg() -> None: + """Test converting to ROS message.""" + twist = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) + covariance = np.arange(36, dtype=float) + twist_cov = TwistWithCovariance(twist, covariance) + + ros_msg = twist_cov.to_ros_msg() + + assert isinstance(ros_msg, ROSTwistWithCovariance) + assert ros_msg.twist.linear.x == 1.0 + assert ros_msg.twist.linear.y == 2.0 + assert ros_msg.twist.linear.z == 3.0 + assert ros_msg.twist.angular.x == 0.1 + assert ros_msg.twist.angular.y == 0.2 + assert ros_msg.twist.angular.z == 0.3 + assert list(ros_msg.covariance) == list(range(36)) + + +@pytest.mark.ros +def test_twist_with_covariance_ros_roundtrip() -> None: + """Test round-trip conversion with ROS messages.""" + twist = Twist(Vector3(1.5, 2.5, 3.5), Vector3(0.15, 0.25, 0.35)) + covariance = np.random.rand(36) + original = TwistWithCovariance(twist, covariance) + + ros_msg = original.to_ros_msg() + restored = TwistWithCovariance.from_ros_msg(ros_msg) + + assert restored == original + + +def test_twist_with_covariance_zero_covariance() -> None: + """Test with zero covariance matrix.""" + twist = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) + twist_cov = TwistWithCovariance(twist) + + assert np.all(twist_cov.covariance == 0.0) + assert np.trace(twist_cov.covariance_matrix) == 0.0 + + +def test_twist_with_covariance_diagonal_covariance() -> None: + """Test with diagonal covariance matrix.""" + twist = Twist() + covariance = np.zeros(36) + # Set diagonal elements + for i in range(6): + covariance[i * 6 + i] = i + 1 + + twist_cov = TwistWithCovariance(twist, covariance) + + cov_matrix = twist_cov.covariance_matrix + assert np.trace(cov_matrix) == sum(range(1, 7)) # 1+2+3+4+5+6 = 21 + + # Check diagonal elements + for i in range(6): + assert cov_matrix[i, i] == i + 1 + + # Check off-diagonal elements are zero + for i in range(6): + for j in range(6): + if i != j: + assert cov_matrix[i, j] == 0.0 + + +@pytest.mark.parametrize( + "linear,angular", + [ + ([0.0, 0.0, 0.0], [0.0, 0.0, 0.0]), + ([1.0, 2.0, 3.0], [0.1, 0.2, 0.3]), + ([-1.0, -2.0, -3.0], [-0.1, -0.2, -0.3]), + ([100.0, -100.0, 0.0], [3.14, -3.14, 0.0]), + ], +) +def test_twist_with_covariance_parametrized_velocities(linear, angular) -> None: + """Parametrized test for various velocity values.""" + twist = Twist(linear, angular) + twist_cov = TwistWithCovariance(twist) + + assert twist_cov.linear.x == linear[0] + assert twist_cov.linear.y == linear[1] + assert twist_cov.linear.z == linear[2] + assert twist_cov.angular.x == angular[0] + assert twist_cov.angular.y == angular[1] + assert twist_cov.angular.z == angular[2] diff --git a/dimos/msgs/geometry_msgs/test_TwistWithCovarianceStamped.py b/dimos/msgs/geometry_msgs/test_TwistWithCovarianceStamped.py new file mode 100644 index 0000000000..93c7a7b23f --- /dev/null +++ b/dimos/msgs/geometry_msgs/test_TwistWithCovarianceStamped.py @@ -0,0 +1,392 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +import numpy as np +import pytest + +try: + from builtin_interfaces.msg import Time as ROSTime + from geometry_msgs.msg import ( + Twist as ROSTwist, + TwistWithCovariance as ROSTwistWithCovariance, + TwistWithCovarianceStamped as ROSTwistWithCovarianceStamped, + Vector3 as ROSVector3, + ) + from std_msgs.msg import Header as ROSHeader +except ImportError: + ROSTwistWithCovarianceStamped = None + ROSTwist = None + ROSHeader = None + ROSTime = None + ROSTwistWithCovariance = None + ROSVector3 = None + + +from dimos.msgs.geometry_msgs.Twist import Twist +from dimos.msgs.geometry_msgs.TwistWithCovariance import TwistWithCovariance +from dimos.msgs.geometry_msgs.TwistWithCovarianceStamped import TwistWithCovarianceStamped +from dimos.msgs.geometry_msgs.Vector3 import Vector3 + + +def test_twist_with_covariance_stamped_default_init() -> None: + """Test default initialization.""" + if ROSVector3 is None: + pytest.skip("ROS not available") + if ROSTwistWithCovariance is None: + pytest.skip("ROS not available") + if ROSTime is None: + pytest.skip("ROS not available") + if ROSHeader is None: + pytest.skip("ROS not available") + if ROSTwist is None: + pytest.skip("ROS not available") + if ROSTwistWithCovarianceStamped is None: + pytest.skip("ROS not available") + twist_cov_stamped = TwistWithCovarianceStamped() + + # Should have current timestamp + assert twist_cov_stamped.ts > 0 + assert twist_cov_stamped.frame_id == "" + + # Twist should be zero + assert twist_cov_stamped.twist.linear.x == 0.0 + assert twist_cov_stamped.twist.linear.y == 0.0 + assert twist_cov_stamped.twist.linear.z == 0.0 + assert twist_cov_stamped.twist.angular.x == 0.0 + assert twist_cov_stamped.twist.angular.y == 0.0 + assert twist_cov_stamped.twist.angular.z == 0.0 + + # Covariance should be all zeros + assert np.all(twist_cov_stamped.covariance == 0.0) + + +def test_twist_with_covariance_stamped_with_timestamp() -> None: + """Test initialization with specific timestamp.""" + ts = 1234567890.123456 + frame_id = "base_link" + twist_cov_stamped = TwistWithCovarianceStamped(ts=ts, frame_id=frame_id) + + assert twist_cov_stamped.ts == ts + assert twist_cov_stamped.frame_id == frame_id + + +def test_twist_with_covariance_stamped_with_twist() -> None: + """Test initialization with twist.""" + ts = 1234567890.123456 + frame_id = "odom" + twist = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) + covariance = np.arange(36, dtype=float) + + twist_cov_stamped = TwistWithCovarianceStamped( + ts=ts, frame_id=frame_id, twist=twist, covariance=covariance + ) + + assert twist_cov_stamped.ts == ts + assert twist_cov_stamped.frame_id == frame_id + assert twist_cov_stamped.twist.linear.x == 1.0 + assert twist_cov_stamped.twist.linear.y == 2.0 + assert twist_cov_stamped.twist.linear.z == 3.0 + assert np.array_equal(twist_cov_stamped.covariance, covariance) + + +def test_twist_with_covariance_stamped_with_tuple() -> None: + """Test initialization with tuple of velocities.""" + ts = 1234567890.123456 + frame_id = "robot_base" + linear = [1.0, 2.0, 3.0] + angular = [0.1, 0.2, 0.3] + covariance = np.arange(36, dtype=float) + + twist_cov_stamped = TwistWithCovarianceStamped( + ts=ts, frame_id=frame_id, twist=(linear, angular), covariance=covariance + ) + + assert twist_cov_stamped.ts == ts + assert twist_cov_stamped.frame_id == frame_id + assert twist_cov_stamped.twist.linear.x == 1.0 + assert twist_cov_stamped.twist.angular.x == 0.1 + assert np.array_equal(twist_cov_stamped.covariance, covariance) + + +def test_twist_with_covariance_stamped_properties() -> None: + """Test convenience properties.""" + twist = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) + covariance = np.eye(6).flatten() + twist_cov_stamped = TwistWithCovarianceStamped( + ts=1234567890.0, frame_id="cmd_vel", twist=twist, covariance=covariance + ) + + # Linear and angular properties + assert twist_cov_stamped.linear.x == 1.0 + assert twist_cov_stamped.linear.y == 2.0 + assert twist_cov_stamped.linear.z == 3.0 + assert twist_cov_stamped.angular.x == 0.1 + assert twist_cov_stamped.angular.y == 0.2 + assert twist_cov_stamped.angular.z == 0.3 + + # Covariance matrix + cov_matrix = twist_cov_stamped.covariance_matrix + assert cov_matrix.shape == (6, 6) + assert np.trace(cov_matrix) == 6.0 + + +def test_twist_with_covariance_stamped_str() -> None: + """Test string representation.""" + twist = Twist(Vector3(1.234, 2.567, 3.891), Vector3(0.111, 0.222, 0.333)) + covariance = np.eye(6).flatten() * 2.0 + twist_cov_stamped = TwistWithCovarianceStamped( + ts=1234567890.0, frame_id="world", twist=twist, covariance=covariance + ) + + str_repr = str(twist_cov_stamped) + assert "TwistWithCovarianceStamped" in str_repr + assert "1.234" in str_repr + assert "2.567" in str_repr + assert "3.891" in str_repr + assert "cov_trace" in str_repr + assert "12.000" in str_repr # Trace of 2*identity is 12 + + +def test_twist_with_covariance_stamped_lcm_encode_decode() -> None: + """Test LCM encoding and decoding.""" + ts = 1234567890.123456 + frame_id = "camera_link" + twist = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) + covariance = np.arange(36, dtype=float) + + source = TwistWithCovarianceStamped( + ts=ts, frame_id=frame_id, twist=twist, covariance=covariance + ) + + # Encode and decode + binary_msg = source.lcm_encode() + decoded = TwistWithCovarianceStamped.lcm_decode(binary_msg) + + # Check timestamp (may lose some precision) + assert abs(decoded.ts - ts) < 1e-6 + assert decoded.frame_id == frame_id + + # Check twist + assert decoded.twist.linear.x == 1.0 + assert decoded.twist.linear.y == 2.0 + assert decoded.twist.linear.z == 3.0 + assert decoded.twist.angular.x == 0.1 + assert decoded.twist.angular.y == 0.2 + assert decoded.twist.angular.z == 0.3 + + # Check covariance + assert np.array_equal(decoded.covariance, covariance) + + +@pytest.mark.ros +def test_twist_with_covariance_stamped_from_ros_msg() -> None: + """Test creating from ROS message.""" + ros_msg = ROSTwistWithCovarianceStamped() + + # Set header + ros_msg.header = ROSHeader() + ros_msg.header.stamp = ROSTime() + ros_msg.header.stamp.sec = 1234567890 + ros_msg.header.stamp.nanosec = 123456000 + ros_msg.header.frame_id = "laser" + + # Set twist with covariance + ros_msg.twist = ROSTwistWithCovariance() + ros_msg.twist.twist = ROSTwist() + ros_msg.twist.twist.linear = ROSVector3(x=1.0, y=2.0, z=3.0) + ros_msg.twist.twist.angular = ROSVector3(x=0.1, y=0.2, z=0.3) + ros_msg.twist.covariance = [float(i) for i in range(36)] + + twist_cov_stamped = TwistWithCovarianceStamped.from_ros_msg(ros_msg) + + assert twist_cov_stamped.ts == 1234567890.123456 + assert twist_cov_stamped.frame_id == "laser" + assert twist_cov_stamped.twist.linear.x == 1.0 + assert twist_cov_stamped.twist.linear.y == 2.0 + assert twist_cov_stamped.twist.linear.z == 3.0 + assert twist_cov_stamped.twist.angular.x == 0.1 + assert twist_cov_stamped.twist.angular.y == 0.2 + assert twist_cov_stamped.twist.angular.z == 0.3 + assert np.array_equal(twist_cov_stamped.covariance, np.arange(36)) + + +@pytest.mark.ros +def test_twist_with_covariance_stamped_to_ros_msg() -> None: + """Test converting to ROS message.""" + ts = 1234567890.567890 + frame_id = "imu" + twist = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) + covariance = np.arange(36, dtype=float) + + twist_cov_stamped = TwistWithCovarianceStamped( + ts=ts, frame_id=frame_id, twist=twist, covariance=covariance + ) + + ros_msg = twist_cov_stamped.to_ros_msg() + + assert isinstance(ros_msg, ROSTwistWithCovarianceStamped) + assert ros_msg.header.frame_id == frame_id + assert ros_msg.header.stamp.sec == 1234567890 + assert abs(ros_msg.header.stamp.nanosec - 567890000) < 100 # Allow small rounding error + + assert ros_msg.twist.twist.linear.x == 1.0 + assert ros_msg.twist.twist.linear.y == 2.0 + assert ros_msg.twist.twist.linear.z == 3.0 + assert ros_msg.twist.twist.angular.x == 0.1 + assert ros_msg.twist.twist.angular.y == 0.2 + assert ros_msg.twist.twist.angular.z == 0.3 + assert list(ros_msg.twist.covariance) == list(range(36)) + + +@pytest.mark.ros +def test_twist_with_covariance_stamped_ros_roundtrip() -> None: + """Test round-trip conversion with ROS messages.""" + ts = 2147483647.987654 # Max int32 value for ROS Time.sec + frame_id = "robot_base" + twist = Twist(Vector3(1.5, 2.5, 3.5), Vector3(0.15, 0.25, 0.35)) + covariance = np.random.rand(36) + + original = TwistWithCovarianceStamped( + ts=ts, frame_id=frame_id, twist=twist, covariance=covariance + ) + + ros_msg = original.to_ros_msg() + restored = TwistWithCovarianceStamped.from_ros_msg(ros_msg) + + # Check timestamp (loses some precision in conversion) + assert abs(restored.ts - ts) < 1e-6 + assert restored.frame_id == frame_id + + # Check twist + assert restored.twist.linear.x == original.twist.linear.x + assert restored.twist.linear.y == original.twist.linear.y + assert restored.twist.linear.z == original.twist.linear.z + assert restored.twist.angular.x == original.twist.angular.x + assert restored.twist.angular.y == original.twist.angular.y + assert restored.twist.angular.z == original.twist.angular.z + + # Check covariance + assert np.allclose(restored.covariance, original.covariance) + + +def test_twist_with_covariance_stamped_zero_timestamp() -> None: + """Test that zero timestamp gets replaced with current time.""" + twist_cov_stamped = TwistWithCovarianceStamped(ts=0.0) + + # Should have been replaced with current time + assert twist_cov_stamped.ts > 0 + assert twist_cov_stamped.ts <= time.time() + + +def test_twist_with_covariance_stamped_inheritance() -> None: + """Test that it properly inherits from TwistWithCovariance and Timestamped.""" + twist = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) + covariance = np.eye(6).flatten() + twist_cov_stamped = TwistWithCovarianceStamped( + ts=1234567890.0, frame_id="test", twist=twist, covariance=covariance + ) + + # Should be instance of parent classes + assert isinstance(twist_cov_stamped, TwistWithCovariance) + + # Should have Timestamped attributes + assert hasattr(twist_cov_stamped, "ts") + assert hasattr(twist_cov_stamped, "frame_id") + + # Should have TwistWithCovariance attributes + assert hasattr(twist_cov_stamped, "twist") + assert hasattr(twist_cov_stamped, "covariance") + + +def test_twist_with_covariance_stamped_is_zero() -> None: + """Test is_zero method inheritance.""" + # Zero twist + twist_cov_stamped1 = TwistWithCovarianceStamped() + assert twist_cov_stamped1.is_zero() + assert not twist_cov_stamped1 # Boolean conversion + + # Non-zero twist + twist = Twist(Vector3(0.5, 0.0, 0.0), Vector3(0.0, 0.0, 0.0)) + twist_cov_stamped2 = TwistWithCovarianceStamped(twist=twist) + assert not twist_cov_stamped2.is_zero() + assert twist_cov_stamped2 # Boolean conversion + + +def test_twist_with_covariance_stamped_sec_nsec() -> None: + """Test the sec_nsec helper function.""" + from dimos.msgs.geometry_msgs.TwistWithCovarianceStamped import sec_nsec + + # Test integer seconds + s, ns = sec_nsec(1234567890.0) + assert s == 1234567890 + assert ns == 0 + + # Test fractional seconds + s, ns = sec_nsec(1234567890.123456789) + assert s == 1234567890 + assert abs(ns - 123456789) < 100 # Allow small rounding error + + # Test small fractional seconds + s, ns = sec_nsec(0.000000001) + assert s == 0 + assert ns == 1 + + # Test large timestamp + s, ns = sec_nsec(9999999999.999999999) + # Due to floating point precision, this might round to 10000000000 + assert s in [9999999999, 10000000000] + if s == 9999999999: + assert abs(ns - 999999999) < 10 + else: + assert ns == 0 + + +@pytest.mark.ros +@pytest.mark.parametrize( + "frame_id", + ["", "map", "odom", "base_link", "cmd_vel", "sensor/velocity/front"], +) +def test_twist_with_covariance_stamped_frame_ids(frame_id) -> None: + """Test various frame ID values.""" + twist_cov_stamped = TwistWithCovarianceStamped(frame_id=frame_id) + assert twist_cov_stamped.frame_id == frame_id + + # Test roundtrip through ROS + ros_msg = twist_cov_stamped.to_ros_msg() + assert ros_msg.header.frame_id == frame_id + + restored = TwistWithCovarianceStamped.from_ros_msg(ros_msg) + assert restored.frame_id == frame_id + + +def test_twist_with_covariance_stamped_different_covariances() -> None: + """Test with different covariance patterns.""" + twist = Twist(Vector3(1.0, 0.0, 0.0), Vector3(0.0, 0.0, 0.5)) + + # Zero covariance + zero_cov = np.zeros(36) + twist_cov1 = TwistWithCovarianceStamped(twist=twist, covariance=zero_cov) + assert np.all(twist_cov1.covariance == 0.0) + + # Identity covariance + identity_cov = np.eye(6).flatten() + twist_cov2 = TwistWithCovarianceStamped(twist=twist, covariance=identity_cov) + assert np.trace(twist_cov2.covariance_matrix) == 6.0 + + # Full covariance + full_cov = np.random.rand(36) + twist_cov3 = TwistWithCovarianceStamped(twist=twist, covariance=full_cov) + assert np.array_equal(twist_cov3.covariance, full_cov) diff --git a/dimos/msgs/geometry_msgs/test_Vector3.py b/dimos/msgs/geometry_msgs/test_Vector3.py new file mode 100644 index 0000000000..7ad4e67f16 --- /dev/null +++ b/dimos/msgs/geometry_msgs/test_Vector3.py @@ -0,0 +1,462 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pytest + +from dimos.msgs.geometry_msgs.Vector3 import Vector3 + + +def test_vector_default_init() -> None: + """Test that default initialization of Vector() has x,y,z components all zero.""" + v = Vector3() + assert v.x == 0.0 + assert v.y == 0.0 + assert v.z == 0.0 + assert len(v.data) == 3 + assert v.to_list() == [0.0, 0.0, 0.0] + assert v.is_zero() # Zero vector should be considered zero + + +def test_vector_specific_init() -> None: + """Test initialization with specific values and different input types.""" + + v1 = Vector3(1.0, 2.0) # 2D vector (now becomes 3D with z=0) + assert v1.x == 1.0 + assert v1.y == 2.0 + assert v1.z == 0.0 + + v2 = Vector3(3.0, 4.0, 5.0) # 3D vector + assert v2.x == 3.0 + assert v2.y == 4.0 + assert v2.z == 5.0 + + v3 = Vector3([6.0, 7.0, 8.0]) + assert v3.x == 6.0 + assert v3.y == 7.0 + assert v3.z == 8.0 + + v4 = Vector3((9.0, 10.0, 11.0)) + assert v4.x == 9.0 + assert v4.y == 10.0 + assert v4.z == 11.0 + + v5 = Vector3(np.array([12.0, 13.0, 14.0])) + assert v5.x == 12.0 + assert v5.y == 13.0 + assert v5.z == 14.0 + + original = Vector3([15.0, 16.0, 17.0]) + v6 = Vector3(original) + assert v6.x == 15.0 + assert v6.y == 16.0 + assert v6.z == 17.0 + + assert v6 is not original + assert v6 == original + + +def test_vector_addition() -> None: + """Test vector addition.""" + v1 = Vector3(1.0, 2.0, 3.0) + v2 = Vector3(4.0, 5.0, 6.0) + + v_add = v1 + v2 + assert v_add.x == 5.0 + assert v_add.y == 7.0 + assert v_add.z == 9.0 + + +def test_vector_subtraction() -> None: + """Test vector subtraction.""" + v1 = Vector3(1.0, 2.0, 3.0) + v2 = Vector3(4.0, 5.0, 6.0) + + v_sub = v2 - v1 + assert v_sub.x == 3.0 + assert v_sub.y == 3.0 + assert v_sub.z == 3.0 + + +def test_vector_scalar_multiplication() -> None: + """Test vector multiplication by a scalar.""" + v1 = Vector3(1.0, 2.0, 3.0) + + v_mul = v1 * 2.0 + assert v_mul.x == 2.0 + assert v_mul.y == 4.0 + assert v_mul.z == 6.0 + + # Test right multiplication + v_rmul = 2.0 * v1 + assert v_rmul.x == 2.0 + assert v_rmul.y == 4.0 + assert v_rmul.z == 6.0 + + +def test_vector_scalar_division() -> None: + """Test vector division by a scalar.""" + v2 = Vector3(4.0, 5.0, 6.0) + + v_div = v2 / 2.0 + assert v_div.x == 2.0 + assert v_div.y == 2.5 + assert v_div.z == 3.0 + + +def test_vector_dot_product() -> None: + """Test vector dot product.""" + v1 = Vector3(1.0, 2.0, 3.0) + v2 = Vector3(4.0, 5.0, 6.0) + + dot = v1.dot(v2) + assert dot == 32.0 + + +def test_vector_length() -> None: + """Test vector length calculation.""" + # 2D vector with length 5 (now 3D with z=0) + v1 = Vector3(3.0, 4.0) + assert v1.length() == 5.0 + + # 3D vector + v2 = Vector3(2.0, 3.0, 6.0) + assert v2.length() == pytest.approx(7.0, 0.001) + + # Test length_squared + assert v1.length_squared() == 25.0 + assert v2.length_squared() == 49.0 + + +def test_vector_normalize() -> None: + """Test vector normalization.""" + v = Vector3(2.0, 3.0, 6.0) + assert not v.is_zero() + + v_norm = v.normalize() + length = v.length() + expected_x = 2.0 / length + expected_y = 3.0 / length + expected_z = 6.0 / length + + assert np.isclose(v_norm.x, expected_x) + assert np.isclose(v_norm.y, expected_y) + assert np.isclose(v_norm.z, expected_z) + assert np.isclose(v_norm.length(), 1.0) + assert not v_norm.is_zero() + + # Test normalizing a zero vector + v_zero = Vector3(0.0, 0.0, 0.0) + assert v_zero.is_zero() + v_zero_norm = v_zero.normalize() + assert v_zero_norm.x == 0.0 + assert v_zero_norm.y == 0.0 + assert v_zero_norm.z == 0.0 + assert v_zero_norm.is_zero() + + +def test_vector_to_2d() -> None: + """Test conversion to 2D vector.""" + v = Vector3(2.0, 3.0, 6.0) + + v_2d = v.to_2d() + assert v_2d.x == 2.0 + assert v_2d.y == 3.0 + assert v_2d.z == 0.0 # z should be 0 for 2D conversion + + # Already 2D vector (z=0) + v2 = Vector3(4.0, 5.0) + v2_2d = v2.to_2d() + assert v2_2d.x == 4.0 + assert v2_2d.y == 5.0 + assert v2_2d.z == 0.0 + + +def test_vector_distance() -> None: + """Test distance calculations between vectors.""" + v1 = Vector3(1.0, 2.0, 3.0) + v2 = Vector3(4.0, 6.0, 8.0) + + # Distance + dist = v1.distance(v2) + expected_dist = np.sqrt(9.0 + 16.0 + 25.0) # sqrt((4-1)² + (6-2)² + (8-3)²) + assert dist == pytest.approx(expected_dist) + + # Distance squared + dist_sq = v1.distance_squared(v2) + assert dist_sq == 50.0 # 9 + 16 + 25 + + +def test_vector_cross_product() -> None: + """Test vector cross product.""" + v1 = Vector3(1.0, 0.0, 0.0) # Unit x vector + v2 = Vector3(0.0, 1.0, 0.0) # Unit y vector + + # v1 × v2 should be unit z vector + cross = v1.cross(v2) + assert cross.x == 0.0 + assert cross.y == 0.0 + assert cross.z == 1.0 + + # Test with more complex vectors + a = Vector3(2.0, 3.0, 4.0) + b = Vector3(5.0, 6.0, 7.0) + c = a.cross(b) + + # Cross product manually calculated: + # (3*7-4*6, 4*5-2*7, 2*6-3*5) + assert c.x == -3.0 + assert c.y == 6.0 + assert c.z == -3.0 + + # Test with vectors that have z=0 (still works as they're 3D) + v_2d1 = Vector3(1.0, 2.0) # (1, 2, 0) + v_2d2 = Vector3(3.0, 4.0) # (3, 4, 0) + cross_2d = v_2d1.cross(v_2d2) + # (2*0-0*4, 0*3-1*0, 1*4-2*3) = (0, 0, -2) + assert cross_2d.x == 0.0 + assert cross_2d.y == 0.0 + assert cross_2d.z == -2.0 + + +def test_vector_zeros() -> None: + """Test Vector3.zeros class method.""" + # 3D zero vector + v_zeros = Vector3.zeros() + assert v_zeros.x == 0.0 + assert v_zeros.y == 0.0 + assert v_zeros.z == 0.0 + assert v_zeros.is_zero() + + +def test_vector_ones() -> None: + """Test Vector3.ones class method.""" + # 3D ones vector + v_ones = Vector3.ones() + assert v_ones.x == 1.0 + assert v_ones.y == 1.0 + assert v_ones.z == 1.0 + + +def test_vector_conversion_methods() -> None: + """Test vector conversion methods (to_list, to_tuple, to_numpy).""" + v = Vector3(1.0, 2.0, 3.0) + + # to_list + assert v.to_list() == [1.0, 2.0, 3.0] + + # to_tuple + assert v.to_tuple() == (1.0, 2.0, 3.0) + + # to_numpy + np_array = v.to_numpy() + assert isinstance(np_array, np.ndarray) + assert np.array_equal(np_array, np.array([1.0, 2.0, 3.0])) + + +def test_vector_equality() -> None: + """Test vector equality.""" + v1 = Vector3(1, 2, 3) + v2 = Vector3(1, 2, 3) + v3 = Vector3(4, 5, 6) + + assert v1 == v2 + assert v1 != v3 + assert v1 != Vector3(1, 2) # Now (1, 2, 0) vs (1, 2, 3) + assert v1 != Vector3(1.1, 2, 3) # Different values + assert v1 != [1, 2, 3] + + +def test_vector_is_zero() -> None: + """Test is_zero method for vectors.""" + # Default zero vector + v0 = Vector3() + assert v0.is_zero() + + # Explicit zero vector + v1 = Vector3(0.0, 0.0, 0.0) + assert v1.is_zero() + + # Zero vector with different initialization (now always 3D) + v2 = Vector3(0.0, 0.0) # Becomes (0, 0, 0) + assert v2.is_zero() + + # Non-zero vectors + v3 = Vector3(1.0, 0.0, 0.0) + assert not v3.is_zero() + + v4 = Vector3(0.0, 2.0, 0.0) + assert not v4.is_zero() + + v5 = Vector3(0.0, 0.0, 3.0) + assert not v5.is_zero() + + # Almost zero (within tolerance) + v6 = Vector3(1e-10, 1e-10, 1e-10) + assert v6.is_zero() + + # Almost zero (outside tolerance) + v7 = Vector3(1e-6, 1e-6, 1e-6) + assert not v7.is_zero() + + +def test_vector_bool_conversion(): + """Test boolean conversion of vectors.""" + # Zero vectors should be False + v0 = Vector3() + assert not bool(v0) + + v1 = Vector3(0.0, 0.0, 0.0) + assert not bool(v1) + + # Almost zero vectors should be False + v2 = Vector3(1e-10, 1e-10, 1e-10) + assert not bool(v2) + + # Non-zero vectors should be True + v3 = Vector3(1.0, 0.0, 0.0) + assert bool(v3) + + v4 = Vector3(0.0, 2.0, 0.0) + assert bool(v4) + + v5 = Vector3(0.0, 0.0, 3.0) + assert bool(v5) + + # Direct use in if statements + if v0: + raise AssertionError("Zero vector should be False in boolean context") + else: + pass # Expected path + + if v3: + pass # Expected path + else: + raise AssertionError("Non-zero vector should be True in boolean context") + + +def test_vector_add() -> None: + """Test vector addition operator.""" + v1 = Vector3(1.0, 2.0, 3.0) + v2 = Vector3(4.0, 5.0, 6.0) + + # Using __add__ method + v_add = v1.__add__(v2) + assert v_add.x == 5.0 + assert v_add.y == 7.0 + assert v_add.z == 9.0 + + # Using + operator + v_add_op = v1 + v2 + assert v_add_op.x == 5.0 + assert v_add_op.y == 7.0 + assert v_add_op.z == 9.0 + + # Adding zero vector should return original vector + v_zero = Vector3.zeros() + assert (v1 + v_zero) == v1 + + +def test_vector_add_dim_mismatch() -> None: + """Test vector addition with different input dimensions (now all vectors are 3D).""" + v1 = Vector3(1.0, 2.0) # Becomes (1, 2, 0) + v2 = Vector3(4.0, 5.0, 6.0) # (4, 5, 6) + + # Using + operator - should work fine now since both are 3D + v_add_op = v1 + v2 + assert v_add_op.x == 5.0 # 1 + 4 + assert v_add_op.y == 7.0 # 2 + 5 + assert v_add_op.z == 6.0 # 0 + 6 + + +def test_yaw_pitch_roll_accessors() -> None: + """Test yaw, pitch, and roll accessor properties.""" + # Test with a 3D vector + v = Vector3(1.0, 2.0, 3.0) + + # According to standard convention: + # roll = rotation around x-axis = x component + # pitch = rotation around y-axis = y component + # yaw = rotation around z-axis = z component + assert v.roll == 1.0 # Should return x component + assert v.pitch == 2.0 # Should return y component + assert v.yaw == 3.0 # Should return z component + + # Test with a 2D vector (z should be 0.0) + v_2d = Vector3(4.0, 5.0) + assert v_2d.roll == 4.0 # Should return x component + assert v_2d.pitch == 5.0 # Should return y component + assert v_2d.yaw == 0.0 # Should return z component (defaults to 0 for 2D) + + # Test with empty vector (all should be 0.0) + v_empty = Vector3() + assert v_empty.roll == 0.0 + assert v_empty.pitch == 0.0 + assert v_empty.yaw == 0.0 + + # Test with negative values + v_neg = Vector3(-1.5, -2.5, -3.5) + assert v_neg.roll == -1.5 + assert v_neg.pitch == -2.5 + assert v_neg.yaw == -3.5 + + +def test_vector_to_quaternion() -> None: + """Test vector to quaternion conversion.""" + # Test with zero Euler angles (should produce identity quaternion) + v_zero = Vector3(0.0, 0.0, 0.0) + q_identity = v_zero.to_quaternion() + + # Identity quaternion should have w=1, x=y=z=0 + assert np.isclose(q_identity.x, 0.0, atol=1e-10) + assert np.isclose(q_identity.y, 0.0, atol=1e-10) + assert np.isclose(q_identity.z, 0.0, atol=1e-10) + assert np.isclose(q_identity.w, 1.0, atol=1e-10) + + # Test with small angles (to avoid gimbal lock issues) + v_small = Vector3(0.1, 0.2, 0.3) # Small roll, pitch, yaw + q_small = v_small.to_quaternion() + + # Quaternion should be normalized (magnitude = 1) + magnitude = np.sqrt(q_small.x**2 + q_small.y**2 + q_small.z**2 + q_small.w**2) + assert np.isclose(magnitude, 1.0, atol=1e-10) + + # Test conversion back to Euler (should be close to original) + v_back = q_small.to_euler() + assert np.isclose(v_back.x, 0.1, atol=1e-6) + assert np.isclose(v_back.y, 0.2, atol=1e-6) + assert np.isclose(v_back.z, 0.3, atol=1e-6) + + # Test with π/2 rotation around x-axis + v_x_90 = Vector3(np.pi / 2, 0.0, 0.0) + q_x_90 = v_x_90.to_quaternion() + + # Should be approximately (sin(π/4), 0, 0, cos(π/4)) = (√2/2, 0, 0, √2/2) + expected = np.sqrt(2) / 2 + assert np.isclose(q_x_90.x, expected, atol=1e-10) + assert np.isclose(q_x_90.y, 0.0, atol=1e-10) + assert np.isclose(q_x_90.z, 0.0, atol=1e-10) + assert np.isclose(q_x_90.w, expected, atol=1e-10) + + +def test_lcm_encode_decode() -> None: + v_source = Vector3(1.0, 2.0, 3.0) + + binary_msg = v_source.lcm_encode() + + v_dest = Vector3.lcm_decode(binary_msg) + + assert isinstance(v_dest, Vector3) + assert v_dest is not v_source + assert v_dest == v_source diff --git a/dimos/msgs/geometry_msgs/test_publish.py b/dimos/msgs/geometry_msgs/test_publish.py new file mode 100644 index 0000000000..50578346ae --- /dev/null +++ b/dimos/msgs/geometry_msgs/test_publish.py @@ -0,0 +1,54 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +import lcm +import pytest + +from dimos.msgs.geometry_msgs import Vector3 + + +@pytest.mark.tool +def test_runpublish() -> None: + for i in range(10): + msg = Vector3(-5 + i, -5 + i, i) + lc = lcm.LCM() + lc.publish("thing1_vector3#geometry_msgs.Vector3", msg.encode()) + time.sleep(0.1) + print(f"Published: {msg}") + + +@pytest.mark.tool +def test_receive() -> None: + lc = lcm.LCM() + + def receive(bla, msg) -> None: + # print("receive", bla, msg) + print(Vector3.decode(msg)) + + lc.subscribe("thing1_vector3#geometry_msgs.Vector3", receive) + + def _loop() -> None: + while True: + """LCM message handling loop""" + try: + lc.handle() + # loop 10000 times + for _ in range(10000000): + 3 + 3 + except Exception as e: + print(f"Error in LCM handling: {e}") + + _loop() diff --git a/dimos/msgs/nav_msgs/OccupancyGrid.py b/dimos/msgs/nav_msgs/OccupancyGrid.py new file mode 100644 index 0000000000..3e144de74f --- /dev/null +++ b/dimos/msgs/nav_msgs/OccupancyGrid.py @@ -0,0 +1,608 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from enum import IntEnum +import time +from typing import TYPE_CHECKING, BinaryIO + +from dimos_lcm.nav_msgs import MapMetaData, OccupancyGrid as LCMOccupancyGrid +from dimos_lcm.std_msgs import Time as LCMTime +import numpy as np +from scipy import ndimage + +from dimos.msgs.geometry_msgs import Pose, Vector3, VectorLike +from dimos.types.timestamped import Timestamped + +if TYPE_CHECKING: + from dimos.msgs.sensor_msgs import PointCloud2 + + +class CostValues(IntEnum): + """Standard cost values for occupancy grid cells. + + These values follow the ROS nav_msgs/OccupancyGrid convention: + - 0: Free space + - 1-99: Occupied space with varying cost levels + - 100: Lethal obstacle (definitely occupied) + - -1: Unknown space + """ + + UNKNOWN = -1 # Unknown space + FREE = 0 # Free space + OCCUPIED = 100 # Occupied/lethal space + + +class OccupancyGrid(Timestamped): + """ + Convenience wrapper for nav_msgs/OccupancyGrid with numpy array support. + """ + + msg_name = "nav_msgs.OccupancyGrid" + + # Attributes + ts: float + frame_id: str + info: MapMetaData + grid: np.ndarray + + def __init__( + self, + grid: np.ndarray | None = None, + width: int | None = None, + height: int | None = None, + resolution: float = 0.05, + origin: Pose | None = None, + frame_id: str = "world", + ts: float = 0.0, + ) -> None: + """Initialize OccupancyGrid. + + Args: + grid: 2D numpy array of int8 values (height x width) + width: Width in cells (used if grid is None) + height: Height in cells (used if grid is None) + resolution: Grid resolution in meters/cell + origin: Origin pose of the grid + frame_id: Reference frame + ts: Timestamp (defaults to current time if 0) + """ + + self.frame_id = frame_id + self.ts = ts if ts != 0 else time.time() + + if grid is not None: + # Initialize from numpy array + if grid.ndim != 2: + raise ValueError("Grid must be a 2D array") + height, width = grid.shape + self.info = MapMetaData( + map_load_time=self._to_lcm_time(), + resolution=resolution, + width=width, + height=height, + origin=origin or Pose(), + ) + self.grid = grid.astype(np.int8) + elif width is not None and height is not None: + # Initialize with dimensions + self.info = MapMetaData( + map_load_time=self._to_lcm_time(), + resolution=resolution, + width=width, + height=height, + origin=origin or Pose(), + ) + self.grid = np.full((height, width), -1, dtype=np.int8) + else: + # Initialize empty + self.info = MapMetaData(map_load_time=self._to_lcm_time()) + self.grid = np.array([], dtype=np.int8) + + def _to_lcm_time(self): + """Convert timestamp to LCM Time.""" + + s = int(self.ts) + return LCMTime(sec=s, nsec=int((self.ts - s) * 1_000_000_000)) + + @property + def width(self) -> int: + """Width of the grid in cells.""" + return self.info.width + + @property + def height(self) -> int: + """Height of the grid in cells.""" + return self.info.height + + @property + def resolution(self) -> float: + """Grid resolution in meters/cell.""" + return self.info.resolution + + @property + def origin(self) -> Pose: + """Origin pose of the grid.""" + return self.info.origin + + @property + def total_cells(self) -> int: + """Total number of cells in the grid.""" + return self.width * self.height + + @property + def occupied_cells(self) -> int: + """Number of occupied cells (value >= 1).""" + return int(np.sum(self.grid >= 1)) + + @property + def free_cells(self) -> int: + """Number of free cells (value == 0).""" + return int(np.sum(self.grid == 0)) + + @property + def unknown_cells(self) -> int: + """Number of unknown cells (value == -1).""" + return int(np.sum(self.grid == -1)) + + @property + def occupied_percent(self) -> float: + """Percentage of cells that are occupied.""" + return (self.occupied_cells / self.total_cells * 100) if self.total_cells > 0 else 0.0 + + @property + def free_percent(self) -> float: + """Percentage of cells that are free.""" + return (self.free_cells / self.total_cells * 100) if self.total_cells > 0 else 0.0 + + @property + def unknown_percent(self) -> float: + """Percentage of cells that are unknown.""" + return (self.unknown_cells / self.total_cells * 100) if self.total_cells > 0 else 0.0 + + def inflate(self, radius: float) -> OccupancyGrid: + """Inflate obstacles by a given radius (binary inflation). + Args: + radius: Inflation radius in meters + Returns: + New OccupancyGrid with inflated obstacles + """ + # Convert radius to grid cells + cell_radius = int(np.ceil(radius / self.resolution)) + + # Get grid as numpy array + grid_array = self.grid + + # Create circular kernel for binary inflation + 2 * cell_radius + 1 + y, x = np.ogrid[-cell_radius : cell_radius + 1, -cell_radius : cell_radius + 1] + kernel = (x**2 + y**2 <= cell_radius**2).astype(np.uint8) + + # Find occupied cells + occupied_mask = grid_array >= CostValues.OCCUPIED + + # Binary inflation + inflated = ndimage.binary_dilation(occupied_mask, structure=kernel) + result_grid = grid_array.copy() + result_grid[inflated] = CostValues.OCCUPIED + + # Create new OccupancyGrid with inflated data using numpy constructor + return OccupancyGrid( + grid=result_grid, + resolution=self.resolution, + origin=self.origin, + frame_id=self.frame_id, + ts=self.ts, + ) + + def world_to_grid(self, point: VectorLike) -> Vector3: + """Convert world coordinates to grid coordinates. + + Args: + point: A vector-like object containing X,Y coordinates + + Returns: + Vector3 with grid coordinates + """ + positionVector = Vector3(point) + # Get origin position + ox = self.origin.position.x + oy = self.origin.position.y + + # Convert to grid coordinates (simplified, assuming no rotation) + grid_x = (positionVector.x - ox) / self.resolution + grid_y = (positionVector.y - oy) / self.resolution + + return Vector3(grid_x, grid_y, 0.0) + + def grid_to_world(self, grid_point: VectorLike) -> Vector3: + """Convert grid coordinates to world coordinates. + + Args: + grid_point: Vector-like object containing grid coordinates + + Returns: + World position as Vector3 + """ + gridVector = Vector3(grid_point) + # Get origin position + ox = self.origin.position.x + oy = self.origin.position.y + + # Convert to world (simplified, no rotation) + x = ox + gridVector.x * self.resolution + y = oy + gridVector.y * self.resolution + + return Vector3(x, y, 0.0) + + def __str__(self) -> str: + """Create a concise string representation.""" + origin_pos = self.origin.position + + parts = [ + f"▦ OccupancyGrid[{self.frame_id}]", + f"{self.width}x{self.height}", + f"({self.width * self.resolution:.1f}x{self.height * self.resolution:.1f}m @", + f"{1 / self.resolution:.0f}cm res)", + f"Origin: ({origin_pos.x:.2f}, {origin_pos.y:.2f})", + f"▣ {self.occupied_percent:.1f}%", + f"□ {self.free_percent:.1f}%", + f"◌ {self.unknown_percent:.1f}%", + ] + + return " ".join(parts) + + def __repr__(self) -> str: + """Create a detailed representation.""" + return ( + f"OccupancyGrid(width={self.width}, height={self.height}, " + f"resolution={self.resolution}, frame_id='{self.frame_id}', " + f"occupied={self.occupied_cells}, free={self.free_cells}, " + f"unknown={self.unknown_cells})" + ) + + def lcm_encode(self) -> bytes: + """Encode OccupancyGrid to LCM bytes.""" + # Create LCM message + lcm_msg = LCMOccupancyGrid() + + # Build header on demand + s = int(self.ts) + lcm_msg.header.stamp.sec = s + lcm_msg.header.stamp.nsec = int((self.ts - s) * 1_000_000_000) + lcm_msg.header.frame_id = self.frame_id + + # Copy map metadata + lcm_msg.info = self.info + + # Convert numpy array to flat data list + if self.grid.size > 0: + flat_data = self.grid.flatten() + lcm_msg.data_length = len(flat_data) + lcm_msg.data = flat_data.tolist() + else: + lcm_msg.data_length = 0 + lcm_msg.data = [] + + return lcm_msg.lcm_encode() + + @classmethod + def lcm_decode(cls, data: bytes | BinaryIO) -> OccupancyGrid: + """Decode LCM bytes to OccupancyGrid.""" + lcm_msg = LCMOccupancyGrid.lcm_decode(data) + + # Extract timestamp and frame_id from header + ts = lcm_msg.header.stamp.sec + (lcm_msg.header.stamp.nsec / 1_000_000_000) + frame_id = lcm_msg.header.frame_id + + # Extract grid data + if lcm_msg.data and lcm_msg.info.width > 0 and lcm_msg.info.height > 0: + grid = np.array(lcm_msg.data, dtype=np.int8).reshape( + (lcm_msg.info.height, lcm_msg.info.width) + ) + else: + grid = np.array([], dtype=np.int8) + + # Create new instance + instance = cls( + grid=grid, + resolution=lcm_msg.info.resolution, + origin=lcm_msg.info.origin, + frame_id=frame_id, + ts=ts, + ) + instance.info = lcm_msg.info + return instance + + @classmethod + def from_pointcloud( + cls, + cloud: PointCloud2, + resolution: float = 0.05, + min_height: float = 0.1, + max_height: float = 2.0, + frame_id: str | None = None, + mark_free_radius: float = 0.4, + ) -> OccupancyGrid: + """Create an OccupancyGrid from a PointCloud2 message. + + Args: + cloud: PointCloud2 message containing 3D points + resolution: Grid resolution in meters/cell (default: 0.05) + min_height: Minimum height threshold for including points (default: 0.1) + max_height: Maximum height threshold for including points (default: 2.0) + frame_id: Reference frame for the grid (default: uses cloud's frame_id) + mark_free_radius: Radius in meters around obstacles to mark as free space (default: 0.0) + If 0, only immediate neighbors are marked free. + Set to preserve unknown areas for exploration. + + Returns: + OccupancyGrid with occupied cells where points were projected + """ + + # Get points as numpy array + points = cloud.as_numpy() + + if len(points) == 0: + # Return empty grid + return cls( + width=1, height=1, resolution=resolution, frame_id=frame_id or cloud.frame_id + ) + + # Filter points by height for obstacles + obstacle_mask = (points[:, 2] >= min_height) & (points[:, 2] <= max_height) + obstacle_points = points[obstacle_mask] + + # Get points below min_height for marking as free space + ground_mask = points[:, 2] < min_height + ground_points = points[ground_mask] + + # Find bounds of the point cloud in X-Y plane (use all points) + if len(points) > 0: + min_x = np.min(points[:, 0]) + max_x = np.max(points[:, 0]) + min_y = np.min(points[:, 1]) + max_y = np.max(points[:, 1]) + else: + # Return empty grid if no points at all + return cls( + width=1, height=1, resolution=resolution, frame_id=frame_id or cloud.frame_id + ) + + # Add some padding around the bounds + padding = 1.0 # 1 meter padding + min_x -= padding + max_x += padding + min_y -= padding + max_y += padding + + # Calculate grid dimensions + width = int(np.ceil((max_x - min_x) / resolution)) + height = int(np.ceil((max_y - min_y) / resolution)) + + # Create origin pose (bottom-left corner of the grid) + origin = Pose() + origin.position.x = min_x + origin.position.y = min_y + origin.position.z = 0.0 + origin.orientation.w = 1.0 # No rotation + + # Initialize grid (all unknown) + grid = np.full((height, width), -1, dtype=np.int8) + + # First, mark ground points as free space + if len(ground_points) > 0: + ground_x = ((ground_points[:, 0] - min_x) / resolution).astype(np.int32) + ground_y = ((ground_points[:, 1] - min_y) / resolution).astype(np.int32) + + # Clip indices to grid bounds + ground_x = np.clip(ground_x, 0, width - 1) + ground_y = np.clip(ground_y, 0, height - 1) + + # Mark ground cells as free + grid[ground_y, ground_x] = 0 # Free space + + # Then mark obstacle points (will override ground if at same location) + if len(obstacle_points) > 0: + obs_x = ((obstacle_points[:, 0] - min_x) / resolution).astype(np.int32) + obs_y = ((obstacle_points[:, 1] - min_y) / resolution).astype(np.int32) + + # Clip indices to grid bounds + obs_x = np.clip(obs_x, 0, width - 1) + obs_y = np.clip(obs_y, 0, height - 1) + + # Mark cells as occupied + grid[obs_y, obs_x] = 100 # Lethal obstacle + + # Apply mark_free_radius to expand free space areas + if mark_free_radius > 0: + # Expand existing free space areas by the specified radius + # This will NOT expand from obstacles, only from free space + + free_mask = grid == 0 # Current free space + free_radius_cells = int(np.ceil(mark_free_radius / resolution)) + + # Create circular kernel + y, x = np.ogrid[ + -free_radius_cells : free_radius_cells + 1, + -free_radius_cells : free_radius_cells + 1, + ] + kernel = x**2 + y**2 <= free_radius_cells**2 + + # Dilate free space areas + expanded_free = ndimage.binary_dilation(free_mask, structure=kernel, iterations=1) + + # Mark expanded areas as free, but don't override obstacles + grid[expanded_free & (grid != 100)] = 0 + + # Create and return OccupancyGrid + # Get timestamp from cloud if available + ts = cloud.ts if hasattr(cloud, "ts") and cloud.ts is not None else 0.0 + + occupancy_grid = cls( + grid=grid, + resolution=resolution, + origin=origin, + frame_id=frame_id or cloud.frame_id, + ts=ts, + ) + + return occupancy_grid + + def gradient(self, obstacle_threshold: int = 50, max_distance: float = 2.0) -> OccupancyGrid: + """Create a gradient OccupancyGrid for path planning. + + Creates a gradient where free space has value 0 and values increase near obstacles. + This can be used as a cost map for path planning algorithms like A*. + + Args: + obstacle_threshold: Cell values >= this are considered obstacles (default: 50) + max_distance: Maximum distance to compute gradient in meters (default: 2.0) + + Returns: + New OccupancyGrid with gradient values: + - -1: Unknown cells (preserved as-is) + - 0: Free space far from obstacles + - 1-99: Increasing cost as you approach obstacles + - 100: At obstacles + + Note: Unknown cells remain as unknown (-1) and do not receive gradient values. + """ + + # Remember which cells are unknown + unknown_mask = self.grid == CostValues.UNKNOWN + + # Create binary obstacle map + # Consider cells >= threshold as obstacles (1), everything else as free (0) + # Unknown cells are not considered obstacles for distance calculation + obstacle_map = (self.grid >= obstacle_threshold).astype(np.float32) + + # Compute distance transform (distance to nearest obstacle in cells) + # Unknown cells are treated as if they don't exist for distance calculation + distance_cells = ndimage.distance_transform_edt(1 - obstacle_map) + + # Convert to meters and clip to max distance + distance_meters = np.clip(distance_cells * self.resolution, 0, max_distance) + + # Invert and scale to 0-100 range + # Far from obstacles (max_distance) -> 0 + # At obstacles (0 distance) -> 100 + gradient_values = (1 - distance_meters / max_distance) * 100 + + # Ensure obstacles are exactly 100 + gradient_values[obstacle_map > 0] = CostValues.OCCUPIED + + # Convert to int8 for OccupancyGrid + gradient_data = gradient_values.astype(np.int8) + + # Preserve unknown cells as unknown (don't apply gradient to them) + gradient_data[unknown_mask] = CostValues.UNKNOWN + + # Create new OccupancyGrid with gradient + gradient_grid = OccupancyGrid( + grid=gradient_data, + resolution=self.resolution, + origin=self.origin, + frame_id=self.frame_id, + ts=self.ts, + ) + + return gradient_grid + + def filter_above(self, threshold: int) -> OccupancyGrid: + """Create a new OccupancyGrid with only values above threshold. + + Args: + threshold: Keep cells with values > threshold + + Returns: + New OccupancyGrid where: + - Cells > threshold: kept as-is + - Cells <= threshold: set to -1 (unknown) + - Unknown cells (-1): preserved + """ + new_grid = self.grid.copy() + + # Create mask for cells to filter (not unknown and <= threshold) + filter_mask = (new_grid != -1) & (new_grid <= threshold) + + # Set filtered cells to unknown + new_grid[filter_mask] = -1 + + # Create new OccupancyGrid + filtered = OccupancyGrid( + new_grid, + resolution=self.resolution, + origin=self.origin, + frame_id=self.frame_id, + ts=self.ts, + ) + + return filtered + + def filter_below(self, threshold: int) -> OccupancyGrid: + """Create a new OccupancyGrid with only values below threshold. + + Args: + threshold: Keep cells with values < threshold + + Returns: + New OccupancyGrid where: + - Cells < threshold: kept as-is + - Cells >= threshold: set to -1 (unknown) + - Unknown cells (-1): preserved + """ + new_grid = self.grid.copy() + + # Create mask for cells to filter (not unknown and >= threshold) + filter_mask = (new_grid != -1) & (new_grid >= threshold) + + # Set filtered cells to unknown + new_grid[filter_mask] = -1 + + # Create new OccupancyGrid + filtered = OccupancyGrid( + new_grid, + resolution=self.resolution, + origin=self.origin, + frame_id=self.frame_id, + ts=self.ts, + ) + + return filtered + + def max(self) -> OccupancyGrid: + """Create a new OccupancyGrid with all non-unknown cells set to maximum value. + + Returns: + New OccupancyGrid where: + - All non-unknown cells: set to CostValues.OCCUPIED (100) + - Unknown cells: preserved as CostValues.UNKNOWN (-1) + """ + new_grid = self.grid.copy() + + # Set all non-unknown cells to max + new_grid[new_grid != CostValues.UNKNOWN] = CostValues.OCCUPIED + + # Create new OccupancyGrid + maxed = OccupancyGrid( + new_grid, + resolution=self.resolution, + origin=self.origin, + frame_id=self.frame_id, + ts=self.ts, + ) + + return maxed diff --git a/dimos/msgs/nav_msgs/Odometry.py b/dimos/msgs/nav_msgs/Odometry.py new file mode 100644 index 0000000000..3a640b242d --- /dev/null +++ b/dimos/msgs/nav_msgs/Odometry.py @@ -0,0 +1,381 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import time +from typing import TYPE_CHECKING, TypeAlias + +from dimos_lcm.nav_msgs import Odometry as LCMOdometry +import numpy as np +from plum import dispatch + +try: + from nav_msgs.msg import Odometry as ROSOdometry +except ImportError: + ROSOdometry = None + +from dimos.msgs.geometry_msgs.Pose import Pose +from dimos.msgs.geometry_msgs.PoseWithCovariance import PoseWithCovariance +from dimos.msgs.geometry_msgs.Twist import Twist +from dimos.msgs.geometry_msgs.TwistWithCovariance import TwistWithCovariance +from dimos.types.timestamped import Timestamped + +if TYPE_CHECKING: + from dimos.msgs.geometry_msgs.Vector3 import Vector3 + +# Types that can be converted to/from Odometry +OdometryConvertable: TypeAlias = ( + LCMOdometry | dict[str, float | str | PoseWithCovariance | TwistWithCovariance | Pose | Twist] +) + + +def sec_nsec(ts): + s = int(ts) + return [s, int((ts - s) * 1_000_000_000)] + + +class Odometry(LCMOdometry, Timestamped): + pose: PoseWithCovariance + twist: TwistWithCovariance + msg_name = "nav_msgs.Odometry" + ts: float + frame_id: str + child_frame_id: str + + @dispatch + def __init__( + self, + ts: float = 0.0, + frame_id: str = "", + child_frame_id: str = "", + pose: PoseWithCovariance | Pose | None = None, + twist: TwistWithCovariance | Twist | None = None, + ) -> None: + """Initialize with timestamp, frame IDs, pose and twist. + + Args: + ts: Timestamp in seconds (defaults to current time if 0) + frame_id: Reference frame ID (e.g., "odom", "map") + child_frame_id: Child frame ID (e.g., "base_link", "base_footprint") + pose: Pose with covariance (or just Pose, covariance will be zero) + twist: Twist with covariance (or just Twist, covariance will be zero) + """ + self.ts = ts if ts != 0 else time.time() + self.frame_id = frame_id + self.child_frame_id = child_frame_id + + # Handle pose + if pose is None: + self.pose = PoseWithCovariance() + elif isinstance(pose, PoseWithCovariance): + self.pose = pose + elif isinstance(pose, Pose): + self.pose = PoseWithCovariance(pose) + else: + self.pose = PoseWithCovariance(Pose(pose)) + + # Handle twist + if twist is None: + self.twist = TwistWithCovariance() + elif isinstance(twist, TwistWithCovariance): + self.twist = twist + elif isinstance(twist, Twist): + self.twist = TwistWithCovariance(twist) + else: + self.twist = TwistWithCovariance(Twist(twist)) + + @dispatch + def __init__(self, odometry: Odometry) -> None: + """Initialize from another Odometry (copy constructor).""" + self.ts = odometry.ts + self.frame_id = odometry.frame_id + self.child_frame_id = odometry.child_frame_id + self.pose = PoseWithCovariance(odometry.pose) + self.twist = TwistWithCovariance(odometry.twist) + + @dispatch + def __init__(self, lcm_odometry: LCMOdometry) -> None: + """Initialize from an LCM Odometry.""" + self.ts = lcm_odometry.header.stamp.sec + (lcm_odometry.header.stamp.nsec / 1_000_000_000) + self.frame_id = lcm_odometry.header.frame_id + self.child_frame_id = lcm_odometry.child_frame_id + self.pose = PoseWithCovariance(lcm_odometry.pose) + self.twist = TwistWithCovariance(lcm_odometry.twist) + + @dispatch + def __init__( + self, + odometry_dict: dict[ + str, float | str | PoseWithCovariance | TwistWithCovariance | Pose | Twist + ], + ) -> None: + """Initialize from a dictionary.""" + self.ts = odometry_dict.get("ts", odometry_dict.get("timestamp", time.time())) + self.frame_id = odometry_dict.get("frame_id", "") + self.child_frame_id = odometry_dict.get("child_frame_id", "") + + # Handle pose + pose = odometry_dict.get("pose") + if pose is None: + self.pose = PoseWithCovariance() + elif isinstance(pose, PoseWithCovariance): + self.pose = pose + elif isinstance(pose, Pose): + self.pose = PoseWithCovariance(pose) + else: + self.pose = PoseWithCovariance(Pose(pose)) + + # Handle twist + twist = odometry_dict.get("twist") + if twist is None: + self.twist = TwistWithCovariance() + elif isinstance(twist, TwistWithCovariance): + self.twist = twist + elif isinstance(twist, Twist): + self.twist = TwistWithCovariance(twist) + else: + self.twist = TwistWithCovariance(Twist(twist)) + + @property + def position(self) -> Vector3: + """Get position from pose.""" + return self.pose.position + + @property + def orientation(self): + """Get orientation from pose.""" + return self.pose.orientation + + @property + def linear_velocity(self) -> Vector3: + """Get linear velocity from twist.""" + return self.twist.linear + + @property + def angular_velocity(self) -> Vector3: + """Get angular velocity from twist.""" + return self.twist.angular + + @property + def x(self) -> float: + """X position.""" + return self.pose.x + + @property + def y(self) -> float: + """Y position.""" + return self.pose.y + + @property + def z(self) -> float: + """Z position.""" + return self.pose.z + + @property + def vx(self) -> float: + """Linear velocity in X.""" + return self.twist.linear.x + + @property + def vy(self) -> float: + """Linear velocity in Y.""" + return self.twist.linear.y + + @property + def vz(self) -> float: + """Linear velocity in Z.""" + return self.twist.linear.z + + @property + def wx(self) -> float: + """Angular velocity around X (roll rate).""" + return self.twist.angular.x + + @property + def wy(self) -> float: + """Angular velocity around Y (pitch rate).""" + return self.twist.angular.y + + @property + def wz(self) -> float: + """Angular velocity around Z (yaw rate).""" + return self.twist.angular.z + + @property + def roll(self) -> float: + """Roll angle in radians.""" + return self.pose.roll + + @property + def pitch(self) -> float: + """Pitch angle in radians.""" + return self.pose.pitch + + @property + def yaw(self) -> float: + """Yaw angle in radians.""" + return self.pose.yaw + + def __repr__(self) -> str: + return ( + f"Odometry(ts={self.ts:.6f}, frame_id='{self.frame_id}', " + f"child_frame_id='{self.child_frame_id}', pose={self.pose!r}, twist={self.twist!r})" + ) + + def __str__(self) -> str: + return ( + f"Odometry:\n" + f" Timestamp: {self.ts:.6f}\n" + f" Frame: {self.frame_id} -> {self.child_frame_id}\n" + f" Position: [{self.x:.3f}, {self.y:.3f}, {self.z:.3f}]\n" + f" Orientation: [roll={self.roll:.3f}, pitch={self.pitch:.3f}, yaw={self.yaw:.3f}]\n" + f" Linear Velocity: [{self.vx:.3f}, {self.vy:.3f}, {self.vz:.3f}]\n" + f" Angular Velocity: [{self.wx:.3f}, {self.wy:.3f}, {self.wz:.3f}]" + ) + + def __eq__(self, other) -> bool: + """Check if two Odometry messages are equal.""" + if not isinstance(other, Odometry): + return False + return ( + abs(self.ts - other.ts) < 1e-6 + and self.frame_id == other.frame_id + and self.child_frame_id == other.child_frame_id + and self.pose == other.pose + and self.twist == other.twist + ) + + def lcm_encode(self) -> bytes: + """Encode to LCM binary format.""" + lcm_msg = LCMOdometry() + + # Set header + [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) + lcm_msg.header.frame_id = self.frame_id + lcm_msg.child_frame_id = self.child_frame_id + + # Set pose with covariance + lcm_msg.pose.pose = self.pose.pose + if isinstance(self.pose.covariance, np.ndarray): + lcm_msg.pose.covariance = self.pose.covariance.tolist() + else: + lcm_msg.pose.covariance = list(self.pose.covariance) + + # Set twist with covariance + lcm_msg.twist.twist = self.twist.twist + if isinstance(self.twist.covariance, np.ndarray): + lcm_msg.twist.covariance = self.twist.covariance.tolist() + else: + lcm_msg.twist.covariance = list(self.twist.covariance) + + return lcm_msg.lcm_encode() + + @classmethod + def lcm_decode(cls, data: bytes) -> Odometry: + """Decode from LCM binary format.""" + lcm_msg = LCMOdometry.lcm_decode(data) + + # Extract timestamp + ts = lcm_msg.header.stamp.sec + (lcm_msg.header.stamp.nsec / 1_000_000_000) + + # Create pose with covariance + pose = Pose( + position=[ + lcm_msg.pose.pose.position.x, + lcm_msg.pose.pose.position.y, + lcm_msg.pose.pose.position.z, + ], + orientation=[ + lcm_msg.pose.pose.orientation.x, + lcm_msg.pose.pose.orientation.y, + lcm_msg.pose.pose.orientation.z, + lcm_msg.pose.pose.orientation.w, + ], + ) + pose_with_cov = PoseWithCovariance(pose, lcm_msg.pose.covariance) + + # Create twist with covariance + twist = Twist( + linear=[ + lcm_msg.twist.twist.linear.x, + lcm_msg.twist.twist.linear.y, + lcm_msg.twist.twist.linear.z, + ], + angular=[ + lcm_msg.twist.twist.angular.x, + lcm_msg.twist.twist.angular.y, + lcm_msg.twist.twist.angular.z, + ], + ) + twist_with_cov = TwistWithCovariance(twist, lcm_msg.twist.covariance) + + return cls( + ts=ts, + frame_id=lcm_msg.header.frame_id, + child_frame_id=lcm_msg.child_frame_id, + pose=pose_with_cov, + twist=twist_with_cov, + ) + + @classmethod + def from_ros_msg(cls, ros_msg: ROSOdometry) -> Odometry: + """Create an Odometry from a ROS nav_msgs/Odometry message. + + Args: + ros_msg: ROS Odometry message + + Returns: + Odometry instance + """ + + # Convert timestamp from ROS header + ts = ros_msg.header.stamp.sec + (ros_msg.header.stamp.nanosec / 1_000_000_000) + + # Convert pose and twist with covariance + pose_with_cov = PoseWithCovariance.from_ros_msg(ros_msg.pose) + twist_with_cov = TwistWithCovariance.from_ros_msg(ros_msg.twist) + + return cls( + ts=ts, + frame_id=ros_msg.header.frame_id, + child_frame_id=ros_msg.child_frame_id, + pose=pose_with_cov, + twist=twist_with_cov, + ) + + def to_ros_msg(self) -> ROSOdometry: + """Convert to a ROS nav_msgs/Odometry message. + + Returns: + ROS Odometry message + """ + + ros_msg = ROSOdometry() + + # Set header + ros_msg.header.frame_id = self.frame_id + ros_msg.header.stamp.sec = int(self.ts) + ros_msg.header.stamp.nanosec = int((self.ts - int(self.ts)) * 1_000_000_000) + + # Set child frame ID + ros_msg.child_frame_id = self.child_frame_id + + # Set pose with covariance + ros_msg.pose = self.pose.to_ros_msg() + + # Set twist with covariance + ros_msg.twist = self.twist.to_ros_msg() + + return ros_msg diff --git a/dimos/msgs/nav_msgs/Path.py b/dimos/msgs/nav_msgs/Path.py new file mode 100644 index 0000000000..fa05ae4d6f --- /dev/null +++ b/dimos/msgs/nav_msgs/Path.py @@ -0,0 +1,233 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import time +from typing import TYPE_CHECKING, BinaryIO + +from dimos_lcm.geometry_msgs import ( + Point as LCMPoint, + Pose as LCMPose, + PoseStamped as LCMPoseStamped, + Quaternion as LCMQuaternion, +) +from dimos_lcm.nav_msgs import Path as LCMPath +from dimos_lcm.std_msgs import Header as LCMHeader, Time as LCMTime + +try: + from nav_msgs.msg import Path as ROSPath +except ImportError: + ROSPath = None + +from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped +from dimos.types.timestamped import Timestamped + +if TYPE_CHECKING: + from collections.abc import Iterator + + +def sec_nsec(ts): + s = int(ts) + return [s, int((ts - s) * 1_000_000_000)] + + +class Path(Timestamped): + msg_name = "nav_msgs.Path" + ts: float + frame_id: str + poses: list[PoseStamped] + + def __init__( + self, + ts: float = 0.0, + frame_id: str = "world", + poses: list[PoseStamped] | None = None, + **kwargs, + ) -> None: + self.frame_id = frame_id + self.ts = ts if ts != 0 else time.time() + self.poses = poses if poses is not None else [] + + def __len__(self) -> int: + """Return the number of poses in the path.""" + return len(self.poses) + + def __bool__(self) -> bool: + """Return True if path has poses.""" + return len(self.poses) > 0 + + def head(self) -> PoseStamped | None: + """Return the first pose in the path, or None if empty.""" + return self.poses[0] if self.poses else None + + def last(self) -> PoseStamped | None: + """Return the last pose in the path, or None if empty.""" + return self.poses[-1] if self.poses else None + + def tail(self) -> Path: + """Return a new Path with all poses except the first.""" + return Path(ts=self.ts, frame_id=self.frame_id, poses=self.poses[1:] if self.poses else []) + + def push(self, pose: PoseStamped) -> Path: + """Return a new Path with the pose appended (immutable).""" + return Path(ts=self.ts, frame_id=self.frame_id, poses=[*self.poses, pose]) + + def push_mut(self, pose: PoseStamped) -> None: + """Append a pose to this path (mutable).""" + self.poses.append(pose) + + def lcm_encode(self) -> bytes: + """Encode Path to LCM bytes.""" + lcm_msg = LCMPath() + + # Set poses + lcm_msg.poses_length = len(self.poses) + lcm_poses = [] # Build list separately to avoid LCM library reuse issues + for pose in self.poses: + lcm_pose = LCMPoseStamped() + # Create new pose objects to avoid LCM library reuse bug + lcm_pose.pose = LCMPose() + lcm_pose.pose.position = LCMPoint() + lcm_pose.pose.orientation = LCMQuaternion() + + # Set the pose geometry data + lcm_pose.pose.position.x = pose.x + lcm_pose.pose.position.y = pose.y + lcm_pose.pose.position.z = pose.z + lcm_pose.pose.orientation.x = pose.orientation.x + lcm_pose.pose.orientation.y = pose.orientation.y + lcm_pose.pose.orientation.z = pose.orientation.z + lcm_pose.pose.orientation.w = pose.orientation.w + + # Create new header to avoid reuse + lcm_pose.header = LCMHeader() + lcm_pose.header.stamp = LCMTime() + + # Set the header with pose timestamp but path's frame_id + [lcm_pose.header.stamp.sec, lcm_pose.header.stamp.nsec] = sec_nsec(pose.ts) + lcm_pose.header.frame_id = self.frame_id # All poses use path's frame_id + lcm_poses.append(lcm_pose) + lcm_msg.poses = lcm_poses + + # Set header with path's own timestamp + [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) + lcm_msg.header.frame_id = self.frame_id + + return lcm_msg.lcm_encode() + + @classmethod + def lcm_decode(cls, data: bytes | BinaryIO) -> Path: + """Decode LCM bytes to Path.""" + lcm_msg = LCMPath.lcm_decode(data) + + # Decode header + header_ts = lcm_msg.header.stamp.sec + (lcm_msg.header.stamp.nsec / 1_000_000_000) + frame_id = lcm_msg.header.frame_id + + # Decode poses - all use the path's frame_id + poses = [] + for lcm_pose in lcm_msg.poses: + pose = PoseStamped( + ts=lcm_pose.header.stamp.sec + (lcm_pose.header.stamp.nsec / 1_000_000_000), + frame_id=frame_id, # Use path's frame_id for all poses + position=[ + lcm_pose.pose.position.x, + lcm_pose.pose.position.y, + lcm_pose.pose.position.z, + ], + orientation=[ + lcm_pose.pose.orientation.x, + lcm_pose.pose.orientation.y, + lcm_pose.pose.orientation.z, + lcm_pose.pose.orientation.w, + ], + ) + poses.append(pose) + + # Use header timestamp for the path + return cls(ts=header_ts, frame_id=frame_id, poses=poses) + + def __str__(self) -> str: + """String representation of Path.""" + return f"Path(frame_id='{self.frame_id}', poses={len(self.poses)})" + + def __getitem__(self, index: int | slice) -> PoseStamped | list[PoseStamped]: + """Allow indexing and slicing of poses.""" + return self.poses[index] + + def __iter__(self) -> Iterator: + """Allow iteration over poses.""" + return iter(self.poses) + + def slice(self, start: int, end: int | None = None) -> Path: + """Return a new Path with a slice of poses.""" + return Path(ts=self.ts, frame_id=self.frame_id, poses=self.poses[start:end]) + + def extend(self, other: Path) -> Path: + """Return a new Path with poses from both paths (immutable).""" + return Path(ts=self.ts, frame_id=self.frame_id, poses=self.poses + other.poses) + + def extend_mut(self, other: Path) -> None: + """Extend this path with poses from another path (mutable).""" + self.poses.extend(other.poses) + + def reverse(self) -> Path: + """Return a new Path with poses in reverse order.""" + return Path(ts=self.ts, frame_id=self.frame_id, poses=list(reversed(self.poses))) + + def clear(self) -> None: + """Clear all poses from this path (mutable).""" + self.poses.clear() + + @classmethod + def from_ros_msg(cls, ros_msg: ROSPath) -> Path: + """Create a Path from a ROS nav_msgs/Path message. + + Args: + ros_msg: ROS Path message + + Returns: + Path instance + """ + + # Convert timestamp from ROS header + ts = ros_msg.header.stamp.sec + (ros_msg.header.stamp.nanosec / 1_000_000_000) + + # Convert poses + poses = [] + for ros_pose_stamped in ros_msg.poses: + poses.append(PoseStamped.from_ros_msg(ros_pose_stamped)) + + return cls(ts=ts, frame_id=ros_msg.header.frame_id, poses=poses) + + def to_ros_msg(self) -> ROSPath: + """Convert to a ROS nav_msgs/Path message. + + Returns: + ROS Path message + """ + + ros_msg = ROSPath() + + # Set header + ros_msg.header.frame_id = self.frame_id + ros_msg.header.stamp.sec = int(self.ts) + ros_msg.header.stamp.nanosec = int((self.ts - int(self.ts)) * 1_000_000_000) + + # Convert poses + for pose in self.poses: + ros_msg.poses.append(pose.to_ros_msg()) + + return ros_msg diff --git a/dimos/msgs/nav_msgs/__init__.py b/dimos/msgs/nav_msgs/__init__.py new file mode 100644 index 0000000000..9df397c57c --- /dev/null +++ b/dimos/msgs/nav_msgs/__init__.py @@ -0,0 +1,5 @@ +from dimos.msgs.nav_msgs.OccupancyGrid import CostValues, MapMetaData, OccupancyGrid +from dimos.msgs.nav_msgs.Odometry import Odometry +from dimos.msgs.nav_msgs.Path import Path + +__all__ = ["CostValues", "MapMetaData", "OccupancyGrid", "Odometry", "Path"] diff --git a/dimos/msgs/nav_msgs/test_OccupancyGrid.py b/dimos/msgs/nav_msgs/test_OccupancyGrid.py new file mode 100644 index 0000000000..a4cd36f9c0 --- /dev/null +++ b/dimos/msgs/nav_msgs/test_OccupancyGrid.py @@ -0,0 +1,471 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test the OccupancyGrid convenience class.""" + +import pickle + +import numpy as np +import pytest + +from dimos.msgs.geometry_msgs import Pose +from dimos.msgs.nav_msgs import OccupancyGrid +from dimos.msgs.sensor_msgs import PointCloud2 +from dimos.protocol.pubsub.lcmpubsub import LCM, Topic +from dimos.utils.testing import get_data + + +def test_empty_grid() -> None: + """Test creating an empty grid.""" + grid = OccupancyGrid() + assert grid.width == 0 + assert grid.height == 0 + assert grid.grid.shape == (0,) + assert grid.total_cells == 0 + assert grid.frame_id == "world" + + +def test_grid_with_dimensions() -> None: + """Test creating a grid with specified dimensions.""" + grid = OccupancyGrid(width=10, height=10, resolution=0.1, frame_id="map") + assert grid.width == 10 + assert grid.height == 10 + assert grid.resolution == 0.1 + assert grid.frame_id == "map" + assert grid.grid.shape == (10, 10) + assert np.all(grid.grid == -1) # All unknown + assert grid.unknown_cells == 100 + assert grid.unknown_percent == 100.0 + + +def test_grid_from_numpy_array() -> None: + """Test creating a grid from a numpy array.""" + data = np.zeros((20, 30), dtype=np.int8) + data[5:10, 10:20] = 100 # Add some obstacles + data[15:18, 5:8] = -1 # Add unknown area + + origin = Pose(1.0, 2.0, 0.0) + grid = OccupancyGrid(grid=data, resolution=0.05, origin=origin, frame_id="odom") + + assert grid.width == 30 + assert grid.height == 20 + assert grid.resolution == 0.05 + assert grid.frame_id == "odom" + assert grid.origin.position.x == 1.0 + assert grid.origin.position.y == 2.0 + assert grid.grid.shape == (20, 30) + + # Check cell counts + assert grid.occupied_cells == 50 # 5x10 obstacle area + assert grid.free_cells == 541 # Total - occupied - unknown + assert grid.unknown_cells == 9 # 3x3 unknown area + + # Check percentages (approximately) + assert abs(grid.occupied_percent - 8.33) < 0.1 + assert abs(grid.free_percent - 90.17) < 0.1 + assert abs(grid.unknown_percent - 1.5) < 0.1 + + +def test_world_grid_coordinate_conversion() -> None: + """Test converting between world and grid coordinates.""" + data = np.zeros((20, 30), dtype=np.int8) + origin = Pose(1.0, 2.0, 0.0) + grid = OccupancyGrid(grid=data, resolution=0.05, origin=origin, frame_id="odom") + + # Test world to grid + grid_pos = grid.world_to_grid((2.5, 3.0)) + assert int(grid_pos.x) == 30 + assert int(grid_pos.y) == 20 + + # Test grid to world + world_pos = grid.grid_to_world((10, 5)) + assert world_pos.x == 1.5 + assert world_pos.y == 2.25 + + +def test_lcm_encode_decode() -> None: + """Test LCM encoding and decoding.""" + data = np.zeros((20, 30), dtype=np.int8) + data[5:10, 10:20] = 100 # Add some obstacles + data[15:18, 5:8] = -1 # Add unknown area + origin = Pose(1.0, 2.0, 0.0) + grid = OccupancyGrid(grid=data, resolution=0.05, origin=origin, frame_id="odom") + + # Set a specific value for testing + # Convert world coordinates to grid indices + grid_pos = grid.world_to_grid((1.5, 2.25)) + grid.grid[int(grid_pos.y), int(grid_pos.x)] = 50 + + # Encode + lcm_data = grid.lcm_encode() + assert isinstance(lcm_data, bytes) + assert len(lcm_data) > 0 + + # Decode + decoded = OccupancyGrid.lcm_decode(lcm_data) + + # Check that data matches exactly (grid arrays should be identical) + assert np.array_equal(grid.grid, decoded.grid) + assert grid.width == decoded.width + assert grid.height == decoded.height + assert abs(grid.resolution - decoded.resolution) < 1e-6 # Use approximate equality for floats + assert abs(grid.origin.position.x - decoded.origin.position.x) < 1e-6 + assert abs(grid.origin.position.y - decoded.origin.position.y) < 1e-6 + assert grid.frame_id == decoded.frame_id + + # Check that the actual grid data was preserved (don't rely on float conversions) + assert decoded.grid[5, 10] == 50 # Value we set should be preserved in grid + + +def test_string_representation() -> None: + """Test string representations.""" + grid = OccupancyGrid(width=10, height=10, resolution=0.1, frame_id="map") + + # Test __str__ + str_repr = str(grid) + assert "OccupancyGrid[map]" in str_repr + assert "10x10" in str_repr + assert "1.0x1.0m" in str_repr + assert "10cm res" in str_repr + + # Test __repr__ + repr_str = repr(grid) + assert "OccupancyGrid(" in repr_str + assert "width=10" in repr_str + assert "height=10" in repr_str + assert "resolution=0.1" in repr_str + + +def test_grid_property_sync() -> None: + """Test that the grid property works correctly.""" + grid = OccupancyGrid(width=5, height=5, resolution=0.1, frame_id="map") + + # Modify via numpy array + grid.grid[2, 3] = 100 + assert grid.grid[2, 3] == 100 + + # Check that we can access grid values + grid.grid[0, 0] = 50 + assert grid.grid[0, 0] == 50 + + +def test_invalid_grid_dimensions() -> None: + """Test handling of invalid grid dimensions.""" + # Test with non-2D array + with pytest.raises(ValueError, match="Grid must be a 2D array"): + OccupancyGrid(grid=np.zeros(10), resolution=0.1) + + +def test_from_pointcloud() -> None: + """Test creating OccupancyGrid from PointCloud2.""" + file_path = get_data("lcm_msgs") / "sensor_msgs/PointCloud2.pickle" + with open(file_path, "rb") as f: + lcm_msg = pickle.loads(f.read()) + + pointcloud = PointCloud2.lcm_decode(lcm_msg) + + # Convert pointcloud to occupancy grid + occupancygrid = OccupancyGrid.from_pointcloud( + pointcloud, resolution=0.05, min_height=0.1, max_height=2.0 + ) + # Apply inflation separately if needed + occupancygrid = occupancygrid.inflate(0.1) + + # Check that grid was created with reasonable properties + assert occupancygrid.width > 0 + assert occupancygrid.height > 0 + assert occupancygrid.resolution == 0.05 + assert occupancygrid.frame_id == pointcloud.frame_id + assert occupancygrid.occupied_cells > 0 # Should have some occupied cells + + +def test_gradient() -> None: + """Test converting occupancy grid to gradient field.""" + # Create a small test grid with an obstacle in the middle + data = np.zeros((10, 10), dtype=np.int8) + data[4:6, 4:6] = 100 # 2x2 obstacle in center + + grid = OccupancyGrid(grid=data, resolution=0.1) # 0.1m per cell + + # Convert to gradient + gradient_grid = grid.gradient(obstacle_threshold=50, max_distance=1.0) + + # Check that we get an OccupancyGrid back + assert isinstance(gradient_grid, OccupancyGrid) + assert gradient_grid.grid.shape == (10, 10) + assert gradient_grid.resolution == grid.resolution + assert gradient_grid.frame_id == grid.frame_id + + # Obstacle cells should have value 100 + assert gradient_grid.grid[4, 4] == 100 + assert gradient_grid.grid[5, 5] == 100 + + # Adjacent cells should have high values (near obstacles) + assert gradient_grid.grid[3, 4] > 85 # Very close to obstacle + assert gradient_grid.grid[4, 3] > 85 # Very close to obstacle + + # Cells at moderate distance should have moderate values + assert 30 < gradient_grid.grid[0, 0] < 60 # Corner is ~0.57m away + + # Check that gradient decreases with distance + assert gradient_grid.grid[3, 4] > gradient_grid.grid[2, 4] # Closer is higher + assert gradient_grid.grid[2, 4] > gradient_grid.grid[0, 4] # Further is lower + + # Test with unknown cells + data_with_unknown = data.copy() + data_with_unknown[0:2, 0:2] = -1 # Add unknown area (close to obstacle) + data_with_unknown[8:10, 8:10] = -1 # Add unknown area (far from obstacle) + + grid_with_unknown = OccupancyGrid(data_with_unknown, resolution=0.1) + gradient_with_unknown = grid_with_unknown.gradient(max_distance=1.0) # 1m max distance + + # Unknown cells should remain unknown (new behavior - unknowns are preserved) + assert gradient_with_unknown.grid[0, 0] == -1 # Should remain unknown + assert gradient_with_unknown.grid[1, 1] == -1 # Should remain unknown + assert gradient_with_unknown.grid[8, 8] == -1 # Should remain unknown + assert gradient_with_unknown.grid[9, 9] == -1 # Should remain unknown + + # Unknown cells count should be preserved + assert gradient_with_unknown.unknown_cells == 8 # All unknowns preserved + + +def test_filter_above() -> None: + """Test filtering cells above threshold.""" + # Create test grid with various values + data = np.array( + [[-1, 0, 20, 50], [10, 30, 60, 80], [40, 70, 90, 100], [-1, 15, 25, -1]], dtype=np.int8 + ) + + grid = OccupancyGrid(grid=data, resolution=0.1) + + # Filter to keep only values > 50 + filtered = grid.filter_above(50) + + # Check that values > 50 are preserved + assert filtered.grid[1, 2] == 60 + assert filtered.grid[1, 3] == 80 + assert filtered.grid[2, 1] == 70 + assert filtered.grid[2, 2] == 90 + assert filtered.grid[2, 3] == 100 + + # Check that values <= 50 are set to -1 (unknown) + assert filtered.grid[0, 1] == -1 # was 0 + assert filtered.grid[0, 2] == -1 # was 20 + assert filtered.grid[0, 3] == -1 # was 50 + assert filtered.grid[1, 0] == -1 # was 10 + assert filtered.grid[1, 1] == -1 # was 30 + assert filtered.grid[2, 0] == -1 # was 40 + + # Check that unknown cells are preserved + assert filtered.grid[0, 0] == -1 + assert filtered.grid[3, 0] == -1 + assert filtered.grid[3, 3] == -1 + + # Check dimensions and metadata preserved + assert filtered.width == grid.width + assert filtered.height == grid.height + assert filtered.resolution == grid.resolution + assert filtered.frame_id == grid.frame_id + + +def test_filter_below() -> None: + """Test filtering cells below threshold.""" + # Create test grid with various values + data = np.array( + [[-1, 0, 20, 50], [10, 30, 60, 80], [40, 70, 90, 100], [-1, 15, 25, -1]], dtype=np.int8 + ) + + grid = OccupancyGrid(grid=data, resolution=0.1) + + # Filter to keep only values < 50 + filtered = grid.filter_below(50) + + # Check that values < 50 are preserved + assert filtered.grid[0, 1] == 0 + assert filtered.grid[0, 2] == 20 + assert filtered.grid[1, 0] == 10 + assert filtered.grid[1, 1] == 30 + assert filtered.grid[2, 0] == 40 + assert filtered.grid[3, 1] == 15 + assert filtered.grid[3, 2] == 25 + + # Check that values >= 50 are set to -1 (unknown) + assert filtered.grid[0, 3] == -1 # was 50 + assert filtered.grid[1, 2] == -1 # was 60 + assert filtered.grid[1, 3] == -1 # was 80 + assert filtered.grid[2, 1] == -1 # was 70 + assert filtered.grid[2, 2] == -1 # was 90 + assert filtered.grid[2, 3] == -1 # was 100 + + # Check that unknown cells are preserved + assert filtered.grid[0, 0] == -1 + assert filtered.grid[3, 0] == -1 + assert filtered.grid[3, 3] == -1 + + # Check dimensions and metadata preserved + assert filtered.width == grid.width + assert filtered.height == grid.height + assert filtered.resolution == grid.resolution + assert filtered.frame_id == grid.frame_id + + +def test_max() -> None: + """Test setting all non-unknown cells to maximum.""" + # Create test grid with various values + data = np.array( + [[-1, 0, 20, 50], [10, 30, 60, 80], [40, 70, 90, 100], [-1, 15, 25, -1]], dtype=np.int8 + ) + + grid = OccupancyGrid(grid=data, resolution=0.1) + + # Apply max + maxed = grid.max() + + # Check that all non-unknown cells are set to 100 + assert maxed.grid[0, 1] == 100 # was 0 + assert maxed.grid[0, 2] == 100 # was 20 + assert maxed.grid[0, 3] == 100 # was 50 + assert maxed.grid[1, 0] == 100 # was 10 + assert maxed.grid[1, 1] == 100 # was 30 + assert maxed.grid[1, 2] == 100 # was 60 + assert maxed.grid[1, 3] == 100 # was 80 + assert maxed.grid[2, 0] == 100 # was 40 + assert maxed.grid[2, 1] == 100 # was 70 + assert maxed.grid[2, 2] == 100 # was 90 + assert maxed.grid[2, 3] == 100 # was 100 (already max) + assert maxed.grid[3, 1] == 100 # was 15 + assert maxed.grid[3, 2] == 100 # was 25 + + # Check that unknown cells are preserved + assert maxed.grid[0, 0] == -1 + assert maxed.grid[3, 0] == -1 + assert maxed.grid[3, 3] == -1 + + # Check dimensions and metadata preserved + assert maxed.width == grid.width + assert maxed.height == grid.height + assert maxed.resolution == grid.resolution + assert maxed.frame_id == grid.frame_id + + # Verify statistics + assert maxed.unknown_cells == 3 # Same as original + assert maxed.occupied_cells == 13 # All non-unknown cells + assert maxed.free_cells == 0 # No free cells + + +@pytest.mark.lcm +def test_lcm_broadcast() -> None: + """Test broadcasting OccupancyGrid and gradient over LCM.""" + file_path = get_data("lcm_msgs") / "sensor_msgs/PointCloud2.pickle" + with open(file_path, "rb") as f: + lcm_msg = pickle.loads(f.read()) + + pointcloud = PointCloud2.lcm_decode(lcm_msg) + + # Create occupancy grid from pointcloud + occupancygrid = OccupancyGrid.from_pointcloud( + pointcloud, resolution=0.05, min_height=0.1, max_height=2.0 + ) + # Apply inflation separately if needed + occupancygrid = occupancygrid.inflate(0.1) + + # Create gradient field with larger max_distance for better visualization + gradient_grid = occupancygrid.gradient(obstacle_threshold=70, max_distance=2.0) + + # Debug: Print actual values to see the difference + print("\n=== DEBUG: Comparing grids ===") + print(f"Original grid unique values: {np.unique(occupancygrid.grid)}") + print(f"Gradient grid unique values: {np.unique(gradient_grid.grid)}") + + # Find an area with occupied cells to show the difference + occupied_indices = np.argwhere(occupancygrid.grid == 100) + if len(occupied_indices) > 0: + # Pick a point near an occupied cell + idx = len(occupied_indices) // 2 # Middle occupied cell + sample_y, sample_x = occupied_indices[idx] + sample_size = 15 + + # Ensure we don't go out of bounds + y_start = max(0, sample_y - sample_size // 2) + y_end = min(occupancygrid.height, y_start + sample_size) + x_start = max(0, sample_x - sample_size // 2) + x_end = min(occupancygrid.width, x_start + sample_size) + + print(f"\nSample area around occupied cell ({sample_x}, {sample_y}):") + print("Original occupancy grid:") + print(occupancygrid.grid[y_start:y_end, x_start:x_end]) + print("\nGradient grid (same area):") + print(gradient_grid.grid[y_start:y_end, x_start:x_end]) + else: + print("\nNo occupied cells found for sampling") + + # Check statistics + print("\nOriginal grid stats:") + print(f" Occupied (100): {np.sum(occupancygrid.grid == 100)} cells") + print(f" Inflated (99): {np.sum(occupancygrid.grid == 99)} cells") + print(f" Free (0): {np.sum(occupancygrid.grid == 0)} cells") + print(f" Unknown (-1): {np.sum(occupancygrid.grid == -1)} cells") + + print("\nGradient grid stats:") + print(f" Max gradient (100): {np.sum(gradient_grid.grid == 100)} cells") + print( + f" High gradient (80-99): {np.sum((gradient_grid.grid >= 80) & (gradient_grid.grid < 100))} cells" + ) + print( + f" Medium gradient (40-79): {np.sum((gradient_grid.grid >= 40) & (gradient_grid.grid < 80))} cells" + ) + print( + f" Low gradient (1-39): {np.sum((gradient_grid.grid >= 1) & (gradient_grid.grid < 40))} cells" + ) + print(f" Zero gradient (0): {np.sum(gradient_grid.grid == 0)} cells") + print(f" Unknown (-1): {np.sum(gradient_grid.grid == -1)} cells") + + # # Save debug images + # import matplotlib.pyplot as plt + + # fig, axes = plt.subplots(1, 2, figsize=(12, 5)) + + # # Original + # ax = axes[0] + # im1 = ax.imshow(occupancygrid.grid, origin="lower", cmap="gray_r", vmin=-1, vmax=100) + # ax.set_title(f"Original Occupancy Grid\n{occupancygrid}") + # plt.colorbar(im1, ax=ax) + + # # Gradient + # ax = axes[1] + # im2 = ax.imshow(gradient_grid.grid, origin="lower", cmap="hot", vmin=-1, vmax=100) + # ax.set_title(f"Gradient Grid\n{gradient_grid}") + # plt.colorbar(im2, ax=ax) + + # plt.tight_layout() + # plt.savefig("lcm_debug_grids.png", dpi=150) + # print("\nSaved debug visualization to lcm_debug_grids.png") + # plt.close() + + # Broadcast all the data + lcm = LCM() + lcm.start() + lcm.publish(Topic("/global_map", PointCloud2), pointcloud) + lcm.publish(Topic("/global_costmap", OccupancyGrid), occupancygrid) + lcm.publish(Topic("/global_gradient", OccupancyGrid), gradient_grid) + + print("\nPublished to LCM:") + print(f" /global_map: PointCloud2 with {len(pointcloud)} points") + print(f" /global_costmap: {occupancygrid}") + print(f" /global_gradient: {gradient_grid}") + print("\nGradient info:") + print(" Values: 0 (free far from obstacles) -> 100 (at obstacles)") + print(f" Unknown cells: {gradient_grid.unknown_cells} (preserved as -1)") + print(" Max distance for gradient: 5.0 meters") diff --git a/dimos/msgs/nav_msgs/test_Odometry.py b/dimos/msgs/nav_msgs/test_Odometry.py new file mode 100644 index 0000000000..e61bb8e8da --- /dev/null +++ b/dimos/msgs/nav_msgs/test_Odometry.py @@ -0,0 +1,504 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +import numpy as np +import pytest + +try: + from builtin_interfaces.msg import Time as ROSTime + from geometry_msgs.msg import ( + Point as ROSPoint, + Pose as ROSPose, + PoseWithCovariance as ROSPoseWithCovariance, + Quaternion as ROSQuaternion, + Twist as ROSTwist, + TwistWithCovariance as ROSTwistWithCovariance, + Vector3 as ROSVector3, + ) + from nav_msgs.msg import Odometry as ROSOdometry + from std_msgs.msg import Header as ROSHeader +except ImportError: + ROSTwist = None + ROSHeader = None + ROSPose = None + ROSPoseWithCovariance = None + ROSQuaternion = None + ROSOdometry = None + ROSPoint = None + ROSTime = None + ROSTwistWithCovariance = None + ROSVector3 = None + + +from dimos.msgs.geometry_msgs.Pose import Pose +from dimos.msgs.geometry_msgs.PoseWithCovariance import PoseWithCovariance +from dimos.msgs.geometry_msgs.Twist import Twist +from dimos.msgs.geometry_msgs.TwistWithCovariance import TwistWithCovariance +from dimos.msgs.geometry_msgs.Vector3 import Vector3 +from dimos.msgs.nav_msgs.Odometry import Odometry + + +def test_odometry_default_init() -> None: + """Test default initialization.""" + if ROSVector3 is None: + pytest.skip("ROS not available") + if ROSTwistWithCovariance is None: + pytest.skip("ROS not available") + if ROSTime is None: + pytest.skip("ROS not available") + if ROSPoint is None: + pytest.skip("ROS not available") + if ROSOdometry is None: + pytest.skip("ROS not available") + if ROSQuaternion is None: + pytest.skip("ROS not available") + if ROSPoseWithCovariance is None: + pytest.skip("ROS not available") + if ROSPose is None: + pytest.skip("ROS not available") + if ROSHeader is None: + pytest.skip("ROS not available") + if ROSTwist is None: + pytest.skip("ROS not available") + odom = Odometry() + + # Should have current timestamp + assert odom.ts > 0 + assert odom.frame_id == "" + assert odom.child_frame_id == "" + + # Pose should be at origin with identity orientation + assert odom.pose.position.x == 0.0 + assert odom.pose.position.y == 0.0 + assert odom.pose.position.z == 0.0 + assert odom.pose.orientation.w == 1.0 + + # Twist should be zero + assert odom.twist.linear.x == 0.0 + assert odom.twist.linear.y == 0.0 + assert odom.twist.linear.z == 0.0 + assert odom.twist.angular.x == 0.0 + assert odom.twist.angular.y == 0.0 + assert odom.twist.angular.z == 0.0 + + # Covariances should be zero + assert np.all(odom.pose.covariance == 0.0) + assert np.all(odom.twist.covariance == 0.0) + + +def test_odometry_with_frames() -> None: + """Test initialization with frame IDs.""" + ts = 1234567890.123456 + frame_id = "odom" + child_frame_id = "base_link" + + odom = Odometry(ts=ts, frame_id=frame_id, child_frame_id=child_frame_id) + + assert odom.ts == ts + assert odom.frame_id == frame_id + assert odom.child_frame_id == child_frame_id + + +def test_odometry_with_pose_and_twist() -> None: + """Test initialization with pose and twist.""" + pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) + twist = Twist(Vector3(0.5, 0.0, 0.0), Vector3(0.0, 0.0, 0.1)) + + odom = Odometry(ts=1000.0, frame_id="odom", child_frame_id="base_link", pose=pose, twist=twist) + + assert odom.pose.pose.position.x == 1.0 + assert odom.pose.pose.position.y == 2.0 + assert odom.pose.pose.position.z == 3.0 + assert odom.twist.twist.linear.x == 0.5 + assert odom.twist.twist.angular.z == 0.1 + + +def test_odometry_with_covariances() -> None: + """Test initialization with pose and twist with covariances.""" + pose = Pose(1.0, 2.0, 3.0) + pose_cov = np.arange(36, dtype=float) + pose_with_cov = PoseWithCovariance(pose, pose_cov) + + twist = Twist(Vector3(0.5, 0.0, 0.0), Vector3(0.0, 0.0, 0.1)) + twist_cov = np.arange(36, 72, dtype=float) + twist_with_cov = TwistWithCovariance(twist, twist_cov) + + odom = Odometry( + ts=1000.0, + frame_id="odom", + child_frame_id="base_link", + pose=pose_with_cov, + twist=twist_with_cov, + ) + + assert odom.pose.position.x == 1.0 + assert np.array_equal(odom.pose.covariance, pose_cov) + assert odom.twist.linear.x == 0.5 + assert np.array_equal(odom.twist.covariance, twist_cov) + + +def test_odometry_copy_constructor() -> None: + """Test copy constructor.""" + original = Odometry( + ts=1000.0, + frame_id="odom", + child_frame_id="base_link", + pose=Pose(1.0, 2.0, 3.0), + twist=Twist(Vector3(0.5, 0.0, 0.0), Vector3(0.0, 0.0, 0.1)), + ) + + copy = Odometry(original) + + assert copy == original + assert copy is not original + assert copy.pose is not original.pose + assert copy.twist is not original.twist + + +def test_odometry_dict_init() -> None: + """Test initialization from dictionary.""" + odom_dict = { + "ts": 1000.0, + "frame_id": "odom", + "child_frame_id": "base_link", + "pose": Pose(1.0, 2.0, 3.0), + "twist": Twist(Vector3(0.5, 0.0, 0.0), Vector3(0.0, 0.0, 0.1)), + } + + odom = Odometry(odom_dict) + + assert odom.ts == 1000.0 + assert odom.frame_id == "odom" + assert odom.child_frame_id == "base_link" + assert odom.pose.position.x == 1.0 + assert odom.twist.linear.x == 0.5 + + +def test_odometry_properties() -> None: + """Test convenience properties.""" + pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) + twist = Twist(Vector3(0.5, 0.6, 0.7), Vector3(0.1, 0.2, 0.3)) + + odom = Odometry(ts=1000.0, frame_id="odom", child_frame_id="base_link", pose=pose, twist=twist) + + # Position properties + assert odom.x == 1.0 + assert odom.y == 2.0 + assert odom.z == 3.0 + assert odom.position.x == 1.0 + assert odom.position.y == 2.0 + assert odom.position.z == 3.0 + + # Orientation properties + assert odom.orientation.x == 0.1 + assert odom.orientation.y == 0.2 + assert odom.orientation.z == 0.3 + assert odom.orientation.w == 0.9 + + # Velocity properties + assert odom.vx == 0.5 + assert odom.vy == 0.6 + assert odom.vz == 0.7 + assert odom.linear_velocity.x == 0.5 + assert odom.linear_velocity.y == 0.6 + assert odom.linear_velocity.z == 0.7 + + # Angular velocity properties + assert odom.wx == 0.1 + assert odom.wy == 0.2 + assert odom.wz == 0.3 + assert odom.angular_velocity.x == 0.1 + assert odom.angular_velocity.y == 0.2 + assert odom.angular_velocity.z == 0.3 + + # Euler angles + assert odom.roll == pose.roll + assert odom.pitch == pose.pitch + assert odom.yaw == pose.yaw + + +def test_odometry_str_repr() -> None: + """Test string representations.""" + odom = Odometry( + ts=1234567890.123456, + frame_id="odom", + child_frame_id="base_link", + pose=Pose(1.234, 2.567, 3.891), + twist=Twist(Vector3(0.5, 0.0, 0.0), Vector3(0.0, 0.0, 0.1)), + ) + + repr_str = repr(odom) + assert "Odometry" in repr_str + assert "1234567890.123456" in repr_str + assert "odom" in repr_str + assert "base_link" in repr_str + + str_repr = str(odom) + assert "Odometry" in str_repr + assert "odom -> base_link" in str_repr + assert "1.234" in str_repr + assert "0.500" in str_repr + + +def test_odometry_equality() -> None: + """Test equality comparison.""" + odom1 = Odometry( + ts=1000.0, + frame_id="odom", + child_frame_id="base_link", + pose=Pose(1.0, 2.0, 3.0), + twist=Twist(Vector3(0.5, 0.0, 0.0), Vector3(0.0, 0.0, 0.1)), + ) + + odom2 = Odometry( + ts=1000.0, + frame_id="odom", + child_frame_id="base_link", + pose=Pose(1.0, 2.0, 3.0), + twist=Twist(Vector3(0.5, 0.0, 0.0), Vector3(0.0, 0.0, 0.1)), + ) + + odom3 = Odometry( + ts=1000.0, + frame_id="odom", + child_frame_id="base_link", + pose=Pose(1.1, 2.0, 3.0), # Different position + twist=Twist(Vector3(0.5, 0.0, 0.0), Vector3(0.0, 0.0, 0.1)), + ) + + assert odom1 == odom2 + assert odom1 != odom3 + assert odom1 != "not an odometry" + + +def test_odometry_lcm_encode_decode() -> None: + """Test LCM encoding and decoding.""" + pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) + pose_cov = np.arange(36, dtype=float) + twist = Twist(Vector3(0.5, 0.6, 0.7), Vector3(0.1, 0.2, 0.3)) + twist_cov = np.arange(36, 72, dtype=float) + + source = Odometry( + ts=1234567890.123456, + frame_id="odom", + child_frame_id="base_link", + pose=PoseWithCovariance(pose, pose_cov), + twist=TwistWithCovariance(twist, twist_cov), + ) + + # Encode and decode + binary_msg = source.lcm_encode() + decoded = Odometry.lcm_decode(binary_msg) + + # Check values (allowing for timestamp precision loss) + assert abs(decoded.ts - source.ts) < 1e-6 + assert decoded.frame_id == source.frame_id + assert decoded.child_frame_id == source.child_frame_id + assert decoded.pose == source.pose + assert decoded.twist == source.twist + + +@pytest.mark.ros +def test_odometry_from_ros_msg() -> None: + """Test creating from ROS message.""" + ros_msg = ROSOdometry() + + # Set header + ros_msg.header = ROSHeader() + ros_msg.header.stamp = ROSTime() + ros_msg.header.stamp.sec = 1234567890 + ros_msg.header.stamp.nanosec = 123456000 + ros_msg.header.frame_id = "odom" + ros_msg.child_frame_id = "base_link" + + # Set pose with covariance + ros_msg.pose = ROSPoseWithCovariance() + ros_msg.pose.pose = ROSPose() + ros_msg.pose.pose.position = ROSPoint(x=1.0, y=2.0, z=3.0) + ros_msg.pose.pose.orientation = ROSQuaternion(x=0.1, y=0.2, z=0.3, w=0.9) + ros_msg.pose.covariance = [float(i) for i in range(36)] + + # Set twist with covariance + ros_msg.twist = ROSTwistWithCovariance() + ros_msg.twist.twist = ROSTwist() + ros_msg.twist.twist.linear = ROSVector3(x=0.5, y=0.6, z=0.7) + ros_msg.twist.twist.angular = ROSVector3(x=0.1, y=0.2, z=0.3) + ros_msg.twist.covariance = [float(i) for i in range(36, 72)] + + odom = Odometry.from_ros_msg(ros_msg) + + assert odom.ts == 1234567890.123456 + assert odom.frame_id == "odom" + assert odom.child_frame_id == "base_link" + assert odom.pose.position.x == 1.0 + assert odom.twist.linear.x == 0.5 + assert np.array_equal(odom.pose.covariance, np.arange(36)) + assert np.array_equal(odom.twist.covariance, np.arange(36, 72)) + + +@pytest.mark.ros +def test_odometry_to_ros_msg() -> None: + """Test converting to ROS message.""" + pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) + pose_cov = np.arange(36, dtype=float) + twist = Twist(Vector3(0.5, 0.6, 0.7), Vector3(0.1, 0.2, 0.3)) + twist_cov = np.arange(36, 72, dtype=float) + + odom = Odometry( + ts=1234567890.567890, + frame_id="odom", + child_frame_id="base_link", + pose=PoseWithCovariance(pose, pose_cov), + twist=TwistWithCovariance(twist, twist_cov), + ) + + ros_msg = odom.to_ros_msg() + + assert isinstance(ros_msg, ROSOdometry) + assert ros_msg.header.frame_id == "odom" + assert ros_msg.header.stamp.sec == 1234567890 + assert abs(ros_msg.header.stamp.nanosec - 567890000) < 100 # Allow small rounding error + assert ros_msg.child_frame_id == "base_link" + + # Check pose + assert ros_msg.pose.pose.position.x == 1.0 + assert ros_msg.pose.pose.position.y == 2.0 + assert ros_msg.pose.pose.position.z == 3.0 + assert ros_msg.pose.pose.orientation.x == 0.1 + assert ros_msg.pose.pose.orientation.y == 0.2 + assert ros_msg.pose.pose.orientation.z == 0.3 + assert ros_msg.pose.pose.orientation.w == 0.9 + assert list(ros_msg.pose.covariance) == list(range(36)) + + # Check twist + assert ros_msg.twist.twist.linear.x == 0.5 + assert ros_msg.twist.twist.linear.y == 0.6 + assert ros_msg.twist.twist.linear.z == 0.7 + assert ros_msg.twist.twist.angular.x == 0.1 + assert ros_msg.twist.twist.angular.y == 0.2 + assert ros_msg.twist.twist.angular.z == 0.3 + assert list(ros_msg.twist.covariance) == list(range(36, 72)) + + +@pytest.mark.ros +def test_odometry_ros_roundtrip() -> None: + """Test round-trip conversion with ROS messages.""" + pose = Pose(1.5, 2.5, 3.5, 0.15, 0.25, 0.35, 0.85) + pose_cov = np.random.rand(36) + twist = Twist(Vector3(0.55, 0.65, 0.75), Vector3(0.15, 0.25, 0.35)) + twist_cov = np.random.rand(36) + + original = Odometry( + ts=2147483647.987654, # Max int32 value for ROS Time.sec + frame_id="world", + child_frame_id="robot", + pose=PoseWithCovariance(pose, pose_cov), + twist=TwistWithCovariance(twist, twist_cov), + ) + + ros_msg = original.to_ros_msg() + restored = Odometry.from_ros_msg(ros_msg) + + # Check values (allowing for timestamp precision loss) + assert abs(restored.ts - original.ts) < 1e-6 + assert restored.frame_id == original.frame_id + assert restored.child_frame_id == original.child_frame_id + assert restored.pose == original.pose + assert restored.twist == original.twist + + +def test_odometry_zero_timestamp() -> None: + """Test that zero timestamp gets replaced with current time.""" + odom = Odometry(ts=0.0) + + # Should have been replaced with current time + assert odom.ts > 0 + assert odom.ts <= time.time() + + +def test_odometry_with_just_pose() -> None: + """Test initialization with just a Pose (no covariance).""" + pose = Pose(1.0, 2.0, 3.0) + + odom = Odometry(pose=pose) + + assert odom.pose.position.x == 1.0 + assert odom.pose.position.y == 2.0 + assert odom.pose.position.z == 3.0 + assert np.all(odom.pose.covariance == 0.0) # Should have zero covariance + assert np.all(odom.twist.covariance == 0.0) # Twist should also be zero + + +def test_odometry_with_just_twist() -> None: + """Test initialization with just a Twist (no covariance).""" + twist = Twist(Vector3(0.5, 0.0, 0.0), Vector3(0.0, 0.0, 0.1)) + + odom = Odometry(twist=twist) + + assert odom.twist.linear.x == 0.5 + assert odom.twist.angular.z == 0.1 + assert np.all(odom.twist.covariance == 0.0) # Should have zero covariance + assert np.all(odom.pose.covariance == 0.0) # Pose should also be zero + + +@pytest.mark.ros +@pytest.mark.parametrize( + "frame_id,child_frame_id", + [ + ("odom", "base_link"), + ("map", "odom"), + ("world", "robot"), + ("base_link", "camera_link"), + ("", ""), # Empty frames + ], +) +def test_odometry_frame_combinations(frame_id, child_frame_id) -> None: + """Test various frame ID combinations.""" + odom = Odometry(frame_id=frame_id, child_frame_id=child_frame_id) + + assert odom.frame_id == frame_id + assert odom.child_frame_id == child_frame_id + + # Test roundtrip through ROS + ros_msg = odom.to_ros_msg() + assert ros_msg.header.frame_id == frame_id + assert ros_msg.child_frame_id == child_frame_id + + restored = Odometry.from_ros_msg(ros_msg) + assert restored.frame_id == frame_id + assert restored.child_frame_id == child_frame_id + + +def test_odometry_typical_robot_scenario() -> None: + """Test a typical robot odometry scenario.""" + # Robot moving forward at 0.5 m/s with slight rotation + odom = Odometry( + ts=1000.0, + frame_id="odom", + child_frame_id="base_footprint", + pose=Pose(10.0, 5.0, 0.0, 0.0, 0.0, np.sin(0.1), np.cos(0.1)), # 0.2 rad yaw + twist=Twist( + Vector3(0.5, 0.0, 0.0), Vector3(0.0, 0.0, 0.05) + ), # Moving forward, turning slightly + ) + + # Check we can access all the typical properties + assert odom.x == 10.0 + assert odom.y == 5.0 + assert odom.z == 0.0 + assert abs(odom.yaw - 0.2) < 0.01 # Approximately 0.2 radians + assert odom.vx == 0.5 # Forward velocity + assert odom.wz == 0.05 # Yaw rate diff --git a/dimos/msgs/nav_msgs/test_Path.py b/dimos/msgs/nav_msgs/test_Path.py new file mode 100644 index 0000000000..9f4c39b8a0 --- /dev/null +++ b/dimos/msgs/nav_msgs/test_Path.py @@ -0,0 +1,391 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import pytest + +try: + from geometry_msgs.msg import PoseStamped as ROSPoseStamped + from nav_msgs.msg import Path as ROSPath +except ImportError: + ROSPoseStamped = None + ROSPath = None + +from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped +from dimos.msgs.geometry_msgs.Quaternion import Quaternion +from dimos.msgs.nav_msgs.Path import Path + + +def create_test_pose(x: float, y: float, z: float, frame_id: str = "map") -> PoseStamped: + """Helper to create a test PoseStamped.""" + return PoseStamped( + frame_id=frame_id, + position=[x, y, z], + orientation=Quaternion(0, 0, 0, 1), # Identity quaternion + ) + + +def test_init_empty() -> None: + """Test creating an empty path.""" + path = Path(frame_id="map") + assert path.frame_id == "map" + assert len(path) == 0 + assert not path # Should be falsy when empty + assert path.poses == [] + + +def test_init_with_poses() -> None: + """Test creating a path with initial poses.""" + poses = [create_test_pose(i, i, 0) for i in range(3)] + path = Path(frame_id="map", poses=poses) + assert len(path) == 3 + assert bool(path) # Should be truthy when has poses + assert path.poses == poses + + +def test_head() -> None: + """Test getting the first pose.""" + poses = [create_test_pose(i, i, 0) for i in range(3)] + path = Path(poses=poses) + assert path.head() == poses[0] + + # Test empty path + empty_path = Path() + assert empty_path.head() is None + + +def test_last() -> None: + """Test getting the last pose.""" + poses = [create_test_pose(i, i, 0) for i in range(3)] + path = Path(poses=poses) + assert path.last() == poses[-1] + + # Test empty path + empty_path = Path() + assert empty_path.last() is None + + +def test_tail() -> None: + """Test getting all poses except the first.""" + poses = [create_test_pose(i, i, 0) for i in range(3)] + path = Path(poses=poses) + tail = path.tail() + assert len(tail) == 2 + assert tail.poses == poses[1:] + assert tail.frame_id == path.frame_id + + # Test single element path + single_path = Path(poses=[poses[0]]) + assert len(single_path.tail()) == 0 + + # Test empty path + empty_path = Path() + assert len(empty_path.tail()) == 0 + + +def test_push_immutable() -> None: + """Test immutable push operation.""" + path = Path(frame_id="map") + pose1 = create_test_pose(1, 1, 0) + pose2 = create_test_pose(2, 2, 0) + + # Push should return new path + path2 = path.push(pose1) + assert len(path) == 0 # Original unchanged + assert len(path2) == 1 + assert path2.poses[0] == pose1 + + # Chain pushes + path3 = path2.push(pose2) + assert len(path2) == 1 # Previous unchanged + assert len(path3) == 2 + assert path3.poses == [pose1, pose2] + + +def test_push_mutable() -> None: + """Test mutable push operation.""" + path = Path(frame_id="map") + pose1 = create_test_pose(1, 1, 0) + pose2 = create_test_pose(2, 2, 0) + + # Push should modify in place + path.push_mut(pose1) + assert len(path) == 1 + assert path.poses[0] == pose1 + + path.push_mut(pose2) + assert len(path) == 2 + assert path.poses == [pose1, pose2] + + +def test_indexing() -> None: + """Test indexing and slicing.""" + poses = [create_test_pose(i, i, 0) for i in range(5)] + path = Path(poses=poses) + + # Single index + assert path[0] == poses[0] + assert path[-1] == poses[-1] + + # Slicing + assert path[1:3] == poses[1:3] + assert path[:2] == poses[:2] + assert path[3:] == poses[3:] + + +def test_iteration() -> None: + """Test iterating over poses.""" + poses = [create_test_pose(i, i, 0) for i in range(3)] + path = Path(poses=poses) + + collected = [] + for pose in path: + collected.append(pose) + assert collected == poses + + +def test_slice_method() -> None: + """Test slice method.""" + poses = [create_test_pose(i, i, 0) for i in range(5)] + path = Path(frame_id="map", poses=poses) + + sliced = path.slice(1, 4) + assert len(sliced) == 3 + assert sliced.poses == poses[1:4] + assert sliced.frame_id == "map" + + # Test open-ended slice + sliced2 = path.slice(2) + assert sliced2.poses == poses[2:] + + +def test_extend_immutable() -> None: + """Test immutable extend operation.""" + poses1 = [create_test_pose(i, i, 0) for i in range(2)] + poses2 = [create_test_pose(i + 2, i + 2, 0) for i in range(2)] + + path1 = Path(frame_id="map", poses=poses1) + path2 = Path(frame_id="odom", poses=poses2) + + extended = path1.extend(path2) + assert len(path1) == 2 # Original unchanged + assert len(extended) == 4 + assert extended.poses == poses1 + poses2 + assert extended.frame_id == "map" # Keeps first path's frame + + +def test_extend_mutable() -> None: + """Test mutable extend operation.""" + poses1 = [create_test_pose(i, i, 0) for i in range(2)] + poses2 = [create_test_pose(i + 2, i + 2, 0) for i in range(2)] + + path1 = Path(frame_id="map", poses=poses1.copy()) # Use copy to avoid modifying original + path2 = Path(frame_id="odom", poses=poses2) + + path1.extend_mut(path2) + assert len(path1) == 4 + # Check poses are the same as concatenation + for _i, (p1, p2) in enumerate(zip(path1.poses, poses1 + poses2, strict=False)): + assert p1.x == p2.x + assert p1.y == p2.y + assert p1.z == p2.z + + +def test_reverse() -> None: + """Test reverse operation.""" + poses = [create_test_pose(i, i, 0) for i in range(3)] + path = Path(poses=poses) + + reversed_path = path.reverse() + assert len(path) == 3 # Original unchanged + assert reversed_path.poses == list(reversed(poses)) + + +def test_clear() -> None: + """Test clear operation.""" + poses = [create_test_pose(i, i, 0) for i in range(3)] + path = Path(poses=poses) + + path.clear() + assert len(path) == 0 + assert path.poses == [] + + +def test_lcm_encode_decode() -> None: + """Test encoding and decoding of Path to/from binary LCM format.""" + # Create path with poses + # Use timestamps that can be represented exactly in float64 + path_ts = 1234567890.5 + poses = [ + PoseStamped( + ts=1234567890.0 + i * 0.1, # Use simpler timestamps + frame_id=f"frame_{i}", + position=[i * 1.5, i * 2.5, i * 3.5], + orientation=(0.1 * i, 0.2 * i, 0.3 * i, 0.9), + ) + for i in range(3) + ] + + path_source = Path(ts=path_ts, frame_id="world", poses=poses) + + # Encode to binary + binary_msg = path_source.lcm_encode() + + # Decode from binary + path_dest = Path.lcm_decode(binary_msg) + + assert isinstance(path_dest, Path) + assert path_dest is not path_source + + # Check header + assert path_dest.frame_id == path_source.frame_id + # Path timestamp should be preserved + assert abs(path_dest.ts - path_source.ts) < 1e-6 # Microsecond precision + + # Check poses + assert len(path_dest.poses) == len(path_source.poses) + + for orig, decoded in zip(path_source.poses, path_dest.poses, strict=False): + # Check pose timestamps + assert abs(decoded.ts - orig.ts) < 1e-6 + # All poses should have the path's frame_id + assert decoded.frame_id == path_dest.frame_id + + # Check position + assert decoded.x == orig.x + assert decoded.y == orig.y + assert decoded.z == orig.z + + # Check orientation + assert decoded.orientation.x == orig.orientation.x + assert decoded.orientation.y == orig.orientation.y + assert decoded.orientation.z == orig.orientation.z + assert decoded.orientation.w == orig.orientation.w + + +def test_lcm_encode_decode_empty() -> None: + """Test encoding and decoding of empty Path.""" + path_source = Path(frame_id="base_link") + + binary_msg = path_source.lcm_encode() + path_dest = Path.lcm_decode(binary_msg) + + assert isinstance(path_dest, Path) + assert path_dest.frame_id == path_source.frame_id + assert len(path_dest.poses) == 0 + + +def test_str_representation() -> None: + """Test string representation.""" + path = Path(frame_id="map") + assert str(path) == "Path(frame_id='map', poses=0)" + + path.push_mut(create_test_pose(1, 1, 0)) + path.push_mut(create_test_pose(2, 2, 0)) + assert str(path) == "Path(frame_id='map', poses=2)" + + +@pytest.mark.ros +def test_path_from_ros_msg() -> None: + """Test creating a Path from a ROS Path message.""" + ros_msg = ROSPath() + ros_msg.header.frame_id = "map" + ros_msg.header.stamp.sec = 123 + ros_msg.header.stamp.nanosec = 456000000 + + # Add some poses + for i in range(3): + ros_pose = ROSPoseStamped() + ros_pose.header.frame_id = "map" + ros_pose.header.stamp.sec = 123 + i + ros_pose.header.stamp.nanosec = 0 + ros_pose.pose.position.x = float(i) + ros_pose.pose.position.y = float(i * 2) + ros_pose.pose.position.z = float(i * 3) + ros_pose.pose.orientation.x = 0.0 + ros_pose.pose.orientation.y = 0.0 + ros_pose.pose.orientation.z = 0.0 + ros_pose.pose.orientation.w = 1.0 + ros_msg.poses.append(ros_pose) + + path = Path.from_ros_msg(ros_msg) + + assert path.frame_id == "map" + assert path.ts == 123.456 + assert len(path.poses) == 3 + + for i, pose in enumerate(path.poses): + assert pose.position.x == float(i) + assert pose.position.y == float(i * 2) + assert pose.position.z == float(i * 3) + assert pose.orientation.w == 1.0 + + +@pytest.mark.ros +def test_path_to_ros_msg() -> None: + """Test converting a Path to a ROS Path message.""" + poses = [ + PoseStamped( + ts=124.0 + i, frame_id="odom", position=[i, i * 2, i * 3], orientation=[0, 0, 0, 1] + ) + for i in range(3) + ] + + path = Path(ts=123.456, frame_id="odom", poses=poses) + + ros_msg = path.to_ros_msg() + + assert isinstance(ros_msg, ROSPath) + assert ros_msg.header.frame_id == "odom" + assert ros_msg.header.stamp.sec == 123 + assert ros_msg.header.stamp.nanosec == 456000000 + assert len(ros_msg.poses) == 3 + + for i, ros_pose in enumerate(ros_msg.poses): + assert ros_pose.pose.position.x == float(i) + assert ros_pose.pose.position.y == float(i * 2) + assert ros_pose.pose.position.z == float(i * 3) + assert ros_pose.pose.orientation.w == 1.0 + + +@pytest.mark.ros +def test_path_ros_roundtrip() -> None: + """Test round-trip conversion between Path and ROS Path.""" + poses = [ + PoseStamped( + ts=100.0 + i * 0.1, + frame_id="world", + position=[i * 1.5, i * 2.5, i * 3.5], + orientation=[0.1, 0.2, 0.3, 0.9], + ) + for i in range(3) + ] + + original = Path(ts=99.789, frame_id="world", poses=poses) + + ros_msg = original.to_ros_msg() + restored = Path.from_ros_msg(ros_msg) + + assert restored.frame_id == original.frame_id + assert restored.ts == original.ts + assert len(restored.poses) == len(original.poses) + + for orig_pose, rest_pose in zip(original.poses, restored.poses, strict=False): + assert rest_pose.position.x == orig_pose.position.x + assert rest_pose.position.y == orig_pose.position.y + assert rest_pose.position.z == orig_pose.position.z + assert rest_pose.orientation.x == orig_pose.orientation.x + assert rest_pose.orientation.y == orig_pose.orientation.y + assert rest_pose.orientation.z == orig_pose.orientation.z + assert rest_pose.orientation.w == orig_pose.orientation.w diff --git a/dimos/msgs/sensor_msgs/CameraInfo.py b/dimos/msgs/sensor_msgs/CameraInfo.py new file mode 100644 index 0000000000..3d2a118b0d --- /dev/null +++ b/dimos/msgs/sensor_msgs/CameraInfo.py @@ -0,0 +1,470 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import time + +# Import LCM types +from dimos_lcm.sensor_msgs import CameraInfo as LCMCameraInfo +from dimos_lcm.std_msgs.Header import Header +import numpy as np + +# Import ROS types +try: + from sensor_msgs.msg import CameraInfo as ROSCameraInfo, RegionOfInterest as ROSRegionOfInterest + from std_msgs.msg import Header as ROSHeader + + ROS_AVAILABLE = True +except ImportError: + ROS_AVAILABLE = False + +from dimos.types.timestamped import Timestamped + + +class CameraInfo(Timestamped): + """Camera calibration information message.""" + + msg_name = "sensor_msgs.CameraInfo" + + def __init__( + self, + height: int = 0, + width: int = 0, + distortion_model: str = "", + D: list[float] | None = None, + K: list[float] | None = None, + R: list[float] | None = None, + P: list[float] | None = None, + binning_x: int = 0, + binning_y: int = 0, + frame_id: str = "", + ts: float | None = None, + ) -> None: + """Initialize CameraInfo. + + Args: + height: Image height + width: Image width + distortion_model: Name of distortion model (e.g., "plumb_bob") + D: Distortion coefficients + K: 3x3 intrinsic camera matrix + R: 3x3 rectification matrix + P: 3x4 projection matrix + binning_x: Horizontal binning + binning_y: Vertical binning + frame_id: Frame ID + ts: Timestamp + """ + self.ts = ts if ts is not None else time.time() + self.frame_id = frame_id + self.height = height + self.width = width + self.distortion_model = distortion_model + + # Initialize distortion coefficients + self.D = D if D is not None else [] + + # Initialize 3x3 intrinsic camera matrix (row-major) + self.K = K if K is not None else [0.0] * 9 + + # Initialize 3x3 rectification matrix (row-major) + self.R = R if R is not None else [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0] + + # Initialize 3x4 projection matrix (row-major) + self.P = P if P is not None else [0.0] * 12 + + self.binning_x = binning_x + self.binning_y = binning_y + + # Region of interest (not used in basic implementation) + self.roi_x_offset = 0 + self.roi_y_offset = 0 + self.roi_height = 0 + self.roi_width = 0 + self.roi_do_rectify = False + + @classmethod + def from_yaml(cls, yaml_file: str) -> CameraInfo: + """Create CameraInfo from YAML file. + + Args: + yaml_file: Path to YAML file containing camera calibration data + + Returns: + CameraInfo instance with loaded calibration data + """ + import yaml + + with open(yaml_file) as f: + data = yaml.safe_load(f) + + # Extract basic parameters + width = data.get("image_width", 0) + height = data.get("image_height", 0) + distortion_model = data.get("distortion_model", "") + + # Extract matrices + camera_matrix = data.get("camera_matrix", {}) + K = camera_matrix.get("data", [0.0] * 9) + + distortion_coeffs = data.get("distortion_coefficients", {}) + D = distortion_coeffs.get("data", []) + + rect_matrix = data.get("rectification_matrix", {}) + R = rect_matrix.get("data", [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]) + + proj_matrix = data.get("projection_matrix", {}) + P = proj_matrix.get("data", [0.0] * 12) + + # Create CameraInfo instance + return cls( + height=height, + width=width, + distortion_model=distortion_model, + D=D, + K=K, + R=R, + P=P, + frame_id="camera_optical", + ) + + def get_K_matrix(self) -> np.ndarray: + """Get intrinsic matrix as numpy array.""" + return np.array(self.K, dtype=np.float64).reshape(3, 3) + + def get_P_matrix(self) -> np.ndarray: + """Get projection matrix as numpy array.""" + return np.array(self.P, dtype=np.float64).reshape(3, 4) + + def get_R_matrix(self) -> np.ndarray: + """Get rectification matrix as numpy array.""" + return np.array(self.R, dtype=np.float64).reshape(3, 3) + + def get_D_coeffs(self) -> np.ndarray: + """Get distortion coefficients as numpy array.""" + return np.array(self.D, dtype=np.float64) + + def set_K_matrix(self, K: np.ndarray): + """Set intrinsic matrix from numpy array.""" + if K.shape != (3, 3): + raise ValueError(f"K matrix must be 3x3, got {K.shape}") + self.K = K.flatten().tolist() + + def set_P_matrix(self, P: np.ndarray): + """Set projection matrix from numpy array.""" + if P.shape != (3, 4): + raise ValueError(f"P matrix must be 3x4, got {P.shape}") + self.P = P.flatten().tolist() + + def set_R_matrix(self, R: np.ndarray): + """Set rectification matrix from numpy array.""" + if R.shape != (3, 3): + raise ValueError(f"R matrix must be 3x3, got {R.shape}") + self.R = R.flatten().tolist() + + def set_D_coeffs(self, D: np.ndarray) -> None: + """Set distortion coefficients from numpy array.""" + self.D = D.flatten().tolist() + + def lcm_encode(self) -> bytes: + """Convert to LCM CameraInfo message.""" + msg = LCMCameraInfo() + + # Header + msg.header = Header() + msg.header.seq = 0 + msg.header.frame_id = self.frame_id + msg.header.stamp.sec = int(self.ts) + msg.header.stamp.nsec = int((self.ts - int(self.ts)) * 1e9) + + # Image dimensions + msg.height = self.height + msg.width = self.width + + # Distortion model + msg.distortion_model = self.distortion_model + + # Distortion coefficients + msg.D_length = len(self.D) + msg.D = self.D + + # Camera matrices (all stored as row-major) + msg.K = self.K + msg.R = self.R + msg.P = self.P + + # Binning + msg.binning_x = self.binning_x + msg.binning_y = self.binning_y + + # ROI + msg.roi.x_offset = self.roi_x_offset + msg.roi.y_offset = self.roi_y_offset + msg.roi.height = self.roi_height + msg.roi.width = self.roi_width + msg.roi.do_rectify = self.roi_do_rectify + + return msg.lcm_encode() + + @classmethod + def lcm_decode(cls, data: bytes) -> CameraInfo: + """Decode from LCM CameraInfo bytes.""" + msg = LCMCameraInfo.lcm_decode(data) + + # Extract timestamp + ts = msg.header.stamp.sec + msg.header.stamp.nsec / 1e9 if hasattr(msg, "header") else None + + camera_info = cls( + height=msg.height, + width=msg.width, + distortion_model=msg.distortion_model, + D=list(msg.D) if msg.D_length > 0 else [], + K=list(msg.K), + R=list(msg.R), + P=list(msg.P), + binning_x=msg.binning_x, + binning_y=msg.binning_y, + frame_id=msg.header.frame_id if hasattr(msg, "header") else "", + ts=ts, + ) + + # Set ROI if present + if hasattr(msg, "roi"): + camera_info.roi_x_offset = msg.roi.x_offset + camera_info.roi_y_offset = msg.roi.y_offset + camera_info.roi_height = msg.roi.height + camera_info.roi_width = msg.roi.width + camera_info.roi_do_rectify = msg.roi.do_rectify + + return camera_info + + @classmethod + def from_ros_msg(cls, ros_msg: ROSCameraInfo) -> CameraInfo: + """Create CameraInfo from ROS sensor_msgs/CameraInfo message. + + Args: + ros_msg: ROS CameraInfo message + + Returns: + CameraInfo instance + """ + if not ROS_AVAILABLE: + raise ImportError("ROS packages not available. Cannot convert from ROS message.") + + # Extract timestamp + ts = ros_msg.header.stamp.sec + ros_msg.header.stamp.nanosec / 1e9 + + camera_info = cls( + height=ros_msg.height, + width=ros_msg.width, + distortion_model=ros_msg.distortion_model, + D=list(ros_msg.d), + K=list(ros_msg.k), + R=list(ros_msg.r), + P=list(ros_msg.p), + binning_x=ros_msg.binning_x, + binning_y=ros_msg.binning_y, + frame_id=ros_msg.header.frame_id, + ts=ts, + ) + + # Set ROI + camera_info.roi_x_offset = ros_msg.roi.x_offset + camera_info.roi_y_offset = ros_msg.roi.y_offset + camera_info.roi_height = ros_msg.roi.height + camera_info.roi_width = ros_msg.roi.width + camera_info.roi_do_rectify = ros_msg.roi.do_rectify + + return camera_info + + def to_ros_msg(self) -> ROSCameraInfo: + """Convert to ROS sensor_msgs/CameraInfo message. + + Returns: + ROS CameraInfo message + """ + if not ROS_AVAILABLE: + raise ImportError("ROS packages not available. Cannot convert to ROS message.") + + ros_msg = ROSCameraInfo() + + # Set header + ros_msg.header = ROSHeader() + ros_msg.header.frame_id = self.frame_id + ros_msg.header.stamp.sec = int(self.ts) + ros_msg.header.stamp.nanosec = int((self.ts - int(self.ts)) * 1e9) + + # Image dimensions + ros_msg.height = self.height + ros_msg.width = self.width + + # Distortion model and coefficients + ros_msg.distortion_model = self.distortion_model + ros_msg.d = self.D + + # Camera matrices (all row-major) + ros_msg.k = self.K + ros_msg.r = self.R + ros_msg.p = self.P + + # Binning + ros_msg.binning_x = self.binning_x + ros_msg.binning_y = self.binning_y + + # ROI + ros_msg.roi = ROSRegionOfInterest() + ros_msg.roi.x_offset = self.roi_x_offset + ros_msg.roi.y_offset = self.roi_y_offset + ros_msg.roi.height = self.roi_height + ros_msg.roi.width = self.roi_width + ros_msg.roi.do_rectify = self.roi_do_rectify + + return ros_msg + + def __repr__(self) -> str: + """String representation.""" + return ( + f"CameraInfo(height={self.height}, width={self.width}, " + f"distortion_model='{self.distortion_model}', " + f"frame_id='{self.frame_id}', ts={self.ts})" + ) + + def __str__(self) -> str: + """Human-readable string.""" + return ( + f"CameraInfo:\n" + f" Resolution: {self.width}x{self.height}\n" + f" Distortion model: {self.distortion_model}\n" + f" Frame ID: {self.frame_id}\n" + f" Binning: {self.binning_x}x{self.binning_y}" + ) + + def __eq__(self, other) -> bool: + """Check if two CameraInfo messages are equal.""" + if not isinstance(other, CameraInfo): + return False + + return ( + self.height == other.height + and self.width == other.width + and self.distortion_model == other.distortion_model + and self.D == other.D + and self.K == other.K + and self.R == other.R + and self.P == other.P + and self.binning_x == other.binning_x + and self.binning_y == other.binning_y + and self.frame_id == other.frame_id + ) + + +class CalibrationProvider: + """Provides lazy-loaded access to camera calibration YAML files in a directory.""" + + def __init__(self, calibration_dir) -> None: + """Initialize with a directory containing calibration YAML files. + + Args: + calibration_dir: Path to directory containing .yaml calibration files + """ + from pathlib import Path + + self._calibration_dir = Path(calibration_dir) + self._cache = {} + + def _to_snake_case(self, name: str) -> str: + """Convert PascalCase to snake_case.""" + import re + + # Insert underscore before capital letters (except first char) + s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) + # Insert underscore before capital letter followed by lowercase + return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() + + def _find_yaml_file(self, name: str): + """Find YAML file matching the given name (tries both snake_case and exact match). + + Args: + name: Attribute name to look for + + Returns: + Path to YAML file if found, None otherwise + """ + # Try exact match first + yaml_file = self._calibration_dir / f"{name}.yaml" + if yaml_file.exists(): + return yaml_file + + # Try snake_case conversion for PascalCase names + snake_name = self._to_snake_case(name) + if snake_name != name: + yaml_file = self._calibration_dir / f"{snake_name}.yaml" + if yaml_file.exists(): + return yaml_file + + return None + + def __getattr__(self, name: str) -> CameraInfo: + """Load calibration YAML file on first access. + + Supports both snake_case and PascalCase attribute names. + For example, both 'single_webcam' and 'SingleWebcam' will load 'single_webcam.yaml'. + + Args: + name: Attribute name (can be PascalCase or snake_case) + + Returns: + CameraInfo object loaded from the YAML file + + Raises: + AttributeError: If no matching YAML file exists + """ + # Check cache first + if name in self._cache: + return self._cache[name] + + # Also check if the snake_case version is cached (for PascalCase access) + snake_name = self._to_snake_case(name) + if snake_name != name and snake_name in self._cache: + return self._cache[snake_name] + + # Find matching YAML file + yaml_file = self._find_yaml_file(name) + if not yaml_file: + raise AttributeError(f"No calibration file found for: {name}") + + # Load and cache the CameraInfo + camera_info = CameraInfo.from_yaml(str(yaml_file)) + + # Cache both the requested name and the snake_case version + self._cache[name] = camera_info + if snake_name != name: + self._cache[snake_name] = camera_info + + return camera_info + + def __dir__(self): + """List available calibrations in both snake_case and PascalCase.""" + calibrations = [] + if self._calibration_dir.exists() and self._calibration_dir.is_dir(): + yaml_files = self._calibration_dir.glob("*.yaml") + for f in yaml_files: + stem = f.stem + calibrations.append(stem) + # Add PascalCase version + pascal = "".join(word.capitalize() for word in stem.split("_")) + if pascal != stem: + calibrations.append(pascal) + return calibrations diff --git a/dimos/msgs/sensor_msgs/Image.py b/dimos/msgs/sensor_msgs/Image.py new file mode 100644 index 0000000000..051169d6a9 --- /dev/null +++ b/dimos/msgs/sensor_msgs/Image.py @@ -0,0 +1,731 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import base64 +import time +from typing import TYPE_CHECKING, Literal, TypedDict + +import cv2 +from dimos_lcm.sensor_msgs.Image import Image as LCMImage +from dimos_lcm.std_msgs.Header import Header +import numpy as np +import reactivex as rx +from reactivex import operators as ops +from turbojpeg import TurboJPEG + +from dimos.msgs.sensor_msgs.image_impls.AbstractImage import ( + HAS_CUDA, + HAS_NVIMGCODEC, + NVIMGCODEC_LAST_USED, + ImageFormat, +) +from dimos.msgs.sensor_msgs.image_impls.CudaImage import CudaImage +from dimos.msgs.sensor_msgs.image_impls.NumpyImage import NumpyImage +from dimos.types.timestamped import Timestamped, TimestampedBufferCollection, to_human_readable +from dimos.utils.reactive import quality_barrier + +if TYPE_CHECKING: + from reactivex.observable import Observable + + from dimos.msgs.sensor_msgs.image_impls.AbstractImage import ( + AbstractImage, + ) + +try: + import cupy as cp # type: ignore +except Exception: + cp = None # type: ignore + +try: + from sensor_msgs.msg import Image as ROSImage +except ImportError: + ROSImage = None + + +class AgentImageMessage(TypedDict): + """Type definition for agent-compatible image representation.""" + + type: Literal["image"] + source_type: Literal["base64"] + mime_type: Literal["image/jpeg", "image/png"] + data: str # Base64 encoded image data + + +class Image(Timestamped): + msg_name = "sensor_msgs.Image" + + def __init__( + self, + impl: AbstractImage | None = None, + *, + data=None, + format: ImageFormat | None = None, + frame_id: str | None = None, + ts: float | None = None, + ) -> None: + """Construct an Image facade. + + Usage: + - Image(impl=) + - Image(data=, format=ImageFormat.RGB, frame_id=str, ts=float) + + Notes: + - When constructed from `data`, uses CudaImage if `data` is a CuPy array and CUDA is available; otherwise NumpyImage. + - `format` defaults to ImageFormat.RGB; `frame_id` defaults to ""; `ts` defaults to `time.time()`. + """ + # Disallow mixing impl with raw kwargs + if impl is not None and any(x is not None for x in (data, format, frame_id, ts)): + raise TypeError( + "Provide either 'impl' or ('data', 'format', 'frame_id', 'ts'), not both" + ) + + if impl is not None: + self._impl = impl + return + + # Raw constructor path + if data is None: + raise TypeError("'data' is required when constructing Image without 'impl'") + fmt = format if format is not None else ImageFormat.BGR + fid = frame_id if frame_id is not None else "" + tstamp = ts if ts is not None else time.time() + + # Detect CuPy array without a hard dependency + is_cu = False + try: + import cupy as _cp # type: ignore + + is_cu = isinstance(data, _cp.ndarray) + except Exception: + is_cu = False + + if is_cu and HAS_CUDA: + self._impl = CudaImage(data, fmt, fid, tstamp) # type: ignore + else: + self._impl = NumpyImage(np.asarray(data), fmt, fid, tstamp) + + def __str__(self) -> str: + dev = "cuda" if self.is_cuda else "cpu" + return ( + f"Image(shape={self.shape}, format={self.format.value}, dtype={self.dtype}, " + f"dev={dev}, ts={to_human_readable(self.ts)})" + ) + + @classmethod + def from_impl(cls, impl: AbstractImage) -> Image: + return cls(impl) + + @classmethod + def from_numpy( + cls, + np_image: np.ndarray, + format: ImageFormat = ImageFormat.BGR, + to_cuda: bool = False, + **kwargs, + ) -> Image: + if kwargs.pop("to_gpu", False): + to_cuda = True + if to_cuda and HAS_CUDA: + return cls( + CudaImage( + np_image if hasattr(np_image, "shape") else np.asarray(np_image), + format, + kwargs.get("frame_id", ""), + kwargs.get("ts", time.time()), + ) + ) # type: ignore + return cls( + NumpyImage( + np.asarray(np_image), + format, + kwargs.get("frame_id", ""), + kwargs.get("ts", time.time()), + ) + ) + + @classmethod + def from_file( + cls, filepath: str, format: ImageFormat = ImageFormat.RGB, to_cuda: bool = False, **kwargs + ) -> Image: + if kwargs.pop("to_gpu", False): + to_cuda = True + arr = cv2.imread(filepath, cv2.IMREAD_UNCHANGED) + if arr is None: + raise ValueError(f"Could not load image from {filepath}") + if arr.ndim == 2: + detected = ImageFormat.GRAY16 if arr.dtype == np.uint16 else ImageFormat.GRAY + elif arr.shape[2] == 3: + detected = ImageFormat.BGR # OpenCV default + elif arr.shape[2] == 4: + detected = ImageFormat.BGRA # OpenCV default + else: + detected = format + return cls(CudaImage(arr, detected) if to_cuda and HAS_CUDA else NumpyImage(arr, detected)) # type: ignore + + @classmethod + def from_opencv( + cls, cv_image: np.ndarray, format: ImageFormat = ImageFormat.BGR, **kwargs + ) -> Image: + """Construct from an OpenCV image (NumPy array).""" + return cls( + NumpyImage(cv_image, format, kwargs.get("frame_id", ""), kwargs.get("ts", time.time())) + ) + + @classmethod + def from_depth( + cls, depth_data, frame_id: str = "", ts: float | None = None, to_cuda: bool = False + ) -> Image: + arr = np.asarray(depth_data) + if arr.dtype != np.float32: + arr = arr.astype(np.float32) + impl = ( + CudaImage(arr, ImageFormat.DEPTH, frame_id, time.time() if ts is None else ts) + if to_cuda and HAS_CUDA + else NumpyImage(arr, ImageFormat.DEPTH, frame_id, time.time() if ts is None else ts) + ) # type: ignore + return cls(impl) + + # Delegation + @property + def is_cuda(self) -> bool: + return self._impl.is_cuda + + @property + def data(self): + return self._impl.data + + @data.setter + def data(self, value) -> None: + # Preserve backend semantics: ensure array type matches implementation + if isinstance(self._impl, NumpyImage): + self._impl.data = np.asarray(value) + elif isinstance(self._impl, CudaImage): # type: ignore + if cp is None: + raise RuntimeError("CuPy not available to set CUDA image data") + self._impl.data = cp.asarray(value) # type: ignore + else: + self._impl.data = value + + @property + def format(self) -> ImageFormat: + return self._impl.format + + @format.setter + def format(self, value) -> None: + if isinstance(value, ImageFormat): + self._impl.format = value + elif isinstance(value, str): + try: + self._impl.format = ImageFormat[value] + except KeyError as e: + raise ValueError(f"Invalid ImageFormat: {value}") from e + else: + raise TypeError("format must be ImageFormat or str name") + + @property + def frame_id(self) -> str: + return self._impl.frame_id + + @frame_id.setter + def frame_id(self, value: str) -> None: + self._impl.frame_id = str(value) + + @property + def ts(self) -> float: + return self._impl.ts + + @ts.setter + def ts(self, value: float) -> None: + self._impl.ts = float(value) + + @property + def height(self) -> int: + return self._impl.height + + @property + def width(self) -> int: + return self._impl.width + + @property + def channels(self) -> int: + return self._impl.channels + + @property + def shape(self): + return self._impl.shape + + @property + def dtype(self): + return self._impl.dtype + + def copy(self) -> Image: + return Image(self._impl.copy()) + + def to_cpu(self) -> Image: + if isinstance(self._impl, NumpyImage): + return self.copy() + + data = self._impl.data.get() # CuPy array to NumPy + + return Image( + NumpyImage( + data, + self._impl.format, + self._impl.frame_id, + self._impl.ts, + ) + ) + + def to_cupy(self) -> Image: + if isinstance(self._impl, CudaImage): + return self.copy() + return Image( + CudaImage( + np.asarray(self._impl.data), self._impl.format, self._impl.frame_id, self._impl.ts + ) + ) # type: ignore + + def to_opencv(self) -> np.ndarray: + return self._impl.to_opencv() + + def to_rgb(self) -> Image: + return Image(self._impl.to_rgb()) + + def to_bgr(self) -> Image: + return Image(self._impl.to_bgr()) + + def to_grayscale(self) -> Image: + return Image(self._impl.to_grayscale()) + + def resize(self, width: int, height: int, interpolation: int = cv2.INTER_LINEAR) -> Image: + return Image(self._impl.resize(width, height, interpolation)) + + def crop(self, x: int, y: int, width: int, height: int) -> Image: + return Image(self._impl.crop(x, y, width, height)) + + @property + def sharpness(self) -> float: + """Return sharpness score.""" + return self._impl.sharpness() + + def save(self, filepath: str) -> bool: + return self._impl.save(filepath) + + def to_base64( + self, + quality: int = 80, + *, + max_width: int | None = None, + max_height: int | None = None, + ) -> str: + """Encode the image as a base64 JPEG string. + + Args: + quality: JPEG quality (0-100). + max_width: Optional maximum width to constrain the encoded image. + max_height: Optional maximum height to constrain the encoded image. + + Returns: + Base64-encoded JPEG representation of the image. + """ + bgr_image = self.to_bgr().to_opencv() + height, width = bgr_image.shape[:2] + + scale = 1.0 + if max_width is not None and width > max_width: + scale = min(scale, max_width / width) + if max_height is not None and height > max_height: + scale = min(scale, max_height / height) + + if scale < 1.0: + new_width = max(1, round(width * scale)) + new_height = max(1, round(height * scale)) + bgr_image = cv2.resize(bgr_image, (new_width, new_height), interpolation=cv2.INTER_AREA) + + encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), int(np.clip(quality, 0, 100))] + success, buffer = cv2.imencode(".jpg", bgr_image, encode_param) + if not success: + raise ValueError("Failed to encode image as JPEG") + + return base64.b64encode(buffer.tobytes()).decode("utf-8") + + def agent_encode(self) -> AgentImageMessage: + return [ + { + "type": "image_url", + "image_url": {"url": f"data:image/jpeg;base64,{self.to_base64()}"}, + } + ] + + # LCM encode/decode + def lcm_encode(self, frame_id: str | None = None) -> bytes: + """Convert to LCM Image message.""" + msg = LCMImage() + + # Header + msg.header = Header() + msg.header.seq = 0 + msg.header.frame_id = frame_id or self.frame_id + + # Set timestamp + if self.ts is not None: + msg.header.stamp.sec = int(self.ts) + msg.header.stamp.nsec = int((self.ts - int(self.ts)) * 1e9) + else: + now = time.time() + msg.header.stamp.sec = int(now) + msg.header.stamp.nsec = int((now - int(now)) * 1e9) + + # Image properties + msg.height = self.height + msg.width = self.width + msg.encoding = _get_lcm_encoding(self.format, self.dtype) + msg.is_bigendian = False + + # Calculate step (bytes per row) + channels = 1 if self.data.ndim == 2 else self.data.shape[2] + msg.step = self.width * self.dtype.itemsize * channels + + # Image data - use raw data to preserve format + image_bytes = self.data.tobytes() + msg.data_length = len(image_bytes) + msg.data = image_bytes + + return msg.lcm_encode() + + @classmethod + def lcm_decode(cls, data: bytes, **kwargs) -> Image: + msg = LCMImage.lcm_decode(data) + fmt, dtype, channels = _parse_lcm_encoding(msg.encoding) + arr = np.frombuffer(msg.data, dtype=dtype) + if channels == 1: + arr = arr.reshape((msg.height, msg.width)) + else: + arr = arr.reshape((msg.height, msg.width, channels)) + return cls( + NumpyImage( + arr, + fmt, + msg.header.frame_id if hasattr(msg, "header") else "", + ( + msg.header.stamp.sec + msg.header.stamp.nsec / 1e9 + if hasattr(msg, "header") + and hasattr(msg.header, "stamp") + and msg.header.stamp.sec > 0 + else time.time() + ), + ) + ) + + def lcm_jpeg_encode(self, quality: int = 75, frame_id: Optional[str] = None) -> bytes: + """Convert to LCM Image message with JPEG-compressed data. + + Args: + quality: JPEG compression quality (0-100, default 75) + frame_id: Optional frame ID override + + Returns: + LCM-encoded bytes with JPEG-compressed image data + """ + jpeg = TurboJPEG() + msg = LCMImage() + + # Header + msg.header = Header() + msg.header.seq = 0 + msg.header.frame_id = frame_id or self.frame_id + + # Set timestamp + if self.ts is not None: + msg.header.stamp.sec = int(self.ts) + msg.header.stamp.nsec = int((self.ts - int(self.ts)) * 1e9) + else: + now = time.time() + msg.header.stamp.sec = int(now) + msg.header.stamp.nsec = int((now - int(now)) * 1e9) + + # Get image in BGR format for JPEG encoding + bgr_image = self.to_bgr().to_opencv() + + # Encode as JPEG + jpeg_data = jpeg.encode(bgr_image, quality=quality) + + # Store JPEG data and metadata + msg.height = self.height + msg.width = self.width + msg.encoding = "jpeg" + msg.is_bigendian = False + msg.step = 0 # Not applicable for compressed format + + msg.data_length = len(jpeg_data) + msg.data = jpeg_data + + return msg.lcm_encode() + + @classmethod + def lcm_jpeg_decode(cls, data: bytes, **kwargs) -> Image: + """Decode an LCM Image message with JPEG-compressed data. + + Args: + data: LCM-encoded bytes containing JPEG-compressed image + + Returns: + Image instance + """ + jpeg = TurboJPEG() + msg = LCMImage.lcm_decode(data) + + if msg.encoding != "jpeg": + raise ValueError(f"Expected JPEG encoding, got {msg.encoding}") + + # Decode JPEG data + bgr_array = jpeg.decode(msg.data) + + return cls( + NumpyImage( + bgr_array, + ImageFormat.BGR, + msg.header.frame_id if hasattr(msg, "header") else "", + ( + msg.header.stamp.sec + msg.header.stamp.nsec / 1e9 + if hasattr(msg, "header") + and hasattr(msg.header, "stamp") + and msg.header.stamp.sec > 0 + else time.time() + ), + ) + ) + + # PnP wrappers + def solve_pnp(self, *args, **kwargs): + return self._impl.solve_pnp(*args, **kwargs) # type: ignore + + def solve_pnp_ransac(self, *args, **kwargs): + return self._impl.solve_pnp_ransac(*args, **kwargs) # type: ignore + + def solve_pnp_batch(self, *args, **kwargs): + return self._impl.solve_pnp_batch(*args, **kwargs) # type: ignore + + def create_csrt_tracker(self, *args, **kwargs): + return self._impl.create_csrt_tracker(*args, **kwargs) # type: ignore + + def csrt_update(self, *args, **kwargs): + return self._impl.csrt_update(*args, **kwargs) # type: ignore + + @classmethod + def from_ros_msg(cls, ros_msg: ROSImage) -> Image: + """Create an Image from a ROS sensor_msgs/Image message. + + Args: + ros_msg: ROS Image message + + Returns: + Image instance + """ + # Convert timestamp from ROS header + ts = ros_msg.header.stamp.sec + (ros_msg.header.stamp.nanosec / 1_000_000_000) + + # Parse encoding to determine format and data type + format_info = cls._parse_encoding(ros_msg.encoding) + + # Convert data from ROS message (array.array) to numpy array + data_array = np.frombuffer(ros_msg.data, dtype=format_info["dtype"]) + + # Reshape to image dimensions + if format_info["channels"] == 1: + data_array = data_array.reshape((ros_msg.height, ros_msg.width)) + else: + data_array = data_array.reshape( + (ros_msg.height, ros_msg.width, format_info["channels"]) + ) + + # Crop to center 1/3 of the image (simulate 120-degree FOV from 360-degree) + original_width = data_array.shape[1] + crop_width = original_width // 3 + start_x = (original_width - crop_width) // 2 + end_x = start_x + crop_width + + # Crop the image horizontally to center 1/3 + if len(data_array.shape) == 2: + # Grayscale image + data_array = data_array[:, start_x:end_x] + else: + # Color image + data_array = data_array[:, start_x:end_x, :] + + # Fix color channel order: if ROS sends RGB but we expect BGR, swap channels + # ROS typically uses rgb8 encoding, but OpenCV/our system expects BGR + if format_info["format"] == ImageFormat.RGB: + # Convert RGB to BGR by swapping channels + if len(data_array.shape) == 3 and data_array.shape[2] == 3: + data_array = data_array[:, :, [2, 1, 0]] # RGB -> BGR + format_info["format"] = ImageFormat.BGR + elif format_info["format"] == ImageFormat.RGBA: + # Convert RGBA to BGRA by swapping channels + if len(data_array.shape) == 3 and data_array.shape[2] == 4: + data_array = data_array[:, :, [2, 1, 0, 3]] # RGBA -> BGRA + format_info["format"] = ImageFormat.BGRA + + return cls( + data=data_array, + format=format_info["format"], + frame_id=ros_msg.header.frame_id, + ts=ts, + ) + + @staticmethod + def _parse_encoding(encoding: str) -> dict: + """Translate ROS encoding strings into format metadata.""" + encoding_map = { + "mono8": {"format": ImageFormat.GRAY, "dtype": np.uint8, "channels": 1}, + "mono16": {"format": ImageFormat.GRAY16, "dtype": np.uint16, "channels": 1}, + "rgb8": {"format": ImageFormat.RGB, "dtype": np.uint8, "channels": 3}, + "rgba8": {"format": ImageFormat.RGBA, "dtype": np.uint8, "channels": 4}, + "bgr8": {"format": ImageFormat.BGR, "dtype": np.uint8, "channels": 3}, + "bgra8": {"format": ImageFormat.BGRA, "dtype": np.uint8, "channels": 4}, + "32FC1": {"format": ImageFormat.DEPTH, "dtype": np.float32, "channels": 1}, + "32FC3": {"format": ImageFormat.RGB, "dtype": np.float32, "channels": 3}, + "64FC1": {"format": ImageFormat.DEPTH, "dtype": np.float64, "channels": 1}, + "16UC1": {"format": ImageFormat.DEPTH16, "dtype": np.uint16, "channels": 1}, + "16SC1": {"format": ImageFormat.DEPTH16, "dtype": np.int16, "channels": 1}, + } + + key = encoding.strip() + for candidate in (key, key.lower(), key.upper()): + if candidate in encoding_map: + return dict(encoding_map[candidate]) + + raise ValueError(f"Unsupported encoding: {encoding}") + + def __repr__(self) -> str: + dev = "cuda" if self.is_cuda else "cpu" + return f"Image(shape={self.shape}, format={self.format.value}, dtype={self.dtype}, dev={dev}, frame_id='{self.frame_id}', ts={self.ts})" + + def __eq__(self, other) -> bool: + if not isinstance(other, Image): + return False + return ( + np.array_equal(self.data, other.data) + and self.format == other.format + and self.frame_id == other.frame_id + and abs(self.ts - other.ts) < 1e-6 + ) + + def __len__(self) -> int: + return int(self.height * self.width) + + def __getstate__(self): + return {"data": self.data, "format": self.format, "frame_id": self.frame_id, "ts": self.ts} + + def __setstate__(self, state) -> None: + self.__init__( + data=state.get("data"), + format=state.get("format"), + frame_id=state.get("frame_id"), + ts=state.get("ts"), + ) + + +# Re-exports for tests +HAS_CUDA = HAS_CUDA +ImageFormat = ImageFormat +NVIMGCODEC_LAST_USED = NVIMGCODEC_LAST_USED +HAS_NVIMGCODEC = HAS_NVIMGCODEC +__all__ = [ + "HAS_CUDA", + "HAS_NVIMGCODEC", + "NVIMGCODEC_LAST_USED", + "ImageFormat", + "sharpness_barrier", + "sharpness_window", +] + + +def sharpness_window(target_frequency: float, source: Observable[Image]) -> Observable[Image]: + """Emit the sharpest Image seen within each sliding time window.""" + if target_frequency <= 0: + raise ValueError("target_frequency must be positive") + + window = TimestampedBufferCollection(1.0 / target_frequency) + source.subscribe(window.add) + + thread_scheduler = ThreadPoolScheduler(max_workers=1) + + def find_best(*_args): + if not window._items: + return None + return max(window._items, key=lambda img: img.sharpness) + + return rx.interval(1.0 / target_frequency).pipe( + ops.observe_on(thread_scheduler), + ops.map(find_best), + ops.filter(lambda img: img is not None), + ) + + +def sharpness_barrier(target_frequency: float): + """Select the sharpest Image within each time window.""" + if target_frequency <= 0: + raise ValueError("target_frequency must be positive") + return quality_barrier(lambda image: image.sharpness, target_frequency) + + +def _get_lcm_encoding(fmt: ImageFormat, dtype: np.dtype) -> str: + if fmt == ImageFormat.GRAY: + if dtype == np.uint8: + return "mono8" + if dtype == np.uint16: + return "mono16" + if fmt == ImageFormat.GRAY16: + return "mono16" + if fmt == ImageFormat.RGB: + return "rgb8" + if fmt == ImageFormat.RGBA: + return "rgba8" + if fmt == ImageFormat.BGR: + return "bgr8" + if fmt == ImageFormat.BGRA: + return "bgra8" + if fmt == ImageFormat.DEPTH: + if dtype == np.float32: + return "32FC1" + if dtype == np.float64: + return "64FC1" + if fmt == ImageFormat.DEPTH16: + if dtype == np.uint16: + return "16UC1" + if dtype == np.int16: + return "16SC1" + raise ValueError(f"Unsupported LCM encoding for fmt={fmt}, dtype={dtype}") + + +def _parse_lcm_encoding(enc: str): + m = { + "mono8": (ImageFormat.GRAY, np.uint8, 1), + "mono16": (ImageFormat.GRAY16, np.uint16, 1), + "rgb8": (ImageFormat.RGB, np.uint8, 3), + "rgba8": (ImageFormat.RGBA, np.uint8, 4), + "bgr8": (ImageFormat.BGR, np.uint8, 3), + "bgra8": (ImageFormat.BGRA, np.uint8, 4), + "32FC1": (ImageFormat.DEPTH, np.float32, 1), + "32FC3": (ImageFormat.RGB, np.float32, 3), + "64FC1": (ImageFormat.DEPTH, np.float64, 1), + "16UC1": (ImageFormat.DEPTH16, np.uint16, 1), + "16SC1": (ImageFormat.DEPTH16, np.int16, 1), + } + if enc not in m: + raise ValueError(f"Unsupported encoding: {enc}") + return m[enc] diff --git a/dimos/msgs/sensor_msgs/Joy.py b/dimos/msgs/sensor_msgs/Joy.py new file mode 100644 index 0000000000..aa8611655a --- /dev/null +++ b/dimos/msgs/sensor_msgs/Joy.py @@ -0,0 +1,181 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import time +from typing import TypeAlias + +from dimos_lcm.sensor_msgs import Joy as LCMJoy + +try: + from sensor_msgs.msg import Joy as ROSJoy +except ImportError: + ROSJoy = None + +from plum import dispatch + +from dimos.types.timestamped import Timestamped + +# Types that can be converted to/from Joy +JoyConvertable: TypeAlias = ( + tuple[list[float], list[int]] | dict[str, list[float] | list[int]] | LCMJoy +) + + +def sec_nsec(ts): + s = int(ts) + return [s, int((ts - s) * 1_000_000_000)] + + +class Joy(Timestamped): + msg_name = "sensor_msgs.Joy" + ts: float + frame_id: str + axes: list[float] + buttons: list[int] + + @dispatch + def __init__( + self, + ts: float = 0.0, + frame_id: str = "", + axes: list[float] | None = None, + buttons: list[int] | None = None, + ) -> None: + """Initialize a Joy message. + + Args: + ts: Timestamp in seconds + frame_id: Frame ID for the message + axes: List of axis values (typically -1.0 to 1.0) + buttons: List of button states (0 or 1) + """ + self.ts = ts if ts != 0 else time.time() + self.frame_id = frame_id + self.axes = axes if axes is not None else [] + self.buttons = buttons if buttons is not None else [] + + @dispatch + def __init__(self, joy_tuple: tuple[list[float], list[int]]) -> None: + """Initialize from a tuple of (axes, buttons).""" + self.ts = time.time() + self.frame_id = "" + self.axes = list(joy_tuple[0]) + self.buttons = list(joy_tuple[1]) + + @dispatch + def __init__(self, joy_dict: dict[str, list[float] | list[int]]) -> None: + """Initialize from a dictionary with 'axes' and 'buttons' keys.""" + self.ts = joy_dict.get("ts", time.time()) + self.frame_id = joy_dict.get("frame_id", "") + self.axes = list(joy_dict.get("axes", [])) + self.buttons = list(joy_dict.get("buttons", [])) + + @dispatch + def __init__(self, joy: Joy) -> None: + """Initialize from another Joy (copy constructor).""" + self.ts = joy.ts + self.frame_id = joy.frame_id + self.axes = list(joy.axes) + self.buttons = list(joy.buttons) + + @dispatch + def __init__(self, lcm_joy: LCMJoy) -> None: + """Initialize from an LCM Joy message.""" + self.ts = lcm_joy.header.stamp.sec + (lcm_joy.header.stamp.nsec / 1_000_000_000) + self.frame_id = lcm_joy.header.frame_id + self.axes = list(lcm_joy.axes) + self.buttons = list(lcm_joy.buttons) + + def lcm_encode(self) -> bytes: + lcm_msg = LCMJoy() + [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) + lcm_msg.header.frame_id = self.frame_id + lcm_msg.axes_length = len(self.axes) + lcm_msg.axes = self.axes + lcm_msg.buttons_length = len(self.buttons) + lcm_msg.buttons = self.buttons + return lcm_msg.lcm_encode() + + @classmethod + def lcm_decode(cls, data: bytes) -> Joy: + lcm_msg = LCMJoy.lcm_decode(data) + return cls( + ts=lcm_msg.header.stamp.sec + (lcm_msg.header.stamp.nsec / 1_000_000_000), + frame_id=lcm_msg.header.frame_id, + axes=list(lcm_msg.axes) if lcm_msg.axes else [], + buttons=list(lcm_msg.buttons) if lcm_msg.buttons else [], + ) + + def __str__(self) -> str: + return ( + f"Joy(axes={len(self.axes)} values, buttons={len(self.buttons)} values, " + f"frame_id='{self.frame_id}')" + ) + + def __repr__(self) -> str: + return ( + f"Joy(ts={self.ts}, frame_id='{self.frame_id}', " + f"axes={self.axes}, buttons={self.buttons})" + ) + + def __eq__(self, other) -> bool: + """Check if two Joy messages are equal.""" + if not isinstance(other, Joy): + return False + return ( + self.axes == other.axes + and self.buttons == other.buttons + and self.frame_id == other.frame_id + ) + + @classmethod + def from_ros_msg(cls, ros_msg: ROSJoy) -> Joy: + """Create a Joy from a ROS sensor_msgs/Joy message. + + Args: + ros_msg: ROS Joy message + + Returns: + Joy instance + """ + # Convert timestamp from ROS header + ts = ros_msg.header.stamp.sec + (ros_msg.header.stamp.nanosec / 1_000_000_000) + + return cls( + ts=ts, + frame_id=ros_msg.header.frame_id, + axes=list(ros_msg.axes), + buttons=list(ros_msg.buttons), + ) + + def to_ros_msg(self) -> ROSJoy: + """Convert to a ROS sensor_msgs/Joy message. + + Returns: + ROS Joy message + """ + ros_msg = ROSJoy() + + # Set header + ros_msg.header.frame_id = self.frame_id + ros_msg.header.stamp.sec = int(self.ts) + ros_msg.header.stamp.nanosec = int((self.ts - int(self.ts)) * 1_000_000_000) + + # Set axes and buttons + ros_msg.axes = self.axes + ros_msg.buttons = self.buttons + + return ros_msg diff --git a/dimos/msgs/sensor_msgs/PointCloud2.py b/dimos/msgs/sensor_msgs/PointCloud2.py new file mode 100644 index 0000000000..b8de431fa0 --- /dev/null +++ b/dimos/msgs/sensor_msgs/PointCloud2.py @@ -0,0 +1,551 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import functools +import struct + +# Import LCM types +from dimos_lcm.sensor_msgs.PointCloud2 import ( + PointCloud2 as LCMPointCloud2, +) +from dimos_lcm.sensor_msgs.PointField import PointField +from dimos_lcm.std_msgs.Header import Header +import numpy as np +import open3d as o3d + +from dimos.msgs.geometry_msgs import Vector3 + +# Import ROS types +try: + from sensor_msgs.msg import PointCloud2 as ROSPointCloud2, PointField as ROSPointField + from std_msgs.msg import Header as ROSHeader + + ROS_AVAILABLE = True +except ImportError: + ROS_AVAILABLE = False + +from dimos.types.timestamped import Timestamped + + +# TODO: encode/decode need to be updated to work with full spectrum of pointcloud2 fields +class PointCloud2(Timestamped): + msg_name = "sensor_msgs.PointCloud2" + + def __init__( + self, + pointcloud: o3d.geometry.PointCloud = None, + frame_id: str = "world", + ts: float | None = None, + ) -> None: + self.ts = ts + self.pointcloud = pointcloud if pointcloud is not None else o3d.geometry.PointCloud() + self.frame_id = frame_id + + @classmethod + def from_numpy( + cls, points: np.ndarray, frame_id: str = "world", timestamp: float | None = None + ) -> PointCloud2: + """Create PointCloud2 from numpy array of shape (N, 3). + + Args: + points: Nx3 numpy array of 3D points + frame_id: Frame ID for the point cloud + timestamp: Timestamp for the point cloud (defaults to current time) + + Returns: + PointCloud2 instance + """ + pcd = o3d.geometry.PointCloud() + pcd.points = o3d.utility.Vector3dVector(points) + return cls(pointcloud=pcd, ts=timestamp, frame_id=frame_id) + + def __str__(self) -> str: + return f"PointCloud2(frame_id='{self.frame_id}', num_points={len(self.pointcloud.points)})" + + @functools.cached_property + def center(self) -> Vector3: + """Calculate the center of the pointcloud in world frame.""" + center = np.asarray(self.pointcloud.points).mean(axis=0) + return Vector3(*center) + + def points(self): + return self.pointcloud.points + + def __add__(self, other: PointCloud2) -> PointCloud2: + """Combine two PointCloud2 instances into one. + + The resulting point cloud contains points from both inputs. + The frame_id and timestamp are taken from the first point cloud. + + Args: + other: Another PointCloud2 instance to combine with + + Returns: + New PointCloud2 instance containing combined points + """ + if not isinstance(other, PointCloud2): + raise ValueError("Can only add PointCloud2 to another PointCloud2") + + return PointCloud2( + pointcloud=self.pointcloud + other.pointcloud, + frame_id=self.frame_id, + ts=max(self.ts, other.ts), + ) + + # TODO what's the usual storage here? is it already numpy? + def as_numpy(self) -> np.ndarray: + """Get points as numpy array.""" + return np.asarray(self.pointcloud.points) + + @functools.cache + def get_axis_aligned_bounding_box(self) -> o3d.geometry.AxisAlignedBoundingBox: + """Get axis-aligned bounding box of the point cloud.""" + return self.pointcloud.get_axis_aligned_bounding_box() + + @functools.cache + def get_oriented_bounding_box(self) -> o3d.geometry.OrientedBoundingBox: + """Get oriented bounding box of the point cloud.""" + return self.pointcloud.get_oriented_bounding_box() + + @functools.cache + def get_bounding_box_dimensions(self) -> tuple[float, float, float]: + """Get dimensions (width, height, depth) of axis-aligned bounding box.""" + bbox = self.get_axis_aligned_bounding_box() + extent = bbox.get_extent() + return tuple(extent) + + def bounding_box_intersects(self, other: PointCloud2) -> bool: + # Get axis-aligned bounding boxes + bbox1 = self.get_axis_aligned_bounding_box() + bbox2 = other.get_axis_aligned_bounding_box() + + # Get min and max bounds + min1 = bbox1.get_min_bound() + max1 = bbox1.get_max_bound() + min2 = bbox2.get_min_bound() + max2 = bbox2.get_max_bound() + + # Check overlap in all three dimensions + # Boxes intersect if they overlap in ALL dimensions + return ( + min1[0] <= max2[0] + and max1[0] >= min2[0] + and min1[1] <= max2[1] + and max1[1] >= min2[1] + and min1[2] <= max2[2] + and max1[2] >= min2[2] + ) + + def lcm_encode(self, frame_id: str | None = None) -> bytes: + """Convert to LCM PointCloud2 message.""" + msg = LCMPointCloud2() + + # Header + msg.header = Header() + msg.header.seq = 0 # Initialize sequence number + msg.header.frame_id = frame_id or self.frame_id + + msg.header.stamp.sec = int(self.ts) + msg.header.stamp.nsec = int((self.ts - int(self.ts)) * 1e9) + + points = self.as_numpy() + if len(points) == 0: + # Empty point cloud + msg.height = 0 + msg.width = 0 + msg.point_step = 16 # 4 floats * 4 bytes (x, y, z, intensity) + msg.row_step = 0 + msg.data_length = 0 + msg.data = b"" + msg.is_dense = True + msg.is_bigendian = False + msg.fields_length = 4 # x, y, z, intensity + msg.fields = self._create_xyz_field() + return msg.lcm_encode() + + # Point cloud dimensions + msg.height = 1 # Unorganized point cloud + msg.width = len(points) + + # Define fields (X, Y, Z, intensity as float32) + msg.fields_length = 4 # x, y, z, intensity + msg.fields = self._create_xyz_field() + + # Point step and row step + msg.point_step = 16 # 4 floats * 4 bytes each (x, y, z, intensity) + msg.row_step = msg.point_step * msg.width + + # Convert points to bytes with intensity padding (little endian float32) + # Add intensity column (zeros) to make it 4 columns: x, y, z, intensity + points_with_intensity = np.column_stack( + [ + points, # x, y, z columns + np.zeros(len(points), dtype=np.float32), # intensity column (padding) + ] + ) + data_bytes = points_with_intensity.astype(np.float32).tobytes() + msg.data_length = len(data_bytes) + msg.data = data_bytes + + # Properties + msg.is_dense = True # No invalid points + msg.is_bigendian = False # Little endian + + return msg.lcm_encode() + + @classmethod + def lcm_decode(cls, data: bytes) -> PointCloud2: + msg = LCMPointCloud2.lcm_decode(data) + + if msg.width == 0 or msg.height == 0: + # Empty point cloud + pc = o3d.geometry.PointCloud() + return cls( + pointcloud=pc, + frame_id=msg.header.frame_id if hasattr(msg, "header") else "", + ts=msg.header.stamp.sec + msg.header.stamp.nsec / 1e9 + if hasattr(msg, "header") and msg.header.stamp.sec > 0 + else None, + ) + + # Parse field information to find X, Y, Z offsets + x_offset = y_offset = z_offset = None + for msgfield in msg.fields: + if msgfield.name == "x": + x_offset = msgfield.offset + elif msgfield.name == "y": + y_offset = msgfield.offset + elif msgfield.name == "z": + z_offset = msgfield.offset + + if any(offset is None for offset in [x_offset, y_offset, z_offset]): + raise ValueError("PointCloud2 message missing X, Y, or Z msgfields") + + # Extract points from binary data + num_points = msg.width * msg.height + points = np.zeros((num_points, 3), dtype=np.float32) + + data = msg.data + point_step = msg.point_step + + for i in range(num_points): + base_offset = i * point_step + + # Extract X, Y, Z (assuming float32, little endian) + x_bytes = data[base_offset + x_offset : base_offset + x_offset + 4] + y_bytes = data[base_offset + y_offset : base_offset + y_offset + 4] + z_bytes = data[base_offset + z_offset : base_offset + z_offset + 4] + + points[i, 0] = struct.unpack(" 0 + else None, + ) + + def _create_xyz_field(self) -> list: + """Create standard X, Y, Z field definitions for LCM PointCloud2.""" + fields = [] + + # X field + x_field = PointField() + x_field.name = "x" + x_field.offset = 0 + x_field.datatype = 7 # FLOAT32 + x_field.count = 1 + fields.append(x_field) + + # Y field + y_field = PointField() + y_field.name = "y" + y_field.offset = 4 + y_field.datatype = 7 # FLOAT32 + y_field.count = 1 + fields.append(y_field) + + # Z field + z_field = PointField() + z_field.name = "z" + z_field.offset = 8 + z_field.datatype = 7 # FLOAT32 + z_field.count = 1 + fields.append(z_field) + + # I field + i_field = PointField() + i_field.name = "intensity" + i_field.offset = 12 + i_field.datatype = 7 # FLOAT32 + i_field.count = 1 + fields.append(i_field) + + return fields + + def __len__(self) -> int: + """Return number of points.""" + return len(self.pointcloud.points) + + def filter_by_height( + self, + min_height: float | None = None, + max_height: float | None = None, + ) -> PointCloud2: + """Filter points based on their height (z-coordinate). + + This method creates a new PointCloud2 containing only points within the specified + height range. All metadata (frame_id, timestamp) is preserved. + + Args: + min_height: Optional minimum height threshold. Points with z < min_height are filtered out. + If None, no lower limit is applied. + max_height: Optional maximum height threshold. Points with z > max_height are filtered out. + If None, no upper limit is applied. + + Returns: + New PointCloud2 instance containing only the filtered points. + + Raises: + ValueError: If both min_height and max_height are None (no filtering would occur). + + Example: + # Remove ground points below 0.1m height + filtered_pc = pointcloud.filter_by_height(min_height=0.1) + + # Keep only points between ground level and 2m height + filtered_pc = pointcloud.filter_by_height(min_height=0.0, max_height=2.0) + + # Remove points above 1.5m (e.g., ceiling) + filtered_pc = pointcloud.filter_by_height(max_height=1.5) + """ + # Validate that at least one threshold is provided + if min_height is None and max_height is None: + raise ValueError("At least one of min_height or max_height must be specified") + + # Get points as numpy array + points = self.as_numpy() + + if len(points) == 0: + # Empty pointcloud - return a copy + return PointCloud2( + pointcloud=o3d.geometry.PointCloud(), + frame_id=self.frame_id, + ts=self.ts, + ) + + # Extract z-coordinates (height values) - column index 2 + heights = points[:, 2] + + # Create boolean mask for filtering based on height thresholds + # Start with all True values + mask = np.ones(len(points), dtype=bool) + + # Apply minimum height filter if specified + if min_height is not None: + mask &= heights >= min_height + + # Apply maximum height filter if specified + if max_height is not None: + mask &= heights <= max_height + + # Apply mask to filter points + filtered_points = points[mask] + + # Create new PointCloud2 with filtered points + return PointCloud2.from_numpy( + points=filtered_points, + frame_id=self.frame_id, + timestamp=self.ts, + ) + + def __repr__(self) -> str: + """String representation.""" + return f"PointCloud(points={len(self)}, frame_id='{self.frame_id}', ts={self.ts})" + + @classmethod + def from_ros_msg(cls, ros_msg: ROSPointCloud2) -> PointCloud2: + """Convert from ROS sensor_msgs/PointCloud2 message. + + Args: + ros_msg: ROS PointCloud2 message + + Returns: + PointCloud2 instance + """ + if not ROS_AVAILABLE: + raise ImportError("ROS packages not available. Cannot convert from ROS message.") + + # Handle empty point cloud + if ros_msg.width == 0 or ros_msg.height == 0: + pc = o3d.geometry.PointCloud() + return cls( + pointcloud=pc, + frame_id=ros_msg.header.frame_id, + ts=ros_msg.header.stamp.sec + ros_msg.header.stamp.nanosec / 1e9, + ) + + # Parse field information to find X, Y, Z offsets + x_offset = y_offset = z_offset = None + for field in ros_msg.fields: + if field.name == "x": + x_offset = field.offset + elif field.name == "y": + y_offset = field.offset + elif field.name == "z": + z_offset = field.offset + + if any(offset is None for offset in [x_offset, y_offset, z_offset]): + raise ValueError("PointCloud2 message missing X, Y, or Z fields") + + # Extract points from binary data using numpy for bulk conversion + num_points = ros_msg.width * ros_msg.height + data = ros_msg.data + point_step = ros_msg.point_step + + # Determine byte order + byte_order = ">" if ros_msg.is_bigendian else "<" + + # Check if we can use fast numpy path (common case: sequential float32 x,y,z) + if ( + x_offset == 0 + and y_offset == 4 + and z_offset == 8 + and point_step >= 12 + and not ros_msg.is_bigendian + ): + # Fast path: direct numpy reshape for tightly packed float32 x,y,z + # This is the most common case for point clouds + if point_step == 12: + # Perfectly packed x,y,z with no padding + points = np.frombuffer(data, dtype=np.float32).reshape(-1, 3) + else: + # Has additional fields after x,y,z, need to extract with stride + dt = np.dtype( + [("x", " 0: + dt_fields.append(("_pad_x", f"V{x_offset}")) + dt_fields.append(("x", f"{byte_order}f4")) + + # Add padding between x and y if needed + gap_xy = y_offset - x_offset - 4 + if gap_xy > 0: + dt_fields.append(("_pad_xy", f"V{gap_xy}")) + dt_fields.append(("y", f"{byte_order}f4")) + + # Add padding between y and z if needed + gap_yz = z_offset - y_offset - 4 + if gap_yz > 0: + dt_fields.append(("_pad_yz", f"V{gap_yz}")) + dt_fields.append(("z", f"{byte_order}f4")) + + # Add padding at the end to match point_step + remaining = point_step - z_offset - 4 + if remaining > 0: + dt_fields.append(("_pad_end", f"V{remaining}")) + + dt = np.dtype(dt_fields) + structured = np.frombuffer(data, dtype=dt, count=num_points) + points = np.column_stack((structured["x"], structured["y"], structured["z"])) + + # Filter out NaN and Inf values if not dense + if not ros_msg.is_dense: + mask = np.isfinite(points).all(axis=1) + points = points[mask] + + # Create Open3D point cloud + pc = o3d.geometry.PointCloud() + pc.points = o3d.utility.Vector3dVector(points) + + # Extract timestamp + ts = ros_msg.header.stamp.sec + ros_msg.header.stamp.nanosec / 1e9 + + return cls( + pointcloud=pc, + frame_id=ros_msg.header.frame_id, + ts=ts, + ) + + def to_ros_msg(self) -> ROSPointCloud2: + """Convert to ROS sensor_msgs/PointCloud2 message. + + Returns: + ROS PointCloud2 message + """ + if not ROS_AVAILABLE: + raise ImportError("ROS packages not available. Cannot convert to ROS message.") + + ros_msg = ROSPointCloud2() + + # Set header + ros_msg.header = ROSHeader() + ros_msg.header.frame_id = self.frame_id + ros_msg.header.stamp.sec = int(self.ts) + ros_msg.header.stamp.nanosec = int((self.ts - int(self.ts)) * 1e9) + + points = self.as_numpy() + + if len(points) == 0: + # Empty point cloud + ros_msg.height = 0 + ros_msg.width = 0 + ros_msg.fields = [] + ros_msg.is_bigendian = False + ros_msg.point_step = 0 + ros_msg.row_step = 0 + ros_msg.data = b"" + ros_msg.is_dense = True + return ros_msg + + # Set dimensions + ros_msg.height = 1 # Unorganized point cloud + ros_msg.width = len(points) + + # Define fields (X, Y, Z as float32) + ros_msg.fields = [ + ROSPointField(name="x", offset=0, datatype=ROSPointField.FLOAT32, count=1), + ROSPointField(name="y", offset=4, datatype=ROSPointField.FLOAT32, count=1), + ROSPointField(name="z", offset=8, datatype=ROSPointField.FLOAT32, count=1), + ] + + # Set point step and row step + ros_msg.point_step = 12 # 3 floats * 4 bytes each + ros_msg.row_step = ros_msg.point_step * ros_msg.width + + # Convert points to bytes (little endian float32) + ros_msg.data = points.astype(np.float32).tobytes() + + # Set properties + ros_msg.is_bigendian = False # Little endian + ros_msg.is_dense = True # No invalid points + + return ros_msg diff --git a/dimos/msgs/sensor_msgs/__init__.py b/dimos/msgs/sensor_msgs/__init__.py new file mode 100644 index 0000000000..56574e448d --- /dev/null +++ b/dimos/msgs/sensor_msgs/__init__.py @@ -0,0 +1,4 @@ +from dimos.msgs.sensor_msgs.CameraInfo import CameraInfo +from dimos.msgs.sensor_msgs.Image import Image, ImageFormat +from dimos.msgs.sensor_msgs.Joy import Joy +from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2 diff --git a/dimos/msgs/sensor_msgs/image_impls/AbstractImage.py b/dimos/msgs/sensor_msgs/image_impls/AbstractImage.py new file mode 100644 index 0000000000..9dd0c647d2 --- /dev/null +++ b/dimos/msgs/sensor_msgs/image_impls/AbstractImage.py @@ -0,0 +1,210 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from abc import ABC, abstractmethod +import base64 +from enum import Enum +import os +from typing import Any + +import cv2 +import numpy as np + +try: + import cupy as cp # type: ignore + + HAS_CUDA = True +except Exception: # pragma: no cover - optional dependency + cp = None # type: ignore + HAS_CUDA = False + +# Optional nvImageCodec (preferred GPU codec) +USE_NVIMGCODEC = os.environ.get("USE_NVIMGCODEC", "0") == "1" +NVIMGCODEC_LAST_USED = False +try: # pragma: no cover - optional dependency + if HAS_CUDA and USE_NVIMGCODEC: + from nvidia import nvimgcodec # type: ignore + + try: + _enc_probe = nvimgcodec.Encoder() # type: ignore[attr-defined] + HAS_NVIMGCODEC = True + except Exception: + nvimgcodec = None # type: ignore + HAS_NVIMGCODEC = False + else: + nvimgcodec = None # type: ignore + HAS_NVIMGCODEC = False +except Exception: # pragma: no cover - optional dependency + nvimgcodec = None # type: ignore + HAS_NVIMGCODEC = False + + +class ImageFormat(Enum): + BGR = "BGR" + RGB = "RGB" + RGBA = "RGBA" + BGRA = "BGRA" + GRAY = "GRAY" + GRAY16 = "GRAY16" + DEPTH = "DEPTH" + DEPTH16 = "DEPTH16" + + +def _is_cu(x) -> bool: + return HAS_CUDA and cp is not None and isinstance(x, cp.ndarray) # type: ignore + + +def _ascontig(x): + if _is_cu(x): + return x if x.flags["C_CONTIGUOUS"] else cp.ascontiguousarray(x) # type: ignore + return x if x.flags["C_CONTIGUOUS"] else np.ascontiguousarray(x) + + +def _to_cpu(x): + return cp.asnumpy(x) if _is_cu(x) else x # type: ignore + + +def _to_cu(x): + if HAS_CUDA and cp is not None and isinstance(x, np.ndarray): # type: ignore + return cp.asarray(x) # type: ignore + return x + + +def _encode_nvimgcodec_cuda(bgr_cu, quality: int = 80) -> bytes: # pragma: no cover - optional + if not HAS_NVIMGCODEC or nvimgcodec is None: + raise RuntimeError("nvimgcodec not available") + if bgr_cu.ndim != 3 or bgr_cu.shape[2] != 3: + raise RuntimeError("nvimgcodec expects HxWx3 image") + if bgr_cu.dtype != cp.uint8: # type: ignore[attr-defined] + raise RuntimeError("nvimgcodec requires uint8 input") + if not bgr_cu.flags["C_CONTIGUOUS"]: + bgr_cu = cp.ascontiguousarray(bgr_cu) # type: ignore[attr-defined] + encoder = nvimgcodec.Encoder() # type: ignore[attr-defined] + try: + img = nvimgcodec.Image(bgr_cu, nvimgcodec.PixelFormat.BGR) # type: ignore[attr-defined] + except Exception: + img = nvimgcodec.Image(cp.asnumpy(bgr_cu), nvimgcodec.PixelFormat.BGR) # type: ignore[attr-defined] + if hasattr(nvimgcodec, "EncodeParams"): + params = nvimgcodec.EncodeParams(quality=quality) # type: ignore[attr-defined] + bitstreams = encoder.encode([img], [params]) + else: + bitstreams = encoder.encode([img]) + bs0 = bitstreams[0] + if hasattr(bs0, "buf"): + return bytes(bs0.buf) + return bytes(bs0) + + +class AbstractImage(ABC): + data: Any + format: ImageFormat + frame_id: str + ts: float + + @property + @abstractmethod + def is_cuda(self) -> bool: # pragma: no cover - abstract + ... + + @property + def height(self) -> int: + return int(self.data.shape[0]) + + @property + def width(self) -> int: + return int(self.data.shape[1]) + + @property + def channels(self) -> int: + if getattr(self.data, "ndim", 0) == 2: + return 1 + if getattr(self.data, "ndim", 0) == 3: + return int(self.data.shape[2]) + raise ValueError("Invalid image dimensions") + + @property + def shape(self): + return tuple(self.data.shape) + + @property + def dtype(self): + return self.data.dtype + + @abstractmethod + def to_opencv(self) -> np.ndarray: # pragma: no cover - abstract + ... + + @abstractmethod + def to_rgb(self) -> AbstractImage: # pragma: no cover - abstract + ... + + @abstractmethod + def to_bgr(self) -> AbstractImage: # pragma: no cover - abstract + ... + + @abstractmethod + def to_grayscale(self) -> AbstractImage: # pragma: no cover - abstract + ... + + @abstractmethod + def resize( + self, width: int, height: int, interpolation: int = cv2.INTER_LINEAR + ) -> AbstractImage: # pragma: no cover - abstract + ... + + @abstractmethod + def sharpness(self) -> float: # pragma: no cover - abstract + ... + + def copy(self) -> AbstractImage: + return self.__class__( + data=self.data.copy(), format=self.format, frame_id=self.frame_id, ts=self.ts + ) # type: ignore + + def save(self, filepath: str) -> bool: + global NVIMGCODEC_LAST_USED + if self.is_cuda and HAS_NVIMGCODEC and nvimgcodec is not None: + try: + bgr = self.to_bgr() + if _is_cu(bgr.data): + jpeg = _encode_nvimgcodec_cuda(bgr.data) + NVIMGCODEC_LAST_USED = True + with open(filepath, "wb") as f: + f.write(jpeg) + return True + except Exception: + NVIMGCODEC_LAST_USED = False + arr = self.to_opencv() + return cv2.imwrite(filepath, arr) + + def to_base64(self, quality: int = 80) -> str: + global NVIMGCODEC_LAST_USED + if self.is_cuda and HAS_NVIMGCODEC and nvimgcodec is not None: + try: + bgr = self.to_bgr() + if _is_cu(bgr.data): + jpeg = _encode_nvimgcodec_cuda(bgr.data, quality=quality) + NVIMGCODEC_LAST_USED = True + return base64.b64encode(jpeg).decode("utf-8") + except Exception: + NVIMGCODEC_LAST_USED = False + bgr = self.to_bgr() + success, buffer = cv2.imencode( + ".jpg", _to_cpu(bgr.data), [int(cv2.IMWRITE_JPEG_QUALITY), int(quality)] + ) + if not success: + raise ValueError("Failed to encode image as JPEG") + return base64.b64encode(buffer.tobytes()).decode("utf-8") diff --git a/dimos/msgs/sensor_msgs/image_impls/CudaImage.py b/dimos/msgs/sensor_msgs/image_impls/CudaImage.py new file mode 100644 index 0000000000..3067138d36 --- /dev/null +++ b/dimos/msgs/sensor_msgs/image_impls/CudaImage.py @@ -0,0 +1,927 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass, field +import time + +import cv2 +import numpy as np + +from dimos.msgs.sensor_msgs.image_impls.AbstractImage import ( + HAS_CUDA, + AbstractImage, + ImageFormat, + _ascontig, + _is_cu, + _to_cpu, +) + +try: + import cupy as cp # type: ignore + from cupyx.scipy import ( + ndimage as cndimage, # type: ignore + signal as csignal, # type: ignore + ) +except Exception: # pragma: no cover + cp = None # type: ignore + cndimage = None # type: ignore + csignal = None # type: ignore + + +_CUDA_SRC = r""" +extern "C" { + +__device__ __forceinline__ void rodrigues_R(const float r[3], float R[9]){ + float theta = sqrtf(r[0]*r[0] + r[1]*r[1] + r[2]*r[2]); + if(theta < 1e-8f){ + R[0]=1.f; R[1]=0.f; R[2]=0.f; + R[3]=0.f; R[4]=1.f; R[5]=0.f; + R[6]=0.f; R[7]=0.f; R[8]=1.f; + return; + } + float kx=r[0]/theta, ky=r[1]/theta, kz=r[2]/theta; + float c=cosf(theta), s=sinf(theta), v=1.f-c; + R[0]=kx*kx*v + c; R[1]=kx*ky*v - kz*s; R[2]=kx*kz*v + ky*s; + R[3]=ky*kx*v + kz*s; R[4]=ky*ky*v + c; R[5]=ky*kz*v - kx*s; + R[6]=kz*kx*v - ky*s; R[7]=kz*ky*v + kx*s; R[8]=kz*kz*v + c; +} + +__device__ __forceinline__ void mat3x3_vec3(const float R[9], const float x[3], float y[3]){ + y[0] = R[0]*x[0] + R[1]*x[1] + R[2]*x[2]; + y[1] = R[3]*x[0] + R[4]*x[1] + R[5]*x[2]; + y[2] = R[6]*x[0] + R[7]*x[1] + R[8]*x[2]; +} + +__device__ __forceinline__ void cross_mat(const float v[3], float S[9]){ + S[0]=0.f; S[1]=-v[2]; S[2]= v[1]; + S[3]= v[2]; S[4]=0.f; S[5]=-v[0]; + S[6]=-v[1]; S[7]= v[0]; S[8]=0.f; +} + +// Solve a 6x6 system (JTJ * x = JTr) with Gauss-Jordan; JTJ is SPD after damping. +__device__ void solve6_gauss_jordan(float A[36], float b[6], float x[6]){ + float M[6][7]; + #pragma unroll + for(int r=0;r<6;++r){ + #pragma unroll + for(int c=0;c<6;++c) M[r][c] = A[r*6 + c]; + M[r][6] = b[r]; + } + for(int piv=0;piv<6;++piv){ + float invd = 1.f / M[piv][piv]; + for(int c=piv;c<7;++c) M[piv][c] *= invd; + for(int r=0;r<6;++r){ + if(r==piv) continue; + float f = M[r][piv]; + if(fabsf(f) < 1e-20f) continue; + for(int c=piv;c<7;++c) M[r][c] -= f * M[piv][c]; + } + } + #pragma unroll + for(int r=0;r<6;++r) x[r] = M[r][6]; +} + +// One block solves one pose; dynamic shared memory holds per-thread accumulators. +__global__ void pnp_gn_batch( + const float* __restrict__ obj, // (B,N,3) + const float* __restrict__ img, // (B,N,2) + const int N, + const float* __restrict__ intr, // (B,4) -> fx, fy, cx, cy + const int max_iters, + const float damping, + float* __restrict__ rvec_out, // (B,3) + float* __restrict__ tvec_out // (B,3) +){ + if(N <= 0) return; + int b = blockIdx.x; + const float* obj_b = obj + b * N * 3; + const float* img_b = img + b * N * 2; + float fx = intr[4*b + 0]; + float fy = intr[4*b + 1]; + float cx = intr[4*b + 2]; + float cy = intr[4*b + 3]; + + __shared__ float s_R[9]; + __shared__ float s_rvec[3]; + __shared__ float s_tvec[3]; + __shared__ float s_JTJ[36]; + __shared__ float s_JTr[6]; + __shared__ int s_done; + + extern __shared__ float scratch[]; + float* sh_JTJ = scratch; + float* sh_JTr = scratch + 36 * blockDim.x; + + if(threadIdx.x==0){ + s_rvec[0]=0.f; s_rvec[1]=0.f; s_rvec[2]=0.f; + s_tvec[0]=0.f; s_tvec[1]=0.f; s_tvec[2]=2.f; + } + __syncthreads(); + + for(int it=0; itmatrix) for NumPy/CuPy arrays.""" + + if cp is not None and ( + isinstance(x, cp.ndarray) # type: ignore[arg-type] + or getattr(x, "__cuda_array_interface__", None) is not None + ): + xp = cp + else: + xp = np + arr = xp.asarray(x, dtype=xp.float64) + + if not inverse and arr.ndim >= 2 and arr.shape[-2:] == (3, 3): + inverse = True + + if not inverse: + vec = arr + if vec.ndim >= 2 and vec.shape[-1] == 1: + vec = vec[..., 0] + if vec.shape[-1] != 3: + raise ValueError("Rodrigues expects vectors of shape (..., 3)") + orig_shape = vec.shape[:-1] + vec = vec.reshape(-1, 3) + n = vec.shape[0] + theta = xp.linalg.norm(vec, axis=1) + small = theta < 1e-12 + + def _skew(v): + vx, vy, vz = v[:, 0], v[:, 1], v[:, 2] + O = xp.zeros_like(vx) + return xp.stack( + [ + xp.stack([O, -vz, vy], axis=-1), + xp.stack([vz, O, -vx], axis=-1), + xp.stack([-vy, vx, O], axis=-1), + ], + axis=-2, + ) + + K = _skew(vec) + theta2 = theta * theta + theta4 = theta2 * theta2 + theta_safe = xp.where(small, 1.0, theta) + theta2_safe = xp.where(small, 1.0, theta2) + A = xp.where(small, 1.0 - theta2 / 6.0 + theta4 / 120.0, xp.sin(theta) / theta_safe)[ + :, None, None + ] + B = xp.where( + small, + 0.5 - theta2 / 24.0 + theta4 / 720.0, + (1.0 - xp.cos(theta)) / theta2_safe, + )[:, None, None] + I = xp.eye(3, dtype=arr.dtype) + I = I[None, :, :] if n == 1 else xp.broadcast_to(I, (n, 3, 3)) + KK = xp.matmul(K, K) + out = I + A * K + B * KK + return out.reshape((*orig_shape, 3, 3)) if orig_shape else out[0] + + mat = arr + if mat.shape[-2:] != (3, 3): + raise ValueError("Rodrigues expects rotation matrices of shape (..., 3, 3)") + orig_shape = mat.shape[:-2] + mat = mat.reshape(-1, 3, 3) + trace = xp.trace(mat, axis1=1, axis2=2) + trace = xp.clip((trace - 1.0) / 2.0, -1.0, 1.0) + theta = xp.arccos(trace) + v = xp.stack( + [ + mat[:, 2, 1] - mat[:, 1, 2], + mat[:, 0, 2] - mat[:, 2, 0], + mat[:, 1, 0] - mat[:, 0, 1], + ], + axis=1, + ) + norm_v = xp.linalg.norm(v, axis=1) + small = theta < 1e-7 + eps = 1e-8 + norm_safe = xp.where(norm_v < eps, 1.0, norm_v) + r_general = theta[:, None] * v / norm_safe[:, None] + r_small = 0.5 * v + r = xp.where(small[:, None], r_small, r_general) + pi_mask = xp.abs(theta - xp.pi) < 1e-4 + if np.any(pi_mask) if xp is np else bool(cp.asnumpy(pi_mask).any()): + diag = xp.diagonal(mat, axis1=1, axis2=2) + axis_candidates = xp.clip((diag + 1.0) / 2.0, 0.0, None) + axis = xp.sqrt(axis_candidates) + signs = xp.sign(v) + axis = xp.where(signs == 0, axis, xp.copysign(axis, signs)) + axis_norm = xp.linalg.norm(axis, axis=1) + axis_norm = xp.where(axis_norm < eps, 1.0, axis_norm) + axis = axis / axis_norm[:, None] + r_pi = theta[:, None] * axis + r = xp.where(pi_mask[:, None], r_pi, r) + out = r.reshape((*orig_shape, 3)) if orig_shape else r[0] + return out + + +def _undistort_points_cuda( + img_px: cp.ndarray, K: cp.ndarray, dist: cp.ndarray, iterations: int = 8 +) -> cp.ndarray: + """Iteratively undistort pixel coordinates on device (Brown–Conrady). + + Returns pixel coordinates after undistortion (fx*xu+cx, fy*yu+cy). + """ + N = img_px.shape[0] + ones = cp.ones((N, 1), dtype=cp.float64) + uv1 = cp.concatenate([img_px.astype(cp.float64), ones], axis=1) + Kinv = cp.linalg.inv(K) + xdyd1 = uv1 @ Kinv.T + xd = xdyd1[:, 0] + yd = xdyd1[:, 1] + xu = xd.copy() + yu = yd.copy() + k1 = dist[0] + k2 = dist[1] if dist.size > 1 else 0.0 + p1 = dist[2] if dist.size > 2 else 0.0 + p2 = dist[3] if dist.size > 3 else 0.0 + k3 = dist[4] if dist.size > 4 else 0.0 + for _ in range(iterations): + r2 = xu * xu + yu * yu + r4 = r2 * r2 + r6 = r4 * r2 + radial = 1.0 + k1 * r2 + k2 * r4 + k3 * r6 + delta_x = 2.0 * p1 * xu * yu + p2 * (r2 + 2.0 * xu * xu) + delta_y = p1 * (r2 + 2.0 * yu * yu) + 2.0 * p2 * xu * yu + xu = (xd - delta_x) / radial + yu = (yd - delta_y) / radial + fx, fy, cx, cy = K[0, 0], K[1, 1], K[0, 2], K[1, 2] + return cp.stack([fx * xu + cx, fy * yu + cy], axis=1) + + +@dataclass +class CudaImage(AbstractImage): + data: any # cupy.ndarray + format: ImageFormat = field(default=ImageFormat.BGR) + frame_id: str = field(default="") + ts: float = field(default_factory=time.time) + + def __post_init__(self): + if not HAS_CUDA or cp is None: + raise RuntimeError("CuPy/CUDA not available") + if not _is_cu(self.data): + # Accept NumPy arrays and move to device automatically + try: + self.data = cp.asarray(self.data) + except Exception as e: + raise ValueError("CudaImage requires a CuPy array") from e + if self.data.ndim < 2: + raise ValueError("Image data must be at least 2D") + self.data = _ascontig(self.data) + + @property + def is_cuda(self) -> bool: + return True + + def to_opencv(self) -> np.ndarray: + if self.format in (ImageFormat.BGR, ImageFormat.RGB, ImageFormat.RGBA, ImageFormat.BGRA): + return _to_cpu(self.to_bgr().data) + return _to_cpu(self.data) + + def to_rgb(self) -> CudaImage: + if self.format == ImageFormat.RGB: + return self.copy() # type: ignore + if self.format == ImageFormat.BGR: + return CudaImage(_bgr_to_rgb_cuda(self.data), ImageFormat.RGB, self.frame_id, self.ts) + if self.format == ImageFormat.RGBA: + return self.copy() # type: ignore + if self.format == ImageFormat.BGRA: + return CudaImage( + _bgra_to_rgba_cuda(self.data), ImageFormat.RGBA, self.frame_id, self.ts + ) + if self.format == ImageFormat.GRAY: + return CudaImage(_gray_to_rgb_cuda(self.data), ImageFormat.RGB, self.frame_id, self.ts) + if self.format in (ImageFormat.GRAY16, ImageFormat.DEPTH16): + gray8 = (self.data.astype(cp.float32) / 256.0).clip(0, 255).astype(cp.uint8) # type: ignore + return CudaImage(_gray_to_rgb_cuda(gray8), ImageFormat.RGB, self.frame_id, self.ts) + return self.copy() # type: ignore + + def to_bgr(self) -> CudaImage: + if self.format == ImageFormat.BGR: + return self.copy() # type: ignore + if self.format == ImageFormat.RGB: + return CudaImage(_rgb_to_bgr_cuda(self.data), ImageFormat.BGR, self.frame_id, self.ts) + if self.format == ImageFormat.RGBA: + return CudaImage( + _rgba_to_bgra_cuda(self.data)[..., :3], ImageFormat.BGR, self.frame_id, self.ts + ) + if self.format == ImageFormat.BGRA: + return CudaImage(self.data[..., :3], ImageFormat.BGR, self.frame_id, self.ts) + if self.format in (ImageFormat.GRAY, ImageFormat.DEPTH): + return CudaImage( + _rgb_to_bgr_cuda(_gray_to_rgb_cuda(self.data)), + ImageFormat.BGR, + self.frame_id, + self.ts, + ) + if self.format in (ImageFormat.GRAY16, ImageFormat.DEPTH16): + gray8 = (self.data.astype(cp.float32) / 256.0).clip(0, 255).astype(cp.uint8) # type: ignore + return CudaImage( + _rgb_to_bgr_cuda(_gray_to_rgb_cuda(gray8)), ImageFormat.BGR, self.frame_id, self.ts + ) + return self.copy() # type: ignore + + def to_grayscale(self) -> CudaImage: + if self.format in (ImageFormat.GRAY, ImageFormat.GRAY16, ImageFormat.DEPTH): + return self.copy() # type: ignore + if self.format == ImageFormat.BGR: + return CudaImage( + _rgb_to_gray_cuda(_bgr_to_rgb_cuda(self.data)), + ImageFormat.GRAY, + self.frame_id, + self.ts, + ) + if self.format == ImageFormat.RGB: + return CudaImage(_rgb_to_gray_cuda(self.data), ImageFormat.GRAY, self.frame_id, self.ts) + if self.format in (ImageFormat.RGBA, ImageFormat.BGRA): + rgb = ( + self.data[..., :3] + if self.format == ImageFormat.RGBA + else _bgra_to_rgba_cuda(self.data)[..., :3] + ) + return CudaImage(_rgb_to_gray_cuda(rgb), ImageFormat.GRAY, self.frame_id, self.ts) + raise ValueError(f"Unsupported format: {self.format}") + + def resize(self, width: int, height: int, interpolation: int = cv2.INTER_LINEAR) -> CudaImage: + return CudaImage( + _resize_bilinear_hwc_cuda(self.data, height, width), self.format, self.frame_id, self.ts + ) + + def crop(self, x: int, y: int, width: int, height: int) -> CudaImage: + """Crop the image to the specified region. + + Args: + x: Starting x coordinate (left edge) + y: Starting y coordinate (top edge) + width: Width of the cropped region + height: Height of the cropped region + + Returns: + A new CudaImage containing the cropped region + """ + # Get current image dimensions + img_height, img_width = self.data.shape[:2] + + # Clamp the crop region to image bounds + x = max(0, min(x, img_width)) + y = max(0, min(y, img_height)) + x_end = min(x + width, img_width) + y_end = min(y + height, img_height) + + # Perform the crop using array slicing + if self.data.ndim == 2: + # Grayscale image + cropped_data = self.data[y:y_end, x:x_end] + else: + # Color image (HxWxC) + cropped_data = self.data[y:y_end, x:x_end, :] + + # Return a new CudaImage with the cropped data + return CudaImage(cropped_data, self.format, self.frame_id, self.ts) + + def sharpness(self) -> float: + if cp is None: + return 0.0 + try: + from cupyx.scipy import ndimage as cndimage # type: ignore + + gray = self.to_grayscale().data.astype(cp.float32) + deriv5 = cp.asarray([1, 2, 0, -2, -1], dtype=cp.float32) + smooth5 = cp.asarray([1, 4, 6, 4, 1], dtype=cp.float32) + gx = cndimage.convolve1d(gray, deriv5, axis=1, mode="reflect") # type: ignore + gx = cndimage.convolve1d(gx, smooth5, axis=0, mode="reflect") # type: ignore + gy = cndimage.convolve1d(gray, deriv5, axis=0, mode="reflect") # type: ignore + gy = cndimage.convolve1d(gy, smooth5, axis=1, mode="reflect") # type: ignore + magnitude = cp.hypot(gx, gy) # type: ignore + mean_mag = float(cp.asnumpy(magnitude.mean())) # type: ignore + except Exception: + return 0.0 + if mean_mag <= 0: + return 0.0 + return float(np.clip((np.log10(mean_mag + 1) - 1.7) / 2.0, 0.0, 1.0)) + + # CUDA tracker (template NCC with small scale pyramid) + @dataclass + class BBox: + x: int + y: int + w: int + h: int + + def create_csrt_tracker(self, bbox: BBox): + if csignal is None: + raise RuntimeError("cupyx.scipy.signal not available for CUDA tracker") + x, y, w, h = map(int, bbox) + gray = self.to_grayscale().data.astype(cp.float32) + tmpl = gray[y : y + h, x : x + w] + if tmpl.size == 0: + raise ValueError("Invalid bbox for CUDA tracker") + return _CudaTemplateTracker(tmpl, x0=x, y0=y) + + def csrt_update(self, tracker) -> tuple[bool, tuple[int, int, int, int]]: + if not isinstance(tracker, _CudaTemplateTracker): + raise TypeError("Expected CUDA tracker instance") + gray = self.to_grayscale().data.astype(cp.float32) + x, y, w, h = tracker.update(gray) + return True, (int(x), int(y), int(w), int(h)) + + # PnP – Gauss–Newton (no distortion in batch), iterative per-instance + def solve_pnp( + self, + object_points: np.ndarray, + image_points: np.ndarray, + camera_matrix: np.ndarray, + dist_coeffs: np.ndarray | None = None, + flags: int = cv2.SOLVEPNP_ITERATIVE, + ) -> tuple[bool, np.ndarray, np.ndarray]: + if not HAS_CUDA or cp is None or (dist_coeffs is not None and np.any(dist_coeffs)): + obj = np.asarray(object_points, dtype=np.float32).reshape(-1, 3) + img = np.asarray(image_points, dtype=np.float32).reshape(-1, 2) + K = np.asarray(camera_matrix, dtype=np.float64) + dist = None if dist_coeffs is None else np.asarray(dist_coeffs, dtype=np.float64) + ok, rvec, tvec = cv2.solvePnP(obj, img, K, dist, flags=flags) + return bool(ok), rvec.astype(np.float64), tvec.astype(np.float64) + + rvec, tvec = _solve_pnp_cuda_kernel(object_points, image_points, camera_matrix) + ok = np.isfinite(rvec).all() and np.isfinite(tvec).all() + return ok, rvec, tvec + + def solve_pnp_batch( + self, + object_points_batch: np.ndarray, + image_points_batch: np.ndarray, + camera_matrix: np.ndarray, + dist_coeffs: np.ndarray | None = None, + iterations: int = 15, + damping: float = 1e-6, + ) -> tuple[np.ndarray, np.ndarray]: + """Batched PnP (each block = one instance).""" + if not HAS_CUDA or cp is None or (dist_coeffs is not None and np.any(dist_coeffs)): + obj = np.asarray(object_points_batch, dtype=np.float32) + img = np.asarray(image_points_batch, dtype=np.float32) + if obj.ndim != 3 or img.ndim != 3 or obj.shape[:2] != img.shape[:2]: + raise ValueError( + "Batched object/image arrays must be shaped (B,N,...) with matching sizes" + ) + K = np.asarray(camera_matrix, dtype=np.float64) + dist = None if dist_coeffs is None else np.asarray(dist_coeffs, dtype=np.float64) + B = obj.shape[0] + r_list = np.empty((B, 3, 1), dtype=np.float64) + t_list = np.empty((B, 3, 1), dtype=np.float64) + for b in range(B): + K_b = K if K.ndim == 2 else K[b] + dist_b = None + if dist is not None: + if dist.ndim == 1: + dist_b = dist + elif dist.ndim == 2: + dist_b = dist[b] + else: + raise ValueError("dist_coeffs must be 1D or batched 2D") + ok, rvec, tvec = cv2.solvePnP( + obj[b], img[b], K_b, dist_b, flags=cv2.SOLVEPNP_ITERATIVE + ) + if not ok: + raise RuntimeError(f"cv2.solvePnP failed for batch index {b}") + r_list[b] = rvec.astype(np.float64) + t_list[b] = tvec.astype(np.float64) + return r_list, t_list + + return _solve_pnp_cuda_kernel( + object_points_batch, + image_points_batch, + camera_matrix, + iterations=iterations, + damping=damping, + ) + + def solve_pnp_ransac( + self, + object_points: np.ndarray, + image_points: np.ndarray, + camera_matrix: np.ndarray, + dist_coeffs: np.ndarray | None = None, + iterations_count: int = 100, + reprojection_error: float = 3.0, + confidence: float = 0.99, + min_sample: int = 6, + ) -> tuple[bool, np.ndarray, np.ndarray, np.ndarray]: + """RANSAC with CUDA PnP solver.""" + if not HAS_CUDA or cp is None or (dist_coeffs is not None and np.any(dist_coeffs)): + obj = np.asarray(object_points, dtype=np.float32) + img = np.asarray(image_points, dtype=np.float32) + K = np.asarray(camera_matrix, dtype=np.float64) + dist = None if dist_coeffs is None else np.asarray(dist_coeffs, dtype=np.float64) + ok, rvec, tvec, mask = cv2.solvePnPRansac( + obj, + img, + K, + dist, + iterationsCount=int(iterations_count), + reprojectionError=float(reprojection_error), + confidence=float(confidence), + flags=cv2.SOLVEPNP_ITERATIVE, + ) + mask_flat = np.zeros((obj.shape[0],), dtype=np.uint8) + if mask is not None and len(mask) > 0: + mask_flat[mask.flatten()] = 1 + return bool(ok), rvec.astype(np.float64), tvec.astype(np.float64), mask_flat + + obj = cp.asarray(object_points, dtype=cp.float32) + img = cp.asarray(image_points, dtype=cp.float32) + camera_matrix_np = np.asarray(_to_cpu(camera_matrix), dtype=np.float32) + fx = float(camera_matrix_np[0, 0]) + fy = float(camera_matrix_np[1, 1]) + cx = float(camera_matrix_np[0, 2]) + cy = float(camera_matrix_np[1, 2]) + N = obj.shape[0] + rng = cp.random.RandomState(1234) + best_inliers = -1 + _best_r, _best_t, best_mask = None, None, None + + for _ in range(iterations_count): + idx = rng.choice(N, size=min_sample, replace=False) + rvec, tvec = _solve_pnp_cuda_kernel(obj[idx], img[idx], camera_matrix_np) + R = _rodrigues(cp.asarray(rvec.flatten())) + Xc = obj @ R.T + cp.asarray(tvec.flatten()) + invZ = 1.0 / cp.clip(Xc[:, 2], 1e-6, None) + u_hat = fx * Xc[:, 0] * invZ + cx + v_hat = fy * Xc[:, 1] * invZ + cy + err = cp.sqrt((img[:, 0] - u_hat) ** 2 + (img[:, 1] - v_hat) ** 2) + mask = (err < reprojection_error).astype(cp.uint8) + inliers = int(mask.sum()) + if inliers > best_inliers: + best_inliers, _best_r, _best_t, best_mask = inliers, rvec, tvec, mask + if inliers >= int(confidence * N): + break + + if best_inliers <= 0: + return False, np.zeros((3, 1)), np.zeros((3, 1)), np.zeros((N,), dtype=np.uint8) + in_idx = cp.nonzero(best_mask)[0] + rvec, tvec = _solve_pnp_cuda_kernel(obj[in_idx], img[in_idx], camera_matrix_np) + return True, rvec, tvec, cp.asnumpy(best_mask) + + +class _CudaTemplateTracker: + def __init__( + self, + tmpl: cp.ndarray, + scale_step: float = 1.05, + lr: float = 0.1, + search_radius: int = 16, + x0: int = 0, + y0: int = 0, + ) -> None: + self.tmpl = tmpl.astype(cp.float32) + self.h, self.w = int(tmpl.shape[0]), int(tmpl.shape[1]) + self.scale_step = float(scale_step) + self.lr = float(lr) + self.search_radius = int(search_radius) + # Cosine window + wy = cp.hanning(self.h).astype(cp.float32) + wx = cp.hanning(self.w).astype(cp.float32) + self.window = wy[:, None] * wx[None, :] + self.tmpl = self.tmpl * self.window + self.y = int(y0) + self.x = int(x0) + + def update(self, gray: cp.ndarray): + H, W = int(gray.shape[0]), int(gray.shape[1]) + r = self.search_radius + x0 = max(0, self.x - r) + y0 = max(0, self.y - r) + x1 = min(W, self.x + self.w + r) + y1 = min(H, self.y + self.h + r) + search = gray[y0:y1, x0:x1] + if search.shape[0] < self.h or search.shape[1] < self.w: + search = gray + x0 = y0 = 0 + best = (self.x, self.y, self.w, self.h) + best_score = -1e9 + for s in (1.0 / self.scale_step, 1.0, self.scale_step): + th = max(1, round(self.h * s)) + tw = max(1, round(self.w * s)) + tmpl_s = _resize_bilinear_hwc_cuda(self.tmpl, th, tw) + if tmpl_s.ndim == 3: + tmpl_s = tmpl_s[..., 0] + tmpl_s = tmpl_s.astype(cp.float32) + tmpl_zm = tmpl_s - tmpl_s.mean() + tmpl_energy = cp.sqrt(cp.sum(tmpl_zm * tmpl_zm)) + 1e-6 + # NCC via correlate2d and local std + ones = cp.ones((th, tw), dtype=cp.float32) + num = csignal.correlate2d(search, tmpl_zm, mode="valid") # type: ignore + sumS = csignal.correlate2d(search, ones, mode="valid") # type: ignore + sumS2 = csignal.correlate2d(search * search, ones, mode="valid") # type: ignore + n = float(th * tw) + meanS = sumS / n + varS = cp.clip(sumS2 - n * meanS * meanS, 0.0, None) + stdS = cp.sqrt(varS) + 1e-6 + res = num / (stdS * tmpl_energy) + ij = cp.unravel_index(cp.argmax(res), res.shape) + dy, dx = int(ij[0].get()), int(ij[1].get()) # type: ignore + score = float(res[ij].get()) # type: ignore + if score > best_score: + best_score = score + best = (x0 + dx, y0 + dy, tw, th) + x, y, w, h = best + patch = gray[y : y + h, x : x + w] + if patch.shape[0] != self.h or patch.shape[1] != self.w: + patch = _resize_bilinear_hwc_cuda(patch, self.h, self.w) + if patch.ndim == 3: + patch = patch[..., 0] + patch = patch.astype(cp.float32) * self.window + self.tmpl = (1.0 - self.lr) * self.tmpl + self.lr * patch + self.x, self.y, self.w, self.h = x, y, w, h + return x, y, w, h diff --git a/dimos/msgs/sensor_msgs/image_impls/NumpyImage.py b/dimos/msgs/sensor_msgs/image_impls/NumpyImage.py new file mode 100644 index 0000000000..d75adc66ea --- /dev/null +++ b/dimos/msgs/sensor_msgs/image_impls/NumpyImage.py @@ -0,0 +1,243 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass, field +import time + +import cv2 +import numpy as np + +from dimos.msgs.sensor_msgs.image_impls.AbstractImage import ( + AbstractImage, + ImageFormat, +) + + +@dataclass +class NumpyImage(AbstractImage): + data: np.ndarray + format: ImageFormat = field(default=ImageFormat.BGR) + frame_id: str = field(default="") + ts: float = field(default_factory=time.time) + + def __post_init__(self): + if not isinstance(self.data, np.ndarray) or self.data.ndim < 2: + raise ValueError("NumpyImage requires a 2D/3D NumPy array") + + @property + def is_cuda(self) -> bool: + return False + + def to_opencv(self) -> np.ndarray: + arr = self.data + if self.format == ImageFormat.BGR: + return arr + if self.format == ImageFormat.RGB: + return cv2.cvtColor(arr, cv2.COLOR_RGB2BGR) + if self.format == ImageFormat.RGBA: + return cv2.cvtColor(arr, cv2.COLOR_RGBA2BGR) + if self.format == ImageFormat.BGRA: + return cv2.cvtColor(arr, cv2.COLOR_BGRA2BGR) + if self.format in ( + ImageFormat.GRAY, + ImageFormat.GRAY16, + ImageFormat.DEPTH, + ImageFormat.DEPTH16, + ): + return arr + raise ValueError(f"Unsupported format: {self.format}") + + def to_rgb(self) -> NumpyImage: + if self.format == ImageFormat.RGB: + return self.copy() # type: ignore + arr = self.data + if self.format == ImageFormat.BGR: + return NumpyImage( + cv2.cvtColor(arr, cv2.COLOR_BGR2RGB), ImageFormat.RGB, self.frame_id, self.ts + ) + if self.format == ImageFormat.RGBA: + return self.copy() # RGBA contains RGB + alpha + if self.format == ImageFormat.BGRA: + rgba = cv2.cvtColor(arr, cv2.COLOR_BGRA2RGBA) + return NumpyImage(rgba, ImageFormat.RGBA, self.frame_id, self.ts) + if self.format in (ImageFormat.GRAY, ImageFormat.GRAY16, ImageFormat.DEPTH16): + gray8 = (arr / 256).astype(np.uint8) if self.format != ImageFormat.GRAY else arr + rgb = cv2.cvtColor(gray8, cv2.COLOR_GRAY2RGB) + return NumpyImage(rgb, ImageFormat.RGB, self.frame_id, self.ts) + return self.copy() # type: ignore + + def to_bgr(self) -> NumpyImage: + if self.format == ImageFormat.BGR: + return self.copy() # type: ignore + arr = self.data + if self.format == ImageFormat.RGB: + return NumpyImage( + cv2.cvtColor(arr, cv2.COLOR_RGB2BGR), ImageFormat.BGR, self.frame_id, self.ts + ) + if self.format == ImageFormat.RGBA: + return NumpyImage( + cv2.cvtColor(arr, cv2.COLOR_RGBA2BGR), ImageFormat.BGR, self.frame_id, self.ts + ) + if self.format == ImageFormat.BGRA: + return NumpyImage( + cv2.cvtColor(arr, cv2.COLOR_BGRA2BGR), ImageFormat.BGR, self.frame_id, self.ts + ) + if self.format in (ImageFormat.GRAY, ImageFormat.GRAY16, ImageFormat.DEPTH16): + gray8 = (arr / 256).astype(np.uint8) if self.format != ImageFormat.GRAY else arr + return NumpyImage( + cv2.cvtColor(gray8, cv2.COLOR_GRAY2BGR), ImageFormat.BGR, self.frame_id, self.ts + ) + return self.copy() # type: ignore + + def to_grayscale(self) -> NumpyImage: + if self.format in (ImageFormat.GRAY, ImageFormat.GRAY16, ImageFormat.DEPTH): + return self.copy() # type: ignore + if self.format == ImageFormat.BGR: + return NumpyImage( + cv2.cvtColor(self.data, cv2.COLOR_BGR2GRAY), + ImageFormat.GRAY, + self.frame_id, + self.ts, + ) + if self.format == ImageFormat.RGB: + return NumpyImage( + cv2.cvtColor(self.data, cv2.COLOR_RGB2GRAY), + ImageFormat.GRAY, + self.frame_id, + self.ts, + ) + if self.format in (ImageFormat.RGBA, ImageFormat.BGRA): + code = cv2.COLOR_RGBA2GRAY if self.format == ImageFormat.RGBA else cv2.COLOR_BGRA2GRAY + return NumpyImage( + cv2.cvtColor(self.data, code), ImageFormat.GRAY, self.frame_id, self.ts + ) + raise ValueError(f"Unsupported format: {self.format}") + + def resize(self, width: int, height: int, interpolation: int = cv2.INTER_LINEAR) -> NumpyImage: + return NumpyImage( + cv2.resize(self.data, (width, height), interpolation=interpolation), + self.format, + self.frame_id, + self.ts, + ) + + def crop(self, x: int, y: int, width: int, height: int) -> NumpyImage: + """Crop the image to the specified region. + + Args: + x: Starting x coordinate (left edge) + y: Starting y coordinate (top edge) + width: Width of the cropped region + height: Height of the cropped region + + Returns: + A new NumpyImage containing the cropped region + """ + # Get current image dimensions + img_height, img_width = self.data.shape[:2] + + # Clamp the crop region to image bounds + x = max(0, min(x, img_width)) + y = max(0, min(y, img_height)) + x_end = min(x + width, img_width) + y_end = min(y + height, img_height) + + # Perform the crop using array slicing + if self.data.ndim == 2: + # Grayscale image + cropped_data = self.data[y:y_end, x:x_end] + else: + # Color image (HxWxC) + cropped_data = self.data[y:y_end, x:x_end, :] + + # Return a new NumpyImage with the cropped data + return NumpyImage(cropped_data, self.format, self.frame_id, self.ts) + + def sharpness(self) -> float: + gray = self.to_grayscale() + sx = cv2.Sobel(gray.data, cv2.CV_32F, 1, 0, ksize=5) + sy = cv2.Sobel(gray.data, cv2.CV_32F, 0, 1, ksize=5) + magnitude = cv2.magnitude(sx, sy) + mean_mag = float(magnitude.mean()) + if mean_mag <= 0: + return 0.0 + return float(np.clip((np.log10(mean_mag + 1) - 1.7) / 2.0, 0.0, 1.0)) + + # PnP wrappers + def solve_pnp( + self, + object_points: np.ndarray, + image_points: np.ndarray, + camera_matrix: np.ndarray, + dist_coeffs: np.ndarray | None = None, + flags: int = cv2.SOLVEPNP_ITERATIVE, + ) -> tuple[bool, np.ndarray, np.ndarray]: + obj = np.asarray(object_points, dtype=np.float32).reshape(-1, 3) + img = np.asarray(image_points, dtype=np.float32).reshape(-1, 2) + K = np.asarray(camera_matrix, dtype=np.float64) + dist = None if dist_coeffs is None else np.asarray(dist_coeffs, dtype=np.float64) + ok, rvec, tvec = cv2.solvePnP(obj, img, K, dist, flags=flags) + return bool(ok), rvec.astype(np.float64), tvec.astype(np.float64) + + def create_csrt_tracker(self, bbox: tuple[int, int, int, int]): + tracker = None + if hasattr(cv2, "legacy") and hasattr(cv2.legacy, "TrackerCSRT_create"): + tracker = cv2.legacy.TrackerCSRT_create() + elif hasattr(cv2, "TrackerCSRT_create"): + tracker = cv2.TrackerCSRT_create() + else: + raise RuntimeError("OpenCV CSRT tracker not available") + ok = tracker.init(self.to_bgr().to_opencv(), tuple(map(int, bbox))) + if not ok: + raise RuntimeError("Failed to initialize CSRT tracker") + return tracker + + def csrt_update(self, tracker) -> tuple[bool, tuple[int, int, int, int]]: + ok, box = tracker.update(self.to_bgr().to_opencv()) + if not ok: + return False, (0, 0, 0, 0) + x, y, w, h = map(int, box) + return True, (x, y, w, h) + + def solve_pnp_ransac( + self, + object_points: np.ndarray, + image_points: np.ndarray, + camera_matrix: np.ndarray, + dist_coeffs: np.ndarray | None = None, + iterations_count: int = 100, + reprojection_error: float = 3.0, + confidence: float = 0.99, + min_sample: int = 6, + ) -> tuple[bool, np.ndarray, np.ndarray, np.ndarray]: + obj = np.asarray(object_points, dtype=np.float32).reshape(-1, 3) + img = np.asarray(image_points, dtype=np.float32).reshape(-1, 2) + K = np.asarray(camera_matrix, dtype=np.float64) + dist = None if dist_coeffs is None else np.asarray(dist_coeffs, dtype=np.float64) + ok, rvec, tvec, inliers = cv2.solvePnPRansac( + obj, + img, + K, + dist, + iterationsCount=int(iterations_count), + reprojectionError=float(reprojection_error), + confidence=float(confidence), + flags=cv2.SOLVEPNP_ITERATIVE, + ) + mask = np.zeros((obj.shape[0],), dtype=np.uint8) + if inliers is not None and len(inliers) > 0: + mask[inliers.flatten()] = 1 + return bool(ok), rvec.astype(np.float64), tvec.astype(np.float64), mask diff --git a/dimos/msgs/sensor_msgs/image_impls/test_image_backend_utils.py b/dimos/msgs/sensor_msgs/image_impls/test_image_backend_utils.py new file mode 100644 index 0000000000..c226e36bf0 --- /dev/null +++ b/dimos/msgs/sensor_msgs/image_impls/test_image_backend_utils.py @@ -0,0 +1,287 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pytest + +from dimos.msgs.sensor_msgs import Image, ImageFormat + +try: + HAS_CUDA = True + print("Running image backend utils tests with CUDA/CuPy support (GPU mode)") +except: + HAS_CUDA = False + print("Running image backend utils tests in CPU-only mode") + +from dimos.perception.common.utils import ( + colorize_depth, + draw_bounding_box, + draw_object_detection_visualization, + draw_segmentation_mask, + project_2d_points_to_3d, + project_3d_points_to_2d, + rectify_image, +) + + +def _has_cupy() -> bool: + try: + import cupy as cp # type: ignore + + try: + ndev = cp.cuda.runtime.getDeviceCount() # type: ignore[attr-defined] + if ndev <= 0: + return False + x = cp.array([1, 2, 3]) + _ = int(x.sum().get()) + return True + except Exception: + return False + except Exception: + return False + + +@pytest.mark.parametrize( + "shape,fmt", [((64, 64, 3), ImageFormat.BGR), ((64, 64), ImageFormat.GRAY)] +) +def test_rectify_image_cpu(shape, fmt) -> None: + arr = (np.random.rand(*shape) * (255 if fmt != ImageFormat.GRAY else 65535)).astype( + np.uint8 if fmt != ImageFormat.GRAY else np.uint16 + ) + img = Image(data=arr, format=fmt, frame_id="cam", ts=123.456) + K = np.array( + [[100.0, 0, arr.shape[1] / 2], [0, 100.0, arr.shape[0] / 2], [0, 0, 1]], dtype=np.float64 + ) + D = np.zeros(5, dtype=np.float64) + out = rectify_image(img, K, D) + assert out.shape[:2] == arr.shape[:2] + assert out.format == fmt + assert out.frame_id == "cam" + assert abs(out.ts - 123.456) < 1e-9 + # With zero distortion, pixels should match + np.testing.assert_array_equal(out.data, arr) + + +@pytest.mark.skipif(not _has_cupy(), reason="CuPy/CUDA not available") +@pytest.mark.parametrize( + "shape,fmt", [((32, 32, 3), ImageFormat.BGR), ((32, 32), ImageFormat.GRAY)] +) +def test_rectify_image_gpu_parity(shape, fmt) -> None: + import cupy as cp # type: ignore + + arr_np = (np.random.rand(*shape) * (255 if fmt != ImageFormat.GRAY else 65535)).astype( + np.uint8 if fmt != ImageFormat.GRAY else np.uint16 + ) + arr_cu = cp.asarray(arr_np) + img = Image(data=arr_cu, format=fmt, frame_id="cam", ts=1.23) + K = np.array( + [[80.0, 0, arr_np.shape[1] / 2], [0, 80.0, arr_np.shape[0] / 2], [0, 0, 1.0]], + dtype=np.float64, + ) + D = np.zeros(5, dtype=np.float64) + out = rectify_image(img, K, D) + # Zero distortion parity and backend preservation + assert out.format == fmt + assert out.frame_id == "cam" + assert abs(out.ts - 1.23) < 1e-9 + assert out.data.__class__.__module__.startswith("cupy") + np.testing.assert_array_equal(cp.asnumpy(out.data), arr_np) + + +@pytest.mark.skipif(not _has_cupy(), reason="CuPy/CUDA not available") +def test_rectify_image_gpu_nonzero_dist_close() -> None: + import cupy as cp # type: ignore + + H, W = 64, 96 + # Structured pattern to make interpolation deterministic enough + x = np.linspace(0, 255, W, dtype=np.float32) + y = np.linspace(0, 255, H, dtype=np.float32) + xv, yv = np.meshgrid(x, y) + arr_np = np.stack( + [ + xv.astype(np.uint8), + yv.astype(np.uint8), + ((xv + yv) / 2).astype(np.uint8), + ], + axis=2, + ) + img_cpu = Image(data=arr_np, format=ImageFormat.BGR, frame_id="cam", ts=0.5) + img_gpu = Image(data=cp.asarray(arr_np), format=ImageFormat.BGR, frame_id="cam", ts=0.5) + + fx, fy = 120.0, 125.0 + cx, cy = W / 2.0, H / 2.0 + K = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1.0]], dtype=np.float64) + D = np.array([0.05, -0.02, 0.001, -0.001, 0.0], dtype=np.float64) + + out_cpu = rectify_image(img_cpu, K, D) + out_gpu = rectify_image(img_gpu, K, D) + # Compare within a small tolerance + # Small numeric differences may remain due to model and casting; keep tight tolerance + np.testing.assert_allclose( + cp.asnumpy(out_gpu.data).astype(np.int16), out_cpu.data.astype(np.int16), atol=4 + ) + + +def test_project_roundtrip_cpu() -> None: + pts3d = np.array([[0.1, 0.2, 1.0], [0.0, 0.0, 2.0], [0.5, -0.3, 3.0]], dtype=np.float32) + fx, fy, cx, cy = 200.0, 220.0, 64.0, 48.0 + K = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1.0]], dtype=np.float64) + uv = project_3d_points_to_2d(pts3d, K) + assert uv.shape == (3, 2) + Z = pts3d[:, 2] + pts3d_back = project_2d_points_to_3d(uv.astype(np.float32), Z.astype(np.float32), K) + # Allow small rounding differences due to int rounding in 2D + assert pts3d_back.shape == (3, 3) + assert np.all(pts3d_back[:, 2] > 0) + + +@pytest.mark.skipif(not _has_cupy(), reason="CuPy/CUDA not available") +def test_project_parity_gpu_cpu() -> None: + import cupy as cp # type: ignore + + pts3d_np = np.array([[0.1, 0.2, 1.0], [0.0, 0.0, 2.0], [0.5, -0.3, 3.0]], dtype=np.float32) + fx, fy, cx, cy = 200.0, 220.0, 64.0, 48.0 + K_np = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1.0]], dtype=np.float64) + uv_cpu = project_3d_points_to_2d(pts3d_np, K_np) + uv_gpu = project_3d_points_to_2d(cp.asarray(pts3d_np), cp.asarray(K_np)) + np.testing.assert_array_equal(cp.asnumpy(uv_gpu), uv_cpu) + + Z_np = pts3d_np[:, 2] + pts3d_cpu = project_2d_points_to_3d(uv_cpu.astype(np.float32), Z_np.astype(np.float32), K_np) + pts3d_gpu = project_2d_points_to_3d( + cp.asarray(uv_cpu.astype(np.float32)), cp.asarray(Z_np.astype(np.float32)), cp.asarray(K_np) + ) + assert pts3d_cpu.shape == cp.asnumpy(pts3d_gpu).shape + + +@pytest.mark.skipif(not _has_cupy(), reason="CuPy/CUDA not available") +def test_project_parity_gpu_cpu_random() -> None: + import cupy as cp # type: ignore + + rng = np.random.RandomState(0) + N = 1000 + Z = rng.uniform(0.1, 5.0, size=(N, 1)).astype(np.float32) + XY = rng.uniform(-1.0, 1.0, size=(N, 2)).astype(np.float32) + pts3d_np = np.concatenate([XY, Z], axis=1) + + fx, fy = 300.0, 320.0 + cx, cy = 128.0, 96.0 + K_np = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1.0]], dtype=np.float64) + + uv_cpu = project_3d_points_to_2d(pts3d_np, K_np) + uv_gpu = project_3d_points_to_2d(cp.asarray(pts3d_np), cp.asarray(K_np)) + np.testing.assert_array_equal(cp.asnumpy(uv_gpu), uv_cpu) + + # Roundtrip + Z_flat = pts3d_np[:, 2] + pts3d_cpu = project_2d_points_to_3d(uv_cpu.astype(np.float32), Z_flat.astype(np.float32), K_np) + pts3d_gpu = project_2d_points_to_3d( + cp.asarray(uv_cpu.astype(np.float32)), + cp.asarray(Z_flat.astype(np.float32)), + cp.asarray(K_np), + ) + assert pts3d_cpu.shape == cp.asnumpy(pts3d_gpu).shape + + +def test_colorize_depth_cpu() -> None: + depth = np.zeros((32, 48), dtype=np.float32) + depth[8:16, 12:24] = 1.5 + out = colorize_depth(depth, max_depth=3.0, overlay_stats=False) + assert isinstance(out, np.ndarray) + assert out.shape == (32, 48, 3) + assert out.dtype == np.uint8 + + +@pytest.mark.skipif(not _has_cupy(), reason="CuPy/CUDA not available") +def test_colorize_depth_gpu_parity() -> None: + import cupy as cp # type: ignore + + depth_np = np.zeros((16, 20), dtype=np.float32) + depth_np[4:8, 5:15] = 2.0 + out_cpu = colorize_depth(depth_np, max_depth=4.0, overlay_stats=False) + out_gpu = colorize_depth(cp.asarray(depth_np), max_depth=4.0, overlay_stats=False) + np.testing.assert_array_equal(cp.asnumpy(out_gpu), out_cpu) + + +def test_draw_bounding_box_cpu() -> None: + img = np.zeros((20, 30, 3), dtype=np.uint8) + out = draw_bounding_box(img, [2, 3, 10, 12], color=(255, 0, 0), thickness=1) + assert isinstance(out, np.ndarray) + assert out.shape == img.shape + assert out.dtype == img.dtype + + +@pytest.mark.skipif(not _has_cupy(), reason="CuPy/CUDA not available") +def test_draw_bounding_box_gpu_parity() -> None: + import cupy as cp # type: ignore + + img_np = np.zeros((20, 30, 3), dtype=np.uint8) + out_cpu = draw_bounding_box(img_np.copy(), [2, 3, 10, 12], color=(0, 255, 0), thickness=2) + img_cu = cp.asarray(img_np) + out_gpu = draw_bounding_box(img_cu, [2, 3, 10, 12], color=(0, 255, 0), thickness=2) + np.testing.assert_array_equal(cp.asnumpy(out_gpu), out_cpu) + + +def test_draw_segmentation_mask_cpu() -> None: + img = np.zeros((20, 30, 3), dtype=np.uint8) + mask = np.zeros((20, 30), dtype=np.uint8) + mask[5:10, 8:15] = 1 + out = draw_segmentation_mask(img, mask, color=(0, 200, 200), alpha=0.5) + assert out.shape == img.shape + + +@pytest.mark.skipif(not _has_cupy(), reason="CuPy/CUDA not available") +def test_draw_segmentation_mask_gpu_parity() -> None: + import cupy as cp # type: ignore + + img_np = np.zeros((20, 30, 3), dtype=np.uint8) + mask_np = np.zeros((20, 30), dtype=np.uint8) + mask_np[2:12, 3:20] = 1 + out_cpu = draw_segmentation_mask(img_np.copy(), mask_np, color=(100, 50, 200), alpha=0.4) + out_gpu = draw_segmentation_mask( + cp.asarray(img_np), cp.asarray(mask_np), color=(100, 50, 200), alpha=0.4 + ) + np.testing.assert_array_equal(cp.asnumpy(out_gpu), out_cpu) + + +def test_draw_object_detection_visualization_cpu() -> None: + img = np.zeros((30, 40, 3), dtype=np.uint8) + objects = [ + { + "object_id": 1, + "bbox": [5, 6, 20, 25], + "label": "box", + "confidence": 0.9, + } + ] + out = draw_object_detection_visualization(img, objects) + assert out.shape == img.shape + + +@pytest.mark.skipif(not _has_cupy(), reason="CuPy/CUDA not available") +def test_draw_object_detection_visualization_gpu_parity() -> None: + import cupy as cp # type: ignore + + img_np = np.zeros((30, 40, 3), dtype=np.uint8) + objects = [ + { + "object_id": 1, + "bbox": [5, 6, 20, 25], + "label": "box", + "confidence": 0.9, + } + ] + out_cpu = draw_object_detection_visualization(img_np.copy(), objects) + out_gpu = draw_object_detection_visualization(cp.asarray(img_np), objects) + np.testing.assert_array_equal(cp.asnumpy(out_gpu), out_cpu) diff --git a/dimos/msgs/sensor_msgs/image_impls/test_image_backends.py b/dimos/msgs/sensor_msgs/image_impls/test_image_backends.py new file mode 100644 index 0000000000..d8012a8f53 --- /dev/null +++ b/dimos/msgs/sensor_msgs/image_impls/test_image_backends.py @@ -0,0 +1,797 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +import cv2 +import numpy as np +import pytest + +from dimos.msgs.sensor_msgs.Image import HAS_CUDA, Image, ImageFormat +from dimos.utils.data import get_data + +IMAGE_PATH = get_data("chair-image.png") + +if HAS_CUDA: + print("Running image backend tests with CUDA/CuPy support (GPU mode)") +else: + print("Running image backend tests in CPU-only mode") + + +def _load_chair_image() -> np.ndarray: + img = cv2.imread(IMAGE_PATH, cv2.IMREAD_UNCHANGED) + if img is None: + raise FileNotFoundError(f"unable to load test image at {IMAGE_PATH}") + return img + + +_CHAIR_BGRA = _load_chair_image() + + +def _prepare_image(fmt: ImageFormat, shape=None) -> np.ndarray: + base = _CHAIR_BGRA + if fmt == ImageFormat.BGR: + arr = cv2.cvtColor(base, cv2.COLOR_BGRA2BGR) + elif fmt == ImageFormat.RGB: + arr = cv2.cvtColor(base, cv2.COLOR_BGRA2RGB) + elif fmt == ImageFormat.BGRA: + arr = base.copy() + elif fmt == ImageFormat.GRAY: + arr = cv2.cvtColor(base, cv2.COLOR_BGRA2GRAY) + else: + raise ValueError(f"unsupported image format {fmt}") + + if shape is None: + return arr.copy() + + if len(shape) == 2: + height, width = shape + orig_h, orig_w = arr.shape[:2] + interp = cv2.INTER_AREA if height <= orig_h and width <= orig_w else cv2.INTER_LINEAR + resized = cv2.resize(arr, (width, height), interpolation=interp) + return resized.copy() + + if len(shape) == 3: + height, width, channels = shape + orig_h, orig_w = arr.shape[:2] + interp = cv2.INTER_AREA if height <= orig_h and width <= orig_w else cv2.INTER_LINEAR + resized = cv2.resize(arr, (width, height), interpolation=interp) + if resized.ndim == 2: + resized = np.repeat(resized[:, :, None], channels, axis=2) + elif resized.shape[2] != channels: + if channels == 4 and resized.shape[2] == 3: + alpha = np.full((height, width, 1), 255, dtype=resized.dtype) + resized = np.concatenate([resized, alpha], axis=2) + elif channels == 3 and resized.shape[2] == 4: + resized = resized[:, :, :3] + else: + raise ValueError(f"cannot adjust image to {channels} channels") + return resized.copy() + + raise ValueError("shape must be a tuple of length 2 or 3") + + +@pytest.fixture +def alloc_timer(request): + """Helper fixture for adaptive testing with optional GPU support.""" + + def _alloc( + arr: np.ndarray, fmt: ImageFormat, *, to_cuda: bool | None = None, label: str | None = None + ): + tag = label or request.node.name + + # Always create CPU image + start = time.perf_counter() + cpu = Image.from_numpy(arr, format=fmt, to_cuda=False) + cpu_time = time.perf_counter() - start + + # Optionally create GPU image if CUDA is available + gpu = None + gpu_time = None + if to_cuda is None: + to_cuda = HAS_CUDA + + if to_cuda and HAS_CUDA: + arr_gpu = np.array(arr, copy=True) + start = time.perf_counter() + gpu = Image.from_numpy(arr_gpu, format=fmt, to_cuda=True) + gpu_time = time.perf_counter() - start + + if gpu_time is not None: + print(f"[alloc {tag}] cpu={cpu_time:.6f}s gpu={gpu_time:.6f}s") + else: + print(f"[alloc {tag}] cpu={cpu_time:.6f}s") + return cpu, gpu, cpu_time, gpu_time + + return _alloc + + +@pytest.mark.parametrize( + "shape,fmt", + [ + ((64, 64, 3), ImageFormat.BGR), + ((64, 64, 4), ImageFormat.BGRA), + ((64, 64, 3), ImageFormat.RGB), + ((64, 64), ImageFormat.GRAY), + ], +) +def test_color_conversions(shape, fmt, alloc_timer) -> None: + """Test color conversions with NumpyImage always, add CudaImage parity when available.""" + arr = _prepare_image(fmt, shape) + cpu, gpu, _, _ = alloc_timer(arr, fmt) + + # Always test CPU backend + cpu_round = cpu.to_rgb().to_bgr().to_opencv() + assert cpu_round.shape[0] == shape[0] + assert cpu_round.shape[1] == shape[1] + assert cpu_round.shape[2] == 3 # to_opencv always returns BGR (3 channels) + assert cpu_round.dtype == np.uint8 + + # Optionally test GPU parity when CUDA is available + if gpu is not None: + gpu_round = gpu.to_rgb().to_bgr().to_opencv() + assert gpu_round.shape == cpu_round.shape + assert gpu_round.dtype == cpu_round.dtype + # Exact match for uint8 color ops + assert np.array_equal(cpu_round, gpu_round) + + +def test_grayscale(alloc_timer) -> None: + """Test grayscale conversion with NumpyImage always, add CudaImage parity when available.""" + arr = _prepare_image(ImageFormat.BGR, (48, 32, 3)) + cpu, gpu, _, _ = alloc_timer(arr, ImageFormat.BGR) + + # Always test CPU backend + cpu_gray = cpu.to_grayscale().to_opencv() + assert cpu_gray.shape == (48, 32) # Grayscale has no channel dimension in OpenCV + assert cpu_gray.dtype == np.uint8 + + # Optionally test GPU parity when CUDA is available + if gpu is not None: + gpu_gray = gpu.to_grayscale().to_opencv() + assert gpu_gray.shape == cpu_gray.shape + assert gpu_gray.dtype == cpu_gray.dtype + # Allow tiny rounding differences (<=1 LSB) — visually indistinguishable + diff = np.abs(cpu_gray.astype(np.int16) - gpu_gray.astype(np.int16)) + assert diff.max() <= 1 + + +@pytest.mark.parametrize("fmt", [ImageFormat.BGR, ImageFormat.RGB, ImageFormat.BGRA]) +def test_resize(fmt, alloc_timer) -> None: + """Test resize with NumpyImage always, add CudaImage parity when available.""" + shape = (60, 80, 3) if fmt in (ImageFormat.BGR, ImageFormat.RGB) else (60, 80, 4) + arr = _prepare_image(fmt, shape) + cpu, gpu, _, _ = alloc_timer(arr, fmt) + + new_w, new_h = 37, 53 + + # Always test CPU backend + cpu_res = cpu.resize(new_w, new_h).to_opencv() + assert ( + cpu_res.shape == (53, 37, 3) if fmt != ImageFormat.BGRA else (53, 37, 3) + ) # to_opencv drops alpha + assert cpu_res.dtype == np.uint8 + + # Optionally test GPU parity when CUDA is available + if gpu is not None: + gpu_res = gpu.resize(new_w, new_h).to_opencv() + assert gpu_res.shape == cpu_res.shape + assert gpu_res.dtype == cpu_res.dtype + # Allow small tolerance due to float interpolation differences + assert np.max(np.abs(cpu_res.astype(np.int16) - gpu_res.astype(np.int16))) <= 1 + + +def test_perf_alloc(alloc_timer) -> None: + """Test allocation performance with NumpyImage always, add CudaImage when available.""" + arr = _prepare_image(ImageFormat.BGR, (480, 640, 3)) + alloc_timer(arr, ImageFormat.BGR, label="test_perf_alloc-setup") + + runs = 5 + + # Always test CPU allocation + t0 = time.perf_counter() + for _ in range(runs): + _ = Image.from_numpy(arr, format=ImageFormat.BGR, to_cuda=False) + cpu_t = (time.perf_counter() - t0) / runs + assert cpu_t > 0 + + # Optionally test GPU allocation when CUDA is available + if HAS_CUDA: + t0 = time.perf_counter() + for _ in range(runs): + _ = Image.from_numpy(arr, format=ImageFormat.BGR, to_cuda=True) + gpu_t = (time.perf_counter() - t0) / runs + print(f"alloc (avg per call) cpu={cpu_t:.6f}s gpu={gpu_t:.6f}s") + assert gpu_t > 0 + else: + print(f"alloc (avg per call) cpu={cpu_t:.6f}s") + + +def test_sharpness(alloc_timer) -> None: + """Test sharpness computation with NumpyImage always, add CudaImage parity when available.""" + arr = _prepare_image(ImageFormat.BGR, (64, 64, 3)) + cpu, gpu, _, _ = alloc_timer(arr, ImageFormat.BGR) + + # Always test CPU backend + s_cpu = cpu.sharpness + assert s_cpu >= 0 # Sharpness should be non-negative + assert s_cpu < 1000 # Reasonable upper bound + + # Optionally test GPU parity when CUDA is available + if gpu is not None: + s_gpu = gpu.sharpness + # Values should be very close; minor border/rounding differences allowed + assert abs(s_cpu - s_gpu) < 5e-2 + + +def test_to_opencv(alloc_timer) -> None: + """Test to_opencv conversion with NumpyImage always, add CudaImage parity when available.""" + # BGRA should drop alpha and produce BGR + arr = _prepare_image(ImageFormat.BGRA, (32, 32, 4)) + cpu, gpu, _, _ = alloc_timer(arr, ImageFormat.BGRA) + + # Always test CPU backend + cpu_bgr = cpu.to_opencv() + assert cpu_bgr.shape == (32, 32, 3) + assert cpu_bgr.dtype == np.uint8 + + # Optionally test GPU parity when CUDA is available + if gpu is not None: + gpu_bgr = gpu.to_opencv() + assert gpu_bgr.shape == cpu_bgr.shape + assert gpu_bgr.dtype == cpu_bgr.dtype + assert np.array_equal(cpu_bgr, gpu_bgr) + + +def test_solve_pnp(alloc_timer) -> None: + """Test solve_pnp with NumpyImage always, add CudaImage parity when available.""" + # Synthetic camera and 3D points + K = np.array([[400.0, 0.0, 32.0], [0.0, 400.0, 24.0], [0.0, 0.0, 1.0]], dtype=np.float64) + dist = None + obj = np.array( + [ + [-0.5, -0.5, 0.0], + [0.5, -0.5, 0.0], + [0.5, 0.5, 0.0], + [-0.5, 0.5, 0.0], + [0.0, 0.0, 0.5], + [0.0, 0.0, 1.0], + ], + dtype=np.float32, + ) + + rvec_true = np.zeros((3, 1), dtype=np.float64) + tvec_true = np.array([[0.0], [0.0], [2.0]], dtype=np.float64) + img_pts, _ = cv2.projectPoints(obj, rvec_true, tvec_true, K, dist) + img_pts = img_pts.reshape(-1, 2).astype(np.float32) + + # Build images using deterministic fixture content + base_bgr = _prepare_image(ImageFormat.BGR, (48, 64, 3)) + cpu, gpu, _, _ = alloc_timer(base_bgr, ImageFormat.BGR) + + # Always test CPU backend + ok_cpu, r_cpu, t_cpu = cpu.solve_pnp(obj, img_pts, K, dist) + assert ok_cpu + + # Validate reprojection error for CPU solver + proj_cpu, _ = cv2.projectPoints(obj, r_cpu, t_cpu, K, dist) + proj_cpu = proj_cpu.reshape(-1, 2) + err_cpu = np.linalg.norm(proj_cpu - img_pts, axis=1) + assert err_cpu.mean() < 1e-3 + assert err_cpu.max() < 1e-2 + + # Optionally test GPU parity when CUDA is available + if gpu is not None: + ok_gpu, r_gpu, t_gpu = gpu.solve_pnp(obj, img_pts, K, dist) + assert ok_gpu + + # Validate reprojection error for GPU solver + proj_gpu, _ = cv2.projectPoints(obj, r_gpu, t_gpu, K, dist) + proj_gpu = proj_gpu.reshape(-1, 2) + err_gpu = np.linalg.norm(proj_gpu - img_pts, axis=1) + assert err_gpu.mean() < 1e-3 + assert err_gpu.max() < 1e-2 + + +def test_perf_grayscale(alloc_timer) -> None: + """Test grayscale performance with NumpyImage always, add CudaImage when available.""" + arr = _prepare_image(ImageFormat.BGR, (480, 640, 3)) + cpu, gpu, _, _ = alloc_timer(arr, ImageFormat.BGR, label="test_perf_grayscale-setup") + + runs = 10 + + # Always test CPU performance + t0 = time.perf_counter() + for _ in range(runs): + _ = cpu.to_grayscale() + cpu_t = (time.perf_counter() - t0) / runs + assert cpu_t > 0 + + # Optionally test GPU performance when CUDA is available + if gpu is not None: + t0 = time.perf_counter() + for _ in range(runs): + _ = gpu.to_grayscale() + gpu_t = (time.perf_counter() - t0) / runs + print(f"grayscale (avg per call) cpu={cpu_t:.6f}s gpu={gpu_t:.6f}s") + assert gpu_t > 0 + else: + print(f"grayscale (avg per call) cpu={cpu_t:.6f}s") + + +def test_perf_resize(alloc_timer) -> None: + """Test resize performance with NumpyImage always, add CudaImage when available.""" + arr = _prepare_image(ImageFormat.BGR, (480, 640, 3)) + cpu, gpu, _, _ = alloc_timer(arr, ImageFormat.BGR, label="test_perf_resize-setup") + + runs = 5 + + # Always test CPU performance + t0 = time.perf_counter() + for _ in range(runs): + _ = cpu.resize(320, 240) + cpu_t = (time.perf_counter() - t0) / runs + assert cpu_t > 0 + + # Optionally test GPU performance when CUDA is available + if gpu is not None: + t0 = time.perf_counter() + for _ in range(runs): + _ = gpu.resize(320, 240) + gpu_t = (time.perf_counter() - t0) / runs + print(f"resize (avg per call) cpu={cpu_t:.6f}s gpu={gpu_t:.6f}s") + assert gpu_t > 0 + else: + print(f"resize (avg per call) cpu={cpu_t:.6f}s") + + +def test_perf_sharpness(alloc_timer) -> None: + """Test sharpness performance with NumpyImage always, add CudaImage when available.""" + arr = _prepare_image(ImageFormat.BGR, (480, 640, 3)) + cpu, gpu, _, _ = alloc_timer(arr, ImageFormat.BGR, label="test_perf_sharpness-setup") + + runs = 3 + + # Always test CPU performance + t0 = time.perf_counter() + for _ in range(runs): + _ = cpu.sharpness + cpu_t = (time.perf_counter() - t0) / runs + assert cpu_t > 0 + + # Optionally test GPU performance when CUDA is available + if gpu is not None: + t0 = time.perf_counter() + for _ in range(runs): + _ = gpu.sharpness + gpu_t = (time.perf_counter() - t0) / runs + print(f"sharpness (avg per call) cpu={cpu_t:.6f}s gpu={gpu_t:.6f}s") + assert gpu_t > 0 + else: + print(f"sharpness (avg per call) cpu={cpu_t:.6f}s") + + +def test_perf_solvepnp(alloc_timer) -> None: + """Test solve_pnp performance with NumpyImage always, add CudaImage when available.""" + K = np.array([[600.0, 0.0, 320.0], [0.0, 600.0, 240.0], [0.0, 0.0, 1.0]], dtype=np.float64) + dist = None + rng = np.random.default_rng(123) + obj = rng.standard_normal((200, 3)).astype(np.float32) + rvec_true = np.array([[0.1], [-0.2], [0.05]]) + tvec_true = np.array([[0.0], [0.0], [3.0]]) + img_pts, _ = cv2.projectPoints(obj, rvec_true, tvec_true, K, dist) + img_pts = img_pts.reshape(-1, 2).astype(np.float32) + base_bgr = _prepare_image(ImageFormat.BGR, (480, 640, 3)) + cpu, gpu, _, _ = alloc_timer(base_bgr, ImageFormat.BGR, label="test_perf_solvepnp-setup") + + runs = 5 + + # Always test CPU performance + t0 = time.perf_counter() + for _ in range(runs): + _ = cpu.solve_pnp(obj, img_pts, K, dist) + cpu_t = (time.perf_counter() - t0) / runs + assert cpu_t > 0 + + # Optionally test GPU performance when CUDA is available + if gpu is not None: + t0 = time.perf_counter() + for _ in range(runs): + _ = gpu.solve_pnp(obj, img_pts, K, dist) + gpu_t = (time.perf_counter() - t0) / runs + print(f"solvePnP (avg per call) cpu={cpu_t:.6f}s gpu={gpu_t:.6f}s") + assert gpu_t > 0 + else: + print(f"solvePnP (avg per call) cpu={cpu_t:.6f}s") + + +# this test is failing with +# raise RuntimeError("OpenCV CSRT tracker not available") +@pytest.mark.skip +def test_perf_tracker(alloc_timer) -> None: + """Test tracker performance with NumpyImage always, add CudaImage when available.""" + # Don't check - just let it fail if CSRT isn't available + + H, W = 240, 320 + img_base = _prepare_image(ImageFormat.BGR, (H, W, 3)) + img1 = img_base.copy() + img2 = img_base.copy() + bbox0 = (80, 60, 40, 30) + x0, y0, w0, h0 = bbox0 + cv2.rectangle(img1, (x0, y0), (x0 + w0, y0 + h0), (255, 255, 255), thickness=-1) + dx, dy = 8, 5 + cv2.rectangle( + img2, + (x0 + dx, y0 + dy), + (x0 + dx + w0, y0 + dy + h0), + (255, 255, 255), + thickness=-1, + ) + cpu1, gpu1, _, _ = alloc_timer(img1, ImageFormat.BGR, label="test_perf_tracker-frame1") + cpu2, gpu2, _, _ = alloc_timer(img2, ImageFormat.BGR, label="test_perf_tracker-frame2") + + # Always test CPU tracker + trk_cpu = cpu1.create_csrt_tracker(bbox0) + + runs = 10 + t0 = time.perf_counter() + for _ in range(runs): + _ = cpu2.csrt_update(trk_cpu) + cpu_t = (time.perf_counter() - t0) / runs + assert cpu_t > 0 + + # Optionally test GPU performance when CUDA is available + if gpu1 is not None and gpu2 is not None: + trk_gpu = gpu1.create_csrt_tracker(bbox0) + t0 = time.perf_counter() + for _ in range(runs): + _ = gpu2.csrt_update(trk_gpu) + gpu_t = (time.perf_counter() - t0) / runs + print(f"tracker (avg per call) cpu={cpu_t:.6f}s gpu={gpu_t:.6f}s") + assert gpu_t > 0 + else: + print(f"tracker (avg per call) cpu={cpu_t:.6f}s") + + +# this test is failing with +# raise RuntimeError("OpenCV CSRT tracker not available") +@pytest.mark.skip +def test_csrt_tracker(alloc_timer) -> None: + """Test CSRT tracker with NumpyImage always, add CudaImage parity when available.""" + # Don't check - just let it fail if CSRT isn't available + + H, W = 100, 100 + # Create two frames with a moving rectangle + img_base = _prepare_image(ImageFormat.BGR, (H, W, 3)) + img1 = img_base.copy() + img2 = img_base.copy() + bbox0 = (30, 30, 20, 15) + x0, y0, w0, h0 = bbox0 + # draw rect in img1 + cv2.rectangle(img1, (x0, y0), (x0 + w0, y0 + h0), (255, 255, 255), thickness=-1) + # shift by (dx,dy) + dx, dy = 5, 3 + cv2.rectangle( + img2, + (x0 + dx, y0 + dy), + (x0 + dx + w0, y0 + dy + h0), + (255, 255, 255), + thickness=-1, + ) + + cpu1, gpu1, _, _ = alloc_timer(img1, ImageFormat.BGR, label="test_csrt_tracker-frame1") + cpu2, gpu2, _, _ = alloc_timer(img2, ImageFormat.BGR, label="test_csrt_tracker-frame2") + + # Always test CPU tracker + trk_cpu = cpu1.create_csrt_tracker(bbox0) + ok_cpu, bbox_cpu = cpu2.csrt_update(trk_cpu) + assert ok_cpu + + # Compare to ground-truth expected bbox + expected = (x0 + dx, y0 + dy, w0, h0) + err_cpu = sum(abs(a - b) for a, b in zip(bbox_cpu, expected, strict=False)) + assert err_cpu <= 8 + + # Optionally test GPU parity when CUDA is available + if gpu1 is not None and gpu2 is not None: + trk_gpu = gpu1.create_csrt_tracker(bbox0) + ok_gpu, bbox_gpu = gpu2.csrt_update(trk_gpu) + assert ok_gpu + + err_gpu = sum(abs(a - b) for a, b in zip(bbox_gpu, expected, strict=False)) + assert err_gpu <= 10 # allow some slack for scale/window effects + + +def test_solve_pnp_ransac(alloc_timer) -> None: + """Test solve_pnp_ransac with NumpyImage always, add CudaImage when available.""" + # Camera with distortion + K = np.array([[500.0, 0.0, 320.0], [0.0, 500.0, 240.0], [0.0, 0.0, 1.0]], dtype=np.float64) + dist = np.array([0.1, -0.05, 0.001, 0.001, 0.0], dtype=np.float64) + rng = np.random.default_rng(202) + obj = rng.uniform(-1.0, 1.0, size=(200, 3)).astype(np.float32) + obj[:, 2] = np.abs(obj[:, 2]) + 2.0 # keep in front of camera + rvec_true = np.array([[0.1], [-0.15], [0.05]], dtype=np.float64) + tvec_true = np.array([[0.2], [-0.1], [3.0]], dtype=np.float64) + img_pts, _ = cv2.projectPoints(obj, rvec_true, tvec_true, K, dist) + img_pts = img_pts.reshape(-1, 2) + # Add outliers + n_out = 20 + idx = rng.choice(len(img_pts), size=n_out, replace=False) + img_pts[idx] += rng.uniform(-50, 50, size=(n_out, 2)) + img_pts = img_pts.astype(np.float32) + + base_bgr = _prepare_image(ImageFormat.BGR, (480, 640, 3)) + cpu, gpu, _, _ = alloc_timer(base_bgr, ImageFormat.BGR, label="test_solve_pnp_ransac-setup") + + # Always test CPU backend + ok_cpu, r_cpu, t_cpu, mask_cpu = cpu.solve_pnp_ransac( + obj, img_pts, K, dist, iterations_count=150, reprojection_error=3.0 + ) + assert ok_cpu + inlier_ratio = mask_cpu.mean() + assert inlier_ratio > 0.7 + + # Reprojection error on inliers + in_idx = np.nonzero(mask_cpu)[0] + proj_cpu, _ = cv2.projectPoints(obj[in_idx], r_cpu, t_cpu, K, dist) + proj_cpu = proj_cpu.reshape(-1, 2) + err = np.linalg.norm(proj_cpu - img_pts[in_idx], axis=1) + assert err.mean() < 1.5 + assert err.max() < 4.0 + + # Optionally test GPU parity when CUDA is available + if gpu is not None: + ok_gpu, r_gpu, t_gpu, mask_gpu = gpu.solve_pnp_ransac( + obj, img_pts, K, dist, iterations_count=150, reprojection_error=3.0 + ) + assert ok_gpu + inlier_ratio_gpu = mask_gpu.mean() + assert inlier_ratio_gpu > 0.7 + + # Reprojection error on inliers for GPU + in_idx_gpu = np.nonzero(mask_gpu)[0] + proj_gpu, _ = cv2.projectPoints(obj[in_idx_gpu], r_gpu, t_gpu, K, dist) + proj_gpu = proj_gpu.reshape(-1, 2) + err_gpu = np.linalg.norm(proj_gpu - img_pts[in_idx_gpu], axis=1) + assert err_gpu.mean() < 1.5 + assert err_gpu.max() < 4.0 + + +def test_solve_pnp_batch(alloc_timer) -> None: + """Test solve_pnp batch processing with NumpyImage always, add CudaImage when available.""" + # Note: Batch processing is primarily a GPU feature, but we can still test CPU loop + # Generate batched problems + B, N = 8, 50 + rng = np.random.default_rng(99) + obj = rng.uniform(-1.0, 1.0, size=(B, N, 3)).astype(np.float32) + obj[:, :, 2] = np.abs(obj[:, :, 2]) + 2.0 + K = np.array([[600.0, 0.0, 320.0], [0.0, 600.0, 240.0], [0.0, 0.0, 1.0]], dtype=np.float64) + r_true = np.zeros((B, 3, 1), dtype=np.float64) + t_true = np.tile(np.array([[0.0], [0.0], [3.0]], dtype=np.float64), (B, 1, 1)) + img = [] + for b in range(B): + ip, _ = cv2.projectPoints(obj[b], r_true[b], t_true[b], K, None) + img.append(ip.reshape(-1, 2)) + img = np.stack(img, axis=0).astype(np.float32) + + base_bgr = _prepare_image(ImageFormat.BGR, (10, 10, 3)) + cpu, gpu, _, _ = alloc_timer(base_bgr, ImageFormat.BGR, label="test_solve_pnp_batch-setup") + + # Always test CPU loop + t0 = time.perf_counter() + r_list = [] + t_list = [] + for b in range(B): + ok, r, t = cpu.solve_pnp(obj[b], img[b], K, None) + assert ok + r_list.append(r) + t_list.append(t) + cpu_total = time.perf_counter() - t0 + cpu_t = cpu_total / B + + # Check reprojection for CPU results + for b in range(min(B, 2)): + proj, _ = cv2.projectPoints(obj[b], r_list[b], t_list[b], K, None) + err = np.linalg.norm(proj.reshape(-1, 2) - img[b], axis=1) + assert err.mean() < 1e-2 + assert err.max() < 1e-1 + + # Optionally test GPU batch when CUDA is available + if gpu is not None and hasattr(gpu._impl, "solve_pnp_batch"): + t0 = time.perf_counter() + r_b, t_b = gpu.solve_pnp_batch(obj, img, K) + gpu_total = time.perf_counter() - t0 + gpu_t = gpu_total / B + print(f"solvePnP-batch (avg per pose) cpu={cpu_t:.6f}s gpu={gpu_t:.6f}s (B={B}, N={N})") + + # Check reprojection for GPU batches + for b in range(min(B, 4)): + proj, _ = cv2.projectPoints(obj[b], r_b[b], t_b[b], K, None) + err = np.linalg.norm(proj.reshape(-1, 2) - img[b], axis=1) + assert err.mean() < 1e-2 + assert err.max() < 1e-1 + else: + print(f"solvePnP-batch (avg per pose) cpu={cpu_t:.6f}s (GPU batch not available)") + + +def test_nvimgcodec_flag_and_fallback(monkeypatch) -> None: + # Test that to_base64() works with and without nvimgcodec by patching runtime flags + import dimos.msgs.sensor_msgs.image_impls.AbstractImage as AbstractImageMod + + arr = _prepare_image(ImageFormat.BGR, (32, 32, 3)) + + # Save original values + original_has_nvimgcodec = AbstractImageMod.HAS_NVIMGCODEC + original_nvimgcodec = AbstractImageMod.nvimgcodec + + try: + # Test 1: Simulate nvimgcodec not available + monkeypatch.setattr(AbstractImageMod, "HAS_NVIMGCODEC", False) + monkeypatch.setattr(AbstractImageMod, "nvimgcodec", None) + + # Should work via cv2 fallback for CPU + img_cpu = Image.from_numpy(arr, format=ImageFormat.BGR, to_cuda=False) + b64_cpu = img_cpu.to_base64() + assert isinstance(b64_cpu, str) and len(b64_cpu) > 0 + + # If CUDA available, test GPU fallback to CPU encoding + if HAS_CUDA: + img_gpu = Image.from_numpy(arr, format=ImageFormat.BGR, to_cuda=True) + b64_gpu = img_gpu.to_base64() + assert isinstance(b64_gpu, str) and len(b64_gpu) > 0 + # Should have fallen back to CPU encoding + assert not AbstractImageMod.NVIMGCODEC_LAST_USED + + # Test 2: Restore nvimgcodec if it was originally available + if original_has_nvimgcodec: + monkeypatch.setattr(AbstractImageMod, "HAS_NVIMGCODEC", True) + monkeypatch.setattr(AbstractImageMod, "nvimgcodec", original_nvimgcodec) + + # Test it still works with nvimgcodec "available" + img2 = Image.from_numpy(arr, format=ImageFormat.BGR, to_cuda=HAS_CUDA) + b64_2 = img2.to_base64() + assert isinstance(b64_2, str) and len(b64_2) > 0 + + finally: + pass + + +@pytest.mark.skipif(not HAS_CUDA, reason="CuPy/CUDA not available") +def test_nvimgcodec_gpu_path(monkeypatch) -> None: + """Test nvimgcodec GPU encoding path when CUDA is available. + + This test specifically verifies that when nvimgcodec is available, + GPU images can be encoded directly without falling back to CPU. + """ + import dimos.msgs.sensor_msgs.image_impls.AbstractImage as AbstractImageMod + + # Check if nvimgcodec was originally available + if not AbstractImageMod.HAS_NVIMGCODEC: + pytest.skip("nvimgcodec library not available") + + # Save original nvimgcodec module reference + + # Create a CUDA image and encode using the actual nvimgcodec if available + arr = _prepare_image(ImageFormat.BGR, (32, 32, 3)) + + # Test with nvimgcodec enabled (should be the default if available) + img = Image.from_numpy(arr, format=ImageFormat.BGR, to_cuda=True) + b64 = img.to_base64() + assert isinstance(b64, str) and len(b64) > 0 + + # Check if GPU encoding was actually used + # Some builds may import nvimgcodec but not support CuPy device buffers + if not getattr(AbstractImageMod, "NVIMGCODEC_LAST_USED", False): + pytest.skip("nvimgcodec present but encode fell back to CPU in this environment") + + # Now test that we can disable nvimgcodec and still encode via fallback + monkeypatch.setattr(AbstractImageMod, "HAS_NVIMGCODEC", False) + monkeypatch.setattr(AbstractImageMod, "nvimgcodec", None) + + # Create another GPU image - should fall back to CPU encoding + img2 = Image.from_numpy(arr, format=ImageFormat.BGR, to_cuda=True) + b64_2 = img2.to_base64() + assert isinstance(b64_2, str) and len(b64_2) > 0 + # Should have fallen back to CPU encoding + assert not AbstractImageMod.NVIMGCODEC_LAST_USED + + +@pytest.mark.skipif(not HAS_CUDA, reason="CuPy/CUDA not available") +def test_to_cpu_format_preservation() -> None: + """Test that to_cpu() preserves image format correctly. + + This tests the fix for the bug where to_cpu() was using to_opencv() + which always returns BGR, but keeping the original format label. + """ + # Test RGB format preservation + rgb_array = np.random.randint(0, 255, (100, 100, 3), dtype=np.uint8) + gpu_img_rgb = Image.from_numpy(rgb_array, format=ImageFormat.RGB, to_cuda=True) + cpu_img_rgb = gpu_img_rgb.to_cpu() + + # Verify format is preserved + assert cpu_img_rgb.format == ImageFormat.RGB, ( + f"Format mismatch: expected RGB, got {cpu_img_rgb.format}" + ) + # Verify data is actually in RGB format (not BGR) + np.testing.assert_array_equal(cpu_img_rgb.data, rgb_array) + + # Test RGBA format preservation + rgba_array = np.random.randint(0, 255, (100, 100, 4), dtype=np.uint8) + gpu_img_rgba = Image.from_numpy(rgba_array, format=ImageFormat.RGBA, to_cuda=True) + cpu_img_rgba = gpu_img_rgba.to_cpu() + + assert cpu_img_rgba.format == ImageFormat.RGBA, ( + f"Format mismatch: expected RGBA, got {cpu_img_rgba.format}" + ) + np.testing.assert_array_equal(cpu_img_rgba.data, rgba_array) + + # Test BGR format (should be unchanged since to_opencv returns BGR) + bgr_array = np.random.randint(0, 255, (100, 100, 3), dtype=np.uint8) + gpu_img_bgr = Image.from_numpy(bgr_array, format=ImageFormat.BGR, to_cuda=True) + cpu_img_bgr = gpu_img_bgr.to_cpu() + + assert cpu_img_bgr.format == ImageFormat.BGR, ( + f"Format mismatch: expected BGR, got {cpu_img_bgr.format}" + ) + np.testing.assert_array_equal(cpu_img_bgr.data, bgr_array) + + # Test BGRA format + bgra_array = np.random.randint(0, 255, (100, 100, 4), dtype=np.uint8) + gpu_img_bgra = Image.from_numpy(bgra_array, format=ImageFormat.BGRA, to_cuda=True) + cpu_img_bgra = gpu_img_bgra.to_cpu() + + assert cpu_img_bgra.format == ImageFormat.BGRA, ( + f"Format mismatch: expected BGRA, got {cpu_img_bgra.format}" + ) + np.testing.assert_array_equal(cpu_img_bgra.data, bgra_array) + + # Test GRAY format + gray_array = np.random.randint(0, 255, (100, 100), dtype=np.uint8) + gpu_img_gray = Image.from_numpy(gray_array, format=ImageFormat.GRAY, to_cuda=True) + cpu_img_gray = gpu_img_gray.to_cpu() + + assert cpu_img_gray.format == ImageFormat.GRAY, ( + f"Format mismatch: expected GRAY, got {cpu_img_gray.format}" + ) + np.testing.assert_array_equal(cpu_img_gray.data, gray_array) + + # Test DEPTH format (float32) + depth_array = np.random.uniform(0.5, 10.0, (100, 100)).astype(np.float32) + gpu_img_depth = Image.from_numpy(depth_array, format=ImageFormat.DEPTH, to_cuda=True) + cpu_img_depth = gpu_img_depth.to_cpu() + + assert cpu_img_depth.format == ImageFormat.DEPTH, ( + f"Format mismatch: expected DEPTH, got {cpu_img_depth.format}" + ) + np.testing.assert_array_equal(cpu_img_depth.data, depth_array) + + # Test DEPTH16 format (uint16) + depth16_array = np.random.randint(100, 65000, (100, 100), dtype=np.uint16) + gpu_img_depth16 = Image.from_numpy(depth16_array, format=ImageFormat.DEPTH16, to_cuda=True) + cpu_img_depth16 = gpu_img_depth16.to_cpu() + + assert cpu_img_depth16.format == ImageFormat.DEPTH16, ( + f"Format mismatch: expected DEPTH16, got {cpu_img_depth16.format}" + ) + np.testing.assert_array_equal(cpu_img_depth16.data, depth16_array) + + # Test GRAY16 format (uint16) + gray16_array = np.random.randint(0, 65535, (100, 100), dtype=np.uint16) + gpu_img_gray16 = Image.from_numpy(gray16_array, format=ImageFormat.GRAY16, to_cuda=True) + cpu_img_gray16 = gpu_img_gray16.to_cpu() + + assert cpu_img_gray16.format == ImageFormat.GRAY16, ( + f"Format mismatch: expected GRAY16, got {cpu_img_gray16.format}" + ) + np.testing.assert_array_equal(cpu_img_gray16.data, gray16_array) diff --git a/dimos/msgs/sensor_msgs/test_CameraInfo.py b/dimos/msgs/sensor_msgs/test_CameraInfo.py new file mode 100644 index 0000000000..c35145255b --- /dev/null +++ b/dimos/msgs/sensor_msgs/test_CameraInfo.py @@ -0,0 +1,457 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pytest + +try: + from sensor_msgs.msg import CameraInfo as ROSCameraInfo, RegionOfInterest as ROSRegionOfInterest + from std_msgs.msg import Header as ROSHeader +except ImportError: + ROSCameraInfo = None + ROSRegionOfInterest = None + ROSHeader = None + +from dimos.msgs.sensor_msgs.CameraInfo import CalibrationProvider, CameraInfo +from dimos.utils.path_utils import get_project_root + + +def test_lcm_encode_decode() -> None: + """Test LCM encode/decode preserves CameraInfo data.""" + print("Testing CameraInfo LCM encode/decode...") + + # Create test camera info with sample calibration data + original = CameraInfo( + height=480, + width=640, + distortion_model="plumb_bob", + D=[-0.1, 0.05, 0.001, -0.002, 0.0], # 5 distortion coefficients + K=[ + 500.0, + 0.0, + 320.0, # fx, 0, cx + 0.0, + 500.0, + 240.0, # 0, fy, cy + 0.0, + 0.0, + 1.0, + ], # 0, 0, 1 + R=[1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0], + P=[ + 500.0, + 0.0, + 320.0, + 0.0, # fx, 0, cx, Tx + 0.0, + 500.0, + 240.0, + 0.0, # 0, fy, cy, Ty + 0.0, + 0.0, + 1.0, + 0.0, + ], # 0, 0, 1, 0 + binning_x=2, + binning_y=2, + frame_id="camera_optical_frame", + ts=1234567890.123456, + ) + + # Set ROI + original.roi_x_offset = 100 + original.roi_y_offset = 50 + original.roi_height = 200 + original.roi_width = 300 + original.roi_do_rectify = True + + # Encode and decode + binary_msg = original.lcm_encode() + decoded = CameraInfo.lcm_decode(binary_msg) + + # Check basic properties + assert original.height == decoded.height, ( + f"Height mismatch: {original.height} vs {decoded.height}" + ) + assert original.width == decoded.width, f"Width mismatch: {original.width} vs {decoded.width}" + print(f"✓ Image dimensions preserved: {decoded.width}x{decoded.height}") + + assert original.distortion_model == decoded.distortion_model, ( + f"Distortion model mismatch: '{original.distortion_model}' vs '{decoded.distortion_model}'" + ) + print(f"✓ Distortion model preserved: '{decoded.distortion_model}'") + + # Check distortion coefficients + assert len(original.D) == len(decoded.D), ( + f"D length mismatch: {len(original.D)} vs {len(decoded.D)}" + ) + np.testing.assert_allclose( + original.D, decoded.D, rtol=1e-9, atol=1e-9, err_msg="Distortion coefficients don't match" + ) + print(f"✓ Distortion coefficients preserved: {len(decoded.D)} coefficients") + + # Check camera matrices + np.testing.assert_allclose( + original.K, decoded.K, rtol=1e-9, atol=1e-9, err_msg="K matrix doesn't match" + ) + print("✓ Intrinsic matrix K preserved") + + np.testing.assert_allclose( + original.R, decoded.R, rtol=1e-9, atol=1e-9, err_msg="R matrix doesn't match" + ) + print("✓ Rectification matrix R preserved") + + np.testing.assert_allclose( + original.P, decoded.P, rtol=1e-9, atol=1e-9, err_msg="P matrix doesn't match" + ) + print("✓ Projection matrix P preserved") + + # Check binning + assert original.binning_x == decoded.binning_x, ( + f"Binning X mismatch: {original.binning_x} vs {decoded.binning_x}" + ) + assert original.binning_y == decoded.binning_y, ( + f"Binning Y mismatch: {original.binning_y} vs {decoded.binning_y}" + ) + print(f"✓ Binning preserved: {decoded.binning_x}x{decoded.binning_y}") + + # Check ROI + assert original.roi_x_offset == decoded.roi_x_offset, "ROI x_offset mismatch" + assert original.roi_y_offset == decoded.roi_y_offset, "ROI y_offset mismatch" + assert original.roi_height == decoded.roi_height, "ROI height mismatch" + assert original.roi_width == decoded.roi_width, "ROI width mismatch" + assert original.roi_do_rectify == decoded.roi_do_rectify, "ROI do_rectify mismatch" + print("✓ ROI preserved") + + # Check metadata + assert original.frame_id == decoded.frame_id, ( + f"Frame ID mismatch: '{original.frame_id}' vs '{decoded.frame_id}'" + ) + print(f"✓ Frame ID preserved: '{decoded.frame_id}'") + + assert abs(original.ts - decoded.ts) < 1e-6, ( + f"Timestamp mismatch: {original.ts} vs {decoded.ts}" + ) + print(f"✓ Timestamp preserved: {decoded.ts}") + + print("✓ LCM encode/decode test passed - all properties preserved!") + + +def test_numpy_matrix_operations() -> None: + """Test numpy matrix getter/setter operations.""" + print("\nTesting numpy matrix operations...") + + camera_info = CameraInfo() + + # Test K matrix + K = np.array([[525.0, 0.0, 319.5], [0.0, 525.0, 239.5], [0.0, 0.0, 1.0]]) + camera_info.set_K_matrix(K) + K_retrieved = camera_info.get_K_matrix() + np.testing.assert_allclose(K, K_retrieved, rtol=1e-9, atol=1e-9) + print("✓ K matrix setter/getter works") + + # Test P matrix + P = np.array([[525.0, 0.0, 319.5, 0.0], [0.0, 525.0, 239.5, 0.0], [0.0, 0.0, 1.0, 0.0]]) + camera_info.set_P_matrix(P) + P_retrieved = camera_info.get_P_matrix() + np.testing.assert_allclose(P, P_retrieved, rtol=1e-9, atol=1e-9) + print("✓ P matrix setter/getter works") + + # Test R matrix + R = np.eye(3) + camera_info.set_R_matrix(R) + R_retrieved = camera_info.get_R_matrix() + np.testing.assert_allclose(R, R_retrieved, rtol=1e-9, atol=1e-9) + print("✓ R matrix setter/getter works") + + # Test D coefficients + D = np.array([-0.2, 0.1, 0.001, -0.002, 0.05]) + camera_info.set_D_coeffs(D) + D_retrieved = camera_info.get_D_coeffs() + np.testing.assert_allclose(D, D_retrieved, rtol=1e-9, atol=1e-9) + print("✓ D coefficients setter/getter works") + + print("✓ All numpy matrix operations passed!") + + +@pytest.mark.ros +def test_ros_conversion() -> None: + """Test ROS message conversion preserves CameraInfo data.""" + print("\nTesting ROS CameraInfo conversion...") + + # Create test camera info + original = CameraInfo( + height=720, + width=1280, + distortion_model="rational_polynomial", + D=[0.1, -0.2, 0.001, 0.002, -0.05, 0.01, -0.02, 0.003], # 8 coefficients + K=[600.0, 0.0, 640.0, 0.0, 600.0, 360.0, 0.0, 0.0, 1.0], + R=[0.999, -0.01, 0.02, 0.01, 0.999, -0.01, -0.02, 0.01, 0.999], + P=[ + 600.0, + 0.0, + 640.0, + -60.0, # Stereo baseline of 0.1m + 0.0, + 600.0, + 360.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + ], + binning_x=1, + binning_y=1, + frame_id="left_camera_optical", + ts=1234567890.987654, + ) + + # Set ROI + original.roi_x_offset = 200 + original.roi_y_offset = 100 + original.roi_height = 400 + original.roi_width = 800 + original.roi_do_rectify = False + + # Test 1: Convert to ROS and back + ros_msg = original.to_ros_msg() + converted = CameraInfo.from_ros_msg(ros_msg) + + # Check all properties + assert original.height == converted.height, ( + f"Height mismatch: {original.height} vs {converted.height}" + ) + assert original.width == converted.width, ( + f"Width mismatch: {original.width} vs {converted.width}" + ) + print(f"✓ Dimensions preserved: {converted.width}x{converted.height}") + + assert original.distortion_model == converted.distortion_model, ( + f"Distortion model mismatch: '{original.distortion_model}' vs '{converted.distortion_model}'" + ) + print(f"✓ Distortion model preserved: '{converted.distortion_model}'") + + np.testing.assert_allclose( + original.D, + converted.D, + rtol=1e-9, + atol=1e-9, + err_msg="D coefficients don't match after ROS conversion", + ) + print(f"✓ Distortion coefficients preserved: {len(converted.D)} coefficients") + + np.testing.assert_allclose( + original.K, + converted.K, + rtol=1e-9, + atol=1e-9, + err_msg="K matrix doesn't match after ROS conversion", + ) + print("✓ K matrix preserved") + + np.testing.assert_allclose( + original.R, + converted.R, + rtol=1e-9, + atol=1e-9, + err_msg="R matrix doesn't match after ROS conversion", + ) + print("✓ R matrix preserved") + + np.testing.assert_allclose( + original.P, + converted.P, + rtol=1e-9, + atol=1e-9, + err_msg="P matrix doesn't match after ROS conversion", + ) + print("✓ P matrix preserved") + + assert original.binning_x == converted.binning_x, "Binning X mismatch" + assert original.binning_y == converted.binning_y, "Binning Y mismatch" + print(f"✓ Binning preserved: {converted.binning_x}x{converted.binning_y}") + + assert original.roi_x_offset == converted.roi_x_offset, "ROI x_offset mismatch" + assert original.roi_y_offset == converted.roi_y_offset, "ROI y_offset mismatch" + assert original.roi_height == converted.roi_height, "ROI height mismatch" + assert original.roi_width == converted.roi_width, "ROI width mismatch" + assert original.roi_do_rectify == converted.roi_do_rectify, "ROI do_rectify mismatch" + print("✓ ROI preserved") + + assert original.frame_id == converted.frame_id, ( + f"Frame ID mismatch: '{original.frame_id}' vs '{converted.frame_id}'" + ) + print(f"✓ Frame ID preserved: '{converted.frame_id}'") + + assert abs(original.ts - converted.ts) < 1e-6, ( + f"Timestamp mismatch: {original.ts} vs {converted.ts}" + ) + print(f"✓ Timestamp preserved: {converted.ts}") + + # Test 2: Create ROS message directly and convert to DIMOS + ros_msg2 = ROSCameraInfo() + ros_msg2.header = ROSHeader() + ros_msg2.header.frame_id = "test_camera" + ros_msg2.header.stamp.sec = 1234567890 + ros_msg2.header.stamp.nanosec = 500000000 + + ros_msg2.height = 1080 + ros_msg2.width = 1920 + ros_msg2.distortion_model = "plumb_bob" + ros_msg2.d = [-0.3, 0.15, 0.0, 0.0, 0.0] + ros_msg2.k = [1000.0, 0.0, 960.0, 0.0, 1000.0, 540.0, 0.0, 0.0, 1.0] + ros_msg2.r = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0] + ros_msg2.p = [1000.0, 0.0, 960.0, 0.0, 0.0, 1000.0, 540.0, 0.0, 0.0, 0.0, 1.0, 0.0] + ros_msg2.binning_x = 4 + ros_msg2.binning_y = 4 + + ros_msg2.roi = ROSRegionOfInterest() + ros_msg2.roi.x_offset = 10 + ros_msg2.roi.y_offset = 20 + ros_msg2.roi.height = 100 + ros_msg2.roi.width = 200 + ros_msg2.roi.do_rectify = True + + # Convert to DIMOS + dimos_info = CameraInfo.from_ros_msg(ros_msg2) + + assert dimos_info.height == 1080, ( + f"Height not preserved: expected 1080, got {dimos_info.height}" + ) + assert dimos_info.width == 1920, f"Width not preserved: expected 1920, got {dimos_info.width}" + assert dimos_info.frame_id == "test_camera", ( + f"Frame ID not preserved: expected 'test_camera', got '{dimos_info.frame_id}'" + ) + assert dimos_info.distortion_model == "plumb_bob", "Distortion model not preserved" + assert len(dimos_info.D) == 5, ( + f"Wrong number of distortion coefficients: expected 5, got {len(dimos_info.D)}" + ) + print("✓ ROS to DIMOS conversion works correctly") + + # Test 3: Empty/minimal CameraInfo + minimal = CameraInfo(frame_id="minimal_camera", ts=1234567890.0) + minimal_ros = minimal.to_ros_msg() + minimal_converted = CameraInfo.from_ros_msg(minimal_ros) + + assert minimal.frame_id == minimal_converted.frame_id, ( + "Minimal CameraInfo frame_id not preserved" + ) + assert len(minimal_converted.D) == 0, "Minimal CameraInfo should have empty D" + print("✓ Minimal CameraInfo handling works") + + print("\n✓ All ROS conversion tests passed!") + + +def test_equality() -> None: + """Test CameraInfo equality comparison.""" + print("\nTesting CameraInfo equality...") + + info1 = CameraInfo( + height=480, + width=640, + distortion_model="plumb_bob", + D=[-0.1, 0.05, 0.0, 0.0, 0.0], + frame_id="camera1", + ) + + info2 = CameraInfo( + height=480, + width=640, + distortion_model="plumb_bob", + D=[-0.1, 0.05, 0.0, 0.0, 0.0], + frame_id="camera1", + ) + + info3 = CameraInfo( + height=720, + width=1280, # Different resolution + distortion_model="plumb_bob", + D=[-0.1, 0.05, 0.0, 0.0, 0.0], + frame_id="camera1", + ) + + assert info1 == info2, "Identical CameraInfo objects should be equal" + assert info1 != info3, "Different CameraInfo objects should not be equal" + assert info1 != "not_camera_info", "CameraInfo should not equal non-CameraInfo object" + + print("✓ Equality comparison works correctly") + + +def test_camera_info_from_yaml() -> None: + """Test loading CameraInfo from YAML file.""" + + # Get path to the single webcam YAML file + yaml_path = get_project_root() / "dimos" / "hardware" / "camera" / "zed" / "single_webcam.yaml" + + # Load CameraInfo from YAML + camera_info = CameraInfo.from_yaml(str(yaml_path)) + + # Verify loaded values + assert camera_info.width == 640 + assert camera_info.height == 376 + assert camera_info.distortion_model == "plumb_bob" + assert camera_info.frame_id == "camera_optical" + + # Check camera matrix K + K = camera_info.get_K_matrix() + assert K.shape == (3, 3) + assert np.isclose(K[0, 0], 379.45267) # fx + assert np.isclose(K[1, 1], 380.67871) # fy + assert np.isclose(K[0, 2], 302.43516) # cx + assert np.isclose(K[1, 2], 228.00954) # cy + + # Check distortion coefficients + D = camera_info.get_D_coeffs() + assert len(D) == 5 + assert np.isclose(D[0], -0.309435) + + # Check projection matrix P + P = camera_info.get_P_matrix() + assert P.shape == (3, 4) + assert np.isclose(P[0, 0], 291.12888) + + print("✓ CameraInfo loaded successfully from YAML file") + + +def test_calibration_provider() -> None: + """Test CalibrationProvider lazy loading of YAML files.""" + # Get the directory containing calibration files (not the file itself) + calibration_dir = get_project_root() / "dimos" / "hardware" / "camera" / "zed" + + # Create CalibrationProvider instance + Calibrations = CalibrationProvider(calibration_dir) + + # Test lazy loading of single_webcam.yaml using snake_case + camera_info = Calibrations.single_webcam + assert isinstance(camera_info, CameraInfo) + assert camera_info.width == 640 + assert camera_info.height == 376 + + # Test PascalCase access to same calibration + camera_info2 = Calibrations.SingleWebcam + assert isinstance(camera_info2, CameraInfo) + assert camera_info2.width == 640 + assert camera_info2.height == 376 + + # Test caching - both access methods should return same object + assert camera_info is camera_info2 # Same object reference + + # Test __dir__ lists available calibrations in both cases + available = dir(Calibrations) + assert "single_webcam" in available + assert "SingleWebcam" in available + + print("✓ CalibrationProvider test passed with both naming conventions!") diff --git a/dimos/msgs/sensor_msgs/test_Joy.py b/dimos/msgs/sensor_msgs/test_Joy.py new file mode 100644 index 0000000000..ae1b4a6379 --- /dev/null +++ b/dimos/msgs/sensor_msgs/test_Joy.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import pytest + +try: + from sensor_msgs.msg import Joy as ROSJoy + from std_msgs.msg import Header as ROSHeader + + ROS_AVAILABLE = True +except ImportError: + ROSJoy = None + ROSHeader = None + ROS_AVAILABLE = False + +from dimos.msgs.sensor_msgs.Joy import Joy + + +def test_lcm_encode_decode() -> None: + """Test LCM encode/decode preserves Joy data.""" + print("Testing Joy LCM encode/decode...") + + # Create test joy message with sample gamepad data + original = Joy( + ts=1234567890.123456789, + frame_id="gamepad", + axes=[0.5, -0.25, 1.0, -1.0, 0.0, 0.75], # 6 axes (e.g., left/right sticks + triggers) + buttons=[1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0], # 12 buttons + ) + + # Encode to LCM bytes + encoded = original.lcm_encode() + assert isinstance(encoded, bytes) + assert len(encoded) > 0 + + # Decode back + decoded = Joy.lcm_decode(encoded) + + # Verify all fields match + assert abs(decoded.ts - original.ts) < 1e-9 + assert decoded.frame_id == original.frame_id + assert decoded.axes == original.axes + assert decoded.buttons == original.buttons + + print("✓ Joy LCM encode/decode test passed") + + +def test_initialization_methods() -> None: + """Test various initialization methods for Joy.""" + print("Testing Joy initialization methods...") + + # Test default initialization + joy1 = Joy() + assert joy1.axes == [] + assert joy1.buttons == [] + assert joy1.frame_id == "" + assert joy1.ts > 0 # Should have current time + + # Test full initialization + joy2 = Joy(ts=1234567890.0, frame_id="xbox_controller", axes=[0.1, 0.2, 0.3], buttons=[1, 0, 1]) + assert joy2.ts == 1234567890.0 + assert joy2.frame_id == "xbox_controller" + assert joy2.axes == [0.1, 0.2, 0.3] + assert joy2.buttons == [1, 0, 1] + + # Test tuple initialization + joy3 = Joy(([0.5, -0.5], [1, 1, 0])) + assert joy3.axes == [0.5, -0.5] + assert joy3.buttons == [1, 1, 0] + + # Test dict initialization + joy4 = Joy({"axes": [0.7, 0.8], "buttons": [0, 1], "frame_id": "ps4_controller"}) + assert joy4.axes == [0.7, 0.8] + assert joy4.buttons == [0, 1] + assert joy4.frame_id == "ps4_controller" + + # Test copy constructor + joy5 = Joy(joy2) + assert joy5.ts == joy2.ts + assert joy5.frame_id == joy2.frame_id + assert joy5.axes == joy2.axes + assert joy5.buttons == joy2.buttons + assert joy5 is not joy2 # Different objects + + print("✓ Joy initialization methods test passed") + + +def test_equality() -> None: + """Test Joy equality comparison.""" + print("Testing Joy equality...") + + joy1 = Joy(ts=1000.0, frame_id="controller1", axes=[0.5, -0.5], buttons=[1, 0, 1]) + + joy2 = Joy(ts=1000.0, frame_id="controller1", axes=[0.5, -0.5], buttons=[1, 0, 1]) + + joy3 = Joy( + ts=1000.0, + frame_id="controller2", # Different frame_id + axes=[0.5, -0.5], + buttons=[1, 0, 1], + ) + + joy4 = Joy( + ts=1000.0, + frame_id="controller1", + axes=[0.6, -0.5], # Different axes + buttons=[1, 0, 1], + ) + + # Same content should be equal + assert joy1 == joy2 + + # Different frame_id should not be equal + assert joy1 != joy3 + + # Different axes should not be equal + assert joy1 != joy4 + + # Different type should not be equal + assert joy1 != "not a joy" + assert joy1 != 42 + + print("✓ Joy equality test passed") + + +def test_string_representation() -> None: + """Test Joy string representations.""" + print("Testing Joy string representations...") + + joy = Joy( + ts=1234567890.123, + frame_id="test_controller", + axes=[0.1, -0.2, 0.3, 0.4], + buttons=[1, 0, 1, 0, 0, 1], + ) + + # Test __str__ + str_repr = str(joy) + assert "Joy" in str_repr + assert "axes=4 values" in str_repr + assert "buttons=6 values" in str_repr + assert "test_controller" in str_repr + + # Test __repr__ + repr_str = repr(joy) + assert "Joy" in repr_str + assert "1234567890.123" in repr_str + assert "test_controller" in repr_str + assert "[0.1, -0.2, 0.3, 0.4]" in repr_str + assert "[1, 0, 1, 0, 0, 1]" in repr_str + + print("✓ Joy string representation test passed") + + +@pytest.mark.ros +def test_ros_conversion() -> None: + """Test conversion to/from ROS Joy messages.""" + print("Testing Joy ROS conversion...") + + # Create a ROS Joy message + ros_msg = ROSJoy() + ros_msg.header = ROSHeader() + ros_msg.header.stamp.sec = 1234567890 + ros_msg.header.stamp.nanosec = 123456789 + ros_msg.header.frame_id = "ros_gamepad" + ros_msg.axes = [0.25, -0.75, 0.0, 1.0, -1.0] + ros_msg.buttons = [1, 1, 0, 0, 1, 0, 1, 0] + + # Convert from ROS + joy = Joy.from_ros_msg(ros_msg) + assert abs(joy.ts - 1234567890.123456789) < 1e-9 + assert joy.frame_id == "ros_gamepad" + assert joy.axes == [0.25, -0.75, 0.0, 1.0, -1.0] + assert joy.buttons == [1, 1, 0, 0, 1, 0, 1, 0] + + # Convert back to ROS + ros_msg2 = joy.to_ros_msg() + assert ros_msg2.header.frame_id == "ros_gamepad" + assert ros_msg2.header.stamp.sec == 1234567890 + assert abs(ros_msg2.header.stamp.nanosec - 123456789) < 100 # Allow small rounding + assert list(ros_msg2.axes) == [0.25, -0.75, 0.0, 1.0, -1.0] + assert list(ros_msg2.buttons) == [1, 1, 0, 0, 1, 0, 1, 0] + + print("✓ Joy ROS conversion test passed") + + +def test_edge_cases() -> None: + """Test Joy with edge cases.""" + print("Testing Joy edge cases...") + + # Empty axes and buttons + joy1 = Joy(axes=[], buttons=[]) + assert joy1.axes == [] + assert joy1.buttons == [] + encoded = joy1.lcm_encode() + decoded = Joy.lcm_decode(encoded) + assert decoded.axes == [] + assert decoded.buttons == [] + + # Large number of axes and buttons + many_axes = [float(i) / 100.0 for i in range(20)] + many_buttons = [i % 2 for i in range(32)] + joy2 = Joy(axes=many_axes, buttons=many_buttons) + assert len(joy2.axes) == 20 + assert len(joy2.buttons) == 32 + encoded = joy2.lcm_encode() + decoded = Joy.lcm_decode(encoded) + # Check axes with floating point tolerance + assert len(decoded.axes) == len(many_axes) + for i, (a, b) in enumerate(zip(decoded.axes, many_axes, strict=False)): + assert abs(a - b) < 1e-6, f"Axis {i}: {a} != {b}" + assert decoded.buttons == many_buttons + + # Extreme axis values + extreme_axes = [-1.0, 1.0, 0.0, -0.999999, 0.999999] + joy3 = Joy(axes=extreme_axes) + assert joy3.axes == extreme_axes + + print("✓ Joy edge cases test passed") diff --git a/dimos/msgs/sensor_msgs/test_PointCloud2.py b/dimos/msgs/sensor_msgs/test_PointCloud2.py new file mode 100644 index 0000000000..d51b827fa7 --- /dev/null +++ b/dimos/msgs/sensor_msgs/test_PointCloud2.py @@ -0,0 +1,311 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import numpy as np +import pytest + +try: + from sensor_msgs.msg import PointCloud2 as ROSPointCloud2, PointField as ROSPointField + from std_msgs.msg import Header as ROSHeader +except ImportError: + ROSPointCloud2 = None + ROSPointField = None + ROSHeader = None + +from dimos.msgs.sensor_msgs import PointCloud2 +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.utils.testing import SensorReplay + +# Try to import ROS types for testing +try: + ROS_AVAILABLE = True +except ImportError: + ROS_AVAILABLE = False + + +def test_lcm_encode_decode() -> None: + """Test LCM encode/decode preserves pointcloud data.""" + replay = SensorReplay("office_lidar", autocast=LidarMessage.from_msg) + lidar_msg: LidarMessage = replay.load_one("lidar_data_021") + + binary_msg = lidar_msg.lcm_encode() + decoded = PointCloud2.lcm_decode(binary_msg) + + # 1. Check number of points + original_points = lidar_msg.as_numpy() + decoded_points = decoded.as_numpy() + + print(f"Original points: {len(original_points)}") + print(f"Decoded points: {len(decoded_points)}") + assert len(original_points) == len(decoded_points), ( + f"Point count mismatch: {len(original_points)} vs {len(decoded_points)}" + ) + + # 2. Check point coordinates are preserved (within floating point tolerance) + if len(original_points) > 0: + np.testing.assert_allclose( + original_points, + decoded_points, + rtol=1e-6, + atol=1e-6, + err_msg="Point coordinates don't match between original and decoded", + ) + print(f"✓ All {len(original_points)} point coordinates match within tolerance") + + # 3. Check frame_id is preserved + assert lidar_msg.frame_id == decoded.frame_id, ( + f"Frame ID mismatch: '{lidar_msg.frame_id}' vs '{decoded.frame_id}'" + ) + print(f"✓ Frame ID preserved: '{decoded.frame_id}'") + + # 4. Check timestamp is preserved (within reasonable tolerance for float precision) + if lidar_msg.ts is not None and decoded.ts is not None: + assert abs(lidar_msg.ts - decoded.ts) < 1e-6, ( + f"Timestamp mismatch: {lidar_msg.ts} vs {decoded.ts}" + ) + print(f"✓ Timestamp preserved: {decoded.ts}") + + # 5. Check pointcloud properties + assert len(lidar_msg.pointcloud.points) == len(decoded.pointcloud.points), ( + "Open3D pointcloud size mismatch" + ) + + # 6. Additional detailed checks + print("✓ Original pointcloud summary:") + print(f" - Points: {len(original_points)}") + print(f" - Bounds: {original_points.min(axis=0)} to {original_points.max(axis=0)}") + print(f" - Mean: {original_points.mean(axis=0)}") + + print("✓ Decoded pointcloud summary:") + print(f" - Points: {len(decoded_points)}") + print(f" - Bounds: {decoded_points.min(axis=0)} to {decoded_points.max(axis=0)}") + print(f" - Mean: {decoded_points.mean(axis=0)}") + + print("✓ LCM encode/decode test passed - all properties preserved!") + + +@pytest.mark.ros +def test_ros_conversion() -> None: + """Test ROS message conversion preserves pointcloud data.""" + if not ROS_AVAILABLE: + print("ROS packages not available - skipping ROS conversion test") + return + + print("\nTesting ROS PointCloud2 conversion...") + + # Create a simple test point cloud + import open3d as o3d + + points = np.array( + [ + [1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + [-1.0, -2.0, -3.0], + [0.5, 0.5, 0.5], + ], + dtype=np.float32, + ) + + pc = o3d.geometry.PointCloud() + pc.points = o3d.utility.Vector3dVector(points) + + # Create DIMOS PointCloud2 + original = PointCloud2( + pointcloud=pc, + frame_id="test_frame", + ts=1234567890.123456, + ) + + # Test 1: Convert to ROS and back + ros_msg = original.to_ros_msg() + converted = PointCloud2.from_ros_msg(ros_msg) + + # Check points are preserved + original_points = original.as_numpy() + converted_points = converted.as_numpy() + + assert len(original_points) == len(converted_points), ( + f"Point count mismatch: {len(original_points)} vs {len(converted_points)}" + ) + + np.testing.assert_allclose( + original_points, + converted_points, + rtol=1e-6, + atol=1e-6, + err_msg="Points don't match after ROS conversion", + ) + print(f"✓ Points preserved: {len(converted_points)} points match") + + # Check metadata + assert original.frame_id == converted.frame_id, ( + f"Frame ID mismatch: '{original.frame_id}' vs '{converted.frame_id}'" + ) + print(f"✓ Frame ID preserved: '{converted.frame_id}'") + + assert abs(original.ts - converted.ts) < 1e-6, ( + f"Timestamp mismatch: {original.ts} vs {converted.ts}" + ) + print(f"✓ Timestamp preserved: {converted.ts}") + + # Test 2: Create ROS message directly and convert to DIMOS + ros_msg2 = ROSPointCloud2() + ros_msg2.header = ROSHeader() + ros_msg2.header.frame_id = "ros_test_frame" + ros_msg2.header.stamp.sec = 1234567890 + ros_msg2.header.stamp.nanosec = 123456000 + + # Set up point cloud data + ros_msg2.height = 1 + ros_msg2.width = 3 + ros_msg2.fields = [ + ROSPointField(name="x", offset=0, datatype=ROSPointField.FLOAT32, count=1), + ROSPointField(name="y", offset=4, datatype=ROSPointField.FLOAT32, count=1), + ROSPointField(name="z", offset=8, datatype=ROSPointField.FLOAT32, count=1), + ] + ros_msg2.is_bigendian = False + ros_msg2.point_step = 12 + ros_msg2.row_step = 36 + + # Pack test points + test_points = np.array( + [ + [1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + [7.0, 8.0, 9.0], + ], + dtype=np.float32, + ) + ros_msg2.data = test_points.tobytes() + ros_msg2.is_dense = True + + # Convert to DIMOS + dimos_pc = PointCloud2.from_ros_msg(ros_msg2) + + assert dimos_pc.frame_id == "ros_test_frame", ( + f"Frame ID not preserved: expected 'ros_test_frame', got '{dimos_pc.frame_id}'" + ) + + decoded_points = dimos_pc.as_numpy() + assert len(decoded_points) == 3, ( + f"Wrong number of points: expected 3, got {len(decoded_points)}" + ) + + np.testing.assert_allclose( + test_points, + decoded_points, + rtol=1e-6, + atol=1e-6, + err_msg="Points from ROS message don't match", + ) + print("✓ ROS to DIMOS conversion works correctly") + + # Test 3: Empty point cloud + empty_pc = PointCloud2( + pointcloud=o3d.geometry.PointCloud(), + frame_id="empty_frame", + ts=1234567890.0, + ) + + empty_ros = empty_pc.to_ros_msg() + assert empty_ros.width == 0, "Empty cloud should have width 0" + assert empty_ros.height == 0, "Empty cloud should have height 0" + assert len(empty_ros.data) == 0, "Empty cloud should have no data" + + empty_converted = PointCloud2.from_ros_msg(empty_ros) + assert len(empty_converted) == 0, "Empty cloud conversion failed" + print("✓ Empty point cloud handling works") + + print("\n✓ All ROS conversion tests passed!") + + +def test_bounding_box_intersects() -> None: + """Test bounding_box_intersects method with various scenarios.""" + # Test 1: Overlapping boxes + pc1 = PointCloud2.from_numpy(np.array([[0, 0, 0], [2, 2, 2]])) + pc2 = PointCloud2.from_numpy(np.array([[1, 1, 1], [3, 3, 3]])) + assert pc1.bounding_box_intersects(pc2) + assert pc2.bounding_box_intersects(pc1) # Should be symmetric + + # Test 2: Non-overlapping boxes + pc3 = PointCloud2.from_numpy(np.array([[0, 0, 0], [1, 1, 1]])) + pc4 = PointCloud2.from_numpy(np.array([[2, 2, 2], [3, 3, 3]])) + assert not pc3.bounding_box_intersects(pc4) + assert not pc4.bounding_box_intersects(pc3) + + # Test 3: Touching boxes (edge case - should be True) + pc5 = PointCloud2.from_numpy(np.array([[0, 0, 0], [1, 1, 1]])) + pc6 = PointCloud2.from_numpy(np.array([[1, 1, 1], [2, 2, 2]])) + assert pc5.bounding_box_intersects(pc6) + assert pc6.bounding_box_intersects(pc5) + + # Test 4: One box completely inside another + pc7 = PointCloud2.from_numpy(np.array([[0, 0, 0], [3, 3, 3]])) + pc8 = PointCloud2.from_numpy(np.array([[1, 1, 1], [2, 2, 2]])) + assert pc7.bounding_box_intersects(pc8) + assert pc8.bounding_box_intersects(pc7) + + # Test 5: Boxes overlapping only in 2 dimensions (not all 3) + pc9 = PointCloud2.from_numpy(np.array([[0, 0, 0], [2, 2, 1]])) + pc10 = PointCloud2.from_numpy(np.array([[1, 1, 2], [3, 3, 3]])) + assert not pc9.bounding_box_intersects(pc10) + assert not pc10.bounding_box_intersects(pc9) + + # Test 6: Real-world detection scenario with floating point coordinates + detection1_points = np.array( + [[-3.5, -0.3, 0.1], [-3.3, -0.2, 0.1], [-3.5, -0.3, 0.3], [-3.3, -0.2, 0.3]] + ) + pc_det1 = PointCloud2.from_numpy(detection1_points) + + detection2_points = np.array( + [[-3.4, -0.25, 0.15], [-3.2, -0.15, 0.15], [-3.4, -0.25, 0.35], [-3.2, -0.15, 0.35]] + ) + pc_det2 = PointCloud2.from_numpy(detection2_points) + + assert pc_det1.bounding_box_intersects(pc_det2) + + # Test 7: Single point clouds + pc_single1 = PointCloud2.from_numpy(np.array([[1.0, 1.0, 1.0]])) + pc_single2 = PointCloud2.from_numpy(np.array([[1.0, 1.0, 1.0]])) + pc_single3 = PointCloud2.from_numpy(np.array([[2.0, 2.0, 2.0]])) + + # Same point should intersect + assert pc_single1.bounding_box_intersects(pc_single2) + # Different points should not intersect + assert not pc_single1.bounding_box_intersects(pc_single3) + + # Test 8: Empty point clouds + pc_empty1 = PointCloud2.from_numpy(np.array([]).reshape(0, 3)) + pc_empty2 = PointCloud2.from_numpy(np.array([]).reshape(0, 3)) + PointCloud2.from_numpy(np.array([[1.0, 1.0, 1.0]])) + + # Empty clouds should handle gracefully (Open3D returns inf bounds) + # This might raise an exception or return False - we should handle gracefully + try: + result = pc_empty1.bounding_box_intersects(pc_empty2) + # If no exception, verify behavior is consistent + assert isinstance(result, bool) + except: + # If it raises an exception, that's also acceptable for empty clouds + pass + + print("✓ All bounding box intersection tests passed!") + + +if __name__ == "__main__": + test_lcm_encode_decode() + test_ros_conversion() + test_bounding_box_intersects() diff --git a/dimos/msgs/sensor_msgs/test_image.py b/dimos/msgs/sensor_msgs/test_image.py new file mode 100644 index 0000000000..65237e4a6c --- /dev/null +++ b/dimos/msgs/sensor_msgs/test_image.py @@ -0,0 +1,148 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pytest +from reactivex import operators as ops + +from dimos.msgs.sensor_msgs.Image import Image, ImageFormat, sharpness_barrier +from dimos.utils.data import get_data +from dimos.utils.testing import TimedSensorReplay + + +@pytest.fixture +def img(): + image_file_path = get_data("cafe.jpg") + return Image.from_file(str(image_file_path)) + + +def test_file_load(img: Image) -> None: + assert isinstance(img.data, np.ndarray) + assert img.width == 1024 + assert img.height == 771 + assert img.channels == 3 + assert img.shape == (771, 1024, 3) + assert img.data.dtype == np.uint8 + assert img.format == ImageFormat.BGR + assert img.frame_id == "" + assert isinstance(img.ts, float) + assert img.ts > 0 + assert img.data.flags["C_CONTIGUOUS"] + + +def test_lcm_encode_decode(img: Image) -> None: + binary_msg = img.lcm_encode() + decoded_img = Image.lcm_decode(binary_msg) + + assert isinstance(decoded_img, Image) + assert decoded_img is not img + assert decoded_img == img + + +def test_rgb_bgr_conversion(img: Image) -> None: + rgb = img.to_rgb() + assert not rgb == img + assert rgb.to_bgr() == img + + +def test_opencv_conversion(img: Image) -> None: + ocv = img.to_opencv() + decoded_img = Image.from_opencv(ocv) + + # artificially patch timestamp + decoded_img.ts = img.ts + assert decoded_img == img + + +@pytest.mark.tool +def test_sharpness_stream() -> None: + get_data("unitree_office_walk") # Preload data for testing + video_store = TimedSensorReplay( + "unitree_office_walk/video", autocast=lambda x: Image.from_numpy(x).to_rgb() + ) + + cnt = 0 + for image in video_store.iterate(): + cnt = cnt + 1 + print(image.sharpness) + if cnt > 30: + return + + +def test_sharpness_barrier() -> None: + import time + from unittest.mock import MagicMock + + # Create mock images with known sharpness values + # This avoids loading real data from disk + mock_images = [] + sharpness_values = [0.3711, 0.3241, 0.3067, 0.2583, 0.3665] # Just 5 images for 1 window + + for i, sharp in enumerate(sharpness_values): + img = MagicMock() + img.sharpness = sharp + img.ts = 1758912038.208 + i * 0.01 # Simulate timestamps + mock_images.append(img) + + # Track what goes into windows and what comes out + start_wall_time = None + window_contents = [] # List of (wall_time, image) + emitted_images = [] + + def track_input(img): + """Track all images going into sharpness_barrier with wall-clock time""" + nonlocal start_wall_time + wall_time = time.time() + if start_wall_time is None: + start_wall_time = wall_time + relative_time = wall_time - start_wall_time + window_contents.append((relative_time, img)) + return img + + def track_output(img) -> None: + """Track what sharpness_barrier emits""" + emitted_images.append(img) + + # Use 20Hz frequency (0.05s windows) for faster test + # Emit images at 100Hz to get ~5 per window + from reactivex import from_iterable, interval + + source = from_iterable(mock_images).pipe( + ops.zip(interval(0.01)), # 100Hz emission rate + ops.map(lambda x: x[0]), # Extract just the image + ) + + source.pipe( + ops.do_action(track_input), # Track inputs + sharpness_barrier(20), # 20Hz = 0.05s windows + ops.do_action(track_output), # Track outputs + ).run() + + # Only need 0.08s for 1 full window at 20Hz plus buffer + time.sleep(0.08) + + # Verify we got correct emissions (items span across 2 windows due to timing) + # Items 1-4 arrive in first window (0-50ms), item 5 arrives in second window (50-100ms) + assert len(emitted_images) == 2, ( + f"Expected exactly 2 emissions (one per window), got {len(emitted_images)}" + ) + + # Group inputs by wall-clock windows and verify we got the sharpest + + # Verify each window emitted the sharpest image from that window + # First window (0-50ms): items 1-4 + assert emitted_images[0].sharpness == 0.3711 # Highest among first 4 items + + # Second window (50-100ms): only item 5 + assert emitted_images[1].sharpness == 0.3665 # Only item in second window diff --git a/dimos/msgs/std_msgs/Bool.py b/dimos/msgs/std_msgs/Bool.py new file mode 100644 index 0000000000..55751a41eb --- /dev/null +++ b/dimos/msgs/std_msgs/Bool.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Bool message type.""" + +from dimos_lcm.std_msgs import Bool as LCMBool + +try: + from std_msgs.msg import Bool as ROSBool +except ImportError: + ROSBool = None + + +class Bool(LCMBool): + """ROS-compatible Bool message.""" + + msg_name = "std_msgs.Bool" + + def __init__(self, data: bool = False) -> None: + """Initialize Bool with data value.""" + self.data = data + + @classmethod + def from_ros_msg(cls, ros_msg: ROSBool) -> "Bool": + """Create a Bool from a ROS std_msgs/Bool message. + + Args: + ros_msg: ROS Bool message + + Returns: + Bool instance + """ + return cls(data=ros_msg.data) + + def to_ros_msg(self) -> ROSBool: + """Convert to a ROS std_msgs/Bool message. + + Returns: + ROS Bool message + """ + if ROSBool is None: + raise ImportError("ROS std_msgs not available") + ros_msg = ROSBool() + ros_msg.data = bool(self.data) + return ros_msg diff --git a/dimos/msgs/std_msgs/Header.py b/dimos/msgs/std_msgs/Header.py new file mode 100644 index 0000000000..1d17913941 --- /dev/null +++ b/dimos/msgs/std_msgs/Header.py @@ -0,0 +1,104 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +import time + +from dimos_lcm.std_msgs import Header as LCMHeader, Time as LCMTime +from plum import dispatch + +# Import the actual LCM header type that's returned from decoding +try: + from lcm_msgs.std_msgs.Header import Header as DecodedLCMHeader +except ImportError: + DecodedLCMHeader = None + + +class Header(LCMHeader): + msg_name = "std_msgs.Header" + ts: float + + @dispatch + def __init__(self) -> None: + """Initialize a Header with current time and empty frame_id.""" + self.ts = time.time() + sec = int(self.ts) + nsec = int((self.ts - sec) * 1_000_000_000) + super().__init__(seq=0, stamp=LCMTime(sec=sec, nsec=nsec), frame_id="") + + @dispatch + def __init__(self, frame_id: str) -> None: + """Initialize a Header with current time and specified frame_id.""" + self.ts = time.time() + sec = int(self.ts) + nsec = int((self.ts - sec) * 1_000_000_000) + super().__init__(seq=1, stamp=LCMTime(sec=sec, nsec=nsec), frame_id=frame_id) + + @dispatch + def __init__(self, timestamp: float, frame_id: str = "", seq: int = 1) -> None: + """Initialize a Header with Unix timestamp, frame_id, and optional seq.""" + sec = int(timestamp) + nsec = int((timestamp - sec) * 1_000_000_000) + super().__init__(seq=seq, stamp=LCMTime(sec=sec, nsec=nsec), frame_id=frame_id) + + @dispatch + def __init__(self, timestamp: datetime, frame_id: str = "") -> None: + """Initialize a Header with datetime object and frame_id.""" + self.ts = timestamp.timestamp() + sec = int(self.ts) + nsec = int((self.ts - sec) * 1_000_000_000) + super().__init__(seq=1, stamp=LCMTime(sec=sec, nsec=nsec), frame_id=frame_id) + + @dispatch + def __init__(self, seq: int, stamp: LCMTime, frame_id: str) -> None: + """Initialize with explicit seq, stamp, and frame_id (LCM compatibility).""" + super().__init__(seq=seq, stamp=stamp, frame_id=frame_id) + + @dispatch + def __init__(self, header: LCMHeader) -> None: + """Initialize from another Header (copy constructor).""" + super().__init__(seq=header.seq, stamp=header.stamp, frame_id=header.frame_id) + + @dispatch + def __init__(self, header: object) -> None: + """Initialize from a decoded LCM header object.""" + # Handle the case where we get an lcm_msgs.std_msgs.Header.Header object + if hasattr(header, "seq") and hasattr(header, "stamp") and hasattr(header, "frame_id"): + super().__init__(seq=header.seq, stamp=header.stamp, frame_id=header.frame_id) + else: + raise ValueError(f"Cannot create Header from {type(header)}") + + @classmethod + def now(cls, frame_id: str = "", seq: int = 1) -> Header: + """Create a Header with current timestamp.""" + ts = time.time() + return cls(ts, frame_id, seq) + + @property + def timestamp(self) -> float: + """Get timestamp as Unix time (float).""" + return self.stamp.sec + (self.stamp.nsec / 1_000_000_000) + + @property + def datetime(self) -> datetime: + """Get timestamp as datetime object.""" + return datetime.fromtimestamp(self.timestamp) + + def __str__(self) -> str: + return f"Header(seq={self.seq}, time={self.timestamp:.6f}, frame_id='{self.frame_id}')" + + def __repr__(self) -> str: + return f"Header(seq={self.seq}, stamp=Time(sec={self.stamp.sec}, nsec={self.stamp.nsec}), frame_id='{self.frame_id}')" diff --git a/dimos/msgs/std_msgs/Int32.py b/dimos/msgs/std_msgs/Int32.py new file mode 100644 index 0000000000..0ce2f03f60 --- /dev/null +++ b/dimos/msgs/std_msgs/Int32.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright 2025 Dimensional Inc. + +"""Int32 message type.""" + +from typing import ClassVar + +from dimos_lcm.std_msgs import Int32 as LCMInt32 + + +class Int32(LCMInt32): + """ROS-compatible Int32 message.""" + + msg_name: ClassVar[str] = "std_msgs.Int32" + + def __init__(self, data: int = 0) -> None: + """Initialize Int32 with data value.""" + self.data = data diff --git a/dimos/msgs/std_msgs/Int8.py b/dimos/msgs/std_msgs/Int8.py new file mode 100644 index 0000000000..d76b479d41 --- /dev/null +++ b/dimos/msgs/std_msgs/Int8.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright 2025 Dimensional Inc. + +"""Int32 message type.""" + +from typing import ClassVar + +from dimos_lcm.std_msgs import Int8 as LCMInt8 + +try: + from std_msgs.msg import Int8 as ROSInt8 +except ImportError: + ROSInt8 = None + + +class Int8(LCMInt8): + """ROS-compatible Int32 message.""" + + msg_name: ClassVar[str] = "std_msgs.Int8" + + def __init__(self, data: int = 0) -> None: + """Initialize Int8 with data value.""" + self.data = data + + @classmethod + def from_ros_msg(cls, ros_msg: ROSInt8) -> "Int8": + """Create a Bool from a ROS std_msgs/Bool message. + + Args: + ros_msg: ROS Int8 message + + Returns: + Int8 instance + """ + return cls(data=ros_msg.data) + + def to_ros_msg(self) -> ROSInt8: + """Convert to a ROS std_msgs/Bool message. + + Returns: + ROS Int8 message + """ + if ROSInt8 is None: + raise ImportError("ROS std_msgs not available") + ros_msg = ROSInt8() + ros_msg.data = self.data + return ros_msg diff --git a/dimos/msgs/std_msgs/__init__.py b/dimos/msgs/std_msgs/__init__.py new file mode 100644 index 0000000000..e517ea1864 --- /dev/null +++ b/dimos/msgs/std_msgs/__init__.py @@ -0,0 +1,20 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .Bool import Bool +from .Header import Header +from .Int8 import Int8 +from .Int32 import Int32 + +__all__ = ["Bool", "Header", "Int8", "Int32"] diff --git a/dimos/msgs/std_msgs/test_header.py b/dimos/msgs/std_msgs/test_header.py new file mode 100644 index 0000000000..314ee5cd37 --- /dev/null +++ b/dimos/msgs/std_msgs/test_header.py @@ -0,0 +1,98 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import datetime +import time + +from dimos.msgs.std_msgs import Header + + +def test_header_initialization_methods() -> None: + """Test various ways to initialize a Header.""" + + # Method 1: With timestamp and frame_id + header1 = Header(123.456, "world") + assert header1.seq == 1 + assert header1.stamp.sec == 123 + assert header1.stamp.nsec == 456000000 + assert header1.frame_id == "world" + + # Method 2: With just frame_id (uses current time) + header2 = Header("base_link") + assert header2.seq == 1 + assert header2.frame_id == "base_link" + # Timestamp should be close to current time + assert abs(header2.timestamp - time.time()) < 0.1 + + # Method 3: Empty header (current time, empty frame_id) + header3 = Header() + assert header3.seq == 0 + assert header3.frame_id == "" + + # Method 4: With datetime object + dt = datetime(2025, 1, 18, 12, 30, 45, 500000) # 500ms + header4 = Header(dt, "sensor") + assert header4.seq == 1 + assert header4.frame_id == "sensor" + expected_timestamp = dt.timestamp() + assert abs(header4.timestamp - expected_timestamp) < 1e-6 + + # Method 5: With custom seq number + header5 = Header(999.123, "custom", seq=42) + assert header5.seq == 42 + assert header5.stamp.sec == 999 + assert header5.stamp.nsec == 123000000 + assert header5.frame_id == "custom" + + # Method 6: Using now() class method + header6 = Header.now("camera") + assert header6.seq == 1 + assert header6.frame_id == "camera" + assert abs(header6.timestamp - time.time()) < 0.1 + + # Method 7: now() with custom seq + header7 = Header.now("lidar", seq=99) + assert header7.seq == 99 + assert header7.frame_id == "lidar" + + +def test_header_properties() -> None: + """Test Header property accessors.""" + header = Header(1234567890.123456789, "test") + + # Test timestamp property + assert abs(header.timestamp - 1234567890.123456789) < 1e-6 + + # Test datetime property + dt = header.datetime + assert isinstance(dt, datetime) + assert abs(dt.timestamp() - 1234567890.123456789) < 1e-6 + + +def test_header_string_representation() -> None: + """Test Header string representations.""" + header = Header(100.5, "map", seq=10) + + # Test __str__ + str_repr = str(header) + assert "seq=10" in str_repr + assert "time=100.5" in str_repr + assert "frame_id='map'" in str_repr + + # Test __repr__ + repr_str = repr(header) + assert "Header(" in repr_str + assert "seq=10" in repr_str + assert "Time(sec=100, nsec=500000000)" in repr_str + assert "frame_id='map'" in repr_str diff --git a/dimos/msgs/tf2_msgs/TFMessage.py b/dimos/msgs/tf2_msgs/TFMessage.py new file mode 100644 index 0000000000..5aabfa4b23 --- /dev/null +++ b/dimos/msgs/tf2_msgs/TFMessage.py @@ -0,0 +1,159 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import TYPE_CHECKING, BinaryIO + +from dimos_lcm.tf2_msgs import TFMessage as LCMTFMessage + +try: + from geometry_msgs.msg import TransformStamped as ROSTransformStamped + from tf2_msgs.msg import TFMessage as ROSTFMessage +except ImportError: + ROSTFMessage = None + ROSTransformStamped = None + +from dimos.msgs.geometry_msgs.Quaternion import Quaternion +from dimos.msgs.geometry_msgs.Transform import Transform +from dimos.msgs.geometry_msgs.Vector3 import Vector3 + +if TYPE_CHECKING: + from collections.abc import Iterator + + +class TFMessage: + """TFMessage that accepts Transform objects and encodes to LCM format.""" + + transforms: list[Transform] + msg_name = "tf2_msgs.TFMessage" + + def __init__(self, *transforms: Transform) -> None: + self.transforms = list(transforms) + + def add_transform(self, transform: Transform, child_frame_id: str = "base_link") -> None: + """Add a transform to the message.""" + self.transforms.append(transform) + self.transforms_length = len(self.transforms) + + def lcm_encode(self) -> bytes: + """Encode as LCM TFMessage. + + Args: + child_frame_ids: Optional list of child frame IDs for each transform. + If not provided, defaults to "base_link" for all. + """ + + res = list(map(lambda t: t.lcm_transform(), self.transforms)) + + lcm_msg = LCMTFMessage( + transforms_length=len(self.transforms), + transforms=res, + ) + + return lcm_msg.lcm_encode() + + @classmethod + def lcm_decode(cls, data: bytes | BinaryIO) -> TFMessage: + """Decode from LCM TFMessage bytes.""" + lcm_msg = LCMTFMessage.lcm_decode(data) + + # Convert LCM TransformStamped objects to Transform objects + transforms = [] + for lcm_transform_stamped in lcm_msg.transforms: + # Extract timestamp + ts = lcm_transform_stamped.header.stamp.sec + ( + lcm_transform_stamped.header.stamp.nsec / 1_000_000_000 + ) + + # Create Transform with our custom types + lcm_trans = lcm_transform_stamped.transform.translation + lcm_rot = lcm_transform_stamped.transform.rotation + + transform = Transform( + translation=Vector3(lcm_trans.x, lcm_trans.y, lcm_trans.z), + rotation=Quaternion(lcm_rot.x, lcm_rot.y, lcm_rot.z, lcm_rot.w), + frame_id=lcm_transform_stamped.header.frame_id, + child_frame_id=lcm_transform_stamped.child_frame_id, + ts=ts, + ) + transforms.append(transform) + + return cls(*transforms) + + def __len__(self) -> int: + """Return number of transforms.""" + return len(self.transforms) + + def __getitem__(self, index: int) -> Transform: + """Get transform by index.""" + return self.transforms[index] + + def __iter__(self) -> Iterator: + """Iterate over transforms.""" + return iter(self.transforms) + + def __repr__(self) -> str: + return f"TFMessage({len(self.transforms)} transforms)" + + def __str__(self) -> str: + lines = [f"TFMessage with {len(self.transforms)} transforms:"] + for i, transform in enumerate(self.transforms): + lines.append(f" [{i}] {transform.frame_id} @ {transform.ts:.3f}") + return "\n".join(lines) + + @classmethod + def from_ros_msg(cls, ros_msg: ROSTFMessage) -> TFMessage: + """Create a TFMessage from a ROS tf2_msgs/TFMessage message. + + Args: + ros_msg: ROS TFMessage message + + Returns: + TFMessage instance + """ + transforms = [] + for ros_transform_stamped in ros_msg.transforms: + # Convert from ROS TransformStamped to our Transform + transform = Transform.from_ros_transform_stamped(ros_transform_stamped) + transforms.append(transform) + + return cls(*transforms) + + def to_ros_msg(self) -> ROSTFMessage: + """Convert to a ROS tf2_msgs/TFMessage message. + + Returns: + ROS TFMessage message + """ + ros_msg = ROSTFMessage() + + # Convert each Transform to ROS TransformStamped + for transform in self.transforms: + ros_msg.transforms.append(transform.to_ros_transform_stamped()) + + return ros_msg diff --git a/dimos/msgs/tf2_msgs/__init__.py b/dimos/msgs/tf2_msgs/__init__.py new file mode 100644 index 0000000000..683e4ec61b --- /dev/null +++ b/dimos/msgs/tf2_msgs/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.msgs.tf2_msgs.TFMessage import TFMessage + +__all__ = ["TFMessage"] diff --git a/dimos/msgs/tf2_msgs/test_TFMessage.py b/dimos/msgs/tf2_msgs/test_TFMessage.py new file mode 100644 index 0000000000..26c0bac570 --- /dev/null +++ b/dimos/msgs/tf2_msgs/test_TFMessage.py @@ -0,0 +1,269 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +try: + from geometry_msgs.msg import TransformStamped as ROSTransformStamped + from tf2_msgs.msg import TFMessage as ROSTFMessage +except ImportError: + ROSTransformStamped = None + ROSTFMessage = None + +from dimos_lcm.tf2_msgs import TFMessage as LCMTFMessage + +from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 +from dimos.msgs.tf2_msgs import TFMessage + + +def test_tfmessage_initialization() -> None: + """Test TFMessage initialization with Transform objects.""" + # Create some transforms + tf1 = Transform( + translation=Vector3(1, 2, 3), rotation=Quaternion(0, 0, 0, 1), frame_id="world", ts=100.0 + ) + tf2 = Transform( + translation=Vector3(4, 5, 6), + rotation=Quaternion(0, 0, 0.707, 0.707), + frame_id="map", + ts=101.0, + ) + + # Create TFMessage with transforms + msg = TFMessage(tf1, tf2) + + assert len(msg) == 2 + assert msg[0] == tf1 + assert msg[1] == tf2 + + # Test iteration + transforms = list(msg) + assert transforms == [tf1, tf2] + + +def test_tfmessage_empty() -> None: + """Test empty TFMessage.""" + msg = TFMessage() + assert len(msg) == 0 + assert list(msg) == [] + + +def test_tfmessage_add_transform() -> None: + """Test adding transforms to TFMessage.""" + msg = TFMessage() + + tf = Transform(translation=Vector3(1, 2, 3), frame_id="base", ts=200.0) + + msg.add_transform(tf) + assert len(msg) == 1 + assert msg[0] == tf + + +def test_tfmessage_lcm_encode_decode() -> None: + """Test encoding TFMessage to LCM bytes.""" + # Create transforms + tf1 = Transform( + translation=Vector3(1.0, 2.0, 3.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + child_frame_id="robot", + frame_id="world", + ts=123.456, + ) + tf2 = Transform( + translation=Vector3(4.0, 5.0, 6.0), + rotation=Quaternion(0.0, 0.0, 0.707, 0.707), + frame_id="robot", + child_frame_id="target", + ts=124.567, + ) + + # Create TFMessage + msg = TFMessage(tf1, tf2) + + # Encode with custom child_frame_ids + encoded = msg.lcm_encode() + + # Decode using LCM to verify + lcm_msg = LCMTFMessage.lcm_decode(encoded) + + assert lcm_msg.transforms_length == 2 + + # Check first transform + ts1 = lcm_msg.transforms[0] + assert ts1.header.frame_id == "world" + assert ts1.child_frame_id == "robot" + assert ts1.header.stamp.sec == 123 + assert ts1.header.stamp.nsec == 456000000 + assert ts1.transform.translation.x == 1.0 + assert ts1.transform.translation.y == 2.0 + assert ts1.transform.translation.z == 3.0 + + # Check second transform + ts2 = lcm_msg.transforms[1] + assert ts2.header.frame_id == "robot" + assert ts2.child_frame_id == "target" + assert ts2.transform.rotation.z == 0.707 + assert ts2.transform.rotation.w == 0.707 + + +@pytest.mark.ros +def test_tfmessage_from_ros_msg() -> None: + """Test creating a TFMessage from a ROS TFMessage message.""" + + ros_msg = ROSTFMessage() + + # Add first transform + tf1 = ROSTransformStamped() + tf1.header.frame_id = "world" + tf1.header.stamp.sec = 123 + tf1.header.stamp.nanosec = 456000000 + tf1.child_frame_id = "robot" + tf1.transform.translation.x = 1.0 + tf1.transform.translation.y = 2.0 + tf1.transform.translation.z = 3.0 + tf1.transform.rotation.x = 0.0 + tf1.transform.rotation.y = 0.0 + tf1.transform.rotation.z = 0.0 + tf1.transform.rotation.w = 1.0 + ros_msg.transforms.append(tf1) + + # Add second transform + tf2 = ROSTransformStamped() + tf2.header.frame_id = "robot" + tf2.header.stamp.sec = 124 + tf2.header.stamp.nanosec = 567000000 + tf2.child_frame_id = "sensor" + tf2.transform.translation.x = 4.0 + tf2.transform.translation.y = 5.0 + tf2.transform.translation.z = 6.0 + tf2.transform.rotation.x = 0.0 + tf2.transform.rotation.y = 0.0 + tf2.transform.rotation.z = 0.707 + tf2.transform.rotation.w = 0.707 + ros_msg.transforms.append(tf2) + + # Convert to TFMessage + tfmsg = TFMessage.from_ros_msg(ros_msg) + + assert len(tfmsg) == 2 + + # Check first transform + assert tfmsg[0].frame_id == "world" + assert tfmsg[0].child_frame_id == "robot" + assert tfmsg[0].ts == 123.456 + assert tfmsg[0].translation.x == 1.0 + assert tfmsg[0].translation.y == 2.0 + assert tfmsg[0].translation.z == 3.0 + assert tfmsg[0].rotation.w == 1.0 + + # Check second transform + assert tfmsg[1].frame_id == "robot" + assert tfmsg[1].child_frame_id == "sensor" + assert tfmsg[1].ts == 124.567 + assert tfmsg[1].translation.x == 4.0 + assert tfmsg[1].translation.y == 5.0 + assert tfmsg[1].translation.z == 6.0 + assert tfmsg[1].rotation.z == 0.707 + assert tfmsg[1].rotation.w == 0.707 + + +@pytest.mark.ros +def test_tfmessage_to_ros_msg() -> None: + """Test converting a TFMessage to a ROS TFMessage message.""" + # Create transforms + tf1 = Transform( + translation=Vector3(1.0, 2.0, 3.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="map", + child_frame_id="base_link", + ts=123.456, + ) + tf2 = Transform( + translation=Vector3(7.0, 8.0, 9.0), + rotation=Quaternion(0.1, 0.2, 0.3, 0.9), + frame_id="base_link", + child_frame_id="lidar", + ts=125.789, + ) + + tfmsg = TFMessage(tf1, tf2) + + # Convert to ROS message + ros_msg = tfmsg.to_ros_msg() + + assert isinstance(ros_msg, ROSTFMessage) + assert len(ros_msg.transforms) == 2 + + # Check first transform + assert ros_msg.transforms[0].header.frame_id == "map" + assert ros_msg.transforms[0].child_frame_id == "base_link" + assert ros_msg.transforms[0].header.stamp.sec == 123 + assert ros_msg.transforms[0].header.stamp.nanosec == 456000000 + assert ros_msg.transforms[0].transform.translation.x == 1.0 + assert ros_msg.transforms[0].transform.translation.y == 2.0 + assert ros_msg.transforms[0].transform.translation.z == 3.0 + assert ros_msg.transforms[0].transform.rotation.w == 1.0 + + # Check second transform + assert ros_msg.transforms[1].header.frame_id == "base_link" + assert ros_msg.transforms[1].child_frame_id == "lidar" + assert ros_msg.transforms[1].header.stamp.sec == 125 + assert ros_msg.transforms[1].header.stamp.nanosec == 789000000 + assert ros_msg.transforms[1].transform.translation.x == 7.0 + assert ros_msg.transforms[1].transform.translation.y == 8.0 + assert ros_msg.transforms[1].transform.translation.z == 9.0 + assert ros_msg.transforms[1].transform.rotation.x == 0.1 + assert ros_msg.transforms[1].transform.rotation.y == 0.2 + assert ros_msg.transforms[1].transform.rotation.z == 0.3 + assert ros_msg.transforms[1].transform.rotation.w == 0.9 + + +@pytest.mark.ros +def test_tfmessage_ros_roundtrip() -> None: + """Test round-trip conversion between TFMessage and ROS TFMessage.""" + # Create transforms with various properties + tf1 = Transform( + translation=Vector3(1.5, 2.5, 3.5), + rotation=Quaternion(0.15, 0.25, 0.35, 0.85), + frame_id="odom", + child_frame_id="base_footprint", + ts=100.123, + ) + tf2 = Transform( + translation=Vector3(0.1, 0.2, 0.3), + rotation=Quaternion(0.0, 0.0, 0.383, 0.924), + frame_id="base_footprint", + child_frame_id="camera", + ts=100.456, + ) + + original = TFMessage(tf1, tf2) + + # Convert to ROS and back + ros_msg = original.to_ros_msg() + restored = TFMessage.from_ros_msg(ros_msg) + + assert len(restored) == len(original) + + for orig_tf, rest_tf in zip(original, restored, strict=False): + assert rest_tf.frame_id == orig_tf.frame_id + assert rest_tf.child_frame_id == orig_tf.child_frame_id + assert rest_tf.ts == orig_tf.ts + assert rest_tf.translation.x == orig_tf.translation.x + assert rest_tf.translation.y == orig_tf.translation.y + assert rest_tf.translation.z == orig_tf.translation.z + assert rest_tf.rotation.x == orig_tf.rotation.x + assert rest_tf.rotation.y == orig_tf.rotation.y + assert rest_tf.rotation.z == orig_tf.rotation.z + assert rest_tf.rotation.w == orig_tf.rotation.w diff --git a/dimos/msgs/tf2_msgs/test_TFMessage_lcmpub.py b/dimos/msgs/tf2_msgs/test_TFMessage_lcmpub.py new file mode 100644 index 0000000000..9471673821 --- /dev/null +++ b/dimos/msgs/tf2_msgs/test_TFMessage_lcmpub.py @@ -0,0 +1,68 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +import pytest + +from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 +from dimos.msgs.tf2_msgs import TFMessage +from dimos.protocol.pubsub.lcmpubsub import LCM, Topic + + +# Publishes a series of transforms representing a robot kinematic chain +# to actual LCM messages, foxglove running in parallel should render this +@pytest.mark.skip +def test_publish_transforms() -> None: + from dimos_lcm.tf2_msgs import TFMessage as LCMTFMessage + + lcm = LCM(autoconf=True) + lcm.start() + + topic = Topic(topic="/tf", lcm_type=LCMTFMessage) + + # Create a robot kinematic chain using our new types + current_time = time.time() + + # 1. World to base_link transform (robot at position) + world_to_base = Transform( + translation=Vector3(4.0, 3.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.382683, 0.923880), # 45 degrees around Z + frame_id="world", + child_frame_id="base_link", + ts=current_time, + ) + + # 2. Base to arm transform (arm lifted up) + base_to_arm = Transform( + translation=Vector3(0.2, 0.0, 1.5), + rotation=Quaternion(0.0, 0.258819, 0.0, 0.965926), # 30 degrees around Y + frame_id="base_link", + child_frame_id="arm_link", + ts=current_time, + ) + + lcm.publish(topic, TFMessage(world_to_base, base_to_arm)) + + time.sleep(0.05) + # 3. Arm to gripper transform (gripper extended) + arm_to_gripper = Transform( + translation=Vector3(0.5, 0.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), # No rotation + frame_id="arm_link", + child_frame_id="gripper_link", + ts=current_time, + ) + + lcm.publish(topic, TFMessage(world_to_base, arm_to_gripper)) diff --git a/dimos/msgs/vision_msgs/BoundingBox2DArray.py b/dimos/msgs/vision_msgs/BoundingBox2DArray.py new file mode 100644 index 0000000000..6568656884 --- /dev/null +++ b/dimos/msgs/vision_msgs/BoundingBox2DArray.py @@ -0,0 +1,19 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos_lcm.vision_msgs.BoundingBox2DArray import BoundingBox2DArray as LCMBoundingBox2DArray + + +class BoundingBox2DArray(LCMBoundingBox2DArray): + msg_name = "vision_msgs.BoundingBox2DArray" diff --git a/dimos/msgs/vision_msgs/BoundingBox3DArray.py b/dimos/msgs/vision_msgs/BoundingBox3DArray.py new file mode 100644 index 0000000000..afa3d793f9 --- /dev/null +++ b/dimos/msgs/vision_msgs/BoundingBox3DArray.py @@ -0,0 +1,19 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos_lcm.vision_msgs.BoundingBox3DArray import BoundingBox3DArray as LCMBoundingBox3DArray + + +class BoundingBox3DArray(LCMBoundingBox3DArray): + msg_name = "vision_msgs.BoundingBox3DArray" diff --git a/dimos/msgs/vision_msgs/Detection2DArray.py b/dimos/msgs/vision_msgs/Detection2DArray.py new file mode 100644 index 0000000000..79c84f7609 --- /dev/null +++ b/dimos/msgs/vision_msgs/Detection2DArray.py @@ -0,0 +1,27 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from dimos_lcm.vision_msgs.Detection2DArray import Detection2DArray as LCMDetection2DArray + +from dimos.types.timestamped import to_timestamp + + +class Detection2DArray(LCMDetection2DArray): + msg_name = "vision_msgs.Detection2DArray" + + # for _get_field_type() to work when decoding in _decode_one() + __annotations__ = LCMDetection2DArray.__annotations__ + + @property + def ts(self) -> float: + return to_timestamp(self.header.stamp) diff --git a/dimos/msgs/vision_msgs/Detection3DArray.py b/dimos/msgs/vision_msgs/Detection3DArray.py new file mode 100644 index 0000000000..21dabb8057 --- /dev/null +++ b/dimos/msgs/vision_msgs/Detection3DArray.py @@ -0,0 +1,19 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos_lcm.vision_msgs.Detection3DArray import Detection3DArray as LCMDetection3DArray + + +class Detection3DArray(LCMDetection3DArray): + msg_name = "vision_msgs.Detection3DArray" diff --git a/dimos/msgs/vision_msgs/__init__.py b/dimos/msgs/vision_msgs/__init__.py new file mode 100644 index 0000000000..af170cbfab --- /dev/null +++ b/dimos/msgs/vision_msgs/__init__.py @@ -0,0 +1,6 @@ +from .BoundingBox2DArray import BoundingBox2DArray +from .BoundingBox3DArray import BoundingBox3DArray +from .Detection2DArray import Detection2DArray +from .Detection3DArray import Detection3DArray + +__all__ = ["BoundingBox2DArray", "BoundingBox3DArray", "Detection2DArray", "Detection3DArray"] diff --git a/dimos/navigation/bbox_navigation.py b/dimos/navigation/bbox_navigation.py new file mode 100644 index 0000000000..db66ab8349 --- /dev/null +++ b/dimos/navigation/bbox_navigation.py @@ -0,0 +1,76 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from dimos_lcm.sensor_msgs import CameraInfo +from reactivex.disposable import Disposable + +from dimos.core import In, Module, Out, rpc +from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Vector3 +from dimos.msgs.vision_msgs import Detection2DArray +from dimos.utils.logging_config import setup_logger + +logger = setup_logger(__name__, level=logging.DEBUG) + + +class BBoxNavigationModule(Module): + """Minimal module that converts 2D bbox center to navigation goals.""" + + detection2d: In[Detection2DArray] = None + camera_info: In[CameraInfo] = None + goal_request: Out[PoseStamped] = None + + def __init__(self, goal_distance: float = 1.0) -> None: + super().__init__() + self.goal_distance = goal_distance + self.camera_intrinsics = None + + @rpc + def start(self) -> None: + unsub = self.camera_info.subscribe( + lambda msg: setattr(self, "camera_intrinsics", [msg.K[0], msg.K[4], msg.K[2], msg.K[5]]) + ) + self._disposables.add(Disposable(unsub)) + + unsub = self.detection2d.subscribe(self._on_detection) + self._disposables.add(Disposable(unsub)) + + @rpc + def stop(self) -> None: + super().stop() + + def _on_detection(self, det: Detection2DArray) -> None: + if det.detections_length == 0 or not self.camera_intrinsics: + return + fx, fy, cx, cy = self.camera_intrinsics + center_x, center_y = ( + det.detections[0].bbox.center.position.x, + det.detections[0].bbox.center.position.y, + ) + x, y, z = ( + (center_x - cx) / fx * self.goal_distance, + (center_y - cy) / fy * self.goal_distance, + self.goal_distance, + ) + goal = PoseStamped( + position=Vector3(z, -x, -y), + orientation=Quaternion(0, 0, 0, 1), + frame_id=det.header.frame_id, + ) + logger.debug( + f"BBox center: ({center_x:.1f}, {center_y:.1f}) → " + f"Goal pose: ({z:.2f}, {-x:.2f}, {-y:.2f}) in frame '{det.header.frame_id}'" + ) + self.goal_request.publish(goal) diff --git a/dimos/navigation/bt_navigator/__init__.py b/dimos/navigation/bt_navigator/__init__.py new file mode 100644 index 0000000000..cfd252ff6a --- /dev/null +++ b/dimos/navigation/bt_navigator/__init__.py @@ -0,0 +1 @@ +from .navigator import BehaviorTreeNavigator diff --git a/dimos/navigation/bt_navigator/goal_validator.py b/dimos/navigation/bt_navigator/goal_validator.py new file mode 100644 index 0000000000..f0c4a9ce37 --- /dev/null +++ b/dimos/navigation/bt_navigator/goal_validator.py @@ -0,0 +1,443 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections import deque + +import numpy as np + +from dimos.msgs.geometry_msgs import Vector3, VectorLike +from dimos.msgs.nav_msgs import CostValues, OccupancyGrid + + +def find_safe_goal( + costmap: OccupancyGrid, + goal: VectorLike, + algorithm: str = "bfs", + cost_threshold: int = 50, + min_clearance: float = 0.3, + max_search_distance: float = 5.0, + connectivity_check_radius: int = 3, +) -> Vector3 | None: + """ + Find a safe goal position when the original goal is in collision or too close to obstacles. + + Args: + costmap: The occupancy grid/costmap + goal: Original goal position in world coordinates + algorithm: Algorithm to use ("bfs", "spiral", "voronoi", "gradient_descent") + cost_threshold: Maximum acceptable cost for a safe position (default: 50) + min_clearance: Minimum clearance from obstacles in meters (default: 0.3m) + max_search_distance: Maximum distance to search from original goal in meters (default: 5.0m) + connectivity_check_radius: Radius in cells to check for connectivity (default: 3) + + Returns: + Safe goal position in world coordinates, or None if no safe position found + """ + + if algorithm == "bfs": + return _find_safe_goal_bfs( + costmap, + goal, + cost_threshold, + min_clearance, + max_search_distance, + connectivity_check_radius, + ) + elif algorithm == "spiral": + return _find_safe_goal_spiral( + costmap, + goal, + cost_threshold, + min_clearance, + max_search_distance, + connectivity_check_radius, + ) + elif algorithm == "voronoi": + return _find_safe_goal_voronoi( + costmap, goal, cost_threshold, min_clearance, max_search_distance + ) + elif algorithm == "gradient_descent": + return _find_safe_goal_gradient( + costmap, + goal, + cost_threshold, + min_clearance, + max_search_distance, + connectivity_check_radius, + ) + else: + raise ValueError(f"Unknown algorithm: {algorithm}") + + +def _find_safe_goal_bfs( + costmap: OccupancyGrid, + goal: VectorLike, + cost_threshold: int, + min_clearance: float, + max_search_distance: float, + connectivity_check_radius: int, +) -> Vector3 | None: + """ + BFS-based search for nearest safe goal position. + This guarantees finding the closest valid position. + + Pros: + - Guarantees finding the closest safe position + - Can check connectivity to avoid isolated spots + - Efficient for small to medium search areas + + Cons: + - Can be slower for large search areas + - Memory usage scales with search area + """ + + # Convert goal to grid coordinates + goal_grid = costmap.world_to_grid(goal) + gx, gy = int(goal_grid.x), int(goal_grid.y) + + # Convert distances to grid cells + clearance_cells = int(np.ceil(min_clearance / costmap.resolution)) + max_search_cells = int(np.ceil(max_search_distance / costmap.resolution)) + + # BFS queue and visited set + queue = deque([(gx, gy, 0)]) + visited = set([(gx, gy)]) + + # 8-connected neighbors + neighbors = [(0, 1), (1, 0), (0, -1), (-1, 0), (1, 1), (1, -1), (-1, 1), (-1, -1)] + + while queue: + x, y, dist = queue.popleft() + + # Check if we've exceeded max search distance + if dist > max_search_cells: + break + + # Check if position is valid + if _is_position_safe( + costmap, x, y, cost_threshold, clearance_cells, connectivity_check_radius + ): + # Convert back to world coordinates + return costmap.grid_to_world((x, y)) + + # Add neighbors to queue + for dx, dy in neighbors: + nx, ny = x + dx, y + dy + + # Check bounds + if 0 <= nx < costmap.width and 0 <= ny < costmap.height: + if (nx, ny) not in visited: + visited.add((nx, ny)) + queue.append((nx, ny, dist + 1)) + + return None + + +def _find_safe_goal_spiral( + costmap: OccupancyGrid, + goal: VectorLike, + cost_threshold: int, + min_clearance: float, + max_search_distance: float, + connectivity_check_radius: int, +) -> Vector3 | None: + """ + Spiral search pattern from goal outward. + + Pros: + - Simple and predictable pattern + - Memory efficient + - Good for uniformly distributed obstacles + + Cons: + - May not find the absolute closest safe position + - Can miss nearby safe spots due to spiral pattern + """ + + # Convert goal to grid coordinates + goal_grid = costmap.world_to_grid(goal) + cx, cy = int(goal_grid.x), int(goal_grid.y) + + # Convert distances to grid cells + clearance_cells = int(np.ceil(min_clearance / costmap.resolution)) + max_radius = int(np.ceil(max_search_distance / costmap.resolution)) + + # Spiral outward + for radius in range(0, max_radius + 1): + if radius == 0: + # Check center point + if _is_position_safe( + costmap, cx, cy, cost_threshold, clearance_cells, connectivity_check_radius + ): + return costmap.grid_to_world((cx, cy)) + else: + # Check points on the square perimeter at this radius + points = [] + + # Top and bottom edges + for x in range(cx - radius, cx + radius + 1): + points.append((x, cy - radius)) # Top + points.append((x, cy + radius)) # Bottom + + # Left and right edges (excluding corners to avoid duplicates) + for y in range(cy - radius + 1, cy + radius): + points.append((cx - radius, y)) # Left + points.append((cx + radius, y)) # Right + + # Check each point + for x, y in points: + if 0 <= x < costmap.width and 0 <= y < costmap.height: + if _is_position_safe( + costmap, x, y, cost_threshold, clearance_cells, connectivity_check_radius + ): + return costmap.grid_to_world((x, y)) + + return None + + +def _find_safe_goal_voronoi( + costmap: OccupancyGrid, + goal: VectorLike, + cost_threshold: int, + min_clearance: float, + max_search_distance: float, +) -> Vector3 | None: + """ + Find safe position using Voronoi diagram (ridge points equidistant from obstacles). + + Pros: + - Finds positions maximally far from obstacles + - Good for narrow passages + - Natural safety margin + + Cons: + - More computationally expensive + - May find positions unnecessarily far from obstacles + - Requires scipy for efficient implementation + """ + + from scipy import ndimage + from skimage.morphology import skeletonize + + # Convert goal to grid coordinates + goal_grid = costmap.world_to_grid(goal) + gx, gy = int(goal_grid.x), int(goal_grid.y) + + # Create binary obstacle map + free_map = (costmap.grid < cost_threshold) & (costmap.grid != CostValues.UNKNOWN) + + # Compute distance transform + distance_field = ndimage.distance_transform_edt(free_map) + + # Find skeleton/medial axis (approximation of Voronoi diagram) + skeleton = skeletonize(free_map) + + # Filter skeleton points by minimum clearance + clearance_cells = int(np.ceil(min_clearance / costmap.resolution)) + valid_skeleton = skeleton & (distance_field >= clearance_cells) + + if not np.any(valid_skeleton): + # Fall back to BFS if no valid skeleton points + return _find_safe_goal_bfs( + costmap, goal, cost_threshold, min_clearance, max_search_distance, 3 + ) + + # Find nearest valid skeleton point to goal + skeleton_points = np.argwhere(valid_skeleton) + if len(skeleton_points) == 0: + return None + + # Calculate distances from goal to all skeleton points + distances = np.sqrt((skeleton_points[:, 1] - gx) ** 2 + (skeleton_points[:, 0] - gy) ** 2) + + # Filter by max search distance + max_search_cells = max_search_distance / costmap.resolution + valid_indices = distances <= max_search_cells + + if not np.any(valid_indices): + return None + + # Find closest valid point + valid_distances = distances[valid_indices] + valid_points = skeleton_points[valid_indices] + closest_idx = np.argmin(valid_distances) + best_y, best_x = valid_points[closest_idx] + + return costmap.grid_to_world((best_x, best_y)) + + +def _find_safe_goal_gradient( + costmap: OccupancyGrid, + goal: VectorLike, + cost_threshold: int, + min_clearance: float, + max_search_distance: float, + connectivity_check_radius: int, +) -> Vector3 | None: + """ + Use gradient descent on the costmap to find a safe position. + + Pros: + - Naturally flows away from obstacles + - Works well with gradient costmaps + - Can handle complex cost distributions + + Cons: + - Can get stuck in local minima + - Requires a gradient costmap + - May not find globally optimal position + """ + + # Convert goal to grid coordinates + goal_grid = costmap.world_to_grid(goal) + x, y = goal_grid.x, goal_grid.y + + # Convert distances to grid cells + clearance_cells = int(np.ceil(min_clearance / costmap.resolution)) + max_search_cells = int(np.ceil(max_search_distance / costmap.resolution)) + + # Create gradient if needed (assuming costmap might already be a gradient) + if np.all((costmap.grid == 0) | (costmap.grid == 100) | (costmap.grid == -1)): + # Binary map, create gradient + gradient_map = costmap.gradient( + obstacle_threshold=cost_threshold, max_distance=min_clearance * 2 + ) + grid = gradient_map.grid + else: + grid = costmap.grid + + # Gradient descent with momentum + momentum = 0.9 + learning_rate = 1.0 + vx, vy = 0.0, 0.0 + + best_x, best_y = None, None + best_cost = float("inf") + + for iteration in range(100): # Max iterations + ix, iy = int(x), int(y) + + # Check if current position is valid + if 0 <= ix < costmap.width and 0 <= iy < costmap.height: + current_cost = grid[iy, ix] + + # Check distance from original goal + dist = np.sqrt((x - goal_grid.x) ** 2 + (y - goal_grid.y) ** 2) + if dist > max_search_cells: + break + + # Check if position is safe + if _is_position_safe( + costmap, ix, iy, cost_threshold, clearance_cells, connectivity_check_radius + ): + if current_cost < best_cost: + best_x, best_y = ix, iy + best_cost = current_cost + + # If cost is very low, we found a good spot + if current_cost < 10: + break + + # Compute gradient using finite differences + gx, gy = 0.0, 0.0 + + if 0 < ix < costmap.width - 1: + gx = (grid[iy, min(ix + 1, costmap.width - 1)] - grid[iy, max(ix - 1, 0)]) / 2.0 + + if 0 < iy < costmap.height - 1: + gy = (grid[min(iy + 1, costmap.height - 1), ix] - grid[max(iy - 1, 0), ix]) / 2.0 + + # Update with momentum + vx = momentum * vx - learning_rate * gx + vy = momentum * vy - learning_rate * gy + + # Update position + x += vx + y += vy + + # Add small random noise to escape local minima + if iteration % 20 == 0: + x += np.random.randn() * 0.5 + y += np.random.randn() * 0.5 + + if best_x is not None and best_y is not None: + return costmap.grid_to_world((best_x, best_y)) + + return None + + +def _is_position_safe( + costmap: OccupancyGrid, + x: int, + y: int, + cost_threshold: int, + clearance_cells: int, + connectivity_check_radius: int, +) -> bool: + """ + Check if a position is safe based on multiple criteria. + + Args: + costmap: The occupancy grid + x, y: Grid coordinates to check + cost_threshold: Maximum acceptable cost + clearance_cells: Minimum clearance in cells + connectivity_check_radius: Radius to check for connectivity + + Returns: + True if position is safe, False otherwise + """ + + # Check bounds first + if not (0 <= x < costmap.width and 0 <= y < costmap.height): + return False + + # Check if position itself is free + if costmap.grid[y, x] >= cost_threshold or costmap.grid[y, x] == CostValues.UNKNOWN: + return False + + # Check clearance around position + for dy in range(-clearance_cells, clearance_cells + 1): + for dx in range(-clearance_cells, clearance_cells + 1): + nx, ny = x + dx, y + dy + if 0 <= nx < costmap.width and 0 <= ny < costmap.height: + # Check if within circular clearance + if dx * dx + dy * dy <= clearance_cells * clearance_cells: + if costmap.grid[ny, nx] >= cost_threshold: + return False + + # Check connectivity (not surrounded by obstacles) + # Count free neighbors in a larger radius + free_count = 0 + total_count = 0 + + for dy in range(-connectivity_check_radius, connectivity_check_radius + 1): + for dx in range(-connectivity_check_radius, connectivity_check_radius + 1): + if dx == 0 and dy == 0: + continue + + nx, ny = x + dx, y + dy + if 0 <= nx < costmap.width and 0 <= ny < costmap.height: + total_count += 1 + if ( + costmap.grid[ny, nx] < cost_threshold + and costmap.grid[ny, nx] != CostValues.UNKNOWN + ): + free_count += 1 + + # Require at least 50% of neighbors to be free (not surrounded) + if total_count > 0 and free_count < total_count * 0.5: + return False + + return True diff --git a/dimos/navigation/bt_navigator/navigator.py b/dimos/navigation/bt_navigator/navigator.py new file mode 100644 index 0000000000..782e815bb3 --- /dev/null +++ b/dimos/navigation/bt_navigator/navigator.py @@ -0,0 +1,360 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Navigator module for coordinating global and local planning. +""" + +from collections.abc import Callable +from enum import Enum +import threading +import time + +from dimos_lcm.std_msgs import Bool, String +from reactivex.disposable import Disposable + +from dimos.core import In, Module, Out, rpc +from dimos.core.rpc_client import RpcCall +from dimos.msgs.geometry_msgs import PoseStamped +from dimos.msgs.nav_msgs import OccupancyGrid +from dimos.navigation.bt_navigator.goal_validator import find_safe_goal +from dimos.navigation.bt_navigator.recovery_server import RecoveryServer +from dimos.protocol.tf import TF +from dimos.utils.logging_config import setup_logger +from dimos.utils.transform_utils import apply_transform + +logger = setup_logger("dimos.navigation.bt_navigator") + + +class NavigatorState(Enum): + """Navigator state machine states.""" + + IDLE = "idle" + FOLLOWING_PATH = "following_path" + RECOVERY = "recovery" + + +class BehaviorTreeNavigator(Module): + """ + Navigator module for coordinating navigation tasks. + + Manages the state machine for navigation, coordinates between global + and local planners, and monitors goal completion. + + Inputs: + - odom: Current robot odometry + + Outputs: + - goal: Goal pose for global planner + """ + + # LCM inputs + odom: In[PoseStamped] = None + goal_request: In[PoseStamped] = None # Input for receiving goal requests + global_costmap: In[OccupancyGrid] = None + + # LCM outputs + target: Out[PoseStamped] = None + goal_reached: Out[Bool] = None + navigation_state: Out[String] = None + + def __init__( + self, + publishing_frequency: float = 1.0, + reset_local_planner: Callable[[], None] | None = None, + check_goal_reached: Callable[[], bool] | None = None, + **kwargs, + ) -> None: + """Initialize the Navigator. + + Args: + publishing_frequency: Frequency to publish goals to global planner (Hz) + goal_tolerance: Distance threshold to consider goal reached (meters) + """ + super().__init__(**kwargs) + + # Parameters + self.publishing_frequency = publishing_frequency + self.publishing_period = 1.0 / publishing_frequency + + # State machine + self.state = NavigatorState.IDLE + self.state_lock = threading.Lock() + + # Current goal + self.current_goal: PoseStamped | None = None + self.original_goal: PoseStamped | None = None + self.goal_lock = threading.Lock() + + # Goal reached state + self._goal_reached = False + + # Latest data + self.latest_odom: PoseStamped | None = None + self.latest_costmap: OccupancyGrid | None = None + + # Control thread + self.control_thread: threading.Thread | None = None + self.stop_event = threading.Event() + + # TF listener + self.tf = TF() + + # Local planner + self.reset_local_planner = reset_local_planner + self.check_goal_reached = check_goal_reached + + # Recovery server for stuck detection + self.recovery_server = RecoveryServer(stuck_duration=5.0) + + logger.info("Navigator initialized with stuck detection") + + @rpc + def set_HolonomicLocalPlanner_reset(self, callable: RpcCall) -> None: + self.reset_local_planner = callable + self.reset_local_planner.set_rpc(self.rpc) + + @rpc + def set_HolonomicLocalPlanner_is_goal_reached(self, callable: RpcCall) -> None: + self.check_goal_reached = callable + self.check_goal_reached.set_rpc(self.rpc) + + @rpc + def start(self) -> None: + super().start() + + # Subscribe to inputs + unsub = self.odom.subscribe(self._on_odom) + self._disposables.add(Disposable(unsub)) + + unsub = self.goal_request.subscribe(self._on_goal_request) + self._disposables.add(Disposable(unsub)) + + unsub = self.global_costmap.subscribe(self._on_costmap) + self._disposables.add(Disposable(unsub)) + + # Start control thread + self.stop_event.clear() + self.control_thread = threading.Thread(target=self._control_loop, daemon=True) + self.control_thread.start() + + logger.info("Navigator started") + + @rpc + def stop(self) -> None: + """Clean up resources including stopping the control thread.""" + + self.stop_navigation() + + self.stop_event.set() + if self.control_thread and self.control_thread.is_alive(): + self.control_thread.join(timeout=2.0) + + super().stop() + + @rpc + def cancel_goal(self) -> bool: + """ + Cancel the current navigation goal. + + Returns: + True if goal was cancelled, False if no goal was active + """ + self.stop_navigation() + return True + + @rpc + def set_goal(self, goal: PoseStamped) -> bool: + """ + Set a new navigation goal. + + Args: + goal: Target pose to navigate to + + Returns: + non-blocking: True if goal was accepted, False otherwise + blocking: True if goal was reached, False otherwise + """ + transformed_goal = self._transform_goal_to_odom_frame(goal) + if not transformed_goal: + logger.error("Failed to transform goal to odometry frame") + return False + + with self.goal_lock: + self.current_goal = transformed_goal + self.original_goal = transformed_goal + + self._goal_reached = False + + with self.state_lock: + self.state = NavigatorState.FOLLOWING_PATH + + return True + + @rpc + def get_state(self) -> NavigatorState: + """Get the current state of the navigator.""" + return self.state + + def _on_odom(self, msg: PoseStamped) -> None: + """Handle incoming odometry messages.""" + self.latest_odom = msg + + if self.state == NavigatorState.FOLLOWING_PATH: + self.recovery_server.update_odom(msg) + + def _on_goal_request(self, msg: PoseStamped) -> None: + """Handle incoming goal requests.""" + self.set_goal(msg) + + def _on_costmap(self, msg: OccupancyGrid) -> None: + """Handle incoming costmap messages.""" + self.latest_costmap = msg + + def _transform_goal_to_odom_frame(self, goal: PoseStamped) -> PoseStamped | None: + """Transform goal pose to the odometry frame.""" + if not goal.frame_id: + return goal + + odom_frame = self.latest_odom.frame_id + if goal.frame_id == odom_frame: + return goal + + try: + transform = None + max_retries = 3 + + for attempt in range(max_retries): + transform = self.tf.get( + parent_frame=odom_frame, + child_frame=goal.frame_id, + ) + + if transform: + break + + if attempt < max_retries - 1: + logger.warning( + f"Transform attempt {attempt + 1}/{max_retries} failed, retrying..." + ) + time.sleep(1.0) + else: + logger.error( + f"Could not find transform from '{goal.frame_id}' to '{odom_frame}' after {max_retries} attempts" + ) + return None + + pose = apply_transform(goal, transform) + transformed_goal = PoseStamped( + position=pose.position, + orientation=pose.orientation, + frame_id=odom_frame, + ts=goal.ts, + ) + return transformed_goal + + except Exception as e: + logger.error(f"Failed to transform goal: {e}") + return None + + def _control_loop(self) -> None: + """Main control loop running in separate thread.""" + while not self.stop_event.is_set(): + with self.state_lock: + current_state = self.state + self.navigation_state.publish(String(data=current_state.value)) + + if current_state == NavigatorState.FOLLOWING_PATH: + with self.goal_lock: + goal = self.current_goal + original_goal = self.original_goal + + if goal is not None and self.latest_costmap is not None: + # Check if robot is stuck + if self.recovery_server.check_stuck(): + logger.warning("Robot is stuck! Cancelling goal and resetting.") + self.cancel_goal() + continue + + costmap = self.latest_costmap.inflate(0.1).gradient(max_distance=1.0) + + # Find safe goal position + safe_goal_pos = find_safe_goal( + costmap, + original_goal.position, + algorithm="bfs", + cost_threshold=60, + min_clearance=0.25, + max_search_distance=5.0, + ) + + # Create new goal with safe position + if safe_goal_pos: + safe_goal = PoseStamped( + position=safe_goal_pos, + orientation=goal.orientation, + frame_id=goal.frame_id, + ts=goal.ts, + ) + self.target.publish(safe_goal) + self.current_goal = safe_goal + else: + logger.warning("Could not find safe goal position, cancelling goal") + self.cancel_goal() + + # Check if goal is reached + if self.check_goal_reached(): + reached_msg = Bool() + reached_msg.data = True + self.goal_reached.publish(reached_msg) + self.stop_navigation() + self._goal_reached = True + logger.info("Goal reached, resetting local planner") + + elif current_state == NavigatorState.RECOVERY: + with self.state_lock: + self.state = NavigatorState.IDLE + + time.sleep(self.publishing_period) + + @rpc + def is_goal_reached(self) -> bool: + """Check if the current goal has been reached. + + Returns: + True if goal was reached, False otherwise + """ + return self._goal_reached + + def stop_navigation(self) -> None: + """Stop navigation and return to IDLE state.""" + with self.goal_lock: + self.current_goal = None + + self._goal_reached = False + + with self.state_lock: + self.state = NavigatorState.IDLE + + self.reset_local_planner() + self.recovery_server.reset() # Reset recovery server when stopping + + logger.info("Navigator stopped") + + +behavior_tree_navigator = BehaviorTreeNavigator.blueprint + +__all__ = ["BehaviorTreeNavigator", "behavior_tree_navigator"] diff --git a/dimos/navigation/bt_navigator/recovery_server.py b/dimos/navigation/bt_navigator/recovery_server.py new file mode 100644 index 0000000000..5b05d35de5 --- /dev/null +++ b/dimos/navigation/bt_navigator/recovery_server.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Recovery server for handling stuck detection and recovery behaviors. +""" + +from dimos.msgs.geometry_msgs import PoseStamped +from dimos.utils.logging_config import setup_logger +from dimos.utils.transform_utils import get_distance + +logger = setup_logger("dimos.navigation.bt_navigator.recovery_server") + + +class RecoveryServer: + """ + Recovery server for detecting stuck situations and executing recovery behaviors. + + Currently implements stuck detection based on time without significant movement. + Will be extended with actual recovery behaviors in the future. + """ + + def __init__( + self, + position_threshold: float = 0.2, + stuck_duration: float = 3.0, + ) -> None: + """Initialize the recovery server. + + Args: + position_threshold: Minimum distance to travel to reset stuck timer (meters) + stuck_duration: Time duration without significant movement to consider stuck (seconds) + """ + self.position_threshold = position_threshold + self.stuck_duration = stuck_duration + + # Store last position that exceeded threshold + self.last_moved_pose = None + self.last_moved_time = None + self.current_odom = None + + logger.info( + f"RecoveryServer initialized with position_threshold={position_threshold}, " + f"stuck_duration={stuck_duration}" + ) + + def update_odom(self, odom: PoseStamped) -> None: + """Update the odometry data for stuck detection. + + Args: + odom: Current robot odometry with timestamp + """ + if odom is None: + return + + # Store current odom for checking stuck + self.current_odom = odom + + # Initialize on first update + if self.last_moved_pose is None: + self.last_moved_pose = odom + self.last_moved_time = odom.ts + return + + # Calculate distance from the reference position (last significant movement) + distance = get_distance(odom, self.last_moved_pose) + + # If robot has moved significantly from the reference, update reference + if distance > self.position_threshold: + self.last_moved_pose = odom + self.last_moved_time = odom.ts + + def check_stuck(self) -> bool: + """Check if the robot is stuck based on time without movement. + + Returns: + True if robot appears to be stuck, False otherwise + """ + if self.last_moved_time is None: + return False + + # Need current odom to check + if self.current_odom is None: + return False + + # Calculate time since last significant movement + current_time = self.current_odom.ts + time_since_movement = current_time - self.last_moved_time + + # Check if stuck based on duration without movement + is_stuck = time_since_movement > self.stuck_duration + + if is_stuck: + logger.warning( + f"Robot appears stuck! No movement for {time_since_movement:.1f} seconds" + ) + + return is_stuck + + def reset(self) -> None: + """Reset the recovery server state.""" + self.last_moved_pose = None + self.last_moved_time = None + self.current_odom = None + logger.debug("RecoveryServer reset") diff --git a/dimos/navigation/frontier_exploration/__init__.py b/dimos/navigation/frontier_exploration/__init__.py new file mode 100644 index 0000000000..7236788842 --- /dev/null +++ b/dimos/navigation/frontier_exploration/__init__.py @@ -0,0 +1 @@ +from .wavefront_frontier_goal_selector import WavefrontFrontierExplorer, wavefront_frontier_explorer diff --git a/dimos/navigation/frontier_exploration/test_wavefront_frontier_goal_selector.py b/dimos/navigation/frontier_exploration/test_wavefront_frontier_goal_selector.py new file mode 100644 index 0000000000..ed5f364a74 --- /dev/null +++ b/dimos/navigation/frontier_exploration/test_wavefront_frontier_goal_selector.py @@ -0,0 +1,456 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +import numpy as np +from PIL import ImageDraw +import pytest + +from dimos.msgs.geometry_msgs import Vector3 +from dimos.msgs.nav_msgs import CostValues, OccupancyGrid +from dimos.navigation.frontier_exploration.utils import costmap_to_pil_image +from dimos.navigation.frontier_exploration.wavefront_frontier_goal_selector import ( + WavefrontFrontierExplorer, +) + + +@pytest.fixture +def explorer(): + """Create a WavefrontFrontierExplorer instance for testing.""" + explorer = WavefrontFrontierExplorer( + min_frontier_perimeter=0.3, # Smaller for faster tests + safe_distance=0.5, # Smaller for faster distance calculations + info_gain_threshold=0.02, + ) + yield explorer + # Cleanup after test + try: + explorer.stop() + except: + pass + + +@pytest.fixture +def quick_costmap(): + """Create a very small costmap for quick tests.""" + width, height = 20, 20 + grid = np.full((height, width), CostValues.UNKNOWN, dtype=np.int8) + + # Simple free space in center + grid[8:12, 8:12] = CostValues.FREE + + # Small extensions + grid[9:11, 6:8] = CostValues.FREE # Left + grid[9:11, 12:14] = CostValues.FREE # Right + + # One obstacle + grid[9:10, 9:10] = CostValues.OCCUPIED + + from dimos.msgs.geometry_msgs import Pose + + origin = Pose() + origin.position.x = -1.0 + origin.position.y = -1.0 + origin.position.z = 0.0 + origin.orientation.w = 1.0 + + occupancy_grid = OccupancyGrid( + grid=grid, resolution=0.1, origin=origin, frame_id="map", ts=time.time() + ) + + class MockLidar: + def __init__(self) -> None: + self.origin = Vector3(0.0, 0.0, 0.0) + + return occupancy_grid, MockLidar() + + +def create_test_costmap(width: int = 40, height: int = 40, resolution: float = 0.1): + """Create a simple test costmap with free, occupied, and unknown regions. + + Default size reduced from 100x100 to 40x40 for faster tests. + """ + grid = np.full((height, width), CostValues.UNKNOWN, dtype=np.int8) + + # Create a smaller free space region with simple shape + # Central room + grid[15:25, 15:25] = CostValues.FREE + + # Small corridors extending from central room + grid[18:22, 10:15] = CostValues.FREE # Left corridor + grid[18:22, 25:30] = CostValues.FREE # Right corridor + grid[10:15, 18:22] = CostValues.FREE # Top corridor + grid[25:30, 18:22] = CostValues.FREE # Bottom corridor + + # Add fewer obstacles for faster processing + grid[19:21, 19:21] = CostValues.OCCUPIED # Central obstacle + grid[13:14, 18:22] = CostValues.OCCUPIED # Top corridor obstacle + + # Create origin at bottom-left, adjusted for map size + from dimos.msgs.geometry_msgs import Pose + + origin = Pose() + # Center the map around (0, 0) in world coordinates + origin.position.x = -(width * resolution) / 2.0 + origin.position.y = -(height * resolution) / 2.0 + origin.position.z = 0.0 + origin.orientation.w = 1.0 + + occupancy_grid = OccupancyGrid( + grid=grid, resolution=resolution, origin=origin, frame_id="map", ts=time.time() + ) + + # Create a mock lidar message with origin + class MockLidar: + def __init__(self) -> None: + self.origin = Vector3(0.0, 0.0, 0.0) + + return occupancy_grid, MockLidar() + + +def test_frontier_detection_with_office_lidar(explorer, quick_costmap) -> None: + """Test frontier detection using a test costmap.""" + # Get test costmap + costmap, first_lidar = quick_costmap + + # Verify we have a valid costmap + assert costmap is not None, "Costmap should not be None" + assert costmap.width > 0 and costmap.height > 0, "Costmap should have valid dimensions" + + print(f"Costmap dimensions: {costmap.width}x{costmap.height}") + print(f"Costmap resolution: {costmap.resolution}") + print(f"Unknown percent: {costmap.unknown_percent:.1f}%") + print(f"Free percent: {costmap.free_percent:.1f}%") + print(f"Occupied percent: {costmap.occupied_percent:.1f}%") + + # Set robot pose near the center of free space in the costmap + # We'll use the lidar origin as a reasonable robot position + robot_pose = first_lidar.origin + print(f"Robot pose: {robot_pose}") + + # Detect frontiers + frontiers = explorer.detect_frontiers(robot_pose, costmap) + + # Verify frontier detection results + assert isinstance(frontiers, list), "Frontiers should be returned as a list" + print(f"Detected {len(frontiers)} frontiers") + + # Test that we get some frontiers (office environment should have unexplored areas) + if len(frontiers) > 0: + print("Frontier detection successful - found unexplored areas") + + # Verify frontiers are Vector objects with valid coordinates + for i, frontier in enumerate(frontiers[:5]): # Check first 5 + assert isinstance(frontier, Vector3), f"Frontier {i} should be a Vector3" + assert hasattr(frontier, "x") and hasattr(frontier, "y"), ( + f"Frontier {i} should have x,y coordinates" + ) + print(f" Frontier {i}: ({frontier.x:.2f}, {frontier.y:.2f})") + else: + print("No frontiers detected - map may be fully explored or parameters too restrictive") + + explorer.stop() # TODO: this should be a in try-finally + + +def test_exploration_goal_selection(explorer) -> None: + """Test the complete exploration goal selection pipeline.""" + # Get test costmap - use regular size for more realistic test + costmap, first_lidar = create_test_costmap() + + # Use lidar origin as robot position + robot_pose = first_lidar.origin + + # Get exploration goal + goal = explorer.get_exploration_goal(robot_pose, costmap) + + if goal is not None: + assert isinstance(goal, Vector3), "Goal should be a Vector3" + print(f"Selected exploration goal: ({goal.x:.2f}, {goal.y:.2f})") + + # Test that goal gets marked as explored + assert len(explorer.explored_goals) == 1, "Goal should be marked as explored" + assert explorer.explored_goals[0] == goal, "Explored goal should match selected goal" + + # Test that goal is within costmap bounds + grid_pos = costmap.world_to_grid(goal) + assert 0 <= grid_pos.x < costmap.width, "Goal x should be within costmap bounds" + assert 0 <= grid_pos.y < costmap.height, "Goal y should be within costmap bounds" + + # Test that goal is at a reasonable distance from robot + distance = np.sqrt((goal.x - robot_pose.x) ** 2 + (goal.y - robot_pose.y) ** 2) + assert 0.1 < distance < 20.0, f"Goal distance {distance:.2f}m should be reasonable" + + else: + print("No exploration goal selected - map may be fully explored") + + explorer.stop() # TODO: this should be a in try-finally + + +def test_exploration_session_reset(explorer) -> None: + """Test exploration session reset functionality.""" + # Get test costmap + costmap, first_lidar = create_test_costmap() + + # Use lidar origin as robot position + robot_pose = first_lidar.origin + + # Select a goal to populate exploration state + goal = explorer.get_exploration_goal(robot_pose, costmap) + + # Verify state is populated (skip if no goals available) + if goal: + initial_explored_count = len(explorer.explored_goals) + assert initial_explored_count > 0, "Should have at least one explored goal" + + # Reset exploration session + explorer.reset_exploration_session() + + # Verify state is cleared + assert len(explorer.explored_goals) == 0, "Explored goals should be cleared after reset" + assert explorer.exploration_direction.x == 0.0 and explorer.exploration_direction.y == 0.0, ( + "Exploration direction should be reset" + ) + assert explorer.last_costmap is None, "Last costmap should be cleared" + assert explorer.no_gain_counter == 0, "No-gain counter should be reset" + + print("Exploration session reset successfully") + explorer.stop() # TODO: this should be a in try-finally + + +def test_frontier_ranking(explorer) -> None: + """Test frontier ranking and scoring logic.""" + # Get test costmap + costmap, first_lidar = create_test_costmap() + + robot_pose = first_lidar.origin + + # Get first set of frontiers + frontiers1 = explorer.detect_frontiers(robot_pose, costmap) + goal1 = explorer.get_exploration_goal(robot_pose, costmap) + + if goal1: + # Verify the selected goal is the first in the ranked list + assert frontiers1[0].x == goal1.x and frontiers1[0].y == goal1.y, ( + "Selected goal should be the highest ranked frontier" + ) + + # Test that goals are being marked as explored + assert len(explorer.explored_goals) == 1, "Goal should be marked as explored" + assert ( + explorer.explored_goals[0].x == goal1.x and explorer.explored_goals[0].y == goal1.y + ), "Explored goal should match selected goal" + + # Get another goal + goal2 = explorer.get_exploration_goal(robot_pose, costmap) + if goal2: + assert len(explorer.explored_goals) == 2, ( + "Second goal should also be marked as explored" + ) + + # Test distance to obstacles + obstacle_dist = explorer._compute_distance_to_obstacles(goal1, costmap) + # Note: Goals might be closer than safe_distance if that's the best available frontier + # The safe_distance is used for scoring, not as a hard constraint + print( + f"Distance to obstacles: {obstacle_dist:.2f}m (safe distance: {explorer.safe_distance}m)" + ) + + print(f"Frontier ranking test passed - selected goal at ({goal1.x:.2f}, {goal1.y:.2f})") + print(f"Total frontiers detected: {len(frontiers1)}") + else: + print("No frontiers found for ranking test") + + explorer.stop() # TODO: this should be a in try-finally + + +def test_exploration_with_no_gain_detection() -> None: + """Test information gain detection and exploration termination.""" + # Get initial costmap + costmap1, first_lidar = create_test_costmap() + + # Initialize explorer with low no-gain threshold for testing + explorer = WavefrontFrontierExplorer(info_gain_threshold=0.01, num_no_gain_attempts=2) + + try: + robot_pose = first_lidar.origin + + # Select multiple goals to populate history + for i in range(6): + goal = explorer.get_exploration_goal(robot_pose, costmap1) + if goal: + print(f"Goal {i + 1}: ({goal.x:.2f}, {goal.y:.2f})") + + # Now use same costmap repeatedly to trigger no-gain detection + initial_counter = explorer.no_gain_counter + + # This should increment no-gain counter + goal = explorer.get_exploration_goal(robot_pose, costmap1) + assert explorer.no_gain_counter > initial_counter, "No-gain counter should increment" + + # Continue until exploration stops + for _ in range(3): + goal = explorer.get_exploration_goal(robot_pose, costmap1) + if goal is None: + break + + # Should have stopped due to no information gain + assert goal is None, "Exploration should stop after no-gain threshold" + assert explorer.no_gain_counter == 0, "Counter should reset after stopping" + finally: + explorer.stop() + + +@pytest.mark.vis +def test_frontier_detection_visualization() -> None: + """Test frontier detection with visualization (marked with @pytest.mark.vis).""" + # Get test costmap + costmap, first_lidar = create_test_costmap() + + # Initialize frontier explorer with default parameters + explorer = WavefrontFrontierExplorer() + + try: + # Use lidar origin as robot position + robot_pose = first_lidar.origin + + # Detect all frontiers for visualization + all_frontiers = explorer.detect_frontiers(robot_pose, costmap) + + # Get selected goal + selected_goal = explorer.get_exploration_goal(robot_pose, costmap) + + print(f"Visualizing {len(all_frontiers)} frontier candidates") + if selected_goal: + print(f"Selected goal: ({selected_goal.x:.2f}, {selected_goal.y:.2f})") + + # Create visualization + image_scale_factor = 4 + base_image = costmap_to_pil_image(costmap, image_scale_factor) + + # Helper function to convert world coordinates to image coordinates + def world_to_image_coords(world_pos: Vector3) -> tuple[int, int]: + grid_pos = costmap.world_to_grid(world_pos) + img_x = int(grid_pos.x * image_scale_factor) + img_y = int((costmap.height - grid_pos.y) * image_scale_factor) # Flip Y + return img_x, img_y + + # Draw visualization + draw = ImageDraw.Draw(base_image) + + # Draw frontier candidates as gray dots + for frontier in all_frontiers[:20]: # Limit to top 20 + x, y = world_to_image_coords(frontier) + radius = 6 + draw.ellipse( + [x - radius, y - radius, x + radius, y + radius], + fill=(128, 128, 128), # Gray + outline=(64, 64, 64), + width=1, + ) + + # Draw robot position as blue dot + robot_x, robot_y = world_to_image_coords(robot_pose) + robot_radius = 10 + draw.ellipse( + [ + robot_x - robot_radius, + robot_y - robot_radius, + robot_x + robot_radius, + robot_y + robot_radius, + ], + fill=(0, 0, 255), # Blue + outline=(0, 0, 128), + width=3, + ) + + # Draw selected goal as red dot + if selected_goal: + goal_x, goal_y = world_to_image_coords(selected_goal) + goal_radius = 12 + draw.ellipse( + [ + goal_x - goal_radius, + goal_y - goal_radius, + goal_x + goal_radius, + goal_y + goal_radius, + ], + fill=(255, 0, 0), # Red + outline=(128, 0, 0), + width=3, + ) + + # Display the image + base_image.show(title="Frontier Detection - Office Lidar") + print("Visualization displayed. Close the image window to continue.") + finally: + explorer.stop() + + +def test_performance_timing() -> None: + """Test performance by timing frontier detection operations.""" + import time + + # Test with different costmap sizes + sizes = [(20, 20), (40, 40), (60, 60)] + results = [] + + for width, height in sizes: + # Create costmap of specified size + costmap, lidar = create_test_costmap(width, height) + + # Create explorer with optimized parameters + explorer = WavefrontFrontierExplorer( + min_frontier_perimeter=0.3, + safe_distance=0.5, + info_gain_threshold=0.02, + ) + + try: + robot_pose = lidar.origin + + # Time frontier detection + start = time.time() + frontiers = explorer.detect_frontiers(robot_pose, costmap) + detect_time = time.time() - start + + # Time goal selection + start = time.time() + explorer.get_exploration_goal(robot_pose, costmap) + goal_time = time.time() - start + + results.append( + { + "size": f"{width}x{height}", + "cells": width * height, + "detect_time": detect_time, + "goal_time": goal_time, + "frontiers": len(frontiers), + } + ) + + print(f"\nSize {width}x{height}:") + print(f" Cells: {width * height}") + print(f" Frontier detection: {detect_time:.4f}s") + print(f" Goal selection: {goal_time:.4f}s") + print(f" Frontiers found: {len(frontiers)}") + finally: + explorer.stop() + + # Check that larger maps take more time (expected behavior) + for result in results: + assert result["detect_time"] < 2.0, f"Detection too slow: {result['detect_time']}s" + assert result["goal_time"] < 1.5, f"Goal selection too slow: {result['goal_time']}s" + + print("\nPerformance test passed - all operations completed within time limits") diff --git a/dimos/navigation/frontier_exploration/utils.py b/dimos/navigation/frontier_exploration/utils.py new file mode 100644 index 0000000000..d307749531 --- /dev/null +++ b/dimos/navigation/frontier_exploration/utils.py @@ -0,0 +1,138 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Utility functions for frontier exploration visualization and testing. +""" + +import numpy as np +from PIL import Image, ImageDraw + +from dimos.msgs.geometry_msgs import Vector3 +from dimos.msgs.nav_msgs import CostValues, OccupancyGrid + + +def costmap_to_pil_image(costmap: OccupancyGrid, scale_factor: int = 2) -> Image.Image: + """ + Convert costmap to PIL Image with ROS-style coloring and optional scaling. + + Args: + costmap: Costmap to convert + scale_factor: Factor to scale up the image for better visibility + + Returns: + PIL Image with ROS-style colors + """ + # Create image array (height, width, 3 for RGB) + img_array = np.zeros((costmap.height, costmap.width, 3), dtype=np.uint8) + + # Apply ROS-style coloring based on costmap values + for i in range(costmap.height): + for j in range(costmap.width): + value = costmap.grid[i, j] + if value == CostValues.FREE: # Free space = light grey + img_array[i, j] = [205, 205, 205] + elif value == CostValues.UNKNOWN: # Unknown = dark gray + img_array[i, j] = [128, 128, 128] + elif value >= CostValues.OCCUPIED: # Occupied/obstacles = black + img_array[i, j] = [0, 0, 0] + else: # Any other values (low cost) = light grey + img_array[i, j] = [205, 205, 205] + + # Flip vertically to match ROS convention (origin at bottom-left) + img_array = np.flipud(img_array) + + # Create PIL image + img = Image.fromarray(img_array, "RGB") + + # Scale up if requested + if scale_factor > 1: + new_size = (img.width * scale_factor, img.height * scale_factor) + img = img.resize(new_size, Image.NEAREST) # Use NEAREST to keep sharp pixels + + return img + + +def draw_frontiers_on_image( + image: Image.Image, + costmap: OccupancyGrid, + frontiers: list[Vector3], + scale_factor: int = 2, + unfiltered_frontiers: list[Vector3] | None = None, +) -> Image.Image: + """ + Draw frontier points on the costmap image. + + Args: + image: PIL Image to draw on + costmap: Original costmap for coordinate conversion + frontiers: List of frontier centroids (top 5) + scale_factor: Scaling factor used for the image + unfiltered_frontiers: All unfiltered frontier results (light green) + + Returns: + PIL Image with frontiers drawn + """ + img_copy = image.copy() + draw = ImageDraw.Draw(img_copy) + + def world_to_image_coords(world_pos: Vector3) -> tuple[int, int]: + """Convert world coordinates to image pixel coordinates.""" + grid_pos = costmap.world_to_grid(world_pos) + # Flip Y coordinate and apply scaling + img_x = int(grid_pos.x * scale_factor) + img_y = int((costmap.height - grid_pos.y) * scale_factor) # Flip Y + return img_x, img_y + + # Draw all unfiltered frontiers as light green circles + if unfiltered_frontiers: + for frontier in unfiltered_frontiers: + x, y = world_to_image_coords(frontier) + radius = 3 * scale_factor + draw.ellipse( + [x - radius, y - radius, x + radius, y + radius], + fill=(144, 238, 144), + outline=(144, 238, 144), + ) # Light green + + # Draw top 5 frontiers as green circles + for i, frontier in enumerate(frontiers[1:]): # Skip the best one for now + x, y = world_to_image_coords(frontier) + radius = 4 * scale_factor + draw.ellipse( + [x - radius, y - radius, x + radius, y + radius], + fill=(0, 255, 0), + outline=(0, 128, 0), + width=2, + ) # Green + + # Add number label + draw.text((x + radius + 2, y - radius), str(i + 2), fill=(0, 255, 0)) + + # Draw best frontier as red circle + if frontiers: + best_frontier = frontiers[0] + x, y = world_to_image_coords(best_frontier) + radius = 6 * scale_factor + draw.ellipse( + [x - radius, y - radius, x + radius, y + radius], + fill=(255, 0, 0), + outline=(128, 0, 0), + width=3, + ) # Red + + # Add "BEST" label + draw.text((x + radius + 2, y - radius), "BEST", fill=(255, 0, 0)) + + return img_copy diff --git a/dimos/navigation/frontier_exploration/wavefront_frontier_goal_selector.py b/dimos/navigation/frontier_exploration/wavefront_frontier_goal_selector.py new file mode 100644 index 0000000000..71677635f5 --- /dev/null +++ b/dimos/navigation/frontier_exploration/wavefront_frontier_goal_selector.py @@ -0,0 +1,819 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Simple wavefront frontier exploration algorithm implementation using dimos types. + +This module provides frontier detection and exploration goal selection +for autonomous navigation using the dimos Costmap and Vector types. +""" + +from collections import deque +from dataclasses import dataclass +from enum import IntFlag +import threading + +from dimos_lcm.std_msgs import Bool +import numpy as np +from reactivex.disposable import Disposable + +from dimos.core import In, Module, Out, rpc +from dimos.msgs.geometry_msgs import PoseStamped, Vector3 +from dimos.msgs.nav_msgs import CostValues, OccupancyGrid +from dimos.utils.logging_config import setup_logger +from dimos.utils.transform_utils import get_distance + +logger = setup_logger("dimos.robot.unitree.frontier_exploration") + + +class PointClassification(IntFlag): + """Point classification flags for frontier detection algorithm.""" + + NoInformation = 0 + MapOpen = 1 + MapClosed = 2 + FrontierOpen = 4 + FrontierClosed = 8 + + +@dataclass +class GridPoint: + """Represents a point in the grid map with classification.""" + + x: int + y: int + classification: int = PointClassification.NoInformation + + +class FrontierCache: + """Cache for grid points to avoid duplicate point creation.""" + + def __init__(self) -> None: + self.points = {} + + def get_point(self, x: int, y: int) -> GridPoint: + """Get or create a grid point at the given coordinates.""" + key = (x, y) + if key not in self.points: + self.points[key] = GridPoint(x, y) + return self.points[key] + + def clear(self) -> None: + """Clear the point cache.""" + self.points.clear() + + +class WavefrontFrontierExplorer(Module): + """ + Wavefront frontier exploration algorithm implementation. + + This class encapsulates the frontier detection and exploration goal selection + functionality using the wavefront algorithm with BFS exploration. + + Inputs: + - costmap: Current costmap for frontier detection + - odometry: Current robot pose + + Outputs: + - goal_request: Exploration goals sent to the navigator + """ + + # LCM inputs + global_costmap: In[OccupancyGrid] = None + odom: In[PoseStamped] = None + goal_reached: In[Bool] = None + explore_cmd: In[Bool] = None + stop_explore_cmd: In[Bool] = None + + # LCM outputs + goal_request: Out[PoseStamped] = None + + def __init__( + self, + min_frontier_perimeter: float = 0.5, + occupancy_threshold: int = 99, + safe_distance: float = 3.0, + lookahead_distance: float = 5.0, + max_explored_distance: float = 10.0, + info_gain_threshold: float = 0.03, + num_no_gain_attempts: int = 2, + goal_timeout: float = 15.0, + **kwargs, + ) -> None: + """ + Initialize the frontier explorer. + + Args: + min_frontier_perimeter: Minimum perimeter in meters to consider a valid frontier + occupancy_threshold: Cost threshold above which a cell is considered occupied (0-255) + safe_distance: Safe distance from obstacles for scoring (meters) + info_gain_threshold: Minimum percentage increase in costmap information required to continue exploration (0.05 = 5%) + num_no_gain_attempts: Maximum number of consecutive attempts with no information gain + """ + super().__init__(**kwargs) + self.min_frontier_perimeter = min_frontier_perimeter + self.occupancy_threshold = occupancy_threshold + self.safe_distance = safe_distance + self.max_explored_distance = max_explored_distance + self.lookahead_distance = lookahead_distance + self.info_gain_threshold = info_gain_threshold + self.num_no_gain_attempts = num_no_gain_attempts + self._cache = FrontierCache() + self.explored_goals = [] # list of explored goals + self.exploration_direction = Vector3(0.0, 0.0, 0.0) # current exploration direction + self.last_costmap = None # store last costmap for information comparison + self.no_gain_counter = 0 # track consecutive no-gain attempts + self.goal_timeout = goal_timeout + + # Latest data + self.latest_costmap: OccupancyGrid | None = None + self.latest_odometry: PoseStamped | None = None + + # Goal reached event + self.goal_reached_event = threading.Event() + + # Exploration state + self.exploration_active = False + self.exploration_thread: threading.Thread | None = None + self.stop_event = threading.Event() + + logger.info("WavefrontFrontierExplorer module initialized") + + @rpc + def start(self) -> None: + super().start() + + unsub = self.global_costmap.subscribe(self._on_costmap) + self._disposables.add(Disposable(unsub)) + + unsub = self.odom.subscribe(self._on_odometry) + self._disposables.add(Disposable(unsub)) + + if self.goal_reached.transport is not None: + unsub = self.goal_reached.subscribe(self._on_goal_reached) + self._disposables.add(Disposable(unsub)) + + if self.explore_cmd.transport is not None: + unsub = self.explore_cmd.subscribe(self._on_explore_cmd) + self._disposables.add(Disposable(unsub)) + + if self.stop_explore_cmd.transport is not None: + unsub = self.stop_explore_cmd.subscribe(self._on_stop_explore_cmd) + self._disposables.add(Disposable(unsub)) + + @rpc + def stop(self) -> None: + self.stop_exploration() + super().stop() + + def _on_costmap(self, msg: OccupancyGrid) -> None: + """Handle incoming costmap messages.""" + self.latest_costmap = msg + + def _on_odometry(self, msg: PoseStamped) -> None: + """Handle incoming odometry messages.""" + self.latest_odometry = msg + + def _on_goal_reached(self, msg: Bool) -> None: + """Handle goal reached messages.""" + if msg.data: + self.goal_reached_event.set() + + def _on_explore_cmd(self, msg: Bool) -> None: + """Handle exploration command messages.""" + if msg.data: + logger.info("Received exploration start command via LCM") + self.explore() + + def _on_stop_explore_cmd(self, msg: Bool) -> None: + """Handle stop exploration command messages.""" + if msg.data: + logger.info("Received exploration stop command via LCM") + self.stop_exploration() + + def _count_costmap_information(self, costmap: OccupancyGrid) -> int: + """ + Count the amount of information in a costmap (free space + obstacles). + + Args: + costmap: Costmap to analyze + + Returns: + Number of cells that are free space or obstacles (not unknown) + """ + free_count = np.sum(costmap.grid == CostValues.FREE) + obstacle_count = np.sum(costmap.grid >= self.occupancy_threshold) + return int(free_count + obstacle_count) + + def _get_neighbors(self, point: GridPoint, costmap: OccupancyGrid) -> list[GridPoint]: + """Get valid neighboring points for a given grid point.""" + neighbors = [] + + # 8-connected neighbors + for dx in [-1, 0, 1]: + for dy in [-1, 0, 1]: + if dx == 0 and dy == 0: + continue + + nx, ny = point.x + dx, point.y + dy + + # Check bounds + if 0 <= nx < costmap.width and 0 <= ny < costmap.height: + neighbors.append(self._cache.get_point(nx, ny)) + + return neighbors + + def _is_frontier_point(self, point: GridPoint, costmap: OccupancyGrid) -> bool: + """ + Check if a point is a frontier point. + A frontier point is an unknown cell adjacent to at least one free cell + and not adjacent to any occupied cells. + """ + # Point must be unknown + cost = costmap.grid[point.y, point.x] + if cost != CostValues.UNKNOWN: + return False + + has_free = False + + for neighbor in self._get_neighbors(point, costmap): + neighbor_cost = costmap.grid[neighbor.y, neighbor.x] + + # If adjacent to occupied space, not a frontier + if neighbor_cost > self.occupancy_threshold: + return False + + # Check if adjacent to free space + if neighbor_cost == CostValues.FREE: + has_free = True + + return has_free + + def _find_free_space( + self, start_x: int, start_y: int, costmap: OccupancyGrid + ) -> tuple[int, int]: + """ + Find the nearest free space point using BFS from the starting position. + """ + queue = deque([self._cache.get_point(start_x, start_y)]) + visited = set() + + while queue: + point = queue.popleft() + + if (point.x, point.y) in visited: + continue + visited.add((point.x, point.y)) + + # Check if this point is free space + if costmap.grid[point.y, point.x] == CostValues.FREE: + return (point.x, point.y) + + # Add neighbors to search + for neighbor in self._get_neighbors(point, costmap): + if (neighbor.x, neighbor.y) not in visited: + queue.append(neighbor) + + # If no free space found, return original position + return (start_x, start_y) + + def _compute_centroid(self, frontier_points: list[Vector3]) -> Vector3: + """Compute the centroid of a list of frontier points.""" + if not frontier_points: + return Vector3(0.0, 0.0, 0.0) + + # Vectorized approach using numpy + points_array = np.array([[point.x, point.y] for point in frontier_points]) + centroid = np.mean(points_array, axis=0) + + return Vector3(centroid[0], centroid[1], 0.0) + + def detect_frontiers(self, robot_pose: Vector3, costmap: OccupancyGrid) -> list[Vector3]: + """ + Main frontier detection algorithm using wavefront exploration. + + Args: + robot_pose: Current robot position in world coordinates + costmap: Costmap for frontier detection + + Returns: + List of frontier centroids in world coordinates + """ + self._cache.clear() + + # Convert robot pose to grid coordinates + grid_pos = costmap.world_to_grid(robot_pose) + grid_x, grid_y = int(grid_pos.x), int(grid_pos.y) + + # Find nearest free space to start exploration + free_x, free_y = self._find_free_space(grid_x, grid_y, costmap) + start_point = self._cache.get_point(free_x, free_y) + start_point.classification = PointClassification.MapOpen + + # Main exploration queue - explore ALL reachable free space + map_queue = deque([start_point]) + frontiers = [] + frontier_sizes = [] + + points_checked = 0 + frontier_candidates = 0 + + while map_queue: + current_point = map_queue.popleft() + points_checked += 1 + + # Skip if already processed + if current_point.classification & PointClassification.MapClosed: + continue + + # Mark as processed + current_point.classification |= PointClassification.MapClosed + + # Check if this point starts a new frontier + if self._is_frontier_point(current_point, costmap): + frontier_candidates += 1 + current_point.classification |= PointClassification.FrontierOpen + frontier_queue = deque([current_point]) + new_frontier = [] + + # Explore this frontier region using BFS + while frontier_queue: + frontier_point = frontier_queue.popleft() + + # Skip if already processed + if frontier_point.classification & PointClassification.FrontierClosed: + continue + + # If this is still a frontier point, add to current frontier + if self._is_frontier_point(frontier_point, costmap): + new_frontier.append(frontier_point) + + # Add neighbors to frontier queue + for neighbor in self._get_neighbors(frontier_point, costmap): + if not ( + neighbor.classification + & ( + PointClassification.FrontierOpen + | PointClassification.FrontierClosed + ) + ): + neighbor.classification |= PointClassification.FrontierOpen + frontier_queue.append(neighbor) + + frontier_point.classification |= PointClassification.FrontierClosed + + # Check if we found a large enough frontier + # Convert minimum perimeter to minimum number of cells based on resolution + min_cells = int(self.min_frontier_perimeter / costmap.resolution) + if len(new_frontier) >= min_cells: + world_points = [] + for point in new_frontier: + world_pos = costmap.grid_to_world( + Vector3(float(point.x), float(point.y), 0.0) + ) + world_points.append(world_pos) + + # Compute centroid in world coordinates (already correctly scaled) + centroid = self._compute_centroid(world_points) + frontiers.append(centroid) # Store centroid + frontier_sizes.append(len(new_frontier)) # Store frontier size + + # Add ALL neighbors to main exploration queue to explore entire free space + for neighbor in self._get_neighbors(current_point, costmap): + if not ( + neighbor.classification + & (PointClassification.MapOpen | PointClassification.MapClosed) + ): + # Check if neighbor is free space or unknown (explorable) + neighbor_cost = costmap.grid[neighbor.y, neighbor.x] + + # Add free space and unknown space to exploration queue + if neighbor_cost == CostValues.FREE or neighbor_cost == CostValues.UNKNOWN: + neighbor.classification |= PointClassification.MapOpen + map_queue.append(neighbor) + + # Extract just the centroids for ranking + frontier_centroids = frontiers + + if not frontier_centroids: + return [] + + # Rank frontiers using original costmap for proper filtering + ranked_frontiers = self._rank_frontiers( + frontier_centroids, frontier_sizes, robot_pose, costmap + ) + + return ranked_frontiers + + def _update_exploration_direction( + self, robot_pose: Vector3, goal_pose: Vector3 | None = None + ) -> None: + """Update the current exploration direction based on robot movement or selected goal.""" + if goal_pose is not None: + # Calculate direction from robot to goal + direction = Vector3(goal_pose.x - robot_pose.x, goal_pose.y - robot_pose.y, 0.0) + magnitude = np.sqrt(direction.x**2 + direction.y**2) + if magnitude > 0.1: # Avoid division by zero for very close goals + self.exploration_direction = Vector3( + direction.x / magnitude, direction.y / magnitude, 0.0 + ) + + def _compute_direction_momentum_score(self, frontier: Vector3, robot_pose: Vector3) -> float: + """Compute direction momentum score for a frontier.""" + if self.exploration_direction.x == 0 and self.exploration_direction.y == 0: + return 0.0 # No momentum if no previous direction + + # Calculate direction from robot to frontier + frontier_direction = Vector3(frontier.x - robot_pose.x, frontier.y - robot_pose.y, 0.0) + magnitude = np.sqrt(frontier_direction.x**2 + frontier_direction.y**2) + + if magnitude < 0.1: + return 0.0 # Too close to calculate meaningful direction + + # Normalize frontier direction + frontier_direction = Vector3( + frontier_direction.x / magnitude, frontier_direction.y / magnitude, 0.0 + ) + + # Calculate dot product for directional alignment + dot_product = ( + self.exploration_direction.x * frontier_direction.x + + self.exploration_direction.y * frontier_direction.y + ) + + # Return momentum score (higher for same direction, lower for opposite) + return max(0.0, dot_product) # Only positive momentum, no penalty for different directions + + def _compute_distance_to_explored_goals(self, frontier: Vector3) -> float: + """Compute distance from frontier to the nearest explored goal.""" + if not self.explored_goals: + return 5.0 # Default consistent value when no explored goals + # Calculate distance to nearest explored goal + min_distance = float("inf") + for goal in self.explored_goals: + distance = np.sqrt((frontier.x - goal.x) ** 2 + (frontier.y - goal.y) ** 2) + min_distance = min(min_distance, distance) + + return min_distance + + def _compute_distance_to_obstacles(self, frontier: Vector3, costmap: OccupancyGrid) -> float: + """ + Compute the minimum distance from a frontier point to the nearest obstacle. + + Args: + frontier: Frontier point in world coordinates + costmap: Costmap to check for obstacles + + Returns: + Minimum distance to nearest obstacle in meters + """ + # Convert frontier to grid coordinates + grid_pos = costmap.world_to_grid(frontier) + grid_x, grid_y = int(grid_pos.x), int(grid_pos.y) + + # Check if frontier is within costmap bounds + if grid_x < 0 or grid_x >= costmap.width or grid_y < 0 or grid_y >= costmap.height: + return 0.0 # Consider out-of-bounds as obstacle + + min_distance = float("inf") + search_radius = ( + int(self.safe_distance / costmap.resolution) + 5 + ) # Search a bit beyond minimum + + # Search in a square around the frontier point + for dy in range(-search_radius, search_radius + 1): + for dx in range(-search_radius, search_radius + 1): + check_x = grid_x + dx + check_y = grid_y + dy + + # Skip if out of bounds + if ( + check_x < 0 + or check_x >= costmap.width + or check_y < 0 + or check_y >= costmap.height + ): + continue + + # Check if this cell is an obstacle + if costmap.grid[check_y, check_x] >= self.occupancy_threshold: + # Calculate distance in meters + distance = np.sqrt(dx**2 + dy**2) * costmap.resolution + min_distance = min(min_distance, distance) + + # If no obstacles found within search radius, return the safe distance + # This indicates the frontier is safely away from obstacles + return min_distance if min_distance != float("inf") else self.safe_distance + + def _compute_comprehensive_frontier_score( + self, frontier: Vector3, frontier_size: int, robot_pose: Vector3, costmap: OccupancyGrid + ) -> float: + """Compute comprehensive score considering multiple criteria.""" + + # 1. Distance from robot (preference for moderate distances) + robot_distance = get_distance(frontier, robot_pose) + + # Distance score: prefer moderate distances (not too close, not too far) + # Normalized to 0-1 range + distance_score = 1.0 / (1.0 + abs(robot_distance - self.lookahead_distance)) + + # 2. Information gain (frontier size) + # Normalize by a reasonable max frontier size + max_expected_frontier_size = self.min_frontier_perimeter / costmap.resolution * 10 + info_gain_score = min(frontier_size / max_expected_frontier_size, 1.0) + + # 3. Distance to explored goals (bonus for being far from explored areas) + # Normalize by a reasonable max distance (e.g., 10 meters) + explored_goals_distance = self._compute_distance_to_explored_goals(frontier) + explored_goals_score = min(explored_goals_distance / self.max_explored_distance, 1.0) + + # 4. Distance to obstacles (score based on safety) + # 0 = too close to obstacles, 1 = at or beyond safe distance + obstacles_distance = self._compute_distance_to_obstacles(frontier, costmap) + if obstacles_distance >= self.safe_distance: + obstacles_score = 1.0 # Fully safe + else: + obstacles_score = obstacles_distance / self.safe_distance # Linear penalty + + # 5. Direction momentum (already in 0-1 range from dot product) + momentum_score = self._compute_direction_momentum_score(frontier, robot_pose) + + logger.info( + f"Distance score: {distance_score:.2f}, Info gain: {info_gain_score:.2f}, Explored goals: {explored_goals_score:.2f}, Obstacles: {obstacles_score:.2f}, Momentum: {momentum_score:.2f}" + ) + + # Combine scores with consistent scaling + total_score = ( + 0.3 * info_gain_score # 30% information gain + + 0.3 * explored_goals_score # 30% distance from explored goals + + 0.2 * distance_score # 20% distance optimization + + 0.15 * obstacles_score # 15% distance from obstacles + + 0.05 * momentum_score # 5% direction momentum + ) + + return total_score + + def _rank_frontiers( + self, + frontier_centroids: list[Vector3], + frontier_sizes: list[int], + robot_pose: Vector3, + costmap: OccupancyGrid, + ) -> list[Vector3]: + """ + Find the single best frontier using comprehensive scoring and filtering. + + Args: + frontier_centroids: List of frontier centroids + frontier_sizes: List of frontier sizes + robot_pose: Current robot position + costmap: Costmap for additional analysis + + Returns: + List containing single best frontier, or empty list if none suitable + """ + if not frontier_centroids: + return [] + + valid_frontiers = [] + + for i, frontier in enumerate(frontier_centroids): + # Compute comprehensive score + frontier_size = frontier_sizes[i] if i < len(frontier_sizes) else 1 + score = self._compute_comprehensive_frontier_score( + frontier, frontier_size, robot_pose, costmap + ) + + valid_frontiers.append((frontier, score)) + + logger.info(f"Valid frontiers: {len(valid_frontiers)}") + + if not valid_frontiers: + return [] + + # Sort by score and return all valid frontiers (highest scores first) + valid_frontiers.sort(key=lambda x: x[1], reverse=True) + + # Extract just the frontiers (remove scores) and return as list + return [frontier for frontier, _ in valid_frontiers] + + def get_exploration_goal(self, robot_pose: Vector3, costmap: OccupancyGrid) -> Vector3 | None: + """ + Get the single best exploration goal using comprehensive frontier scoring. + + Args: + robot_pose: Current robot position in world coordinates + costmap: Costmap for additional analysis + + Returns: + Single best frontier goal in world coordinates, or None if no suitable frontiers found + """ + # Check if we should compare costmaps for information gain + if len(self.explored_goals) > 5 and self.last_costmap is not None: + current_info = self._count_costmap_information(costmap) + last_info = self._count_costmap_information(self.last_costmap) + + # Check if information increase meets minimum percentage threshold + if last_info > 0: # Avoid division by zero + info_increase_percent = (current_info - last_info) / last_info + if info_increase_percent < self.info_gain_threshold: + logger.info( + f"Information increase ({info_increase_percent:.2f}) below threshold ({self.info_gain_threshold:.2f})" + ) + logger.info( + f"Current information: {current_info}, Last information: {last_info}" + ) + self.no_gain_counter += 1 + if self.no_gain_counter >= self.num_no_gain_attempts: + logger.info( + f"No information gain for {self.no_gain_counter} consecutive attempts" + ) + self.no_gain_counter = 0 # Reset counter when stopping due to no gain + self.stop_exploration() + return None + else: + self.no_gain_counter = 0 + + # Always detect new frontiers to get most up-to-date information + # The new algorithm filters out explored areas and returns only the best frontier + frontiers = self.detect_frontiers(robot_pose, costmap) + + if not frontiers: + # Store current costmap before returning + self.last_costmap = costmap + self.reset_exploration_session() + return None + + # Update exploration direction based on best goal selection + if frontiers: + self._update_exploration_direction(robot_pose, frontiers[0]) + + # Store the selected goal as explored + selected_goal = frontiers[0] + self.mark_explored_goal(selected_goal) + + # Store current costmap for next comparison + self.last_costmap = costmap + + return selected_goal + + # Store current costmap before returning + self.last_costmap = costmap + return None + + def mark_explored_goal(self, goal: Vector3) -> None: + """Mark a goal as explored.""" + self.explored_goals.append(goal) + + def reset_exploration_session(self) -> None: + """ + Reset all exploration state variables for a new exploration session. + + Call this method when starting a new exploration or when the robot + needs to forget its previous exploration history. + """ + self.explored_goals.clear() # Clear all previously explored goals + self.exploration_direction = Vector3(0.0, 0.0, 0.0) # Reset exploration direction + self.last_costmap = None # Clear last costmap comparison + self.no_gain_counter = 0 # Reset no-gain attempt counter + self._cache.clear() # Clear frontier point cache + + logger.info("Exploration session reset - all state variables cleared") + + @rpc + def explore(self) -> bool: + """ + Start autonomous frontier exploration. + + Returns: + bool: True if exploration started, False if already exploring + """ + if self.exploration_active: + logger.warning("Exploration already active") + return False + + self.exploration_active = True + self.stop_event.clear() + + # Start exploration thread + self.exploration_thread = threading.Thread(target=self._exploration_loop, daemon=True) + self.exploration_thread.start() + + logger.info("Started autonomous frontier exploration") + return True + + @rpc + def stop_exploration(self) -> bool: + """ + Stop autonomous frontier exploration. + + Returns: + bool: True if exploration was stopped, False if not exploring + """ + if not self.exploration_active: + return False + + self.exploration_active = False + self.no_gain_counter = 0 # Reset counter when exploration stops + self.stop_event.set() + + # Only join if we're NOT being called from the exploration thread itself + if ( + self.exploration_thread + and self.exploration_thread.is_alive() + and threading.current_thread() != self.exploration_thread + ): + self.exploration_thread.join(timeout=2.0) + + logger.info("Stopped autonomous frontier exploration") + return True + + @rpc + def is_exploration_active(self) -> bool: + return self.exploration_active + + def _exploration_loop(self) -> None: + """Main exploration loop running in separate thread.""" + # Track number of goals published + goals_published = 0 + consecutive_failures = 0 + max_consecutive_failures = 10 # Allow more attempts before giving up + + while self.exploration_active and not self.stop_event.is_set(): + # Check if we have required data + if self.latest_costmap is None or self.latest_odometry is None: + threading.Event().wait(0.5) + continue + + # Get robot pose from odometry + robot_pose = Vector3( + self.latest_odometry.position.x, self.latest_odometry.position.y, 0.0 + ) + + # Get exploration goal + costmap = self.latest_costmap.inflate(0.25) + goal = self.get_exploration_goal(robot_pose, costmap) + + if goal: + # Publish goal to navigator + goal_msg = PoseStamped() + goal_msg.position.x = goal.x + goal_msg.position.y = goal.y + goal_msg.position.z = 0.0 + goal_msg.orientation.w = 1.0 # No rotation + goal_msg.frame_id = "world" + goal_msg.ts = self.latest_costmap.ts + + self.goal_request.publish(goal_msg) + logger.info(f"Published frontier goal: ({goal.x:.2f}, {goal.y:.2f})") + + goals_published += 1 + consecutive_failures = 0 # Reset failure counter on success + + # Clear the goal reached event for next iteration + self.goal_reached_event.clear() + + # Wait for goal to be reached or timeout + logger.info("Waiting for goal to be reached...") + goal_reached = self.goal_reached_event.wait(timeout=self.goal_timeout) + + if goal_reached: + logger.info("Goal reached, finding next frontier") + else: + logger.warning("Goal timeout after 30 seconds, finding next frontier anyway") + else: + consecutive_failures += 1 + + # Only give up if we've published at least 2 goals AND had many consecutive failures + if goals_published >= 2 and consecutive_failures >= max_consecutive_failures: + logger.info( + f"Exploration complete after {goals_published} goals and {consecutive_failures} consecutive failures finding new frontiers" + ) + self.exploration_active = False + break + elif goals_published < 2: + logger.info( + f"No frontier found, but only {goals_published} goals published so far. Retrying in 2 seconds..." + ) + threading.Event().wait(2.0) + else: + logger.info( + f"No frontier found (attempt {consecutive_failures}/{max_consecutive_failures}). Retrying in 2 seconds..." + ) + threading.Event().wait(2.0) + + +wavefront_frontier_explorer = WavefrontFrontierExplorer.blueprint + +__all__ = ["WavefrontFrontierExplorer", "wavefront_frontier_explorer"] diff --git a/dimos/navigation/global_planner/__init__.py b/dimos/navigation/global_planner/__init__.py new file mode 100644 index 0000000000..275619659b --- /dev/null +++ b/dimos/navigation/global_planner/__init__.py @@ -0,0 +1,4 @@ +from dimos.navigation.global_planner.algo import astar +from dimos.navigation.global_planner.planner import AstarPlanner, astar_planner + +__all__ = ["AstarPlanner", "astar", "astar_planner"] diff --git a/dimos/navigation/global_planner/algo.py b/dimos/navigation/global_planner/algo.py new file mode 100644 index 0000000000..16f8dc3600 --- /dev/null +++ b/dimos/navigation/global_planner/algo.py @@ -0,0 +1,215 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import heapq + +from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, VectorLike +from dimos.msgs.nav_msgs import CostValues, OccupancyGrid, Path +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.robot.unitree.global_planner.astar") + + +def astar( + costmap: OccupancyGrid, + goal: VectorLike, + start: VectorLike = (0.0, 0.0), + cost_threshold: int = 90, + unknown_penalty: float = 0.8, +) -> Path | None: + """ + A* path planning algorithm from start to goal position. + + Args: + costmap: Costmap object containing the environment + goal: Goal position as any vector-like object + start: Start position as any vector-like object (default: origin [0,0]) + cost_threshold: Cost threshold above which a cell is considered an obstacle + + Returns: + Path object containing waypoints, or None if no path found + """ + + # Convert world coordinates to grid coordinates directly using vector-like inputs + start_vector = costmap.world_to_grid(start) + goal_vector = costmap.world_to_grid(goal) + logger.debug(f"ASTAR {costmap} {start_vector} -> {goal_vector}") + + # Store positions as tuples for dictionary keys + start_tuple = (int(start_vector.x), int(start_vector.y)) + goal_tuple = (int(goal_vector.x), int(goal_vector.y)) + + # Check if goal is out of bounds + if not (0 <= goal_tuple[0] < costmap.width and 0 <= goal_tuple[1] < costmap.height): + return None + + # Define possible movements (8-connected grid with diagonal movements) + directions = [ + (0, 1), + (1, 0), + (0, -1), + (-1, 0), + (1, 1), + (1, -1), + (-1, 1), + (-1, -1), + ] + + # Cost for each movement (straight vs diagonal) + sc = 1.0 # Straight cost + dc = 1.42 # Diagonal cost (approximately sqrt(2)) + movement_costs = [sc, sc, sc, sc, dc, dc, dc, dc] + + # A* algorithm implementation + open_set = [] # Priority queue for nodes to explore + closed_set = set() # Set of explored nodes + + # Dictionary to store cost from start and parents for each node + g_score = {start_tuple: 0} + parents = {} + + # Heuristic function (Octile distance for 8-connected grid) + def heuristic(x1, y1, x2, y2): + dx = abs(x2 - x1) + dy = abs(y2 - y1) + # Octile distance: optimal for 8-connected grids with diagonal movement + return (dx + dy) + (dc - 2 * sc) * min(dx, dy) + + # Start with the starting node + f_score = g_score[start_tuple] + heuristic( + start_tuple[0], start_tuple[1], goal_tuple[0], goal_tuple[1] + ) + heapq.heappush(open_set, (f_score, start_tuple)) + + # Track nodes already in open set to avoid duplicates + open_set_hash = {start_tuple} + + while open_set: + # Get the node with the lowest f_score + _current_f, current = heapq.heappop(open_set) + current_x, current_y = current + + # Remove from open set hash + if current in open_set_hash: + open_set_hash.remove(current) + + # Skip if already processed (can happen with duplicate entries) + if current in closed_set: + continue + + # Check if we've reached the goal + if current == goal_tuple: + # Reconstruct the path + waypoints = [] + while current in parents: + world_point = costmap.grid_to_world(current) + # Create PoseStamped with identity quaternion (no orientation) + pose = PoseStamped( + frame_id="world", + position=[world_point.x, world_point.y, 0.0], + orientation=Quaternion(0, 0, 0, 1), # Identity quaternion + ) + waypoints.append(pose) + current = parents[current] + + # Add the start position + start_world_point = costmap.grid_to_world(start_tuple) + start_pose = PoseStamped( + frame_id="world", + position=[start_world_point.x, start_world_point.y, 0.0], + orientation=Quaternion(0, 0, 0, 1), + ) + waypoints.append(start_pose) + + # Reverse the path (start to goal) + waypoints.reverse() + + # Add the goal position if it's not already included + goal_point = costmap.grid_to_world(goal_tuple) + + if ( + not waypoints + or (waypoints[-1].x - goal_point.x) ** 2 + (waypoints[-1].y - goal_point.y) ** 2 + > 1e-10 + ): + goal_pose = PoseStamped( + frame_id="world", + position=[goal_point.x, goal_point.y, 0.0], + orientation=Quaternion(0, 0, 0, 1), + ) + waypoints.append(goal_pose) + + return Path(frame_id="world", poses=waypoints) + + # Add current node to closed set + closed_set.add(current) + + # Explore neighbors + for i, (dx, dy) in enumerate(directions): + neighbor_x, neighbor_y = current_x + dx, current_y + dy + neighbor = (neighbor_x, neighbor_y) + + # Check if the neighbor is valid + if not (0 <= neighbor_x < costmap.width and 0 <= neighbor_y < costmap.height): + continue + + # Check if the neighbor is already explored + if neighbor in closed_set: + continue + + # Get the neighbor's cost value + neighbor_val = costmap.grid[neighbor_y, neighbor_x] + + # Skip if it's a hard obstacle + if neighbor_val >= cost_threshold: + continue + + # Calculate movement cost with penalties + # Unknown cells get half the penalty of obstacles + if neighbor_val == CostValues.UNKNOWN: # Unknown cell (-1) + # Unknown cells have a moderate traversal cost (half of obstacle threshold) + cell_cost = cost_threshold * unknown_penalty + elif neighbor_val == CostValues.FREE: # Free space (0) + # Free cells have minimal cost + cell_cost = 0.0 + else: + # Other cells use their actual cost value (1-99) + cell_cost = neighbor_val + + # Calculate cost penalty based on cell cost (higher cost = higher penalty) + # This encourages the planner to prefer lower-cost paths + cost_penalty = cell_cost / CostValues.OCCUPIED # Normalized penalty (divide by 100) + + tentative_g_score = g_score[current] + movement_costs[i] * (1.0 + cost_penalty) + + # Get the current g_score for the neighbor or set to infinity if not yet explored + neighbor_g_score = g_score.get(neighbor, float("inf")) + + # If this path to the neighbor is better than any previous one + if tentative_g_score < neighbor_g_score: + # Update the neighbor's scores and parent + parents[neighbor] = current + g_score[neighbor] = tentative_g_score + f_score = tentative_g_score + heuristic( + neighbor_x, neighbor_y, goal_tuple[0], goal_tuple[1] + ) + + # Add the neighbor to the open set with its f_score + # Only add if not already in open set to reduce duplicates + if neighbor not in open_set_hash: + heapq.heappush(open_set, (f_score, neighbor)) + open_set_hash.add(neighbor) + + # If we get here, no path was found + return None diff --git a/dimos/navigation/global_planner/planner.py b/dimos/navigation/global_planner/planner.py new file mode 100644 index 0000000000..89ac134b08 --- /dev/null +++ b/dimos/navigation/global_planner/planner.py @@ -0,0 +1,224 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from reactivex.disposable import Disposable + +from dimos.core import In, Module, Out, rpc +from dimos.msgs.geometry_msgs import Pose, PoseStamped +from dimos.msgs.nav_msgs import OccupancyGrid, Path +from dimos.navigation.global_planner.algo import astar +from dimos.utils.logging_config import setup_logger +from dimos.utils.transform_utils import euler_to_quaternion + +logger = setup_logger(__file__) + +import math + +from dimos.msgs.geometry_msgs import Quaternion, Vector3 + + +def add_orientations_to_path(path: Path, goal_orientation: Quaternion = None) -> Path: + """Add orientations to path poses based on direction of movement. + + Args: + path: Path with poses to add orientations to + goal_orientation: Desired orientation for the final pose + + Returns: + Path with orientations added to all poses + """ + if not path.poses or len(path.poses) < 2: + return path + + # Calculate orientations for all poses except the last one + for i in range(len(path.poses) - 1): + current_pose = path.poses[i] + next_pose = path.poses[i + 1] + + # Calculate direction to next point + dx = next_pose.position.x - current_pose.position.x + dy = next_pose.position.y - current_pose.position.y + + # Calculate yaw angle + yaw = math.atan2(dy, dx) + + # Convert to quaternion (roll=0, pitch=0, yaw) + orientation = euler_to_quaternion(Vector3(0, 0, yaw)) + current_pose.orientation = orientation + + # Set last pose orientation + identity_quat = Quaternion(0, 0, 0, 1) + if goal_orientation is not None and goal_orientation != identity_quat: + # Use the provided goal orientation if it's not the identity + path.poses[-1].orientation = goal_orientation + elif len(path.poses) > 1: + # Use the previous pose's orientation + path.poses[-1].orientation = path.poses[-2].orientation + else: + # Single pose with identity goal orientation + path.poses[-1].orientation = identity_quat + + return path + + +def resample_path(path: Path, spacing: float) -> Path: + """Resample a path to have approximately uniform spacing between poses. + + Args: + path: The original Path + spacing: Desired distance between consecutive poses + + Returns: + A new Path with resampled poses + """ + if len(path) < 2 or spacing <= 0: + return path + + resampled = [] + resampled.append(path.poses[0]) + + accumulated_distance = 0.0 + + for i in range(1, len(path.poses)): + current = path.poses[i] + prev = path.poses[i - 1] + + # Calculate segment distance + dx = current.x - prev.x + dy = current.y - prev.y + segment_length = (dx**2 + dy**2) ** 0.5 + + if segment_length < 1e-10: + continue + + # Direction vector + dir_x = dx / segment_length + dir_y = dy / segment_length + + # Add points along this segment + while accumulated_distance + segment_length >= spacing: + # Distance along segment for next point + dist_along = spacing - accumulated_distance + if dist_along < 0: + break + + # Create new pose + new_x = prev.x + dir_x * dist_along + new_y = prev.y + dir_y * dist_along + new_pose = PoseStamped( + frame_id=path.frame_id, + position=[new_x, new_y, 0.0], + orientation=prev.orientation, # Keep same orientation + ) + resampled.append(new_pose) + + # Update for next iteration + accumulated_distance = 0 + segment_length -= dist_along + prev = new_pose + + accumulated_distance += segment_length + + # Add last pose if not already there + if len(path.poses) > 1: + last = path.poses[-1] + if not resampled or (resampled[-1].x != last.x or resampled[-1].y != last.y): + resampled.append(last) + + return Path(frame_id=path.frame_id, poses=resampled) + + +class AstarPlanner(Module): + # LCM inputs + target: In[PoseStamped] = None + global_costmap: In[OccupancyGrid] = None + odom: In[PoseStamped] = None + + # LCM outputs + path: Out[Path] = None + + def __init__(self) -> None: + super().__init__() + + # Latest data + self.latest_costmap: OccupancyGrid | None = None + self.latest_odom: PoseStamped | None = None + + @rpc + def start(self) -> None: + super().start() + + unsub = self.target.subscribe(self._on_target) + self._disposables.add(Disposable(unsub)) + + unsub = self.global_costmap.subscribe(self._on_costmap) + self._disposables.add(Disposable(unsub)) + + unsub = self.odom.subscribe(self._on_odom) + self._disposables.add(Disposable(unsub)) + + logger.info("A* planner started") + + @rpc + def stop(self) -> None: + super().stop() + + def _on_costmap(self, msg: OccupancyGrid) -> None: + """Handle incoming costmap messages.""" + self.latest_costmap = msg + + def _on_odom(self, msg: PoseStamped) -> None: + """Handle incoming odometry messages.""" + self.latest_odom = msg + + def _on_target(self, msg: PoseStamped) -> None: + """Handle incoming target messages and trigger planning.""" + if self.latest_costmap is None or self.latest_odom is None: + logger.warning("Cannot plan: missing costmap or odometry data") + return + + path = self.plan(msg) + if path: + # Add orientations to the path, using the goal's orientation for the final pose + path = add_orientations_to_path(path, msg.orientation) + self.path.publish(path) + + def plan(self, goal: Pose) -> Path | None: + """Plan a path from current position to goal.""" + if self.latest_costmap is None or self.latest_odom is None: + logger.warning("Cannot plan: missing costmap or odometry data") + return None + + logger.debug(f"Planning path to goal {goal}") + + # Get current position from odometry + robot_pos = self.latest_odom.position + costmap = self.latest_costmap.inflate(0.2).gradient(max_distance=1.5) + + # Run A* planning + path = astar(costmap, goal.position, robot_pos) + + if path: + path = resample_path(path, 0.1) + logger.debug(f"Path found with {len(path.poses)} waypoints") + return path + + logger.warning("No path found to the goal.") + return None + + +astar_planner = AstarPlanner.blueprint + +__all__ = ["AstarPlanner", "astar_planner"] diff --git a/dimos/navigation/local_planner/__init__.py b/dimos/navigation/local_planner/__init__.py new file mode 100644 index 0000000000..9e0f62931a --- /dev/null +++ b/dimos/navigation/local_planner/__init__.py @@ -0,0 +1,2 @@ +from dimos.navigation.local_planner.holonomic_local_planner import HolonomicLocalPlanner +from dimos.navigation.local_planner.local_planner import BaseLocalPlanner diff --git a/dimos/navigation/local_planner/holonomic_local_planner.py b/dimos/navigation/local_planner/holonomic_local_planner.py new file mode 100644 index 0000000000..acb8dcec98 --- /dev/null +++ b/dimos/navigation/local_planner/holonomic_local_planner.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Gradient-Augmented Look-Ahead Pursuit (GLAP) holonomic local planner. +""" + +import numpy as np + +from dimos.core import rpc +from dimos.msgs.geometry_msgs import Twist, Vector3 +from dimos.navigation.local_planner.local_planner import BaseLocalPlanner +from dimos.utils.transform_utils import get_distance, normalize_angle, quaternion_to_euler + + +class HolonomicLocalPlanner(BaseLocalPlanner): + """ + Gradient-Augmented Look-Ahead Pursuit (GLAP) holonomic local planner. + + This planner combines path following with obstacle avoidance using + costmap gradients to produce smooth holonomic velocity commands. + + Args: + lookahead_dist: Look-ahead distance in meters (default: 1.0) + k_rep: Repulsion gain for obstacle avoidance (default: 1.0) + alpha: Low-pass filter coefficient [0-1] (default: 0.5) + v_max: Maximum velocity per component in m/s (default: 0.8) + goal_tolerance: Distance threshold to consider goal reached (default: 0.5) + control_frequency: Control loop frequency in Hz (default: 10.0) + """ + + def __init__( + self, + lookahead_dist: float = 1.0, + k_rep: float = 0.5, + k_angular: float = 0.75, + alpha: float = 0.5, + v_max: float = 0.8, + goal_tolerance: float = 0.5, + orientation_tolerance: float = 0.2, + control_frequency: float = 10.0, + **kwargs, + ) -> None: + """Initialize the GLAP planner with specified parameters.""" + super().__init__( + goal_tolerance=goal_tolerance, + orientation_tolerance=orientation_tolerance, + control_frequency=control_frequency, + **kwargs, + ) + + # Algorithm parameters + self.lookahead_dist = lookahead_dist + self.k_rep = k_rep + self.alpha = alpha + self.v_max = v_max + self.k_angular = k_angular + + # Previous velocity for filtering (vx, vy, vtheta) + self.v_prev = np.array([0.0, 0.0, 0.0]) + + @rpc + def start(self) -> None: + super().start() + + @rpc + def stop(self) -> None: + super().stop() + + def compute_velocity(self) -> Twist | None: + """ + Compute velocity commands using GLAP algorithm. + + Returns: + Twist with linear and angular velocities in robot frame + """ + if self.latest_odom is None or self.latest_path is None or self.latest_costmap is None: + return None + + pose = np.array([self.latest_odom.position.x, self.latest_odom.position.y]) + + euler = quaternion_to_euler(self.latest_odom.orientation) + robot_yaw = euler.z + + path_points = [] + for pose_stamped in self.latest_path.poses: + path_points.append([pose_stamped.position.x, pose_stamped.position.y]) + + if len(path_points) == 0: + return None + + path = np.array(path_points) + + costmap = self.latest_costmap.grid + + v_follow_odom = self._compute_path_following(pose, path) + + v_rep_odom = self._compute_obstacle_repulsion(pose, costmap) + + v_odom = v_follow_odom + v_rep_odom + + # Transform velocity from odom frame to robot frame + cos_yaw = np.cos(robot_yaw) + sin_yaw = np.sin(robot_yaw) + + v_robot_x = cos_yaw * v_odom[0] + sin_yaw * v_odom[1] + v_robot_y = -sin_yaw * v_odom[0] + cos_yaw * v_odom[1] + + # Compute angular velocity + closest_idx, _ = self._find_closest_point_on_path(pose, path) + + # Check if we're near the final goal + goal_pose = self.latest_path.poses[-1] + distance_to_goal = get_distance(self.latest_odom, goal_pose) + + if distance_to_goal < self.goal_tolerance: + # Near goal - rotate to match final goal orientation + goal_euler = quaternion_to_euler(goal_pose.orientation) + desired_yaw = goal_euler.z + else: + # Not near goal - align with path direction + lookahead_point = self._find_lookahead_point(path, closest_idx) + dx = lookahead_point[0] - pose[0] + dy = lookahead_point[1] - pose[1] + desired_yaw = np.arctan2(dy, dx) + + yaw_error = normalize_angle(desired_yaw - robot_yaw) + k_angular = self.k_angular + v_theta = k_angular * yaw_error + + # Slow down linear velocity when turning + # Scale linear velocity based on angular velocity magnitude + angular_speed = abs(v_theta) + max_angular_speed = self.v_max + + # Calculate speed reduction factor (1.0 when not turning, 0.2 when at max turn rate) + turn_slowdown = 1.0 - 0.8 * min(angular_speed / max_angular_speed, 1.0) + + # Apply speed reduction to linear velocities + v_robot_x = np.clip(v_robot_x * turn_slowdown, -self.v_max, self.v_max) + v_robot_y = np.clip(v_robot_y * turn_slowdown, -self.v_max, self.v_max) + v_theta = np.clip(v_theta, -self.v_max, self.v_max) + + v_raw = np.array([v_robot_x, v_robot_y, v_theta]) + v_filtered = self.alpha * v_raw + (1 - self.alpha) * self.v_prev + self.v_prev = v_filtered + + return Twist( + linear=Vector3(v_filtered[0], v_filtered[1], 0.0), + angular=Vector3(0.0, 0.0, v_filtered[2]), + ) + + def _compute_path_following(self, pose: np.ndarray, path: np.ndarray) -> np.ndarray: + """ + Compute path following velocity using pure pursuit. + + Args: + pose: Current robot position [x, y] + path: Path waypoints as Nx2 array + + Returns: + Path following velocity vector [vx, vy] + """ + closest_idx, _ = self._find_closest_point_on_path(pose, path) + + carrot = self._find_lookahead_point(path, closest_idx) + + direction = carrot - pose + distance = np.linalg.norm(direction) + + if distance < 1e-6: + return np.zeros(2) + + v_follow = self.v_max * direction / distance + + return v_follow + + def _compute_obstacle_repulsion(self, pose: np.ndarray, costmap: np.ndarray) -> np.ndarray: + """ + Compute obstacle repulsion velocity from costmap gradient. + + Args: + pose: Current robot position [x, y] + costmap: 2D costmap array + + Returns: + Repulsion velocity vector [vx, vy] + """ + grid_point = self.latest_costmap.world_to_grid(pose) + grid_x = int(grid_point.x) + grid_y = int(grid_point.y) + + height, width = costmap.shape + if not (1 <= grid_x < width - 1 and 1 <= grid_y < height - 1): + return np.zeros(2) + + # Compute gradient using central differences + # Note: costmap is in row-major order (y, x) + gx = (costmap[grid_y, grid_x + 1] - costmap[grid_y, grid_x - 1]) / ( + 2.0 * self.latest_costmap.resolution + ) + gy = (costmap[grid_y + 1, grid_x] - costmap[grid_y - 1, grid_x]) / ( + 2.0 * self.latest_costmap.resolution + ) + + # Gradient points towards higher cost, so negate for repulsion + v_rep = -self.k_rep * np.array([gx, gy]) + + return v_rep + + def _find_closest_point_on_path( + self, pose: np.ndarray, path: np.ndarray + ) -> tuple[int, np.ndarray]: + """ + Find the closest point on the path to current pose. + + Args: + pose: Current position [x, y] + path: Path waypoints as Nx2 array + + Returns: + Tuple of (closest_index, closest_point) + """ + distances = np.linalg.norm(path - pose, axis=1) + closest_idx = np.argmin(distances) + return closest_idx, path[closest_idx] + + def _find_lookahead_point(self, path: np.ndarray, start_idx: int) -> np.ndarray: + """ + Find look-ahead point on path at specified distance. + + Args: + path: Path waypoints as Nx2 array + start_idx: Starting index for search + + Returns: + Look-ahead point [x, y] + """ + accumulated_dist = 0.0 + + for i in range(start_idx, len(path) - 1): + segment_dist = np.linalg.norm(path[i + 1] - path[i]) + + if accumulated_dist + segment_dist >= self.lookahead_dist: + remaining_dist = self.lookahead_dist - accumulated_dist + t = remaining_dist / segment_dist + carrot = path[i] + t * (path[i + 1] - path[i]) + return carrot + + accumulated_dist += segment_dist + + return path[-1] + + def _clip(self, v: np.ndarray) -> np.ndarray: + """Instance method to clip velocity with access to v_max.""" + return np.clip(v, -self.v_max, self.v_max) + + +holonomic_local_planner = HolonomicLocalPlanner.blueprint + +__all__ = ["HolonomicLocalPlanner", "holonomic_local_planner"] diff --git a/dimos/navigation/local_planner/local_planner.py b/dimos/navigation/local_planner/local_planner.py new file mode 100644 index 0000000000..0a569f00ed --- /dev/null +++ b/dimos/navigation/local_planner/local_planner.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Base Local Planner Module for robot navigation. +Subscribes to local costmap, odometry, and path, publishes movement commands. +""" + +from abc import abstractmethod +import threading +import time + +from reactivex.disposable import Disposable + +from dimos.core import In, Module, Out, rpc +from dimos.msgs.geometry_msgs import PoseStamped, Twist +from dimos.msgs.nav_msgs import OccupancyGrid, Path +from dimos.utils.logging_config import setup_logger +from dimos.utils.transform_utils import get_distance, normalize_angle, quaternion_to_euler + +logger = setup_logger(__file__) + + +class BaseLocalPlanner(Module): + """ + local planner module for obstacle avoidance and path following. + + Subscribes to: + - /local_costmap: Local occupancy grid for obstacle detection + - /odom: Robot odometry for current pose + - /path: Path to follow (continuously updated at ~1Hz) + + Publishes: + - /cmd_vel: Velocity commands for robot movement + """ + + # LCM inputs + local_costmap: In[OccupancyGrid] = None + odom: In[PoseStamped] = None + path: In[Path] = None + + # LCM outputs + cmd_vel: Out[Twist] = None + + def __init__( + self, + goal_tolerance: float = 0.5, + orientation_tolerance: float = 0.2, + control_frequency: float = 10.0, + **kwargs, + ) -> None: + """Initialize the local planner module. + + Args: + goal_tolerance: Distance threshold to consider goal reached (meters) + orientation_tolerance: Orientation threshold to consider goal reached (radians) + control_frequency: Frequency for control loop (Hz) + """ + super().__init__(**kwargs) + + # Parameters + self.goal_tolerance = goal_tolerance + self.orientation_tolerance = orientation_tolerance + self.control_frequency = control_frequency + self.control_period = 1.0 / control_frequency + + # Latest data + self.latest_costmap: OccupancyGrid | None = None + self.latest_odom: PoseStamped | None = None + self.latest_path: Path | None = None + + # Control thread + self.planning_thread: threading.Thread | None = None + self.stop_planning = threading.Event() + + logger.info("Local planner module initialized") + + @rpc + def start(self) -> None: + super().start() + + unsub = self.local_costmap.subscribe(self._on_costmap) + self._disposables.add(Disposable(unsub)) + + unsub = self.odom.subscribe(self._on_odom) + self._disposables.add(Disposable(unsub)) + + unsub = self.path.subscribe(self._on_path) + self._disposables.add(Disposable(unsub)) + + @rpc + def stop(self) -> None: + self.cancel_planning() + super().stop() + + def _on_costmap(self, msg: OccupancyGrid) -> None: + self.latest_costmap = msg + + def _on_odom(self, msg: PoseStamped) -> None: + self.latest_odom = msg + + def _on_path(self, msg: Path) -> None: + self.latest_path = msg + + if msg and len(msg.poses) > 0: + if self.planning_thread is None or not self.planning_thread.is_alive(): + self._start_planning_thread() + + def _start_planning_thread(self) -> None: + """Start the planning thread.""" + self.stop_planning.clear() + self.planning_thread = threading.Thread(target=self._follow_path_loop, daemon=True) + self.planning_thread.start() + logger.debug("Started follow path thread") + + def _follow_path_loop(self) -> None: + """Main planning loop that runs in a separate thread.""" + while not self.stop_planning.is_set(): + if self.is_goal_reached(): + self.stop_planning.set() + stop_cmd = Twist() + self.cmd_vel.publish(stop_cmd) + break + + # Compute and publish velocity + self._plan() + + time.sleep(self.control_period) + + def _plan(self) -> None: + """Compute and publish velocity command.""" + cmd_vel = self.compute_velocity() + + if cmd_vel is not None: + self.cmd_vel.publish(cmd_vel) + + @abstractmethod + def compute_velocity(self) -> Twist | None: + """ + Compute velocity commands based on current costmap, odometry, and path. + Must be implemented by derived classes. + + Returns: + Twist message with linear and angular velocity commands, or None if no command + """ + pass + + @rpc + def is_goal_reached(self) -> bool: + """ + Check if the robot has reached the goal position and orientation. + + Returns: + True if goal is reached within tolerance, False otherwise + """ + if self.latest_odom is None or self.latest_path is None: + return False + + if len(self.latest_path.poses) == 0: + return True + + goal_pose = self.latest_path.poses[-1] + distance = get_distance(self.latest_odom, goal_pose) + + # Check distance tolerance + if distance >= self.goal_tolerance: + return False + + # Check orientation tolerance + current_euler = quaternion_to_euler(self.latest_odom.orientation) + goal_euler = quaternion_to_euler(goal_pose.orientation) + + # Calculate yaw difference and normalize to [-pi, pi] + yaw_error = normalize_angle(goal_euler.z - current_euler.z) + + return abs(yaw_error) < self.orientation_tolerance + + @rpc + def reset(self) -> None: + """Reset the local planner state, clearing the current path.""" + # Clear the latest path + self.latest_path = None + self.latest_odom = None + self.latest_costmap = None + self.cancel_planning() + logger.info("Local planner reset") + + @rpc + def cancel_planning(self) -> None: + """Stop the local planner and any running threads.""" + if self.planning_thread and self.planning_thread.is_alive(): + self.stop_planning.set() + self.planning_thread.join(timeout=1.0) + self.planning_thread = None + stop_cmd = Twist() + self.cmd_vel.publish(stop_cmd) diff --git a/dimos/navigation/local_planner/test_base_local_planner.py b/dimos/navigation/local_planner/test_base_local_planner.py new file mode 100644 index 0000000000..8786b1a925 --- /dev/null +++ b/dimos/navigation/local_planner/test_base_local_planner.py @@ -0,0 +1,406 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Unit tests for the GLAP (Gradient-Augmented Look-Ahead Pursuit) holonomic local planner. +""" + +import numpy as np +import pytest + +from dimos.msgs.geometry_msgs import Pose, PoseStamped, Quaternion +from dimos.msgs.nav_msgs import OccupancyGrid, Path +from dimos.navigation.local_planner.holonomic_local_planner import HolonomicLocalPlanner + + +class TestHolonomicLocalPlanner: + """Test suite for HolonomicLocalPlanner.""" + + @pytest.fixture + def planner(self): + """Create a planner instance for testing.""" + planner = HolonomicLocalPlanner( + lookahead_dist=1.5, + k_rep=1.0, + alpha=1.0, # No filtering for deterministic tests + v_max=1.0, + goal_tolerance=0.5, + control_frequency=10.0, + ) + yield planner + # TODO: This should call `planner.stop()` but that causes errors. + # Calling just this for now to fix thread leaks. + planner._close_module() + + @pytest.fixture + def empty_costmap(self): + """Create an empty costmap (all free space).""" + costmap = OccupancyGrid( + grid=np.zeros((100, 100), dtype=np.int8), resolution=0.1, origin=Pose() + ) + costmap.origin.position.x = -5.0 + costmap.origin.position.y = -5.0 + return costmap + + def test_straight_path_no_obstacles(self, planner, empty_costmap) -> None: + """Test that planner follows straight path with no obstacles.""" + # Set current position at origin + planner.latest_odom = PoseStamped() + planner.latest_odom.position.x = 0.0 + planner.latest_odom.position.y = 0.0 + + # Create straight path along +X + path = Path() + for i in range(10): + ps = PoseStamped() + ps.position.x = float(i) + ps.position.y = 0.0 + ps.orientation.w = 1.0 # Identity quaternion + path.poses.append(ps) + planner.latest_path = path + + # Set empty costmap + planner.latest_costmap = empty_costmap + + # Compute velocity + vel = planner.compute_velocity() + + # Should move along +X + assert vel is not None + assert vel.linear.x > 0.9 # Close to v_max + assert abs(vel.linear.y) < 0.1 # Near zero + assert abs(vel.angular.z) < 0.1 # Small angular velocity when aligned with path + + def test_obstacle_gradient_repulsion(self, planner) -> None: + """Test that obstacle gradients create repulsive forces.""" + # Set position at origin + planner.latest_odom = PoseStamped() + planner.latest_odom.position.x = 0.0 + planner.latest_odom.position.y = 0.0 + + # Simple path forward + path = Path() + ps = PoseStamped() + ps.position.x = 5.0 + ps.position.y = 0.0 + ps.orientation.w = 1.0 + path.poses.append(ps) + planner.latest_path = path + + # Create costmap with gradient pointing south (higher cost north) + costmap_grid = np.zeros((100, 100), dtype=np.int8) + for i in range(100): + costmap_grid[i, :] = max(0, 50 - i) # Gradient from north to south + + planner.latest_costmap = OccupancyGrid(grid=costmap_grid, resolution=0.1, origin=Pose()) + planner.latest_costmap.origin.position.x = -5.0 + planner.latest_costmap.origin.position.y = -5.0 + + # Compute velocity + vel = planner.compute_velocity() + + # Should have positive Y component (pushed north by gradient) + assert vel is not None + assert vel.linear.y > 0.1 # Repulsion pushes north + + def test_lowpass_filter(self) -> None: + """Test that low-pass filter smooths velocity commands.""" + # Create planner with alpha=0.5 for filtering + planner = HolonomicLocalPlanner( + lookahead_dist=1.0, + k_rep=0.0, # No repulsion + alpha=0.5, # 50% filtering + v_max=1.0, + ) + + # Setup similar to straight path test + planner.latest_odom = PoseStamped() + planner.latest_odom.position.x = 0.0 + planner.latest_odom.position.y = 0.0 + + path = Path() + ps = PoseStamped() + ps.position.x = 5.0 + ps.position.y = 0.0 + ps.orientation.w = 1.0 + path.poses.append(ps) + planner.latest_path = path + + planner.latest_costmap = OccupancyGrid( + grid=np.zeros((100, 100), dtype=np.int8), resolution=0.1, origin=Pose() + ) + planner.latest_costmap.origin.position.x = -5.0 + planner.latest_costmap.origin.position.y = -5.0 + + # First call - previous velocity is zero + vel1 = planner.compute_velocity() + assert vel1 is not None + + # Store first velocity + first_vx = vel1.linear.x + + # Second call - should be filtered + vel2 = planner.compute_velocity() + assert vel2 is not None + + # With alpha=0.5 and same conditions: + # v2 = 0.5 * v_raw + 0.5 * v1 + # The filtering effect should be visible + # v2 should be between v1 and the raw velocity + assert vel2.linear.x != first_vx # Should be different due to filtering + assert 0 < vel2.linear.x <= planner.v_max # Should still be positive and within limits + planner._close_module() + + def test_no_path(self, planner, empty_costmap) -> None: + """Test that planner returns None when no path is available.""" + planner.latest_odom = PoseStamped() + planner.latest_costmap = empty_costmap + planner.latest_path = Path() # Empty path + + vel = planner.compute_velocity() + assert vel is None + + def test_no_odometry(self, planner, empty_costmap) -> None: + """Test that planner returns None when no odometry is available.""" + planner.latest_odom = None + planner.latest_costmap = empty_costmap + + path = Path() + ps = PoseStamped() + ps.position.x = 1.0 + ps.position.y = 0.0 + path.poses.append(ps) + planner.latest_path = path + + vel = planner.compute_velocity() + assert vel is None + + def test_no_costmap(self, planner) -> None: + """Test that planner returns None when no costmap is available.""" + planner.latest_odom = PoseStamped() + planner.latest_costmap = None + + path = Path() + ps = PoseStamped() + ps.position.x = 1.0 + ps.position.y = 0.0 + path.poses.append(ps) + planner.latest_path = path + + vel = planner.compute_velocity() + assert vel is None + + def test_goal_reached(self, planner, empty_costmap) -> None: + """Test velocity when robot is at goal.""" + # Set robot at goal position + planner.latest_odom = PoseStamped() + planner.latest_odom.position.x = 5.0 + planner.latest_odom.position.y = 0.0 + + # Path with single point at robot position + path = Path() + ps = PoseStamped() + ps.position.x = 5.0 + ps.position.y = 0.0 + ps.orientation.w = 1.0 + path.poses.append(ps) + planner.latest_path = path + + planner.latest_costmap = empty_costmap + + # Compute velocity + vel = planner.compute_velocity() + + # Should have near-zero velocity + assert vel is not None + assert abs(vel.linear.x) < 0.1 + assert abs(vel.linear.y) < 0.1 + + def test_velocity_saturation(self, planner, empty_costmap) -> None: + """Test that velocities are capped at v_max.""" + # Set robot far from goal to maximize commanded velocity + planner.latest_odom = PoseStamped() + planner.latest_odom.position.x = 0.0 + planner.latest_odom.position.y = 0.0 + + # Create path far away + path = Path() + ps = PoseStamped() + ps.position.x = 100.0 # Very far + ps.position.y = 0.0 + ps.orientation.w = 1.0 + path.poses.append(ps) + planner.latest_path = path + + planner.latest_costmap = empty_costmap + + # Compute velocity + vel = planner.compute_velocity() + + # Velocity should be saturated at v_max + assert vel is not None + assert abs(vel.linear.x) <= planner.v_max + 0.01 # Small tolerance + assert abs(vel.linear.y) <= planner.v_max + 0.01 + assert abs(vel.angular.z) <= planner.v_max + 0.01 + + def test_lookahead_interpolation(self, planner, empty_costmap) -> None: + """Test that lookahead point is correctly interpolated on path.""" + # Set robot at origin + planner.latest_odom = PoseStamped() + planner.latest_odom.position.x = 0.0 + planner.latest_odom.position.y = 0.0 + + # Create path with waypoints closer than lookahead distance + path = Path() + for i in range(5): + ps = PoseStamped() + ps.position.x = i * 0.5 # 0.5m spacing + ps.position.y = 0.0 + ps.orientation.w = 1.0 + path.poses.append(ps) + planner.latest_path = path + + planner.latest_costmap = empty_costmap + + # Compute velocity + vel = planner.compute_velocity() + + # Should move forward along path + assert vel is not None + assert vel.linear.x > 0.5 # Moving forward + assert abs(vel.linear.y) < 0.1 # Staying on path + + def test_curved_path_following(self, planner, empty_costmap) -> None: + """Test following a curved path.""" + # Set robot at origin + planner.latest_odom = PoseStamped() + planner.latest_odom.position.x = 0.0 + planner.latest_odom.position.y = 0.0 + + # Create curved path (quarter circle) + path = Path() + for i in range(10): + angle = (np.pi / 2) * (i / 9.0) # 0 to 90 degrees + ps = PoseStamped() + ps.position.x = 2.0 * np.cos(angle) + ps.position.y = 2.0 * np.sin(angle) + ps.orientation.w = 1.0 + path.poses.append(ps) + planner.latest_path = path + + planner.latest_costmap = empty_costmap + + # Compute velocity + vel = planner.compute_velocity() + + # Should have both X and Y components for curved motion + assert vel is not None + # Test general behavior: should be moving (not exact values) + assert vel.linear.x > 0 # Moving forward (any positive value) + assert vel.linear.y > 0 # Turning left (any positive value) + # Ensure we have meaningful movement, not just noise + total_linear = np.sqrt(vel.linear.x**2 + vel.linear.y**2) + assert total_linear > 0.1 # Some reasonable movement + + def test_robot_frame_transformation(self, empty_costmap) -> None: + """Test that velocities are correctly transformed to robot frame.""" + # Create planner with no filtering for deterministic test + planner = HolonomicLocalPlanner( + lookahead_dist=1.0, + k_rep=0.0, # No repulsion + alpha=1.0, # No filtering + v_max=1.0, + ) + + # Set robot at origin but rotated 90 degrees (facing +Y in odom frame) + planner.latest_odom = PoseStamped() + planner.latest_odom.position.x = 0.0 + planner.latest_odom.position.y = 0.0 + # Quaternion for 90 degree rotation around Z + planner.latest_odom.orientation = Quaternion(0.0, 0.0, 0.7071068, 0.7071068) + + # Create path along +X axis in odom frame + path = Path() + for i in range(5): + ps = PoseStamped() + ps.position.x = float(i) + ps.position.y = 0.0 + ps.orientation.w = 1.0 + path.poses.append(ps) + planner.latest_path = path + + planner.latest_costmap = empty_costmap + + # Compute velocity + vel = planner.compute_velocity() + + # Robot is facing +Y, path is along +X + # So in robot frame: forward is +Y direction, path is to the right + assert vel is not None + # Test relative magnitudes and signs rather than exact values + # Path is to the right, so Y velocity should be negative + assert vel.linear.y < 0 # Should move right (negative Y in robot frame) + # Should turn to align with path + assert vel.angular.z < 0 # Should turn right (negative angular velocity) + # X velocity should be relatively small compared to Y + assert abs(vel.linear.x) < abs(vel.linear.y) # Lateral movement dominates + planner._close_module() + + def test_angular_velocity_computation(self, empty_costmap) -> None: + """Test that angular velocity is computed to align with path.""" + planner = HolonomicLocalPlanner( + lookahead_dist=2.0, + k_rep=0.0, # No repulsion + alpha=1.0, # No filtering + v_max=1.0, + ) + + # Robot at origin facing +X + planner.latest_odom = PoseStamped() + planner.latest_odom.position.x = 0.0 + planner.latest_odom.position.y = 0.0 + planner.latest_odom.orientation.w = 1.0 # Identity quaternion + + # Create path at 45 degrees + path = Path() + for i in range(5): + ps = PoseStamped() + ps.position.x = float(i) + ps.position.y = float(i) # Diagonal path + ps.orientation.w = 1.0 + path.poses.append(ps) + planner.latest_path = path + + planner.latest_costmap = empty_costmap + + # Compute velocity + vel = planner.compute_velocity() + + # Path is at 45 degrees, robot facing 0 degrees + # Should have positive angular velocity to turn left + assert vel is not None + # Test general behavior without exact thresholds + assert vel.linear.x > 0 # Moving forward (any positive value) + assert vel.linear.y > 0 # Moving left (holonomic, any positive value) + assert vel.angular.z > 0 # Turning left (positive angular velocity) + # Verify the robot is actually moving with reasonable speed + total_linear = np.sqrt(vel.linear.x**2 + vel.linear.y**2) + assert total_linear > 0.1 # Some meaningful movement + # Since path is diagonal, X and Y should be similar magnitude + assert ( + abs(vel.linear.x - vel.linear.y) < max(vel.linear.x, vel.linear.y) * 0.5 + ) # Within 50% of each other + planner._close_module() diff --git a/dimos/navigation/rosnav/__init__.py b/dimos/navigation/rosnav/__init__.py new file mode 100644 index 0000000000..2ed1f51d04 --- /dev/null +++ b/dimos/navigation/rosnav/__init__.py @@ -0,0 +1,2 @@ +from dimos.navigation.rosnav.nav_bot import NavBot, ROSNavigationModule +from dimos.navigation.rosnav.rosnav import ROSNav diff --git a/dimos/navigation/rosnav/nav_bot.py b/dimos/navigation/rosnav/nav_bot.py new file mode 100644 index 0000000000..0e3fc08cc8 --- /dev/null +++ b/dimos/navigation/rosnav/nav_bot.py @@ -0,0 +1,422 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +NavBot class for navigation-related functionality. +Encapsulates ROS bridge and topic remapping for Unitree robots. +""" + +import logging +import threading +import time + +# ROS2 message imports +from geometry_msgs.msg import ( + PointStamped as ROSPointStamped, + PoseStamped as ROSPoseStamped, + TwistStamped as ROSTwistStamped, +) +from nav_msgs.msg import Odometry as ROSOdometry, Path as ROSPath +import rclpy +from rclpy.node import Node +from sensor_msgs.msg import Joy as ROSJoy, PointCloud2 as ROSPointCloud2 +from std_msgs.msg import Bool as ROSBool, Int8 as ROSInt8 +from tf2_msgs.msg import TFMessage as ROSTFMessage + +from dimos import core +from dimos.core import In, Out, rpc +from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Transform, Twist, Vector3 +from dimos.msgs.nav_msgs import Odometry, Path +from dimos.msgs.sensor_msgs import PointCloud2 +from dimos.msgs.std_msgs import Bool +from dimos.msgs.tf2_msgs.TFMessage import TFMessage +from dimos.navigation.rosnav import ROSNav +from dimos.protocol import pubsub +from dimos.utils.logging_config import setup_logger +from dimos.utils.transform_utils import euler_to_quaternion + +logger = setup_logger("dimos.robot.unitree_webrtc.nav_bot", level=logging.INFO) + + +class ROSNavigationModule(ROSNav): + """ + Handles navigation control and odometry remapping. + """ + + goal_req: In[PoseStamped] = None + cancel_goal: In[Bool] = None + + pointcloud: Out[PointCloud2] = None + global_pointcloud: Out[PointCloud2] = None + + goal_active: Out[PoseStamped] = None + path_active: Out[Path] = None + goal_reached: Out[Bool] = None + odom: Out[Odometry] = None + cmd_vel: Out[Twist] = None + odom_pose: Out[PoseStamped] = None + + def __init__(self, sensor_to_base_link_transform=None, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + if not rclpy.ok(): + rclpy.init() + self._node = Node("navigation_module") + + self.goal_reach = None + self.sensor_to_base_link_transform = sensor_to_base_link_transform or [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ] + self.spin_thread = None + + # ROS2 Publishers + self.goal_pose_pub = self._node.create_publisher(ROSPoseStamped, "/goal_pose", 10) + self.cancel_goal_pub = self._node.create_publisher(ROSBool, "/cancel_goal", 10) + self.soft_stop_pub = self._node.create_publisher(ROSInt8, "/soft_stop", 10) + self.joy_pub = self._node.create_publisher(ROSJoy, "/joy", 10) + + # ROS2 Subscribers + self.goal_reached_sub = self._node.create_subscription( + ROSBool, "/goal_reached", self._on_ros_goal_reached, 10 + ) + self.odom_sub = self._node.create_subscription( + ROSOdometry, "/state_estimation", self._on_ros_odom, 10 + ) + self.cmd_vel_sub = self._node.create_subscription( + ROSTwistStamped, "/cmd_vel", self._on_ros_cmd_vel, 10 + ) + self.goal_waypoint_sub = self._node.create_subscription( + ROSPointStamped, "/way_point", self._on_ros_goal_waypoint, 10 + ) + self.registered_scan_sub = self._node.create_subscription( + ROSPointCloud2, "/registered_scan", self._on_ros_registered_scan, 10 + ) + self.global_pointcloud_sub = self._node.create_subscription( + ROSPointCloud2, "/terrain_map_ext", self._on_ros_global_pointcloud, 10 + ) + self.path_sub = self._node.create_subscription(ROSPath, "/path", self._on_ros_path, 10) + self.tf_sub = self._node.create_subscription(ROSTFMessage, "/tf", self._on_ros_tf, 10) + + logger.info("NavigationModule initialized with ROS2 node") + + @rpc + def start(self) -> None: + self._running = True + self.spin_thread = threading.Thread(target=self._spin_node, daemon=True) + self.spin_thread.start() + + self.goal_req.subscribe(self._on_goal_pose) + self.cancel_goal.subscribe(self._on_cancel_goal) + + logger.info("NavigationModule started with ROS2 spinning") + + def _spin_node(self) -> None: + while self._running and rclpy.ok(): + try: + rclpy.spin_once(self._node, timeout_sec=0.1) + except Exception as e: + if self._running: + logger.error(f"ROS2 spin error: {e}") + + def _on_ros_goal_reached(self, msg: ROSBool) -> None: + self.goal_reach = msg.data + dimos_bool = Bool(data=msg.data) + self.goal_reached.publish(dimos_bool) + + def _on_ros_goal_waypoint(self, msg: ROSPointStamped) -> None: + dimos_pose = PoseStamped( + ts=time.time(), + frame_id=msg.header.frame_id, + position=Vector3(msg.point.x, msg.point.y, msg.point.z), + orientation=Quaternion(0.0, 0.0, 0.0, 1.0), + ) + self.goal_active.publish(dimos_pose) + + def _on_ros_cmd_vel(self, msg: ROSTwistStamped) -> None: + # Extract the twist from the stamped message + dimos_twist = Twist( + linear=Vector3(msg.twist.linear.x, msg.twist.linear.y, msg.twist.linear.z), + angular=Vector3(msg.twist.angular.x, msg.twist.angular.y, msg.twist.angular.z), + ) + self.cmd_vel.publish(dimos_twist) + + def _on_ros_odom(self, msg: ROSOdometry) -> None: + dimos_odom = Odometry.from_ros_msg(msg) + self.odom.publish(dimos_odom) + + dimos_pose = PoseStamped( + ts=dimos_odom.ts, + frame_id=dimos_odom.frame_id, + position=dimos_odom.pose.pose.position, + orientation=dimos_odom.pose.pose.orientation, + ) + self.odom_pose.publish(dimos_pose) + + def _on_ros_registered_scan(self, msg: ROSPointCloud2) -> None: + dimos_pointcloud = PointCloud2.from_ros_msg(msg) + self.pointcloud.publish(dimos_pointcloud) + + def _on_ros_global_pointcloud(self, msg: ROSPointCloud2) -> None: + dimos_pointcloud = PointCloud2.from_ros_msg(msg) + self.global_pointcloud.publish(dimos_pointcloud) + + def _on_ros_path(self, msg: ROSPath) -> None: + dimos_path = Path.from_ros_msg(msg) + self.path_active.publish(dimos_path) + + def _on_ros_tf(self, msg: ROSTFMessage) -> None: + ros_tf = TFMessage.from_ros_msg(msg) + + translation = Vector3( + self.sensor_to_base_link_transform[0], + self.sensor_to_base_link_transform[1], + self.sensor_to_base_link_transform[2], + ) + euler_angles = Vector3( + self.sensor_to_base_link_transform[3], + self.sensor_to_base_link_transform[4], + self.sensor_to_base_link_transform[5], + ) + rotation = euler_to_quaternion(euler_angles) + + sensor_to_base_link_tf = Transform( + translation=translation, + rotation=rotation, + frame_id="sensor", + child_frame_id="base_link", + ts=time.time(), + ) + + map_to_world_tf = Transform( + translation=Vector3(0.0, 0.0, 0.0), + rotation=euler_to_quaternion(Vector3(0.0, 0.0, 0.0)), + frame_id="map", + child_frame_id="world", + ts=time.time(), + ) + + self.tf.publish(sensor_to_base_link_tf, map_to_world_tf, *ros_tf.transforms) + + def _on_goal_pose(self, msg: PoseStamped) -> None: + self.navigate_to(msg) + + def _on_cancel_goal(self, msg: Bool) -> None: + if msg.data: + self.stop() + + def _set_autonomy_mode(self) -> None: + joy_msg = ROSJoy() + joy_msg.axes = [ + 0.0, # axis 0 + 0.0, # axis 1 + -1.0, # axis 2 + 0.0, # axis 3 + 1.0, # axis 4 + 1.0, # axis 5 + 0.0, # axis 6 + 0.0, # axis 7 + ] + joy_msg.buttons = [ + 0, # button 0 + 0, # button 1 + 0, # button 2 + 0, # button 3 + 0, # button 4 + 0, # button 5 + 0, # button 6 + 1, # button 7 - controls autonomy mode + 0, # button 8 + 0, # button 9 + 0, # button 10 + ] + self.joy_pub.publish(joy_msg) + logger.info("Setting autonomy mode via Joy message") + + @rpc + def navigate_to(self, pose: PoseStamped, timeout: float = 60.0) -> bool: + """ + Navigate to a target pose by publishing to ROS topics. + + Args: + pose: Target pose to navigate to + timeout: Maximum time to wait for goal (seconds) + + Returns: + True if navigation was successful + """ + logger.info( + f"Navigating to goal: ({pose.position.x:.2f}, {pose.position.y:.2f}, {pose.position.z:.2f})" + ) + + self.goal_reach = None + self._set_autonomy_mode() + + # Enable soft stop (0 = enable) + soft_stop_msg = ROSInt8() + soft_stop_msg.data = 0 + self.soft_stop_pub.publish(soft_stop_msg) + + ros_pose = pose.to_ros_msg() + self.goal_pose_pub.publish(ros_pose) + + # Wait for goal to be reached + start_time = time.time() + while time.time() - start_time < timeout: + if self.goal_reach is not None: + soft_stop_msg.data = 2 + self.soft_stop_pub.publish(soft_stop_msg) + return self.goal_reach + time.sleep(0.1) + + self.stop_navigation() + logger.warning(f"Navigation timed out after {timeout} seconds") + return False + + @rpc + def stop_navigation(self) -> bool: + """ + Stop current navigation by publishing to ROS topics. + + Returns: + True if stop command was sent successfully + """ + logger.info("Stopping navigation") + + cancel_msg = ROSBool() + cancel_msg.data = True + self.cancel_goal_pub.publish(cancel_msg) + + soft_stop_msg = ROSInt8() + soft_stop_msg.data = 2 + self.soft_stop_pub.publish(soft_stop_msg) + + return True + + @rpc + def stop(self) -> None: + try: + self._running = False + if self.spin_thread: + self.spin_thread.join(timeout=1) + self._node.destroy_node() + except Exception as e: + logger.error(f"Error during shutdown: {e}") + + +class NavBot: + """ + NavBot wrapper that deploys NavigationModule with proper DIMOS/ROS2 integration. + """ + + def __init__(self, dimos=None, sensor_to_base_link_transform=None) -> None: + """ + Initialize NavBot. + + Args: + dimos: DIMOS instance (creates new one if None) + sensor_to_base_link_transform: Optional [x, y, z, roll, pitch, yaw] transform + """ + if dimos is None: + self.dimos = core.start(2) + else: + self.dimos = dimos + + self.sensor_to_base_link_transform = sensor_to_base_link_transform or [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ] + self.navigation_module = None + + def start(self) -> None: + logger.info("Deploying navigation module...") + self.navigation_module = self.dimos.deploy( + ROSNavigationModule, sensor_to_base_link_transform=self.sensor_to_base_link_transform + ) + + self.navigation_module.goal_req.transport = core.LCMTransport("/goal", PoseStamped) + self.navigation_module.cancel_goal.transport = core.LCMTransport("/cancel_goal", Bool) + + self.navigation_module.pointcloud.transport = core.LCMTransport( + "/pointcloud_map", PointCloud2 + ) + self.navigation_module.global_pointcloud.transport = core.LCMTransport( + "/global_pointcloud", PointCloud2 + ) + self.navigation_module.goal_active.transport = core.LCMTransport( + "/goal_active", PoseStamped + ) + self.navigation_module.path_active.transport = core.LCMTransport("/path_active", Path) + self.navigation_module.goal_reached.transport = core.LCMTransport("/goal_reached", Bool) + self.navigation_module.odom.transport = core.LCMTransport("/odom", Odometry) + self.navigation_module.cmd_vel.transport = core.LCMTransport("/cmd_vel", Twist) + self.navigation_module.odom_pose.transport = core.LCMTransport("/odom_pose", PoseStamped) + + self.navigation_module.start() + + def shutdown(self) -> None: + logger.info("Shutting down NavBot...") + + if self.navigation_module: + self.navigation_module.stop() + + if rclpy.ok(): + rclpy.shutdown() + + logger.info("NavBot shutdown complete") + + +def main() -> None: + pubsub.lcm.autoconf() + nav_bot = NavBot() + nav_bot.start() + + logger.info("\nTesting navigation in 2 seconds...") + time.sleep(2) + + test_pose = PoseStamped( + ts=time.time(), + frame_id="map", + position=Vector3(1.0, 1.0, 0.0), + orientation=Quaternion(0.0, 0.0, 0.0, 0.0), + ) + + logger.info("Sending navigation goal to: (1.0, 1.0, 0.0)") + + if nav_bot.navigation_module: + success = nav_bot.navigation_module.navigate_to(test_pose, timeout=30.0) + if success: + logger.info("✓ Navigation goal reached!") + else: + logger.warning("✗ Navigation failed or timed out") + + try: + logger.info("\nNavBot running. Press Ctrl+C to stop.") + while True: + time.sleep(1) + except KeyboardInterrupt: + logger.info("\nShutting down...") + nav_bot.shutdown() + + +if __name__ == "__main__": + main() diff --git a/dimos/navigation/rosnav/rosnav.py b/dimos/navigation/rosnav/rosnav.py new file mode 100644 index 0000000000..440a0f4269 --- /dev/null +++ b/dimos/navigation/rosnav/rosnav.py @@ -0,0 +1,47 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.core import In, Module, Out +from dimos.msgs.geometry_msgs import PoseStamped, Twist +from dimos.msgs.nav_msgs import Path +from dimos.msgs.sensor_msgs import PointCloud2 +from dimos.msgs.std_msgs import Bool + + +class ROSNav(Module): + goal_req: In[PoseStamped] = None # type: ignore + goal_active: Out[PoseStamped] = None # type: ignore + path_active: Out[Path] = None # type: ignore + cancel_goal: In[Bool] = None # type: ignore + cmd_vel: Out[Twist] = None # type: ignore + + # PointcloudPerception attributes + pointcloud: Out[PointCloud2] = None # type: ignore + + # Global3DMapSpec attributes + global_pointcloud: Out[PointCloud2] = None # type: ignore + + def start(self) -> None: + pass + + def stop(self) -> None: + pass + + def navigate_to(self, target: PoseStamped) -> None: + # TODO: Implement navigation logic + pass + + def stop_navigation(self) -> None: + # TODO: Implement stop logic + pass diff --git a/dimos/navigation/visual/query.py b/dimos/navigation/visual/query.py new file mode 100644 index 0000000000..45a0ede40d --- /dev/null +++ b/dimos/navigation/visual/query.py @@ -0,0 +1,44 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from dimos.models.qwen.video_query import BBox +from dimos.models.vl.base import VlModel +from dimos.msgs.sensor_msgs import Image +from dimos.utils.generic import extract_json_from_llm_response + + +def get_object_bbox_from_image( + vl_model: VlModel, image: Image, object_description: str +) -> BBox | None: + prompt = ( + f"Look at this image and find the '{object_description}'. " + "Return ONLY a JSON object with format: {'name': 'object_name', 'bbox': [x1, y1, x2, y2]} " + "where x1,y1 is the top-left and x2,y2 is the bottom-right corner of the bounding box. If not found, return None." + ) + + response = vl_model.query(image, prompt) + + result = extract_json_from_llm_response(response) + if not result: + return None + + try: + ret = tuple(map(float, result["bbox"])) + if len(ret) == 4: + return ret + except Exception: + pass + + return None diff --git a/dimos/perception/common/__init__.py b/dimos/perception/common/__init__.py index a4dd07c913..67481bc449 100644 --- a/dimos/perception/common/__init__.py +++ b/dimos/perception/common/__init__.py @@ -1,3 +1,3 @@ -from .detection2d_tracker import target2dTracker, get_tracked_results -from .cuboid_fit import * -from .ibvs import * \ No newline at end of file +from .detection2d_tracker import get_tracked_results, target2dTracker +from .ibvs import * +from .utils import * diff --git a/dimos/perception/common/cuboid_fit.py b/dimos/perception/common/cuboid_fit.py deleted file mode 100644 index 8bf37be170..0000000000 --- a/dimos/perception/common/cuboid_fit.py +++ /dev/null @@ -1,285 +0,0 @@ -import numpy as np -from sklearn.decomposition import PCA -import matplotlib.pyplot as plt -import cv2 -import argparse -from pathlib import Path - -def depth_to_point_cloud(depth_image, camera_matrix, subsample_factor=4): - """ - Convert depth image to point cloud using camera intrinsics. - Subsamples points to reduce density. - - Args: - depth_image: HxW depth image in meters - camera_matrix: 3x3 camera intrinsic matrix - subsample_factor: Factor to subsample points (higher = fewer points) - - Returns: - Nx3 array of 3D points - """ - # Get focal length and principal point from camera matrix - fx = camera_matrix[0, 0] - fy = camera_matrix[1, 1] - cx = camera_matrix[0, 2] - cy = camera_matrix[1, 2] - - # Create pixel coordinate grid - rows, cols = depth_image.shape - x_grid, y_grid = np.meshgrid(np.arange(0, cols, subsample_factor), - np.arange(0, rows, subsample_factor)) - - # Flatten grid and depth - x = x_grid.flatten() - y = y_grid.flatten() - z = depth_image[y_grid, x_grid].flatten() - - # Remove points with invalid depth - valid = z > 0 - x = x[valid] - y = y[valid] - z = z[valid] - - # Convert to 3D points - X = (x - cx) * z / fx - Y = (y - cy) * z / fy - Z = z - - return np.column_stack([X, Y, Z]) - -def fit_cuboid(points, n_iterations=5, inlier_thresh=2.0): - """ - Fit a cuboid to a point cloud using iteratively refined PCA. - - Args: - points: Nx3 array of points - n_iterations: Number of refinement iterations - inlier_thresh: Threshold for inlier detection in standard deviations - - Returns: - dict containing: - - center: 3D center point - - dimensions: 3D dimensions - - rotation: 3x3 rotation matrix - - error: fitting error - """ - points = np.asarray(points) - if len(points) < 4: - return None - - # Initial center estimate using median for robustness - best_error = float('inf') - best_params = None - center = np.median(points, axis=0) - current_points = points - center - - for iteration in range(n_iterations): - if len(current_points) < 4: # Need at least 4 points for PCA - break - - # Perform PCA - pca = PCA(n_components=3) - pca.fit(current_points) - - # Get rotation matrix from PCA - rotation = pca.components_ - - # Transform points to PCA space - local_points = current_points @ rotation.T - - # Initialize mask for this iteration - inlier_mask = np.ones(len(current_points), dtype=bool) - dimensions = np.zeros(3) - - # Filter points along each dimension - for dim in range(3): - points_1d = local_points[inlier_mask, dim] - if len(points_1d) < 4: - break - - median = np.median(points_1d) - mad = np.median(np.abs(points_1d - median)) - sigma = mad * 1.4826 # Convert MAD to standard deviation estimate - - # Avoid issues with constant values - if sigma < 1e-6: - continue - - # Update mask for this dimension - dim_inliers = np.abs(points_1d - median) < (inlier_thresh * sigma) - inlier_mask[inlier_mask] = dim_inliers - - # Calculate dimension based on robust statistics - valid_points = points_1d[dim_inliers] - if len(valid_points) > 0: - dimensions[dim] = np.max(valid_points) - np.min(valid_points) - - # Skip if we don't have enough inliers - if np.sum(inlier_mask) < 4: - continue - - # Calculate error for this iteration - # Mean squared distance from points to cuboid surface - half_dims = dimensions / 2 - dx = np.abs(local_points[:, 0]) - half_dims[0] - dy = np.abs(local_points[:, 1]) - half_dims[1] - dz = np.abs(local_points[:, 2]) - half_dims[2] - - outside_dist = np.sqrt(np.maximum(dx, 0)**2 + - np.maximum(dy, 0)**2 + - np.maximum(dz, 0)**2) - inside_dist = np.minimum(np.maximum(np.maximum(dx, dy), dz), 0) - distances = outside_dist + inside_dist - error = np.mean(distances**2) - - if error < best_error: - best_error = error - best_params = { - 'center': center, - 'rotation': rotation, - 'dimensions': dimensions, - 'error': error - } - - # Update points for next iteration - current_points = current_points[inlier_mask] - - return best_params - -def compute_fitting_error(local_points, dimensions): - """Compute mean squared distance from points to cuboid surface.""" - half_dims = dimensions / 2 - dx = np.abs(local_points[:, 0]) - half_dims[0] - dy = np.abs(local_points[:, 1]) - half_dims[1] - dz = np.abs(local_points[:, 2]) - half_dims[2] - - outside_dist = np.sqrt(np.maximum(dx, 0)**2 + - np.maximum(dy, 0)**2 + - np.maximum(dz, 0)**2) - inside_dist = np.minimum(np.maximum(np.maximum(dx, dy), dz), 0) - - distances = outside_dist + inside_dist - return np.mean(distances**2) - -def get_cuboid_corners(center, dimensions, rotation): - """Get the 8 corners of a cuboid.""" - half_dims = dimensions / 2 - corners_local = np.array([ - [-1, -1, -1], # 0: left bottom back - [-1, -1, 1], # 1: left bottom front - [-1, 1, -1], # 2: left top back - [-1, 1, 1], # 3: left top front - [ 1, -1, -1], # 4: right bottom back - [ 1, -1, 1], # 5: right bottom front - [ 1, 1, -1], # 6: right top back - [ 1, 1, 1] # 7: right top front - ]) * half_dims - - return corners_local @ rotation + center - -def visualize_fit(image, cuboid_params, camera_matrix, R=None, t=None): - """ - Draw the fitted cuboid on the image. - """ - # Get corners in world coordinates - corners = get_cuboid_corners( - cuboid_params['center'], - cuboid_params['dimensions'], - cuboid_params['rotation'] - ) - - # Transform corners if R and t are provided - if R is not None and t is not None: - corners = (R @ corners.T).T + t - - # Project corners to image space - corners_img = cv2.projectPoints( - corners, - np.zeros(3), np.zeros(3), # Already in camera frame - camera_matrix, - None - )[0].reshape(-1, 2).astype(int) - - # Define edges for visualization - edges = [ - # Bottom face - (0, 1), (1, 5), (5, 4), (4, 0), - # Top face - (2, 3), (3, 7), (7, 6), (6, 2), - # Vertical edges - (0, 2), (1, 3), (5, 7), (4, 6) - ] - - # Draw edges - vis_img = image.copy() - for i, j in edges: - cv2.line(vis_img, - tuple(corners_img[i]), - tuple(corners_img[j]), - (0, 255, 0), 2) - - # Add text with dimensions - dims = cuboid_params['dimensions'] - dim_text = f"Dims: {dims[0]:.3f} x {dims[1]:.3f} x {dims[2]:.3f}" - cv2.putText(vis_img, dim_text, (10, 30), - cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) - - return vis_img - -def plot_3d_fit(points, cuboid_params, title="3D Cuboid Fit"): - """Plot points and fitted cuboid in 3D.""" - fig = plt.figure(figsize=(10, 10)) - ax = fig.add_subplot(111, projection='3d') - - # Plot points - ax.scatter(points[:, 0], points[:, 1], points[:, 2], - c='b', marker='.', alpha=0.1, label='Points') - - # Plot fitted cuboid - corners = get_cuboid_corners( - cuboid_params['center'], - cuboid_params['dimensions'], - cuboid_params['rotation'] - ) - - # Define edges - edges = [ - # Bottom face - (0, 1), (1, 5), (5, 4), (4, 0), - # Top face - (2, 3), (3, 7), (7, 6), (6, 2), - # Vertical edges - (0, 2), (1, 3), (5, 7), (4, 6) - ] - - # Plot edges - for i, j in edges: - ax.plot3D([corners[i,0], corners[j,0]], - [corners[i,1], corners[j,1]], - [corners[i,2], corners[j,2]], 'r-') - - # Set labels and title - ax.set_xlabel('X') - ax.set_ylabel('Y') - ax.set_zlabel('Z') - ax.set_title(title) - - # Make scaling uniform - all_points = np.vstack([points, corners]) - max_range = np.array([ - all_points[:,0].max() - all_points[:,0].min(), - all_points[:,1].max() - all_points[:,1].min(), - all_points[:,2].max() - all_points[:,2].min() - ]).max() / 2.0 - - mid_x = (all_points[:,0].max() + all_points[:,0].min()) * 0.5 - mid_y = (all_points[:,1].max() + all_points[:,1].min()) * 0.5 - mid_z = (all_points[:,2].max() + all_points[:,2].min()) * 0.5 - - ax.set_xlim(mid_x - max_range, mid_x + max_range) - ax.set_ylim(mid_y - max_range, mid_y + max_range) - ax.set_zlim(mid_z - max_range, mid_z + max_range) - - ax.set_box_aspect([1,1,1]) - plt.legend() - return fig, ax diff --git a/dimos/perception/common/detection2d_tracker.py b/dimos/perception/common/detection2d_tracker.py index 62b465a4a9..7645acd380 100644 --- a/dimos/perception/common/detection2d_tracker.py +++ b/dimos/perception/common/detection2d_tracker.py @@ -1,5 +1,22 @@ -import numpy as np +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from collections import deque +from collections.abc import Sequence + +import numpy as np + def compute_iou(bbox1, bbox2): """ @@ -10,24 +27,25 @@ def compute_iou(bbox1, bbox2): y1 = max(bbox1[1], bbox2[1]) x2 = min(bbox1[2], bbox2[2]) y2 = min(bbox1[3], bbox2[3]) - + inter_area = max(0, x2 - x1) * max(0, y2 - y1) area1 = (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1]) area2 = (bbox2[2] - bbox2[0]) * (bbox2[3] - bbox2[1]) - + union_area = area1 + area2 - inter_area if union_area == 0: return 0 return inter_area / union_area + def get_tracked_results(tracked_targets): """ Extract tracked results from a list of target2d objects. - + Args: tracked_targets (list[target2d]): List of target2d objects (published targets) returned by the tracker's update() function. - + Returns: tuple: (tracked_masks, tracked_bboxes, tracked_track_ids, tracked_probs, tracked_names) where each is a list of the corresponding attribute from each target. @@ -37,7 +55,7 @@ def get_tracked_results(tracked_targets): tracked_track_ids = [] tracked_probs = [] tracked_names = [] - + for target in tracked_targets: # Extract the latest values stored in each target. tracked_masks.append(target.latest_mask) @@ -48,9 +66,8 @@ def get_tracked_results(tracked_targets): tracked_probs.append(target.score) # Use the stored name (if any). If not available, you can use a default value. tracked_names.append(target.name) - - return tracked_masks, tracked_bboxes, tracked_track_ids, tracked_probs, tracked_names + return tracked_masks, tracked_bboxes, tracked_track_ids, tracked_probs, tracked_names class target2d: @@ -59,7 +76,18 @@ class target2d: Stores the latest bounding box and mask along with a short history of track IDs, detection probabilities, and computed texture values. """ - def __init__(self, initial_mask, initial_bbox, track_id, prob, name, texture_value, target_id, history_size=10): + + def __init__( + self, + initial_mask, + initial_bbox, + track_id, + prob: float, + name: str, + texture_value, + target_id, + history_size: int = 10, + ) -> None: """ Args: initial_mask (torch.Tensor): Latest segmentation mask. @@ -76,66 +104,72 @@ def __init__(self, initial_mask, initial_bbox, track_id, prob, name, texture_val self.latest_bbox = initial_bbox self.name = name self.score = 1.0 - + self.track_id = track_id self.probs_history = deque(maxlen=history_size) self.texture_history = deque(maxlen=history_size) - - self.frame_count = deque(maxlen=history_size) # Total frames this target has been seen. - self.missed_frames = 0 # Consecutive frames when no detection was assigned. + + self.frame_count = deque(maxlen=history_size) # Total frames this target has been seen. + self.missed_frames = 0 # Consecutive frames when no detection was assigned. self.history_size = history_size - def update(self, mask, bbox, track_id, prob, name, texture_value): + def update(self, mask, bbox, track_id, prob: float, name: str, texture_value) -> None: """ Update the target with a new detection. """ self.latest_mask = mask self.latest_bbox = bbox self.name = name - + self.track_id = track_id self.probs_history.append(prob) self.texture_history.append(texture_value) - + self.frame_count.append(1) self.missed_frames = 0 - - def mark_missed(self): + + def mark_missed(self) -> None: """ Increment the count of consecutive frames where this target was not updated. """ self.missed_frames += 1 self.frame_count.append(0) - def compute_score(self, frame_shape, min_area_ratio, max_area_ratio, - texture_range=(0.0, 1.0), border_safe_distance=50, - weights=None): + def compute_score( + self, + frame_shape, + min_area_ratio, + max_area_ratio, + texture_range=(0.0, 1.0), + border_safe_distance: int = 50, + weights=None, + ): """ Compute a combined score for the target based on several factors. - + Factors: - **Detection probability:** Average over recent frames. - **Temporal stability:** How consistently the target has appeared. - **Texture quality:** Normalized using the provided min and max values. - **Border proximity:** Computed from the minimum distance from the bbox to the frame edges. - **Size:** How the object's area (relative to the frame) compares to acceptable bounds. - + Args: frame_shape (tuple): (height, width) of the frame. min_area_ratio (float): Minimum acceptable ratio (bbox area / frame area). max_area_ratio (float): Maximum acceptable ratio. texture_range (tuple): (min_texture, max_texture) expected values. border_safe_distance (float): Distance (in pixels) considered safe from the border. - weights (dict): Weights for each component. Expected keys: + weights (dict): Weights for each component. Expected keys: 'prob', 'temporal', 'texture', 'border', and 'size'. - + Returns: float: The combined (normalized) score in the range [0, 1]. """ # Default weights if none provided. if weights is None: weights = {"prob": 1.0, "temporal": 1.0, "texture": 1.0, "border": 1.0, "size": 1.0} - + h, w = frame_shape x1, y1, x2, y2 = self.latest_bbox bbox_area = (x2 - x1) * (y2 - y1) @@ -183,42 +217,51 @@ def compute_score(self, frame_shape, min_area_ratio, max_area_ratio, w_size = weights.get("size", 1.0) total_weight = w_prob + w_temporal + w_texture + w_border + w_size - #print(f"track_id: {self.target_id}, avg_prob: {avg_prob:.2f}, temporal_stability: {temporal_stability:.2f}, normalized_texture: {normalized_texture:.2f}, border_factor: {border_factor:.2f}, size_factor: {size_factor:.2f}") - - final_score = (w_prob * avg_prob + - w_temporal * temporal_stability + - w_texture * normalized_texture + - w_border * border_factor + - w_size * size_factor) / total_weight - + # print(f"track_id: {self.target_id}, avg_prob: {avg_prob:.2f}, temporal_stability: {temporal_stability:.2f}, normalized_texture: {normalized_texture:.2f}, border_factor: {border_factor:.2f}, size_factor: {size_factor:.2f}") + + final_score = ( + w_prob * avg_prob + + w_temporal * temporal_stability + + w_texture * normalized_texture + + w_border * border_factor + + w_size * size_factor + ) / total_weight + self.score = final_score - + return final_score + class target2dTracker: """ Tracker that maintains a history of targets across frames. New segmentation detections (frame, masks, bboxes, track_ids, probabilities, and computed texture values) are matched to existing targets or used to create new ones. - + The tracker uses a scoring system that incorporates: - **Detection probability** - **Temporal stability** - **Texture quality** (normalized within a specified range) - **Proximity to image borders** (a continuous penalty based on the distance) - **Object size** relative to the frame - + Targets are published if their score exceeds the start threshold and are removed if their score falls below the stop threshold or if they are missed for too many consecutive frames. """ - def __init__(self, history_size=10, - score_threshold_start=0.5, score_threshold_stop=0.3, - min_frame_count=10, - max_missed_frames=3, - min_area_ratio=0.001, max_area_ratio=0.1, - texture_range=(0.0, 1.0), - border_safe_distance=50, - weights=None): + + def __init__( + self, + history_size: int = 10, + score_threshold_start: float = 0.5, + score_threshold_stop: float = 0.3, + min_frame_count: int = 10, + max_missed_frames: int = 3, + min_area_ratio: float = 0.001, + max_area_ratio: float = 0.1, + texture_range=(0.0, 1.0), + border_safe_distance: int = 50, + weights=None, + ) -> None: """ Args: history_size (int): Maximum history length (number of frames) per target. @@ -246,14 +289,23 @@ def __init__(self, history_size=10, if weights is None: weights = {"prob": 1.0, "temporal": 1.0, "texture": 1.0, "border": 1.0, "size": 1.0} self.weights = weights - + self.targets = {} # Dictionary mapping target_id -> target2d instance. self.next_target_id = 0 - def update(self, frame, masks, bboxes, track_ids, probs, names, texture_values): + def update( + self, + frame, + masks, + bboxes, + track_ids, + probs: Sequence[float], + names: Sequence[str], + texture_values, + ): """ Update the tracker with new detections from the current frame. - + Args: frame (np.ndarray): Current BGR frame. masks (list[torch.Tensor]): List of segmentation masks. @@ -262,7 +314,7 @@ def update(self, frame, masks, bboxes, track_ids, probs, names, texture_values): probs (list): List of detection probabilities. names (list): List of class names. texture_values (list): List of computed texture values. - + Returns: published_targets (list[target2d]): Targets that are active and have scores above the start threshold. @@ -271,7 +323,9 @@ def update(self, frame, masks, bboxes, track_ids, probs, names, texture_values): frame_shape = frame.shape[:2] # (height, width) # For each detection, try to match with an existing target. - for mask, bbox, det_tid, prob, name, texture in zip(masks, bboxes, track_ids, probs, names, texture_values): + for mask, bbox, det_tid, prob, name, texture in zip( + masks, bboxes, track_ids, probs, names, texture_values, strict=False + ): matched_target = None # First, try matching by detection track ID if valid. @@ -295,7 +349,9 @@ def update(self, frame, masks, bboxes, track_ids, probs, names, texture_values): matched_target.update(mask, bbox, det_tid, prob, name, texture) updated_target_ids.add(matched_target.target_id) else: - new_target = target2d(mask, bbox, det_tid, prob, name, texture, self.next_target_id, self.history_size) + new_target = target2d( + mask, bbox, det_tid, prob, name, texture, self.next_target_id, self.history_size + ) self.targets[self.next_target_id] = new_target updated_target_ids.add(self.next_target_id) self.next_target_id += 1 @@ -308,23 +364,33 @@ def update(self, frame, masks, bboxes, track_ids, probs, names, texture_values): del self.targets[target_id] continue # Skip further checks for this target. # Remove targets whose score falls below the stop threshold. - score = target.compute_score(frame_shape, self.min_area_ratio, self.max_area_ratio, - texture_range=self.texture_range, - border_safe_distance=self.border_safe_distance, - weights=self.weights) + score = target.compute_score( + frame_shape, + self.min_area_ratio, + self.max_area_ratio, + texture_range=self.texture_range, + border_safe_distance=self.border_safe_distance, + weights=self.weights, + ) if score < self.score_threshold_stop: del self.targets[target_id] # Publish targets with scores above the start threshold. published_targets = [] for target in self.targets.values(): - score = target.compute_score(frame_shape, self.min_area_ratio, self.max_area_ratio, - texture_range=self.texture_range, - border_safe_distance=self.border_safe_distance, - weights=self.weights) - if score >= self.score_threshold_start and \ - sum(target.frame_count) >= self.min_frame_count and \ - target.missed_frames <= 5: + score = target.compute_score( + frame_shape, + self.min_area_ratio, + self.max_area_ratio, + texture_range=self.texture_range, + border_safe_distance=self.border_safe_distance, + weights=self.weights, + ) + if ( + score >= self.score_threshold_start + and sum(target.frame_count) >= self.min_frame_count + and target.missed_frames <= 5 + ): published_targets.append(target) return published_targets diff --git a/dimos/perception/common/export_tensorrt.py b/dimos/perception/common/export_tensorrt.py index cdc5663b5d..9d73b4ae3f 100644 --- a/dimos/perception/common/export_tensorrt.py +++ b/dimos/perception/common/export_tensorrt.py @@ -1,22 +1,51 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import argparse + from ultralytics import YOLO, FastSAM -from pathlib import Path + def parse_args(): - parser = argparse.ArgumentParser(description='Export YOLO/FastSAM models to different formats') - parser.add_argument('--model_path', type=str, required=True, help='Path to the model weights') - parser.add_argument('--model_type', type=str, choices=['yolo', 'fastsam'], required=True, help='Type of model to export') - parser.add_argument('--precision', type=str, choices=['fp32', 'fp16', 'int8'], default='fp32', help='Precision for export') - parser.add_argument('--format', type=str, choices=['onnx', 'engine'], default='onnx', help='Export format') + parser = argparse.ArgumentParser(description="Export YOLO/FastSAM models to different formats") + parser.add_argument("--model_path", type=str, required=True, help="Path to the model weights") + parser.add_argument( + "--model_type", + type=str, + choices=["yolo", "fastsam"], + required=True, + help="Type of model to export", + ) + parser.add_argument( + "--precision", + type=str, + choices=["fp32", "fp16", "int8"], + default="fp32", + help="Precision for export", + ) + parser.add_argument( + "--format", type=str, choices=["onnx", "engine"], default="onnx", help="Export format" + ) return parser.parse_args() -def main(): + +def main() -> None: args = parse_args() - half = args.precision == 'fp16' - int8 = args.precision == 'int8' + half = args.precision == "fp16" + int8 = args.precision == "int8" # Load the appropriate model - if args.model_type == 'yolo': - + if args.model_type == "yolo": model = YOLO(args.model_path) else: model = FastSAM(args.model_path) @@ -24,5 +53,6 @@ def main(): # Export the model model.export(format=args.format, half=half, int8=int8) -if __name__ == '__main__': - main() \ No newline at end of file + +if __name__ == "__main__": + main() diff --git a/dimos/perception/common/ibvs.py b/dimos/perception/common/ibvs.py index 040bd66799..2978aff84f 100644 --- a/dimos/perception/common/ibvs.py +++ b/dimos/perception/common/ibvs.py @@ -1,10 +1,25 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import numpy as np + class PersonDistanceEstimator: - def __init__(self, K, camera_pitch, camera_height): + def __init__(self, K, camera_pitch, camera_height) -> None: """ Initialize the distance estimator using ground plane constraint. - + Args: K: 3x3 Camera intrinsic matrix in OpenCV format (Assumed to be already for an undistorted image) @@ -14,106 +29,104 @@ def __init__(self, K, camera_pitch, camera_height): """ self.K = K self.camera_height = camera_height - + # Precompute the inverse intrinsic matrix self.K_inv = np.linalg.inv(K) - + # Transform from camera to robot frame (z-forward to x-forward) - self.T = np.array([[0, 0, 1], - [-1, 0, 0], - [0, -1, 0]]) - + self.T = np.array([[0, 0, 1], [-1, 0, 0], [0, -1, 0]]) + # Pitch rotation matrix (positive is upward) theta = -camera_pitch # Negative since positive pitch is negative rotation about robot Y - self.R_pitch = np.array([ - [ np.cos(theta), 0, np.sin(theta)], - [ 0, 1, 0], - [-np.sin(theta), 0, np.cos(theta)] - ]) - + self.R_pitch = np.array( + [[np.cos(theta), 0, np.sin(theta)], [0, 1, 0], [-np.sin(theta), 0, np.cos(theta)]] + ) + # Combined transform from camera to robot frame self.A = self.R_pitch @ self.T - + # Store focal length and principal point for angle calculation self.fx = K[0, 0] self.cx = K[0, 2] - def estimate_distance_angle(self, bbox: tuple, robot_pitch: float = None): + def estimate_distance_angle(self, bbox: tuple, robot_pitch: float | None = None): """ Estimate distance and angle to person using ground plane constraint. - + Args: bbox: tuple (x_min, y_min, x_max, y_max) where y_max represents the feet position robot_pitch: Current pitch of the robot body (in radians) If provided, this will be combined with the camera's fixed pitch - + Returns: depth: distance to person along camera's z-axis (meters) angle: horizontal angle in camera frame (radians, positive right) """ x_min, _, x_max, y_max = bbox - + # Get center point of feet u_c = (x_min + x_max) / 2.0 v_feet = y_max - + # Create homogeneous feet point and get ray direction p_feet = np.array([u_c, v_feet, 1.0]) d_feet_cam = self.K_inv @ p_feet - + # If robot_pitch is provided, recalculate the transformation matrix if robot_pitch is not None: # Combined pitch (fixed camera pitch + current robot pitch) total_pitch = -camera_pitch - robot_pitch # Both negated for correct rotation direction - R_total_pitch = np.array([ - [ np.cos(total_pitch), 0, np.sin(total_pitch)], - [ 0, 1, 0], - [-np.sin(total_pitch), 0, np.cos(total_pitch)] - ]) + R_total_pitch = np.array( + [ + [np.cos(total_pitch), 0, np.sin(total_pitch)], + [0, 1, 0], + [-np.sin(total_pitch), 0, np.cos(total_pitch)], + ] + ) # Use the updated transformation matrix A = R_total_pitch @ self.T else: # Use the precomputed transformation matrix A = self.A - + # Convert ray to robot frame using appropriate transformation d_feet_robot = A @ d_feet_cam - + # Ground plane intersection (z=0) # camera_height + t * d_feet_robot[2] = 0 if abs(d_feet_robot[2]) < 1e-6: raise ValueError("Feet ray is parallel to ground plane") - + # Solve for scaling factor t t = -self.camera_height / d_feet_robot[2] - + # Get 3D feet position in robot frame p_feet_robot = t * d_feet_robot - + # Convert back to camera frame p_feet_cam = self.A.T @ p_feet_robot - + # Extract depth (z-coordinate in camera frame) depth = p_feet_cam[2] - + # Calculate horizontal angle from image center angle = np.arctan((u_c - self.cx) / self.fx) - + return depth, angle class ObjectDistanceEstimator: - """ Estimate distance to an object using the ground plane constraint. This class assumes the camera is mounted on a robot and uses the camera's intrinsic parameters to estimate the distance to a detected object. """ - def __init__(self, K, camera_pitch, camera_height): + + def __init__(self, K, camera_pitch, camera_height) -> None: """ Initialize the distance estimator using ground plane constraint. - + Args: K: 3x3 Camera intrinsic matrix in OpenCV format (Assumed to be already for an undistorted image) @@ -123,26 +136,22 @@ def __init__(self, K, camera_pitch, camera_height): """ self.K = K self.camera_height = camera_height - + # Precompute the inverse intrinsic matrix self.K_inv = np.linalg.inv(K) - + # Transform from camera to robot frame (z-forward to x-forward) - self.T = np.array([[0, 0, 1], - [-1, 0, 0], - [0, -1, 0]]) - + self.T = np.array([[0, 0, 1], [-1, 0, 0], [0, -1, 0]]) + # Pitch rotation matrix (positive is upward) theta = -camera_pitch # Negative since positive pitch is negative rotation about robot Y - self.R_pitch = np.array([ - [ np.cos(theta), 0, np.sin(theta)], - [ 0, 1, 0], - [-np.sin(theta), 0, np.cos(theta)] - ]) - + self.R_pitch = np.array( + [[np.cos(theta), 0, np.sin(theta)], [0, 1, 0], [-np.sin(theta), 0, np.cos(theta)]] + ) + # Combined transform from camera to robot frame self.A = self.R_pitch @ self.T - + # Store focal length and principal point for angle calculation self.fx = K[0, 0] self.fy = K[1, 1] @@ -152,30 +161,30 @@ def __init__(self, K, camera_pitch, camera_height): def estimate_object_size(self, bbox: tuple, distance: float): """ Estimate the physical size of an object based on its bbox and known distance. - + Args: bbox: tuple (x_min, y_min, x_max, y_max) bounding box in the image distance: Known distance to the object (in meters) robot_pitch: Current pitch of the robot body (in radians), if any - + Returns: estimated_size: Estimated physical height of the object (in meters) """ - x_min, y_min, x_max, y_max = bbox - + _x_min, y_min, _x_max, y_max = bbox + # Calculate object height in pixels object_height_px = y_max - y_min - + # Calculate the physical height using the known distance and focal length estimated_size = object_height_px * distance / self.fy self.estimated_object_size = estimated_size - + return estimated_size - - def set_estimated_object_size(self, size: float): + + def set_estimated_object_size(self, size: float) -> None: """ Set the estimated object size for future distance calculations. - + Args: size: Estimated physical size of the object (in meters) """ @@ -184,7 +193,7 @@ def set_estimated_object_size(self, size: float): def estimate_distance_angle(self, bbox: tuple): """ Estimate distance and angle to object using size-based estimation. - + Args: bbox: tuple (x_min, y_min, x_max, y_max) where y_max represents the bottom of the object @@ -192,7 +201,7 @@ def estimate_distance_angle(self, bbox: tuple): If provided, this will be combined with the camera's fixed pitch initial_distance: Initial distance estimate for the object (in meters) Used to calibrate object size if not previously known - + Returns: depth: distance to object along camera's z-axis (meters) angle: horizontal angle in camera frame (radians, positive right) @@ -202,45 +211,43 @@ def estimate_distance_angle(self, bbox: tuple): # we can't estimate the distance if self.estimated_object_size is None: return None, None - + x_min, y_min, x_max, y_max = bbox - + # Calculate center of the object for angle calculation u_c = (x_min + x_max) / 2.0 - + # If we have an initial distance estimate and no object size yet, # calculate and store the object size using the initial distance object_height_px = y_max - y_min depth = self.estimated_object_size * self.fy / object_height_px - + # Calculate horizontal angle from image center angle = np.arctan((u_c - self.cx) / self.fx) - + return depth, angle # Example usage: if __name__ == "__main__": # Example camera calibration - K = np.array([[600, 0, 320], - [ 0, 600, 240], - [ 0, 0, 1]], dtype=np.float32) - + K = np.array([[600, 0, 320], [0, 600, 240], [0, 0, 1]], dtype=np.float32) + # Camera mounted 1.2m high, pitched down 10 degrees camera_pitch = np.deg2rad(0) # negative for downward pitch camera_height = 1.0 # meters - + estimator = PersonDistanceEstimator(K, camera_pitch, camera_height) object_estimator = ObjectDistanceEstimator(K, camera_pitch, camera_height) - + # Example detection bbox = (300, 100, 380, 400) # x1, y1, x2, y2 - + depth, angle = estimator.estimate_distance_angle(bbox) # Estimate object size based on the known distance object_size = object_estimator.estimate_object_size(bbox, depth) depth_obj, angle_obj = object_estimator.estimate_distance_angle(bbox) - + print(f"Estimated person depth: {depth:.2f} m") print(f"Estimated person angle: {np.rad2deg(angle):.1f}°") print(f"Estimated object depth: {depth_obj:.2f} m") @@ -252,15 +259,15 @@ def estimate_distance_angle(self, bbox: tuple): height = y_max - y_min center_x = (x_min + x_max) // 2 center_y = (y_min + y_max) // 2 - + new_width = max(width - 20, 2) # Ensure width is at least 2 pixels new_height = max(height - 20, 2) # Ensure height is at least 2 pixels - + x_min = center_x - new_width // 2 x_max = center_x + new_width // 2 y_min = center_y - new_height // 2 y_max = center_y + new_height // 2 - + bbox = (x_min, y_min, x_max, y_max) # Re-estimate distance and angle with the new bbox @@ -270,4 +277,4 @@ def estimate_distance_angle(self, bbox: tuple): print(f"New estimated person depth: {depth:.2f} m") print(f"New estimated person angle: {np.rad2deg(angle):.1f}°") print(f"New estimated object depth: {depth_obj:.2f} m") - print(f"New estimated object angle: {np.rad2deg(angle_obj):.1f}°") \ No newline at end of file + print(f"New estimated object angle: {np.rad2deg(angle_obj):.1f}°") diff --git a/dimos/perception/common/utils.py b/dimos/perception/common/utils.py new file mode 100644 index 0000000000..2676206bd7 --- /dev/null +++ b/dimos/perception/common/utils.py @@ -0,0 +1,949 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Union + +import cv2 +from dimos_lcm.sensor_msgs import CameraInfo +from dimos_lcm.vision_msgs import BoundingBox2D, Detection2D, Detection3D +import numpy as np +import torch +import yaml + +from dimos.msgs.geometry_msgs import Pose, Quaternion, Vector3 +from dimos.msgs.sensor_msgs import Image +from dimos.msgs.std_msgs import Header +from dimos.types.manipulation import ObjectData +from dimos.types.vector import Vector +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.perception.common.utils") + +# Optional CuPy support +try: # pragma: no cover - optional dependency + import cupy as cp # type: ignore + + _HAS_CUDA = True +except Exception: # pragma: no cover - optional dependency + cp = None # type: ignore + _HAS_CUDA = False + + +def _is_cu_array(x) -> bool: + return _HAS_CUDA and cp is not None and isinstance(x, cp.ndarray) # type: ignore + + +def _to_numpy(x): + return cp.asnumpy(x) if _is_cu_array(x) else x # type: ignore + + +def _to_cupy(x): + if _HAS_CUDA and cp is not None and isinstance(x, np.ndarray): # type: ignore + try: + return cp.asarray(x) # type: ignore + except Exception: + return x + return x + + +def load_camera_info(yaml_path: str, frame_id: str = "camera_link") -> CameraInfo: + """ + Load ROS-style camera_info YAML file and convert to CameraInfo LCM message. + + Args: + yaml_path: Path to camera_info YAML file (ROS format) + frame_id: Frame ID for the camera (default: "camera_link") + + Returns: + CameraInfo: LCM CameraInfo message with all calibration data + """ + with open(yaml_path) as f: + camera_info_data = yaml.safe_load(f) + + # Extract image dimensions + width = camera_info_data.get("image_width", 1280) + height = camera_info_data.get("image_height", 720) + + # Extract camera matrix (K) - already in row-major format + K = camera_info_data["camera_matrix"]["data"] + + # Extract distortion coefficients + D = camera_info_data["distortion_coefficients"]["data"] + + # Extract rectification matrix (R) if available, else use identity + R = camera_info_data.get("rectification_matrix", {}).get("data", [1, 0, 0, 0, 1, 0, 0, 0, 1]) + + # Extract projection matrix (P) if available + P = camera_info_data.get("projection_matrix", {}).get("data", None) + + # If P not provided, construct from K + if P is None: + fx = K[0] + fy = K[4] + cx = K[2] + cy = K[5] + P = [fx, 0, cx, 0, 0, fy, cy, 0, 0, 0, 1, 0] + + # Create header + header = Header(frame_id) + + # Create and return CameraInfo message + return CameraInfo( + D_length=len(D), + header=header, + height=height, + width=width, + distortion_model=camera_info_data.get("distortion_model", "plumb_bob"), + D=D, + K=K, + R=R, + P=P, + binning_x=0, + binning_y=0, + ) + + +def load_camera_info_opencv(yaml_path: str) -> tuple[np.ndarray, np.ndarray]: + """ + Load ROS-style camera_info YAML file and convert to OpenCV camera matrix and distortion coefficients. + + Args: + yaml_path: Path to camera_info YAML file (ROS format) + + Returns: + K: 3x3 camera intrinsic matrix + dist: 1xN distortion coefficients array (for plumb_bob model) + """ + with open(yaml_path) as f: + camera_info = yaml.safe_load(f) + + # Extract camera matrix (K) + camera_matrix_data = camera_info["camera_matrix"]["data"] + K = np.array(camera_matrix_data).reshape(3, 3) + + # Extract distortion coefficients + dist_coeffs_data = camera_info["distortion_coefficients"]["data"] + dist = np.array(dist_coeffs_data) + + # Ensure dist is 1D array for OpenCV compatibility + if dist.ndim == 2: + dist = dist.flatten() + + return K, dist + + +def rectify_image_cpu(image: Image, camera_matrix: np.ndarray, dist_coeffs: np.ndarray) -> Image: + """CPU rectification using OpenCV. Preserves backend by caller. + + Returns an Image with numpy or cupy data depending on caller choice. + """ + src = _to_numpy(image.data) + rect = cv2.undistort(src, camera_matrix, dist_coeffs) + # Caller decides whether to convert back to GPU. + return Image(data=rect, format=image.format, frame_id=image.frame_id, ts=image.ts) + + +def rectify_image_cuda(image: Image, camera_matrix: np.ndarray, dist_coeffs: np.ndarray) -> Image: + """GPU rectification using CuPy bilinear sampling. + + Generates an undistorted output grid and samples from the distorted source. + Falls back to CPU if CUDA not available. + """ + if not _HAS_CUDA or cp is None or not image.is_cuda: # type: ignore + return rectify_image_cpu(image, camera_matrix, dist_coeffs) + + xp = cp # type: ignore + + # Source (distorted) image on device + src = image.data + if src.ndim not in (2, 3): + raise ValueError("Unsupported image rank for rectification") + H, W = int(src.shape[0]), int(src.shape[1]) + + # Extract intrinsics and distortion as float64 + K = xp.asarray(camera_matrix, dtype=xp.float64) + dist = xp.asarray(dist_coeffs, dtype=xp.float64).reshape(-1) + fx, fy, cx, cy = K[0, 0], K[1, 1], K[0, 2], K[1, 2] + k1 = dist[0] if dist.size > 0 else 0.0 + k2 = dist[1] if dist.size > 1 else 0.0 + p1 = dist[2] if dist.size > 2 else 0.0 + p2 = dist[3] if dist.size > 3 else 0.0 + k3 = dist[4] if dist.size > 4 else 0.0 + + # Build undistorted target grid (pixel coords) + u = xp.arange(W, dtype=xp.float64) + v = xp.arange(H, dtype=xp.float64) + uu, vv = xp.meshgrid(u, v, indexing="xy") + + # Convert to normalized undistorted coords + xu = (uu - cx) / fx + yu = (vv - cy) / fy + + # Apply forward distortion model to get distorted normalized coords + r2 = xu * xu + yu * yu + r4 = r2 * r2 + r6 = r4 * r2 + radial = 1.0 + k1 * r2 + k2 * r4 + k3 * r6 + delta_x = 2.0 * p1 * xu * yu + p2 * (r2 + 2.0 * xu * xu) + delta_y = p1 * (r2 + 2.0 * yu * yu) + 2.0 * p2 * xu * yu + xd = xu * radial + delta_x + yd = yu * radial + delta_y + + # Back to pixel coordinates in the source (distorted) image + us = fx * xd + cx + vs = fy * yd + cy + + # Bilinear sample from src at (vs, us) + def _bilinear_sample_cuda(img, x_src, y_src): + h, w = int(img.shape[0]), int(img.shape[1]) + # Base integer corners (not clamped) + x0i = xp.floor(x_src).astype(xp.int32) + y0i = xp.floor(y_src).astype(xp.int32) + x1i = x0i + 1 + y1i = y0i + 1 + + # Masks for in-bounds neighbors (BORDER_CONSTANT behavior) + m00 = (x0i >= 0) & (x0i < w) & (y0i >= 0) & (y0i < h) + m10 = (x1i >= 0) & (x1i < w) & (y0i >= 0) & (y0i < h) + m01 = (x0i >= 0) & (x0i < w) & (y1i >= 0) & (y1i < h) + m11 = (x1i >= 0) & (x1i < w) & (y1i >= 0) & (y1i < h) + + # Clamp indices for safe gather, but multiply contributions by masks + x0 = xp.clip(x0i, 0, w - 1) + y0 = xp.clip(y0i, 0, h - 1) + x1 = xp.clip(x1i, 0, w - 1) + y1 = xp.clip(y1i, 0, h - 1) + + # Weights + wx = (x_src - x0i).astype(xp.float64) + wy = (y_src - y0i).astype(xp.float64) + w00 = (1.0 - wx) * (1.0 - wy) + w10 = wx * (1.0 - wy) + w01 = (1.0 - wx) * wy + w11 = wx * wy + + # Cast masks for arithmetic + m00f = m00.astype(xp.float64) + m10f = m10.astype(xp.float64) + m01f = m01.astype(xp.float64) + m11f = m11.astype(xp.float64) + + if img.ndim == 2: + Ia = img[y0, x0].astype(xp.float64) + Ib = img[y0, x1].astype(xp.float64) + Ic = img[y1, x0].astype(xp.float64) + Id = img[y1, x1].astype(xp.float64) + out = w00 * m00f * Ia + w10 * m10f * Ib + w01 * m01f * Ic + w11 * m11f * Id + else: + Ia = img[y0, x0].astype(xp.float64) + Ib = img[y0, x1].astype(xp.float64) + Ic = img[y1, x0].astype(xp.float64) + Id = img[y1, x1].astype(xp.float64) + # Expand weights and masks for channel broadcasting + w00e = (w00 * m00f)[..., None] + w10e = (w10 * m10f)[..., None] + w01e = (w01 * m01f)[..., None] + w11e = (w11 * m11f)[..., None] + out = w00e * Ia + w10e * Ib + w01e * Ic + w11e * Id + + # Cast back to original dtype with clipping for integers + if img.dtype == xp.uint8: + out = xp.clip(xp.rint(out), 0, 255).astype(xp.uint8) + elif img.dtype == xp.uint16: + out = xp.clip(xp.rint(out), 0, 65535).astype(xp.uint16) + elif img.dtype == xp.int16: + out = xp.clip(xp.rint(out), -32768, 32767).astype(xp.int16) + else: + out = out.astype(img.dtype, copy=False) + return out + + rect = _bilinear_sample_cuda(src, us, vs) + return Image(data=rect, format=image.format, frame_id=image.frame_id, ts=image.ts) + + +def rectify_image(image: Image, camera_matrix: np.ndarray, dist_coeffs: np.ndarray) -> Image: + """ + Rectify (undistort) an image using camera calibration parameters. + + Args: + image: Input Image object to rectify + camera_matrix: 3x3 camera intrinsic matrix (K) + dist_coeffs: Distortion coefficients array + + Returns: + Image: Rectified Image object with same format and metadata + """ + if image.is_cuda and _HAS_CUDA: + return rectify_image_cuda(image, camera_matrix, dist_coeffs) + return rectify_image_cpu(image, camera_matrix, dist_coeffs) + + +def project_3d_points_to_2d_cuda( + points_3d: "cp.ndarray", camera_intrinsics: Union[list[float], "cp.ndarray"] +) -> "cp.ndarray": + xp = cp # type: ignore + pts = points_3d.astype(xp.float64, copy=False) + mask = pts[:, 2] > 0 + if not bool(xp.any(mask)): + return xp.zeros((0, 2), dtype=xp.int32) + valid = pts[mask] + if isinstance(camera_intrinsics, list) and len(camera_intrinsics) == 4: + fx, fy, cx, cy = [xp.asarray(v, dtype=xp.float64) for v in camera_intrinsics] + else: + K = camera_intrinsics.astype(xp.float64, copy=False) + fx, fy, cx, cy = K[0, 0], K[1, 1], K[0, 2], K[1, 2] + u = (valid[:, 0] * fx / valid[:, 2]) + cx + v = (valid[:, 1] * fy / valid[:, 2]) + cy + return xp.stack([u, v], axis=1).astype(xp.int32) + + +def project_3d_points_to_2d_cpu( + points_3d: np.ndarray, camera_intrinsics: list[float] | np.ndarray +) -> np.ndarray: + pts = np.asarray(points_3d, dtype=np.float64) + valid_mask = pts[:, 2] > 0 + if not np.any(valid_mask): + return np.zeros((0, 2), dtype=np.int32) + valid_points = pts[valid_mask] + if isinstance(camera_intrinsics, list) and len(camera_intrinsics) == 4: + fx, fy, cx, cy = [float(v) for v in camera_intrinsics] + else: + K = np.array(camera_intrinsics, dtype=np.float64) + fx, fy, cx, cy = K[0, 0], K[1, 1], K[0, 2], K[1, 2] + u = (valid_points[:, 0] * fx / valid_points[:, 2]) + cx + v = (valid_points[:, 1] * fy / valid_points[:, 2]) + cy + return np.column_stack([u, v]).astype(np.int32) + + +def project_3d_points_to_2d( + points_3d: Union[np.ndarray, "cp.ndarray"], + camera_intrinsics: Union[list[float], np.ndarray, "cp.ndarray"], +) -> Union[np.ndarray, "cp.ndarray"]: + """ + Project 3D points to 2D image coordinates using camera intrinsics. + + Args: + points_3d: Nx3 array of 3D points (X, Y, Z) + camera_intrinsics: Camera parameters as [fx, fy, cx, cy] list or 3x3 matrix + + Returns: + Nx2 array of 2D image coordinates (u, v) + """ + if len(points_3d) == 0: + return ( + cp.zeros((0, 2), dtype=cp.int32) + if _is_cu_array(points_3d) + else np.zeros((0, 2), dtype=np.int32) + ) + + # Filter out points with zero or negative depth + if _is_cu_array(points_3d) or _is_cu_array(camera_intrinsics): + xp = cp # type: ignore + pts = points_3d if _is_cu_array(points_3d) else xp.asarray(points_3d) + K = camera_intrinsics if _is_cu_array(camera_intrinsics) else camera_intrinsics + return project_3d_points_to_2d_cuda(pts, K) # type: ignore[arg-type] + return project_3d_points_to_2d_cpu(np.asarray(points_3d), np.asarray(camera_intrinsics)) + + +def project_2d_points_to_3d_cuda( + points_2d: "cp.ndarray", + depth_values: "cp.ndarray", + camera_intrinsics: Union[list[float], "cp.ndarray"], +) -> "cp.ndarray": + xp = cp # type: ignore + pts = points_2d.astype(xp.float64, copy=False) + depths = depth_values.astype(xp.float64, copy=False) + valid = depths > 0 + if not bool(xp.any(valid)): + return xp.zeros((0, 3), dtype=xp.float32) + uv = pts[valid] + Z = depths[valid] + if isinstance(camera_intrinsics, list) and len(camera_intrinsics) == 4: + fx, fy, cx, cy = [xp.asarray(v, dtype=xp.float64) for v in camera_intrinsics] + else: + K = camera_intrinsics.astype(xp.float64, copy=False) + fx, fy, cx, cy = K[0, 0], K[1, 1], K[0, 2], K[1, 2] + X = (uv[:, 0] - cx) * Z / fx + Y = (uv[:, 1] - cy) * Z / fy + return xp.stack([X, Y, Z], axis=1).astype(xp.float32) + + +def project_2d_points_to_3d_cpu( + points_2d: np.ndarray, + depth_values: np.ndarray, + camera_intrinsics: list[float] | np.ndarray, +) -> np.ndarray: + pts = np.asarray(points_2d, dtype=np.float64) + depths = np.asarray(depth_values, dtype=np.float64) + valid_mask = depths > 0 + if not np.any(valid_mask): + return np.zeros((0, 3), dtype=np.float32) + valid_points_2d = pts[valid_mask] + valid_depths = depths[valid_mask] + if isinstance(camera_intrinsics, list) and len(camera_intrinsics) == 4: + fx, fy, cx, cy = [float(v) for v in camera_intrinsics] + else: + camera_matrix = np.array(camera_intrinsics, dtype=np.float64) + fx = camera_matrix[0, 0] + fy = camera_matrix[1, 1] + cx = camera_matrix[0, 2] + cy = camera_matrix[1, 2] + X = (valid_points_2d[:, 0] - cx) * valid_depths / fx + Y = (valid_points_2d[:, 1] - cy) * valid_depths / fy + Z = valid_depths + return np.column_stack([X, Y, Z]).astype(np.float32) + + +def project_2d_points_to_3d( + points_2d: Union[np.ndarray, "cp.ndarray"], + depth_values: Union[np.ndarray, "cp.ndarray"], + camera_intrinsics: Union[list[float], np.ndarray, "cp.ndarray"], +) -> Union[np.ndarray, "cp.ndarray"]: + """ + Project 2D image points to 3D coordinates using depth values and camera intrinsics. + + Args: + points_2d: Nx2 array of 2D image coordinates (u, v) + depth_values: N-length array of depth values (Z coordinates) for each point + camera_intrinsics: Camera parameters as [fx, fy, cx, cy] list or 3x3 matrix + + Returns: + Nx3 array of 3D points (X, Y, Z) + """ + if len(points_2d) == 0: + return ( + cp.zeros((0, 3), dtype=cp.float32) + if _is_cu_array(points_2d) + else np.zeros((0, 3), dtype=np.float32) + ) + + # Ensure depth_values is a numpy array + if _is_cu_array(points_2d) or _is_cu_array(depth_values) or _is_cu_array(camera_intrinsics): + xp = cp # type: ignore + pts = points_2d if _is_cu_array(points_2d) else xp.asarray(points_2d) + depths = depth_values if _is_cu_array(depth_values) else xp.asarray(depth_values) + K = camera_intrinsics if _is_cu_array(camera_intrinsics) else camera_intrinsics + return project_2d_points_to_3d_cuda(pts, depths, K) # type: ignore[arg-type] + return project_2d_points_to_3d_cpu( + np.asarray(points_2d), np.asarray(depth_values), np.asarray(camera_intrinsics) + ) + + +def colorize_depth( + depth_img: Union[np.ndarray, "cp.ndarray"], max_depth: float = 5.0, overlay_stats: bool = True +) -> Union[np.ndarray, "cp.ndarray"] | None: + """ + Normalize and colorize depth image using COLORMAP_JET with optional statistics overlay. + + Args: + depth_img: Depth image (H, W) in meters + max_depth: Maximum depth value for normalization + overlay_stats: Whether to overlay depth statistics on the image + + Returns: + Colorized depth image (H, W, 3) in RGB format, or None if input is None + """ + if depth_img is None: + return None + + was_cu = _is_cu_array(depth_img) + xp = cp if was_cu else np # type: ignore + depth = depth_img if was_cu else np.asarray(depth_img) + + valid_mask = xp.isfinite(depth) & (depth > 0) + depth_norm = xp.zeros_like(depth, dtype=xp.float32) + if bool(valid_mask.any() if not was_cu else xp.any(valid_mask)): + depth_norm = xp.where(valid_mask, xp.clip(depth / max_depth, 0, 1), depth_norm) + + # Use CPU for colormap/text; convert back to GPU if needed + depth_norm_np = _to_numpy(depth_norm) + depth_colored = cv2.applyColorMap((depth_norm_np * 255).astype(np.uint8), cv2.COLORMAP_JET) + depth_rgb_np = cv2.cvtColor(depth_colored, cv2.COLOR_BGR2RGB) + depth_rgb_np = (depth_rgb_np * 0.6).astype(np.uint8) + + if overlay_stats and (np.any(_to_numpy(valid_mask))): + valid_depths = _to_numpy(depth)[_to_numpy(valid_mask)] + min_depth = float(np.min(valid_depths)) + max_depth_actual = float(np.max(valid_depths)) + h, w = depth_rgb_np.shape[:2] + center_y, center_x = h // 2, w // 2 + center_region = _to_numpy(depth)[ + max(0, center_y - 2) : min(h, center_y + 3), max(0, center_x - 2) : min(w, center_x + 3) + ] + center_mask = np.isfinite(center_region) & (center_region > 0) + if center_mask.any(): + center_depth = float(np.median(center_region[center_mask])) + else: + depth_np = _to_numpy(depth) + vm_np = _to_numpy(valid_mask) + center_depth = float(depth_np[center_y, center_x]) if vm_np[center_y, center_x] else 0.0 + + font = cv2.FONT_HERSHEY_SIMPLEX + font_scale = 0.6 + thickness = 1 + line_type = cv2.LINE_AA + text_color = (255, 255, 255) + bg_color = (0, 0, 0) + padding = 5 + + min_text = f"Min: {min_depth:.2f}m" + (text_w, text_h), _ = cv2.getTextSize(min_text, font, font_scale, thickness) + cv2.rectangle( + depth_rgb_np, + (padding, padding), + (padding + text_w + 4, padding + text_h + 6), + bg_color, + -1, + ) + cv2.putText( + depth_rgb_np, + min_text, + (padding + 2, padding + text_h + 2), + font, + font_scale, + text_color, + thickness, + line_type, + ) + + max_text = f"Max: {max_depth_actual:.2f}m" + (text_w, text_h), _ = cv2.getTextSize(max_text, font, font_scale, thickness) + cv2.rectangle( + depth_rgb_np, + (w - padding - text_w - 4, padding), + (w - padding, padding + text_h + 6), + bg_color, + -1, + ) + cv2.putText( + depth_rgb_np, + max_text, + (w - padding - text_w - 2, padding + text_h + 2), + font, + font_scale, + text_color, + thickness, + line_type, + ) + + if center_depth > 0: + center_text = f"{center_depth:.2f}m" + (text_w, text_h), _ = cv2.getTextSize(center_text, font, font_scale, thickness) + center_text_x = center_x - text_w // 2 + center_text_y = center_y + text_h // 2 + cross_size = 10 + cross_color = (255, 255, 255) + cv2.line( + depth_rgb_np, + (center_x - cross_size, center_y), + (center_x + cross_size, center_y), + cross_color, + 1, + ) + cv2.line( + depth_rgb_np, + (center_x, center_y - cross_size), + (center_x, center_y + cross_size), + cross_color, + 1, + ) + cv2.rectangle( + depth_rgb_np, + (center_text_x - 2, center_text_y - text_h - 2), + (center_text_x + text_w + 2, center_text_y + 2), + bg_color, + -1, + ) + cv2.putText( + depth_rgb_np, + center_text, + (center_text_x, center_text_y), + font, + font_scale, + text_color, + thickness, + line_type, + ) + + return _to_cupy(depth_rgb_np) if was_cu else depth_rgb_np + + +def draw_bounding_box( + image: Union[np.ndarray, "cp.ndarray"], + bbox: list[float], + color: tuple[int, int, int] = (0, 255, 0), + thickness: int = 2, + label: str | None = None, + confidence: float | None = None, + object_id: int | None = None, + font_scale: float = 0.6, +) -> Union[np.ndarray, "cp.ndarray"]: + """ + Draw a bounding box with optional label on an image. + + Args: + image: Image to draw on (H, W, 3) + bbox: Bounding box [x1, y1, x2, y2] + color: RGB color tuple for the box + thickness: Line thickness for the box + label: Optional class label + confidence: Optional confidence score + object_id: Optional object ID + font_scale: Font scale for text + + Returns: + Image with bounding box drawn + """ + was_cu = _is_cu_array(image) + img_np = _to_numpy(image) + x1, y1, x2, y2 = map(int, bbox) + cv2.rectangle(img_np, (x1, y1), (x2, y2), color, thickness) + + # Create label text + text_parts = [] + if label is not None: + text_parts.append(str(label)) + if object_id is not None: + text_parts.append(f"ID: {object_id}") + if confidence is not None: + text_parts.append(f"({confidence:.2f})") + + if text_parts: + text = ", ".join(text_parts) + + # Draw text background + text_size = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, font_scale, 1)[0] + cv2.rectangle( + img_np, + (x1, y1 - text_size[1] - 5), + (x1 + text_size[0], y1), + (0, 0, 0), + -1, + ) + + # Draw text + cv2.putText( + img_np, + text, + (x1, y1 - 5), + cv2.FONT_HERSHEY_SIMPLEX, + font_scale, + (255, 255, 255), + 1, + ) + + return _to_cupy(img_np) if was_cu else img_np + + +def draw_segmentation_mask( + image: Union[np.ndarray, "cp.ndarray"], + mask: Union[np.ndarray, "cp.ndarray"], + color: tuple[int, int, int] = (0, 200, 200), + alpha: float = 0.5, + draw_contours: bool = True, + contour_thickness: int = 2, +) -> Union[np.ndarray, "cp.ndarray"]: + """ + Draw segmentation mask overlay on an image. + + Args: + image: Image to draw on (H, W, 3) + mask: Segmentation mask (H, W) - boolean or uint8 + color: RGB color for the mask + alpha: Transparency factor (0.0 = transparent, 1.0 = opaque) + draw_contours: Whether to draw mask contours + contour_thickness: Thickness of contour lines + + Returns: + Image with mask overlay drawn + """ + if mask is None: + return image + + was_cu = _is_cu_array(image) + img_np = _to_numpy(image) + mask_np = _to_numpy(mask) + + try: + mask_np = mask_np.astype(np.uint8) + colored_mask = np.zeros_like(img_np) + colored_mask[mask_np > 0] = color + mask_area = mask_np > 0 + img_np[mask_area] = cv2.addWeighted( + img_np[mask_area], 1 - alpha, colored_mask[mask_area], alpha, 0 + ) + if draw_contours: + contours, _ = cv2.findContours(mask_np, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + cv2.drawContours(img_np, contours, -1, color, contour_thickness) + except Exception as e: + logger.warning(f"Error drawing segmentation mask: {e}") + + return _to_cupy(img_np) if was_cu else img_np + + +def draw_object_detection_visualization( + image: Union[np.ndarray, "cp.ndarray"], + objects: list[ObjectData], + draw_masks: bool = False, + bbox_color: tuple[int, int, int] = (0, 255, 0), + mask_color: tuple[int, int, int] = (0, 200, 200), + font_scale: float = 0.6, +) -> Union[np.ndarray, "cp.ndarray"]: + """ + Create object detection visualization with bounding boxes and optional masks. + + Args: + image: Base image to draw on (H, W, 3) + objects: List of ObjectData with detection information + draw_masks: Whether to draw segmentation masks + bbox_color: Default color for bounding boxes + mask_color: Default color for segmentation masks + font_scale: Font scale for text labels + + Returns: + Image with detection visualization + """ + was_cu = _is_cu_array(image) + viz_image = _to_numpy(image).copy() + + for obj in objects: + try: + # Draw segmentation mask first (if enabled and available) + if draw_masks and "segmentation_mask" in obj and obj["segmentation_mask"] is not None: + viz_image = draw_segmentation_mask( + viz_image, obj["segmentation_mask"], color=mask_color, alpha=0.5 + ) + + # Draw bounding box + if "bbox" in obj and obj["bbox"] is not None: + # Use object's color if available, otherwise default + color = bbox_color + if "color" in obj and obj["color"] is not None: + obj_color = obj["color"] + if isinstance(obj_color, np.ndarray): + color = tuple(int(c) for c in obj_color) + elif isinstance(obj_color, list | tuple): + color = tuple(int(c) for c in obj_color[:3]) + + viz_image = draw_bounding_box( + viz_image, + obj["bbox"], + color=color, + label=obj.get("label"), + confidence=obj.get("confidence"), + object_id=obj.get("object_id"), + font_scale=font_scale, + ) + + except Exception as e: + logger.warning(f"Error drawing object visualization: {e}") + + return _to_cupy(viz_image) if was_cu else viz_image + + +def detection_results_to_object_data( + bboxes: list[list[float]], + track_ids: list[int], + class_ids: list[int], + confidences: list[float], + names: list[str], + masks: list[np.ndarray] | None = None, + source: str = "detection", +) -> list[ObjectData]: + """ + Convert detection/segmentation results to ObjectData format. + + Args: + bboxes: List of bounding boxes [x1, y1, x2, y2] + track_ids: List of tracking IDs + class_ids: List of class indices + confidences: List of detection confidences + names: List of class names + masks: Optional list of segmentation masks + source: Source type ("detection" or "segmentation") + + Returns: + List of ObjectData dictionaries + """ + objects = [] + + for i in range(len(bboxes)): + # Calculate basic properties from bbox + bbox = bboxes[i] + width = bbox[2] - bbox[0] + height = bbox[3] - bbox[1] + bbox[0] + width / 2 + bbox[1] + height / 2 + + # Create ObjectData + object_data: ObjectData = { + "object_id": track_ids[i] if i < len(track_ids) else i, + "bbox": bbox, + "depth": -1.0, # Will be populated by depth estimation or point cloud processing + "confidence": confidences[i] if i < len(confidences) else 1.0, + "class_id": class_ids[i] if i < len(class_ids) else 0, + "label": names[i] if i < len(names) else f"{source}_object", + "movement_tolerance": 1.0, # Default to freely movable + "segmentation_mask": masks[i].cpu().numpy() + if masks and i < len(masks) and isinstance(masks[i], torch.Tensor) + else masks[i] + if masks and i < len(masks) + else None, + # Initialize 3D properties (will be populated by point cloud processing) + "position": Vector(0, 0, 0), + "rotation": Vector(0, 0, 0), + "size": { + "width": 0.0, + "height": 0.0, + "depth": 0.0, + }, + } + objects.append(object_data) + + return objects + + +def combine_object_data( + list1: list[ObjectData], list2: list[ObjectData], overlap_threshold: float = 0.8 +) -> list[ObjectData]: + """ + Combine two ObjectData lists, removing duplicates based on segmentation mask overlap. + """ + combined = list1.copy() + used_ids = set(obj.get("object_id", 0) for obj in list1) + next_id = max(used_ids) + 1 if used_ids else 1 + + for obj2 in list2: + obj_copy = obj2.copy() + + # Handle duplicate object_id + if obj_copy.get("object_id", 0) in used_ids: + obj_copy["object_id"] = next_id + next_id += 1 + used_ids.add(obj_copy["object_id"]) + + # Check mask overlap + mask2 = obj2.get("segmentation_mask") + m2 = _to_numpy(mask2) if mask2 is not None else None + if m2 is None or np.sum(m2 > 0) == 0: + combined.append(obj_copy) + continue + + mask2_area = np.sum(m2 > 0) + is_duplicate = False + + for obj1 in list1: + mask1 = obj1.get("segmentation_mask") + if mask1 is None: + continue + + m1 = _to_numpy(mask1) + intersection = np.sum((m1 > 0) & (m2 > 0)) + if intersection / mask2_area >= overlap_threshold: + is_duplicate = True + break + + if not is_duplicate: + combined.append(obj_copy) + + return combined + + +def point_in_bbox(point: tuple[int, int], bbox: list[float]) -> bool: + """ + Check if a point is inside a bounding box. + + Args: + point: (x, y) coordinates + bbox: Bounding box [x1, y1, x2, y2] + + Returns: + True if point is inside bbox + """ + x, y = point + x1, y1, x2, y2 = bbox + return x1 <= x <= x2 and y1 <= y <= y2 + + +def bbox2d_to_corners(bbox_2d: BoundingBox2D) -> tuple[float, float, float, float]: + """ + Convert BoundingBox2D from center format to corner format. + + Args: + bbox_2d: BoundingBox2D with center and size + + Returns: + Tuple of (x1, y1, x2, y2) corner coordinates + """ + center_x = bbox_2d.center.position.x + center_y = bbox_2d.center.position.y + half_width = bbox_2d.size_x / 2.0 + half_height = bbox_2d.size_y / 2.0 + + x1 = center_x - half_width + y1 = center_y - half_height + x2 = center_x + half_width + y2 = center_y + half_height + + return x1, y1, x2, y2 + + +def find_clicked_detection( + click_pos: tuple[int, int], detections_2d: list[Detection2D], detections_3d: list[Detection3D] +) -> Detection3D | None: + """ + Find which detection was clicked based on 2D bounding boxes. + + Args: + click_pos: (x, y) click position + detections_2d: List of Detection2D objects + detections_3d: List of Detection3D objects (must be 1:1 correspondence) + + Returns: + Corresponding Detection3D object if found, None otherwise + """ + click_x, click_y = click_pos + + for i, det_2d in enumerate(detections_2d): + if det_2d.bbox and i < len(detections_3d): + x1, y1, x2, y2 = bbox2d_to_corners(det_2d.bbox) + + if x1 <= click_x <= x2 and y1 <= click_y <= y2: + return detections_3d[i] + + return None + + +def extract_pose_from_detection3d(detection3d: Detection3D): + """Extract PoseStamped from Detection3D message. + + Args: + detection3d: Detection3D message + + Returns: + Pose or None if no valid detection + """ + if not detection3d or not detection3d.bbox or not detection3d.bbox.center: + return None + + # Extract position + pos = detection3d.bbox.center.position + position = Vector3(pos.x, pos.y, pos.z) + + # Extract orientation + orient = detection3d.bbox.center.orientation + orientation = Quaternion(orient.x, orient.y, orient.z, orient.w) + + pose = Pose(position=position, orientation=orientation) + return pose diff --git a/dimos/perception/detection/__init__.py b/dimos/perception/detection/__init__.py new file mode 100644 index 0000000000..72663a69b0 --- /dev/null +++ b/dimos/perception/detection/__init__.py @@ -0,0 +1,7 @@ +from dimos.perception.detection.detectors import * +from dimos.perception.detection.module2D import ( + Detection2DModule, +) +from dimos.perception.detection.module3D import ( + Detection3DModule, +) diff --git a/dimos/perception/detection/conftest.py b/dimos/perception/detection/conftest.py new file mode 100644 index 0000000000..dcc20e5b25 --- /dev/null +++ b/dimos/perception/detection/conftest.py @@ -0,0 +1,311 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Callable, Generator +import functools +from typing import TypedDict + +from dimos_lcm.foxglove_msgs.ImageAnnotations import ImageAnnotations +from dimos_lcm.foxglove_msgs.SceneUpdate import SceneUpdate +from dimos_lcm.visualization_msgs.MarkerArray import MarkerArray +import pytest + +from dimos.core import LCMTransport +from dimos.msgs.geometry_msgs import Transform +from dimos.msgs.sensor_msgs import CameraInfo, Image, PointCloud2 +from dimos.msgs.vision_msgs import Detection2DArray +from dimos.perception.detection.module2D import Detection2DModule +from dimos.perception.detection.module3D import Detection3DModule +from dimos.perception.detection.moduleDB import ObjectDBModule +from dimos.perception.detection.type import ( + Detection2D, + Detection3DPC, + ImageDetections2D, + ImageDetections3DPC, +) +from dimos.protocol.tf import TF +from dimos.robot.unitree_webrtc.modular.connection_module import ConnectionModule +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.robot.unitree_webrtc.type.odometry import Odometry +from dimos.utils.data import get_data +from dimos.utils.testing import TimedSensorReplay + + +class Moment(TypedDict, total=False): + odom_frame: Odometry + lidar_frame: LidarMessage + image_frame: Image + camera_info: CameraInfo + transforms: list[Transform] + tf: TF + annotations: ImageAnnotations | None + detections: ImageDetections3DPC | None + markers: MarkerArray | None + scene_update: SceneUpdate | None + + +class Moment2D(Moment): + detections2d: ImageDetections2D + + +class Moment3D(Moment): + detections3dpc: ImageDetections3DPC + + +@pytest.fixture(scope="session") +def tf(): + t = TF() + yield t + t.stop() + + +@pytest.fixture(scope="session") +def get_moment(tf): + @functools.lru_cache(maxsize=1) + def moment_provider(**kwargs) -> Moment: + print("MOMENT PROVIDER ARGS:", kwargs) + seek = kwargs.get("seek", 10.0) + + data_dir = "unitree_go2_lidar_corrected" + get_data(data_dir) + + lidar_frame_result = TimedSensorReplay(f"{data_dir}/lidar").find_closest_seek(seek) + if lidar_frame_result is None: + raise ValueError("No lidar frame found") + lidar_frame: LidarMessage = lidar_frame_result + + image_frame = TimedSensorReplay( + f"{data_dir}/video", + ).find_closest(lidar_frame.ts) + + if image_frame is None: + raise ValueError("No image frame found") + + image_frame.frame_id = "camera_optical" + + odom_frame = TimedSensorReplay(f"{data_dir}/odom", autocast=Odometry.from_msg).find_closest( + lidar_frame.ts + ) + + if odom_frame is None: + raise ValueError("No odom frame found") + + transforms = ConnectionModule._odom_to_tf(odom_frame) + + tf.receive_transform(*transforms) + camera_info_out = ConnectionModule._camera_info() + # ConnectionModule._camera_info() returns Out[CameraInfo], extract the value + from typing import cast + + camera_info = cast("CameraInfo", camera_info_out) + return { + "odom_frame": odom_frame, + "lidar_frame": lidar_frame, + "image_frame": image_frame, + "camera_info": camera_info, + "transforms": transforms, + "tf": tf, + } + + return moment_provider + + +@pytest.fixture(scope="session") +def publish_moment(): + def publisher(moment: Moment | Moment2D | Moment3D) -> None: + detections2d_val = moment.get("detections2d") + if detections2d_val: + # 2d annotations + annotations: LCMTransport[ImageAnnotations] = LCMTransport( + "/annotations", ImageAnnotations + ) + assert isinstance(detections2d_val, ImageDetections2D) + annotations.publish(detections2d_val.to_foxglove_annotations()) + + detections: LCMTransport[Detection2DArray] = LCMTransport( + "/detections", Detection2DArray + ) + detections.publish(detections2d_val.to_ros_detection2d_array()) + + annotations.lcm.stop() + detections.lcm.stop() + + detections3dpc_val = moment.get("detections3dpc") + if detections3dpc_val: + scene_update: LCMTransport[SceneUpdate] = LCMTransport("/scene_update", SceneUpdate) + # 3d scene update + assert isinstance(detections3dpc_val, ImageDetections3DPC) + scene_update.publish(detections3dpc_val.to_foxglove_scene_update()) + scene_update.lcm.stop() + + lidar_frame = moment.get("lidar_frame") + if lidar_frame: + lidar: LCMTransport[PointCloud2] = LCMTransport("/lidar", PointCloud2) + lidar.publish(lidar_frame) + lidar.lcm.stop() + + image_frame = moment.get("image_frame") + if image_frame: + image: LCMTransport[Image] = LCMTransport("/image", Image) + image.publish(image_frame) + image.lcm.stop() + + camera_info_val = moment.get("camera_info") + if camera_info_val: + camera_info: LCMTransport[CameraInfo] = LCMTransport("/camera_info", CameraInfo) + camera_info.publish(camera_info_val) + camera_info.lcm.stop() + + tf = moment.get("tf") + transforms = moment.get("transforms") + if tf is not None and transforms is not None: + tf.publish(*transforms) + + # moduleDB.scene_update.transport = LCMTransport("/scene_update", SceneUpdate) + # moduleDB.target.transport = LCMTransport("/target", PoseStamped) + + return publisher + + +@pytest.fixture(scope="session") +def imageDetections2d(get_moment_2d) -> ImageDetections2D: + moment = get_moment_2d() + assert len(moment["detections2d"]) > 0, "No detections found in the moment" + return moment["detections2d"] + + +@pytest.fixture(scope="session") +def detection2d(get_moment_2d) -> Detection2D: + moment = get_moment_2d() + assert len(moment["detections2d"]) > 0, "No detections found in the moment" + return moment["detections2d"][0] + + +@pytest.fixture(scope="session") +def detections3dpc(get_moment_3dpc) -> Detection3DPC: + moment = get_moment_3dpc(seek=10.0) + assert len(moment["detections3dpc"]) > 0, "No detections found in the moment" + return moment["detections3dpc"] + + +@pytest.fixture(scope="session") +def detection3dpc(detections3dpc) -> Detection3DPC: + return detections3dpc[0] + + +@pytest.fixture(scope="session") +def get_moment_2d(get_moment) -> Generator[Callable[[], Moment2D], None, None]: + from dimos.perception.detection.detectors import Yolo2DDetector + + module = Detection2DModule(detector=lambda: Yolo2DDetector(device="cpu")) + + @functools.lru_cache(maxsize=1) + def moment_provider(**kwargs) -> Moment2D: + moment = get_moment(**kwargs) + detections = module.process_image_frame(moment.get("image_frame")) + + return { + **moment, + "detections2d": detections, + } + + yield moment_provider + + module._close_module() + + +@pytest.fixture(scope="session") +def get_moment_3dpc(get_moment_2d) -> Generator[Callable[[], Moment3D], None, None]: + module: Detection3DModule | None = None + + @functools.lru_cache(maxsize=1) + def moment_provider(**kwargs) -> Moment3D: + nonlocal module + moment = get_moment_2d(**kwargs) + + if not module: + module = Detection3DModule(camera_info=moment["camera_info"]) + + lidar_frame = moment.get("lidar_frame") + if lidar_frame is None: + raise ValueError("No lidar frame found") + + camera_transform = moment["tf"].get("camera_optical", lidar_frame.frame_id) + if camera_transform is None: + raise ValueError("No camera_optical transform in tf") + + detections3dpc = module.process_frame( + moment["detections2d"], moment["lidar_frame"], camera_transform + ) + + return { + **moment, + "detections3dpc": detections3dpc, + } + + yield moment_provider + if module is not None: + module._close_module() + + +@pytest.fixture(scope="session") +def object_db_module(get_moment): + """Create and populate an ObjectDBModule with detections from multiple frames.""" + from dimos.perception.detection.detectors import Yolo2DDetector + + module2d = Detection2DModule(detector=lambda: Yolo2DDetector(device="cpu")) + module3d = Detection3DModule(camera_info=ConnectionModule._camera_info()) + moduleDB = ObjectDBModule( + camera_info=ConnectionModule._camera_info(), + goto=lambda obj_id: None, # No-op for testing + ) + + # Process 5 frames to build up object history + for i in range(5): + seek_value = 10.0 + (i * 2) + moment = get_moment(seek=seek_value) + + # Process 2D detections + imageDetections2d = module2d.process_image_frame(moment["image_frame"]) + + # Get camera transform + camera_transform = moment["tf"].get("camera_optical", moment.get("lidar_frame").frame_id) + + # Process 3D detections + imageDetections3d = module3d.process_frame( + imageDetections2d, moment["lidar_frame"], camera_transform + ) + + # Add to database + moduleDB.add_detections(imageDetections3d) + + yield moduleDB + + module2d._close_module() + module3d._close_module() + moduleDB._close_module() + + +@pytest.fixture(scope="session") +def first_object(object_db_module): + """Get the first object from the database.""" + objects = list(object_db_module.objects.values()) + assert len(objects) > 0, "No objects found in database" + return objects[0] + + +@pytest.fixture(scope="session") +def all_objects(object_db_module): + """Get all objects from the database.""" + return list(object_db_module.objects.values()) diff --git a/dimos/perception/detection/detectors/__init__.py b/dimos/perception/detection/detectors/__init__.py new file mode 100644 index 0000000000..d6383d084e --- /dev/null +++ b/dimos/perception/detection/detectors/__init__.py @@ -0,0 +1,3 @@ +# from dimos.perception.detection.detectors.detic import Detic2DDetector +from dimos.perception.detection.detectors.types import Detector +from dimos.perception.detection.detectors.yolo import Yolo2DDetector diff --git a/dimos/perception/detection2d/config/custom_tracker.yaml b/dimos/perception/detection/detectors/config/custom_tracker.yaml similarity index 100% rename from dimos/perception/detection2d/config/custom_tracker.yaml rename to dimos/perception/detection/detectors/config/custom_tracker.yaml diff --git a/dimos/perception/detection/detectors/conftest.py b/dimos/perception/detection/detectors/conftest.py new file mode 100644 index 0000000000..7caca818c9 --- /dev/null +++ b/dimos/perception/detection/detectors/conftest.py @@ -0,0 +1,38 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from dimos.msgs.sensor_msgs import Image +from dimos.perception.detection.detectors.person.yolo import YoloPersonDetector +from dimos.perception.detection.detectors.yolo import Yolo2DDetector +from dimos.utils.data import get_data + + +@pytest.fixture(scope="session") +def test_image(): + """Load the test image used for detector tests.""" + return Image.from_file(get_data("cafe.jpg")) + + +@pytest.fixture(scope="session") +def person_detector(): + """Create a YoloPersonDetector instance.""" + return YoloPersonDetector() + + +@pytest.fixture(scope="session") +def bbox_detector(): + """Create a Yolo2DDetector instance for general object detection.""" + return Yolo2DDetector() diff --git a/dimos/perception/detection2d/detic_2d_det.py b/dimos/perception/detection/detectors/detic.py similarity index 62% rename from dimos/perception/detection2d/detic_2d_det.py rename to dimos/perception/detection/detectors/detic.py index c320438325..4432988f28 100644 --- a/dimos/perception/detection2d/detic_2d_det.py +++ b/dimos/perception/detection/detectors/detic.py @@ -1,141 +1,172 @@ -import cv2 -import numpy as np +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Sequence import os -import torch import sys -from pathlib import Path + +import numpy as np # Add Detic to Python path -detic_path = os.path.join(os.path.dirname(__file__), '..', '..', 'models', 'Detic') -if detic_path not in sys.path: - sys.path.append(detic_path) - sys.path.append(os.path.join(detic_path, 'third_party/CenterNet2')) +from dimos.constants import DIMOS_PROJECT_ROOT +from dimos.msgs.sensor_msgs import Image +from dimos.perception.detection.detectors.types import Detector +from dimos.perception.detection2d.utils import plot_results + +detic_path = DIMOS_PROJECT_ROOT / "dimos/models/Detic" +if str(detic_path) not in sys.path: + sys.path.append(str(detic_path)) + sys.path.append(str(detic_path / "third_party/CenterNet2")) # PIL patch for compatibility import PIL.Image -if not hasattr(PIL.Image, 'LINEAR') and hasattr(PIL.Image, 'BILINEAR'): - PIL.Image.LINEAR = PIL.Image.BILINEAR + +if not hasattr(PIL.Image, "LINEAR") and hasattr(PIL.Image, "BILINEAR"): + PIL.Image.LINEAR = PIL.Image.BILINEAR # type: ignore[attr-defined] # Detectron2 imports from detectron2.config import get_cfg -from detectron2.utils.visualizer import Visualizer from detectron2.data import MetadataCatalog + # Simple tracking implementation class SimpleTracker: """Simple IOU-based tracker implementation without external dependencies""" - - def __init__(self, iou_threshold=0.3, max_age=5): + + def __init__(self, iou_threshold: float = 0.3, max_age: int = 5) -> None: self.iou_threshold = iou_threshold self.max_age = max_age self.next_id = 1 - self.tracks = {} # id -> {bbox, class_id, age, etc} - + self.tracks = {} # id -> {bbox, class_id, age, mask, etc} + def _calculate_iou(self, bbox1, bbox2): """Calculate IoU between two bboxes in format [x1,y1,x2,y2]""" x1 = max(bbox1[0], bbox2[0]) y1 = max(bbox1[1], bbox2[1]) x2 = min(bbox1[2], bbox2[2]) y2 = min(bbox1[3], bbox2[3]) - + if x2 < x1 or y2 < y1: return 0.0 - + intersection = (x2 - x1) * (y2 - y1) area1 = (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1]) area2 = (bbox2[2] - bbox2[0]) * (bbox2[3] - bbox2[1]) union = area1 + area2 - intersection - + return intersection / union if union > 0 else 0 - - def update(self, detections): + + def update(self, detections, masks): """Update tracker with new detections - + Args: detections: List of [x1,y1,x2,y2,score,class_id] - + masks: List of segmentation masks corresponding to detections + Returns: - List of [track_id, bbox, score, class_id] + List of [track_id, bbox, score, class_id, mask] """ if len(detections) == 0: # Age existing tracks for track_id in list(self.tracks.keys()): - self.tracks[track_id]['age'] += 1 + self.tracks[track_id]["age"] += 1 # Remove old tracks - if self.tracks[track_id]['age'] > self.max_age: + if self.tracks[track_id]["age"] > self.max_age: del self.tracks[track_id] return [] - + # Convert to numpy for easier handling if not isinstance(detections, np.ndarray): detections = np.array(detections) - + result = [] matched_indices = set() - + # Update existing tracks for track_id, track in list(self.tracks.items()): - track['age'] += 1 - - if track['age'] > self.max_age: + track["age"] += 1 + + if track["age"] > self.max_age: del self.tracks[track_id] continue - + # Find best matching detection for this track best_iou = self.iou_threshold best_idx = -1 - + for i, det in enumerate(detections): if i in matched_indices: continue - + # Check class match - if det[5] != track['class_id']: + if det[5] != track["class_id"]: continue - - iou = self._calculate_iou(track['bbox'], det[:4]) + + iou = self._calculate_iou(track["bbox"], det[:4]) if iou > best_iou: best_iou = iou best_idx = i - + # If we found a match, update the track if best_idx >= 0: - self.tracks[track_id]['bbox'] = detections[best_idx][:4] - self.tracks[track_id]['score'] = detections[best_idx][4] - self.tracks[track_id]['age'] = 0 + self.tracks[track_id]["bbox"] = detections[best_idx][:4] + self.tracks[track_id]["score"] = detections[best_idx][4] + self.tracks[track_id]["age"] = 0 + self.tracks[track_id]["mask"] = masks[best_idx] matched_indices.add(best_idx) - - # Add to results - result.append([track_id, detections[best_idx][:4], - detections[best_idx][4], int(detections[best_idx][5])]) - + + # Add to results with mask + result.append( + [ + track_id, + detections[best_idx][:4], + detections[best_idx][4], + int(detections[best_idx][5]), + self.tracks[track_id]["mask"], + ] + ) + # Create new tracks for unmatched detections for i, det in enumerate(detections): if i in matched_indices: continue - + # Create new track new_id = self.next_id self.next_id += 1 - + self.tracks[new_id] = { - 'bbox': det[:4], - 'score': det[4], - 'class_id': int(det[5]), - 'age': 0 + "bbox": det[:4], + "score": det[4], + "class_id": int(det[5]), + "age": 0, + "mask": masks[i], } - - # Add to results - result.append([new_id, det[:4], det[4], int(det[5])]) - + + # Add to results with mask directly from the track + result.append([new_id, det[:4], det[4], int(det[5]), masks[i]]) + return result -class Detic2DDetector: - def __init__(self, model_path=None, device="cuda", vocabulary=None, threshold=0.5): +class Detic2DDetector(Detector): + def __init__( + self, model_path=None, device: str = "cuda", vocabulary=None, threshold: float = 0.5 + ) -> None: """ Initialize the Detic detector with open vocabulary support. - + Args: model_path (str): Path to a custom Detic model weights (optional) device (str): Device to run inference on ('cuda' or 'cpu') @@ -144,82 +175,89 @@ def __init__(self, model_path=None, device="cuda", vocabulary=None, threshold=0. """ self.device = device self.threshold = threshold - + # Set up Detic paths - already added to sys.path at module level - + # Import Detic modules from centernet.config import add_centernet_config from detic.config import add_detic_config - from detic.modeling.utils import reset_cls_test from detic.modeling.text.text_encoder import build_text_encoder - + from detic.modeling.utils import reset_cls_test + # Keep reference to these functions for later use self.reset_cls_test = reset_cls_test self.build_text_encoder = build_text_encoder - + # Setup model configuration self.cfg = get_cfg() add_centernet_config(self.cfg) add_detic_config(self.cfg) - + # Use default Detic config - self.cfg.merge_from_file(os.path.join( - detic_path, - "configs/Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml" - )) - + self.cfg.merge_from_file( + os.path.join( + detic_path, "configs/Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.yaml" + ) + ) + # Set default weights if not provided if model_path is None: - self.cfg.MODEL.WEIGHTS = 'https://dl.fbaipublicfiles.com/detic/Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.pth' + self.cfg.MODEL.WEIGHTS = "https://dl.fbaipublicfiles.com/detic/Detic_LCOCOI21k_CLIP_SwinB_896b32_4x_ft4x_max-size.pth" else: self.cfg.MODEL.WEIGHTS = model_path - + # Set device if device == "cpu": - self.cfg.MODEL.DEVICE = 'cpu' - + self.cfg.MODEL.DEVICE = "cpu" + # Set detection threshold self.cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = threshold - self.cfg.MODEL.ROI_BOX_HEAD.ZEROSHOT_WEIGHT_PATH = 'rand' + self.cfg.MODEL.ROI_BOX_HEAD.ZEROSHOT_WEIGHT_PATH = "rand" self.cfg.MODEL.ROI_HEADS.ONE_CLASS_PER_PROPOSAL = True - + # Built-in datasets for Detic - use absolute paths with detic_path self.builtin_datasets = { - 'lvis': { - 'metadata': 'lvis_v1_val', - 'classifier': os.path.join(detic_path, 'datasets/metadata/lvis_v1_clip_a+cname.npy') + "lvis": { + "metadata": "lvis_v1_val", + "classifier": os.path.join( + detic_path, "datasets/metadata/lvis_v1_clip_a+cname.npy" + ), }, - 'objects365': { - 'metadata': 'objects365_v2_val', - 'classifier': os.path.join(detic_path, 'datasets/metadata/o365_clip_a+cnamefix.npy') + "objects365": { + "metadata": "objects365_v2_val", + "classifier": os.path.join( + detic_path, "datasets/metadata/o365_clip_a+cnamefix.npy" + ), }, - 'openimages': { - 'metadata': 'oid_val_expanded', - 'classifier': os.path.join(detic_path, 'datasets/metadata/oid_clip_a+cname.npy') + "openimages": { + "metadata": "oid_val_expanded", + "classifier": os.path.join(detic_path, "datasets/metadata/oid_clip_a+cname.npy"), + }, + "coco": { + "metadata": "coco_2017_val", + "classifier": os.path.join(detic_path, "datasets/metadata/coco_clip_a+cname.npy"), }, - 'coco': { - 'metadata': 'coco_2017_val', - 'classifier': os.path.join(detic_path, 'datasets/metadata/coco_clip_a+cname.npy') - } } - + # Override config paths to use absolute paths - self.cfg.MODEL.ROI_BOX_HEAD.CAT_FREQ_PATH = os.path.join(detic_path, 'datasets/metadata/lvis_v1_train_cat_info.json') - + self.cfg.MODEL.ROI_BOX_HEAD.CAT_FREQ_PATH = os.path.join( + detic_path, "datasets/metadata/lvis_v1_train_cat_info.json" + ) + # Initialize model self.predictor = None - + # Setup with initial vocabulary - vocabulary = vocabulary or 'lvis' + vocabulary = vocabulary or "lvis" self.setup_vocabulary(vocabulary) - + # Initialize our simple tracker self.tracker = SimpleTracker(iou_threshold=0.5, max_age=5) - + def setup_vocabulary(self, vocabulary): """ Setup the model's vocabulary. - + Args: vocabulary: Either a string ('lvis', 'objects365', 'openimages', 'coco') or a list of class names for custom vocabulary. @@ -227,13 +265,14 @@ def setup_vocabulary(self, vocabulary): if self.predictor is None: # Initialize the model from detectron2.engine import DefaultPredictor + self.predictor = DefaultPredictor(self.cfg) - + if isinstance(vocabulary, str) and vocabulary in self.builtin_datasets: # Use built-in dataset dataset = vocabulary - metadata = MetadataCatalog.get(self.builtin_datasets[dataset]['metadata']) - classifier = self.builtin_datasets[dataset]['classifier'] + metadata = MetadataCatalog.get(self.builtin_datasets[dataset]["metadata"]) + classifier = self.builtin_datasets[dataset]["classifier"] num_classes = len(metadata.thing_classes) self.class_names = metadata.thing_classes else: @@ -241,37 +280,37 @@ def setup_vocabulary(self, vocabulary): if isinstance(vocabulary, str): # If it's a string but not a built-in dataset, treat as a file try: - with open(vocabulary, 'r') as f: + with open(vocabulary) as f: class_names = [line.strip() for line in f if line.strip()] except: # Default to LVIS if there's an issue print(f"Error loading vocabulary from {vocabulary}, using LVIS") - return self.setup_vocabulary('lvis') + return self.setup_vocabulary("lvis") else: # Assume it's a list of class names class_names = vocabulary - + # Create classifier from text embeddings metadata = MetadataCatalog.get("__unused") metadata.thing_classes = class_names self.class_names = class_names - + # Generate CLIP embeddings for custom vocabulary classifier = self._get_clip_embeddings(class_names) num_classes = len(class_names) - + # Reset model with new vocabulary self.reset_cls_test(self.predictor.model, classifier, num_classes) return self.class_names - - def _get_clip_embeddings(self, vocabulary, prompt='a '): + + def _get_clip_embeddings(self, vocabulary, prompt: str = "a "): """ Generate CLIP embeddings for a vocabulary list. - + Args: vocabulary (list): List of class names prompt (str): Prompt prefix to use for CLIP - + Returns: torch.Tensor: Tensor of embeddings """ @@ -280,76 +319,91 @@ def _get_clip_embeddings(self, vocabulary, prompt='a '): texts = [prompt + x for x in vocabulary] emb = text_encoder(texts).detach().permute(1, 0).contiguous().cpu() return emb - - def process_image(self, image): + + def process_image(self, image: Image): """ Process an image and return detection results. - + Args: image: Input image in BGR format (OpenCV) - + Returns: - tuple: (bboxes, track_ids, class_ids, confidences, names) + tuple: (bboxes, track_ids, class_ids, confidences, names, masks) - bboxes: list of [x1, y1, x2, y2] coordinates - track_ids: list of tracking IDs (or -1 if no tracking) - class_ids: list of class indices - confidences: list of detection confidences - names: list of class names + - masks: list of segmentation masks (numpy arrays) """ # Run inference with Detic - outputs = self.predictor(image) + outputs = self.predictor(image.to_opencv()) instances = outputs["instances"].to("cpu") - - # Extract bounding boxes, classes, and scores + + # Extract bounding boxes, classes, scores, and masks if len(instances) == 0: - return [], [], [], [], [] - + return [], [], [], [], [] # , [] + boxes = instances.pred_boxes.tensor.numpy() class_ids = instances.pred_classes.numpy() scores = instances.scores.numpy() - + masks = instances.pred_masks.numpy() + # Convert boxes to [x1, y1, x2, y2] format bboxes = [] for box in boxes: x1, y1, x2, y2 = box.tolist() bboxes.append([x1, y1, x2, y2]) - + # Get class names - names = [self.class_names[class_id] for class_id in class_ids] - + [self.class_names[class_id] for class_id in class_ids] + # Apply tracking detections = [] + filtered_masks = [] for i, bbox in enumerate(bboxes): if scores[i] >= self.threshold: # Format for tracker: [x1, y1, x2, y2, score, class_id] - detections.append(bbox + [scores[i], class_ids[i]]) - + detections.append([*bbox, scores[i], class_ids[i]]) + filtered_masks.append(masks[i]) + if not detections: - return [], [], [], [], [] - - # Update tracker with detections - track_results = self.tracker.update(detections) - + return [], [], [], [], [] # , [] + + # Update tracker with detections and correctly aligned masks + track_results = self.tracker.update(detections, filtered_masks) + # Process tracking results track_ids = [] tracked_bboxes = [] tracked_class_ids = [] tracked_scores = [] tracked_names = [] - - for track_id, bbox, score, class_id in track_results: + tracked_masks = [] + + for track_id, bbox, score, class_id, mask in track_results: track_ids.append(int(track_id)) tracked_bboxes.append(bbox.tolist() if isinstance(bbox, np.ndarray) else bbox) tracked_class_ids.append(int(class_id)) tracked_scores.append(score) tracked_names.append(self.class_names[int(class_id)]) - - return tracked_bboxes, track_ids, tracked_class_ids, tracked_scores, tracked_names - - def visualize_results(self, image, bboxes, track_ids, class_ids, confidences, names): + tracked_masks.append(mask) + + return ( + tracked_bboxes, + track_ids, + tracked_class_ids, + tracked_scores, + tracked_names, + # tracked_masks, + ) + + def visualize_results( + self, image, bboxes, track_ids, class_ids, confidences, names: Sequence[str] + ): """ Generate visualization of detection results. - + Args: image: Original input image bboxes: List of bounding boxes @@ -357,15 +411,14 @@ def visualize_results(self, image, bboxes, track_ids, class_ids, confidences, na class_ids: List of class indices confidences: List of detection confidences names: List of class names - + Returns: Image with visualized detections """ - from dimos.perception.detection2d.utils import plot_results + return plot_results(image, bboxes, track_ids, class_ids, confidences, names) - - def cleanup(self): + + def cleanup(self) -> None: """Clean up resources.""" # Nothing specific to clean up for Detic pass - diff --git a/dimos/perception/detection/detectors/person/test_person_detectors.py b/dimos/perception/detection/detectors/person/test_person_detectors.py new file mode 100644 index 0000000000..d912bec3a0 --- /dev/null +++ b/dimos/perception/detection/detectors/person/test_person_detectors.py @@ -0,0 +1,160 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from dimos.perception.detection.type import Detection2DPerson, ImageDetections2D + + +@pytest.fixture(scope="session") +def people(person_detector, test_image): + return person_detector.process_image(test_image) + + +@pytest.fixture(scope="session") +def person(people): + return people[0] + + +def test_person_detection(people) -> None: + """Test that we can detect people with pose keypoints.""" + assert len(people) > 0 + + # Check first person + person = people[0] + assert isinstance(person, Detection2DPerson) + assert person.confidence > 0 + assert len(person.bbox) == 4 # bbox is now a tuple + assert person.keypoints.shape == (17, 2) + assert person.keypoint_scores.shape == (17,) + + +def test_person_properties(people) -> None: + """Test Detection2DPerson object properties and methods.""" + person = people[0] + + # Test bounding box properties + assert person.width > 0 + assert person.height > 0 + assert len(person.center) == 2 + + # Test keypoint access + nose_xy, nose_conf = person.get_keypoint("nose") + assert nose_xy.shape == (2,) + assert 0 <= nose_conf <= 1 + + # Test visible keypoints + visible = person.get_visible_keypoints(threshold=0.5) + assert len(visible) > 0 + assert all(isinstance(name, str) for name, _, _ in visible) + assert all(xy.shape == (2,) for _, xy, _ in visible) + assert all(0 <= conf <= 1 for _, _, conf in visible) + + +def test_person_normalized_coords(people) -> None: + """Test normalized coordinates if available.""" + person = people[0] + + if person.keypoints_normalized is not None: + assert person.keypoints_normalized.shape == (17, 2) + # Check all values are in 0-1 range + assert (person.keypoints_normalized >= 0).all() + assert (person.keypoints_normalized <= 1).all() + + if person.bbox_normalized is not None: + assert person.bbox_normalized.shape == (4,) + assert (person.bbox_normalized >= 0).all() + assert (person.bbox_normalized <= 1).all() + + +def test_multiple_people(people) -> None: + """Test that multiple people can be detected.""" + print(f"\nDetected {len(people)} people in test image") + + for i, person in enumerate(people[:3]): # Show first 3 + print(f"\nPerson {i}:") + print(f" Confidence: {person.confidence:.3f}") + print(f" Size: {person.width:.1f} x {person.height:.1f}") + + visible = person.get_visible_keypoints(threshold=0.8) + print(f" High-confidence keypoints (>0.8): {len(visible)}") + for name, xy, conf in visible[:5]: + print(f" {name}: ({xy[0]:.1f}, {xy[1]:.1f}) conf={conf:.3f}") + + +def test_image_detections2d_structure(people) -> None: + """Test that process_image returns ImageDetections2D.""" + assert isinstance(people, ImageDetections2D) + assert len(people.detections) > 0 + assert all(isinstance(d, Detection2DPerson) for d in people.detections) + + +def test_invalid_keypoint(test_image) -> None: + """Test error handling for invalid keypoint names.""" + # Create a dummy Detection2DPerson + import numpy as np + + person = Detection2DPerson( + # Detection2DBBox fields + bbox=(0.0, 0.0, 100.0, 100.0), + track_id=0, + class_id=0, + confidence=0.9, + name="person", + ts=test_image.ts, + image=test_image, + # Detection2DPerson fields + keypoints=np.zeros((17, 2)), + keypoint_scores=np.zeros(17), + ) + + with pytest.raises(ValueError): + person.get_keypoint("invalid_keypoint") + + +def test_person_annotations(person) -> None: + # Test text annotations + text_anns = person.to_text_annotation() + print(f"\nText annotations: {len(text_anns)}") + for i, ann in enumerate(text_anns): + print(f" {i}: {ann.text}") + assert len(text_anns) == 3 # confidence, name/track_id, keypoints count + assert any("keypoints:" in ann.text for ann in text_anns) + + # Test points annotations + points_anns = person.to_points_annotation() + print(f"\nPoints annotations: {len(points_anns)}") + + # Count different types (use actual LCM constants) + from dimos_lcm.foxglove_msgs.ImageAnnotations import PointsAnnotation + + bbox_count = sum(1 for ann in points_anns if ann.type == PointsAnnotation.LINE_LOOP) # 2 + keypoint_count = sum(1 for ann in points_anns if ann.type == PointsAnnotation.POINTS) # 1 + skeleton_count = sum(1 for ann in points_anns if ann.type == PointsAnnotation.LINE_LIST) # 4 + + print(f" - Bounding boxes: {bbox_count}") + print(f" - Keypoint circles: {keypoint_count}") + print(f" - Skeleton lines: {skeleton_count}") + + assert bbox_count >= 1 # At least the person bbox + assert keypoint_count >= 1 # At least some visible keypoints + assert skeleton_count >= 1 # At least some skeleton connections + + # Test full image annotations + img_anns = person.to_image_annotations() + assert img_anns.texts_length == len(text_anns) + assert img_anns.points_length == len(points_anns) + + print("\n✓ Person annotations working correctly!") + print(f" - {len(person.get_visible_keypoints(0.5))}/17 visible keypoints") diff --git a/dimos/perception/detection/detectors/person/yolo.py b/dimos/perception/detection/detectors/person/yolo.py new file mode 100644 index 0000000000..6421ab7d1d --- /dev/null +++ b/dimos/perception/detection/detectors/person/yolo.py @@ -0,0 +1,80 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ultralytics import YOLO + +from dimos.msgs.sensor_msgs import Image +from dimos.perception.detection.detectors.types import Detector +from dimos.perception.detection.type import ImageDetections2D +from dimos.utils.data import get_data +from dimos.utils.gpu_utils import is_cuda_available +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.perception.detection.yolo.person") + + +class YoloPersonDetector(Detector): + def __init__( + self, + model_path: str = "models_yolo", + model_name: str = "yolo11n-pose.pt", + device: str | None = None, + ) -> None: + self.model = YOLO(get_data(model_path) / model_name, task="track") + + self.tracker = get_data(model_path) / "botsort.yaml" + + if device: + self.device = device + return + + if is_cuda_available(): + self.device = "cuda" + logger.info("Using CUDA for YOLO person detector") + else: + self.device = "cpu" + logger.info("Using CPU for YOLO person detector") + + def process_image(self, image: Image) -> ImageDetections2D: + """Process image and return detection results. + + Args: + image: Input image + + Returns: + ImageDetections2D containing Detection2DPerson objects with pose keypoints + """ + results = self.model.track( + source=image.to_opencv(), + verbose=False, + conf=0.5, + tracker=self.tracker, + persist=True, + device=self.device, + ) + return ImageDetections2D.from_ultralytics_result(image, results) + + def stop(self) -> None: + """ + Clean up resources used by the detector, including tracker threads. + """ + if hasattr(self.model, "predictor") and self.model.predictor is not None: + predictor = self.model.predictor + if hasattr(predictor, "trackers") and predictor.trackers: + for tracker in predictor.trackers: + if hasattr(tracker, "tracker") and hasattr(tracker.tracker, "gmc"): + gmc = tracker.tracker.gmc + if hasattr(gmc, "executor") and gmc.executor is not None: + gmc.executor.shutdown(wait=True) + self.model.predictor = None diff --git a/dimos/perception/detection/detectors/test_bbox_detectors.py b/dimos/perception/detection/detectors/test_bbox_detectors.py new file mode 100644 index 0000000000..a86690279f --- /dev/null +++ b/dimos/perception/detection/detectors/test_bbox_detectors.py @@ -0,0 +1,158 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from dimos.perception.detection.type import Detection2D, ImageDetections2D + + +@pytest.fixture(params=["bbox_detector", "person_detector"], scope="session") +def detector(request): + """Parametrized fixture that provides both bbox and person detectors.""" + return request.getfixturevalue(request.param) + + +@pytest.fixture(scope="session") +def detections(detector, test_image): + """Get ImageDetections2D from any detector.""" + return detector.process_image(test_image) + + +def test_detection_basic(detections) -> None: + """Test that we can detect objects with all detectors.""" + assert len(detections.detections) > 0 + + # Check first detection + detection = detections.detections[0] + assert isinstance(detection, Detection2D) + assert detection.confidence > 0 + assert len(detection.bbox) == 4 # bbox is a tuple (x1, y1, x2, y2) + assert detection.class_id >= 0 + assert detection.name is not None + + +def test_detection_bbox_properties(detections) -> None: + """Test Detection2D bbox properties work for all detectors.""" + detection = detections.detections[0] + + # Test bounding box is valid + x1, y1, x2, y2 = detection.bbox + assert x2 > x1, "x2 should be greater than x1" + assert y2 > y1, "y2 should be greater than y1" + assert all(coord >= 0 for coord in detection.bbox), "Coordinates should be non-negative" + + # Test bbox volume + volume = detection.bbox_2d_volume() + assert volume > 0 + expected_volume = (x2 - x1) * (y2 - y1) + assert abs(volume - expected_volume) < 0.01 + + # Test center calculation + center_x, center_y, width, height = detection.get_bbox_center() + assert center_x == (x1 + x2) / 2.0 + assert center_y == (y1 + y2) / 2.0 + assert width == x2 - x1 + assert height == y2 - y1 + + +def test_detection_cropped_image(detections, test_image) -> None: + """Test cropping image to detection bbox.""" + detection = detections.detections[0] + + # Test cropped image + cropped = detection.cropped_image(padding=20) + assert cropped is not None + + # Cropped image should be smaller than original (usually) + if test_image.shape: + assert cropped.shape[0] <= test_image.shape[0] + assert cropped.shape[1] <= test_image.shape[1] + + +def test_detection_annotations(detections) -> None: + """Test annotation generation for detections.""" + detection = detections.detections[0] + + # Test text annotations - all detections should have at least 2 + text_annotations = detection.to_text_annotation() + assert len(text_annotations) >= 2 # confidence and name/track_id (person has keypoints too) + + # Test points annotations - at least bbox + points_annotations = detection.to_points_annotation() + assert len(points_annotations) >= 1 # At least the bbox polygon + + # Test image annotations + annotations = detection.to_image_annotations() + assert annotations.texts_length >= 2 + assert annotations.points_length >= 1 + + +def test_detection_ros_conversion(detections) -> None: + """Test conversion to ROS Detection2D message.""" + detection = detections.detections[0] + + ros_det = detection.to_ros_detection2d() + + # Check bbox conversion + center_x, center_y, width, height = detection.get_bbox_center() + assert abs(ros_det.bbox.center.position.x - center_x) < 0.01 + assert abs(ros_det.bbox.center.position.y - center_y) < 0.01 + assert abs(ros_det.bbox.size_x - width) < 0.01 + assert abs(ros_det.bbox.size_y - height) < 0.01 + + # Check confidence and class_id + assert len(ros_det.results) > 0 + assert ros_det.results[0].hypothesis.score == detection.confidence + assert ros_det.results[0].hypothesis.class_id == detection.class_id + + +def test_detection_is_valid(detections) -> None: + """Test bbox validation.""" + detection = detections.detections[0] + + # Detection from real detector should be valid + assert detection.is_valid() + + +def test_image_detections2d_structure(detections) -> None: + """Test that process_image returns ImageDetections2D.""" + assert isinstance(detections, ImageDetections2D) + assert len(detections.detections) > 0 + assert all(isinstance(d, Detection2D) for d in detections.detections) + + +def test_multiple_detections(detections) -> None: + """Test that multiple objects can be detected.""" + print(f"\nDetected {len(detections.detections)} objects in test image") + + for i, detection in enumerate(detections.detections[:5]): # Show first 5 + print(f"\nDetection {i}:") + print(f" Class: {detection.name} (id: {detection.class_id})") + print(f" Confidence: {detection.confidence:.3f}") + print( + f" Bbox: ({detection.bbox[0]:.1f}, {detection.bbox[1]:.1f}, {detection.bbox[2]:.1f}, {detection.bbox[3]:.1f})" + ) + print(f" Track ID: {detection.track_id}") + + +def test_detection_string_representation(detections) -> None: + """Test string representation of detections.""" + detection = detections.detections[0] + str_repr = str(detection) + + # Should contain class name (either Detection2DBBox or Detection2DPerson) + assert "Detection2D" in str_repr + + # Should show object name + assert detection.name in str_repr or f"class_{detection.class_id}" in str_repr diff --git a/dimos/perception/detection/detectors/types.py b/dimos/perception/detection/detectors/types.py new file mode 100644 index 0000000000..1a3b0b5471 --- /dev/null +++ b/dimos/perception/detection/detectors/types.py @@ -0,0 +1,23 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC, abstractmethod + +from dimos.msgs.sensor_msgs import Image +from dimos.perception.detection.type import ImageDetections2D + + +class Detector(ABC): + @abstractmethod + def process_image(self, image: Image) -> ImageDetections2D: ... diff --git a/dimos/perception/detection/detectors/yolo.py b/dimos/perception/detection/detectors/yolo.py new file mode 100644 index 0000000000..64e56ad456 --- /dev/null +++ b/dimos/perception/detection/detectors/yolo.py @@ -0,0 +1,83 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ultralytics import YOLO + +from dimos.msgs.sensor_msgs import Image +from dimos.perception.detection.detectors.types import Detector +from dimos.perception.detection.type import ImageDetections2D +from dimos.utils.data import get_data +from dimos.utils.gpu_utils import is_cuda_available +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.perception.detection.yolo_2d_det") + + +class Yolo2DDetector(Detector): + def __init__( + self, + model_path: str = "models_yolo", + model_name: str = "yolo11n.pt", + device: str | None = None, + ) -> None: + self.model = YOLO( + get_data(model_path) / model_name, + task="detect", + ) + + if device: + self.device = device + return + + if is_cuda_available(): + self.device = "cuda" + logger.debug("Using CUDA for YOLO 2d detector") + else: + self.device = "cpu" + logger.debug("Using CPU for YOLO 2d detector") + + def process_image(self, image: Image) -> ImageDetections2D: + """ + Process an image and return detection results. + + Args: + image: Input image + + Returns: + ImageDetections2D containing all detected objects + """ + results = self.model.track( + source=image.to_opencv(), + device=self.device, + conf=0.5, + iou=0.6, + persist=True, + verbose=False, + ) + + return ImageDetections2D.from_ultralytics_result(image, results) + + def stop(self) -> None: + """ + Clean up resources used by the detector, including tracker threads. + """ + if hasattr(self.model, "predictor") and self.model.predictor is not None: + predictor = self.model.predictor + if hasattr(predictor, "trackers") and predictor.trackers: + for tracker in predictor.trackers: + if hasattr(tracker, "tracker") and hasattr(tracker.tracker, "gmc"): + gmc = tracker.tracker.gmc + if hasattr(gmc, "executor") and gmc.executor is not None: + gmc.executor.shutdown(wait=True) + self.model.predictor = None diff --git a/dimos/perception/detection/module2D.py b/dimos/perception/detection/module2D.py new file mode 100644 index 0000000000..4bc99bab28 --- /dev/null +++ b/dimos/perception/detection/module2D.py @@ -0,0 +1,172 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from dimos_lcm.foxglove_msgs.ImageAnnotations import ( + ImageAnnotations, +) +from dimos_lcm.sensor_msgs import CameraInfo +from reactivex import operators as ops +from reactivex.observable import Observable +from reactivex.subject import Subject + +from dimos.core import In, Module, Out, rpc +from dimos.core.module import ModuleConfig +from dimos.msgs.geometry_msgs import Transform, Vector3 +from dimos.msgs.sensor_msgs import Image +from dimos.msgs.sensor_msgs.Image import sharpness_barrier +from dimos.msgs.vision_msgs import Detection2DArray +from dimos.perception.detection.detectors import Detector +from dimos.perception.detection.detectors.person.yolo import YoloPersonDetector +from dimos.perception.detection.type import ( + ImageDetections2D, +) +from dimos.utils.decorators.decorators import simple_mcache +from dimos.utils.reactive import backpressure + + +@dataclass +class Config(ModuleConfig): + max_freq: float = 10 + detector: Callable[[Any], Detector] | None = YoloPersonDetector + camera_info: CameraInfo = CameraInfo() + + +class Detection2DModule(Module): + default_config = Config + config: Config + detector: Detector + + image: In[Image] = None # type: ignore + + detections: Out[Detection2DArray] = None # type: ignore + annotations: Out[ImageAnnotations] = None # type: ignore + + detected_image_0: Out[Image] = None # type: ignore + detected_image_1: Out[Image] = None # type: ignore + detected_image_2: Out[Image] = None # type: ignore + + cnt: int = 0 + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.config: Config = Config(**kwargs) + self.detector = self.config.detector() + self.vlm_detections_subject = Subject() + self.previous_detection_count = 0 + + def process_image_frame(self, image: Image) -> ImageDetections2D: + return self.detector.process_image(image) + + @simple_mcache + def sharp_image_stream(self) -> Observable[Image]: + return backpressure( + self.image.pure_observable().pipe( + sharpness_barrier(self.config.max_freq), + ) + ) + + @simple_mcache + def detection_stream_2d(self) -> Observable[ImageDetections2D]: + return backpressure(self.image.observable().pipe(ops.map(self.process_image_frame))) + + def pixel_to_3d( + self, + pixel: tuple[int, int], + camera_info: CameraInfo, + assumed_depth: float = 1.0, + ) -> Vector3: + """Unproject 2D pixel coordinates to 3D position in camera optical frame. + + Args: + camera_info: Camera calibration information + assumed_depth: Assumed depth in meters (default 1.0m from camera) + + Returns: + Vector3 position in camera optical frame coordinates + """ + # Extract camera intrinsics + fx, fy = camera_info.K[0], camera_info.K[4] + cx, cy = camera_info.K[2], camera_info.K[5] + + # Unproject pixel to normalized camera coordinates + x_norm = (pixel[0] - cx) / fx + y_norm = (pixel[1] - cy) / fy + + # Create 3D point at assumed depth in camera optical frame + # Camera optical frame: X right, Y down, Z forward + return Vector3(x_norm * assumed_depth, y_norm * assumed_depth, assumed_depth) + + def track(self, detections: ImageDetections2D) -> None: + sensor_frame = self.tf.get("sensor", "camera_optical", detections.image.ts, 5.0) + + if not sensor_frame: + return + + if not detections.detections: + return + + sensor_frame.child_frame_id = "sensor_frame" + transforms = [sensor_frame] + + current_count = len(detections.detections) + max_count = max(current_count, self.previous_detection_count) + + # Publish transforms for all detection slots up to max_count + for index in range(max_count): + if index < current_count: + # Active detection - compute real position + detection = detections.detections[index] + position_3d = self.pixel_to_3d( + detection.center_bbox, self.config.camera_info, assumed_depth=1.0 + ) + else: + # No detection at this index - publish zero transform + position_3d = Vector3(0.0, 0.0, 0.0) + + transforms.append( + Transform( + frame_id=sensor_frame.child_frame_id, + child_frame_id=f"det_{index}", + ts=detections.image.ts, + translation=position_3d, + ) + ) + + self.previous_detection_count = current_count + self.tf.publish(*transforms) + + @rpc + def start(self) -> None: + self.detection_stream_2d().subscribe(self.track) + + self.detection_stream_2d().subscribe( + lambda det: self.detections.publish(det.to_ros_detection2d_array()) + ) + + self.detection_stream_2d().subscribe( + lambda det: self.annotations.publish(det.to_foxglove_annotations()) + ) + + def publish_cropped_images(detections: ImageDetections2D) -> None: + for index, detection in enumerate(detections[:3]): + image_topic = getattr(self, "detected_image_" + str(index)) + image_topic.publish(detection.cropped_image()) + + self.detection_stream_2d().subscribe(publish_cropped_images) + + @rpc + def stop(self) -> None: ... diff --git a/dimos/perception/detection/module3D.py b/dimos/perception/detection/module3D.py new file mode 100644 index 0000000000..9016ae6006 --- /dev/null +++ b/dimos/perception/detection/module3D.py @@ -0,0 +1,133 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from dimos_lcm.foxglove_msgs.ImageAnnotations import ImageAnnotations +from lcm_msgs.foxglove_msgs import SceneUpdate +from reactivex import operators as ops +from reactivex.observable import Observable + +from dimos.agents2 import skill +from dimos.core import In, Out, rpc +from dimos.msgs.geometry_msgs import Transform +from dimos.msgs.sensor_msgs import Image, PointCloud2 +from dimos.msgs.vision_msgs import Detection2DArray +from dimos.perception.detection.module2D import Config as Module2DConfig, Detection2DModule +from dimos.perception.detection.type import ( + ImageDetections2D, + ImageDetections3DPC, +) +from dimos.perception.detection.type.detection3d import Detection3DPC +from dimos.types.timestamped import align_timestamped +from dimos.utils.reactive import backpressure + + +class Config(Module2DConfig): ... + + +class Detection3DModule(Detection2DModule): + image: In[Image] = None # type: ignore + pointcloud: In[PointCloud2] = None # type: ignore + + detections: Out[Detection2DArray] = None # type: ignore + annotations: Out[ImageAnnotations] = None # type: ignore + scene_update: Out[SceneUpdate] = None # type: ignore + + # just for visualization, + # emits latest pointclouds of detected objects in a frame + detected_pointcloud_0: Out[PointCloud2] = None # type: ignore + detected_pointcloud_1: Out[PointCloud2] = None # type: ignore + detected_pointcloud_2: Out[PointCloud2] = None # type: ignore + + # just for visualization, emits latest top 3 detections in a frame + detected_image_0: Out[Image] = None # type: ignore + detected_image_1: Out[Image] = None # type: ignore + detected_image_2: Out[Image] = None # type: ignore + + detection_3d_stream: Observable[ImageDetections3DPC] | None = None + + def process_frame( + self, + detections: ImageDetections2D, + pointcloud: PointCloud2, + transform: Transform, + ) -> ImageDetections3DPC: + if not transform: + return ImageDetections3DPC(detections.image, []) + + detection3d_list: list[Detection3DPC] = [] + for detection in detections: + detection3d = Detection3DPC.from_2d( + detection, + world_pointcloud=pointcloud, + camera_info=self.config.camera_info, + world_to_optical_transform=transform, + ) + if detection3d is not None: + detection3d_list.append(detection3d) + + return ImageDetections3DPC(detections.image, detection3d_list) + + @skill # type: ignore[arg-type] + def ask_vlm(self, question: str) -> str | ImageDetections3DPC: + """ + query visual model about the view in front of the camera + you can ask to mark objects like: + + "red cup on the table left of the pencil" + "laptop on the desk" + "a person wearing a red shirt" + """ + from dimos.models.vl.qwen import QwenVlModel + + model = QwenVlModel() + result = model.query(self.image.get_next(), question) + + if isinstance(result, str) or not result or not len(result): + return "No detections" + + detections: ImageDetections2D = result + pc = self.pointcloud.get_next() + transform = self.tf.get("camera_optical", pc.frame_id, detections.image.ts, 5.0) + return self.process_frame(detections, pc, transform) + + @rpc + def start(self) -> None: + super().start() + + def detection2d_to_3d(args): + detections, pc = args + transform = self.tf.get("camera_optical", pc.frame_id, detections.image.ts, 5.0) + return self.process_frame(detections, pc, transform) + + self.detection_stream_3d = align_timestamped( + backpressure(self.detection_stream_2d()), + self.pointcloud.observable(), + match_tolerance=0.25, + buffer_size=20.0, + ).pipe(ops.map(detection2d_to_3d)) + + self.detection_stream_3d.subscribe(self._publish_detections) + + @rpc + def stop(self) -> None: + super().stop() + + def _publish_detections(self, detections: ImageDetections3DPC) -> None: + if not detections: + return + + for index, detection in enumerate(detections[:3]): + pointcloud_topic = getattr(self, "detected_pointcloud_" + str(index)) + pointcloud_topic.publish(detection.pointcloud) diff --git a/dimos/perception/detection/moduleDB.py b/dimos/perception/detection/moduleDB.py new file mode 100644 index 0000000000..d9cc5434ab --- /dev/null +++ b/dimos/perception/detection/moduleDB.py @@ -0,0 +1,310 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from collections.abc import Callable +from copy import copy +import threading +import time +from typing import Any + +from dimos_lcm.foxglove_msgs.ImageAnnotations import ImageAnnotations +from lcm_msgs.foxglove_msgs import SceneUpdate +from reactivex.observable import Observable + +from dimos.core import In, Out, rpc +from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Transform, Vector3 +from dimos.msgs.sensor_msgs import Image, PointCloud2 +from dimos.msgs.vision_msgs import Detection2DArray +from dimos.perception.detection.module3D import Detection3DModule +from dimos.perception.detection.type import ImageDetections3DPC, TableStr +from dimos.perception.detection.type.detection3d import Detection3DPC + + +# Represents an object in space, as collection of 3d detections over time +class Object3D(Detection3DPC): + best_detection: Detection3DPC | None = None # type: ignore + center: Vector3 | None = None # type: ignore + track_id: str | None = None # type: ignore + detections: int = 0 + + def to_repr_dict(self) -> dict[str, Any]: + if self.center is None: + center_str = "None" + else: + center_str = ( + "[" + ", ".join(list(map(lambda n: f"{n:1f}", self.center.to_list()))) + "]" + ) + return { + "object_id": self.track_id, + "detections": self.detections, + "center": center_str, + } + + def __init__( + self, track_id: str, detection: Detection3DPC | None = None, *args, **kwargs + ) -> None: + if detection is None: + return + self.ts = detection.ts + self.track_id = track_id + self.class_id = detection.class_id + self.name = detection.name + self.confidence = detection.confidence + self.pointcloud = detection.pointcloud + self.bbox = detection.bbox + self.transform = detection.transform + self.center = detection.center + self.frame_id = detection.frame_id + self.detections = self.detections + 1 + self.best_detection = detection + + def __add__(self, detection: Detection3DPC) -> "Object3D": + if self.track_id is None: + raise ValueError("Cannot add detection to object with None track_id") + new_object = Object3D(self.track_id) + new_object.bbox = detection.bbox + new_object.confidence = max(self.confidence, detection.confidence) + new_object.ts = max(self.ts, detection.ts) + new_object.track_id = self.track_id + new_object.class_id = self.class_id + new_object.name = self.name + new_object.transform = self.transform + new_object.pointcloud = self.pointcloud + detection.pointcloud + new_object.frame_id = self.frame_id + new_object.center = (self.center + detection.center) / 2 + new_object.detections = self.detections + 1 + + if detection.bbox_2d_volume() > self.bbox_2d_volume(): + new_object.best_detection = detection + else: + new_object.best_detection = self.best_detection + + return new_object + + def get_image(self) -> Image | None: + return self.best_detection.image if self.best_detection else None + + def scene_entity_label(self) -> str: + return f"{self.name} ({self.detections})" + + def agent_encode(self): + return { + "id": self.track_id, + "name": self.name, + "detections": self.detections, + "last_seen": f"{round(time.time() - self.ts)}s ago", + # "position": self.to_pose().position.agent_encode(), + } + + def to_pose(self) -> PoseStamped: + if self.best_detection is None or self.center is None: + raise ValueError("Cannot compute pose without best_detection and center") + + optical_inverse = Transform( + translation=Vector3(0.0, 0.0, 0.0), + rotation=Quaternion(-0.5, 0.5, -0.5, 0.5), + frame_id="camera_link", + child_frame_id="camera_optical", + ).inverse() + + print("transform is", self.best_detection.transform) + + global_transform = optical_inverse + self.best_detection.transform + + print("inverse optical is", global_transform) + + print("obj center is", self.center) + global_pose = global_transform.to_pose() + print("Global pose:", global_pose) + global_pose.frame_id = self.best_detection.frame_id + print("remap to", self.best_detection.frame_id) + return PoseStamped( + position=self.center, orientation=Quaternion(), frame_id=self.best_detection.frame_id + ) + + +class ObjectDBModule(Detection3DModule, TableStr): + cnt: int = 0 + objects: dict[str, Object3D] + object_stream: Observable[Object3D] | None = None + + goto: Callable[[PoseStamped], Any] | None = None + + image: In[Image] = None # type: ignore + pointcloud: In[PointCloud2] = None # type: ignore + + detections: Out[Detection2DArray] = None # type: ignore + annotations: Out[ImageAnnotations] = None # type: ignore + + detected_pointcloud_0: Out[PointCloud2] = None # type: ignore + detected_pointcloud_1: Out[PointCloud2] = None # type: ignore + detected_pointcloud_2: Out[PointCloud2] = None # type: ignore + + detected_image_0: Out[Image] = None # type: ignore + detected_image_1: Out[Image] = None # type: ignore + detected_image_2: Out[Image] = None # type: ignore + + scene_update: Out[SceneUpdate] = None # type: ignore + + target: Out[PoseStamped] = None # type: ignore + + remembered_locations: dict[str, PoseStamped] + + def __init__(self, goto: Callable[[PoseStamped], Any], *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.goto = goto + self.objects = {} + self.remembered_locations = {} + + def closest_object(self, detection: Detection3DPC) -> Object3D | None: + # Filter objects to only those with matching names + matching_objects = [obj for obj in self.objects.values() if obj.name == detection.name] + + if not matching_objects: + return None + + # Sort by distance + distances = sorted(matching_objects, key=lambda obj: detection.center.distance(obj.center)) + + return distances[0] + + def add_detections(self, detections: list[Detection3DPC]) -> list[Object3D]: + return [ + detection for detection in map(self.add_detection, detections) if detection is not None + ] + + def add_detection(self, detection: Detection3DPC): + """Add detection to existing object or create new one.""" + closest = self.closest_object(detection) + if closest and closest.bounding_box_intersects(detection): + return self.add_to_object(closest, detection) + else: + return self.create_new_object(detection) + + def add_to_object(self, closest: Object3D, detection: Detection3DPC): + new_object = closest + detection + if closest.track_id is not None: + self.objects[closest.track_id] = new_object + return new_object + + def create_new_object(self, detection: Detection3DPC): + new_object = Object3D(f"obj_{self.cnt}", detection) + if new_object.track_id is not None: + self.objects[new_object.track_id] = new_object + self.cnt += 1 + return new_object + + def agent_encode(self) -> str: + ret = [] + for obj in copy(self.objects).values(): + # we need at least 3 detectieons to consider it a valid object + # for this to be serious we need a ratio of detections within the window of observations + # if len(obj.detections) < 3: + # continue + ret.append(str(obj.agent_encode())) + if not ret: + return "No objects detected yet." + return "\n".join(ret) + + def vlm_query(self, description: str) -> Object3D | None: # type: ignore[override] + imageDetections2D = super().ask_vlm(description) + print("VLM query found", imageDetections2D, "detections") + time.sleep(3) + + if not imageDetections2D.detections: + return None + + ret = [] + for obj in self.objects.values(): + if obj.ts != imageDetections2D.ts: + print( + "Skipping", + obj.track_id, + "ts", + obj.ts, + "!=", + imageDetections2D.ts, + ) + continue + if obj.class_id != -100: + continue + if obj.name != imageDetections2D.detections[0].name: + print("Skipping", obj.name, "!=", imageDetections2D.detections[0].name) + continue + ret.append(obj) + ret.sort(key=lambda x: x.ts) + + return ret[0] if ret else None + + def lookup(self, label: str) -> list[Detection3DPC]: + """Look up a detection by label.""" + return [] + + @rpc + def start(self) -> None: + Detection3DModule.start(self) + + def update_objects(imageDetections: ImageDetections3DPC): + for detection in imageDetections.detections: + # print(detection) + return self.add_detection(detection) + + def scene_thread() -> None: + while True: + scene_update = self.to_foxglove_scene_update() + self.scene_update.publish(scene_update) + time.sleep(1.0) + + threading.Thread(target=scene_thread, daemon=True).start() + + self.detection_stream_3d.subscribe(update_objects) + + def goto_object(self, object_id: str) -> Object3D | None: + """Go to object by id.""" + return self.objects.get(object_id, None) + + def to_foxglove_scene_update(self) -> "SceneUpdate": + """Convert all detections to a Foxglove SceneUpdate message. + + Returns: + SceneUpdate containing SceneEntity objects for all detections + """ + + # Create SceneUpdate message with all detections + scene_update = SceneUpdate() + scene_update.deletions_length = 0 + scene_update.deletions = [] + scene_update.entities = [] + + for obj in copy(self.objects).values(): + # we need at least 3 detectieons to consider it a valid object + # for this to be serious we need a ratio of detections within the window of observations + # if obj.class_id != -100 and obj.detections < 2: + # continue + + # print( + # f"Object {obj.track_id}: {len(obj.detections)} detections, confidence {obj.confidence}" + # ) + # print(obj.to_pose()) + + scene_update.entities.append( + obj.to_foxglove_scene_entity( + entity_id=f"object_{obj.name}_{obj.track_id}_{obj.detections}" + ) + ) + + scene_update.entities_length = len(scene_update.entities) + return scene_update + + def __len__(self) -> int: + return len(self.objects.values()) diff --git a/dimos/perception/detection/person_tracker.py b/dimos/perception/detection/person_tracker.py new file mode 100644 index 0000000000..568214d972 --- /dev/null +++ b/dimos/perception/detection/person_tracker.py @@ -0,0 +1,115 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from reactivex import operators as ops +from reactivex.observable import Observable + +from dimos.core import In, Module, Out, rpc +from dimos.msgs.geometry_msgs import PoseStamped, Transform, Vector3 +from dimos.msgs.sensor_msgs import CameraInfo, Image +from dimos.msgs.vision_msgs import Detection2DArray +from dimos.perception.detection.type import ImageDetections2D +from dimos.types.timestamped import align_timestamped +from dimos.utils.reactive import backpressure + + +class PersonTracker(Module): + detections: In[Detection2DArray] = None # type: ignore + image: In[Image] = None # type: ignore + target: Out[PoseStamped] = None # type: ignore + + camera_info: CameraInfo + + def __init__(self, cameraInfo: CameraInfo, **kwargs) -> None: + super().__init__(**kwargs) + self.camera_info = cameraInfo + + def center_to_3d( + self, + pixel: tuple[int, int], + camera_info: CameraInfo, + assumed_depth: float = 1.0, + ) -> Vector3: + """Unproject 2D pixel coordinates to 3D position in camera_link frame. + + Args: + camera_info: Camera calibration information + assumed_depth: Assumed depth in meters (default 1.0m from camera) + + Returns: + Vector3 position in camera_link frame coordinates (Z up, X forward) + """ + # Extract camera intrinsics + fx, fy = camera_info.K[0], camera_info.K[4] + cx, cy = camera_info.K[2], camera_info.K[5] + + # Unproject pixel to normalized camera coordinates + x_norm = (pixel[0] - cx) / fx + y_norm = (pixel[1] - cy) / fy + + # Create 3D point at assumed depth in camera optical frame + # Camera optical frame: X right, Y down, Z forward + x_optical = x_norm * assumed_depth + y_optical = y_norm * assumed_depth + z_optical = assumed_depth + + # Transform from camera optical frame to camera_link frame + # Optical: X right, Y down, Z forward + # Link: X forward, Y left, Z up + # Transformation: x_link = z_optical, y_link = -x_optical, z_link = -y_optical + return Vector3(z_optical, -x_optical, -y_optical) + + def detections_stream(self) -> Observable[ImageDetections2D]: + return backpressure( + align_timestamped( + self.image.pure_observable(), + self.detections.pure_observable().pipe( + ops.filter(lambda d: d.detections_length > 0) # type: ignore[attr-defined] + ), + match_tolerance=0.0, + buffer_size=2.0, + ).pipe(ops.map(lambda pair: ImageDetections2D.from_ros_detection2d_array(*pair))) + ) + + @rpc + def start(self) -> None: + self.detections_stream().subscribe(self.track) + + @rpc + def stop(self) -> None: + super().stop() + + def track(self, detections2D: ImageDetections2D) -> None: + if len(detections2D) == 0: + return + + target = max(detections2D.detections, key=lambda det: det.bbox_2d_volume()) + vector = self.center_to_3d(target.center_bbox, self.camera_info, 2.0) + + pose_in_camera = PoseStamped( + ts=detections2D.ts, + position=vector, + frame_id="camera_link", + ) + + tf_world_to_camera = self.tf.get("world", "camera_link", detections2D.ts, 5.0) + if not tf_world_to_camera: + return + + tf_camera_to_target = Transform.from_pose("target", pose_in_camera) + tf_world_to_target = tf_world_to_camera + tf_camera_to_target + pose_in_world = tf_world_to_target.to_pose(ts=detections2D.ts) + + self.target.publish(pose_in_world) diff --git a/dimos/perception/detection/reid/__init__.py b/dimos/perception/detection/reid/__init__.py new file mode 100644 index 0000000000..31d50a894b --- /dev/null +++ b/dimos/perception/detection/reid/__init__.py @@ -0,0 +1,13 @@ +from dimos.perception.detection.reid.embedding_id_system import EmbeddingIDSystem +from dimos.perception.detection.reid.module import Config, ReidModule +from dimos.perception.detection.reid.type import IDSystem, PassthroughIDSystem + +__all__ = [ + "Config", + "EmbeddingIDSystem", + # ID Systems + "IDSystem", + "PassthroughIDSystem", + # Module + "ReidModule", +] diff --git a/dimos/perception/detection/reid/embedding_id_system.py b/dimos/perception/detection/reid/embedding_id_system.py new file mode 100644 index 0000000000..c1c406fe56 --- /dev/null +++ b/dimos/perception/detection/reid/embedding_id_system.py @@ -0,0 +1,264 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Callable +from typing import Literal + +import numpy as np + +from dimos.models.embedding.base import Embedding, EmbeddingModel +from dimos.perception.detection.reid.type import IDSystem +from dimos.perception.detection.type import Detection2DBBox + + +class EmbeddingIDSystem(IDSystem): + """Associates short-term track_ids to long-term unique detection IDs via embedding similarity. + + Maintains: + - All embeddings per track_id (as numpy arrays) for robust group comparison + - Negative constraints from co-occurrence (tracks in same frame = different objects) + - Mapping from track_id to unique long-term ID + """ + + def __init__( + self, + model: Callable[[], EmbeddingModel[Embedding]], + padding: int = 0, + similarity_threshold: float = 0.63, + comparison_mode: Literal["max", "mean", "top_k_mean"] = "top_k_mean", + top_k: int = 30, + max_embeddings_per_track: int = 500, + min_embeddings_for_matching: int = 10, + ) -> None: + """Initialize track associator. + + Args: + model: Callable (class or function) that returns an embedding model for feature extraction + padding: Padding to add around detection bbox when cropping (default: 0) + similarity_threshold: Minimum similarity for associating tracks (0-1) + comparison_mode: How to aggregate similarities between embedding groups + - "max": Use maximum similarity between any pair + - "mean": Use mean of all pairwise similarities + - "top_k_mean": Use mean of top-k similarities + top_k: Number of top similarities to average (if using top_k_mean) + max_embeddings_per_track: Maximum number of embeddings to keep per track + min_embeddings_for_matching: Minimum embeddings before attempting to match tracks + """ + # Call model factory (class or function) to get model instance + self.model = model() + + # Call warmup if available + if hasattr(self.model, "warmup"): + self.model.warmup() + + self.padding = padding + self.similarity_threshold = similarity_threshold + self.comparison_mode = comparison_mode + self.top_k = top_k + self.max_embeddings_per_track = max_embeddings_per_track + self.min_embeddings_for_matching = min_embeddings_for_matching + + # Track embeddings (list of all embeddings as numpy arrays) + self.track_embeddings: dict[int, list[np.ndarray]] = {} + + # Negative constraints (track_ids that co-occurred = different objects) + self.negative_pairs: dict[int, set[int]] = {} + + # Track ID to long-term unique ID mapping + self.track_to_long_term: dict[int, int] = {} + self.long_term_counter: int = 0 + + # Similarity history for optional adaptive thresholding + self.similarity_history: list[float] = [] + + def register_detection(self, detection: Detection2DBBox) -> int: + """ + Register detection and return long-term ID. + + Args: + detection: Detection to register + + Returns: + Long-term unique ID for this detection + """ + # Extract embedding from detection's cropped image + cropped_image = detection.cropped_image(padding=self.padding) + embedding = self.model.embed(cropped_image) + assert not isinstance(embedding, list), "Expected single embedding for single image" + # Move embedding to CPU immediately to free GPU memory + embedding = embedding.to_cpu() + + # Update and associate track + self.update_embedding(detection.track_id, embedding) + return self.associate(detection.track_id) + + def update_embedding(self, track_id: int, new_embedding: Embedding) -> None: + """Add new embedding to track's embedding collection. + + Args: + track_id: Short-term track ID from detector + new_embedding: New embedding to add to collection + """ + # Convert to numpy array (already on CPU from feature extractor) + new_vec = new_embedding.to_numpy() + + # Ensure normalized for cosine similarity + norm = np.linalg.norm(new_vec) + if norm > 0: + new_vec = new_vec / norm + + if track_id not in self.track_embeddings: + self.track_embeddings[track_id] = [] + + embeddings = self.track_embeddings[track_id] + embeddings.append(new_vec) + + # Keep only most recent embeddings if limit exceeded + if len(embeddings) > self.max_embeddings_per_track: + embeddings.pop(0) # Remove oldest + + def _compute_group_similarity( + self, query_embeddings: list[np.ndarray], candidate_embeddings: list[np.ndarray] + ) -> float: + """Compute similarity between two groups of embeddings. + + Args: + query_embeddings: List of embeddings for query track + candidate_embeddings: List of embeddings for candidate track + + Returns: + Aggregated similarity score + """ + # Compute all pairwise similarities efficiently + query_matrix = np.stack(query_embeddings) # [M, D] + candidate_matrix = np.stack(candidate_embeddings) # [N, D] + + # Cosine similarity via matrix multiplication (already normalized) + similarities = query_matrix @ candidate_matrix.T # [M, N] + + if self.comparison_mode == "max": + # Maximum similarity across all pairs + return float(np.max(similarities)) + + elif self.comparison_mode == "mean": + # Mean of all pairwise similarities + return float(np.mean(similarities)) + + elif self.comparison_mode == "top_k_mean": + # Mean of top-k similarities + flat_sims = similarities.flatten() + k = min(self.top_k, len(flat_sims)) + top_k_sims = np.partition(flat_sims, -k)[-k:] + return float(np.mean(top_k_sims)) + + else: + raise ValueError(f"Unknown comparison mode: {self.comparison_mode}") + + def add_negative_constraints(self, track_ids: list[int]) -> None: + """Record that these track_ids co-occurred in same frame (different objects). + + Args: + track_ids: List of track_ids present in current frame + """ + # All pairs of track_ids in same frame can't be same object + for i, tid1 in enumerate(track_ids): + for tid2 in track_ids[i + 1 :]: + self.negative_pairs.setdefault(tid1, set()).add(tid2) + self.negative_pairs.setdefault(tid2, set()).add(tid1) + + def associate(self, track_id: int) -> int: + """Associate track_id to long-term unique detection ID. + + Args: + track_id: Short-term track ID to associate + + Returns: + Long-term unique detection ID + """ + # Already has assignment + if track_id in self.track_to_long_term: + return self.track_to_long_term[track_id] + + # Need embeddings to compare + if track_id not in self.track_embeddings or not self.track_embeddings[track_id]: + # Create new ID if no embeddings yet + new_id = self.long_term_counter + self.long_term_counter += 1 + self.track_to_long_term[track_id] = new_id + return new_id + + # Get query embeddings + query_embeddings = self.track_embeddings[track_id] + + # Don't attempt matching until we have enough embeddings for the query track + if len(query_embeddings) < self.min_embeddings_for_matching: + # Not ready yet - return -1 + return -1 + + # Build candidate list (only tracks with assigned long_term_ids) + best_similarity = -1.0 + best_track_id = None + + for other_tid, other_embeddings in self.track_embeddings.items(): + # Skip self + if other_tid == track_id: + continue + + # Skip if negative constraint (co-occurred) + if other_tid in self.negative_pairs.get(track_id, set()): + continue + + # Skip if no long_term_id yet + if other_tid not in self.track_to_long_term: + continue + + # Skip if not enough embeddings + if len(other_embeddings) < self.min_embeddings_for_matching: + continue + + # Compute group similarity + similarity = self._compute_group_similarity(query_embeddings, other_embeddings) + + if similarity > best_similarity: + best_similarity = similarity + best_track_id = other_tid + + # Check if best match exceeds threshold + if best_track_id is not None and best_similarity >= self.similarity_threshold: + matched_long_term_id = self.track_to_long_term[best_track_id] + print( + f"Track {track_id}: matched with track {best_track_id} " + f"(long_term_id={matched_long_term_id}, similarity={best_similarity:.4f}, " + f"mode={self.comparison_mode}, embeddings: {len(query_embeddings)} vs {len(self.track_embeddings[best_track_id])}), threshold: {self.similarity_threshold}" + ) + + # Track similarity history + self.similarity_history.append(best_similarity) + + # Associate with existing long_term_id + self.track_to_long_term[track_id] = matched_long_term_id + return matched_long_term_id + + # Create new unique detection ID + new_id = self.long_term_counter + self.long_term_counter += 1 + self.track_to_long_term[track_id] = new_id + + if best_track_id is not None: + print( + f"Track {track_id}: creating new ID {new_id} " + f"(best similarity={best_similarity:.4f} with id={self.track_to_long_term[best_track_id]} below threshold={self.similarity_threshold})" + ) + + return new_id diff --git a/dimos/perception/detection/reid/module.py b/dimos/perception/detection/reid/module.py new file mode 100644 index 0000000000..3cef9f2ff2 --- /dev/null +++ b/dimos/perception/detection/reid/module.py @@ -0,0 +1,112 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos_lcm.foxglove_msgs.ImageAnnotations import ( + ImageAnnotations, + TextAnnotation, +) +from dimos_lcm.foxglove_msgs.Point2 import Point2 +from reactivex import operators as ops +from reactivex.observable import Observable + +from dimos.core import In, Module, ModuleConfig, Out, rpc +from dimos.msgs.foxglove_msgs.Color import Color +from dimos.msgs.sensor_msgs import Image +from dimos.msgs.vision_msgs import Detection2DArray +from dimos.perception.detection.reid.embedding_id_system import EmbeddingIDSystem +from dimos.perception.detection.reid.type import IDSystem +from dimos.perception.detection.type import ImageDetections2D +from dimos.types.timestamped import align_timestamped, to_ros_stamp +from dimos.utils.reactive import backpressure + + +class Config(ModuleConfig): + idsystem: IDSystem + + +class ReidModule(Module): + default_config = Config + + detections: In[Detection2DArray] = None # type: ignore + image: In[Image] = None # type: ignore + annotations: Out[ImageAnnotations] = None # type: ignore + + def __init__(self, idsystem: IDSystem | None = None, **kwargs) -> None: + super().__init__(**kwargs) + if idsystem is None: + try: + from dimos.models.embedding import TorchReIDModel + + idsystem = EmbeddingIDSystem(model=TorchReIDModel, padding=0) + except Exception as e: + raise RuntimeError( + "TorchReIDModel not available. Please install with: pip install dimos[torchreid]" + ) from e + + self.idsystem = idsystem + + def detections_stream(self) -> Observable[ImageDetections2D]: + return backpressure( + align_timestamped( + self.image.pure_observable(), + self.detections.pure_observable().pipe( + ops.filter(lambda d: d.detections_length > 0) # type: ignore[attr-defined] + ), + match_tolerance=0.0, + buffer_size=2.0, + ).pipe(ops.map(lambda pair: ImageDetections2D.from_ros_detection2d_array(*pair))) # type: ignore[misc] + ) + + @rpc + def start(self) -> None: + self.detections_stream().subscribe(self.ingress) + + @rpc + def stop(self) -> None: + super().stop() + + def ingress(self, imageDetections: ImageDetections2D) -> None: + text_annotations = [] + + for detection in imageDetections: + # Register detection and get long-term ID + long_term_id = self.idsystem.register_detection(detection) + + # Skip annotation if not ready yet (long_term_id == -1) + if long_term_id == -1: + continue + + # Create text annotation for long_term_id above the detection + x1, y1, _, _ = detection.bbox + font_size = imageDetections.image.width / 60 + + text_annotations.append( + TextAnnotation( + timestamp=to_ros_stamp(detection.ts), + position=Point2(x=x1, y=y1 - font_size * 1.5), + text=f"PERSON: {long_term_id}", + font_size=font_size, + text_color=Color(r=0.0, g=1.0, b=1.0, a=1.0), # Cyan + background_color=Color(r=0.0, g=0.0, b=0.0, a=0.8), + ) + ) + + # Publish annotations (even if empty to clear previous annotations) + annotations = ImageAnnotations( + texts=text_annotations, + texts_length=len(text_annotations), + points=[], + points_length=0, + ) + self.annotations.publish(annotations) diff --git a/dimos/perception/detection/reid/test_embedding_id_system.py b/dimos/perception/detection/reid/test_embedding_id_system.py new file mode 100644 index 0000000000..840ecb2fb8 --- /dev/null +++ b/dimos/perception/detection/reid/test_embedding_id_system.py @@ -0,0 +1,270 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import torch + +from dimos.msgs.sensor_msgs import Image +from dimos.perception.detection.reid.embedding_id_system import EmbeddingIDSystem +from dimos.utils.data import get_data + + +@pytest.fixture(scope="session") +def mobileclip_model(): + """Load MobileCLIP model once for all tests.""" + from dimos.models.embedding.mobileclip import MobileCLIPModel + + model_path = get_data("models_mobileclip") / "mobileclip2_s0.pt" + model = MobileCLIPModel(model_name="MobileCLIP2-S0", model_path=model_path) + model.warmup() + return model + + +@pytest.fixture +def track_associator(mobileclip_model): + """Create fresh EmbeddingIDSystem for each test.""" + return EmbeddingIDSystem(model=lambda: mobileclip_model, similarity_threshold=0.75) + + +@pytest.fixture(scope="session") +def test_image(): + """Load test image.""" + return Image.from_file(get_data("cafe.jpg")).to_rgb() + + +@pytest.mark.gpu +def test_update_embedding_single(track_associator, mobileclip_model, test_image) -> None: + """Test updating embedding for a single track.""" + embedding = mobileclip_model.embed(test_image) + + # First update + track_associator.update_embedding(track_id=1, new_embedding=embedding) + + assert 1 in track_associator.track_embeddings + assert track_associator.embedding_counts[1] == 1 + + # Verify embedding is on device and normalized + emb_vec = track_associator.track_embeddings[1] + assert isinstance(emb_vec, torch.Tensor) + assert emb_vec.device.type in ["cuda", "cpu"] + norm = torch.norm(emb_vec).item() + assert abs(norm - 1.0) < 0.01, "Embedding should be normalized" + + +@pytest.mark.gpu +def test_update_embedding_running_average(track_associator, mobileclip_model, test_image) -> None: + """Test running average of embeddings.""" + embedding1 = mobileclip_model.embed(test_image) + embedding2 = mobileclip_model.embed(test_image) + + # Add first embedding + track_associator.update_embedding(track_id=1, new_embedding=embedding1) + first_vec = track_associator.track_embeddings[1].clone() + + # Add second embedding (same image, should be very similar) + track_associator.update_embedding(track_id=1, new_embedding=embedding2) + avg_vec = track_associator.track_embeddings[1] + + assert track_associator.embedding_counts[1] == 2 + + # Average should still be normalized + norm = torch.norm(avg_vec).item() + assert abs(norm - 1.0) < 0.01, "Average embedding should be normalized" + + # Average should be similar to both originals (same image) + similarity1 = (first_vec @ avg_vec).item() + assert similarity1 > 0.99, "Average should be very similar to original" + + +@pytest.mark.gpu +def test_negative_constraints(track_associator) -> None: + """Test negative constraint recording.""" + # Simulate frame with 3 tracks + track_ids = [1, 2, 3] + track_associator.add_negative_constraints(track_ids) + + # Check that all pairs are recorded + assert 2 in track_associator.negative_pairs[1] + assert 3 in track_associator.negative_pairs[1] + assert 1 in track_associator.negative_pairs[2] + assert 3 in track_associator.negative_pairs[2] + assert 1 in track_associator.negative_pairs[3] + assert 2 in track_associator.negative_pairs[3] + + +@pytest.mark.gpu +def test_associate_new_track(track_associator, mobileclip_model, test_image) -> None: + """Test associating a new track creates new long_term_id.""" + embedding = mobileclip_model.embed(test_image) + track_associator.update_embedding(track_id=1, new_embedding=embedding) + + # First association should create new long_term_id + long_term_id = track_associator.associate(track_id=1) + + assert long_term_id == 0, "First track should get long_term_id=0" + assert track_associator.track_to_long_term[1] == 0 + assert track_associator.long_term_counter == 1 + + +@pytest.mark.gpu +def test_associate_similar_tracks(track_associator, mobileclip_model, test_image) -> None: + """Test associating similar tracks to same long_term_id.""" + # Create embeddings from same image (should be very similar) + embedding1 = mobileclip_model.embed(test_image) + embedding2 = mobileclip_model.embed(test_image) + + # Add first track + track_associator.update_embedding(track_id=1, new_embedding=embedding1) + long_term_id_1 = track_associator.associate(track_id=1) + + # Add second track with similar embedding + track_associator.update_embedding(track_id=2, new_embedding=embedding2) + long_term_id_2 = track_associator.associate(track_id=2) + + # Should get same long_term_id (similarity > 0.75) + assert long_term_id_1 == long_term_id_2, "Similar tracks should get same long_term_id" + assert track_associator.long_term_counter == 1, "Only one long_term_id should be created" + + +@pytest.mark.gpu +def test_associate_with_negative_constraint(track_associator, mobileclip_model, test_image) -> None: + """Test that negative constraints prevent association.""" + # Create similar embeddings + embedding1 = mobileclip_model.embed(test_image) + embedding2 = mobileclip_model.embed(test_image) + + # Add first track + track_associator.update_embedding(track_id=1, new_embedding=embedding1) + long_term_id_1 = track_associator.associate(track_id=1) + + # Add negative constraint (tracks co-occurred) + track_associator.add_negative_constraints([1, 2]) + + # Add second track with similar embedding + track_associator.update_embedding(track_id=2, new_embedding=embedding2) + long_term_id_2 = track_associator.associate(track_id=2) + + # Should get different long_term_ids despite high similarity + assert long_term_id_1 != long_term_id_2, ( + "Co-occurring tracks should get different long_term_ids" + ) + assert track_associator.long_term_counter == 2, "Two long_term_ids should be created" + + +@pytest.mark.gpu +def test_associate_different_objects(track_associator, mobileclip_model, test_image) -> None: + """Test that dissimilar embeddings get different long_term_ids.""" + # Create embeddings for image and text (very different) + image_emb = mobileclip_model.embed(test_image) + text_emb = mobileclip_model.embed_text("a dog") + + # Add first track (image) + track_associator.update_embedding(track_id=1, new_embedding=image_emb) + long_term_id_1 = track_associator.associate(track_id=1) + + # Add second track (text - very different embedding) + track_associator.update_embedding(track_id=2, new_embedding=text_emb) + long_term_id_2 = track_associator.associate(track_id=2) + + # Should get different long_term_ids (similarity < 0.75) + assert long_term_id_1 != long_term_id_2, "Different objects should get different long_term_ids" + assert track_associator.long_term_counter == 2 + + +@pytest.mark.gpu +def test_associate_returns_cached(track_associator, mobileclip_model, test_image) -> None: + """Test that repeated calls return same long_term_id.""" + embedding = mobileclip_model.embed(test_image) + track_associator.update_embedding(track_id=1, new_embedding=embedding) + + # First call + long_term_id_1 = track_associator.associate(track_id=1) + + # Second call should return cached result + long_term_id_2 = track_associator.associate(track_id=1) + + assert long_term_id_1 == long_term_id_2 + assert track_associator.long_term_counter == 1, "Should not create new ID" + + +@pytest.mark.gpu +def test_associate_not_ready(track_associator) -> None: + """Test that associate returns -1 for track without embedding.""" + long_term_id = track_associator.associate(track_id=999) + assert long_term_id == -1, "Should return -1 for track without embedding" + + +@pytest.mark.gpu +def test_gpu_performance(track_associator, mobileclip_model, test_image) -> None: + """Test that embeddings stay on GPU for performance.""" + embedding = mobileclip_model.embed(test_image) + track_associator.update_embedding(track_id=1, new_embedding=embedding) + + # Embedding should stay on device + emb_vec = track_associator.track_embeddings[1] + assert isinstance(emb_vec, torch.Tensor) + # Device comparison (handle "cuda" vs "cuda:0") + expected_device = mobileclip_model.device + assert emb_vec.device.type == torch.device(expected_device).type + + # Running average should happen on GPU + embedding2 = mobileclip_model.embed(test_image) + track_associator.update_embedding(track_id=1, new_embedding=embedding2) + + avg_vec = track_associator.track_embeddings[1] + assert avg_vec.device.type == torch.device(expected_device).type + + +@pytest.mark.gpu +def test_similarity_threshold_configurable(mobileclip_model) -> None: + """Test that similarity threshold is configurable.""" + associator_strict = EmbeddingIDSystem(model=lambda: mobileclip_model, similarity_threshold=0.95) + associator_loose = EmbeddingIDSystem(model=lambda: mobileclip_model, similarity_threshold=0.50) + + assert associator_strict.similarity_threshold == 0.95 + assert associator_loose.similarity_threshold == 0.50 + + +@pytest.mark.gpu +def test_multi_track_scenario(track_associator, mobileclip_model, test_image) -> None: + """Test realistic scenario with multiple tracks across frames.""" + # Frame 1: Track 1 appears + emb1 = mobileclip_model.embed(test_image) + track_associator.update_embedding(1, emb1) + track_associator.add_negative_constraints([1]) + lt1 = track_associator.associate(1) + + # Frame 2: Track 1 and Track 2 appear (different objects) + text_emb = mobileclip_model.embed_text("a dog") + track_associator.update_embedding(1, emb1) # Update average + track_associator.update_embedding(2, text_emb) + track_associator.add_negative_constraints([1, 2]) # Co-occur = different + lt2 = track_associator.associate(2) + + # Track 2 should get different ID despite any similarity + assert lt1 != lt2 + + # Frame 3: Track 1 disappears, Track 3 appears (same as Track 1) + emb3 = mobileclip_model.embed(test_image) + track_associator.update_embedding(3, emb3) + track_associator.add_negative_constraints([2, 3]) + lt3 = track_associator.associate(3) + + # Track 3 should match Track 1 (not co-occurring, similar embedding) + assert lt3 == lt1 + + print("\nMulti-track scenario results:") + print(f" Track 1 -> long_term_id {lt1}") + print(f" Track 2 -> long_term_id {lt2} (different object, co-occurred)") + print(f" Track 3 -> long_term_id {lt3} (re-identified as Track 1)") diff --git a/dimos/perception/detection/reid/test_module.py b/dimos/perception/detection/reid/test_module.py new file mode 100644 index 0000000000..cd580a1111 --- /dev/null +++ b/dimos/perception/detection/reid/test_module.py @@ -0,0 +1,44 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from dimos.core import LCMTransport +from dimos.msgs.foxglove_msgs import ImageAnnotations +from dimos.perception.detection.reid.embedding_id_system import EmbeddingIDSystem +from dimos.perception.detection.reid.module import ReidModule + + +@pytest.mark.tool +def test_reid_ingress(imageDetections2d) -> None: + try: + from dimos.models.embedding import TorchReIDModel + except Exception: + pytest.skip("TorchReIDModel not available") + + # Create TorchReID-based IDSystem for testing + reid_model = TorchReIDModel(model_name="osnet_x1_0") + reid_model.warmup() + idsystem = EmbeddingIDSystem( + model=lambda: reid_model, + padding=20, + similarity_threshold=0.75, + ) + + reid_module = ReidModule(idsystem=idsystem, warmup=False) + print("Processing detections through ReidModule...") + reid_module.annotations._transport = LCMTransport("/annotations", ImageAnnotations) + reid_module.ingress(imageDetections2d) + reid_module._close_module() + print("✓ ReidModule ingress test completed successfully") diff --git a/dimos/perception/detection/reid/type.py b/dimos/perception/detection/reid/type.py new file mode 100644 index 0000000000..0ef2da961c --- /dev/null +++ b/dimos/perception/detection/reid/type.py @@ -0,0 +1,50 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from abc import ABC, abstractmethod + +from dimos.perception.detection.type import Detection2DBBox, ImageDetections2D + + +class IDSystem(ABC): + """Abstract base class for ID assignment systems.""" + + def register_detections(self, detections: ImageDetections2D) -> None: + """Register multiple detections.""" + for detection in detections.detections: + if isinstance(detection, Detection2DBBox): + self.register_detection(detection) + + @abstractmethod + def register_detection(self, detection: Detection2DBBox) -> int: + """ + Register a single detection, returning assigned (long term) ID. + + Args: + detection: Detection to register + + Returns: + Long-term unique ID for this detection + """ + ... + + +class PassthroughIDSystem(IDSystem): + """Simple ID system that returns track_id with no object permanence.""" + + def register_detection(self, detection: Detection2DBBox) -> int: + """Return detection's track_id as long-term ID (no permanence).""" + return detection.track_id diff --git a/dimos/perception/detection/test_moduleDB.py b/dimos/perception/detection/test_moduleDB.py new file mode 100644 index 0000000000..62c72b7ded --- /dev/null +++ b/dimos/perception/detection/test_moduleDB.py @@ -0,0 +1,61 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import time + +from lcm_msgs.foxglove_msgs import SceneUpdate +import pytest + +from dimos.core import LCMTransport +from dimos.msgs.foxglove_msgs import ImageAnnotations +from dimos.msgs.geometry_msgs import PoseStamped +from dimos.msgs.sensor_msgs import Image, PointCloud2 +from dimos.msgs.vision_msgs import Detection2DArray +from dimos.perception.detection.moduleDB import ObjectDBModule +from dimos.robot.unitree_webrtc.modular import deploy_connection +from dimos.robot.unitree_webrtc.modular.connection_module import ConnectionModule + + +@pytest.mark.module +def test_moduleDB(dimos_cluster) -> None: + connection = deploy_connection(dimos_cluster) + + moduleDB = dimos_cluster.deploy( + ObjectDBModule, + camera_info=ConnectionModule._camera_info(), + goto=lambda obj_id: print(f"Going to {obj_id}"), + ) + moduleDB.image.connect(connection.video) + moduleDB.pointcloud.connect(connection.lidar) + + moduleDB.annotations.transport = LCMTransport("/annotations", ImageAnnotations) + moduleDB.detections.transport = LCMTransport("/detections", Detection2DArray) + + moduleDB.detected_pointcloud_0.transport = LCMTransport("/detected/pointcloud/0", PointCloud2) + moduleDB.detected_pointcloud_1.transport = LCMTransport("/detected/pointcloud/1", PointCloud2) + moduleDB.detected_pointcloud_2.transport = LCMTransport("/detected/pointcloud/2", PointCloud2) + + moduleDB.detected_image_0.transport = LCMTransport("/detected/image/0", Image) + moduleDB.detected_image_1.transport = LCMTransport("/detected/image/1", Image) + moduleDB.detected_image_2.transport = LCMTransport("/detected/image/2", Image) + + moduleDB.scene_update.transport = LCMTransport("/scene_update", SceneUpdate) + moduleDB.target.transport = LCMTransport("/target", PoseStamped) + + connection.start() + moduleDB.start() + + time.sleep(4) + print("STARTING QUERY!!") + print("VLM RES", moduleDB.navigate_to_object_in_view("white floor")) + time.sleep(30) diff --git a/dimos/perception/detection/type/__init__.py b/dimos/perception/detection/type/__init__.py new file mode 100644 index 0000000000..04589441ec --- /dev/null +++ b/dimos/perception/detection/type/__init__.py @@ -0,0 +1,41 @@ +from dimos.perception.detection.type.detection2d import ( + Detection2D, + Detection2DBBox, + Detection2DPerson, + ImageDetections2D, +) +from dimos.perception.detection.type.detection3d import ( + Detection3D, + Detection3DBBox, + Detection3DPC, + ImageDetections3DPC, + PointCloudFilter, + height_filter, + radius_outlier, + raycast, + statistical, +) +from dimos.perception.detection.type.imageDetections import ImageDetections +from dimos.perception.detection.type.utils import TableStr + +__all__ = [ + # 2D Detection types + "Detection2D", + "Detection2DBBox", + "Detection2DPerson", + # 3D Detection types + "Detection3D", + "Detection3DBBox", + "Detection3DPC", + # Base types + "ImageDetections", + "ImageDetections2D", + "ImageDetections3DPC", + # Point cloud filters + "PointCloudFilter", + "TableStr", + "height_filter", + "radius_outlier", + "raycast", + "statistical", +] diff --git a/dimos/perception/detection/type/detection2d/__init__.py b/dimos/perception/detection/type/detection2d/__init__.py new file mode 100644 index 0000000000..1db1a8c384 --- /dev/null +++ b/dimos/perception/detection/type/detection2d/__init__.py @@ -0,0 +1,25 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.perception.detection.type.detection2d.base import Detection2D +from dimos.perception.detection.type.detection2d.bbox import Detection2DBBox +from dimos.perception.detection.type.detection2d.imageDetections2D import ImageDetections2D +from dimos.perception.detection.type.detection2d.person import Detection2DPerson + +__all__ = [ + "Detection2D", + "Detection2DBBox", + "Detection2DPerson", + "ImageDetections2D", +] diff --git a/dimos/perception/detection/type/detection2d/base.py b/dimos/perception/detection/type/detection2d/base.py new file mode 100644 index 0000000000..5cba3d673f --- /dev/null +++ b/dimos/perception/detection/type/detection2d/base.py @@ -0,0 +1,51 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import abstractmethod + +from dimos_lcm.foxglove_msgs.ImageAnnotations import PointsAnnotation, TextAnnotation +from dimos_lcm.vision_msgs import Detection2D as ROSDetection2D + +from dimos.msgs.foxglove_msgs import ImageAnnotations +from dimos.msgs.sensor_msgs import Image +from dimos.types.timestamped import Timestamped + + +class Detection2D(Timestamped): + """Abstract base class for 2D detections.""" + + @abstractmethod + def cropped_image(self, padding: int = 20) -> Image: + """Return a cropped version of the image focused on the detection area.""" + ... + + @abstractmethod + def to_image_annotations(self) -> ImageAnnotations: + """Convert detection to Foxglove ImageAnnotations for visualization.""" + ... + + @abstractmethod + def to_text_annotation(self) -> list[TextAnnotation]: + """Return text annotations for visualization.""" + ... + + @abstractmethod + def to_points_annotation(self) -> list[PointsAnnotation]: + """Return points/shape annotations for visualization.""" + ... + + @abstractmethod + def to_ros_detection2d(self) -> ROSDetection2D: + """Convert detection to ROS Detection2D message.""" + ... diff --git a/dimos/perception/detection/type/detection2d/bbox.py b/dimos/perception/detection/type/detection2d/bbox.py new file mode 100644 index 0000000000..46e8fe2cc7 --- /dev/null +++ b/dimos/perception/detection/type/detection2d/bbox.py @@ -0,0 +1,406 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass +import hashlib +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from ultralytics.engine.results import Results + + from dimos.msgs.sensor_msgs import Image + +from dimos_lcm.foxglove_msgs.ImageAnnotations import ( + PointsAnnotation, + TextAnnotation, +) +from dimos_lcm.foxglove_msgs.Point2 import Point2 +from dimos_lcm.vision_msgs import ( + BoundingBox2D, + Detection2D as ROSDetection2D, + ObjectHypothesis, + ObjectHypothesisWithPose, + Point2D, + Pose2D, +) +from rich.console import Console +from rich.text import Text + +from dimos.msgs.foxglove_msgs import ImageAnnotations +from dimos.msgs.foxglove_msgs.Color import Color +from dimos.msgs.std_msgs import Header +from dimos.perception.detection.type.detection2d.base import Detection2D +from dimos.types.timestamped import to_ros_stamp, to_timestamp +from dimos.utils.decorators.decorators import simple_mcache + +Bbox = tuple[float, float, float, float] +CenteredBbox = tuple[float, float, float, float] + + +def _hash_to_color(name: str) -> str: + """Generate a consistent color for a given name using hash.""" + # List of rich colors to choose from + colors = [ + "cyan", + "magenta", + "yellow", + "blue", + "green", + "red", + "bright_cyan", + "bright_magenta", + "bright_yellow", + "bright_blue", + "bright_green", + "bright_red", + "purple", + "white", + "pink", + ] + + # Hash the name and pick a color + hash_value = hashlib.md5(name.encode()).digest()[0] + return colors[hash_value % len(colors)] + + +@dataclass +class Detection2DBBox(Detection2D): + bbox: Bbox + track_id: int + class_id: int + confidence: float + name: str + ts: float + image: Image + + def to_repr_dict(self) -> dict[str, Any]: + """Return a dictionary representation of the detection for display purposes.""" + x1, y1, x2, y2 = self.bbox + return { + "name": self.name, + "class": str(self.class_id), + "track": str(self.track_id), + "conf": f"{self.confidence:.2f}", + "bbox": f"[{x1:.0f},{y1:.0f},{x2:.0f},{y2:.0f}]", + } + + def center_to_3d( + self, + pixel: tuple[int, int], + camera_info: CameraInfo, + assumed_depth: float = 1.0, + ) -> PoseStamped: + """Unproject 2D pixel coordinates to 3D position in camera optical frame. + + Args: + camera_info: Camera calibration information + assumed_depth: Assumed depth in meters (default 1.0m from camera) + + Returns: + Vector3 position in camera optical frame coordinates + """ + # Extract camera intrinsics + fx, fy = camera_info.K[0], camera_info.K[4] + cx, cy = camera_info.K[2], camera_info.K[5] + + # Unproject pixel to normalized camera coordinates + x_norm = (pixel[0] - cx) / fx + y_norm = (pixel[1] - cy) / fy + + # Create 3D point at assumed depth in camera optical frame + # Camera optical frame: X right, Y down, Z forward + return Vector3(x_norm * assumed_depth, y_norm * assumed_depth, assumed_depth) + + # return focused image, only on the bbox + def cropped_image(self, padding: int = 20) -> Image: + """Return a cropped version of the image focused on the bounding box. + + Args: + padding: Pixels to add around the bounding box (default: 20) + + Returns: + Cropped Image containing only the detection area plus padding + """ + x1, y1, x2, y2 = map(int, self.bbox) + return self.image.crop( + x1 - padding, y1 - padding, x2 - x1 + 2 * padding, y2 - y1 + 2 * padding + ) + + def __str__(self) -> str: + console = Console(force_terminal=True, legacy_windows=False) + d = self.to_repr_dict() + + # Build the string representation + parts = [ + Text(f"{self.__class__.__name__}("), + ] + + # Add any extra fields (e.g., points for Detection3D) + extra_keys = [k for k in d.keys() if k not in ["class"]] + for key in extra_keys: + if d[key] == "None": + parts.append(Text(f"{key}={d[key]}", style="dim")) + else: + parts.append(Text(f"{key}={d[key]}", style=_hash_to_color(key))) + + parts.append(Text(")")) + + # Render to string + with console.capture() as capture: + console.print(*parts, end="") + return capture.get().strip() + + @property + def center_bbox(self) -> tuple[float, float]: + """Get center point of bounding box.""" + x1, y1, x2, y2 = self.bbox + return ((x1 + x2) / 2, (y1 + y2) / 2) + + def bbox_2d_volume(self) -> float: + x1, y1, x2, y2 = self.bbox + width = max(0.0, x2 - x1) + height = max(0.0, y2 - y1) + return width * height + + @simple_mcache + def is_valid(self) -> bool: + """Check if detection bbox is valid. + + Validates that: + - Bounding box has positive dimensions + - Bounding box is within image bounds (if image has shape) + + Returns: + True if bbox is valid, False otherwise + """ + x1, y1, x2, y2 = self.bbox + + # Check positive dimensions + if x2 <= x1 or y2 <= y1: + return False + + # Check if within image bounds (if image has shape) + if self.image.shape: + h, w = self.image.shape[:2] + if not (0 <= x1 <= w and 0 <= y1 <= h and 0 <= x2 <= w and 0 <= y2 <= h): + return False + + return True + + @classmethod + def from_ultralytics_result(cls, result: Results, idx: int, image: Image) -> Detection2DBBox: + """Create Detection2DBBox from ultralytics Results object. + + Args: + result: Ultralytics Results object containing detection data + idx: Index of the detection in the results + image: Source image + + Returns: + Detection2DBBox instance + """ + if result.boxes is None: + raise ValueError("Result has no boxes") + + # Extract bounding box coordinates + bbox_array = result.boxes.xyxy[idx].cpu().numpy() + bbox: Bbox = ( + float(bbox_array[0]), + float(bbox_array[1]), + float(bbox_array[2]), + float(bbox_array[3]), + ) + + # Extract confidence + confidence = float(result.boxes.conf[idx].cpu()) + + # Extract class ID and name + class_id = int(result.boxes.cls[idx].cpu()) + name = ( + result.names.get(class_id, f"class_{class_id}") + if hasattr(result, "names") + else f"class_{class_id}" + ) + + # Extract track ID if available + track_id = -1 + if hasattr(result.boxes, "id") and result.boxes.id is not None: + track_id = int(result.boxes.id[idx].cpu()) + + return cls( + bbox=bbox, + track_id=track_id, + class_id=class_id, + confidence=confidence, + name=name, + ts=image.ts, + image=image, + ) + + def get_bbox_center(self) -> CenteredBbox: + x1, y1, x2, y2 = self.bbox + center_x = (x1 + x2) / 2.0 + center_y = (y1 + y2) / 2.0 + width = float(x2 - x1) + height = float(y2 - y1) + return (center_x, center_y, width, height) + + def to_ros_bbox(self) -> BoundingBox2D: + center_x, center_y, width, height = self.get_bbox_center() + return BoundingBox2D( + center=Pose2D( + position=Point2D(x=center_x, y=center_y), + theta=0.0, + ), + size_x=width, + size_y=height, + ) + + def lcm_encode(self): + return self.to_image_annotations().lcm_encode() + + def to_text_annotation(self) -> list[TextAnnotation]: + x1, y1, _x2, y2 = self.bbox + + font_size = self.image.width / 80 + + # Build label text - exclude class_id if it's -1 (VLM detection) + if self.class_id == -1: + label_text = f"{self.name}_{self.track_id}" + else: + label_text = f"{self.name}_{self.class_id}_{self.track_id}" + + annotations = [ + TextAnnotation( + timestamp=to_ros_stamp(self.ts), + position=Point2(x=x1, y=y1), + text=label_text, + font_size=font_size, + text_color=Color(r=1.0, g=1.0, b=1.0, a=1), + background_color=Color(r=0, g=0, b=0, a=1), + ), + ] + + # Only show confidence if it's not 1.0 + if self.confidence != 1.0: + annotations.append( + TextAnnotation( + timestamp=to_ros_stamp(self.ts), + position=Point2(x=x1, y=y2 + font_size), + text=f"confidence: {self.confidence:.3f}", + font_size=font_size, + text_color=Color(r=1.0, g=1.0, b=1.0, a=1), + background_color=Color(r=0, g=0, b=0, a=1), + ) + ) + + return annotations + + def to_points_annotation(self) -> list[PointsAnnotation]: + x1, y1, x2, y2 = self.bbox + + thickness = 1 + + # Use consistent color based on object name, brighter for outline + outline_color = Color.from_string(self.name, alpha=1.0, brightness=1.25) + + return [ + PointsAnnotation( + timestamp=to_ros_stamp(self.ts), + outline_color=outline_color, + fill_color=Color.from_string(self.name, alpha=0.2), + thickness=thickness, + points_length=4, + points=[ + Point2(x1, y1), + Point2(x1, y2), + Point2(x2, y2), + Point2(x2, y1), + ], + type=PointsAnnotation.LINE_LOOP, + ) + ] + + # this is almost never called directly since this is a single detection + # and ImageAnnotations message normally contains multiple detections annotations + # so ImageDetections2D and ImageDetections3D normally implements this for whole image + def to_image_annotations(self) -> ImageAnnotations: + points = self.to_points_annotation() + texts = self.to_text_annotation() + + return ImageAnnotations( + texts=texts, + texts_length=len(texts), + points=points, + points_length=len(points), + ) + + @classmethod + def from_ros_detection2d(cls, ros_det: ROSDetection2D, **kwargs) -> Detection2D: + """Convert from ROS Detection2D message to Detection2D object.""" + # Extract bbox from ROS format + center_x = ros_det.bbox.center.position.x + center_y = ros_det.bbox.center.position.y + width = ros_det.bbox.size_x + height = ros_det.bbox.size_y + + # Convert centered bbox to corner format + x1 = center_x - width / 2.0 + y1 = center_y - height / 2.0 + x2 = center_x + width / 2.0 + y2 = center_y + height / 2.0 + bbox = (x1, y1, x2, y2) + + # Extract hypothesis info + class_id = 0 + confidence = 0.0 + if ros_det.results: + hypothesis = ros_det.results[0].hypothesis + class_id = hypothesis.class_id + confidence = hypothesis.score + + # Extract track_id + track_id = int(ros_det.id) if ros_det.id.isdigit() else 0 + + # Extract timestamp + ts = to_timestamp(ros_det.header.stamp) + + name = kwargs.pop("name", f"class_{class_id}") + + return cls( + bbox=bbox, + track_id=track_id, + class_id=class_id, + confidence=confidence, + name=name, + ts=ts, + **kwargs, + ) + + def to_ros_detection2d(self) -> ROSDetection2D: + return ROSDetection2D( + header=Header(self.ts, "camera_link"), + bbox=self.to_ros_bbox(), + results=[ + ObjectHypothesisWithPose( + ObjectHypothesis( + class_id=self.class_id, + score=self.confidence, + ) + ) + ], + id=str(self.track_id), + ) diff --git a/dimos/perception/detection/type/detection2d/imageDetections2D.py b/dimos/perception/detection/type/detection2d/imageDetections2D.py new file mode 100644 index 0000000000..0c505ae2b5 --- /dev/null +++ b/dimos/perception/detection/type/detection2d/imageDetections2D.py @@ -0,0 +1,81 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from dimos.perception.detection.type.detection2d.base import Detection2D +from dimos.perception.detection.type.detection2d.bbox import Detection2DBBox +from dimos.perception.detection.type.imageDetections import ImageDetections + +if TYPE_CHECKING: + from dimos_lcm.vision_msgs import Detection2DArray + from ultralytics.engine.results import Results + + from dimos.msgs.sensor_msgs import Image + + +class ImageDetections2D(ImageDetections[Detection2D]): + @classmethod + def from_ros_detection2d_array( + cls, image: Image, ros_detections: Detection2DArray, **kwargs + ) -> ImageDetections2D: + """Convert from ROS Detection2DArray message to ImageDetections2D object.""" + detections: list[Detection2D] = [] + for ros_det in ros_detections.detections: + detection = Detection2DBBox.from_ros_detection2d(ros_det, image=image, **kwargs) + if detection.is_valid(): # type: ignore[attr-defined] + detections.append(detection) + + return cls(image=image, detections=detections) + + @classmethod + def from_ultralytics_result( + cls, image: Image, results: list[Results], **kwargs + ) -> ImageDetections2D: + """Create ImageDetections2D from ultralytics Results. + + Dispatches to appropriate Detection2D subclass based on result type: + - If keypoints present: creates Detection2DPerson + - Otherwise: creates Detection2DBBox + + Args: + image: Source image + results: List of ultralytics Results objects + **kwargs: Additional arguments passed to detection constructors + + Returns: + ImageDetections2D containing appropriate detection types + """ + from dimos.perception.detection.type.detection2d.person import Detection2DPerson + + detections: list[Detection2D] = [] + for result in results: + if result.boxes is None: + continue + + num_detections = len(result.boxes.xyxy) + for i in range(num_detections): + detection: Detection2D + if result.keypoints is not None: + # Pose detection with keypoints + detection = Detection2DPerson.from_ultralytics_result(result, i, image) + else: + # Regular bbox detection + detection = Detection2DBBox.from_ultralytics_result(result, i, image) + if detection.is_valid(): + detections.append(detection) + + return cls(image=image, detections=detections) diff --git a/dimos/perception/detection/type/detection2d/person.py b/dimos/perception/detection/type/detection2d/person.py new file mode 100644 index 0000000000..1d84613051 --- /dev/null +++ b/dimos/perception/detection/type/detection2d/person.py @@ -0,0 +1,342 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass + +# Import for type checking only to avoid circular imports +from typing import TYPE_CHECKING + +from dimos_lcm.foxglove_msgs.ImageAnnotations import PointsAnnotation, TextAnnotation +from dimos_lcm.foxglove_msgs.Point2 import Point2 +import numpy as np + +from dimos.msgs.foxglove_msgs.Color import Color +from dimos.msgs.sensor_msgs import Image +from dimos.perception.detection.type.detection2d.bbox import Bbox, Detection2DBBox +from dimos.types.timestamped import to_ros_stamp +from dimos.utils.decorators.decorators import simple_mcache + +if TYPE_CHECKING: + from ultralytics.engine.results import Results + + +@dataclass +class Detection2DPerson(Detection2DBBox): + """Represents a detected person with pose keypoints.""" + + # Pose keypoints - additional fields beyond Detection2DBBox + keypoints: np.ndarray # [17, 2] - x,y coordinates + keypoint_scores: np.ndarray # [17] - confidence scores + + # Optional normalized coordinates + bbox_normalized: np.ndarray | None = None # [x1, y1, x2, y2] in 0-1 range + keypoints_normalized: np.ndarray | None = None # [17, 2] in 0-1 range + + # Image dimensions for context + image_width: int | None = None + image_height: int | None = None + + # Keypoint names (class attribute) + KEYPOINT_NAMES = [ + "nose", + "left_eye", + "right_eye", + "left_ear", + "right_ear", + "left_shoulder", + "right_shoulder", + "left_elbow", + "right_elbow", + "left_wrist", + "right_wrist", + "left_hip", + "right_hip", + "left_knee", + "right_knee", + "left_ankle", + "right_ankle", + ] + + @classmethod + def from_ultralytics_result( + cls, result: "Results", idx: int, image: Image + ) -> "Detection2DPerson": + """Create Detection2DPerson from ultralytics Results object with pose keypoints. + + Args: + result: Ultralytics Results object containing detection and keypoint data + idx: Index of the detection in the results + image: Source image + + Returns: + Detection2DPerson instance + + Raises: + ValueError: If the result doesn't contain keypoints or is not a person detection + """ + # Validate that this is a pose detection result + if not hasattr(result, "keypoints") or result.keypoints is None: + raise ValueError( + "Cannot create Detection2DPerson from result without keypoints. " + "This appears to be a regular detection result, not a pose detection. " + "Use Detection2DBBox.from_ultralytics_result() instead." + ) + + if not hasattr(result, "boxes") or result.boxes is None: + raise ValueError("Cannot create Detection2DPerson from result without bounding boxes") + + # Check if this is actually a person detection (class 0 in COCO) + class_id = int(result.boxes.cls[idx].cpu()) + if class_id != 0: # Person is class 0 in COCO + class_name = ( + result.names.get(class_id, f"class_{class_id}") + if hasattr(result, "names") + else f"class_{class_id}" + ) + raise ValueError( + f"Cannot create Detection2DPerson from non-person detection. " + f"Got class {class_id} ({class_name}), expected class 0 (person)." + ) + + # Extract bounding box as tuple for Detection2DBBox + bbox_array = result.boxes.xyxy[idx].cpu().numpy() + + bbox: Bbox = ( + float(bbox_array[0]), + float(bbox_array[1]), + float(bbox_array[2]), + float(bbox_array[3]), + ) + + bbox_norm = ( + result.boxes.xyxyn[idx].cpu().numpy() if hasattr(result.boxes, "xyxyn") else None + ) + + confidence = float(result.boxes.conf[idx].cpu()) + class_id = int(result.boxes.cls[idx].cpu()) + + # Extract keypoints + if result.keypoints.xy is None or result.keypoints.conf is None: + raise ValueError("Keypoints xy or conf data is missing from the result") + + keypoints = result.keypoints.xy[idx].cpu().numpy() + keypoint_scores = result.keypoints.conf[idx].cpu().numpy() + keypoints_norm = ( + result.keypoints.xyn[idx].cpu().numpy() + if hasattr(result.keypoints, "xyn") and result.keypoints.xyn is not None + else None + ) + + # Get image dimensions + height, width = result.orig_shape + + # Extract track ID if available + track_id = idx # Use index as default + if hasattr(result.boxes, "id") and result.boxes.id is not None: + track_id = int(result.boxes.id[idx].cpu()) + + # Get class name + name = result.names.get(class_id, "person") if hasattr(result, "names") else "person" + + return cls( + # Detection2DBBox fields + bbox=bbox, + track_id=track_id, + class_id=class_id, + confidence=confidence, + name=name, + ts=image.ts, + image=image, + # Person specific fields + keypoints=keypoints, + keypoint_scores=keypoint_scores, + bbox_normalized=bbox_norm, + keypoints_normalized=keypoints_norm, + image_width=width, + image_height=height, + ) + + @classmethod + def from_yolo(cls, result: "Results", idx: int, image: Image) -> "Detection2DPerson": + """Alias for from_ultralytics_result for backward compatibility.""" + return cls.from_ultralytics_result(result, idx, image) + + @classmethod + def from_ros_detection2d(cls, *args, **kwargs) -> "Detection2DPerson": + """Conversion from ROS Detection2D is not supported for Detection2DPerson. + + The ROS Detection2D message format does not include keypoint data, + which is required for Detection2DPerson. Use Detection2DBBox for + round-trip ROS conversions, or store keypoints separately. + + Raises: + NotImplementedError: Always raised as this conversion is impossible + """ + raise NotImplementedError( + "Cannot convert from ROS Detection2D to Detection2DPerson. " + "The ROS Detection2D message format does not contain keypoint data " + "(keypoints and keypoint_scores) which are required fields for Detection2DPerson. " + "Consider using Detection2DBBox for ROS conversions, or implement a custom " + "message format that includes pose keypoints." + ) + + def get_keypoint(self, name: str) -> tuple[np.ndarray, float]: + """Get specific keypoint by name. + Returns: + Tuple of (xy_coordinates, confidence_score) + """ + if name not in self.KEYPOINT_NAMES: + raise ValueError(f"Invalid keypoint name: {name}. Must be one of {self.KEYPOINT_NAMES}") + + idx = self.KEYPOINT_NAMES.index(name) + return self.keypoints[idx], self.keypoint_scores[idx] + + def get_visible_keypoints(self, threshold: float = 0.5) -> list[tuple[str, np.ndarray, float]]: + """Get all keypoints above confidence threshold. + Returns: + List of tuples: (keypoint_name, xy_coordinates, confidence) + """ + visible = [] + for i, (name, score) in enumerate( + zip(self.KEYPOINT_NAMES, self.keypoint_scores, strict=False) + ): + if score > threshold: + visible.append((name, self.keypoints[i], score)) + return visible + + @simple_mcache + def is_valid(self) -> bool: + valid_keypoints = sum(1 for score in self.keypoint_scores if score > 0.8) + return valid_keypoints >= 5 + + @property + def width(self) -> float: + """Get width of bounding box.""" + x1, _, x2, _ = self.bbox + return x2 - x1 + + @property + def height(self) -> float: + """Get height of bounding box.""" + _, y1, _, y2 = self.bbox + return y2 - y1 + + @property + def center(self) -> tuple[float, float]: + """Get center point of bounding box.""" + x1, y1, x2, y2 = self.bbox + return ((x1 + x2) / 2, (y1 + y2) / 2) + + def to_points_annotation(self) -> list[PointsAnnotation]: + """Override to include keypoint visualizations along with bounding box.""" + annotations = [] + + # First add the bounding box from parent class + annotations.extend(super().to_points_annotation()) + + # Add keypoints as circles + visible_keypoints = self.get_visible_keypoints(threshold=0.3) + + # Create points for visible keypoints + if visible_keypoints: + keypoint_points = [] + for _name, xy, _conf in visible_keypoints: + keypoint_points.append(Point2(float(xy[0]), float(xy[1]))) + + # Add keypoints as circles + annotations.append( + PointsAnnotation( + timestamp=to_ros_stamp(self.ts), + outline_color=Color(r=0.0, g=1.0, b=0.0, a=1.0), # Green outline + fill_color=Color(r=0.0, g=1.0, b=0.0, a=0.5), # Semi-transparent green + thickness=2.0, + points_length=len(keypoint_points), + points=keypoint_points, + type=PointsAnnotation.POINTS, # Draw as individual points/circles + ) + ) + + # Add skeleton connections (COCO skeleton) + skeleton_connections = [ + # Face + (0, 1), + (0, 2), + (1, 3), + (2, 4), # nose to eyes, eyes to ears + # Arms + (5, 6), # shoulders + (5, 7), + (7, 9), # left arm + (6, 8), + (8, 10), # right arm + # Torso + (5, 11), + (6, 12), + (11, 12), # shoulders to hips, hip to hip + # Legs + (11, 13), + (13, 15), # left leg + (12, 14), + (14, 16), # right leg + ] + + # Draw skeleton lines between connected keypoints + for start_idx, end_idx in skeleton_connections: + if ( + start_idx < len(self.keypoint_scores) + and end_idx < len(self.keypoint_scores) + and self.keypoint_scores[start_idx] > 0.3 + and self.keypoint_scores[end_idx] > 0.3 + ): + start_point = Point2( + float(self.keypoints[start_idx][0]), float(self.keypoints[start_idx][1]) + ) + end_point = Point2( + float(self.keypoints[end_idx][0]), float(self.keypoints[end_idx][1]) + ) + + annotations.append( + PointsAnnotation( + timestamp=to_ros_stamp(self.ts), + outline_color=Color(r=0.0, g=0.8, b=1.0, a=0.8), # Cyan + thickness=1.5, + points_length=2, + points=[start_point, end_point], + type=PointsAnnotation.LINE_LIST, + ) + ) + + return annotations + + def to_text_annotation(self) -> list[TextAnnotation]: + """Override to include pose information in text annotations.""" + # Get base annotations from parent + annotations = super().to_text_annotation() + + # Add pose-specific info + visible_count = len(self.get_visible_keypoints(threshold=0.5)) + x1, _y1, _x2, y2 = self.bbox + + annotations.append( + TextAnnotation( + timestamp=to_ros_stamp(self.ts), + position=Point2(x=x1, y=y2 + 40), # Below confidence text + text=f"keypoints: {visible_count}/17", + font_size=18, + text_color=Color(r=0.0, g=1.0, b=0.0, a=1), + background_color=Color(r=0, g=0, b=0, a=0.7), + ) + ) + + return annotations diff --git a/dimos/perception/detection/type/detection2d/test_bbox.py b/dimos/perception/detection/type/detection2d/test_bbox.py new file mode 100644 index 0000000000..a12e4e0d76 --- /dev/null +++ b/dimos/perception/detection/type/detection2d/test_bbox.py @@ -0,0 +1,87 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest + + +def test_detection2d(detection2d) -> None: + # def test_detection_basic_properties(detection2d): + """Test basic detection properties.""" + assert detection2d.track_id >= 0 + assert detection2d.class_id >= 0 + assert 0.0 <= detection2d.confidence <= 1.0 + assert detection2d.name is not None + assert detection2d.ts > 0 + + # def test_bounding_box_format(detection2d): + """Test bounding box format and validity.""" + bbox = detection2d.bbox + assert len(bbox) == 4, "Bounding box should have 4 values" + + x1, y1, x2, y2 = bbox + assert x2 > x1, "x2 should be greater than x1" + assert y2 > y1, "y2 should be greater than y1" + assert x1 >= 0, "x1 should be non-negative" + assert y1 >= 0, "y1 should be non-negative" + + # def test_bbox_2d_volume(detection2d): + """Test bounding box volume calculation.""" + volume = detection2d.bbox_2d_volume() + assert volume > 0, "Bounding box volume should be positive" + + # Calculate expected volume + x1, y1, x2, y2 = detection2d.bbox + expected_volume = (x2 - x1) * (y2 - y1) + assert volume == pytest.approx(expected_volume, abs=0.001) + + # def test_bbox_center_calculation(detection2d): + """Test bounding box center calculation.""" + center_bbox = detection2d.get_bbox_center() + assert len(center_bbox) == 4, "Center bbox should have 4 values" + + center_x, center_y, width, height = center_bbox + x1, y1, x2, y2 = detection2d.bbox + + # Verify center calculations + assert center_x == pytest.approx((x1 + x2) / 2.0, abs=0.001) + assert center_y == pytest.approx((y1 + y2) / 2.0, abs=0.001) + assert width == pytest.approx(x2 - x1, abs=0.001) + assert height == pytest.approx(y2 - y1, abs=0.001) + + # def test_cropped_image(detection2d): + """Test cropped image generation.""" + padding = 20 + cropped = detection2d.cropped_image(padding=padding) + + assert cropped is not None, "Cropped image should not be None" + + # The actual cropped image is (260, 192, 3) + assert cropped.width == 192 + assert cropped.height == 260 + assert cropped.shape == (260, 192, 3) + + # def test_to_ros_bbox(detection2d): + """Test ROS bounding box conversion.""" + ros_bbox = detection2d.to_ros_bbox() + + assert ros_bbox is not None + assert hasattr(ros_bbox, "center") + assert hasattr(ros_bbox, "size_x") + assert hasattr(ros_bbox, "size_y") + + # Verify values match + center_x, center_y, width, height = detection2d.get_bbox_center() + assert ros_bbox.center.position.x == pytest.approx(center_x, abs=0.001) + assert ros_bbox.center.position.y == pytest.approx(center_y, abs=0.001) + assert ros_bbox.size_x == pytest.approx(width, abs=0.001) + assert ros_bbox.size_y == pytest.approx(height, abs=0.001) diff --git a/dimos/perception/detection/type/detection2d/test_imageDetections2D.py b/dimos/perception/detection/type/detection2d/test_imageDetections2D.py new file mode 100644 index 0000000000..120072cfb6 --- /dev/null +++ b/dimos/perception/detection/type/detection2d/test_imageDetections2D.py @@ -0,0 +1,52 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest + +from dimos.perception.detection.type import ImageDetections2D + + +def test_from_ros_detection2d_array(get_moment_2d) -> None: + moment = get_moment_2d() + + detections2d = moment["detections2d"] + + test_image = detections2d.image + + # Convert to ROS detection array + ros_array = detections2d.to_ros_detection2d_array() + + # Convert back to ImageDetections2D + recovered = ImageDetections2D.from_ros_detection2d_array(test_image, ros_array) + + # Verify we got the same number of detections + assert len(recovered.detections) == len(detections2d.detections) + + # Verify the detection matches + original_det = detections2d.detections[0] + recovered_det = recovered.detections[0] + + # Check bbox is approximately the same (allow 1 pixel tolerance due to float conversion) + for orig_val, rec_val in zip(original_det.bbox, recovered_det.bbox, strict=False): + assert orig_val == pytest.approx(rec_val, abs=1.0) + + # Check other properties + assert recovered_det.track_id == original_det.track_id + assert recovered_det.class_id == original_det.class_id + assert recovered_det.confidence == pytest.approx(original_det.confidence, abs=0.01) + + print("\nSuccessfully round-tripped detection through ROS format:") + print(f" Original bbox: {original_det.bbox}") + print(f" Recovered bbox: {recovered_det.bbox}") + print(f" Track ID: {recovered_det.track_id}") + print(f" Confidence: {recovered_det.confidence:.3f}") diff --git a/dimos/perception/detection/type/detection2d/test_person.py b/dimos/perception/detection/type/detection2d/test_person.py new file mode 100644 index 0000000000..2ff1e81237 --- /dev/null +++ b/dimos/perception/detection/type/detection2d/test_person.py @@ -0,0 +1,71 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest + + +def test_person_ros_confidence() -> None: + """Test that Detection2DPerson preserves confidence when converting to ROS format.""" + + from dimos.msgs.sensor_msgs import Image + from dimos.perception.detection.detectors.person.yolo import YoloPersonDetector + from dimos.perception.detection.type.detection2d.person import Detection2DPerson + from dimos.utils.data import get_data + + # Load test image + image_path = get_data("cafe.jpg") + image = Image.from_file(image_path) + + # Run pose detection + detector = YoloPersonDetector(device="cpu") + detections = detector.process_image(image) + + # Find a Detection2DPerson (should have at least one person in cafe.jpg) + person_detections = [d for d in detections.detections if isinstance(d, Detection2DPerson)] + assert len(person_detections) > 0, "No person detections found in cafe.jpg" + + # Test each person detection + for person_det in person_detections: + original_confidence = person_det.confidence + assert 0.0 <= original_confidence <= 1.0, "Confidence should be between 0 and 1" + + # Convert to ROS format + ros_det = person_det.to_ros_detection2d() + + # Extract confidence from ROS message + assert len(ros_det.results) > 0, "ROS detection should have results" + ros_confidence = ros_det.results[0].hypothesis.score + + # Verify confidence is preserved (allow small floating point tolerance) + assert original_confidence == pytest.approx(ros_confidence, abs=0.001), ( + f"Confidence mismatch: {original_confidence} != {ros_confidence}" + ) + + print("\nSuccessfully preserved confidence in ROS conversion for Detection2DPerson:") + print(f" Original confidence: {original_confidence:.3f}") + print(f" ROS confidence: {ros_confidence:.3f}") + print(f" Track ID: {person_det.track_id}") + print(f" Visible keypoints: {len(person_det.get_visible_keypoints(threshold=0.3))}/17") + + +def test_person_from_ros_raises() -> None: + """Test that Detection2DPerson.from_ros_detection2d() raises NotImplementedError.""" + from dimos.perception.detection.type.detection2d.person import Detection2DPerson + + with pytest.raises(NotImplementedError) as exc_info: + Detection2DPerson.from_ros_detection2d() + + # Verify the error message is informative + error_msg = str(exc_info.value) + assert "keypoint data" in error_msg.lower() + assert "Detection2DBBox" in error_msg diff --git a/dimos/perception/detection/type/detection3d/__init__.py b/dimos/perception/detection/type/detection3d/__init__.py new file mode 100644 index 0000000000..0e765b175f --- /dev/null +++ b/dimos/perception/detection/type/detection3d/__init__.py @@ -0,0 +1,37 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.perception.detection.type.detection3d.base import Detection3D +from dimos.perception.detection.type.detection3d.bbox import Detection3DBBox +from dimos.perception.detection.type.detection3d.imageDetections3DPC import ImageDetections3DPC +from dimos.perception.detection.type.detection3d.pointcloud import Detection3DPC +from dimos.perception.detection.type.detection3d.pointcloud_filters import ( + PointCloudFilter, + height_filter, + radius_outlier, + raycast, + statistical, +) + +__all__ = [ + "Detection3D", + "Detection3DBBox", + "Detection3DPC", + "ImageDetections3DPC", + "PointCloudFilter", + "height_filter", + "radius_outlier", + "raycast", + "statistical", +] diff --git a/dimos/perception/detection/type/detection3d/base.py b/dimos/perception/detection/type/detection3d/base.py new file mode 100644 index 0000000000..7988c19a47 --- /dev/null +++ b/dimos/perception/detection/type/detection3d/base.py @@ -0,0 +1,46 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from abc import abstractmethod +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from dimos.perception.detection.type.detection2d import Detection2DBBox + +if TYPE_CHECKING: + from dimos_lcm.sensor_msgs import CameraInfo + + from dimos.msgs.geometry_msgs import Transform + + +@dataclass +class Detection3D(Detection2DBBox): + """Abstract base class for 3D detections.""" + + transform: Transform + frame_id: str + + @classmethod + @abstractmethod + def from_2d( + cls, + det: Detection2DBBox, + distance: float, + camera_info: CameraInfo, + world_to_optical_transform: Transform, + ) -> Detection3D | None: + """Create a 3D detection from a 2D detection.""" + ... diff --git a/dimos/perception/detection/type/detection3d/bbox.py b/dimos/perception/detection/type/detection3d/bbox.py new file mode 100644 index 0000000000..30ca882d16 --- /dev/null +++ b/dimos/perception/detection/type/detection3d/bbox.py @@ -0,0 +1,64 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass +import functools +from typing import Any + +from dimos.msgs.geometry_msgs import PoseStamped, Transform, Vector3 +from dimos.perception.detection.type.detection2d import Detection2DBBox + + +@dataclass +class Detection3DBBox(Detection2DBBox): + """3D bounding box detection with center, size, and orientation. + + Represents a 3D detection as an oriented bounding box in world space. + """ + + transform: Transform # Camera to world transform + frame_id: str # Frame ID (e.g., "world", "map") + center: Vector3 # Center point in world frame + size: Vector3 # Width, height, depth + orientation: tuple[float, float, float, float] # Quaternion (x, y, z, w) + + @functools.cached_property + def pose(self) -> PoseStamped: + """Convert detection to a PoseStamped using bounding box center. + + Returns pose in world frame with the detection's orientation. + """ + return PoseStamped( + ts=self.ts, + frame_id=self.frame_id, + position=self.center, + orientation=self.orientation, + ) + + def to_repr_dict(self) -> dict[str, Any]: + # Calculate distance from camera + camera_pos = self.transform.translation + distance = (self.center - camera_pos).magnitude() + + parent_dict = super().to_repr_dict() + # Remove bbox key if present + parent_dict.pop("bbox", None) + + return { + **parent_dict, + "dist": f"{distance:.2f}m", + "size": f"[{self.size.x:.2f},{self.size.y:.2f},{self.size.z:.2f}]", + } diff --git a/dimos/perception/detection/type/detection3d/imageDetections3DPC.py b/dimos/perception/detection/type/detection3d/imageDetections3DPC.py new file mode 100644 index 0000000000..f843fb96fd --- /dev/null +++ b/dimos/perception/detection/type/detection3d/imageDetections3DPC.py @@ -0,0 +1,45 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from lcm_msgs.foxglove_msgs import SceneUpdate + +from dimos.perception.detection.type.detection3d.pointcloud import Detection3DPC +from dimos.perception.detection.type.imageDetections import ImageDetections + + +class ImageDetections3DPC(ImageDetections[Detection3DPC]): + """Specialized class for 3D detections in an image.""" + + def to_foxglove_scene_update(self) -> SceneUpdate: + """Convert all detections to a Foxglove SceneUpdate message. + + Returns: + SceneUpdate containing SceneEntity objects for all detections + """ + + # Create SceneUpdate message with all detections + scene_update = SceneUpdate() + scene_update.deletions_length = 0 + scene_update.deletions = [] + scene_update.entities = [] + + # Process each detection + for i, detection in enumerate(self.detections): + entity = detection.to_foxglove_scene_entity(entity_id=f"detection_{detection.name}_{i}") + scene_update.entities.append(entity) + + scene_update.entities_length = len(scene_update.entities) + return scene_update diff --git a/dimos/perception/detection/type/detection3d/pointcloud.py b/dimos/perception/detection/type/detection3d/pointcloud.py new file mode 100644 index 0000000000..56423d2f29 --- /dev/null +++ b/dimos/perception/detection/type/detection3d/pointcloud.py @@ -0,0 +1,327 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass +import functools +from typing import TYPE_CHECKING, Any + +from lcm_msgs.builtin_interfaces import Duration +from lcm_msgs.foxglove_msgs import CubePrimitive, SceneEntity, TextPrimitive +from lcm_msgs.geometry_msgs import Point, Pose, Quaternion, Vector3 as LCMVector3 +import numpy as np + +from dimos.msgs.foxglove_msgs.Color import Color +from dimos.msgs.geometry_msgs import PoseStamped, Transform, Vector3 +from dimos.msgs.sensor_msgs import PointCloud2 +from dimos.perception.detection.type.detection3d.base import Detection3D +from dimos.perception.detection.type.detection3d.pointcloud_filters import ( + PointCloudFilter, + radius_outlier, + raycast, + statistical, +) +from dimos.types.timestamped import to_ros_stamp + +if TYPE_CHECKING: + from dimos_lcm.sensor_msgs import CameraInfo + + from dimos.perception.detection.type.detection2d import Detection2DBBox + + +@dataclass +class Detection3DPC(Detection3D): + pointcloud: PointCloud2 + + @functools.cached_property + def center(self) -> Vector3: + return Vector3(*self.pointcloud.center) + + @functools.cached_property + def pose(self) -> PoseStamped: + """Convert detection to a PoseStamped using pointcloud center. + + Returns pose in world frame with identity rotation. + The pointcloud is already in world frame. + """ + return PoseStamped( + ts=self.ts, + frame_id=self.frame_id, + position=self.center, + orientation=(0.0, 0.0, 0.0, 1.0), # Identity quaternion + ) + + def get_bounding_box(self): + """Get axis-aligned bounding box of the detection's pointcloud.""" + return self.pointcloud.get_axis_aligned_bounding_box() + + def get_oriented_bounding_box(self): + """Get oriented bounding box of the detection's pointcloud.""" + return self.pointcloud.get_oriented_bounding_box() + + def get_bounding_box_dimensions(self) -> tuple[float, float, float]: + """Get dimensions (width, height, depth) of the detection's bounding box.""" + return self.pointcloud.get_bounding_box_dimensions() + + def bounding_box_intersects(self, other: Detection3DPC) -> bool: + """Check if this detection's bounding box intersects with another's.""" + return self.pointcloud.bounding_box_intersects(other.pointcloud) + + def to_repr_dict(self) -> dict[str, Any]: + # Calculate distance from camera + # The pointcloud is in world frame, and transform gives camera position in world + center_world = self.center + # Camera position in world frame is the translation part of the transform + camera_pos = self.transform.translation + # Use Vector3 subtraction and magnitude + distance = (center_world - camera_pos).magnitude() + + parent_dict = super().to_repr_dict() + # Remove bbox key if present + parent_dict.pop("bbox", None) + + return { + **parent_dict, + "dist": f"{distance:.2f}m", + "points": str(len(self.pointcloud)), + } + + def to_foxglove_scene_entity(self, entity_id: str | None = None) -> SceneEntity: + """Convert detection to a Foxglove SceneEntity with cube primitive and text label. + + Args: + entity_id: Optional custom entity ID. If None, generates one from name and hash. + + Returns: + SceneEntity with cube bounding box and text label + """ + + # Create a cube primitive for the bounding box + cube = CubePrimitive() + + # Get the axis-aligned bounding box + aabb = self.get_bounding_box() + + # Set pose from axis-aligned bounding box + cube.pose = Pose() + cube.pose.position = Point() + # Get center of the axis-aligned bounding box + aabb_center = aabb.get_center() + cube.pose.position.x = aabb_center[0] + cube.pose.position.y = aabb_center[1] + cube.pose.position.z = aabb_center[2] + + # For axis-aligned box, use identity quaternion (no rotation) + cube.pose.orientation = Quaternion() + cube.pose.orientation.x = 0 + cube.pose.orientation.y = 0 + cube.pose.orientation.z = 0 + cube.pose.orientation.w = 1 + + # Set size from axis-aligned bounding box + cube.size = LCMVector3() + aabb_extent = aabb.get_extent() + cube.size.x = aabb_extent[0] # width + cube.size.y = aabb_extent[1] # height + cube.size.z = aabb_extent[2] # depth + + # Set color based on name hash + cube.color = Color.from_string(self.name, alpha=0.2) + + # Create text label + text = TextPrimitive() + text.pose = Pose() + text.pose.position = Point() + text.pose.position.x = aabb_center[0] + text.pose.position.y = aabb_center[1] + text.pose.position.z = aabb_center[2] + aabb_extent[2] / 2 + 0.1 # Above the box + text.pose.orientation = Quaternion() + text.pose.orientation.x = 0 + text.pose.orientation.y = 0 + text.pose.orientation.z = 0 + text.pose.orientation.w = 1 + text.billboard = True + text.font_size = 20.0 + text.scale_invariant = True + text.color = Color() + text.color.r = 1.0 + text.color.g = 1.0 + text.color.b = 1.0 + text.color.a = 1.0 + text.text = self.scene_entity_label() + + # Create scene entity + entity = SceneEntity() + entity.timestamp = to_ros_stamp(self.ts) + entity.frame_id = self.frame_id + entity.id = str(self.track_id) + entity.lifetime = Duration() + entity.lifetime.sec = 0 # Persistent + entity.lifetime.nanosec = 0 + entity.frame_locked = False + + # Initialize all primitive arrays + entity.metadata_length = 0 + entity.metadata = [] + entity.arrows_length = 0 + entity.arrows = [] + entity.cubes_length = 1 + entity.cubes = [cube] + entity.spheres_length = 0 + entity.spheres = [] + entity.cylinders_length = 0 + entity.cylinders = [] + entity.lines_length = 0 + entity.lines = [] + entity.triangles_length = 0 + entity.triangles = [] + entity.texts_length = 1 + entity.texts = [text] + entity.models_length = 0 + entity.models = [] + + return entity + + def scene_entity_label(self) -> str: + return f"{self.track_id}/{self.name} ({self.confidence:.0%})" + + @classmethod + def from_2d( # type: ignore[override] + cls, + det: Detection2DBBox, + world_pointcloud: PointCloud2, + camera_info: CameraInfo, + world_to_optical_transform: Transform, + # filters are to be adjusted based on the sensor noise characteristics if feeding + # sensor data directly + filters: list[PointCloudFilter] | None = None, + ) -> Detection3DPC | None: + """Create a Detection3D from a 2D detection by projecting world pointcloud. + + This method handles: + 1. Projecting world pointcloud to camera frame + 2. Filtering points within the 2D detection bounding box + 3. Cleaning up the pointcloud (height filter, outlier removal) + 4. Hidden point removal from camera perspective + + Args: + det: The 2D detection + world_pointcloud: Full pointcloud in world frame + camera_info: Camera calibration info + world_to_camerlka_transform: Transform from world to camera frame + filters: List of functions to apply to the pointcloud for filtering + Returns: + Detection3D with filtered pointcloud, or None if no valid points + """ + # Set default filters if none provided + if filters is None: + filters = [ + # height_filter(0.1), + raycast(), + radius_outlier(), + statistical(), + ] + + # Extract camera parameters + fx, fy = camera_info.K[0], camera_info.K[4] + cx, cy = camera_info.K[2], camera_info.K[5] + image_width = camera_info.width + image_height = camera_info.height + + camera_matrix = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]]) + + # Convert pointcloud to numpy array + world_points = world_pointcloud.as_numpy() + + # Project points to camera frame + points_homogeneous = np.hstack([world_points, np.ones((world_points.shape[0], 1))]) + extrinsics_matrix = world_to_optical_transform.to_matrix() + points_camera = (extrinsics_matrix @ points_homogeneous.T).T + + # Filter out points behind the camera + valid_mask = points_camera[:, 2] > 0 + points_camera = points_camera[valid_mask] + world_points = world_points[valid_mask] + + if len(world_points) == 0: + return None + + # Project to 2D + points_2d_homogeneous = (camera_matrix @ points_camera[:, :3].T).T + points_2d = points_2d_homogeneous[:, :2] / points_2d_homogeneous[:, 2:3] + + # Filter points within image bounds + in_image_mask = ( + (points_2d[:, 0] >= 0) + & (points_2d[:, 0] < image_width) + & (points_2d[:, 1] >= 0) + & (points_2d[:, 1] < image_height) + ) + points_2d = points_2d[in_image_mask] + world_points = world_points[in_image_mask] + + if len(world_points) == 0: + return None + + # Extract bbox from Detection2D + x_min, y_min, x_max, y_max = det.bbox + + # Find points within this detection box (with small margin) + margin = 5 # pixels + in_box_mask = ( + (points_2d[:, 0] >= x_min - margin) + & (points_2d[:, 0] <= x_max + margin) + & (points_2d[:, 1] >= y_min - margin) + & (points_2d[:, 1] <= y_max + margin) + ) + + detection_points = world_points[in_box_mask] + + if detection_points.shape[0] == 0: + # print(f"No points found in detection bbox after projection. {det.name}") + return None + + # Create initial pointcloud for this detection + initial_pc = PointCloud2.from_numpy( + detection_points, + frame_id=world_pointcloud.frame_id, + timestamp=world_pointcloud.ts, + ) + + # Apply filters - each filter gets all arguments + detection_pc = initial_pc + for filter_func in filters: + result = filter_func(det, detection_pc, camera_info, world_to_optical_transform) + if result is None: + return None + detection_pc = result + + # Final check for empty pointcloud + if len(detection_pc.pointcloud.points) == 0: + return None + + # Create Detection3D with filtered pointcloud + return cls( + image=det.image, + bbox=det.bbox, + track_id=det.track_id, + class_id=det.class_id, + confidence=det.confidence, + name=det.name, + ts=det.ts, + pointcloud=detection_pc, + transform=world_to_optical_transform, + frame_id=world_pointcloud.frame_id, + ) diff --git a/dimos/perception/detection/type/detection3d/pointcloud_filters.py b/dimos/perception/detection/type/detection3d/pointcloud_filters.py new file mode 100644 index 0000000000..1c6085b690 --- /dev/null +++ b/dimos/perception/detection/type/detection3d/pointcloud_filters.py @@ -0,0 +1,82 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from collections.abc import Callable + +from dimos_lcm.sensor_msgs import CameraInfo + +from dimos.msgs.geometry_msgs import Transform +from dimos.msgs.sensor_msgs import PointCloud2 +from dimos.perception.detection.type.detection2d import Detection2DBBox + +# Filters take Detection2DBBox, PointCloud2, CameraInfo, Transform and return filtered PointCloud2 or None +PointCloudFilter = Callable[ + [Detection2DBBox, PointCloud2, CameraInfo, Transform], PointCloud2 | None +] + + +def height_filter(height: float = 0.1) -> PointCloudFilter: + return lambda det, pc, ci, tf: pc.filter_by_height(height) + + +def statistical(nb_neighbors: int = 40, std_ratio: float = 0.5) -> PointCloudFilter: + def filter_func( + det: Detection2DBBox, pc: PointCloud2, ci: CameraInfo, tf: Transform + ) -> PointCloud2 | None: + try: + statistical, _removed = pc.pointcloud.remove_statistical_outlier( + nb_neighbors=nb_neighbors, std_ratio=std_ratio + ) + return PointCloud2(statistical, pc.frame_id, pc.ts) + except Exception: + # print("statistical filter failed:", e) + return None + + return filter_func + + +def raycast() -> PointCloudFilter: + def filter_func( + det: Detection2DBBox, pc: PointCloud2, ci: CameraInfo, tf: Transform + ) -> PointCloud2 | None: + try: + camera_pos = tf.inverse().translation + camera_pos_np = camera_pos.to_numpy() + _, visible_indices = pc.pointcloud.hidden_point_removal(camera_pos_np, radius=100.0) + visible_pcd = pc.pointcloud.select_by_index(visible_indices) + return PointCloud2(visible_pcd, pc.frame_id, pc.ts) + except Exception: + # print("raycast filter failed:", e) + return None + + return filter_func + + +def radius_outlier(min_neighbors: int = 20, radius: float = 0.3) -> PointCloudFilter: + """ + Remove isolated points: keep only points that have at least `min_neighbors` + neighbors within `radius` meters (same units as your point cloud). + """ + + def filter_func( + det: Detection2DBBox, pc: PointCloud2, ci: CameraInfo, tf: Transform + ) -> PointCloud2 | None: + filtered_pcd, _removed = pc.pointcloud.remove_radius_outlier( + nb_points=min_neighbors, radius=radius + ) + return PointCloud2(filtered_pcd, pc.frame_id, pc.ts) + + return filter_func diff --git a/dimos/perception/detection/type/detection3d/test_imageDetections3DPC.py b/dimos/perception/detection/type/detection3d/test_imageDetections3DPC.py new file mode 100644 index 0000000000..4ad2660738 --- /dev/null +++ b/dimos/perception/detection/type/detection3d/test_imageDetections3DPC.py @@ -0,0 +1,37 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + + +@pytest.mark.skip +def test_to_foxglove_scene_update(detections3dpc) -> None: + # Convert to scene update + scene_update = detections3dpc.to_foxglove_scene_update() + + # Verify scene update structure + assert scene_update is not None + assert scene_update.deletions_length == 0 + assert len(scene_update.deletions) == 0 + assert scene_update.entities_length == len(detections3dpc.detections) + assert len(scene_update.entities) == len(detections3dpc.detections) + + # Verify each entity corresponds to a detection + for _i, (entity, detection) in enumerate( + zip(scene_update.entities, detections3dpc.detections, strict=False) + ): + assert entity.id == str(detection.track_id) + assert entity.frame_id == detection.frame_id + assert entity.cubes_length == 1 + assert entity.texts_length == 1 diff --git a/dimos/perception/detection/type/detection3d/test_pointcloud.py b/dimos/perception/detection/type/detection3d/test_pointcloud.py new file mode 100644 index 0000000000..f616fe7f33 --- /dev/null +++ b/dimos/perception/detection/type/detection3d/test_pointcloud.py @@ -0,0 +1,137 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pytest + + +def test_detection3dpc(detection3dpc) -> None: + # def test_oriented_bounding_box(detection3dpc): + """Test oriented bounding box calculation and values.""" + obb = detection3dpc.get_oriented_bounding_box() + assert obb is not None, "Oriented bounding box should not be None" + + # Verify OBB center values + assert obb.center[0] == pytest.approx(-3.36002, abs=0.1) + assert obb.center[1] == pytest.approx(-0.196446, abs=0.1) + assert obb.center[2] == pytest.approx(0.220184, abs=0.1) + + # Verify OBB extent values + assert obb.extent[0] == pytest.approx(0.531275, abs=0.12) + assert obb.extent[1] == pytest.approx(0.461054, abs=0.1) + assert obb.extent[2] == pytest.approx(0.155, abs=0.1) + + # def test_bounding_box_dimensions(detection3dpc): + """Test bounding box dimension calculation.""" + dims = detection3dpc.get_bounding_box_dimensions() + assert len(dims) == 3, "Bounding box dimensions should have 3 values" + assert dims[0] == pytest.approx(0.350, abs=0.1) + assert dims[1] == pytest.approx(0.250, abs=0.1) + assert dims[2] == pytest.approx(0.550, abs=0.1) + + # def test_axis_aligned_bounding_box(detection3dpc): + """Test axis-aligned bounding box calculation.""" + aabb = detection3dpc.get_bounding_box() + assert aabb is not None, "Axis-aligned bounding box should not be None" + + # Verify AABB min values + assert aabb.min_bound[0] == pytest.approx(-3.575, abs=0.1) + assert aabb.min_bound[1] == pytest.approx(-0.375, abs=0.1) + assert aabb.min_bound[2] == pytest.approx(-0.075, abs=0.1) + + # Verify AABB max values + assert aabb.max_bound[0] == pytest.approx(-3.075, abs=0.1) + assert aabb.max_bound[1] == pytest.approx(-0.125, abs=0.1) + assert aabb.max_bound[2] == pytest.approx(0.475, abs=0.1) + + # def test_point_cloud_properties(detection3dpc): + """Test point cloud data and boundaries.""" + pc_points = detection3dpc.pointcloud.points() + assert len(pc_points) > 60 + assert detection3dpc.pointcloud.frame_id == "world", ( + f"Expected frame_id 'world', got '{detection3dpc.pointcloud.frame_id}'" + ) + + # Extract xyz coordinates from points + points = np.array([[pt[0], pt[1], pt[2]] for pt in pc_points]) + + min_pt = np.min(points, axis=0) + max_pt = np.max(points, axis=0) + center = np.mean(points, axis=0) + + # Verify point cloud boundaries + assert min_pt[0] == pytest.approx(-3.575, abs=0.1) + assert min_pt[1] == pytest.approx(-0.375, abs=0.1) + assert min_pt[2] == pytest.approx(-0.075, abs=0.1) + + assert max_pt[0] == pytest.approx(-3.075, abs=0.1) + assert max_pt[1] == pytest.approx(-0.125, abs=0.1) + assert max_pt[2] == pytest.approx(0.475, abs=0.1) + + assert center[0] == pytest.approx(-3.326, abs=0.1) + assert center[1] == pytest.approx(-0.202, abs=0.1) + assert center[2] == pytest.approx(0.160, abs=0.1) + + # def test_foxglove_scene_entity_generation(detection3dpc): + """Test Foxglove scene entity creation and structure.""" + entity = detection3dpc.to_foxglove_scene_entity("test_entity_123") + + # Verify entity metadata + assert entity.id == "1", f"Expected entity ID '1', got '{entity.id}'" + assert entity.frame_id == "world", f"Expected frame_id 'world', got '{entity.frame_id}'" + assert entity.cubes_length == 1, f"Expected 1 cube, got {entity.cubes_length}" + assert entity.texts_length == 1, f"Expected 1 text, got {entity.texts_length}" + + # def test_foxglove_cube_properties(detection3dpc): + """Test Foxglove cube primitive properties.""" + entity = detection3dpc.to_foxglove_scene_entity("test_entity_123") + cube = entity.cubes[0] + + # Verify position + assert cube.pose.position.x == pytest.approx(-3.325, abs=0.1) + assert cube.pose.position.y == pytest.approx(-0.250, abs=0.1) + assert cube.pose.position.z == pytest.approx(0.200, abs=0.1) + + # Verify size + assert cube.size.x == pytest.approx(0.350, abs=0.1) + assert cube.size.y == pytest.approx(0.250, abs=0.1) + assert cube.size.z == pytest.approx(0.550, abs=0.1) + + # Verify color (green with alpha) + assert cube.color.r == pytest.approx(0.08235294117647059, abs=0.1) + assert cube.color.g == pytest.approx(0.7176470588235294, abs=0.1) + assert cube.color.b == pytest.approx(0.28627450980392155, abs=0.1) + assert cube.color.a == pytest.approx(0.2, abs=0.1) + + # def test_foxglove_text_label(detection3dpc): + """Test Foxglove text label properties.""" + entity = detection3dpc.to_foxglove_scene_entity("test_entity_123") + text = entity.texts[0] + + assert text.text in ["1/suitcase (81%)", "1/suitcase (82%)"], ( + f"Expected text '1/suitcase (81%)' or '1/suitcase (82%)', got '{text.text}'" + ) + assert text.pose.position.x == pytest.approx(-3.325, abs=0.1) + assert text.pose.position.y == pytest.approx(-0.250, abs=0.1) + assert text.pose.position.z == pytest.approx(0.575, abs=0.1) + assert text.font_size == 20.0, f"Expected font size 20.0, got {text.font_size}" + + # def test_detection_pose(detection3dpc): + """Test detection pose and frame information.""" + assert detection3dpc.pose.x == pytest.approx(-3.327, abs=0.1) + assert detection3dpc.pose.y == pytest.approx(-0.202, abs=0.1) + assert detection3dpc.pose.z == pytest.approx(0.160, abs=0.1) + assert detection3dpc.pose.frame_id == "world", ( + f"Expected frame_id 'world', got '{detection3dpc.pose.frame_id}'" + ) diff --git a/dimos/perception/detection/type/imageDetections.py b/dimos/perception/detection/type/imageDetections.py new file mode 100644 index 0000000000..1a597595ea --- /dev/null +++ b/dimos/perception/detection/type/imageDetections.py @@ -0,0 +1,81 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Generic, TypeVar + +from dimos_lcm.vision_msgs import Detection2DArray + +from dimos.msgs.foxglove_msgs import ImageAnnotations +from dimos.msgs.std_msgs import Header +from dimos.perception.detection.type.utils import TableStr + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dimos.msgs.sensor_msgs import Image + from dimos.perception.detection.type.detection2d.base import Detection2D + + T = TypeVar("T", bound=Detection2D) +else: + from dimos.perception.detection.type.detection2d.base import Detection2D + + T = TypeVar("T", bound=Detection2D) + + +class ImageDetections(Generic[T], TableStr): + image: Image + detections: list[T] + + @property + def ts(self) -> float: + return self.image.ts + + def __init__(self, image: Image, detections: list[T] | None = None) -> None: + self.image = image + self.detections = detections or [] + for det in self.detections: + if not det.ts: + det.ts = image.ts + + def __len__(self) -> int: + return len(self.detections) + + def __iter__(self) -> Iterator: + return iter(self.detections) + + def __getitem__(self, index): + return self.detections[index] + + def to_ros_detection2d_array(self) -> Detection2DArray: + return Detection2DArray( + detections_length=len(self.detections), + header=Header(self.image.ts, "camera_optical"), + detections=[det.to_ros_detection2d() for det in self.detections], + ) + + def to_foxglove_annotations(self) -> ImageAnnotations: + def flatten(xss): + return [x for xs in xss for x in xs] + + texts = flatten(det.to_text_annotation() for det in self.detections) + points = flatten(det.to_points_annotation() for det in self.detections) + + return ImageAnnotations( + texts=texts, + texts_length=len(texts), + points=points, + points_length=len(points), + ) diff --git a/dimos/perception/detection/type/test_detection3d.py b/dimos/perception/detection/type/test_detection3d.py new file mode 100644 index 0000000000..031623afe3 --- /dev/null +++ b/dimos/perception/detection/type/test_detection3d.py @@ -0,0 +1,36 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + + +def test_guess_projection(get_moment_2d, publish_moment) -> None: + moment = get_moment_2d() + for key, value in moment.items(): + print(key, "====================================") + print(value) + + moment.get("camera_info") + detection2d = moment.get("detections2d")[0] + tf = moment.get("tf") + tf.get("camera_optical", "world", detection2d.ts, 5.0) + + # for stash + # detection3d = Detection3D.from_2d(detection2d, 1.5, camera_info, transform) + # print(detection3d) + + # foxglove bridge needs 2 messages per topic to pass to foxglove + publish_moment(moment) + time.sleep(0.1) + publish_moment(moment) diff --git a/dimos/perception/detection/type/test_object3d.py b/dimos/perception/detection/type/test_object3d.py new file mode 100644 index 0000000000..4acd2f1afa --- /dev/null +++ b/dimos/perception/detection/type/test_object3d.py @@ -0,0 +1,177 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from dimos.perception.detection.moduleDB import Object3D +from dimos.perception.detection.type.detection3d import ImageDetections3DPC + + +def test_first_object(first_object) -> None: + # def test_object3d_properties(first_object): + """Test basic properties of an Object3D.""" + assert first_object.track_id is not None + assert isinstance(first_object.track_id, str) + assert first_object.name is not None + assert first_object.class_id >= 0 + assert 0.0 <= first_object.confidence <= 1.0 + assert first_object.ts > 0 + assert first_object.frame_id is not None + assert first_object.best_detection is not None + + # def test_object3d_center(first_object): + """Test Object3D center calculation.""" + assert first_object.center is not None + assert hasattr(first_object.center, "x") + assert hasattr(first_object.center, "y") + assert hasattr(first_object.center, "z") + + # Center should be within reasonable bounds + assert -10 < first_object.center.x < 10 + assert -10 < first_object.center.y < 10 + assert -10 < first_object.center.z < 10 + + +def test_object3d_repr_dict(first_object) -> None: + """Test to_repr_dict method.""" + repr_dict = first_object.to_repr_dict() + + assert "object_id" in repr_dict + assert "detections" in repr_dict + assert "center" in repr_dict + + assert repr_dict["object_id"] == first_object.track_id + assert repr_dict["detections"] == first_object.detections + + # Center should be formatted as string with coordinates + assert isinstance(repr_dict["center"], str) + assert repr_dict["center"].startswith("[") + assert repr_dict["center"].endswith("]") + + # def test_object3d_scene_entity_label(first_object): + """Test scene entity label generation.""" + label = first_object.scene_entity_label() + + assert isinstance(label, str) + assert first_object.name in label + assert f"({first_object.detections})" in label + + # def test_object3d_agent_encode(first_object): + """Test agent encoding.""" + encoded = first_object.agent_encode() + + assert isinstance(encoded, dict) + assert "id" in encoded + assert "name" in encoded + assert "detections" in encoded + assert "last_seen" in encoded + + assert encoded["id"] == first_object.track_id + assert encoded["name"] == first_object.name + assert encoded["detections"] == first_object.detections + assert encoded["last_seen"].endswith("s ago") + + # def test_object3d_image_property(first_object): + """Test get_image method returns best_detection's image.""" + assert first_object.get_image() is not None + assert first_object.get_image() is first_object.best_detection.image + + +def test_all_objeects(all_objects) -> None: + # def test_object3d_multiple_detections(all_objects): + """Test objects that have been built from multiple detections.""" + # Find objects with multiple detections + multi_detection_objects = [obj for obj in all_objects if obj.detections > 1] + + if multi_detection_objects: + obj = multi_detection_objects[0] + + # Since detections is now a counter, we can only test that we have multiple detections + # and that best_detection exists + assert obj.detections > 1 + assert obj.best_detection is not None + assert obj.confidence is not None + assert obj.ts > 0 + + # Test that best_detection has reasonable properties + assert obj.best_detection.bbox_2d_volume() > 0 + + # def test_object_db_module_objects_structure(all_objects): + """Test the structure of objects in the database.""" + for obj in all_objects: + assert isinstance(obj, Object3D) + assert hasattr(obj, "track_id") + assert hasattr(obj, "detections") + assert hasattr(obj, "best_detection") + assert hasattr(obj, "center") + assert obj.detections >= 1 + + +def test_objectdb_module(object_db_module) -> None: + # def test_object_db_module_populated(object_db_module): + """Test that ObjectDBModule is properly populated.""" + assert len(object_db_module.objects) > 0, "Database should contain objects" + assert object_db_module.cnt > 0, "Object counter should be greater than 0" + + # def test_object3d_addition(object_db_module): + """Test Object3D addition operator.""" + # Get existing objects from the database + objects = list(object_db_module.objects.values()) + if len(objects) < 2: + pytest.skip("Not enough objects in database") + + # Get detections from two different objects + det1 = objects[0].best_detection + det2 = objects[1].best_detection + + # Create a new object with the first detection + obj = Object3D("test_track_combined", det1) + + # Add the second detection from a different object + combined = obj + det2 + + assert combined.track_id == "test_track_combined" + assert combined.detections == 2 + + # Since detections is now a counter, we can't check if specific detections are in the list + # We can only verify the count and that best_detection is properly set + + # Best detection should be determined by the Object3D logic + assert combined.best_detection is not None + + # Center should be valid (no specific value check since we're using real detections) + assert hasattr(combined, "center") + assert combined.center is not None + + # def test_image_detections3d_scene_update(object_db_module): + """Test ImageDetections3DPC to Foxglove scene update conversion.""" + # Get some detections + objects = list(object_db_module.objects.values()) + if not objects: + pytest.skip("No objects in database") + + detections = [obj.best_detection for obj in objects[:3]] # Take up to 3 + + image_detections = ImageDetections3DPC(image=detections[0].image, detections=detections) + + scene_update = image_detections.to_foxglove_scene_update() + + assert scene_update is not None + assert scene_update.entities_length == len(detections) + + for i, entity in enumerate(scene_update.entities): + assert entity.id == str(detections[i].track_id) + assert entity.frame_id == detections[i].frame_id + assert entity.cubes_length == 1 + assert entity.texts_length == 1 diff --git a/dimos/perception/detection/type/utils.py b/dimos/perception/detection/type/utils.py new file mode 100644 index 0000000000..89cf41b404 --- /dev/null +++ b/dimos/perception/detection/type/utils.py @@ -0,0 +1,101 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import hashlib + +from rich.console import Console +from rich.table import Table +from rich.text import Text + +from dimos.types.timestamped import to_timestamp + + +def _hash_to_color(name: str) -> str: + """Generate a consistent color for a given name using hash.""" + # List of rich colors to choose from + colors = [ + "cyan", + "magenta", + "yellow", + "blue", + "green", + "red", + "bright_cyan", + "bright_magenta", + "bright_yellow", + "bright_blue", + "bright_green", + "bright_red", + "purple", + "white", + "pink", + ] + + # Hash the name and pick a color + hash_value = hashlib.md5(name.encode()).digest()[0] + return colors[hash_value % len(colors)] + + +class TableStr: + """Mixin class that provides table-based string representation for detection collections.""" + + def __str__(self) -> str: + console = Console(force_terminal=True, legacy_windows=False) + + # Create a table for detections + table = Table( + title=f"{self.__class__.__name__} [{len(self.detections)} detections @ {to_timestamp(self.image.ts):.3f}]", + show_header=True, + show_edge=True, + ) + + # Dynamically build columns based on the first detection's dict keys + if not self.detections: + return ( + f" {self.__class__.__name__} [0 detections @ {to_timestamp(self.image.ts):.3f}]" + ) + + # Cache all repr_dicts to avoid double computation + detection_dicts = [det.to_repr_dict() for det in self] + + first_dict = detection_dicts[0] + table.add_column("#", style="dim") + for col in first_dict.keys(): + color = _hash_to_color(col) + table.add_column(col.title(), style=color) + + # Add each detection to the table + for i, d in enumerate(detection_dicts): + row = [str(i)] + + for key in first_dict.keys(): + if key == "conf": + # Color-code confidence + conf_color = ( + "green" + if float(d[key]) > 0.8 + else "yellow" + if float(d[key]) > 0.5 + else "red" + ) + row.append(Text(f"{d[key]}", style=conf_color)) + elif key == "points" and d.get(key) == "None": + row.append(Text(d.get(key, ""), style="dim")) + else: + row.append(str(d.get(key, ""))) + table.add_row(*row) + + with console.capture() as capture: + console.print(table) + return capture.get().strip() diff --git a/dimos/perception/detection2d/__init__.py b/dimos/perception/detection2d/__init__.py deleted file mode 100644 index 265f6982d9..0000000000 --- a/dimos/perception/detection2d/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .utils import * -from .yolo_2d_det import * \ No newline at end of file diff --git a/dimos/perception/detection2d/utils.py b/dimos/perception/detection2d/utils.py index ffe560b777..c44a013325 100644 --- a/dimos/perception/detection2d/utils.py +++ b/dimos/perception/detection2d/utils.py @@ -1,13 +1,36 @@ -import numpy as np +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Sequence + import cv2 +import numpy as np -from dimos.utils.ros_utils import distance_angle_to_goal_xy -def filter_detections(bboxes, track_ids, class_ids, confidences, names, - class_filter=None, name_filter=None, track_id_filter=None): +def filter_detections( + bboxes, + track_ids, + class_ids, + confidences, + names: Sequence[str], + class_filter=None, + name_filter=None, + track_id_filter=None, +): """ Filter detection results based on class IDs, names, and/or tracking IDs. - + Args: bboxes: List of bounding boxes [x1, y1, x2, y2] track_ids: List of tracking IDs @@ -17,9 +40,9 @@ def filter_detections(bboxes, track_ids, class_ids, confidences, names, class_filter: List/set of class IDs to keep, or None to keep all name_filter: List/set of class names to keep, or None to keep all track_id_filter: List/set of track IDs to keep, or None to keep all - + Returns: - tuple: (filtered_bboxes, filtered_track_ids, filtered_class_ids, + tuple: (filtered_bboxes, filtered_track_ids, filtered_class_ids, filtered_confidences, filtered_names) """ # Convert filters to sets for efficient lookup @@ -29,30 +52,30 @@ def filter_detections(bboxes, track_ids, class_ids, confidences, names, name_filter = set(name_filter) if track_id_filter is not None: track_id_filter = set(track_id_filter) - + # Initialize lists for filtered results filtered_bboxes = [] filtered_track_ids = [] filtered_class_ids = [] filtered_confidences = [] filtered_names = [] - + # Filter detections for bbox, track_id, class_id, conf, name in zip( - bboxes, track_ids, class_ids, confidences, names): - + bboxes, track_ids, class_ids, confidences, names, strict=False + ): # Check if detection passes all specified filters keep = True - + if class_filter is not None: keep = keep and (class_id in class_filter) - + if name_filter is not None: keep = keep and (name in name_filter) - + if track_id_filter is not None: keep = keep and (track_id in track_id_filter) - + # If detection passes all filters, add it to results if keep: filtered_bboxes.append(bbox) @@ -60,20 +83,26 @@ def filter_detections(bboxes, track_ids, class_ids, confidences, names, filtered_class_ids.append(class_id) filtered_confidences.append(conf) filtered_names.append(name) - - return (filtered_bboxes, filtered_track_ids, filtered_class_ids, - filtered_confidences, filtered_names) + + return ( + filtered_bboxes, + filtered_track_ids, + filtered_class_ids, + filtered_confidences, + filtered_names, + ) + def extract_detection_results(result, class_filter=None, name_filter=None, track_id_filter=None): """ Extract and optionally filter detection information from a YOLO result object. - + Args: result: Ultralytics result object class_filter: List/set of class IDs to keep, or None to keep all name_filter: List/set of class names to keep, or None to keep all track_id_filter: List/set of track IDs to keep, or None to keep all - + Returns: tuple: (bboxes, track_ids, class_ids, confidences, names) - bboxes: list of [x1, y1, x2, y2] coordinates @@ -94,19 +123,19 @@ def extract_detection_results(result, class_filter=None, name_filter=None, track for box in result.boxes: # Extract bounding box coordinates x1, y1, x2, y2 = box.xyxy[0].tolist() - + # Extract tracking ID if available track_id = -1 - if hasattr(box, 'id') and box.id is not None: + if hasattr(box, "id") and box.id is not None: track_id = int(box.id[0].item()) - + # Extract class information cls_idx = int(box.cls[0]) name = result.names[cls_idx] - + # Extract confidence conf = float(box.conf[0]) - + # Check filters before adding to results keep = True if class_filter is not None: @@ -115,7 +144,7 @@ def extract_detection_results(result, class_filter=None, name_filter=None, track keep = keep and (name in name_filter) if track_id_filter is not None: keep = keep and (track_id in track_id_filter) - + if keep: bboxes.append([x1, y1, x2, y2]) track_ids.append(track_id) @@ -126,10 +155,12 @@ def extract_detection_results(result, class_filter=None, name_filter=None, track return bboxes, track_ids, class_ids, confidences, names -def plot_results(image, bboxes, track_ids, class_ids, confidences, names, alpha=0.5): +def plot_results( + image, bboxes, track_ids, class_ids, confidences, names: Sequence[str], alpha: float = 0.5 +): """ Draw bounding boxes and labels on the image. - + Args: image: Original input image bboxes: List of bounding boxes [x1, y1, x2, y2] @@ -138,13 +169,13 @@ def plot_results(image, bboxes, track_ids, class_ids, confidences, names, alpha= confidences: List of detection confidences names: List of class names alpha: Transparency of the overlay - + Returns: Image with visualized detections """ vis_img = image.copy() - for bbox, track_id, conf, name in zip(bboxes, track_ids, confidences, names): + for bbox, track_id, conf, name in zip(bboxes, track_ids, confidences, names, strict=False): # Generate consistent color based on track_id or class name if track_id != -1: np.random.seed(track_id) @@ -152,7 +183,7 @@ def plot_results(image, bboxes, track_ids, class_ids, confidences, names, alpha= np.random.seed(hash(name) % 100000) color = np.random.randint(0, 255, (3,), dtype=np.uint8) np.random.seed(None) - + # Draw bounding box x1, y1, x2, y2 = map(int, bbox) cv2.rectangle(vis_img, (x1, y1), (x2, y2), color.tolist(), 2) @@ -164,163 +195,115 @@ def plot_results(image, bboxes, track_ids, class_ids, confidences, names, alpha= label = f"{name} {conf:.2f}" # Calculate text size for background rectangle - (text_w, text_h), _ = cv2.getTextSize( - label, - cv2.FONT_HERSHEY_SIMPLEX, - 0.5, - 1 - ) + (text_w, text_h), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1) # Draw background rectangle for text - cv2.rectangle( - vis_img, - (x1, y1-text_h-8), - (x1+text_w+4, y1), - color.tolist(), - -1 - ) + cv2.rectangle(vis_img, (x1, y1 - text_h - 8), (x1 + text_w + 4, y1), color.tolist(), -1) # Draw text with white color for better visibility cv2.putText( - vis_img, - label, - (x1+2, y1-5), - cv2.FONT_HERSHEY_SIMPLEX, - 0.5, - (255, 255, 255), - 1 + vis_img, label, (x1 + 2, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1 ) return vis_img -def calculate_depth_from_bbox(depth_model, frame, bbox): + +def calculate_depth_from_bbox(depth_map, bbox): """ Calculate the average depth of an object within a bounding box. Uses the 25th to 75th percentile range to filter outliers. - + Args: - depth_model: Depth model - frame: The image frame + depth_map: The depth map bbox: Bounding box in format [x1, y1, x2, y2] - + Returns: float: Average depth in meters, or None if depth estimation fails """ try: - # Get depth map for the entire frame - depth_map = depth_model.infer_depth(frame) - depth_map = np.array(depth_map) - # Extract region of interest from the depth map x1, y1, x2, y2 = map(int, bbox) roi_depth = depth_map[y1:y2, x1:x2] - + if roi_depth.size == 0: return None - + # Calculate 25th and 75th percentile to filter outliers p25 = np.percentile(roi_depth, 25) p75 = np.percentile(roi_depth, 75) - + # Filter depth values within this range filtered_depth = roi_depth[(roi_depth >= p25) & (roi_depth <= p75)] - + # Calculate average depth (convert to meters) if filtered_depth.size > 0: return np.mean(filtered_depth) / 1000.0 # Convert mm to meters - + return None except Exception as e: print(f"Error calculating depth from bbox: {e}") return None -def calculate_distance_angle_from_bbox(bbox, depth, camera_intrinsics): + +def calculate_distance_angle_from_bbox(bbox, depth: int, camera_intrinsics): """ Calculate distance and angle to object center based on bbox and depth. - + Args: bbox: Bounding box [x1, y1, x2, y2] depth: Depth value in meters camera_intrinsics: List [fx, fy, cx, cy] with camera parameters - + Returns: tuple: (distance, angle) in meters and radians """ if camera_intrinsics is None: raise ValueError("Camera intrinsics required for distance calculation") - + # Extract camera parameters - fx, fy, cx, cy = camera_intrinsics - + fx, _fy, cx, _cy = camera_intrinsics + # Calculate center of bounding box in pixels x1, y1, x2, y2 = bbox center_x = (x1 + x2) / 2 - center_y = (y1 + y2) / 2 - + (y1 + y2) / 2 + # Calculate normalized image coordinates x_norm = (center_x - cx) / fx - + # Calculate angle (positive to the right) angle = np.arctan(x_norm) - + # Calculate distance using depth and angle distance = depth / np.cos(angle) if np.cos(angle) != 0 else depth - + return distance, angle -def calculate_object_size_from_bbox(bbox, depth, camera_intrinsics): + +def calculate_object_size_from_bbox(bbox, depth: int, camera_intrinsics): """ Estimate physical width and height of object in meters. - + Args: bbox: Bounding box [x1, y1, x2, y2] depth: Depth value in meters camera_intrinsics: List [fx, fy, cx, cy] with camera parameters - + Returns: tuple: (width, height) in meters """ if camera_intrinsics is None: return 0.0, 0.0 - + fx, fy, _, _ = camera_intrinsics - + # Calculate bbox dimensions in pixels x1, y1, x2, y2 = bbox width_px = x2 - x1 height_px = y2 - y1 - + # Convert to meters using similar triangles and depth width_m = (width_px * depth) / fx height_m = (height_px * depth) / fy - - return width_m, height_m -def calculate_position_rotation_from_bbox(bbox, depth, camera_intrinsics): - """ - Calculate position (xyz) and rotation (roll, pitch, yaw) for an object - based on its bounding box and depth. - - Args: - bbox: Bounding box [x1, y1, x2, y2] - depth: Depth value in meters - camera_intrinsics: List [fx, fy, cx, cy] with camera parameters - - Returns: - Tuple of (position_dict, rotation_dict) - """ - # Calculate distance and angle to object - distance, angle = calculate_distance_angle_from_bbox(bbox, depth, camera_intrinsics) - - # Convert distance and angle to x,y coordinates (in camera frame) - # Note: We negate the angle since positive angle means object is to the right, - # but we want positive y to be to the left in the standard coordinate system - x, y = distance_angle_to_goal_xy(distance, -angle) - - # For now, rotation is only in yaw (around z-axis) - # We can use the negative of the angle as an estimate of the object's yaw - # assuming objects tend to face the camera - position = {"x": x, "y": y, "z": 0.0} # z=0 assuming objects are on the ground - rotation = {"roll": 0.0, "pitch": 0.0, "yaw": -angle} # Only yaw is meaningful with monocular camera - - return position, rotation \ No newline at end of file + return width_m, height_m diff --git a/dimos/perception/detection2d/yolo_2d_det.py b/dimos/perception/detection2d/yolo_2d_det.py deleted file mode 100644 index 0c78e56950..0000000000 --- a/dimos/perception/detection2d/yolo_2d_det.py +++ /dev/null @@ -1,123 +0,0 @@ -import cv2 -import numpy as np -from ultralytics import YOLO -from dimos.perception.detection2d.utils import extract_detection_results, plot_results, filter_detections -import os - - -class Yolo2DDetector: - def __init__(self, model_path="models/yolo11n.engine", device="cuda"): - """ - Initialize the YOLO detector. - - Args: - model_path (str): Path to the YOLO model weights - device (str): Device to run inference on ('cuda' or 'cpu') - """ - self.device = device - self.model = YOLO(model_path) - - module_dir = os.path.dirname(__file__) - self.tracker_config = os.path.join(module_dir, 'config', 'custom_tracker.yaml') - - def process_image(self, image): - """ - Process an image and return detection results. - - Args: - image: Input image in BGR format (OpenCV) - - Returns: - tuple: (bboxes, track_ids, class_ids, confidences, names) - - bboxes: list of [x1, y1, x2, y2] coordinates - - track_ids: list of tracking IDs (or -1 if no tracking) - - class_ids: list of class indices - - confidences: list of detection confidences - - names: list of class names - """ - results = self.model.track( - source=image, - device=self.device, - conf=0.5, - iou=0.6, - persist=True, - verbose=False, - tracker=self.tracker_config - ) - - if len(results) > 0: - # Extract detection results - bboxes, track_ids, class_ids, confidences, names = extract_detection_results(results[0]) - return bboxes, track_ids, class_ids, confidences, names - - return [], [], [], [], [] - - def visualize_results(self, image, bboxes, track_ids, class_ids, confidences, names): - """ - Generate visualization of detection results. - - Args: - image: Original input image - bboxes: List of bounding boxes - track_ids: List of tracking IDs - class_ids: List of class indices - confidences: List of detection confidences - names: List of class names - - Returns: - Image with visualized detections - """ - return plot_results(image, bboxes, track_ids, class_ids, confidences, names) - - -def main(): - """Example usage of the Yolo2DDetector class.""" - # Initialize video capture - cap = cv2.VideoCapture(0) - - # Initialize detector - detector = Yolo2DDetector() - - enable_person_filter = True - - try: - while cap.isOpened(): - ret, frame = cap.read() - if not ret: - break - - # Process frame - bboxes, track_ids, class_ids, confidences, names = detector.process_image(frame) - - # Apply person filtering if enabled - if enable_person_filter and len(bboxes) > 0: - # Person is class_id 0 in COCO dataset - bboxes, track_ids, class_ids, confidences, names = filter_detections( - bboxes, track_ids, class_ids, confidences, names, - class_filter=[0], # 0 is the class_id for person - name_filter=['person'] - ) - - # Visualize results - if len(bboxes) > 0: - frame = detector.visualize_results( - frame, - bboxes, - track_ids, - class_ids, - confidences, - names - ) - - # Display results - cv2.imshow("YOLO Detection", frame) - if cv2.waitKey(1) & 0xFF == ord('q'): - break - - finally: - cap.release() - cv2.destroyAllWindows() - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/dimos/perception/grasp_generation/__init__.py b/dimos/perception/grasp_generation/__init__.py new file mode 100644 index 0000000000..16281fe0b6 --- /dev/null +++ b/dimos/perception/grasp_generation/__init__.py @@ -0,0 +1 @@ +from .utils import * diff --git a/dimos/perception/grasp_generation/grasp_generation.py b/dimos/perception/grasp_generation/grasp_generation.py new file mode 100644 index 0000000000..adca8dd3e0 --- /dev/null +++ b/dimos/perception/grasp_generation/grasp_generation.py @@ -0,0 +1,229 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Dimensional-hosted grasp generation for manipulation pipeline. +""" + +import asyncio + +import numpy as np +import open3d as o3d + +from dimos.perception.grasp_generation.utils import parse_grasp_results +from dimos.types.manipulation import ObjectData +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.perception.grasp_generation") + + +class HostedGraspGenerator: + """ + Dimensional-hosted grasp generator using WebSocket communication. + """ + + def __init__(self, server_url: str) -> None: + """ + Initialize Dimensional-hosted grasp generator. + + Args: + server_url: WebSocket URL for Dimensional-hosted grasp generator server + """ + self.server_url = server_url + logger.info(f"Initialized grasp generator with server: {server_url}") + + def generate_grasps_from_objects( + self, objects: list[ObjectData], full_pcd: o3d.geometry.PointCloud + ) -> list[dict]: + """ + Generate grasps from ObjectData objects using grasp generator. + + Args: + objects: List of ObjectData with point clouds + full_pcd: Open3D point cloud of full scene + + Returns: + Parsed grasp results as list of dictionaries + """ + try: + # Combine all point clouds + all_points = [] + all_colors = [] + valid_objects = 0 + + for obj in objects: + if "point_cloud_numpy" not in obj or obj["point_cloud_numpy"] is None: + continue + + points = obj["point_cloud_numpy"] + if not isinstance(points, np.ndarray) or points.size == 0: + continue + + if len(points.shape) != 2 or points.shape[1] != 3: + continue + + colors = None + if "colors_numpy" in obj and obj["colors_numpy"] is not None: + colors = obj["colors_numpy"] + if isinstance(colors, np.ndarray) and colors.size > 0: + if ( + colors.shape[0] != points.shape[0] + or len(colors.shape) != 2 + or colors.shape[1] != 3 + ): + colors = None + + all_points.append(points) + if colors is not None: + all_colors.append(colors) + valid_objects += 1 + + if not all_points: + return [] + + # Combine point clouds + combined_points = np.vstack(all_points) + combined_colors = None + if len(all_colors) == valid_objects and len(all_colors) > 0: + combined_colors = np.vstack(all_colors) + + # Send grasp request + grasps = self._send_grasp_request_sync(combined_points, combined_colors) + + if not grasps: + return [] + + # Parse and return results in list of dictionaries format + return parse_grasp_results(grasps) + + except Exception as e: + logger.error(f"Grasp generation failed: {e}") + return [] + + def _send_grasp_request_sync( + self, points: np.ndarray, colors: np.ndarray | None + ) -> list[dict] | None: + """Send synchronous grasp request to grasp server.""" + + try: + # Prepare colors + colors = np.ones((points.shape[0], 3), dtype=np.float32) * 0.5 + + # Ensure correct data types + points = points.astype(np.float32) + colors = colors.astype(np.float32) + + # Validate ranges + if np.any(np.isnan(points)) or np.any(np.isinf(points)): + logger.error("Points contain NaN or Inf values") + return None + if np.any(np.isnan(colors)) or np.any(np.isinf(colors)): + logger.error("Colors contain NaN or Inf values") + return None + + colors = np.clip(colors, 0.0, 1.0) + + # Run async request in sync context + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + result = loop.run_until_complete(self._async_grasp_request(points, colors)) + return result + finally: + loop.close() + + except Exception as e: + logger.error(f"Error in synchronous grasp request: {e}") + return None + + async def _async_grasp_request( + self, points: np.ndarray, colors: np.ndarray + ) -> list[dict] | None: + """Async grasp request helper.""" + import json + + import websockets + + try: + async with websockets.connect(self.server_url) as websocket: + request = { + "points": points.tolist(), + "colors": colors.tolist(), + "lims": [-1.0, 1.0, -1.0, 1.0, 0.0, 2.0], + } + + await websocket.send(json.dumps(request)) + response = await websocket.recv() + grasps = json.loads(response) + + if isinstance(grasps, dict) and "error" in grasps: + logger.error(f"Server returned error: {grasps['error']}") + return None + elif isinstance(grasps, int | float) and grasps == 0: + return None + elif not isinstance(grasps, list): + logger.error(f"Server returned unexpected response type: {type(grasps)}") + return None + elif len(grasps) == 0: + return None + + return self._convert_grasp_format(grasps) + + except Exception as e: + logger.error(f"Async grasp request failed: {e}") + return None + + def _convert_grasp_format(self, grasps: list[dict]) -> list[dict]: + """Convert Dimensional Grasp format to visualization format.""" + converted = [] + + for i, grasp in enumerate(grasps): + rotation_matrix = np.array(grasp.get("rotation_matrix", np.eye(3))) + euler_angles = self._rotation_matrix_to_euler(rotation_matrix) + + converted_grasp = { + "id": f"grasp_{i}", + "score": grasp.get("score", 0.0), + "width": grasp.get("width", 0.0), + "height": grasp.get("height", 0.0), + "depth": grasp.get("depth", 0.0), + "translation": grasp.get("translation", [0, 0, 0]), + "rotation_matrix": rotation_matrix.tolist(), + "euler_angles": euler_angles, + } + converted.append(converted_grasp) + + converted.sort(key=lambda x: x["score"], reverse=True) + return converted + + def _rotation_matrix_to_euler(self, rotation_matrix: np.ndarray) -> dict[str, float]: + """Convert rotation matrix to Euler angles (in radians).""" + sy = np.sqrt(rotation_matrix[0, 0] ** 2 + rotation_matrix[1, 0] ** 2) + + singular = sy < 1e-6 + + if not singular: + x = np.arctan2(rotation_matrix[2, 1], rotation_matrix[2, 2]) + y = np.arctan2(-rotation_matrix[2, 0], sy) + z = np.arctan2(rotation_matrix[1, 0], rotation_matrix[0, 0]) + else: + x = np.arctan2(-rotation_matrix[1, 2], rotation_matrix[1, 1]) + y = np.arctan2(-rotation_matrix[2, 0], sy) + z = 0 + + return {"roll": x, "pitch": y, "yaw": z} + + def cleanup(self) -> None: + """Clean up resources.""" + logger.info("Grasp generator cleaned up") diff --git a/dimos/perception/grasp_generation/utils.py b/dimos/perception/grasp_generation/utils.py new file mode 100644 index 0000000000..d83d02e596 --- /dev/null +++ b/dimos/perception/grasp_generation/utils.py @@ -0,0 +1,528 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utilities for grasp generation and visualization.""" + +import cv2 +import numpy as np +import open3d as o3d + +from dimos.perception.common.utils import project_3d_points_to_2d + + +def create_gripper_geometry( + grasp_data: dict, + finger_length: float = 0.08, + finger_thickness: float = 0.004, +) -> list[o3d.geometry.TriangleMesh]: + """ + Create a simple fork-like gripper geometry from grasp data. + + Args: + grasp_data: Dictionary containing grasp parameters + - translation: 3D position list + - rotation_matrix: 3x3 rotation matrix defining gripper coordinate system + * X-axis: gripper width direction (opening/closing) + * Y-axis: finger length direction + * Z-axis: approach direction (toward object) + - width: Gripper opening width + finger_length: Length of gripper fingers (longer) + finger_thickness: Thickness of gripper fingers + base_height: Height of gripper base (longer) + color: RGB color for the gripper (solid blue) + + Returns: + List of Open3D TriangleMesh geometries for the gripper + """ + + translation = np.array(grasp_data["translation"]) + rotation_matrix = np.array(grasp_data["rotation_matrix"]) + + width = grasp_data.get("width", 0.04) + + # Create transformation matrix + transform = np.eye(4) + transform[:3, :3] = rotation_matrix + transform[:3, 3] = translation + + geometries = [] + + # Gripper dimensions + finger_width = 0.006 # Thickness of each finger + handle_length = 0.05 # Length of handle extending backward + + # Build gripper in local coordinate system: + # X-axis = width direction (left/right finger separation) + # Y-axis = finger length direction (fingers extend along +Y) + # Z-axis = approach direction (toward object, handle extends along -Z) + # IMPORTANT: Fingertips should be at origin (translation point) + + # Create left finger extending along +Y, positioned at +X + left_finger = o3d.geometry.TriangleMesh.create_box( + width=finger_width, # Thin finger + height=finger_length, # Extends along Y (finger length direction) + depth=finger_thickness, # Thin in Z direction + ) + left_finger.translate( + [ + width / 2 - finger_width / 2, # Position at +X (half width from center) + -finger_length, # Shift so fingertips are at origin + -finger_thickness / 2, # Center in Z + ] + ) + + # Create right finger extending along +Y, positioned at -X + right_finger = o3d.geometry.TriangleMesh.create_box( + width=finger_width, # Thin finger + height=finger_length, # Extends along Y (finger length direction) + depth=finger_thickness, # Thin in Z direction + ) + right_finger.translate( + [ + -width / 2 - finger_width / 2, # Position at -X (half width from center) + -finger_length, # Shift so fingertips are at origin + -finger_thickness / 2, # Center in Z + ] + ) + + # Create base connecting fingers - flat like a stickman body + base = o3d.geometry.TriangleMesh.create_box( + width=width + finger_width, # Full width plus finger thickness + height=finger_thickness, # Flat like fingers (stickman style) + depth=finger_thickness, # Thin like fingers + ) + base.translate( + [ + -width / 2 - finger_width / 2, # Start from left finger position + -finger_length - finger_thickness, # Behind fingers, adjusted for fingertips at origin + -finger_thickness / 2, # Center in Z + ] + ) + + # Create handle extending backward - flat stick like stickman arm + handle = o3d.geometry.TriangleMesh.create_box( + width=finger_width, # Same width as fingers + height=handle_length, # Extends backward along Y direction (same plane) + depth=finger_thickness, # Thin like fingers (same plane) + ) + handle.translate( + [ + -finger_width / 2, # Center in X + -finger_length + - finger_thickness + - handle_length, # Extend backward from base, adjusted for fingertips at origin + -finger_thickness / 2, # Same Z plane as other components + ] + ) + + # Use solid red color for all parts (user changed to red) + solid_color = [1.0, 0.0, 0.0] # Red color + + left_finger.paint_uniform_color(solid_color) + right_finger.paint_uniform_color(solid_color) + base.paint_uniform_color(solid_color) + handle.paint_uniform_color(solid_color) + + # Apply transformation to all parts + left_finger.transform(transform) + right_finger.transform(transform) + base.transform(transform) + handle.transform(transform) + + geometries.extend([left_finger, right_finger, base, handle]) + + return geometries + + +def create_all_gripper_geometries( + grasp_list: list[dict], max_grasps: int = -1 +) -> list[o3d.geometry.TriangleMesh]: + """ + Create gripper geometries for multiple grasps. + + Args: + grasp_list: List of grasp dictionaries + max_grasps: Maximum number of grasps to visualize (-1 for all) + + Returns: + List of all gripper geometries + """ + all_geometries = [] + + grasps_to_show = grasp_list if max_grasps < 0 else grasp_list[:max_grasps] + + for grasp in grasps_to_show: + gripper_parts = create_gripper_geometry(grasp) + all_geometries.extend(gripper_parts) + + return all_geometries + + +def draw_grasps_on_image( + image: np.ndarray, + grasp_data: dict | dict[int | str, list[dict]] | list[dict], + camera_intrinsics: list[float] | np.ndarray, # [fx, fy, cx, cy] or 3x3 matrix + max_grasps: int = -1, # -1 means show all grasps + finger_length: float = 0.08, # Match 3D gripper + finger_thickness: float = 0.004, # Match 3D gripper +) -> np.ndarray: + """ + Draw fork-like gripper visualizations on the image matching 3D gripper design. + + Args: + image: Base image to draw on + grasp_data: Can be: + - A single grasp dict + - A list of grasp dicts + - A dictionary mapping object IDs or "scene" to list of grasps + camera_intrinsics: Camera parameters as [fx, fy, cx, cy] list or 3x3 matrix + max_grasps: Maximum number of grasps to visualize (-1 for all) + finger_length: Length of gripper fingers (matches 3D design) + finger_thickness: Thickness of gripper fingers (matches 3D design) + + Returns: + Image with grasps drawn + """ + result = image.copy() + + # Convert camera intrinsics to 3x3 matrix if needed + if isinstance(camera_intrinsics, list) and len(camera_intrinsics) == 4: + fx, fy, cx, cy = camera_intrinsics + camera_matrix = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]]) + else: + camera_matrix = np.array(camera_intrinsics) + + # Convert input to standard format + if isinstance(grasp_data, dict) and not any( + key in grasp_data for key in ["scene", 0, 1, 2, 3, 4, 5] + ): + # Single grasp + grasps_to_draw = [(grasp_data, 0)] + elif isinstance(grasp_data, list): + # List of grasps + grasps_to_draw = [(grasp, i) for i, grasp in enumerate(grasp_data)] + else: + # Dictionary of grasps by object ID + grasps_to_draw = [] + for _obj_id, grasps in grasp_data.items(): + for i, grasp in enumerate(grasps): + grasps_to_draw.append((grasp, i)) + + # Limit number of grasps if specified + if max_grasps > 0: + grasps_to_draw = grasps_to_draw[:max_grasps] + + # Define grasp colors (solid red to match 3D design) + def get_grasp_color(index: int) -> tuple: + # Use solid red color for all grasps to match 3D design + return (0, 0, 255) # Red in BGR format for OpenCV + + # Draw each grasp + for grasp, index in grasps_to_draw: + try: + color = get_grasp_color(index) + thickness = max(1, 4 - index // 3) + + # Extract grasp parameters (using translation and rotation_matrix) + if "translation" not in grasp or "rotation_matrix" not in grasp: + continue + + translation = np.array(grasp["translation"]) + rotation_matrix = np.array(grasp["rotation_matrix"]) + width = grasp.get("width", 0.04) + + # Match 3D gripper dimensions + finger_width = 0.006 # Thickness of each finger (matches 3D) + handle_length = 0.05 # Length of handle extending backward (matches 3D) + + # Create gripper geometry in local coordinate system matching 3D design: + # X-axis = width direction (left/right finger separation) + # Y-axis = finger length direction (fingers extend along +Y) + # Z-axis = approach direction (toward object, handle extends along -Z) + # IMPORTANT: Fingertips should be at origin (translation point) + + # Left finger extending along +Y, positioned at +X + left_finger_points = np.array( + [ + [ + width / 2 - finger_width / 2, + -finger_length, + -finger_thickness / 2, + ], # Back left + [ + width / 2 + finger_width / 2, + -finger_length, + -finger_thickness / 2, + ], # Back right + [ + width / 2 + finger_width / 2, + 0, + -finger_thickness / 2, + ], # Front right (at origin) + [ + width / 2 - finger_width / 2, + 0, + -finger_thickness / 2, + ], # Front left (at origin) + ] + ) + + # Right finger extending along +Y, positioned at -X + right_finger_points = np.array( + [ + [ + -width / 2 - finger_width / 2, + -finger_length, + -finger_thickness / 2, + ], # Back left + [ + -width / 2 + finger_width / 2, + -finger_length, + -finger_thickness / 2, + ], # Back right + [ + -width / 2 + finger_width / 2, + 0, + -finger_thickness / 2, + ], # Front right (at origin) + [ + -width / 2 - finger_width / 2, + 0, + -finger_thickness / 2, + ], # Front left (at origin) + ] + ) + + # Base connecting fingers - flat rectangle behind fingers + base_points = np.array( + [ + [ + -width / 2 - finger_width / 2, + -finger_length - finger_thickness, + -finger_thickness / 2, + ], # Back left + [ + width / 2 + finger_width / 2, + -finger_length - finger_thickness, + -finger_thickness / 2, + ], # Back right + [ + width / 2 + finger_width / 2, + -finger_length, + -finger_thickness / 2, + ], # Front right + [ + -width / 2 - finger_width / 2, + -finger_length, + -finger_thickness / 2, + ], # Front left + ] + ) + + # Handle extending backward - thin rectangle + handle_points = np.array( + [ + [ + -finger_width / 2, + -finger_length - finger_thickness - handle_length, + -finger_thickness / 2, + ], # Back left + [ + finger_width / 2, + -finger_length - finger_thickness - handle_length, + -finger_thickness / 2, + ], # Back right + [ + finger_width / 2, + -finger_length - finger_thickness, + -finger_thickness / 2, + ], # Front right + [ + -finger_width / 2, + -finger_length - finger_thickness, + -finger_thickness / 2, + ], # Front left + ] + ) + + # Transform all points to world frame + def transform_points(points): + # Apply rotation and translation + world_points = (rotation_matrix @ points.T).T + translation + return world_points + + left_finger_world = transform_points(left_finger_points) + right_finger_world = transform_points(right_finger_points) + base_world = transform_points(base_points) + handle_world = transform_points(handle_points) + + # Project to 2D + left_finger_2d = project_3d_points_to_2d(left_finger_world, camera_matrix) + right_finger_2d = project_3d_points_to_2d(right_finger_world, camera_matrix) + base_2d = project_3d_points_to_2d(base_world, camera_matrix) + handle_2d = project_3d_points_to_2d(handle_world, camera_matrix) + + # Draw left finger + pts = left_finger_2d.astype(np.int32) + cv2.polylines(result, [pts], True, color, thickness) + + # Draw right finger + pts = right_finger_2d.astype(np.int32) + cv2.polylines(result, [pts], True, color, thickness) + + # Draw base + pts = base_2d.astype(np.int32) + cv2.polylines(result, [pts], True, color, thickness) + + # Draw handle + pts = handle_2d.astype(np.int32) + cv2.polylines(result, [pts], True, color, thickness) + + # Draw grasp center (fingertips at origin) + center_2d = project_3d_points_to_2d(translation.reshape(1, -1), camera_matrix)[0] + cv2.circle(result, tuple(center_2d.astype(int)), 3, color, -1) + + except Exception: + # Skip this grasp if there's an error + continue + + return result + + +def get_standard_coordinate_transform(): + """ + Get a standard coordinate transformation matrix for consistent visualization. + + This transformation ensures that: + - X (red) axis points right + - Y (green) axis points up + - Z (blue) axis points toward viewer + + Returns: + 4x4 transformation matrix + """ + # Standard transformation matrix to ensure consistent coordinate frame orientation + transform = np.array( + [ + [1, 0, 0, 0], # X points right + [0, -1, 0, 0], # Y points up (flip from OpenCV to standard) + [0, 0, -1, 0], # Z points toward viewer (flip depth) + [0, 0, 0, 1], + ] + ) + return transform + + +def visualize_grasps_3d( + point_cloud: o3d.geometry.PointCloud, + grasp_list: list[dict], + max_grasps: int = -1, +) -> None: + """ + Visualize grasps in 3D with point cloud. + + Args: + point_cloud: Open3D point cloud + grasp_list: List of grasp dictionaries + max_grasps: Maximum number of grasps to visualize + """ + # Apply standard coordinate transformation + transform = get_standard_coordinate_transform() + + # Transform point cloud + pc_copy = o3d.geometry.PointCloud(point_cloud) + pc_copy.transform(transform) + geometries = [pc_copy] + + # Transform gripper geometries + gripper_geometries = create_all_gripper_geometries(grasp_list, max_grasps) + for geom in gripper_geometries: + geom.transform(transform) + geometries.extend(gripper_geometries) + + # Add transformed coordinate frame + origin_frame = o3d.geometry.TriangleMesh.create_coordinate_frame(size=0.1) + origin_frame.transform(transform) + geometries.append(origin_frame) + + o3d.visualization.draw_geometries(geometries, window_name="3D Grasp Visualization") + + +def parse_grasp_results(grasps: list[dict]) -> list[dict]: + """ + Parse grasp results into visualization format. + + Args: + grasps: List of grasp dictionaries + + Returns: + List of dictionaries containing: + - id: Unique grasp identifier + - score: Confidence score (float) + - width: Gripper opening width (float) + - translation: 3D position [x, y, z] + - rotation_matrix: 3x3 rotation matrix as nested list + """ + if not grasps: + return [] + + parsed_grasps = [] + + for i, grasp in enumerate(grasps): + # Extract data from each grasp + translation = grasp.get("translation", [0, 0, 0]) + rotation_matrix = np.array(grasp.get("rotation_matrix", np.eye(3))) + score = float(grasp.get("score", 0.0)) + width = float(grasp.get("width", 0.08)) + + parsed_grasp = { + "id": f"grasp_{i}", + "score": score, + "width": width, + "translation": translation, + "rotation_matrix": rotation_matrix.tolist(), + } + parsed_grasps.append(parsed_grasp) + + return parsed_grasps + + +def create_grasp_overlay( + rgb_image: np.ndarray, + grasps: list[dict], + camera_intrinsics: list[float] | np.ndarray, +) -> np.ndarray: + """ + Create grasp visualization overlay on RGB image. + + Args: + rgb_image: RGB input image + grasps: List of grasp dictionaries in viz format + camera_intrinsics: Camera parameters + + Returns: + RGB image with grasp overlay + """ + try: + bgr_image = cv2.cvtColor(rgb_image, cv2.COLOR_RGB2BGR) + + result_bgr = draw_grasps_on_image( + bgr_image, + grasps, + camera_intrinsics, + max_grasps=-1, + ) + return cv2.cvtColor(result_bgr, cv2.COLOR_BGR2RGB) + except Exception: + return rgb_image.copy() diff --git a/dimos/perception/object_detection_stream.py b/dimos/perception/object_detection_stream.py index 0e4c4783b0..a82cbe9db5 100644 --- a/dimos/perception/object_detection_stream.py +++ b/dimos/perception/object_detection_stream.py @@ -1,18 +1,50 @@ -import cv2 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + import numpy as np -from reactivex import Observable -from reactivex import operators as ops +from reactivex import Observable, operators as ops from dimos.perception.detection2d.yolo_2d_det import Yolo2DDetector -from dimos.perception.detection2d.detic_2d_det import Detic2DDetector + +try: + from dimos.perception.detection2d.detic_2d_det import Detic2DDetector + + DETIC_AVAILABLE = True +except (ModuleNotFoundError, ImportError): + DETIC_AVAILABLE = False + Detic2DDetector = None +from collections.abc import Callable +from typing import TYPE_CHECKING + from dimos.models.depth.metric3d import Metric3D +from dimos.perception.common.utils import draw_object_detection_visualization from dimos.perception.detection2d.utils import ( calculate_depth_from_bbox, calculate_object_size_from_bbox, - calculate_position_rotation_from_bbox + calculate_position_rotation_from_bbox, ) from dimos.types.vector import Vector -from typing import Optional, Union +from dimos.utils.logging_config import setup_logger +from dimos.utils.transform_utils import transform_robot_to_map + +if TYPE_CHECKING: + from dimos.types.manipulation import ObjectData + +# Initialize logger for the ObjectDetectionStream +logger = setup_logger("dimos.perception.object_detection_stream") + class ObjectDetectionStream: """ @@ -21,120 +53,176 @@ class ObjectDetectionStream: 2. Estimates depth using Metric3D 3. Calculates 3D position and dimensions using camera intrinsics 4. Transforms coordinates to map frame - + 5. Draws bounding boxes and segmentation masks on the frame + Provides a stream of structured object data with position and rotation information. """ - + def __init__( self, camera_intrinsics=None, # [fx, fy, cx, cy] - device="cuda", - gt_depth_scale=1000.0, - min_confidence=0.5, + device: str = "cuda", + gt_depth_scale: float = 1000.0, + min_confidence: float = 0.7, class_filter=None, # Optional list of class names to filter (e.g., ["person", "car"]) - transform_to_map=None, # Optional function to transform coordinates to map frame - detector: Optional[Union[Detic2DDetector, Yolo2DDetector]] = None, - video_stream: Observable = None - ): + get_pose: Callable | None = None, # Optional function to transform coordinates to map frame + detector: Detic2DDetector | Yolo2DDetector | None = None, + video_stream: Observable = None, + disable_depth: bool = False, # Flag to disable monocular Metric3D depth estimation + draw_masks: bool = False, # Flag to enable drawing segmentation masks + ) -> None: """ Initialize the ObjectDetectionStream. - + Args: camera_intrinsics: List [fx, fy, cx, cy] with camera parameters device: Device to run inference on ("cuda" or "cpu") gt_depth_scale: Ground truth depth scale for Metric3D min_confidence: Minimum confidence for detections class_filter: Optional list of class names to filter - transform_to_map: Optional function to transform pose to map coordinates + get_pose: Optional function to transform pose to map coordinates detector: Optional detector instance (Detic or Yolo) video_stream: Observable of video frames to process (if provided, returns a stream immediately) + disable_depth: Flag to disable monocular Metric3D depth estimation + draw_masks: Flag to enable drawing segmentation masks """ self.min_confidence = min_confidence self.class_filter = class_filter - self.transform_to_map = transform_to_map + self.get_pose = get_pose + self.disable_depth = disable_depth + self.draw_masks = draw_masks # Initialize object detector - self.detector = detector or Detic2DDetector(vocabulary=None, threshold=min_confidence) - - # Initialize depth estimation model - self.depth_model = Metric3D(gt_depth_scale) - + if detector is not None: + self.detector = detector + else: + if DETIC_AVAILABLE: + try: + self.detector = Detic2DDetector(vocabulary=None, threshold=min_confidence) + logger.info("Using Detic2DDetector") + except Exception as e: + logger.warning( + f"Failed to initialize Detic2DDetector: {e}. Falling back to Yolo2DDetector." + ) + self.detector = Yolo2DDetector() + else: + logger.info("Detic not available. Using Yolo2DDetector.") + self.detector = Yolo2DDetector() # Set up camera intrinsics self.camera_intrinsics = camera_intrinsics - if camera_intrinsics is not None: - self.depth_model.update_intrinsic(camera_intrinsics) - - # Create 3x3 camera matrix for calculations - fx, fy, cx, cy = camera_intrinsics - self.camera_matrix = np.array([ - [fx, 0, cx], - [0, fy, cy], - [0, 0, 1] - ], dtype=np.float32) + + # Initialize depth estimation model + self.depth_model = None + if not disable_depth: + try: + self.depth_model = Metric3D(gt_depth_scale) + + if camera_intrinsics is not None: + self.depth_model.update_intrinsic(camera_intrinsics) + + # Create 3x3 camera matrix for calculations + fx, fy, cx, cy = camera_intrinsics + self.camera_matrix = np.array( + [[fx, 0, cx], [0, fy, cy], [0, 0, 1]], dtype=np.float32 + ) + else: + raise ValueError("camera_intrinsics must be provided") + + logger.info("Depth estimation enabled with Metric3D") + except Exception as e: + logger.warning(f"Failed to initialize Metric3D depth model: {e}") + logger.warning("Falling back to disable_depth=True mode") + self.disable_depth = True + self.depth_model = None else: - raise ValueError("camera_intrinsics must be provided") - + logger.info("Depth estimation disabled") + # If video_stream is provided, create and store the stream immediately self.stream = None if video_stream is not None: self.stream = self.create_stream(video_stream) - + def create_stream(self, video_stream: Observable) -> Observable: """ Create an Observable stream of object data from a video stream. - + Args: video_stream: Observable that emits video frames - + Returns: - Observable that emits dictionaries containing object data + Observable that emits dictionaries containing object data with position and rotation information """ + def process_frame(frame): - # Detect objects - bboxes, track_ids, class_ids, confidences, names = self.detector.process_image(frame) - + # TODO: More modular detector output interface + bboxes, track_ids, class_ids, confidences, names, *mask_data = ( + *self.detector.process_image(frame), + [], + ) + + masks = ( + mask_data[0] + if mask_data and len(mask_data[0]) == len(bboxes) + else [None] * len(bboxes) + ) + # Create visualization viz_frame = frame.copy() - + # Process detections objects = [] - + if not self.disable_depth: + depth_map = self.depth_model.infer_depth(frame) + depth_map = np.array(depth_map) + else: + depth_map = None + for i, bbox in enumerate(bboxes): # Skip if confidence is too low if i < len(confidences) and confidences[i] < self.min_confidence: continue - + # Skip if class filter is active and class not in filter class_name = names[i] if i < len(names) else None if self.class_filter and class_name not in self.class_filter: continue - - # Get depth for this object - depth = calculate_depth_from_bbox(self.depth_model, frame, bbox) - if depth is None: - # Skip objects with invalid depth - continue - - # Calculate object position and rotation - position, rotation = calculate_position_rotation_from_bbox(bbox, depth, self.camera_intrinsics) - - # Get object dimensions - width, height = calculate_object_size_from_bbox(bbox, depth, self.camera_intrinsics) - - # Transform to map frame if a transform function is provided - try: - if self.transform_to_map: - position = Vector([position['x'], position['y'], position['z']]) - rotation = Vector([rotation['roll'], rotation['pitch'], rotation['yaw']]) - position, rotation = self.transform_to_map(position, rotation, source_frame="base_link") - position = dict(x=position.x, y=position.y, z=position.z) - rotation = dict(roll=rotation.x, pitch=rotation.y, yaw=rotation.z) - except Exception as e: - print(f"Error transforming to map frame: {e}") - position, rotation = position, rotation - - # Create object data dictionary - object_data = { + + if not self.disable_depth and depth_map is not None: + # Get depth for this object + depth = calculate_depth_from_bbox(depth_map, bbox) + if depth is None: + # Skip objects with invalid depth + continue + # Calculate object position and rotation + position, rotation = calculate_position_rotation_from_bbox( + bbox, depth, self.camera_intrinsics + ) + # Get object dimensions + width, height = calculate_object_size_from_bbox( + bbox, depth, self.camera_intrinsics + ) + + # Transform to map frame if a transform function is provided + try: + if self.get_pose: + # position and rotation are already Vector objects, no need to convert + robot_pose = self.get_pose() + position, rotation = transform_robot_to_map( + robot_pose["position"], robot_pose["rotation"], position, rotation + ) + except Exception as e: + logger.error(f"Error transforming to map frame: {e}") + position, rotation = position, rotation + + else: + depth = -1 + position = Vector(0, 0, 0) + rotation = Vector(0, 0, 0) + width = -1 + height = -1 + + # Create a properly typed ObjectData instance + object_data: ObjectData = { "object_id": track_ids[i] if i < len(track_ids) else -1, "bbox": bbox, "depth": depth, @@ -143,103 +231,88 @@ def process_frame(frame): "label": class_name, "position": position, "rotation": rotation, - "size": { - "width": width, - "height": height - } + "size": {"width": width, "height": height}, + "segmentation_mask": masks[i], } - + objects.append(object_data) - - # Add visualization - x1, y1, x2, y2 = map(int, bbox) - color = (0, 255, 0) # Green for detected objects - - # Draw bounding box - cv2.rectangle(viz_frame, (x1, y1), (x2, y2), color, 2) - - # Add text for class and position - text = f"{class_name}: {depth:.2f}m" - pos_text = f"Pos: ({position['x']:.2f}, {position['y']:.2f})" - - # Draw text background - text_size = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 2)[0] - cv2.rectangle(viz_frame, (x1, y1 - text_size[1] - 5), (x1 + text_size[0], y1), (0, 0, 0), -1) - - # Draw text - cv2.putText(viz_frame, text, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2) - - # Position text below - cv2.putText(viz_frame, pos_text, (x1, y1 + 15), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2) - - return { - "frame": frame, - "viz_frame": viz_frame, - "objects": objects - } - self.stream = video_stream.pipe( - ops.map(process_frame) - ) + + # Create visualization using common function + viz_frame = draw_object_detection_visualization( + viz_frame, objects, draw_masks=self.draw_masks, font_scale=1.5 + ) + + return {"frame": frame, "viz_frame": viz_frame, "objects": objects} + + self.stream = video_stream.pipe(ops.map(process_frame)) return self.stream - + def get_stream(self): """ Returns the current detection stream if available. Creates a new one with the provided video_stream if not already created. - + Returns: Observable: The reactive stream of detection results """ if self.stream is None: - raise ValueError("Stream not initialized. Either provide a video_stream during initialization or call create_stream first.") + raise ValueError( + "Stream not initialized. Either provide a video_stream during initialization or call create_stream first." + ) return self.stream - + def get_formatted_stream(self): """ Returns a formatted stream of object detection data for better readability. This is especially useful for LLMs like Claude that need structured text input. - + Returns: Observable: A stream of formatted string representations of object data """ if self.stream is None: - raise ValueError("Stream not initialized. Either provide a video_stream during initialization or call create_stream first.") - + raise ValueError( + "Stream not initialized. Either provide a video_stream during initialization or call create_stream first." + ) + def format_detection_data(result): # Extract objects from result objects = result.get("objects", []) if not objects: return "No objects detected." - + formatted_data = "[DETECTED OBJECTS]\n" - - for i, obj in enumerate(objects): - pos = obj["position"] - rot = obj["rotation"] - size = obj["size"] - bbox = obj["bbox"] - - # Format each object with a multiline f-string for better readability - bbox_str = f"[{int(bbox[0])}, {int(bbox[1])}, {int(bbox[2])}, {int(bbox[3])}]" - formatted_data += f"Object {i+1}: {obj['label']}\n"\ - f" ID: {obj['object_id']}\n"\ - f" Confidence: {obj['confidence']:.2f}\n"\ - f" Position: x={pos['x']:.2f}m, y={pos['y']:.2f}m, z={pos['z']:.2f}m\n"\ - f" Rotation: yaw={rot['yaw']:.2f} rad\n"\ - f" Size: width={size['width']:.2f}m, height={size['height']:.2f}m\n"\ - f" Depth: {obj['depth']:.2f}m\n"\ - f" Bounding box: {bbox_str}\n"\ - "----------------------------------\n" - + try: + for i, obj in enumerate(objects): + pos = obj["position"] + rot = obj["rotation"] + size = obj["size"] + bbox = obj["bbox"] + + # Format each object with a multiline f-string for better readability + bbox_str = f"[{bbox[0]}, {bbox[1]}, {bbox[2]}, {bbox[3]}]" + formatted_data += ( + f"Object {i + 1}: {obj['label']}\n" + f" ID: {obj['object_id']}\n" + f" Confidence: {obj['confidence']:.2f}\n" + f" Position: x={pos.x:.2f}m, y={pos.y:.2f}m, z={pos.z:.2f}m\n" + f" Rotation: yaw={rot.z:.2f} rad\n" + f" Size: width={size['width']:.2f}m, height={size['height']:.2f}m\n" + f" Depth: {obj['depth']:.2f}m\n" + f" Bounding box: {bbox_str}\n" + "----------------------------------\n" + ) + except Exception as e: + logger.warning(f"Error formatting object {i}: {e}") + formatted_data += f"Object {i + 1}: [Error formatting data]" + formatted_data += "\n----------------------------------\n" + return formatted_data - + # Return a new stream with the formatter applied - return self.stream.pipe( - ops.map(format_detection_data) - ) + return self.stream.pipe(ops.map(format_detection_data)) - def cleanup(self): + def cleanup(self) -> None: """Clean up resources.""" pass diff --git a/dimos/perception/object_tracker.py b/dimos/perception/object_tracker.py index 578208d148..f5fa48581a 100644 --- a/dimos/perception/object_tracker.py +++ b/dimos/perception/object_tracker.py @@ -1,302 +1,629 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import threading +import time + import cv2 -from reactivex import Observable -from reactivex import operators as ops +from dimos_lcm.sensor_msgs import CameraInfo + +# Import LCM messages +from dimos_lcm.vision_msgs import ( + Detection2D, + Detection3D, + ObjectHypothesisWithPose, +) import numpy as np -from dimos.perception.common.ibvs import ObjectDistanceEstimator -from dimos.models.depth.metric3d import Metric3D -from dimos.perception.detection2d.utils import calculate_depth_from_bbox -class ObjectTrackingStream: - def __init__(self, camera_intrinsics=None, camera_pitch=0.0, camera_height=1.0, - reid_threshold=5, reid_fail_tolerance=10, gt_depth_scale=1000.0): +from reactivex.disposable import Disposable + +from dimos.core import In, Module, Out, rpc +from dimos.manipulation.visual_servoing.utils import visualize_detections_3d +from dimos.msgs.geometry_msgs import Pose, Quaternion, Transform, Vector3 +from dimos.msgs.sensor_msgs import Image, ImageFormat +from dimos.msgs.std_msgs import Header +from dimos.msgs.vision_msgs import Detection2DArray, Detection3DArray +from dimos.protocol.tf import TF +from dimos.types.timestamped import align_timestamped +from dimos.utils.logging_config import setup_logger +from dimos.utils.transform_utils import ( + euler_to_quaternion, + optical_to_robot_frame, + yaw_towards_point, +) + +logger = setup_logger("dimos.perception.object_tracker") + + +class ObjectTracking(Module): + """Module for object tracking with LCM input/output.""" + + # LCM inputs + color_image: In[Image] = None + depth: In[Image] = None + camera_info: In[CameraInfo] = None + + # LCM outputs + detection2darray: Out[Detection2DArray] = None + detection3darray: Out[Detection3DArray] = None + tracked_overlay: Out[Image] = None # Visualization output + + def __init__( + self, + reid_threshold: int = 10, + reid_fail_tolerance: int = 5, + frame_id: str = "camera_link", + ) -> None: """ - Initialize an object tracking stream using OpenCV's CSRT tracker with ORB re-ID. - + Initialize an object tracking module using OpenCV's CSRT tracker with ORB re-ID. + Args: - camera_intrinsics: List in format [fx, fy, cx, cy] where: - - fx: Focal length in x direction (pixels) - - fy: Focal length in y direction (pixels) - - cx: Principal point x-coordinate (pixels) - - cy: Principal point y-coordinate (pixels) - camera_pitch: Camera pitch angle in radians (positive is up) - camera_height: Height of the camera from the ground in meters + camera_intrinsics: Optional [fx, fy, cx, cy] camera parameters. + If None, will use camera_info input. reid_threshold: Minimum good feature matches needed to confirm re-ID. - reid_fail_tolerance: Number of consecutive frames Re-ID can fail before + reid_fail_tolerance: Number of consecutive frames Re-ID can fail before tracking is stopped. - gt_depth_scale: Ground truth depth scale factor for Metric3D model + frame_id: TF frame ID for the camera (default: "camera_link") """ + # Call parent Module init + super().__init__() + + self.camera_intrinsics = None + self.reid_threshold = reid_threshold + self.reid_fail_tolerance = reid_fail_tolerance + self.frame_id = frame_id + self.tracker = None - self.tracking_bbox = None # Stores (x, y, w, h) for tracker initialization + self.tracking_bbox = None # Stores (x, y, w, h) for tracker initialization self.tracking_initialized = False self.orb = cv2.ORB_create() self.bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False) - self.original_des = None # Store original ORB descriptors - self.reid_threshold = reid_threshold - self.reid_fail_tolerance = reid_fail_tolerance - self.reid_fail_count = 0 # Counter for consecutive re-id failures - - # Initialize distance estimator if camera parameters are provided - self.distance_estimator = None - if camera_intrinsics is not None: - # Convert [fx, fy, cx, cy] to 3x3 camera matrix - fx, fy, cx, cy = camera_intrinsics - K = np.array([ - [fx, 0, cx], - [0, fy, cy], - [0, 0, 1] - ], dtype=np.float32) - - self.distance_estimator = ObjectDistanceEstimator( - K=K, - camera_pitch=camera_pitch, - camera_height=camera_height - ) - - # Initialize depth model - self.depth_model = Metric3D(gt_depth_scale) - if camera_intrinsics is not None: - self.depth_model.update_intrinsic(camera_intrinsics) - - def track(self, bbox, frame=None, distance=None, size=None): + self.original_des = None # Store original ORB descriptors + self.original_kps = None # Store original ORB keypoints + self.reid_fail_count = 0 # Counter for consecutive re-id failures + self.last_good_matches = [] # Store good matches for visualization + self.last_roi_kps = None # Store last ROI keypoints for visualization + self.last_roi_bbox = None # Store last ROI bbox for visualization + self.reid_confirmed = False # Store current reid confirmation state + self.tracking_frame_count = 0 # Count frames since tracking started + self.reid_warmup_frames = 3 # Number of frames before REID starts + + self._frame_lock = threading.Lock() + self._latest_rgb_frame: np.ndarray | None = None + self._latest_depth_frame: np.ndarray | None = None + self._latest_camera_info: CameraInfo | None = None + + # Tracking thread control + self.tracking_thread: threading.Thread | None = None + self.stop_tracking = threading.Event() + self.tracking_rate = 30.0 # Hz + self.tracking_period = 1.0 / self.tracking_rate + + # Initialize TF publisher + self.tf = TF() + + # Store latest detections for RPC access + self._latest_detection2d: Detection2DArray | None = None + self._latest_detection3d: Detection3DArray | None = None + self._detection_event = threading.Event() + + @rpc + def start(self) -> None: + super().start() + + # Subscribe to aligned rgb and depth streams + def on_aligned_frames(frames_tuple) -> None: + rgb_msg, depth_msg = frames_tuple + with self._frame_lock: + self._latest_rgb_frame = rgb_msg.data + + depth_data = depth_msg.data + # Convert from millimeters to meters if depth is DEPTH16 format + if depth_msg.format == ImageFormat.DEPTH16: + depth_data = depth_data.astype(np.float32) / 1000.0 + self._latest_depth_frame = depth_data + + # Create aligned observable for RGB and depth + aligned_frames = align_timestamped( + self.color_image.observable(), + self.depth.observable(), + buffer_size=2.0, # 2 second buffer + match_tolerance=0.5, # 500ms tolerance + ) + unsub = aligned_frames.subscribe(on_aligned_frames) + self._disposables.add(unsub) + + # Subscribe to camera info stream separately (doesn't need alignment) + def on_camera_info(camera_info_msg: CameraInfo) -> None: + self._latest_camera_info = camera_info_msg + # Extract intrinsics from camera info K matrix + # K is a 3x3 matrix in row-major order: [fx, 0, cx, 0, fy, cy, 0, 0, 1] + self.camera_intrinsics = [ + camera_info_msg.K[0], + camera_info_msg.K[4], + camera_info_msg.K[2], + camera_info_msg.K[5], + ] + + unsub = self.camera_info.subscribe(on_camera_info) + self._disposables.add(Disposable(unsub)) + + @rpc + def stop(self) -> None: + self.stop_track() + + self.stop_tracking.set() + + if self.tracking_thread and self.tracking_thread.is_alive(): + self.tracking_thread.join(timeout=2.0) + + super().stop() + + @rpc + def track( + self, + bbox: list[float], + ) -> dict: """ - Set the initial bounding box for tracking. Features are extracted later. - + Initialize tracking with a bounding box and process current frame. + Args: bbox: Bounding box in format [x1, y1, x2, y2] - frame: Optional - Current frame for depth estimation and feature extraction - distance: Optional - Known distance to object (meters) - size: Optional - Known size of object (meters) - + Returns: - bool: True if intention to track is set (bbox is valid) + Dict containing tracking results with 2D and 3D detections """ + if self._latest_rgb_frame is None: + logger.warning("No RGB frame available for tracking") + + # Initialize tracking x1, y1, x2, y2 = map(int, bbox) w, h = x2 - x1, y2 - y1 if w <= 0 or h <= 0: - print(f"Warning: Invalid initial bbox provided: {bbox}. Tracking not started.") - self.stop_track() # Ensure clean state - return False - - self.tracking_bbox = (x1, y1, w, h) # Store in (x, y, w, h) format + logger.warning(f"Invalid initial bbox provided: {bbox}. Tracking not started.") + + # Set tracking parameters + self.tracking_bbox = (x1, y1, w, h) # Store in (x, y, w, h) format self.tracker = cv2.legacy.TrackerCSRT_create() - self.tracking_initialized = False # Reset flag - self.original_des = None # Clear previous descriptors - self.reid_fail_count = 0 # Reset counter on new track - print(f"Tracking target set with bbox: {self.tracking_bbox}") - - # Calculate depth only if distance and size not provided - if frame is not None and distance is None and size is None: - depth_estimate = calculate_depth_from_bbox(self.depth_model, frame, bbox) - if depth_estimate is not None: - print(f"Estimated depth for object: {depth_estimate:.2f}m") - - # Update distance estimator if needed - if self.distance_estimator is not None: - if size is not None: self.distance_estimator.set_estimated_object_size(size) - elif distance is not None: self.distance_estimator.estimate_object_size(bbox, distance) - elif depth_estimate is not None: self.distance_estimator.estimate_object_size(bbox, depth_estimate) - else: print("No distance or size provided. Cannot estimate object size.") - - return True # Indicate intention to track is set - - def calculate_depth_from_bbox(self, frame, bbox): - """ - Calculate the average depth of an object within a bounding box. - Uses the 25th to 75th percentile range to filter outliers. - - Args: - frame: The image frame - bbox: Bounding box in format [x1, y1, x2, y2] - - Returns: - float: Average depth in meters, or None if depth estimation fails - """ - try: - # Get depth map for the entire frame - depth_map = self.depth_model.infer_depth(frame) - depth_map = np.array(depth_map) - - # Extract region of interest from the depth map - x1, y1, x2, y2 = map(int, bbox) - roi_depth = depth_map[y1:y2, x1:x2] - - if roi_depth.size == 0: - return None - - # Calculate 25th and 75th percentile to filter outliers - p25 = np.percentile(roi_depth, 25) - p75 = np.percentile(roi_depth, 75) - - # Filter depth values within this range - filtered_depth = roi_depth[(roi_depth >= p25) & (roi_depth <= p75)] - - # Calculate average depth (convert to meters) - if filtered_depth.size > 0: - return np.mean(filtered_depth) / 1000.0 # Convert mm to meters - - return None - except Exception as e: - print(f"Error calculating depth from bbox: {e}") - return None - + self.tracking_initialized = False + self.original_des = None + self.reid_fail_count = 0 + logger.info(f"Tracking target set with bbox: {self.tracking_bbox}") + + # Extract initial features + roi = self._latest_rgb_frame[y1:y2, x1:x2] + if roi.size > 0: + self.original_kps, self.original_des = self.orb.detectAndCompute(roi, None) + if self.original_des is None: + logger.warning("No ORB features found in initial ROI. REID will be disabled.") + else: + logger.info(f"Initial ORB features extracted: {len(self.original_des)}") + + # Initialize the tracker + init_success = self.tracker.init(self._latest_rgb_frame, self.tracking_bbox) + if init_success: + self.tracking_initialized = True + self.tracking_frame_count = 0 # Reset frame counter + logger.info("Tracker initialized successfully.") + else: + logger.error("Tracker initialization failed.") + self.stop_track() + else: + logger.error("Empty ROI during tracker initialization.") + self.stop_track() + + # Start tracking thread + self._start_tracking_thread() + + # Return initial tracking result + return {"status": "tracking_started", "bbox": self.tracking_bbox} + def reid(self, frame, current_bbox) -> bool: """Check if features in current_bbox match stored original features.""" - if self.original_des is None: return True # Cannot re-id if no original features + # During warm-up period, always return True + if self.tracking_frame_count < self.reid_warmup_frames: + return True + + if self.original_des is None: + return False x1, y1, x2, y2 = map(int, current_bbox) roi = frame[y1:y2, x1:x2] - if roi.size == 0: return False # Empty ROI cannot match + if roi.size == 0: + return False # Empty ROI cannot match + + kps_current, des_current = self.orb.detectAndCompute(roi, None) + if des_current is None or len(des_current) < 2: + return False # Need at least 2 descriptors for knnMatch - _, des_current = self.orb.detectAndCompute(roi, None) - if des_current is None or len(des_current) < 2: return False # Need at least 2 descriptors for knnMatch + # Store ROI keypoints and bbox for visualization + self.last_roi_kps = kps_current + self.last_roi_bbox = [x1, y1, x2, y2] # Handle case where original_des has only 1 descriptor (cannot use knnMatch with k=2) if len(self.original_des) < 2: matches = self.bf.match(self.original_des, des_current) + self.last_good_matches = matches # Store all matches for visualization good_matches = len(matches) else: matches = self.bf.knnMatch(self.original_des, des_current, k=2) # Apply Lowe's ratio test robustly + good_matches_list = [] good_matches = 0 for match_pair in matches: if len(match_pair) == 2: m, n = match_pair if m.distance < 0.75 * n.distance: + good_matches_list.append(m) good_matches += 1 - - # print(f"ReID: Good Matches={good_matches}, Threshold={self.reid_threshold}") # Debug + self.last_good_matches = good_matches_list # Store good matches for visualization + return good_matches >= self.reid_threshold - def stop_track(self): + def _start_tracking_thread(self) -> None: + """Start the tracking thread.""" + self.stop_tracking.clear() + self.tracking_thread = threading.Thread(target=self._tracking_loop, daemon=True) + self.tracking_thread.start() + logger.info("Started tracking thread") + + def _tracking_loop(self) -> None: + """Main tracking loop that runs in a separate thread.""" + while not self.stop_tracking.is_set() and self.tracking_initialized: + # Process tracking for current frame + self._process_tracking() + + # Sleep to maintain tracking rate + time.sleep(self.tracking_period) + + logger.info("Tracking loop ended") + + def _reset_tracking_state(self) -> None: + """Reset tracking state without stopping the thread.""" + self.tracker = None + self.tracking_bbox = None + self.tracking_initialized = False + self.original_des = None + self.original_kps = None + self.reid_fail_count = 0 # Reset counter + self.last_good_matches = [] + self.last_roi_kps = None + self.last_roi_bbox = None + self.reid_confirmed = False # Reset reid confirmation state + self.tracking_frame_count = 0 # Reset frame counter + + # Publish empty detections to clear any visualizations + empty_2d = Detection2DArray(detections_length=0, header=Header(), detections=[]) + empty_3d = Detection3DArray(detections_length=0, header=Header(), detections=[]) + self._latest_detection2d = empty_2d + self._latest_detection3d = empty_3d + self._detection_event.clear() + self.detection2darray.publish(empty_2d) + self.detection3darray.publish(empty_3d) + + @rpc + def stop_track(self) -> bool: """ Stop tracking the current object. This resets the tracker and all tracking state. - + Returns: bool: True if tracking was successfully stopped """ - self.tracker = None - self.tracking_bbox = None - self.tracking_initialized = False - self.original_des = None - self.reid_fail_count = 0 # Reset counter + # Reset tracking state first + self._reset_tracking_state() + + # Stop tracking thread if running (only if called from outside the thread) + if self.tracking_thread and self.tracking_thread.is_alive(): + # Check if we're being called from within the tracking thread + if threading.current_thread() != self.tracking_thread: + self.stop_tracking.set() + self.tracking_thread.join(timeout=1.0) + self.tracking_thread = None + else: + # If called from within thread, just set the stop flag + self.stop_tracking.set() + + logger.info("Tracking stopped") return True - - def create_stream(self, video_stream: Observable) -> Observable: + + @rpc + def is_tracking(self) -> bool: """ - Create an Observable stream of object tracking results from a video stream. - - Args: - video_stream: Observable that emits video frames - + Check if the tracker is currently tracking an object successfully. + Returns: - Observable that emits dictionaries containing tracking results and visualizations + bool: True if tracking is active and REID is confirmed, False otherwise """ - def process_frame(frame): - viz_frame = frame.copy() - tracker_succeeded = False # Success from tracker.update() - reid_confirmed_this_frame = False # Result of reid() call for this frame - final_success = False # Overall success considering re-id tolerance - target_data = None - current_bbox_x1y1x2y2 = None # Store current bbox if tracking succeeds - - if self.tracker is not None and self.tracking_bbox is not None: - if not self.tracking_initialized: - # Extract initial features and initialize tracker on first frame - x_init, y_init, w_init, h_init = self.tracking_bbox - roi = frame[y_init:y_init+h_init, x_init:x_init+w_init] - - if roi.size > 0: - _, self.original_des = self.orb.detectAndCompute(roi, None) - if self.original_des is None: - print("Warning: No ORB features found in initial ROI during stream processing.") - else: - print(f"Initial ORB features extracted: {len(self.original_des)}") - - # Initialize the tracker - init_success = self.tracker.init(frame, self.tracking_bbox) - if init_success: - self.tracking_initialized = True - tracker_succeeded = True - reid_confirmed_this_frame = True # Assume re-id true on init - current_bbox_x1y1x2y2 = [x_init, y_init, x_init + w_init, y_init + h_init] - print("Tracker initialized successfully.") - else: - print("Error: Tracker initialization failed in stream.") - self.stop_track() # Reset if init fails - else: - print("Error: Empty ROI during tracker initialization in stream.") - self.stop_track() # Reset if ROI is bad - - else: # Tracker already initialized, perform update and re-id - tracker_succeeded, bbox_cv = self.tracker.update(frame) - if tracker_succeeded: - x, y, w, h = map(int, bbox_cv) - current_bbox_x1y1x2y2 = [x, y, x + w, y + h] - # Perform re-ID check - reid_confirmed_this_frame = self.reid(frame, current_bbox_x1y1x2y2) - - if reid_confirmed_this_frame: - self.reid_fail_count = 0 # Reset counter on success - else: - self.reid_fail_count += 1 # Increment counter on failure - print(f"Re-ID failed ({self.reid_fail_count}/{self.reid_fail_tolerance}). Continuing track...") - - # --- Determine final success and stop tracking if needed --- - if tracker_succeeded: - if self.reid_fail_count >= self.reid_fail_tolerance: - print(f"Re-ID failed consecutively {self.reid_fail_count} times. Target lost.") - final_success = False # Stop tracking - else: - final_success = True # Tracker ok, Re-ID ok or within tolerance - else: # Tracker update failed + return self.tracking_initialized and self.reid_confirmed + + def _process_tracking(self) -> None: + """Process current frame for tracking and publish detections.""" + if self.tracker is None or not self.tracking_initialized: + return + + # Get local copies of frames under lock + with self._frame_lock: + if self._latest_rgb_frame is None or self._latest_depth_frame is None: + return + frame = self._latest_rgb_frame.copy() + depth_frame = self._latest_depth_frame.copy() + tracker_succeeded = False + reid_confirmed_this_frame = False + final_success = False + current_bbox_x1y1x2y2 = None + + # Perform tracker update + tracker_succeeded, bbox_cv = self.tracker.update(frame) + if tracker_succeeded: + x, y, w, h = map(int, bbox_cv) + current_bbox_x1y1x2y2 = [x, y, x + w, y + h] + # Perform re-ID check + reid_confirmed_this_frame = self.reid(frame, current_bbox_x1y1x2y2) + self.reid_confirmed = reid_confirmed_this_frame # Store for is_tracking() RPC + + if reid_confirmed_this_frame: + self.reid_fail_count = 0 + else: + self.reid_fail_count += 1 + else: + self.reid_confirmed = False # No tracking if tracker failed + + # Determine final success + if tracker_succeeded: + if self.reid_fail_count >= self.reid_fail_tolerance: + logger.warning( + f"Re-ID failed consecutively {self.reid_fail_count} times. Target lost." + ) final_success = False - if self.tracking_initialized: - print("Tracker update failed. Stopping track.") - - # --- Post-processing based on final_success --- - if final_success and current_bbox_x1y1x2y2 is not None: - # Tracking is considered successful (tracker ok, re-id ok or within tolerance) - x1, y1, x2, y2 = current_bbox_x1y1x2y2 - # Visualize based on *this frame's* re-id result - viz_color = (0, 255, 0) if reid_confirmed_this_frame else (0, 165, 255) # Green if confirmed, Orange if failed but tolerated - cv2.rectangle(viz_frame, (x1, y1), (x2, y2), viz_color, 2) - - target_data = { - "target_id": 0, "bbox": current_bbox_x1y1x2y2, "confidence": 1.0, - "reid_confirmed": reid_confirmed_this_frame # Report actual re-id status - } - - dist_text = "Object Tracking" - if not reid_confirmed_this_frame: dist_text += " (Re-ID Failed - Tolerated)" - - if self.distance_estimator is not None and self.distance_estimator.estimated_object_size is not None: - distance, angle = self.distance_estimator.estimate_distance_angle(current_bbox_x1y1x2y2) - if distance is not None: - target_data["distance"] = distance - target_data["angle"] = angle - dist_text = f"Object: {distance:.2f}m, {np.rad2deg(angle):.1f} deg" - if not reid_confirmed_this_frame: dist_text += " (Re-ID Failed - Tolerated)" - - text_size = cv2.getTextSize(dist_text, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)[0] - label_bg_y = max(y1 - text_size[1] - 5, 0) - cv2.rectangle(viz_frame, (x1, label_bg_y), (x1 + text_size[0], y1), (0,0,0), -1) - cv2.putText(viz_frame, dist_text, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1) - - elif self.tracking_initialized: # Tracking stopped this frame (either tracker fail or re-id tolerance exceeded) - self.stop_track() # Reset tracker state and counter - - # else: # Not tracking or initialization failed, do nothing, return empty result - # pass - - return { - "frame": frame, - "viz_frame": viz_frame, - "targets": [target_data] if target_data else [] - } - - return video_stream.pipe( - ops.map(process_frame) + self._reset_tracking_state() + else: + final_success = True + else: + final_success = False + if self.tracking_initialized: + logger.info("Tracker update failed. Stopping track.") + self._reset_tracking_state() + + self.tracking_frame_count += 1 + + if not reid_confirmed_this_frame and self.tracking_frame_count >= self.reid_warmup_frames: + return + + # Create detections if tracking succeeded + header = Header(self.frame_id) + detection2darray = Detection2DArray(detections_length=0, header=header, detections=[]) + detection3darray = Detection3DArray(detections_length=0, header=header, detections=[]) + + if final_success and current_bbox_x1y1x2y2 is not None: + x1, y1, x2, y2 = current_bbox_x1y1x2y2 + center_x = (x1 + x2) / 2.0 + center_y = (y1 + y2) / 2.0 + width = float(x2 - x1) + height = float(y2 - y1) + + # Create Detection2D + detection_2d = Detection2D() + detection_2d.id = "0" + detection_2d.results_length = 1 + detection_2d.header = header + + # Create hypothesis + hypothesis = ObjectHypothesisWithPose() + hypothesis.hypothesis.class_id = "tracked_object" + hypothesis.hypothesis.score = 1.0 + detection_2d.results = [hypothesis] + + # Create bounding box + detection_2d.bbox.center.position.x = center_x + detection_2d.bbox.center.position.y = center_y + detection_2d.bbox.center.theta = 0.0 + detection_2d.bbox.size_x = width + detection_2d.bbox.size_y = height + + detection2darray = Detection2DArray() + detection2darray.detections_length = 1 + detection2darray.header = header + detection2darray.detections = [detection_2d] + + # Create Detection3D if depth is available + if depth_frame is not None: + # Calculate 3D position using depth and camera intrinsics + depth_value = self._get_depth_from_bbox(current_bbox_x1y1x2y2, depth_frame) + if ( + depth_value is not None + and depth_value > 0 + and self.camera_intrinsics is not None + ): + fx, fy, cx, cy = self.camera_intrinsics + + # Convert pixel coordinates to 3D in optical frame + z_optical = depth_value + x_optical = (center_x - cx) * z_optical / fx + y_optical = (center_y - cy) * z_optical / fy + + # Create pose in optical frame + optical_pose = Pose() + optical_pose.position = Vector3(x_optical, y_optical, z_optical) + optical_pose.orientation = Quaternion(0.0, 0.0, 0.0, 1.0) # Identity for now + + # Convert to robot frame + robot_pose = optical_to_robot_frame(optical_pose) + + # Calculate orientation: object facing towards camera (origin) + yaw = yaw_towards_point(robot_pose.position) + euler = Vector3(0.0, 0.0, yaw) # Only yaw, no roll/pitch + robot_pose.orientation = euler_to_quaternion(euler) + + # Estimate object size in meters + size_x = width * z_optical / fx + size_y = height * z_optical / fy + size_z = 0.1 # Default depth size + + # Create Detection3D + detection_3d = Detection3D() + detection_3d.id = "0" + detection_3d.results_length = 1 + detection_3d.header = header + + # Reuse hypothesis from 2D + detection_3d.results = [hypothesis] + + # Create 3D bounding box with robot frame pose + detection_3d.bbox.center = Pose() + detection_3d.bbox.center.position = robot_pose.position + detection_3d.bbox.center.orientation = robot_pose.orientation + detection_3d.bbox.size = Vector3(size_x, size_y, size_z) + + detection3darray = Detection3DArray() + detection3darray.detections_length = 1 + detection3darray.header = header + detection3darray.detections = [detection_3d] + + # Publish transform for tracked object + # The optical pose is in camera optical frame, so publish it relative to the camera frame + tracked_object_tf = Transform( + translation=robot_pose.position, + rotation=robot_pose.orientation, + frame_id=self.frame_id, # Use configured camera frame + child_frame_id="tracked_object", + ts=header.ts, + ) + self.tf.publish(tracked_object_tf) + + # Store latest detections for RPC access + self._latest_detection2d = detection2darray + self._latest_detection3d = detection3darray + + # Signal that new detections are available + if detection2darray.detections_length > 0 or detection3darray.detections_length > 0: + self._detection_event.set() + + # Publish detections + self.detection2darray.publish(detection2darray) + self.detection3darray.publish(detection3darray) + + # Create and publish visualization if tracking is active + if self.tracking_initialized: + # Convert single detection to list for visualization + detections_3d = ( + detection3darray.detections if detection3darray.detections_length > 0 else [] + ) + detections_2d = ( + detection2darray.detections if detection2darray.detections_length > 0 else [] + ) + + if detections_3d and detections_2d: + # Extract 2D bbox for visualization + det_2d = detections_2d[0] + bbox_2d = [] + if det_2d.bbox: + x1 = det_2d.bbox.center.position.x - det_2d.bbox.size_x / 2 + y1 = det_2d.bbox.center.position.y - det_2d.bbox.size_y / 2 + x2 = det_2d.bbox.center.position.x + det_2d.bbox.size_x / 2 + y2 = det_2d.bbox.center.position.y + det_2d.bbox.size_y / 2 + bbox_2d = [[x1, y1, x2, y2]] + + # Create visualization + viz_image = visualize_detections_3d( + frame, detections_3d, show_coordinates=True, bboxes_2d=bbox_2d + ) + + # Overlay REID feature matches if available + if self.last_good_matches and self.last_roi_kps and self.last_roi_bbox: + viz_image = self._draw_reid_matches(viz_image) + + # Convert to Image message and publish + viz_msg = Image.from_numpy(viz_image) + self.tracked_overlay.publish(viz_msg) + + def _draw_reid_matches(self, image: np.ndarray) -> np.ndarray: + """Draw REID feature matches on the image.""" + viz_image = image.copy() + + x1, y1, _x2, _y2 = self.last_roi_bbox + + # Draw keypoints from current ROI in green + for kp in self.last_roi_kps: + pt = (int(kp.pt[0] + x1), int(kp.pt[1] + y1)) + cv2.circle(viz_image, pt, 3, (0, 255, 0), -1) + + for match in self.last_good_matches: + current_kp = self.last_roi_kps[match.trainIdx] + pt_current = (int(current_kp.pt[0] + x1), int(current_kp.pt[1] + y1)) + + # Draw a larger circle for matched points in yellow + cv2.circle(viz_image, pt_current, 5, (0, 255, 255), 2) # Yellow for matched points + + # Draw match strength indicator (smaller circle with intensity based on distance) + # Lower distance = better match = brighter color + intensity = int(255 * (1.0 - min(match.distance / 100.0, 1.0))) + cv2.circle(viz_image, pt_current, 2, (intensity, intensity, 255), -1) + + text = f"REID Matches: {len(self.last_good_matches)}/{len(self.last_roi_kps) if self.last_roi_kps else 0}" + cv2.putText(viz_image, text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2) + + if self.tracking_frame_count < self.reid_warmup_frames: + status_text = ( + f"REID: WARMING UP ({self.tracking_frame_count}/{self.reid_warmup_frames})" + ) + status_color = (255, 255, 0) # Yellow + elif len(self.last_good_matches) >= self.reid_threshold: + status_text = "REID: CONFIRMED" + status_color = (0, 255, 0) # Green + else: + status_text = f"REID: WEAK ({self.reid_fail_count}/{self.reid_fail_tolerance})" + status_color = (0, 165, 255) # Orange + + cv2.putText( + viz_image, status_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, status_color, 2 ) - - def cleanup(self): - """Clean up resources.""" - self.stop_track() \ No newline at end of file + + return viz_image + + def _get_depth_from_bbox(self, bbox: list[int], depth_frame: np.ndarray) -> float | None: + """Calculate depth from bbox using the 25th percentile of closest points. + + Args: + bbox: Bounding box coordinates [x1, y1, x2, y2] + depth_frame: Depth frame to extract depth values from + + Returns: + Depth value or None if not available + """ + if depth_frame is None: + return None + + x1, y1, x2, y2 = bbox + + # Ensure bbox is within frame bounds + y1 = max(0, y1) + y2 = min(depth_frame.shape[0], y2) + x1 = max(0, x1) + x2 = min(depth_frame.shape[1], x2) + + # Extract depth values from the entire bbox + roi_depth = depth_frame[y1:y2, x1:x2] + + # Get valid (finite and positive) depth values + valid_depths = roi_depth[np.isfinite(roi_depth) & (roi_depth > 0)] + + if len(valid_depths) > 0: + depth_25th_percentile = float(np.percentile(valid_depths, 25)) + return depth_25th_percentile + + return None + + +object_tracking = ObjectTracking.blueprint + +__all__ = ["ObjectTracking", "object_tracking"] diff --git a/dimos/perception/object_tracker_2d.py b/dimos/perception/object_tracker_2d.py new file mode 100644 index 0000000000..0256b7beb9 --- /dev/null +++ b/dimos/perception/object_tracker_2d.py @@ -0,0 +1,299 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import threading +import time + +import cv2 + +# Import LCM messages +from dimos_lcm.vision_msgs import ( + BoundingBox2D, + Detection2D, + ObjectHypothesis, + ObjectHypothesisWithPose, + Point2D, + Pose2D, +) +import numpy as np +from reactivex.disposable import Disposable + +from dimos.core import In, Module, Out, rpc +from dimos.msgs.sensor_msgs import Image, ImageFormat +from dimos.msgs.std_msgs import Header +from dimos.msgs.vision_msgs import Detection2DArray +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.perception.object_tracker_2d", level=logging.INFO) + + +class ObjectTracker2D(Module): + """Pure 2D object tracking module using OpenCV's CSRT tracker.""" + + color_image: In[Image] = None + + detection2darray: Out[Detection2DArray] = None + tracked_overlay: Out[Image] = None # Visualization output + + def __init__( + self, + frame_id: str = "camera_link", + ) -> None: + """ + Initialize 2D object tracking module using OpenCV's CSRT tracker. + + Args: + frame_id: TF frame ID for the camera (default: "camera_link") + """ + super().__init__() + + self.frame_id = frame_id + + # Tracker state + self.tracker = None + self.tracking_bbox = None # Stores (x, y, w, h) + self.tracking_initialized = False + + # Stuck detection + self._last_bbox = None + self._stuck_count = 0 + self._max_stuck_frames = 10 # Higher threshold for stationary objects + + # Frame management + self._frame_lock = threading.Lock() + self._latest_rgb_frame: np.ndarray | None = None + self._frame_arrival_time: float | None = None + + # Tracking thread control + self.tracking_thread: threading.Thread | None = None + self.stop_tracking_event = threading.Event() + self.tracking_rate = 5.0 # Hz + self.tracking_period = 1.0 / self.tracking_rate + + # Store latest detection for RPC access + self._latest_detection2d: Detection2DArray | None = None + + @rpc + def start(self) -> None: + super().start() + + def on_frame(frame_msg: Image) -> None: + arrival_time = time.perf_counter() + with self._frame_lock: + self._latest_rgb_frame = frame_msg.data + self._frame_arrival_time = arrival_time + + unsub = self.color_image.subscribe(on_frame) + self._disposables.add(Disposable(unsub)) + logger.info("ObjectTracker2D module started") + + @rpc + def stop(self) -> None: + self.stop_track() + if self.tracking_thread and self.tracking_thread.is_alive(): + self.stop_tracking_event.set() + self.tracking_thread.join(timeout=2.0) + + super().stop() + + @rpc + def track(self, bbox: list[float]) -> dict: + """ + Initialize tracking with a bounding box. + + Args: + bbox: Bounding box in format [x1, y1, x2, y2] + + Returns: + Dict containing tracking status + """ + if self._latest_rgb_frame is None: + logger.warning("No RGB frame available for tracking") + return {"status": "no_frame"} + + # Initialize tracking + x1, y1, x2, y2 = map(int, bbox) + w, h = x2 - x1, y2 - y1 + if w <= 0 or h <= 0: + logger.warning(f"Invalid initial bbox provided: {bbox}. Tracking not started.") + return {"status": "invalid_bbox"} + + self.tracking_bbox = (x1, y1, w, h) + self.tracker = cv2.legacy.TrackerCSRT_create() + self.tracking_initialized = False + logger.info(f"Tracking target set with bbox: {self.tracking_bbox}") + + # Convert RGB to BGR for CSRT (OpenCV expects BGR) + frame_bgr = cv2.cvtColor(self._latest_rgb_frame, cv2.COLOR_RGB2BGR) + init_success = self.tracker.init(frame_bgr, self.tracking_bbox) + if init_success: + self.tracking_initialized = True + logger.info("Tracker initialized successfully.") + else: + logger.error("Tracker initialization failed.") + self.stop_track() + return {"status": "init_failed"} + + # Start tracking thread + self._start_tracking_thread() + + return {"status": "tracking_started", "bbox": self.tracking_bbox} + + def _start_tracking_thread(self) -> None: + """Start the tracking thread.""" + self.stop_tracking_event.clear() + self.tracking_thread = threading.Thread(target=self._tracking_loop, daemon=True) + self.tracking_thread.start() + logger.info("Started tracking thread") + + def _tracking_loop(self) -> None: + """Main tracking loop that runs in a separate thread.""" + while not self.stop_tracking_event.is_set() and self.tracking_initialized: + self._process_tracking() + time.sleep(self.tracking_period) + logger.info("Tracking loop ended") + + def _reset_tracking_state(self) -> None: + """Reset tracking state without stopping the thread.""" + self.tracker = None + self.tracking_bbox = None + self.tracking_initialized = False + self._last_bbox = None + self._stuck_count = 0 + + # Publish empty detection + empty_2d = Detection2DArray( + detections_length=0, header=Header(time.time(), self.frame_id), detections=[] + ) + self._latest_detection2d = empty_2d + self.detection2darray.publish(empty_2d) + + @rpc + def stop_track(self) -> bool: + """ + Stop tracking the current object. + + Returns: + bool: True if tracking was successfully stopped + """ + self._reset_tracking_state() + + # Stop tracking thread if running + if self.tracking_thread and self.tracking_thread.is_alive(): + if threading.current_thread() != self.tracking_thread: + self.stop_tracking_event.set() + self.tracking_thread.join(timeout=1.0) + self.tracking_thread = None + else: + self.stop_tracking_event.set() + + logger.info("Tracking stopped") + return True + + @rpc + def is_tracking(self) -> bool: + """ + Check if the tracker is currently tracking an object. + + Returns: + bool: True if tracking is active + """ + return self.tracking_initialized + + def _process_tracking(self) -> None: + """Process current frame for tracking and publish 2D detections.""" + if self.tracker is None or not self.tracking_initialized: + return + + # Get frame copy + with self._frame_lock: + if self._latest_rgb_frame is None: + return + frame = self._latest_rgb_frame.copy() + + # Convert RGB to BGR for CSRT (OpenCV expects BGR) + frame_bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) + + tracker_succeeded, bbox_cv = self.tracker.update(frame_bgr) + + if not tracker_succeeded: + logger.info("Tracker update failed. Stopping track.") + self._reset_tracking_state() + return + + # Extract bbox + x, y, w, h = map(int, bbox_cv) + current_bbox_x1y1x2y2 = [x, y, x + w, y + h] + x1, y1, x2, y2 = current_bbox_x1y1x2y2 + + # Check if tracker is stuck + if self._last_bbox is not None: + if (x1, y1, x2, y2) == self._last_bbox: + self._stuck_count += 1 + if self._stuck_count >= self._max_stuck_frames: + logger.warning(f"Tracker stuck for {self._stuck_count} frames. Stopping track.") + self._reset_tracking_state() + return + else: + self._stuck_count = 0 + + self._last_bbox = (x1, y1, x2, y2) + + center_x = (x1 + x2) / 2.0 + center_y = (y1 + y2) / 2.0 + width = float(x2 - x1) + height = float(y2 - y1) + + # Create 2D detection header + header = Header(time.time(), self.frame_id) + + # Create Detection2D with all fields in constructors + detection_2d = Detection2D( + id="0", + results_length=1, + header=header, + bbox=BoundingBox2D( + center=Pose2D(position=Point2D(x=center_x, y=center_y), theta=0.0), + size_x=width, + size_y=height, + ), + results=[ + ObjectHypothesisWithPose( + hypothesis=ObjectHypothesis(class_id="tracked_object", score=1.0) + ) + ], + ) + + detection2darray = Detection2DArray( + detections_length=1, header=header, detections=[detection_2d] + ) + + # Store and publish + self._latest_detection2d = detection2darray + self.detection2darray.publish(detection2darray) + + # Create visualization + viz_image = self._draw_visualization(frame, current_bbox_x1y1x2y2) + viz_copy = viz_image.copy() # Force copy needed to prevent frame reuse + viz_msg = Image.from_numpy(viz_copy, format=ImageFormat.RGB) + self.tracked_overlay.publish(viz_msg) + + def _draw_visualization(self, image: np.ndarray, bbox: list[int]) -> np.ndarray: + """Draw tracking visualization.""" + viz_image = image.copy() + x1, y1, x2, y2 = bbox + cv2.rectangle(viz_image, (x1, y1), (x2, y2), (0, 255, 0), 2) + cv2.putText(viz_image, "TRACKING", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) + return viz_image diff --git a/dimos/perception/object_tracker_3d.py b/dimos/perception/object_tracker_3d.py new file mode 100644 index 0000000000..231ae26748 --- /dev/null +++ b/dimos/perception/object_tracker_3d.py @@ -0,0 +1,301 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# Import LCM messages +from dimos_lcm.sensor_msgs import CameraInfo +from dimos_lcm.vision_msgs import Detection3D, ObjectHypothesisWithPose +import numpy as np + +from dimos.core import In, Out, rpc +from dimos.manipulation.visual_servoing.utils import visualize_detections_3d +from dimos.msgs.geometry_msgs import Pose, Quaternion, Transform, Vector3 +from dimos.msgs.sensor_msgs import Image, ImageFormat +from dimos.msgs.std_msgs import Header +from dimos.msgs.vision_msgs import Detection2DArray, Detection3DArray +from dimos.perception.object_tracker_2d import ObjectTracker2D +from dimos.protocol.tf import TF +from dimos.types.timestamped import align_timestamped +from dimos.utils.logging_config import setup_logger +from dimos.utils.transform_utils import ( + euler_to_quaternion, + optical_to_robot_frame, + yaw_towards_point, +) + +logger = setup_logger("dimos.perception.object_tracker_3d") + + +class ObjectTracker3D(ObjectTracker2D): + """3D object tracking module extending ObjectTracker2D with depth capabilities.""" + + # Additional inputs (2D tracker already has color_image) + depth: In[Image] = None + camera_info: In[CameraInfo] = None + + # Additional outputs (2D tracker already has detection2darray and tracked_overlay) + detection3darray: Out[Detection3DArray] = None + + def __init__(self, **kwargs) -> None: + """ + Initialize 3D object tracking module. + + Args: + **kwargs: Arguments passed to parent ObjectTracker2D + """ + super().__init__(**kwargs) + + # Additional state for 3D tracking + self.camera_intrinsics = None + self._latest_depth_frame: np.ndarray | None = None + self._latest_camera_info: CameraInfo | None = None + + # TF publisher for tracked object + self.tf = TF() + + # Store latest 3D detection + self._latest_detection3d: Detection3DArray | None = None + + @rpc + def start(self) -> None: + super().start() + + # Subscribe to aligned RGB and depth streams + def on_aligned_frames(frames_tuple) -> None: + rgb_msg, depth_msg = frames_tuple + with self._frame_lock: + self._latest_rgb_frame = rgb_msg.data + + depth_data = depth_msg.data + # Convert from millimeters to meters if depth is DEPTH16 format + if depth_msg.format == ImageFormat.DEPTH16: + depth_data = depth_data.astype(np.float32) / 1000.0 + self._latest_depth_frame = depth_data + + # Create aligned observable for RGB and depth + aligned_frames = align_timestamped( + self.color_image.observable(), + self.depth.observable(), + buffer_size=2.0, # 2 second buffer + match_tolerance=0.5, # 500ms tolerance + ) + unsub = aligned_frames.subscribe(on_aligned_frames) + self._disposables.add(unsub) + + # Subscribe to camera info + def on_camera_info(camera_info_msg: CameraInfo) -> None: + self._latest_camera_info = camera_info_msg + # Extract intrinsics: K is [fx, 0, cx, 0, fy, cy, 0, 0, 1] + self.camera_intrinsics = [ + camera_info_msg.K[0], + camera_info_msg.K[4], + camera_info_msg.K[2], + camera_info_msg.K[5], + ] + + self.camera_info.subscribe(on_camera_info) + + logger.info("ObjectTracker3D module started with aligned frame subscription") + + @rpc + def stop(self) -> None: + super().stop() + + def _process_tracking(self) -> None: + """Override to add 3D detection creation after 2D tracking.""" + # Call parent 2D tracking + super()._process_tracking() + + # Enhance with 3D if we have depth and a valid 2D detection + if ( + self._latest_detection2d + and self._latest_detection2d.detections_length > 0 + and self._latest_depth_frame is not None + and self.camera_intrinsics is not None + ): + detection_3d = self._create_detection3d_from_2d(self._latest_detection2d) + if detection_3d: + self._latest_detection3d = detection_3d + self.detection3darray.publish(detection_3d) + + # Update visualization with 3D info + with self._frame_lock: + if self._latest_rgb_frame is not None: + frame = self._latest_rgb_frame.copy() + + # Extract 2D bbox for visualization + det_2d = self._latest_detection2d.detections[0] + x1 = det_2d.bbox.center.position.x - det_2d.bbox.size_x / 2 + y1 = det_2d.bbox.center.position.y - det_2d.bbox.size_y / 2 + x2 = det_2d.bbox.center.position.x + det_2d.bbox.size_x / 2 + y2 = det_2d.bbox.center.position.y + det_2d.bbox.size_y / 2 + bbox_2d = [[x1, y1, x2, y2]] + + # Create 3D visualization + viz_image = visualize_detections_3d( + frame, detection_3d.detections, show_coordinates=True, bboxes_2d=bbox_2d + ) + + # Overlay Re-ID matches + if self.last_good_matches and self.last_roi_kps and self.last_roi_bbox: + viz_image = self._draw_reid_overlay(viz_image) + + viz_msg = Image.from_numpy(viz_image) + self.tracked_overlay.publish(viz_msg) + + def _create_detection3d_from_2d(self, detection2d: Detection2DArray) -> Detection3DArray | None: + """Create 3D detection from 2D detection using depth.""" + if detection2d.detections_length == 0: + return None + + det_2d = detection2d.detections[0] + + # Get bbox center + center_x = det_2d.bbox.center.position.x + center_y = det_2d.bbox.center.position.y + width = det_2d.bbox.size_x + height = det_2d.bbox.size_y + + # Convert to bbox coordinates + x1 = int(center_x - width / 2) + y1 = int(center_y - height / 2) + x2 = int(center_x + width / 2) + y2 = int(center_y + height / 2) + + # Get depth value + depth_value = self._get_depth_from_bbox([x1, y1, x2, y2], self._latest_depth_frame) + + if depth_value is None or depth_value <= 0: + return None + + fx, fy, cx, cy = self.camera_intrinsics + + # Convert pixel coordinates to 3D in optical frame + z_optical = depth_value + x_optical = (center_x - cx) * z_optical / fx + y_optical = (center_y - cy) * z_optical / fy + + # Create pose in optical frame + optical_pose = Pose() + optical_pose.position = Vector3(x_optical, y_optical, z_optical) + optical_pose.orientation = Quaternion(0.0, 0.0, 0.0, 1.0) + + # Convert to robot frame + robot_pose = optical_to_robot_frame(optical_pose) + + # Calculate orientation: object facing towards camera + yaw = yaw_towards_point(robot_pose.position) + euler = Vector3(0.0, 0.0, yaw) + robot_pose.orientation = euler_to_quaternion(euler) + + # Estimate object size in meters + size_x = width * z_optical / fx + size_y = height * z_optical / fy + size_z = 0.1 # Default depth size + + # Create Detection3D + header = Header(self.frame_id) + detection_3d = Detection3D() + detection_3d.id = "0" + detection_3d.results_length = 1 + detection_3d.header = header + + # Create hypothesis + hypothesis = ObjectHypothesisWithPose() + hypothesis.hypothesis.class_id = "tracked_object" + hypothesis.hypothesis.score = 1.0 + detection_3d.results = [hypothesis] + + # Create 3D bounding box + detection_3d.bbox.center = Pose() + detection_3d.bbox.center.position = robot_pose.position + detection_3d.bbox.center.orientation = robot_pose.orientation + detection_3d.bbox.size = Vector3(size_x, size_y, size_z) + + detection3darray = Detection3DArray() + detection3darray.detections_length = 1 + detection3darray.header = header + detection3darray.detections = [detection_3d] + + # Publish TF for tracked object + tracked_object_tf = Transform( + translation=robot_pose.position, + rotation=robot_pose.orientation, + frame_id=self.frame_id, + child_frame_id="tracked_object", + ts=header.ts, + ) + self.tf.publish(tracked_object_tf) + + return detection3darray + + def _get_depth_from_bbox(self, bbox: list[int], depth_frame: np.ndarray) -> float | None: + """ + Calculate depth from bbox using the 25th percentile of closest points. + + Args: + bbox: Bounding box coordinates [x1, y1, x2, y2] + depth_frame: Depth frame to extract depth values from + + Returns: + Depth value or None if not available + """ + if depth_frame is None: + return None + + x1, y1, x2, y2 = bbox + + # Ensure bbox is within frame bounds + y1 = max(0, y1) + y2 = min(depth_frame.shape[0], y2) + x1 = max(0, x1) + x2 = min(depth_frame.shape[1], x2) + + # Extract depth values from the bbox + roi_depth = depth_frame[y1:y2, x1:x2] + + # Get valid (finite and positive) depth values + valid_depths = roi_depth[np.isfinite(roi_depth) & (roi_depth > 0)] + + if len(valid_depths) > 0: + return float(np.percentile(valid_depths, 25)) + + return None + + def _draw_reid_overlay(self, image: np.ndarray) -> np.ndarray: + """Draw Re-ID feature matches on visualization.""" + import cv2 + + viz_image = image.copy() + x1, y1, _x2, _y2 = self.last_roi_bbox + + # Draw keypoints + for kp in self.last_roi_kps: + pt = (int(kp.pt[0] + x1), int(kp.pt[1] + y1)) + cv2.circle(viz_image, pt, 3, (0, 255, 0), -1) + + # Draw matches + for match in self.last_good_matches: + current_kp = self.last_roi_kps[match.trainIdx] + pt_current = (int(current_kp.pt[0] + x1), int(current_kp.pt[1] + y1)) + cv2.circle(viz_image, pt_current, 5, (0, 255, 255), 2) + + intensity = int(255 * (1.0 - min(match.distance / 100.0, 1.0))) + cv2.circle(viz_image, pt_current, 2, (intensity, intensity, 255), -1) + + # Draw match count + text = f"REID: {len(self.last_good_matches)}/{len(self.last_roi_kps)}" + cv2.putText(viz_image, text, (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2) + + return viz_image diff --git a/dimos/perception/person_tracker.py b/dimos/perception/person_tracker.py index dd86e9d0ef..16b505578b 100644 --- a/dimos/perception/person_tracker.py +++ b/dimos/perception/person_tracker.py @@ -1,27 +1,52 @@ -from dimos.perception.detection2d.yolo_2d_det import Yolo2DDetector -from dimos.perception.detection2d.utils import filter_detections -from dimos.perception.common.ibvs import PersonDistanceEstimator -from reactivex import Observable -from reactivex import operators as ops -import numpy as np +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + import cv2 +import numpy as np +from reactivex import Observable, interval, operators as ops +from reactivex.disposable import Disposable +from dimos.core import In, Module, Out, rpc +from dimos.msgs.sensor_msgs import Image +from dimos.perception.common.ibvs import PersonDistanceEstimator +from dimos.perception.detection2d.utils import filter_detections +from dimos.perception.detection2d.yolo_2d_det import Yolo2DDetector +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.perception.person_tracker") + + +class PersonTrackingStream(Module): + """Module for person tracking with LCM input/output.""" + + # LCM inputs + video: In[Image] = None + + # LCM outputs + tracking_data: Out[dict] = None -class PersonTrackingStream: def __init__( self, - model_path="yolo11n.pt", - device="cuda", camera_intrinsics=None, - camera_pitch=0.0, - camera_height=1.0 - ): + camera_pitch: float = 0.0, + camera_height: float = 1.0, + ) -> None: """ Initialize a person tracking stream using Yolo2DDetector and PersonDistanceEstimator. - + Args: - model_path: Path to the YOLO model file - device: Computation device ("cuda" or "cpu") camera_intrinsics: List in format [fx, fy, cx, cy] where: - fx: Focal length in x direction (pixels) - fy: Focal length in y direction (pixels) @@ -30,115 +55,206 @@ def __init__( camera_pitch: Camera pitch angle in radians (positive is up) camera_height: Height of the camera from the ground in meters """ - self.detector = Yolo2DDetector( - model_path=model_path, - device=device - ) - + # Call parent Module init + super().__init__() + + self.camera_intrinsics = camera_intrinsics + self.camera_pitch = camera_pitch + self.camera_height = camera_height + + self.detector = Yolo2DDetector() + # Initialize distance estimator if camera_intrinsics is None: raise ValueError("Camera intrinsics are required for distance estimation") - + # Validate camera intrinsics format [fx, fy, cx, cy] - if not isinstance(camera_intrinsics, (list, tuple, np.ndarray)) or len(camera_intrinsics) != 4: + if ( + not isinstance(camera_intrinsics, list | tuple | np.ndarray) + or len(camera_intrinsics) != 4 + ): raise ValueError("Camera intrinsics must be provided as [fx, fy, cx, cy]") - + # Convert [fx, fy, cx, cy] to 3x3 camera matrix fx, fy, cx, cy = camera_intrinsics - K = np.array([ - [fx, 0, cx], - [0, fy, cy], - [0, 0, 1] - ], dtype=np.float32) - + K = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]], dtype=np.float32) + self.distance_estimator = PersonDistanceEstimator( - K=K, - camera_pitch=camera_pitch, - camera_height=camera_height + K=K, camera_pitch=camera_pitch, camera_height=camera_height ) - + + # For tracking latest frame data + self._latest_frame: np.ndarray | None = None + self._process_interval = 0.1 # Process at 10Hz + + # Tracking state - starts disabled + self._tracking_enabled = False + + @rpc + def start(self) -> None: + """Start the person tracking module and subscribe to LCM streams.""" + + super().start() + + # Subscribe to video stream + def set_video(image_msg: Image) -> None: + if hasattr(image_msg, "data"): + self._latest_frame = image_msg.data + else: + logger.warning("Received image message without data attribute") + + unsub = self.video.subscribe(set_video) + self._disposables.add(Disposable(unsub)) + + # Start periodic processing + unsub = interval(self._process_interval).subscribe(lambda _: self._process_frame()) + self._disposables.add(unsub) + + logger.info("PersonTracking module started and subscribed to LCM streams") + + @rpc + def stop(self) -> None: + super().stop() + + def _process_frame(self) -> None: + """Process the latest frame if available.""" + if self._latest_frame is None: + return + + # Only process and publish if tracking is enabled + if not self._tracking_enabled: + return + + # Process frame through tracking pipeline + result = self._process_tracking(self._latest_frame) + + # Publish result to LCM + if result: + self.tracking_data.publish(result) + + def _process_tracking(self, frame): + """Process a single frame for person tracking.""" + # Detect people in the frame + bboxes, track_ids, class_ids, confidences, names = self.detector.process_image(frame) + + # Filter to keep only person detections using filter_detections + ( + filtered_bboxes, + filtered_track_ids, + filtered_class_ids, + filtered_confidences, + filtered_names, + ) = filter_detections( + bboxes, + track_ids, + class_ids, + confidences, + names, + class_filter=[0], # 0 is the class_id for person + name_filter=["person"], + ) + + # Create visualization + viz_frame = self.detector.visualize_results( + frame, + filtered_bboxes, + filtered_track_ids, + filtered_class_ids, + filtered_confidences, + filtered_names, + ) + + # Calculate distance and angle for each person + targets = [] + for i, bbox in enumerate(filtered_bboxes): + target_data = { + "target_id": filtered_track_ids[i] if i < len(filtered_track_ids) else -1, + "bbox": bbox, + "confidence": filtered_confidences[i] if i < len(filtered_confidences) else None, + } + + distance, angle = self.distance_estimator.estimate_distance_angle(bbox) + target_data["distance"] = distance + target_data["angle"] = angle + + # Add text to visualization + _x1, y1, x2, _y2 = map(int, bbox) + dist_text = f"{distance:.2f}m, {np.rad2deg(angle):.1f} deg" + + # Add black background for better visibility + text_size = cv2.getTextSize(dist_text, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 2)[0] + # Position at top-right corner + cv2.rectangle( + viz_frame, (x2 - text_size[0], y1 - text_size[1] - 5), (x2, y1), (0, 0, 0), -1 + ) + + # Draw text in white at top-right + cv2.putText( + viz_frame, + dist_text, + (x2 - text_size[0], y1 - 5), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + (255, 255, 255), + 2, + ) + + targets.append(target_data) + + # Create the result dictionary + return {"frame": frame, "viz_frame": viz_frame, "targets": targets} + + @rpc + def enable_tracking(self) -> bool: + """Enable person tracking. + + Returns: + bool: True if tracking was enabled successfully + """ + self._tracking_enabled = True + logger.info("Person tracking enabled") + return True + + @rpc + def disable_tracking(self) -> bool: + """Disable person tracking. + + Returns: + bool: True if tracking was disabled successfully + """ + self._tracking_enabled = False + logger.info("Person tracking disabled") + return True + + @rpc + def is_tracking_enabled(self) -> bool: + """Check if tracking is currently enabled. + + Returns: + bool: True if tracking is enabled + """ + return self._tracking_enabled + + @rpc + def get_tracking_data(self) -> dict: + """Get the latest tracking data. + + Returns: + Dictionary containing tracking results + """ + if self._latest_frame is not None: + return self._process_tracking(self._latest_frame) + return {"frame": None, "viz_frame": None, "targets": []} + def create_stream(self, video_stream: Observable) -> Observable: """ Create an Observable stream of person tracking results from a video stream. - + Args: video_stream: Observable that emits video frames - + Returns: Observable that emits dictionaries containing tracking results and visualizations """ - def process_frame(frame): - # Detect people in the frame - bboxes, track_ids, class_ids, confidences, names = self.detector.process_image(frame) - - # Filter to keep only person detections using filter_detections - filtered_bboxes, filtered_track_ids, filtered_class_ids, filtered_confidences, filtered_names = ( - filter_detections( - bboxes, track_ids, class_ids, confidences, names, - class_filter=[0], # 0 is the class_id for person - name_filter=['person'] - ) - ) - - # Create visualization - viz_frame = self.detector.visualize_results( - frame, - filtered_bboxes, - filtered_track_ids, - filtered_class_ids, - filtered_confidences, - filtered_names - ) - - # Calculate distance and angle for each person - targets = [] - for i, bbox in enumerate(filtered_bboxes): - target_data = { - "target_id": filtered_track_ids[i] if i < len(filtered_track_ids) else -1, - "bbox": bbox, - "confidence": filtered_confidences[i] if i < len(filtered_confidences) else None, - } - - distance, angle = self.distance_estimator.estimate_distance_angle(bbox) - target_data["distance"] = distance - target_data["angle"] = angle - - # Add text to visualization - x1, y1, x2, y2 = map(int, bbox) - dist_text = f"{distance:.2f}m, {np.rad2deg(angle):.1f} deg" - - # Add black background for better visibility - text_size = cv2.getTextSize(dist_text, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 2)[0] - # Position at top-right corner - cv2.rectangle( - viz_frame, - (x2 - text_size[0], y1 - text_size[1] - 5), - (x2, y1), - (0, 0, 0), -1 - ) - - # Draw text in white at top-right - cv2.putText( - viz_frame, dist_text, - (x2 - text_size[0], y1 - 5), - cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2 - ) - - targets.append(target_data) - - # Create the result dictionary - result = { - "frame": frame, - "viz_frame": viz_frame, - "targets": targets - } - - return result - - return video_stream.pipe( - ops.map(process_frame) - ) - - def cleanup(self): - """Clean up resources.""" - pass # No specific cleanup needed for now + + return video_stream.pipe(ops.map(self._process_tracking)) diff --git a/dimos/perception/pointcloud/__init__.py b/dimos/perception/pointcloud/__init__.py new file mode 100644 index 0000000000..a380e2aadf --- /dev/null +++ b/dimos/perception/pointcloud/__init__.py @@ -0,0 +1,3 @@ +from .cuboid_fit import * +from .pointcloud_filtering import * +from .utils import * diff --git a/dimos/perception/pointcloud/cuboid_fit.py b/dimos/perception/pointcloud/cuboid_fit.py new file mode 100644 index 0000000000..376ae08da0 --- /dev/null +++ b/dimos/perception/pointcloud/cuboid_fit.py @@ -0,0 +1,414 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import cv2 +import numpy as np +import open3d as o3d + + +def fit_cuboid( + points: np.ndarray | o3d.geometry.PointCloud, method: str = "minimal" +) -> dict | None: + """ + Fit a cuboid to a point cloud using Open3D's built-in methods. + + Args: + points: Nx3 array of points or Open3D PointCloud + method: Fitting method: + - 'minimal': Minimal oriented bounding box (best fit) + - 'oriented': PCA-based oriented bounding box + - 'axis_aligned': Axis-aligned bounding box + + Returns: + Dictionary containing: + - center: 3D center point + - dimensions: 3D dimensions (extent) + - rotation: 3x3 rotation matrix + - error: Fitting error + - bounding_box: Open3D OrientedBoundingBox object + Returns None if insufficient points or fitting fails. + + Raises: + ValueError: If method is invalid or inputs are malformed + """ + # Validate method + valid_methods = ["minimal", "oriented", "axis_aligned"] + if method not in valid_methods: + raise ValueError(f"method must be one of {valid_methods}, got '{method}'") + + # Convert to point cloud if needed + if isinstance(points, np.ndarray): + points = np.asarray(points) + if len(points.shape) != 2 or points.shape[1] != 3: + raise ValueError(f"points array must be Nx3, got shape {points.shape}") + if len(points) < 4: + return None + + pcd = o3d.geometry.PointCloud() + pcd.points = o3d.utility.Vector3dVector(points) + elif isinstance(points, o3d.geometry.PointCloud): + pcd = points + points = np.asarray(pcd.points) + if len(points) < 4: + return None + else: + raise ValueError(f"points must be numpy array or Open3D PointCloud, got {type(points)}") + + try: + # Get bounding box based on method + if method == "minimal": + obb = pcd.get_minimal_oriented_bounding_box(robust=True) + elif method == "oriented": + obb = pcd.get_oriented_bounding_box(robust=True) + elif method == "axis_aligned": + # Convert axis-aligned to oriented format for consistency + aabb = pcd.get_axis_aligned_bounding_box() + obb = o3d.geometry.OrientedBoundingBox() + obb.center = aabb.get_center() + obb.extent = aabb.get_extent() + obb.R = np.eye(3) # Identity rotation for axis-aligned + + # Extract parameters + center = np.asarray(obb.center) + dimensions = np.asarray(obb.extent) + rotation = np.asarray(obb.R) + + # Calculate fitting error + error = _compute_fitting_error(points, center, dimensions, rotation) + + return { + "center": center, + "dimensions": dimensions, + "rotation": rotation, + "error": error, + "bounding_box": obb, + "method": method, + } + + except Exception as e: + # Log error but don't crash - return None for graceful handling + print(f"Warning: Cuboid fitting failed with method '{method}': {e}") + return None + + +def fit_cuboid_simple(points: np.ndarray | o3d.geometry.PointCloud) -> dict | None: + """ + Simple wrapper for minimal oriented bounding box fitting. + + Args: + points: Nx3 array of points or Open3D PointCloud + + Returns: + Dictionary with center, dimensions, rotation, and bounding_box, + or None if insufficient points + """ + return fit_cuboid(points, method="minimal") + + +def _compute_fitting_error( + points: np.ndarray, center: np.ndarray, dimensions: np.ndarray, rotation: np.ndarray +) -> float: + """ + Compute fitting error as mean squared distance from points to cuboid surface. + + Args: + points: Nx3 array of points + center: 3D center point + dimensions: 3D dimensions + rotation: 3x3 rotation matrix + + Returns: + Mean squared error + """ + if len(points) == 0: + return 0.0 + + # Transform points to local coordinates + local_points = (points - center) @ rotation + half_dims = dimensions / 2 + + # Calculate distance to cuboid surface + dx = np.abs(local_points[:, 0]) - half_dims[0] + dy = np.abs(local_points[:, 1]) - half_dims[1] + dz = np.abs(local_points[:, 2]) - half_dims[2] + + # Points outside: distance to nearest face + # Points inside: negative distance to nearest face + outside_dist = np.sqrt(np.maximum(dx, 0) ** 2 + np.maximum(dy, 0) ** 2 + np.maximum(dz, 0) ** 2) + inside_dist = np.minimum(np.minimum(dx, dy), dz) + distances = np.where((dx > 0) | (dy > 0) | (dz > 0), outside_dist, -inside_dist) + + return float(np.mean(distances**2)) + + +def get_cuboid_corners( + center: np.ndarray, dimensions: np.ndarray, rotation: np.ndarray +) -> np.ndarray: + """ + Get the 8 corners of a cuboid. + + Args: + center: 3D center point + dimensions: 3D dimensions + rotation: 3x3 rotation matrix + + Returns: + 8x3 array of corner coordinates + """ + half_dims = dimensions / 2 + corners_local = ( + np.array( + [ + [-1, -1, -1], # 0: left bottom back + [-1, -1, 1], # 1: left bottom front + [-1, 1, -1], # 2: left top back + [-1, 1, 1], # 3: left top front + [1, -1, -1], # 4: right bottom back + [1, -1, 1], # 5: right bottom front + [1, 1, -1], # 6: right top back + [1, 1, 1], # 7: right top front + ] + ) + * half_dims + ) + + # Apply rotation and translation + return corners_local @ rotation.T + center + + +def visualize_cuboid_on_image( + image: np.ndarray, + cuboid_params: dict, + camera_matrix: np.ndarray, + extrinsic_rotation: np.ndarray | None = None, + extrinsic_translation: np.ndarray | None = None, + color: tuple[int, int, int] = (0, 255, 0), + thickness: int = 2, + show_dimensions: bool = True, +) -> np.ndarray: + """ + Draw a fitted cuboid on an image using camera projection. + + Args: + image: Input image to draw on + cuboid_params: Dictionary containing cuboid parameters + camera_matrix: Camera intrinsic matrix (3x3) + extrinsic_rotation: Optional external rotation (3x3) + extrinsic_translation: Optional external translation (3x1) + color: Line color as (B, G, R) tuple + thickness: Line thickness + show_dimensions: Whether to display dimension text + + Returns: + Image with cuboid visualization + + Raises: + ValueError: If required parameters are missing or invalid + """ + # Validate inputs + required_keys = ["center", "dimensions", "rotation"] + if not all(key in cuboid_params for key in required_keys): + raise ValueError(f"cuboid_params must contain keys: {required_keys}") + + if camera_matrix.shape != (3, 3): + raise ValueError(f"camera_matrix must be 3x3, got {camera_matrix.shape}") + + # Get corners in world coordinates + corners = get_cuboid_corners( + cuboid_params["center"], cuboid_params["dimensions"], cuboid_params["rotation"] + ) + + # Transform corners if extrinsic parameters are provided + if extrinsic_rotation is not None and extrinsic_translation is not None: + if extrinsic_rotation.shape != (3, 3): + raise ValueError(f"extrinsic_rotation must be 3x3, got {extrinsic_rotation.shape}") + if extrinsic_translation.shape not in [(3,), (3, 1)]: + raise ValueError( + f"extrinsic_translation must be (3,) or (3,1), got {extrinsic_translation.shape}" + ) + + extrinsic_translation = extrinsic_translation.flatten() + corners = (extrinsic_rotation @ corners.T).T + extrinsic_translation + + try: + # Project 3D corners to image coordinates + corners_img, _ = cv2.projectPoints( + corners.astype(np.float32), + np.zeros(3), + np.zeros(3), # No additional rotation/translation + camera_matrix.astype(np.float32), + None, # No distortion + ) + corners_img = corners_img.reshape(-1, 2).astype(int) + + # Check if corners are within image bounds + h, w = image.shape[:2] + valid_corners = ( + (corners_img[:, 0] >= 0) + & (corners_img[:, 0] < w) + & (corners_img[:, 1] >= 0) + & (corners_img[:, 1] < h) + ) + + if not np.any(valid_corners): + print("Warning: All cuboid corners are outside image bounds") + return image.copy() + + except Exception as e: + print(f"Warning: Failed to project cuboid corners: {e}") + return image.copy() + + # Define edges for wireframe visualization + edges = [ + # Bottom face + (0, 1), + (1, 5), + (5, 4), + (4, 0), + # Top face + (2, 3), + (3, 7), + (7, 6), + (6, 2), + # Vertical edges + (0, 2), + (1, 3), + (5, 7), + (4, 6), + ] + + # Draw edges + vis_img = image.copy() + for i, j in edges: + # Only draw edge if both corners are valid + if valid_corners[i] and valid_corners[j]: + cv2.line(vis_img, tuple(corners_img[i]), tuple(corners_img[j]), color, thickness) + + # Add dimension text if requested + if show_dimensions and np.any(valid_corners): + dims = cuboid_params["dimensions"] + dim_text = f"Dims: {dims[0]:.3f} x {dims[1]:.3f} x {dims[2]:.3f}" + + # Find a good position for text (top-left of image) + text_pos = (10, 30) + font_scale = 0.7 + + # Add background rectangle for better readability + text_size = cv2.getTextSize(dim_text, cv2.FONT_HERSHEY_SIMPLEX, font_scale, 2)[0] + cv2.rectangle( + vis_img, + (text_pos[0] - 5, text_pos[1] - text_size[1] - 5), + (text_pos[0] + text_size[0] + 5, text_pos[1] + 5), + (0, 0, 0), + -1, + ) + + cv2.putText(vis_img, dim_text, text_pos, cv2.FONT_HERSHEY_SIMPLEX, font_scale, color, 2) + + return vis_img + + +def compute_cuboid_volume(cuboid_params: dict) -> float: + """ + Compute the volume of a cuboid. + + Args: + cuboid_params: Dictionary containing cuboid parameters + + Returns: + Volume in cubic units + """ + if "dimensions" not in cuboid_params: + raise ValueError("cuboid_params must contain 'dimensions' key") + + dims = cuboid_params["dimensions"] + return float(np.prod(dims)) + + +def compute_cuboid_surface_area(cuboid_params: dict) -> float: + """ + Compute the surface area of a cuboid. + + Args: + cuboid_params: Dictionary containing cuboid parameters + + Returns: + Surface area in square units + """ + if "dimensions" not in cuboid_params: + raise ValueError("cuboid_params must contain 'dimensions' key") + + dims = cuboid_params["dimensions"] + return 2.0 * (dims[0] * dims[1] + dims[1] * dims[2] + dims[2] * dims[0]) + + +def check_cuboid_quality(cuboid_params: dict, points: np.ndarray) -> dict: + """ + Assess the quality of a cuboid fit. + + Args: + cuboid_params: Dictionary containing cuboid parameters + points: Original points used for fitting + + Returns: + Dictionary with quality metrics + """ + if len(points) == 0: + return {"error": "No points provided"} + + # Basic metrics + volume = compute_cuboid_volume(cuboid_params) + surface_area = compute_cuboid_surface_area(cuboid_params) + error = cuboid_params.get("error", 0.0) + + # Aspect ratio analysis + dims = cuboid_params["dimensions"] + aspect_ratios = [ + dims[0] / dims[1] if dims[1] > 0 else float("inf"), + dims[1] / dims[2] if dims[2] > 0 else float("inf"), + dims[2] / dims[0] if dims[0] > 0 else float("inf"), + ] + max_aspect_ratio = max(aspect_ratios) + + # Volume ratio (cuboid volume vs convex hull volume) + try: + pcd = o3d.geometry.PointCloud() + pcd.points = o3d.utility.Vector3dVector(points) + hull, _ = pcd.compute_convex_hull() + hull_volume = hull.get_volume() + volume_ratio = volume / hull_volume if hull_volume > 0 else float("inf") + except: + volume_ratio = None + + return { + "fitting_error": error, + "volume": volume, + "surface_area": surface_area, + "max_aspect_ratio": max_aspect_ratio, + "volume_ratio": volume_ratio, + "num_points": len(points), + "method": cuboid_params.get("method", "unknown"), + } + + +# Backward compatibility +def visualize_fit(image, cuboid_params, camera_matrix, R=None, t=None): + """ + Legacy function for backward compatibility. + Use visualize_cuboid_on_image instead. + """ + return visualize_cuboid_on_image( + image, cuboid_params, camera_matrix, R, t, show_dimensions=True + ) diff --git a/dimos/perception/pointcloud/pointcloud_filtering.py b/dimos/perception/pointcloud/pointcloud_filtering.py new file mode 100644 index 0000000000..4ca8a0c84b --- /dev/null +++ b/dimos/perception/pointcloud/pointcloud_filtering.py @@ -0,0 +1,358 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import cv2 +import numpy as np +import open3d as o3d +import torch + +from dimos.perception.pointcloud.cuboid_fit import fit_cuboid +from dimos.perception.pointcloud.utils import ( + create_point_cloud_and_extract_masks, + load_camera_matrix_from_yaml, +) +from dimos.types.manipulation import ObjectData +from dimos.types.vector import Vector + + +class PointcloudFiltering: + """ + A production-ready point cloud filtering pipeline for segmented objects. + + This class takes segmentation results and produces clean, filtered point clouds + for each object with consistent coloring and optional outlier removal. + """ + + def __init__( + self, + color_intrinsics: str | list[float] | np.ndarray | None = None, + depth_intrinsics: str | list[float] | np.ndarray | None = None, + color_weight: float = 0.3, + enable_statistical_filtering: bool = True, + statistical_neighbors: int = 20, + statistical_std_ratio: float = 1.5, + enable_radius_filtering: bool = True, + radius_filtering_radius: float = 0.015, + radius_filtering_min_neighbors: int = 25, + enable_subsampling: bool = True, + voxel_size: float = 0.005, + max_num_objects: int = 10, + min_points_for_cuboid: int = 10, + cuboid_method: str = "oriented", + max_bbox_size_percent: float = 30.0, + ) -> None: + """ + Initialize the point cloud filtering pipeline. + + Args: + color_intrinsics: Camera intrinsics for color image + depth_intrinsics: Camera intrinsics for depth image + color_weight: Weight for blending generated color with original (0.0-1.0) + enable_statistical_filtering: Enable/disable statistical outlier filtering + statistical_neighbors: Number of neighbors for statistical filtering + statistical_std_ratio: Standard deviation ratio for statistical filtering + enable_radius_filtering: Enable/disable radius outlier filtering + radius_filtering_radius: Search radius for radius filtering (meters) + radius_filtering_min_neighbors: Min neighbors within radius + enable_subsampling: Enable/disable point cloud subsampling + voxel_size: Voxel size for downsampling (meters, when subsampling enabled) + max_num_objects: Maximum number of objects to process (top N by confidence) + min_points_for_cuboid: Minimum points required for cuboid fitting + cuboid_method: Method for cuboid fitting ('minimal', 'oriented', 'axis_aligned') + max_bbox_size_percent: Maximum percentage of image size for object bboxes (0-100) + + Raises: + ValueError: If invalid parameters are provided + """ + # Validate parameters + if not 0.0 <= color_weight <= 1.0: + raise ValueError(f"color_weight must be between 0.0 and 1.0, got {color_weight}") + if not 0.0 <= max_bbox_size_percent <= 100.0: + raise ValueError( + f"max_bbox_size_percent must be between 0.0 and 100.0, got {max_bbox_size_percent}" + ) + + # Store settings + self.color_weight = color_weight + self.enable_statistical_filtering = enable_statistical_filtering + self.statistical_neighbors = statistical_neighbors + self.statistical_std_ratio = statistical_std_ratio + self.enable_radius_filtering = enable_radius_filtering + self.radius_filtering_radius = radius_filtering_radius + self.radius_filtering_min_neighbors = radius_filtering_min_neighbors + self.enable_subsampling = enable_subsampling + self.voxel_size = voxel_size + self.max_num_objects = max_num_objects + self.min_points_for_cuboid = min_points_for_cuboid + self.cuboid_method = cuboid_method + self.max_bbox_size_percent = max_bbox_size_percent + + # Load camera matrices + self.color_camera_matrix = load_camera_matrix_from_yaml(color_intrinsics) + self.depth_camera_matrix = load_camera_matrix_from_yaml(depth_intrinsics) + + # Store the full point cloud + self.full_pcd = None + + def generate_color_from_id(self, object_id: int) -> np.ndarray: + """Generate a consistent color for a given object ID.""" + np.random.seed(object_id) + color = np.random.randint(0, 255, 3, dtype=np.uint8) + np.random.seed(None) + return color + + def _validate_inputs( + self, color_img: np.ndarray, depth_img: np.ndarray, objects: list[ObjectData] + ): + """Validate input parameters.""" + if color_img.shape[:2] != depth_img.shape: + raise ValueError("Color and depth image dimensions don't match") + + def _prepare_masks(self, masks: list[np.ndarray], target_shape: tuple) -> list[np.ndarray]: + """Prepare and validate masks to match target shape.""" + processed_masks = [] + for mask in masks: + # Convert mask to numpy if it's a tensor + if hasattr(mask, "cpu"): + mask = mask.cpu().numpy() + + mask = mask.astype(bool) + + # Handle shape mismatches + if mask.shape != target_shape: + if len(mask.shape) > 2: + mask = mask[:, :, 0] + + if mask.shape != target_shape: + mask = cv2.resize( + mask.astype(np.uint8), + (target_shape[1], target_shape[0]), + interpolation=cv2.INTER_NEAREST, + ).astype(bool) + + processed_masks.append(mask) + + return processed_masks + + def _apply_color_mask( + self, pcd: o3d.geometry.PointCloud, rgb_color: np.ndarray + ) -> o3d.geometry.PointCloud: + """Apply weighted color mask to point cloud.""" + if len(np.asarray(pcd.colors)) > 0: + original_colors = np.asarray(pcd.colors) + generated_color = rgb_color.astype(np.float32) / 255.0 + colored_mask = ( + 1.0 - self.color_weight + ) * original_colors + self.color_weight * generated_color + colored_mask = np.clip(colored_mask, 0.0, 1.0) + pcd.colors = o3d.utility.Vector3dVector(colored_mask) + return pcd + + def _apply_filtering(self, pcd: o3d.geometry.PointCloud) -> o3d.geometry.PointCloud: + """Apply optional filtering to point cloud based on enabled flags.""" + current_pcd = pcd + + # Apply statistical filtering if enabled + if self.enable_statistical_filtering: + current_pcd, _ = current_pcd.remove_statistical_outlier( + nb_neighbors=self.statistical_neighbors, std_ratio=self.statistical_std_ratio + ) + + # Apply radius filtering if enabled + if self.enable_radius_filtering: + current_pcd, _ = current_pcd.remove_radius_outlier( + nb_points=self.radius_filtering_min_neighbors, radius=self.radius_filtering_radius + ) + + return current_pcd + + def _apply_subsampling(self, pcd: o3d.geometry.PointCloud) -> o3d.geometry.PointCloud: + """Apply subsampling to limit point cloud size using Open3D's voxel downsampling.""" + if self.enable_subsampling: + return pcd.voxel_down_sample(self.voxel_size) + return pcd + + def _extract_masks_from_objects(self, objects: list[ObjectData]) -> list[np.ndarray]: + """Extract segmentation masks from ObjectData objects.""" + return [obj["segmentation_mask"] for obj in objects] + + def get_full_point_cloud(self) -> o3d.geometry.PointCloud: + """Get the full point cloud.""" + return self._apply_subsampling(self.full_pcd) + + def process_images( + self, color_img: np.ndarray, depth_img: np.ndarray, objects: list[ObjectData] + ) -> list[ObjectData]: + """ + Process color and depth images with object detection results to create filtered point clouds. + + Args: + color_img: RGB image as numpy array (H, W, 3) + depth_img: Depth image as numpy array (H, W) in meters + objects: List of ObjectData from object detection stream + + Returns: + List of updated ObjectData with pointcloud and 3D information. Each ObjectData + dictionary is enhanced with the following new fields: + + **3D Spatial Information** (added when sufficient points for cuboid fitting): + - "position": Vector(x, y, z) - 3D center position in world coordinates (meters) + - "rotation": Vector(roll, pitch, yaw) - 3D orientation as Euler angles (radians) + - "size": {"width": float, "height": float, "depth": float} - 3D bounding box dimensions (meters) + + **Point Cloud Data**: + - "point_cloud": o3d.geometry.PointCloud - Filtered Open3D point cloud with colors + - "color": np.ndarray - Consistent RGB color [R,G,B] (0-255) generated from object_id + + **Grasp Generation Arrays** (Dimensional grasp format): + - "point_cloud_numpy": np.ndarray - Nx3 XYZ coordinates as float32 (meters) + - "colors_numpy": np.ndarray - Nx3 RGB colors as float32 (0.0-1.0 range) + + Raises: + ValueError: If inputs are invalid + RuntimeError: If processing fails + """ + # Validate inputs + self._validate_inputs(color_img, depth_img, objects) + + if not objects: + return [] + + # Filter to top N objects by confidence + if len(objects) > self.max_num_objects: + # Sort objects by confidence (highest first), handle None confidences + sorted_objects = sorted( + objects, + key=lambda obj: obj.get("confidence", 0.0) + if obj.get("confidence") is not None + else 0.0, + reverse=True, + ) + objects = sorted_objects[: self.max_num_objects] + + # Filter out objects with bboxes too large + image_area = color_img.shape[0] * color_img.shape[1] + max_bbox_area = image_area * (self.max_bbox_size_percent / 100.0) + + filtered_objects = [] + for obj in objects: + if "bbox" in obj and obj["bbox"] is not None: + bbox = obj["bbox"] + # Calculate bbox area (assuming bbox format [x1, y1, x2, y2]) + bbox_area = (bbox[2] - bbox[0]) * (bbox[3] - bbox[1]) + if bbox_area <= max_bbox_area: + filtered_objects.append(obj) + else: + filtered_objects.append(obj) + + objects = filtered_objects + + # Extract masks from ObjectData + masks = self._extract_masks_from_objects(objects) + + # Prepare masks + processed_masks = self._prepare_masks(masks, depth_img.shape) + + # Create point clouds efficiently + self.full_pcd, masked_pcds = create_point_cloud_and_extract_masks( + color_img, depth_img, processed_masks, self.depth_camera_matrix, depth_scale=1.0 + ) + + # Process each object and update ObjectData + updated_objects = [] + + for i, (obj, _mask, pcd) in enumerate( + zip(objects, processed_masks, masked_pcds, strict=False) + ): + # Skip empty point clouds + if len(np.asarray(pcd.points)) == 0: + continue + + # Create a copy of the object data to avoid modifying the original + updated_obj = obj.copy() + + # Generate consistent color + object_id = obj.get("object_id", i) + rgb_color = self.generate_color_from_id(object_id) + + # Apply color mask + pcd = self._apply_color_mask(pcd, rgb_color) + + # Apply subsampling to control point cloud size + pcd = self._apply_subsampling(pcd) + + # Apply filtering (optional based on flags) + pcd_filtered = self._apply_filtering(pcd) + + # Fit cuboid and extract 3D information + points = np.asarray(pcd_filtered.points) + if len(points) >= self.min_points_for_cuboid: + cuboid_params = fit_cuboid(points, method=self.cuboid_method) + if cuboid_params is not None: + # Update position, rotation, and size from cuboid + center = cuboid_params["center"] + dimensions = cuboid_params["dimensions"] + rotation_matrix = cuboid_params["rotation"] + + # Convert rotation matrix to euler angles (roll, pitch, yaw) + sy = np.sqrt( + rotation_matrix[0, 0] * rotation_matrix[0, 0] + + rotation_matrix[1, 0] * rotation_matrix[1, 0] + ) + singular = sy < 1e-6 + + if not singular: + roll = np.arctan2(rotation_matrix[2, 1], rotation_matrix[2, 2]) + pitch = np.arctan2(-rotation_matrix[2, 0], sy) + yaw = np.arctan2(rotation_matrix[1, 0], rotation_matrix[0, 0]) + else: + roll = np.arctan2(-rotation_matrix[1, 2], rotation_matrix[1, 1]) + pitch = np.arctan2(-rotation_matrix[2, 0], sy) + yaw = 0 + + # Update position, rotation, and size from cuboid + updated_obj["position"] = Vector(center[0], center[1], center[2]) + updated_obj["rotation"] = Vector(roll, pitch, yaw) + updated_obj["size"] = { + "width": float(dimensions[0]), + "height": float(dimensions[1]), + "depth": float(dimensions[2]), + } + + # Add point cloud data to ObjectData + updated_obj["point_cloud"] = pcd_filtered + updated_obj["color"] = rgb_color + + # Extract numpy arrays for grasp generation + points_array = np.asarray(pcd_filtered.points).astype(np.float32) # Nx3 XYZ coordinates + if pcd_filtered.has_colors(): + colors_array = np.asarray(pcd_filtered.colors).astype( + np.float32 + ) # Nx3 RGB (0-1 range) + else: + # If no colors, create array of zeros + colors_array = np.zeros((len(points_array), 3), dtype=np.float32) + + updated_obj["point_cloud_numpy"] = points_array + updated_obj["colors_numpy"] = colors_array + + updated_objects.append(updated_obj) + + return updated_objects + + def cleanup(self) -> None: + """Clean up resources.""" + if torch.cuda.is_available(): + torch.cuda.empty_cache() diff --git a/dimos/perception/pointcloud/test_pointcloud_filtering.py b/dimos/perception/pointcloud/test_pointcloud_filtering.py new file mode 100644 index 0000000000..719feeb984 --- /dev/null +++ b/dimos/perception/pointcloud/test_pointcloud_filtering.py @@ -0,0 +1,263 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from typing import TYPE_CHECKING + +import cv2 +import numpy as np +import open3d as o3d +import pytest + +from dimos.perception.pointcloud.pointcloud_filtering import PointcloudFiltering +from dimos.perception.pointcloud.utils import load_camera_matrix_from_yaml + +if TYPE_CHECKING: + from dimos.types.manipulation import ObjectData + + +class TestPointcloudFiltering: + def test_pointcloud_filtering_initialization(self) -> None: + """Test PointcloudFiltering initializes correctly with default parameters.""" + try: + filtering = PointcloudFiltering() + assert filtering is not None + assert filtering.color_weight == 0.3 + assert filtering.enable_statistical_filtering + assert filtering.enable_radius_filtering + assert filtering.enable_subsampling + except Exception as e: + pytest.skip(f"Skipping test due to initialization error: {e}") + + def test_pointcloud_filtering_with_custom_params(self) -> None: + """Test PointcloudFiltering with custom parameters.""" + try: + filtering = PointcloudFiltering( + color_weight=0.5, + enable_statistical_filtering=False, + enable_radius_filtering=False, + voxel_size=0.01, + max_num_objects=5, + ) + assert filtering.color_weight == 0.5 + assert not filtering.enable_statistical_filtering + assert not filtering.enable_radius_filtering + assert filtering.voxel_size == 0.01 + assert filtering.max_num_objects == 5 + except Exception as e: + pytest.skip(f"Skipping test due to initialization error: {e}") + + def test_pointcloud_filtering_process_images(self) -> None: + """Test PointcloudFiltering can process RGB-D images and return filtered point clouds.""" + try: + # Import data inside method to avoid pytest fixture confusion + from dimos.utils.data import get_data + + # Load test RGB-D data + data_dir = get_data("rgbd_frames") + + # Load first frame + color_path = os.path.join(data_dir, "color", "00000.png") + depth_path = os.path.join(data_dir, "depth", "00000.png") + intrinsics_path = os.path.join(data_dir, "color_camera_info.yaml") + + assert os.path.exists(color_path), f"Color image not found: {color_path}" + assert os.path.exists(depth_path), f"Depth image not found: {depth_path}" + assert os.path.exists(intrinsics_path), f"Intrinsics file not found: {intrinsics_path}" + + # Load images + color_img = cv2.imread(color_path) + color_img = cv2.cvtColor(color_img, cv2.COLOR_BGR2RGB) + + depth_img = cv2.imread(depth_path, cv2.IMREAD_ANYDEPTH) + if depth_img.dtype == np.uint16: + depth_img = depth_img.astype(np.float32) / 1000.0 + + # Load camera intrinsics + camera_matrix = load_camera_matrix_from_yaml(intrinsics_path) + if camera_matrix is None: + pytest.skip("Failed to load camera intrinsics") + + # Create mock objects with segmentation masks + height, width = color_img.shape[:2] + + # Create simple rectangular masks for testing + mock_objects = [] + + # Object 1: Top-left quadrant + mask1 = np.zeros((height, width), dtype=bool) + mask1[height // 4 : height // 2, width // 4 : width // 2] = True + + obj1: ObjectData = { + "object_id": 1, + "confidence": 0.9, + "bbox": [width // 4, height // 4, width // 2, height // 2], + "segmentation_mask": mask1, + "name": "test_object_1", + } + mock_objects.append(obj1) + + # Object 2: Bottom-right quadrant + mask2 = np.zeros((height, width), dtype=bool) + mask2[height // 2 : 3 * height // 4, width // 2 : 3 * width // 4] = True + + obj2: ObjectData = { + "object_id": 2, + "confidence": 0.8, + "bbox": [width // 2, height // 2, 3 * width // 4, 3 * height // 4], + "segmentation_mask": mask2, + "name": "test_object_2", + } + mock_objects.append(obj2) + + # Initialize filtering with intrinsics + filtering = PointcloudFiltering( + color_intrinsics=camera_matrix, + depth_intrinsics=camera_matrix, + enable_statistical_filtering=False, # Disable for faster testing + enable_radius_filtering=False, # Disable for faster testing + voxel_size=0.01, # Larger voxel for faster processing + ) + + # Process images + results = filtering.process_images(color_img, depth_img, mock_objects) + + print( + f"Processing results - Input objects: {len(mock_objects)}, Output objects: {len(results)}" + ) + + # Verify results + assert isinstance(results, list), "Results should be a list" + assert len(results) <= len(mock_objects), "Should not return more objects than input" + + # Check each result object + for i, result in enumerate(results): + print(f"Object {i}: {result.get('name', 'unknown')}") + + # Verify required fields exist + assert "point_cloud" in result, "Result should contain point_cloud" + assert "color" in result, "Result should contain color" + assert "point_cloud_numpy" in result, "Result should contain point_cloud_numpy" + + # Verify point cloud is valid Open3D object + pcd = result["point_cloud"] + assert isinstance(pcd, o3d.geometry.PointCloud), ( + "point_cloud should be Open3D PointCloud" + ) + + # Verify numpy arrays + points_array = result["point_cloud_numpy"] + assert isinstance(points_array, np.ndarray), ( + "point_cloud_numpy should be numpy array" + ) + assert points_array.shape[1] == 3, "Point array should have 3 columns (x,y,z)" + assert points_array.dtype == np.float32, "Point array should be float32" + + # Verify color + color = result["color"] + assert isinstance(color, np.ndarray), "Color should be numpy array" + assert color.shape == (3,), "Color should be RGB triplet" + assert color.dtype == np.uint8, "Color should be uint8" + + # Check if 3D information was added (when enough points for cuboid fitting) + points = np.asarray(pcd.points) + if len(points) >= filtering.min_points_for_cuboid: + if "position" in result: + assert "rotation" in result, "Should have rotation if position exists" + assert "size" in result, "Should have size if position exists" + + # Verify position format + from dimos.types.vector import Vector + + position = result["position"] + assert isinstance(position, Vector), "Position should be Vector" + + # Verify size format + size = result["size"] + assert isinstance(size, dict), "Size should be dict" + assert "width" in size and "height" in size and "depth" in size + + print(f" - Points: {len(points)}") + print(f" - Color: {color}") + if "position" in result: + print(f" - Position: {result['position']}") + print(f" - Size: {result['size']}") + + # Test full point cloud access + full_pcd = filtering.get_full_point_cloud() + if full_pcd is not None: + assert isinstance(full_pcd, o3d.geometry.PointCloud), ( + "Full point cloud should be Open3D PointCloud" + ) + full_points = np.asarray(full_pcd.points) + print(f"Full point cloud points: {len(full_points)}") + + print("All pointcloud filtering tests passed!") + + except Exception as e: + pytest.skip(f"Skipping test due to error: {e}") + + def test_pointcloud_filtering_empty_objects(self) -> None: + """Test PointcloudFiltering with empty object list.""" + try: + from dimos.utils.data import get_data + + # Load test data + data_dir = get_data("rgbd_frames") + color_path = os.path.join(data_dir, "color", "00000.png") + depth_path = os.path.join(data_dir, "depth", "00000.png") + + if not (os.path.exists(color_path) and os.path.exists(depth_path)): + pytest.skip("Test images not found") + + color_img = cv2.imread(color_path) + color_img = cv2.cvtColor(color_img, cv2.COLOR_BGR2RGB) + depth_img = cv2.imread(depth_path, cv2.IMREAD_ANYDEPTH) + if depth_img.dtype == np.uint16: + depth_img = depth_img.astype(np.float32) / 1000.0 + + filtering = PointcloudFiltering() + + # Test with empty object list + results = filtering.process_images(color_img, depth_img, []) + + assert isinstance(results, list), "Results should be a list" + assert len(results) == 0, "Should return empty list for empty input" + + except Exception as e: + pytest.skip(f"Skipping test due to error: {e}") + + def test_color_generation_consistency(self) -> None: + """Test that color generation is consistent for the same object ID.""" + try: + filtering = PointcloudFiltering() + + # Test color generation consistency + color1 = filtering.generate_color_from_id(42) + color2 = filtering.generate_color_from_id(42) + color3 = filtering.generate_color_from_id(43) + + assert np.array_equal(color1, color2), "Same ID should generate same color" + assert not np.array_equal(color1, color3), ( + "Different IDs should generate different colors" + ) + assert color1.shape == (3,), "Color should be RGB triplet" + assert color1.dtype == np.uint8, "Color should be uint8" + + except Exception as e: + pytest.skip(f"Skipping test due to error: {e}") + + +if __name__ == "__main__": + pytest.main(["-v", __file__]) diff --git a/dimos/perception/pointcloud/utils.py b/dimos/perception/pointcloud/utils.py new file mode 100644 index 0000000000..97dc8d3716 --- /dev/null +++ b/dimos/perception/pointcloud/utils.py @@ -0,0 +1,1113 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Point cloud utilities for RGBD data processing. + +This module provides efficient utilities for creating and manipulating point clouds +from RGBD images using Open3D. +""" + +import os +from typing import Any + +import cv2 +import numpy as np +import open3d as o3d +from scipy.spatial import cKDTree +import yaml + +from dimos.perception.common.utils import project_3d_points_to_2d + + +def load_camera_matrix_from_yaml( + camera_info: str | list[float] | np.ndarray | dict | None, +) -> np.ndarray | None: + """ + Load camera intrinsic matrix from various input formats. + + Args: + camera_info: Can be: + - Path to YAML file containing camera parameters + - List of [fx, fy, cx, cy] + - 3x3 numpy array (returned as-is) + - Dict with camera parameters + - None (returns None) + + Returns: + 3x3 camera intrinsic matrix or None if input is None + + Raises: + ValueError: If camera_info format is invalid or file cannot be read + FileNotFoundError: If YAML file path doesn't exist + """ + if camera_info is None: + return None + + # Handle case where camera_info is already a matrix + if isinstance(camera_info, np.ndarray) and camera_info.shape == (3, 3): + return camera_info.astype(np.float32) + + # Handle case where camera_info is [fx, fy, cx, cy] format + if isinstance(camera_info, list) and len(camera_info) == 4: + fx, fy, cx, cy = camera_info + return np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]], dtype=np.float32) + + # Handle case where camera_info is a dict + if isinstance(camera_info, dict): + return _extract_matrix_from_dict(camera_info) + + # Handle case where camera_info is a path to a YAML file + if isinstance(camera_info, str): + if not os.path.isfile(camera_info): + raise FileNotFoundError(f"Camera info file not found: {camera_info}") + + try: + with open(camera_info) as f: + data = yaml.safe_load(f) + return _extract_matrix_from_dict(data) + except Exception as e: + raise ValueError(f"Failed to read camera info from {camera_info}: {e}") + + raise ValueError( + f"Invalid camera_info format. Expected str, list, dict, or numpy array, got {type(camera_info)}" + ) + + +def _extract_matrix_from_dict(data: dict) -> np.ndarray: + """Extract camera matrix from dictionary with various formats.""" + # ROS format with 'K' field (most common) + if "K" in data: + k_data = data["K"] + if len(k_data) == 9: + return np.array(k_data, dtype=np.float32).reshape(3, 3) + + # Standard format with 'camera_matrix' + if "camera_matrix" in data: + if "data" in data["camera_matrix"]: + matrix_data = data["camera_matrix"]["data"] + if len(matrix_data) == 9: + return np.array(matrix_data, dtype=np.float32).reshape(3, 3) + + # Explicit intrinsics format + if all(k in data for k in ["fx", "fy", "cx", "cy"]): + fx, fy = float(data["fx"]), float(data["fy"]) + cx, cy = float(data["cx"]), float(data["cy"]) + return np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]], dtype=np.float32) + + # Error case - provide helpful debug info + available_keys = list(data.keys()) + if "K" in data: + k_info = f"K field length: {len(data['K']) if hasattr(data['K'], '__len__') else 'unknown'}" + else: + k_info = "K field not found" + + raise ValueError( + f"Cannot extract camera matrix from data. " + f"Available keys: {available_keys}. {k_info}. " + f"Expected formats: 'K' (9 elements), 'camera_matrix.data' (9 elements), " + f"or individual 'fx', 'fy', 'cx', 'cy' fields." + ) + + +def create_o3d_point_cloud_from_rgbd( + color_img: np.ndarray, + depth_img: np.ndarray, + intrinsic: np.ndarray, + depth_scale: float = 1.0, + depth_trunc: float = 3.0, +) -> o3d.geometry.PointCloud: + """ + Create an Open3D point cloud from RGB and depth images. + + Args: + color_img: RGB image as numpy array (H, W, 3) + depth_img: Depth image as numpy array (H, W) + intrinsic: Camera intrinsic matrix (3x3 numpy array) + depth_scale: Scale factor to convert depth to meters + depth_trunc: Maximum depth in meters + + Returns: + Open3D point cloud object + + Raises: + ValueError: If input dimensions are invalid + """ + # Validate inputs + if len(color_img.shape) != 3 or color_img.shape[2] != 3: + raise ValueError(f"color_img must be (H, W, 3), got {color_img.shape}") + if len(depth_img.shape) != 2: + raise ValueError(f"depth_img must be (H, W), got {depth_img.shape}") + if color_img.shape[:2] != depth_img.shape: + raise ValueError( + f"Color and depth image dimensions don't match: {color_img.shape[:2]} vs {depth_img.shape}" + ) + if intrinsic.shape != (3, 3): + raise ValueError(f"intrinsic must be (3, 3), got {intrinsic.shape}") + + # Convert to Open3D format + color_o3d = o3d.geometry.Image(color_img.astype(np.uint8)) + + # Filter out inf and nan values from depth image + depth_filtered = depth_img.copy() + + # Create mask for valid depth values (finite, positive, non-zero) + valid_mask = np.isfinite(depth_filtered) & (depth_filtered > 0) + + # Set invalid values to 0 (which Open3D treats as no depth) + depth_filtered[~valid_mask] = 0.0 + + depth_o3d = o3d.geometry.Image(depth_filtered.astype(np.float32)) + + # Create Open3D intrinsic object + height, width = color_img.shape[:2] + fx, fy = intrinsic[0, 0], intrinsic[1, 1] + cx, cy = intrinsic[0, 2], intrinsic[1, 2] + intrinsic_o3d = o3d.camera.PinholeCameraIntrinsic( + width, + height, + fx, + fy, # fx, fy + cx, + cy, # cx, cy + ) + + # Create RGBD image + rgbd = o3d.geometry.RGBDImage.create_from_color_and_depth( + color_o3d, + depth_o3d, + depth_scale=depth_scale, + depth_trunc=depth_trunc, + convert_rgb_to_intensity=False, + ) + + # Create point cloud + pcd = o3d.geometry.PointCloud.create_from_rgbd_image(rgbd, intrinsic_o3d) + + return pcd + + +def create_point_cloud_and_extract_masks( + color_img: np.ndarray, + depth_img: np.ndarray, + masks: list[np.ndarray], + intrinsic: np.ndarray, + depth_scale: float = 1.0, + depth_trunc: float = 3.0, +) -> tuple[o3d.geometry.PointCloud, list[o3d.geometry.PointCloud]]: + """ + Efficiently create a point cloud once and extract multiple masked regions. + + Args: + color_img: RGB image (H, W, 3) + depth_img: Depth image (H, W) + masks: List of boolean masks, each of shape (H, W) + intrinsic: Camera intrinsic matrix (3x3 numpy array) + depth_scale: Scale factor to convert depth to meters + depth_trunc: Maximum depth in meters + + Returns: + Tuple of (full_point_cloud, list_of_masked_point_clouds) + """ + if not masks: + return o3d.geometry.PointCloud(), [] + + # Create the full point cloud + full_pcd = create_o3d_point_cloud_from_rgbd( + color_img, depth_img, intrinsic, depth_scale, depth_trunc + ) + + if len(np.asarray(full_pcd.points)) == 0: + return full_pcd, [o3d.geometry.PointCloud() for _ in masks] + + # Create pixel-to-point mapping + valid_depth_mask = np.isfinite(depth_img) & (depth_img > 0) & (depth_img <= depth_trunc) + + valid_depth = valid_depth_mask.flatten() + if not np.any(valid_depth): + return full_pcd, [o3d.geometry.PointCloud() for _ in masks] + + pixel_to_point = np.full(len(valid_depth), -1, dtype=np.int32) + pixel_to_point[valid_depth] = np.arange(np.sum(valid_depth)) + + # Extract point clouds for each mask + masked_pcds = [] + max_points = len(np.asarray(full_pcd.points)) + + for mask in masks: + if mask.shape != depth_img.shape: + masked_pcds.append(o3d.geometry.PointCloud()) + continue + + mask_flat = mask.flatten() + valid_mask_indices = mask_flat & valid_depth + point_indices = pixel_to_point[valid_mask_indices] + valid_point_indices = point_indices[point_indices >= 0] + + if len(valid_point_indices) > 0: + valid_point_indices = np.clip(valid_point_indices, 0, max_points - 1) + valid_point_indices = np.unique(valid_point_indices) + masked_pcd = full_pcd.select_by_index(valid_point_indices.tolist()) + else: + masked_pcd = o3d.geometry.PointCloud() + + masked_pcds.append(masked_pcd) + + return full_pcd, masked_pcds + + +def filter_point_cloud_statistical( + pcd: o3d.geometry.PointCloud, nb_neighbors: int = 20, std_ratio: float = 2.0 +) -> tuple[o3d.geometry.PointCloud, np.ndarray]: + """ + Apply statistical outlier filtering to point cloud. + + Args: + pcd: Input point cloud + nb_neighbors: Number of neighbors to analyze for each point + std_ratio: Threshold level based on standard deviation + + Returns: + Tuple of (filtered_point_cloud, outlier_indices) + """ + if len(np.asarray(pcd.points)) == 0: + return pcd, np.array([]) + + return pcd.remove_statistical_outlier(nb_neighbors=nb_neighbors, std_ratio=std_ratio) + + +def filter_point_cloud_radius( + pcd: o3d.geometry.PointCloud, nb_points: int = 16, radius: float = 0.05 +) -> tuple[o3d.geometry.PointCloud, np.ndarray]: + """ + Apply radius-based outlier filtering to point cloud. + + Args: + pcd: Input point cloud + nb_points: Minimum number of points within radius + radius: Search radius in meters + + Returns: + Tuple of (filtered_point_cloud, outlier_indices) + """ + if len(np.asarray(pcd.points)) == 0: + return pcd, np.array([]) + + return pcd.remove_radius_outlier(nb_points=nb_points, radius=radius) + + +def overlay_point_clouds_on_image( + base_image: np.ndarray, + point_clouds: list[o3d.geometry.PointCloud], + camera_intrinsics: list[float] | np.ndarray, + colors: list[tuple[int, int, int]], + point_size: int = 2, + alpha: float = 0.7, +) -> np.ndarray: + """ + Overlay multiple colored point clouds onto an image. + + Args: + base_image: Base image to overlay onto (H, W, 3) - assumed to be RGB + point_clouds: List of Open3D point cloud objects + camera_intrinsics: Camera parameters as [fx, fy, cx, cy] list or 3x3 matrix + colors: List of RGB color tuples for each point cloud. If None, generates distinct colors. + point_size: Size of points to draw (in pixels) + alpha: Blending factor for overlay (0.0 = fully transparent, 1.0 = fully opaque) + + Returns: + Image with overlaid point clouds (H, W, 3) + """ + if len(point_clouds) == 0: + return base_image.copy() + + # Create overlay image + overlay = base_image.copy() + height, width = base_image.shape[:2] + + # Process each point cloud + for i, pcd in enumerate(point_clouds): + if pcd is None: + continue + + points_3d = np.asarray(pcd.points) + if len(points_3d) == 0: + continue + + # Project 3D points to 2D + points_2d = project_3d_points_to_2d(points_3d, camera_intrinsics) + + if len(points_2d) == 0: + continue + + # Filter points within image bounds + valid_mask = ( + (points_2d[:, 0] >= 0) + & (points_2d[:, 0] < width) + & (points_2d[:, 1] >= 0) + & (points_2d[:, 1] < height) + ) + valid_points_2d = points_2d[valid_mask] + + if len(valid_points_2d) == 0: + continue + + # Get color for this point cloud + color = colors[i % len(colors)] + + # Ensure color is a tuple of integers for OpenCV + if isinstance(color, list | tuple | np.ndarray): + color = tuple(int(c) for c in color[:3]) + else: + color = (255, 255, 255) + + # Draw points on overlay + for point in valid_points_2d: + u, v = point + # Draw a small filled circle for each point + cv2.circle(overlay, (u, v), point_size, color, -1) + + # Blend overlay with base image + result = cv2.addWeighted(base_image, 1 - alpha, overlay, alpha, 0) + + return result + + +def create_point_cloud_overlay_visualization( + base_image: np.ndarray, + objects: list[dict], + intrinsics: np.ndarray, +) -> np.ndarray: + """ + Create a visualization showing object point clouds and bounding boxes overlaid on a base image. + + Args: + base_image: Base image to overlay onto (H, W, 3) + objects: List of object dictionaries containing 'point_cloud', 'color', 'position', 'rotation', 'size' keys + intrinsics: Camera intrinsics as [fx, fy, cx, cy] or 3x3 matrix + + Returns: + Visualization image with overlaid point clouds and bounding boxes (H, W, 3) + """ + # Extract point clouds and colors from objects + point_clouds = [] + colors = [] + for obj in objects: + if "point_cloud" in obj and obj["point_cloud"] is not None: + point_clouds.append(obj["point_cloud"]) + + # Convert color to tuple + color = obj["color"] + if isinstance(color, np.ndarray): + color = tuple(int(c) for c in color) + elif isinstance(color, list | tuple): + color = tuple(int(c) for c in color[:3]) + colors.append(color) + + # Create visualization + if point_clouds: + result = overlay_point_clouds_on_image( + base_image=base_image, + point_clouds=point_clouds, + camera_intrinsics=intrinsics, + colors=colors, + point_size=3, + alpha=0.8, + ) + else: + result = base_image.copy() + + # Draw 3D bounding boxes + height_img, width_img = result.shape[:2] + for i, obj in enumerate(objects): + if all(key in obj and obj[key] is not None for key in ["position", "rotation", "size"]): + try: + # Create and project 3D bounding box + corners_3d = create_3d_bounding_box_corners( + obj["position"], obj["rotation"], obj["size"] + ) + corners_2d = project_3d_points_to_2d(corners_3d, intrinsics) + + # Check if any corners are visible + valid_mask = ( + (corners_2d[:, 0] >= 0) + & (corners_2d[:, 0] < width_img) + & (corners_2d[:, 1] >= 0) + & (corners_2d[:, 1] < height_img) + ) + + if np.any(valid_mask): + # Get color + bbox_color = colors[i] if i < len(colors) else (255, 255, 255) + draw_3d_bounding_box_on_image(result, corners_2d, bbox_color, thickness=2) + except: + continue + + return result + + +def create_3d_bounding_box_corners(position, rotation, size: int): + """ + Create 8 corners of a 3D bounding box from position, rotation, and size. + + Args: + position: Vector or dict with x, y, z coordinates + rotation: Vector or dict with roll, pitch, yaw angles + size: Dict with width, height, depth + + Returns: + 8x3 numpy array of corner coordinates + """ + # Convert position to numpy array + if hasattr(position, "x"): # Vector object + center = np.array([position.x, position.y, position.z]) + else: # Dictionary + center = np.array([position["x"], position["y"], position["z"]]) + + # Convert rotation (euler angles) to rotation matrix + if hasattr(rotation, "x"): # Vector object (roll, pitch, yaw) + roll, pitch, yaw = rotation.x, rotation.y, rotation.z + else: # Dictionary + roll, pitch, yaw = rotation["roll"], rotation["pitch"], rotation["yaw"] + + # Create rotation matrix from euler angles (ZYX order) + cos_r, sin_r = np.cos(roll), np.sin(roll) + cos_p, sin_p = np.cos(pitch), np.sin(pitch) + cos_y, sin_y = np.cos(yaw), np.sin(yaw) + + # Rotation matrix for ZYX euler angles + R = np.array( + [ + [ + cos_y * cos_p, + cos_y * sin_p * sin_r - sin_y * cos_r, + cos_y * sin_p * cos_r + sin_y * sin_r, + ], + [ + sin_y * cos_p, + sin_y * sin_p * sin_r + cos_y * cos_r, + sin_y * sin_p * cos_r - cos_y * sin_r, + ], + [-sin_p, cos_p * sin_r, cos_p * cos_r], + ] + ) + + # Get dimensions + width = size.get("width", 0.1) + height = size.get("height", 0.1) + depth = size.get("depth", 0.1) + + # Create 8 corners of the bounding box (before rotation) + corners = np.array( + [ + [-width / 2, -height / 2, -depth / 2], # 0 + [width / 2, -height / 2, -depth / 2], # 1 + [width / 2, height / 2, -depth / 2], # 2 + [-width / 2, height / 2, -depth / 2], # 3 + [-width / 2, -height / 2, depth / 2], # 4 + [width / 2, -height / 2, depth / 2], # 5 + [width / 2, height / 2, depth / 2], # 6 + [-width / 2, height / 2, depth / 2], # 7 + ] + ) + + # Apply rotation and translation + rotated_corners = corners @ R.T + center + + return rotated_corners + + +def draw_3d_bounding_box_on_image(image, corners_2d, color, thickness: int = 2) -> None: + """ + Draw a 3D bounding box on an image using projected 2D corners. + + Args: + image: Image to draw on + corners_2d: 8x2 array of 2D corner coordinates + color: RGB color tuple + thickness: Line thickness + """ + # Define the 12 edges of a cube (connecting corner indices) + edges = [ + (0, 1), + (1, 2), + (2, 3), + (3, 0), # Bottom face + (4, 5), + (5, 6), + (6, 7), + (7, 4), # Top face + (0, 4), + (1, 5), + (2, 6), + (3, 7), # Vertical edges + ] + + # Draw each edge + for start_idx, end_idx in edges: + start_point = tuple(corners_2d[start_idx].astype(int)) + end_point = tuple(corners_2d[end_idx].astype(int)) + cv2.line(image, start_point, end_point, color, thickness) + + +def extract_and_cluster_misc_points( + full_pcd: o3d.geometry.PointCloud, + all_objects: list[dict], + eps: float = 0.03, + min_points: int = 100, + enable_filtering: bool = True, + voxel_size: float = 0.02, +) -> tuple[list[o3d.geometry.PointCloud], o3d.geometry.VoxelGrid]: + """ + Extract miscellaneous/background points and cluster them using DBSCAN. + + Args: + full_pcd: Complete scene point cloud + all_objects: List of objects with point clouds to subtract + eps: DBSCAN epsilon parameter (max distance between points in cluster) + min_points: DBSCAN min_samples parameter (min points to form cluster) + enable_filtering: Whether to apply statistical and radius filtering + voxel_size: Size of voxels for voxel grid generation + + Returns: + Tuple of (clustered_point_clouds, voxel_grid) + """ + if full_pcd is None or len(np.asarray(full_pcd.points)) == 0: + return [], o3d.geometry.VoxelGrid() + + if not all_objects: + # If no objects detected, cluster the full point cloud + clusters = _cluster_point_cloud_dbscan(full_pcd, eps, min_points) + voxel_grid = _create_voxel_grid_from_clusters(clusters, voxel_size) + return clusters, voxel_grid + + try: + # Start with a copy of the full point cloud + misc_pcd = o3d.geometry.PointCloud(full_pcd) + + # Remove object points by combining all object point clouds + all_object_points = [] + for obj in all_objects: + if "point_cloud" in obj and obj["point_cloud"] is not None: + obj_points = np.asarray(obj["point_cloud"].points) + if len(obj_points) > 0: + all_object_points.append(obj_points) + + if not all_object_points: + # No object points to remove, cluster full point cloud + clusters = _cluster_point_cloud_dbscan(misc_pcd, eps, min_points) + voxel_grid = _create_voxel_grid_from_clusters(clusters, voxel_size) + return clusters, voxel_grid + + # Combine all object points + combined_obj_points = np.vstack(all_object_points) + + # For efficiency, downsample both point clouds + misc_downsampled = misc_pcd.voxel_down_sample(voxel_size=0.005) + + # Create object point cloud for efficient operations + obj_pcd = o3d.geometry.PointCloud() + obj_pcd.points = o3d.utility.Vector3dVector(combined_obj_points) + obj_downsampled = obj_pcd.voxel_down_sample(voxel_size=0.005) + + misc_points = np.asarray(misc_downsampled.points) + obj_points_down = np.asarray(obj_downsampled.points) + + if len(misc_points) == 0 or len(obj_points_down) == 0: + clusters = _cluster_point_cloud_dbscan(misc_downsampled, eps, min_points) + voxel_grid = _create_voxel_grid_from_clusters(clusters, voxel_size) + return clusters, voxel_grid + + # Build tree for object points + obj_tree = cKDTree(obj_points_down) + + # Find distances from misc points to nearest object points + distances, _ = obj_tree.query(misc_points, k=1) + + # Keep points that are far enough from any object point + threshold = 0.015 # 1.5cm threshold + keep_mask = distances > threshold + + if not np.any(keep_mask): + return [], o3d.geometry.VoxelGrid() + + # Filter misc points + misc_indices = np.where(keep_mask)[0] + final_misc_pcd = misc_downsampled.select_by_index(misc_indices) + + if len(np.asarray(final_misc_pcd.points)) == 0: + return [], o3d.geometry.VoxelGrid() + + # Apply additional filtering if enabled + if enable_filtering: + # Apply statistical outlier filtering + filtered_misc_pcd, _ = filter_point_cloud_statistical( + final_misc_pcd, nb_neighbors=30, std_ratio=2.0 + ) + + if len(np.asarray(filtered_misc_pcd.points)) == 0: + return [], o3d.geometry.VoxelGrid() + + # Apply radius outlier filtering + final_filtered_misc_pcd, _ = filter_point_cloud_radius( + filtered_misc_pcd, + nb_points=20, + radius=0.03, # 3cm radius + ) + + if len(np.asarray(final_filtered_misc_pcd.points)) == 0: + return [], o3d.geometry.VoxelGrid() + + final_misc_pcd = final_filtered_misc_pcd + + # Cluster the misc points using DBSCAN + clusters = _cluster_point_cloud_dbscan(final_misc_pcd, eps, min_points) + + # Create voxel grid from all misc points (before clustering) + voxel_grid = _create_voxel_grid_from_point_cloud(final_misc_pcd, voxel_size) + + return clusters, voxel_grid + + except Exception as e: + print(f"Error in misc point extraction and clustering: {e}") + # Fallback: return downsampled full point cloud as single cluster + try: + downsampled = full_pcd.voxel_down_sample(voxel_size=0.02) + if len(np.asarray(downsampled.points)) > 0: + voxel_grid = _create_voxel_grid_from_point_cloud(downsampled, voxel_size) + return [downsampled], voxel_grid + else: + return [], o3d.geometry.VoxelGrid() + except: + return [], o3d.geometry.VoxelGrid() + + +def _create_voxel_grid_from_point_cloud( + pcd: o3d.geometry.PointCloud, voxel_size: float = 0.02 +) -> o3d.geometry.VoxelGrid: + """ + Create a voxel grid from a point cloud. + + Args: + pcd: Input point cloud + voxel_size: Size of each voxel + + Returns: + Open3D VoxelGrid object + """ + if len(np.asarray(pcd.points)) == 0: + return o3d.geometry.VoxelGrid() + + try: + # Create voxel grid from point cloud + voxel_grid = o3d.geometry.VoxelGrid.create_from_point_cloud(pcd, voxel_size) + + # Color the voxels with a semi-transparent gray + for voxel in voxel_grid.get_voxels(): + voxel.color = [0.5, 0.5, 0.5] # Gray color + + print( + f"Created voxel grid with {len(voxel_grid.get_voxels())} voxels (voxel_size={voxel_size})" + ) + return voxel_grid + + except Exception as e: + print(f"Error creating voxel grid: {e}") + return o3d.geometry.VoxelGrid() + + +def _create_voxel_grid_from_clusters( + clusters: list[o3d.geometry.PointCloud], voxel_size: float = 0.02 +) -> o3d.geometry.VoxelGrid: + """ + Create a voxel grid from multiple clustered point clouds. + + Args: + clusters: List of clustered point clouds + voxel_size: Size of each voxel + + Returns: + Open3D VoxelGrid object + """ + if not clusters: + return o3d.geometry.VoxelGrid() + + # Combine all clusters into one point cloud + combined_points = [] + for cluster in clusters: + points = np.asarray(cluster.points) + if len(points) > 0: + combined_points.append(points) + + if not combined_points: + return o3d.geometry.VoxelGrid() + + # Create combined point cloud + all_points = np.vstack(combined_points) + combined_pcd = o3d.geometry.PointCloud() + combined_pcd.points = o3d.utility.Vector3dVector(all_points) + + return _create_voxel_grid_from_point_cloud(combined_pcd, voxel_size) + + +def _cluster_point_cloud_dbscan( + pcd: o3d.geometry.PointCloud, eps: float = 0.05, min_points: int = 50 +) -> list[o3d.geometry.PointCloud]: + """ + Cluster a point cloud using DBSCAN and return list of clustered point clouds. + + Args: + pcd: Point cloud to cluster + eps: DBSCAN epsilon parameter + min_points: DBSCAN min_samples parameter + + Returns: + List of point clouds, one for each cluster + """ + if len(np.asarray(pcd.points)) == 0: + return [] + + try: + # Apply DBSCAN clustering + labels = np.array(pcd.cluster_dbscan(eps=eps, min_points=min_points)) + + # Get unique cluster labels (excluding noise points labeled as -1) + unique_labels = np.unique(labels) + cluster_pcds = [] + + for label in unique_labels: + if label == -1: # Skip noise points + continue + + # Get indices for this cluster + cluster_indices = np.where(labels == label)[0] + + if len(cluster_indices) > 0: + # Create point cloud for this cluster + cluster_pcd = pcd.select_by_index(cluster_indices) + + # Assign a random color to this cluster + cluster_color = np.random.rand(3) # Random RGB color + cluster_pcd.paint_uniform_color(cluster_color) + + cluster_pcds.append(cluster_pcd) + + print( + f"DBSCAN clustering found {len(cluster_pcds)} clusters from {len(np.asarray(pcd.points))} points" + ) + return cluster_pcds + + except Exception as e: + print(f"Error in DBSCAN clustering: {e}") + return [pcd] # Return original point cloud as fallback + + +def get_standard_coordinate_transform(): + """ + Get a standard coordinate transformation matrix for consistent visualization. + + This transformation ensures that: + - X (red) axis points right + - Y (green) axis points up + - Z (blue) axis points toward viewer + + Returns: + 4x4 transformation matrix + """ + # Standard transformation matrix to ensure consistent coordinate frame orientation + transform = np.array( + [ + [1, 0, 0, 0], # X points right + [0, -1, 0, 0], # Y points up (flip from OpenCV to standard) + [0, 0, -1, 0], # Z points toward viewer (flip depth) + [0, 0, 0, 1], + ] + ) + return transform + + +def visualize_clustered_point_clouds( + clustered_pcds: list[o3d.geometry.PointCloud], + window_name: str = "Clustered Point Clouds", + point_size: float = 2.0, + show_coordinate_frame: bool = True, + coordinate_frame_size: float = 0.1, +) -> None: + """ + Visualize multiple clustered point clouds with different colors. + + Args: + clustered_pcds: List of point clouds (already colored) + window_name: Name of the visualization window + point_size: Size of points in the visualization + show_coordinate_frame: Whether to show coordinate frame + coordinate_frame_size: Size of the coordinate frame + """ + if not clustered_pcds: + print("Warning: No clustered point clouds to visualize") + return + + # Apply standard coordinate transformation + transform = get_standard_coordinate_transform() + geometries = [] + for pcd in clustered_pcds: + pcd_copy = o3d.geometry.PointCloud(pcd) + pcd_copy.transform(transform) + geometries.append(pcd_copy) + + # Add coordinate frame + if show_coordinate_frame: + coordinate_frame = o3d.geometry.TriangleMesh.create_coordinate_frame( + size=coordinate_frame_size + ) + coordinate_frame.transform(transform) + geometries.append(coordinate_frame) + + total_points = sum(len(np.asarray(pcd.points)) for pcd in clustered_pcds) + print(f"Visualizing {len(clustered_pcds)} clusters with {total_points} total points") + + try: + vis = o3d.visualization.Visualizer() + vis.create_window(window_name=window_name, width=1280, height=720) + for geom in geometries: + vis.add_geometry(geom) + render_option = vis.get_render_option() + render_option.point_size = point_size + vis.run() + vis.destroy_window() + except Exception as e: + print(f"Failed to create interactive visualization: {e}") + o3d.visualization.draw_geometries( + geometries, window_name=window_name, width=1280, height=720 + ) + + +def visualize_pcd( + pcd: o3d.geometry.PointCloud, + window_name: str = "Point Cloud Visualization", + point_size: float = 1.0, + show_coordinate_frame: bool = True, + coordinate_frame_size: float = 0.1, +) -> None: + """ + Visualize an Open3D point cloud using Open3D's visualization window. + + Args: + pcd: Open3D point cloud to visualize + window_name: Name of the visualization window + point_size: Size of points in the visualization + show_coordinate_frame: Whether to show coordinate frame + coordinate_frame_size: Size of the coordinate frame + """ + if pcd is None: + print("Warning: Point cloud is None, nothing to visualize") + return + + if len(np.asarray(pcd.points)) == 0: + print("Warning: Point cloud is empty, nothing to visualize") + return + + # Apply standard coordinate transformation + transform = get_standard_coordinate_transform() + pcd_copy = o3d.geometry.PointCloud(pcd) + pcd_copy.transform(transform) + geometries = [pcd_copy] + + # Add coordinate frame + if show_coordinate_frame: + coordinate_frame = o3d.geometry.TriangleMesh.create_coordinate_frame( + size=coordinate_frame_size + ) + coordinate_frame.transform(transform) + geometries.append(coordinate_frame) + + print(f"Visualizing point cloud with {len(np.asarray(pcd.points))} points") + + try: + vis = o3d.visualization.Visualizer() + vis.create_window(window_name=window_name, width=1280, height=720) + for geom in geometries: + vis.add_geometry(geom) + render_option = vis.get_render_option() + render_option.point_size = point_size + vis.run() + vis.destroy_window() + except Exception as e: + print(f"Failed to create interactive visualization: {e}") + o3d.visualization.draw_geometries( + geometries, window_name=window_name, width=1280, height=720 + ) + + +def visualize_voxel_grid( + voxel_grid: o3d.geometry.VoxelGrid, + window_name: str = "Voxel Grid Visualization", + show_coordinate_frame: bool = True, + coordinate_frame_size: float = 0.1, +) -> None: + """ + Visualize an Open3D voxel grid using Open3D's visualization window. + + Args: + voxel_grid: Open3D voxel grid to visualize + window_name: Name of the visualization window + show_coordinate_frame: Whether to show coordinate frame + coordinate_frame_size: Size of the coordinate frame + """ + if voxel_grid is None: + print("Warning: Voxel grid is None, nothing to visualize") + return + + if len(voxel_grid.get_voxels()) == 0: + print("Warning: Voxel grid is empty, nothing to visualize") + return + + # VoxelGrid doesn't support transform, so we need to transform the source points instead + # For now, just visualize as-is with transformed coordinate frame + geometries = [voxel_grid] + + # Add coordinate frame + if show_coordinate_frame: + coordinate_frame = o3d.geometry.TriangleMesh.create_coordinate_frame( + size=coordinate_frame_size + ) + coordinate_frame.transform(get_standard_coordinate_transform()) + geometries.append(coordinate_frame) + + print(f"Visualizing voxel grid with {len(voxel_grid.get_voxels())} voxels") + + try: + vis = o3d.visualization.Visualizer() + vis.create_window(window_name=window_name, width=1280, height=720) + for geom in geometries: + vis.add_geometry(geom) + vis.run() + vis.destroy_window() + except Exception as e: + print(f"Failed to create interactive visualization: {e}") + o3d.visualization.draw_geometries( + geometries, window_name=window_name, width=1280, height=720 + ) + + +def combine_object_pointclouds( + point_clouds: list[np.ndarray] | list[o3d.geometry.PointCloud], + colors: list[np.ndarray] | None = None, +) -> o3d.geometry.PointCloud: + """ + Combine multiple point clouds into a single Open3D point cloud. + + Args: + point_clouds: List of point clouds as numpy arrays or Open3D point clouds + colors: List of colors as numpy arrays + Returns: + Combined Open3D point cloud + """ + all_points = [] + all_colors = [] + + for i, pcd in enumerate(point_clouds): + if isinstance(pcd, np.ndarray): + points = pcd[:, :3] + all_points.append(points) + if colors: + all_colors.append(colors[i]) + + elif isinstance(pcd, o3d.geometry.PointCloud): + points = np.asarray(pcd.points) + all_points.append(points) + if pcd.has_colors(): + colors = np.asarray(pcd.colors) + all_colors.append(colors) + + if not all_points: + return o3d.geometry.PointCloud() + + combined_pcd = o3d.geometry.PointCloud() + combined_pcd.points = o3d.utility.Vector3dVector(np.vstack(all_points)) + + if all_colors: + combined_pcd.colors = o3d.utility.Vector3dVector(np.vstack(all_colors)) + + return combined_pcd + + +def extract_centroids_from_masks( + rgb_image: np.ndarray, + depth_image: np.ndarray, + masks: list[np.ndarray], + camera_intrinsics: list[float] | np.ndarray, +) -> list[dict[str, Any]]: + """ + Extract 3D centroids and orientations from segmentation masks. + + Args: + rgb_image: RGB image (H, W, 3) + depth_image: Depth image (H, W) in meters + masks: List of boolean masks (H, W) + camera_intrinsics: Camera parameters as [fx, fy, cx, cy] or 3x3 matrix + + Returns: + List of dictionaries containing: + - centroid: 3D centroid position [x, y, z] in camera frame + - orientation: Normalized direction vector from camera to centroid + - num_points: Number of valid 3D points + - mask_idx: Index of the mask in the input list + """ + # Extract camera parameters + if isinstance(camera_intrinsics, list) and len(camera_intrinsics) == 4: + fx, fy, cx, cy = camera_intrinsics + else: + fx = camera_intrinsics[0, 0] + fy = camera_intrinsics[1, 1] + cx = camera_intrinsics[0, 2] + cy = camera_intrinsics[1, 2] + + results = [] + + for mask_idx, mask in enumerate(masks): + if mask is None or mask.sum() == 0: + continue + + # Get pixel coordinates where mask is True + y_coords, x_coords = np.where(mask) + + # Get depth values at mask locations + depths = depth_image[y_coords, x_coords] + + # Convert to 3D points in camera frame + X = (x_coords - cx) * depths / fx + Y = (y_coords - cy) * depths / fy + Z = depths + + # Calculate centroid + centroid_x = np.mean(X) + centroid_y = np.mean(Y) + centroid_z = np.mean(Z) + centroid = np.array([centroid_x, centroid_y, centroid_z]) + + # Calculate orientation as normalized direction from camera origin to centroid + # Camera origin is at (0, 0, 0) + orientation = centroid / np.linalg.norm(centroid) + + results.append( + { + "centroid": centroid, + "orientation": orientation, + "num_points": int(mask.sum()), + "mask_idx": mask_idx, + } + ) + + return results diff --git a/dimos/perception/segmentation/__init__.py b/dimos/perception/segmentation/__init__.py index a8f9a291ce..a48a76d6a4 100644 --- a/dimos/perception/segmentation/__init__.py +++ b/dimos/perception/segmentation/__init__.py @@ -1,2 +1,2 @@ -from .utils import * from .sam_2d_seg import * +from .utils import * diff --git a/dimos/perception/segmentation/image_analyzer.py b/dimos/perception/segmentation/image_analyzer.py index 5371f178ae..074ee7d605 100644 --- a/dimos/perception/segmentation/image_analyzer.py +++ b/dimos/perception/segmentation/image_analyzer.py @@ -1,25 +1,40 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import base64 -import requests -from openai import OpenAI -import cv2 -import numpy as np import os +import cv2 +from openai import OpenAI + NORMAL_PROMPT = "What are in these images? Give a short word answer with at most two words, \ if not sure, give a description of its shape or color like 'small tube', 'blue item'. \" \ if does not look like an object, say 'unknown'. Export objects as a list of strings \ in this exact format '['object 1', 'object 2', '...']'." -RICH_PROMPT = "What are in these images? Give a detailed description of each item, the first n images will be \ +RICH_PROMPT = ( + "What are in these images? Give a detailed description of each item, the first n images will be \ cropped patches of the original image detected by the object detection model. \ The last image will be the original image. Use the last image only for context, \ do not describe objects in the last image. \ Export the objects as a list of strings in this exact format, '['description of object 1', '...', '...']', \ don't include anything else. " +) class ImageAnalyzer: - def __init__(self): + def __init__(self) -> None: """ Initializes the ImageAnalyzer with OpenAI API credentials. """ @@ -38,7 +53,7 @@ def encode_image(self, image): _, buffer = cv2.imencode(".jpg", image) return base64.b64encode(buffer).decode("utf-8") - def analyze_images(self, images, detail="auto", prompt_type="normal"): + def analyze_images(self, images, detail: str = "auto", prompt_type: str = "normal"): """ Takes a list of cropped images and returns descriptions from OpenAI's Vision model. @@ -53,7 +68,10 @@ def analyze_images(self, images, detail="auto", prompt_type="normal"): image_data = [ { "type": "image_url", - "image_url": {"url": f"data:image/jpeg;base64,{self.encode_image(img)}", "detail": detail}, + "image_url": { + "url": f"data:image/jpeg;base64,{self.encode_image(img)}", + "detail": detail, + }, } for img in images ] @@ -70,7 +88,7 @@ def analyze_images(self, images, detail="auto", prompt_type="normal"): messages=[ { "role": "user", - "content": [{"type": "text", "text": prompt}] + image_data, + "content": [{"type": "text", "text": prompt}, *image_data], } ], max_tokens=300, @@ -78,16 +96,16 @@ def analyze_images(self, images, detail="auto", prompt_type="normal"): ) # Accessing the content of the response using dot notation - return [choice.message.content for choice in response.choices][0] + return next(choice.message.content for choice in response.choices) -def main(): +def main() -> None: # Define the directory containing cropped images cropped_images_dir = "cropped_images" if not os.path.exists(cropped_images_dir): print(f"Directory '{cropped_images_dir}' does not exist.") return - + # Load all images from the directory images = [] for filename in os.listdir(cropped_images_dir): @@ -98,45 +116,47 @@ def main(): images.append(image) else: print(f"Warning: Could not read image {image_path}") - + if not images: print("No valid images found in the directory.") return - + # Initialize ImageAnalyzer analyzer = ImageAnalyzer() - + # Analyze images results = analyzer.analyze_images(images) - + # Split results into a list of items - object_list = [item.strip()[2:] for item in results.split('\n')] + object_list = [item.strip()[2:] for item in results.split("\n")] # Overlay text on images and display them - for i, (img, obj) in enumerate(zip(images, object_list)): + for i, (img, obj) in enumerate(zip(images, object_list, strict=False)): if obj: # Only process non-empty lines # Add text to image font = cv2.FONT_HERSHEY_SIMPLEX font_scale = 0.5 thickness = 2 text = obj.strip() - + # Get text size (text_width, text_height), _ = cv2.getTextSize(text, font, font_scale, thickness) - + # Position text at top of image x = 10 y = text_height + 10 - + # Add white background for text - cv2.rectangle(img, (x-5, y-text_height-5), (x+text_width+5, y+5), (255,255,255), -1) + cv2.rectangle( + img, (x - 5, y - text_height - 5), (x + text_width + 5, y + 5), (255, 255, 255), -1 + ) # Add text - cv2.putText(img, text, (x, y), font, font_scale, (0,0,0), thickness) - + cv2.putText(img, text, (x, y), font, font_scale, (0, 0, 0), thickness) + # Save or display the image cv2.imwrite(f"annotated_image_{i}.jpg", img) print(f"Detected object: {obj}") if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/dimos/perception/segmentation/sam_2d_seg.py b/dimos/perception/segmentation/sam_2d_seg.py index 9a0437f910..b13ebc4c65 100644 --- a/dimos/perception/segmentation/sam_2d_seg.py +++ b/dimos/perception/segmentation/sam_2d_seg.py @@ -1,30 +1,70 @@ -import cv2 -import numpy as np +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections import deque +from collections.abc import Sequence +from concurrent.futures import ThreadPoolExecutor +import os import time + +import cv2 +import onnxruntime from ultralytics import FastSAM -from dimos.perception.segmentation.utils import extract_masks_bboxes_probs_names, \ - filter_segmentation_results, \ - plot_results, \ - crop_images_from_bboxes -from dimos.perception.common.detection2d_tracker import target2dTracker, get_tracked_results + +from dimos.perception.common.detection2d_tracker import get_tracked_results, target2dTracker from dimos.perception.segmentation.image_analyzer import ImageAnalyzer -import os -from collections import deque -from concurrent.futures import ThreadPoolExecutor +from dimos.perception.segmentation.utils import ( + crop_images_from_bboxes, + extract_masks_bboxes_probs_names, + filter_segmentation_results, + plot_results, +) +from dimos.utils.data import get_data +from dimos.utils.gpu_utils import is_cuda_available +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.perception.segmentation.sam_2d_seg") + class Sam2DSegmenter: - def __init__(self, model_path="FastSAM-s.pt", device="cuda", - min_analysis_interval=5.0, use_tracker=True, use_analyzer=True, - use_rich_labeling=False): + def __init__( + self, + model_path: str = "models_fastsam", + model_name: str = "FastSAM-s.onnx", + min_analysis_interval: float = 5.0, + use_tracker: bool = True, + use_analyzer: bool = True, + use_rich_labeling: bool = False, + use_filtering: bool = True, + ) -> None: + if is_cuda_available(): + logger.info("Using CUDA for SAM 2d segmenter") + if hasattr(onnxruntime, "preload_dlls"): # Handles CUDA 11 / onnxruntime-gpu<=1.18 + onnxruntime.preload_dlls(cuda=True, cudnn=True) + self.device = "cuda" + else: + logger.info("Using CPU for SAM 2d segmenter") + self.device = "cpu" # Core components - self.device = device - self.model = FastSAM(model_path) + self.model = FastSAM(get_data(model_path) / model_name) self.use_tracker = use_tracker self.use_analyzer = use_analyzer self.use_rich_labeling = use_rich_labeling + self.use_filtering = use_filtering module_dir = os.path.dirname(__file__) - self.tracker_config = os.path.join(module_dir, 'config', 'custom_tracker.yaml') + self.tracker_config = os.path.join(module_dir, "config", "custom_tracker.yaml") # Initialize tracker if enabled if self.use_tracker: @@ -38,9 +78,9 @@ def __init__(self, model_path="FastSAM-s.pt", device="cuda", max_area_ratio=0.4, texture_range=(0.0, 0.35), border_safe_distance=100, - weights={"prob": 1.0, "temporal": 3.0, "texture": 2.0, "border": 3.0, "size": 1.0} + weights={"prob": 1.0, "temporal": 3.0, "texture": 2.0, "border": 3.0, "size": 1.0}, ) - + # Initialize analyzer components if enabled if self.use_analyzer: self.image_analyzer = ImageAnalyzer() @@ -58,21 +98,39 @@ def process_image(self, image): source=image, device=self.device, retina_masks=True, - conf=0.6, - iou=0.9, + conf=0.3, + iou=0.5, persist=True, verbose=False, - tracker=self.tracker_config, ) if len(results) > 0: # Get initial segmentation results - masks, bboxes, track_ids, probs, names, areas = extract_masks_bboxes_probs_names(results[0]) - + masks, bboxes, track_ids, probs, names, areas = extract_masks_bboxes_probs_names( + results[0] + ) + # Filter results - filtered_masks, filtered_bboxes, filtered_track_ids, filtered_probs, filtered_names, filtered_texture_values = \ - filter_segmentation_results(image, masks, bboxes, track_ids, probs, names, areas) - + if self.use_filtering: + ( + filtered_masks, + filtered_bboxes, + filtered_track_ids, + filtered_probs, + filtered_names, + filtered_texture_values, + ) = filter_segmentation_results( + image, masks, bboxes, track_ids, probs, names, areas + ) + else: + # Use original results without filtering + filtered_masks = masks + filtered_bboxes = bboxes + filtered_track_ids = track_ids + filtered_probs = probs + filtered_names = names + filtered_texture_values = [] + if self.use_tracker: # Update tracker with filtered results tracked_targets = self.tracker.update( @@ -84,37 +142,73 @@ def process_image(self, image): filtered_names, filtered_texture_values, ) - + # Get tracked results - tracked_masks, tracked_bboxes, tracked_target_ids, tracked_probs, tracked_names = get_tracked_results(tracked_targets) - + tracked_masks, tracked_bboxes, tracked_target_ids, tracked_probs, tracked_names = ( + get_tracked_results(tracked_targets) + ) + if self.use_analyzer: # Update analysis queue with tracked IDs target_id_set = set(tracked_target_ids) # Remove untracked objects from object_names all_target_ids = list(self.tracker.targets.keys()) - self.object_names = {track_id: name for track_id, name in self.object_names.items() - if track_id in all_target_ids} - + self.object_names = { + track_id: name + for track_id, name in self.object_names.items() + if track_id in all_target_ids + } + # Remove untracked objects from queue and results - self.to_be_analyzed = deque([track_id for track_id in self.to_be_analyzed - if track_id in target_id_set]) - + self.to_be_analyzed = deque( + [track_id for track_id in self.to_be_analyzed if track_id in target_id_set] + ) + # Filter out any IDs being analyzed from the to_be_analyzed queue if self.current_queue_ids: - self.to_be_analyzed = deque([tid for tid in self.to_be_analyzed - if tid not in self.current_queue_ids]) - + self.to_be_analyzed = deque( + [ + tid + for tid in self.to_be_analyzed + if tid not in self.current_queue_ids + ] + ) + # Add new track_ids to analysis queue for track_id in tracked_target_ids: - if track_id not in self.object_names and track_id not in self.to_be_analyzed: + if ( + track_id not in self.object_names + and track_id not in self.to_be_analyzed + ): self.to_be_analyzed.append(track_id) - - return tracked_masks, tracked_bboxes, tracked_target_ids, tracked_probs, tracked_names + + return ( + tracked_masks, + tracked_bboxes, + tracked_target_ids, + tracked_probs, + tracked_names, + ) else: - # Return filtered results directly if tracker is disabled - return filtered_masks, filtered_bboxes, filtered_track_ids, filtered_probs, filtered_names + # When tracker disabled, just use the filtered results directly + if self.use_analyzer: + # Add unanalyzed IDs to the analysis queue + for track_id in filtered_track_ids: + if ( + track_id not in self.object_names + and track_id not in self.to_be_analyzed + ): + self.to_be_analyzed.append(track_id) + + # Simply return filtered results + return ( + filtered_masks, + filtered_bboxes, + filtered_track_ids, + filtered_probs, + filtered_names, + ) return [], [], [], [], [] def check_analysis_status(self, tracked_target_ids): @@ -123,7 +217,7 @@ def check_analysis_status(self, tracked_target_ids): return None, None current_time = time.time() - + # Check if current queue analysis is complete if self.current_future and self.current_future.done(): try: @@ -131,7 +225,7 @@ def check_analysis_status(self, tracked_target_ids): if results is not None: # Map results to track IDs object_list = eval(results) - for track_id, result in zip(self.current_queue_ids, object_list): + for track_id, result in zip(self.current_queue_ids, object_list, strict=False): self.object_names[track_id] = result except Exception as e: print(f"Queue analysis failed: {e}") @@ -140,12 +234,14 @@ def check_analysis_status(self, tracked_target_ids): self.last_analysis_time = current_time # If enough time has passed and we have items to analyze, start new analysis - if (not self.current_future and self.to_be_analyzed and - current_time - self.last_analysis_time >= self.min_analysis_interval): - + if ( + not self.current_future + and self.to_be_analyzed + and current_time - self.last_analysis_time >= self.min_analysis_interval + ): queue_indices = [] queue_ids = [] - + # Collect all valid track IDs from the queue while self.to_be_analyzed: track_id = self.to_be_analyzed[0] @@ -154,12 +250,12 @@ def check_analysis_status(self, tracked_target_ids): queue_indices.append(bbox_idx) queue_ids.append(track_id) self.to_be_analyzed.popleft() - + if queue_indices: return queue_indices, queue_ids return None, None - def run_analysis(self, frame, tracked_bboxes, tracked_target_ids): + def run_analysis(self, frame, tracked_bboxes, tracked_target_ids) -> None: """Run queue image analysis in background.""" if not self.use_analyzer: return @@ -177,79 +273,77 @@ def run_analysis(self, frame, tracked_bboxes, tracked_target_ids): cropped_images.append(frame) else: prompt_type = "normal" - + self.current_future = self.analysis_executor.submit( - self.image_analyzer.analyze_images, - cropped_images, - prompt_type=prompt_type + self.image_analyzer.analyze_images, cropped_images, prompt_type=prompt_type ) - def get_object_names(self, track_ids, tracked_names): + def get_object_names(self, track_ids, tracked_names: Sequence[str]): """Get object names for the given track IDs, falling back to tracked names.""" if not self.use_analyzer: return tracked_names - - return [self.object_names.get(track_id, tracked_name) - for track_id, tracked_name in zip(track_ids, tracked_names)] - def visualize_results(self, image, masks, bboxes, track_ids, probs, names): + return [ + self.object_names.get(track_id, tracked_name) + for track_id, tracked_name in zip(track_ids, tracked_names, strict=False) + ] + + def visualize_results( + self, image, masks, bboxes, track_ids, probs: Sequence[float], names: Sequence[str] + ): """Generate an overlay visualization with segmentation results and object names.""" return plot_results(image, masks, bboxes, track_ids, probs, names) - def cleanup(self): + def cleanup(self) -> None: """Cleanup resources.""" if self.use_analyzer: self.analysis_executor.shutdown() -def main(): +def main() -> None: # Example usage with different configurations cap = cv2.VideoCapture(0) - + # Example 1: Full functionality with rich labeling segmenter = Sam2DSegmenter( - min_analysis_interval=4.0, - use_tracker=True, + min_analysis_interval=4.0, + use_tracker=True, use_analyzer=True, - use_rich_labeling=True # Enable rich labeling + use_rich_labeling=True, # Enable rich labeling ) - + # Example 2: Full functionality with normal labeling # segmenter = Sam2DSegmenter(min_analysis_interval=4.0, use_tracker=True, use_analyzer=True) - + # Example 3: Tracker only (analyzer disabled) # segmenter = Sam2DSegmenter(use_analyzer=False) - + # Example 4: Basic segmentation only (both tracker and analyzer disabled) # segmenter = Sam2DSegmenter(use_tracker=False, use_analyzer=False) + # Example 5: Analyzer without tracker (new capability) + # segmenter = Sam2DSegmenter(use_tracker=False, use_analyzer=True) + try: while cap.isOpened(): ret, frame = cap.read() if not ret: break - start_time = time.time() - + time.time() + # Process image and get results masks, bboxes, target_ids, probs, names = segmenter.process_image(frame) - + # Run analysis if enabled - if segmenter.use_tracker and segmenter.use_analyzer: + if segmenter.use_analyzer: segmenter.run_analysis(frame, bboxes, target_ids) names = segmenter.get_object_names(target_ids, names) - #processing_time = time.time() - start_time - #print(f"Processing time: {processing_time:.2f}s") + # processing_time = time.time() - start_time + # print(f"Processing time: {processing_time:.2f}s") - overlay = segmenter.visualize_results( - frame, - masks, - bboxes, - target_ids, - probs, - names - ) + overlay = segmenter.visualize_results(frame, masks, bboxes, target_ids, probs, names) cv2.imshow("Segmentation", overlay) key = cv2.waitKey(1) diff --git a/dimos/perception/segmentation/test_sam_2d_seg.py b/dimos/perception/segmentation/test_sam_2d_seg.py new file mode 100644 index 0000000000..23eaf02fa3 --- /dev/null +++ b/dimos/perception/segmentation/test_sam_2d_seg.py @@ -0,0 +1,210 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import time + +import numpy as np +import pytest +from reactivex import operators as ops + +from dimos.perception.segmentation.sam_2d_seg import Sam2DSegmenter +from dimos.perception.segmentation.utils import extract_masks_bboxes_probs_names +from dimos.stream.video_provider import VideoProvider + + +@pytest.mark.heavy +class TestSam2DSegmenter: + def test_sam_segmenter_initialization(self) -> None: + """Test FastSAM segmenter initializes correctly with default model path.""" + try: + # Try to initialize with the default model path and existing device setting + segmenter = Sam2DSegmenter(use_analyzer=False) + assert segmenter is not None + assert segmenter.model is not None + except Exception as e: + # If the model file doesn't exist, the test should still pass with a warning + pytest.skip(f"Skipping test due to model initialization error: {e}") + + def test_sam_segmenter_process_image(self) -> None: + """Test FastSAM segmenter can process video frames and return segmentation masks.""" + # Import get data inside method to avoid pytest fixture confusion + from dimos.utils.data import get_data + + # Get test video path directly + video_path = get_data("assets") / "trimmed_video_office.mov" + try: + # Initialize segmenter without analyzer for faster testing + segmenter = Sam2DSegmenter(use_analyzer=False) + + # Note: conf and iou are parameters for process_image, not constructor + # We'll monkey patch the process_image method to use lower thresholds + + def patched_process_image(image): + results = segmenter.model.track( + source=image, + device=segmenter.device, + retina_masks=True, + conf=0.1, # Lower confidence threshold for testing + iou=0.5, # Lower IoU threshold + persist=True, + verbose=False, + tracker=segmenter.tracker_config + if hasattr(segmenter, "tracker_config") + else None, + ) + + if len(results) > 0: + masks, bboxes, track_ids, probs, names, _areas = ( + extract_masks_bboxes_probs_names(results[0]) + ) + return masks, bboxes, track_ids, probs, names + return [], [], [], [], [] + + # Replace the method + segmenter.process_image = patched_process_image + + # Create video provider and directly get a video stream observable + assert os.path.exists(video_path), f"Test video not found: {video_path}" + video_provider = VideoProvider(dev_name="test_video", video_source=video_path) + + video_stream = video_provider.capture_video_as_observable(realtime=False, fps=1) + + # Use ReactiveX operators to process the stream + def process_frame(frame): + try: + # Process frame with FastSAM + masks, bboxes, track_ids, probs, names = segmenter.process_image(frame) + print( + f"SAM results - masks: {len(masks)}, bboxes: {len(bboxes)}, track_ids: {len(track_ids)}, names: {len(names)}" + ) + + return { + "frame": frame, + "masks": masks, + "bboxes": bboxes, + "track_ids": track_ids, + "probs": probs, + "names": names, + } + except Exception as e: + print(f"Error in process_frame: {e}") + return {} + + # Create the segmentation stream using pipe and map operator + segmentation_stream = video_stream.pipe(ops.map(process_frame)) + + # Collect results from the stream + results = [] + frames_processed = 0 + target_frames = 5 + + def on_next(result) -> None: + nonlocal frames_processed, results + if not result: + return + + results.append(result) + frames_processed += 1 + + # Stop processing after target frames + if frames_processed >= target_frames: + subscription.dispose() + + def on_error(error) -> None: + pytest.fail(f"Error in segmentation stream: {error}") + + def on_completed() -> None: + pass + + # Subscribe and wait for results + subscription = segmentation_stream.subscribe( + on_next=on_next, on_error=on_error, on_completed=on_completed + ) + + # Wait for frames to be processed + timeout = 30.0 # seconds + start_time = time.time() + while frames_processed < target_frames and time.time() - start_time < timeout: + time.sleep(0.5) + + # Clean up subscription + subscription.dispose() + video_provider.dispose_all() + + # Check if we have results + if len(results) == 0: + pytest.skip( + "No segmentation results found, but test connection established correctly" + ) + return + + print(f"Processed {len(results)} frames with segmentation results") + + # Analyze the first result + result = results[0] + + # Check that we have a frame + assert "frame" in result, "Result doesn't contain a frame" + assert isinstance(result["frame"], np.ndarray), "Frame is not a numpy array" + + # Check that segmentation results are valid + assert isinstance(result["masks"], list) + assert isinstance(result["bboxes"], list) + assert isinstance(result["track_ids"], list) + assert isinstance(result["probs"], list) + assert isinstance(result["names"], list) + + # All result lists should be the same length + assert ( + len(result["masks"]) + == len(result["bboxes"]) + == len(result["track_ids"]) + == len(result["probs"]) + == len(result["names"]) + ) + + # If we have masks, check that they have valid shape + if result.get("masks") and len(result["masks"]) > 0: + assert result["masks"][0].shape == ( + result["frame"].shape[0], + result["frame"].shape[1], + ), "Mask shape should match image dimensions" + print(f"Found {len(result['masks'])} masks in first frame") + else: + print("No masks found in first frame, but test connection established correctly") + + # Test visualization function + if result["masks"]: + vis_frame = segmenter.visualize_results( + result["frame"], + result["masks"], + result["bboxes"], + result["track_ids"], + result["probs"], + result["names"], + ) + assert isinstance(vis_frame, np.ndarray), "Visualization output should be an image" + assert vis_frame.shape == result["frame"].shape, ( + "Visualization should have same dimensions as input frame" + ) + + # We've already tested visualization above, so no need for a duplicate test + + except Exception as e: + pytest.skip(f"Skipping test due to error: {e}") + + +if __name__ == "__main__": + pytest.main(["-v", __file__]) diff --git a/dimos/perception/segmentation/utils.py b/dimos/perception/segmentation/utils.py index cd9544928c..24d6ce4bf2 100644 --- a/dimos/perception/segmentation/utils.py +++ b/dimos/perception/segmentation/utils.py @@ -1,11 +1,28 @@ -import numpy as np +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Sequence + import cv2 +import numpy as np import torch -import time class SimpleTracker: - def __init__(self, history_size=100, min_count=10, count_window=20): + def __init__( + self, history_size: int = 100, min_count: int = 10, count_window: int = 20 + ) -> None: """ Simple temporal tracker that counts appearances in a fixed window. :param history_size: Number of past frames to remember @@ -25,34 +42,34 @@ def update(self, track_ids): self.history.pop(0) # Consider only the latest `count_window` frames for counting - recent_history = self.history[-self.count_window:] + recent_history = self.history[-self.count_window :] all_tracks = np.concatenate(recent_history) if recent_history else np.array([]) - + # Compute occurrences efficiently using numpy unique_ids, counts = np.unique(all_tracks, return_counts=True) - id_counts = dict(zip(unique_ids, counts)) - + id_counts = dict(zip(unique_ids, counts, strict=False)) + # Update total counts but ensure it only contains IDs within the history size total_tracked_ids = np.concatenate(self.history) if self.history else np.array([]) unique_total_ids, total_counts = np.unique(total_tracked_ids, return_counts=True) - self.total_counts = dict(zip(unique_total_ids, total_counts)) - + self.total_counts = dict(zip(unique_total_ids, total_counts, strict=False)) + # Return IDs that appear often enough return [track_id for track_id, count in id_counts.items() if count >= self.min_count] - + def get_total_counts(self): """Returns the total count of each tracking ID seen over time, limited to history size.""" return self.total_counts -def extract_masks_bboxes_probs_names(result, max_size=0.7): +def extract_masks_bboxes_probs_names(result, max_size: float = 0.7): """ Extracts masks, bounding boxes, probabilities, and class names from one Ultralytics result object. - + Parameters: result: Ultralytics result object max_size: float, maximum allowed size of object relative to image (0-1) - + Returns: tuple: (masks, bboxes, track_ids, probs, names, areas) """ @@ -65,20 +82,20 @@ def extract_masks_bboxes_probs_names(result, max_size=0.7): if result.masks is None: return masks, bboxes, track_ids, probs, names, areas - + total_area = result.masks.orig_shape[0] * result.masks.orig_shape[1] - for box, mask_data in zip(result.boxes, result.masks.data): + for box, mask_data in zip(result.boxes, result.masks.data, strict=False): mask_numpy = mask_data # Extract bounding box x1, y1, x2, y2 = box.xyxy[0].tolist() - + # Extract track_id if available track_id = -1 # default if no tracking - if hasattr(box, 'id') and box.id is not None: + if hasattr(box, "id") and box.id is not None: track_id = int(box.id[0].item()) - + # Extract probability and class index conf = float(box.conf[0]) cls_idx = int(box.cls[0]) @@ -96,15 +113,16 @@ def extract_masks_bboxes_probs_names(result, max_size=0.7): return masks, bboxes, track_ids, probs, names, areas -def compute_texture_map(frame, blur_size=3): + +def compute_texture_map(frame, blur_size: int = 3): """ Compute texture map using gradient statistics. Returns high values for textured regions and low values for smooth regions. - + Parameters: frame: BGR image blur_size: Size of Gaussian blur kernel for pre-processing - + Returns: numpy array: Texture map with values normalized to [0,1] """ @@ -113,32 +131,42 @@ def compute_texture_map(frame, blur_size=3): gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) else: gray = frame - + # Pre-process with slight blur to reduce noise if blur_size > 0: gray = cv2.GaussianBlur(gray, (blur_size, blur_size), 0) - + # Compute gradients in x and y directions grad_x = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=3) grad_y = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=3) - + # Compute gradient magnitude and direction magnitude = np.sqrt(grad_x**2 + grad_y**2) - + # Compute local standard deviation of gradient magnitude texture_map = cv2.GaussianBlur(magnitude, (15, 15), 0) - + # Normalize to [0,1] texture_map = (texture_map - texture_map.min()) / (texture_map.max() - texture_map.min() + 1e-8) - + return texture_map -def filter_segmentation_results(frame, masks, bboxes, track_ids, probs, names, areas, texture_threshold=0.07, size_filter=800): +def filter_segmentation_results( + frame, + masks, + bboxes, + track_ids, + probs: Sequence[float], + names: Sequence[str], + areas, + texture_threshold: float = 0.07, + size_filter: int = 800, +): """ Filters segmentation results using both overlap and saliency detection. Uses mask_sum tensor for efficient overlap detection. - + Parameters: masks: list of torch.Tensor containing mask data bboxes: list of bounding boxes [x1, y1, x2, y2] @@ -149,38 +177,42 @@ def filter_segmentation_results(frame, masks, bboxes, track_ids, probs, names, a frame: BGR image for computing saliency texture_threshold: Average texture value required for mask to be kept size_filter: Minimum size of the object to be kept - + Returns: tuple: (filtered_masks, filtered_bboxes, filtered_track_ids, filtered_probs, filtered_names, filtered_texture_values, texture_map) """ if len(masks) <= 1: return masks, bboxes, track_ids, probs, names, [] - + # Compute texture map once and convert to tensor texture_map = compute_texture_map(frame) - + # Sort by area (smallest to largest) sorted_indices = torch.tensor(areas).argsort(descending=False) device = masks[0].device # Get the device of the first mask - + # Create mask_sum tensor where each pixel stores the index of the mask that claims it mask_sum = torch.zeros_like(masks[0], dtype=torch.int32) - - texture_map = torch.from_numpy(texture_map).to(device) # Convert texture_map to tensor and move to device - + + texture_map = torch.from_numpy(texture_map).to( + device + ) # Convert texture_map to tensor and move to device + filtered_texture_values = [] # List to store texture values of filtered masks - + for i, idx in enumerate(sorted_indices): mask = masks[idx] # Compute average texture value within mask texture_value = torch.mean(texture_map[mask > 0]) if torch.any(mask > 0) else 0 - + # Only claim pixels if mask passes texture threshold if texture_value >= texture_threshold: mask_sum[mask > 0] = i - filtered_texture_values.append(texture_value.item()) # Store the texture value as a Python float - + filtered_texture_values.append( + texture_value.item() + ) # Store the texture value as a Python float + # Get indices that appear in mask_sum (these are the masks we want to keep) keep_indices, counts = torch.unique(mask_sum[mask_sum > 0], return_counts=True) size_indices = counts > size_filter @@ -188,20 +220,35 @@ def filter_segmentation_results(frame, masks, bboxes, track_ids, probs, names, a sorted_indices = sorted_indices.cpu() keep_indices = keep_indices.cpu() - + # Map back to original indices and filter final_indices = sorted_indices[keep_indices].tolist() - + filtered_masks = [masks[i] for i in final_indices] filtered_bboxes = [bboxes[i] for i in final_indices] filtered_track_ids = [track_ids[i] for i in final_indices] filtered_probs = [probs[i] for i in final_indices] filtered_names = [names[i] for i in final_indices] - return filtered_masks, filtered_bboxes, filtered_track_ids, filtered_probs, filtered_names, filtered_texture_values - - -def plot_results(image, masks, bboxes, track_ids, probs, names, alpha=0.5): + return ( + filtered_masks, + filtered_bboxes, + filtered_track_ids, + filtered_probs, + filtered_names, + filtered_texture_values, + ) + + +def plot_results( + image, + masks, + bboxes, + track_ids, + probs: Sequence[float], + names: Sequence[str], + alpha: float = 0.5, +): """ Draws bounding boxes, masks, and labels on the given image with enhanced visualization. Includes object names in the overlay and improved text visibility. @@ -209,13 +256,21 @@ def plot_results(image, masks, bboxes, track_ids, probs, names, alpha=0.5): h, w = image.shape[:2] overlay = image.copy() - for mask, bbox, track_id, prob, name in zip(masks, bboxes, track_ids, probs, names): + for mask, bbox, track_id, prob, name in zip( + masks, bboxes, track_ids, probs, names, strict=False + ): # Convert mask tensor to numpy if needed if isinstance(mask, torch.Tensor): mask = mask.cpu().numpy() + # Ensure mask is in proper format for OpenCV resize + if mask.dtype == bool: + mask = mask.astype(np.uint8) + elif mask.dtype != np.uint8 and mask.dtype != np.float32: + mask = mask.astype(np.float32) + mask_resized = cv2.resize(mask, (w, h), interpolation=cv2.INTER_LINEAR) - + # Generate consistent color based on track_id if track_id != -1: np.random.seed(track_id) @@ -223,7 +278,7 @@ def plot_results(image, masks, bboxes, track_ids, probs, names, alpha=0.5): np.random.seed(None) else: color = np.random.randint(0, 255, (3,), dtype=np.uint8) - + # Apply mask color overlay[mask_resized > 0.5] = color @@ -237,31 +292,20 @@ def plot_results(image, masks, bboxes, track_ids, probs, names, alpha=0.5): label += f" {name}" # Calculate text size for background rectangle - (text_w, text_h), _ = cv2.getTextSize( - label, - cv2.FONT_HERSHEY_SIMPLEX, - 0.5, - 1 - ) + (text_w, text_h), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1) # Draw background rectangle for text - cv2.rectangle( - overlay, - (x1, y1-text_h-8), - (x1+text_w+4, y1), - color.tolist(), - -1 - ) + cv2.rectangle(overlay, (x1, y1 - text_h - 8), (x1 + text_w + 4, y1), color.tolist(), -1) # Draw text with white color for better visibility cv2.putText( overlay, label, - (x1+2, y1-5), + (x1 + 2, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), # White text - 1 + 1, ) # Blend overlay with original image @@ -269,7 +313,7 @@ def plot_results(image, masks, bboxes, track_ids, probs, names, alpha=0.5): return result -def crop_images_from_bboxes(image, bboxes, buffer=0): +def crop_images_from_bboxes(image, bboxes, buffer: int = 0): """ Crops regions from an image based on bounding boxes with an optional buffer. @@ -293,7 +337,7 @@ def crop_images_from_bboxes(image, bboxes, buffer=0): x2 = min(width, x2 + buffer) y2 = min(height, y2 + buffer) - cropped_image = image[int(y1):int(y2), int(x1):int(x2)] + cropped_image = image[int(y1) : int(y2), int(x1) : int(x2)] cropped_images.append(cropped_image) return cropped_images diff --git a/dimos/perception/semantic_seg.py b/dimos/perception/semantic_seg.py deleted file mode 100644 index 4f5a1b5b80..0000000000 --- a/dimos/perception/semantic_seg.py +++ /dev/null @@ -1,230 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.perception.segmentation import Sam2DSegmenter -from dimos.models.depth.metric3d import Metric3D -from dimos.hardware.camera import Camera -from reactivex import Observable -from reactivex import operators as ops -from dimos.types.segmentation import SegmentationType -import numpy as np -import cv2 - - -class SemanticSegmentationStream: - def __init__( - self, - model_path: str = "FastSAM-s.pt", - device: str = "cuda", - enable_mono_depth: bool = True, - enable_rich_labeling: bool = True, - camera_params: dict = None, - gt_depth_scale=256.0 - ): - """ - Initialize a semantic segmentation stream using Sam2DSegmenter. - - Args: - model_path: Path to the FastSAM model file - device: Computation device ("cuda" or "cpu") - enable_mono_depth: Whether to enable monocular depth processing - enable_rich_labeling: Whether to enable rich labeling - camera_params: Dictionary containing either: - - Direct intrinsics: [fx, fy, cx, cy] - - Physical parameters: resolution, focal_length, sensor_size - """ - self.segmenter = Sam2DSegmenter( - model_path=model_path, - device=device, - min_analysis_interval=5.0, - use_tracker=True, - use_analyzer=True, - use_rich_labeling=enable_rich_labeling - ) - - self.enable_mono_depth = enable_mono_depth - if enable_mono_depth: - self.depth_model = Metric3D(gt_depth_scale) - - if camera_params: - # Check if direct intrinsics are provided - if 'intrinsics' in camera_params: - intrinsics = camera_params['intrinsics'] - if len(intrinsics) != 4: - raise ValueError("Intrinsics must be a list of 4 values: [fx, fy, cx, cy]") - self.depth_model.update_intrinsic(intrinsics) - else: - # Create camera object and calculate intrinsics from physical parameters - self.camera = Camera( - resolution=camera_params.get('resolution'), - focal_length=camera_params.get('focal_length'), - sensor_size=camera_params.get('sensor_size') - ) - intrinsics = self.camera.calculate_intrinsics() - self.depth_model.update_intrinsic([ - intrinsics['focal_length_x'], - intrinsics['focal_length_y'], - intrinsics['principal_point_x'], - intrinsics['principal_point_y'] - ]) - else: - raise ValueError("Camera parameters are required for monocular depth processing.") - - def create_stream(self, video_stream: Observable) -> Observable[SegmentationType]: - """ - Create an Observable stream of segmentation results from a video stream. - - Args: - video_stream: Observable that emits video frames - - Returns: - Observable that emits SegmentationType objects containing masks and metadata - """ - def process_frame(frame): - # Process image and get results - masks, bboxes, target_ids, probs, names = self.segmenter.process_image(frame) - - # Run analysis if enabled - if self.segmenter.use_analyzer: - self.segmenter.run_analysis(frame, bboxes, target_ids) - names = self.segmenter.get_object_names(target_ids, names) - - viz_frame = self.segmenter.visualize_results( - frame, - masks, - bboxes, - target_ids, - probs, - names - ) - - # Process depth if enabled - depth_viz = None - object_depths = [] - if self.enable_mono_depth: - # Get depth map - depth_map = self.depth_model.infer_depth(frame) - depth_map = np.array(depth_map) - - # Calculate average depth for each object - object_depths = [] - for mask in masks: - # Convert mask to numpy if needed - mask_np = mask.cpu().numpy() if hasattr(mask, 'cpu') else mask - # Get depth values where mask is True - object_depth = depth_map[mask_np > 0.5] - # Calculate average depth (in meters) - avg_depth = np.mean(object_depth) if len(object_depth) > 0 else 0 - object_depths.append(avg_depth/1000) - - # Create colorized depth visualization - depth_viz = self._create_depth_visualization(depth_map) - - # Overlay depth values on the visualization frame - for bbox, depth in zip(bboxes, object_depths): - x1, y1, x2, y2 = map(int, bbox) - # Draw depth text at bottom left of bounding box - depth_text = f"{depth:.2f}mm" - # Add black background for better visibility - text_size = cv2.getTextSize(depth_text, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 2)[0] - cv2.rectangle(viz_frame, - (x1, y2 - text_size[1] - 5), - (x1 + text_size[0], y2), - (0, 0, 0), -1) - # Draw text in white - cv2.putText(viz_frame, depth_text, - (x1, y2 - 5), - cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2) - - # Create metadata in the new requested format - objects = [] - for i in range(len(bboxes)): - obj_data = { - "object_id": target_ids[i] if i < len(target_ids) else None, - "bbox": bboxes[i], - "prob": probs[i] if i < len(probs) else None, - "label": names[i] if i < len(names) else None, - } - - # Add depth if available - if self.enable_mono_depth and i < len(object_depths): - obj_data["depth"] = object_depths[i] - - objects.append(obj_data) - - # Create the new metadata dictionary - metadata = { - "frame": frame, - "viz_frame": viz_frame, - "objects": objects - } - - # Add depth visualization if available - if depth_viz is not None: - metadata["depth_viz"] = depth_viz - - # Convert masks to numpy arrays if they aren't already - numpy_masks = [mask.cpu().numpy() if hasattr(mask, 'cpu') else mask for mask in masks] - - return SegmentationType(masks=numpy_masks, metadata=metadata) - - return video_stream.pipe( - ops.map(process_frame) - ) - - def _create_depth_visualization(self, depth_map): - """ - Create a colorized visualization of the depth map. - - Args: - depth_map: Raw depth map in meters - - Returns: - Colorized depth map visualization - """ - # Normalize depth map to 0-255 range for visualization - depth_min = np.min(depth_map) - depth_max = np.max(depth_map) - depth_normalized = ((depth_map - depth_min) / (depth_max - depth_min) * 255).astype(np.uint8) - - # Apply colormap (using JET colormap for better depth perception) - depth_colored = cv2.applyColorMap(depth_normalized, cv2.COLORMAP_JET) - - # Add depth scale bar - scale_height = 30 - scale_width = depth_map.shape[1] # Match width with depth map - scale_bar = np.zeros((scale_height, scale_width, 3), dtype=np.uint8) - - # Create gradient for scale bar - for i in range(scale_width): - color = cv2.applyColorMap(np.array([[i * 255 // scale_width]], dtype=np.uint8), cv2.COLORMAP_JET) - scale_bar[:, i] = color[0, 0] - - # Add depth values to scale bar - cv2.putText(scale_bar, f"{depth_min:.1f}mm", (5, 20), - cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1) - cv2.putText(scale_bar, f"{depth_max:.1f}mm", (scale_width-60, 20), - cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1) - - # Combine depth map and scale bar - combined_viz = np.vstack((depth_colored, scale_bar)) - - return combined_viz - - def cleanup(self): - """Clean up resources.""" - self.segmenter.cleanup() - if self.enable_mono_depth: - del self.depth_model - diff --git a/dimos/perception/spatial_perception.py b/dimos/perception/spatial_perception.py index c689b07d07..a11ccd615c 100644 --- a/dimos/perception/spatial_perception.py +++ b/dimos/perception/spatial_perception.py @@ -16,58 +16,75 @@ Spatial Memory module for creating a semantic map of the environment. """ -import logging -import uuid -import time +from datetime import datetime import os -import math -from typing import Dict, List, Tuple, Optional, Any, Union +import time +from typing import Any, Optional +import uuid -import numpy as np import cv2 -from reactivex import Observable, disposable -from reactivex import operators as ops -from reactivex.subject import Subject -from reactivex.disposable import CompositeDisposable -from datetime import datetime +import numpy as np +from reactivex import Observable, disposable, interval, operators as ops +from reactivex.disposable import Disposable -from dimos.utils.logging_config import setup_logger -from dimos.agents.memory.spatial_vector_db import SpatialVectorDB from dimos.agents.memory.image_embedding import ImageEmbeddingProvider +from dimos.agents.memory.spatial_vector_db import SpatialVectorDB from dimos.agents.memory.visual_memory import VisualMemory -from dimos.types.vector import Vector +from dimos.constants import DIMOS_PROJECT_ROOT +from dimos.core import In, Module, rpc +from dimos.msgs.geometry_msgs import Pose, PoseStamped, Vector3 +from dimos.msgs.sensor_msgs import Image from dimos.types.robot_location import RobotLocation +from dimos.types.vector import Vector +from dimos.utils.logging_config import setup_logger + +_OUTPUT_DIR = DIMOS_PROJECT_ROOT / "assets" / "output" +_MEMORY_DIR = _OUTPUT_DIR / "memory" +_SPATIAL_MEMORY_DIR = _MEMORY_DIR / "spatial_memory" +_DB_PATH = _SPATIAL_MEMORY_DIR / "chromadb_data" +_VISUAL_MEMORY_PATH = _SPATIAL_MEMORY_DIR / "visual_memory.pkl" + + +logger = setup_logger(__file__) -logger = setup_logger("dimos.perception.spatial_memory") -class SpatialMemory: +class SpatialMemory(Module): """ - A class for building and querying Robot spatial memory. - - This class processes video frames from ROSControl, associates them with - XY locations, and stores them in a vector database for later retrieval. - It also maintains a list of named robot locations that can be queried by name. + A Dask module for building and querying Robot spatial memory. + + This module processes video frames and odometry data from LCM streams, + associates them with XY locations, and stores them in a vector database + for later retrieval via RPC calls. It also maintains a list of named + robot locations that can be queried by name. """ - + + # LCM inputs + color_image: In[Image] = None + odom: In[PoseStamped] = None + def __init__( self, collection_name: str = "spatial_memory", - embedding_model: str = "clip", + embedding_model: str = "clip", embedding_dimensions: int = 512, min_distance_threshold: float = 0.01, # Min distance in meters to store a new frame min_time_threshold: float = 1.0, # Min time in seconds to record a new frame - db_path: Optional[str] = None, # Path for ChromaDB persistence - visual_memory_path: Optional[str] = None, # Path for saving/loading visual memory - new_memory: bool = False, # Whether to create a new memory from scratch - output_dir: Optional[str] = None, # Directory for storing visual memory data + db_path: str | None = str(_DB_PATH), # Path for ChromaDB persistence + visual_memory_path: str | None = str( + _VISUAL_MEMORY_PATH + ), # Path for saving/loading visual memory + new_memory: bool = True, # Whether to create a new memory from scratch + output_dir: str | None = str( + _SPATIAL_MEMORY_DIR + ), # Directory for storing visual memory data chroma_client: Any = None, # Optional ChromaDB client for persistence - visual_memory: Optional['VisualMemory'] = None, # Optional VisualMemory instance for storing images - video_stream: Optional[Observable] = None, # Video stream to process - transform_provider: Optional[callable] = None, # Function that returns position and rotation - ): + visual_memory: Optional[ + "VisualMemory" + ] = None, # Optional VisualMemory instance for storing images + ) -> None: """ Initialize the spatial perception system. - + Args: collection_name: Name of the vector database collection embedding_model: Model to use for image embeddings ("clip", "resnet", etc.) @@ -83,24 +100,27 @@ def __init__( self.embedding_dimensions = embedding_dimensions self.min_distance_threshold = min_distance_threshold self.min_time_threshold = min_time_threshold - + # Set up paths for persistence + # Call parent Module init + super().__init__() + self.db_path = db_path self.visual_memory_path = visual_memory_path - self.output_dir = output_dir - + # Setup ChromaDB client if not provided self._chroma_client = chroma_client if chroma_client is None and db_path is not None: # Create db directory if needed os.makedirs(db_path, exist_ok=True) - + # Clean up existing DB if creating new memory if new_memory and os.path.exists(db_path): try: - logger.info(f"Creating new ChromaDB database (new_memory=True)") + logger.info("Creating new ChromaDB database (new_memory=True)") # Try to delete any existing database files import shutil + for item in os.listdir(db_path): item_path = os.path.join(db_path, item) if os.path.isfile(item_path): @@ -110,14 +130,14 @@ def __init__( logger.info(f"Removed existing ChromaDB files from {db_path}") except Exception as e: logger.error(f"Error clearing ChromaDB directory: {e}") - - from chromadb.config import Settings + import chromadb + from chromadb.config import Settings + self._chroma_client = chromadb.PersistentClient( - path=db_path, - settings=Settings(anonymized_telemetry=False) + path=db_path, settings=Settings(anonymized_telemetry=False) ) - + # Initialize or load visual memory self._visual_memory = visual_memory if visual_memory is None: @@ -127,95 +147,230 @@ def __init__( else: try: logger.info(f"Loading existing visual memory from {visual_memory_path}...") - self._visual_memory = VisualMemory.load(visual_memory_path, output_dir=output_dir) + self._visual_memory = VisualMemory.load( + visual_memory_path, output_dir=output_dir + ) logger.info(f"Loaded {self._visual_memory.count()} images from previous runs") except Exception as e: logger.error(f"Error loading visual memory: {e}") self._visual_memory = VisualMemory(output_dir=output_dir) - - # Initialize vector database + + self.embedding_provider: ImageEmbeddingProvider = ImageEmbeddingProvider( + model_name=embedding_model, dimensions=embedding_dimensions + ) + self.vector_db: SpatialVectorDB = SpatialVectorDB( collection_name=collection_name, chroma_client=self._chroma_client, - visual_memory=self._visual_memory - ) - - self.embedding_provider: ImageEmbeddingProvider = ImageEmbeddingProvider( - model_name=embedding_model, - dimensions=embedding_dimensions + visual_memory=self._visual_memory, + embedding_provider=self.embedding_provider, ) - - self.last_position: Optional[Vector] = None - self.last_record_time: Optional[float] = None - + + self.last_position: Vector3 | None = None + self.last_record_time: float | None = None + self.frame_count: int = 0 self.stored_frame_count: int = 0 - + # For tracking stream subscription self._subscription = None - + # List to store robot locations - self.robot_locations: List[RobotLocation] = [] - + self.robot_locations: list[RobotLocation] = [] + + # Track latest data for processing + self._latest_video_frame: np.ndarray | None = None + self._latest_odom: PoseStamped | None = None + self._process_interval = 1 + logger.info(f"SpatialMemory initialized with model {embedding_model}") - - # Start processing video stream if provided - if video_stream is not None and transform_provider is not None: - self.start_continuous_processing(video_stream, transform_provider) - - - def query_by_location(self, x: float, y: float, radius: float = 2.0, limit: int = 5) -> List[Dict]: + + @rpc + def start(self) -> None: + super().start() + + # Subscribe to LCM streams + def set_video(image_msg: Image) -> None: + # Convert Image message to numpy array + if hasattr(image_msg, "data"): + frame = image_msg.data + frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) + self._latest_video_frame = frame + else: + logger.warning("Received image message without data attribute") + + def set_odom(odom_msg: PoseStamped) -> None: + self._latest_odom = odom_msg + + unsub = self.color_image.subscribe(set_video) + self._disposables.add(Disposable(unsub)) + + unsub = self.odom.subscribe(set_odom) + self._disposables.add(Disposable(unsub)) + + # Start periodic processing using interval + unsub = interval(self._process_interval).subscribe(lambda _: self._process_frame()) + self._disposables.add(Disposable(unsub)) + + @rpc + def stop(self) -> None: + self.stop_continuous_processing() + + # Save data before shutdown + self.save() + + if self._visual_memory: + self._visual_memory.clear() + + super().stop() + + def _process_frame(self) -> None: + """Process the latest frame with pose data if available.""" + if self._latest_video_frame is None or self._latest_odom is None: + return + + # Extract position and rotation from odometry + position = self._latest_odom.position + orientation = self._latest_odom.orientation + + # Create Pose object with position and orientation + current_pose = Pose( + position=Vector3(position.x, position.y, position.z), orientation=orientation + ) + + # Process the frame directly + try: + self.frame_count += 1 + + # Check distance constraint + if self.last_position is not None: + distance_moved = np.linalg.norm( + [ + current_pose.position.x - self.last_position.x, + current_pose.position.y - self.last_position.y, + current_pose.position.z - self.last_position.z, + ] + ) + if distance_moved < self.min_distance_threshold: + logger.debug( + f"Position has not moved enough: {distance_moved:.4f}m < {self.min_distance_threshold}m, skipping frame" + ) + return + + # Check time constraint + if self.last_record_time is not None: + time_elapsed = time.time() - self.last_record_time + if time_elapsed < self.min_time_threshold: + logger.debug( + f"Time since last record too short: {time_elapsed:.2f}s < {self.min_time_threshold}s, skipping frame" + ) + return + + current_time = time.time() + + # Get embedding for the frame + frame_embedding = self.embedding_provider.get_embedding(self._latest_video_frame) + + frame_id = f"frame_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}" + + # Get euler angles from quaternion orientation for metadata + euler = orientation.to_euler() + + # Create metadata dictionary with primitive types only + metadata = { + "pos_x": float(current_pose.position.x), + "pos_y": float(current_pose.position.y), + "pos_z": float(current_pose.position.z), + "rot_x": float(euler.x), + "rot_y": float(euler.y), + "rot_z": float(euler.z), + "timestamp": current_time, + "frame_id": frame_id, + } + + # Store in vector database + self.vector_db.add_image_vector( + vector_id=frame_id, + image=self._latest_video_frame, + embedding=frame_embedding, + metadata=metadata, + ) + + # Update tracking variables + self.last_position = current_pose.position + self.last_record_time = current_time + self.stored_frame_count += 1 + + logger.info( + f"Stored frame at position ({current_pose.position.x:.2f}, {current_pose.position.y:.2f}, {current_pose.position.z:.2f}), " + f"rotation ({euler.x:.2f}, {euler.y:.2f}, {euler.z:.2f}) " + f"stored {self.stored_frame_count}/{self.frame_count} frames" + ) + + # Periodically save visual memory to disk + if self._visual_memory is not None and self.visual_memory_path is not None: + if self.stored_frame_count % 100 == 0: + self.save() + + except Exception as e: + logger.error(f"Error processing frame: {e}") + + @rpc + def query_by_location( + self, x: float, y: float, radius: float = 2.0, limit: int = 5 + ) -> list[dict]: """ Query the vector database for images near the specified location. - + Args: x: X coordinate y: Y coordinate radius: Search radius in meters limit: Maximum number of results to return - + Returns: List of results, each containing the image and its metadata """ return self.vector_db.query_by_location(x, y, radius, limit) - - def start_continuous_processing(self, video_stream: Observable, transform_provider: callable) -> disposable.Disposable: + + def start_continuous_processing( + self, video_stream: Observable, get_pose: callable + ) -> disposable.Disposable: """ Start continuous processing of video frames from an Observable stream. - + Args: video_stream: Observable of video frames - transform_provider: Callable that returns position and rotation for each frame - + get_pose: Callable that returns position and rotation for each frame + Returns: Disposable subscription that can be used to stop processing """ # Stop any existing subscription self.stop_continuous_processing() - + # Map each video frame to include transform data combined_stream = video_stream.pipe( - ops.map(lambda video_frame: { - "frame": video_frame, - **transform_provider() - }), + ops.map(lambda video_frame: {"frame": video_frame, **get_pose()}), # Filter out bad transforms - ops.filter(lambda data: data.get('position') is not None and data.get('rotation') is not None) + ops.filter( + lambda data: data.get("position") is not None and data.get("rotation") is not None + ), ) - + # Process with spatial memory result_stream = self.process_stream(combined_stream) - + # Subscribe to the result stream self._subscription = result_stream.subscribe( on_next=self._on_frame_processed, on_error=lambda e: logger.error(f"Error in spatial memory stream: {e}"), - on_completed=lambda: logger.info("Spatial memory stream completed") + on_completed=lambda: logger.info("Spatial memory stream completed"), ) - + logger.info("Continuous spatial memory processing started") return self._subscription - + def stop_continuous_processing(self) -> None: """ Stop continuous processing of video frames. @@ -227,25 +382,28 @@ def stop_continuous_processing(self) -> None: logger.info("Stopped continuous spatial memory processing") except Exception as e: logger.error(f"Error stopping spatial memory processing: {e}") - - def _on_frame_processed(self, result: Dict[str, Any]) -> None: + + def _on_frame_processed(self, result: dict[str, Any]) -> None: """ Handle updates from the spatial memory processing stream. """ # Log successful frame storage (if stored) - position = result.get('position') + position = result.get("position") if position is not None: - logger.debug(f"Spatial memory updated with frame at ({position[0]:.2f}, {position[1]:.2f}, {position[2]:.2f})") - + logger.debug( + f"Spatial memory updated with frame at ({position[0]:.2f}, {position[1]:.2f}, {position[2]:.2f})" + ) + # Periodically save visual memory to disk (e.g., every 100 frames) if self._visual_memory is not None and self.visual_memory_path is not None: if self.stored_frame_count % 100 == 0: self.save() - + + @rpc def save(self) -> bool: """ Save the visual memory component to disk. - + Returns: True if memory was saved successfully, False otherwise """ @@ -257,128 +415,141 @@ def save(self) -> bool: except Exception as e: logger.error(f"Failed to save visual memory: {e}") return False - + def process_stream(self, combined_stream: Observable) -> Observable: """ Process a combined stream of video frames and positions. - + This method handles a stream where each item already contains both the frame and position, - such as the stream created by combining video and transform streams with the + such as the stream created by combining video and transform streams with the with_latest_from operator. - + Args: combined_stream: Observable stream of dictionaries containing 'frame' and 'position' - + Returns: Observable of processing results, including the stored frame and its metadata """ - self.last_position = None - self.last_record_time = None - + def process_combined_data(data): self.frame_count += 1 - - frame = data.get('frame') - position_vec = data.get('position') # Use .get() for consistency - rotation_vec = data.get('rotation') # Get rotation data if available - - - if not position_vec or not rotation_vec: + + frame = data.get("frame") + position_vec = data.get("position") # Use .get() for consistency + rotation_vec = data.get("rotation") # Get rotation data if available + + if position_vec is None or rotation_vec is None: logger.info("No position or rotation data available, skipping frame") return None - - if self.last_position is not None and (self.last_position - position_vec).length() < self.min_distance_threshold: - logger.debug("Position has not moved, skipping frame") - return None - if self.last_record_time is not None and (time.time() - self.last_record_time) < self.min_time_threshold: + # position_vec is already a Vector3, no need to recreate it + position_v3 = position_vec + + if self.last_position is not None: + distance_moved = np.linalg.norm( + [ + position_v3.x - self.last_position.x, + position_v3.y - self.last_position.y, + position_v3.z - self.last_position.z, + ] + ) + if distance_moved < self.min_distance_threshold: + logger.debug("Position has not moved, skipping frame") + return None + + if ( + self.last_record_time is not None + and (time.time() - self.last_record_time) < self.min_time_threshold + ): logger.debug("Time since last record too short, skipping frame") return None - + current_time = time.time() - + frame_embedding = self.embedding_provider.get_embedding(frame) - + frame_id = f"frame_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}" - + # Create metadata dictionary with primitive types only metadata = { - "pos_x": float(position_vec.x), - "pos_y": float(position_vec.y), - "pos_z": float(position_vec.z), + "pos_x": float(position_v3.x), + "pos_y": float(position_v3.y), + "pos_z": float(position_v3.z), "rot_x": float(rotation_vec.x), "rot_y": float(rotation_vec.y), "rot_z": float(rotation_vec.z), "timestamp": current_time, - "frame_id": frame_id + "frame_id": frame_id, } - + self.vector_db.add_image_vector( - vector_id=frame_id, - image=frame, - embedding=frame_embedding, - metadata=metadata + vector_id=frame_id, image=frame, embedding=frame_embedding, metadata=metadata ) - - self.last_position = position_vec + + self.last_position = position_v3 self.last_record_time = current_time self.stored_frame_count += 1 - - logger.info(f"Stored frame at position {position_vec}, rotation {rotation_vec})" - f" stored {self.stored_frame_count}/{self.frame_count} frames") - + + logger.info( + f"Stored frame at position ({position_v3.x:.2f}, {position_v3.y:.2f}, {position_v3.z:.2f}), " + f"rotation ({rotation_vec.x:.2f}, {rotation_vec.y:.2f}, {rotation_vec.z:.2f}) " + f"stored {self.stored_frame_count}/{self.frame_count} frames" + ) + # Create return dictionary with primitive-compatible values return { "frame": frame, - "position": (position_vec.x, position_vec.y, position_vec.z), + "position": (position_v3.x, position_v3.y, position_v3.z), "rotation": (rotation_vec.x, rotation_vec.y, rotation_vec.z), "frame_id": frame_id, - "timestamp": current_time + "timestamp": current_time, } - + return combined_stream.pipe( - ops.map(process_combined_data), - ops.filter(lambda result: result is not None) + ops.map(process_combined_data), ops.filter(lambda result: result is not None) ) - def query_by_image(self, image: np.ndarray, limit: int = 5) -> List[Dict]: + @rpc + def query_by_image(self, image: np.ndarray, limit: int = 5) -> list[dict]: """ Query the vector database for images similar to the provided image. - + Args: image: Query image limit: Maximum number of results to return - + Returns: List of results, each containing the image and its metadata """ embedding = self.embedding_provider.get_embedding(image) return self.vector_db.query_by_embedding(embedding, limit) - - def query_by_text(self, text: str, limit: int = 5) -> List[Dict]: + + @rpc + def query_by_text(self, text: str, limit: int = 5) -> list[dict]: """ Query the vector database for images matching the provided text description. - + This method uses CLIP's text-to-image matching capability to find images that semantically match the text query (e.g., "where is the kitchen"). - + Args: text: Text query to search for limit: Maximum number of results to return - + Returns: List of results, each containing the image, its metadata, and similarity score """ logger.info(f"Querying spatial memory with text: '{text}'") return self.vector_db.query_by_text(text, limit) - + + @rpc def add_robot_location(self, location: RobotLocation) -> bool: """ Add a named robot location to spatial memory. - + Args: location: The RobotLocation object to add - + Returns: True if successfully added, False otherwise """ @@ -387,27 +558,73 @@ def add_robot_location(self, location: RobotLocation) -> bool: self.robot_locations.append(location) logger.info(f"Added robot location '{location.name}' at position {location.position}") return True - + except Exception as e: logger.error(f"Error adding robot location: {e}") return False - - def get_robot_locations(self) -> List[RobotLocation]: + + @rpc + def add_named_location( + self, + name: str, + position: list[float] | None = None, + rotation: list[float] | None = None, + description: str | None = None, + ) -> bool: + """ + Add a named robot location to spatial memory using current or specified position. + + Args: + name: Name of the location + position: Optional position [x, y, z], uses current position if None + rotation: Optional rotation [roll, pitch, yaw], uses current rotation if None + description: Optional description of the location + + Returns: + True if successfully added, False otherwise + """ + # Use current position/rotation if not provided + if position is None and self._latest_odom is not None: + pos = self._latest_odom.position + position = [pos.x, pos.y, pos.z] + + if rotation is None and self._latest_odom is not None: + euler = self._latest_odom.orientation.to_euler() + rotation = [euler.x, euler.y, euler.z] + + if position is None: + logger.error("No position available for robot location") + return False + + # Create RobotLocation object + location = RobotLocation( + name=name, + position=Vector(position), + rotation=Vector(rotation) if rotation else Vector([0, 0, 0]), + description=description or f"Location: {name}", + timestamp=time.time(), + ) + + return self.add_robot_location(location) + + @rpc + def get_robot_locations(self) -> list[RobotLocation]: """ Get all stored robot locations. - + Returns: List of RobotLocation objects """ return self.robot_locations - - def find_robot_location(self, name: str) -> Optional[RobotLocation]: + + @rpc + def find_robot_location(self, name: str) -> RobotLocation | None: """ Find a robot location by name. - + Args: name: Name of the location to find - + Returns: RobotLocation object if found, None otherwise """ @@ -415,17 +632,36 @@ def find_robot_location(self, name: str) -> Optional[RobotLocation]: for location in self.robot_locations: if location.name.lower() == name.lower(): return location - + return None - - def cleanup(self): - """Clean up resources.""" - # Stop any ongoing processing - self.stop_continuous_processing() - - # Save data if possible - self.save() - - # Log cleanup - if self.vector_db: - logger.info(f"Cleaning up SpatialMemory, stored {self.stored_frame_count} frames") + + @rpc + def get_stats(self) -> dict[str, int]: + """Get statistics about the spatial memory module. + + Returns: + Dictionary containing: + - frame_count: Total number of frames processed + - stored_frame_count: Number of frames actually stored + """ + return {"frame_count": self.frame_count, "stored_frame_count": self.stored_frame_count} + + @rpc + def tag_location(self, robot_location: RobotLocation) -> bool: + try: + self.vector_db.tag_location(robot_location) + except Exception: + return False + return True + + @rpc + def query_tagged_location(self, query: str) -> RobotLocation | None: + location, semantic_distance = self.vector_db.query_tagged_location(query) + if semantic_distance < 0.3: + return location + return None + + +spatial_memory = SpatialMemory.blueprint + +__all__ = ["SpatialMemory", "spatial_memory"] diff --git a/dimos/perception/test_spatial_memory.py b/dimos/perception/test_spatial_memory.py new file mode 100644 index 0000000000..f42638df73 --- /dev/null +++ b/dimos/perception/test_spatial_memory.py @@ -0,0 +1,202 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import shutil +import tempfile +import time + +import numpy as np +import pytest +from reactivex import operators as ops + +from dimos.msgs.geometry_msgs import Pose +from dimos.perception.spatial_perception import SpatialMemory +from dimos.stream.video_provider import VideoProvider + + +@pytest.mark.heavy +class TestSpatialMemory: + @pytest.fixture(scope="class") + def temp_dir(self): + # Create a temporary directory for storing spatial memory data + temp_dir = tempfile.mkdtemp() + yield temp_dir + # Clean up + shutil.rmtree(temp_dir) + + @pytest.fixture(scope="class") + def spatial_memory(self, temp_dir): + # Create a single SpatialMemory instance to be reused across all tests + memory = SpatialMemory( + collection_name="test_collection", + embedding_model="clip", + new_memory=True, + db_path=os.path.join(temp_dir, "chroma_db"), + visual_memory_path=os.path.join(temp_dir, "visual_memory.pkl"), + output_dir=os.path.join(temp_dir, "images"), + min_distance_threshold=0.01, + min_time_threshold=0.01, + ) + yield memory + # Clean up + memory.stop() + + def test_spatial_memory_initialization(self, spatial_memory) -> None: + """Test SpatialMemory initializes correctly with CLIP model.""" + # Use the shared spatial_memory fixture + assert spatial_memory is not None + assert spatial_memory.embedding_model == "clip" + assert spatial_memory.embedding_provider is not None + + def test_image_embedding(self, spatial_memory) -> None: + """Test generating image embeddings using CLIP.""" + # Use the shared spatial_memory fixture + # Create a test image - use a simple colored square + test_image = np.zeros((224, 224, 3), dtype=np.uint8) + test_image[50:150, 50:150] = [0, 0, 255] # Blue square + + # Generate embedding + embedding = spatial_memory.embedding_provider.get_embedding(test_image) + + # Check embedding shape and characteristics + assert embedding is not None + assert isinstance(embedding, np.ndarray) + assert embedding.shape[0] == spatial_memory.embedding_dimensions + + # Check that embedding is normalized (unit vector) + assert np.isclose(np.linalg.norm(embedding), 1.0, atol=1e-5) + + # Test text embedding + text_embedding = spatial_memory.embedding_provider.get_text_embedding("a blue square") + assert text_embedding is not None + assert isinstance(text_embedding, np.ndarray) + assert text_embedding.shape[0] == spatial_memory.embedding_dimensions + assert np.isclose(np.linalg.norm(text_embedding), 1.0, atol=1e-5) + + def test_spatial_memory_processing(self, spatial_memory, temp_dir) -> None: + """Test processing video frames and building spatial memory with CLIP embeddings.""" + try: + # Use the shared spatial_memory fixture + memory = spatial_memory + + from dimos.utils.data import get_data + + video_path = get_data("assets") / "trimmed_video_office.mov" + assert os.path.exists(video_path), f"Test video not found: {video_path}" + video_provider = VideoProvider(dev_name="test_video", video_source=video_path) + video_stream = video_provider.capture_video_as_observable(realtime=False, fps=15) + + # Create a frame counter for position generation + frame_counter = 0 + + # Process each video frame directly + def process_frame(frame): + nonlocal frame_counter + + # Generate a unique position for this frame to ensure minimum distance threshold is met + pos = Pose(frame_counter * 0.5, frame_counter * 0.5, 0) + transform = {"position": pos, "timestamp": time.time()} + frame_counter += 1 + + # Create a dictionary with frame, position and rotation for SpatialMemory.process_stream + return { + "frame": frame, + "position": transform["position"], + "rotation": transform["position"], # Using position as rotation for testing + } + + # Create a stream that processes each frame + formatted_stream = video_stream.pipe(ops.map(process_frame)) + + # Process the stream using SpatialMemory's built-in processing + print("Creating spatial memory stream...") + spatial_stream = memory.process_stream(formatted_stream) + + # Stream is now created above using memory.process_stream() + + # Collect results from the stream + results = [] + + frames_processed = 0 + target_frames = 100 # Process more frames for thorough testing + + def on_next(result) -> None: + nonlocal results, frames_processed + if not result: # Skip None results + return + + results.append(result) + frames_processed += 1 + + # Stop processing after target frames + if frames_processed >= target_frames: + subscription.dispose() + + def on_error(error) -> None: + pytest.fail(f"Error in spatial stream: {error}") + + def on_completed() -> None: + pass + + # Subscribe and wait for results + subscription = spatial_stream.subscribe( + on_next=on_next, on_error=on_error, on_completed=on_completed + ) + + # Wait for frames to be processed + timeout = 30.0 # seconds + start_time = time.time() + while frames_processed < target_frames and time.time() - start_time < timeout: + time.sleep(0.5) + + subscription.dispose() + + assert len(results) > 0, "Failed to process any frames with spatial memory" + + relevant_queries = ["office", "room with furniture"] + irrelevant_query = "star wars" + + for query in relevant_queries: + results = memory.query_by_text(query, limit=2) + print(f"\nResults for query: '{query}'") + + assert len(results) > 0, f"No results found for relevant query: {query}" + + similarities = [1 - r.get("distance") for r in results] + print(f"Similarities: {similarities}") + + assert any(d > 0.22 for d in similarities), ( + f"Expected at least one result with similarity > 0.22 for query '{query}'" + ) + + results = memory.query_by_text(irrelevant_query, limit=2) + print(f"\nResults for query: '{irrelevant_query}'") + + if results: + similarities = [1 - r.get("distance") for r in results] + print(f"Similarities: {similarities}") + + assert all(d < 0.25 for d in similarities), ( + f"Expected all results to have similarity < 0.25 for irrelevant query '{irrelevant_query}'" + ) + + except Exception as e: + pytest.fail(f"Error in test: {e}") + finally: + video_provider.dispose_all() + + +if __name__ == "__main__": + pytest.main(["-v", __file__]) diff --git a/dimos/perception/test_spatial_memory_module.py b/dimos/perception/test_spatial_memory_module.py new file mode 100644 index 0000000000..f89c975d89 --- /dev/null +++ b/dimos/perception/test_spatial_memory_module.py @@ -0,0 +1,229 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import os +import tempfile +import time + +import pytest +from reactivex import operators as ops + +from dimos import core +from dimos.core import Module, Out, rpc +from dimos.msgs.sensor_msgs import Image +from dimos.perception.spatial_perception import SpatialMemory +from dimos.protocol import pubsub +from dimos.robot.unitree_webrtc.type.odometry import Odometry +from dimos.utils.data import get_data +from dimos.utils.logging_config import setup_logger +from dimos.utils.testing import TimedSensorReplay + +logger = setup_logger("test_spatial_memory_module") + +pubsub.lcm.autoconf() + + +class VideoReplayModule(Module): + """Module that replays video data from TimedSensorReplay.""" + + video_out: Out[Image] = None + + def __init__(self, video_path: str) -> None: + super().__init__() + self.video_path = video_path + self._subscription = None + + @rpc + def start(self) -> None: + """Start replaying video data.""" + # Use TimedSensorReplay to replay video frames + video_replay = TimedSensorReplay(self.video_path, autocast=Image.from_numpy) + + # Subscribe to the replay stream and publish to LCM + self._subscription = ( + video_replay.stream() + .pipe( + ops.sample(2), # Sample every 2 seconds for resource-constrained systems + ops.take(5), # Only take 5 frames total + ) + .subscribe(self.video_out.publish) + ) + + logger.info("VideoReplayModule started") + + @rpc + def stop(self) -> None: + """Stop replaying video data.""" + if self._subscription: + self._subscription.dispose() + self._subscription = None + logger.info("VideoReplayModule stopped") + + +class OdometryReplayModule(Module): + """Module that replays odometry data from TimedSensorReplay.""" + + odom_out: Out[Odometry] = None + + def __init__(self, odom_path: str) -> None: + super().__init__() + self.odom_path = odom_path + self._subscription = None + + @rpc + def start(self) -> None: + """Start replaying odometry data.""" + # Use TimedSensorReplay to replay odometry + odom_replay = TimedSensorReplay(self.odom_path, autocast=Odometry.from_msg) + + # Subscribe to the replay stream and publish to LCM + self._subscription = ( + odom_replay.stream() + .pipe( + ops.sample(0.5), # Sample every 500ms + ops.take(10), # Only take 10 odometry updates total + ) + .subscribe(self.odom_out.publish) + ) + + logger.info("OdometryReplayModule started") + + @rpc + def stop(self) -> None: + """Stop replaying odometry data.""" + if self._subscription: + self._subscription.dispose() + self._subscription = None + logger.info("OdometryReplayModule stopped") + + +@pytest.mark.gpu +class TestSpatialMemoryModule: + @pytest.fixture(scope="function") + def temp_dir(self): + """Create a temporary directory for test data.""" + # Use standard tempfile module to ensure proper permissions + temp_dir = tempfile.mkdtemp(prefix="spatial_memory_test_") + + yield temp_dir + + @pytest.mark.asyncio + async def test_spatial_memory_module_with_replay(self, temp_dir): + """Test SpatialMemory module with TimedSensorReplay inputs.""" + + # Start Dask + dimos = core.start(1) + + try: + # Get test data paths + data_path = get_data("unitree_office_walk") + video_path = os.path.join(data_path, "video") + odom_path = os.path.join(data_path, "odom") + + # Deploy modules + # Video replay module + video_module = dimos.deploy(VideoReplayModule, video_path) + video_module.video_out.transport = core.LCMTransport("/test_video", Image) + + # Odometry replay module + odom_module = dimos.deploy(OdometryReplayModule, odom_path) + odom_module.odom_out.transport = core.LCMTransport("/test_odom", Odometry) + + # Spatial memory module + spatial_memory = dimos.deploy( + SpatialMemory, + collection_name="test_spatial_memory", + embedding_model="clip", + embedding_dimensions=512, + min_distance_threshold=0.5, # 0.5m for test + min_time_threshold=1.0, # 1 second + db_path=os.path.join(temp_dir, "chroma_db"), + visual_memory_path=os.path.join(temp_dir, "visual_memory.pkl"), + new_memory=True, + output_dir=os.path.join(temp_dir, "images"), + ) + + # Connect streams + spatial_memory.video.connect(video_module.video_out) + spatial_memory.odom.connect(odom_module.odom_out) + + # Start all modules + video_module.start() + odom_module.start() + spatial_memory.start() + logger.info("All modules started, processing in background...") + + # Wait for frames to be processed with timeout + timeout = 10.0 # 10 second timeout + start_time = time.time() + + # Keep checking stats while modules are running + while (time.time() - start_time) < timeout: + stats = spatial_memory.get_stats() + if stats["frame_count"] > 0 and stats["stored_frame_count"] > 0: + logger.info( + f"Frames processing - Frame count: {stats['frame_count']}, Stored: {stats['stored_frame_count']}" + ) + break + await asyncio.sleep(0.5) + else: + # Timeout reached + stats = spatial_memory.get_stats() + logger.error( + f"Timeout after {timeout}s - Frame count: {stats['frame_count']}, Stored: {stats['stored_frame_count']}" + ) + raise AssertionError(f"No frames processed within {timeout} seconds") + + await asyncio.sleep(2) + + mid_stats = spatial_memory.get_stats() + logger.info( + f"Mid-test stats - Frame count: {mid_stats['frame_count']}, Stored: {mid_stats['stored_frame_count']}" + ) + assert mid_stats["frame_count"] >= stats["frame_count"], ( + "Frame count should increase or stay same" + ) + + # Test query while modules are still running + try: + text_results = spatial_memory.query_by_text("office") + logger.info(f"Query by text 'office' returned {len(text_results)} results") + assert len(text_results) > 0, "Should have at least one result" + except Exception as e: + logger.warning(f"Query by text failed: {e}") + + final_stats = spatial_memory.get_stats() + logger.info( + f"Final stats - Frame count: {final_stats['frame_count']}, Stored: {final_stats['stored_frame_count']}" + ) + + video_module.stop() + odom_module.stop() + logger.info("Stopped replay modules") + + logger.info("All spatial memory module tests passed!") + + finally: + # Cleanup + if "dimos" in locals(): + dimos.close() + + +if __name__ == "__main__": + pytest.main(["-v", "-s", __file__]) + # test = TestSpatialMemoryModule() + # asyncio.run( + # test.test_spatial_memory_module_with_replay(tempfile.mkdtemp(prefix="spatial_memory_test_")) + # ) diff --git a/dimos/perception/visual_servoing.py b/dimos/perception/visual_servoing.py deleted file mode 100644 index ba8883cc9d..0000000000 --- a/dimos/perception/visual_servoing.py +++ /dev/null @@ -1,489 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time -import threading -from typing import Dict, Optional, List, Tuple -import logging -import numpy as np - -from dimos.utils.simple_controller import VisualServoingController - -# Configure logging -logger = logging.getLogger(__name__) - - -def calculate_iou(box1, box2): - """Calculate Intersection over Union between two bounding boxes.""" - x1 = max(box1[0], box2[0]) - y1 = max(box1[1], box2[1]) - x2 = min(box1[2], box2[2]) - y2 = min(box1[3], box2[3]) - - intersection = max(0, x2 - x1) * max(0, y2 - y1) - area1 = (box1[2] - box1[0]) * (box1[3] - box1[1]) - area2 = (box2[2] - box2[0]) * (box2[3] - box2[1]) - union = area1 + area2 - intersection - - return intersection / union if union > 0 else 0 - - -class VisualServoing: - """ - A class that performs visual servoing to track and follow a human target. - - The class will use the provided tracking stream to detect people and estimate - their distance and angle, then use a VisualServoingController to generate - appropriate velocity commands to track the target. - """ - - def __init__(self, tracking_stream=None, max_linear_speed=0.8, max_angular_speed=1.5, - desired_distance=1.5, max_lost_frames=10000, iou_threshold=0.6): - """Initialize the visual servoing. - - Args: - tracking_stream: Observable tracking stream (must be already set up) - max_linear_speed: Maximum linear speed in m/s - max_angular_speed: Maximum angular speed in rad/s - desired_distance: Desired distance to maintain from target in meters - max_lost_frames: Maximum number of frames target can be lost before stopping tracking - iou_threshold: Minimum IOU threshold to consider bounding boxes as matching - """ - self.tracking_stream = tracking_stream - self.max_linear_speed = max_linear_speed - self.max_angular_speed = max_angular_speed - self.desired_distance = desired_distance - self.max_lost_frames = max_lost_frames - self.iou_threshold = iou_threshold - - # Initialize the controller with PID parameters tuned for slow-moving robot - # Distance PID: (kp, ki, kd, output_limits, integral_limit, deadband, output_deadband) - distance_pid_params = ( - 1.0, # kp: Moderate proportional gain for smooth approach - 0.2, # ki: Small integral gain to eliminate steady-state error - 0.1, # kd: Some damping for smooth motion - (-self.max_linear_speed, self.max_linear_speed), # output_limits - 0.5, # integral_limit: Prevent windup - 0.1, # deadband: Small deadband for distance control - 0.05, # output_deadband: Minimum output to overcome friction - ) - - # Angle PID: (kp, ki, kd, output_limits, integral_limit, deadband, output_deadband) - angle_pid_params = ( - 1.4, # kp: Higher proportional gain for responsive turning - 0.1, # ki: Small integral gain - 0.05, # kd: Light damping to prevent oscillation - (-self.max_angular_speed, self.max_angular_speed), # output_limits - 0.3, # integral_limit: Prevent windup - 0.1, # deadband: Small deadband for angle control - 0.1, # output_deadband: Minimum output to overcome friction - True, # Invert output for angular control - ) - - # Initialize the visual servoing controller - self.controller = VisualServoingController( - distance_pid_params=distance_pid_params, - angle_pid_params=angle_pid_params - ) - - # Initialize tracking state - self.last_control_time = time.time() - self.running = False - self.current_target = None # (target_id, bbox) - self.target_lost_frames = 0 - - # Add variables to track current distance and angle - self.current_distance = None - self.current_angle = None - - # Stream subscription management - self.subscription = None - self.latest_result = None - self.result_lock = threading.Lock() - self.stop_event = threading.Event() - - # Subscribe to the tracking stream - self._subscribe_to_tracking_stream() - - def start_tracking(self, desired_distance: int = None, point: Tuple[int, int] = None, timeout_wait_for_target: float = 20.0,) -> bool: - """ - Start tracking a human target using visual servoing. - - Args: - point: Optional tuple of (x, y) coordinates in image space. If provided, - will find the target whose bounding box contains this point. - If None, will track the closest person. - - Returns: - bool: True if tracking was successfully started, False otherwise - """ - if desired_distance is not None: - self.desired_distance = desired_distance - - if self.tracking_stream is None: - self.running = False - return False - - # Get the latest frame and targets from person tracker - try: - # Try getting the result multiple times with delays - for attempt in range(10): - result = self._get_current_tracking_result() - - if result is not None: - break - - logger.warning(f"Attempt {attempt + 1}: No tracking result, retrying in 1 second...") - time.sleep(3) # Wait 1 second between attempts - - if result is None: - logger.warning("Stream error, no targets found after multiple attempts") - return False - - targets = result.get("targets") - - # If bbox is provided, find matching target based on IOU - if point is not None and not self.running: - # Find the target with highest IOU to the provided bbox - best_target = self._find_target_by_point(point, targets) - # If no bbox is provided, find the closest person - elif not self.running: - if timeout_wait_for_target > 0.0 and len(targets) == 0: - # Wait for target to appear - start_time = time.time() - while time.time() - start_time < timeout_wait_for_target: - time.sleep(0.2) - result = self._get_current_tracking_result() - targets = result.get("targets") - if len(targets) > 0: - break - best_target = self._find_closest_target(targets) - else: - # Already tracking - return True - - if best_target: - # Set as current target and reset lost counter - target_id = best_target.get("target_id") - target_bbox = best_target.get("bbox") - self.current_target = (target_id, target_bbox) - self.target_lost_frames = 0 - self.running = True - logger.info(f"Started tracking target ID: {target_id}") - - # Get distance and angle and compute control (store as initial control values) - distance = best_target.get("distance") - angle = best_target.get("angle") - self._compute_control(distance, angle) - return True - else: - if point is not None: - logger.warning("No matching target found") - else: - logger.warning("No suitable target found for tracking") - self.running = False - return False - except Exception as e: - logger.error(f"Error starting tracking: {e}") - self.running = False - return False - - def _find_target_by_point(self, point, targets): - """Find the target whose bounding box contains the given point. - - Args: - point: Tuple of (x, y) coordinates in image space - targets: List of target dictionaries - - Returns: - dict: The target whose bbox contains the point, or None if no match - """ - x, y = point - for target in targets: - bbox = target.get("bbox") - if not bbox: - continue - - x1, y1, x2, y2 = bbox - if x1 <= x <= x2 and y1 <= y <= y2: - return target - return None - - def updateTracking(self) -> Dict[str, any]: - """ - Update tracking of current target. - - Returns: - Dict with linear_vel, angular_vel, and running state - """ - if not self.running or self.current_target is None: - self.running = False - self.current_distance = None - self.current_angle = None - return {"linear_vel": 0.0, "angular_vel": 0.0} - - # Get the latest tracking result - result = self._get_current_tracking_result() - - # Get targets from result - targets = result.get("targets") - - # Try to find current target by ID or IOU - current_target_id, current_bbox = self.current_target - target_found = False - - # First try to find by ID - for target in targets: - if target.get("target_id") == current_target_id: - # Found by ID, update bbox - self.current_target = (current_target_id, target.get("bbox")) - self.target_lost_frames = 0 - target_found = True - - # Store current distance and angle - self.current_distance = target.get("distance") - self.current_angle = target.get("angle") - - # Compute control - control = self._compute_control(self.current_distance, self.current_angle) - return control - - # If not found by ID, try to find by IOU - if not target_found and current_bbox is not None: - best_target = self._find_best_target_by_iou(current_bbox, targets) - if best_target: - # Update target - new_id = best_target.get("target_id") - new_bbox = best_target.get("bbox") - self.current_target = (new_id, new_bbox) - self.target_lost_frames = 0 - logger.info(f"Target ID updated: {current_target_id} -> {new_id}") - - # Store current distance and angle - self.current_distance = best_target.get("distance") - self.current_angle = best_target.get("angle") - - # Compute control - control = self._compute_control(self.current_distance, self.current_angle) - return control - - # Target not found, increment lost counter - if not target_found: - self.target_lost_frames += 1 - logger.warning(f"Target lost: frame {self.target_lost_frames}/{self.max_lost_frames}") - - # Check if target is lost for too many frames - if self.target_lost_frames >= self.max_lost_frames: - logger.info("Target lost for too many frames, stopping tracking") - self.stop_tracking() - return {"linear_vel": 0.0, "angular_vel": 0.0, "running": False} - - return {"linear_vel": 0.0, "angular_vel": 0.0} - - - def _compute_control(self, distance: float, angle: float) -> Dict[str, float]: - """ - Compute control commands based on measured distance and angle. - - Args: - distance: Measured distance to target in meters - angle: Measured angle to target in radians - - Returns: - Dict with linear_vel and angular_vel keys - """ - current_time = time.time() - dt = current_time - self.last_control_time - self.last_control_time = current_time - - # Compute control with visual servoing controller - linear_vel, angular_vel = self.controller.compute_control( - measured_distance=distance, - measured_angle=angle, - desired_distance=self.desired_distance, - desired_angle=0.0, # Keep target centered - dt=dt - ) - - # Log control values for debugging - logger.debug(f"Distance: {distance:.2f}m, Angle: {np.rad2deg(angle):.1f}°") - logger.debug(f"Control: linear={linear_vel:.2f}m/s, angular={angular_vel:.2f}rad/s") - - return { - "linear_vel": linear_vel, - "angular_vel": angular_vel - } - - def _find_best_target_by_iou(self, bbox: List[float], targets: List[Dict]) -> Optional[Dict]: - """ - Find the target with highest IOU to the given bbox. - - Args: - bbox: Bounding box to match [x1, y1, x2, y2] - targets: List of target dictionaries - - Returns: - Best matching target or None if no match found - """ - if not targets: - return None - - best_iou = self.iou_threshold - best_target = None - - for target in targets: - target_bbox = target.get("bbox") - if target_bbox is None: - continue - - iou = calculate_iou(bbox, target_bbox) - if iou > best_iou: - best_iou = iou - best_target = target - - return best_target - - def _find_closest_target(self, targets: List[Dict]) -> Optional[Dict]: - """ - Find the target with shortest distance to the camera. - - Args: - targets: List of target dictionaries - - Returns: - The closest target or None if no targets available - """ - if not targets: - return None - - closest_target = None - min_distance = float('inf') - - for target in targets: - distance = target.get("distance") - if distance is not None and distance < min_distance: - min_distance = distance - closest_target = target - - return closest_target - - def _subscribe_to_tracking_stream(self): - """ - Subscribe to the already set up tracking stream. - """ - if self.tracking_stream is None: - logger.warning("No tracking stream provided to subscribe to") - return - - try: - # Set up subscription to process frames - self.subscription = self.tracking_stream.subscribe( - on_next=self._on_tracking_result, - on_error=self._on_tracking_error, - on_completed=self._on_tracking_completed - ) - - logger.info("Subscribed to tracking stream successfully") - except Exception as e: - logger.error(f"Error subscribing to tracking stream: {e}") - - def _on_tracking_result(self, result): - """ - Callback for tracking stream results. - - This updates the latest result for use by _get_current_tracking_result. - - Args: - result: The result from the tracking stream - """ - if self.stop_event.is_set(): - return - - # Update the latest result - with self.result_lock: - self.latest_result = result - - def _on_tracking_error(self, error): - """ - Callback for tracking stream errors. - - Args: - error: The error from the tracking stream - """ - logger.error(f"Tracking stream error: {error}") - self.stop_event.set() - - def _on_tracking_completed(self): - """Callback for tracking stream completion.""" - logger.info("Tracking stream completed") - self.stop_event.set() - - def _get_current_tracking_result(self) -> Optional[Dict]: - """ - Get the current tracking result. - - Returns the latest result cached from the tracking stream subscription. - - Returns: - Dict with 'frame' and 'targets' or None if not available - """ - # Return the latest cached result - with self.result_lock: - return self.latest_result - - def stop_tracking(self): - """Stop tracking and reset controller state.""" - self.running = False - self.current_target = None - self.target_lost_frames = 0 - self.current_distance = None - self.current_angle = None - return {"linear_vel": 0.0, "angular_vel": 0.0, "running": False} - - def is_goal_reached(self, distance_threshold=0.2, angle_threshold=0.1) -> bool: - """ - Check if the robot has reached the tracking goal (desired distance and angle). - - Args: - distance_threshold: Maximum allowed difference between current and desired distance (meters) - angle_threshold: Maximum allowed difference between current and desired angle (radians) - - Returns: - bool: True if both distance and angle are within threshold of desired values - """ - if not self.running or self.current_target is None: - return False - - # Use the stored distance and angle values - if self.current_distance is None or self.current_angle is None: - return False - - # Check if within thresholds - distance_error = abs(self.current_distance - self.desired_distance) - angle_error = abs(self.current_angle) # Desired angle is always 0 (centered) - - logger.debug(f"Goal check - Distance error: {distance_error:.2f}m, Angle error: {angle_error:.2f}rad") - - return (distance_error <= distance_threshold) and (angle_error <= angle_threshold) - - def cleanup(self): - """Clean up all resources used by the visual servoing.""" - self.stop_event.set() - if self.subscription: - self.subscription.dispose() - self.subscription = None - - def __del__(self): - """Destructor to ensure cleanup on object deletion.""" - self.cleanup() diff --git a/dimos/protocol/__init__.py b/dimos/protocol/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/dimos/protocol/encode/__init__.py b/dimos/protocol/encode/__init__.py new file mode 100644 index 0000000000..66bbbbb21c --- /dev/null +++ b/dimos/protocol/encode/__init__.py @@ -0,0 +1,89 @@ +from abc import ABC, abstractmethod +import json +from typing import Generic, Protocol, TypeVar + +MsgT = TypeVar("MsgT") +EncodingT = TypeVar("EncodingT") + + +class LCMMessage(Protocol): + """Protocol for LCM message types that have encode/decode methods.""" + + def encode(self) -> bytes: + """Encode the message to bytes.""" + ... + + @staticmethod + def decode(data: bytes) -> "LCMMessage": + """Decode bytes to a message instance.""" + ... + + +# TypeVar for LCM message types +LCMMsgT = TypeVar("LCMMsgT", bound=LCMMessage) + + +class Encoder(ABC, Generic[MsgT, EncodingT]): + """Base class for message encoders/decoders.""" + + @staticmethod + @abstractmethod + def encode(msg: MsgT) -> EncodingT: + raise NotImplementedError("Subclasses must implement this method.") + + @staticmethod + @abstractmethod + def decode(data: EncodingT) -> MsgT: + raise NotImplementedError("Subclasses must implement this method.") + + +class JSON(Encoder[MsgT, bytes]): + @staticmethod + def encode(msg: MsgT) -> bytes: + return json.dumps(msg).encode("utf-8") + + @staticmethod + def decode(data: bytes) -> MsgT: + return json.loads(data.decode("utf-8")) + + +class LCM(Encoder[LCMMsgT, bytes]): + """Encoder for LCM message types.""" + + @staticmethod + def encode(msg: LCMMsgT) -> bytes: + return msg.encode() + + @staticmethod + def decode(data: bytes) -> LCMMsgT: + # Note: This is a generic implementation. In practice, you would need + # to pass the specific message type to decode with. This method would + # typically be overridden in subclasses for specific message types. + raise NotImplementedError( + "LCM.decode requires a specific message type. Use LCMTypedEncoder[MessageType] instead." + ) + + +class LCMTypedEncoder(LCM, Generic[LCMMsgT]): + """Typed LCM encoder for specific message types.""" + + def __init__(self, message_type: type[LCMMsgT]) -> None: + self.message_type = message_type + + @staticmethod + def decode(data: bytes) -> LCMMsgT: + # This is a generic implementation and should be overridden in specific instances + raise NotImplementedError( + "LCMTypedEncoder.decode must be overridden with a specific message type" + ) + + +def create_lcm_typed_encoder(message_type: type[LCMMsgT]) -> type[LCMTypedEncoder[LCMMsgT]]: + """Factory function to create a typed LCM encoder for a specific message type.""" + + class SpecificLCMEncoder(LCMTypedEncoder): + @staticmethod + def decode(data: bytes) -> LCMMsgT: + return message_type.decode(data) # type: ignore[return-value] + + return SpecificLCMEncoder diff --git a/dimos/protocol/pubsub/__init__.py b/dimos/protocol/pubsub/__init__.py new file mode 100644 index 0000000000..89bd292fda --- /dev/null +++ b/dimos/protocol/pubsub/__init__.py @@ -0,0 +1,3 @@ +import dimos.protocol.pubsub.lcmpubsub as lcm +from dimos.protocol.pubsub.memory import Memory +from dimos.protocol.pubsub.spec import PubSub diff --git a/dimos/protocol/pubsub/jpeg_shm.py b/dimos/protocol/pubsub/jpeg_shm.py new file mode 100644 index 0000000000..68a97ec6b6 --- /dev/null +++ b/dimos/protocol/pubsub/jpeg_shm.py @@ -0,0 +1,20 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.protocol.pubsub.lcmpubsub import JpegSharedMemoryEncoderMixin +from dimos.protocol.pubsub.shmpubsub import SharedMemoryPubSubBase + + +class JpegSharedMemory(JpegSharedMemoryEncoderMixin, SharedMemoryPubSubBase): + pass diff --git a/dimos/protocol/pubsub/lcmpubsub.py b/dimos/protocol/pubsub/lcmpubsub.py new file mode 100644 index 0000000000..ef158ffb30 --- /dev/null +++ b/dimos/protocol/pubsub/lcmpubsub.py @@ -0,0 +1,168 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable + +from turbojpeg import TurboJPEG + +from dimos.msgs.sensor_msgs import Image +from dimos.msgs.sensor_msgs.image_impls.AbstractImage import ImageFormat +from dimos.protocol.pubsub.spec import PickleEncoderMixin, PubSub, PubSubEncoderMixin +from dimos.protocol.service.lcmservice import LCMConfig, LCMService, autoconf +from dimos.utils.logging_config import setup_logger + +if TYPE_CHECKING: + from collections.abc import Callable + import threading + +logger = setup_logger(__name__) + + +@runtime_checkable +class LCMMsg(Protocol): + msg_name: str + + @classmethod + def lcm_decode(cls, data: bytes) -> LCMMsg: + """Decode bytes into an LCM message instance.""" + ... + + def lcm_encode(self) -> bytes: + """Encode this message instance into bytes.""" + ... + + +@dataclass +class Topic: + topic: str = "" + lcm_type: type[LCMMsg] | None = None + + def __str__(self) -> str: + if self.lcm_type is None: + return self.topic + return f"{self.topic}#{self.lcm_type.msg_name}" + + +class LCMPubSubBase(LCMService, PubSub[Topic, Any]): + default_config = LCMConfig + _stop_event: threading.Event + _thread: threading.Thread | None + _callbacks: dict[str, list[Callable[[Any], None]]] + + def __init__(self, **kwargs) -> None: + LCMService.__init__(self, **kwargs) + super().__init__(**kwargs) + self._callbacks = {} + + def publish(self, topic: Topic, message: bytes) -> None: + """Publish a message to the specified channel.""" + if self.l is None: + logger.error("Tried to publish after LCM was closed") + return + self.l.publish(str(topic), message) + + def subscribe( + self, topic: Topic, callback: Callable[[bytes, Topic], Any] + ) -> Callable[[], None]: + if self.l is None: + logger.error("Tried to subscribe after LCM was closed") + + def noop() -> None: + pass + + return noop + + lcm_subscription = self.l.subscribe(str(topic), lambda _, msg: callback(msg, topic)) + + def unsubscribe() -> None: + if self.l is None: + return + self.l.unsubscribe(lcm_subscription) + + return unsubscribe + + +class LCMEncoderMixin(PubSubEncoderMixin[Topic, Any]): + def encode(self, msg: LCMMsg, _: Topic) -> bytes: + return msg.lcm_encode() + + def decode(self, msg: bytes, topic: Topic) -> LCMMsg: + if topic.lcm_type is None: + raise ValueError( + f"Cannot decode message for topic '{topic.topic}': no lcm_type specified" + ) + return topic.lcm_type.lcm_decode(msg) + + +class JpegEncoderMixin(PubSubEncoderMixin[Topic, Any]): + def encode(self, msg: LCMMsg, _: Topic) -> bytes: + return msg.lcm_jpeg_encode() + + def decode(self, msg: bytes, topic: Topic) -> LCMMsg: + if topic.lcm_type is None: + raise ValueError( + f"Cannot decode message for topic '{topic.topic}': no lcm_type specified" + ) + return topic.lcm_type.lcm_jpeg_decode(msg) + + +class JpegSharedMemoryEncoderMixin(PubSubEncoderMixin[str, Image]): + def __init__(self, quality: int = 75, **kwargs): + super().__init__(**kwargs) + self.jpeg = TurboJPEG() + self.quality = quality + + def encode(self, msg: Any, _topic: str) -> bytes: + if not isinstance(msg, Image): + raise ValueError("Can only encode images.") + + bgr_image = msg.to_bgr().to_opencv() + return self.jpeg.encode(bgr_image, quality=self.quality) + + def decode(self, msg: bytes, _topic: str) -> Image: + bgr_array = self.jpeg.decode(msg) + return Image(data=bgr_array, format=ImageFormat.BGR) + + +class LCM( + LCMEncoderMixin, + LCMPubSubBase, +): ... + + +class PickleLCM( + PickleEncoderMixin, + LCMPubSubBase, +): ... + + +class JpegLCM( + JpegEncoderMixin, + LCMPubSubBase, +): ... + + +__all__ = [ + "LCM", + "JpegLCM", + "LCMEncoderMixin", + "LCMMsg", + "LCMMsg", + "LCMPubSubBase", + "PickleLCM", + "autoconf", +] diff --git a/dimos/protocol/pubsub/memory.py b/dimos/protocol/pubsub/memory.py new file mode 100644 index 0000000000..513dfd32cd --- /dev/null +++ b/dimos/protocol/pubsub/memory.py @@ -0,0 +1,60 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections import defaultdict +from collections.abc import Callable +from typing import Any + +from dimos.protocol import encode +from dimos.protocol.pubsub.spec import PubSub, PubSubEncoderMixin + + +class Memory(PubSub[str, Any]): + def __init__(self) -> None: + self._map: defaultdict[str, list[Callable[[Any, str], None]]] = defaultdict(list) + + def publish(self, topic: str, message: Any) -> None: + for cb in self._map[topic]: + cb(message, topic) + + def subscribe(self, topic: str, callback: Callable[[Any, str], None]) -> Callable[[], None]: + self._map[topic].append(callback) + + def unsubscribe() -> None: + try: + self._map[topic].remove(callback) + if not self._map[topic]: + del self._map[topic] + except (KeyError, ValueError): + pass + + return unsubscribe + + def unsubscribe(self, topic: str, callback: Callable[[Any, str], None]) -> None: + try: + self._map[topic].remove(callback) + if not self._map[topic]: + del self._map[topic] + except (KeyError, ValueError): + pass + + +class MemoryWithJSONEncoder(PubSubEncoderMixin, Memory): + """Memory PubSub with JSON encoding/decoding.""" + + def encode(self, msg: Any, topic: str) -> bytes: + return encode.JSON.encode(msg) + + def decode(self, msg: bytes, topic: str) -> Any: + return encode.JSON.decode(msg) diff --git a/dimos/protocol/pubsub/redispubsub.py b/dimos/protocol/pubsub/redispubsub.py new file mode 100644 index 0000000000..7d6c798f2c --- /dev/null +++ b/dimos/protocol/pubsub/redispubsub.py @@ -0,0 +1,198 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections import defaultdict +from collections.abc import Callable +from dataclasses import dataclass, field +import json +import threading +import time +from types import TracebackType +from typing import Any + +import redis + +from dimos.protocol.pubsub.spec import PubSub +from dimos.protocol.service.spec import Service + + +@dataclass +class RedisConfig: + host: str = "localhost" + port: int = 6379 + db: int = 0 + kwargs: dict[str, Any] = field(default_factory=dict) + + +class Redis(PubSub[str, Any], Service[RedisConfig]): + """Redis-based pub/sub implementation.""" + + default_config = RedisConfig + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + + # Redis connections + self._client = None + self._pubsub = None + + # Subscription management + self._callbacks: dict[str, list[Callable[[Any, str], None]]] = defaultdict(list) + self._listener_thread = None + self._running = False + + def start(self) -> None: + """Start the Redis pub/sub service.""" + if self._running: + return + self._connect() + + def stop(self) -> None: + """Stop the Redis pub/sub service.""" + self.close() + + def _connect(self): + """Connect to Redis and set up pub/sub.""" + try: + self._client = redis.Redis( + host=self.config.host, + port=self.config.port, + db=self.config.db, + decode_responses=True, + **self.config.kwargs, + ) + # Test connection + self._client.ping() + + self._pubsub = self._client.pubsub() + self._running = True + + # Start listener thread + self._listener_thread = threading.Thread(target=self._listen_loop, daemon=True) + self._listener_thread.start() + + except Exception as e: + raise ConnectionError( + f"Failed to connect to Redis at {self.config.host}:{self.config.port}: {e}" + ) + + def _listen_loop(self) -> None: + """Listen for messages from Redis and dispatch to callbacks.""" + while self._running: + try: + if not self._pubsub: + break + message = self._pubsub.get_message(timeout=0.1) + if message and message["type"] == "message": + topic = message["channel"] + data = message["data"] + + # Try to deserialize JSON, fall back to raw data + try: + data = json.loads(data) + except (json.JSONDecodeError, TypeError): + pass + + # Call all callbacks for this topic + for callback in self._callbacks.get(topic, []): + try: + callback(data, topic) + except Exception as e: + # Log error but continue processing other callbacks + print(f"Error in callback for topic {topic}: {e}") + + except Exception as e: + if self._running: # Only log if we're still supposed to be running + print(f"Error in Redis listener loop: {e}") + time.sleep(0.1) # Brief pause before retrying + + def publish(self, topic: str, message: Any) -> None: + """Publish a message to a topic.""" + if not self._client: + raise RuntimeError("Redis client not connected") + + # Serialize message as JSON if it's not a string + if isinstance(message, str): + data = message + else: + data = json.dumps(message) + + self._client.publish(topic, data) + + def subscribe(self, topic: str, callback: Callable[[Any, str], None]) -> Callable[[], None]: + """Subscribe to a topic with a callback.""" + if not self._pubsub: + raise RuntimeError("Redis pubsub not initialized") + + # If this is the first callback for this topic, subscribe to Redis channel + if topic not in self._callbacks or not self._callbacks[topic]: + self._pubsub.subscribe(topic) + + # Add callback to our list + self._callbacks[topic].append(callback) + + # Return unsubscribe function + def unsubscribe() -> None: + self.unsubscribe(topic, callback) + + return unsubscribe + + def unsubscribe(self, topic: str, callback: Callable[[Any, str], None]) -> None: + """Unsubscribe a callback from a topic.""" + if topic in self._callbacks: + try: + self._callbacks[topic].remove(callback) + + # If no more callbacks for this topic, unsubscribe from Redis channel + if not self._callbacks[topic]: + if self._pubsub: + self._pubsub.unsubscribe(topic) + del self._callbacks[topic] + + except ValueError: + pass # Callback wasn't in the list + + def close(self) -> None: + """Close Redis connections and stop listener thread.""" + self._running = False + + if self._listener_thread and self._listener_thread.is_alive(): + self._listener_thread.join(timeout=1.0) + + if self._pubsub: + try: + self._pubsub.close() + except Exception: + pass + self._pubsub = None + + if self._client: + try: + self._client.close() + except Exception: + pass + self._client = None + + self._callbacks.clear() + + def __enter__(self): + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() diff --git a/dimos/protocol/pubsub/shm/ipc_factory.py b/dimos/protocol/pubsub/shm/ipc_factory.py new file mode 100644 index 0000000000..9aedbfa1c4 --- /dev/null +++ b/dimos/protocol/pubsub/shm/ipc_factory.py @@ -0,0 +1,309 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# frame_ipc.py +# Python 3.9+ +from abc import ABC, abstractmethod +from multiprocessing.shared_memory import SharedMemory +import os +import time + +import numpy as np + +_UNLINK_ON_GC = os.getenv("DIMOS_IPC_UNLINK_ON_GC", "0").lower() not in ("0", "false", "no") + + +def _open_shm_with_retry(name: str) -> SharedMemory: + tries = int(os.getenv("DIMOS_IPC_ATTACH_RETRIES", "40")) # ~40 tries + base_ms = float(os.getenv("DIMOS_IPC_ATTACH_BACKOFF_MS", "5")) # 5 ms + cap_ms = float(os.getenv("DIMOS_IPC_ATTACH_BACKOFF_CAP_MS", "200")) # 200 ms + last = None + for i in range(tries): + try: + return SharedMemory(name=name) + except FileNotFoundError as e: + last = e + # exponential backoff, capped + time.sleep(min((base_ms * (2**i)), cap_ms) / 1000.0) + raise FileNotFoundError(f"SHM not found after {tries} retries: {name}") from last + + +def _sanitize_shm_name(name: str) -> str: + # Python's SharedMemory expects names like 'psm_abc', without leading '/' + return name.lstrip("/") if isinstance(name, str) else name + + +# --------------------------- +# 1) Abstract interface +# --------------------------- + + +class FrameChannel(ABC): + """Single-slot 'freshest frame' IPC channel with a tiny control block. + - Double-buffered to avoid torn reads. + - Descriptor is JSON-safe; attach() reconstructs in another process. + """ + + @property + @abstractmethod + def device(self) -> str: # "cpu" or "cuda" + ... + + @property + @abstractmethod + def shape(self) -> tuple: ... + + @property + @abstractmethod + def dtype(self) -> np.dtype: ... + + @abstractmethod + def publish(self, frame) -> None: + """Write into inactive buffer, then flip visible index (write control last).""" + ... + + @abstractmethod + def read(self, last_seq: int = -1, require_new: bool = True): + """Return (seq:int, ts_ns:int, view-or-None).""" + ... + + @abstractmethod + def descriptor(self) -> dict: + """Tiny JSON-safe descriptor (names/handles/shape/dtype/device).""" + ... + + @classmethod + @abstractmethod + def attach(cls, desc: dict) -> "FrameChannel": + """Attach in another process.""" + ... + + @abstractmethod + def close(self) -> None: + """Detach resources (owner also unlinks manager if applicable).""" + ... + + +from multiprocessing.shared_memory import SharedMemory +import os +import weakref + + +def _safe_unlink(name: str) -> None: + try: + shm = SharedMemory(name=name) + shm.unlink() + except FileNotFoundError: + pass + except Exception: + pass + + +# --------------------------- +# 2) CPU shared-memory backend +# --------------------------- + + +class CpuShmChannel(FrameChannel): + def __init__( + self, + shape, + dtype=np.uint8, + *, + data_name: str | None = None, + ctrl_name: str | None = None, + ) -> None: + self._shape = tuple(shape) + self._dtype = np.dtype(dtype) + self._nbytes = int(self._dtype.itemsize * np.prod(self._shape)) + + def _create_or_open(name: str, size: int): + try: + shm = SharedMemory(create=True, size=size, name=name) + owner = True + except FileExistsError: + shm = SharedMemory(name=name) # attach existing + owner = False + return shm, owner + + if data_name is None or ctrl_name is None: + # fallback: random names (old behavior) + self._shm_data = SharedMemory(create=True, size=2 * self._nbytes) + self._shm_ctrl = SharedMemory(create=True, size=24) + self._is_owner = True + else: + self._shm_data, own_d = _create_or_open(data_name, 2 * self._nbytes) + self._shm_ctrl, own_c = _create_or_open(ctrl_name, 24) + self._is_owner = own_d and own_c + + self._ctrl = np.ndarray((3,), dtype=np.int64, buffer=self._shm_ctrl.buf) + if self._is_owner: + self._ctrl[:] = 0 # initialize only once + + # only owners set unlink finalizers (beware cross-process timing) + self._finalizer_data = ( + weakref.finalize(self, _safe_unlink, self._shm_data.name) + if (_UNLINK_ON_GC and self._is_owner) + else None + ) + self._finalizer_ctrl = ( + weakref.finalize(self, _safe_unlink, self._shm_ctrl.name) + if (_UNLINK_ON_GC and self._is_owner) + else None + ) + + def descriptor(self): + return { + "kind": "cpu", + "shape": self._shape, + "dtype": self._dtype.str, + "nbytes": self._nbytes, + "data_name": self._shm_data.name, + "ctrl_name": self._shm_ctrl.name, + } + + @property + def device(self) -> str: + return "cpu" + + @property + def shape(self): + return self._shape + + @property + def dtype(self): + return self._dtype + + def publish(self, frame) -> None: + assert isinstance(frame, np.ndarray) + assert frame.shape == self._shape and frame.dtype == self._dtype + active = int(self._ctrl[2]) + inactive = 1 - active + view = np.ndarray( + self._shape, + dtype=self._dtype, + buffer=self._shm_data.buf, + offset=inactive * self._nbytes, + ) + np.copyto(view, frame, casting="no") + ts = np.int64(time.time_ns()) + # Publish order: ts -> idx -> seq + self._ctrl[1] = ts + self._ctrl[2] = inactive + self._ctrl[0] += 1 + + def read(self, last_seq: int = -1, require_new: bool = True): + for _ in range(3): + seq1 = int(self._ctrl[0]) + idx = int(self._ctrl[2]) + ts = int(self._ctrl[1]) + view = np.ndarray( + self._shape, dtype=self._dtype, buffer=self._shm_data.buf, offset=idx * self._nbytes + ) + if seq1 == int(self._ctrl[0]): + if require_new and seq1 == last_seq: + return seq1, ts, None + return seq1, ts, view + return last_seq, 0, None + + def descriptor(self): + return { + "kind": "cpu", + "shape": self._shape, + "dtype": self._dtype.str, + "nbytes": self._nbytes, + "data_name": self._shm_data.name, + "ctrl_name": self._shm_ctrl.name, + } + + @classmethod + def attach(cls, desc: str): + obj = object.__new__(cls) + obj._shape = tuple(desc["shape"]) + obj._dtype = np.dtype(desc["dtype"]) + obj._nbytes = int(desc["nbytes"]) + data_name = desc["data_name"] + ctrl_name = desc["ctrl_name"] + try: + obj._shm_data = _open_shm_with_retry(data_name) + obj._shm_ctrl = _open_shm_with_retry(ctrl_name) + except FileNotFoundError as e: + raise FileNotFoundError( + f"CPU IPC attach failed: control/data SHM not found " + f"(ctrl='{ctrl_name}', data='{data_name}'). " + f"Ensure the writer is running on the same host and the channel is alive." + ) from e + obj._ctrl = np.ndarray((3,), dtype=np.int64, buffer=obj._shm_ctrl.buf) + # attachments don’t own/unlink + obj._finalizer_data = obj._finalizer_ctrl = None + return obj + + def close(self) -> None: + if getattr(self, "_is_owner", False): + try: + self._shm_ctrl.close() + finally: + try: + _safe_unlink(self._shm_ctrl.name) + except: + pass + if hasattr(self, "_shm_data"): + try: + self._shm_data.close() + finally: + try: + _safe_unlink(self._shm_data.name) + except: + pass + return + # readers: just close handles + try: + self._shm_ctrl.close() + except: + pass + try: + self._shm_data.close() + except: + pass + + +# --------------------------- +# 3) Factories +# --------------------------- + + +class CPU_IPC_Factory: + """Creates/attaches CPU shared-memory channels.""" + + @staticmethod + def create(shape, dtype=np.uint8) -> CpuShmChannel: + return CpuShmChannel(shape, dtype=dtype) + + @staticmethod + def attach(desc: dict) -> CpuShmChannel: + assert desc.get("kind") == "cpu", "Descriptor kind mismatch" + return CpuShmChannel.attach(desc) + + +# --------------------------- +# 4) Runtime selector +# --------------------------- + + +def make_frame_channel( + shape, dtype=np.uint8, prefer: str = "auto", device: int = 0 +) -> FrameChannel: + """Choose CUDA IPC if available (or requested), otherwise CPU SHM.""" + # TODO: Implement the CUDA version of creating this factory + return CPU_IPC_Factory.create(shape, dtype=dtype) diff --git a/dimos/protocol/pubsub/shmpubsub.py b/dimos/protocol/pubsub/shmpubsub.py new file mode 100644 index 0000000000..bbbf2192d7 --- /dev/null +++ b/dimos/protocol/pubsub/shmpubsub.py @@ -0,0 +1,324 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# --------------------------------------------------------------------------- +# SharedMemory Pub/Sub over unified IPC channels (CPU/CUDA) +# --------------------------------------------------------------------------- + +from __future__ import annotations + +from collections import defaultdict +from dataclasses import dataclass +import hashlib +import os +import struct +import threading +import time +from typing import TYPE_CHECKING, Any +import uuid + +import numpy as np + +from dimos.protocol.pubsub.shm.ipc_factory import CpuShmChannel +from dimos.protocol.pubsub.spec import PickleEncoderMixin, PubSub, PubSubEncoderMixin +from dimos.utils.logging_config import setup_logger + +if TYPE_CHECKING: + from collections.abc import Callable + +logger = setup_logger("dimos.protocol.pubsub.sharedmemory") + + +# -------------------------------------------------------------------------------------- +# Configuration (kept local to PubSub now that Service is gone) +# -------------------------------------------------------------------------------------- + + +@dataclass +class SharedMemoryConfig: + prefer: str = "auto" # "auto" | "cpu" (DIMOS_IPC_BACKEND overrides), TODO: "cuda" + default_capacity: int = 3686400 # payload bytes (excludes 4-byte header) + close_channels_on_stop: bool = True + + +# -------------------------------------------------------------------------------------- +# Core PubSub with integrated SHM/IPC transport (previously the Service logic) +# -------------------------------------------------------------------------------------- + + +class SharedMemoryPubSubBase(PubSub[str, Any]): + """ + Pub/Sub over SharedMemory/CUDA-IPC, modeled after LCMPubSubBase but self-contained. + Wire format per topic/frame: [len:uint32_le] + payload bytes (padded to fixed capacity). + Features ported from Service: + - start()/stop() lifecycle + - one frame channel per topic + - per-topic fanout thread (reads from channel, invokes subscribers) + - CPU/CUDA backend selection (auto + env override) + - reconfigure(topic, capacity=...) + - drop initial empty frame; synchronous local delivery; echo suppression + """ + + # Per-topic state + # TODO: implement "is_cuda" below capacity, above cp + class _TopicState: + __slots__ = ( + "capacity", + "channel", + "cp", + "dtype", + "last_local_payload", + "last_seq", + "shape", + "stop", + "subs", + "suppress_counts", + "thread", + ) + + def __init__(self, channel, capacity: int, cp_mod) -> None: + self.channel = channel + self.capacity = int(capacity) + self.shape = (self.capacity + 20,) # +20 for header: length(4) + uuid(16) + self.dtype = np.uint8 + self.subs: list[Callable[[bytes, str], None]] = [] + self.stop = threading.Event() + self.thread: threading.Thread | None = None + self.last_seq = 0 # start at 0 to avoid b"" on first poll + # TODO: implement an initializer variable for is_cuda once CUDA IPC is in + self.cp = cp_mod + self.last_local_payload: bytes | None = None + self.suppress_counts: dict[bytes, int] = defaultdict(int) # UUID bytes as key + + # ----- init / lifecycle ------------------------------------------------- + + def __init__( + self, + *, + prefer: str = "auto", + default_capacity: int = 3686400, + close_channels_on_stop: bool = True, + **_: Any, + ) -> None: + super().__init__() + self.config = SharedMemoryConfig( + prefer=prefer, + default_capacity=default_capacity, + close_channels_on_stop=close_channels_on_stop, + ) + self._topics: dict[str, SharedMemoryPubSubBase._TopicState] = {} + self._lock = threading.Lock() + + def start(self) -> None: + pref = (self.config.prefer or "auto").lower() + backend = os.getenv("DIMOS_IPC_BACKEND", pref).lower() + logger.info(f"SharedMemory PubSub starting (backend={backend})") + # No global thread needed; per-topic fanout starts on first subscribe. + + def stop(self) -> None: + with self._lock: + for _topic, st in list(self._topics.items()): + # stop fanout + try: + if st.thread: + st.stop.set() + st.thread.join(timeout=0.5) + st.thread = None + except Exception: + pass + # close/unlink channels if configured + if self.config.close_channels_on_stop: + try: + st.channel.close() + except Exception: + pass + self._topics.clear() + logger.info("SharedMemory PubSub stopped.") + + # ----- PubSub API (bytes on the wire) ---------------------------------- + + def publish(self, topic: str, message: bytes) -> None: + if not isinstance(message, bytes | bytearray | memoryview): + raise TypeError(f"publish expects bytes-like, got {type(message)!r}") + + st = self._ensure_topic(topic) + + # Normalize once + payload_bytes = bytes(message) + L = len(payload_bytes) + if L > st.capacity: + logger.error(f"Payload too large: {L} > capacity {st.capacity}") + raise ValueError(f"Payload too large: {L} > capacity {st.capacity}") + + # Create a unique identifier using UUID4 + message_id = uuid.uuid4().bytes # 16 bytes + + # Mark this message to suppress its echo + st.suppress_counts[message_id] += 1 + + # Synchronous local delivery first (zero extra copies) + for cb in list(st.subs): + try: + cb(payload_bytes, topic) + except Exception: + logger.warn(f"Payload couldn't be pushed to topic: {topic}") + pass + + # Build host frame [len:4] + [uuid:16] + payload and publish + # We embed the message UUID in the frame for echo suppression + host = np.zeros(st.shape, dtype=st.dtype) + # Pack: length(4) + uuid(16) + payload + header = struct.pack(" Callable[[], None]: + """Subscribe a callback(message: bytes, topic). Returns unsubscribe.""" + st = self._ensure_topic(topic) + st.subs.append(callback) + if st.thread is None: + st.thread = threading.Thread(target=self._fanout_loop, args=(topic, st), daemon=True) + st.thread.start() + + def _unsub() -> None: + try: + st.subs.remove(callback) + except ValueError: + pass + if not st.subs and st.thread: + st.stop.set() + st.thread.join(timeout=0.5) + st.thread = None + st.stop.clear() + + return _unsub + + # ----- Capacity mgmt ---------------------------------------------------- + + def reconfigure(self, topic: str, *, capacity: int) -> dict: + """Change payload capacity (bytes) for a topic; returns new descriptor.""" + st = self._ensure_topic(topic) + new_cap = int(capacity) + new_shape = (new_cap + 20,) # +20 for header: length(4) + uuid(16) + desc = st.channel.reconfigure(new_shape, np.uint8) + st.capacity = new_cap + st.shape = new_shape + st.dtype = np.uint8 + st.last_seq = -1 + return desc + + # ----- Internals -------------------------------------------------------- + + def _ensure_topic(self, topic: str) -> _TopicState: + with self._lock: + st = self._topics.get(topic) + if st is not None: + return st + cap = int(self.config.default_capacity) + + def _names_for_topic(topic: str, capacity: int) -> tuple[str, str]: + # Python’s SharedMemory requires names without a leading '/' + h = hashlib.blake2b(f"{topic}:{capacity}".encode(), digest_size=12).hexdigest() + return f"psm_{h}_data", f"psm_{h}_ctrl" + + data_name, ctrl_name = _names_for_topic(topic, cap) + ch = CpuShmChannel((cap + 20,), np.uint8, data_name=data_name, ctrl_name=ctrl_name) + st = SharedMemoryPubSubBase._TopicState(ch, cap, None) + self._topics[topic] = st + return st + + def _fanout_loop(self, topic: str, st: _TopicState) -> None: + while not st.stop.is_set(): + seq, _ts_ns, view = st.channel.read(last_seq=st.last_seq, require_new=True) + if view is None: + time.sleep(0.001) + continue + st.last_seq = seq + + host = np.array(view, copy=True) + + try: + # Read header: length(4) + uuid(16) + L = struct.unpack(" st.capacity + 16: + continue + + # Extract UUID + message_id = host[4:20].tobytes() + + # Extract actual payload (after removing the 16 bytes for uuid) + payload_len = L - 16 + if payload_len > 0: + payload = host[20 : 20 + payload_len].tobytes() + else: + continue + + # Drop exactly the number of local echoes we created + cnt = st.suppress_counts.get(message_id, 0) + if cnt > 0: + if cnt == 1: + del st.suppress_counts[message_id] + else: + st.suppress_counts[message_id] = cnt - 1 + continue # suppressed + + except Exception: + continue + + for cb in list(st.subs): + try: + cb(payload, topic) + except Exception: + pass + + +# -------------------------------------------------------------------------------------- +# Encoders + concrete PubSub classes +# -------------------------------------------------------------------------------------- + + +class SharedMemoryBytesEncoderMixin(PubSubEncoderMixin[str, bytes]): + """Identity encoder for raw bytes.""" + + def encode(self, msg: bytes, _: str) -> bytes: + if isinstance(msg, bytes | bytearray | memoryview): + return bytes(msg) + raise TypeError(f"SharedMemory expects bytes-like, got {type(msg)!r}") + + def decode(self, msg: bytes, _: str) -> bytes: + return msg + + +class SharedMemory( + SharedMemoryBytesEncoderMixin, + SharedMemoryPubSubBase, +): + """SharedMemory pubsub that transports raw bytes.""" + + ... + + +class PickleSharedMemory( + PickleEncoderMixin[str, Any], + SharedMemoryPubSubBase, +): + """SharedMemory pubsub that transports arbitrary Python objects via pickle.""" + + ... diff --git a/dimos/protocol/pubsub/spec.py b/dimos/protocol/pubsub/spec.py new file mode 100644 index 0000000000..ef5a4f450f --- /dev/null +++ b/dimos/protocol/pubsub/spec.py @@ -0,0 +1,154 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC, abstractmethod +import asyncio +from collections.abc import AsyncIterator, Callable +from contextlib import asynccontextmanager +from dataclasses import dataclass +import pickle +from typing import Any, Generic, TypeVar + +from dimos.utils.logging_config import setup_logger + +MsgT = TypeVar("MsgT") +TopicT = TypeVar("TopicT") + + +logger = setup_logger(__name__) + + +class PubSub(Generic[TopicT, MsgT], ABC): + """Abstract base class for pub/sub implementations with sugar methods.""" + + @abstractmethod + def publish(self, topic: TopicT, message: MsgT) -> None: + """Publish a message to a topic.""" + ... + + @abstractmethod + def subscribe( + self, topic: TopicT, callback: Callable[[MsgT, TopicT], None] + ) -> Callable[[], None]: + """Subscribe to a topic with a callback. returns unsubscribe function""" + ... + + @dataclass(slots=True) + class _Subscription: + _bus: "PubSub[Any, Any]" + _topic: Any + _cb: Callable[[Any, Any], None] + _unsubscribe_fn: Callable[[], None] + + def unsubscribe(self) -> None: + self._unsubscribe_fn() + + # context-manager helper + def __enter__(self): + return self + + def __exit__(self, *exc) -> None: + self.unsubscribe() + + # public helper: returns disposable object + def sub(self, topic: TopicT, cb: Callable[[MsgT, TopicT], None]) -> "_Subscription": + unsubscribe_fn = self.subscribe(topic, cb) + return self._Subscription(self, topic, cb, unsubscribe_fn) + + # async iterator + async def aiter(self, topic: TopicT, *, max_pending: int | None = None) -> AsyncIterator[MsgT]: + q: asyncio.Queue[MsgT] = asyncio.Queue(maxsize=max_pending or 0) + + def _cb(msg: MsgT, topic: TopicT) -> None: + q.put_nowait(msg) + + unsubscribe_fn = self.subscribe(topic, _cb) + try: + while True: + yield await q.get() + finally: + unsubscribe_fn() + + # async context manager returning a queue + + @asynccontextmanager + async def queue(self, topic: TopicT, *, max_pending: int | None = None): + q: asyncio.Queue[MsgT] = asyncio.Queue(maxsize=max_pending or 0) + + def _queue_cb(msg: MsgT, topic: TopicT) -> None: + q.put_nowait(msg) + + unsubscribe_fn = self.subscribe(topic, _queue_cb) + try: + yield q + finally: + unsubscribe_fn() + + +class PubSubEncoderMixin(Generic[TopicT, MsgT], ABC): + """Mixin that encodes messages before publishing and decodes them after receiving. + + Usage: Just specify encoder and decoder as a subclass: + + class MyPubSubWithJSON(PubSubEncoderMixin, MyPubSub): + def encoder(msg, topic): + json.dumps(msg).encode('utf-8') + def decoder(msg, topic): + data: json.loads(data.decode('utf-8')) + """ + + @abstractmethod + def encode(self, msg: MsgT, topic: TopicT) -> bytes: ... + + @abstractmethod + def decode(self, msg: bytes, topic: TopicT) -> MsgT: ... + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._encode_callback_map: dict = {} + + def publish(self, topic: TopicT, message: MsgT) -> None: + """Encode the message and publish it.""" + if getattr(self, "_stop_event", None) is not None and self._stop_event.is_set(): + return + encoded_message = self.encode(message, topic) + if encoded_message is None: + return + super().publish(topic, encoded_message) # type: ignore[misc] + + def subscribe( + self, topic: TopicT, callback: Callable[[MsgT, TopicT], None] + ) -> Callable[[], None]: + """Subscribe with automatic decoding.""" + + def wrapper_cb(encoded_data: bytes, topic: TopicT) -> None: + decoded_message = self.decode(encoded_data, topic) + callback(decoded_message, topic) + + return super().subscribe(topic, wrapper_cb) # type: ignore[misc] + + +class PickleEncoderMixin(PubSubEncoderMixin[TopicT, MsgT]): + def encode(self, msg: MsgT, *_: TopicT) -> bytes: + try: + return pickle.dumps(msg) + except Exception as e: + print("Pickle encoding error:", e) + import traceback + + traceback.print_exc() + print("Tried to pickle:", msg) + + def decode(self, msg: bytes, _: TopicT) -> MsgT: + return pickle.loads(msg) diff --git a/dimos/protocol/pubsub/test_encoder.py b/dimos/protocol/pubsub/test_encoder.py new file mode 100644 index 0000000000..9a47c14105 --- /dev/null +++ b/dimos/protocol/pubsub/test_encoder.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +from dimos.protocol.pubsub.memory import Memory, MemoryWithJSONEncoder + + +def test_json_encoded_pubsub() -> None: + """Test memory pubsub with JSON encoding.""" + pubsub = MemoryWithJSONEncoder() + received_messages = [] + + def callback(message, topic) -> None: + received_messages.append(message) + + # Subscribe to a topic + pubsub.subscribe("json_topic", callback) + + # Publish various types of messages + test_messages = [ + "hello world", + 42, + 3.14, + True, + None, + {"name": "Alice", "age": 30, "active": True}, + [1, 2, 3, "four", {"five": 5}], + {"nested": {"data": [1, 2, {"deep": True}]}}, + ] + + for msg in test_messages: + pubsub.publish("json_topic", msg) + + # Verify all messages were received and properly decoded + assert len(received_messages) == len(test_messages) + for original, received in zip(test_messages, received_messages, strict=False): + assert original == received + + +def test_json_encoding_edge_cases() -> None: + """Test edge cases for JSON encoding.""" + pubsub = MemoryWithJSONEncoder() + received_messages = [] + + def callback(message, topic) -> None: + received_messages.append(message) + + pubsub.subscribe("edge_cases", callback) + + # Test edge cases + edge_cases = [ + "", # empty string + [], # empty list + {}, # empty dict + 0, # zero + False, # False boolean + [None, None, None], # list with None values + {"": "empty_key", "null": None, "empty_list": [], "empty_dict": {}}, + ] + + for case in edge_cases: + pubsub.publish("edge_cases", case) + + assert received_messages == edge_cases + + +def test_multiple_subscribers_with_encoding() -> None: + """Test that multiple subscribers work with encoding.""" + pubsub = MemoryWithJSONEncoder() + received_messages_1 = [] + received_messages_2 = [] + + def callback_1(message, topic) -> None: + received_messages_1.append(message) + + def callback_2(message, topic) -> None: + received_messages_2.append(f"callback_2: {message}") + + pubsub.subscribe("json_topic", callback_1) + pubsub.subscribe("json_topic", callback_2) + pubsub.publish("json_topic", {"multi": "subscriber test"}) + + # Both callbacks should receive the message + assert received_messages_1[-1] == {"multi": "subscriber test"} + assert received_messages_2[-1] == "callback_2: {'multi': 'subscriber test'}" + + +# def test_unsubscribe_with_encoding(): +# """Test unsubscribe works correctly with encoded callbacks.""" +# pubsub = MemoryWithJSONEncoder() +# received_messages_1 = [] +# received_messages_2 = [] + +# def callback_1(message): +# received_messages_1.append(message) + +# def callback_2(message): +# received_messages_2.append(message) + +# pubsub.subscribe("json_topic", callback_1) +# pubsub.subscribe("json_topic", callback_2) + +# # Unsubscribe first callback +# pubsub.unsubscribe("json_topic", callback_1) +# pubsub.publish("json_topic", "only callback_2 should get this") + +# # Only callback_2 should receive the message +# assert len(received_messages_1) == 0 +# assert received_messages_2 == ["only callback_2 should get this"] + + +def test_data_actually_encoded_in_transit() -> None: + """Validate that data is actually encoded in transit by intercepting raw bytes.""" + + # Create a spy memory that captures what actually gets published + class SpyMemory(Memory): + def __init__(self) -> None: + super().__init__() + self.raw_messages_received = [] + + def publish(self, topic: str, message) -> None: + # Capture what actually gets published + self.raw_messages_received.append((topic, message, type(message))) + super().publish(topic, message) + + # Create encoder that uses our spy memory + class SpyMemoryWithJSON(MemoryWithJSONEncoder, SpyMemory): + pass + + pubsub = SpyMemoryWithJSON() + received_decoded = [] + + def callback(message, topic) -> None: + received_decoded.append(message) + + pubsub.subscribe("test_topic", callback) + + # Publish a complex object + original_message = {"name": "Alice", "age": 30, "items": [1, 2, 3]} + pubsub.publish("test_topic", original_message) + + # Verify the message was received and decoded correctly + assert len(received_decoded) == 1 + assert received_decoded[0] == original_message + + # Verify the underlying transport actually received JSON bytes, not the original object + assert len(pubsub.raw_messages_received) == 1 + topic, raw_message, raw_type = pubsub.raw_messages_received[0] + + assert topic == "test_topic" + assert raw_type == bytes # Should be bytes, not dict + assert isinstance(raw_message, bytes) + + # Verify it's actually JSON + decoded_raw = json.loads(raw_message.decode("utf-8")) + assert decoded_raw == original_message diff --git a/dimos/protocol/pubsub/test_lcmpubsub.py b/dimos/protocol/pubsub/test_lcmpubsub.py new file mode 100644 index 0000000000..b089483164 --- /dev/null +++ b/dimos/protocol/pubsub/test_lcmpubsub.py @@ -0,0 +1,194 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +import pytest + +from dimos.msgs.geometry_msgs import Pose, Quaternion, Vector3 +from dimos.protocol.pubsub.lcmpubsub import ( + LCM, + LCMPubSubBase, + PickleLCM, + Topic, +) + + +@pytest.fixture +def lcm_pub_sub_base(): + lcm = LCMPubSubBase(autoconf=True) + lcm.start() + yield lcm + lcm.stop() + + +@pytest.fixture +def pickle_lcm(): + lcm = PickleLCM(autoconf=True) + lcm.start() + yield lcm + lcm.stop() + + +@pytest.fixture +def lcm(): + lcm = LCM(autoconf=True) + lcm.start() + yield lcm + lcm.stop() + + +class MockLCMMessage: + """Mock LCM message for testing""" + + msg_name = "geometry_msgs.Mock" + + def __init__(self, data) -> None: + self.data = data + + def lcm_encode(self) -> bytes: + return str(self.data).encode("utf-8") + + @classmethod + def lcm_decode(cls, data: bytes) -> "MockLCMMessage": + return cls(data.decode("utf-8")) + + def __eq__(self, other): + return isinstance(other, MockLCMMessage) and self.data == other.data + + +def test_LCMPubSubBase_pubsub(lcm_pub_sub_base) -> None: + lcm = lcm_pub_sub_base + + received_messages = [] + + topic = Topic(topic="/test_topic", lcm_type=MockLCMMessage) + test_message = MockLCMMessage("test_data") + + def callback(msg, topic) -> None: + received_messages.append((msg, topic)) + + lcm.subscribe(topic, callback) + lcm.publish(topic, test_message.lcm_encode()) + time.sleep(0.1) + + assert len(received_messages) == 1 + + received_data = received_messages[0][0] + received_topic = received_messages[0][1] + + print(f"Received data: {received_data}, Topic: {received_topic}") + + assert isinstance(received_data, bytes) + assert received_data.decode() == "test_data" + + assert isinstance(received_topic, Topic) + assert received_topic == topic + + +def test_lcm_autodecoder_pubsub(lcm) -> None: + received_messages = [] + + topic = Topic(topic="/test_topic", lcm_type=MockLCMMessage) + test_message = MockLCMMessage("test_data") + + def callback(msg, topic) -> None: + received_messages.append((msg, topic)) + + lcm.subscribe(topic, callback) + lcm.publish(topic, test_message) + time.sleep(0.1) + + assert len(received_messages) == 1 + + received_data = received_messages[0][0] + received_topic = received_messages[0][1] + + print(f"Received data: {received_data}, Topic: {received_topic}") + + assert isinstance(received_data, MockLCMMessage) + assert received_data == test_message + + assert isinstance(received_topic, Topic) + assert received_topic == topic + + +test_msgs = [ + (Vector3(1, 2, 3)), + (Quaternion(1, 2, 3, 4)), + (Pose(Vector3(1, 2, 3), Quaternion(0, 0, 0, 1))), +] + + +# passes some geometry types through LCM +@pytest.mark.parametrize("test_message", test_msgs) +def test_lcm_geometry_msgs_pubsub(test_message, lcm) -> None: + received_messages = [] + + topic = Topic(topic="/test_topic", lcm_type=test_message.__class__) + + def callback(msg, topic) -> None: + received_messages.append((msg, topic)) + + lcm.subscribe(topic, callback) + lcm.publish(topic, test_message) + + time.sleep(0.1) + + assert len(received_messages) == 1 + + received_data = received_messages[0][0] + received_topic = received_messages[0][1] + + print(f"Received data: {received_data}, Topic: {received_topic}") + + assert isinstance(received_data, test_message.__class__) + assert received_data == test_message + + assert isinstance(received_topic, Topic) + assert received_topic == topic + + print(test_message, topic) + + +# passes some geometry types through pickle LCM +@pytest.mark.parametrize("test_message", test_msgs) +def test_lcm_geometry_msgs_autopickle_pubsub(test_message, pickle_lcm) -> None: + lcm = pickle_lcm + received_messages = [] + + topic = Topic(topic="/test_topic") + + def callback(msg, topic) -> None: + received_messages.append((msg, topic)) + + lcm.subscribe(topic, callback) + lcm.publish(topic, test_message) + + time.sleep(0.1) + + assert len(received_messages) == 1 + + received_data = received_messages[0][0] + received_topic = received_messages[0][1] + + print(f"Received data: {received_data}, Topic: {received_topic}") + + assert isinstance(received_data, test_message.__class__) + assert received_data == test_message + + assert isinstance(received_topic, Topic) + assert received_topic == topic + + print(test_message, topic) diff --git a/dimos/protocol/pubsub/test_spec.py b/dimos/protocol/pubsub/test_spec.py new file mode 100644 index 0000000000..2bc8ae3ea1 --- /dev/null +++ b/dimos/protocol/pubsub/test_spec.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +from collections.abc import Callable +from contextlib import contextmanager +import time +from typing import Any + +import pytest + +from dimos.msgs.geometry_msgs import Vector3 +from dimos.protocol.pubsub.memory import Memory + + +@contextmanager +def memory_context(): + """Context manager for Memory PubSub implementation.""" + memory = Memory() + try: + yield memory + finally: + # Cleanup logic can be added here if needed + pass + + +# Use Any for context manager type to accommodate both Memory and Redis +testdata: list[tuple[Callable[[], Any], Any, list[Any]]] = [ + (memory_context, "topic", ["value1", "value2", "value3"]), +] + +try: + from dimos.protocol.pubsub.redispubsub import Redis + + @contextmanager + def redis_context(): + redis_pubsub = Redis() + redis_pubsub.start() + yield redis_pubsub + redis_pubsub.stop() + + testdata.append( + (redis_context, "redis_topic", ["redis_value1", "redis_value2", "redis_value3"]) + ) + +except (ConnectionError, ImportError): + # either redis is not installed or the server is not running + print("Redis not available") + + +try: + from dimos.protocol.pubsub.lcmpubsub import LCM, Topic + + @contextmanager + def lcm_context(): + lcm_pubsub = LCM(autoconf=True) + lcm_pubsub.start() + yield lcm_pubsub + lcm_pubsub.stop() + + testdata.append( + ( + lcm_context, + Topic(topic="/test_topic", lcm_type=Vector3), + [Vector3(1, 2, 3), Vector3(4, 5, 6), Vector3(7, 8, 9)], # Using Vector3 as mock data, + ) + ) + +except (ConnectionError, ImportError): + # either redis is not installed or the server is not running + print("LCM not available") + + +from dimos.protocol.pubsub.shmpubsub import PickleSharedMemory + + +@contextmanager +def shared_memory_cpu_context(): + shared_mem_pubsub = PickleSharedMemory(prefer="cpu") + shared_mem_pubsub.start() + yield shared_mem_pubsub + shared_mem_pubsub.stop() + + +testdata.append( + ( + shared_memory_cpu_context, + "/shared_mem_topic_cpu", + [b"shared_mem_value1", b"shared_mem_value2", b"shared_mem_value3"], + ) +) + + +@pytest.mark.parametrize("pubsub_context, topic, values", testdata) +def test_store(pubsub_context, topic, values) -> None: + with pubsub_context() as x: + # Create a list to capture received messages + received_messages = [] + + # Define callback function that stores received messages + def callback(message, _) -> None: + received_messages.append(message) + + # Subscribe to the topic with our callback + x.subscribe(topic, callback) + + # Publish the first value to the topic + x.publish(topic, values[0]) + + # Give Redis time to process the message if needed + time.sleep(0.1) + + print("RECEIVED", received_messages) + # Verify the callback was called with the correct value + assert len(received_messages) == 1 + assert received_messages[0] == values[0] + + +@pytest.mark.parametrize("pubsub_context, topic, values", testdata) +def test_multiple_subscribers(pubsub_context, topic, values) -> None: + """Test that multiple subscribers receive the same message.""" + with pubsub_context() as x: + # Create lists to capture received messages for each subscriber + received_messages_1 = [] + received_messages_2 = [] + + # Define callback functions + def callback_1(message, topic) -> None: + received_messages_1.append(message) + + def callback_2(message, topic) -> None: + received_messages_2.append(message) + + # Subscribe both callbacks to the same topic + x.subscribe(topic, callback_1) + x.subscribe(topic, callback_2) + + # Publish the first value + x.publish(topic, values[0]) + + # Give Redis time to process the message if needed + time.sleep(0.1) + + # Verify both callbacks received the message + assert len(received_messages_1) == 1 + assert received_messages_1[0] == values[0] + assert len(received_messages_2) == 1 + assert received_messages_2[0] == values[0] + + +@pytest.mark.parametrize("pubsub_context, topic, values", testdata) +def test_unsubscribe(pubsub_context, topic, values) -> None: + """Test that unsubscribed callbacks don't receive messages.""" + with pubsub_context() as x: + # Create a list to capture received messages + received_messages = [] + + # Define callback function + def callback(message, topic) -> None: + received_messages.append(message) + + # Subscribe and get unsubscribe function + unsubscribe = x.subscribe(topic, callback) + + # Unsubscribe using the returned function + unsubscribe() + + # Publish the first value + x.publish(topic, values[0]) + + # Give time to process the message if needed + time.sleep(0.1) + + # Verify the callback was not called after unsubscribing + assert len(received_messages) == 0 + + +@pytest.mark.parametrize("pubsub_context, topic, values", testdata) +def test_multiple_messages(pubsub_context, topic, values) -> None: + """Test that subscribers receive multiple messages in order.""" + with pubsub_context() as x: + # Create a list to capture received messages + received_messages = [] + + # Define callback function + def callback(message, topic) -> None: + received_messages.append(message) + + # Subscribe to the topic + x.subscribe(topic, callback) + + # Publish the rest of the values (after the first one used in basic tests) + messages_to_send = values[1:] if len(values) > 1 else values + for msg in messages_to_send: + x.publish(topic, msg) + + # Give Redis time to process the messages if needed + time.sleep(0.2) + + # Verify all messages were received in order + assert len(received_messages) == len(messages_to_send) + assert received_messages == messages_to_send + + +@pytest.mark.parametrize("pubsub_context, topic, values", testdata) +@pytest.mark.asyncio +async def test_async_iterator(pubsub_context, topic, values) -> None: + """Test that async iterator receives messages correctly.""" + with pubsub_context() as x: + # Get the messages to send (using the rest of the values) + messages_to_send = values[1:] if len(values) > 1 else values + received_messages = [] + + # Create the async iterator + async_iter = x.aiter(topic) + + # Create a task to consume messages from the async iterator + async def consume_messages() -> None: + try: + async for message in async_iter: + received_messages.append(message) + # Stop after receiving all expected messages + if len(received_messages) >= len(messages_to_send): + break + except asyncio.CancelledError: + pass + + # Start the consumer task + consumer_task = asyncio.create_task(consume_messages()) + + # Give the consumer a moment to set up + await asyncio.sleep(0.1) + + # Publish messages + for msg in messages_to_send: + x.publish(topic, msg) + # Small delay to ensure message is processed + await asyncio.sleep(0.1) + + # Wait for the consumer to finish or timeout + try: + await asyncio.wait_for(consumer_task, timeout=1.0) # Longer timeout for Redis + except asyncio.TimeoutError: + consumer_task.cancel() + try: + await consumer_task + except asyncio.CancelledError: + pass + + # Verify all messages were received in order + assert len(received_messages) == len(messages_to_send) + assert received_messages == messages_to_send diff --git a/dimos/protocol/rpc/__init__.py b/dimos/protocol/rpc/__init__.py new file mode 100644 index 0000000000..4061c9e9cf --- /dev/null +++ b/dimos/protocol/rpc/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.protocol.rpc.lcmrpc import LCMRPC +from dimos.protocol.rpc.spec import RPCClient, RPCServer, RPCSpec diff --git a/dimos/protocol/rpc/lcmrpc.py b/dimos/protocol/rpc/lcmrpc.py new file mode 100644 index 0000000000..7ff98b1338 --- /dev/null +++ b/dimos/protocol/rpc/lcmrpc.py @@ -0,0 +1,27 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.constants import LCM_MAX_CHANNEL_NAME_LENGTH +from dimos.protocol.pubsub.lcmpubsub import PickleLCM, Topic +from dimos.protocol.rpc.pubsubrpc import PassThroughPubSubRPC +from dimos.utils.generic import short_id + + +class LCMRPC(PassThroughPubSubRPC, PickleLCM): + def topicgen(self, name: str, req_or_res: bool) -> Topic: + suffix = "res" if req_or_res else "req" + topic = f"/rpc/{name}/{suffix}" + if len(topic) > LCM_MAX_CHANNEL_NAME_LENGTH: + topic = f"/rpc/{short_id(name)}/{suffix}" + return Topic(topic=topic) diff --git a/dimos/protocol/rpc/off_test_pubsubrpc.py b/dimos/protocol/rpc/off_test_pubsubrpc.py new file mode 100644 index 0000000000..940baad2f7 --- /dev/null +++ b/dimos/protocol/rpc/off_test_pubsubrpc.py @@ -0,0 +1,216 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Callable +from contextlib import contextmanager +import time + +import pytest + +from dimos.core import Module, rpc, start +from dimos.protocol.rpc.lcmrpc import LCMRPC +from dimos.protocol.service.lcmservice import autoconf + +testgrid: list[Callable] = [] + + +# test module we'll use for binding RPC methods +class MyModule(Module): + @rpc + def add(self, a: int, b: int = 30) -> int: + print(f"A + B = {a + b}") + return a + b + + @rpc + def subtract(self, a: int, b: int) -> int: + print(f"A - B = {a - b}") + return a - b + + +# This tests a generic RPC-over-PubSub implementation that can be used via any +# pubsub transport such as LCM or Redis in this test. +# +# (For transport systems that have call/reply type of functionaltity, we will +# not use PubSubRPC but implement protocol native RPC conforimg to +# RPCClient/RPCServer spec in spec.py) + + +# LCMRPC (mixed in PassThroughPubSubRPC into lcm pubsub) +@contextmanager +def lcm_rpc_context(): + server = LCMRPC(autoconf=True) + client = LCMRPC(autoconf=True) + server.start() + client.start() + yield [server, client] + server.stop() + client.stop() + + +testgrid.append(lcm_rpc_context) + + +# RedisRPC (mixed in in PassThroughPubSubRPC into redis pubsub) +try: + from dimos.protocol.rpc.redisrpc import RedisRPC + + @contextmanager + def redis_rpc_context(): + server = RedisRPC() + client = RedisRPC() + server.start() + client.start() + yield [server, client] + server.stop() + client.stop() + + testgrid.append(redis_rpc_context) + +except (ConnectionError, ImportError): + print("Redis not available") + + +@pytest.mark.parametrize("rpc_context", testgrid) +def test_basics(rpc_context) -> None: + with rpc_context() as (server, client): + + def remote_function(a: int, b: int): + return a + b + + # You can bind an arbitrary function to arbitrary name + # topics are: + # + # - /rpc/add/req + # - /rpc/add/res + server.serve_rpc(remote_function, "add") + + msgs = [] + + def receive_msg(response) -> None: + msgs.append(response) + print(f"Received response: {response}") + + client.call("add", ([1, 2], {}), receive_msg) + + time.sleep(0.1) + assert len(msgs) > 0 + + +@pytest.mark.parametrize("rpc_context", testgrid) +def test_module_autobind(rpc_context) -> None: + with rpc_context() as (server, client): + module = MyModule() + print("\n") + + # We take an endpoint name from __class__.__name__, + # so topics are: + # + # - /rpc/MyModule/method_name1/req + # - /rpc/MyModule/method_name1/res + # + # - /rpc/MyModule/method_name2/req + # - /rpc/MyModule/method_name2/res + # + # etc + server.serve_module_rpc(module) + + # can override the __class__.__name__ with something else + server.serve_module_rpc(module, "testmodule") + + msgs = [] + + def receive_msg(msg) -> None: + msgs.append(msg) + + client.call("MyModule/add", ([1, 2], {}), receive_msg) + client.call("testmodule/subtract", ([3, 1], {}), receive_msg) + + time.sleep(0.1) + assert len(msgs) == 2 + assert msgs == [3, 2] + + +# Default rpc.call() either doesn't wait for response or accepts a callback +# but also we support different calling strategies, +# +# can do blocking calls +@pytest.mark.parametrize("rpc_context", testgrid) +def test_sync(rpc_context) -> None: + with rpc_context() as (server, client): + module = MyModule() + print("\n") + + server.serve_module_rpc(module) + assert 3 == client.call_sync("MyModule/add", ([1, 2], {}))[0] + + +# Default rpc.call() either doesn't wait for response or accepts a callback +# but also we support different calling strategies, +# +# can do blocking calls +@pytest.mark.parametrize("rpc_context", testgrid) +def test_kwargs(rpc_context) -> None: + with rpc_context() as (server, client): + module = MyModule() + print("\n") + + server.serve_module_rpc(module) + + assert 3 == client.call_sync("MyModule/add", ([1, 2], {}))[0] + + +# or async calls as well +@pytest.mark.parametrize("rpc_context", testgrid) +@pytest.mark.asyncio +async def test_async(rpc_context) -> None: + with rpc_context() as (server, client): + module = MyModule() + print("\n") + server.serve_module_rpc(module) + assert 3 == await client.call_async("MyModule/add", ([1, 2], {})) + + +# or async calls as well +@pytest.mark.module +def test_rpc_full_deploy() -> None: + autoconf() + + # test module we'll use for binding RPC methods + class CallerModule(Module): + remote: Callable[[int, int], int] + + def __init__(self, remote: Callable[[int, int], int]) -> None: + self.remote = remote + super().__init__() + + @rpc + def add(self, a: int, b: int = 30) -> int: + return self.remote(a, b) + + dimos = start(2) + + module = dimos.deploy(MyModule) + caller = dimos.deploy(CallerModule, module.add) + + print("deployed", module) + print("deployed", caller) + + # standard list args + assert caller.add(1, 2) == 3 + # default args + assert caller.add(1) == 31 + # kwargs + assert caller.add(1, b=1) == 2 + + dimos.shutdown() diff --git a/dimos/protocol/rpc/pubsubrpc.py b/dimos/protocol/rpc/pubsubrpc.py new file mode 100644 index 0000000000..033cb7a5e2 --- /dev/null +++ b/dimos/protocol/rpc/pubsubrpc.py @@ -0,0 +1,154 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from abc import abstractmethod +from collections.abc import Callable +import time +from typing import ( + TYPE_CHECKING, + Any, + Generic, + TypedDict, + TypeVar, +) + +from dimos.protocol.pubsub.spec import PubSub +from dimos.protocol.rpc.spec import Args, RPCSpec +from dimos.utils.logging_config import setup_logger + +if TYPE_CHECKING: + from types import FunctionType + +logger = setup_logger(__file__) + +MsgT = TypeVar("MsgT") +TopicT = TypeVar("TopicT") + +# (name, true_if_response_topic) -> TopicT +TopicGen = Callable[[str, bool], TopicT] +MsgGen = Callable[[str, list], MsgT] + + +class RPCReq(TypedDict): + id: float | None + name: str + args: Args + + +class RPCRes(TypedDict): + id: float + res: Any + + +class PubSubRPCMixin(RPCSpec, PubSub[TopicT, MsgT], Generic[TopicT, MsgT]): + @abstractmethod + def topicgen(self, name: str, req_or_res: bool) -> TopicT: ... + + @abstractmethod + def _decodeRPCRes(self, msg: MsgT) -> RPCRes: ... + + @abstractmethod + def _decodeRPCReq(self, msg: MsgT) -> RPCReq: ... + + @abstractmethod + def _encodeRPCReq(self, res: RPCReq) -> MsgT: ... + + @abstractmethod + def _encodeRPCRes(self, res: RPCRes) -> MsgT: ... + + def call(self, name: str, arguments: Args, cb: Callable | None): + if cb is None: + return self.call_nowait(name, arguments) + + return self.call_cb(name, arguments, cb) + + def call_cb(self, name: str, arguments: Args, cb: Callable) -> Any: + topic_req = self.topicgen(name, False) + topic_res = self.topicgen(name, True) + msg_id = float(time.time()) + + req: RPCReq = {"name": name, "args": arguments, "id": msg_id} + + def receive_response(msg: MsgT, _: TopicT) -> None: + res = self._decodeRPCRes(msg) + if res.get("id") != msg_id: + return + time.sleep(0.01) + if unsub is not None: + unsub() + cb(res.get("res")) + + unsub = self.subscribe(topic_res, receive_response) + + self.publish(topic_req, self._encodeRPCReq(req)) + return unsub + + def call_nowait(self, name: str, arguments: Args) -> None: + topic_req = self.topicgen(name, False) + req: RPCReq = {"name": name, "args": arguments, "id": None} + self.publish(topic_req, self._encodeRPCReq(req)) + + def serve_rpc(self, f: FunctionType, name: str | None = None): + if not name: + name = f.__name__ + + topic_req = self.topicgen(name, False) + topic_res = self.topicgen(name, True) + + def receive_call(msg: MsgT, _: TopicT) -> None: + req = self._decodeRPCReq(msg) + + if req.get("name") != name: + return + args = req.get("args") + if args is None: + return + + # Execute RPC handler in a separate thread to avoid deadlock when + # the handler makes nested RPC calls. + def execute_and_respond() -> None: + try: + response = f(*args[0], **args[1]) + req_id = req.get("id") + if req_id is not None: + self.publish(topic_res, self._encodeRPCRes({"id": req_id, "res": response})) + except Exception as e: + logger.exception(f"Exception in RPC handler for {name}: {e}", exc_info=e) + + get_thread_pool = getattr(self, "_get_call_thread_pool", None) + if get_thread_pool: + get_thread_pool().submit(execute_and_respond) + else: + execute_and_respond() + + return self.subscribe(topic_req, receive_call) + + +# simple PUBSUB RPC implementation that doesn't encode +# special request/response messages, assumes pubsub implementation +# supports generic dictionary pubsub +class PassThroughPubSubRPC(PubSubRPCMixin[TopicT, dict], Generic[TopicT]): + def _encodeRPCReq(self, req: RPCReq) -> dict: + return dict(req) + + def _decodeRPCRes(self, msg: dict) -> RPCRes: + return msg # type: ignore[return-value] + + def _encodeRPCRes(self, res: RPCRes) -> dict: + return dict(res) + + def _decodeRPCReq(self, msg: dict) -> RPCReq: + return msg # type: ignore[return-value] diff --git a/dimos/protocol/rpc/redisrpc.py b/dimos/protocol/rpc/redisrpc.py new file mode 100644 index 0000000000..b0a715fe43 --- /dev/null +++ b/dimos/protocol/rpc/redisrpc.py @@ -0,0 +1,21 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.protocol.pubsub.redispubsub import Redis +from dimos.protocol.rpc.pubsubrpc import PassThroughPubSubRPC + + +class RedisRPC(PassThroughPubSubRPC, Redis): + def topicgen(self, name: str, req_or_res: bool) -> str: + return f"/rpc/{name}/{'res' if req_or_res else 'req'}" diff --git a/dimos/protocol/rpc/spec.py b/dimos/protocol/rpc/spec.py new file mode 100644 index 0000000000..283b84f1dd --- /dev/null +++ b/dimos/protocol/rpc/spec.py @@ -0,0 +1,90 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +from collections.abc import Callable +import threading +from typing import Any, Protocol, overload + + +class Empty: ... + + +Args = tuple[list, dict[str, Any]] + + +# module that we can inspect for RPCs +class RPCInspectable(Protocol): + @property + def rpcs(self) -> dict[str, Callable]: ... + + +class RPCClient(Protocol): + # if we don't provide callback, we don't get a return unsub f + @overload + def call(self, name: str, arguments: Args, cb: None) -> None: ... + + # if we provide callback, we do get return unsub f + @overload + def call(self, name: str, arguments: Args, cb: Callable[[Any], None]) -> Callable[[], Any]: ... + + def call(self, name: str, arguments: Args, cb: Callable | None) -> Callable[[], Any] | None: ... + + # we expect to crash if we don't get a return value after 10 seconds + # but callers can override this timeout for extra long functions + def call_sync( + self, name: str, arguments: Args, rpc_timeout: float | None = 30.0 + ) -> tuple[Any, Callable[[], None]]: + event = threading.Event() + + def receive_value(val) -> None: + event.result = val # attach to event + event.set() + + unsub_fn = self.call(name, arguments, receive_value) + if not event.wait(rpc_timeout): + raise TimeoutError(f"RPC call to '{name}' timed out after {rpc_timeout} seconds") + return event.result, unsub_fn + + async def call_async(self, name: str, arguments: Args) -> Any: + loop = asyncio.get_event_loop() + future = loop.create_future() + + def receive_value(val) -> None: + try: + loop.call_soon_threadsafe(future.set_result, val) + except Exception as e: + loop.call_soon_threadsafe(future.set_exception, e) + + self.call(name, arguments, receive_value) + + return await future + + +class RPCServer(Protocol): + def serve_rpc(self, f: Callable, name: str) -> Callable[[], None]: ... + + def serve_module_rpc(self, module: RPCInspectable, name: str | None = None) -> None: + for fname in module.rpcs.keys(): + if not name: + name = module.__class__.__name__ + + def override_f(*args, fname=fname, **kwargs): + return getattr(module, fname)(*args, **kwargs) + + topic = name + "/" + fname + self.serve_rpc(override_f, topic) + + +class RPCSpec(RPCServer, RPCClient): ... diff --git a/dimos/protocol/rpc/test_lcmrpc.py b/dimos/protocol/rpc/test_lcmrpc.py new file mode 100644 index 0000000000..6ee00b23e0 --- /dev/null +++ b/dimos/protocol/rpc/test_lcmrpc.py @@ -0,0 +1,45 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Generator + +import pytest + +from dimos.constants import LCM_MAX_CHANNEL_NAME_LENGTH +from dimos.protocol.rpc.lcmrpc import LCMRPC + + +@pytest.fixture +def lcmrpc() -> Generator[LCMRPC, None, None]: + ret = LCMRPC() + ret.start() + yield ret + ret.stop() + + +def test_short_name(lcmrpc) -> None: + actual = lcmrpc.topicgen("Hello/say", req_or_res=True) + assert actual.topic == "/rpc/Hello/say/res" + + +def test_long_name(lcmrpc) -> None: + long = "GreatyLongComplexExampleClassNameForTestingStuff/create" + long_topic = lcmrpc.topicgen(long, req_or_res=True).topic + assert long_topic == "/rpc/2cudPuFGMJdWxM5KZb/res" + + less_long = long[:-1] + less_long_topic = lcmrpc.topicgen(less_long, req_or_res=True).topic + assert less_long_topic == "/rpc/GreatyLongComplexExampleClassNameForTestingStuff/creat/res" + + assert len(less_long_topic) == LCM_MAX_CHANNEL_NAME_LENGTH diff --git a/dimos/protocol/rpc/test_lcmrpc_timeout.py b/dimos/protocol/rpc/test_lcmrpc_timeout.py new file mode 100644 index 0000000000..74cf4963c7 --- /dev/null +++ b/dimos/protocol/rpc/test_lcmrpc_timeout.py @@ -0,0 +1,164 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import threading +import time + +import pytest + +from dimos.protocol.rpc.lcmrpc import LCMRPC +from dimos.protocol.service.lcmservice import autoconf + + +@pytest.fixture(scope="session", autouse=True) +def setup_lcm_autoconf(): + """Setup LCM autoconf once for the entire test session""" + autoconf() + yield + + +@pytest.fixture +def lcm_server(): + """Fixture that provides started LCMRPC server""" + server = LCMRPC() + server.start() + + yield server + + server.stop() + + +@pytest.fixture +def lcm_client(): + """Fixture that provides started LCMRPC client""" + client = LCMRPC() + client.start() + + yield client + + client.stop() + + +def test_lcmrpc_timeout_no_reply(lcm_server, lcm_client) -> None: + """Test that RPC calls timeout when no reply is received""" + server = lcm_server + client = lcm_client + + # Track if the function was called + function_called = threading.Event() + + # Serve a function that never responds + def never_responds(a: int, b: int): + # Signal that the function was called + function_called.set() + # Simulating a server that receives the request but never sends a reply + time.sleep(1) # Long sleep to ensure timeout happens first + return a + b + + server.serve_rpc(never_responds, "slow_add") + + # Test with call_sync and explicit timeout + start_time = time.time() + + # Should raise TimeoutError when timeout occurs + with pytest.raises(TimeoutError, match="RPC call to 'slow_add' timed out after 0.1 seconds"): + client.call_sync("slow_add", ([1, 2], {}), rpc_timeout=0.1) + + elapsed = time.time() - start_time + + # Should timeout after ~0.1 seconds + assert elapsed < 0.3, f"Timeout took too long: {elapsed}s" + + # Verify the function was actually called + assert function_called.wait(0.5), "Server function was never called" + + +def test_lcmrpc_timeout_nonexistent_service(lcm_client) -> None: + """Test that RPC calls timeout when calling a non-existent service""" + client = lcm_client + + # Call a service that doesn't exist + start_time = time.time() + + # Should raise TimeoutError when timeout occurs + with pytest.raises( + TimeoutError, match="RPC call to 'nonexistent/service' timed out after 0.1 seconds" + ): + client.call_sync("nonexistent/service", ([1, 2], {}), rpc_timeout=0.1) + + elapsed = time.time() - start_time + + # Should timeout after ~0.1 seconds + assert elapsed < 0.3, f"Timeout took too long: {elapsed}s" + + +def test_lcmrpc_callback_with_timeout(lcm_server, lcm_client) -> None: + """Test that callback-based RPC calls handle timeouts properly""" + server = lcm_server + client = lcm_client + # Track if the function was called + function_called = threading.Event() + + # Serve a function that never responds + def never_responds(a: int, b: int): + function_called.set() + time.sleep(1) + return a + b + + server.serve_rpc(never_responds, "slow_add") + + callback_called = threading.Event() + received_value = [] + + def callback(value) -> None: + received_value.append(value) + callback_called.set() + + # Make the call with callback + unsub = client.call("slow_add", ([1, 2], {}), callback) + + # Wait for a short time - callback should not be called + callback_called.wait(0.2) + assert not callback_called.is_set(), "Callback should not have been called" + assert len(received_value) == 0 + + # Verify the server function was actually called + assert function_called.wait(0.5), "Server function was never called" + + # Clean up - unsubscribe if possible + if unsub: + unsub() + + +def test_lcmrpc_normal_operation(lcm_server, lcm_client) -> None: + """Sanity check that normal RPC calls still work""" + server = lcm_server + client = lcm_client + + def quick_add(a: int, b: int): + return a + b + + server.serve_rpc(quick_add, "add") + + # Normal call should work quickly + start_time = time.time() + result = client.call_sync("add", ([5, 3], {}), rpc_timeout=0.5)[0] + elapsed = time.time() - start_time + + assert result == 8 + assert elapsed < 0.2, f"Normal call took too long: {elapsed}s" + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/dimos/protocol/service/__init__.py b/dimos/protocol/service/__init__.py new file mode 100644 index 0000000000..4726ad5f83 --- /dev/null +++ b/dimos/protocol/service/__init__.py @@ -0,0 +1,2 @@ +from dimos.protocol.service.lcmservice import LCMService +from dimos.protocol.service.spec import Configurable, Service diff --git a/dimos/protocol/service/lcmservice.py b/dimos/protocol/service/lcmservice.py new file mode 100644 index 0000000000..1b19a5cfeb --- /dev/null +++ b/dimos/protocol/service/lcmservice.py @@ -0,0 +1,343 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass +from functools import cache +import os +import subprocess +import sys +import threading +import traceback +from typing import Protocol, runtime_checkable + +import lcm + +from dimos.protocol.service.spec import Service +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.protocol.service.lcmservice") + + +@cache +def check_root() -> bool: + """Return True if the current process is running as root (UID 0).""" + try: + return os.geteuid() == 0 # type: ignore[attr-defined] + except AttributeError: + # Platforms without geteuid (e.g. Windows) – assume non-root. + return False + + +def check_multicast() -> list[str]: + """Check if multicast configuration is needed and return required commands.""" + commands_needed = [] + + sudo = "" if check_root() else "sudo " + + # Check if loopback interface has multicast enabled + try: + result = subprocess.run(["ip", "link", "show", "lo"], capture_output=True, text=True) + if "MULTICAST" not in result.stdout: + commands_needed.append(f"{sudo}ifconfig lo multicast") + except Exception: + commands_needed.append(f"{sudo}ifconfig lo multicast") + + # Check if multicast route exists + try: + result = subprocess.run( + ["ip", "route", "show", "224.0.0.0/4"], capture_output=True, text=True + ) + if not result.stdout.strip(): + commands_needed.append(f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo") + except Exception: + commands_needed.append(f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo") + + return commands_needed + + +def check_buffers() -> tuple[list[str], int | None]: + """Check if buffer configuration is needed and return required commands and current size. + + Returns: + Tuple of (commands_needed, current_max_buffer_size) + """ + commands_needed = [] + current_max = None + + sudo = "" if check_root() else "sudo " + + # Check current buffer settings + try: + result = subprocess.run(["sysctl", "net.core.rmem_max"], capture_output=True, text=True) + current_max = int(result.stdout.split("=")[1].strip()) if result.returncode == 0 else None + if not current_max or current_max < 2097152: + commands_needed.append(f"{sudo}sysctl -w net.core.rmem_max=2097152") + except: + commands_needed.append(f"{sudo}sysctl -w net.core.rmem_max=2097152") + + try: + result = subprocess.run(["sysctl", "net.core.rmem_default"], capture_output=True, text=True) + current_default = ( + int(result.stdout.split("=")[1].strip()) if result.returncode == 0 else None + ) + if not current_default or current_default < 2097152: + commands_needed.append(f"{sudo}sysctl -w net.core.rmem_default=2097152") + except: + commands_needed.append(f"{sudo}sysctl -w net.core.rmem_default=2097152") + + return commands_needed, current_max + + +def check_system() -> None: + """Check if system configuration is needed and exit only for critical issues. + + Multicast configuration is critical for LCM to work. + Buffer sizes are performance optimizations - warn but don't fail in containers. + """ + if os.environ.get("CI"): + logger.debug("CI environment detected: Skipping system configuration checks.") + return + + multicast_commands = check_multicast() + buffer_commands, current_buffer_size = check_buffers() + + # Check multicast first - this is critical + if multicast_commands: + logger.error( + "Critical: Multicast configuration required. Please run the following commands:" + ) + for cmd in multicast_commands: + logger.error(f" {cmd}") + logger.error("\nThen restart your application.") + sys.exit(1) + + # Buffer configuration is just for performance + elif buffer_commands: + if current_buffer_size: + logger.warning( + f"UDP buffer size limited to {current_buffer_size} bytes ({current_buffer_size // 1024}KB). Large LCM packets may fail." + ) + else: + logger.warning("UDP buffer sizes are limited. Large LCM packets may fail.") + logger.warning("For better performance, consider running:") + for cmd in buffer_commands: + logger.warning(f" {cmd}") + logger.warning("Note: This may not be possible in Docker containers.") + + +def autoconf() -> None: + """Auto-configure system by running checks and executing required commands if needed.""" + if os.environ.get("CI"): + logger.info("CI environment detected: Skipping automatic system configuration.") + return + + commands_needed = [] + + # Check multicast configuration + commands_needed.extend(check_multicast()) + + # Check buffer configuration + buffer_commands, _ = check_buffers() + commands_needed.extend(buffer_commands) + + if not commands_needed: + return + + logger.info("System configuration required. Executing commands...") + + for cmd in commands_needed: + logger.info(f" Running: {cmd}") + try: + # Split command into parts for subprocess + cmd_parts = cmd.split() + subprocess.run(cmd_parts, capture_output=True, text=True, check=True) + logger.info(" ✓ Success") + except subprocess.CalledProcessError as e: + # Check if this is a multicast/route command or a sysctl command + if "route" in cmd or "multicast" in cmd: + # Multicast/route failures should still fail + logger.error(f" ✗ Failed to configure multicast: {e}") + logger.error(f" stdout: {e.stdout}") + logger.error(f" stderr: {e.stderr}") + raise + elif "sysctl" in cmd: + # Sysctl failures are just warnings (likely docker/container) + logger.warning( + f" ✗ Not able to auto-configure UDP buffer sizes (likely docker image): {e}" + ) + except Exception as e: + logger.error(f" ✗ Error: {e}") + if "route" in cmd or "multicast" in cmd: + raise + + logger.info("System configuration completed.") + + +@dataclass +class LCMConfig: + ttl: int = 0 + url: str | None = None + autoconf: bool = True + lcm: lcm.LCM | None = None + + +@runtime_checkable +class LCMMsg(Protocol): + msg_name: str + + @classmethod + def lcm_decode(cls, data: bytes) -> LCMMsg: + """Decode bytes into an LCM message instance.""" + ... + + def lcm_encode(self) -> bytes: + """Encode this message instance into bytes.""" + ... + + +@dataclass +class Topic: + topic: str = "" + lcm_type: type[LCMMsg] | None = None + + def __str__(self) -> str: + if self.lcm_type is None: + return self.topic + return f"{self.topic}#{self.lcm_type.msg_name}" + + +class LCMService(Service[LCMConfig]): + default_config = LCMConfig + l: lcm.LCM | None + _stop_event: threading.Event + _l_lock: threading.Lock + _thread: threading.Thread | None + _call_thread_pool: ThreadPoolExecutor | None = None + _call_thread_pool_lock: threading.RLock = threading.RLock() + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + + # we support passing an existing LCM instance + if self.config.lcm: + # TODO: If we pass LCM in, it's unsafe to use in this thread and the _loop thread. + self.l = self.config.lcm + else: + self.l = lcm.LCM(self.config.url) if self.config.url else lcm.LCM() + + self._l_lock = threading.Lock() + + self._stop_event = threading.Event() + self._thread = None + + def __getstate__(self): + """Exclude unpicklable runtime attributes when serializing.""" + state = self.__dict__.copy() + # Remove unpicklable attributes + state.pop("l", None) + state.pop("_stop_event", None) + state.pop("_thread", None) + state.pop("_l_lock", None) + state.pop("_call_thread_pool", None) + state.pop("_call_thread_pool_lock", None) + return state + + def __setstate__(self, state) -> None: + """Restore object from pickled state.""" + self.__dict__.update(state) + # Reinitialize runtime attributes + self.l = None + self._stop_event = threading.Event() + self._thread = None + self._l_lock = threading.Lock() + self._call_thread_pool = None + self._call_thread_pool_lock = threading.RLock() + + def start(self) -> None: + # Reinitialize LCM if it's None (e.g., after unpickling) + if self.l is None: + if self.config.lcm: + self.l = self.config.lcm + else: + self.l = lcm.LCM(self.config.url) if self.config.url else lcm.LCM() + + if self.config.autoconf: + autoconf() + else: + try: + check_system() + except Exception as e: + print(f"Error checking system configuration: {e}") + + self._stop_event.clear() + self._thread = threading.Thread(target=self._lcm_loop) + self._thread.daemon = True + self._thread.start() + + def _lcm_loop(self) -> None: + """LCM message handling loop.""" + while not self._stop_event.is_set(): + try: + with self._l_lock: + if self.l is None: + break + self.l.handle_timeout(50) + except Exception as e: + stack_trace = traceback.format_exc() + print(f"Error in LCM handling: {e}\n{stack_trace}") + + def stop(self) -> None: + """Stop the LCM loop.""" + self._stop_event.set() + if self._thread is not None: + # Only join if we're not the LCM thread (avoid "cannot join current thread") + if threading.current_thread() != self._thread: + self._thread.join(timeout=1.0) + if self._thread.is_alive(): + logger.warning("LCM thread did not stop cleanly within timeout") + + # Clean up LCM instance if we created it + if not self.config.lcm: + with self._l_lock: + if self.l is not None: + del self.l + self.l = None + + with self._call_thread_pool_lock: + if self._call_thread_pool: + # Check if we're being called from within the thread pool + # If so, we can't wait for shutdown (would cause "cannot join current thread") + current_thread = threading.current_thread() + is_pool_thread = False + + # Check if current thread is one of the pool's threads + # ThreadPoolExecutor threads have names like "ThreadPoolExecutor-N_M" + if hasattr(self._call_thread_pool, "_threads"): + is_pool_thread = current_thread in self._call_thread_pool._threads + elif "ThreadPoolExecutor" in current_thread.name: + # Fallback: check thread name pattern + is_pool_thread = True + + # Don't wait if we're in a pool thread to avoid deadlock + self._call_thread_pool.shutdown(wait=not is_pool_thread) + self._call_thread_pool = None + + def _get_call_thread_pool(self) -> ThreadPoolExecutor: + with self._call_thread_pool_lock: + if self._call_thread_pool is None: + self._call_thread_pool = ThreadPoolExecutor(max_workers=4) + return self._call_thread_pool diff --git a/dimos/protocol/service/spec.py b/dimos/protocol/service/spec.py new file mode 100644 index 0000000000..d55c1bfacf --- /dev/null +++ b/dimos/protocol/service/spec.py @@ -0,0 +1,34 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC +from typing import Generic, TypeVar + +# Generic type for service configuration +ConfigT = TypeVar("ConfigT") + + +class Configurable(Generic[ConfigT]): + default_config: type[ConfigT] + + def __init__(self, **kwargs) -> None: + self.config: ConfigT = self.default_config(**kwargs) + + +class Service(Configurable[ConfigT], ABC): + def start(self) -> None: + super().start() + + def stop(self) -> None: + super().stop() diff --git a/dimos/protocol/service/test_lcmservice.py b/dimos/protocol/service/test_lcmservice.py new file mode 100644 index 0000000000..1c9a51b2e5 --- /dev/null +++ b/dimos/protocol/service/test_lcmservice.py @@ -0,0 +1,422 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import subprocess +from unittest.mock import patch + +import pytest + +from dimos.protocol.service.lcmservice import ( + autoconf, + check_buffers, + check_multicast, + check_root, +) + + +def get_sudo_prefix() -> str: + """Return 'sudo ' if not running as root, empty string if running as root.""" + return "" if check_root() else "sudo " + + +def test_check_multicast_all_configured() -> None: + """Test check_multicast when system is properly configured.""" + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock successful checks with realistic output format + mock_run.side_effect = [ + type( + "MockResult", + (), + { + "stdout": "1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\n link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00", + "returncode": 0, + }, + )(), + type("MockResult", (), {"stdout": "224.0.0.0/4 dev lo scope link", "returncode": 0})(), + ] + + result = check_multicast() + assert result == [] + + +def test_check_multicast_missing_multicast_flag() -> None: + """Test check_multicast when loopback interface lacks multicast.""" + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock interface without MULTICAST flag (realistic current system state) + mock_run.side_effect = [ + type( + "MockResult", + (), + { + "stdout": "1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\n link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00", + "returncode": 0, + }, + )(), + type("MockResult", (), {"stdout": "224.0.0.0/4 dev lo scope link", "returncode": 0})(), + ] + + result = check_multicast() + sudo = get_sudo_prefix() + assert result == [f"{sudo}ifconfig lo multicast"] + + +def test_check_multicast_missing_route() -> None: + """Test check_multicast when multicast route is missing.""" + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock missing route - interface has multicast but no route + mock_run.side_effect = [ + type( + "MockResult", + (), + { + "stdout": "1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\n link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00", + "returncode": 0, + }, + )(), + type("MockResult", (), {"stdout": "", "returncode": 0})(), # Empty output - no route + ] + + result = check_multicast() + sudo = get_sudo_prefix() + assert result == [f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo"] + + +def test_check_multicast_all_missing() -> None: + """Test check_multicast when both multicast flag and route are missing (current system state).""" + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock both missing - matches actual current system state + mock_run.side_effect = [ + type( + "MockResult", + (), + { + "stdout": "1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\n link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00", + "returncode": 0, + }, + )(), + type("MockResult", (), {"stdout": "", "returncode": 0})(), # Empty output - no route + ] + + result = check_multicast() + sudo = get_sudo_prefix() + expected = [ + f"{sudo}ifconfig lo multicast", + f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo", + ] + assert result == expected + + +def test_check_multicast_subprocess_exception() -> None: + """Test check_multicast when subprocess calls fail.""" + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock subprocess exceptions + mock_run.side_effect = Exception("Command failed") + + result = check_multicast() + sudo = get_sudo_prefix() + expected = [ + f"{sudo}ifconfig lo multicast", + f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo", + ] + assert result == expected + + +def test_check_buffers_all_configured() -> None: + """Test check_buffers when system is properly configured.""" + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock sufficient buffer sizes + mock_run.side_effect = [ + type("MockResult", (), {"stdout": "net.core.rmem_max = 2097152", "returncode": 0})(), + type( + "MockResult", (), {"stdout": "net.core.rmem_default = 2097152", "returncode": 0} + )(), + ] + + commands, buffer_size = check_buffers() + assert commands == [] + assert buffer_size == 2097152 + + +def test_check_buffers_low_max_buffer() -> None: + """Test check_buffers when rmem_max is too low.""" + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock low rmem_max + mock_run.side_effect = [ + type("MockResult", (), {"stdout": "net.core.rmem_max = 1048576", "returncode": 0})(), + type( + "MockResult", (), {"stdout": "net.core.rmem_default = 2097152", "returncode": 0} + )(), + ] + + commands, buffer_size = check_buffers() + sudo = get_sudo_prefix() + assert commands == [f"{sudo}sysctl -w net.core.rmem_max=2097152"] + assert buffer_size == 1048576 + + +def test_check_buffers_low_default_buffer() -> None: + """Test check_buffers when rmem_default is too low.""" + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock low rmem_default + mock_run.side_effect = [ + type("MockResult", (), {"stdout": "net.core.rmem_max = 2097152", "returncode": 0})(), + type( + "MockResult", (), {"stdout": "net.core.rmem_default = 1048576", "returncode": 0} + )(), + ] + + commands, buffer_size = check_buffers() + sudo = get_sudo_prefix() + assert commands == [f"{sudo}sysctl -w net.core.rmem_default=2097152"] + assert buffer_size == 2097152 + + +def test_check_buffers_both_low() -> None: + """Test check_buffers when both buffer sizes are too low.""" + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock both low + mock_run.side_effect = [ + type("MockResult", (), {"stdout": "net.core.rmem_max = 1048576", "returncode": 0})(), + type( + "MockResult", (), {"stdout": "net.core.rmem_default = 1048576", "returncode": 0} + )(), + ] + + commands, buffer_size = check_buffers() + sudo = get_sudo_prefix() + expected = [ + f"{sudo}sysctl -w net.core.rmem_max=2097152", + f"{sudo}sysctl -w net.core.rmem_default=2097152", + ] + assert commands == expected + assert buffer_size == 1048576 + + +def test_check_buffers_subprocess_exception() -> None: + """Test check_buffers when subprocess calls fail.""" + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock subprocess exceptions + mock_run.side_effect = Exception("Command failed") + + commands, buffer_size = check_buffers() + sudo = get_sudo_prefix() + expected = [ + f"{sudo}sysctl -w net.core.rmem_max=2097152", + f"{sudo}sysctl -w net.core.rmem_default=2097152", + ] + assert commands == expected + assert buffer_size is None + + +def test_check_buffers_parsing_error() -> None: + """Test check_buffers when output parsing fails.""" + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock malformed output + mock_run.side_effect = [ + type("MockResult", (), {"stdout": "invalid output", "returncode": 0})(), + type("MockResult", (), {"stdout": "also invalid", "returncode": 0})(), + ] + + commands, buffer_size = check_buffers() + sudo = get_sudo_prefix() + expected = [ + f"{sudo}sysctl -w net.core.rmem_max=2097152", + f"{sudo}sysctl -w net.core.rmem_default=2097152", + ] + assert commands == expected + assert buffer_size is None + + +def test_check_buffers_dev_container() -> None: + """Test check_buffers in dev container where sysctl fails.""" + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock dev container behavior - sysctl returns non-zero + mock_run.side_effect = [ + type( + "MockResult", + (), + { + "stdout": "sysctl: cannot stat /proc/sys/net/core/rmem_max: No such file or directory", + "returncode": 255, + }, + )(), + type( + "MockResult", + (), + { + "stdout": "sysctl: cannot stat /proc/sys/net/core/rmem_default: No such file or directory", + "returncode": 255, + }, + )(), + ] + + commands, buffer_size = check_buffers() + sudo = get_sudo_prefix() + expected = [ + f"{sudo}sysctl -w net.core.rmem_max=2097152", + f"{sudo}sysctl -w net.core.rmem_default=2097152", + ] + assert commands == expected + assert buffer_size is None + + +def test_autoconf_no_config_needed() -> None: + """Test autoconf when no configuration is needed.""" + # Clear CI environment variable for this test + with patch.dict(os.environ, {"CI": ""}, clear=False): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock all checks passing + mock_run.side_effect = [ + # check_multicast calls + type( + "MockResult", + (), + { + "stdout": "1: lo: mtu 65536", + "returncode": 0, + }, + )(), + type( + "MockResult", (), {"stdout": "224.0.0.0/4 dev lo scope link", "returncode": 0} + )(), + # check_buffers calls + type( + "MockResult", (), {"stdout": "net.core.rmem_max = 2097152", "returncode": 0} + )(), + type( + "MockResult", (), {"stdout": "net.core.rmem_default = 2097152", "returncode": 0} + )(), + ] + + with patch("dimos.protocol.service.lcmservice.logger") as mock_logger: + autoconf() + # Should not log anything when no config is needed + mock_logger.info.assert_not_called() + mock_logger.error.assert_not_called() + mock_logger.warning.assert_not_called() + + +def test_autoconf_with_config_needed_success() -> None: + """Test autoconf when configuration is needed and commands succeed.""" + # Clear CI environment variable for this test + with patch.dict(os.environ, {"CI": ""}, clear=False): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock checks failing, then mock the execution succeeding + mock_run.side_effect = [ + # check_multicast calls + type( + "MockResult", + (), + {"stdout": "1: lo: mtu 65536", "returncode": 0}, + )(), + type("MockResult", (), {"stdout": "", "returncode": 0})(), + # check_buffers calls + type( + "MockResult", (), {"stdout": "net.core.rmem_max = 1048576", "returncode": 0} + )(), + type( + "MockResult", (), {"stdout": "net.core.rmem_default = 1048576", "returncode": 0} + )(), + # Command execution calls + type( + "MockResult", (), {"stdout": "success", "returncode": 0} + )(), # ifconfig lo multicast + type("MockResult", (), {"stdout": "success", "returncode": 0})(), # route add... + type("MockResult", (), {"stdout": "success", "returncode": 0})(), # sysctl rmem_max + type( + "MockResult", (), {"stdout": "success", "returncode": 0} + )(), # sysctl rmem_default + ] + + from unittest.mock import call + + with patch("dimos.protocol.service.lcmservice.logger") as mock_logger: + autoconf() + + sudo = get_sudo_prefix() + # Verify the expected log calls + expected_info_calls = [ + call("System configuration required. Executing commands..."), + call(f" Running: {sudo}ifconfig lo multicast"), + call(" ✓ Success"), + call(f" Running: {sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo"), + call(" ✓ Success"), + call(f" Running: {sudo}sysctl -w net.core.rmem_max=2097152"), + call(" ✓ Success"), + call(f" Running: {sudo}sysctl -w net.core.rmem_default=2097152"), + call(" ✓ Success"), + call("System configuration completed."), + ] + + mock_logger.info.assert_has_calls(expected_info_calls) + + +def test_autoconf_with_command_failures() -> None: + """Test autoconf when some commands fail.""" + # Clear CI environment variable for this test + with patch.dict(os.environ, {"CI": ""}, clear=False): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock checks failing, then mock some commands failing + mock_run.side_effect = [ + # check_multicast calls + type( + "MockResult", + (), + {"stdout": "1: lo: mtu 65536", "returncode": 0}, + )(), + type("MockResult", (), {"stdout": "", "returncode": 0})(), + # check_buffers calls (no buffer issues for simpler test) + type( + "MockResult", (), {"stdout": "net.core.rmem_max = 2097152", "returncode": 0} + )(), + type( + "MockResult", (), {"stdout": "net.core.rmem_default = 2097152", "returncode": 0} + )(), + # Command execution calls - first succeeds, second fails + type( + "MockResult", (), {"stdout": "success", "returncode": 0} + )(), # ifconfig lo multicast + subprocess.CalledProcessError( + 1, + [ + *get_sudo_prefix().split(), + "route", + "add", + "-net", + "224.0.0.0", + "netmask", + "240.0.0.0", + "dev", + "lo", + ], + "Permission denied", + "Operation not permitted", + ), + ] + + with patch("dimos.protocol.service.lcmservice.logger") as mock_logger: + # The function should raise on multicast/route failures + with pytest.raises(subprocess.CalledProcessError): + autoconf() + + # Verify it logged the failure before raising + info_calls = [call[0][0] for call in mock_logger.info.call_args_list] + error_calls = [call[0][0] for call in mock_logger.error.call_args_list] + + assert "System configuration required. Executing commands..." in info_calls + assert " ✓ Success" in info_calls # First command succeeded + assert any( + "✗ Failed to configure multicast" in call for call in error_calls + ) # Second command failed diff --git a/dimos/protocol/service/test_spec.py b/dimos/protocol/service/test_spec.py new file mode 100644 index 0000000000..9842f9c49f --- /dev/null +++ b/dimos/protocol/service/test_spec.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass + +from dimos.protocol.service.spec import Service + + +@dataclass +class DatabaseConfig: + host: str = "localhost" + port: int = 5432 + database_name: str = "test_db" + timeout: float = 30.0 + max_connections: int = 10 + ssl_enabled: bool = False + + +class DatabaseService(Service[DatabaseConfig]): + default_config = DatabaseConfig + + def start(self) -> None: ... + def stop(self) -> None: ... + + +def test_default_configuration() -> None: + """Test that default configuration is applied correctly.""" + service = DatabaseService() + + # Check that all default values are set + assert service.config.host == "localhost" + assert service.config.port == 5432 + assert service.config.database_name == "test_db" + assert service.config.timeout == 30.0 + assert service.config.max_connections == 10 + assert service.config.ssl_enabled is False + + +def test_partial_configuration_override() -> None: + """Test that partial configuration correctly overrides defaults.""" + service = DatabaseService(host="production-db", port=3306, ssl_enabled=True) + + # Check overridden values + assert service.config.host == "production-db" + assert service.config.port == 3306 + assert service.config.ssl_enabled is True + + # Check that defaults are preserved for non-overridden values + assert service.config.database_name == "test_db" + assert service.config.timeout == 30.0 + assert service.config.max_connections == 10 + + +def test_complete_configuration_override() -> None: + """Test that all configuration values can be overridden.""" + service = DatabaseService( + host="custom-host", + port=9999, + database_name="custom_db", + timeout=60.0, + max_connections=50, + ssl_enabled=True, + ) + + # Check that all values match the custom config + assert service.config.host == "custom-host" + assert service.config.port == 9999 + assert service.config.database_name == "custom_db" + assert service.config.timeout == 60.0 + assert service.config.max_connections == 50 + assert service.config.ssl_enabled is True + + +def test_service_subclassing() -> None: + @dataclass + class ExtraConfig(DatabaseConfig): + extra_param: str = "default_value" + + class ExtraDatabaseService(DatabaseService): + default_config = ExtraConfig + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + bla = ExtraDatabaseService(host="custom-host2", extra_param="extra_value") + + assert bla.config.host == "custom-host2" + assert bla.config.extra_param == "extra_value" + assert bla.config.port == 5432 # Default value from DatabaseConfig diff --git a/dimos/protocol/skill/__init__.py b/dimos/protocol/skill/__init__.py new file mode 100644 index 0000000000..15ebf0b59c --- /dev/null +++ b/dimos/protocol/skill/__init__.py @@ -0,0 +1 @@ +from dimos.protocol.skill.skill import SkillContainer, skill diff --git a/dimos/protocol/skill/comms.py b/dimos/protocol/skill/comms.py new file mode 100644 index 0000000000..b0adecf5c5 --- /dev/null +++ b/dimos/protocol/skill/comms.py @@ -0,0 +1,99 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +from abc import abstractmethod +from dataclasses import dataclass +from typing import TYPE_CHECKING, Generic, TypeVar + +from dimos.protocol.pubsub.lcmpubsub import PickleLCM +from dimos.protocol.service import Service +from dimos.protocol.skill.type import SkillMsg + +if TYPE_CHECKING: + from collections.abc import Callable + + from dimos.protocol.pubsub.spec import PubSub + +# defines a protocol for communication between skills and agents +# it has simple requirements of pub/sub semantics capable of sending and receiving SkillMsg objects + + +class SkillCommsSpec: + @abstractmethod + def publish(self, msg: SkillMsg) -> None: ... + + @abstractmethod + def subscribe(self, cb: Callable[[SkillMsg], None]) -> None: ... + + @abstractmethod + def start(self) -> None: ... + + @abstractmethod + def stop(self) -> None: ... + + +MsgT = TypeVar("MsgT") +TopicT = TypeVar("TopicT") + + +@dataclass +class PubSubCommsConfig(Generic[TopicT, MsgT]): + topic: TopicT | None = None + pubsub: type[PubSub[TopicT, MsgT]] | PubSub[TopicT, MsgT] | None = None + autostart: bool = True + + +# implementation of the SkillComms using any standard PubSub mechanism +class PubSubComms(Service[PubSubCommsConfig], SkillCommsSpec): + default_config: type[PubSubCommsConfig] = PubSubCommsConfig + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + pubsub_config = getattr(self.config, "pubsub", None) + if pubsub_config is not None: + if callable(pubsub_config): + self.pubsub = pubsub_config() + else: + self.pubsub = pubsub_config + else: + raise ValueError("PubSub configuration is missing") + + if getattr(self.config, "autostart", True): + self.start() + + def start(self) -> None: + self.pubsub.start() + + def stop(self) -> None: + self.pubsub.stop() + + def publish(self, msg: SkillMsg) -> None: + self.pubsub.publish(self.config.topic, msg) + + def subscribe(self, cb: Callable[[SkillMsg], None]) -> None: + self.pubsub.subscribe(self.config.topic, lambda msg, topic: cb(msg)) + + +@dataclass +class LCMCommsConfig(PubSubCommsConfig[str, SkillMsg]): + topic: str = "/skill" + pubsub: type[PubSub] | PubSub | None = PickleLCM + # lcm needs to be started only if receiving + # skill comms are broadcast only in modules so we don't autostart + autostart: bool = False + + +class LCMSkillComms(PubSubComms): + default_config: type[LCMCommsConfig] = LCMCommsConfig diff --git a/dimos/protocol/skill/coordinator.py b/dimos/protocol/skill/coordinator.py new file mode 100644 index 0000000000..a672ceacee --- /dev/null +++ b/dimos/protocol/skill/coordinator.py @@ -0,0 +1,641 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +from copy import copy +from dataclasses import dataclass +from enum import Enum +import json +import threading +import time +from typing import Any, Literal + +from langchain_core.messages import ToolMessage +from langchain_core.tools import tool as langchain_tool +from rich.console import Console +from rich.table import Table +from rich.text import Text + +from dimos.core import rpc +from dimos.core.module import Module, get_loop +from dimos.protocol.skill.comms import LCMSkillComms, SkillCommsSpec +from dimos.protocol.skill.skill import SkillConfig, SkillContainer +from dimos.protocol.skill.type import MsgType, Output, Reducer, Return, SkillMsg, Stream +from dimos.protocol.skill.utils import interpret_tool_call_args +from dimos.utils.logging_config import setup_logger + +logger = setup_logger(__file__) + + +@dataclass +class SkillCoordinatorConfig: + skill_transport: type[SkillCommsSpec] = LCMSkillComms + + +class SkillStateEnum(Enum): + pending = 0 + running = 1 + completed = 2 + error = 3 + + def colored_name(self) -> Text: + """Return the state name as a rich Text object with color.""" + colors = { + SkillStateEnum.pending: "yellow", + SkillStateEnum.running: "blue", + SkillStateEnum.completed: "green", + SkillStateEnum.error: "red", + } + return Text(self.name, style=colors.get(self, "white")) + + +# This object maintains the state of a skill run on a caller end +class SkillState: + call_id: str + name: str + state: SkillStateEnum + skill_config: SkillConfig + + msg_count: int = 0 + sent_tool_msg: bool = False + + start_msg: SkillMsg[Literal[MsgType.start]] = None + end_msg: SkillMsg[Literal[MsgType.ret]] = None + error_msg: SkillMsg[Literal[MsgType.error]] = None + ret_msg: SkillMsg[Literal[MsgType.ret]] = None + reduced_stream_msg: list[SkillMsg[Literal[MsgType.reduced_stream]]] = None + + def __init__(self, call_id: str, name: str, skill_config: SkillConfig | None = None) -> None: + super().__init__() + + self.skill_config = skill_config or SkillConfig( + name=name, + stream=Stream.none, + ret=Return.none, + reducer=Reducer.all, + output=Output.standard, + schema={}, + ) + + self.state = SkillStateEnum.pending + self.call_id = call_id + self.name = name + + def duration(self) -> float: + """Calculate the duration of the skill run.""" + if self.start_msg and self.end_msg: + return self.end_msg.ts - self.start_msg.ts + elif self.start_msg: + return time.time() - self.start_msg.ts + else: + return 0.0 + + def content(self) -> dict[str, Any] | str | int | float | None: + if self.state == SkillStateEnum.running: + if self.reduced_stream_msg: + return self.reduced_stream_msg.content + + if self.state == SkillStateEnum.completed: + if self.reduced_stream_msg: # are we a streaming skill? + return self.reduced_stream_msg.content + return self.ret_msg.content + + if self.state == SkillStateEnum.error: + print("Error msg:", self.error_msg.content) + if self.reduced_stream_msg: + (self.reduced_stream_msg.content + "\n" + self.error_msg.content) + else: + return self.error_msg.content + + def agent_encode(self) -> ToolMessage | str: + # tool call can emit a single ToolMessage + # subsequent messages are considered SituationalAwarenessMessages, + # those are collapsed into a HumanMessage, that's artificially prepended to history + + if not self.sent_tool_msg: + self.sent_tool_msg = True + return ToolMessage( + self.content() or "Querying, please wait, you will receive a response soon.", + name=self.name, + tool_call_id=self.call_id, + ) + else: + return json.dumps( + { + "name": self.name, + "call_id": self.call_id, + "state": self.state.name, + "data": self.content(), + "ran_for": self.duration(), + } + ) + + # returns True if the agent should be called for this message + def handle_msg(self, msg: SkillMsg) -> bool: + self.msg_count += 1 + if msg.type == MsgType.stream: + self.state = SkillStateEnum.running + self.reduced_stream_msg = self.skill_config.reducer(self.reduced_stream_msg, msg) + + if ( + self.skill_config.stream == Stream.none + or self.skill_config.stream == Stream.passive + ): + return False + + if self.skill_config.stream == Stream.call_agent: + return True + + if msg.type == MsgType.ret: + self.state = SkillStateEnum.completed + self.ret_msg = msg + if self.skill_config.ret == Return.call_agent: + return True + return False + + if msg.type == MsgType.error: + self.state = SkillStateEnum.error + self.error_msg = msg + return True + + if msg.type == MsgType.start: + self.state = SkillStateEnum.running + self.start_msg = msg + return False + + return False + + def __len__(self) -> int: + return self.msg_count + + def __str__(self) -> str: + # For standard string representation, we'll use rich's Console to render the colored text + console = Console(force_terminal=True, legacy_windows=False) + colored_state = self.state.colored_name() + + # Build the parts of the string + parts = [Text(f"SkillState({self.name} "), colored_state, Text(f", call_id={self.call_id}")] + + if self.state == SkillStateEnum.completed or self.state == SkillStateEnum.error: + parts.append(Text(", ran for=")) + else: + parts.append(Text(", running for=")) + + parts.append(Text(f"{self.duration():.2f}s")) + + if len(self): + parts.append(Text(f", msg_count={self.msg_count})")) + else: + parts.append(Text(", No Messages)")) + + # Combine all parts into a single Text object + combined = Text() + for part in parts: + combined.append(part) + + # Render to string with console + with console.capture() as capture: + console.print(combined, end="") + return capture.get() + + +# subclassed the dict just to have a better string representation +class SkillStateDict(dict[str, SkillState]): + """Custom dict for skill states with better string representation.""" + + def table(self) -> Table: + # Add skill states section + states_table = Table(show_header=True) + states_table.add_column("Call ID", style="dim", width=12) + states_table.add_column("Skill", style="white") + states_table.add_column("State", style="white") + states_table.add_column("Duration", style="yellow") + states_table.add_column("Messages", style="dim") + + for call_id, skill_state in self.items(): + # Get colored state name + state_text = skill_state.state.colored_name() + + # Duration formatting + if ( + skill_state.state == SkillStateEnum.completed + or skill_state.state == SkillStateEnum.error + ): + duration = f"{skill_state.duration():.2f}s" + else: + duration = f"{skill_state.duration():.2f}s..." + + # Messages info + msg_count = str(len(skill_state)) + + states_table.add_row( + call_id[:8] + "...", skill_state.name, state_text, duration, msg_count + ) + + if not self: + states_table.add_row("", "[dim]No active skills[/dim]", "", "", "") + return states_table + + def __str__(self) -> str: + console = Console(force_terminal=True, legacy_windows=False) + + # Render to string with title above + with console.capture() as capture: + console.print(Text(" SkillState", style="bold blue")) + console.print(self.table()) + return capture.get().strip() + + +# This class is responsible for managing the lifecycle of skills, +# handling skill calls, and coordinating communication between the agent and skills. +# +# It aggregates skills from static and dynamic containers, manages skill states, +# and decides when to notify the agent about updates. +class SkillCoordinator(Module): + default_config = SkillCoordinatorConfig + empty: bool = True + + _static_containers: list[SkillContainer] + _dynamic_containers: list[SkillContainer] + _skill_state: SkillStateDict # key is call_id, not skill_name + _skills: dict[str, SkillConfig] + _updates_available: asyncio.Event | None + _loop: asyncio.AbstractEventLoop | None + _loop_thread: threading.Thread | None + _agent_loop: asyncio.AbstractEventLoop | None + + def __init__(self) -> None: + # TODO: Why isn't this super().__init__() ? + SkillContainer.__init__(self) + self._loop, self._loop_thread = get_loop() + self._static_containers = [] + self._dynamic_containers = [] + self._skills = {} + self._skill_state = SkillStateDict() + # Defer event creation until we're in the correct loop context + self._updates_available = None + self._agent_loop = None + self._pending_notifications = 0 # Count pending notifications + self._closed_coord = False + self._transport_unsub_fn = None + + def _ensure_updates_available(self) -> asyncio.Event: + """Lazily create the updates available event in the correct loop context.""" + if self._updates_available is None: + # Create the event in the current running loop, not the stored loop + try: + loop = asyncio.get_running_loop() + # print(f"[DEBUG] Creating _updates_available event in current loop {id(loop)}") + # Always use the current running loop for the event + # This ensures the event is created in the context where it will be used + self._updates_available = asyncio.Event() + # Store the loop where the event was created - this is the agent's loop + self._agent_loop = loop + # print( + # f"[DEBUG] Created _updates_available event {id(self._updates_available)} in agent loop {id(loop)}" + # ) + except RuntimeError: + # No running loop, defer event creation until we have the proper context + # print(f"[DEBUG] No running loop, deferring event creation") + # Don't create the event yet - wait for the proper loop context + pass + else: + ... + # print(f"[DEBUG] Reusing _updates_available event {id(self._updates_available)}") + return self._updates_available + + @rpc + def start(self) -> None: + super().start() + self.skill_transport.start() + self._transport_unsub_fn = self.skill_transport.subscribe(self.handle_message) + + @rpc + def stop(self) -> None: + self._close_module() + self._closed_coord = True + self.skill_transport.stop() + if self._transport_unsub_fn: + self._transport_unsub_fn() + + # Stop all registered skill containers + for container in self._static_containers: + container.stop() + for container in self._dynamic_containers: + container.stop() + + super().stop() + + def len(self) -> int: + return len(self._skills) + + def __len__(self) -> int: + return self.len() + + # this can be converted to non-langchain json schema output + # and langchain takes this output as well + # just faster for now + def get_tools(self) -> list[dict]: + return [ + langchain_tool(skill_config.f) + for skill_config in self.skills().values() + if not skill_config.hide_skill + ] + + # internal skill call + def call_skill( + self, call_id: str | Literal[False], skill_name: str, args: dict[str, Any] + ) -> None: + if not call_id: + call_id = str(time.time()) + skill_config = self.get_skill_config(skill_name) + if not skill_config: + logger.error( + f"Skill {skill_name} not found in registered skills, but agent tried to call it (did a dynamic skill expire?)" + ) + return + + self._skill_state[call_id] = SkillState( + call_id=call_id, name=skill_name, skill_config=skill_config + ) + + # TODO agent often calls the skill again if previous response is still loading. + # maybe create a new skill_state linked to a previous one? not sure + + arg_keywords = args.get("args") or {} + arg_list = [] + + if isinstance(arg_keywords, list): + arg_list = arg_keywords + arg_keywords = {} + + arg_list, arg_keywords = interpret_tool_call_args(args) + + return skill_config.call( + call_id, + *arg_list, + **arg_keywords, + ) + + # Receives a message from active skill + # Updates local skill state (appends to streamed data if needed etc) + # + # Checks if agent needs to be notified (if ToolConfig has Return=call_agent or Stream=call_agent) + def handle_message(self, msg: SkillMsg) -> None: + if self._closed_coord: + import traceback + + traceback.print_stack() + return + # logger.info(f"SkillMsg from {msg.skill_name}, {msg.call_id} - {msg}") + + if self._skill_state.get(msg.call_id) is None: + logger.warn( + f"Skill state for {msg.skill_name} (call_id={msg.call_id}) not found, (skill not called by our agent?) initializing. (message received: {msg})" + ) + self._skill_state[msg.call_id] = SkillState(call_id=msg.call_id, name=msg.skill_name) + + should_notify = self._skill_state[msg.call_id].handle_msg(msg) + + if should_notify: + updates_available = self._ensure_updates_available() + if updates_available is None: + print("[DEBUG] Event not created yet, deferring notification") + return + + try: + current_loop = asyncio.get_running_loop() + agent_loop = getattr(self, "_agent_loop", self._loop) + # print( + # f"[DEBUG] handle_message: current_loop={id(current_loop)}, agent_loop={id(agent_loop) if agent_loop else 'None'}, event={id(updates_available)}" + # ) + if agent_loop and agent_loop != current_loop: + # print( + # f"[DEBUG] Calling set() via call_soon_threadsafe from loop {id(current_loop)} to agent loop {id(agent_loop)}" + # ) + agent_loop.call_soon_threadsafe(updates_available.set) + else: + # print(f"[DEBUG] Calling set() directly in current loop {id(current_loop)}") + updates_available.set() + except RuntimeError: + # No running loop, use call_soon_threadsafe if we have an agent loop + agent_loop = getattr(self, "_agent_loop", self._loop) + # print( + # f"[DEBUG] No current running loop, agent_loop={id(agent_loop) if agent_loop else 'None'}" + # ) + if agent_loop: + # print( + # f"[DEBUG] Calling set() via call_soon_threadsafe to agent loop {id(agent_loop)}" + # ) + agent_loop.call_soon_threadsafe(updates_available.set) + else: + # print(f"[DEBUG] Event creation was deferred, can't notify") + pass + + def has_active_skills(self) -> bool: + if not self.has_passive_skills(): + return False + for skill_run in self._skill_state.values(): + # check if this skill will notify agent + if skill_run.skill_config.ret == Return.call_agent: + return True + if skill_run.skill_config.stream == Stream.call_agent: + return True + return False + + def has_passive_skills(self) -> bool: + # check if dict is empty + if self._skill_state == {}: + return False + return True + + async def wait_for_updates(self, timeout: float | None = None) -> True: + """Wait for skill updates to become available. + + This method should be called by the agent when it's ready to receive updates. + It will block until updates are available or timeout is reached. + + Args: + timeout: Optional timeout in seconds + + Returns: + True if updates are available, False on timeout + """ + updates_available = self._ensure_updates_available() + if updates_available is None: + # Force event creation now that we're in the agent's loop context + # print(f"[DEBUG] wait_for_updates: Creating event in current loop context") + current_loop = asyncio.get_running_loop() + self._updates_available = asyncio.Event() + self._agent_loop = current_loop + updates_available = self._updates_available + # print( + # f"[DEBUG] wait_for_updates: Created event {id(updates_available)} in loop {id(current_loop)}" + # ) + + try: + current_loop = asyncio.get_running_loop() + + # Double-check the loop context before waiting + if self._agent_loop != current_loop: + # print(f"[DEBUG] Loop context changed! Recreating event for loop {id(current_loop)}") + self._updates_available = asyncio.Event() + self._agent_loop = current_loop + updates_available = self._updates_available + + # print( + # f"[DEBUG] wait_for_updates: current_loop={id(current_loop)}, event={id(updates_available)}, is_set={updates_available.is_set()}" + # ) + if timeout: + # print(f"[DEBUG] Waiting for event with timeout {timeout}") + await asyncio.wait_for(updates_available.wait(), timeout=timeout) + else: + print("[DEBUG] Waiting for event without timeout") + await updates_available.wait() + print("[DEBUG] Event was set! Returning True") + return True + except asyncio.TimeoutError: + print("[DEBUG] Timeout occurred while waiting for event") + return False + except RuntimeError as e: + if "bound to a different event loop" in str(e): + print( + "[DEBUG] Event loop binding error detected, recreating event and returning False to retry" + ) + # Recreate the event in the current loop + current_loop = asyncio.get_running_loop() + self._updates_available = asyncio.Event() + self._agent_loop = current_loop + return False + else: + raise + + def generate_snapshot(self, clear: bool = True) -> SkillStateDict: + """Generate a fresh snapshot of completed skills and optionally clear them.""" + ret = copy(self._skill_state) + + if clear: + updates_available = self._ensure_updates_available() + if updates_available is not None: + # print(f"[DEBUG] generate_snapshot: clearing event {id(updates_available)}") + updates_available.clear() + else: + ... + # rint(f"[DEBUG] generate_snapshot: event not created yet, nothing to clear") + to_delete = [] + # Since snapshot is being sent to agent, we can clear the finished skill runs + for call_id, skill_run in self._skill_state.items(): + if skill_run.state == SkillStateEnum.completed: + logger.info(f"Skill {skill_run.name} (call_id={call_id}) finished") + to_delete.append(call_id) + if skill_run.state == SkillStateEnum.error: + error_msg = skill_run.error_msg.content.get("msg", "Unknown error") + error_traceback = skill_run.error_msg.content.get( + "traceback", "No traceback available" + ) + + logger.error( + f"Skill error for {skill_run.name} (call_id={call_id}): {error_msg}" + ) + print(error_traceback) + to_delete.append(call_id) + + elif ( + skill_run.state == SkillStateEnum.running + and skill_run.reduced_stream_msg is not None + ): + # preserve ret as a copy + ret[call_id] = copy(skill_run) + logger.debug( + f"Resetting accumulator for skill {skill_run.name} (call_id={call_id})" + ) + skill_run.reduced_stream_msg = None + + for call_id in to_delete: + logger.debug(f"Call {call_id} finished, removing from state") + del self._skill_state[call_id] + + return ret + + def __str__(self) -> str: + console = Console(force_terminal=True, legacy_windows=False) + + # Create main table without any header + table = Table(show_header=False) + + # Add containers section + containers_table = Table(show_header=True, show_edge=False, box=None) + containers_table.add_column("Type", style="cyan") + containers_table.add_column("Container", style="white") + + # Add static containers + for container in self._static_containers: + containers_table.add_row("Static", str(container)) + + # Add dynamic containers + for container in self._dynamic_containers: + containers_table.add_row("Dynamic", str(container)) + + if not self._static_containers and not self._dynamic_containers: + containers_table.add_row("", "[dim]No containers registered[/dim]") + + # Add skill states section + states_table = self._skill_state.table() + states_table.show_edge = False + states_table.box = None + + # Combine into main table + table.add_column("Section", style="bold") + table.add_column("Details", style="none") + table.add_row("Containers", containers_table) + table.add_row("Skills", states_table) + + # Render to string with title above + with console.capture() as capture: + console.print(Text(" SkillCoordinator", style="bold blue")) + console.print(table) + return capture.get().strip() + + # Given skillcontainers can run remotely, we are + # Caching available skills from static containers + # + # Dynamic containers will be queried at runtime via + # .skills() method + def register_skills(self, container: SkillContainer) -> None: + self.empty = False + if not container.dynamic_skills(): + logger.info(f"Registering static skill container, {container}") + self._static_containers.append(container) + for name, skill_config in container.skills().items(): + self._skills[name] = skill_config.bind(getattr(container, name)) + else: + logger.info(f"Registering dynamic skill container, {container}") + self._dynamic_containers.append(container) + + def get_skill_config(self, skill_name: str) -> SkillConfig | None: + skill_config = self._skills.get(skill_name) + if not skill_config: + skill_config = self.skills().get(skill_name) + return skill_config + + def skills(self) -> dict[str, SkillConfig]: + # Static container skilling is already cached + all_skills: dict[str, SkillConfig] = {**self._skills} + + # Then aggregate skills from dynamic containers + for container in self._dynamic_containers: + for skill_name, skill_config in container.skills().items(): + all_skills[skill_name] = skill_config.bind(getattr(container, skill_name)) + + return all_skills diff --git a/dimos/protocol/skill/schema.py b/dimos/protocol/skill/schema.py new file mode 100644 index 0000000000..49dc1caa37 --- /dev/null +++ b/dimos/protocol/skill/schema.py @@ -0,0 +1,103 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +from typing import Union, get_args, get_origin + + +def python_type_to_json_schema(python_type) -> dict: + """Convert Python type annotations to JSON Schema format.""" + # Handle None/NoneType + if python_type is type(None) or python_type is None: + return {"type": "null"} + + # Handle Union types (including Optional) + origin = get_origin(python_type) + if origin is Union: + args = get_args(python_type) + # Handle Optional[T] which is Union[T, None] + if len(args) == 2 and type(None) in args: + non_none_type = args[0] if args[1] is type(None) else args[1] + schema = python_type_to_json_schema(non_none_type) + # For OpenAI function calling, we don't use anyOf for optional params + return schema + else: + # For other Union types, use anyOf + return {"anyOf": [python_type_to_json_schema(arg) for arg in args]} + + # Handle List/list types + if origin in (list, list): + args = get_args(python_type) + if args: + return {"type": "array", "items": python_type_to_json_schema(args[0])} + return {"type": "array"} + + # Handle Dict/dict types + if origin in (dict, dict): + return {"type": "object"} + + # Handle basic types + type_map = { + str: {"type": "string"}, + int: {"type": "integer"}, + float: {"type": "number"}, + bool: {"type": "boolean"}, + list: {"type": "array"}, + dict: {"type": "object"}, + } + + return type_map.get(python_type, {"type": "string"}) + + +def function_to_schema(func) -> dict: + """Convert a function to OpenAI function schema format.""" + try: + signature = inspect.signature(func) + except ValueError as e: + raise ValueError(f"Failed to get signature for function {func.__name__}: {e!s}") + + properties = {} + required = [] + + for param_name, param in signature.parameters.items(): + # Skip 'self' parameter for methods + if param_name == "self": + continue + + # Get the type annotation + if param.annotation != inspect.Parameter.empty: + param_schema = python_type_to_json_schema(param.annotation) + else: + # Default to string if no type annotation + param_schema = {"type": "string"} + + # Add description from docstring if available (would need more sophisticated parsing) + properties[param_name] = param_schema + + # Add to required list if no default value + if param.default == inspect.Parameter.empty: + required.append(param_name) + + return { + "type": "function", + "function": { + "name": func.__name__, + "description": (func.__doc__ or "").strip(), + "parameters": { + "type": "object", + "properties": properties, + "required": required, + }, + }, + } diff --git a/dimos/protocol/skill/skill.py b/dimos/protocol/skill/skill.py new file mode 100644 index 0000000000..7ad260eaa5 --- /dev/null +++ b/dimos/protocol/skill/skill.py @@ -0,0 +1,246 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +from collections.abc import Callable +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass +from typing import Any + +# from dimos.core.core import rpc +from dimos.protocol.skill.comms import LCMSkillComms, SkillCommsSpec +from dimos.protocol.skill.schema import function_to_schema +from dimos.protocol.skill.type import ( + MsgType, + Output, + Reducer, + Return, + SkillConfig, + SkillMsg, + Stream, +) + +# skill is a decorator that allows us to specify a skill behaviour for a function. +# +# there are several parameters that can be specified: +# - ret: how to return the value from the skill, can be one of: +# +# Return.none: doesn't return anything to an agent +# Return.passive: doesn't schedule an agent call but +# returns the value to the agent when agent is called +# Return.call_agent: calls the agent with the value, scheduling an agent call +# +# - stream: if the skill streams values, it can behave in several ways: +# +# Stream.none: no streaming, skill doesn't emit any values +# Stream.passive: doesn't schedule an agent call upon emitting a value, +# returns the streamed value to the agent when agent is called +# Stream.call_agent: calls the agent with every value emitted, scheduling an agent call +# +# - reducer: defines an optional strategy for passive streams and how we collapse potential +# multiple values into something meaningful for the agent +# +# Reducer.none: no reduction, every emitted value is returned to the agent +# Reducer.latest: only the latest value is returned to the agent +# Reducer.average: assumes the skill emits a number, +# the average of all values is returned to the agent + + +def rpc(fn: Callable[..., Any]) -> Callable[..., Any]: + fn.__rpc__ = True # type: ignore[attr-defined] + return fn + + +def skill( + reducer: Reducer = Reducer.latest, + stream: Stream = Stream.none, + ret: Return = Return.call_agent, + output: Output = Output.standard, + hide_skill: bool = False, +) -> Callable: + def decorator(f: Callable[..., Any]) -> Any: + def wrapper(self, *args, **kwargs): + skill = f"{f.__name__}" + + call_id = kwargs.get("call_id", None) + if call_id: + del kwargs["call_id"] + + return self.call_skill(call_id, skill, args, kwargs) + # def run_function(): + # return self.call_skill(call_id, skill, args, kwargs) + # + # thread = threading.Thread(target=run_function) + # thread.start() + # return None + + return f(self, *args, **kwargs) + + # sig = inspect.signature(f) + # params = list(sig.parameters.values()) + # if params and params[0].name == "self": + # params = params[1:] # Remove first parameter 'self' + # wrapper.__signature__ = sig.replace(parameters=params) + + skill_config = SkillConfig( + name=f.__name__, + reducer=reducer, + stream=stream, + # if stream is passive, ret must be passive too + ret=ret.passive if stream == Stream.passive else ret, + output=output, + schema=function_to_schema(f), + hide_skill=hide_skill, + ) + + wrapper.__rpc__ = True # type: ignore[attr-defined] + wrapper._skill_config = skill_config # type: ignore[attr-defined] + wrapper.__name__ = f.__name__ # Preserve original function name + wrapper.__doc__ = f.__doc__ # Preserve original docstring + return wrapper + + return decorator + + +@dataclass +class SkillContainerConfig: + skill_transport: type[SkillCommsSpec] = LCMSkillComms + + +def threaded(f: Callable[..., Any]) -> Callable[..., None]: + """Decorator to run a function in a thread pool.""" + + def wrapper(self, *args, **kwargs): + if self._skill_thread_pool is None: + self._skill_thread_pool = ThreadPoolExecutor( + max_workers=50, thread_name_prefix="skill_worker" + ) + self._skill_thread_pool.submit(f, self, *args, **kwargs) + return None + + return wrapper + + +# Inherited by any class that wants to provide skills +# (This component works standalone but commonly used by DimOS modules) +# +# Hosts the function execution and handles correct publishing of skill messages +# according to the individual skill decorator configuration +# +# - It allows us to specify a communication layer for skills (LCM for now by default) +# - introspection of available skills via the `skills` RPC method +# - ability to provide dynamic context dependant skills with dynamic_skills flag +# for this you'll need to override the `skills` method to return a dynamic set of skills +# SkillCoordinator will call this method to get the skills available upon every request to +# the agent + + +class SkillContainer: + skill_transport_class: type[SkillCommsSpec] = LCMSkillComms + _skill_thread_pool: ThreadPoolExecutor | None = None + _skill_transport: SkillCommsSpec | None = None + + @rpc + def dynamic_skills(self) -> bool: + return False + + def __str__(self) -> str: + return f"SkillContainer({self.__class__.__name__})" + + @rpc + def stop(self) -> None: + if self._skill_transport: + self._skill_transport.stop() + self._skill_transport = None + + if self._skill_thread_pool: + self._skill_thread_pool.shutdown(wait=True) + self._skill_thread_pool = None + + # Continue the MRO chain if there's a parent stop() method + if hasattr(super(), "stop"): + super().stop() + + # TODO: figure out standard args/kwargs passing format, + # use same interface as skill coordinator call_skill method + @threaded + def call_skill( + self, call_id: str, skill_name: str, args: tuple[Any, ...], kwargs: dict[str, Any] + ) -> None: + f = getattr(self, skill_name, None) + + if f is None: + raise ValueError(f"Function '{skill_name}' not found in {self.__class__.__name__}") + + config = getattr(f, "_skill_config", None) + if config is None: + raise ValueError(f"Function '{skill_name}' in {self.__class__.__name__} is not a skill") + + # we notify the skill transport about the start of the skill call + self.skill_transport.publish(SkillMsg(call_id, skill_name, None, type=MsgType.start)) + + try: + val = f(*args, **kwargs) + + # check if the skill returned a coroutine, if it is, block until it resolves + if isinstance(val, asyncio.Future): + val = asyncio.run(val) + + # check if the skill is a generator, if it is, we need to iterate over it + if hasattr(val, "__iter__") and not isinstance(val, str): + last_value = None + for v in val: + last_value = v + self.skill_transport.publish( + SkillMsg(call_id, skill_name, v, type=MsgType.stream) + ) + self.skill_transport.publish( + SkillMsg(call_id, skill_name, last_value, type=MsgType.ret) + ) + + else: + self.skill_transport.publish(SkillMsg(call_id, skill_name, val, type=MsgType.ret)) + + except Exception as e: + import traceback + + formatted_traceback = "".join(traceback.TracebackException.from_exception(e).format()) + + self.skill_transport.publish( + SkillMsg( + call_id, + skill_name, + {"msg": str(e), "traceback": formatted_traceback}, + type=MsgType.error, + ) + ) + + @rpc + def skills(self) -> dict[str, SkillConfig]: + # Avoid recursion by excluding this property itself + # Also exclude known properties that shouldn't be accessed + excluded = {"skills", "tf", "rpc", "skill_transport"} + return { + name: getattr(self, name)._skill_config + for name in dir(self) + if not name.startswith("_") + and name not in excluded + and hasattr(getattr(self, name), "_skill_config") + } + + @property + def skill_transport(self) -> SkillCommsSpec: + if self._skill_transport is None: + self._skill_transport = self.skill_transport_class() + return self._skill_transport diff --git a/dimos/protocol/skill/test_coordinator.py b/dimos/protocol/skill/test_coordinator.py new file mode 100644 index 0000000000..e8d8c45a0c --- /dev/null +++ b/dimos/protocol/skill/test_coordinator.py @@ -0,0 +1,157 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import asyncio +from collections.abc import Generator +import datetime +import time + +import pytest + +from dimos.core import Module, rpc +from dimos.msgs.sensor_msgs import Image +from dimos.protocol.skill.coordinator import SkillCoordinator +from dimos.protocol.skill.skill import skill +from dimos.protocol.skill.type import Output, Reducer, Stream +from dimos.utils.data import get_data + + +class SkillContainerTest(Module): + @rpc + def start(self) -> None: + super().start() + + @rpc + def stop(self) -> None: + super().stop() + + @skill() + def add(self, x: int, y: int) -> int: + """adds x and y.""" + time.sleep(2) + return x + y + + @skill() + def delayadd(self, x: int, y: int) -> int: + """waits 0.3 seconds before adding x and y.""" + time.sleep(0.3) + return x + y + + @skill(stream=Stream.call_agent, reducer=Reducer.all) + def counter(self, count_to: int, delay: float | None = 0.05) -> Generator[int, None, None]: + """Counts from 1 to count_to, with an optional delay between counts.""" + for i in range(1, count_to + 1): + if delay > 0: + time.sleep(delay) + yield i + + @skill(stream=Stream.passive, reducer=Reducer.sum) + def counter_passive_sum( + self, count_to: int, delay: float | None = 0.05 + ) -> Generator[int, None, None]: + """Counts from 1 to count_to, with an optional delay between counts.""" + for i in range(1, count_to + 1): + if delay > 0: + time.sleep(delay) + yield i + + @skill(stream=Stream.passive, reducer=Reducer.latest) + def current_time(self, frequency: float | None = 10) -> Generator[str, None, None]: + """Provides current time.""" + while True: + yield str(datetime.datetime.now()) + time.sleep(1 / frequency) + + @skill(stream=Stream.passive, reducer=Reducer.latest) + def uptime_seconds(self, frequency: float | None = 10) -> Generator[float, None, None]: + """Provides current uptime.""" + start_time = datetime.datetime.now() + while True: + yield (datetime.datetime.now() - start_time).total_seconds() + time.sleep(1 / frequency) + + @skill() + def current_date(self, frequency: float | None = 10) -> str: + """Provides current date.""" + return datetime.datetime.now() + + @skill(output=Output.image) + def take_photo(self) -> str: + """Takes a camera photo""" + print("Taking photo...") + img = Image.from_file(get_data("cafe-smol.jpg")) + print("Photo taken.") + return img + + +@pytest.mark.asyncio +async def test_coordinator_parallel_calls() -> None: + skillCoordinator = SkillCoordinator() + skillCoordinator.register_skills(SkillContainerTest()) + + skillCoordinator.start() + skillCoordinator.call_skill("test-call-0", "add", {"args": [0, 2]}) + + time.sleep(0.1) + + cnt = 0 + while await skillCoordinator.wait_for_updates(1): + print(skillCoordinator) + + skillstates = skillCoordinator.generate_snapshot() + + skill_id = f"test-call-{cnt}" + tool_msg = skillstates[skill_id].agent_encode() + assert tool_msg.content == cnt + 2 + + cnt += 1 + if cnt < 5: + skillCoordinator.call_skill( + f"test-call-{cnt}-delay", + "delayadd", + {"args": [cnt, 2]}, + ) + skillCoordinator.call_skill( + f"test-call-{cnt}", + "add", + {"args": [cnt, 2]}, + ) + + await asyncio.sleep(0.1 * cnt) + + skillCoordinator.stop() + + +@pytest.mark.asyncio +async def test_coordinator_generator() -> None: + container = SkillContainerTest() + skillCoordinator = SkillCoordinator() + skillCoordinator.register_skills(container) + skillCoordinator.start() + + # here we call a skill that generates a sequence of messages + skillCoordinator.call_skill("test-gen-0", "counter", {"args": [10]}) + skillCoordinator.call_skill("test-gen-1", "counter_passive_sum", {"args": [5]}) + skillCoordinator.call_skill("test-gen-2", "take_photo", {"args": []}) + + # periodically agent is stopping it's thinking cycle and asks for updates + while await skillCoordinator.wait_for_updates(2): + print(skillCoordinator) + agent_update = skillCoordinator.generate_snapshot(clear=True) + print(agent_update) + await asyncio.sleep(0.125) + + print("coordinator loop finished") + print(skillCoordinator) + container.stop() + skillCoordinator.stop() diff --git a/dimos/protocol/skill/test_utils.py b/dimos/protocol/skill/test_utils.py new file mode 100644 index 0000000000..db332357fe --- /dev/null +++ b/dimos/protocol/skill/test_utils.py @@ -0,0 +1,87 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.protocol.skill.utils import interpret_tool_call_args + + +def test_list() -> None: + args, kwargs = interpret_tool_call_args([1, 2, 3]) + assert args == [1, 2, 3] + assert kwargs == {} + + +def test_none() -> None: + args, kwargs = interpret_tool_call_args(None) + assert args == [] + assert kwargs == {} + + +def test_none_nested() -> None: + args, kwargs = interpret_tool_call_args({"args": None}) + assert args == [] + assert kwargs == {} + + +def test_non_dict() -> None: + args, kwargs = interpret_tool_call_args("test") + assert args == ["test"] + assert kwargs == {} + + +def test_dict_with_args_and_kwargs() -> None: + args, kwargs = interpret_tool_call_args({"args": [1, 2], "kwargs": {"key": "value"}}) + assert args == [1, 2] + assert kwargs == {"key": "value"} + + +def test_dict_with_only_kwargs() -> None: + args, kwargs = interpret_tool_call_args({"kwargs": {"a": 1, "b": 2}}) + assert args == [] + assert kwargs == {"a": 1, "b": 2} + + +def test_dict_as_kwargs() -> None: + args, kwargs = interpret_tool_call_args({"x": 10, "y": 20}) + assert args == [] + assert kwargs == {"x": 10, "y": 20} + + +def test_dict_with_only_args_first_pass() -> None: + args, kwargs = interpret_tool_call_args({"args": [5, 6, 7]}) + assert args == [5, 6, 7] + assert kwargs == {} + + +def test_dict_with_only_args_nested() -> None: + args, kwargs = interpret_tool_call_args({"args": {"inner": "value"}}) + assert args == [] + assert kwargs == {"inner": "value"} + + +def test_empty_list() -> None: + args, kwargs = interpret_tool_call_args([]) + assert args == [] + assert kwargs == {} + + +def test_empty_dict() -> None: + args, kwargs = interpret_tool_call_args({}) + assert args == [] + assert kwargs == {} + + +def test_integer() -> None: + args, kwargs = interpret_tool_call_args(42) + assert args == [42] + assert kwargs == {} diff --git a/dimos/protocol/skill/type.py b/dimos/protocol/skill/type.py new file mode 100644 index 0000000000..9b1c4ce5f5 --- /dev/null +++ b/dimos/protocol/skill/type.py @@ -0,0 +1,272 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from enum import Enum +import time +from typing import Any, Generic, Literal, TypeVar + +from dimos.types.timestamped import Timestamped +from dimos.utils.generic import truncate_display_string + +# This file defines protocol messages used for communication between skills and agents + + +class Output(Enum): + standard = 0 + human = 1 + image = 2 # this is same as separate_message, but maybe clearer for users + + +class Stream(Enum): + # no streaming + none = 0 + # passive stream, doesn't schedule an agent call, but returns the value to the agent + passive = 1 + # calls the agent with every value emitted, schedules an agent call + call_agent = 2 + + +class Return(Enum): + # doesn't return anything to an agent + none = 0 + # returns the value to the agent, but doesn't schedule an agent call + passive = 1 + # calls the agent with the value, scheduling an agent call + call_agent = 2 + # calls the function to get a value, when the agent is being called + callback = 3 # TODO: this is a work in progress, not implemented yet + + +@dataclass +class SkillConfig: + name: str + reducer: ReducerF + stream: Stream + ret: Return + output: Output + schema: dict[str, Any] + f: Callable | None = None + autostart: bool = False + hide_skill: bool = False + + def bind(self, f: Callable) -> SkillConfig: + self.f = f + return self + + def call(self, call_id, *args, **kwargs) -> Any: + if self.f is None: + raise ValueError( + "Function is not bound to the SkillConfig. This should be called only within AgentListener." + ) + + return self.f(*args, **kwargs, call_id=call_id) + + def __str__(self) -> str: + parts = [f"name={self.name}"] + + # Only show reducer if stream is not none (streaming is happening) + if self.stream != Stream.none: + parts.append(f"stream={self.stream.name}") + + # Always show return mode + parts.append(f"ret={self.ret.name}") + return f"Skill({', '.join(parts)})" + + +class MsgType(Enum): + pending = 0 + start = 1 + stream = 2 + reduced_stream = 3 + ret = 4 + error = 5 + + +M = TypeVar("M", bound="MsgType") + + +def maybe_encode(something: Any) -> str: + if hasattr(something, "agent_encode"): + return something.agent_encode() + return something + + +class SkillMsg(Timestamped, Generic[M]): + ts: float + type: M + call_id: str + skill_name: str + content: str | int | float | dict | list + + def __init__( + self, + call_id: str, + skill_name: str, + content: Any, + type: M, + ) -> None: + self.ts = time.time() + self.call_id = call_id + self.skill_name = skill_name + # any tool output can be a custom type that knows how to encode itself + # like a costmap, path, transform etc could be translatable into strings + + self.content = maybe_encode(content) + self.type = type + + @property + def end(self) -> bool: + return self.type == MsgType.ret or self.type == MsgType.error + + @property + def start(self) -> bool: + return self.type == MsgType.start + + def __str__(self) -> str: + time_ago = time.time() - self.ts + + if self.type == MsgType.start: + return f"Start({time_ago:.1f}s ago)" + if self.type == MsgType.ret: + return f"Ret({time_ago:.1f}s ago, val={truncate_display_string(self.content)})" + if self.type == MsgType.error: + return f"Error({time_ago:.1f}s ago, val={truncate_display_string(self.content)})" + if self.type == MsgType.pending: + return f"Pending({time_ago:.1f}s ago)" + if self.type == MsgType.stream: + return f"Stream({time_ago:.1f}s ago, val={truncate_display_string(self.content)})" + if self.type == MsgType.reduced_stream: + return f"Stream({time_ago:.1f}s ago, val={truncate_display_string(self.content)})" + + +# typing looks complex but it's a standard reducer function signature, using SkillMsgs +# (Optional[accumulator], msg) -> accumulator +ReducerF = Callable[ + [SkillMsg[Literal[MsgType.reduced_stream]] | None, SkillMsg[Literal[MsgType.stream]]], + SkillMsg[Literal[MsgType.reduced_stream]], +] + + +C = TypeVar("C") # content type +A = TypeVar("A") # accumulator type +# define a naive reducer function type that's generic in terms of the accumulator type +SimpleReducerF = Callable[[A | None, C], A] + + +def make_reducer(simple_reducer: SimpleReducerF) -> ReducerF: + """ + Converts a naive reducer function into a standard reducer function. + The naive reducer function should accept an accumulator and a message, + and return the updated accumulator. + """ + + def reducer( + accumulator: SkillMsg[Literal[MsgType.reduced_stream]] | None, + msg: SkillMsg[Literal[MsgType.stream]], + ) -> SkillMsg[Literal[MsgType.reduced_stream]]: + # Extract the content from the accumulator if it exists + acc_value = accumulator.content if accumulator else None + + # Apply the simple reducer to get the new accumulated value + new_value = simple_reducer(acc_value, msg.content) + + # Wrap the result in a SkillMsg with reduced_stream type + return SkillMsg( + call_id=msg.call_id, + skill_name=msg.skill_name, + content=new_value, + type=MsgType.reduced_stream, + ) + + return reducer + + +# just a convinience class to hold reducer functions +def _make_skill_msg( + msg: SkillMsg[Literal[MsgType.stream]], content: Any +) -> SkillMsg[Literal[MsgType.reduced_stream]]: + """Helper to create a reduced stream message with new content.""" + return SkillMsg( + call_id=msg.call_id, + skill_name=msg.skill_name, + content=content, + type=MsgType.reduced_stream, + ) + + +def sum_reducer( + accumulator: SkillMsg[Literal[MsgType.reduced_stream]] | None, + msg: SkillMsg[Literal[MsgType.stream]], +) -> SkillMsg[Literal[MsgType.reduced_stream]]: + """Sum reducer that adds values together.""" + acc_value = accumulator.content if accumulator else None + new_value = acc_value + msg.content if acc_value else msg.content + return _make_skill_msg(msg, new_value) + + +def latest_reducer( + accumulator: SkillMsg[Literal[MsgType.reduced_stream]] | None, + msg: SkillMsg[Literal[MsgType.stream]], +) -> SkillMsg[Literal[MsgType.reduced_stream]]: + """Latest reducer that keeps only the most recent value.""" + return _make_skill_msg(msg, msg.content) + + +def all_reducer( + accumulator: SkillMsg[Literal[MsgType.reduced_stream]] | None, + msg: SkillMsg[Literal[MsgType.stream]], +) -> SkillMsg[Literal[MsgType.reduced_stream]]: + """All reducer that collects all values into a list.""" + acc_value = accumulator.content if accumulator else None + new_value = [*acc_value, msg.content] if acc_value else [msg.content] + return _make_skill_msg(msg, new_value) + + +def accumulate_list( + accumulator: SkillMsg[Literal[MsgType.reduced_stream]] | None, + msg: SkillMsg[Literal[MsgType.stream]], +) -> SkillMsg[Literal[MsgType.reduced_stream]]: + """All reducer that collects all values into a list.""" + acc_value = accumulator.content if accumulator else [] + return _make_skill_msg(msg, acc_value + msg.content) + + +def accumulate_dict( + accumulator: SkillMsg[Literal[MsgType.reduced_stream]] | None, + msg: SkillMsg[Literal[MsgType.stream]], +) -> SkillMsg[Literal[MsgType.reduced_stream]]: + """All reducer that collects all values into a list.""" + acc_value = accumulator.content if accumulator else {} + return _make_skill_msg(msg, {**acc_value, **msg.content}) + + +def accumulate_string( + accumulator: SkillMsg[Literal[MsgType.reduced_stream]] | None, + msg: SkillMsg[Literal[MsgType.stream]], +) -> SkillMsg[Literal[MsgType.reduced_stream]]: + """All reducer that collects all values into a list.""" + acc_value = accumulator.content if accumulator else "" + return _make_skill_msg(msg, acc_value + "\n" + msg.content) + + +class Reducer: + sum = sum_reducer + latest = latest_reducer + all = all_reducer + accumulate_list = accumulate_list + accumulate_dict = accumulate_dict + string = accumulate_string diff --git a/dimos/protocol/skill/utils.py b/dimos/protocol/skill/utils.py new file mode 100644 index 0000000000..f3d052070f --- /dev/null +++ b/dimos/protocol/skill/utils.py @@ -0,0 +1,41 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any + + +def interpret_tool_call_args( + args: Any, first_pass: bool = True +) -> tuple[list[Any], dict[str, Any]]: + """ + Agents sometimes produce bizarre calls. This tries to interpret the args better. + """ + + if isinstance(args, list): + return args, {} + if args is None: + return [], {} + if not isinstance(args, dict): + return [args], {} + if args.keys() == {"args", "kwargs"}: + return args["args"], args["kwargs"] + if args.keys() == {"kwargs"}: + return [], args["kwargs"] + if args.keys() != {"args"}: + return [], args + + if first_pass: + return interpret_tool_call_args(args["args"], first_pass=False) + + return [], args diff --git a/dimos/protocol/tf/__init__.py b/dimos/protocol/tf/__init__.py new file mode 100644 index 0000000000..96cdbcf285 --- /dev/null +++ b/dimos/protocol/tf/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.protocol.tf.tf import LCMTF, TF, MultiTBuffer, PubSubTF, TBuffer, TFConfig, TFSpec + +__all__ = ["LCMTF", "TF", "MultiTBuffer", "PubSubTF", "TBuffer", "TFConfig", "TFSpec"] diff --git a/dimos/protocol/tf/test_tf.py b/dimos/protocol/tf/test_tf.py new file mode 100644 index 0000000000..c25e1014f9 --- /dev/null +++ b/dimos/protocol/tf/test_tf.py @@ -0,0 +1,679 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +import time + +import pytest + +from dimos.core import TF +from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Transform, Vector3 +from dimos.protocol.tf import MultiTBuffer, TBuffer + + +# from https://foxglove.dev/blog/understanding-ros-transforms +def test_tf_ros_example() -> None: + tf = TF() + + base_link_to_arm = Transform( + translation=Vector3(1.0, -1.0, 0.0), + rotation=Quaternion.from_euler(Vector3(0, 0, math.pi / 6)), + frame_id="base_link", + child_frame_id="arm", + ts=time.time(), + ) + + arm_to_end = Transform( + translation=Vector3(1.0, 1.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), # Identity rotation + frame_id="arm", + child_frame_id="end_effector", + ts=time.time(), + ) + + tf.publish(base_link_to_arm, arm_to_end) + time.sleep(0.2) + + end_effector_global_pose = tf.get("base_link", "end_effector") + + assert end_effector_global_pose.translation.x == pytest.approx(1.366, abs=1e-3) + assert end_effector_global_pose.translation.y == pytest.approx(0.366, abs=1e-3) + + tf.stop() + + +def test_tf_main() -> None: + """Test TF broadcasting and querying between two TF instances. + If you run foxglove-bridge this will show up in the UI""" + + # here we create broadcasting and receiving TF instance. + # this is to verify that comms work multiprocess, normally + # you'd use only one instance in your module + broadcaster = TF() + querier = TF() + + # Create a transform from world to robot + current_time = time.time() + + world_to_charger = Transform( + translation=Vector3(2.0, -2.0, 0.0), + rotation=Quaternion.from_euler(Vector3(0, 0, 2)), + frame_id="world", + child_frame_id="charger", + ts=current_time, + ) + + world_to_robot = Transform( + translation=Vector3(1.0, 2.0, 3.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), # Identity rotation + frame_id="world", + child_frame_id="robot", + ts=current_time, + ) + + # Broadcast the transform + broadcaster.publish(world_to_robot) + broadcaster.publish(world_to_charger) + # Give time for the message to propagate + time.sleep(0.05) + + # Verify frames are available + frames = querier.get_frames() + assert "world" in frames + assert "robot" in frames + + # Add another transform in the chain + robot_to_sensor = Transform( + translation=Vector3(0.5, 0.0, 0.2), + rotation=Quaternion(0.0, 0.0, 0.707107, 0.707107), # 90 degrees around Z + frame_id="robot", + child_frame_id="sensor", + ts=current_time, + ) + + broadcaster.publish(robot_to_sensor) + + time.sleep(0.05) + + # we can now query (from a separate process given we use querier) the transform tree + chain_transform = querier.get("world", "sensor") + + # broadcaster will agree with us + assert broadcaster.get("world", "sensor") == chain_transform + + # The chain should compose: world->robot (1,2,3) + robot->sensor (0.5,0,0.2) + # Expected translation: (1.5, 2.0, 3.2) + assert abs(chain_transform.translation.x - 1.5) < 0.001 + assert abs(chain_transform.translation.y - 2.0) < 0.001 + assert abs(chain_transform.translation.z - 3.2) < 0.001 + + # we see something on camera + random_object_in_view = PoseStamped( + frame_id="random_object", + position=Vector3(1, 0, 0), + ) + + print("Random obj", random_object_in_view) + + # random_object is perceived by the sensor + # we create a transform pointing from sensor to object + random_t = random_object_in_view.new_transform_from("sensor") + + # we could have also done + assert random_t == random_object_in_view.new_transform_to("sensor").inverse() + + print("randm t", random_t) + + # we broadcast our object location + broadcaster.publish(random_t) + + ## we could also publish world -> random_object if we wanted to + # broadcaster.publish( + # broadcaster.get("world", "sensor") + random_object_in_view.new_transform("sensor").inverse() + # ) + ## (this would mess with the transform system because it expects trees not graphs) + ## and our random_object would get re-connected to world from sensor + + print(broadcaster) + + # Give time for the message to propagate + time.sleep(0.05) + + # we know where the object is in the world frame now + world_object = broadcaster.get("world", "random_object") + + # both instances agree + assert querier.get("world", "random_object") == world_object + + print("world object", world_object) + + # if you have "diagon" https://diagon.arthursonzogni.com/ installed you can draw a graph + print(broadcaster.graph()) + + assert abs(world_object.translation.x - 1.5) < 0.001 + assert abs(world_object.translation.y - 3.0) < 0.001 + assert abs(world_object.translation.z - 3.2) < 0.001 + + # this doesn't work atm + robot_to_charger = broadcaster.get("robot", "charger") + + # Expected: robot->world->charger + print(f"robot_to_charger translation: {robot_to_charger.translation}") + print(f"robot_to_charger rotation: {robot_to_charger.rotation}") + + assert abs(robot_to_charger.translation.x - 1.0) < 0.001 + assert abs(robot_to_charger.translation.y - (-4.0)) < 0.001 + assert abs(robot_to_charger.translation.z - (-3.0)) < 0.001 + + # Stop services (they were autostarted but don't know how to autostop) + broadcaster.stop() + querier.stop() + + +class TestTBuffer: + def test_add_transform(self) -> None: + buffer = TBuffer(buffer_size=10.0) + transform = Transform( + translation=Vector3(1.0, 2.0, 3.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="world", + child_frame_id="robot", + ts=time.time(), + ) + + buffer.add(transform) + assert len(buffer) == 1 + assert buffer[0] == transform + + def test_get(self) -> None: + buffer = TBuffer() + base_time = time.time() + + # Add transforms at different times + for i in range(3): + transform = Transform( + translation=Vector3(float(i), 0.0, 0.0), + frame_id="world", + child_frame_id="robot", + ts=base_time + i * 0.5, + ) + buffer.add(transform) + + # Test getting latest transform + latest = buffer.get() + assert latest is not None + assert latest.translation.x == 2.0 + + # Test getting transform at specific time + middle = buffer.get(time_point=base_time + 0.75) + assert middle is not None + assert middle.translation.x == 2.0 # Closest to i=1 + + # Test time tolerance + result = buffer.get(time_point=base_time + 10.0, time_tolerance=0.1) + assert result is None # Outside tolerance + + def test_buffer_pruning(self) -> None: + buffer = TBuffer(buffer_size=1.0) # 1 second buffer + + # Add old transform + old_time = time.time() - 2.0 + old_transform = Transform( + translation=Vector3(1.0, 0.0, 0.0), + frame_id="world", + child_frame_id="robot", + ts=old_time, + ) + buffer.add(old_transform) + + # Add recent transform + recent_transform = Transform( + translation=Vector3(2.0, 0.0, 0.0), + frame_id="world", + child_frame_id="robot", + ts=time.time(), + ) + buffer.add(recent_transform) + + # Old transform should be pruned + assert len(buffer) == 1 + assert buffer[0].translation.x == 2.0 + + +class TestMultiTBuffer: + def test_multiple_frame_pairs(self) -> None: + ttbuffer = MultiTBuffer(buffer_size=10.0) + + # Add transforms for different frame pairs + transform1 = Transform( + translation=Vector3(1.0, 0.0, 0.0), + frame_id="world", + child_frame_id="robot1", + ts=time.time(), + ) + + transform2 = Transform( + translation=Vector3(2.0, 0.0, 0.0), + frame_id="world", + child_frame_id="robot2", + ts=time.time(), + ) + + ttbuffer.receive_transform(transform1, transform2) + + # Should have two separate buffers + assert len(ttbuffer.buffers) == 2 + assert ("world", "robot1") in ttbuffer.buffers + assert ("world", "robot2") in ttbuffer.buffers + + def test_graph(self) -> None: + ttbuffer = MultiTBuffer(buffer_size=10.0) + + # Add transforms for different frame pairs + transform1 = Transform( + translation=Vector3(1.0, 0.0, 0.0), + frame_id="world", + child_frame_id="robot1", + ts=time.time(), + ) + + transform2 = Transform( + translation=Vector3(2.0, 0.0, 0.0), + frame_id="world", + child_frame_id="robot2", + ts=time.time(), + ) + + ttbuffer.receive_transform(transform1, transform2) + + print(ttbuffer.graph()) + + def test_get_latest_transform(self) -> None: + ttbuffer = MultiTBuffer() + + # Add multiple transforms + for i in range(3): + transform = Transform( + translation=Vector3(float(i), 0.0, 0.0), + frame_id="world", + child_frame_id="robot", + ts=time.time() + i * 0.1, + ) + ttbuffer.receive_transform(transform) + time.sleep(0.01) + + # Get latest transform + latest = ttbuffer.get("world", "robot") + assert latest is not None + assert latest.translation.x == 2.0 + + def test_get_transform_at_time(self) -> None: + ttbuffer = MultiTBuffer() + base_time = time.time() + + # Add transforms at known times + for i in range(5): + transform = Transform( + translation=Vector3(float(i), 0.0, 0.0), + frame_id="world", + child_frame_id="robot", + ts=base_time + i * 0.5, + ) + ttbuffer.receive_transform(transform) + + # Get transform closest to middle time + middle_time = base_time + 1.25 # Should be closest to i=2 (t=1.0) or i=3 (t=1.5) + result = ttbuffer.get("world", "robot", time_point=middle_time) + assert result is not None + # At t=1.25, it's equidistant from i=2 (t=1.0) and i=3 (t=1.5) + # The implementation picks the later one when equidistant + assert result.translation.x == 3.0 + + def test_time_tolerance(self) -> None: + ttbuffer = MultiTBuffer() + base_time = time.time() + + # Add single transform + transform = Transform( + translation=Vector3(1.0, 0.0, 0.0), + frame_id="world", + child_frame_id="robot", + ts=base_time, + ) + ttbuffer.receive_transform(transform) + + # Within tolerance + result = ttbuffer.get("world", "robot", time_point=base_time + 0.1, time_tolerance=0.2) + assert result is not None + + # Outside tolerance + result = ttbuffer.get("world", "robot", time_point=base_time + 0.5, time_tolerance=0.1) + assert result is None + + def test_nonexistent_frame_pair(self) -> None: + ttbuffer = MultiTBuffer() + + # Try to get transform for non-existent frame pair + result = ttbuffer.get("foo", "bar") + assert result is None + + def test_get_transform_search_direct(self) -> None: + ttbuffer = MultiTBuffer() + base_time = time.time() + + # Add direct transform + transform = Transform( + translation=Vector3(1.0, 0.0, 0.0), + frame_id="world", + child_frame_id="robot", + ts=base_time, + ) + ttbuffer.receive_transform(transform) + + # Search should return single transform + result = ttbuffer.get_transform_search("world", "robot") + assert result is not None + assert len(result) == 1 + assert result[0].translation.x == 1.0 + + def test_get_transform_search_chain(self) -> None: + ttbuffer = MultiTBuffer() + base_time = time.time() + + # Create transform chain: world -> robot -> sensor + transform1 = Transform( + translation=Vector3(1.0, 0.0, 0.0), + frame_id="world", + child_frame_id="robot", + ts=base_time, + ) + transform2 = Transform( + translation=Vector3(0.0, 2.0, 0.0), + frame_id="robot", + child_frame_id="sensor", + ts=base_time, + ) + ttbuffer.receive_transform(transform1, transform2) + + # Search should find chain + result = ttbuffer.get_transform_search("world", "sensor") + assert result is not None + assert len(result) == 2 + assert result[0].translation.x == 1.0 # world -> robot + assert result[1].translation.y == 2.0 # robot -> sensor + + def test_get_transform_search_complex_chain(self) -> None: + ttbuffer = MultiTBuffer() + base_time = time.time() + + # Create more complex graph: + # world -> base -> arm -> hand + # \-> robot -> sensor + transforms = [ + Transform( + frame_id="world", + child_frame_id="base", + translation=Vector3(1.0, 0.0, 0.0), + ts=base_time, + ), + Transform( + frame_id="base", + child_frame_id="arm", + translation=Vector3(0.0, 1.0, 0.0), + ts=base_time, + ), + Transform( + frame_id="arm", + child_frame_id="hand", + translation=Vector3(0.0, 0.0, 1.0), + ts=base_time, + ), + Transform( + frame_id="world", + child_frame_id="robot", + translation=Vector3(2.0, 0.0, 0.0), + ts=base_time, + ), + Transform( + frame_id="robot", + child_frame_id="sensor", + translation=Vector3(0.0, 2.0, 0.0), + ts=base_time, + ), + ] + + for t in transforms: + ttbuffer.receive_transform(t) + + # Find path world -> hand (should go through base -> arm) + result = ttbuffer.get_transform_search("world", "hand") + assert result is not None + assert len(result) == 3 + assert result[0].child_frame_id == "base" + assert result[1].child_frame_id == "arm" + assert result[2].child_frame_id == "hand" + + def test_get_transform_search_no_path(self) -> None: + ttbuffer = MultiTBuffer() + base_time = time.time() + + # Create disconnected transforms + transform1 = Transform(frame_id="world", child_frame_id="robot", ts=base_time) + transform2 = Transform(frame_id="base", child_frame_id="sensor", ts=base_time) + ttbuffer.receive_transform(transform1, transform2) + + # No path exists + result = ttbuffer.get_transform_search("world", "sensor") + assert result is None + + def test_get_transform_search_with_time(self) -> None: + ttbuffer = MultiTBuffer() + base_time = time.time() + + # Add transforms at different times + old_transform = Transform( + frame_id="world", + child_frame_id="robot", + translation=Vector3(1.0, 0.0, 0.0), + ts=base_time - 10.0, + ) + new_transform = Transform( + frame_id="world", + child_frame_id="robot", + translation=Vector3(2.0, 0.0, 0.0), + ts=base_time, + ) + ttbuffer.receive_transform(old_transform, new_transform) + + # Search at specific time + result = ttbuffer.get_transform_search("world", "robot", time_point=base_time) + assert result is not None + assert result[0].translation.x == 2.0 + + # Search with time tolerance + result = ttbuffer.get_transform_search( + "world", "robot", time_point=base_time + 1.0, time_tolerance=0.1 + ) + assert result is None # Outside tolerance + + def test_get_transform_search_shortest_path(self) -> None: + ttbuffer = MultiTBuffer() + base_time = time.time() + + # Create graph with multiple paths: + # world -> A -> B -> target (3 hops) + # world -> target (direct, 1 hop) + transforms = [ + Transform(frame_id="world", child_frame_id="A", ts=base_time), + Transform(frame_id="A", child_frame_id="B", ts=base_time), + Transform(frame_id="B", child_frame_id="target", ts=base_time), + Transform(frame_id="world", child_frame_id="target", ts=base_time), + ] + + for t in transforms: + ttbuffer.receive_transform(t) + + # BFS should find the direct path (shortest) + result = ttbuffer.get_transform_search("world", "target") + assert result is not None + assert len(result) == 1 # Direct path, not the 3-hop path + assert result[0].child_frame_id == "target" + + def test_string_representations(self) -> None: + # Test empty buffers + empty_buffer = TBuffer() + assert str(empty_buffer) == "TBuffer(empty)" + + empty_ttbuffer = MultiTBuffer() + assert str(empty_ttbuffer) == "MultiTBuffer(empty)" + + # Test TBuffer with data + buffer = TBuffer() + base_time = time.time() + for i in range(3): + transform = Transform( + translation=Vector3(float(i), 0.0, 0.0), + frame_id="world", + child_frame_id="robot", + ts=base_time + i * 0.1, + ) + buffer.add(transform) + + buffer_str = str(buffer) + assert "3 msgs" in buffer_str + assert "world -> robot" in buffer_str + assert "0.20s" in buffer_str # duration + + # Test MultiTBuffer with multiple frame pairs + ttbuffer = MultiTBuffer() + transforms = [ + Transform(frame_id="world", child_frame_id="robot1", ts=base_time), + Transform(frame_id="world", child_frame_id="robot2", ts=base_time + 0.5), + Transform(frame_id="robot1", child_frame_id="sensor", ts=base_time + 1.0), + ] + + for t in transforms: + ttbuffer.receive_transform(t) + + ttbuffer_str = str(ttbuffer) + print("\nMultiTBuffer string representation:") + print(ttbuffer_str) + + assert "MultiTBuffer(3 buffers):" in ttbuffer_str + assert "TBuffer(world -> robot1, 1 msgs" in ttbuffer_str + assert "TBuffer(world -> robot2, 1 msgs" in ttbuffer_str + assert "TBuffer(robot1 -> sensor, 1 msgs" in ttbuffer_str + + def test_get_with_transform_chain_composition(self) -> None: + ttbuffer = MultiTBuffer() + base_time = time.time() + + # Create transform chain: world -> robot -> sensor + # world -> robot: translate by (1, 0, 0) + transform1 = Transform( + translation=Vector3(1.0, 0.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), # Identity + frame_id="world", + child_frame_id="robot", + ts=base_time, + ) + + # robot -> sensor: translate by (0, 2, 0) and rotate 90 degrees around Z + import math + + # 90 degrees around Z: quaternion (0, 0, sin(45°), cos(45°)) + transform2 = Transform( + translation=Vector3(0.0, 2.0, 0.0), + rotation=Quaternion(0.0, 0.0, math.sin(math.pi / 4), math.cos(math.pi / 4)), + frame_id="robot", + child_frame_id="sensor", + ts=base_time, + ) + + ttbuffer.receive_transform(transform1, transform2) + + # Get composed transform from world to sensor + result = ttbuffer.get("world", "sensor") + assert result is not None + + # The composed transform should: + # 1. Apply world->robot translation: (1, 0, 0) + # 2. Apply robot->sensor translation in robot frame: (0, 2, 0) + # Total translation: (1, 2, 0) + assert abs(result.translation.x - 1.0) < 1e-6 + assert abs(result.translation.y - 2.0) < 1e-6 + assert abs(result.translation.z - 0.0) < 1e-6 + + # Rotation should be 90 degrees around Z (same as transform2) + assert abs(result.rotation.x - 0.0) < 1e-6 + assert abs(result.rotation.y - 0.0) < 1e-6 + assert abs(result.rotation.z - math.sin(math.pi / 4)) < 1e-6 + assert abs(result.rotation.w - math.cos(math.pi / 4)) < 1e-6 + + # Frame IDs should be correct + assert result.frame_id == "world" + assert result.child_frame_id == "sensor" + + def test_get_with_longer_transform_chain(self) -> None: + ttbuffer = MultiTBuffer() + base_time = time.time() + + # Create longer chain: world -> base -> arm -> hand + # Each adds a translation along different axes + transforms = [ + Transform( + translation=Vector3(1.0, 0.0, 0.0), # Move 1 along X + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="world", + child_frame_id="base", + ts=base_time, + ), + Transform( + translation=Vector3(0.0, 2.0, 0.0), # Move 2 along Y + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="base", + child_frame_id="arm", + ts=base_time, + ), + Transform( + translation=Vector3(0.0, 0.0, 3.0), # Move 3 along Z + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="arm", + child_frame_id="hand", + ts=base_time, + ), + ] + + for t in transforms: + ttbuffer.receive_transform(t) + + # Get composed transform from world to hand + result = ttbuffer.get("world", "hand") + assert result is not None + + # Total translation should be sum of all: (1, 2, 3) + assert abs(result.translation.x - 1.0) < 1e-6 + assert abs(result.translation.y - 2.0) < 1e-6 + assert abs(result.translation.z - 3.0) < 1e-6 + + # Rotation should still be identity (all rotations were identity) + assert abs(result.rotation.x - 0.0) < 1e-6 + assert abs(result.rotation.y - 0.0) < 1e-6 + assert abs(result.rotation.z - 0.0) < 1e-6 + assert abs(result.rotation.w - 1.0) < 1e-6 + + assert result.frame_id == "world" + assert result.child_frame_id == "hand" diff --git a/dimos/protocol/tf/tf.py b/dimos/protocol/tf/tf.py new file mode 100644 index 0000000000..f60e216176 --- /dev/null +++ b/dimos/protocol/tf/tf.py @@ -0,0 +1,352 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import abstractmethod +from collections import deque +from dataclasses import dataclass, field +from functools import reduce +from typing import TypeVar + +from dimos.msgs.geometry_msgs import Transform +from dimos.msgs.tf2_msgs import TFMessage +from dimos.protocol.pubsub.lcmpubsub import LCM, Topic +from dimos.protocol.pubsub.spec import PubSub +from dimos.protocol.service.lcmservice import Service +from dimos.types.timestamped import TimestampedCollection + +CONFIG = TypeVar("CONFIG") + + +# generic configuration for transform service +@dataclass +class TFConfig: + buffer_size: float = 10.0 # seconds + rate_limit: float = 10.0 # Hz + + +# generic specification for transform service +class TFSpec(Service[TFConfig]): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + + @abstractmethod + def publish(self, *args: Transform) -> None: ... + + @abstractmethod + def publish_static(self, *args: Transform) -> None: ... + + def get_frames(self) -> set[str]: + return set() + + @abstractmethod + def get( + self, + parent_frame: str, + child_frame: str, + time_point: float | None = None, + time_tolerance: float | None = None, + ): ... + + def receive_transform(self, *args: Transform) -> None: ... + + def receive_tfmessage(self, msg: TFMessage) -> None: + for transform in msg.transforms: + self.receive_transform(transform) + + +MsgT = TypeVar("MsgT") +TopicT = TypeVar("TopicT") + + +# stores a single transform +class TBuffer(TimestampedCollection[Transform]): + def __init__(self, buffer_size: float = 10.0) -> None: + super().__init__() + self.buffer_size = buffer_size + + def add(self, transform: Transform) -> None: + super().add(transform) + self._prune_old_transforms(transform.ts) + + def _prune_old_transforms(self, current_time) -> None: + if not self._items: + return + + cutoff_time = current_time - self.buffer_size + + while self._items and self._items[0].ts < cutoff_time: + self._items.pop(0) + + def get(self, time_point: float | None = None, time_tolerance: float = 1.0) -> Transform | None: + """Get transform at specified time or latest if no time given.""" + if time_point is None: + # Return the latest transform + return self[-1] if len(self) > 0 else None + + return self.find_closest(time_point, time_tolerance) + + def __str__(self) -> str: + if not self._items: + return "TBuffer(empty)" + + # Get unique frame info from the transforms + frame_pairs = set() + if self._items: + frame_pairs.add((self._items[0].frame_id, self._items[0].child_frame_id)) + + time_range = self.time_range() + if time_range: + from dimos.types.timestamped import to_human_readable + + start_time = to_human_readable(time_range[0]) + end_time = to_human_readable(time_range[1]) + duration = time_range[1] - time_range[0] + + frame_str = ( + f"{self._items[0].frame_id} -> {self._items[0].child_frame_id}" + if self._items + else "unknown" + ) + + return ( + f"TBuffer(" + f"{frame_str}, " + f"{len(self._items)} msgs, " + f"{duration:.2f}s [{start_time} - {end_time}])" + ) + + return f"TBuffer({len(self._items)} msgs)" + + +# stores multiple transform buffers +# creates a new buffer on demand when new transform is detected +class MultiTBuffer: + def __init__(self, buffer_size: float = 10.0) -> None: + self.buffers: dict[tuple[str, str], TBuffer] = {} + self.buffer_size = buffer_size + + def receive_transform(self, *args: Transform) -> None: + for transform in args: + key = (transform.frame_id, transform.child_frame_id) + if key not in self.buffers: + self.buffers[key] = TBuffer(self.buffer_size) + self.buffers[key].add(transform) + + def get_frames(self) -> set[str]: + frames = set() + for parent, child in self.buffers: + frames.add(parent) + frames.add(child) + return frames + + def get_connections(self, frame_id: str) -> set[str]: + """Get all frames connected to the given frame (both as parent and child).""" + connections = set() + for parent, child in self.buffers: + if parent == frame_id: + connections.add(child) + if child == frame_id: + connections.add(parent) + return connections + + def get_transform( + self, + parent_frame: str, + child_frame: str, + time_point: float | None = None, + time_tolerance: float | None = None, + ) -> Transform | None: + # Check forward direction + key = (parent_frame, child_frame) + if key in self.buffers: + return self.buffers[key].get(time_point, time_tolerance) + + # Check reverse direction and return inverse + reverse_key = (child_frame, parent_frame) + if reverse_key in self.buffers: + transform = self.buffers[reverse_key].get(time_point, time_tolerance) + return transform.inverse() if transform else None + + return None + + def get(self, *args, **kwargs) -> Transform | None: + simple = self.get_transform(*args, **kwargs) + if simple is not None: + return simple + + complex = self.get_transform_search(*args, **kwargs) + + if complex is None: + return None + + return reduce(lambda t1, t2: t1 + t2, complex) + + def get_transform_search( + self, + parent_frame: str, + child_frame: str, + time_point: float | None = None, + time_tolerance: float | None = None, + ) -> list[Transform] | None: + """Search for shortest transform chain between parent and child frames using BFS.""" + # Check if direct transform exists (already checked in get_transform, but for clarity) + direct = self.get_transform(parent_frame, child_frame, time_point, time_tolerance) + if direct is not None: + return [direct] + + # BFS to find shortest path + queue: deque[tuple[str, list[Transform]]] = deque([(parent_frame, [])]) + visited = {parent_frame} + + while queue: + current_frame, path = queue.popleft() + + if current_frame == child_frame: + return path + + # Get all connections for current frame + connections = self.get_connections(current_frame) + + for next_frame in connections: + if next_frame not in visited: + visited.add(next_frame) + + # Get the transform between current and next frame + transform = self.get_transform( + current_frame, next_frame, time_point, time_tolerance + ) + if transform: + queue.append((next_frame, [*path, transform])) + + return None + + def graph(self) -> str: + import subprocess + + def connection_str(connection: tuple[str, str]) -> str: + (frame_from, frame_to) = connection + return f"{frame_from} -> {frame_to}" + + graph_str = "\n".join(map(connection_str, self.buffers.keys())) + + try: + result = subprocess.run( + ["diagon", "GraphDAG", "-style=Unicode"], + input=graph_str, + capture_output=True, + text=True, + ) + return result.stdout if result.returncode == 0 else graph_str + except Exception: + return "no diagon installed" + + def __str__(self) -> str: + if not self.buffers: + return f"{self.__class__.__name__}(empty)" + + lines = [f"{self.__class__.__name__}({len(self.buffers)} buffers):"] + for buffer in self.buffers.values(): + lines.append(f" {buffer}") + + return "\n".join(lines) + + +@dataclass +class PubSubTFConfig(TFConfig): + topic: Topic | None = None # Required field but needs default for dataclass inheritance + pubsub: type[PubSub] | PubSub | None = None + autostart: bool = True + + +class PubSubTF(MultiTBuffer, TFSpec): + default_config: type[PubSubTFConfig] = PubSubTFConfig + + def __init__(self, **kwargs) -> None: + TFSpec.__init__(self, **kwargs) + MultiTBuffer.__init__(self, self.config.buffer_size) + + pubsub_config = getattr(self.config, "pubsub", None) + if pubsub_config is not None: + if callable(pubsub_config): + self.pubsub = pubsub_config() + else: + self.pubsub = pubsub_config + else: + raise ValueError("PubSub configuration is missing") + + if self.config.autostart: + self.start() + + def start(self, sub: bool = True) -> None: + self.pubsub.start() + if sub: + topic = getattr(self.config, "topic", None) + if topic: + self.pubsub.subscribe(topic, self.receive_msg) + + def stop(self) -> None: + self.pubsub.stop() + + def publish(self, *args: Transform) -> None: + """Send transforms using the configured PubSub.""" + if not self.pubsub: + raise ValueError("PubSub is not configured.") + + self.receive_transform(*args) + topic = getattr(self.config, "topic", None) + if topic: + self.pubsub.publish(topic, TFMessage(*args)) + + def publish_static(self, *args: Transform) -> None: + raise NotImplementedError("Static transforms not implemented in PubSubTF.") + + def publish_all(self) -> None: + """Publish all transforms currently stored in all buffers.""" + all_transforms = [] + for buffer in self.buffers.values(): + # Get the latest transform from each buffer + latest = buffer.get() # get() with no args returns latest + if latest: + all_transforms.append(latest) + + if all_transforms: + self.publish(*all_transforms) + + def get( + self, + parent_frame: str, + child_frame: str, + time_point: float | None = None, + time_tolerance: float | None = None, + ) -> Transform | None: + return super().get(parent_frame, child_frame, time_point, time_tolerance) + + def receive_msg(self, msg: TFMessage, topic: Topic) -> None: + self.receive_tfmessage(msg) + + +@dataclass +class LCMPubsubConfig(PubSubTFConfig): + topic: Topic = field(default_factory=lambda: Topic("/tf", TFMessage)) + pubsub: type[PubSub] | PubSub | None = LCM + autostart: bool = True + + +class LCMTF(PubSubTF): + default_config: type[LCMPubsubConfig] = LCMPubsubConfig + + +TF = LCMTF diff --git a/dimos/protocol/tf/tflcmcpp.py b/dimos/protocol/tf/tflcmcpp.py new file mode 100644 index 0000000000..0d5b31b9b6 --- /dev/null +++ b/dimos/protocol/tf/tflcmcpp.py @@ -0,0 +1,93 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import datetime +from typing import Union + +from dimos.msgs.geometry_msgs import Transform +from dimos.protocol.service.lcmservice import LCMConfig, LCMService +from dimos.protocol.tf.tf import TFConfig, TFSpec + + +# this doesn't work due to tf_lcm_py package +class TFLCM(TFSpec, LCMService): + """A service for managing and broadcasting transforms using LCM. + This is not a separete module, You can include this in your module + if you need to access transforms. + + Ideally we would have a generic pubsub for transforms so we are + transport agnostic (TODO) + + For now we are not doing this because we want to use cpp buffer/lcm + implementation. We also don't want to manually hook up tf stream + for each module. + """ + + default_config = Union[TFConfig, LCMConfig] + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + + import tf_lcm_py as tf + + self.l = tf.LCM() + self.buffer = tf.Buffer(self.config.buffer_size) + self.listener = tf.TransformListener(self.l, self.buffer) + self.broadcaster = tf.TransformBroadcaster() + self.static_broadcaster = tf.StaticTransformBroadcaster() + + # will call the underlying LCMService.start + self.start() + + def send(self, *args: Transform) -> None: + for t in args: + self.broadcaster.send_transform(t.lcm_transform()) + + def send_static(self, *args: Transform) -> None: + for t in args: + self.static_broadcaster.send_static_transform(t) + + def lookup( + self, + parent_frame: str, + child_frame: str, + time_point: float | None = None, + time_tolerance: float | None = None, + ): + return self.buffer.lookup_transform( + parent_frame, + child_frame, + datetime.now(), + lcm_module=self.l, + ) + + def can_transform( + self, parent_frame: str, child_frame: str, time_point: float | datetime | None = None + ) -> bool: + if not time_point: + time_point = datetime.now() + + if isinstance(time_point, float): + time_point = datetime.fromtimestamp(time_point) + + return self.buffer.can_transform(parent_frame, child_frame, time_point) + + def get_frames(self) -> set[str]: + return set(self.buffer.get_all_frame_names()) + + def start(self) -> None: + super().start() + ... + + def stop(self) -> None: ... diff --git a/dimos/robot/abstract_robot.py b/dimos/robot/abstract_robot.py deleted file mode 100644 index 4c6345f1b0..0000000000 --- a/dimos/robot/abstract_robot.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Abstract base class for all DIMOS robot implementations. - -This module defines the AbstractRobot class which serves as the foundation for -all robot implementations in DIMOS, establishing a common interface regardless -of the underlying hardware or communication protocol (ROS, WebRTC, etc). -""" - -from abc import ABC, abstractmethod -from typing import Any, Union, Optional -from reactivex.observable import Observable -import numpy as np - - -class AbstractRobot(ABC): - """Abstract base class for all robot implementations. - - This class defines the minimal interface that all robot implementations - must provide, regardless of whether they use ROS, WebRTC, or other - communication protocols. - """ - - @abstractmethod - def connect(self) -> bool: - """Establish a connection to the robot. - - This method should handle all necessary setup to establish - communication with the robot hardware. - - Returns: - bool: True if connection was successful, False otherwise. - """ - pass - - @abstractmethod - def move(self, *args, **kwargs) -> bool: - """Move the robot. - - This is a generic movement interface that should be implemented - by all robot classes. The exact parameters will depend on the - specific robot implementation. - - Returns: - bool: True if movement command was successfully sent. - """ - pass - - @abstractmethod - def get_video_stream(self, fps: int = 30) -> Observable: - """Get a video stream from the robot's camera. - - Args: - fps: Frames per second for the video stream. Defaults to 30. - - Returns: - Observable: An observable stream of video frames. - """ - pass - - @abstractmethod - def stop(self) -> None: - """Clean up resources and stop the robot. - - This method should handle all necessary cleanup when shutting down - the robot connection, including stopping any ongoing movements. - """ - pass diff --git a/dimos/robot/agilex/README.md b/dimos/robot/agilex/README.md new file mode 100644 index 0000000000..1e678cae65 --- /dev/null +++ b/dimos/robot/agilex/README.md @@ -0,0 +1,371 @@ +# DIMOS Manipulator Robot Development Guide + +This guide explains how to create robot classes, integrate agents, and use the DIMOS module system with LCM transport. + +## Table of Contents +1. [Robot Class Architecture](#robot-class-architecture) +2. [Module System & LCM Transport](#module-system--lcm-transport) +3. [Agent Integration](#agent-integration) +4. [Complete Example](#complete-example) + +## Robot Class Architecture + +### Basic Robot Class Structure + +A DIMOS robot class should follow this pattern: + +```python +from typing import Optional, List +from dimos import core +from dimos.types.robot_capabilities import RobotCapability + +class YourRobot: + """Your robot implementation.""" + + def __init__(self, robot_capabilities: Optional[List[RobotCapability]] = None): + # Core components + self.dimos = None + self.modules = {} + self.skill_library = SkillLibrary() + + # Define capabilities + self.capabilities = robot_capabilities or [ + RobotCapability.VISION, + RobotCapability.MANIPULATION, + ] + + async def start(self): + """Start the robot modules.""" + # Initialize DIMOS with worker count + self.dimos = core.start(2) # Number of workers needed + + # Deploy modules + # ... (see Module System section) + + def stop(self): + """Stop all modules and clean up.""" + # Stop modules + # Close DIMOS + if self.dimos: + self.dimos.close() +``` + +### Key Components Explained + +1. **Initialization**: Store references to modules, skills, and capabilities +2. **Async Start**: Modules must be deployed asynchronously +3. **Proper Cleanup**: Always stop modules before closing DIMOS + +## Module System & LCM Transport + +### Understanding DIMOS Modules + +Modules are the building blocks of DIMOS robots. They: +- Process data streams (inputs) +- Produce outputs +- Can be connected together +- Communicate via LCM (Lightweight Communications and Marshalling) + +### Deploying a Module + +```python +# Deploy a camera module +self.camera = self.dimos.deploy( + ZEDModule, # Module class + camera_id=0, # Module parameters + resolution="HD720", + depth_mode="NEURAL", + fps=30, + publish_rate=30.0, + frame_id="camera_frame" +) +``` + +### Setting Up LCM Transport + +LCM transport enables inter-module communication: + +```python +# Enable LCM auto-configuration +from dimos.protocol import pubsub +pubsub.lcm.autoconf() + +# Configure output transport +self.camera.color_image.transport = core.LCMTransport( + "/camera/color_image", # Topic name + Image # Message type +) +self.camera.depth_image.transport = core.LCMTransport( + "/camera/depth_image", + Image +) +``` + +### Connecting Modules + +Connect module outputs to inputs: + +```python +# Connect manipulation module to camera outputs +self.manipulation.rgb_image.connect(self.camera.color_image) +self.manipulation.depth_image.connect(self.camera.depth_image) +self.manipulation.camera_info.connect(self.camera.camera_info) +``` + +### Module Communication Pattern + +``` +┌──────────────┐ LCM ┌────────────────┐ LCM ┌──────────────┐ +│ Camera │────────▶│ Manipulation │────────▶│ Visualization│ +│ Module │ Messages│ Module │ Messages│ Output │ +└──────────────┘ └────────────────┘ └──────────────┘ + ▲ ▲ + │ │ + └──────────────────────────┘ + Direct Connection via RPC call +``` + +## Agent Integration + +### Setting Up Agent with Robot + +The run file pattern for agent integration: + +```python +#!/usr/bin/env python3 +import asyncio +import reactivex as rx +from dimos.agents.claude_agent import ClaudeAgent +from dimos.web.robot_web_interface import RobotWebInterface + +def main(): + # 1. Create and start robot + robot = YourRobot() + asyncio.run(robot.start()) + + # 2. Set up skills + skills = robot.get_skills() + skills.add(YourSkill) + skills.create_instance("YourSkill", robot=robot) + + # 3. Set up reactive streams + agent_response_subject = rx.subject.Subject() + agent_response_stream = agent_response_subject.pipe(ops.share()) + + # 4. Create web interface + web_interface = RobotWebInterface( + port=5555, + text_streams={"agent_responses": agent_response_stream}, + audio_subject=rx.subject.Subject() + ) + + # 5. Create agent + agent = ClaudeAgent( + dev_name="your_agent", + input_query_stream=web_interface.query_stream, + skills=skills, + system_query="Your system prompt here", + model_name="claude-3-5-haiku-latest" + ) + + # 6. Connect agent responses + agent.get_response_observable().subscribe( + lambda x: agent_response_subject.on_next(x) + ) + + # 7. Run interface + web_interface.run() +``` + +### Key Integration Points + +1. **Reactive Streams**: Use RxPy for event-driven communication +2. **Web Interface**: Provides user input/output +3. **Agent**: Processes natural language and executes skills +4. **Skills**: Define robot capabilities as executable actions + +## Complete Example + +### Step 1: Create Robot Class (`my_robot.py`) + +```python +import asyncio +from typing import Optional, List +from dimos import core +from dimos.hardware.camera import CameraModule +from dimos.manipulation.module import ManipulationModule +from dimos.skills.skills import SkillLibrary +from dimos.types.robot_capabilities import RobotCapability +from dimos_lcm.sensor_msgs import Image, CameraInfo +from dimos.protocol import pubsub + +class MyRobot: + def __init__(self, robot_capabilities: Optional[List[RobotCapability]] = None): + self.dimos = None + self.camera = None + self.manipulation = None + self.skill_library = SkillLibrary() + + self.capabilities = robot_capabilities or [ + RobotCapability.VISION, + RobotCapability.MANIPULATION, + ] + + async def start(self): + # Start DIMOS + self.dimos = core.start(2) + + # Enable LCM + pubsub.lcm.autoconf() + + # Deploy camera + self.camera = self.dimos.deploy( + CameraModule, + camera_id=0, + fps=30 + ) + + # Configure camera LCM + self.camera.color_image.transport = core.LCMTransport("/camera/rgb", Image) + self.camera.depth_image.transport = core.LCMTransport("/camera/depth", Image) + self.camera.camera_info.transport = core.LCMTransport("/camera/info", CameraInfo) + + # Deploy manipulation + self.manipulation = self.dimos.deploy(ManipulationModule) + + # Connect modules + self.manipulation.rgb_image.connect(self.camera.color_image) + self.manipulation.depth_image.connect(self.camera.depth_image) + self.manipulation.camera_info.connect(self.camera.camera_info) + + # Configure manipulation output + self.manipulation.viz_image.transport = core.LCMTransport("/viz/output", Image) + + # Start modules + self.camera.start() + self.manipulation.start() + + await asyncio.sleep(2) # Allow initialization + + def get_skills(self): + return self.skill_library + + def stop(self): + if self.manipulation: + self.manipulation.stop() + if self.camera: + self.camera.stop() + if self.dimos: + self.dimos.close() +``` + +### Step 2: Create Run Script (`run.py`) + +```python +#!/usr/bin/env python3 +import asyncio +import os +from my_robot import MyRobot +from dimos.agents.claude_agent import ClaudeAgent +from dimos.skills.basic import BasicSkill +from dimos.web.robot_web_interface import RobotWebInterface +import reactivex as rx +import reactivex.operators as ops + +SYSTEM_PROMPT = """You are a helpful robot assistant.""" + +def main(): + # Check API key + if not os.getenv("ANTHROPIC_API_KEY"): + print("Please set ANTHROPIC_API_KEY") + return + + # Create robot + robot = MyRobot() + + try: + # Start robot + asyncio.run(robot.start()) + + # Set up skills + skills = robot.get_skills() + skills.add(BasicSkill) + skills.create_instance("BasicSkill", robot=robot) + + # Set up streams + agent_response_subject = rx.subject.Subject() + agent_response_stream = agent_response_subject.pipe(ops.share()) + + # Create web interface + web_interface = RobotWebInterface( + port=5555, + text_streams={"agent_responses": agent_response_stream} + ) + + # Create agent + agent = ClaudeAgent( + dev_name="my_agent", + input_query_stream=web_interface.query_stream, + skills=skills, + system_query=SYSTEM_PROMPT + ) + + # Connect responses + agent.get_response_observable().subscribe( + lambda x: agent_response_subject.on_next(x) + ) + + print("Robot ready at http://localhost:5555") + + # Run + web_interface.run() + + finally: + robot.stop() + +if __name__ == "__main__": + main() +``` + +### Step 3: Define Skills (`skills.py`) + +```python +from dimos.skills import Skill, skill + +@skill( + description="Perform a basic action", + parameters={ + "action": "The action to perform" + } +) +class BasicSkill(Skill): + def __init__(self, robot): + self.robot = robot + + def run(self, action: str): + # Implement skill logic + return f"Performed: {action}" +``` + +## Best Practices + +1. **Module Lifecycle**: Always start DIMOS before deploying modules +2. **LCM Topics**: Use descriptive topic names with namespaces +3. **Error Handling**: Wrap module operations in try-except blocks +4. **Resource Cleanup**: Ensure proper cleanup in stop() methods +5. **Async Operations**: Use asyncio for non-blocking operations +6. **Stream Management**: Use RxPy for reactive programming patterns + +## Debugging Tips + +1. **Check Module Status**: Print module.io().result() to see connections +2. **Monitor LCM**: Use Foxglove to visualize LCM messages +3. **Log Everything**: Use dimos.utils.logging_config.setup_logger() +4. **Test Modules Independently**: Deploy and test one module at a time + +## Common Issues + +1. **"Module not started"**: Ensure start() is called after deployment +2. **"No data received"**: Check LCM transport configuration +3. **"Connection failed"**: Verify input/output types match +4. **"Cleanup errors"**: Stop modules before closing DIMOS \ No newline at end of file diff --git a/dimos/robot/agilex/README_CN.md b/dimos/robot/agilex/README_CN.md new file mode 100644 index 0000000000..482a09dd6d --- /dev/null +++ b/dimos/robot/agilex/README_CN.md @@ -0,0 +1,465 @@ +# DIMOS 机械臂机器人开发指南 + +本指南介绍如何创建机器人类、集成智能体(Agent)以及使用 DIMOS 模块系统和 LCM 传输。 + +## 目录 +1. [机器人类架构](#机器人类架构) +2. [模块系统与 LCM 传输](#模块系统与-lcm-传输) +3. [智能体集成](#智能体集成) +4. [完整示例](#完整示例) + +## 机器人类架构 + +### 基本机器人类结构 + +DIMOS 机器人类应遵循以下模式: + +```python +from typing import Optional, List +from dimos import core +from dimos.types.robot_capabilities import RobotCapability + +class YourRobot: + """您的机器人实现。""" + + def __init__(self, robot_capabilities: Optional[List[RobotCapability]] = None): + # 核心组件 + self.dimos = None + self.modules = {} + self.skill_library = SkillLibrary() + + # 定义能力 + self.capabilities = robot_capabilities or [ + RobotCapability.VISION, + RobotCapability.MANIPULATION, + ] + + async def start(self): + """启动机器人模块。""" + # 初始化 DIMOS,指定工作线程数 + self.dimos = core.start(2) # 需要的工作线程数 + + # 部署模块 + # ... (参见模块系统章节) + + def stop(self): + """停止所有模块并清理资源。""" + # 停止模块 + # 关闭 DIMOS + if self.dimos: + self.dimos.close() +``` + +### 关键组件说明 + +1. **初始化**:存储模块、技能和能力的引用 +2. **异步启动**:模块必须异步部署 +3. **正确清理**:在关闭 DIMOS 之前始终停止模块 + +## 模块系统与 LCM 传输 + +### 理解 DIMOS 模块 + +模块是 DIMOS 机器人的构建块。它们: +- 处理数据流(输入) +- 产生输出 +- 可以相互连接 +- 通过 LCM(轻量级通信和编组)进行通信 + +### 部署模块 + +```python +# 部署相机模块 +self.camera = self.dimos.deploy( + ZEDModule, # 模块类 + camera_id=0, # 模块参数 + resolution="HD720", + depth_mode="NEURAL", + fps=30, + publish_rate=30.0, + frame_id="camera_frame" +) +``` + +### 设置 LCM 传输 + +LCM 传输实现模块间通信: + +```python +# 启用 LCM 自动配置 +from dimos.protocol import pubsub +pubsub.lcm.autoconf() + +# 配置输出传输 +self.camera.color_image.transport = core.LCMTransport( + "/camera/color_image", # 主题名称 + Image # 消息类型 +) +self.camera.depth_image.transport = core.LCMTransport( + "/camera/depth_image", + Image +) +``` + +### 连接模块 + +将模块输出连接到输入: + +```python +# 将操作模块连接到相机输出 +self.manipulation.rgb_image.connect(self.camera.color_image) # ROS set_callback +self.manipulation.depth_image.connect(self.camera.depth_image) +self.manipulation.camera_info.connect(self.camera.camera_info) +``` + +### 模块通信模式 + +``` +┌──────────────┐ LCM ┌────────────────┐ LCM ┌──────────────┐ +│ 相机模块 │────────▶│ 操作模块 │────────▶│ 可视化输出 │ +│ │ 消息 │ │ 消息 │ │ +└──────────────┘ └────────────────┘ └──────────────┘ + ▲ ▲ + │ │ + └──────────────────────────┘ + 直接连接(RPC指令) +``` + +## 智能体集成 + +### 设置智能体与机器人 + +运行文件的智能体集成模式: + +```python +#!/usr/bin/env python3 +import asyncio +import reactivex as rx +from dimos.agents.claude_agent import ClaudeAgent +from dimos.web.robot_web_interface import RobotWebInterface + +def main(): + # 1. 创建并启动机器人 + robot = YourRobot() + asyncio.run(robot.start()) + + # 2. 设置技能 + skills = robot.get_skills() + skills.add(YourSkill) + skills.create_instance("YourSkill", robot=robot) + + # 3. 设置响应式流 + agent_response_subject = rx.subject.Subject() + agent_response_stream = agent_response_subject.pipe(ops.share()) + + # 4. 创建 Web 界面 + web_interface = RobotWebInterface( + port=5555, + text_streams={"agent_responses": agent_response_stream}, + audio_subject=rx.subject.Subject() + ) + + # 5. 创建智能体 + agent = ClaudeAgent( + dev_name="your_agent", + input_query_stream=web_interface.query_stream, + skills=skills, + system_query="您的系统提示词", + model_name="claude-3-5-haiku-latest" + ) + + # 6. 连接智能体响应 + agent.get_response_observable().subscribe( + lambda x: agent_response_subject.on_next(x) + ) + + # 7. 运行界面 + web_interface.run() +``` + +### 关键集成点 + +1. **响应式流**:使用 RxPy 进行事件驱动通信 +2. **Web 界面**:提供用户输入/输出 +3. **智能体**:处理自然语言并执行技能 +4. **技能**:将机器人能力定义为可执行动作 + +## 完整示例 + +### 步骤 1:创建机器人类(`my_robot.py`) + +```python +import asyncio +from typing import Optional, List +from dimos import core +from dimos.hardware.camera import CameraModule +from dimos.manipulation.module import ManipulationModule +from dimos.skills.skills import SkillLibrary +from dimos.types.robot_capabilities import RobotCapability +from dimos_lcm.sensor_msgs import Image, CameraInfo +from dimos.protocol import pubsub + +class MyRobot: + def __init__(self, robot_capabilities: Optional[List[RobotCapability]] = None): + self.dimos = None + self.camera = None + self.manipulation = None + self.skill_library = SkillLibrary() + + self.capabilities = robot_capabilities or [ + RobotCapability.VISION, + RobotCapability.MANIPULATION, + ] + + async def start(self): + # 启动 DIMOS + self.dimos = core.start(2) + + # 启用 LCM + pubsub.lcm.autoconf() + + # 部署相机 + self.camera = self.dimos.deploy( + CameraModule, + camera_id=0, + fps=30 + ) + + # 配置相机 LCM + self.camera.color_image.transport = core.LCMTransport("/camera/rgb", Image) + self.camera.depth_image.transport = core.LCMTransport("/camera/depth", Image) + self.camera.camera_info.transport = core.LCMTransport("/camera/info", CameraInfo) + + # 部署操作模块 + self.manipulation = self.dimos.deploy(ManipulationModule) + + # 连接模块 + self.manipulation.rgb_image.connect(self.camera.color_image) + self.manipulation.depth_image.connect(self.camera.depth_image) + self.manipulation.camera_info.connect(self.camera.camera_info) + + # 配置操作输出 + self.manipulation.viz_image.transport = core.LCMTransport("/viz/output", Image) + + # 启动模块 + self.camera.start() + self.manipulation.start() + + await asyncio.sleep(2) # 允许初始化 + + def get_skills(self): + return self.skill_library + + def stop(self): + if self.manipulation: + self.manipulation.stop() + if self.camera: + self.camera.stop() + if self.dimos: + self.dimos.close() +``` + +### 步骤 2:创建运行脚本(`run.py`) + +```python +#!/usr/bin/env python3 +import asyncio +import os +from my_robot import MyRobot +from dimos.agents.claude_agent import ClaudeAgent +from dimos.skills.basic import BasicSkill +from dimos.web.robot_web_interface import RobotWebInterface +import reactivex as rx +import reactivex.operators as ops + +SYSTEM_PROMPT = """您是一个有用的机器人助手。""" + +def main(): + # 检查 API 密钥 + if not os.getenv("ANTHROPIC_API_KEY"): + print("请设置 ANTHROPIC_API_KEY") + return + + # 创建机器人 + robot = MyRobot() + + try: + # 启动机器人 + asyncio.run(robot.start()) + + # 设置技能 + skills = robot.get_skills() + skills.add(BasicSkill) + skills.create_instance("BasicSkill", robot=robot) + + # 设置流 + agent_response_subject = rx.subject.Subject() + agent_response_stream = agent_response_subject.pipe(ops.share()) + + # 创建 Web 界面 + web_interface = RobotWebInterface( + port=5555, + text_streams={"agent_responses": agent_response_stream} + ) + + # 创建智能体 + agent = ClaudeAgent( + dev_name="my_agent", + input_query_stream=web_interface.query_stream, + skills=skills, + system_query=SYSTEM_PROMPT + ) + + # 连接响应 + agent.get_response_observable().subscribe( + lambda x: agent_response_subject.on_next(x) + ) + + print("机器人就绪,访问 http://localhost:5555") + + # 运行 + web_interface.run() + + finally: + robot.stop() + +if __name__ == "__main__": + main() +``` + +### 步骤 3:定义技能(`skills.py`) + +```python +from dimos.skills import Skill, skill + +@skill( + description="执行一个基本动作", + parameters={ + "action": "要执行的动作" + } +) +class BasicSkill(Skill): + def __init__(self, robot): + self.robot = robot + + def run(self, action: str): + # 实现技能逻辑 + return f"已执行:{action}" +``` + +## 最佳实践 + +1. **模块生命周期**:在部署模块之前始终先启动 DIMOS +2. **LCM 主题**:使用带命名空间的描述性主题名称 +3. **错误处理**:用 try-except 块包装模块操作 +4. **资源清理**:确保在 stop() 方法中正确清理 +5. **异步操作**:使用 asyncio 进行非阻塞操作 +6. **流管理**:使用 RxPy 进行响应式编程模式 + +## 调试技巧 + +1. **检查模块状态**:打印 module.io().result() 查看连接 +2. **监控 LCM**:使用 Foxglove 可视化 LCM 消息 +3. **记录一切**:使用 dimos.utils.logging_config.setup_logger() +4. **独立测试模块**:一次部署和测试一个模块 + +## 常见问题 + +1. **"模块未启动"**:确保在部署后调用 start() +2. **"未收到数据"**:检查 LCM 传输配置 +3. **"连接失败"**:验证输入/输出类型是否匹配 +4. **"清理错误"**:在关闭 DIMOS 之前停止模块 + +## 高级主题 + +### 自定义模块开发 + +创建自定义模块的基本结构: + +```python +from dimos.core import Module, In, Out, rpc + +class CustomModule(Module): + # 定义输入 + input_data: In[DataType] = None + + # 定义输出 + output_data: Out[DataType] = None + + def __init__(self, param1, param2, **kwargs): + super().__init__(**kwargs) + self.param1 = param1 + self.param2 = param2 + + @rpc + def start(self): + """启动模块处理。""" + self.input_data.subscribe(self._process_data) + + def _process_data(self, data): + """处理输入数据。""" + # 处理逻辑 + result = self.process(data) + # 发布输出 + self.output_data.publish(result) + + @rpc + def stop(self): + """停止模块。""" + # 清理资源 + pass +``` + +### 技能开发指南 + +技能是机器人可执行的高级动作: + +```python +from dimos.skills import Skill, skill +from typing import Optional + +@skill( + description="复杂操作技能", + parameters={ + "target": "目标对象", + "location": "目标位置" + } +) +class ComplexSkill(Skill): + def __init__(self, robot, **kwargs): + super().__init__(**kwargs) + self.robot = robot + + def run(self, target: str, location: Optional[str] = None): + """执行技能逻辑。""" + try: + # 1. 感知阶段 + object_info = self.robot.detect_object(target) + + # 2. 规划阶段 + if location: + plan = self.robot.plan_movement(object_info, location) + + # 3. 执行阶段 + result = self.robot.execute_plan(plan) + + return { + "success": True, + "message": f"成功移动 {target} 到 {location}" + } + + except Exception as e: + return { + "success": False, + "error": str(e) + } +``` + +### 性能优化 + +1. **并行处理**:使用多个工作线程处理不同模块 +2. **数据缓冲**:为高频数据流实现缓冲机制 +3. **延迟加载**:仅在需要时初始化重型模块 +4. **资源池化**:重用昂贵的资源(如神经网络模型) + +希望本指南能帮助您快速上手 DIMOS 机器人开发! \ No newline at end of file diff --git a/dimos/robot/agilex/piper_arm.py b/dimos/robot/agilex/piper_arm.py new file mode 100644 index 0000000000..642d39c7cb --- /dev/null +++ b/dimos/robot/agilex/piper_arm.py @@ -0,0 +1,181 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio + +# Import LCM message types +from dimos_lcm.sensor_msgs import CameraInfo + +from dimos import core +from dimos.hardware.camera.zed import ZEDModule +from dimos.manipulation.visual_servoing.manipulation_module import ManipulationModule +from dimos.msgs.sensor_msgs import Image +from dimos.protocol import pubsub +from dimos.robot.foxglove_bridge import FoxgloveBridge +from dimos.robot.robot import Robot +from dimos.skills.skills import SkillLibrary +from dimos.types.robot_capabilities import RobotCapability +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.robot.agilex.piper_arm") + + +class PiperArmRobot(Robot): + """Piper Arm robot with ZED camera and manipulation capabilities.""" + + def __init__(self, robot_capabilities: list[RobotCapability] | None = None) -> None: + super().__init__() + self.dimos = None + self.stereo_camera = None + self.manipulation_interface = None + self.skill_library = SkillLibrary() + + # Initialize capabilities + self.capabilities = robot_capabilities or [ + RobotCapability.VISION, + RobotCapability.MANIPULATION, + ] + + async def start(self) -> None: + """Start the robot modules.""" + # Start Dimos + self.dimos = core.start(2) # Need 2 workers for ZED and manipulation modules + self.foxglove_bridge = FoxgloveBridge() + + # Enable LCM auto-configuration + pubsub.lcm.autoconf() + + # Deploy ZED module + logger.info("Deploying ZED module...") + self.stereo_camera = self.dimos.deploy( + ZEDModule, + camera_id=0, + resolution="HD720", + depth_mode="NEURAL", + fps=30, + enable_tracking=False, # We don't need tracking for manipulation + publish_rate=30.0, + frame_id="zed_camera", + ) + + # Configure ZED LCM transports + self.stereo_camera.color_image.transport = core.LCMTransport("/zed/color_image", Image) + self.stereo_camera.depth_image.transport = core.LCMTransport("/zed/depth_image", Image) + self.stereo_camera.camera_info.transport = core.LCMTransport("/zed/camera_info", CameraInfo) + + # Deploy manipulation module + logger.info("Deploying manipulation module...") + self.manipulation_interface = self.dimos.deploy(ManipulationModule) + + # Connect manipulation inputs to ZED outputs + self.manipulation_interface.rgb_image.connect(self.stereo_camera.color_image) + self.manipulation_interface.depth_image.connect(self.stereo_camera.depth_image) + self.manipulation_interface.camera_info.connect(self.stereo_camera.camera_info) + + # Configure manipulation output + self.manipulation_interface.viz_image.transport = core.LCMTransport( + "/manipulation/viz", Image + ) + + # Print module info + logger.info("Modules configured:") + print("\nZED Module:") + print(self.stereo_camera.io()) + print("\nManipulation Module:") + print(self.manipulation_interface.io()) + + # Start modules + logger.info("Starting modules...") + self.foxglove_bridge.start() + self.stereo_camera.start() + self.manipulation_interface.start() + + # Give modules time to initialize + await asyncio.sleep(2) + + logger.info("PiperArmRobot initialized and started") + + def pick_and_place( + self, pick_x: int, pick_y: int, place_x: int | None = None, place_y: int | None = None + ): + """Execute pick and place task. + + Args: + pick_x: X coordinate for pick location + pick_y: Y coordinate for pick location + place_x: X coordinate for place location (optional) + place_y: Y coordinate for place location (optional) + + Returns: + Result of the pick and place operation + """ + if self.manipulation_interface: + return self.manipulation_interface.pick_and_place(pick_x, pick_y, place_x, place_y) + else: + logger.error("Manipulation module not initialized") + return False + + def handle_keyboard_command(self, key: str): + """Pass keyboard commands to manipulation module. + + Args: + key: Keyboard key pressed + + Returns: + Action taken or None + """ + if self.manipulation_interface: + return self.manipulation_interface.handle_keyboard_command(key) + else: + logger.error("Manipulation module not initialized") + return None + + def stop(self) -> None: + """Stop all modules and clean up.""" + logger.info("Stopping PiperArmRobot...") + + try: + if self.manipulation_interface: + self.manipulation_interface.stop() + + if self.stereo_camera: + self.stereo_camera.stop() + except Exception as e: + logger.warning(f"Error stopping modules: {e}") + + # Close dimos last to ensure workers are available for cleanup + if self.dimos: + self.dimos.close() + + logger.info("PiperArmRobot stopped") + + +async def run_piper_arm() -> None: + """Run the Piper Arm robot.""" + robot = PiperArmRobot() + + await robot.start() + + # Keep the robot running + try: + while True: + await asyncio.sleep(1) + except KeyboardInterrupt: + logger.info("Keyboard interrupt received") + finally: + await robot.stop() + + +if __name__ == "__main__": + asyncio.run(run_piper_arm()) diff --git a/dimos/robot/agilex/run.py b/dimos/robot/agilex/run.py new file mode 100644 index 0000000000..90258e5d82 --- /dev/null +++ b/dimos/robot/agilex/run.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Run script for Piper Arm robot with Claude agent integration. +Provides manipulation capabilities with natural language interface. +""" + +import asyncio +import os +import sys + +from dotenv import load_dotenv +import reactivex as rx +import reactivex.operators as ops + +from dimos.agents.claude_agent import ClaudeAgent +from dimos.robot.agilex.piper_arm import PiperArmRobot +from dimos.skills.kill_skill import KillSkill +from dimos.skills.manipulation.pick_and_place import PickAndPlace +from dimos.stream.audio.pipelines import stt, tts +from dimos.utils.logging_config import setup_logger +from dimos.web.robot_web_interface import RobotWebInterface + +logger = setup_logger("dimos.robot.agilex.run") + +# Load environment variables +load_dotenv() + +# System prompt for the Piper Arm manipulation agent +SYSTEM_PROMPT = """You are an intelligent robotic assistant controlling a Piper Arm robot with advanced manipulation capabilities. Your primary role is to help users with pick and place tasks using natural language understanding. + +## Your Capabilities: +1. **Visual Perception**: You have access to a ZED stereo camera that provides RGB and depth information +2. **Object Manipulation**: You can pick up and place objects using a 6-DOF robotic arm with a gripper +3. **Language Understanding**: You use the Qwen vision-language model to identify objects and locations from natural language descriptions + +## Available Skills: +- **PickAndPlace**: Execute pick and place operations based on object and location descriptions + - Pick only: "Pick up the red mug" + - Pick and place: "Move the book to the shelf" +- **KillSkill**: Stop any currently running skill + +## Guidelines: +1. **Safety First**: Always ensure safe operation. If unsure about an object's graspability or a placement location's stability, ask for clarification +2. **Clear Communication**: Explain what you're doing and ask for confirmation when needed +3. **Error Handling**: If a task fails, explain why and suggest alternatives +4. **Precision**: When users give specific object descriptions, use them exactly as provided to the vision model + +## Interaction Examples: +- User: "Pick up the coffee mug" + You: "I'll pick up the coffee mug for you." [Execute PickAndPlace with object_query="coffee mug"] + +- User: "Put the toy on the table" + You: "I'll place the toy on the table." [Execute PickAndPlace with object_query="toy", target_query="on the table"] + +- User: "What do you see?" + +Remember: You're here to assist with manipulation tasks. Be helpful, precise, and always prioritize safe operation of the robot.""" + + +def main(): + """Main entry point.""" + print("\n" + "=" * 60) + print("Piper Arm Robot with Claude Agent") + print("=" * 60) + print("\nThis system integrates:") + print(" - Piper Arm 6-DOF robot") + print(" - ZED stereo camera") + print(" - Claude AI for natural language understanding") + print(" - Qwen VLM for visual object detection") + print(" - Web interface with text and voice input") + print(" - Foxglove visualization via LCM") + print("\nStarting system...\n") + + # Check for API key + if not os.getenv("ANTHROPIC_API_KEY"): + print("WARNING: ANTHROPIC_API_KEY not found in environment") + print("Please set your API key in .env file or environment") + sys.exit(1) + + logger.info("Starting Piper Arm Robot with Agent") + + # Create robot instance + robot = PiperArmRobot() + + try: + # Start the robot (this is async, so we need asyncio.run) + logger.info("Initializing robot...") + asyncio.run(robot.start()) + logger.info("Robot initialized successfully") + + # Set up skill library + skills = robot.get_skills() + skills.add(PickAndPlace) + skills.add(KillSkill) + + # Create skill instances + skills.create_instance("PickAndPlace", robot=robot) + skills.create_instance("KillSkill", robot=robot, skill_library=skills) + + logger.info(f"Skills registered: {[skill.__name__ for skill in skills.get_class_skills()]}") + + # Set up streams for agent and web interface + agent_response_subject = rx.subject.Subject() + agent_response_stream = agent_response_subject.pipe(ops.share()) + audio_subject = rx.subject.Subject() + + # Set up streams for web interface + streams = {} + + text_streams = { + "agent_responses": agent_response_stream, + } + + # Create web interface first (needed for agent) + try: + web_interface = RobotWebInterface( + port=5555, text_streams=text_streams, audio_subject=audio_subject, **streams + ) + logger.info("Web interface created successfully") + except Exception as e: + logger.error(f"Failed to create web interface: {e}") + raise + + # Set up speech-to-text + stt_node = stt() + stt_node.consume_audio(audio_subject.pipe(ops.share())) + + # Create Claude agent + agent = ClaudeAgent( + dev_name="piper_arm_agent", + input_query_stream=web_interface.query_stream, # Use text input from web interface + # input_query_stream=stt_node.emit_text(), # Uncomment to use voice input + skills=skills, + system_query=SYSTEM_PROMPT, + model_name="claude-3-5-haiku-latest", + thinking_budget_tokens=0, + max_output_tokens_per_request=4096, + ) + + # Subscribe to agent responses + agent.get_response_observable().subscribe(lambda x: agent_response_subject.on_next(x)) + + # Set up text-to-speech for agent responses + tts_node = tts() + tts_node.consume_text(agent.get_response_observable()) + + logger.info("=" * 60) + logger.info("Piper Arm Agent Ready!") + logger.info("Web interface available at: http://localhost:5555") + logger.info("Foxglove visualization available at: ws://localhost:8765") + logger.info("You can:") + logger.info(" - Type commands in the web interface") + logger.info(" - Use voice commands") + logger.info(" - Ask the robot to pick up objects") + logger.info(" - Ask the robot to move objects to locations") + logger.info("=" * 60) + + # Run web interface (this blocks) + web_interface.run() + + except KeyboardInterrupt: + logger.info("Keyboard interrupt received") + except Exception as e: + logger.error(f"Error running robot: {e}") + import traceback + + traceback.print_exc() + finally: + logger.info("Shutting down...") + # Stop the robot (this is also async) + robot.stop() + logger.info("Robot stopped") + + +if __name__ == "__main__": + main() diff --git a/dimos/robot/all_blueprints.py b/dimos/robot/all_blueprints.py new file mode 100644 index 0000000000..c177723e66 --- /dev/null +++ b/dimos/robot/all_blueprints.py @@ -0,0 +1,64 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.core.blueprints import ModuleBlueprintSet + +# The blueprints are defined as import strings so as not to trigger unnecessary imports. +all_blueprints = { + "unitree-go2": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:standard", + "unitree-go2-basic": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:basic", + "unitree-go2-shm": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:standard_with_shm", + "unitree-go2-jpegshm": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:standard_with_jpegshm", + "unitree-go2-jpeglcm": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:standard_with_jpeglcm", + "unitree-go2-agentic": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:agentic", + "demo-osm": "dimos.mapping.osm.demo_osm:demo_osm", + "demo-remapping": "dimos.robot.unitree_webrtc.demo_remapping:remapping", + "demo-remapping-transport": "dimos.robot.unitree_webrtc.demo_remapping:remapping_and_transport", +} + + +all_modules = { + "astar_planner": "dimos.navigation.global_planner.planner", + "behavior_tree_navigator": "dimos.navigation.bt_navigator.navigator", + "connection": "dimos.robot.unitree_webrtc.unitree_go2", + "depth_module": "dimos.robot.unitree_webrtc.depth_module", + "detection_2d": "dimos.perception.detection2d.module2D", + "foxglove_bridge": "dimos.robot.foxglove_bridge", + "holonomic_local_planner": "dimos.navigation.local_planner.holonomic_local_planner", + "human_input": "dimos.agents2.cli.human", + "llm_agent": "dimos.agents2.agent", + "mapper": "dimos.robot.unitree_webrtc.type.map", + "navigation_skill": "dimos.agents2.skills.navigation", + "object_tracking": "dimos.perception.object_tracker", + "osm_skill": "dimos.agents2.skills.osm.py", + "spatial_memory": "dimos.perception.spatial_perception", + "utilization": "dimos.utils.monitoring", + "wavefront_frontier_explorer": "dimos.navigation.frontier_exploration.wavefront_frontier_goal_selector", + "websocket_vis": "dimos.web.websocket_vis.websocket_vis_module", +} + + +def get_blueprint_by_name(name: str) -> ModuleBlueprintSet: + if name not in all_blueprints: + raise ValueError(f"Unknown blueprint set name: {name}") + module_path, attr = all_blueprints[name].split(":") + module = __import__(module_path, fromlist=[attr]) + return getattr(module, attr) + + +def get_module_by_name(name: str) -> ModuleBlueprintSet: + if name not in all_modules: + raise ValueError(f"Unknown module name: {name}") + python_module = __import__(all_modules[name], fromlist=[name]) + return getattr(python_module, name)() diff --git a/dimos/robot/cli/README.md b/dimos/robot/cli/README.md new file mode 100644 index 0000000000..da1d7443da --- /dev/null +++ b/dimos/robot/cli/README.md @@ -0,0 +1,65 @@ +# Robot CLI + +To avoid having so many runfiles, I created a common script to run any blueprint. + +For example, to run the standard Unitree Go2 blueprint run: + +```bash +dimos-robot run unitree-go2 +``` + +For the one with agents run: + +```bash +dimos-robot run unitree-go2-agentic +``` + +You can dynamically connect additional modules. For example: + +```bash +dimos-robot run unitree-go2 --extra-module llm_agent --extra-module human_input --extra-module navigation_skill +``` + +## Definitions + +Blueprints can be defined anywhere, but they're all linked together in `dimos/robot/all_blueprints.py`. E.g.: + +```python +all_blueprints = { + "unitree-go2": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:standard", + "unitree-go2-agentic": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:agentic", + ... +} +``` + +(They are defined as imports to avoid triggering unrelated imports.) + +## `GlobalConfig` + +This tool also initializes the global config and passes it to the blueprint. + +`GlobalConfig` contains configuration options that are useful across many modules. For example: + +```python +class GlobalConfig(BaseSettings): + robot_ip: str | None = None + use_simulation: bool = False + use_replay: bool = False + n_dask_workers: int = 2 +``` + +Configuration values can be set from multiple places in order of precedence (later entries override earlier ones): + +- Default value defined on GlobalConfig. (`use_simulation = False`) +- Value defined in `.env` (`USE_SIMULATION=true`) +- Value in the environment variable (`USE_SIMULATION=true`) +- Value coming from the CLI (`--use-simulation` or `--no-use-simulation`) +- Value defined on the blueprint (`blueprint.global_config(use_simulation=True)`) + +For environment variables/`.env` values, you have to prefix the name with `DIMOS_`. + +For the command line, you call it like this: + +```bash +dimos-robot --use-simulation run unitree-go2 +``` \ No newline at end of file diff --git a/dimos/robot/cli/dimos_robot.py b/dimos/robot/cli/dimos_robot.py new file mode 100644 index 0000000000..bafa53e4a9 --- /dev/null +++ b/dimos/robot/cli/dimos_robot.py @@ -0,0 +1,129 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from enum import Enum +import inspect +from typing import Optional, get_args, get_origin + +import typer + +from dimos.core.blueprints import autoconnect +from dimos.core.global_config import GlobalConfig +from dimos.protocol import pubsub +from dimos.robot.all_blueprints import all_blueprints, get_blueprint_by_name, get_module_by_name + +RobotType = Enum("RobotType", {key.replace("-", "_").upper(): key for key in all_blueprints.keys()}) + +main = typer.Typer() + + +def create_dynamic_callback(): + fields = GlobalConfig.model_fields + + # Build the function signature dynamically + params = [ + inspect.Parameter("ctx", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=typer.Context), + ] + + # Create parameters for each field in GlobalConfig + for field_name, field_info in fields.items(): + field_type = field_info.annotation + + # Handle Optional types + # Check for Optional/Union with None + if get_origin(field_type) is type(Optional[str]): # noqa: UP045 + inner_types = get_args(field_type) + if len(inner_types) == 2 and type(None) in inner_types: + # It's Optional[T], get the actual type T + actual_type = next(t for t in inner_types if t != type(None)) + else: + actual_type = field_type + else: + actual_type = field_type + + # Convert field name from snake_case to kebab-case for CLI + cli_option_name = field_name.replace("_", "-") + + # Special handling for boolean fields + if actual_type is bool: + # For boolean fields, create --flag/--no-flag pattern + param = inspect.Parameter( + field_name, + inspect.Parameter.KEYWORD_ONLY, + default=typer.Option( + None, # None means use the model's default if not provided + f"--{cli_option_name}/--no-{cli_option_name}", + help=f"Override {field_name} in GlobalConfig", + ), + annotation=Optional[bool], # noqa: UP045 + ) + else: + # For non-boolean fields, use regular option + param = inspect.Parameter( + field_name, + inspect.Parameter.KEYWORD_ONLY, + default=typer.Option( + None, # None means use the model's default if not provided + f"--{cli_option_name}", + help=f"Override {field_name} in GlobalConfig", + ), + annotation=Optional[actual_type], # noqa: UP045 + ) + params.append(param) + + def callback(**kwargs) -> None: + ctx = kwargs.pop("ctx") + overrides = {k: v for k, v in kwargs.items() if v is not None} + ctx.obj = GlobalConfig().model_copy(update=overrides) + + callback.__signature__ = inspect.Signature(params) + + return callback + + +main.callback()(create_dynamic_callback()) + + +@main.command() +def run( + ctx: typer.Context, + robot_type: RobotType = typer.Argument(..., help="Type of robot to run"), + extra_modules: list[str] = typer.Option( + [], "--extra-module", help="Extra modules to add to the blueprint" + ), +) -> None: + """Run the robot with the specified configuration.""" + config: GlobalConfig = ctx.obj + pubsub.lcm.autoconf() + blueprint = get_blueprint_by_name(robot_type.value) + + if extra_modules: + loaded_modules = [get_module_by_name(mod_name) for mod_name in extra_modules] + blueprint = autoconnect(blueprint, *loaded_modules) + + dimos = blueprint.build(global_config=config) + dimos.loop() + + +@main.command() +def show_config(ctx: typer.Context) -> None: + """Show current configuration status.""" + config: GlobalConfig = ctx.obj + + for field_name, value in config.model_dump().items(): + typer.echo(f"{field_name}: {value}") + + +if __name__ == "__main__": + main() diff --git a/dimos/robot/connection_interface.py b/dimos/robot/connection_interface.py new file mode 100644 index 0000000000..6480827214 --- /dev/null +++ b/dimos/robot/connection_interface.py @@ -0,0 +1,71 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC, abstractmethod + +from reactivex.observable import Observable + +from dimos.types.vector import Vector + +__all__ = ["ConnectionInterface"] + + +class ConnectionInterface(ABC): + """Abstract base class for robot connection interfaces. + + This class defines the minimal interface that all connection types (ROS, WebRTC, etc.) + must implement to provide robot control and data streaming capabilities. + """ + + @abstractmethod + def move(self, velocity: Vector, duration: float = 0.0) -> bool: + """Send movement command to the robot using velocity commands. + + Args: + velocity: Velocity vector [x, y, yaw] where: + x: Forward/backward velocity (m/s) + y: Left/right velocity (m/s) + yaw: Rotational velocity (rad/s) + duration: How long to move (seconds). If 0, command is continuous + + Returns: + bool: True if command was sent successfully + """ + pass + + @abstractmethod + def get_video_stream(self, fps: int = 30) -> Observable | None: + """Get the video stream from the robot's camera. + + Args: + fps: Frames per second for the video stream + + Returns: + Observable: An observable stream of video frames or None if not available + """ + pass + + @abstractmethod + def stop(self) -> bool: + """Stop the robot's movement. + + Returns: + bool: True if stop command was sent successfully + """ + pass + + @abstractmethod + def disconnect(self) -> None: + """Disconnect from the robot and clean up resources.""" + pass diff --git a/dimos/robot/foxglove_bridge.py b/dimos/robot/foxglove_bridge.py new file mode 100644 index 0000000000..00c43f6f1b --- /dev/null +++ b/dimos/robot/foxglove_bridge.py @@ -0,0 +1,75 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import logging +import threading + +# this is missing, I'm just trying to import lcm_foxglove_bridge.py from dimos_lcm +from dimos_lcm.foxglove_bridge import FoxgloveBridge as LCMFoxgloveBridge + +from dimos.core import Module, rpc + + +class FoxgloveBridge(Module): + _thread: threading.Thread + _loop: asyncio.AbstractEventLoop + + def __init__(self, *args, shm_channels=None, jpeg_shm_channels=None, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.shm_channels = shm_channels or [] + self.jpeg_shm_channels = jpeg_shm_channels or [] + + @rpc + def start(self) -> None: + super().start() + + def run_bridge() -> None: + self._loop = asyncio.new_event_loop() + asyncio.set_event_loop(self._loop) + try: + for logger in ["lcm_foxglove_bridge", "FoxgloveServer"]: + logger = logging.getLogger(logger) + logger.setLevel(logging.ERROR) + for handler in logger.handlers: + handler.setLevel(logging.ERROR) + + bridge = LCMFoxgloveBridge( + host="0.0.0.0", + port=8765, + debug=False, + num_threads=4, + shm_channels=self.shm_channels, + jpeg_shm_channels=self.jpeg_shm_channels, + ) + self._loop.run_until_complete(bridge.run()) + except Exception as e: + print(f"Foxglove bridge error: {e}") + + self._thread = threading.Thread(target=run_bridge, daemon=True) + self._thread.start() + + @rpc + def stop(self) -> None: + if self._loop and self._loop.is_running(): + self._loop.call_soon_threadsafe(self._loop.stop) + self._thread.join(timeout=2) + + super().stop() + + +foxglove_bridge = FoxgloveBridge.blueprint + + +__all__ = ["FoxgloveBridge", "foxglove_bridge"] diff --git a/dimos/robot/global_planner/algo.py b/dimos/robot/global_planner/algo.py deleted file mode 100644 index c553a5e659..0000000000 --- a/dimos/robot/global_planner/algo.py +++ /dev/null @@ -1,246 +0,0 @@ -import math -import heapq -from typing import Optional, Tuple -from collections import deque -from dimos.types.path import Path -from dimos.types.vector import VectorLike, Vector -from dimos.types.costmap import Costmap - - -def find_nearest_free_cell( - costmap: Costmap, - position: VectorLike, - cost_threshold: int = 90, - max_search_radius: int = 20 -) -> Tuple[int, int]: - """ - Find the nearest unoccupied cell in the costmap using BFS. - - Args: - costmap: Costmap object containing the environment - position: Position to find nearest free cell from - cost_threshold: Cost threshold above which a cell is considered an obstacle - max_search_radius: Maximum search radius in cells - - Returns: - Tuple of (x, y) in grid coordinates of the nearest free cell, - or the original position if no free cell is found within max_search_radius - """ - # Convert world coordinates to grid coordinates - grid_pos = costmap.world_to_grid(position) - start_x, start_y = int(grid_pos.x), int(grid_pos.y) - - # If the cell is already free, return it - if 0 <= start_x < costmap.width and 0 <= start_y < costmap.height: - if costmap.grid[start_y, start_x] < cost_threshold: - return (start_x, start_y) - - # BFS to find nearest free cell - queue = deque([(start_x, start_y, 0)]) # (x, y, distance) - visited = set([(start_x, start_y)]) - - # Possible movements (8-connected grid) - directions = [ - (0, 1), (1, 0), (0, -1), (-1, 0), # horizontal/vertical - (1, 1), (1, -1), (-1, 1), (-1, -1) # diagonal - ] - - while queue: - x, y, dist = queue.popleft() - - # Check if we've reached the maximum search radius - if dist > max_search_radius: - print(f"Could not find free cell within {max_search_radius} cells of ({start_x}, {start_y})") - return (start_x, start_y) # Return original position if no free cell found - - # Check if this cell is valid and free - if 0 <= x < costmap.width and 0 <= y < costmap.height: - if costmap.grid[y, x] < cost_threshold: - print(f"Found free cell at ({x}, {y}), {dist} cells away from ({start_x}, {start_y})") - return (x, y) - - # Add neighbors to the queue - for dx, dy in directions: - nx, ny = x + dx, y + dy - if (nx, ny) not in visited: - visited.add((nx, ny)) - queue.append((nx, ny, dist + 1)) - - # If the queue is empty and no free cell is found, return the original position - return (start_x, start_y) - - -def astar( - costmap: Costmap, - goal: VectorLike, - start: VectorLike = (0.0, 0.0), - cost_threshold: int = 90, - allow_diagonal: bool = True, -) -> Optional[Path]: - """ - A* path planning algorithm from start to goal position. - - Args: - costmap: Costmap object containing the environment - goal: Goal position as any vector-like object - start: Start position as any vector-like object (default: origin [0,0]) - cost_threshold: Cost threshold above which a cell is considered an obstacle - allow_diagonal: Whether to allow diagonal movements - - Returns: - Path object containing waypoints, or None if no path found - """ - # Convert world coordinates to grid coordinates directly using vector-like inputs - start_vector = costmap.world_to_grid(start) - goal_vector = costmap.world_to_grid(goal) - - # Store original positions for reference - original_start = (int(start_vector.x), int(start_vector.y)) - original_goal = (int(goal_vector.x), int(goal_vector.y)) - - adjusted_start = original_start - adjusted_goal = original_goal - - # Check if start is out of bounds or in an obstacle - start_valid = (0 <= start_vector.x < costmap.width and - 0 <= start_vector.y < costmap.height) - - start_in_obstacle = False - if start_valid: - start_in_obstacle = costmap.grid[int(start_vector.y), int(start_vector.x)] >= cost_threshold - - if not start_valid or start_in_obstacle: - print("Start position is out of bounds or in an obstacle, finding nearest free cell") - adjusted_start = find_nearest_free_cell(costmap, start, cost_threshold) - # Update start_vector for later use - start_vector = Vector(adjusted_start[0], adjusted_start[1]) - - # Check if goal is out of bounds or in an obstacle - goal_valid = (0 <= goal_vector.x < costmap.width and - 0 <= goal_vector.y < costmap.height) - - goal_in_obstacle = False - if goal_valid: - goal_in_obstacle = costmap.grid[int(goal_vector.y), int(goal_vector.x)] >= cost_threshold - - if not goal_valid or goal_in_obstacle: - print("Goal position is out of bounds or in an obstacle, finding nearest free cell") - adjusted_goal = find_nearest_free_cell(costmap, goal, cost_threshold) - # Update goal_vector for later use - goal_vector = Vector(adjusted_goal[0], adjusted_goal[1]) - - # Define possible movements (8-connected grid) - if allow_diagonal: - # 8-connected grid: horizontal, vertical, and diagonal movements - directions = [ - (0, 1), - (1, 0), - (0, -1), - (-1, 0), - (1, 1), - (1, -1), - (-1, 1), - (-1, -1), - ] - else: - # 4-connected grid: only horizontal and vertical ts - directions = [(0, 1), (1, 0), (0, -1), (-1, 0)] - - # Cost for each movement (straight vs diagonal) - sc = 1.0 - dc = 1.42 - movement_costs = [sc, sc, sc, sc, dc, dc, dc, dc] if allow_diagonal else [sc, sc, sc, sc] - - # A* algorithm implementation - open_set = [] # Priority queue for nodes to explore - closed_set = set() # Set of explored nodes - - # Use adjusted positions as tuples for dictionary keys - start_tuple = adjusted_start - goal_tuple = adjusted_goal - - # Dictionary to store cost from start and parents for each node - g_score = {start_tuple: 0} - parents = {} - - # Heuristic function (Euclidean distance) - def heuristic(x1, y1, x2, y2): - return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) - - # Start with the starting node - f_score = g_score[start_tuple] + heuristic(start_tuple[0], start_tuple[1], goal_tuple[0], goal_tuple[1]) - heapq.heappush(open_set, (f_score, start_tuple)) - - while open_set: - # Get the node with the lowest f_score - _, current = heapq.heappop(open_set) - current_x, current_y = current - - # Check if we've reached the goal - if current == goal_tuple: - # Reconstruct the path - waypoints = [] - while current in parents: - world_point = costmap.grid_to_world(current) - waypoints.append(world_point) - current = parents[current] - - # Add the start position - start_world_point = costmap.grid_to_world(start_tuple) - waypoints.append(start_world_point) - - # Reverse the path (start to goal) - waypoints.reverse() - - # Add the goal position if it's not already included - goal_point = costmap.grid_to_world(goal_tuple) - - if not waypoints or waypoints[-1].distance(goal_point) > 1e-5: - waypoints.append(goal_point) - - # If we adjusted the goal, add the original goal as the final point - if adjusted_goal != original_goal and goal_valid: - original_goal_point = costmap.grid_to_world(original_goal) - waypoints.append(original_goal_point) - - return Path(waypoints) - - # Add current node to closed set - closed_set.add(current) - - # Explore neighbors - for i, (dx, dy) in enumerate(directions): - neighbor_x, neighbor_y = current_x + dx, current_y + dy - neighbor = (neighbor_x, neighbor_y) - - # Check if the neighbor is valid - if not (0 <= neighbor_x < costmap.width and 0 <= neighbor_y < costmap.height): - continue - - # Check if the neighbor is already explored - if neighbor in closed_set: - continue - - # Check if the neighbor is an obstacle - neighbor_val = costmap.grid[neighbor_y, neighbor_x] - if neighbor_val >= cost_threshold: # or neighbor_val < 0: - continue - - obstacle_proximity_penalty = costmap.grid[neighbor_y, neighbor_x] / 25 - tentative_g_score = g_score[current] + movement_costs[i] + (obstacle_proximity_penalty * movement_costs[i]) - - # Get the current g_score for the neighbor or set to infinity if not yet explored - neighbor_g_score = g_score.get(neighbor, float("inf")) - - # If this path to the neighbor is better than any previous one - if tentative_g_score < neighbor_g_score: - # Update the neighbor's scores and parent - parents[neighbor] = current - g_score[neighbor] = tentative_g_score - f_score = tentative_g_score + heuristic(neighbor_x, neighbor_y, goal_tuple[0], goal_tuple[1]) - - # Add the neighbor to the open set with its f_score - heapq.heappush(open_set, (f_score, neighbor)) - - # If we get here, no path was found - return None diff --git a/dimos/robot/global_planner/planner.py b/dimos/robot/global_planner/planner.py deleted file mode 100644 index 0a59428917..0000000000 --- a/dimos/robot/global_planner/planner.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dataclasses import dataclass -from abc import abstractmethod -from typing import Callable, Optional -import threading - -from dimos.types.path import Path -from dimos.types.costmap import Costmap -from dimos.types.vector import VectorLike, to_vector, Vector -from dimos.robot.global_planner.algo import astar -from dimos.utils.logging_config import setup_logger -from dimos.web.websocket_vis.helpers import Visualizable - -logger = setup_logger("dimos.robot.unitree.global_planner") - - -@dataclass -class Planner(Visualizable): - set_local_nav: Callable[[Path, Optional[threading.Event]], bool] - - @abstractmethod - def plan(self, goal: VectorLike) -> Path: ... - - def set_goal( - self, goal: VectorLike, goal_theta: Optional[float] = None, stop_event: Optional[threading.Event] = None - ): - path = self.plan(goal) - if not path: - logger.warning("No path found to the goal.") - return False - print("pathing success", path) - return self.set_local_nav(path, stop_event=stop_event, goal_theta=goal_theta) - - -@dataclass -class AstarPlanner(Planner): - get_costmap: Callable[[], Costmap] - get_robot_pos: Callable[[], Vector] - set_local_nav: Callable[[Path], bool] - conservativism: int = 8 - - def plan(self, goal: VectorLike) -> Path: - goal = to_vector(goal).to_2d() - pos = self.get_robot_pos().to_2d() - costmap = self.get_costmap().smudge(preserve_unknown=False) - - # self.vis("costmap", costmap) - self.vis("target", goal) - - print("ASTAR ", costmap, goal, pos) - path = astar(costmap, goal, pos) - - if path: - path = path.resample(0.1) - self.vis("a*", path) - return path - - logger.warning("No path found to the goal.") diff --git a/dimos/robot/local_planner/__init__.py b/dimos/robot/local_planner/__init__.py deleted file mode 100644 index 72a7adea57..0000000000 --- a/dimos/robot/local_planner/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from dimos.robot.local_planner.local_planner import ( - BaseLocalPlanner, - navigate_to_goal_local, - navigate_path_local, -) - -from dimos.robot.local_planner.vfh_local_planner import VFHPurePursuitPlanner \ No newline at end of file diff --git a/dimos/robot/local_planner/local_planner.py b/dimos/robot/local_planner/local_planner.py deleted file mode 100644 index f32d8db81f..0000000000 --- a/dimos/robot/local_planner/local_planner.py +++ /dev/null @@ -1,1188 +0,0 @@ -#!/usr/bin/env python3 - -import math -import numpy as np -from typing import Dict, Tuple, Optional, List, Any, Callable, Protocol -from abc import ABC, abstractmethod -import cv2 -from reactivex import Observable -from reactivex.subject import Subject -import threading -import time -import logging -from collections import deque -from dimos.utils.logging_config import setup_logger -from dimos.utils.ros_utils import ( - normalize_angle, - distance_angle_to_goal_xy -) - -from dimos.robot.robot import Robot -from dimos.types.vector import VectorLike, Vector, to_tuple -from dimos.types.path import Path -from dimos.types.costmap import Costmap -from dimos.robot.global_planner.algo import astar -from nav_msgs.msg import OccupancyGrid - -logger = setup_logger("dimos.robot.unitree.local_planner", level=logging.DEBUG) - -class BaseLocalPlanner(ABC): - """ - Abstract base class for local planners that handle obstacle avoidance and path following. - - This class defines the common interface and shared functionality that all local planners - must implement, regardless of the specific algorithm used. - """ - - def __init__(self, - get_costmap: Callable[[], Optional[OccupancyGrid]], - transform: object, - move_vel_control: Callable[[float, float, float], None], - safety_threshold: float = 0.5, - max_linear_vel: float = 0.8, - max_angular_vel: float = 1.0, - lookahead_distance: float = 1.0, - goal_tolerance: float = 0.75, - angle_tolerance: float = 0.15, - robot_width: float = 0.5, - robot_length: float = 0.7, - visualization_size: int = 400, - control_frequency: float = 10.0, - safe_goal_distance: float = 1.5): # Control frequency in Hz - """ - Initialize the base local planner. - - Args: - get_costmap: Function to get the latest local costmap - transform: Object with transform methods (transform_point, transform_rot, etc.) - move_vel_control: Function to send velocity commands - safety_threshold: Distance to maintain from obstacles (meters) - max_linear_vel: Maximum linear velocity (m/s) - max_angular_vel: Maximum angular velocity (rad/s) - lookahead_distance: Lookahead distance for path following (meters) - goal_tolerance: Distance at which the goal is considered reached (meters) - angle_tolerance: Angle at which the goal orientation is considered reached (radians) - robot_width: Width of the robot for visualization (meters) - robot_length: Length of the robot for visualization (meters) - visualization_size: Size of the visualization image in pixels - control_frequency: Frequency at which the planner is called (Hz) - safe_goal_distance: Distance at which to adjust the goal and ignore obstacles (meters) - """ - # Store callables for robot interactions - self.get_costmap = get_costmap - self.transform = transform - self.move_vel_control = move_vel_control - - # Store parameters - self.safety_threshold = safety_threshold - self.max_linear_vel = max_linear_vel - self.max_angular_vel = max_angular_vel - self.lookahead_distance = lookahead_distance - self.goal_tolerance = goal_tolerance - self.angle_tolerance = angle_tolerance - self.robot_width = robot_width - self.robot_length = robot_length - self.visualization_size = visualization_size - self.control_frequency = control_frequency - self.control_period = 1.0 / control_frequency # Period in seconds - self.safe_goal_distance = safe_goal_distance # Distance to ignore obstacles at goal - self.ignore_obstacles = False # Flag for derived classes to check - - # Goal and Waypoint Tracking - self.goal_xy: Optional[Tuple[float, float]] = None # Current target for planning - self.goal_theta: Optional[float] = None # Goal orientation in odom frame - self.position_reached: bool = False # Flag indicating if position goal is reached - self.waypoints: Optional[Path] = None # Full path if following waypoints - self.waypoints_in_odom: Optional[Path] = None # Full path in odom frame - self.waypoint_frame: Optional[str] = None # Frame of the waypoints - self.current_waypoint_index: int = 0 # Index of the next waypoint to reach - self.final_goal_reached: bool = False # Flag indicating if the final waypoint is reached - - # Stuck detection - self.stuck_detection_window_seconds = 8.0 # Time window for stuck detection (seconds) - self.position_history_size = int(self.stuck_detection_window_seconds * control_frequency) - self.position_history = deque(maxlen=self.position_history_size) # History of recent positions - self.stuck_distance_threshold = 0.1 # Distance threshold for stuck detection (meters) - self.unstuck_distance_threshold = 0.5 # Distance threshold for unstuck detection (meters) - self.stuck_time_threshold = 4.0 # Time threshold for stuck detection (seconds) - self.is_recovery_active = False # Whether recovery behavior is active - self.recovery_start_time = 0.0 # When recovery behavior started - self.recovery_duration = 8.0 # How long to run recovery before giving up (seconds) - self.last_update_time = time.time() # Last time position was updated - self.navigation_failed = False # Flag indicating if navigation should be terminated - - def reset(self): - """ - Reset all navigation and state tracking variables. - Should be called whenever a new goal is set. - """ - # Reset stuck detection state - self.position_history.clear() - self.is_recovery_active = False - self.recovery_start_time = 0.0 - self.last_update_time = time.time() - - # Reset navigation state flags - self.navigation_failed = False - self.position_reached = False - self.final_goal_reached = False - self.ignore_obstacles = False - - logger.info("Local planner state has been reset") - - def set_goal(self, goal_xy: VectorLike, frame: str = "odom", goal_theta: Optional[float] = None): - """Set a single goal position, converting to odom frame if necessary. - This clears any existing waypoints being followed. - - Args: - goal_xy: The goal position to set. - frame: The frame of the goal position. - goal_theta: Optional goal orientation in radians (in the specified frame) - """ - # Reset all state variables - self.reset() - - # Clear waypoint following state - self.waypoints = None - self.current_waypoint_index = 0 - self.goal_xy = None # Clear previous goal - self.goal_theta = None # Clear previous goal orientation - - target_goal_xy: Optional[Tuple[float, float]] = None - - target_goal_xy = self.transform.transform_point(goal_xy, source_frame=frame, target_frame="odom").to_tuple() - - logger.info(f"Goal set directly in odom frame: ({target_goal_xy[0]:.2f}, {target_goal_xy[1]:.2f})") - - # Check if goal is valid (in bounds and not colliding) - if not self.is_goal_in_costmap_bounds(target_goal_xy) or self.check_goal_collision(target_goal_xy): - logger.warning("Goal is in collision or out of bounds. Adjusting goal to valid position.") - self.goal_xy = self.adjust_goal_to_valid_position(target_goal_xy) - else: - self.goal_xy = target_goal_xy # Set the adjusted or original valid goal - - # Set goal orientation if provided - if goal_theta is not None: - transformed_rot = self.transform.transform_rot(Vector(0.0, 0.0, goal_theta), source_frame=frame, target_frame="odom") - self.goal_theta = transformed_rot[2] - - def set_goal_waypoints(self, waypoints: Path, frame: str = "map", goal_theta: Optional[float] = None): - """Sets a path of waypoints for the robot to follow. - - Args: - waypoints: A list of waypoints to follow. Each waypoint is a tuple of (x, y) coordinates in odom frame. - frame: The frame of the waypoints. - goal_theta: Optional final orientation in radians (in the specified frame) - """ - # Reset all state variables - self.reset() - - if not isinstance(waypoints, Path) or len(waypoints) == 0: - logger.warning("Invalid or empty path provided to set_goal_waypoints. Ignoring.") - self.waypoints = None - self.waypoint_frame = None - self.goal_xy = None - self.goal_theta = None - self.current_waypoint_index = 0 - return - - logger.info(f"Setting goal waypoints with {len(waypoints)} points.") - self.waypoints = waypoints - self.waypoint_frame = frame - self.current_waypoint_index = 0 - - # Transform waypoints to odom frame - self.waypoints_in_odom = self.transform.transform_path(self.waypoints, source_frame=frame, target_frame="odom") - - # Set the initial target to the first waypoint, adjusting if necessary - first_waypoint = self.waypoints_in_odom[0] - if not self.is_goal_in_costmap_bounds(first_waypoint) or self.check_goal_collision(first_waypoint): - logger.warning("First waypoint is invalid. Adjusting...") - self.goal_xy = self.adjust_goal_to_valid_position(first_waypoint) - else: - self.goal_xy = to_tuple(first_waypoint) # Initial target - - # Set goal orientation if provided - if goal_theta is not None: - transformed_rot = self.transform.transform_rot(Vector(0.0, 0.0, goal_theta), source_frame=frame, target_frame="odom") - self.goal_theta = transformed_rot[2] - - def _get_robot_pose(self) -> Tuple[Tuple[float, float], float]: - """ - Get the current robot position and orientation. - - Returns: - Tuple containing: - - position as (x, y) tuple - - orientation (theta) in radians - """ - [pos, rot] = self.transform.transform_euler("base_link", "odom") - return (pos[0], pos[1]), rot[2] - - def _get_final_goal_position(self) -> Optional[Tuple[float, float]]: - """ - Get the final goal position (either last waypoint or direct goal). - - Returns: - Tuple (x, y) of the final goal, or None if no goal is set - """ - if self.waypoints_in_odom is not None and len(self.waypoints_in_odom) > 0: - return to_tuple(self.waypoints_in_odom[-1]) - elif self.goal_xy is not None: - return self.goal_xy - return None - - def _distance_to_position(self, target_position: Tuple[float, float]) -> float: - """ - Calculate distance from the robot to a target position. - - Args: - target_position: Target (x, y) position - - Returns: - Distance in meters - """ - robot_pos, _ = self._get_robot_pose() - return np.linalg.norm([target_position[0] - robot_pos[0], - target_position[1] - robot_pos[1]]) - - def plan(self) -> Dict[str, float]: - """ - Main planning method that computes velocity commands. - This includes common planning logic like waypoint following, - with algorithm-specific calculations delegated to subclasses. - - Returns: - Dict[str, float]: Velocity commands with 'x_vel' and 'angular_vel' keys - """ - # If goal orientation is specified, rotate to match it - if self.position_reached and self.goal_theta is not None and not self._is_goal_orientation_reached(): - logger.info("Position goal reached. Rotating to target orientation.") - return self._rotate_to_goal_orientation() - - # Check if the robot is stuck and handle accordingly - if self.check_if_stuck() and not self.position_reached: - # Check if we're stuck but close to our goal - final_goal_pos = self._get_final_goal_position() - - # If we have a goal position, check distance to it - if final_goal_pos is not None: - distance_to_goal = self._distance_to_position(final_goal_pos) - - # If we're stuck but within 2x safe_goal_distance of the goal, consider it a success - if distance_to_goal < 2.0 * self.safe_goal_distance: - logger.info(f"Robot is stuck but within {distance_to_goal:.2f}m of goal (< {2.0 * self.safe_goal_distance:.2f}m). Considering navigation successful.") - self.position_reached = True - return {'x_vel': 0.0, 'angular_vel': 0.0} - - # Otherwise, execute normal recovery behavior - logger.warning("Robot is stuck - executing recovery behavior") - return self.execute_recovery_behavior() - - # Reset obstacle ignore flag - self.ignore_obstacles = False - - # --- Waypoint Following Mode --- - if self.waypoints is not None: - if self.final_goal_reached: - logger.info("Final waypoint reached. Stopping.") - return {'x_vel': 0.0, 'angular_vel': 0.0} - - # Get current robot pose - robot_pos, robot_theta = self._get_robot_pose() - robot_pos_np = np.array(robot_pos) - - # Check if close to final waypoint - if self.waypoints_in_odom is not None and len(self.waypoints_in_odom) > 0: - final_waypoint = self.waypoints_in_odom[-1] - dist_to_final = np.linalg.norm(robot_pos_np - final_waypoint) - - # If we're close to the final waypoint, adjust it and ignore obstacles - if dist_to_final < self.safe_goal_distance: - final_wp_tuple = to_tuple(final_waypoint) - adjusted_goal = self.adjust_goal_to_valid_position(final_wp_tuple) - # Create a new Path with the adjusted final waypoint - new_waypoints = self.waypoints_in_odom[:-1] # Get all but the last waypoint - new_waypoints.append(adjusted_goal) # Append the adjusted goal - self.waypoints_in_odom = new_waypoints - self.ignore_obstacles = True - logger.debug(f"Within safe distance of final waypoint. Ignoring obstacles.") - - # Update the target goal based on waypoint progression - just_reached_final = self._update_waypoint_target(robot_pos_np) - - # If the helper indicates the final goal was just reached, stop immediately - if just_reached_final: - return {'x_vel': 0.0, 'angular_vel': 0.0} - - # --- Single Goal or Current Waypoint Target Set --- - if self.goal_xy is None: - # If no goal is set (e.g., empty path or rejected goal), stop. - return {'x_vel': 0.0, 'angular_vel': 0.0} - - # Get necessary data for planning - costmap = self.get_costmap() - if costmap is None: - logger.warning("Local costmap is None. Cannot plan.") - return {'x_vel': 0.0, 'angular_vel': 0.0} - - # Check if close to single goal mode goal - if self.waypoints is None: - # Get distance to goal - goal_distance = self._distance_to_position(self.goal_xy) - - # If within safe distance of goal, adjust it and ignore obstacles - if goal_distance < self.safe_goal_distance: - self.goal_xy = self.adjust_goal_to_valid_position(self.goal_xy) - self.ignore_obstacles = True - logger.debug(f"Within safe distance of goal. Ignoring obstacles.") - - # First check position - if goal_distance < self.goal_tolerance or self.position_reached: - self.position_reached = True - - else: - self.position_reached = False - - # Call the algorithm-specific planning implementation - return self._compute_velocity_commands() - - @abstractmethod - def _compute_velocity_commands(self) -> Dict[str, float]: - """ - Algorithm-specific method to compute velocity commands. - Must be implemented by derived classes. - - Returns: - Dict[str, float]: Velocity commands with 'x_vel' and 'angular_vel' keys - """ - pass - - def _rotate_to_goal_orientation(self) -> Dict[str, float]: - """Compute velocity commands to rotate to the goal orientation. - - Returns: - Dict[str, float]: Velocity commands with zero linear velocity - """ - # Get current robot orientation - _, robot_theta = self._get_robot_pose() - - # Calculate the angle difference - angle_diff = normalize_angle(self.goal_theta - robot_theta) - - # Determine rotation direction and speed - if abs(angle_diff) < self.angle_tolerance: - # Already at correct orientation - return {'x_vel': 0.0, 'angular_vel': 0.0} - - # Calculate rotation speed - proportional to the angle difference - # but capped at max_angular_vel - direction = 1.0 if angle_diff > 0 else -1.0 - angular_vel = direction * min(abs(angle_diff) * 2.0, self.max_angular_vel) - - # logger.debug(f"Rotating to goal orientation: angle_diff={angle_diff:.4f}, angular_vel={angular_vel:.4f}") - return {'x_vel': 0.0, 'angular_vel': angular_vel} - - def _is_goal_orientation_reached(self) -> bool: - """Check if the current robot orientation matches the goal orientation. - - Returns: - bool: True if orientation is reached or no orientation goal is set - """ - if self.goal_theta is None: - return True # No orientation goal set - - # Get current robot orientation - _, robot_theta = self._get_robot_pose() - - # Calculate the angle difference and normalize - angle_diff = abs(normalize_angle(self.goal_theta - robot_theta)) - - logger.debug(f"Orientation error: {angle_diff:.4f} rad, tolerance: {self.angle_tolerance:.4f} rad") - return angle_diff <= self.angle_tolerance - - def _update_waypoint_target(self, robot_pos_np: np.ndarray) -> bool: - """Helper function to manage waypoint progression and update the target goal. - - Args: - robot_pos_np: Current robot position as a numpy array [x, y]. - - Returns: - bool: True if the final waypoint has just been reached, False otherwise. - """ - if self.waypoints is None or len(self.waypoints) == 0: - return False # Not in waypoint mode or empty path - - self.waypoints_in_odom = self.transform.transform_path(self.waypoints, source_frame=self.waypoint_frame, target_frame="odom") - - # Check if final goal is reached - final_waypoint = self.waypoints_in_odom[-1] - dist_to_final = np.linalg.norm(robot_pos_np - final_waypoint) - - if dist_to_final < self.goal_tolerance: - self.position_reached = True - self.goal_xy = to_tuple(final_waypoint) - - # If goal orientation is not specified or achieved, consider fully reached - if self.goal_theta is None or self._is_goal_orientation_reached(): - self.final_goal_reached = True - logger.info("Reached final waypoint with correct orientation.") - return True - else: - logger.info("Reached final waypoint position, rotating to target orientation.") - return False - - # Always find the lookahead point - lookahead_point = None - for i in range(self.current_waypoint_index, len(self.waypoints_in_odom)): - wp = self.waypoints_in_odom[i] - dist_to_wp = np.linalg.norm(robot_pos_np - wp) - if dist_to_wp >= self.lookahead_distance: - lookahead_point = wp - # Update current waypoint index to this point - self.current_waypoint_index = i - break - - # If no point is far enough, target the final waypoint - if lookahead_point is None: - lookahead_point = self.waypoints_in_odom[-1] - self.current_waypoint_index = len(self.waypoints_in_odom) - 1 - - # Set the lookahead point as the immediate target, adjusting if needed - if not self.is_goal_in_costmap_bounds(lookahead_point) or self.check_goal_collision(lookahead_point): - logger.debug("Lookahead point is invalid. Adjusting...") - adjusted_lookahead = self.adjust_goal_to_valid_position(lookahead_point) - # Only update if adjustment didn't fail completely - if adjusted_lookahead is not None: - self.goal_xy = adjusted_lookahead - else: - self.goal_xy = to_tuple(lookahead_point) - - return False # Final goal not reached in this update cycle - - @abstractmethod - def update_visualization(self) -> np.ndarray: - """ - Generate visualization of the planning state. - Must be implemented by derived classes. - - Returns: - np.ndarray: Visualization image as numpy array - """ - pass - - def create_stream(self, frequency_hz: float = None) -> Observable: - """ - Create an Observable stream that emits the visualization image at a fixed frequency. - - Args: - frequency_hz: Optional frequency override (defaults to 1/4 of control_frequency if None) - - Returns: - Observable: Stream of visualization frames - """ - # Default to 1/4 of control frequency if not specified (to reduce CPU usage) - if frequency_hz is None: - frequency_hz = self.control_frequency / 4.0 - - subject = Subject() - sleep_time = 1.0 / frequency_hz - - def frame_emitter(): - while True: - try: - # Generate the frame using the updated method - frame = self.update_visualization() - subject.on_next(frame) - except Exception as e: - logger.error(f"Error in frame emitter thread: {e}") - # Optionally, emit an error frame or simply skip - # subject.on_error(e) # This would terminate the stream - time.sleep(sleep_time) - - emitter_thread = threading.Thread(target=frame_emitter, daemon=True) - emitter_thread.start() - logger.info(f"Started visualization frame emitter thread at {frequency_hz:.1f} Hz") - return subject - - @abstractmethod - def check_collision(self, direction: float) -> bool: - """ - Check if there's a collision in the given direction. - Must be implemented by derived classes. - - Args: - direction: Direction to check for collision in radians - - Returns: - bool: True if collision detected, False otherwise - """ - pass - - def is_goal_reached(self) -> bool: - """Check if the final goal (single or last waypoint) is reached, including orientation.""" - if self.waypoints is not None: - # Waypoint mode: check if the final waypoint and orientation have been reached - return self.final_goal_reached - else: - # Single goal mode: check distance to the single goal and orientation - if self.goal_xy is None: - return False # No goal set - - return self.position_reached and self._is_goal_orientation_reached() - - def check_goal_collision(self, goal_xy: VectorLike) -> bool: - """Check if the current goal is in collision with obstacles in the costmap. - - Returns: - bool: True if goal is in collision, False if goal is safe or cannot be checked - """ - - costmap = self.get_costmap() - if costmap is None: - logger.warning("Cannot check collision: No costmap available") - return False - - # Check if the position is occupied - collision_threshold = 80 # Consider values above 80 as obstacles - - # Use Costmap's is_occupied method - return costmap.is_occupied(goal_xy, threshold=collision_threshold) - - def is_goal_in_costmap_bounds(self, goal_xy: VectorLike) -> bool: - """Check if the goal position is within the bounds of the costmap. - - Args: - goal_xy: Goal position (x, y) in odom frame - - Returns: - bool: True if the goal is within the costmap bounds, False otherwise - """ - costmap = self.get_costmap() - if costmap is None: - logger.warning("Cannot check bounds: No costmap available") - return False - - # Get goal position in grid coordinates - goal_point = costmap.world_to_grid(goal_xy) - goal_cell_x, goal_cell_y = goal_point.x, goal_point.y - - # Check if goal is within the costmap bounds - is_in_bounds = 0 <= goal_cell_x < costmap.width and 0 <= goal_cell_y < costmap.height - - if not is_in_bounds: - logger.warning(f"Goal ({goal_xy[0]:.2f}, {goal_xy[1]:.2f}) is outside costmap bounds") - - return is_in_bounds - - def adjust_goal_to_valid_position(self, goal_xy: VectorLike, clearance: float = 0.5) -> Tuple[float, float]: - """Find a valid (non-colliding) goal position by moving it towards the robot. - - Args: - goal_xy: Original goal position (x, y) in odom frame - clearance: Additional distance to move back from obstacles for better clearance (meters) - - Returns: - Tuple[float, float]: A valid goal position, or the original goal if already valid - """ - [pos, rot] = self.transform.transform_euler("base_link", "odom") - - robot_x, robot_y = pos[0], pos[1] - - # Original goal - goal_x, goal_y = to_tuple(goal_xy) - - if not self.check_goal_collision((goal_x, goal_y)): - return (goal_x, goal_y) - - # Calculate vector from goal to robot - dx = robot_x - goal_x - dy = robot_y - goal_y - distance = np.sqrt(dx*dx + dy*dy) - - if distance < 0.001: # Goal is at robot position - return to_tuple(goal_xy) - - # Normalize direction vector - dx /= distance - dy /= distance - - # Step size - step_size = 0.25 # meters - - # Move goal towards robot step by step - current_x, current_y = goal_x, goal_y - steps = 0 - max_steps = 50 # Safety limit - - # Variables to store the first valid position found - valid_found = False - valid_x, valid_y = None, None - - while steps < max_steps: - # Move towards robot - current_x += dx * step_size - current_y += dy * step_size - steps += 1 - - # Check if we've reached or passed the robot - new_distance = np.sqrt((current_x - robot_x)**2 + (current_y - robot_y)**2) - if new_distance < step_size: - # We've reached the robot without finding a valid point - # Move back one step from robot to avoid self-collision - current_x = robot_x - dx * step_size - current_y = robot_y - dy * step_size - break - - # Check if this position is valid - if not self.check_goal_collision((current_x, current_y)) and self.is_goal_in_costmap_bounds((current_x, current_y)): - # Store the first valid position - if not valid_found: - valid_found = True - valid_x, valid_y = current_x, current_y - - # If clearance is requested, continue searching for a better position - if clearance > 0: - continue - - # Calculate position with additional clearance - if clearance > 0: - - # Calculate clearance position - clearance_x = current_x + dx * clearance - clearance_y = current_y + dy * clearance - - logger.info(f"Checking clearance position at ({clearance_x:.2f}, {clearance_y:.2f})") - - # Check if the clearance position is also valid - if (not self.check_goal_collision((clearance_x, clearance_y)) and - self.is_goal_in_costmap_bounds((clearance_x, clearance_y))): - logger.info(f"Found valid goal with clearance at ({clearance_x:.2f}, {clearance_y:.2f})") - return (clearance_x, clearance_y) - - # Return the valid position without clearance - logger.info(f"Found valid goal at ({current_x:.2f}, {current_y:.2f})") - return (current_x, current_y) - - # If we found a valid position earlier but couldn't add clearance - if valid_found: - logger.info(f"Using valid goal found at ({valid_x:.2f}, {valid_y:.2f})") - return (valid_x, valid_y) - - logger.warning(f"Could not find valid goal after {steps} steps, using closest point to robot") - return (current_x, current_y) - - def check_if_stuck(self) -> bool: - """ - Check if the robot is stuck by analyzing movement history. - - Returns: - bool: True if the robot is determined to be stuck, False otherwise - """ - # Get current position and time - current_time = time.time() - - # Get current robot position - [pos, _] = self.transform.transform_euler("base_link", "odom") - current_position = (pos[0], pos[1], current_time) - - # Add current position to history (newest is appended at the end) - self.position_history.append(current_position) - - # Need enough history to make a determination - min_history_size = self.stuck_detection_window_seconds * self.control_frequency - if len(self.position_history) < min_history_size: - return False - - # Find positions within our detection window (positions are already in order from oldest to newest) - window_start_time = current_time - self.stuck_detection_window_seconds - window_positions = [] - - # Collect positions within the window (newest entries will be at the end) - for pos_x, pos_y, timestamp in self.position_history: - if timestamp >= window_start_time: - window_positions.append((pos_x, pos_y, timestamp)) - - # Need at least a few positions in the window - if len(window_positions) < 3: - return False - - # Ensure correct order: oldest to newest - window_positions.sort(key=lambda p: p[2]) - - # Get the oldest and newest positions in the window - oldest_x, oldest_y, oldest_time = window_positions[0] - newest_x, newest_y, newest_time = window_positions[-1] - - # Calculate time range in the window (should always be positive) - time_range = newest_time - oldest_time - - # Calculate displacement from oldest to newest position - displacement = np.sqrt((newest_x - oldest_x)**2 + (newest_y - oldest_y)**2) - - # Check if we're stuck - moved less than threshold over minimum time - # Only consider it if the time range makes sense (positive and sufficient) - is_currently_stuck = (time_range >= self.stuck_time_threshold and - time_range <= self.stuck_detection_window_seconds and - displacement < self.stuck_distance_threshold) - - if is_currently_stuck: - logger.warning(f"Robot appears to be stuck! Displacement {displacement:.3f}m over {time_range:.1f}s") - - # Don't trigger recovery if it's already active - if not self.is_recovery_active: - self.is_recovery_active = True - self.recovery_start_time = current_time - return True - - # Check if we've been trying to recover for too long - elif current_time - self.recovery_start_time > self.recovery_duration: - logger.error(f"Recovery behavior has been active for {self.recovery_duration}s without success") - # Reset recovery state - maybe a different behavior will work - self.is_recovery_active = False - self.recovery_start_time = current_time - - # If we've moved enough, we're not stuck anymore - elif self.is_recovery_active and displacement > self.unstuck_distance_threshold: - logger.info(f"Robot has escaped from stuck state (moved {displacement:.3f}m)") - self.is_recovery_active = False - - return self.is_recovery_active - - def execute_recovery_behavior(self) -> Dict[str, float]: - """ - Execute a recovery behavior when the robot is stuck. - - Returns: - Dict[str, float]: Velocity commands for the recovery behavior - """ - # Calculate how long we've been in recovery - recovery_time = time.time() - self.recovery_start_time - - # Calculate recovery phases based on control frequency - backup_phase_time = 3.0 # seconds - rotate_phase_time = 2.0 # seconds - - # Simple recovery behavior state machine - if recovery_time < backup_phase_time: - # First try backing up - logger.info("Recovery: backing up") - return {'x_vel': -0.2, 'angular_vel': 0.0} - elif recovery_time < backup_phase_time + rotate_phase_time: - # Then try rotating - logger.info("Recovery: rotating to find new path") - rotation_direction = 1.0 if np.random.random() > 0.5 else -1.0 - return {'x_vel': 0.0, 'angular_vel': rotation_direction * self.max_angular_vel * 0.7} - else: - # If we're still stuck after backup and rotation, terminate navigation - logger.error("Recovery failed after backup and rotation. Navigation terminated.") - # Set a flag to indicate navigation should terminate - self.navigation_failed = True - # Stop the robot - return {'x_vel': 0.0, 'angular_vel': 0.0} - -def navigate_to_goal_local( - robot, goal_xy_robot: Tuple[float, float], goal_theta: Optional[float] = None, distance: float = 0.0, timeout: float = 60.0, - stop_event: Optional[threading.Event] = None -) -> bool: - """ - Navigates the robot to a goal specified in the robot's local frame - using the local planner. - - Args: - robot: Robot instance to control - goal_xy_robot: Tuple (x, y) representing the goal position relative - to the robot's current position and orientation. - distance: Desired distance to maintain from the goal in meters. - If non-zero, the robot will stop this far away from the goal. - timeout: Maximum time (in seconds) allowed to reach the goal. - stop_event: Optional threading.Event to signal when navigation should stop - - Returns: - bool: True if the goal was reached within the timeout, False otherwise. - """ - logger.info(f"Starting navigation to local goal {goal_xy_robot} with distance {distance}m and timeout {timeout}s.") - - goal_x, goal_y = goal_xy_robot - - # Calculate goal orientation to face the target - if goal_theta is None: - goal_theta = np.arctan2(goal_y, goal_x) - - # If distance is non-zero, adjust the goal to stop at the desired distance - if distance > 0: - # Calculate magnitude of the goal vector - goal_distance = np.sqrt(goal_x**2 + goal_y**2) - - # Only adjust if goal is further than the desired distance - if goal_distance > distance: - goal_x, goal_y = distance_angle_to_goal_xy(goal_distance - distance, goal_theta) - - # Set the goal in the robot's frame with orientation to face the original target - robot.local_planner.set_goal((goal_x, goal_y), frame="base_link", goal_theta=goal_theta) - - # Get control period from robot's local planner for consistent timing - control_period = 1.0 / robot.local_planner.control_frequency - - start_time = time.time() - goal_reached = False - - try: - while time.time() - start_time < timeout and not (stop_event and stop_event.is_set()): - # Check if goal has been reached - if robot.local_planner.is_goal_reached(): - logger.info("Goal reached successfully.") - goal_reached = True - break - - # Check if navigation failed flag is set - if robot.local_planner.navigation_failed: - logger.error("Navigation aborted due to repeated recovery failures.") - goal_reached = False - break - - # Get planned velocity towards the goal - vel_command = robot.local_planner.plan() - x_vel = vel_command.get("x_vel", 0.0) - angular_vel = vel_command.get("angular_vel", 0.0) - - # Send velocity command - robot.local_planner.move_vel_control(x=x_vel, y=0, yaw=angular_vel) - - # Control loop frequency - use robot's control frequency - time.sleep(control_period) - - if not goal_reached: - logger.warning(f"Navigation timed out after {timeout} seconds before reaching goal.") - - except KeyboardInterrupt: - logger.info("Navigation to local goal interrupted by user.") - goal_reached = False # Consider interruption as failure - except Exception as e: - logger.error(f"Error during navigation to local goal: {e}") - goal_reached = False # Consider error as failure - finally: - logger.info("Stopping robot after navigation attempt.") - robot.local_planner.move_vel_control(0, 0, 0) # Stop the robot - - return goal_reached - -def navigate_path_local( - robot, path: Path, timeout: float = 120.0, goal_theta: Optional[float] = None, - stop_event: Optional[threading.Event] = None -) -> bool: - """ - Navigates the robot along a path of waypoints using the waypoint following capability - of the local planner. - - Args: - robot: Robot instance to control - path: Path object containing waypoints in odom/map frame - timeout: Maximum time (in seconds) allowed to follow the complete path - goal_theta: Optional final orientation in radians - stop_event: Optional threading.Event to signal when navigation should stop - - Returns: - bool: True if the entire path was successfully followed, False otherwise - """ - logger.info(f"Starting navigation along path with {len(path)} waypoints and timeout {timeout}s.") - - # Set the path in the local planner - robot.local_planner.set_goal_waypoints(path, goal_theta=goal_theta) - - # Get control period from robot's local planner for consistent timing - control_period = 1.0 / robot.local_planner.control_frequency - - start_time = time.time() - path_completed = False - - try: - while time.time() - start_time < timeout and not (stop_event and stop_event.is_set()): - # Check if the entire path has been traversed - if robot.local_planner.is_goal_reached(): - logger.info("Path traversed successfully.") - path_completed = True - break - - # Check if navigation failed flag is set - if robot.local_planner.navigation_failed: - logger.error("Navigation aborted due to repeated recovery failures.") - path_completed = False - break - - # Get planned velocity towards the current waypoint target - vel_command = robot.local_planner.plan() - x_vel = vel_command.get("x_vel", 0.0) - angular_vel = vel_command.get("angular_vel", 0.0) - - # Send velocity command - robot.local_planner.move_vel_control(x=x_vel, y=0, yaw=angular_vel) - - # Control loop frequency - use robot's control frequency - time.sleep(control_period) - - if not path_completed: - logger.warning(f"Path following timed out after {timeout} seconds before completing the path.") - - except KeyboardInterrupt: - logger.info("Path navigation interrupted by user.") - path_completed = False - except Exception as e: - logger.error(f"Error during path navigation: {e}") - path_completed = False - finally: - logger.info("Stopping robot after path navigation attempt.") - robot.local_planner.move_vel_control(0, 0, 0) # Stop the robot - - return path_completed - -def visualize_local_planner_state( - occupancy_grid: np.ndarray, - grid_resolution: float, - grid_origin: Tuple[float, float, float], - robot_pose: Tuple[float, float, float], - visualization_size: int = 400, - robot_width: float = 0.5, - robot_length: float = 0.7, - map_size_meters: float = 10.0, - goal_xy: Optional[Tuple[float, float]] = None, - goal_theta: Optional[float] = None, - histogram: Optional[np.ndarray] = None, - selected_direction: Optional[float] = None, - waypoints: Optional['Path'] = None, - current_waypoint_index: Optional[int] = None -) -> np.ndarray: - """Generate a bird's eye view visualization of the local costmap. - Optionally includes VFH histogram, selected direction, and waypoints path. - - Args: - occupancy_grid: 2D numpy array of the occupancy grid - grid_resolution: Resolution of the grid in meters/cell - grid_origin: Tuple (x, y, theta) of the grid origin in the odom frame - robot_pose: Tuple (x, y, theta) of the robot pose in the odom frame - visualization_size: Size of the visualization image in pixels - robot_width: Width of the robot in meters - robot_length: Length of the robot in meters - map_size_meters: Size of the map to visualize in meters - goal_xy: Optional tuple (x, y) of the goal position in the odom frame - goal_theta: Optional goal orientation in radians (in odom frame) - histogram: Optional numpy array of the VFH histogram - selected_direction: Optional selected direction angle in radians - waypoints: Optional Path object containing waypoints to visualize - current_waypoint_index: Optional index of the current target waypoint - """ - - robot_x, robot_y, robot_theta = robot_pose - grid_origin_x, grid_origin_y, _ = grid_origin - vis_size = visualization_size - scale = vis_size / map_size_meters - - vis_img = np.ones((vis_size, vis_size, 3), dtype=np.uint8) * 255 - center_x = vis_size // 2 - center_y = vis_size // 2 - - grid_height, grid_width = occupancy_grid.shape - - # Calculate robot position relative to grid origin - robot_rel_x = robot_x - grid_origin_x - robot_rel_y = robot_y - grid_origin_y - robot_cell_x = int(robot_rel_x / grid_resolution) - robot_cell_y = int(robot_rel_y / grid_resolution) - - half_size_cells = int(map_size_meters / grid_resolution / 2) - - # Draw grid cells (using standard occupancy coloring) - for y in range(max(0, robot_cell_y - half_size_cells), - min(grid_height, robot_cell_y + half_size_cells)): - for x in range(max(0, robot_cell_x - half_size_cells), - min(grid_width, robot_cell_x + half_size_cells)): - cell_rel_x_meters = (x - robot_cell_x) * grid_resolution - cell_rel_y_meters = (y - robot_cell_y) * grid_resolution - - img_x = int(center_x + cell_rel_x_meters * scale) - img_y = int(center_y - cell_rel_y_meters * scale) # Flip y-axis - - if 0 <= img_x < vis_size and 0 <= img_y < vis_size: - cell_value = occupancy_grid[y, x] - if cell_value == -1: - color = (200, 200, 200) # Unknown (Light gray) - elif cell_value == 0: - color = (255, 255, 255) # Free (White) - else: # Occupied - # Scale darkness based on occupancy value (0-100) - darkness = 255 - int(155 * (cell_value / 100)) - 100 - color = (darkness, darkness, darkness) # Shades of gray/black - - cell_size_px = max(1, int(grid_resolution * scale)) - cv2.rectangle(vis_img, - (img_x - cell_size_px//2, img_y - cell_size_px//2), - (img_x + cell_size_px//2, img_y + cell_size_px//2), - color, -1) - - # Draw waypoints path if provided - if waypoints is not None and len(waypoints) > 0: - try: - path_points = [] - for i, waypoint in enumerate(waypoints): - # Convert waypoint from odom frame to visualization frame - wp_x, wp_y = waypoint[0], waypoint[1] - wp_rel_x = wp_x - robot_x - wp_rel_y = wp_y - robot_y - - wp_img_x = int(center_x + wp_rel_x * scale) - wp_img_y = int(center_y - wp_rel_y * scale) # Flip y-axis - - if 0 <= wp_img_x < vis_size and 0 <= wp_img_y < vis_size: - path_points.append((wp_img_x, wp_img_y)) - - # Draw each waypoint as a small circle - cv2.circle(vis_img, (wp_img_x, wp_img_y), 3, (0, 128, 0), -1) # Dark green dots - - # Highlight current target waypoint - if current_waypoint_index is not None and i == current_waypoint_index: - cv2.circle(vis_img, (wp_img_x, wp_img_y), 6, (0, 0, 255), 2) # Red circle - - # Connect waypoints with lines to show the path - if len(path_points) > 1: - for i in range(len(path_points) - 1): - cv2.line(vis_img, path_points[i], path_points[i + 1], (0, 200, 0), 1) # Green line - except Exception as e: - logger.error(f"Error drawing waypoints: {e}") - - # Draw histogram - if histogram is not None: - num_bins = len(histogram) - # Find absolute maximum value (ignoring any negative debug values) - abs_histogram = np.abs(histogram) - max_hist_value = np.max(abs_histogram) if np.max(abs_histogram) > 0 else 1.0 - hist_scale = (vis_size / 2) * 0.8 # Scale histogram lines to 80% of half the viz size - - for i in range(num_bins): - # Angle relative to robot's forward direction - angle_relative_to_robot = (i / num_bins) * 2 * math.pi - math.pi - # Angle in the visualization frame (relative to image +X axis) - vis_angle = angle_relative_to_robot + robot_theta - - # Get the value and check if it's a special debug value (negative) - hist_val = histogram[i] - is_debug_value = hist_val < 0 - - # Use absolute value for line length - normalized_val = min(1.0, abs(hist_val) / max_hist_value) - line_length = normalized_val * hist_scale - - # Calculate endpoint using the visualization angle - end_x = int(center_x + line_length * math.cos(vis_angle)) - end_y = int(center_y - line_length * math.sin(vis_angle)) # Flipped Y - - # Color based on value and whether it's a debug value - if is_debug_value: - # Use green for debug values (minimum cost bin) - color = (0, 255, 0) # Green - line_width = 2 # Thicker line for emphasis - else: - # Regular coloring for normal values (blue to red gradient based on obstacle density) - blue = max(0, 255 - int(normalized_val * 255)) - red = min(255, int(normalized_val * 255)) - color = (blue, 0, red) # BGR format: obstacles are redder, clear areas are bluer - line_width = 1 - - cv2.line(vis_img, (center_x, center_y), (end_x, end_y), color, line_width) - - # Draw robot - robot_length_px = int(robot_length * scale) - robot_width_px = int(robot_width * scale) - robot_pts = np.array([ - [-robot_length_px/2, -robot_width_px/2], [robot_length_px/2, -robot_width_px/2], - [robot_length_px/2, robot_width_px/2], [-robot_length_px/2, robot_width_px/2] - ], dtype=np.float32) - rotation_matrix = np.array([ - [math.cos(robot_theta), -math.sin(robot_theta)], - [math.sin(robot_theta), math.cos(robot_theta)] - ]) - robot_pts = np.dot(robot_pts, rotation_matrix.T) - robot_pts[:, 0] += center_x - robot_pts[:, 1] = center_y - robot_pts[:, 1] # Flip y-axis - cv2.fillPoly(vis_img, [robot_pts.reshape((-1, 1, 2)).astype(np.int32)], (0, 0, 255)) # Red robot - - # Draw robot direction line - front_x = int(center_x + (robot_length_px/2) * math.cos(robot_theta)) - front_y = int(center_y - (robot_length_px/2) * math.sin(robot_theta)) - cv2.line(vis_img, (center_x, center_y), (front_x, front_y), (255, 0, 0), 2) # Blue line - - # Draw selected direction - if selected_direction is not None: - # selected_direction is relative to robot frame - # Angle in the visualization frame (relative to image +X axis) - vis_angle_selected = selected_direction + robot_theta - - # Make slightly longer than max histogram line - sel_dir_line_length = (vis_size / 2) * 0.9 - - sel_end_x = int(center_x + sel_dir_line_length * math.cos(vis_angle_selected)) - sel_end_y = int(center_y - sel_dir_line_length * math.sin(vis_angle_selected)) # Flipped Y - - cv2.line(vis_img, (center_x, center_y), (sel_end_x, sel_end_y), (0, 165, 255), 2) # BGR for Orange - - # Draw goal - if goal_xy is not None: - goal_x, goal_y = goal_xy - goal_rel_x_map = goal_x - robot_x - goal_rel_y_map = goal_y - robot_y - goal_img_x = int(center_x + goal_rel_x_map * scale) - goal_img_y = int(center_y - goal_rel_y_map * scale) # Flip y-axis - if 0 <= goal_img_x < vis_size and 0 <= goal_img_y < vis_size: - cv2.circle(vis_img, (goal_img_x, goal_img_y), 5, (0, 255, 0), -1) # Green circle - cv2.circle(vis_img, (goal_img_x, goal_img_y), 8, (0, 0, 0), 1) # Black outline - - # Draw goal orientation - if goal_theta is not None and goal_xy is not None: - # For waypoint mode, only draw orientation at the final waypoint - if waypoints is not None and len(waypoints) > 0: - # Use the final waypoint position - final_waypoint = waypoints[-1] - goal_x, goal_y = final_waypoint[0], final_waypoint[1] - else: - # Use the current goal position - goal_x, goal_y = goal_xy - - goal_rel_x_map = goal_x - robot_x - goal_rel_y_map = goal_y - robot_y - goal_img_x = int(center_x + goal_rel_x_map * scale) - goal_img_y = int(center_y - goal_rel_y_map * scale) # Flip y-axis - - # Calculate goal orientation vector direction in visualization frame - # goal_theta is already in odom frame, need to adjust for visualization orientation - goal_dir_length = 30 # Length of direction indicator in pixels - goal_dir_end_x = int(goal_img_x + goal_dir_length * math.cos(goal_theta)) - goal_dir_end_y = int(goal_img_y - goal_dir_length * math.sin(goal_theta)) # Flip y-axis - - # Draw goal orientation arrow - if 0 <= goal_img_x < vis_size and 0 <= goal_img_y < vis_size: - cv2.arrowedLine(vis_img, (goal_img_x, goal_img_y), (goal_dir_end_x, goal_dir_end_y), - (255, 0, 255), 4) # Magenta arrow - - # Add scale bar - scale_bar_length_px = int(1.0 * scale) - scale_bar_x = vis_size - scale_bar_length_px - 10 - scale_bar_y = vis_size - 20 - cv2.line(vis_img, (scale_bar_x, scale_bar_y), - (scale_bar_x + scale_bar_length_px, scale_bar_y), (0, 0, 0), 2) - cv2.putText(vis_img, "1m", (scale_bar_x, scale_bar_y - 5), - cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 0), 1) - - # Add status info - status_text = [] - if waypoints is not None: - if current_waypoint_index is not None: - status_text.append(f"WP: {current_waypoint_index}/{len(waypoints)}") - else: - status_text.append(f"WPs: {len(waypoints)}") - - y_pos = 20 - for text in status_text: - cv2.putText(vis_img, text, (10, y_pos), - cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1) - y_pos += 20 - - return vis_img \ No newline at end of file diff --git a/dimos/robot/local_planner/vfh_local_planner.py b/dimos/robot/local_planner/vfh_local_planner.py deleted file mode 100644 index 53d4588bf3..0000000000 --- a/dimos/robot/local_planner/vfh_local_planner.py +++ /dev/null @@ -1,393 +0,0 @@ -#!/usr/bin/env python3 - -import math -import numpy as np -from typing import Dict, Tuple, Optional, Callable -import cv2 -import logging -import time - -from dimos.utils.logging_config import setup_logger -from dimos.utils.ros_utils import normalize_angle - -from dimos.robot.local_planner.local_planner import BaseLocalPlanner, visualize_local_planner_state -from dimos.types.costmap import Costmap -from nav_msgs.msg import OccupancyGrid - -logger = setup_logger("dimos.robot.unitree.vfh_local_planner", level=logging.DEBUG) - -class VFHPurePursuitPlanner(BaseLocalPlanner): - """ - A local planner that combines Vector Field Histogram (VFH) for obstacle avoidance - with Pure Pursuit for goal tracking. - """ - - def __init__(self, - get_costmap: Callable[[], Optional[OccupancyGrid]], - transform: object, - move_vel_control: Callable[[float, float, float], None], - safety_threshold: float = 0.8, - histogram_bins: int = 144, - max_linear_vel: float = 0.8, - max_angular_vel: float = 1.0, - lookahead_distance: float = 1.0, - goal_tolerance: float = 0.2, - angle_tolerance: float = 0.1, # ~5.7 degrees - robot_width: float = 0.5, - robot_length: float = 0.7, - visualization_size: int = 400, - control_frequency: float = 10.0, - safe_goal_distance: float = 1.0): - """ - Initialize the VFH + Pure Pursuit planner. - - Args: - get_costmap: Function to get the latest local costmap - transform: Object with transform methods (transform_point, transform_rot, etc.) - move_vel_control: Function to send velocity commands - safety_threshold: Distance to maintain from obstacles (meters) - histogram_bins: Number of directional bins in the polar histogram - max_linear_vel: Maximum linear velocity (m/s) - max_angular_vel: Maximum angular velocity (rad/s) - lookahead_distance: Lookahead distance for pure pursuit (meters) - goal_tolerance: Distance at which the goal is considered reached (meters) - angle_tolerance: Angle at which the goal orientation is considered reached (radians) - robot_width: Width of the robot for visualization (meters) - robot_length: Length of the robot for visualization (meters) - visualization_size: Size of the visualization image in pixels - control_frequency: Frequency at which the planner is called (Hz) - safe_goal_distance: Distance at which to adjust the goal and ignore obstacles (meters) - """ - # Initialize base class - super().__init__( - get_costmap=get_costmap, - transform=transform, - move_vel_control=move_vel_control, - safety_threshold=safety_threshold, - max_linear_vel=max_linear_vel, - max_angular_vel=max_angular_vel, - lookahead_distance=lookahead_distance, - goal_tolerance=goal_tolerance, - angle_tolerance=angle_tolerance, - robot_width=robot_width, - robot_length=robot_length, - visualization_size=visualization_size, - control_frequency=control_frequency, - safe_goal_distance=safe_goal_distance - ) - - # VFH specific parameters - self.histogram_bins = histogram_bins - self.histogram = None - self.selected_direction = None - - # VFH tuning parameters - self.alpha = 0.2 # Histogram smoothing factor - self.obstacle_weight = 10.0 - self.goal_weight = 1.0 - self.prev_direction_weight = 0.5 - self.prev_selected_angle = 0.0 - self.prev_linear_vel = 0.0 - self.linear_vel_filter_factor = 0.4 - self.low_speed_nudge = 0.1 - - # Add after other initialization - self.angle_mapping = np.linspace(-np.pi, np.pi, self.histogram_bins, endpoint=False) - self.smoothing_kernel = np.array([self.alpha, (1-2*self.alpha), self.alpha]) - - def _compute_velocity_commands(self) -> Dict[str, float]: - """ - VFH + Pure Pursuit specific implementation of velocity command computation. - - Returns: - Dict[str, float]: Velocity commands with 'x_vel' and 'angular_vel' keys - """ - # Get necessary data for planning - costmap = self.get_costmap() - if costmap is None: - logger.warning("No costmap available for planning") - return {'x_vel': 0.0, 'angular_vel': 0.0} - - [pos, rot] = self.transform.transform_euler("base_link", "odom") - robot_x, robot_y, robot_theta = pos[0], pos[1], rot[2] - robot_pose = (robot_x, robot_y, robot_theta) - - # Calculate goal-related parameters - goal_x, goal_y = self.goal_xy - dx = goal_x - robot_x - dy = goal_y - robot_y - goal_distance = np.linalg.norm([dx, dy]) - goal_direction = np.arctan2(dy, dx) - robot_theta - goal_direction = normalize_angle(goal_direction) - - self.histogram = self.build_polar_histogram(costmap, robot_pose) - - # If we're ignoring obstacles near the goal, zero out the histogram - if self.ignore_obstacles: - self.histogram = np.zeros_like(self.histogram) - - self.selected_direction = self.select_direction( - self.goal_weight, - self.obstacle_weight, - self.prev_direction_weight, - self.histogram, - goal_direction, - ) - - # Calculate Pure Pursuit Velocities - linear_vel, angular_vel = self.compute_pure_pursuit(goal_distance, self.selected_direction) - - # Slow down when turning sharply - if abs(self.selected_direction) > 0.25: # ~15 degrees - # Scale from 1.0 (small turn) to 0.5 (sharp turn at 90 degrees or more) - turn_factor = max(0.25, 1.0 - (abs(self.selected_direction) / (np.pi/2))) - linear_vel *= turn_factor - - # Apply Collision Avoidance Stop - skip if ignoring obstacles - if not self.ignore_obstacles and self.check_collision(self.selected_direction, safety_threshold=0.5): - # Re-select direction prioritizing obstacle avoidance if colliding - self.selected_direction = self.select_direction( - self.goal_weight * 0.2, - self.obstacle_weight, - self.prev_direction_weight * 0.2, - self.histogram, - goal_direction - ) - linear_vel, angular_vel = self.compute_pure_pursuit(goal_distance, self.selected_direction) - - if self.check_collision(0.0, safety_threshold=self.safety_threshold): - logger.warning("Collision detected ahead. Stopping.") - linear_vel = 0.0 - - self.prev_linear_vel = linear_vel - filtered_linear_vel = self.prev_linear_vel * self.linear_vel_filter_factor + linear_vel * (1 - self.linear_vel_filter_factor) - - return {'x_vel': filtered_linear_vel, 'angular_vel': angular_vel} - - def _smooth_histogram(self, histogram: np.ndarray) -> np.ndarray: - """ - Apply advanced smoothing to the polar histogram to better identify valleys - and reduce noise. - - Args: - histogram: Raw histogram to smooth - - Returns: - np.ndarray: Smoothed histogram - """ - # Apply a windowed average with variable width based on obstacle density - smoothed = np.zeros_like(histogram) - bins = len(histogram) - - # First pass: basic smoothing with a 5-point kernel - # This uses a wider window than the original 3-point smoother - for i in range(bins): - # Compute indices with wrap-around - indices = [(i + j) % bins for j in range(-2, 3)] - # Apply weighted average (more weight to the center) - weights = [0.1, 0.2, 0.4, 0.2, 0.1] # Sum = 1.0 - smoothed[i] = sum(histogram[idx] * weight for idx, weight in zip(indices, weights)) - - # Second pass: peak and valley enhancement - enhanced = np.zeros_like(smoothed) - for i in range(bins): - # Check neighboring values - prev_idx = (i - 1) % bins - next_idx = (i + 1) % bins - - # Enhance valleys (low values) - if smoothed[i] < smoothed[prev_idx] and smoothed[i] < smoothed[next_idx]: - # It's a local minimum - make it even lower - enhanced[i] = smoothed[i] * 0.8 - # Enhance peaks (high values) - elif smoothed[i] > smoothed[prev_idx] and smoothed[i] > smoothed[next_idx]: - # It's a local maximum - make it even higher - enhanced[i] = min(1.0, smoothed[i] * 1.2) - else: - enhanced[i] = smoothed[i] - - return enhanced - - def build_polar_histogram(self, costmap: Costmap, robot_pose: Tuple[float, float, float]): - """ - Build a polar histogram of obstacle densities around the robot. - - Args: - costmap: Costmap object with grid and metadata - robot_pose: Tuple (x, y, theta) of the robot pose in the odom frame - - Returns: - np.ndarray: Polar histogram of obstacle densities - """ - - # Get grid and find all obstacle cells - occupancy_grid = costmap.grid - y_indices, x_indices = np.where(occupancy_grid > 0) - if len(y_indices) == 0: # No obstacles - return np.zeros(self.histogram_bins) - - # Get robot position in grid coordinates - robot_x, robot_y, robot_theta = robot_pose - robot_point = costmap.world_to_grid((robot_x, robot_y)) - robot_cell_x, robot_cell_y = robot_point.x, robot_point.y - - # Vectorized distance and angle calculation - dx_cells = x_indices - robot_cell_x - dy_cells = y_indices - robot_cell_y - distances = np.sqrt(dx_cells**2 + dy_cells**2) * costmap.resolution - angles_grid = np.arctan2(dy_cells, dx_cells) - angles_robot = normalize_angle(angles_grid - robot_theta) - - # Convert to bin indices - bin_indices = ((angles_robot + np.pi) / (2 * np.pi) * self.histogram_bins).astype(int) % self.histogram_bins - - # Get obstacle values - obstacle_values = occupancy_grid[y_indices, x_indices] / 100.0 - - # Build histogram - histogram = np.zeros(self.histogram_bins) - mask = distances > 0 - # Weight obstacles by inverse square of distance and cell value - np.add.at(histogram, bin_indices[mask], obstacle_values[mask] / (distances[mask] ** 2)) - - # Apply the enhanced smoothing - return self._smooth_histogram(histogram) - - def select_direction(self, goal_weight, obstacle_weight, prev_direction_weight, histogram, goal_direction): - """ - Select best direction based on a simple weighted cost function. - - Args: - goal_weight: Weight for the goal direction component - obstacle_weight: Weight for the obstacle avoidance component - prev_direction_weight: Weight for previous direction consistency - histogram: Polar histogram of obstacle density - goal_direction: Desired direction to goal - - Returns: - float: Selected direction in radians - """ - # Normalize histogram if needed - if np.max(histogram) > 0: - histogram = histogram / np.max(histogram) - - # Calculate costs for each possible direction - angle_diffs = np.abs(normalize_angle(self.angle_mapping - goal_direction)) - prev_diffs = np.abs(normalize_angle(self.angle_mapping - self.prev_selected_angle)) - - # Combine costs with weights - obstacle_costs = obstacle_weight * histogram - goal_costs = goal_weight * angle_diffs - prev_costs = prev_direction_weight * prev_diffs - - total_costs = obstacle_costs + goal_costs + prev_costs - - # Select direction with lowest cost - min_cost_idx = np.argmin(total_costs) - selected_angle = self.angle_mapping[min_cost_idx] - - # Update history for next iteration - self.prev_selected_angle = selected_angle - - return selected_angle - - def compute_pure_pursuit(self, goal_distance: float, goal_direction: float) -> Tuple[float, float]: - """ Compute pure pursuit velocities.""" - if goal_distance < self.goal_tolerance: - return 0.0, 0.0 - - lookahead = min(self.lookahead_distance, goal_distance) - linear_vel = min(self.max_linear_vel, goal_distance) - angular_vel = 2.0 * np.sin(goal_direction) / lookahead - angular_vel = max(-self.max_angular_vel, min(angular_vel, self.max_angular_vel)) - - return linear_vel, angular_vel - - def check_collision(self, selected_direction: float, safety_threshold: float = 1.0) -> bool: - """Check if there's an obstacle in the selected direction within safety threshold.""" - # Skip collision check if ignoring obstacles - if self.ignore_obstacles: - return False - - # Get the latest costmap and robot pose - costmap = self.get_costmap() - if costmap is None: - return False # No costmap available - - [pos, rot] = self.transform.transform_euler("base_link", "odom") - robot_x, robot_y, robot_theta = pos[0], pos[1], rot[2] - - # Direction in world frame - direction_world = robot_theta + selected_direction - - # Safety distance in cells - safety_cells = int(safety_threshold / costmap.resolution) - - # Get robot position in grid coordinates - robot_point = costmap.world_to_grid((robot_x, robot_y)) - robot_cell_x, robot_cell_y = robot_point.x, robot_point.y - - # Check for obstacles along the selected direction - for dist in range(1, safety_cells + 1): - # Calculate cell position - cell_x = robot_cell_x + int(dist * np.cos(direction_world)) - cell_y = robot_cell_y + int(dist * np.sin(direction_world)) - - # Check if cell is within grid bounds - if not (0 <= cell_x < costmap.width and 0 <= cell_y < costmap.height): - continue - - # Check if cell contains an obstacle (threshold at 50) - if costmap.grid[int(cell_y), int(cell_x)] > 50: - return True - - return False # No collision detected - - def update_visualization(self) -> np.ndarray: - """Generate visualization of the planning state.""" - try: - costmap = self.get_costmap() - if costmap is None: - raise ValueError("Costmap is None") - - [pos, rot] = self.transform.transform_euler("base_link", "odom") - robot_x, robot_y, robot_theta = pos[0], pos[1], rot[2] - robot_pose = (robot_x, robot_y, robot_theta) - - goal_xy = self.goal_xy # This could be a lookahead point or final goal - - # Get the latest histogram and selected direction, if available - histogram = getattr(self, 'histogram', None) - selected_direction = getattr(self, 'selected_direction', None) - - # Get waypoint data if in waypoint mode - waypoints_to_draw = self.waypoints_in_odom - current_wp_index_to_draw = self.current_waypoint_index if self.waypoints_in_odom is not None else None - # Ensure index is valid before passing - if waypoints_to_draw is not None and current_wp_index_to_draw is not None: - if not (0 <= current_wp_index_to_draw < len(waypoints_to_draw)): - current_wp_index_to_draw = None # Invalidate index if out of bounds - - return visualize_local_planner_state( - occupancy_grid=costmap.grid, - grid_resolution=costmap.resolution, - grid_origin=(costmap.origin.x, costmap.origin.y, costmap.origin_theta), - robot_pose=robot_pose, - goal_xy=goal_xy, # Current target (lookahead or final) - goal_theta=self.goal_theta, # Pass goal orientation if available - visualization_size=self.visualization_size, - robot_width=self.robot_width, - robot_length=self.robot_length, - histogram=histogram, - selected_direction=selected_direction, - waypoints=waypoints_to_draw, # Pass the full path - current_waypoint_index=current_wp_index_to_draw # Pass the target index - ) - except Exception as e: - logger.error(f"Error during visualization update: {e}") - # Return a blank image with error text - blank = np.ones((self.visualization_size, self.visualization_size, 3), dtype=np.uint8) * 255 - cv2.putText(blank, "Viz Error", - (self.visualization_size // 4, self.visualization_size // 2), - cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2) - return blank diff --git a/dimos/robot/position_stream.py b/dimos/robot/position_stream.py index 9a59fe012a..8cb5966b24 100644 --- a/dimos/robot/position_stream.py +++ b/dimos/robot/position_stream.py @@ -19,38 +19,36 @@ """ import logging -import numpy as np -from typing import Tuple, Optional, Callable, Dict, Any import time -from reactivex import Subject, Observable -from reactivex import operators as ops -import rclpy -from rclpy.node import Node + from geometry_msgs.msg import PoseStamped from nav_msgs.msg import Odometry +from rclpy.node import Node +from reactivex import Observable, Subject, operators as ops from dimos.utils.logging_config import setup_logger logger = setup_logger("dimos.robot.position_stream", level=logging.INFO) + class PositionStreamProvider: """ A provider for streaming position updates from ROS. - + This class creates an Observable stream of position updates by subscribing to ROS odometry or pose topics. """ - + def __init__( self, ros_node: Node, odometry_topic: str = "/odom", - pose_topic: Optional[str] = None, - use_odometry: bool = True - ): + pose_topic: str | None = None, + use_odometry: bool = True, + ) -> None: """ Initialize the position stream provider. - + Args: ros_node: ROS node to use for subscriptions odometry_topic: Name of the odometry topic (if use_odometry is True) @@ -61,108 +59,103 @@ def __init__( self.odometry_topic = odometry_topic self.pose_topic = pose_topic self.use_odometry = use_odometry - + self._subject = Subject() - + self.last_position = None self.last_update_time = None - + self._create_subscription() - - logger.info(f"PositionStreamProvider initialized with " - f"{'odometry topic' if use_odometry else 'pose topic'}: " - f"{odometry_topic if use_odometry else pose_topic}") - + + logger.info( + f"PositionStreamProvider initialized with " + f"{'odometry topic' if use_odometry else 'pose topic'}: " + f"{odometry_topic if use_odometry else pose_topic}" + ) + def _create_subscription(self): """Create the appropriate ROS subscription based on configuration.""" if self.use_odometry: self.subscription = self.ros_node.create_subscription( - Odometry, - self.odometry_topic, - self._odometry_callback, - 10 + Odometry, self.odometry_topic, self._odometry_callback, 10 ) logger.info(f"Subscribed to odometry topic: {self.odometry_topic}") else: if not self.pose_topic: raise ValueError("Pose topic must be specified when use_odometry is False") - + self.subscription = self.ros_node.create_subscription( - PoseStamped, - self.pose_topic, - self._pose_callback, - 10 + PoseStamped, self.pose_topic, self._pose_callback, 10 ) logger.info(f"Subscribed to pose topic: {self.pose_topic}") - - - def _odometry_callback(self, msg: Odometry): + + def _odometry_callback(self, msg: Odometry) -> None: """ Process odometry messages and extract position. - + Args: msg: Odometry message from ROS """ x = msg.pose.pose.position.x y = msg.pose.pose.position.y - + self._update_position(x, y) - - def _pose_callback(self, msg: PoseStamped): + + def _pose_callback(self, msg: PoseStamped) -> None: """ Process pose messages and extract position. - + Args: msg: PoseStamped message from ROS """ x = msg.pose.position.x y = msg.pose.position.y - + self._update_position(x, y) - - def _update_position(self, x: float, y: float): + + def _update_position(self, x: float, y: float) -> None: """ Update the current position and emit to subscribers. - + Args: x: X coordinate y: Y coordinate """ current_time = time.time() position = (x, y) - + if self.last_update_time: update_rate = 1.0 / (current_time - self.last_update_time) logger.debug(f"Position update rate: {update_rate:.1f} Hz") - + self.last_position = position self.last_update_time = current_time - + self._subject.on_next(position) logger.debug(f"Position updated: ({x:.2f}, {y:.2f})") - + def get_position_stream(self) -> Observable: """ Get an Observable stream of position updates. - + Returns: Observable that emits (x, y) tuples """ return self._subject.pipe( ops.share() # Share the stream among multiple subscribers ) - - def get_current_position(self) -> Optional[Tuple[float, float]]: + + def get_current_position(self) -> tuple[float, float] | None: """ Get the most recent position. - + Returns: Tuple of (x, y) coordinates, or None if no position has been received """ return self.last_position - - def cleanup(self): + + def cleanup(self) -> None: """Clean up resources.""" - if hasattr(self, 'subscription') and self.subscription: + if hasattr(self, "subscription") and self.subscription: self.ros_node.destroy_subscription(self.subscription) logger.info("Position subscription destroyed") diff --git a/dimos/robot/recorder.py b/dimos/robot/recorder.py index 8f5ccc9218..acc9c0140e 100644 --- a/dimos/robot/recorder.py +++ b/dimos/robot/recorder.py @@ -14,12 +14,14 @@ # UNDER DEVELOPMENT 🚧🚧🚧, NEEDS TESTING +from collections.abc import Callable +from queue import Queue import threading import time -from queue import Queue -from typing import Any, Callable, Literal +from types import TracebackType +from typing import Literal -#from dimos.data.recording import Recorder +# from dimos.data.recording import Recorder class RobotRecorder: @@ -41,7 +43,7 @@ def __init__( get_observation: Callable, prepare_action: Callable, frequency_hz: int = 5, - recorder_kwargs: dict = None, + recorder_kwargs: dict | None = None, on_static: Literal["record", "omit"] = "omit", ) -> None: """Initializes the RobotRecorder. @@ -78,11 +80,16 @@ def __init__( self._worker_thread = threading.Thread(target=self._process_queue, daemon=True) self._worker_thread.start() - def __enter__(self): + def __enter__(self) -> None: """Enter the context manager, starting the recording.""" self.start_recording(self.task) - def __exit__(self, exc_type, exc_value, traceback) -> None: + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: """Exit the context manager, stopping the recording.""" self.stop_recording() @@ -125,7 +132,9 @@ def _process_queue(self) -> None: """Processes the recording queue asynchronously.""" while True: image, instruction, action, state = self.recording_queue.get() - self.recorder.record(observation={"image": image, "instruction": instruction}, action=action, state=state) + self.recorder.record( + observation={"image": image, "instruction": instruction}, action=action, state=state + ) self.recording_queue.task_done() def record_current_state(self) -> None: @@ -154,4 +163,4 @@ def record_current_state(self) -> None: def record_last_state(self) -> None: """Records the final pose and image after the movement completes.""" - self.record_current_state() \ No newline at end of file + self.record_current_state() diff --git a/dimos/robot/robot.py b/dimos/robot/robot.py index ba1eb3cf58..002dcb4710 100644 --- a/dimos/robot/robot.py +++ b/dimos/robot/robot.py @@ -12,354 +12,82 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Base module for all DIMOS robots. +"""Minimal robot interface for DIMOS robots.""" -This module provides the foundation for all DIMOS robots, including both physical -and simulated implementations, with common functionality for movement, control, -and video streaming. -""" +from abc import ABC, abstractmethod -from abc import ABC -import os -import logging -from typing import TYPE_CHECKING, Optional, Dict, Tuple, Any +from reactivex import Observable -import chromadb -from dimos.hardware.interface import HardwareInterface +from dimos.mapping.types import LatLon +from dimos.msgs.geometry_msgs import PoseStamped from dimos.perception.spatial_perception import SpatialMemory -from dimos.agents.memory.visual_memory import VisualMemory -from dimos.types.robot_location import RobotLocation -from dimos.utils.logging_config import setup_logger +from dimos.types.robot_capabilities import RobotCapability -if TYPE_CHECKING: - from dimos.robot.ros_control import ROSControl -else: - ROSControl = 'ROSControl' - -from dimos.skills.skills import SkillLibrary -from dimos.stream.frame_processor import FrameProcessor -from dimos.stream.video_operators import VideoOperators as vops -from reactivex import Observable, operators as ops -from reactivex.disposable import CompositeDisposable -from reactivex.scheduler import ThreadPoolScheduler - -from dimos.utils.threadpool import get_scheduler - -logger = setup_logger("dimos.robot.robot") +# TODO: Delete class Robot(ABC): - """Base class for all DIMOS robots. - - This abstract base class defines the common interface and functionality for all - DIMOS robots, whether physical or simulated. It provides methods for movement, - rotation, video streaming, and hardware configuration management. - - Attributes: - agent_config: Configuration for the robot's agent. - hardware_interface: Interface to the robot's hardware components. - ros_control: ROS-based control system for the robot. - output_dir: Directory for storing output files. - disposables: Collection of disposable resources for cleanup. - pool_scheduler: Thread pool scheduler for managing concurrent operations. - """ + """Minimal abstract base class for all DIMOS robots. - def __init__(self, - hardware_interface: HardwareInterface = None, - ros_control: ROSControl = None, - output_dir: str = os.path.join(os.getcwd(), "assets", "output"), - pool_scheduler: ThreadPoolScheduler = None, - skill_library: SkillLibrary = None, - spatial_memory_dir: str = None, - spatial_memory_collection: str = "spatial_memory", - new_memory: bool = False,): - """Initialize a Robot instance. - - Args: - hardware_interface: Interface to the robot's hardware. Defaults to None. - ros_control: ROS-based control system. Defaults to None. - output_dir: Directory for storing output files. Defaults to "./assets/output". - pool_scheduler: Thread pool scheduler. If None, one will be created. - skill_library: Skill library instance. If None, one will be created. - spatial_memory_dir: Directory for storing spatial memory data. If None, uses output_dir/spatial_memory. - spatial_memory_collection: Name of the collection in the ChromaDB database. - new_memory: If True, creates a new spatial memory from scratch. Defaults to False. - """ - self.hardware_interface = hardware_interface - self.ros_control = ros_control - self.output_dir = output_dir - self.disposables = CompositeDisposable() - self.pool_scheduler = pool_scheduler if pool_scheduler else get_scheduler() - self.skill_library = skill_library if skill_library else SkillLibrary() - - # Create output directory if it doesn't exist - os.makedirs(self.output_dir, exist_ok=True) - - # Create output directory if it doesn't exist - logger.info(f"Robot outputs will be saved to: {self.output_dir}") - - # Initialize spatial memory properties - self.spatial_memory_dir = spatial_memory_dir or os.path.join(self.output_dir, "spatial_memory") - self.spatial_memory_collection = spatial_memory_collection - self.db_path = os.path.join(self.spatial_memory_dir, "chromadb_data") - self.visual_memory_path = os.path.join(self.spatial_memory_dir, "visual_memory.pkl") - - # Create spatial memory directory - os.makedirs(self.spatial_memory_dir, exist_ok=True) - os.makedirs(self.db_path, exist_ok=True) - - # Import SpatialMemory here to avoid circular imports - from dimos.perception.spatial_perception import SpatialMemory - - # Initialize spatial memory - this will be handled by SpatialMemory class - video_stream = None - transform_provider = None - - # Only create video stream if ROS control is available - if self.ros_control is not None and self.ros_control.video_provider is not None: - # Get video stream - video_stream = self.get_ros_video_stream(fps=10) # Lower FPS for processing - - # Define transform provider - def transform_provider(): - position, rotation = self.ros_control.transform_euler("base_link") - if position is None or rotation is None: - return { - "position": None, - "rotation": None - } - return { - "position": position, - "rotation": rotation - } - - # Create SpatialMemory instance - it will handle all initialization internally - self._spatial_memory = SpatialMemory( - collection_name=self.spatial_memory_collection, - db_path=self.db_path, - visual_memory_path=self.visual_memory_path, - new_memory=new_memory, - output_dir=self.spatial_memory_dir, - video_stream=video_stream, - transform_provider=transform_provider - ) - - def get_ros_video_stream(self, fps: int = 30) -> Observable: - """Get the ROS video stream with rate limiting and frame processing. - - Args: - fps: Frames per second for the video stream. Defaults to 30. - - Returns: - Observable: An observable stream of video frames. - - Raises: - RuntimeError: If no ROS video provider is available. - """ - if not self.ros_control or not self.ros_control.video_provider: - raise RuntimeError("No ROS video provider available") - - print(f"Starting ROS video stream at {fps} FPS...") - - # Get base stream from video provider - video_stream = self.ros_control.video_provider.capture_video_as_observable( - fps=fps) + This class provides the essential interface that all robot implementations + can share, with no required methods - just common properties and helpers. + """ - # Add minimal processing pipeline with proper thread handling - processed_stream = video_stream.pipe( - ops.subscribe_on(self.pool_scheduler), - ops.observe_on(self.pool_scheduler), # Ensure thread safety - ops.share() # Share the stream - ) + def __init__(self) -> None: + """Initialize the robot with basic properties.""" + self.capabilities: list[RobotCapability] = [] + self.skill_library = None - return processed_stream + def has_capability(self, capability: RobotCapability) -> bool: + """Check if the robot has a specific capability. - def move(self, distance: float, speed: float = 0.5) -> bool: - """Move the robot using velocity commands. - - DEPRECATED: Use move_vel instead for direct velocity control. - Args: - distance: Distance to move forward in meters (must be positive). - speed: Speed to move at in m/s. Defaults to 0.5. - - Returns: - bool: True if movement succeeded. - - Raises: - RuntimeError: If no ROS control interface is available. - """ - pass + capability: The capability to check for - def reverse(self, distance: float, speed: float = 0.5) -> bool: - """Move the robot backward by a specified distance. - - DEPRECATED: Use move_vel with negative x value instead for direct velocity control. - - Args: - distance: Distance to move backward in meters (must be positive). - speed: Speed to move at in m/s. Defaults to 0.5. - Returns: - bool: True if movement succeeded. - - Raises: - RuntimeError: If no ROS control interface is available. + bool: True if the robot has the capability """ - pass + return capability in self.capabilities - def spin(self, degrees: float, speed: float = 45.0) -> bool: - """Rotate the robot by a specified angle. + def get_skills(self): + """Get the robot's skill library. - Args: - degrees: Angle to rotate in degrees (positive for counter-clockwise, - negative for clockwise). - speed: Angular speed in degrees/second. Defaults to 45.0. - Returns: - bool: True if rotation succeeded. - - Raises: - RuntimeError: If no ROS control interface is available. + The robot's skill library for managing skills """ - if self.ros_control is None: - raise RuntimeError( - "No ROS control interface available for rotation") - return self.ros_control.spin(degrees, speed) + return self.skill_library - def webrtc_req(self, api_id: int, topic: str = None, parameter: str = '', - priority: int = 0, request_id: str = None, data=None, timeout: float = 1000.0) -> bool: - """Send a WebRTC request command to the robot. - - Args: - api_id: The API ID for the command. - topic: The API topic to publish to. Defaults to ROSControl.webrtc_api_topic. - parameter: Optional parameter string. Defaults to ''. - priority: Priority level as defined by PriorityQueue(). Defaults to 0 (no priority). - data: Optional data dictionary. - timeout: Maximum time to wait for the command to complete. - - Returns: - bool: True if command was sent successfully. - - Raises: - RuntimeError: If no ROS control interface is available. - - """ - if self.ros_control is None: - raise RuntimeError("No ROS control interface available for WebRTC commands") - return self.ros_control.queue_webrtc_req( - api_id=api_id, - topic=topic, - parameter=parameter, - priority=priority, - request_id=request_id, - data=data, - timeout=timeout - ) + def cleanup(self) -> None: + """Clean up robot resources. - def move_vel(self, x: float, y: float, yaw: float, duration: float = 0.0) -> bool: - """Move the robot using direct movement commands. - - Args: - x: Forward/backward velocity (m/s) - y: Left/right velocity (m/s) - yaw: Rotational velocity (rad/s) - duration: How long to move (seconds). If 0, command is continuous - - Returns: - bool: True if command was sent successfully - - Raises: - RuntimeError: If no ROS control interface is available. - """ - if self.ros_control is None: - raise RuntimeError("No ROS control interface available for movement") - return self.ros_control.move_vel(x, y, yaw, duration) - - def pose_command(self, roll: float, pitch: float, yaw: float) -> bool: - """Send a pose command to the robot. - - Args: - roll: Roll angle in radians. - pitch: Pitch angle in radians. - yaw: Yaw angle in radians. - - Returns: - bool: True if command was sent successfully. - - Raises: - RuntimeError: If no ROS control interface is available. + Override this method to provide cleanup logic. """ - if self.ros_control is None: - raise RuntimeError("No ROS control interface available for pose commands") - return self.ros_control.pose_command(roll, pitch, yaw) + pass - def update_hardware_interface(self, - new_hardware_interface: HardwareInterface): - """Update the hardware interface with a new configuration. - - Args: - new_hardware_interface: New hardware interface to use for the robot. - """ - self.hardware_interface = new_hardware_interface - def get_hardware_configuration(self): - """Retrieve the current hardware configuration. - - Returns: - The current hardware configuration from the hardware interface. - - Raises: - AttributeError: If hardware_interface is None. - """ - return self.hardware_interface.get_configuration() +# TODO: Delete +class UnitreeRobot(Robot): + @abstractmethod + def get_odom(self) -> PoseStamped: ... - def set_hardware_configuration(self, configuration): - """Set a new hardware configuration. - - Args: - configuration: The new hardware configuration to set. - - Raises: - AttributeError: If hardware_interface is None. - """ - self.hardware_interface.set_configuration(configuration) + @abstractmethod + def explore(self) -> bool: ... + @abstractmethod + def stop_exploration(self) -> bool: ... - + @abstractmethod + def is_exploration_active(self) -> bool: ... - - def get_spatial_memory(self) -> Optional[SpatialMemory]: - """Simple getter for the spatial memory instance. - - Returns: - The spatial memory instance or None if not set. - """ - return self._spatial_memory if self._spatial_memory else None - + @property + @abstractmethod + def spatial_memory(self) -> SpatialMemory | None: ... - - def cleanup(self): - """Clean up resources used by the robot. - - This method should be called when the robot is no longer needed to - ensure proper release of resources such as ROS connections and - subscriptions. - """ - # Dispose of resources - if self.disposables: - self.disposables.dispose() - - if self.ros_control: - self.ros_control.cleanup() - self.disposables.dispose() -class MockRobot(Robot): - def __init__(self): - super().__init__() - self.ros_control = None - self.hardware_interface = None - self.skill_library = SkillLibrary() +# TODO: Delete +class GpsRobot(ABC): + @property + @abstractmethod + def gps_position_stream(self) -> Observable[LatLon]: ... - def my_print(self): - print("Hello, world!") \ No newline at end of file + @abstractmethod + def set_gps_travel_goal_points(self, points: list[LatLon]) -> None: ... diff --git a/dimos/robot/ros_bridge.py b/dimos/robot/ros_bridge.py new file mode 100644 index 0000000000..b067f88a22 --- /dev/null +++ b/dimos/robot/ros_bridge.py @@ -0,0 +1,205 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from enum import Enum +import logging +import threading +from typing import Any + +try: + import rclpy + from rclpy.executors import SingleThreadedExecutor + from rclpy.node import Node + from rclpy.qos import QoSDurabilityPolicy, QoSHistoryPolicy, QoSProfile, QoSReliabilityPolicy +except ImportError: + rclpy = None + SingleThreadedExecutor = None + Node = None + QoSProfile = None + QoSReliabilityPolicy = None + QoSHistoryPolicy = None + QoSDurabilityPolicy = None + +from dimos.core.resource import Resource +from dimos.protocol.pubsub.lcmpubsub import LCM, Topic +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.robot.ros_bridge", level=logging.INFO) + + +class BridgeDirection(Enum): + """Direction of message bridging.""" + + ROS_TO_DIMOS = "ros_to_dimos" + DIMOS_TO_ROS = "dimos_to_ros" + + +class ROSBridge(Resource): + """Unidirectional bridge between ROS and DIMOS for message passing.""" + + def __init__(self, node_name: str = "dimos_ros_bridge") -> None: + """Initialize the ROS-DIMOS bridge. + + Args: + node_name: Name for the ROS node (default: "dimos_ros_bridge") + """ + if not rclpy.ok(): + rclpy.init() + + self.node = Node(node_name) + self.lcm = LCM() + self.lcm.start() + + self._executor = SingleThreadedExecutor() + self._executor.add_node(self.node) + + self._spin_thread = threading.Thread(target=self._ros_spin, daemon=True) + self._spin_thread.start() # TODO: don't forget to shut it down + + self._bridges: dict[str, dict[str, Any]] = {} + + self._qos = QoSProfile( + reliability=QoSReliabilityPolicy.RELIABLE, + history=QoSHistoryPolicy.KEEP_LAST, + durability=QoSDurabilityPolicy.VOLATILE, + depth=10, + ) + + logger.info(f"ROSBridge initialized with node name: {node_name}") + + def start(self) -> None: + pass + + def stop(self) -> None: + """Shutdown the bridge and clean up resources.""" + self._executor.shutdown() + self.node.destroy_node() + + if rclpy.ok(): + rclpy.shutdown() + + logger.info("ROSBridge shutdown complete") + + def _ros_spin(self) -> None: + """Background thread for spinning ROS executor.""" + try: + self._executor.spin() + finally: + self._executor.shutdown() + + def add_topic( + self, + topic_name: str, + dimos_type: type, + ros_type: type, + direction: BridgeDirection, + remap_topic: str | None = None, + ) -> None: + """Add unidirectional bridging for a topic. + + Args: + topic_name: Name of the topic (e.g., "/cmd_vel") + dimos_type: DIMOS message type (e.g., dimos.msgs.geometry_msgs.Twist) + ros_type: ROS message type (e.g., geometry_msgs.msg.Twist) + direction: Direction of bridging (ROS_TO_DIMOS or DIMOS_TO_ROS) + remap_topic: Optional remapped topic name for the other side + """ + if topic_name in self._bridges: + logger.warning(f"Topic {topic_name} already bridged") + return + + # Determine actual topic names for each side + ros_topic_name = topic_name + dimos_topic_name = topic_name + + if remap_topic: + if direction == BridgeDirection.ROS_TO_DIMOS: + dimos_topic_name = remap_topic + else: # DIMOS_TO_ROS + ros_topic_name = remap_topic + + # Create DIMOS/LCM topic + dimos_topic = Topic(dimos_topic_name, dimos_type) + + ros_subscription = None + ros_publisher = None + dimos_subscription = None + + if direction == BridgeDirection.ROS_TO_DIMOS: + + def ros_callback(msg) -> None: + self._ros_to_dimos(msg, dimos_topic, dimos_type, topic_name) + + ros_subscription = self.node.create_subscription( + ros_type, ros_topic_name, ros_callback, self._qos + ) + logger.info(f" ROS → DIMOS: Subscribing to ROS topic {ros_topic_name}") + + elif direction == BridgeDirection.DIMOS_TO_ROS: + ros_publisher = self.node.create_publisher(ros_type, ros_topic_name, self._qos) + + def dimos_callback(msg, _topic) -> None: + self._dimos_to_ros(msg, ros_publisher, topic_name) + + dimos_subscription = self.lcm.subscribe(dimos_topic, dimos_callback) + logger.info(f" DIMOS → ROS: Subscribing to DIMOS topic {dimos_topic_name}") + else: + raise ValueError(f"Invalid bridge direction: {direction}") + + self._bridges[topic_name] = { + "dimos_topic": dimos_topic, + "dimos_type": dimos_type, + "ros_type": ros_type, + "ros_subscription": ros_subscription, + "ros_publisher": ros_publisher, + "dimos_subscription": dimos_subscription, + "direction": direction, + "ros_topic_name": ros_topic_name, + "dimos_topic_name": dimos_topic_name, + } + + direction_str = { + BridgeDirection.ROS_TO_DIMOS: "ROS → DIMOS", + BridgeDirection.DIMOS_TO_ROS: "DIMOS → ROS", + }[direction] + + logger.info(f"Bridged topic: {topic_name} ({direction_str})") + if remap_topic: + logger.info(f" Remapped: ROS '{ros_topic_name}' ↔ DIMOS '{dimos_topic_name}'") + logger.info(f" DIMOS type: {dimos_type.__name__}, ROS type: {ros_type.__name__}") + + def _ros_to_dimos( + self, ros_msg: Any, dimos_topic: Topic, dimos_type: type, _topic_name: str + ) -> None: + """Convert ROS message to DIMOS and publish. + + Args: + ros_msg: ROS message + dimos_topic: DIMOS topic to publish to + dimos_type: DIMOS message type + topic_name: Name of the topic for tracking + """ + dimos_msg = dimos_type.from_ros_msg(ros_msg) + self.lcm.publish(dimos_topic, dimos_msg) + + def _dimos_to_ros(self, dimos_msg: Any, ros_publisher, _topic_name: str) -> None: + """Convert DIMOS message to ROS and publish. + + Args: + dimos_msg: DIMOS message + ros_publisher: ROS publisher to use + _topic_name: Name of the topic (unused, kept for consistency) + """ + ros_msg = dimos_msg.to_ros_msg() + ros_publisher.publish(ros_msg) diff --git a/dimos/robot/ros_command_queue.py b/dimos/robot/ros_command_queue.py index ca69e2419c..770f44e1a6 100644 --- a/dimos/robot/ros_command_queue.py +++ b/dimos/robot/ros_command_queue.py @@ -20,25 +20,30 @@ Commands are processed sequentially and only when the robot is in IDLE state. """ +from collections.abc import Callable +from enum import Enum, auto +from queue import Empty, PriorityQueue import threading import time +from typing import Any, NamedTuple import uuid -from enum import Enum, auto -from queue import PriorityQueue, Empty -from typing import Callable, Optional, NamedTuple, Dict, Any, Tuple, List + from dimos.utils.logging_config import setup_logger -import logging # Initialize logger for the ros command queue module logger = setup_logger("dimos.robot.ros_command_queue") + class CommandType(Enum): """Types of commands that can be queued""" + WEBRTC = auto() # WebRTC API requests - ACTION = auto() # Any action client or function call + ACTION = auto() # Any action client or function call + class WebRTCRequest(NamedTuple): """Class to represent a WebRTC request in the queue""" + id: str # Unique ID for tracking api_id: int # API ID for the command topic: str # Topic to publish to @@ -46,31 +51,36 @@ class WebRTCRequest(NamedTuple): priority: int # Priority level timeout: float # How long to wait for this request to complete + class ROSCommand(NamedTuple): """Class to represent a command in the queue""" + id: str # Unique ID for tracking cmd_type: CommandType # Type of command execute_func: Callable # Function to execute the command - params: Dict[str, Any] # Parameters for the command (for debugging/logging) + params: dict[str, Any] # Parameters for the command (for debugging/logging) priority: int # Priority level (lower is higher priority) timeout: float # How long to wait for this command to complete + class ROSCommandQueue: """ Manages a queue of commands for the robot. - + Commands are executed sequentially, with only one command being processed at a time. Commands are only executed when the robot is in the IDLE state. """ - - def __init__(self, - webrtc_func: Callable, - is_ready_func: Callable[[], bool] = None, - is_busy_func: Optional[Callable[[], bool]] = None, - debug: bool = True): + + def __init__( + self, + webrtc_func: Callable, + is_ready_func: Callable[[], bool] | None = None, + is_busy_func: Callable[[], bool] | None = None, + debug: bool = True, + ) -> None: """ Initialize the ROSCommandQueue. - + Args: webrtc_func: Function to send WebRTC requests is_ready_func: Function to check if the robot is ready for a command @@ -81,53 +91,55 @@ def __init__(self, self._is_ready_func = is_ready_func or (lambda: True) self._is_busy_func = is_busy_func self._debug = debug - + # Queue of commands to process self._queue = PriorityQueue() self._current_command = None self._last_command_time = 0 - + # Last known robot state self._last_ready_state = None self._last_busy_state = None self._stuck_in_busy_since = None - + # Command execution status self._should_stop = False self._queue_thread = None - + # Stats self._command_count = 0 self._success_count = 0 self._failure_count = 0 self._command_history = [] - - self._max_queue_wait_time = 30.0 # Maximum time to wait for robot to be ready before forcing - + + self._max_queue_wait_time = ( + 30.0 # Maximum time to wait for robot to be ready before forcing + ) + logger.info("ROSCommandQueue initialized") - - def start(self): + + def start(self) -> None: """Start the queue processing thread""" if self._queue_thread is not None and self._queue_thread.is_alive(): logger.warning("Queue processing thread already running") return - + self._should_stop = False self._queue_thread = threading.Thread(target=self._process_queue, daemon=True) self._queue_thread.start() logger.info("Queue processing thread started") - - def stop(self, timeout=2.0): + + def stop(self, timeout: float = 2.0) -> None: """ Stop the queue processing thread - + Args: timeout: Maximum time to wait for the thread to stop """ if self._queue_thread is None or not self._queue_thread.is_alive(): logger.warning("Queue processing thread not running") return - + self._should_stop = True try: self._queue_thread.join(timeout=timeout) @@ -137,13 +149,20 @@ def stop(self, timeout=2.0): logger.info("Queue processing thread stopped") except Exception as e: logger.error(f"Error stopping queue processing thread: {e}") - - def queue_webrtc_request(self, api_id: int, topic: str = None, parameter: str = '', - request_id: str = None, data: Dict[str, Any] = None, - priority: int = 0, timeout: float = 30.0) -> str: + + def queue_webrtc_request( + self, + api_id: int, + topic: str | None = None, + parameter: str = "", + request_id: str | None = None, + data: dict[str, Any] | None = None, + priority: int = 0, + timeout: float = 30.0, + ) -> str: """ Queue a WebRTC request - + Args: api_id: API ID for the command topic: Topic to publish to @@ -152,21 +171,21 @@ def queue_webrtc_request(self, api_id: int, topic: str = None, parameter: str = data: Data to include in the request priority: Priority level (lower is higher priority) timeout: Maximum time to wait for the command to complete - + Returns: str: Unique ID for the request """ request_id = request_id or str(uuid.uuid4()) - + # Create a function that will execute this WebRTC request - def execute_webrtc(): + def execute_webrtc() -> bool: try: logger.info(f"Executing WebRTC request: {api_id} (ID: {request_id})") if self._debug: logger.debug(f"[WebRTC Queue] SENDING request: API ID {api_id}") - + result = self._webrtc_func( - api_id=api_id, + api_id=api_id, topic=topic, parameter=parameter, request_id=request_id, @@ -177,30 +196,36 @@ def execute_webrtc(): if self._debug: logger.debug(f"[WebRTC Queue] Request API ID {api_id} FAILED to send") return False - + if self._debug: logger.debug(f"[WebRTC Queue] Request API ID {api_id} sent SUCCESSFULLY") - + # Allow time for the robot to process the command start_time = time.time() stabilization_delay = 0.5 # Half-second delay for stabilization time.sleep(stabilization_delay) - + # Wait for the robot to complete the command (timeout check) while self._is_busy_func() and (time.time() - start_time) < timeout: - if self._debug and (time.time() - start_time) % 5 < 0.1: # Print every ~5 seconds - logger.debug(f"[WebRTC Queue] Still waiting on API ID {api_id} - elapsed: {time.time()-start_time:.1f}s") + if ( + self._debug and (time.time() - start_time) % 5 < 0.1 + ): # Print every ~5 seconds + logger.debug( + f"[WebRTC Queue] Still waiting on API ID {api_id} - elapsed: {time.time() - start_time:.1f}s" + ) time.sleep(0.1) - + # Check if we timed out if self._is_busy_func() and (time.time() - start_time) >= timeout: logger.warning(f"WebRTC request timed out: {api_id} (ID: {request_id})") return False - + wait_time = time.time() - start_time if self._debug: - logger.debug(f"[WebRTC Queue] Request API ID {api_id} completed after {wait_time:.1f}s") - + logger.debug( + f"[WebRTC Queue] Request API ID {api_id} completed after {wait_time:.1f}s" + ) + logger.info(f"WebRTC request completed: {api_id} (ID: {request_id})") return True except Exception as e: @@ -208,201 +233,224 @@ def execute_webrtc(): if self._debug: logger.debug(f"[WebRTC Queue] ERROR processing request: {e}") return False - + # Create the command and queue it command = ROSCommand( id=request_id, cmd_type=CommandType.WEBRTC, execute_func=execute_webrtc, - params={'api_id': api_id, 'topic': topic, 'request_id': request_id}, + params={"api_id": api_id, "topic": topic, "request_id": request_id}, priority=priority, - timeout=timeout + timeout=timeout, ) - + # Queue the command self._queue.put((priority, self._command_count, command)) self._command_count += 1 if self._debug: - logger.debug(f"[WebRTC Queue] Added request ID {request_id} for API ID {api_id} - Queue size now: {self.queue_size}") + logger.debug( + f"[WebRTC Queue] Added request ID {request_id} for API ID {api_id} - Queue size now: {self.queue_size}" + ) logger.info(f"Queued WebRTC request: {api_id} (ID: {request_id}, Priority: {priority})") - + return request_id - - def queue_action_client_request(self, action_name: str, execute_func: Callable, - priority: int = 0, timeout: float = 30.0, **kwargs) -> str: + + def queue_action_client_request( + self, + action_name: str, + execute_func: Callable, + priority: int = 0, + timeout: float = 30.0, + **kwargs, + ) -> str: """ Queue any action client request or function - + Args: action_name: Name of the action for logging/tracking execute_func: Function to execute the command priority: Priority level (lower is higher priority) timeout: Maximum time to wait for the command to complete **kwargs: Additional parameters to pass to the execute function - + Returns: str: Unique ID for the request """ request_id = str(uuid.uuid4()) - + # Create the command command = ROSCommand( id=request_id, cmd_type=CommandType.ACTION, execute_func=execute_func, - params={'action_name': action_name, **kwargs}, + params={"action_name": action_name, **kwargs}, priority=priority, - timeout=timeout + timeout=timeout, ) - + # Queue the command self._queue.put((priority, self._command_count, command)) self._command_count += 1 - - action_params = ', '.join([f"{k}={v}" for k, v in kwargs.items()]) - logger.info(f"Queued action request: {action_name} (ID: {request_id}, Priority: {priority}, Params: {action_params})") - + + action_params = ", ".join([f"{k}={v}" for k, v in kwargs.items()]) + logger.info( + f"Queued action request: {action_name} (ID: {request_id}, Priority: {priority}, Params: {action_params})" + ) + return request_id - - def _process_queue(self): + + def _process_queue(self) -> None: """Process commands in the queue""" logger.info("Starting queue processing") logger.info("[WebRTC Queue] Processing thread started") - + while not self._should_stop: # Print queue status self._print_queue_status() - + # Check if we're ready to process a command if not self._queue.empty() and self._current_command is None: current_time = time.time() is_ready = self._is_ready_func() is_busy = self._is_busy_func() if self._is_busy_func else False - + if self._debug: - logger.debug(f"[WebRTC Queue] Status: {self.queue_size} requests waiting | Robot ready: {is_ready} | Robot busy: {is_busy}") - + logger.debug( + f"[WebRTC Queue] Status: {self.queue_size} requests waiting | Robot ready: {is_ready} | Robot busy: {is_busy}" + ) + # Track robot state changes if is_ready != self._last_ready_state: - logger.debug(f"Robot ready state changed: {self._last_ready_state} -> {is_ready}") + logger.debug( + f"Robot ready state changed: {self._last_ready_state} -> {is_ready}" + ) self._last_ready_state = is_ready - + if is_busy != self._last_busy_state: logger.debug(f"Robot busy state changed: {self._last_busy_state} -> {is_busy}") self._last_busy_state = is_busy - + # If the robot has transitioned to busy, record the time if is_busy: self._stuck_in_busy_since = current_time else: self._stuck_in_busy_since = None - + # Check if we've been waiting too long for the robot to be ready force_processing = False - if (not is_ready and is_busy and - self._stuck_in_busy_since is not None and - current_time - self._stuck_in_busy_since > self._max_queue_wait_time): + if ( + not is_ready + and is_busy + and self._stuck_in_busy_since is not None + and current_time - self._stuck_in_busy_since > self._max_queue_wait_time + ): logger.warning( f"Robot has been busy for {current_time - self._stuck_in_busy_since:.1f}s, " f"forcing queue to continue" ) force_processing = True - + # Process the next command if ready or forcing if is_ready or force_processing: if self._debug and is_ready: logger.debug("[WebRTC Queue] Robot is READY for next command") - + try: # Get the next command _, _, command = self._queue.get(block=False) self._current_command = command self._last_command_time = current_time - + # Log the command cmd_info = f"ID: {command.id}, Type: {command.cmd_type.name}" if command.cmd_type == CommandType.WEBRTC: - api_id = command.params.get('api_id') + api_id = command.params.get("api_id") cmd_info += f", API: {api_id}" if self._debug: logger.debug(f"[WebRTC Queue] DEQUEUED request: API ID {api_id}") elif command.cmd_type == CommandType.ACTION: - action_name = command.params.get('action_name') + action_name = command.params.get("action_name") cmd_info += f", Action: {action_name}" if self._debug: logger.debug(f"[WebRTC Queue] DEQUEUED action: {action_name}") - + forcing_str = " (FORCED)" if force_processing else "" logger.info(f"Processing command{forcing_str}: {cmd_info}") - + # Execute the command try: # Where command execution occurs success = command.execute_func() - + if success: self._success_count += 1 logger.info(f"Command succeeded: {cmd_info}") if self._debug: - logger.debug(f"[WebRTC Queue] Command {command.id} marked as COMPLETED") + logger.debug( + f"[WebRTC Queue] Command {command.id} marked as COMPLETED" + ) else: self._failure_count += 1 logger.warning(f"Command failed: {cmd_info}") if self._debug: logger.debug(f"[WebRTC Queue] Command {command.id} FAILED") - + # Record command history - self._command_history.append({ - 'id': command.id, - 'type': command.cmd_type.name, - 'params': command.params, - 'success': success, - 'time': time.time() - self._last_command_time - }) - + self._command_history.append( + { + "id": command.id, + "type": command.cmd_type.name, + "params": command.params, + "success": success, + "time": time.time() - self._last_command_time, + } + ) + except Exception as e: self._failure_count += 1 logger.error(f"Error executing command: {e}") if self._debug: logger.debug(f"[WebRTC Queue] ERROR executing command: {e}") - + # Mark the command as complete self._current_command = None if self._debug: - logger.debug("[WebRTC Queue] Adding 0.5s stabilization delay before next command") + logger.debug( + "[WebRTC Queue] Adding 0.5s stabilization delay before next command" + ) time.sleep(0.5) - + except Empty: pass - + # Sleep to avoid busy-waiting time.sleep(0.1) - + logger.info("Queue processing stopped") - - def _print_queue_status(self): + + def _print_queue_status(self) -> None: """Print the current queue status""" current_time = time.time() - + # Only print once per second to avoid spamming the log if current_time - self._last_command_time < 1.0 and self._current_command is None: return - + is_ready = self._is_ready_func() - is_busy = self._is_busy_func() if self._is_busy_func else False + self._is_busy_func() if self._is_busy_func else False queue_size = self.queue_size - + # Get information about the current command current_command_info = "None" if self._current_command is not None: current_command_info = f"{self._current_command.cmd_type.name}" if self._current_command.cmd_type == CommandType.WEBRTC: - api_id = self._current_command.params.get('api_id') + api_id = self._current_command.params.get("api_id") current_command_info += f" (API: {api_id})" elif self._current_command.cmd_type == CommandType.ACTION: - action_name = self._current_command.params.get('action_name') + action_name = self._current_command.params.get("action_name") current_command_info += f" (Action: {action_name})" - + # Print the status status = ( f"Queue: {queue_size} items | " @@ -410,17 +458,16 @@ def _print_queue_status(self): f"Current: {current_command_info} | " f"Stats: {self._success_count} OK, {self._failure_count} FAIL" ) - + logger.debug(status) self._last_command_time = current_time - + @property def queue_size(self) -> int: """Get the number of commands in the queue""" return self._queue.qsize() - + @property - def current_command(self) -> Optional[ROSCommand]: + def current_command(self) -> ROSCommand | None: """Get the current command being processed""" return self._current_command - \ No newline at end of file diff --git a/dimos/robot/ros_control.py b/dimos/robot/ros_control.py index 13f90d4c2e..2e9eb95204 100644 --- a/dimos/robot/ros_control.py +++ b/dimos/robot/ros_control.py @@ -12,40 +12,38 @@ # See the License for the specific language governing permissions and # limitations under the License. -import rclpy -from rclpy.node import Node -from rclpy.executors import MultiThreadedExecutor -from rclpy.action import ActionClient -from geometry_msgs.msg import Twist -from nav2_msgs.action import Spin - -from sensor_msgs.msg import Image, CompressedImage -from cv_bridge import CvBridge +from abc import ABC, abstractmethod from enum import Enum, auto +import math import threading import time -from typing import Optional, Tuple, Dict, Any, Type -from abc import ABC, abstractmethod +from typing import Any + +from builtin_interfaces.msg import Duration +from cv_bridge import CvBridge +from geometry_msgs.msg import Point, Twist, Vector3 +from nav2_msgs.action import Spin +from nav_msgs.msg import OccupancyGrid, Odometry +import rclpy +from rclpy.action import ActionClient +from rclpy.executors import MultiThreadedExecutor +from rclpy.node import Node from rclpy.qos import ( + QoSDurabilityPolicy, + QoSHistoryPolicy, QoSProfile, QoSReliabilityPolicy, - QoSHistoryPolicy, - QoSDurabilityPolicy, ) -from dimos.stream.ros_video_provider import ROSVideoProvider -import math -from builtin_interfaces.msg import Duration -from geometry_msgs.msg import Point, Vector3, Twist -from dimos.robot.ros_command_queue import ROSCommandQueue -from dimos.utils.logging_config import setup_logger - -from nav_msgs.msg import OccupancyGrid - +from sensor_msgs.msg import CompressedImage, Image import tf2_ros -from dimos.robot.ros_transform import ROSTransformAbility -from dimos.robot.ros_observable_topic import ROSObservableTopicAbility -from nav_msgs.msg import Odometry +from dimos.robot.connection_interface import ConnectionInterface +from dimos.robot.ros_command_queue import ROSCommandQueue +from dimos.robot.ros_observable_topic import ROSObservableTopicAbility +from dimos.robot.ros_transform import ROSTransformAbility +from dimos.stream.ros_video_provider import ROSVideoProvider +from dimos.types.vector import Vector +from dimos.utils.logging_config import setup_logger logger = setup_logger("dimos.robot.ros_control") @@ -61,27 +59,31 @@ class RobotMode(Enum): MOVING = auto() ERROR = auto() -class ROSControl(ROSTransformAbility, ROSObservableTopicAbility, ABC): + +class ROSControl(ROSTransformAbility, ROSObservableTopicAbility, ConnectionInterface, ABC): """Abstract base class for ROS-controlled robots""" - def __init__(self, - node_name: str, - camera_topics: Dict[str, str] = None, - max_linear_velocity: float = 1.0, - mock_connection: bool = False, - max_angular_velocity: float = 2.0, - state_topic: str = None, - imu_topic: str = None, - state_msg_type: Type = None, - imu_msg_type: Type = None, - webrtc_topic: str = None, - webrtc_api_topic: str = None, - webrtc_msg_type: Type = None, - move_vel_topic: str = None, - pose_topic: str = None, - odom_topic: str = '/odom', - global_costmap_topic: str = "map", - costmap_topic: str = '/local_costmap/costmap', - debug: bool = False): + + def __init__( + self, + node_name: str, + camera_topics: dict[str, str] | None = None, + max_linear_velocity: float = 1.0, + mock_connection: bool = False, + max_angular_velocity: float = 2.0, + state_topic: str | None = None, + imu_topic: str | None = None, + state_msg_type: type | None = None, + imu_msg_type: type | None = None, + webrtc_topic: str | None = None, + webrtc_api_topic: str | None = None, + webrtc_msg_type: type | None = None, + move_vel_topic: str | None = None, + pose_topic: str | None = None, + odom_topic: str = "/odom", + global_costmap_topic: str = "map", + costmap_topic: str = "/local_costmap/costmap", + debug: bool = False, + ) -> None: """ Initialize base ROS control interface Args: @@ -159,9 +161,7 @@ def __init__(self, ) self._subscriptions.append(self._global_costmap_sub) else: - logger.warning( - "No costmap topic provided - costmap data tracking will be unavailable" - ) + logger.warning("No costmap topic provided - costmap data tracking will be unavailable") # Initialize data handling self._video_provider = None @@ -204,25 +204,23 @@ def __init__(self, ) self._subscriptions.append(self._imu_sub) else: - logger.warning("No IMU topic and/or message type provided - IMU data tracking will be unavailable") - + logger.warning( + "No IMU topic and/or message type provided - IMU data tracking will be unavailable" + ) + if self._odom_topic: self._odom_sub = self._node.create_subscription( - Odometry, - self._odom_topic, - self._odom_callback, - sensor_qos + Odometry, self._odom_topic, self._odom_callback, sensor_qos ) self._subscriptions.append(self._odom_sub) else: - logger.warning("No odometry topic provided - odometry data tracking will be unavailable") - + logger.warning( + "No odometry topic provided - odometry data tracking will be unavailable" + ) + if self._costmap_topic: self._costmap_sub = self._node.create_subscription( - OccupancyGrid, - self._costmap_topic, - self._costmap_callback, - sensor_qos + OccupancyGrid, self._costmap_topic, self._costmap_callback, sensor_qos ) self._subscriptions.append(self._costmap_sub) else: @@ -236,9 +234,7 @@ def __init__(self, self._spin_client.wait_for_server() # Publishers - self._move_vel_pub = self._node.create_publisher( - Twist, move_vel_topic, command_qos - ) + self._move_vel_pub = self._node.create_publisher(Twist, move_vel_topic, command_qos) self._pose_pub = self._node.create_publisher(Vector3, pose_topic, command_qos) if webrtc_msg_type: @@ -256,12 +252,12 @@ def __init__(self, self._command_queue.start() else: logger.warning("No WebRTC message type provided - WebRTC commands will be unavailable") - + # Initialize TF Buffer and Listener for transform abilities self._tf_buffer = tf2_ros.Buffer() self._tf_listener = tf2_ros.TransformListener(self._tf_buffer, self._node) logger.info(f"TF Buffer and Listener initialized for {node_name}") - + # Start ROS spin in a background thread via the executor self._spin_thread = threading.Thread(target=self._ros_spin, daemon=True) self._spin_thread.start() @@ -269,7 +265,7 @@ def __init__(self, logger.info(f"{node_name} initialized with multi-threaded executor") print(f"{node_name} initialized with multi-threaded executor") - def get_global_costmap(self) -> Optional[OccupancyGrid]: + def get_global_costmap(self) -> OccupancyGrid | None: """ Get current global_costmap data @@ -287,25 +283,25 @@ def get_global_costmap(self) -> Optional[OccupancyGrid]: else: return None - def _global_costmap_callback(self, msg): + def _global_costmap_callback(self, msg) -> None: """Callback for costmap data""" self._global_costmap_data = msg - def _imu_callback(self, msg): + def _imu_callback(self, msg) -> None: """Callback for IMU data""" self._imu_state = msg # Log IMU state (very verbose) - #logger.debug(f"IMU state updated: {self._imu_state}") + # logger.debug(f"IMU state updated: {self._imu_state}") - def _odom_callback(self, msg): + def _odom_callback(self, msg) -> None: """Callback for odometry data""" self._odom_data = msg - - def _costmap_callback(self, msg): + + def _costmap_callback(self, msg) -> None: """Callback for costmap data""" self._costmap_data = msg - def _state_callback(self, msg): + def _state_callback(self, msg) -> None: """Callback for state messages to track mode and progress""" # Call the abstract method to update RobotMode enum based on the received state @@ -315,11 +311,11 @@ def _state_callback(self, msg): # logger.debug(f"Robot state updated: {self._robot_state}") @property - def robot_state(self) -> Optional[Any]: + def robot_state(self) -> Any | None: """Get the full robot state message""" return self._robot_state - def _ros_spin(self): + def _ros_spin(self) -> None: """Background thread for spinning the multi-threaded executor.""" self._executor.add_node(self._node) try: @@ -336,7 +332,7 @@ def _update_mode(self, *args, **kwargs): """Update robot mode based on state - to be implemented by child classes""" pass - def get_state(self) -> Optional[Any]: + def get_state(self) -> Any | None: """ Get current robot state @@ -347,14 +343,12 @@ def get_state(self) -> Optional[Any]: ROS msg containing the robot state information """ if not self._state_topic: - logger.warning( - "No state topic provided - robot state tracking will be unavailable" - ) + logger.warning("No state topic provided - robot state tracking will be unavailable") return None return self._robot_state - def get_imu_state(self) -> Optional[Any]: + def get_imu_state(self) -> Any | None: """ Get current IMU state @@ -365,28 +359,28 @@ def get_imu_state(self) -> Optional[Any]: ROS msg containing the IMU state information """ if not self._imu_topic: - logger.warning( - "No IMU topic provided - IMU data tracking will be unavailable" - ) + logger.warning("No IMU topic provided - IMU data tracking will be unavailable") return None return self._imu_state - def get_odometry(self) -> Optional[Odometry]: + def get_odometry(self) -> Odometry | None: """ Get current odometry data - + Returns: Optional[Odometry]: Current odometry data or None if not available """ if not self._odom_topic: - logger.warning("No odometry topic provided - odometry data tracking will be unavailable") + logger.warning( + "No odometry topic provided - odometry data tracking will be unavailable" + ) return None return self._odom_data - - def get_costmap(self) -> Optional[OccupancyGrid]: + + def get_costmap(self) -> OccupancyGrid | None: """ Get current costmap data - + Returns: Optional[OccupancyGrid]: Current costmap data or None if not available """ @@ -394,8 +388,8 @@ def get_costmap(self) -> Optional[OccupancyGrid]: logger.warning("No costmap topic provided - costmap data tracking will be unavailable") return None return self._costmap_data - - def _image_callback(self, msg): + + def _image_callback(self, msg) -> None: """Convert ROS image to numpy array and push to data stream""" if self._video_provider and self._bridge: try: @@ -409,16 +403,30 @@ def _image_callback(self, msg): self._video_provider.push_data(frame) except Exception as e: logger.error(f"Error converting image: {e}") - print(f"Full conversion error: {str(e)}") + print(f"Full conversion error: {e!s}") @property - def video_provider(self) -> Optional[ROSVideoProvider]: + def video_provider(self) -> ROSVideoProvider | None: """Data provider property for streaming data""" return self._video_provider + def get_video_stream(self, fps: int = 30) -> Observable | None: + """Get the video stream from the robot's camera. + + Args: + fps: Frames per second for the video stream + + Returns: + Observable: An observable stream of video frames or None if not available + """ + if not self.video_provider: + return None + + return self.video_provider.get_stream(fps=fps) + def _send_action_client_goal( - self, client, goal_msg, description=None, time_allowance=20.0 - ): + self, client, goal_msg, description: str | None = None, time_allowance: float = 20.0 + ) -> bool: """ Generic function to send any action client goal and wait for completion. @@ -441,16 +449,12 @@ def _send_action_client_goal( self._action_success = None # Send the goal - send_goal_future = client.send_goal_async( - goal_msg, feedback_callback=lambda feedback: None - ) + send_goal_future = client.send_goal_async(goal_msg, feedback_callback=lambda feedback: None) send_goal_future.add_done_callback(self._goal_response_callback) # Wait for completion start_time = time.time() - while ( - self._action_success is None and time.time() - start_time < time_allowance - ): + while self._action_success is None and time.time() - start_time < time_allowance: time.sleep(0.1) elapsed = time.time() - start_time @@ -463,76 +467,55 @@ def _send_action_client_goal( logger.error(f"Action timed out after {time_allowance}s") return False elif self._action_success: - logger.info(f"Action succeeded") + logger.info("Action succeeded") return True else: - logger.error(f"Action failed") + logger.error("Action failed") return False - def move( - self, distance: float, speed: float = 0.5, time_allowance: float = 120 - ) -> bool: - """ - Move the robot forward by a specified distance + def move(self, velocity: Vector, duration: float = 0.0) -> bool: + """Send velocity commands to the robot. Args: - distance: Distance to move forward in meters (must be positive) - speed: Speed to move at in m/s (default 0.5) - time_allowance: Maximum time to wait for the request to complete + velocity: Velocity vector [x, y, yaw] where: + x: Linear velocity in x direction (m/s) + y: Linear velocity in y direction (m/s) + yaw: Angular velocity around z axis (rad/s) + duration: Duration to apply command (seconds). If 0, apply once. Returns: - bool: True if movement succeeded + bool: True if command was sent successfully """ - try: - if distance <= 0: - logger.error("Distance must be positive") - return False + x, y, yaw = velocity.x, velocity.y, velocity.z - speed = min(abs(speed), self.MAX_LINEAR_VELOCITY) - - # Define function to execute the move - def execute_move(): - # Create DriveOnHeading goal - goal = DriveOnHeading.Goal() - goal.target.x = distance - goal.target.y = 0.0 - goal.target.z = 0.0 - goal.speed = speed - goal.time_allowance = Duration(sec=time_allowance) - - logger.info(f"Moving forward: distance={distance}m, speed={speed}m/s") + # Clamp velocities to safe limits + x = self._clamp_velocity(x, self.MAX_LINEAR_VELOCITY) + y = self._clamp_velocity(y, self.MAX_LINEAR_VELOCITY) + yaw = self._clamp_velocity(yaw, self.MAX_ANGULAR_VELOCITY) - return self._send_action_client_goal( - self._drive_client, - goal, - f"Sending Action Client goal in ROSControl.execute_move for {distance}m at {speed}m/s", - time_allowance, - ) + # Create and send command + cmd = Twist() + cmd.linear.x = float(x) + cmd.linear.y = float(y) + cmd.angular.z = float(yaw) - # Queue the action - cmd_id = self._command_queue.queue_action_client_request( - action_name="move", - execute_func=execute_move, - priority=0, - timeout=time_allowance, - distance=distance, - speed=speed, - ) - logger.info( - f"Queued move command: {cmd_id} - Distance: {distance}m, Speed: {speed}m/s" - ) + try: + if duration > 0: + start_time = time.time() + while time.time() - start_time < duration: + self._move_vel_pub.publish(cmd) + time.sleep(0.1) # 10Hz update rate + # Stop after duration + self.stop() + else: + self._move_vel_pub.publish(cmd) return True except Exception as e: - logger.error(f"Forward movement failed: {e}") - import traceback - - logger.error(traceback.format_exc()) + self._logger.error(f"Failed to send movement command: {e}") return False - def reverse( - self, distance: float, speed: float = 0.5, time_allowance: float = 120 - ) -> bool: + def reverse(self, distance: float, speed: float = 0.5, time_allowance: float = 120) -> bool: """ Move the robot backward by a specified distance @@ -602,9 +585,7 @@ def execute_reverse(): logger.error(traceback.format_exc()) return False - def spin( - self, degrees: float, speed: float = 45.0, time_allowance: float = 120 - ) -> bool: + def spin(self, degrees: float, speed: float = 45.0, time_allowance: float = 120) -> bool: """ Rotate the robot by a specified angle @@ -652,9 +633,7 @@ def execute_spin(): degrees=degrees, speed=speed, ) - logger.info( - f"Queued spin command: {cmd_id} - Degrees: {degrees}, Speed: {speed}deg/s" - ) + logger.info(f"Queued spin command: {cmd_id} - Degrees: {degrees}, Speed: {speed}deg/s") return True except Exception as e: @@ -664,32 +643,6 @@ def execute_spin(): logger.error(traceback.format_exc()) return False - def _goal_response_callback(self, future): - """Handle the goal response.""" - goal_handle = future.result() - if not goal_handle.accepted: - logger.warn("Goal was rejected!") - print("[ROSControl] Goal was REJECTED by the action server") - self._action_success = False - return - - logger.info("Goal accepted") - print("[ROSControl] Goal was ACCEPTED by the action server") - result_future = goal_handle.get_result_async() - result_future.add_done_callback(self._goal_result_callback) - - def _goal_result_callback(self, future): - """Handle the goal result.""" - try: - result = future.result().result - logger.info("Goal completed") - print(f"[ROSControl] Goal COMPLETED with result: {result}") - self._action_success = True - except Exception as e: - logger.error(f"Goal failed with error: {e}") - print(f"[ROSControl] Goal FAILED with error: {e}") - self._action_success = False - def stop(self) -> bool: """Stop all robot movement""" try: @@ -701,7 +654,7 @@ def stop(self) -> bool: logger.error(f"Failed to stop movement: {e}") return False - def cleanup(self): + def cleanup(self) -> None: """Cleanup the executor, ROS node, and stop robot.""" self.stop() @@ -717,13 +670,17 @@ def cleanup(self): self._node.destroy_node() rclpy.shutdown() + def disconnect(self) -> None: + """Disconnect from the robot and clean up resources.""" + self.cleanup() + def webrtc_req( self, api_id: int, - topic: str = None, + topic: str | None = None, parameter: str = "", priority: int = 0, - request_id: str = None, + request_id: str | None = None, data=None, ) -> bool: """ @@ -766,7 +723,7 @@ def get_robot_mode(self) -> RobotMode: """ return self._mode - def print_robot_mode(self): + def print_robot_mode(self) -> None: """Print the current robot mode to the console""" mode = self.get_robot_mode() print(f"Current RobotMode: {mode.name}") @@ -775,11 +732,11 @@ def print_robot_mode(self): def queue_webrtc_req( self, api_id: int, - topic: str = None, + topic: str | None = None, parameter: str = "", priority: int = 0, timeout: float = 90.0, - request_id: str = None, + request_id: str | None = None, data=None, ) -> str: """ @@ -807,46 +764,6 @@ def queue_webrtc_req( data=data, ) - def move_vel(self, x: float, y: float, yaw: float, duration: float = 0.0) -> bool: - """ - Send movement command to the robot using velocity commands - - Args: - x: Forward/backward velocity (m/s) - y: Left/right velocity (m/s) - yaw: Rotational velocity (rad/s) - duration: How long to move (seconds). If 0, command is continuous - - Returns: - bool: True if command was sent successfully - """ - # Clamp velocities to safe limits - x = self._clamp_velocity(x, self.MAX_LINEAR_VELOCITY) - y = self._clamp_velocity(y, self.MAX_LINEAR_VELOCITY) - yaw = self._clamp_velocity(yaw, self.MAX_ANGULAR_VELOCITY) - - # Create and send command - cmd = Twist() - cmd.linear.x = float(x) - cmd.linear.y = float(y) - cmd.angular.z = float(yaw) - - try: - if duration > 0: - start_time = time.time() - while time.time() - start_time < duration: - self._move_vel_pub.publish(cmd) - time.sleep(0.1) # 10Hz update rate - # Stop after duration - self.stop() - else: - self._move_vel_pub.publish(cmd) - return True - - except Exception as e: - self._logger.error(f"Failed to send movement command: {e}") - return False - def move_vel_control(self, x: float, y: float, yaw: float) -> bool: """ Send a single velocity command without duration handling. @@ -902,21 +819,47 @@ def pose_command(self, roll: float, pitch: float, yaw: float) -> bool: except Exception as e: logger.error(f"Failed to send pose command: {e}") return False - + def get_position_stream(self): """ Get a stream of position updates from ROS. - + Returns: Observable that emits (x, y) tuples representing the robot's position """ from dimos.robot.position_stream import PositionStreamProvider - + # Create a position stream provider position_provider = PositionStreamProvider( ros_node=self._node, odometry_topic="/odom", # Default odometry topic - use_odometry=True + use_odometry=True, ) - + return position_provider.get_position_stream() + + def _goal_response_callback(self, future) -> None: + """Handle the goal response.""" + goal_handle = future.result() + if not goal_handle.accepted: + logger.warn("Goal was rejected!") + print("[ROSControl] Goal was REJECTED by the action server") + self._action_success = False + return + + logger.info("Goal accepted") + print("[ROSControl] Goal was ACCEPTED by the action server") + result_future = goal_handle.get_result_async() + result_future.add_done_callback(self._goal_result_callback) + + def _goal_result_callback(self, future) -> None: + """Handle the goal result.""" + try: + result = future.result().result + logger.info("Goal completed") + print(f"[ROSControl] Goal COMPLETED with result: {result}") + self._action_success = True + except Exception as e: + logger.error(f"Goal failed with error: {e}") + print(f"[ROSControl] Goal FAILED with error: {e}") + self._action_success = False diff --git a/dimos/robot/ros_observable_topic.py b/dimos/robot/ros_observable_topic.py index 8826a894f4..7cfc70fd8b 100644 --- a/dimos/robot/ros_observable_topic.py +++ b/dimos/robot/ros_observable_topic.py @@ -12,33 +12,32 @@ # See the License for the specific language governing permissions and # limitations under the License. import asyncio -import functools +from collections.abc import Callable import enum +import functools +from typing import Any, Union + +from nav_msgs import msg +from rclpy.qos import ( + QoSDurabilityPolicy, + QoSHistoryPolicy, + QoSProfile, + QoSReliabilityPolicy, +) import reactivex as rx from reactivex import operators as ops from reactivex.disposable import Disposable from reactivex.scheduler import ThreadPoolScheduler from rxpy_backpressure import BackPressure -from nav_msgs import msg +from dimos.msgs.nav_msgs import OccupancyGrid +from dimos.types.vector import Vector from dimos.utils.logging_config import setup_logger from dimos.utils.threadpool import get_scheduler -from dimos.types.costmap import Costmap -from dimos.types.vector import Vector - -from typing import Union, Callable, Any - -from rclpy.qos import ( - QoSProfile, - QoSReliabilityPolicy, - QoSHistoryPolicy, - QoSDurabilityPolicy, -) -__all__ = ["ROSObservableTopicAbility", "QOS"] +__all__ = ["QOS", "ROSObservableTopicAbility"] -ConversionType = Costmap -TopicType = Union[ConversionType, msg.OccupancyGrid, msg.Odometry] +TopicType = Union[OccupancyGrid, msg.OccupancyGrid, msg.Odometry] class QOS(enum.Enum): @@ -82,15 +81,15 @@ class ROSObservableTopicAbility: # └──► observe_on(pool) ─► backpressure.latest ─► sub3 (slower) # def _maybe_conversion(self, msg_type: TopicType, callback) -> Callable[[TopicType], Any]: - if msg_type == Costmap: - return lambda msg: callback(Costmap.from_msg(msg)) + if msg_type == "Costmap": + return lambda msg: callback(OccupancyGrid.from_msg(msg)) # just for test, not sure if this Vector auto-instantiation is used irl if msg_type == Vector: return lambda msg: callback(Vector.from_msg(msg)) return callback def _sub_msg_type(self, msg_type): - if msg_type == Costmap: + if msg_type == "Costmap": return msg.OccupancyGrid if msg_type == Vector: @@ -98,7 +97,7 @@ def _sub_msg_type(self, msg_type): return msg_type - @functools.lru_cache(maxsize=None) + @functools.cache def topic( self, topic_name: str, @@ -116,7 +115,10 @@ def topic( # upstream ROS callback def _on_subscribe(obs, _): ros_sub = self._node.create_subscription( - self._sub_msg_type(msg_type), topic_name, self._maybe_conversion(msg_type, obs.on_next), qos_profile + self._sub_msg_type(msg_type), + topic_name, + self._maybe_conversion(msg_type, obs.on_next), + qos_profile, ) return Disposable(lambda: self._node.destroy_subscription(ros_sub)) @@ -158,7 +160,9 @@ def _subscribe(observer, sch=None): # odom.dispose() # clean up the subscription # # see test_ros_observable_topic.py test_topic_latest for more details - def topic_latest(self, topic_name: str, msg_type: TopicType, timeout: float | None = 100.0, qos=QOS.SENSOR): + def topic_latest( + self, topic_name: str, msg_type: TopicType, timeout: float | None = 100.0, qos=QOS.SENSOR + ): """ Blocks the current thread until the first message is received, then returns `reader()` (sync) and keeps one ROS subscription alive @@ -173,7 +177,9 @@ def topic_latest(self, topic_name: str, msg_type: TopicType, timeout: float | No conn = core.connect() # starts the ROS subscription immediately try: - first_val = core.pipe(ops.first(), *([ops.timeout(timeout)] if timeout is not None else [])).run() + first_val = core.pipe( + ops.first(), *([ops.timeout(timeout)] if timeout is not None else []) + ).run() except Exception: conn.dispose() msg = f"{topic_name} message not received after {timeout} seconds. Is robot connected?" @@ -204,14 +210,16 @@ def reader(): # odom.dispose() # clean up the subscription # # see test_ros_observable_topic.py test_topic_latest for more details - async def topic_latest_async(self, topic_name: str, msg_type: TopicType, qos=QOS.SENSOR, timeout: float = 30.0): + async def topic_latest_async( + self, topic_name: str, msg_type: TopicType, qos=QOS.SENSOR, timeout: float = 30.0 + ): loop = asyncio.get_running_loop() first = loop.create_future() cache = {"val": None} core = self.topic(topic_name, msg_type, qos=qos) # single ROS callback - def _on_next(v): + def _on_next(v) -> None: cache["val"] = v if not first.done(): loop.call_soon_threadsafe(first.set_result, v) diff --git a/dimos/robot/ros_transform.py b/dimos/robot/ros_transform.py index 2fc7a7a2ba..d54eb8cd15 100644 --- a/dimos/robot/ros_transform.py +++ b/dimos/robot/ros_transform.py @@ -12,16 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -import rclpy -from typing import Optional + from geometry_msgs.msg import TransformStamped -from tf2_ros import Buffer -import tf2_ros +import rclpy +from scipy.spatial.transform import Rotation as R from tf2_geometry_msgs import PointStamped -from dimos.utils.logging_config import setup_logger -from dimos.types.vector import Vector +import tf2_ros +from tf2_ros import Buffer + from dimos.types.path import Path -from scipy.spatial.transform import Rotation as R +from dimos.types.vector import Vector +from dimos.utils.logging_config import setup_logger logger = setup_logger("dimos.robot.ros_transform") @@ -37,6 +38,7 @@ def to_euler_rot(msg: TransformStamped) -> [Vector, Vector]: def to_euler_pos(msg: TransformStamped) -> [Vector, Vector]: return Vector(msg.transform.translation).to_2d() + def to_euler(msg: TransformStamped) -> [Vector, Vector]: return [to_euler_pos(msg), to_euler_rot(msg)] @@ -53,10 +55,14 @@ def tf_buffer(self) -> Buffer: return self._tf_buffer - def transform_euler_pos(self, source_frame: str, target_frame: str = "map", timeout: float = 1.0): + def transform_euler_pos( + self, source_frame: str, target_frame: str = "map", timeout: float = 1.0 + ): return to_euler_pos(self.transform(source_frame, target_frame, timeout)) - def transform_euler_rot(self, source_frame: str, target_frame: str = "map", timeout: float = 1.0): + def transform_euler_rot( + self, source_frame: str, target_frame: str = "map", timeout: float = 1.0 + ): return to_euler_rot(self.transform(source_frame, target_frame, timeout)) def transform_euler(self, source_frame: str, target_frame: str = "map", timeout: float = 1.0): @@ -65,7 +71,7 @@ def transform_euler(self, source_frame: str, target_frame: str = "map", timeout: def transform( self, source_frame: str, target_frame: str = "map", timeout: float = 1.0 - ) -> Optional[TransformStamped]: + ) -> TransformStamped | None: try: transform = self.tf_buffer.lookup_transform( target_frame, @@ -82,7 +88,9 @@ def transform( logger.error(f"Transform lookup failed: {e}") return None - def transform_point(self, point: Vector, source_frame: str, target_frame: str = "map", timeout: float = 1.0): + def transform_point( + self, point: Vector, source_frame: str, target_frame: str = "map", timeout: float = 1.0 + ): """Transform a point from source_frame to target_frame. Args: @@ -97,7 +105,10 @@ def transform_point(self, point: Vector, source_frame: str, target_frame: str = try: # Wait for transform to become available self.tf_buffer.can_transform( - target_frame, source_frame, rclpy.time.Time(), rclpy.duration.Duration(seconds=timeout) + target_frame, + source_frame, + rclpy.time.Time(), + rclpy.duration.Duration(seconds=timeout), ) # Create a PointStamped message @@ -109,20 +120,28 @@ def transform_point(self, point: Vector, source_frame: str, target_frame: str = ps.point.z = point[2] if len(point) > 2 else 0.0 # Transform point - transformed_ps = self.tf_buffer.transform(ps, target_frame, rclpy.duration.Duration(seconds=timeout)) + transformed_ps = self.tf_buffer.transform( + ps, target_frame, rclpy.duration.Duration(seconds=timeout) + ) # Return as Vector type if len(point) > 2: - return Vector(transformed_ps.point.x, transformed_ps.point.y, transformed_ps.point.z) + return Vector( + transformed_ps.point.x, transformed_ps.point.y, transformed_ps.point.z + ) else: return Vector(transformed_ps.point.x, transformed_ps.point.y) - except (tf2_ros.LookupException, tf2_ros.ConnectivityException, tf2_ros.ExtrapolationException) as e: + except ( + tf2_ros.LookupException, + tf2_ros.ConnectivityException, + tf2_ros.ExtrapolationException, + ) as e: logger.error(f"Transform from {source_frame} to {target_frame} failed: {e}") return None - - - def transform_path(self, path: Path, source_frame: str, target_frame: str = "map", timeout: float = 1.0): + def transform_path( + self, path: Path, source_frame: str, target_frame: str = "map", timeout: float = 1.0 + ): """Transform a path from source_frame to target_frame. Args: @@ -141,7 +160,9 @@ def transform_path(self, path: Path, source_frame: str, target_frame: str = "map transformed_path.append(transformed_point) return transformed_path - def transform_rot(self, rotation: Vector, source_frame: str, target_frame: str = "map", timeout: float = 1.0): + def transform_rot( + self, rotation: Vector, source_frame: str, target_frame: str = "map", timeout: float = 1.0 + ): """Transform a rotation from source_frame to target_frame. Args: @@ -156,36 +177,50 @@ def transform_rot(self, rotation: Vector, source_frame: str, target_frame: str = try: # Wait for transform to become available self.tf_buffer.can_transform( - target_frame, source_frame, rclpy.time.Time(), rclpy.duration.Duration(seconds=timeout) + target_frame, + source_frame, + rclpy.time.Time(), + rclpy.duration.Duration(seconds=timeout), ) - + # Create a rotation matrix from the input Euler angles - input_rotation = R.from_euler('xyz', rotation, degrees=False) - + input_rotation = R.from_euler("xyz", rotation, degrees=False) + # Get the transform from source to target frame transform = self.transform(source_frame, target_frame, timeout) if transform is None: return None - + # Extract the rotation from the transform q = transform.transform.rotation transform_rotation = R.from_quat([q.x, q.y, q.z, q.w]) - + # Compose the rotations # The resulting rotation is the composition of the transform rotation and input rotation result_rotation = transform_rotation * input_rotation - + # Convert back to Euler angles - euler_angles = result_rotation.as_euler('xyz', degrees=False) - + euler_angles = result_rotation.as_euler("xyz", degrees=False) + # Return as Vector type return Vector(euler_angles) - - except (tf2_ros.LookupException, tf2_ros.ConnectivityException, tf2_ros.ExtrapolationException) as e: + + except ( + tf2_ros.LookupException, + tf2_ros.ConnectivityException, + tf2_ros.ExtrapolationException, + ) as e: logger.error(f"Transform rotation from {source_frame} to {target_frame} failed: {e}") return None - def transform_pose(self, position: Vector, rotation: Vector, source_frame: str, target_frame: str = "map", timeout: float = 1.0): + def transform_pose( + self, + position: Vector, + rotation: Vector, + source_frame: str, + target_frame: str = "map", + timeout: float = 1.0, + ): """Transform a pose from source_frame to target_frame. Args: @@ -196,14 +231,14 @@ def transform_pose(self, position: Vector, rotation: Vector, source_frame: str, timeout: Time to wait for the transform to become available (seconds) Returns: - Tuple of (transformed_position, transformed_rotation) as Vectors, + Tuple of (transformed_position, transformed_rotation) as Vectors, or (None, None) if either transform failed """ # Transform position transformed_position = self.transform_point(position, source_frame, target_frame, timeout) - + # Transform rotation transformed_rotation = self.transform_rot(rotation, source_frame, target_frame, timeout) - + # Return results (both might be None if transforms failed) - return transformed_position, transformed_rotation \ No newline at end of file + return transformed_position, transformed_rotation diff --git a/dimos/robot/test_ros_bridge.py b/dimos/robot/test_ros_bridge.py new file mode 100644 index 0000000000..435766b938 --- /dev/null +++ b/dimos/robot/test_ros_bridge.py @@ -0,0 +1,434 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import threading +import time +import unittest + +import numpy as np +import pytest + +try: + from geometry_msgs.msg import TransformStamped, TwistStamped as ROSTwistStamped + import rclpy + from rclpy.node import Node + from sensor_msgs.msg import PointCloud2 as ROSPointCloud2, PointField + from tf2_msgs.msg import TFMessage as ROSTFMessage +except ImportError: + rclpy = None + Node = None + ROSTwistStamped = None + ROSPointCloud2 = None + PointField = None + ROSTFMessage = None + TransformStamped = None + +from dimos.msgs.geometry_msgs import TwistStamped +from dimos.msgs.sensor_msgs import PointCloud2 +from dimos.msgs.tf2_msgs import TFMessage +from dimos.protocol.pubsub.lcmpubsub import LCM, Topic +from dimos.robot.ros_bridge import BridgeDirection, ROSBridge + + +@pytest.mark.ros +class TestROSBridge(unittest.TestCase): + """Test suite for ROS-DIMOS bridge.""" + + def setUp(self) -> None: + """Set up test fixtures.""" + # Skip if ROS is not available + if rclpy is None: + self.skipTest("ROS not available") + + # Initialize ROS if not already done + if not rclpy.ok(): + rclpy.init() + + # Create test bridge + self.bridge = ROSBridge("test_ros_bridge") + + # Create test node for publishing/subscribing + self.test_node = Node("test_node") + + # Track received messages + self.ros_messages = [] + self.dimos_messages = [] + self.message_timestamps = {"ros": [], "dimos": []} + + def tearDown(self) -> None: + """Clean up test fixtures.""" + self.test_node.destroy_node() + self.bridge.stop() + if rclpy.ok(): + rclpy.try_shutdown() + + def test_ros_to_dimos_twist(self) -> None: + """Test ROS TwistStamped to DIMOS conversion and transmission.""" + # Set up bridge + self.bridge.add_topic( + "/test_twist", TwistStamped, ROSTwistStamped, BridgeDirection.ROS_TO_DIMOS + ) + + # Subscribe to DIMOS side + lcm = LCM() + lcm.start() + topic = Topic("/test_twist", TwistStamped) + + def dimos_callback(msg, _topic) -> None: + self.dimos_messages.append(msg) + self.message_timestamps["dimos"].append(time.time()) + + lcm.subscribe(topic, dimos_callback) + + # Publish from ROS side + ros_pub = self.test_node.create_publisher(ROSTwistStamped, "/test_twist", 10) + + # Send test messages + for i in range(10): + msg = ROSTwistStamped() + msg.header.stamp = self.test_node.get_clock().now().to_msg() + msg.header.frame_id = f"frame_{i}" + msg.twist.linear.x = float(i) + msg.twist.linear.y = float(i * 2) + msg.twist.angular.z = float(i * 0.1) + + ros_pub.publish(msg) + self.message_timestamps["ros"].append(time.time()) + time.sleep(0.01) # 100Hz + + # Allow time for processing + time.sleep(0.5) + + # Verify messages received + self.assertEqual(len(self.dimos_messages), 10, "Should receive all 10 messages") + + # Verify message content + for i, msg in enumerate(self.dimos_messages): + self.assertEqual(msg.frame_id, f"frame_{i}") + self.assertAlmostEqual(msg.linear.x, float(i), places=5) + self.assertAlmostEqual(msg.linear.y, float(i * 2), places=5) + self.assertAlmostEqual(msg.angular.z, float(i * 0.1), places=5) + + def test_dimos_to_ros_twist(self) -> None: + """Test DIMOS TwistStamped to ROS conversion and transmission.""" + # Set up bridge + self.bridge.add_topic( + "/test_twist_reverse", TwistStamped, ROSTwistStamped, BridgeDirection.DIMOS_TO_ROS + ) + + # Subscribe to ROS side + def ros_callback(msg) -> None: + self.ros_messages.append(msg) + self.message_timestamps["ros"].append(time.time()) + + self.test_node.create_subscription(ROSTwistStamped, "/test_twist_reverse", ros_callback, 10) + + # Use the bridge's LCM instance for publishing + topic = Topic("/test_twist_reverse", TwistStamped) + + # Send test messages + for i in range(10): + msg = TwistStamped(ts=time.time(), frame_id=f"dimos_frame_{i}") + msg.linear.x = float(i * 3) + msg.linear.y = float(i * 4) + msg.angular.z = float(i * 0.2) + + self.bridge.lcm.publish(topic, msg) + self.message_timestamps["dimos"].append(time.time()) + time.sleep(0.01) # 100Hz + + # Allow time for processing and spin the test node + for _ in range(50): # Spin for 0.5 seconds + rclpy.spin_once(self.test_node, timeout_sec=0.01) + + # Verify messages received + self.assertEqual(len(self.ros_messages), 10, "Should receive all 10 messages") + + # Verify message content + for i, msg in enumerate(self.ros_messages): + self.assertEqual(msg.header.frame_id, f"dimos_frame_{i}") + self.assertAlmostEqual(msg.twist.linear.x, float(i * 3), places=5) + self.assertAlmostEqual(msg.twist.linear.y, float(i * 4), places=5) + self.assertAlmostEqual(msg.twist.angular.z, float(i * 0.2), places=5) + + def test_frequency_preservation(self) -> None: + """Test that message frequencies are preserved through the bridge.""" + # Set up bridge + self.bridge.add_topic( + "/test_freq", TwistStamped, ROSTwistStamped, BridgeDirection.ROS_TO_DIMOS + ) + + # Subscribe to DIMOS side + lcm = LCM() + lcm.start() + topic = Topic("/test_freq", TwistStamped) + + receive_times = [] + + def dimos_callback(_msg, _topic) -> None: + receive_times.append(time.time()) + + lcm.subscribe(topic, dimos_callback) + + # Publish from ROS at specific frequencies + ros_pub = self.test_node.create_publisher(ROSTwistStamped, "/test_freq", 10) + + # Test different frequencies + test_frequencies = [10, 50, 100] # Hz + + for target_freq in test_frequencies: + receive_times.clear() + send_times = [] + period = 1.0 / target_freq + + # Send messages at target frequency + start_time = time.time() + while time.time() - start_time < 1.0: # Run for 1 second + msg = ROSTwistStamped() + msg.header.stamp = self.test_node.get_clock().now().to_msg() + msg.twist.linear.x = 1.0 + + ros_pub.publish(msg) + send_times.append(time.time()) + time.sleep(period) + + # Allow processing time + time.sleep(0.2) + + # Calculate actual frequencies + if len(send_times) > 1: + send_intervals = np.diff(send_times) + send_freq = 1.0 / np.mean(send_intervals) + else: + send_freq = 0 + + if len(receive_times) > 1: + receive_intervals = np.diff(receive_times) + receive_freq = 1.0 / np.mean(receive_intervals) + else: + receive_freq = 0 + + # Verify frequency preservation (within 10% tolerance) + self.assertAlmostEqual( + receive_freq, + send_freq, + delta=send_freq * 0.1, + msg=f"Frequency not preserved for {target_freq}Hz: sent={send_freq:.1f}Hz, received={receive_freq:.1f}Hz", + ) + + def test_pointcloud_conversion(self) -> None: + """Test PointCloud2 message conversion with numpy optimization.""" + # Set up bridge + self.bridge.add_topic( + "/test_cloud", PointCloud2, ROSPointCloud2, BridgeDirection.ROS_TO_DIMOS + ) + + # Subscribe to DIMOS side + lcm = LCM() + lcm.start() + topic = Topic("/test_cloud", PointCloud2) + + received_cloud = [] + + def dimos_callback(msg, _topic) -> None: + received_cloud.append(msg) + + lcm.subscribe(topic, dimos_callback) + + # Create test point cloud + ros_pub = self.test_node.create_publisher(ROSPointCloud2, "/test_cloud", 10) + + # Generate test points + num_points = 1000 + points = np.random.randn(num_points, 3).astype(np.float32) + + # Create ROS PointCloud2 message + msg = ROSPointCloud2() + msg.header.stamp = self.test_node.get_clock().now().to_msg() + msg.header.frame_id = "test_frame" + msg.height = 1 + msg.width = num_points + msg.fields = [ + PointField(name="x", offset=0, datatype=PointField.FLOAT32, count=1), + PointField(name="y", offset=4, datatype=PointField.FLOAT32, count=1), + PointField(name="z", offset=8, datatype=PointField.FLOAT32, count=1), + ] + msg.is_bigendian = False + msg.point_step = 12 + msg.row_step = msg.point_step * msg.width + msg.data = points.tobytes() + msg.is_dense = True + + # Send point cloud + ros_pub.publish(msg) + + # Allow processing time + time.sleep(0.5) + + # Verify reception + self.assertEqual(len(received_cloud), 1, "Should receive point cloud") + + # Verify point data + received_points = received_cloud[0].as_numpy() + self.assertEqual(received_points.shape, points.shape) + np.testing.assert_array_almost_equal(received_points, points, decimal=5) + + def test_tf_high_frequency(self) -> None: + """Test TF message handling at high frequency.""" + # Set up bridge + self.bridge.add_topic("/test_tf", TFMessage, ROSTFMessage, BridgeDirection.ROS_TO_DIMOS) + + # Subscribe to DIMOS side + lcm = LCM() + lcm.start() + topic = Topic("/test_tf", TFMessage) + + received_tfs = [] + receive_times = [] + + def dimos_callback(msg, _topic) -> None: + received_tfs.append(msg) + receive_times.append(time.time()) + + lcm.subscribe(topic, dimos_callback) + + # Publish TF at high frequency (100Hz) + ros_pub = self.test_node.create_publisher(ROSTFMessage, "/test_tf", 100) + + target_freq = 100 # Hz + period = 1.0 / target_freq + num_messages = 100 # 1 second worth + + send_times = [] + for i in range(num_messages): + msg = ROSTFMessage() + transform = TransformStamped() + transform.header.stamp = self.test_node.get_clock().now().to_msg() + transform.header.frame_id = "world" + transform.child_frame_id = f"link_{i}" + transform.transform.translation.x = float(i) + transform.transform.rotation.w = 1.0 + msg.transforms = [transform] + + ros_pub.publish(msg) + send_times.append(time.time()) + time.sleep(period) + + # Allow processing time + time.sleep(0.5) + + # Check message count (allow 5% loss tolerance) + min_expected = int(num_messages * 0.95) + self.assertGreaterEqual( + len(received_tfs), + min_expected, + f"Should receive at least {min_expected} of {num_messages} TF messages", + ) + + # Check frequency preservation + if len(receive_times) > 1: + receive_intervals = np.diff(receive_times) + receive_freq = 1.0 / np.mean(receive_intervals) + + # For high frequency, allow 20% tolerance + self.assertAlmostEqual( + receive_freq, + target_freq, + delta=target_freq * 0.2, + msg=f"High frequency TF not preserved: expected={target_freq}Hz, got={receive_freq:.1f}Hz", + ) + + def test_bidirectional_bridge(self) -> None: + """Test simultaneous bidirectional message flow.""" + # Set up bidirectional bridges for same topic type + self.bridge.add_topic( + "/ros_to_dimos", TwistStamped, ROSTwistStamped, BridgeDirection.ROS_TO_DIMOS + ) + + self.bridge.add_topic( + "/dimos_to_ros", TwistStamped, ROSTwistStamped, BridgeDirection.DIMOS_TO_ROS + ) + + dimos_received = [] + ros_received = [] + + # DIMOS subscriber - use bridge's LCM + topic_r2d = Topic("/ros_to_dimos", TwistStamped) + self.bridge.lcm.subscribe(topic_r2d, lambda msg, _: dimos_received.append(msg)) + + # ROS subscriber + self.test_node.create_subscription( + ROSTwistStamped, "/dimos_to_ros", lambda msg: ros_received.append(msg), 10 + ) + + # Set up publishers + ros_pub = self.test_node.create_publisher(ROSTwistStamped, "/ros_to_dimos", 10) + topic_d2r = Topic("/dimos_to_ros", TwistStamped) + + # Keep track of whether threads should continue + stop_spinning = threading.Event() + + # Spin the test node in background to receive messages + def spin_test_node() -> None: + while not stop_spinning.is_set(): + rclpy.spin_once(self.test_node, timeout_sec=0.01) + + spin_thread = threading.Thread(target=spin_test_node, daemon=True) + spin_thread.start() + + # Send messages in both directions simultaneously + def send_ros_messages() -> None: + for i in range(50): + msg = ROSTwistStamped() + msg.header.stamp = self.test_node.get_clock().now().to_msg() + msg.twist.linear.x = float(i) + ros_pub.publish(msg) + time.sleep(0.02) # 50Hz + + def send_dimos_messages() -> None: + for i in range(50): + msg = TwistStamped(ts=time.time()) + msg.linear.y = float(i * 2) + self.bridge.lcm.publish(topic_d2r, msg) + time.sleep(0.02) # 50Hz + + # Run both senders in parallel + ros_thread = threading.Thread(target=send_ros_messages) + dimos_thread = threading.Thread(target=send_dimos_messages) + + ros_thread.start() + dimos_thread.start() + + ros_thread.join() + dimos_thread.join() + + # Allow processing time + time.sleep(0.5) + stop_spinning.set() + spin_thread.join(timeout=1.0) + + # Verify both directions worked + self.assertGreaterEqual(len(dimos_received), 45, "Should receive most ROS->DIMOS messages") + self.assertGreaterEqual(len(ros_received), 45, "Should receive most DIMOS->ROS messages") + + # Verify message integrity + for i, msg in enumerate(dimos_received[:45]): + self.assertAlmostEqual(msg.linear.x, float(i), places=5) + + for i, msg in enumerate(ros_received[:45]): + self.assertAlmostEqual(msg.twist.linear.y, float(i * 2), places=5) + + +if __name__ == "__main__": + unittest.main() diff --git a/dimos/robot/test_ros_observable_topic.py b/dimos/robot/test_ros_observable_topic.py index 2dd6370b05..0ffed24d35 100644 --- a/dimos/robot/test_ros_observable_topic.py +++ b/dimos/robot/test_ros_observable_topic.py @@ -1,17 +1,30 @@ #!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio import threading import time -from nav_msgs import msg + import pytest -from dimos.robot.ros_observable_topic import ROSObservableTopicAbility -from dimos.utils.logging_config import setup_logger -from dimos.types.costmap import Costmap + from dimos.types.vector import Vector -import asyncio +from dimos.utils.logging_config import setup_logger class MockROSNode: - def __init__(self): + def __init__(self) -> None: self.logger = setup_logger("ROS") self.sub_id_cnt = 0 @@ -22,7 +35,7 @@ def _get_sub_id(self): self.sub_id_cnt += 1 return sub_id - def create_subscription(self, msg_type, topic_name, callback, qos): + def create_subscription(self, msg_type, topic_name: str, callback, qos): # Mock implementation of ROS subscription sub_id = self._get_sub_id() @@ -31,7 +44,7 @@ def create_subscription(self, msg_type, topic_name, callback, qos): self.logger.info(f"Subscribed {topic_name} subid {sub_id}") # Create message simulation thread - def simulate_messages(): + def simulate_messages() -> None: message_count = 0 while not stop_event.is_set(): message_count += 1 @@ -47,7 +60,7 @@ def simulate_messages(): thread.start() return sub_id - def destroy_subscription(self, subscription): + def destroy_subscription(self, subscription) -> None: if subscription in self.subs: self.subs[subscription].set() self.logger.info(f"Destroyed subscription: {subscription}") @@ -55,11 +68,18 @@ def destroy_subscription(self, subscription): self.logger.info(f"Unknown subscription: {subscription}") -class MockRobot(ROSObservableTopicAbility): - def __init__(self): - self.logger = setup_logger("ROBOT") - # Initialize the mock ROS node - self._node = MockROSNode() +# we are doing this in order to avoid importing ROS dependencies if ros tests aren't runnin +@pytest.fixture +def robot(): + from dimos.robot.ros_observable_topic import ROSObservableTopicAbility + + class MockRobot(ROSObservableTopicAbility): + def __init__(self) -> None: + self.logger = setup_logger("ROBOT") + # Initialize the mock ROS node + self._node = MockROSNode() + + return MockRobot() # This test verifies a bunch of basics: @@ -69,8 +89,10 @@ def __init__(self): # 3. that the system unsubscribes from ROS when observers are disposed # 4. that the system replays the last message to new observers, # before the new ROS sub starts producing -def test_parallel_and_cleanup(): - robot = MockRobot() +@pytest.mark.ros +def test_parallel_and_cleanup(robot) -> None: + from nav_msgs import msg + received_messages = [] obs1 = robot.topic("/odom", msg.Odometry) @@ -96,7 +118,9 @@ def test_parallel_and_cleanup(): assert i in received_messages, f"Expected {i} in received messages, got {received_messages}" # ensure that ROS end has only a single subscription - assert len(robot._node.subs) == 1, f"Expected 1 subscription, got {len(robot._node.subs)}: {robot._node.subs}" + assert len(robot._node.subs) == 1, ( + f"Expected 1 subscription, got {len(robot._node.subs)}: {robot._node.subs}" + ) subscription1.dispose() subscription2.dispose() @@ -137,8 +161,9 @@ def test_parallel_and_cleanup(): # ROS thread ─► ReplaySubject─► observe_on(pool) ─► backpressure.latest ─► sub1 (fast) # ├──► observe_on(pool) ─► backpressure.latest ─► sub2 (slow) # └──► observe_on(pool) ─► backpressure.latest ─► sub3 (slower) -def test_parallel_and_hog(): - robot = MockRobot() +@pytest.mark.ros +def test_parallel_and_hog(robot) -> None: + from nav_msgs import msg obs1 = robot.topic("/odom", msg.Odometry) obs2 = robot.topic("/odom", msg.Odometry) @@ -176,8 +201,9 @@ def test_parallel_and_hog(): @pytest.mark.asyncio -async def test_topic_latest_async(): - robot = MockRobot() +@pytest.mark.ros +async def test_topic_latest_async(robot) -> None: + from nav_msgs import msg odom = await robot.topic_latest_async("/odom", msg.Odometry) assert odom() == 1 @@ -188,15 +214,16 @@ async def test_topic_latest_async(): assert robot._node.subs == {} -def test_topic_auto_conversion(): - robot = MockRobot() +@pytest.mark.ros +def test_topic_auto_conversion(robot) -> None: odom = robot.topic("/vector", Vector).subscribe(lambda x: print(x)) time.sleep(0.5) odom.dispose() -def test_topic_latest_sync(): - robot = MockRobot() +@pytest.mark.ros +def test_topic_latest_sync(robot) -> None: + from nav_msgs import msg odom = robot.topic_latest("/odom", msg.Odometry) assert odom() == 1 @@ -207,13 +234,14 @@ def test_topic_latest_sync(): assert robot._node.subs == {} -def test_topic_latest_sync_benchmark(): - robot = MockRobot() +@pytest.mark.ros +def test_topic_latest_sync_benchmark(robot) -> None: + from nav_msgs import msg odom = robot.topic_latest("/odom", msg.Odometry) start_time = time.time() - for i in range(100): + for _i in range(100): odom() end_time = time.time() elapsed = end_time - start_time @@ -223,14 +251,7 @@ def test_topic_latest_sync_benchmark(): assert odom() == 1 time.sleep(0.45) - assert odom() == 5 + assert odom() >= 5 odom.dispose() time.sleep(0.1) assert robot._node.subs == {} - - -if __name__ == "__main__": - test_parallel_and_cleanup() - test_parallel_and_hog() - test_topic_latest_sync() - asyncio.run(test_topic_latest_async()) diff --git a/dimos/robot/unitree/external/go2_ros2_sdk b/dimos/robot/unitree/external/go2_ros2_sdk deleted file mode 160000 index de298797d0..0000000000 --- a/dimos/robot/unitree/external/go2_ros2_sdk +++ /dev/null @@ -1 +0,0 @@ -Subproject commit de298797d04340ead4ad763668c3a62d48b7214a diff --git a/dimos/robot/unitree/external/go2_webrtc_connect b/dimos/robot/unitree/external/go2_webrtc_connect deleted file mode 160000 index 5235733a4e..0000000000 --- a/dimos/robot/unitree/external/go2_webrtc_connect +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5235733a4e8faf1e90929b3b080deee75723c2a5 diff --git a/dimos/robot/unitree/unitree_go2.py b/dimos/robot/unitree/unitree_go2.py index 54ba4c3327..a8e28dd80a 100644 --- a/dimos/robot/unitree/unitree_go2.py +++ b/dimos/robot/unitree/unitree_go2.py @@ -12,29 +12,27 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging import multiprocessing -from typing import Optional, Union, Tuple +import os + import numpy as np -from dimos.robot.robot import Robot -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills -from dimos.skills.skills import AbstractRobotSkill, AbstractSkill, SkillLibrary -from dimos.stream.video_providers.unitree import UnitreeVideoProvider from reactivex.disposable import CompositeDisposable -import logging -import time -from dimos.robot.unitree.external.go2_webrtc_connect.go2_webrtc_driver.webrtc_driver import WebRTCConnectionMethod -import os -from dimos.robot.unitree.unitree_ros_control import UnitreeROSControl from reactivex.scheduler import ThreadPoolScheduler -import threading -from dimos.utils.logging_config import setup_logger -from dimos.perception.person_tracker import PersonTrackingStream + from dimos.perception.object_tracker import ObjectTrackingStream -from dimos.robot.local_planner import VFHPurePursuitPlanner, navigate_path_local +from dimos.perception.person_tracker import PersonTrackingStream from dimos.robot.global_planner.planner import AstarPlanner -from dimos.types.path import Path +from dimos.robot.local_planner.local_planner import navigate_path_local +from dimos.robot.local_planner.vfh_local_planner import VFHPurePursuitPlanner +from dimos.robot.robot import Robot +from dimos.robot.unitree.unitree_ros_control import UnitreeROSControl +from dimos.robot.unitree.unitree_skills import MyUnitreeSkills +from dimos.skills.skills import AbstractRobotSkill, SkillLibrary from dimos.types.costmap import Costmap -from dimos.utils.reactive import backpressure +from dimos.types.robot_capabilities import RobotCapability +from dimos.types.vector import Vector +from dimos.utils.logging_config import setup_logger # Set up logging logger = setup_logger("dimos.robot.unitree.unitree_go2", level=logging.DEBUG) @@ -45,60 +43,63 @@ class UnitreeGo2(Robot): + """Unitree Go2 robot implementation using ROS2 control interface. + + This class extends the base Robot class to provide specific functionality + for the Unitree Go2 quadruped robot using ROS2 for communication and control. + """ + def __init__( self, - ros_control: Optional[UnitreeROSControl] = None, - ip=None, - connection_method: WebRTCConnectionMethod = WebRTCConnectionMethod.LocalSTA, - serial_number: str = None, + video_provider=None, output_dir: str = os.path.join(os.getcwd(), "assets", "output"), - use_ros: bool = True, - use_webrtc: bool = False, - disable_video_stream: bool = False, - mock_connection: bool = False, - skills: Optional[Union[MyUnitreeSkills, AbstractSkill]] = None, - spatial_memory_dir: str = None, + skill_library: SkillLibrary = None, + robot_capabilities: list[RobotCapability] | None = None, spatial_memory_collection: str = "spatial_memory", new_memory: bool = False, - ): - """Initialize the UnitreeGo2 robot. + disable_video_stream: bool = False, + mock_connection: bool = False, + enable_perception: bool = True, + ) -> None: + """Initialize UnitreeGo2 robot with ROS control interface. Args: - ros_control: ROS control interface, if None a new one will be created - ip: IP address of the robot (for LocalSTA connection) - connection_method: WebRTC connection method (LocalSTA or LocalAP) - serial_number: Serial number of the robot (for LocalSTA with serial) + video_provider: Provider for video streams output_dir: Directory for output files - use_ros: Whether to use ROSControl and ROS video provider - use_webrtc: Whether to use WebRTC video provider ONLY - disable_video_stream: Whether to disable the video stream - mock_connection: Whether to mock the connection to the robot - skills: Skills library or custom skill implementation. Default is MyUnitreeSkills() if None. - spatial_memory_dir: Directory for storing spatial memory data. If None, uses output_dir/spatial_memory. - spatial_memory_collection: Name of the collection in the ChromaDB database. - new_memory: If True, creates a new spatial memory from scratch. + skill_library: Library of robot skills + robot_capabilities: List of robot capabilities + spatial_memory_collection: Collection name for spatial memory + new_memory: Whether to create new memory collection + disable_video_stream: Whether to disable video streaming + mock_connection: Whether to use mock connection for testing + enable_perception: Whether to enable perception streams and spatial memory """ - print(f"Initializing UnitreeGo2 with use_ros: {use_ros} and use_webrtc: {use_webrtc}") - if not (use_ros ^ use_webrtc): # XOR operator ensures exactly one is True - raise ValueError("Exactly one video/control provider (ROS or WebRTC) must be enabled") - - # Initialize ros_control if it is not provided and use_ros is True - if ros_control is None and use_ros: - ros_control = UnitreeROSControl( - node_name="unitree_go2", disable_video_stream=disable_video_stream, mock_connection=mock_connection - ) + # Create ROS control interface + ros_control = UnitreeROSControl( + node_name="unitree_go2", + video_provider=video_provider, + disable_video_stream=disable_video_stream, + mock_connection=mock_connection, + ) - # Initialize skill library - if skills is None: - skills = MyUnitreeSkills(robot=self) + # Initialize skill library if not provided + if skill_library is None: + skill_library = MyUnitreeSkills() + # Initialize base robot with connection interface super().__init__( - ros_control=ros_control, + connection_interface=ros_control, output_dir=output_dir, - skill_library=skills, - spatial_memory_dir=spatial_memory_dir, + skill_library=skill_library, + capabilities=robot_capabilities + or [ + RobotCapability.LOCOMOTION, + RobotCapability.VISION, + RobotCapability.AUDIO, + ], spatial_memory_collection=spatial_memory_collection, new_memory=new_memory, + enable_perception=enable_perception, ) if self.skill_library is not None: @@ -116,7 +117,6 @@ def __init__( self.camera_height = 0.44 # meters # Initialize UnitreeGo2-specific attributes - self.ip = ip self.disposables = CompositeDisposable() self.main_stream_obs = None @@ -124,77 +124,86 @@ def __init__( self.optimal_thread_count = multiprocessing.cpu_count() self.thread_pool_scheduler = ThreadPoolScheduler(self.optimal_thread_count // 2) - if (connection_method == WebRTCConnectionMethod.LocalSTA) and (ip is None): - raise ValueError("IP address is required for LocalSTA connection") - - # Choose data provider based on configuration - if use_ros and not disable_video_stream: - # Use ROS video provider from ROSControl - self.video_stream = self.ros_control.video_provider - elif use_webrtc and not disable_video_stream: - # Use WebRTC ONLY video provider - self.video_stream = UnitreeVideoProvider( - dev_name="UnitreeGo2", - connection_method=connection_method, - serial_number=serial_number, - ip=self.ip if connection_method == WebRTCConnectionMethod.LocalSTA else None, - ) - else: - self.video_stream = None - # Initialize visual servoing if enabled - if self.video_stream is not None: - self.video_stream_ros = self.get_ros_video_stream(fps=8) - self.person_tracker = PersonTrackingStream( - camera_intrinsics=self.camera_intrinsics, - camera_pitch=self.camera_pitch, - camera_height=self.camera_height, - ) - self.object_tracker = ObjectTrackingStream( - camera_intrinsics=self.camera_intrinsics, - camera_pitch=self.camera_pitch, - camera_height=self.camera_height, - ) - person_tracking_stream = self.person_tracker.create_stream(self.video_stream_ros) - object_tracking_stream = self.object_tracker.create_stream(self.video_stream_ros) - - self.person_tracking_stream = person_tracking_stream - self.object_tracking_stream = object_tracking_stream + if not disable_video_stream: + self.video_stream_ros = self.get_video_stream(fps=8) + if enable_perception: + self.person_tracker = PersonTrackingStream( + camera_intrinsics=self.camera_intrinsics, + camera_pitch=self.camera_pitch, + camera_height=self.camera_height, + ) + self.object_tracker = ObjectTrackingStream( + camera_intrinsics=self.camera_intrinsics, + camera_pitch=self.camera_pitch, + camera_height=self.camera_height, + ) + person_tracking_stream = self.person_tracker.create_stream(self.video_stream_ros) + object_tracking_stream = self.object_tracker.create_stream(self.video_stream_ros) + + self.person_tracking_stream = person_tracking_stream + self.object_tracking_stream = object_tracking_stream + else: + # Video stream is available but perception tracking is disabled + self.person_tracker = None + self.object_tracker = None + self.person_tracking_stream = None + self.object_tracking_stream = None + else: + # Video stream is disabled + self.video_stream_ros = None + self.person_tracker = None + self.object_tracker = None + self.person_tracking_stream = None + self.object_tracking_stream = None # Initialize the local planner and create BEV visualization stream - self.local_planner = VFHPurePursuitPlanner( - get_costmap=self.ros_control.topic_latest("/local_costmap/costmap", Costmap), - transform=self.ros_control, - move_vel_control=self.ros_control.move_vel_control, - robot_width=0.36, # Unitree Go2 width in meters - robot_length=0.6, # Unitree Go2 length in meters - max_linear_vel=0.5, - lookahead_distance=2.0, - visualization_size=500, # 500x500 pixel visualization - ) + # Note: These features require ROS-specific methods that may not be available on all connection interfaces + if hasattr(self.connection_interface, "topic_latest") and hasattr( + self.connection_interface, "transform_euler" + ): + self.local_planner = VFHPurePursuitPlanner( + get_costmap=self.connection_interface.topic_latest( + "/local_costmap/costmap", Costmap + ), + transform=self.connection_interface, + move_vel_control=self.connection_interface.move_vel_control, + robot_width=0.36, # Unitree Go2 width in meters + robot_length=0.6, # Unitree Go2 length in meters + max_linear_vel=0.5, + lookahead_distance=2.0, + visualization_size=500, # 500x500 pixel visualization + ) - self.global_planner = AstarPlanner( - conservativism=20, # how close to obstacles robot is allowed to path plan - set_local_nav=lambda path, stop_event=None, goal_theta=None: navigate_path_local(self, path, timeout=120.0, goal_theta=goal_theta, stop_event=stop_event), - get_costmap=self.ros_control.topic_latest("map", Costmap), - get_robot_pos=lambda: self.ros_control.transform_euler_pos("base_link"), - ) + self.global_planner = AstarPlanner( + conservativism=20, # how close to obstacles robot is allowed to path plan + set_local_nav=lambda path, stop_event=None, goal_theta=None: navigate_path_local( + self, path, timeout=120.0, goal_theta=goal_theta, stop_event=stop_event + ), + get_costmap=self.connection_interface.topic_latest("map", Costmap), + get_robot_pos=lambda: self.connection_interface.transform_euler_pos("base_link"), + ) - # Create the visualization stream at 5Hz - self.local_planner_viz_stream = self.local_planner.create_stream(frequency_hz=5.0) + # Create the visualization stream at 5Hz + self.local_planner_viz_stream = self.local_planner.create_stream(frequency_hz=5.0) + else: + self.local_planner = None + self.global_planner = None + self.local_planner_viz_stream = None - def get_skills(self) -> Optional[SkillLibrary]: + def get_skills(self) -> SkillLibrary | None: return self.skill_library - def get_pose(self) -> Tuple[Tuple[float, float, float], Tuple[float, float, float]]: + def get_pose(self) -> dict: """ Get the current pose (position and rotation) of the robot in the map frame. - + Returns: - Tuple containing: - - position: Tuple[float, float, float] (x, y, z) - - rotation: Tuple[float, float, float] (roll, pitch, yaw) in radians + Dictionary containing: + - position: Vector (x, y, z) + - rotation: Vector (roll, pitch, yaw) in radians """ - [position, rotation] = self.ros_control.transform_euler("base_link") - - return position, rotation + position_tuple, orientation_tuple = self.connection_interface.get_pose_odom_transform() + position = Vector(position_tuple[0], position_tuple[1], position_tuple[2]) + rotation = Vector(orientation_tuple[0], orientation_tuple[1], orientation_tuple[2]) + return {"position": position, "rotation": rotation} diff --git a/dimos/robot/unitree/unitree_ros_control.py b/dimos/robot/unitree/unitree_ros_control.py index 63bc622f8b..8ab46f5cdc 100644 --- a/dimos/robot/unitree/unitree_ros_control.py +++ b/dimos/robot/unitree/unitree_ros_control.py @@ -12,76 +12,67 @@ # See the License for the specific language governing permissions and # limitations under the License. -from go2_interfaces.msg import Go2State, IMU + +from go2_interfaces.msg import IMU, Go2State +from sensor_msgs.msg import CameraInfo, CompressedImage, Image from unitree_go.msg import WebRtcReq -from enum import Enum, auto -import threading -import time -from typing import Optional, Tuple, Dict, Any, Type -from abc import ABC, abstractmethod -from sensor_msgs.msg import Image, CompressedImage, CameraInfo -from dimos.robot.ros_control import ROSControl, RobotMode + +from dimos.robot.ros_control import RobotMode, ROSControl from dimos.utils.logging_config import setup_logger logger = setup_logger("dimos.robot.unitree.unitree_ros_control") + class UnitreeROSControl(ROSControl): """Hardware interface for Unitree Go2 robot using ROS2""" - + # ROS Camera Topics CAMERA_TOPICS = { - 'raw': { - 'topic': 'camera/image_raw', - 'type': Image - }, - 'compressed': { - 'topic': 'camera/compressed', - 'type': CompressedImage - }, - 'info': { - 'topic': 'camera/camera_info', - 'type': CameraInfo - } + "raw": {"topic": "camera/image_raw", "type": Image}, + "compressed": {"topic": "camera/compressed", "type": CompressedImage}, + "info": {"topic": "camera/camera_info", "type": CameraInfo}, } # Hard coded ROS Message types and Topic names for Unitree Go2 DEFAULT_STATE_MSG_TYPE = Go2State DEFAULT_IMU_MSG_TYPE = IMU DEFAULT_WEBRTC_MSG_TYPE = WebRtcReq - DEFAULT_STATE_TOPIC = 'go2_states' - DEFAULT_IMU_TOPIC = 'imu' - DEFAULT_WEBRTC_TOPIC = 'webrtc_req' - DEFAULT_CMD_VEL_TOPIC = 'cmd_vel_out' - DEFAULT_POSE_TOPIC = 'pose_cmd' - DEFAULT_ODOM_TOPIC = 'odom' - DEFAULT_COSTMAP_TOPIC = 'local_costmap/costmap' + DEFAULT_STATE_TOPIC = "go2_states" + DEFAULT_IMU_TOPIC = "imu" + DEFAULT_WEBRTC_TOPIC = "webrtc_req" + DEFAULT_CMD_VEL_TOPIC = "cmd_vel_out" + DEFAULT_POSE_TOPIC = "pose_cmd" + DEFAULT_ODOM_TOPIC = "odom" + DEFAULT_COSTMAP_TOPIC = "local_costmap/costmap" DEFAULT_MAX_LINEAR_VELOCITY = 1.0 DEFAULT_MAX_ANGULAR_VELOCITY = 2.0 # Hard coded WebRTC API parameters for Unitree Go2 - DEFAULT_WEBRTC_API_TOPIC = 'rt/api/sport/request' - - def __init__(self, - node_name: str = "unitree_hardware_interface", - state_topic: str = None, - imu_topic: str = None, - webrtc_topic: str = None, - webrtc_api_topic: str = None, - move_vel_topic: str = None, - pose_topic: str = None, - odom_topic: str = None, - costmap_topic: str = None, - state_msg_type: Type = None, - imu_msg_type: Type = None, - webrtc_msg_type: Type = None, - max_linear_velocity: float = None, - max_angular_velocity: float = None, - use_raw: bool = False, - debug: bool = False, - disable_video_stream: bool = False, - mock_connection: bool = False): + DEFAULT_WEBRTC_API_TOPIC = "rt/api/sport/request" + + def __init__( + self, + node_name: str = "unitree_hardware_interface", + state_topic: str | None = None, + imu_topic: str | None = None, + webrtc_topic: str | None = None, + webrtc_api_topic: str | None = None, + move_vel_topic: str | None = None, + pose_topic: str | None = None, + odom_topic: str | None = None, + costmap_topic: str | None = None, + state_msg_type: type | None = None, + imu_msg_type: type | None = None, + webrtc_msg_type: type | None = None, + max_linear_velocity: float | None = None, + max_angular_velocity: float | None = None, + use_raw: bool = False, + debug: bool = False, + disable_video_stream: bool = False, + mock_connection: bool = False, + ) -> None: """ Initialize Unitree ROS control interface with default values for Unitree Go2 - + Args: node_name: Name for the ROS node state_topic: ROS Topic name for robot state (defaults to DEFAULT_STATE_TOPIC) @@ -99,17 +90,15 @@ def __init__(self, use_raw: Whether to use raw camera topics (defaults to False) debug: Whether to enable debug logging disable_video_stream: Whether to run without video stream for testing. - mock_connection: Whether to run without active ActionClient servers for testing. + mock_connection: Whether to run without active ActionClient servers for testing. """ - + logger.info("Initializing Unitree ROS control interface") # Select which camera topics to use active_camera_topics = None if not disable_video_stream: - active_camera_topics = { - 'main': self.CAMERA_TOPICS['raw' if use_raw else 'compressed'] - } - + active_camera_topics = {"main": self.CAMERA_TOPICS["raw" if use_raw else "compressed"]} + # Use default values if not provided state_topic = state_topic or self.DEFAULT_STATE_TOPIC imu_topic = imu_topic or self.DEFAULT_IMU_TOPIC @@ -124,7 +113,7 @@ def __init__(self, webrtc_msg_type = webrtc_msg_type or self.DEFAULT_WEBRTC_MSG_TYPE max_linear_velocity = max_linear_velocity or self.DEFAULT_MAX_LINEAR_VELOCITY max_angular_velocity = max_angular_velocity or self.DEFAULT_MAX_ANGULAR_VELOCITY - + super().__init__( node_name=node_name, camera_topics=active_camera_topics, @@ -142,14 +131,14 @@ def __init__(self, costmap_topic=costmap_topic, max_linear_velocity=max_linear_velocity, max_angular_velocity=max_angular_velocity, - debug=debug + debug=debug, ) - + # Unitree-specific RobotMode State update conditons - def _update_mode(self, msg: Go2State): + def _update_mode(self, msg: Go2State) -> None: """ Implementation of abstract method to update robot mode - + Logic: - If progress is 0 and mode is 1, then state is IDLE - If progress is 1 OR mode is NOT equal to 1, then state is MOVING @@ -157,7 +146,7 @@ def _update_mode(self, msg: Go2State): # Direct access to protected instance variables from the parent class mode = msg.mode progress = msg.progress - + if progress == 0 and mode == 1: self._mode = RobotMode.IDLE logger.debug("Robot mode set to IDLE (progress=0, mode=1)") @@ -166,4 +155,4 @@ def _update_mode(self, msg: Go2State): logger.debug(f"Robot mode set to MOVING (progress={progress}, mode={mode})") else: self._mode = RobotMode.UNKNOWN - logger.debug(f"Robot mode set to UNKNOWN (progress={progress}, mode={mode})") \ No newline at end of file + logger.debug(f"Robot mode set to UNKNOWN (progress={progress}, mode={mode})") diff --git a/dimos/robot/unitree/unitree_skills.py b/dimos/robot/unitree/unitree_skills.py index 9c2e406c97..04946d5ff7 100644 --- a/dimos/robot/unitree/unitree_skills.py +++ b/dimos/robot/unitree/unitree_skills.py @@ -14,159 +14,184 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List, Optional, Tuple, Type, Union import time +from typing import TYPE_CHECKING + from pydantic import Field -import threading if TYPE_CHECKING: - from dimos.robot.robot import Robot, MockRobot + from dimos.robot.robot import MockRobot, Robot else: - Robot = 'Robot' - MockRobot = 'MockRobot' + Robot = "Robot" + MockRobot = "MockRobot" from dimos.skills.skills import AbstractRobotSkill, AbstractSkill, SkillLibrary from dimos.types.constants import Colors -from inspect import signature, Parameter -from typing import Callable, Any, get_type_hints +from dimos.types.vector import Vector # Module-level constant for Unitree ROS control definitions -UNITREE_ROS_CONTROLS: List[Tuple[str, int, str]] = [ - ("Damp", 1001, - "Lowers the robot to the ground fully." - ), - ("BalanceStand", 1002, - "Activates a mode that maintains the robot in a balanced standing position." - ), - ("StandUp", 1004, - "Commands the robot to transition from a sitting or prone position to a standing posture." - ), - ("StandDown", 1005, - "Instructs the robot to move from a standing position to a sitting or prone posture." - ), - ("RecoveryStand", 1006, - "Recovers the robot to a state from which it can take more commands. Useful to run after multiple dynamic commands like front flips." - ), - ("Euler", 1007, - "Adjusts the robot's orientation using Euler angles, providing precise control over its rotation." - ), +UNITREE_ROS_CONTROLS: list[tuple[str, int, str]] = [ + ("Damp", 1001, "Lowers the robot to the ground fully."), + ( + "BalanceStand", + 1002, + "Activates a mode that maintains the robot in a balanced standing position.", + ), + ( + "StandUp", + 1004, + "Commands the robot to transition from a sitting or prone position to a standing posture.", + ), + ( + "StandDown", + 1005, + "Instructs the robot to move from a standing position to a sitting or prone posture.", + ), + ( + "RecoveryStand", + 1006, + "Recovers the robot to a state from which it can take more commands. Useful to run after multiple dynamic commands like front flips.", + ), + # ( + # "Euler", + # 1007, + # "Adjusts the robot's orientation using Euler angles, providing precise control over its rotation.", + # ), # ("Move", 1008, "Move the robot using velocity commands."), # Intentionally omitted - ("Sit", 1009, - "Commands the robot to sit down from a standing or moving stance."), - ("RiseSit", 1010, - "Commands the robot to rise back to a standing position from a sitting posture." - ), - ("SwitchGait", 1011, - "Switches the robot's walking pattern or style dynamically, suitable for different terrains or speeds." - ), - ("Trigger", 1012, - "Triggers a specific action or custom routine programmed into the robot."), - ("BodyHeight", 1013, - "Adjusts the height of the robot's body from the ground, useful for navigating various obstacles." - ), - ("FootRaiseHeight", 1014, - "Controls how high the robot lifts its feet during movement, which can be adjusted for different surfaces." - ), - ("SpeedLevel", 1015, - "Sets or adjusts the speed at which the robot moves, with various levels available for different operational needs." - ), - ("Hello", 1016, - "Performs a greeting action, which could involve a wave or other friendly gesture." - ), - ("Stretch", 1017, - "Engages the robot in a stretching routine." - ), - ("TrajectoryFollow", 1018, - "Directs the robot to follow a predefined trajectory, which could involve complex paths or maneuvers." - ), - ("ContinuousGait", 1019, - "Enables a mode for continuous walking or running, ideal for long-distance travel." - ), - ("Content", 1020, - "To display or trigger when the robot is happy." - ), - ("Wallow", 1021, - "The robot falls onto its back and rolls around." - ), - ("Dance1", 1022, - "Performs a predefined dance routine 1, programmed for entertainment or demonstration." - ), - ("Dance2", 1023, - "Performs another variant of a predefined dance routine 2."), - ("GetBodyHeight", 1024, - "Retrieves the current height of the robot's body from the ground."), - ("GetFootRaiseHeight", 1025, - "Retrieves the current height at which the robot's feet are being raised during movement." - ), - ("GetSpeedLevel", 1026, - "Returns the current speed level at which the robot is operating."), - ("SwitchJoystick", 1027, - "Toggles the control mode to joystick input, allowing for manual direction of the robot's movements." - ), - ("Pose", 1028, - "Directs the robot to take a specific pose or stance, which could be used for tasks or performances." - ), - ("Scrape", 1029, - "Robot falls to its hind legs and makes scraping motions with its front legs." - ), - ("FrontFlip", 1030, - "Executes a front flip, a complex and dynamic maneuver." - ), - ("FrontJump", 1031, - "Commands the robot to perform a forward jump." - ), - ("FrontPounce", 1032, - "Initiates a pouncing movement forward, mimicking animal-like pouncing behavior." - ), - ("WiggleHips", 1033, - "Causes the robot to wiggle its hips." - ), - ("GetState", 1034, - "Retrieves the current operational state of the robot, including status reports or diagnostic information." - ), - ("EconomicGait", 1035, - "Engages a more energy-efficient walking or running mode to conserve battery life." - ), - ("FingerHeart", 1036, - "Performs a finger heart gesture while on its hind legs." - ), - ("Handstand", 1301, - "Commands the robot to perform a handstand, demonstrating balance and control." - ), - ("CrossStep", 1302, - "Engages the robot in a cross-stepping routine, useful for complex locomotion or dance moves." - ), - ("OnesidedStep", 1303, - "Commands the robot to perform a stepping motion that predominantly uses one side." - ), - ("Bound", 1304, - "Initiates a bounding motion, similar to a light, repetitive hopping or leaping." - ), - ("LeadFollow", 1045, - "Engages follow-the-leader behavior, where the robot follows a designated leader or follows a signal." - ), - ("LeftFlip", 1042, - "Executes a flip towards the left side." - ), - ("RightFlip", 1043, - "Performs a flip towards the right side." - ), - ("Backflip", 1044, - "Executes a backflip, a complex and dynamic maneuver." - ) + ("Sit", 1009, "Commands the robot to sit down from a standing or moving stance."), + # ( + # "RiseSit", + # 1010, + # "Commands the robot to rise back to a standing position from a sitting posture.", + # ), + # ( + # "SwitchGait", + # 1011, + # "Switches the robot's walking pattern or style dynamically, suitable for different terrains or speeds.", + # ), + # ("Trigger", 1012, "Triggers a specific action or custom routine programmed into the robot."), + # ( + # "BodyHeight", + # 1013, + # "Adjusts the height of the robot's body from the ground, useful for navigating various obstacles.", + # ), + # ( + # "FootRaiseHeight", + # 1014, + # "Controls how high the robot lifts its feet during movement, which can be adjusted for different surfaces.", + # ), + ( + "SpeedLevel", + 1015, + "Sets or adjusts the speed at which the robot moves, with various levels available for different operational needs.", + ), + ( + "ShakeHand", + 1016, + "Performs a greeting action, which could involve a wave or other friendly gesture.", + ), + ("Stretch", 1017, "Engages the robot in a stretching routine."), + # ( + # "TrajectoryFollow", + # 1018, + # "Directs the robot to follow a predefined trajectory, which could involve complex paths or maneuvers.", + # ), + # ( + # "ContinuousGait", + # 1019, + # "Enables a mode for continuous walking or running, ideal for long-distance travel.", + # ), + ("Content", 1020, "To display or trigger when the robot is happy."), + ("Wallow", 1021, "The robot falls onto its back and rolls around."), + ( + "Dance1", + 1022, + "Performs a predefined dance routine 1, programmed for entertainment or demonstration.", + ), + ("Dance2", 1023, "Performs another variant of a predefined dance routine 2."), + # ("GetBodyHeight", 1024, "Retrieves the current height of the robot's body from the ground."), + # ( + # "GetFootRaiseHeight", + # 1025, + # "Retrieves the current height at which the robot's feet are being raised during movement.", + # ), + # ("GetSpeedLevel", 1026, "Returns the current speed level at which the robot is operating."), + # ( + # "SwitchJoystick", + # 1027, + # "Toggles the control mode to joystick input, allowing for manual direction of the robot's movements.", + # ), + ( + "Pose", + 1028, + "Directs the robot to take a specific pose or stance, which could be used for tasks or performances.", + ), + ( + "Scrape", + 1029, + "Robot falls to its hind legs and makes scraping motions with its front legs.", + ), + ("FrontFlip", 1030, "Executes a front flip, a complex and dynamic maneuver."), + ("FrontJump", 1031, "Commands the robot to perform a forward jump."), + ( + "FrontPounce", + 1032, + "Initiates a pouncing movement forward, mimicking animal-like pouncing behavior.", + ), + # ("WiggleHips", 1033, "Causes the robot to wiggle its hips."), + # ( + # "GetState", + # 1034, + # "Retrieves the current operational state of the robot, including status reports or diagnostic information.", + # ), + # ( + # "EconomicGait", + # 1035, + # "Engages a more energy-efficient walking or running mode to conserve battery life.", + # ), + # ("FingerHeart", 1036, "Performs a finger heart gesture while on its hind legs."), + # ( + # "Handstand", + # 1301, + # "Commands the robot to perform a handstand, demonstrating balance and control.", + # ), + # ( + # "CrossStep", + # 1302, + # "Engages the robot in a cross-stepping routine, useful for complex locomotion or dance moves.", + # ), + # ( + # "OnesidedStep", + # 1303, + # "Commands the robot to perform a stepping motion that predominantly uses one side.", + # ), + # ( + # "Bound", + # 1304, + # "Initiates a bounding motion, similar to a light, repetitive hopping or leaping.", + # ), + # ( + # "LeadFollow", + # 1045, + # "Engages follow-the-leader behavior, where the robot follows a designated leader or follows a signal.", + # ), + # ("LeftFlip", 1042, "Executes a flip towards the left side."), + # ("RightFlip", 1043, "Performs a flip towards the right side."), + # ("Backflip", 1044, "Executes a backflip, a complex and dynamic maneuver."), ] # region MyUnitreeSkills + class MyUnitreeSkills(SkillLibrary): """My Unitree Skills.""" - _robot: Optional[Robot] = None + _robot: Robot | None = None @classmethod - def register_skills(cls, skill_classes: Union['AbstractSkill', list['AbstractSkill']]): + def register_skills(cls, skill_classes: AbstractSkill | list[AbstractSkill]) -> None: """Add multiple skill classes as class attributes. - + Args: skill_classes: List of skill classes to add """ @@ -176,7 +201,7 @@ def register_skills(cls, skill_classes: Union['AbstractSkill', list['AbstractSki else: setattr(cls, skill_classes.__name__, skill_classes) - def __init__(self, robot: Optional[Robot] = None): + def __init__(self, robot: Robot | None = None) -> None: super().__init__() self._robot: Robot = None @@ -187,19 +212,21 @@ def __init__(self, robot: Optional[Robot] = None): self._robot = robot self.initialize_skills() - def initialize_skills(self): + def initialize_skills(self) -> None: # Create the skills and add them to the list of skills self.register_skills(self.create_skills_live()) # Provide the robot instance to each skill for skill_class in self: - print(f"{Colors.GREEN_PRINT_COLOR}Creating instance for skill: {skill_class}{Colors.RESET_COLOR}") + print( + f"{Colors.GREEN_PRINT_COLOR}Creating instance for skill: {skill_class}{Colors.RESET_COLOR}" + ) self.create_instance(skill_class.__name__, robot=self._robot) - + # Refresh the class skills self.refresh_class_skills() - def create_skills_live(self) -> List[AbstractRobotSkill]: + def create_skills_live(self) -> list[AbstractRobotSkill]: # ================================================ # Procedurally created skills # ================================================ @@ -214,7 +241,8 @@ def __call__(self): raise RuntimeError( f"{Colors.RED_PRINT_COLOR}" f"No App ID provided to {self.__class__.__name__} Skill" - f"{Colors.RESET_COLOR}") + f"{Colors.RESET_COLOR}" + ) else: self._robot.webrtc_req(api_id=self._app_id) string = f"{Colors.GREEN_PRINT_COLOR}{self.__class__.__name__} was successful: id={self._app_id}{Colors.RESET_COLOR}" @@ -226,123 +254,38 @@ def __call__(self): skill_class = type( name, # Name of the class (BaseUnitreeSkill,), # Base classes - { - '__doc__': description, - '_app_id': app_id - }) + {"__doc__": description, "_app_id": app_id}, + ) skills_classes.append(skill_class) return skills_classes # region Class-based Skills - + class Move(AbstractRobotSkill): - """Move the robot using direct velocity commands. - - This skill works with both ROS and WebRTC robot implementations. - """ + """Move the robot using direct velocity commands. Determine duration required based on user distance instructions.""" x: float = Field(..., description="Forward velocity (m/s).") y: float = Field(default=0.0, description="Left/right velocity (m/s)") yaw: float = Field(default=0.0, description="Rotational velocity (rad/s)") - duration: float = Field(default=0.0, description="How long to move (seconds). If 0, command is continuous") + duration: float = Field(default=0.0, description="How long to move (seconds).") def __call__(self): super().__call__() - - from dimos.types.vector import Vector - vector = Vector(self.x, self.y, self.yaw) - - # Handle duration for continuous movement - if self.duration > 0: - import time - import threading - import asyncio - - # Create a stop event - stop_event = threading.Event() - - # Function to continuously send movement commands - async def continuous_move(): - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - start_time = time.time() - try: - while not stop_event.is_set() and (time.time() - start_time) < self.duration: - self._robot.move(vector) - await asyncio.sleep(0.001) # Send commands at 1000Hz - # Always stop at the end - self._robot.move(Vector(0, 0, 0)) - finally: - loop.close() - - # Run movement in a separate thread with asyncio event loop - move_thread = threading.Thread(target=lambda: asyncio.run(continuous_move())) - move_thread.daemon = True - move_thread.start() - - # Wait for the full duration - time.sleep(self.duration) - stop_event.set() - move_thread.join(timeout=0.5) # Wait for thread to finish with timeout - else: - # Just execute the move command once for continuous movement - self._robot.move(vector) - return True + return self._robot.move(Vector(self.x, self.y, self.yaw), duration=self.duration) class Reverse(AbstractRobotSkill): - """Reverse the robot using direct velocity commands. - - This skill works with both ROS and WebRTC robot implementations. - """ + """Reverse the robot using direct velocity commands. Determine duration required based on user distance instructions.""" x: float = Field(..., description="Backward velocity (m/s). Positive values move backward.") y: float = Field(default=0.0, description="Left/right velocity (m/s)") yaw: float = Field(default=0.0, description="Rotational velocity (rad/s)") - duration: float = Field(default=0.0, description="How long to move (seconds). If 0, command is continuous") + duration: float = Field(default=0.0, description="How long to move (seconds).") def __call__(self): super().__call__() - from dimos.types.vector import Vector - # Use negative x for backward movement - vector = Vector(-self.x, self.y, self.yaw) - - # Handle duration for continuous movement - if self.duration > 0: - import time - import threading - import asyncio - - # Create a stop event - stop_event = threading.Event() - - # Function to continuously send movement commands - async def continuous_move(): - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - start_time = time.time() - try: - while not stop_event.is_set() and (time.time() - start_time) < self.duration: - self._robot.move(vector) - await asyncio.sleep(0.001) # Send commands at 1000Hz - # Always stop at the end - self._robot.move(Vector(0, 0, 0)) - finally: - loop.close() - - # Run movement in a separate thread with asyncio event loop - move_thread = threading.Thread(target=lambda: asyncio.run(continuous_move())) - move_thread.daemon = True - move_thread.start() - - # Wait for the full duration - time.sleep(self.duration) - stop_event.set() - move_thread.join(timeout=0.5) # Wait for thread to finish with timeout - else: - # Just execute the move command once for continuous movement - self._robot.move(vector) - return True + # Use move with negative x for backward movement + return self._robot.move(Vector(-self.x, self.y, self.yaw), duration=self.duration) class SpinLeft(AbstractRobotSkill): """Spin the robot left using degree commands.""" @@ -367,6 +310,6 @@ class Wait(AbstractSkill): seconds: float = Field(..., description="Seconds to wait") - def __call__(self): - time.sleep(self.seconds) + def __call__(self) -> str: + time.sleep(self.seconds) return f"Wait completed with length={self.seconds}s" diff --git a/dimos/robot/unitree_webrtc/connection.py b/dimos/robot/unitree_webrtc/connection.py index 48ea276883..4aee995c02 100644 --- a/dimos/robot/unitree_webrtc/connection.py +++ b/dimos/robot/unitree_webrtc/connection.py @@ -1,40 +1,92 @@ -import functools +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import asyncio +from dataclasses import dataclass +import functools import threading -from typing import TypeAlias, Literal -from dimos.utils.reactive import backpressure, callback_to_observable -from dimos.types.vector import Vector -from dimos.types.position import Position -from dimos.robot.unitree_webrtc.type.lidar import LidarMessage -from dimos.robot.unitree_webrtc.type.odometry import Odometry -from go2_webrtc_driver.webrtc_driver import Go2WebRTCConnection, WebRTCConnectionMethod # type: ignore[import-not-found] -from go2_webrtc_driver.constants import RTC_TOPIC, VUI_COLOR, SPORT_CMD -from reactivex.subject import Subject -from reactivex.observable import Observable +import time +from typing import Literal, TypeAlias + +from aiortc import MediaStreamTrack +from go2_webrtc_driver.constants import RTC_TOPIC, SPORT_CMD, VUI_COLOR +from go2_webrtc_driver.webrtc_driver import ( # type: ignore[import-not-found] + Go2WebRTCConnection, + WebRTCConnectionMethod, +) import numpy as np from reactivex import operators as ops -from aiortc import MediaStreamTrack -from dimos.robot.unitree_webrtc.type.lowstate import LowStateMsg -from dimos.robot.abstract_robot import AbstractRobot +from reactivex.observable import Observable +from reactivex.subject import Subject +from dimos.core import rpc +from dimos.core.resource import Resource +from dimos.msgs.geometry_msgs import Pose, Transform, Twist +from dimos.msgs.sensor_msgs import Image +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.robot.unitree_webrtc.type.lowstate import LowStateMsg +from dimos.robot.unitree_webrtc.type.odometry import Odometry +from dimos.utils.decorators.decorators import simple_mcache +from dimos.utils.reactive import backpressure, callback_to_observable VideoMessage: TypeAlias = np.ndarray[tuple[int, int, Literal[3]], np.uint8] -class WebRTCRobot(AbstractRobot): - def __init__(self, ip: str, mode: str = "ai"): +@dataclass +class SerializableVideoFrame: + """Pickleable wrapper for av.VideoFrame with all metadata""" + + data: np.ndarray + pts: int | None = None + time: float | None = None + dts: int | None = None + width: int | None = None + height: int | None = None + format: str | None = None + + @classmethod + def from_av_frame(cls, frame): + return cls( + data=frame.to_ndarray(format="rgb24"), + pts=frame.pts, + time=frame.time, + dts=frame.dts, + width=frame.width, + height=frame.height, + format=frame.format.name if hasattr(frame, "format") and frame.format else None, + ) + + def to_ndarray(self, format=None): + return self.data + + +class UnitreeWebRTCConnection(Resource): + def __init__(self, ip: str, mode: str = "ai") -> None: self.ip = ip self.mode = mode + self.stop_timer = None + self.cmd_vel_timeout = 0.2 self.conn = Go2WebRTCConnection(WebRTCConnectionMethod.LocalSTA, ip=self.ip) self.connect() - def connect(self): + def connect(self) -> None: self.loop = asyncio.new_event_loop() self.task = None self.connected_event = asyncio.Event() self.connection_ready = threading.Event() - async def async_connect(): + async def async_connect() -> None: await self.conn.connect() await self.conn.datachannel.disableTrafficSaving(True) @@ -50,7 +102,7 @@ async def async_connect(): while True: await asyncio.sleep(1) - def start_background_loop(): + def start_background_loop() -> None: asyncio.set_event_loop(self.loop) self.task = self.loop.create_task(async_connect()) self.loop.run_forever() @@ -60,17 +112,109 @@ def start_background_loop(): self.thread.start() self.connection_ready.wait() - def move(self, vector: Vector): - self.conn.datachannel.pub_sub.publish_without_callback( - RTC_TOPIC["WIRELESS_CONTROLLER"], - data={"lx": vector.x, "ly": vector.y, "rx": vector.z, "ry": 0}, - ) + def start(self) -> None: + pass + + def stop(self) -> None: + # Cancel timer + if self.stop_timer: + self.stop_timer.cancel() + self.stop_timer = None + + if self.task: + self.task.cancel() + + async def async_disconnect() -> None: + try: + await self.conn.disconnect() + except Exception: + pass + + if self.loop.is_running(): + asyncio.run_coroutine_threadsafe(async_disconnect(), self.loop) + + self.loop.call_soon_threadsafe(self.loop.stop) + + if self.thread.is_alive(): + self.thread.join(timeout=2.0) + + def move(self, twist: Twist, duration: float = 0.0) -> bool: + """Send movement command to the robot using Twist commands. + + Args: + twist: Twist message with linear and angular velocities + duration: How long to move (seconds). If 0, command is continuous + + Returns: + bool: True if command was sent successfully + """ + x, y, yaw = twist.linear.x, twist.linear.y, twist.angular.z + + # WebRTC coordinate mapping: + # x - Positive right, negative left + # y - positive forward, negative backwards + # yaw - Positive rotate right, negative rotate left + async def async_move() -> None: + self.conn.datachannel.pub_sub.publish_without_callback( + RTC_TOPIC["WIRELESS_CONTROLLER"], + data={"lx": -y, "ly": x, "rx": -yaw, "ry": 0}, + ) + + async def async_move_duration() -> None: + """Send movement commands continuously for the specified duration.""" + start_time = time.time() + sleep_time = 0.01 + + while time.time() - start_time < duration: + await async_move() + await asyncio.sleep(sleep_time) + + # Cancel existing timer and start a new one + if self.stop_timer: + self.stop_timer.cancel() + + # Auto-stop after 0.5 seconds if no new commands + self.stop_timer = threading.Timer(self.cmd_vel_timeout, self.stop) + self.stop_timer.daemon = True + self.stop_timer.start() + + try: + if duration > 0: + # Send continuous move commands for the duration + future = asyncio.run_coroutine_threadsafe(async_move_duration(), self.loop) + future.result() + # Stop after duration + self.stop() + else: + # Single command for continuous movement + future = asyncio.run_coroutine_threadsafe(async_move(), self.loop) + future.result() + return True + except Exception as e: + print(f"Failed to send movement command: {e}") + return False # Generic conversion of unitree subscription to Subject (used for all subs) def unitree_sub_stream(self, topic_name: str): + def subscribe_in_thread(cb) -> None: + # Run the subscription in the background thread that has the event loop + def run_subscription() -> None: + self.conn.datachannel.pub_sub.subscribe(topic_name, cb) + + # Use call_soon_threadsafe to run in the background thread + self.loop.call_soon_threadsafe(run_subscription) + + def unsubscribe_in_thread(cb) -> None: + # Run the unsubscription in the background thread that has the event loop + def run_unsubscription() -> None: + self.conn.datachannel.pub_sub.unsubscribe(topic_name) + + # Use call_soon_threadsafe to run in the background thread + self.loop.call_soon_threadsafe(run_unsubscription) + return callback_to_observable( - start=lambda cb: self.conn.datachannel.pub_sub.subscribe(topic_name, cb), - stop=lambda: self.conn.datachannel.pub_sub.unsubscribe(topic_name), + start=subscribe_in_thread, + stop=unsubscribe_in_thread, ) # Generic sync API call (we jump into the client thread) @@ -80,38 +224,67 @@ def publish_request(self, topic: str, data: dict): ) return future.result() - @functools.cache + @simple_mcache def raw_lidar_stream(self) -> Subject[LidarMessage]: return backpressure(self.unitree_sub_stream(RTC_TOPIC["ULIDAR_ARRAY"])) - @functools.cache - def raw_odom_stream(self) -> Subject[Position]: + @simple_mcache + def raw_odom_stream(self) -> Subject[Pose]: return backpressure(self.unitree_sub_stream(RTC_TOPIC["ROBOTODOM"])) - @functools.cache + @simple_mcache def lidar_stream(self) -> Subject[LidarMessage]: - return backpressure(self.raw_lidar_stream().pipe(ops.map(lambda raw_frame: LidarMessage.from_msg(raw_frame)))) + return backpressure( + self.raw_lidar_stream().pipe( + ops.map(lambda raw_frame: LidarMessage.from_msg(raw_frame, ts=time.time())) + ) + ) + + @simple_mcache + def tf_stream(self) -> Subject[Transform]: + base_link = functools.partial(Transform.from_pose, "base_link") + return backpressure(self.odom_stream().pipe(ops.map(base_link))) - @functools.cache - def odom_stream(self) -> Subject[Position]: + @simple_mcache + def odom_stream(self) -> Subject[Pose]: return backpressure(self.raw_odom_stream().pipe(ops.map(Odometry.from_msg))) - @functools.cache + @simple_mcache + def video_stream(self) -> Observable[Image]: + return backpressure( + self.raw_video_stream().pipe( + ops.filter(lambda frame: frame is not None), + ops.map( + lambda frame: Image.from_numpy( + # np.ascontiguousarray(frame.to_ndarray("rgb24")), + frame.to_ndarray(format="rgb24"), + frame_id="camera_optical", + ) + ), + ) + ) + + @simple_mcache def lowstate_stream(self) -> Subject[LowStateMsg]: return backpressure(self.unitree_sub_stream(RTC_TOPIC["LOW_STATE"])) def standup_ai(self): return self.publish_request(RTC_TOPIC["SPORT_MOD"], {"api_id": SPORT_CMD["BalanceStand"]}) - def standup_normal(self): - return self.publish_request(RTC_TOPIC["SPORT_MOD"], {"api_id": SPORT_CMD["StandUp"]}) + def standup_normal(self) -> bool: + self.publish_request(RTC_TOPIC["SPORT_MOD"], {"api_id": SPORT_CMD["StandUp"]}) + time.sleep(0.5) + self.publish_request(RTC_TOPIC["SPORT_MOD"], {"api_id": SPORT_CMD["RecoveryStand"]}) + return True + @rpc def standup(self): if self.mode == "ai": return self.standup_ai() else: return self.standup_normal() + @rpc def liedown(self): return self.publish_request(RTC_TOPIC["SPORT_MOD"], {"api_id": SPORT_CMD["StandDown"]}) @@ -121,6 +294,7 @@ async def handstand(self): {"api_id": SPORT_CMD["Standup"], "parameter": {"data": True}}, ) + @rpc def color(self, color: VUI_COLOR = VUI_COLOR.RED, colortime: int = 60) -> bool: return self.publish_request( RTC_TOPIC["VUI"], @@ -133,8 +307,8 @@ def color(self, color: VUI_COLOR = VUI_COLOR.RED, colortime: int = 60) -> bool: }, ) - @functools.lru_cache(maxsize=None) - def video_stream(self) -> Observable[VideoMessage]: + @simple_mcache + def raw_video_stream(self) -> Observable[VideoMessage]: subject: Subject[VideoMessage] = Subject() stop_event = threading.Event() @@ -143,17 +317,28 @@ async def accept_track(track: MediaStreamTrack) -> VideoMessage: if stop_event.is_set(): return frame = await track.recv() - subject.on_next(frame.to_ndarray(format="bgr24")) + serializable_frame = SerializableVideoFrame.from_av_frame(frame) + subject.on_next(serializable_frame) self.conn.video.add_track_callback(accept_track) - self.conn.video.switchVideoChannel(True) - def stop(cb): + # Run the video channel switching in the background thread + def switch_video_channel() -> None: + self.conn.video.switchVideoChannel(True) + + self.loop.call_soon_threadsafe(switch_video_channel) + + def stop() -> None: stop_event.set() # Signal the loop to stop self.conn.video.track_callbacks.remove(accept_track) - self.conn.video.switchVideoChannel(False) - return backpressure(subject.pipe(ops.finally_action(stop))) + # Run the video channel switching off in the background thread + def switch_video_channel_off() -> None: + self.conn.video.switchVideoChannel(False) + + self.loop.call_soon_threadsafe(switch_video_channel_off) + + return subject.pipe(ops.finally_action(stop)) def get_video_stream(self, fps: int = 30) -> Observable[VideoMessage]: """Get the video stream from the robot's camera. @@ -173,23 +358,43 @@ def get_video_stream(self, fps: int = 30) -> Observable[VideoMessage]: if stream is None: print("Warning: Video stream is not available") return stream + except Exception as e: print(f"Error getting video stream: {e}") return None - def stop(self): + def stop(self) -> bool: + """Stop the robot's movement. + + Returns: + bool: True if stop command was sent successfully + """ + # Cancel timer since we're explicitly stopping + if self.stop_timer: + self.stop_timer.cancel() + self.stop_timer = None + + return self.move(Twist()) + + def disconnect(self) -> None: + """Disconnect from the robot and clean up resources.""" + # Cancel timer + if self.stop_timer: + self.stop_timer.cancel() + self.stop_timer = None + if hasattr(self, "task") and self.task: self.task.cancel() if hasattr(self, "conn"): - async def disconnect(): + async def async_disconnect() -> None: try: await self.conn.disconnect() except: pass if hasattr(self, "loop") and self.loop.is_running(): - asyncio.run_coroutine_threadsafe(disconnect(), self.loop) + asyncio.run_coroutine_threadsafe(async_disconnect(), self.loop) if hasattr(self, "loop") and self.loop.is_running(): self.loop.call_soon_threadsafe(self.loop.stop) diff --git a/dimos/robot/unitree_webrtc/demo_remapping.py b/dimos/robot/unitree_webrtc/demo_remapping.py new file mode 100644 index 0000000000..a0b594f95a --- /dev/null +++ b/dimos/robot/unitree_webrtc/demo_remapping.py @@ -0,0 +1,30 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.core.transport import LCMTransport +from dimos.msgs.sensor_msgs import Image +from dimos.robot.unitree_webrtc.unitree_go2 import ConnectionModule +from dimos.robot.unitree_webrtc.unitree_go2_blueprints import standard + +remapping = standard.remappings( + [ + (ConnectionModule, "color_image", "rgb_image"), + ] +) + +remapping_and_transport = remapping.transports( + { + ("rgb_image", Image): LCMTransport("/go2/color_image", Image), + } +) diff --git a/dimos/robot/unitree_webrtc/depth_module.py b/dimos/robot/unitree_webrtc/depth_module.py new file mode 100644 index 0000000000..9e9b57b24b --- /dev/null +++ b/dimos/robot/unitree_webrtc/depth_module.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import threading +import time + +from dimos_lcm.sensor_msgs import CameraInfo +import numpy as np + +from dimos.core import In, Module, Out, rpc +from dimos.core.global_config import GlobalConfig +from dimos.msgs.sensor_msgs import Image, ImageFormat +from dimos.utils.logging_config import setup_logger + +logger = setup_logger(__name__) + + +class DepthModule(Module): + """ + Depth module for Unitree Go2 that processes RGB images to generate depth using Metric3D. + + Subscribes to: + - /go2/color_image: RGB camera images from Unitree + - /go2/camera_info: Camera calibration information + + Publishes: + - /go2/depth_image: Depth images generated by Metric3D + """ + + # LCM inputs + color_image: In[Image] = None + camera_info: In[CameraInfo] = None + + # LCM outputs + depth_image: Out[Image] = None + + def __init__( + self, + gt_depth_scale: float = 0.5, + global_config: GlobalConfig | None = None, + **kwargs, + ) -> None: + """ + Initialize Depth Module. + + Args: + gt_depth_scale: Ground truth depth scaling factor + """ + super().__init__(**kwargs) + + self.camera_intrinsics = None + self.gt_depth_scale = gt_depth_scale + self.metric3d = None + self._camera_info_received = False + + # Processing state + self._running = False + self._latest_frame = None + self._last_image = None + self._last_timestamp = None + self._last_depth = None + self._cannot_process_depth = False + + # Threading + self._processing_thread: threading.Thread | None = None + self._stop_processing = threading.Event() + + if global_config: + if global_config.use_simulation: + self.gt_depth_scale = 1.0 + + @rpc + def start(self) -> None: + super().start() + + if self._running: + logger.warning("Camera module already running") + return + + # Set running flag before starting + self._running = True + + # Subscribe to video and camera info inputs + self.color_image.subscribe(self._on_video) + self.camera_info.subscribe(self._on_camera_info) + + # Start processing thread + self._start_processing_thread() + + logger.info("Depth module started") + + @rpc + def stop(self) -> None: + if not self._running: + return + + self._running = False + self._stop_processing.set() + + # Wait for thread to finish + if self._processing_thread and self._processing_thread.is_alive(): + self._processing_thread.join(timeout=2.0) + + super().stop() + + def _on_camera_info(self, msg: CameraInfo) -> None: + """Process camera info to extract intrinsics.""" + if self.metric3d is not None: + return # Already initialized + + try: + # Extract intrinsics from camera matrix K + K = msg.K + fx = K[0] + fy = K[4] + cx = K[2] + cy = K[5] + + self.camera_intrinsics = [fx, fy, cx, cy] + + # Initialize Metric3D with camera intrinsics + from dimos.models.depth.metric3d import Metric3D + + self.metric3d = Metric3D(camera_intrinsics=self.camera_intrinsics) + self._camera_info_received = True + + logger.info( + f"Initialized Metric3D with intrinsics from camera_info: {self.camera_intrinsics}" + ) + + except Exception as e: + logger.error(f"Error processing camera info: {e}") + + def _on_video(self, msg: Image) -> None: + """Store latest video frame for processing.""" + if not self._running: + return + + # Simply store the latest frame - processing happens in main loop + self._latest_frame = msg + logger.debug( + f"Received video frame: format={msg.format}, shape={msg.data.shape if hasattr(msg.data, 'shape') else 'unknown'}" + ) + + def _start_processing_thread(self) -> None: + """Start the processing thread.""" + self._stop_processing.clear() + self._processing_thread = threading.Thread(target=self._main_processing_loop, daemon=True) + self._processing_thread.start() + logger.info("Started depth processing thread") + + def _main_processing_loop(self) -> None: + """Main processing loop that continuously processes latest frames.""" + logger.info("Starting main processing loop") + + while not self._stop_processing.is_set(): + # Process latest frame if available + if self._latest_frame is not None: + try: + msg = self._latest_frame + self._latest_frame = None # Clear to avoid reprocessing + # Store for publishing + self._last_image = msg.data + self._last_timestamp = msg.ts if msg.ts else time.time() + # Process depth + self._process_depth(self._last_image) + + except Exception as e: + logger.error(f"Error in main processing loop: {e}", exc_info=True) + else: + # Small sleep to avoid busy waiting + time.sleep(0.001) + + logger.info("Main processing loop stopped") + + def _process_depth(self, img_array: np.ndarray) -> None: + """Process depth estimation using Metric3D.""" + if self._cannot_process_depth: + self._last_depth = None + return + + # Wait for camera info to initialize Metric3D + if self.metric3d is None: + logger.debug("Waiting for camera_info to initialize Metric3D") + return + + try: + logger.debug(f"Processing depth for image shape: {img_array.shape}") + + # Generate depth map + depth_array = self.metric3d.infer_depth(img_array) * self.gt_depth_scale + + self._last_depth = depth_array + logger.debug(f"Generated depth map shape: {depth_array.shape}") + + self._publish_depth() + + except Exception as e: + logger.error(f"Error processing depth: {e}") + self._cannot_process_depth = True + + def _publish_depth(self) -> None: + """Publish depth image.""" + if not self._running: + return + + try: + # Publish depth image + if self._last_depth is not None: + # Convert depth to uint16 (millimeters) for more efficient storage + # Clamp to valid range [0, 65.535] meters before converting + depth_clamped = np.clip(self._last_depth, 0, 65.535) + depth_uint16 = (depth_clamped * 1000).astype(np.uint16) + depth_msg = Image( + data=depth_uint16, + format=ImageFormat.DEPTH16, # Use DEPTH16 format for uint16 depth + frame_id="camera_link", + ts=self._last_timestamp, + ) + self.depth_image.publish(depth_msg) + logger.debug(f"Published depth image (uint16): shape={depth_uint16.shape}") + + except Exception as e: + logger.error(f"Error publishing depth data: {e}", exc_info=True) + + +depth_module = DepthModule.blueprint + + +__all__ = ["DepthModule", "depth_module"] diff --git a/dimos/robot/unitree_webrtc/g1_joystick_module.py b/dimos/robot/unitree_webrtc/g1_joystick_module.py new file mode 100644 index 0000000000..2c6a5e64e5 --- /dev/null +++ b/dimos/robot/unitree_webrtc/g1_joystick_module.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Pygame Joystick Module for testing G1 humanoid control.""" + +import os +import threading + +# Force X11 driver to avoid OpenGL threading issues +os.environ["SDL_VIDEODRIVER"] = "x11" + +from dimos.core import Module, Out, rpc +from dimos.msgs.geometry_msgs import Twist, Vector3 + + +class G1JoystickModule(Module): + """Pygame-based joystick control module for G1 humanoid testing. + + Outputs standard Twist messages on /cmd_vel for velocity control. + Simplified version without mode switching since G1 handles that differently. + """ + + twist_out: Out[Twist] = None # Standard velocity commands + + def __init__(self, *args, **kwargs) -> None: + Module.__init__(self, *args, **kwargs) + self.pygame_ready = False + self.running = False + + @rpc + def start(self) -> bool: + """Initialize pygame and start control loop.""" + super().start() + + try: + import pygame + except ImportError: + print("ERROR: pygame not installed. Install with: pip install pygame") + return False + + self.keys_held = set() + self.pygame_ready = True + self.running = True + + # Start pygame loop in background thread + self._thread = threading.Thread(target=self._pygame_loop, daemon=True) + self._thread.start() + + return True + + @rpc + def stop(self) -> None: + super().stop() + + self.running = False + self.pygame_ready = False + + stop_twist = Twist() + stop_twist.linear = Vector3(0, 0, 0) + stop_twist.angular = Vector3(0, 0, 0) + + self._thread.join(2) + + self.twist_out.publish(stop_twist) + + def _pygame_loop(self) -> None: + """Main pygame event loop - ALL pygame operations happen here.""" + import pygame + + pygame.init() + self.screen = pygame.display.set_mode((500, 400), pygame.SWSURFACE) + pygame.display.set_caption("G1 Humanoid Joystick Control") + self.clock = pygame.time.Clock() + self.font = pygame.font.Font(None, 24) + + print("G1 JoystickModule started - Focus pygame window to control") + print("Controls:") + print(" WS = Forward/Back") + print(" AD = Turn Left/Right") + print(" Space = Emergency Stop") + print(" ESC = Quit") + + while self.running and self.pygame_ready: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + self.running = False + elif event.type == pygame.KEYDOWN: + self.keys_held.add(event.key) + + if event.key == pygame.K_SPACE: + # Emergency stop - clear all keys and send zero twist + self.keys_held.clear() + stop_twist = Twist() + stop_twist.linear = Vector3(0, 0, 0) + stop_twist.angular = Vector3(0, 0, 0) + self.twist_out.publish(stop_twist) + print("EMERGENCY STOP!") + elif event.key == pygame.K_ESCAPE: + # ESC quits + self.running = False + + elif event.type == pygame.KEYUP: + self.keys_held.discard(event.key) + + # Generate Twist message from held keys + twist = Twist() + twist.linear = Vector3(0, 0, 0) + twist.angular = Vector3(0, 0, 0) + + # Forward/backward (W/S) + if pygame.K_w in self.keys_held: + twist.linear.x = 0.5 + if pygame.K_s in self.keys_held: + twist.linear.x = -0.5 + + # Turning (A/D) + if pygame.K_a in self.keys_held: + twist.angular.z = 0.5 + if pygame.K_d in self.keys_held: + twist.angular.z = -0.5 + + # Always publish twist at 50Hz + self.twist_out.publish(twist) + + self._update_display(twist) + + # Maintain 50Hz rate + self.clock.tick(50) + + pygame.quit() + print("G1 JoystickModule stopped") + + def _update_display(self, twist) -> None: + """Update pygame window with current status.""" + import pygame + + self.screen.fill((30, 30, 30)) + + y_pos = 20 + + texts = [ + "G1 Humanoid Control", + "", + f"Linear X (Forward/Back): {twist.linear.x:+.2f} m/s", + f"Angular Z (Turn L/R): {twist.angular.z:+.2f} rad/s", + "", + "Keys: " + ", ".join([pygame.key.name(k).upper() for k in self.keys_held if k < 256]), + ] + + for text in texts: + if text: + color = (0, 255, 255) if text == "G1 Humanoid Control" else (255, 255, 255) + surf = self.font.render(text, True, color) + self.screen.blit(surf, (20, y_pos)) + y_pos += 30 + + if twist.linear.x != 0 or twist.linear.y != 0 or twist.angular.z != 0: + pygame.draw.circle(self.screen, (255, 0, 0), (450, 30), 15) # Red = moving + else: + pygame.draw.circle(self.screen, (0, 255, 0), (450, 30), 15) # Green = stopped + + y_pos = 300 + help_texts = ["WS: Move | AD: Turn", "Space: E-Stop | ESC: Quit"] + for text in help_texts: + surf = self.font.render(text, True, (150, 150, 150)) + self.screen.blit(surf, (20, y_pos)) + y_pos += 25 + + pygame.display.flip() diff --git a/dimos/robot/unitree_webrtc/g1_run.py b/dimos/robot/unitree_webrtc/g1_run.py new file mode 100644 index 0000000000..b8c0bc77c7 --- /dev/null +++ b/dimos/robot/unitree_webrtc/g1_run.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Run script for Unitree G1 humanoid robot with Claude agent integration. +Provides interaction capabilities with natural language interface and ZED vision. +""" + +import argparse +import os +import sys +import time + +from dotenv import load_dotenv +import reactivex as rx +import reactivex.operators as ops + +from dimos.agents.claude_agent import ClaudeAgent +from dimos.robot.unitree_webrtc.unitree_g1 import UnitreeG1 +from dimos.robot.unitree_webrtc.unitree_skills import MyUnitreeSkills +from dimos.skills.kill_skill import KillSkill +from dimos.skills.navigation import GetPose +from dimos.utils.logging_config import setup_logger +from dimos.web.robot_web_interface import RobotWebInterface + +logger = setup_logger("dimos.robot.unitree_webrtc.g1_run") + +# Load environment variables +load_dotenv() + +# System prompt - loaded from prompt.txt +SYSTEM_PROMPT_PATH = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), + "assets/agent/prompt.txt", +) + + +def main(): + """Main entry point.""" + # Parse command line arguments + parser = argparse.ArgumentParser(description="Unitree G1 Robot with Claude Agent") + parser.add_argument("--replay", type=str, help="Path to recording to replay") + parser.add_argument("--record", type=str, help="Path to save recording") + args = parser.parse_args() + + print("\n" + "=" * 60) + print("Unitree G1 Humanoid Robot with Claude Agent") + print("=" * 60) + print("\nThis system integrates:") + print(" - Unitree G1 humanoid robot") + print(" - ZED camera for stereo vision and depth") + print(" - WebRTC communication for robot control") + print(" - Claude AI for natural language understanding") + print(" - Web interface with text and voice input") + + if args.replay: + print(f"\nREPLAY MODE: Replaying from {args.replay}") + elif args.record: + print(f"\nRECORDING MODE: Recording to {args.record}") + + print("\nStarting system...\n") + + # Check for API key + if not os.getenv("ANTHROPIC_API_KEY"): + print("WARNING: ANTHROPIC_API_KEY not found in environment") + print("Please set your API key in .env file or environment") + sys.exit(1) + + # Check for robot IP (not needed in replay mode) + robot_ip = os.getenv("ROBOT_IP") + if not robot_ip and not args.replay: + print("ERROR: ROBOT_IP not found in environment") + print("Please set the robot IP address in .env file") + sys.exit(1) + + # Load system prompt + try: + with open(SYSTEM_PROMPT_PATH) as f: + system_prompt = f.read() + except FileNotFoundError: + logger.error(f"System prompt file not found at {SYSTEM_PROMPT_PATH}") + sys.exit(1) + + logger.info("Starting Unitree G1 Robot with Agent") + + # Create robot instance with recording/replay support + robot = UnitreeG1( + ip=robot_ip or "0.0.0.0", # Dummy IP for replay mode + recording_path=args.record, + replay_path=args.replay, + ) + robot.start() + time.sleep(3) + + try: + logger.info("Robot initialized successfully") + + # Set up minimal skill library for G1 with robot_type="g1" + skills = MyUnitreeSkills(robot=robot, robot_type="g1") + skills.add(KillSkill) + skills.add(GetPose) + + # Create skill instances + skills.create_instance("KillSkill", robot=robot, skill_library=skills) + skills.create_instance("GetPose", robot=robot) + + logger.info(f"Skills registered: {[skill.__name__ for skill in skills.get_class_skills()]}") + + # Set up streams for agent and web interface + agent_response_subject = rx.subject.Subject() + agent_response_stream = agent_response_subject.pipe(ops.share()) + audio_subject = rx.subject.Subject() + + # Set up streams for web interface + text_streams = { + "agent_responses": agent_response_stream, + } + + # Create web interface + try: + web_interface = RobotWebInterface( + port=5555, text_streams=text_streams, audio_subject=audio_subject + ) + logger.info("Web interface created successfully") + except Exception as e: + logger.error(f"Failed to create web interface: {e}") + raise + + # Create Claude agent with minimal configuration + agent = ClaudeAgent( + dev_name="unitree_g1_agent", + input_query_stream=web_interface.query_stream, # Text input from web + skills=skills, + system_query=system_prompt, + model_name="claude-3-5-haiku-latest", + thinking_budget_tokens=0, + max_output_tokens_per_request=8192, + ) + + # Subscribe to agent responses + agent.get_response_observable().subscribe(lambda x: agent_response_subject.on_next(x)) + + logger.info("=" * 60) + logger.info("Unitree G1 Agent Ready!") + logger.info("Web interface available at: http://localhost:5555") + logger.info("You can:") + logger.info(" - Type commands in the web interface") + logger.info(" - Use voice commands") + logger.info(" - Ask the robot to move or perform actions") + logger.info(" - Ask the robot to describe what it sees") + logger.info("=" * 60) + + # Run web interface (this blocks) + web_interface.run() + + except KeyboardInterrupt: + logger.info("Keyboard interrupt received") + except Exception as e: + logger.error(f"Error running robot: {e}") + import traceback + + traceback.print_exc() + finally: + logger.info("Shutting down...") + logger.info("Shutdown complete") + + +if __name__ == "__main__": + main() diff --git a/dimos/robot/unitree_webrtc/modular/__init__.py b/dimos/robot/unitree_webrtc/modular/__init__.py new file mode 100644 index 0000000000..d823cd796e --- /dev/null +++ b/dimos/robot/unitree_webrtc/modular/__init__.py @@ -0,0 +1,2 @@ +from dimos.robot.unitree_webrtc.modular.connection_module import deploy_connection +from dimos.robot.unitree_webrtc.modular.navigation import deploy_navigation diff --git a/dimos/robot/unitree_webrtc/modular/connection_module.py b/dimos/robot/unitree_webrtc/modular/connection_module.py new file mode 100644 index 0000000000..bad9af22a1 --- /dev/null +++ b/dimos/robot/unitree_webrtc/modular/connection_module.py @@ -0,0 +1,338 @@ +#!/usr/bin/env python3 + +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from dataclasses import dataclass +import functools +import logging +import os +import queue +import warnings + +from dimos_lcm.sensor_msgs import CameraInfo +import reactivex as rx +from reactivex import operators as ops +from reactivex.observable import Observable + +from dimos.agents2 import Output, Reducer, Stream, skill +from dimos.constants import DEFAULT_CAPACITY_COLOR_IMAGE +from dimos.core import DimosCluster, In, LCMTransport, Module, ModuleConfig, Out, pSHMTransport, rpc +from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Transform, Twist, Vector3 +from dimos.msgs.sensor_msgs.Image import Image +from dimos.msgs.std_msgs import Header +from dimos.robot.unitree_webrtc.connection import UnitreeWebRTCConnection +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.utils.data import get_data +from dimos.utils.logging_config import setup_logger +from dimos.utils.testing import TimedSensorReplay, TimedSensorStorage + +logger = setup_logger("dimos.robot.unitree_webrtc.unitree_go2", level=logging.INFO) + +# Suppress verbose loggers +logging.getLogger("aiortc.codecs.h264").setLevel(logging.ERROR) +logging.getLogger("lcm_foxglove_bridge").setLevel(logging.ERROR) +logging.getLogger("websockets.server").setLevel(logging.ERROR) +logging.getLogger("FoxgloveServer").setLevel(logging.ERROR) +logging.getLogger("asyncio").setLevel(logging.ERROR) +logging.getLogger("root").setLevel(logging.WARNING) + + +# Suppress warnings +warnings.filterwarnings("ignore", message="coroutine.*was never awaited") +warnings.filterwarnings("ignore", message="H264Decoder.*failed to decode") + +image_resize_factor = 1 +originalwidth, originalheight = (1280, 720) + + +class FakeRTC(UnitreeWebRTCConnection): + dir_name = "unitree_go2_office_walk2" + + # we don't want UnitreeWebRTCConnection to init + def __init__( + self, + **kwargs, + ) -> None: + get_data(self.dir_name) + self.replay_config = { + "loop": kwargs.get("loop"), + "seek": kwargs.get("seek"), + "duration": kwargs.get("duration"), + } + + def connect(self) -> None: + pass + + def start(self) -> None: + pass + + def standup(self) -> None: + print("standup suppressed") + + def liedown(self) -> None: + print("liedown suppressed") + + @functools.cache + def lidar_stream(self): + print("lidar stream start") + lidar_store = TimedSensorReplay(f"{self.dir_name}/lidar") + return lidar_store.stream(**self.replay_config) + + @functools.cache + def odom_stream(self): + print("odom stream start") + odom_store = TimedSensorReplay(f"{self.dir_name}/odom") + return odom_store.stream(**self.replay_config) + + # we don't have raw video stream in the data set + @functools.cache + def video_stream(self): + print("video stream start") + video_store = TimedSensorReplay(f"{self.dir_name}/video") + + return video_store.stream(**self.replay_config) + + def move(self, vector: Twist, duration: float = 0.0) -> None: + pass + + def publish_request(self, topic: str, data: dict): + """Fake publish request for testing.""" + return {"status": "ok", "message": "Fake publish"} + + +@dataclass +class ConnectionModuleConfig(ModuleConfig): + ip: str | None = None + connection_type: str = "fake" # or "fake" or "mujoco" + loop: bool = False # For fake connection + speed: float = 1.0 # For fake connection + + +class ConnectionModule(Module): + camera_info: Out[CameraInfo] = None + odom: Out[PoseStamped] = None + lidar: Out[LidarMessage] = None + video: Out[Image] = None + movecmd: In[Twist] = None + + connection = None + + default_config = ConnectionModuleConfig + + # mega temporary, skill should have a limit decorator for number of + # parallel calls + video_running: bool = False + + def __init__(self, connection_type: str = "webrtc", *args, **kwargs) -> None: + self.connection_config = kwargs + self.connection_type = connection_type + Module.__init__(self, *args, **kwargs) + + @skill(stream=Stream.passive, output=Output.image, reducer=Reducer.latest) + def video_stream_tool(self) -> Image: + """implicit video stream skill, don't call this directly""" + if self.video_running: + return "video stream already running" + self.video_running = True + _queue = queue.Queue(maxsize=1) + self.connection.video_stream().subscribe(_queue.put) + + yield from iter(_queue.get, None) + + @rpc + def record(self, recording_name: str) -> None: + lidar_store: TimedSensorStorage = TimedSensorStorage(f"{recording_name}/lidar") + lidar_store.save_stream(self.connection.lidar_stream()).subscribe(lambda x: x) + + odom_store: TimedSensorStorage = TimedSensorStorage(f"{recording_name}/odom") + odom_store.save_stream(self.connection.odom_stream()).subscribe(lambda x: x) + + video_store: TimedSensorStorage = TimedSensorStorage(f"{recording_name}/video") + video_store.save_stream(self.connection.video_stream()).subscribe(lambda x: x) + + @rpc + def start(self): + """Start the connection and subscribe to sensor streams.""" + + super().start() + + match self.connection_type: + case "webrtc": + self.connection = UnitreeWebRTCConnection(**self.connection_config) + case "fake": + self.connection = FakeRTC(**self.connection_config, seek=12.0) + case "mujoco": + from dimos.robot.unitree_webrtc.mujoco_connection import MujocoConnection + + self.connection = MujocoConnection(**self.connection_config) + self.connection.start() + case _: + raise ValueError(f"Unknown connection type: {self.connection_type}") + + unsub = self.connection.odom_stream().subscribe( + lambda odom: self._publish_tf(odom) and self.odom.publish(odom) + ) + self._disposables.add(unsub) + + # Connect sensor streams to outputs + unsub = self.connection.lidar_stream().subscribe(self.lidar.publish) + self._disposables.add(unsub) + + # self.connection.lidar_stream().subscribe(lambda lidar: print("LIDAR", lidar.ts)) + # self.connection.video_stream().subscribe(lambda video: print("IMAGE", video.ts)) + # self.connection.odom_stream().subscribe(lambda odom: print("ODOM", odom.ts)) + + def resize(image: Image) -> Image: + return image.resize( + int(originalwidth / image_resize_factor), int(originalheight / image_resize_factor) + ) + + unsub = self.connection.video_stream().subscribe(self.video.publish) + self._disposables.add(unsub) + unsub = self.camera_info_stream().subscribe(self.camera_info.publish) + self._disposables.add(unsub) + unsub = self.movecmd.subscribe(self.connection.move) + self._disposables.add(unsub) + + @rpc + def stop(self) -> None: + if self.connection: + self.connection.stop() + + super().stop() + + @classmethod + def _odom_to_tf(cls, odom: PoseStamped) -> list[Transform]: + camera_link = Transform( + translation=Vector3(0.3, 0.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="base_link", + child_frame_id="camera_link", + ts=odom.ts, + ) + + camera_optical = Transform( + translation=Vector3(0.0, 0.0, 0.0), + rotation=Quaternion(-0.5, 0.5, -0.5, 0.5), + frame_id="camera_link", + child_frame_id="camera_optical", + ts=odom.ts, + ) + + sensor = Transform( + translation=Vector3(0.0, 0.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="world", + child_frame_id="sensor", + ts=odom.ts, + ) + + return [ + Transform.from_pose("base_link", odom), + camera_link, + camera_optical, + sensor, + ] + + def _publish_tf(self, msg) -> None: + self.odom.publish(msg) + self.tf.publish(*self._odom_to_tf(msg)) + + @rpc + def publish_request(self, topic: str, data: dict): + """Publish a request to the WebRTC connection. + Args: + topic: The RTC topic to publish to + data: The data dictionary to publish + Returns: + The result of the publish request + """ + return self.connection.publish_request(topic, data) + + @classmethod + def _camera_info(cls) -> Out[CameraInfo]: + fx, fy, cx, cy = list( + map( + lambda x: int(x / image_resize_factor), + [819.553492, 820.646595, 625.284099, 336.808987], + ) + ) + width, height = tuple( + map( + lambda x: int(x / image_resize_factor), + [originalwidth, originalheight], + ) + ) + + # Camera matrix K (3x3) + K = [fx, 0, cx, 0, fy, cy, 0, 0, 1] + + # No distortion coefficients for now + D = [0.0, 0.0, 0.0, 0.0, 0.0] + + # Identity rotation matrix + R = [1, 0, 0, 0, 1, 0, 0, 0, 1] + + # Projection matrix P (3x4) + P = [fx, 0, cx, 0, 0, fy, cy, 0, 0, 0, 1, 0] + + base_msg = { + "D_length": len(D), + "height": height, + "width": width, + "distortion_model": "plumb_bob", + "D": D, + "K": K, + "R": R, + "P": P, + "binning_x": 0, + "binning_y": 0, + } + + return CameraInfo(**base_msg, header=Header("camera_optical")) + + @functools.cache + def camera_info_stream(self) -> Observable[CameraInfo]: + return rx.interval(1).pipe(ops.map(lambda _: self._camera_info())) + + +def deploy_connection(dimos: DimosCluster, **kwargs): + foxglove_bridge = dimos.deploy(FoxgloveBridge) + foxglove_bridge.start() + + connection = dimos.deploy( + ConnectionModule, + ip=os.getenv("ROBOT_IP"), + connection_type=os.getenv("CONNECTION_TYPE", "fake"), + **kwargs, + ) + + connection.odom.transport = LCMTransport("/odom", PoseStamped) + + connection.video.transport = pSHMTransport( + "/image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE + ) + + connection.lidar.transport = pSHMTransport( + "/lidar", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE + ) + + connection.video.transport = LCMTransport("/image", Image) + connection.lidar.transport = LCMTransport("/lidar", LidarMessage) + connection.movecmd.transport = LCMTransport("/cmd_vel", Twist) + connection.camera_info.transport = LCMTransport("/camera_info", CameraInfo) + + return connection diff --git a/dimos/robot/unitree_webrtc/modular/detect.py b/dimos/robot/unitree_webrtc/modular/detect.py new file mode 100644 index 0000000000..46f561b109 --- /dev/null +++ b/dimos/robot/unitree_webrtc/modular/detect.py @@ -0,0 +1,180 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pickle + +from dimos_lcm.sensor_msgs import CameraInfo + +from dimos.msgs.sensor_msgs import Image +from dimos.msgs.std_msgs import Header +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.robot.unitree_webrtc.type.odometry import Odometry + +image_resize_factor = 1 +originalwidth, originalheight = (1280, 720) + + +def camera_info() -> CameraInfo: + fx, fy, cx, cy = list( + map( + lambda x: int(x / image_resize_factor), + [819.553492, 820.646595, 625.284099, 336.808987], + ) + ) + width, height = tuple( + map( + lambda x: int(x / image_resize_factor), + [originalwidth, originalheight], + ) + ) + + # Camera matrix K (3x3) + K = [fx, 0, cx, 0, fy, cy, 0, 0, 1] + + # No distortion coefficients for now + D = [0.0, 0.0, 0.0, 0.0, 0.0] + + # Identity rotation matrix + R = [1, 0, 0, 0, 1, 0, 0, 0, 1] + + # Projection matrix P (3x4) + P = [fx, 0, cx, 0, 0, fy, cy, 0, 0, 0, 1, 0] + + base_msg = { + "D_length": len(D), + "height": height, + "width": width, + "distortion_model": "plumb_bob", + "D": D, + "K": K, + "R": R, + "P": P, + "binning_x": 0, + "binning_y": 0, + } + + return CameraInfo( + **base_msg, + header=Header("camera_optical"), + ) + + +def transform_chain(odom_frame: Odometry) -> list: + from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 + from dimos.protocol.tf import TF + + camera_link = Transform( + translation=Vector3(0.3, 0.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="base_link", + child_frame_id="camera_link", + ts=odom_frame.ts, + ) + + camera_optical = Transform( + translation=Vector3(0.0, 0.0, 0.0), + rotation=Quaternion(-0.5, 0.5, -0.5, 0.5), + frame_id="camera_link", + child_frame_id="camera_optical", + ts=camera_link.ts, + ) + + tf = TF() + tf.publish( + Transform.from_pose("base_link", odom_frame), + camera_link, + camera_optical, + ) + + return tf + + +def broadcast( + timestamp: float, + lidar_frame: LidarMessage, + video_frame: Image, + odom_frame: Odometry, + detections, + annotations, +) -> None: + from dimos_lcm.foxglove_msgs.ImageAnnotations import ImageAnnotations + + from dimos.core import LCMTransport + from dimos.msgs.geometry_msgs import PoseStamped + + lidar_transport = LCMTransport("/lidar", LidarMessage) + odom_transport = LCMTransport("/odom", PoseStamped) + video_transport = LCMTransport("/image", Image) + camera_info_transport = LCMTransport("/camera_info", CameraInfo) + + lidar_transport.broadcast(None, lidar_frame) + video_transport.broadcast(None, video_frame) + odom_transport.broadcast(None, odom_frame) + camera_info_transport.broadcast(None, camera_info()) + + transform_chain(odom_frame) + + print(lidar_frame) + print(video_frame) + print(odom_frame) + video_transport = LCMTransport("/image", Image) + annotations_transport = LCMTransport("/annotations", ImageAnnotations) + annotations_transport.broadcast(None, annotations) + + +def process_data(): + from dimos.msgs.sensor_msgs import Image + from dimos.perception.detection.module2D import Detection2DModule, build_imageannotations + from dimos.robot.unitree_webrtc.type.lidar import LidarMessage + from dimos.robot.unitree_webrtc.type.odometry import Odometry + from dimos.utils.data import get_data + from dimos.utils.testing import TimedSensorReplay + + get_data("unitree_office_walk") + target = 1751591272.9654856 + lidar_store = TimedSensorReplay("unitree_office_walk/lidar", autocast=LidarMessage.from_msg) + video_store = TimedSensorReplay("unitree_office_walk/video", autocast=Image.from_numpy) + odom_store = TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) + + def attach_frame_id(image: Image) -> Image: + image.frame_id = "camera_optical" + return image + + lidar_frame = lidar_store.find_closest(target, tolerance=1) + video_frame = attach_frame_id(video_store.find_closest(target, tolerance=1)) + odom_frame = odom_store.find_closest(target, tolerance=1) + + detector = Detection2DModule() + detections = detector.detect(video_frame) + annotations = build_imageannotations(detections) + + data = (target, lidar_frame, video_frame, odom_frame, detections, annotations) + + with open("filename.pkl", "wb") as file: + pickle.dump(data, file) + + return data + + +def main() -> None: + try: + with open("filename.pkl", "rb") as file: + data = pickle.load(file) + except FileNotFoundError: + print("Processing data and creating pickle file...") + data = process_data() + broadcast(*data) + + +main() diff --git a/dimos/robot/unitree_webrtc/modular/ivan_unitree.py b/dimos/robot/unitree_webrtc/modular/ivan_unitree.py new file mode 100644 index 0000000000..e7a2bcabc8 --- /dev/null +++ b/dimos/robot/unitree_webrtc/modular/ivan_unitree.py @@ -0,0 +1,134 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import time + +from dimos.agents2.spec import Model, Provider +from dimos.core import LCMTransport, start + +# from dimos.msgs.detection2d import Detection2DArray +from dimos.msgs.foxglove_msgs import ImageAnnotations +from dimos.msgs.sensor_msgs import Image +from dimos.msgs.vision_msgs import Detection2DArray +from dimos.perception.detection.module2D import Detection2DModule +from dimos.perception.detection.reid import ReidModule +from dimos.protocol.pubsub import lcm +from dimos.robot.foxglove_bridge import FoxgloveBridge +from dimos.robot.unitree_webrtc.modular import deploy_connection +from dimos.robot.unitree_webrtc.modular.connection_module import ConnectionModule +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.robot.unitree_webrtc.unitree_go2", level=logging.INFO) + + +def detection_unitree() -> None: + dimos = start(8) + connection = deploy_connection(dimos) + + def goto(pose) -> bool: + print("NAVIGATION REQUESTED:", pose) + return True + + detector = dimos.deploy( + Detection2DModule, + # goto=goto, + camera_info=ConnectionModule._camera_info(), + ) + + detector.image.connect(connection.video) + # detector.pointcloud.connect(mapper.global_map) + # detector.pointcloud.connect(connection.lidar) + + detector.annotations.transport = LCMTransport("/annotations", ImageAnnotations) + detector.detections.transport = LCMTransport("/detections", Detection2DArray) + + # detector.detected_pointcloud_0.transport = LCMTransport("/detected/pointcloud/0", PointCloud2) + # detector.detected_pointcloud_1.transport = LCMTransport("/detected/pointcloud/1", PointCloud2) + # detector.detected_pointcloud_2.transport = LCMTransport("/detected/pointcloud/2", PointCloud2) + + detector.detected_image_0.transport = LCMTransport("/detected/image/0", Image) + detector.detected_image_1.transport = LCMTransport("/detected/image/1", Image) + detector.detected_image_2.transport = LCMTransport("/detected/image/2", Image) + # detector.scene_update.transport = LCMTransport("/scene_update", SceneUpdate) + + # reidModule = dimos.deploy(ReidModule) + + # reidModule.image.connect(connection.video) + # reidModule.detections.connect(detector.detections) + # reidModule.annotations.transport = LCMTransport("/reid/annotations", ImageAnnotations) + + # nav = deploy_navigation(dimos, connection) + + # person_tracker = dimos.deploy(PersonTracker, cameraInfo=ConnectionModule._camera_info()) + # person_tracker.image.connect(connection.video) + # person_tracker.detections.connect(detector.detections) + # person_tracker.target.transport = LCMTransport("/goal_request", PoseStamped) + + reid = dimos.deploy(ReidModule) + + reid.image.connect(connection.video) + reid.detections.connect(detector.detections) + reid.annotations.transport = LCMTransport("/reid/annotations", ImageAnnotations) + + detector.start() + # person_tracker.start() + connection.start() + reid.start() + + from dimos.agents2 import Agent + from dimos.agents2.cli.human import HumanInput + + agent = Agent( + system_prompt="You are a helpful assistant for controlling a Unitree Go2 robot.", + model=Model.GPT_4O, # Could add CLAUDE models to enum + provider=Provider.OPENAI, # Would need ANTHROPIC provider + ) + + human_input = dimos.deploy(HumanInput) + agent.register_skills(human_input) + # agent.register_skills(connection) + agent.register_skills(detector) + + bridge = FoxgloveBridge( + shm_channels=[ + "/image#sensor_msgs.Image", + "/lidar#sensor_msgs.PointCloud2", + ] + ) + # bridge = FoxgloveBridge() + time.sleep(1) + bridge.start() + + # agent.run_implicit_skill("video_stream_tool") + # agent.run_implicit_skill("human") + + # agent.start() + # agent.loop_thread() + + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + connection.stop() + logger.info("Shutting down...") + + +def main() -> None: + lcm.autoconf() + detection_unitree() + + +if __name__ == "__main__": + main() diff --git a/dimos/robot/unitree_webrtc/modular/navigation.py b/dimos/robot/unitree_webrtc/modular/navigation.py new file mode 100644 index 0000000000..9aa03d104e --- /dev/null +++ b/dimos/robot/unitree_webrtc/modular/navigation.py @@ -0,0 +1,93 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos_lcm.std_msgs import Bool, String + +from dimos.core import LCMTransport +from dimos.msgs.geometry_msgs import PoseStamped, Twist +from dimos.msgs.nav_msgs import OccupancyGrid, Path +from dimos.navigation.bt_navigator.navigator import BehaviorTreeNavigator +from dimos.navigation.frontier_exploration import WavefrontFrontierExplorer +from dimos.navigation.global_planner import AstarPlanner +from dimos.navigation.local_planner.holonomic_local_planner import HolonomicLocalPlanner +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.robot.unitree_webrtc.type.map import Map +from dimos.web.websocket_vis.websocket_vis_module import WebsocketVisModule + + +def deploy_navigation(dimos, connection): + mapper = dimos.deploy(Map, voxel_size=0.5, cost_resolution=0.05, global_publish_interval=2.5) + mapper.lidar.connect(connection.lidar) + mapper.global_map.transport = LCMTransport("/global_map", LidarMessage) + mapper.global_costmap.transport = LCMTransport("/global_costmap", OccupancyGrid) + mapper.local_costmap.transport = LCMTransport("/local_costmap", OccupancyGrid) + + """Deploy and configure navigation modules.""" + global_planner = dimos.deploy(AstarPlanner) + local_planner = dimos.deploy(HolonomicLocalPlanner) + navigator = dimos.deploy( + BehaviorTreeNavigator, + reset_local_planner=local_planner.reset, + check_goal_reached=local_planner.is_goal_reached, + ) + frontier_explorer = dimos.deploy(WavefrontFrontierExplorer) + + navigator.goal.transport = LCMTransport("/navigation_goal", PoseStamped) + navigator.goal_request.transport = LCMTransport("/goal_request", PoseStamped) + navigator.goal_reached.transport = LCMTransport("/goal_reached", Bool) + navigator.navigation_state.transport = LCMTransport("/navigation_state", String) + navigator.global_costmap.transport = LCMTransport("/global_costmap", OccupancyGrid) + global_planner.path.transport = LCMTransport("/global_path", Path) + local_planner.cmd_vel.transport = LCMTransport("/cmd_vel", Twist) + frontier_explorer.goal_request.transport = LCMTransport("/goal_request", PoseStamped) + frontier_explorer.goal_reached.transport = LCMTransport("/goal_reached", Bool) + frontier_explorer.explore_cmd.transport = LCMTransport("/explore_cmd", Bool) + frontier_explorer.stop_explore_cmd.transport = LCMTransport("/stop_explore_cmd", Bool) + + global_planner.target.connect(navigator.goal) + + global_planner.global_costmap.connect(mapper.global_costmap) + global_planner.odom.connect(connection.odom) + + local_planner.path.connect(global_planner.path) + local_planner.local_costmap.connect(mapper.local_costmap) + local_planner.odom.connect(connection.odom) + + connection.movecmd.connect(local_planner.cmd_vel) + + navigator.odom.connect(connection.odom) + + frontier_explorer.costmap.connect(mapper.global_costmap) + frontier_explorer.odometry.connect(connection.odom) + websocket_vis = dimos.deploy(WebsocketVisModule, port=7779) + websocket_vis.click_goal.transport = LCMTransport("/goal_request", PoseStamped) + + websocket_vis.robot_pose.connect(connection.odom) + websocket_vis.path.connect(global_planner.path) + websocket_vis.global_costmap.connect(mapper.global_costmap) + + mapper.start() + global_planner.start() + local_planner.start() + navigator.start() + websocket_vis.start() + + return { + "mapper": mapper, + "global_planner": global_planner, + "local_planner": local_planner, + "navigator": navigator, + "frontier_explorer": frontier_explorer, + "websocket_vis": websocket_vis, + } diff --git a/dimos/robot/unitree_webrtc/mujoco_connection.py b/dimos/robot/unitree_webrtc/mujoco_connection.py new file mode 100644 index 0000000000..b68097ea33 --- /dev/null +++ b/dimos/robot/unitree_webrtc/mujoco_connection.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import atexit +import functools +import logging +import threading +import time + +from reactivex import Observable + +from dimos.mapping.types import LatLon +from dimos.msgs.geometry_msgs import Twist +from dimos.msgs.sensor_msgs import Image +from dimos.utils.data import get_data + +LIDAR_FREQUENCY = 10 +ODOM_FREQUENCY = 50 +VIDEO_FREQUENCY = 30 + +logger = logging.getLogger(__name__) + + +class MujocoConnection: + def __init__(self, *args, **kwargs) -> None: + try: + from dimos.simulation.mujoco.mujoco import MujocoThread + except ImportError: + raise ImportError("'mujoco' is not installed. Use `pip install -e .[sim]`") + get_data("mujoco_sim") + self.mujoco_thread = MujocoThread() + self._stream_threads: list[threading.Thread] = [] + self._stop_events: list[threading.Event] = [] + self._is_cleaned_up = False + + # Register cleanup on exit + atexit.register(self.stop) + + def start(self) -> None: + self.mujoco_thread.start() + + def stop(self) -> None: + """Clean up all resources. Can be called multiple times safely.""" + if self._is_cleaned_up: + return + + self._is_cleaned_up = True + + # Stop all stream threads + for stop_event in self._stop_events: + stop_event.set() + + # Wait for threads to finish + for thread in self._stream_threads: + if thread.is_alive(): + thread.join(timeout=2.0) + if thread.is_alive(): + logger.warning(f"Stream thread {thread.name} did not stop gracefully") + + # Clean up the MuJoCo thread + if hasattr(self, "mujoco_thread") and self.mujoco_thread: + self.mujoco_thread.cleanup() + + # Clear references + self._stream_threads.clear() + self._stop_events.clear() + + # Clear cached methods to prevent memory leaks + if hasattr(self, "lidar_stream"): + self.lidar_stream.cache_clear() + if hasattr(self, "odom_stream"): + self.odom_stream.cache_clear() + if hasattr(self, "video_stream"): + self.video_stream.cache_clear() + + def standup(self) -> None: + print("standup supressed") + + def liedown(self) -> None: + print("liedown supressed") + + @functools.cache + def lidar_stream(self): + def on_subscribe(observer, scheduler): + if self._is_cleaned_up: + observer.on_completed() + return lambda: None + + stop_event = threading.Event() + self._stop_events.append(stop_event) + + def run() -> None: + try: + while not stop_event.is_set() and not self._is_cleaned_up: + lidar_to_publish = self.mujoco_thread.get_lidar_message() + + if lidar_to_publish: + observer.on_next(lidar_to_publish) + + time.sleep(1 / LIDAR_FREQUENCY) + except Exception as e: + logger.error(f"Lidar stream error: {e}") + finally: + observer.on_completed() + + thread = threading.Thread(target=run, daemon=True) + self._stream_threads.append(thread) + thread.start() + + def dispose() -> None: + stop_event.set() + + return dispose + + return Observable(on_subscribe) + + @functools.cache + def odom_stream(self): + def on_subscribe(observer, scheduler): + if self._is_cleaned_up: + observer.on_completed() + return lambda: None + + stop_event = threading.Event() + self._stop_events.append(stop_event) + + def run() -> None: + try: + while not stop_event.is_set() and not self._is_cleaned_up: + odom_to_publish = self.mujoco_thread.get_odom_message() + if odom_to_publish: + observer.on_next(odom_to_publish) + + time.sleep(1 / ODOM_FREQUENCY) + except Exception as e: + logger.error(f"Odom stream error: {e}") + finally: + observer.on_completed() + + thread = threading.Thread(target=run, daemon=True) + self._stream_threads.append(thread) + thread.start() + + def dispose() -> None: + stop_event.set() + + return dispose + + return Observable(on_subscribe) + + @functools.cache + def gps_stream(self): + def on_subscribe(observer, scheduler): + if self._is_cleaned_up: + observer.on_completed() + return lambda: None + + stop_event = threading.Event() + self._stop_events.append(stop_event) + + def run() -> None: + lat = 37.78092426217621 + lon = -122.40682866540769 + try: + while not stop_event.is_set() and not self._is_cleaned_up: + observer.on_next(LatLon(lat=lat, lon=lon)) + lat += 0.00001 + time.sleep(1) + finally: + observer.on_completed() + + thread = threading.Thread(target=run, daemon=True) + self._stream_threads.append(thread) + thread.start() + + def dispose() -> None: + stop_event.set() + + return dispose + + return Observable(on_subscribe) + + @functools.cache + def video_stream(self): + def on_subscribe(observer, scheduler): + if self._is_cleaned_up: + observer.on_completed() + return lambda: None + + stop_event = threading.Event() + self._stop_events.append(stop_event) + + def run() -> None: + try: + while not stop_event.is_set() and not self._is_cleaned_up: + with self.mujoco_thread.pixels_lock: + if self.mujoco_thread.shared_pixels is not None: + img = Image.from_numpy(self.mujoco_thread.shared_pixels.copy()) + observer.on_next(img) + time.sleep(1 / VIDEO_FREQUENCY) + except Exception as e: + logger.error(f"Video stream error: {e}") + finally: + observer.on_completed() + + thread = threading.Thread(target=run, daemon=True) + self._stream_threads.append(thread) + thread.start() + + def dispose() -> None: + stop_event.set() + + return dispose + + return Observable(on_subscribe) + + def move(self, twist: Twist, duration: float = 0.0) -> None: + if not self._is_cleaned_up: + self.mujoco_thread.move(twist, duration) + + def publish_request(self, topic: str, data: dict) -> None: + pass diff --git a/dimos/robot/unitree_webrtc/params/front_camera_720.yaml b/dimos/robot/unitree_webrtc/params/front_camera_720.yaml new file mode 100644 index 0000000000..eb09710667 --- /dev/null +++ b/dimos/robot/unitree_webrtc/params/front_camera_720.yaml @@ -0,0 +1,26 @@ +image_width: 1280 +image_height: 720 +camera_name: narrow_stereo +camera_matrix: + rows: 3 + cols: 3 + data: [864.39938, 0. , 639.19798, + 0. , 863.73849, 373.28118, + 0. , 0. , 1. ] +distortion_model: plumb_bob +distortion_coefficients: + rows: 1 + cols: 5 + data: [-0.354630, 0.102054, -0.001614, -0.001249, 0.000000] +rectification_matrix: + rows: 3 + cols: 3 + data: [1., 0., 0., + 0., 1., 0., + 0., 0., 1.] +projection_matrix: + rows: 3 + cols: 4 + data: [651.42609, 0. , 633.16224, 0. , + 0. , 804.93951, 373.8537 , 0. , + 0. , 0. , 1. , 0. ] \ No newline at end of file diff --git a/dimos/robot/unitree_webrtc/params/sim_camera.yaml b/dimos/robot/unitree_webrtc/params/sim_camera.yaml new file mode 100644 index 0000000000..8fc1574953 --- /dev/null +++ b/dimos/robot/unitree_webrtc/params/sim_camera.yaml @@ -0,0 +1,26 @@ +image_width: 320 +image_height: 240 +camera_name: sim_camera +camera_matrix: + rows: 3 + cols: 3 + data: [277., 0. , 160. , + 0. , 277., 120. , + 0. , 0. , 1. ] +distortion_model: plumb_bob +distortion_coefficients: + rows: 1 + cols: 5 + data: [0.0, 0.0, 0.0, 0.0, 0.0] +rectification_matrix: + rows: 3 + cols: 3 + data: [1., 0., 0., + 0., 1., 0., + 0., 0., 1.] +projection_matrix: + rows: 3 + cols: 4 + data: [277., 0. , 160. , 0. , + 0. , 277., 120. , 0. , + 0. , 0. , 1. , 0. ] \ No newline at end of file diff --git a/dimos/robot/unitree_webrtc/rosnav.py b/dimos/robot/unitree_webrtc/rosnav.py new file mode 100644 index 0000000000..bd91fafb90 --- /dev/null +++ b/dimos/robot/unitree_webrtc/rosnav.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import time + +from dimos.core import In, Module, Out, rpc +from dimos.msgs.geometry_msgs import PoseStamped +from dimos.msgs.sensor_msgs import Joy +from dimos.msgs.std_msgs.Bool import Bool +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.robot.unitree_webrtc.nav_bot", level=logging.INFO) + + +class NavigationModule(Module): + goal_pose: Out[PoseStamped] = None + goal_reached: In[Bool] = None + cancel_goal: Out[Bool] = None + joy: Out[Joy] = None + + def __init__(self, *args, **kwargs) -> None: + """Initialize NavigationModule.""" + Module.__init__(self, *args, **kwargs) + self.goal_reach = None + + @rpc + def start(self) -> None: + """Start the navigation module.""" + if self.goal_reached: + self.goal_reached.subscribe(self._on_goal_reached) + logger.info("NavigationModule started") + + def _on_goal_reached(self, msg: Bool) -> None: + """Handle goal reached status messages.""" + self.goal_reach = msg.data + + def _set_autonomy_mode(self) -> None: + """ + Set autonomy mode by publishing Joy message. + """ + + joy_msg = Joy( + frame_id="dimos", + axes=[ + 0.0, # axis 0 + 0.0, # axis 1 + -1.0, # axis 2 + 0.0, # axis 3 + 1.0, # axis 4 + 1.0, # axis 5 + 0.0, # axis 6 + 0.0, # axis 7 + ], + buttons=[ + 0, # button 0 + 0, # button 1 + 0, # button 2 + 0, # button 3 + 0, # button 4 + 0, # button 5 + 0, # button 6 + 1, # button 7 - controls autonomy mode + 0, # button 8 + 0, # button 9 + 0, # button 10 + ], + ) + + if self.joy: + self.joy.publish(joy_msg) + logger.info("Setting autonomy mode via Joy message") + + @rpc + def go_to(self, pose: PoseStamped, timeout: float = 60.0) -> bool: + """ + Navigate to a target pose by publishing to LCM topics. + + Args: + pose: Target pose to navigate to + blocking: If True, block until goal is reached + timeout: Maximum time to wait for goal (seconds) + + Returns: + True if navigation was successful (or started if non-blocking) + """ + logger.info( + f"Navigating to goal: ({pose.position.x:.2f}, {pose.position.y:.2f}, {pose.position.z:.2f})" + ) + + self.goal_reach = None + self._set_autonomy_mode() + self.goal_pose.publish(pose) + time.sleep(0.2) + self.goal_pose.publish(pose) + + start_time = time.time() + while time.time() - start_time < timeout: + if self.goal_reach is not None: + return self.goal_reach + time.sleep(0.1) + + self.stop() + + logger.warning(f"Navigation timed out after {timeout} seconds") + return False + + @rpc + def stop(self) -> bool: + """ + Cancel current navigation by publishing to cancel_goal. + + Returns: + True if cancel command was sent successfully + """ + logger.info("Cancelling navigation") + + if self.cancel_goal: + cancel_msg = Bool(data=True) + self.cancel_goal.publish(cancel_msg) + return True + + return False diff --git a/dimos/robot/unitree_webrtc/test_tooling.py b/dimos/robot/unitree_webrtc/test_tooling.py deleted file mode 100644 index 917cca69a0..0000000000 --- a/dimos/robot/unitree_webrtc/test_tooling.py +++ /dev/null @@ -1,71 +0,0 @@ -import os -import sys -import time - -import pytest -from dotenv import load_dotenv -import reactivex.operators as ops - -from dimos.robot.unitree_webrtc.unitree_go2 import UnitreeGo2 -from dimos.robot.unitree_webrtc.testing.multimock import Multimock -from dimos.robot.unitree_webrtc.testing.helpers import show3d_stream -from dimos.robot.unitree_webrtc.type.map import Map -from dimos.robot.unitree_webrtc.type.lidar import LidarMessage -from dimos.robot.unitree_webrtc.type.odometry import position_from_odom - - -@pytest.mark.tool -def test_record_lidar(): - load_dotenv() - robot = UnitreeGo2(ip=os.getenv("ROBOT_IP"), mode="ai") - - print("Robot is standing up...") - robot.standup() - - lidar_store = Multimock("athens_lidar") - odom_store = Multimock("athens_odom") - lidar_store.consume(robot.raw_lidar_stream()).subscribe(print) - odom_store.consume(robot.raw_odom_stream()).subscribe(print) - - print("Recording, CTRL+C to kill") - - try: - while True: - time.sleep(0.1) - - except KeyboardInterrupt: - print("Robot is lying down...") - robot.liedown() - print("Exit") - sys.exit(0) - - -@pytest.mark.tool -def test_replay_recording(): - odom_stream = Multimock("athens_odom").stream().pipe(ops.map(position_from_odom)) - odom_stream.subscribe(lambda x: print(x)) - - map = Map() - - def lidarmsg(msg): - frame = LidarMessage.from_msg(msg) - map.add_frame(frame) - return [map, map.costmap.smudge()] - - global_map_stream = Multimock("athens_lidar").stream().pipe(ops.map(lidarmsg)) - show3d_stream(global_map_stream.pipe(ops.map(lambda x: x[0])), clearframe=True).run() - - -@pytest.mark.tool -def compare_events(): - odom_events = Multimock("athens_odom").list() - - map = Map() - - def lidarmsg(msg): - frame = LidarMessage.from_msg(msg) - map.add_frame(frame) - return [map, map.costmap.smudge()] - - global_map_stream = Multimock("athens_lidar").stream().pipe(ops.map(lidarmsg)) - show3d_stream(global_map_stream.pipe(ops.map(lambda x: x[0])), clearframe=True).run() diff --git a/dimos/robot/unitree_webrtc/test_unitree_go2_integration.py b/dimos/robot/unitree_webrtc/test_unitree_go2_integration.py new file mode 100644 index 0000000000..7acdfc1980 --- /dev/null +++ b/dimos/robot/unitree_webrtc/test_unitree_go2_integration.py @@ -0,0 +1,200 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio + +import pytest + +from dimos import core +from dimos.core import Module, Out, rpc +from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Twist, Vector3 +from dimos.msgs.nav_msgs import OccupancyGrid +from dimos.msgs.sensor_msgs import Image +from dimos.navigation.bt_navigator.navigator import BehaviorTreeNavigator +from dimos.navigation.frontier_exploration import WavefrontFrontierExplorer +from dimos.navigation.global_planner import AstarPlanner +from dimos.navigation.local_planner.holonomic_local_planner import HolonomicLocalPlanner +from dimos.protocol import pubsub +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.robot.unitree_webrtc.type.map import Map +from dimos.robot.unitree_webrtc.unitree_go2 import ConnectionModule +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("test_unitree_go2_integration") + +pubsub.lcm.autoconf() + + +class MovementControlModule(Module): + """Simple module to send movement commands for testing.""" + + movecmd: Out[Twist] = None + + def __init__(self) -> None: + super().__init__() + self.commands_sent = [] + + @rpc + def send_move_command(self, x: float, y: float, yaw: float) -> None: + """Send a movement command.""" + cmd = Twist(linear=Vector3(x, y, 0.0), angular=Vector3(0.0, 0.0, yaw)) + self.movecmd.publish(cmd) + self.commands_sent.append(cmd) + logger.info(f"Sent move command: x={x}, y={y}, yaw={yaw}") + + @rpc + def get_command_count(self) -> int: + """Get number of commands sent.""" + return len(self.commands_sent) + + +@pytest.mark.module +class TestUnitreeGo2CoreModules: + @pytest.mark.asyncio + async def test_unitree_go2_navigation_stack(self) -> None: + """Test UnitreeGo2 core navigation modules without perception/visualization.""" + + # Start Dask + dimos = core.start(4) + + try: + # Deploy ConnectionModule with playback mode (uses test data) + connection = dimos.deploy( + ConnectionModule, + ip="127.0.0.1", # IP doesn't matter for playback + playback=True, # Enable playback mode + ) + + # Configure LCM transports + connection.lidar.transport = core.LCMTransport("/lidar", LidarMessage) + connection.odom.transport = core.LCMTransport("/odom", PoseStamped) + connection.video.transport = core.LCMTransport("/video", Image) + + # Deploy Map module + mapper = dimos.deploy(Map, voxel_size=0.5, global_publish_interval=2.5) + mapper.global_map.transport = core.LCMTransport("/global_map", LidarMessage) + mapper.global_costmap.transport = core.LCMTransport("/global_costmap", OccupancyGrid) + mapper.local_costmap.transport = core.LCMTransport("/local_costmap", OccupancyGrid) + mapper.lidar.connect(connection.lidar) + + # Deploy navigation stack + global_planner = dimos.deploy(AstarPlanner) + local_planner = dimos.deploy(HolonomicLocalPlanner) + navigator = dimos.deploy(BehaviorTreeNavigator, local_planner=local_planner) + + # Set up transports first + from dimos_lcm.std_msgs import Bool + + from dimos.msgs.nav_msgs import Path + + navigator.goal.transport = core.LCMTransport("/navigation_goal", PoseStamped) + navigator.goal_request.transport = core.LCMTransport("/goal_request", PoseStamped) + navigator.goal_reached.transport = core.LCMTransport("/goal_reached", Bool) + navigator.global_costmap.transport = core.LCMTransport("/global_costmap", OccupancyGrid) + global_planner.path.transport = core.LCMTransport("/global_path", Path) + local_planner.cmd_vel.transport = core.LCMTransport("/cmd_vel", Twist) + + # Configure navigation connections + global_planner.target.connect(navigator.goal) + global_planner.global_costmap.connect(mapper.global_costmap) + global_planner.odom.connect(connection.odom) + + local_planner.path.connect(global_planner.path) + local_planner.local_costmap.connect(mapper.local_costmap) + local_planner.odom.connect(connection.odom) + + connection.movecmd.connect(local_planner.cmd_vel) + navigator.odom.connect(connection.odom) + + # Deploy movement control module for testing + movement = dimos.deploy(MovementControlModule) + movement.movecmd.transport = core.LCMTransport("/test_move", Twist) + connection.movecmd.connect(movement.movecmd) + + # Start all modules + connection.start() + mapper.start() + global_planner.start() + local_planner.start() + navigator.start() + + logger.info("All core modules started") + + # Wait for initialization + await asyncio.sleep(3) + + # Test movement commands + movement.send_move_command(0.5, 0.0, 0.0) + await asyncio.sleep(0.5) + + movement.send_move_command(0.0, 0.0, 0.3) + await asyncio.sleep(0.5) + + movement.send_move_command(0.0, 0.0, 0.0) + await asyncio.sleep(0.5) + + # Check commands were sent + cmd_count = movement.get_command_count() + assert cmd_count == 3, f"Expected 3 commands, got {cmd_count}" + logger.info(f"Successfully sent {cmd_count} movement commands") + + # Test navigation + target_pose = PoseStamped( + frame_id="world", + position=Vector3(2.0, 1.0, 0.0), + orientation=Quaternion(0, 0, 0, 1), + ) + + # Set navigation goal (non-blocking) + try: + navigator.set_goal(target_pose) + logger.info("Navigation goal set") + except Exception as e: + logger.warning(f"Navigation goal setting failed: {e}") + + await asyncio.sleep(2) + + # Cancel navigation + navigator.cancel_goal() + logger.info("Navigation cancelled") + + # Test frontier exploration + frontier_explorer = dimos.deploy(WavefrontFrontierExplorer) + frontier_explorer.costmap.connect(mapper.global_costmap) + frontier_explorer.odometry.connect(connection.odom) + frontier_explorer.goal_request.transport = core.LCMTransport( + "/frontier_goal", PoseStamped + ) + frontier_explorer.goal_reached.transport = core.LCMTransport("/frontier_reached", Bool) + frontier_explorer.start() + + # Try to start exploration + result = frontier_explorer.explore() + logger.info(f"Exploration started: {result}") + + await asyncio.sleep(2) + + # Stop exploration + frontier_explorer.stop_exploration() + logger.info("Exploration stopped") + + logger.info("All core navigation tests passed!") + + finally: + dimos.close() + logger.info("Closed Dask cluster") + + +if __name__ == "__main__": + pytest.main(["-v", "-s", __file__]) diff --git a/dimos/robot/unitree_webrtc/testing/helpers.py b/dimos/robot/unitree_webrtc/testing/helpers.py index 6f815abd56..5159deab4c 100644 --- a/dimos/robot/unitree_webrtc/testing/helpers.py +++ b/dimos/robot/unitree_webrtc/testing/helpers.py @@ -1,6 +1,22 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Callable, Iterable import time +from typing import Any, Protocol + import open3d as o3d -from typing import Callable, Union, Any, Protocol, Iterable from reactivex.observable import Observable color1 = [1, 0.706, 0] @@ -14,7 +30,7 @@ # # (in case there is some preparation within the fuction and this time needs to be subtracted # from the benchmark target) -def benchmark(calls: int, targetf: Callable[[], Union[int, None]]) -> float: +def benchmark(calls: int, targetf: Callable[[], int | None]) -> float: start = time.time() timemod = 0 for _ in range(calls): @@ -25,7 +41,12 @@ def benchmark(calls: int, targetf: Callable[[], Union[int, None]]) -> float: return (end - start + timemod) * 1000 / calls -O3dDrawable = o3d.geometry.Geometry | o3d.geometry.LineSet | o3d.geometry.TriangleMesh | o3d.geometry.PointCloud +O3dDrawable = ( + o3d.geometry.Geometry + | o3d.geometry.LineSet + | o3d.geometry.TriangleMesh + | o3d.geometry.PointCloud +) class ReturnsDrawable(Protocol): @@ -70,8 +91,8 @@ def show3d_stream( Subsequent geometries update the visualizer. If no new geometry, just poll events. geometry_observable: Observable of objects with .o3d_geometry or Open3D geometry """ - import threading import queue + import threading import time from typing import Any diff --git a/dimos/robot/unitree_webrtc/testing/mock.py b/dimos/robot/unitree_webrtc/testing/mock.py index ab28fcce02..20eb357cc0 100644 --- a/dimos/robot/unitree_webrtc/testing/mock.py +++ b/dimos/robot/unitree_webrtc/testing/mock.py @@ -1,32 +1,47 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Iterator +import glob import os import pickle -import glob -from typing import Union, Iterator, cast, overload -from dimos.robot.unitree_webrtc.type.lidar import LidarMessage, RawLidarMsg +from typing import cast, overload -from reactivex import operators as ops -from reactivex import interval, from_iterable +from reactivex import from_iterable, interval, operators as ops from reactivex.observable import Observable +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage, RawLidarMsg + class Mock: - def __init__(self, root="office", autocast: bool = True): + def __init__(self, root: str = "office", autocast: bool = True) -> None: current_dir = os.path.dirname(os.path.abspath(__file__)) self.root = os.path.join(current_dir, f"mockdata/{root}") self.autocast = autocast self.cnt = 0 @overload - def load(self, name: Union[int, str], /) -> LidarMessage: ... + def load(self, name: int | str, /) -> LidarMessage: ... @overload - def load(self, *names: Union[int, str]) -> list[LidarMessage]: ... + def load(self, *names: int | str) -> list[LidarMessage]: ... - def load(self, *names: Union[int, str]) -> Union[LidarMessage, list[LidarMessage]]: + def load(self, *names: int | str) -> LidarMessage | list[LidarMessage]: if len(names) == 1: return self.load_one(names[0]) return list(map(lambda name: self.load_one(name), names)) - def load_one(self, name: Union[int, str]) -> LidarMessage: + def load_one(self, name: int | str) -> LidarMessage: if isinstance(name, int): file_name = f"/lidar_data_{name:03d}.pickle" else: @@ -34,7 +49,7 @@ def load_one(self, name: Union[int, str]) -> LidarMessage: full_path = self.root + file_name with open(full_path, "rb") as f: - return LidarMessage.from_msg(cast(RawLidarMsg, pickle.load(f))) + return LidarMessage.from_msg(cast("RawLidarMsg", pickle.load(f))) def iterate(self) -> Iterator[LidarMessage]: pattern = os.path.join(self.root, "lidar_data_*.pickle") @@ -44,7 +59,7 @@ def iterate(self) -> Iterator[LidarMessage]: filename = os.path.splitext(basename)[0] yield self.load_one(filename) - def stream(self, rate_hz=10.0): + def stream(self, rate_hz: float = 10.0): sleep_time = 1.0 / rate_hz return from_iterable(self.iterate()).pipe( diff --git a/dimos/robot/unitree_webrtc/testing/multimock.py b/dimos/robot/unitree_webrtc/testing/multimock.py index 5049e669da..eab10e14bb 100644 --- a/dimos/robot/unitree_webrtc/testing/multimock.py +++ b/dimos/robot/unitree_webrtc/testing/multimock.py @@ -1,4 +1,18 @@ #!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """Multimock – lightweight persistence & replay helper built on RxPy. A directory of pickle files acts as a tiny append-only log of (timestamp, data) @@ -19,13 +33,19 @@ import os import pickle import time -from typing import Any, Generic, Iterator, List, Tuple, TypeVar, Union, Optional -from reactivex.scheduler import ThreadPoolScheduler +from typing import TYPE_CHECKING, Any, Generic, TypeVar from reactivex import from_iterable, interval, operators as ops -from reactivex.observable import Observable -from dimos.utils.threadpool import get_scheduler + from dimos.robot.unitree_webrtc.type.timeseries import TEvent, Timeseries +from dimos.utils.threadpool import get_scheduler + +if TYPE_CHECKING: + import builtins + from collections.abc import Iterator + + from reactivex.observable import Observable + from reactivex.scheduler import ThreadPoolScheduler T = TypeVar("T") @@ -66,11 +86,11 @@ def save_one(self, frame: Any) -> int: return self.cnt - def load(self, *names: Union[int, str]) -> List[Tuple[float, T]]: + def load(self, *names: int | str) -> builtins.list[tuple[float, T]]: """Load multiple items by name or index.""" return list(map(self.load_one, names)) - def load_one(self, name: Union[int, str]) -> TEvent[T]: + def load_one(self, name: int | str) -> TEvent[T]: """Load a single item by name or index.""" if isinstance(name, int): file_name = f"/{self.file_prefix}_{name:03d}.pickle" @@ -92,7 +112,7 @@ def iterate(self) -> Iterator[TEvent[T]]: timestamp, data = pickle.load(f) yield TEvent(timestamp, data) - def list(self) -> List[TEvent[T]]: + def list(self) -> builtins.list[TEvent[T]]: return list(self.iterate()) def interval_stream(self, rate_hz: float = 10.0) -> Observable[T]: @@ -106,7 +126,7 @@ def interval_stream(self, rate_hz: float = 10.0) -> Observable[T]: def stream( self, replay_speed: float = 1.0, - scheduler: Optional[ThreadPoolScheduler] = None, + scheduler: ThreadPoolScheduler | None = None, ) -> Observable[T]: def _generator(): prev_ts: float | None = None diff --git a/dimos/robot/unitree_webrtc/testing/test_actors.py b/dimos/robot/unitree_webrtc/testing/test_actors.py new file mode 100644 index 0000000000..4612f45a79 --- /dev/null +++ b/dimos/robot/unitree_webrtc/testing/test_actors.py @@ -0,0 +1,111 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import asyncio +from collections.abc import Callable +import time + +import pytest + +from dimos import core +from dimos.core import Module, rpc +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.robot.unitree_webrtc.type.map import Map as Mapper + + +@pytest.fixture +def dimos(): + return core.start(2) + + +@pytest.fixture +def client(): + return core.start(2) + + +class Consumer: + testf: Callable[[int], int] + + def __init__(self, counter=None) -> None: + self.testf = counter + print("consumer init with", counter) + + async def waitcall(self, n: int): + async def task() -> None: + await asyncio.sleep(n) + + print("sleep finished, calling") + res = await self.testf(n) + print("res is", res) + + asyncio.create_task(task()) + return n + + +class Counter(Module): + @rpc + def addten(self, x: int): + print(f"counter adding to {x}") + return x + 10 + + +@pytest.mark.tool +def test_wait(client) -> None: + counter = client.submit(Counter, actor=True).result() + + async def addten(n): + return await counter.addten(n) + + consumer = client.submit(Consumer, counter=addten, actor=True).result() + + print("waitcall1", consumer.waitcall(2).result()) + print("waitcall2", consumer.waitcall(2).result()) + time.sleep(1) + + +@pytest.mark.tool +def test_basic(dimos) -> None: + counter = dimos.deploy(Counter) + consumer = dimos.deploy( + Consumer, + counter=lambda x: counter.addten(x).result(), + ) + + print(consumer) + print(counter) + print("starting consumer") + consumer.start().result() + + res = consumer.inc(10).result() + + print("result is", res) + assert res == 20 + + +@pytest.mark.tool +def test_mapper_start(dimos) -> None: + mapper = dimos.deploy(Mapper) + mapper.lidar.transport = core.LCMTransport("/lidar", LidarMessage) + print("start res", mapper.start().result()) + + +if __name__ == "__main__": + dimos = core.start(2) + test_basic(dimos) + test_mapper_start(dimos) + + +@pytest.mark.tool +def test_counter(dimos) -> None: + counter = dimos.deploy(Counter) + assert counter.addten(10) == 20 diff --git a/dimos/robot/unitree_webrtc/testing/test_mock.py b/dimos/robot/unitree_webrtc/testing/test_mock.py index fce99e6b77..73eeef05ba 100644 --- a/dimos/robot/unitree_webrtc/testing/test_mock.py +++ b/dimos/robot/unitree_webrtc/testing/test_mock.py @@ -1,10 +1,28 @@ #!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import time -from dimos.robot.unitree_webrtc.type.lidar import LidarMessage + +import pytest + from dimos.robot.unitree_webrtc.testing.mock import Mock +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage -def test_mock_load_cast(): +@pytest.mark.needsdata +def test_mock_load_cast() -> None: mock = Mock("test") # Load a frame with type casting @@ -22,7 +40,8 @@ def test_mock_load_cast(): assert len(frame.pointcloud.points) > 0 -def test_mock_iterate(): +@pytest.mark.needsdata +def test_mock_iterate() -> None: """Test the iterate method of the Mock class.""" mock = Mock("office") @@ -34,7 +53,8 @@ def test_mock_iterate(): assert frame.pointcloud.has_points() -def test_mock_stream(): +@pytest.mark.needsdata +def test_mock_stream() -> None: frames = [] sub1 = Mock("office").stream(rate_hz=30.0).subscribe(on_next=frames.append) time.sleep(0.1) diff --git a/dimos/robot/unitree_webrtc/testing/test_multimock.py b/dimos/robot/unitree_webrtc/testing/test_multimock.py deleted file mode 100644 index 230d960c58..0000000000 --- a/dimos/robot/unitree_webrtc/testing/test_multimock.py +++ /dev/null @@ -1,84 +0,0 @@ -import time -import pytest - -from reactivex import operators as ops - -from dimos.utils.reactive import backpressure -from dimos.robot.unitree_webrtc.testing.helpers import show3d_stream -from dimos.web.websocket_vis.server import WebsocketVis -from dimos.robot.unitree_webrtc.type.map import Map -from dimos.robot.unitree_webrtc.type.lidar import LidarMessage -from dimos.robot.unitree_webrtc.type.odometry import Odometry -from dimos.robot.unitree_webrtc.type.timeseries import to_datetime -from dimos.robot.unitree_webrtc.testing.multimock import Multimock - - -@pytest.mark.vis -def test_multimock_stream(): - backpressure(Multimock("athens_odom").stream().pipe(ops.map(Odometry.from_msg))).subscribe(lambda x: print(x)) - map = Map() - - def lidarmsg(msg): - frame = LidarMessage.from_msg(msg) - map.add_frame(frame) - return [map, map.costmap.smudge()] - - mapstream = Multimock("athens_lidar").stream().pipe(ops.map(lidarmsg)) - show3d_stream(mapstream.pipe(ops.map(lambda x: x[0])), clearframe=True).run() - time.sleep(5) - - -def test_clock_mismatch(): - for odometry_raw in Multimock("athens_odom").iterate(): - print( - odometry_raw.ts - to_datetime(odometry_raw.data["data"]["header"]["stamp"]), - odometry_raw.data["data"]["header"]["stamp"], - ) - - -def test_odom_stream(): - for odometry_raw in Multimock("athens_odom").iterate(): - print(Odometry.from_msg(odometry_raw.data)) - - -def test_lidar_stream(): - for lidar_raw in Multimock("athens_lidar").iterate(): - lidarmsg = LidarMessage.from_msg(lidar_raw.data) - print(lidarmsg) - print(lidar_raw) - - -def test_multimock_timeseries(): - odom = Odometry.from_msg(Multimock("athens_odom").load_one(1).data) - lidar_raw = Multimock("athens_lidar").load_one(1).data - lidar = LidarMessage.from_msg(lidar_raw) - map = Map() - map.add_frame(lidar) - print(odom) - print(lidar) - print(lidar_raw) - print(map.costmap) - - -def test_origin_changes(): - for lidar_raw in Multimock("athens_lidar").iterate(): - print(LidarMessage.from_msg(lidar_raw.data).origin) - - -@pytest.mark.vis -def test_webui_multistream(): - websocket_vis = WebsocketVis() - websocket_vis.start() - - odom_stream = Multimock("athens_odom").stream().pipe(ops.map(Odometry.from_msg)) - lidar_stream = backpressure(Multimock("athens_lidar").stream().pipe(ops.map(LidarMessage.from_msg))) - - map = Map() - map_stream = map.consume(lidar_stream) - - costmap_stream = map_stream.pipe(ops.map(lambda x: ["costmap", map.costmap.smudge(preserve_unknown=False)])) - - websocket_vis.connect(costmap_stream) - websocket_vis.connect(odom_stream.pipe(ops.map(lambda pos: ["robot_pos", pos.pos.to_2d()]))) - - show3d_stream(lidar_stream, clearframe=True).run() diff --git a/dimos/robot/unitree_webrtc/testing/test_tooling.py b/dimos/robot/unitree_webrtc/testing/test_tooling.py new file mode 100644 index 0000000000..38a3dba593 --- /dev/null +++ b/dimos/robot/unitree_webrtc/testing/test_tooling.py @@ -0,0 +1,72 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +import time + +from dotenv import load_dotenv +import pytest + +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.robot.unitree_webrtc.type.odometry import Odometry +from dimos.utils.reactive import backpressure +from dimos.utils.testing import TimedSensorReplay, TimedSensorStorage + + +@pytest.mark.tool +def test_record_all() -> None: + from dimos.robot.unitree_webrtc.unitree_go2 import UnitreeGo2 + + load_dotenv() + robot = UnitreeGo2(ip=os.getenv("ROBOT_IP"), mode="ai") + + print("Robot is standing up...") + + robot.standup() + + lidar_store = TimedSensorStorage("unitree/lidar") + odom_store = TimedSensorStorage("unitree/odom") + video_store = TimedSensorStorage("unitree/video") + + lidar_store.save_stream(robot.raw_lidar_stream()).subscribe(print) + odom_store.save_stream(robot.raw_odom_stream()).subscribe(print) + video_store.save_stream(robot.video_stream()).subscribe(print) + + print("Recording, CTRL+C to kill") + + try: + while True: + time.sleep(0.1) + + except KeyboardInterrupt: + print("Robot is lying down...") + robot.liedown() + print("Exit") + sys.exit(0) + + +@pytest.mark.tool +def test_replay_all() -> None: + lidar_store = TimedSensorReplay("unitree/lidar", autocast=LidarMessage.from_msg) + odom_store = TimedSensorReplay("unitree/odom", autocast=Odometry.from_msg) + video_store = TimedSensorReplay("unitree/video") + + backpressure(odom_store.stream()).subscribe(print) + backpressure(lidar_store.stream()).subscribe(print) + backpressure(video_store.stream()).subscribe(print) + + print("Replaying for 3 seconds...") + time.sleep(3) + print("Stopping replay after 3 seconds") diff --git a/dimos/robot/unitree_webrtc/type/costmap.py b/dimos/robot/unitree_webrtc/type/costmap.py deleted file mode 100644 index 49e600ab46..0000000000 --- a/dimos/robot/unitree_webrtc/type/costmap.py +++ /dev/null @@ -1,330 +0,0 @@ -import base64 -import pickle -import numpy as np -from typing import Optional -from scipy import ndimage -from dimos.types.vector import Vector, VectorLike, x, y, to_vector -import open3d as o3d -from matplotlib import cm # any matplotlib colormap - -DTYPE2STR = { - np.float32: "f32", - np.float64: "f64", - np.int32: "i32", - np.int8: "i8", -} - -STR2DTYPE = {v: k for k, v in DTYPE2STR.items()} - - -def encode_ndarray(arr: np.ndarray, compress: bool = False): - arr_c = np.ascontiguousarray(arr) - payload = arr_c.tobytes() - b64 = base64.b64encode(payload).decode("ascii") - - return { - "type": "grid", - "shape": arr_c.shape, - "dtype": DTYPE2STR[arr_c.dtype.type], - "data": b64, - } - - -class Costmap: - """Class to hold ROS OccupancyGrid data.""" - - def __init__( - self, - grid: np.ndarray, - origin: VectorLike, - resolution: float = 0.05, - ): - """Initialize Costmap with its core attributes.""" - self.grid = grid - self.resolution = resolution - self.origin = to_vector(origin).to_2d() - self.width = self.grid.shape[1] - self.height = self.grid.shape[0] - - def serialize(self) -> dict: - """Serialize the Costmap instance to a dictionary.""" - return { - "type": "costmap", - "grid": encode_ndarray(self.grid), - "origin": self.origin.serialize(), - "resolution": self.resolution, - } - - def save_pickle(self, pickle_path: str): - """Save costmap to a pickle file. - - Args: - pickle_path: Path to save the pickle file - """ - with open(pickle_path, "wb") as f: - pickle.dump(self, f) - - @classmethod - def create_empty(cls, width: int = 100, height: int = 100, resolution: float = 0.1) -> "Costmap": - """Create an empty costmap with specified dimensions.""" - return cls( - grid=np.zeros((height, width), dtype=np.int8), - resolution=resolution, - origin=(0.0, 0.0), - ) - - def world_to_grid(self, point: VectorLike) -> Vector: - """Convert world coordinates to grid coordinates. - - Args: - point: A vector-like object containing X,Y coordinates - - Returns: - Vector containing grid_x and grid_y coordinates - """ - return (to_vector(point) - self.origin) / self.resolution - - def grid_to_world(self, grid_point: VectorLike) -> Vector: - return to_vector(grid_point) * self.resolution + self.origin - - def is_occupied(self, point: VectorLike, threshold: int = 50) -> bool: - """Check if a position in world coordinates is occupied. - - Args: - point: Vector-like object containing X,Y coordinates - threshold: Cost threshold above which a cell is considered occupied (0-100) - - Returns: - True if position is occupied or out of bounds, False otherwise - """ - grid_pos = self.world_to_grid(point) - - if 0 <= grid_pos.x < self.width and 0 <= grid_pos.y < self.height: - # Consider unknown (-1) as unoccupied for navigation purposes - # Convert to int coordinates for grid indexing - grid_y, grid_x = int(grid_pos.y), int(grid_pos.x) - value = self.grid[grid_y, grid_x] - return bool(value > 0 and value >= threshold) - return True # Consider out-of-bounds as occupied - - def get_value(self, point: VectorLike) -> Optional[int]: - grid_pos = self.world_to_grid(point) - - if 0 <= grid_pos.x < self.width and 0 <= grid_pos.y < self.height: - grid_y, grid_x = int(grid_pos.y), int(grid_pos.x) - return int(self.grid[grid_y, grid_x]) - return None - - def set_value(self, point: VectorLike, value: int = 0) -> bool: - grid_pos = self.world_to_grid(point) - - if 0 <= grid_pos.x < self.width and 0 <= grid_pos.y < self.height: - grid_y, grid_x = int(grid_pos.y), int(grid_pos.x) - self.grid[grid_y, grid_x] = value - return True - return False - - def smudge( - self, - kernel_size: int = 3, - iterations: int = 20, - decay_factor: float = 0.9, - threshold: int = 90, - preserve_unknown: bool = False, - ) -> "Costmap": - """ - Creates a new costmap with expanded obstacles (smudged). - - Args: - kernel_size: Size of the convolution kernel for dilation (must be odd) - iterations: Number of dilation iterations - decay_factor: Factor to reduce cost as distance increases (0.0-1.0) - threshold: Minimum cost value to consider as an obstacle for expansion - preserve_unknown: Whether to keep unknown (-1) cells as unknown - - Returns: - A new Costmap instance with expanded obstacles - """ - # Make sure kernel size is odd - if kernel_size % 2 == 0: - kernel_size += 1 - - # Create a copy of the grid for processing - grid_copy = self.grid.copy() - - # Create a mask of unknown cells if needed - unknown_mask = None - if preserve_unknown: - unknown_mask = grid_copy == -1 - # Temporarily replace unknown cells with 0 for processing - # This allows smudging to go over unknown areas - grid_copy[unknown_mask] = 0 - - # Create a mask of cells that are above the threshold - obstacle_mask = grid_copy >= threshold - - # Create a binary map of obstacles - binary_map = obstacle_mask.astype(np.uint8) * 100 - - # Create a circular kernel for dilation (instead of square) - y, x = np.ogrid[ - -kernel_size // 2 : kernel_size // 2 + 1, - -kernel_size // 2 : kernel_size // 2 + 1, - ] - kernel = (x * x + y * y <= (kernel_size // 2) * (kernel_size // 2)).astype(np.uint8) - - # Create distance map using dilation - # Each iteration adds one 'ring' of cells around obstacles - dilated_map = binary_map.copy() - - # Store each layer of dilation with decreasing values - layers = [] - - # First layer is the original obstacle cells - layers.append(binary_map.copy()) - - for i in range(iterations): - # Dilate the binary map - dilated = ndimage.binary_dilation(dilated_map > 0, structure=kernel, iterations=1).astype(np.uint8) - - # Calculate the new layer (cells that were just added in this iteration) - new_layer = (dilated - (dilated_map > 0).astype(np.uint8)) * 100 - - # Apply decay factor based on distance from obstacle - new_layer = new_layer * (decay_factor ** (i + 1)) - - # Add to layers list - layers.append(new_layer) - - # Update dilated map for next iteration - dilated_map = dilated * 100 - - # Combine all layers to create a distance-based cost map - smudged_map = np.zeros_like(grid_copy) - for layer in layers: - # For each cell, keep the maximum value across all layers - smudged_map = np.maximum(smudged_map, layer) - - # Preserve original obstacles - smudged_map[obstacle_mask] = grid_copy[obstacle_mask] - - # When preserve_unknown is true, restore all original unknown cells - # This overlays unknown cells on top of the smudged map - if preserve_unknown and unknown_mask is not None: - smudged_map[unknown_mask] = -1 - - # Ensure cost values are in valid range (0-100) except for unknown (-1) - if preserve_unknown and unknown_mask is not None: - valid_cells = ~unknown_mask - smudged_map[valid_cells] = np.clip(smudged_map[valid_cells], 0, 100) - else: - smudged_map = np.clip(smudged_map, 0, 100) - - # Create a new costmap with the smudged grid - return Costmap( - grid=smudged_map.astype(np.int8), - resolution=self.resolution, - origin=self.origin, - ) - - def __str__(self) -> str: - """ - Create a string representation of the Costmap. - - Returns: - A formatted string with key costmap information - """ - # Calculate occupancy statistics - total_cells = self.width * self.height - occupied_cells = np.sum(self.grid >= 0.1) - unknown_cells = np.sum(self.grid == -1) - free_cells = total_cells - occupied_cells - unknown_cells - - # Calculate percentages - occupied_percent = (occupied_cells / total_cells) * 100 - unknown_percent = (unknown_cells / total_cells) * 100 - free_percent = (free_cells / total_cells) * 100 - - cell_info = [ - "▦ Costmap", - f"{self.width}x{self.height}", - f"({self.width * self.resolution:.1f}x{self.height * self.resolution:.1f}m @", - f"{1 / self.resolution:.0f}cm res)", - f"Origin: ({x(self.origin):.2f}, {y(self.origin):.2f})", - f"▣ {occupied_percent:.1f}%", - f"□ {free_percent:.1f}%", - f"◌ {unknown_percent:.1f}%", - ] - - return " ".join(cell_info) - - @property - def o3d_geometry(self): - return self.pointcloud - - @property - def pointcloud(self, *, res: float = 0.25, origin=(0.0, 0.0), show_unknown: bool = False): - """ - Visualise a 2-D costmap (int8, −1…100) as an Open3D PointCloud. - - • −1 → ‘unknown’ (optionally drawn as mid-grey, or skipped) - • 0 → free - • 1-99→ graduated cost (turbo colour-ramp) - • 100 → lethal / obstacle (red end of ramp) - - Parameters - ---------- - res : float - Cell size in metres. - origin : (float, float) - World-space coord of costmap [row0,col0] centre. - show_unknown : bool - If true, draw unknown cells in grey; otherwise omit them. - """ - cost = np.asarray(self.grid, dtype=np.int16) - if cost.ndim != 2: - raise ValueError("cost map must be 2-D (H×W)") - - H, W = cost.shape - ys, xs = np.mgrid[0:H, 0:W] - - # ---------- flatten & mask -------------------------------------------------- - xs = xs.ravel() - ys = ys.ravel() - vals = cost.ravel() - - unknown_mask = vals == -1 - if not show_unknown: - keep = ~unknown_mask - xs, ys, vals = xs[keep], ys[keep], vals[keep] - - # ---------- 3-D points ------------------------------------------------------ - xyz = np.column_stack( - ( - (xs + 0.5) * res + origin[0], # X - (ys + 0.5) * res + origin[1], # Y - np.zeros_like(xs, dtype=np.float32), # Z = 0 - ) - ) - - # ---------- colours --------------------------------------------------------- - rgb = np.empty((len(vals), 3), dtype=np.float32) - - if show_unknown: - # mid-grey for unknown - rgb[unknown_mask[~unknown_mask if not show_unknown else slice(None)]] = ( - 0.4, - 0.4, - 0.4, - ) - - # normalise valid costs: 0…100 → 0…1 - norm = np.clip(vals.astype(np.float32), 0, 100) / 100.0 - rgb_valid = cm.turbo(norm)[:, :3] # type: ignore[attr-defined] # strip alpha - rgb[:] = rgb_valid # unknown already set if needed - - pcd = o3d.geometry.PointCloud() - pcd.points = o3d.utility.Vector3dVector(xyz) - pcd.colors = o3d.utility.Vector3dVector(rgb) - - return pcd diff --git a/dimos/robot/unitree_webrtc/type/lidar.py b/dimos/robot/unitree_webrtc/type/lidar.py index 37a51b702a..ea1c9fe7e2 100644 --- a/dimos/robot/unitree_webrtc/type/lidar.py +++ b/dimos/robot/unitree_webrtc/type/lidar.py @@ -1,12 +1,26 @@ -from dimos.robot.unitree_webrtc.testing.helpers import color -from datetime import datetime -from dimos.robot.unitree_webrtc.type.timeseries import Timestamped, to_datetime, to_human_readable -from dimos.types.vector import Vector -from dataclasses import dataclass, field -from typing import List, TypedDict +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +from typing import TypedDict + import numpy as np import open3d as o3d -from copy import copy + +from dimos.msgs.geometry_msgs import Vector3 +from dimos.msgs.sensor_msgs import PointCloud2 +from dimos.types.timestamped import to_human_readable class RawLidarPoints(TypedDict): @@ -17,11 +31,11 @@ class RawLidarData(TypedDict): """Data portion of the LIDAR message""" frame_id: str - origin: List[float] + origin: list[float] resolution: float src_size: int stamp: float - width: List[int] + width: list[int] data: RawLidarPoints @@ -33,29 +47,46 @@ class RawLidarMsg(TypedDict): data: RawLidarData -@dataclass -class LidarMessage(Timestamped): - ts: datetime - origin: Vector - resolution: float - pointcloud: o3d.geometry.PointCloud - raw_msg: RawLidarMsg = field(repr=False, default=None) +class LidarMessage(PointCloud2): + resolution: float # we lose resolution when encoding PointCloud2 + origin: Vector3 + raw_msg: RawLidarMsg | None + # _costmap: Optional[Costmap] = None # TODO: Fix after costmap migration + + def __init__(self, **kwargs) -> None: + super().__init__( + pointcloud=kwargs.get("pointcloud"), + ts=kwargs.get("ts"), + frame_id="world", + ) + + self.origin = kwargs.get("origin") + self.resolution = kwargs.get("resolution", 0.05) @classmethod - def from_msg(cls, raw_message: RawLidarMsg) -> "LidarMessage": + def from_msg(cls: "LidarMessage", raw_message: RawLidarMsg, **kwargs) -> "LidarMessage": data = raw_message["data"] points = data["data"]["points"] - point_cloud = o3d.geometry.PointCloud() - point_cloud.points = o3d.utility.Vector3dVector(points) - return cls( - ts=to_datetime(data["stamp"]), - origin=Vector(data["origin"]), - resolution=data["resolution"], - pointcloud=point_cloud, - raw_msg=raw_message, - ) - - def __repr__(self): + pointcloud = o3d.geometry.PointCloud() + pointcloud.points = o3d.utility.Vector3dVector(points) + + origin = Vector3(data["origin"]) + # webrtc decoding via native decompression doesn't require us + # to shift the pointcloud by it's origin + # + # pointcloud.translate((origin / 2).to_tuple()) + cls_data = { + "origin": origin, + "resolution": data["resolution"], + "pointcloud": pointcloud, + # - this is broken in unitree webrtc api "stamp":1.758148e+09 + "ts": time.time(), # data["stamp"], + "raw_msg": raw_message, + **kwargs, + } + return cls(**cls_data) + + def __repr__(self) -> str: return f"LidarMessage(ts={to_human_readable(self.ts)}, origin={self.origin}, resolution={self.resolution}, {self.pointcloud})" def __iadd__(self, other: "LidarMessage") -> "LidarMessage": @@ -63,21 +94,19 @@ def __iadd__(self, other: "LidarMessage") -> "LidarMessage": return self def __add__(self, other: "LidarMessage") -> "LidarMessage": - # Create a new point cloud combining both - # Determine which message is more recent - if self.timestamp >= other.timestamp: - timestamp = self.timestamp + if self.ts >= other.ts: + ts = self.ts origin = self.origin resolution = self.resolution else: - timestamp = other.timestamp + ts = other.ts origin = other.origin resolution = other.resolution # Return a new LidarMessage with combined data return LidarMessage( - timestamp=timestamp, + ts=ts, origin=origin, resolution=resolution, pointcloud=self.pointcloud + other.pointcloud, @@ -87,53 +116,16 @@ def __add__(self, other: "LidarMessage") -> "LidarMessage": def o3d_geometry(self): return self.pointcloud - def icp(self, other: "LidarMessage") -> o3d.pipelines.registration.RegistrationResult: - self.estimate_normals() - other.estimate_normals() - - reg_p2l = o3d.pipelines.registration.registration_icp( - self.pointcloud, - other.pointcloud, - 0.1, - np.identity(4), - o3d.pipelines.registration.TransformationEstimationPointToPlane(), - o3d.pipelines.registration.ICPConvergenceCriteria(max_iteration=100), - ) - - return reg_p2l - - def transform(self, transform) -> "LidarMessage": - self.pointcloud.transform(transform) - return self - - def clone(self) -> "LidarMessage": - return self.copy() - - def copy(self) -> "LidarMessage": - return LidarMessage( - ts=self.ts, - origin=copy(self.origin), - resolution=self.resolution, - # TODO: seems to work, but will it cause issues because of the shallow copy? - pointcloud=copy(self.pointcloud), - ) - - def icptransform(self, other): - return self.transform(self.icp(other).transformation) - - def estimate_normals(self) -> "LidarMessage": - # Check if normals already exist by testing if the normals attribute has data - if not self.pointcloud.has_normals() or len(self.pointcloud.normals) == 0: - self.pointcloud.estimate_normals(search_param=o3d.geometry.KDTreeSearchParamHybrid(radius=0.1, max_nn=30)) - return self - - def color(self, color_choice) -> "LidarMessage": - def get_color(color_choice): - if isinstance(color_choice, int): - return color[color_choice] - return color_choice - - self.pointcloud.paint_uniform_color(get_color(color_choice)) - # Looks like we'll be displaying so might as well? - self.estimate_normals() - return self + # TODO: Fix after costmap migration + # def costmap(self, voxel_size: float = 0.2) -> Costmap: + # if not self._costmap: + # down_sampled_pointcloud = self.pointcloud.voxel_down_sample(voxel_size=voxel_size) + # inflate_radius_m = 1.0 * voxel_size if voxel_size > self.resolution else 0.0 + # grid, origin_xy = pointcloud_to_costmap( + # down_sampled_pointcloud, + # resolution=self.resolution, + # inflate_radius_m=inflate_radius_m, + # ) + # self._costmap = Costmap(grid=grid, origin=[*origin_xy, 0.0], resolution=self.resolution) + # + # return self._costmap diff --git a/dimos/robot/unitree_webrtc/type/lowstate.py b/dimos/robot/unitree_webrtc/type/lowstate.py index 48c0d23a5f..c50504135c 100644 --- a/dimos/robot/unitree_webrtc/type/lowstate.py +++ b/dimos/robot/unitree_webrtc/type/lowstate.py @@ -1,4 +1,18 @@ -from typing import TypedDict, List, Literal +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Literal, TypedDict raw_odom_msg_sample = { "type": "msg", @@ -47,11 +61,11 @@ class MotorState(TypedDict): q: float temperature: int lost: int - reserve: List[int] + reserve: list[int] class ImuState(TypedDict): - rpy: List[float] + rpy: list[float] class BmsState(TypedDict): @@ -60,15 +74,15 @@ class BmsState(TypedDict): soc: int current: int cycle: int - bq_ntc: List[int] - mcu_ntc: List[int] + bq_ntc: list[int] + mcu_ntc: list[int] class LowStateData(TypedDict): imu_state: ImuState - motor_state: List[MotorState] + motor_state: list[MotorState] bms_state: BmsState - foot_force: List[int] + foot_force: list[int] temperature_ntc1: int power_v: float diff --git a/dimos/robot/unitree_webrtc/type/map.py b/dimos/robot/unitree_webrtc/type/map.py index eef15bdeef..ea02ae47d0 100644 --- a/dimos/robot/unitree_webrtc/type/map.py +++ b/dimos/robot/unitree_webrtc/type/map.py @@ -1,46 +1,126 @@ -import open3d as o3d +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + import numpy as np -from dataclasses import dataclass -from typing import Tuple, Optional +import open3d as o3d +from reactivex import interval +from reactivex.disposable import Disposable +from dimos.core import In, Module, Out, rpc +from dimos.core.global_config import GlobalConfig +from dimos.msgs.nav_msgs import OccupancyGrid +from dimos.msgs.sensor_msgs import PointCloud2 from dimos.robot.unitree_webrtc.type.lidar import LidarMessage -from dimos.robot.unitree_webrtc.type.costmap import Costmap -from reactivex.observable import Observable -import reactivex.operators as ops +class Map(Module): + lidar: In[LidarMessage] = None + global_map: Out[LidarMessage] = None + global_costmap: Out[OccupancyGrid] = None + local_costmap: Out[OccupancyGrid] = None -@dataclass -class Map: pointcloud: o3d.geometry.PointCloud = o3d.geometry.PointCloud() - voxel_size: float = 0.05 - cost_resolution: float = 0.05 + def __init__( + self, + voxel_size: float = 0.05, + cost_resolution: float = 0.05, + global_publish_interval: float | None = None, + min_height: float = 0.15, + max_height: float = 0.6, + global_config: GlobalConfig | None = None, + **kwargs, + ) -> None: + self.voxel_size = voxel_size + self.cost_resolution = cost_resolution + self.global_publish_interval = global_publish_interval + self.min_height = min_height + self.max_height = max_height + + if global_config: + if global_config.use_simulation: + self.min_height = 0.3 + + super().__init__(**kwargs) + + @rpc + def start(self) -> None: + super().start() + + unsub = self.lidar.subscribe(self.add_frame) + self._disposables.add(Disposable(unsub)) + + def publish(_) -> None: + self.global_map.publish(self.to_lidar_message()) + + # temporary, not sure if it belogs in mapper + # used only for visualizations, not for any algo + occupancygrid = OccupancyGrid.from_pointcloud( + self.to_lidar_message(), + resolution=self.cost_resolution, + min_height=self.min_height, + max_height=self.max_height, + ) + + self.global_costmap.publish(occupancygrid) + + if self.global_publish_interval is not None: + unsub = interval(self.global_publish_interval).subscribe(publish) + self._disposables.add(unsub) + + @rpc + def stop(self) -> None: + super().stop() + + def to_PointCloud2(self) -> PointCloud2: + return PointCloud2( + pointcloud=self.pointcloud, + ts=time.time(), + ) + + def to_lidar_message(self) -> LidarMessage: + return LidarMessage( + pointcloud=self.pointcloud, + origin=[0.0, 0.0, 0.0], + resolution=self.voxel_size, + ts=time.time(), + ) + + @rpc def add_frame(self, frame: LidarMessage) -> "Map": """Voxelise *frame* and splice it into the running map.""" new_pct = frame.pointcloud.voxel_down_sample(voxel_size=self.voxel_size) - self.pointcloud = splice_cylinder(self.pointcloud, new_pct, shrink=0.5) - return self - def consume(self, observable: Observable[LidarMessage]) -> Observable["Map"]: - """Reactive operator that folds a stream of `LidarMessage` into the map.""" - return observable.pipe(ops.map(self.add_frame)) + # Skip for empty pointclouds. + if len(new_pct.points) == 0: + return self + + self.pointcloud = splice_cylinder(self.pointcloud, new_pct, shrink=0.5) + local_costmap = OccupancyGrid.from_pointcloud( + frame, + resolution=self.cost_resolution, + min_height=0.15, + max_height=0.6, + ).gradient(max_distance=0.25) + self.local_costmap.publish(local_costmap) @property def o3d_geometry(self) -> o3d.geometry.PointCloud: return self.pointcloud - @property - def costmap(self) -> Costmap: - """Return a fully inflated cost-map in a `Costmap` wrapper.""" - inflate_radius_m = 0.5 * self.voxel_size if self.voxel_size > self.cost_resolution else 0.0 - grid, origin_xy = pointcloud_to_costmap( - self.pointcloud, - resolution=self.cost_resolution, - inflate_radius_m=inflate_radius_m, - ) - return Costmap(grid=grid, origin=[*origin_xy, 0.0], resolution=self.cost_resolution) - def splice_sphere( map_pcd: o3d.geometry.PointCloud, @@ -77,92 +157,17 @@ def splice_cylinder( map_pts = np.asarray(map_pcd.points) planar_dists_map = np.linalg.norm(map_pts[:, axes] - center[axes], axis=1) - victims = np.nonzero((planar_dists_map < radius) & (map_pts[:, axis] >= axis_min) & (map_pts[:, axis] <= axis_max))[ - 0 - ] + victims = np.nonzero( + (planar_dists_map < radius) + & (map_pts[:, axis] >= axis_min) + & (map_pts[:, axis] <= axis_max) + )[0] survivors = map_pcd.select_by_index(victims, invert=True) return survivors + patch_pcd -def _inflate_lethal(costmap: np.ndarray, radius: int, lethal_val: int = 100) -> np.ndarray: - """Return *costmap* with lethal cells dilated by *radius* grid steps (circular).""" - if radius <= 0 or not np.any(costmap == lethal_val): - return costmap - - mask = costmap == lethal_val - dilated = mask.copy() - for dy in range(-radius, radius + 1): - for dx in range(-radius, radius + 1): - if dx * dx + dy * dy > radius * radius or (dx == 0 and dy == 0): - continue - dilated |= np.roll(mask, shift=(dy, dx), axis=(0, 1)) - - out = costmap.copy() - out[dilated] = lethal_val - return out - - -def pointcloud_to_costmap( - pcd: o3d.geometry.PointCloud, - *, - resolution: float = 0.05, - ground_z: float = 0.0, - obs_min_height: float = 0.15, - max_height: Optional[float] = 0.5, - inflate_radius_m: Optional[float] = None, - default_unknown: int = -1, - cost_free: int = 0, - cost_lethal: int = 100, -) -> Tuple[np.ndarray, np.ndarray]: - """Rasterise *pcd* into a 2-D int8 cost-map with optional obstacle inflation. - - Grid origin is **aligned** to the `resolution` lattice so that when - `resolution == voxel_size` every voxel centroid lands squarely inside a cell - (no alternating blank lines). - """ - - pts = np.asarray(pcd.points, dtype=np.float32) - if pts.size == 0: - return np.full((1, 1), default_unknown, np.int8), np.zeros(2, np.float32) - - # 0. Ceiling filter -------------------------------------------------------- - if max_height is not None: - pts = pts[pts[:, 2] <= max_height] - if pts.size == 0: - return np.full((1, 1), default_unknown, np.int8), np.zeros(2, np.float32) - - # 1. Bounding box & aligned origin --------------------------------------- - xy_min = pts[:, :2].min(axis=0) - xy_max = pts[:, :2].max(axis=0) - - # Align origin to the resolution grid (anchor = 0,0) - origin = np.floor(xy_min / resolution) * resolution - - # Grid dimensions (inclusive) ------------------------------------------- - Nx, Ny = (np.ceil((xy_max - origin) / resolution).astype(int) + 1).tolist() - - # 2. Bin points ------------------------------------------------------------ - idx_xy = np.floor((pts[:, :2] - origin) / resolution).astype(np.int32) - np.clip(idx_xy[:, 0], 0, Nx - 1, out=idx_xy[:, 0]) - np.clip(idx_xy[:, 1], 0, Ny - 1, out=idx_xy[:, 1]) - - lin = idx_xy[:, 1] * Nx + idx_xy[:, 0] - z_max = np.full(Nx * Ny, -np.inf, np.float32) - np.maximum.at(z_max, lin, pts[:, 2]) - z_max = z_max.reshape(Ny, Nx) - - # 3. Cost rules ----------------------------------------------------------- - costmap = np.full_like(z_max, default_unknown, np.int8) - known = z_max != -np.inf - costmap[known] = cost_free - - lethal = z_max >= (ground_z + obs_min_height) - costmap[lethal] = cost_lethal - - # 4. Optional inflation ---------------------------------------------------- - if inflate_radius_m and inflate_radius_m > 0: - cells = int(np.ceil(inflate_radius_m / resolution)) - costmap = _inflate_lethal(costmap, cells, lethal_val=cost_lethal) - - return costmap, origin.astype(np.float32) +mapper = Map.blueprint + + +__all__ = ["Map", "mapper"] diff --git a/dimos/robot/unitree_webrtc/type/odometry.py b/dimos/robot/unitree_webrtc/type/odometry.py index df10bd8d54..52a8544fbc 100644 --- a/dimos/robot/unitree_webrtc/type/odometry.py +++ b/dimos/robot/unitree_webrtc/type/odometry.py @@ -1,9 +1,23 @@ -from typing import TypedDict, Literal -from datetime import datetime -from dataclasses import dataclass -from dimos.types.vector import Vector -from dimos.types.position import Position -from dimos.robot.unitree_webrtc.type.timeseries import Timestamped, to_human_readable +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import time +from typing import Literal, TypedDict + +from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Vector3 +from dimos.robot.unitree_webrtc.type.timeseries import ( + Timestamped, +) raw_odometry_msg_sample = { "type": "msg", @@ -41,14 +55,14 @@ class Orientation(TypedDict): w: float -class Pose(TypedDict): +class PoseData(TypedDict): position: RawPosition orientation: Orientation class OdometryData(TypedDict): header: Header - pose: Pose + pose: PoseData class RawOdometryMessage(TypedDict): @@ -57,18 +71,35 @@ class RawOdometryMessage(TypedDict): data: OdometryData -@dataclass -class Odometry(Timestamped, Position): - ts: datetime +class Odometry(PoseStamped, Timestamped): + name = "geometry_msgs.PoseStamped" + + def __init__(self, frame_id: str = "base_link", *args, **kwargs) -> None: + super().__init__(frame_id=frame_id, *args, **kwargs) @classmethod def from_msg(cls, msg: RawOdometryMessage) -> "Odometry": pose = msg["data"]["pose"] - orientation = pose["orientation"] - position = pose["position"] - pos = Vector(position.get("x"), position.get("y"), position.get("z")) - rot = Vector(orientation.get("x"), orientation.get("y"), orientation.get("z")) - return cls(pos=pos, rot=rot, ts=msg["data"]["header"]["stamp"]) + + # Extract position + pos = Vector3( + pose["position"].get("x"), + pose["position"].get("y"), + pose["position"].get("z"), + ) + + rot = Quaternion( + pose["orientation"].get("x"), + pose["orientation"].get("y"), + pose["orientation"].get("z"), + pose["orientation"].get("w"), + ) + + # ts = to_timestamp(msg["data"]["header"]["stamp"]) + # lidar / video timestamps are not available from the robot + # so we are deferring to local time for everything + ts = time.time() + return Odometry(position=pos, orientation=rot, ts=ts, frame_id="world") def __repr__(self) -> str: - return f"Odom ts({to_human_readable(self.ts)}) pos({self.pos}), rot({self.rot})" + return f"Odom pos({self.position}), rot({self.orientation})" diff --git a/dimos/robot/unitree_webrtc/type/test_lidar.py b/dimos/robot/unitree_webrtc/type/test_lidar.py index 2c80f9013a..93435e8e4b 100644 --- a/dimos/robot/unitree_webrtc/type/test_lidar.py +++ b/dimos/robot/unitree_webrtc/type/test_lidar.py @@ -1,120 +1,28 @@ -import pytest -import time -import open3d as o3d +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import itertools -from dimos.types.vector import Vector from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.utils.testing import SensorReplay -from dimos.robot.unitree_webrtc.testing.mock import Mock -from dimos.robot.unitree_webrtc.testing.helpers import show3d, multivis, benchmark +def test_init() -> None: + lidar = SensorReplay("office_lidar") -def test_load(): - mock = Mock("test") - frame = mock.load("a") - - # Validate the result - assert isinstance(frame, LidarMessage) - assert isinstance(frame.timestamp, float) - assert isinstance(frame.origin, Vector) - assert isinstance(frame.resolution, float) - assert isinstance(frame.pointcloud, o3d.geometry.PointCloud) - assert len(frame.pointcloud.points) > 0 - - -def test_add(): - mock = Mock("test") - [frame_a, frame_b] = mock.load("a", "b") - - # Get original point counts - points_a = len(frame_a.pointcloud.points) - points_b = len(frame_b.pointcloud.points) - - # Add the frames - combined = frame_a + frame_b - - assert isinstance(combined, LidarMessage) - assert len(combined.pointcloud.points) == points_a + points_b - - # Check metadata is from the most recent message - if frame_a.timestamp >= frame_b.timestamp: - assert combined.timestamp == frame_a.timestamp - assert combined.origin == frame_a.origin - assert combined.resolution == frame_a.resolution - else: - assert combined.timestamp == frame_b.timestamp - assert combined.origin == frame_b.origin - assert combined.resolution == frame_b.resolution - - -@pytest.mark.vis -def test_icp_vis(): - mock = Mock("test") - [framea, frameb] = mock.load("a", "b") - - # framea.pointcloud = framea.pointcloud.voxel_down_sample(voxel_size=0.1) - # frameb.pointcloud = frameb.pointcloud.voxel_down_sample(voxel_size=0.1) - - framea.color(0) - frameb.color(1) - - # Normally this is a mutating operation (for efficiency) - # but here we need an original frame A for the visualizer - framea_icp = framea.copy().icptransform(frameb) - - multivis( - show3d(framea, title="frame a"), - show3d(frameb, title="frame b"), - show3d((framea + frameb), title="union"), - show3d((framea_icp + frameb), title="ICP"), - ) - - -@pytest.mark.benchmark -def test_benchmark_icp(): - frames = Mock("dynamic_house").iterate() - - prev_frame = None - - def icptest(): - nonlocal prev_frame - start = time.time() - - current_frame = frames.__next__() - if not prev_frame: - prev_frame = frames.__next__() - end = time.time() - - current_frame.icptransform(prev_frame) - # for subtracting the time of the function exec - return (end - start) * -1 - - ms = benchmark(100, icptest) - assert ms < 20, "ICP took too long" - - print(f"ICP takes {ms:.2f} ms") - - -@pytest.mark.vis -def test_downsample(): - mock = Mock("test") - [framea, frameb] = mock.load("a", "b") - - # framea.pointcloud = framea.pointcloud.voxel_down_sample(voxel_size=0.1) - # frameb.pointcloud = frameb.pointcloud.voxel_down_sample(voxel_size=0.1) - - # framea.color(0) - # frameb.color(1) - - # Normally this is a mutating operation (for efficiency) - # but here we need an original frame A for the visualizer - # framea_icp = framea.copy().icptransform(frameb) - pcd = framea.copy().pointcloud - newpcd, _, _ = pcd.voxel_down_sample_and_trace( - voxel_size=0.25, min_bound=pcd.get_min_bound(), max_bound=pcd.get_max_bound(), approximate_class=False - ) - - multivis( - show3d(framea, title="frame a"), - show3d(newpcd, title="frame a downsample"), - ) + for raw_frame in itertools.islice(lidar.iterate(), 5): + assert isinstance(raw_frame, dict) + frame = LidarMessage.from_msg(raw_frame) + assert isinstance(frame, LidarMessage) diff --git a/dimos/robot/unitree_webrtc/type/test_map.py b/dimos/robot/unitree_webrtc/type/test_map.py index 8533371a45..12ee8f832d 100644 --- a/dimos/robot/unitree_webrtc/type/test_map.py +++ b/dimos/robot/unitree_webrtc/type/test_map.py @@ -1,29 +1,58 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import pytest + +from dimos.robot.unitree_webrtc.testing.helpers import show3d from dimos.robot.unitree_webrtc.testing.mock import Mock -from dimos.robot.unitree_webrtc.testing.helpers import show3d_stream, show3d -from dimos.robot.unitree_webrtc.utils.reactive import backpressure -from dimos.robot.unitree_webrtc.type.map import splice_sphere, Map -from dimos.robot.unitree_webrtc.lidar import lidar +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.robot.unitree_webrtc.type.map import Map, splice_sphere +from dimos.utils.testing import SensorReplay @pytest.mark.vis -def test_costmap_vis(): +def test_costmap_vis() -> None: map = Map() - for frame in Mock("office").iterate(): + map.start() + mock = Mock("office") + frames = list(mock.iterate()) + + for frame in frames: print(frame) map.add_frame(frame) - costmap = map.costmap - print(costmap) - show3d(costmap.smudge().pointcloud, title="Costmap").run() + + # Get global map and costmap + global_map = map.to_lidar_message() + print(f"Global map has {len(global_map.pointcloud.points)} points") + show3d(global_map.pointcloud, title="Global Map").run() @pytest.mark.vis -def test_reconstruction_with_realtime_vis(): - show3d_stream(Map().consume(Mock("office").stream(rate_hz=60.0)), clearframe=True).run() +def test_reconstruction_with_realtime_vis() -> None: + map = Map() + map.start() + mock = Mock("office") + + # Process frames and visualize final map + for frame in mock.iterate(): + map.add_frame(frame) + + show3d(map.pointcloud, title="Reconstructed Map").run() @pytest.mark.vis -def test_splice_vis(): +def test_splice_vis() -> None: mock = Mock("test") target = mock.load("a") insert = mock.load("b") @@ -31,9 +60,41 @@ def test_splice_vis(): @pytest.mark.vis -def test_robot_vis(): - show3d_stream( - Map().consume(backpressure(lidar())), - clearframe=True, - title="gloal dynamic map test", - ) +def test_robot_vis() -> None: + map = Map() + map.start() + mock = Mock("office") + + # Process all frames + for frame in mock.iterate(): + map.add_frame(frame) + + show3d(map.pointcloud, title="global dynamic map test").run() + + +def test_robot_mapping() -> None: + lidar_replay = SensorReplay("office_lidar", autocast=LidarMessage.from_msg) + map = Map(voxel_size=0.5) + + # Mock the output streams to avoid publishing errors + class MockStream: + def publish(self, msg) -> None: + pass # Do nothing + + map.local_costmap = MockStream() + map.global_costmap = MockStream() + map.global_map = MockStream() + + # Process all frames from replay + for frame in lidar_replay.iterate(): + map.add_frame(frame) + + # Check the built map + global_map = map.to_lidar_message() + pointcloud = global_map.pointcloud + + # Verify map has points + assert len(pointcloud.points) > 0 + print(f"Map contains {len(pointcloud.points)} points") + + map._close_module() diff --git a/dimos/robot/unitree_webrtc/type/test_odometry.py b/dimos/robot/unitree_webrtc/type/test_odometry.py index 3061eeb92e..b1a251b254 100644 --- a/dimos/robot/unitree_webrtc/type/test_odometry.py +++ b/dimos/robot/unitree_webrtc/type/test_odometry.py @@ -1,8 +1,108 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from operator import add, sub +import os +import threading + +from dotenv import load_dotenv +import pytest +import reactivex.operators as ops + from dimos.robot.unitree_webrtc.type.odometry import Odometry -from dimos.robot.unitree_webrtc.testing.multimock import Multimock +from dimos.utils.testing import SensorReplay, SensorStorage + +_EXPECTED_TOTAL_RAD = -4.05212 + + +def test_dataset_size() -> None: + """Ensure the replay contains the expected number of messages.""" + assert sum(1 for _ in SensorReplay(name="raw_odometry_rotate_walk").iterate()) == 179 + + +def test_odometry_conversion_and_count() -> None: + """Each replay entry converts to :class:`Odometry` and count is correct.""" + for raw in SensorReplay(name="raw_odometry_rotate_walk").iterate(): + odom = Odometry.from_msg(raw) + assert isinstance(raw, dict) + assert isinstance(odom, Odometry) + + +def test_last_yaw_value() -> None: + """Verify yaw of the final message (regression guard).""" + last_msg = SensorReplay(name="raw_odometry_rotate_walk").stream().pipe(ops.last()).run() + + assert last_msg is not None, "Replay is empty" + assert last_msg["data"]["pose"]["orientation"] == { + "x": 0.01077, + "y": 0.008505, + "z": 0.499171, + "w": -0.866395, + } + + +def test_total_rotation_travel_iterate() -> None: + total_rad = 0.0 + prev_yaw: float | None = None + + for odom in SensorReplay(name="raw_odometry_rotate_walk", autocast=Odometry.from_msg).iterate(): + yaw = odom.orientation.radians.z + if prev_yaw is not None: + diff = yaw - prev_yaw + total_rad += diff + prev_yaw = yaw + + assert total_rad == pytest.approx(_EXPECTED_TOTAL_RAD, abs=0.001) + + +def test_total_rotation_travel_rxpy() -> None: + total_rad = ( + SensorReplay(name="raw_odometry_rotate_walk", autocast=Odometry.from_msg) + .stream() + .pipe( + ops.map(lambda odom: odom.orientation.radians.z), + ops.pairwise(), # [1,2,3,4] -> [[1,2], [2,3], [3,4]] + ops.starmap(sub), # [sub(1,2), sub(2,3), sub(3,4)] + ops.reduce(add), + ) + .run() + ) + + assert total_rad == pytest.approx(4.05, abs=0.01) + + +# data collection tool +@pytest.mark.tool +def test_store_odometry_stream() -> None: + from dimos.robot.unitree_webrtc.unitree_go2 import UnitreeGo2 + + load_dotenv() + + robot = UnitreeGo2(ip=os.getenv("ROBOT_IP"), mode="ai") + robot.standup() + + storage = SensorStorage("raw_odometry_rotate_walk") + storage.save_stream(robot.raw_odom_stream()) + shutdown = threading.Event() -def test_odometry_time(): - (timestamp, odom_raw) = Multimock("athens_odom").load_one(33) - print("RAW MSG", odom_raw) - print(Odometry.from_msg(odom_raw)) + try: + while not shutdown.wait(0.1): + pass + except KeyboardInterrupt: + shutdown.set() + finally: + robot.liedown() diff --git a/dimos/robot/unitree_webrtc/type/test_timeseries.py b/dimos/robot/unitree_webrtc/type/test_timeseries.py index 00f29c3202..b7c955933d 100644 --- a/dimos/robot/unitree_webrtc/type/test_timeseries.py +++ b/dimos/robot/unitree_webrtc/type/test_timeseries.py @@ -1,7 +1,20 @@ -from datetime import timedelta, datetime -from typing import TypeVar -from dimos.robot.unitree_webrtc.type.timeseries import TEvent, TList +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import datetime, timedelta +from dimos.robot.unitree_webrtc.type.timeseries import TEvent, TList fixed_date = datetime(2025, 5, 13, 15, 2, 5).astimezone() start_event = TEvent(fixed_date, 1) @@ -10,22 +23,22 @@ sample_list = TList([start_event, TEvent(fixed_date + timedelta(seconds=2), 5), end_event]) -def test_repr(): +def test_repr() -> None: assert ( str(sample_list) == "Timeseries(date=2025-05-13, start=15:02:05, end=15:02:15, duration=0:00:10, events=3, freq=0.30Hz)" ) -def test_equals(): +def test_equals() -> None: assert start_event == TEvent(start_event.ts, 1) assert start_event != TEvent(start_event.ts, 2) assert start_event != TEvent(start_event.ts + timedelta(seconds=1), 1) -def test_range(): +def test_range() -> None: assert sample_list.time_range() == (start_event.ts, end_event.ts) -def test_duration(): +def test_duration() -> None: assert sample_list.duration() == timedelta(seconds=10) diff --git a/dimos/robot/unitree_webrtc/type/timeseries.py b/dimos/robot/unitree_webrtc/type/timeseries.py index 84d2910622..a85afba93f 100644 --- a/dimos/robot/unitree_webrtc/type/timeseries.py +++ b/dimos/robot/unitree_webrtc/type/timeseries.py @@ -1,8 +1,25 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from __future__ import annotations -from datetime import datetime, timedelta, timezone -from typing import Iterable, TypeVar, Generic, Tuple, Union, TypedDict + from abc import ABC, abstractmethod +from datetime import datetime, timedelta, timezone +from typing import TYPE_CHECKING, Generic, TypedDict, TypeVar, Union +if TYPE_CHECKING: + from collections.abc import Iterable PAYLOAD = TypeVar("PAYLOAD") @@ -15,7 +32,7 @@ class RosStamp(TypedDict): EpochLike = Union[int, float, datetime, RosStamp] -def from_ros_stamp(stamp: dict[str, int], tz: timezone = None) -> datetime: +def from_ros_stamp(stamp: dict[str, int], tz: timezone | None = None) -> datetime: """Convert ROS-style timestamp {'sec': int, 'nanosec': int} to datetime.""" return datetime.fromtimestamp(stamp["sec"] + stamp["nanosec"] / 1e9, tz=tz) @@ -25,12 +42,12 @@ def to_human_readable(ts: EpochLike) -> str: return dt.strftime("%Y-%m-%d %H:%M:%S") -def to_datetime(ts: EpochLike, tz: timezone = None) -> datetime: +def to_datetime(ts: EpochLike, tz: timezone | None = None) -> datetime: if isinstance(ts, datetime): # if ts.tzinfo is None: # ts = ts.astimezone(tz) return ts - if isinstance(ts, (int, float)): + if isinstance(ts, int | float): return datetime.fromtimestamp(ts, tz=tz) if isinstance(ts, dict) and "sec" in ts and "nanosec" in ts: return datetime.fromtimestamp(ts["sec"] + ts["nanosec"] / 1e9, tz=tz) @@ -40,14 +57,16 @@ def to_datetime(ts: EpochLike, tz: timezone = None) -> datetime: class Timestamped(ABC): """Abstract class for an event with a timestamp.""" - def __init__(self, timestamp: EpochLike): - self.ts = to_datetime(timestamp) + ts: datetime + + def __init__(self, ts: EpochLike) -> None: + self.ts = to_datetime(ts) class TEvent(Timestamped, Generic[PAYLOAD]): """Concrete class for an event with a timestamp and data.""" - def __init__(self, timestamp: EpochLike, data: PAYLOAD): + def __init__(self, timestamp: EpochLike, data: PAYLOAD) -> None: super().__init__(timestamp) self.data = data @@ -84,7 +103,7 @@ def frequency(self) -> float: """Calculate the frequency of events in Hz.""" return len(list(self)) / (self.duration().total_seconds() or 1) - def time_range(self) -> Tuple[datetime, datetime]: + def time_range(self) -> tuple[datetime, datetime]: """Return (earliest_ts, latest_ts). Empty input ⇒ ValueError.""" return self.start_time, self.end_time @@ -103,7 +122,7 @@ def closest_to(self, timestamp: EpochLike) -> EVENT: min_dist = float("inf") for event in self: - dist = abs(event.ts.timestamp() - target_ts) + dist = abs(event.ts - target_ts) if dist > min_dist: break diff --git a/dimos/robot/unitree_webrtc/type/vector.py b/dimos/robot/unitree_webrtc/type/vector.py index 368867dd4d..be00e3403c 100644 --- a/dimos/robot/unitree_webrtc/type/vector.py +++ b/dimos/robot/unitree_webrtc/type/vector.py @@ -1,14 +1,28 @@ -import numpy as np +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import builtins +from collections.abc import Iterable from typing import ( - Tuple, - List, - TypeVar, - Protocol, - runtime_checkable, Any, - Iterable, + Protocol, + TypeVar, Union, + runtime_checkable, ) + +import numpy as np from numpy.typing import NDArray T = TypeVar("T", bound="Vector") @@ -39,7 +53,7 @@ def yaw(self) -> float: return self.x @property - def tuple(self) -> Tuple[float, ...]: + def tuple(self) -> tuple[float, ...]: """Tuple representation of the vector.""" return tuple(self._data) @@ -116,7 +130,6 @@ def __add__(self: T, other: Union["Vector", Iterable[float]]) -> T: def __sub__(self: T, other: Union["Vector", Iterable[float]]) -> T: if isinstance(other, Vector): - print(self, other) return self.__class__(self._data - other._data) return self.__class__(self._data - np.array(other, dtype=float)) @@ -188,9 +201,7 @@ def distance_squared(self, other: Union["Vector", Iterable[float]]) -> float: def angle(self, other: Union["Vector", Iterable[float]]) -> float: """Compute the angle (in radians) between this vector and another.""" - if self.length() < 1e-10 or ( - isinstance(other, Vector) and other.length() < 1e-10 - ): + if self.length() < 1e-10 or (isinstance(other, Vector) and other.length() < 1e-10): return 0.0 if isinstance(other, Vector): @@ -258,11 +269,11 @@ def unit_z(cls: type[T], dim: int = 3) -> T: v[2] = 1.0 return cls(v) - def to_list(self) -> List[float]: + def to_list(self) -> list[float]: """Convert the vector to a list.""" return [float(x) for x in self._data] - def to_tuple(self) -> Tuple[float, ...]: + def to_tuple(self) -> builtins.tuple[float, ...]: """Convert the vector to a tuple.""" return tuple(self._data) @@ -313,7 +324,7 @@ def to_vector(value: VectorLike) -> Vector: return Vector(value) -def to_tuple(value: VectorLike) -> Tuple[float, ...]: +def to_tuple(value: VectorLike) -> tuple[float, ...]: """Convert a vector-compatible value to a tuple. Args: @@ -334,7 +345,7 @@ def to_tuple(value: VectorLike) -> Tuple[float, ...]: return tuple(float(x) for x in data) -def to_list(value: VectorLike) -> List[float]: +def to_list(value: VectorLike) -> list[float]: """Convert a vector-compatible value to a list. Args: @@ -435,150 +446,3 @@ def z(value: VectorLike) -> float: else: arr = to_numpy(value) return float(arr[2]) if len(arr) > 2 else 0.0 - - -if __name__ == "__main__": - # Test vectors in various directions - test_vectors = [ - Vector(1, 0), # Right - Vector(1, 1), # Up-Right - Vector(0, 1), # Up - Vector(-1, 1), # Up-Left - Vector(-1, 0), # Left - Vector(-1, -1), # Down-Left - Vector(0, -1), # Down - Vector(1, -1), # Down-Right - Vector(0.5, 0.5), # Up-Right (shorter) - Vector(-3, 4), # Up-Left (longer) - ] - - for v in test_vectors: - print(str(v)) - - # Test the vector compatibility functions - print("Testing vectortypes.py conversion functions\n") - - # Create test vectors in different formats - vector_obj = Vector(1.0, 2.0, 3.0) - numpy_arr = np.array([4.0, 5.0, 6.0]) - tuple_vec = (7.0, 8.0, 9.0) - list_vec = [10.0, 11.0, 12.0] - - print("Original values:") - print(f"Vector: {vector_obj}") - print(f"NumPy: {numpy_arr}") - print(f"Tuple: {tuple_vec}") - print(f"List: {list_vec}") - print() - - # Test to_numpy - print("to_numpy() conversions:") - print(f"Vector → NumPy: {to_numpy(vector_obj)}") - print(f"NumPy → NumPy: {to_numpy(numpy_arr)}") - print(f"Tuple → NumPy: {to_numpy(tuple_vec)}") - print(f"List → NumPy: {to_numpy(list_vec)}") - print() - - # Test to_vector - print("to_vector() conversions:") - print(f"Vector → Vector: {to_vector(vector_obj)}") - print(f"NumPy → Vector: {to_vector(numpy_arr)}") - print(f"Tuple → Vector: {to_vector(tuple_vec)}") - print(f"List → Vector: {to_vector(list_vec)}") - print() - - # Test to_tuple - print("to_tuple() conversions:") - print(f"Vector → Tuple: {to_tuple(vector_obj)}") - print(f"NumPy → Tuple: {to_tuple(numpy_arr)}") - print(f"Tuple → Tuple: {to_tuple(tuple_vec)}") - print(f"List → Tuple: {to_tuple(list_vec)}") - print() - - # Test to_list - print("to_list() conversions:") - print(f"Vector → List: {to_list(vector_obj)}") - print(f"NumPy → List: {to_list(numpy_arr)}") - print(f"Tuple → List: {to_list(tuple_vec)}") - print(f"List → List: {to_list(list_vec)}") - print() - - # Test component extraction - print("Component extraction:") - print("x() function:") - print(f"x(Vector): {x(vector_obj)}") - print(f"x(NumPy): {x(numpy_arr)}") - print(f"x(Tuple): {x(tuple_vec)}") - print(f"x(List): {x(list_vec)}") - print() - - print("y() function:") - print(f"y(Vector): {y(vector_obj)}") - print(f"y(NumPy): {y(numpy_arr)}") - print(f"y(Tuple): {y(tuple_vec)}") - print(f"y(List): {y(list_vec)}") - print() - - print("z() function:") - print(f"z(Vector): {z(vector_obj)}") - print(f"z(NumPy): {z(numpy_arr)}") - print(f"z(Tuple): {z(tuple_vec)}") - print(f"z(List): {z(list_vec)}") - print() - - # Test dimension checking - print("Dimension checking:") - vec2d = Vector(1.0, 2.0) - vec3d = Vector(1.0, 2.0, 3.0) - arr2d = np.array([1.0, 2.0]) - arr3d = np.array([1.0, 2.0, 3.0]) - - print(f"is_2d(Vector(1,2)): {is_2d(vec2d)}") - print(f"is_2d(Vector(1,2,3)): {is_2d(vec3d)}") - print(f"is_2d(np.array([1,2])): {is_2d(arr2d)}") - print(f"is_2d(np.array([1,2,3])): {is_2d(arr3d)}") - print(f"is_2d((1,2)): {is_2d((1.0, 2.0))}") - print(f"is_2d((1,2,3)): {is_2d((1.0, 2.0, 3.0))}") - print() - - print(f"is_3d(Vector(1,2)): {is_3d(vec2d)}") - print(f"is_3d(Vector(1,2,3)): {is_3d(vec3d)}") - print(f"is_3d(np.array([1,2])): {is_3d(arr2d)}") - print(f"is_3d(np.array([1,2,3])): {is_3d(arr3d)}") - print(f"is_3d((1,2)): {is_3d((1.0, 2.0))}") - print(f"is_3d((1,2,3)): {is_3d((1.0, 2.0, 3.0))}") - print() - - # Test the Protocol interface - print("Testing VectorLike Protocol:") - print(f"isinstance(Vector(1,2), VectorLike): {isinstance(vec2d, VectorLike)}") - print(f"isinstance(np.array([1,2]), VectorLike): {isinstance(arr2d, VectorLike)}") - print( - f"isinstance((1,2), VectorLike): {isinstance((1.0, 2.0), VectorLike)}" - ) - print( - f"isinstance([1,2], VectorLike): {isinstance([1.0, 2.0], VectorLike)}" - ) - print() - - # Test mixed operations using different vector types - # These functions aren't defined in vectortypes, but demonstrate the concept - def distance(a: VectorLike, b: VectorLike) -> float: - a_np = to_numpy(a) - b_np = to_numpy(b) - diff = a_np - b_np - return float(np.sqrt(np.sum(diff * diff))) - - def midpoint(a: VectorLike, b: VectorLike) -> NDArray[np.float64]: - a_np = to_numpy(a) - b_np = to_numpy(b) - return (a_np + b_np) / 2 - - print("Mixed operations between different vector types:") - print( - f"distance(Vector(1,2,3), [4,5,6]): {distance(vec3d, [4.0, 5.0, 6.0])}" - ) - print( - f"distance(np.array([1,2,3]), (4,5,6)): {distance(arr3d, (4.0, 5.0, 6.0))}" - ) - print(f"midpoint(Vector(1,2,3), np.array([4,5,6])): {midpoint(vec3d, numpy_arr)}") diff --git a/dimos/robot/unitree_webrtc/unitree_b1/README.md b/dimos/robot/unitree_webrtc/unitree_b1/README.md new file mode 100644 index 0000000000..8616fc286a --- /dev/null +++ b/dimos/robot/unitree_webrtc/unitree_b1/README.md @@ -0,0 +1,219 @@ +# Unitree B1 Dimensional Integration + +This module provides UDP-based control for the Unitree B1 quadruped robot with DimOS integration with ROS Twist cmd_vel interface. + +## Overview + +The system consists of two components: +1. **Server Side**: C++ UDP server running on the B1's internal computer +2. **Client Side**: Python control module running on external machine + +Key features: +- 50Hz continuous UDP streaming +- 100ms command timeout for automatic stop +- Standard Twist velocity interface +- Emergency stop (Space/Q keys) +- IDLE/STAND/WALK mode control +- Optional pygame joystick interface + +## Server Side Setup (B1 Internal Computer) + +### Prerequisites + +The B1 robot runs Ubuntu with the following requirements: +- Unitree Legged SDK v3.8.3 for B1 +- Boost (>= 1.71.0) +- CMake (>= 3.16.3) +- g++ (>= 9.4.0) + +### Step 1: Connect to B1 Robot + +1. **Connect to B1's WiFi Access Point**: + - SSID: `Unitree_B1_XXXXX` (where XXXXX is your robot's ID) + - Password: `00000000` (8 zeros) + +2. **SSH into the B1**: + ```bash + ssh unitree@192.168.12.1 + # Default password: 123 + ``` + +### Step 2: Build the UDP Server + +1. **Add joystick_server_udp.cpp to CMakeLists.txt**: + ```bash + # Edit the CMakeLists.txt in the unitree_legged_sdk_B1 directory + vim CMakeLists.txt + + # Add this line with the other add_executable statements: + add_executable(joystick_server example/joystick_server_udp.cpp) + target_link_libraries(joystick_server ${EXTRA_LIBS})``` + +2. **Build the server**: + ```bash + mkdir build + cd build + cmake ../ + make + ``` + +### Step 3: Run the UDP Server + +```bash +# Navigate to build directory +cd Unitree/sdk/unitree_legged_sdk_B1/build/ +./joystick_server + +# You should see: +# UDP Unitree B1 Joystick Control Server +# Communication level: HIGH-level +# Server port: 9090 +# WARNING: Make sure the robot is standing on the ground. +# Press Enter to continue... +``` + +The server will now listen for UDP packets on port 9090 and control the B1 robot. + +### Server Safety Features + +- **100ms timeout**: Robot stops if no packets received for 100ms +- **Packet validation**: Only accepts correctly formatted 19-byte packets +- **Mode restrictions**: Velocities only applied in WALK mode +- **Emergency stop**: Mode 0 (IDLE) stops all movement + +## Client Side Setup (External Machine) + +### Prerequisites + +- Python 3.10+ +- DimOS framework installed +- pygame (optional, for joystick control) + +### Step 1: Install Dependencies + +```bash +# Install Dimensional +pip install -e .[cpu,sim] +``` + +### Step 2: Connect to B1 Network + +1. **Connect your machine to B1's WiFi**: + - SSID: `Unitree_B1_XXXXX` + - Password: `00000000` + +2. **Verify connection**: + ```bash + ping 192.168.12.1 # Should get responses + ``` + +### Step 3: Run the Client + +#### With Joystick Control (Recommended for Testing) + +```bash +python -m dimos.robot.unitree_webrtc.unitree_b1.unitree_b1 \ + --ip 192.168.12.1 \ + --port 9090 \ + --joystick +``` + +**Joystick Controls**: +- `0/1/2` - Switch between IDLE/STAND/WALK modes +- `WASD` - Move forward/backward, turn left/right (only in WALK mode) +- `JL` - Strafe left/right (only in WALK mode) +- `Space/Q` - Emergency stop (switches to IDLE) +- `ESC` - Quit pygame window +- `Ctrl+C` - Exit program + +#### Test Mode (No Robot Required) + +```bash +python -m dimos.robot.unitree_webrtc.unitree_b1.unitree_b1 \ + --test \ + --joystick +``` + +This prints commands instead of sending UDP packets - useful for development. + +## Safety Features + +### Client Side +- **Command freshness tracking**: Stops sending if no new commands for 100ms +- **Emergency stop**: Q or Space immediately sets IDLE mode +- **Mode safety**: Movement only allowed in WALK mode +- **Graceful shutdown**: Sends stop commands on exit + +### Server Side +- **Packet timeout**: Robot stops if no packets for 100ms +- **Continuous monitoring**: Checks timeout before every control update +- **Safe defaults**: Starts in IDLE mode +- **Packet validation**: Rejects malformed packets + +## Architecture + +``` +External Machine (Client) B1 Robot (Server) +┌─────────────────────┐ ┌──────────────────┐ +│ Joystick Module │ │ │ +│ (pygame input) │ │ joystick_server │ +│ ↓ │ │ _udp.cpp │ +│ Twist msg │ │ │ +│ ↓ │ WiFi AP │ │ +│ B1ConnectionModule │◄─────────►│ UDP Port 9090 │ +│ (Twist → B1Command) │ 192.168. │ │ +│ ↓ │ 12.1 │ │ +│ UDP packets 50Hz │ │ Unitree SDK │ +└─────────────────────┘ └──────────────────┘ +``` + +## Setting up ROS Navigation stack with Unitree B1 + +### Setup external Wireless USB Adapter on onboard hardware +This is because the onboard hardware (mini PC, jetson, etc.) needs to connect to both the B1 wifi AP network to send cmd_vel messages over UDP, as well as the network running dimensional + + +Plug in wireless adapter +```bash +nmcli device status +nmcli device wifi list ifname *DEVICE_NAME* +# Connect to b1 network +nmcli device wifi connect "Unitree_B1-251" password "00000000" ifname *DEVICE_NAME* +# Verify connection +nmcli connection show --active +``` + +### *TODO: add more docs* + + +## Troubleshooting + +### Cannot connect to B1 +- Ensure WiFi connection to B1's AP +- Check IP: should be `192.168.12.1` +- Verify server is running: `ssh unitree@192.168.12.1` + +### Robot not responding +- Verify server shows "Client connected" message +- Check robot is in WALK mode (press '2') +- Ensure no timeout messages in server output + +### Timeout issues +- Check network latency: `ping 192.168.12.1` +- Ensure 50Hz sending rate is maintained +- Look for "Command timeout" messages + +### Emergency situations +- Press Space or Q for immediate stop +- Use Ctrl+C to exit cleanly +- Robot auto-stops after 100ms without commands + +## Development Notes + +- Packets are 19 bytes: 4 floats + uint16 + uint8 +- Coordinate system: B1 uses different conventions, hence negations in `b1_command.py` +- LCM topics: `/cmd_vel` for Twist, `/b1/mode` for Int32 mode changes + +## License + +Copyright 2025 Dimensional Inc. Licensed under Apache License 2.0. diff --git a/dimos/robot/unitree_webrtc/unitree_b1/__init__.py b/dimos/robot/unitree_webrtc/unitree_b1/__init__.py new file mode 100644 index 0000000000..e6e5a0f04a --- /dev/null +++ b/dimos/robot/unitree_webrtc/unitree_b1/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. + +"""Unitree B1 robot module.""" + +from .unitree_b1 import UnitreeB1 + +__all__ = ["UnitreeB1"] diff --git a/dimos/robot/unitree_webrtc/unitree_b1/b1_command.py b/dimos/robot/unitree_webrtc/unitree_b1/b1_command.py new file mode 100644 index 0000000000..82545fa2c6 --- /dev/null +++ b/dimos/robot/unitree_webrtc/unitree_b1/b1_command.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright 2025 Dimensional Inc. + +"""Internal B1 command structure for UDP communication.""" + +import struct + +from pydantic import BaseModel, Field + + +class B1Command(BaseModel): + """Internal B1 robot command matching UDP packet structure. + + This is an internal type - external interfaces use standard Twist messages. + """ + + # Direct joystick values matching C++ NetworkJoystickCmd struct + lx: float = Field(default=0.0, ge=-1.0, le=1.0) # Turn velocity (left stick X) + ly: float = Field(default=0.0, ge=-1.0, le=1.0) # Forward/back velocity (left stick Y) + rx: float = Field(default=0.0, ge=-1.0, le=1.0) # Strafe velocity (right stick X) + ry: float = Field(default=0.0, ge=-1.0, le=1.0) # Pitch/height adjustment (right stick Y) + buttons: int = Field(default=0, ge=0, le=65535) # Button states (uint16) + mode: int = Field( + default=0, ge=0, le=255 + ) # Control mode (uint8): 0=idle, 1=stand, 2=walk, 6=recovery + + @classmethod + def from_twist(cls, twist, mode: int = 2): + """Create B1Command from standard ROS Twist message. + + This is the key integration point for navigation and planning. + + Args: + twist: ROS Twist message with linear and angular velocities + mode: Robot mode (default is walk mode for navigation) + + Returns: + B1Command configured for the given Twist + """ + # Max velocities from ROS needed to clamp to joystick ranges properly + MAX_LINEAR_VEL = 1.0 # m/s + MAX_ANGULAR_VEL = 2.0 # rad/s + + if mode == 2: # WALK mode - velocity control + return cls( + # Scale and clamp to joystick range [-1, 1] + lx=max(-1.0, min(1.0, -twist.angular.z / MAX_ANGULAR_VEL)), + ly=max(-1.0, min(1.0, twist.linear.x / MAX_LINEAR_VEL)), + rx=max(-1.0, min(1.0, -twist.linear.y / MAX_LINEAR_VEL)), + ry=0.0, # No pitch control in walk mode + mode=mode, + ) + elif mode == 1: # STAND mode - body pose control + # Map Twist pose controls to B1 joystick axes + # Already in normalized units, just clamp to [-1, 1] + return cls( + lx=max(-1.0, min(1.0, -twist.angular.z)), # ROS yaw → B1 yaw + ly=max(-1.0, min(1.0, twist.linear.z)), # ROS height → B1 bodyHeight + rx=max(-1.0, min(1.0, -twist.angular.x)), # ROS roll → B1 roll + ry=max(-1.0, min(1.0, twist.angular.y)), # ROS pitch → B1 pitch + mode=mode, + ) + else: + # IDLE mode - no controls + return cls(mode=mode) + + def to_bytes(self) -> bytes: + """Pack to 19-byte UDP packet matching C++ struct. + + Format: 4 floats + uint16 + uint8 = 19 bytes (little-endian) + """ + return struct.pack(" str: + """Human-readable representation.""" + mode_names = {0: "IDLE", 1: "STAND", 2: "WALK", 6: "RECOVERY"} + mode_str = mode_names.get(self.mode, f"MODE_{self.mode}") + + if self.lx != 0 or self.ly != 0 or self.rx != 0 or self.ry != 0: + return f"B1Cmd[{mode_str}] LX:{self.lx:+.2f} LY:{self.ly:+.2f} RX:{self.rx:+.2f} RY:{self.ry:+.2f}" + else: + return f"B1Cmd[{mode_str}] (idle)" diff --git a/dimos/robot/unitree_webrtc/unitree_b1/connection.py b/dimos/robot/unitree_webrtc/unitree_b1/connection.py new file mode 100644 index 0000000000..73285b4d76 --- /dev/null +++ b/dimos/robot/unitree_webrtc/unitree_b1/connection.py @@ -0,0 +1,400 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright 2025 Dimensional Inc. + +"""B1 Connection Module that accepts standard Twist commands and converts to UDP packets.""" + +import logging +import socket +import threading +import time + +from reactivex.disposable import Disposable + +from dimos.core import In, Module, Out, rpc +from dimos.msgs.geometry_msgs import PoseStamped, Twist, TwistStamped +from dimos.msgs.nav_msgs.Odometry import Odometry +from dimos.msgs.std_msgs import Int32 +from dimos.utils.logging_config import setup_logger + +from .b1_command import B1Command + +# Setup logger with DEBUG level for troubleshooting +logger = setup_logger("dimos.robot.unitree_webrtc.unitree_b1.connection", level=logging.DEBUG) + + +class RobotMode: + """Constants for B1 robot modes.""" + + IDLE = 0 + STAND = 1 + WALK = 2 + RECOVERY = 6 + + +class B1ConnectionModule(Module): + """UDP connection module for B1 robot with standard Twist interface. + + Accepts standard ROS Twist messages on /cmd_vel and mode changes on /b1/mode, + internally converts to B1Command format, and sends UDP packets at 50Hz. + """ + + cmd_vel: In[TwistStamped] = None # Timestamped velocity commands from ROS + mode_cmd: In[Int32] = None # Mode changes + odom_in: In[Odometry] = None # External odometry from ROS SLAM/lidar + + odom_pose: Out[PoseStamped] = None # Converted pose for internal use + + def __init__( + self, ip: str = "192.168.12.1", port: int = 9090, test_mode: bool = False, *args, **kwargs + ) -> None: + """Initialize B1 connection module. + + Args: + ip: Robot IP address + port: UDP port for joystick server + test_mode: If True, print commands instead of sending UDP + """ + Module.__init__(self, *args, **kwargs) + + self.ip = ip + self.port = port + self.test_mode = test_mode + self.current_mode = RobotMode.IDLE # Start in IDLE mode + self._current_cmd = B1Command(mode=RobotMode.IDLE) + self.cmd_lock = threading.Lock() # Thread lock for _current_cmd access + # Thread control + self.running = False + self.send_thread = None + self.socket = None + self.packet_count = 0 + self.last_command_time = time.time() + self.command_timeout = 0.2 # 200ms safety timeout + self.watchdog_thread = None + self.watchdog_running = False + self.timeout_active = False + + @rpc + def start(self) -> None: + """Start the connection and subscribe to command streams.""" + + super().start() + + # Setup UDP socket (unless in test mode) + if not self.test_mode: + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + logger.info(f"B1 Connection started - UDP to {self.ip}:{self.port} at 50Hz") + else: + logger.info(f"[TEST MODE] B1 Connection started - would send to {self.ip}:{self.port}") + + # Subscribe to input streams + if self.cmd_vel: + unsub = self.cmd_vel.subscribe(self.handle_twist_stamped) + self._disposables.add(Disposable(unsub)) + if self.mode_cmd: + unsub = self.mode_cmd.subscribe(self.handle_mode) + self._disposables.add(Disposable(unsub)) + if self.odom_in: + unsub = self.odom_in.subscribe(self._publish_odom_pose) + self._disposables.add(Disposable(unsub)) + + # Start threads + self.running = True + self.watchdog_running = True + + # Start 50Hz sending thread + self.send_thread = threading.Thread(target=self._send_loop, daemon=True) + self.send_thread.start() + + # Start watchdog thread + self.watchdog_thread = threading.Thread(target=self._watchdog_loop, daemon=True) + self.watchdog_thread.start() + + @rpc + def stop(self) -> None: + """Stop the connection and send stop commands.""" + + self.set_mode(RobotMode.IDLE) # IDLE + with self.cmd_lock: + self._current_cmd = B1Command(mode=RobotMode.IDLE) # Zero all velocities + + # Send multiple stop packets + if not self.test_mode and self.socket: + stop_cmd = B1Command(mode=RobotMode.IDLE) + for _ in range(5): + data = stop_cmd.to_bytes() + self.socket.sendto(data, (self.ip, self.port)) + time.sleep(0.02) + + self.running = False + self.watchdog_running = False + + if self.send_thread: + self.send_thread.join(timeout=0.5) + if self.watchdog_thread: + self.watchdog_thread.join(timeout=0.5) + + if self.socket: + self.socket.close() + self.socket = None + + super().stop() + + def handle_twist_stamped(self, twist_stamped: TwistStamped) -> None: + """Handle timestamped Twist message and convert to B1Command. + + This is called automatically when messages arrive on cmd_vel input. + """ + # Extract Twist from TwistStamped + twist = Twist(linear=twist_stamped.linear, angular=twist_stamped.angular) + + logger.debug( + f"Received cmd_vel: linear=({twist.linear.x:.3f}, {twist.linear.y:.3f}, {twist.linear.z:.3f}), angular=({twist.angular.x:.3f}, {twist.angular.y:.3f}, {twist.angular.z:.3f})" + ) + + # In STAND mode, all twist values control body pose, not movement + # W/S: height (linear.z), A/D: yaw (angular.z), J/L: roll (angular.x), I/K: pitch (angular.y) + if self.current_mode == RobotMode.STAND: + # In STAND mode, don't auto-switch since all inputs are valid body pose controls + has_movement = False + else: + # In other modes, consider linear x/y and angular.z as movement + has_movement = ( + abs(twist.linear.x) > 0.01 + or abs(twist.linear.y) > 0.01 + or abs(twist.angular.z) > 0.01 + ) + + if has_movement and self.current_mode not in (RobotMode.STAND, RobotMode.WALK): + logger.info("Auto-switching to WALK mode for ROS control") + self.set_mode(RobotMode.WALK) + elif not has_movement and self.current_mode == RobotMode.WALK: + logger.info("Auto-switching to IDLE mode (zero velocities)") + self.set_mode(RobotMode.IDLE) + + if self.test_mode: + logger.info( + f"[TEST] Received TwistStamped: linear=({twist.linear.x:.2f}, {twist.linear.y:.2f}), angular.z={twist.angular.z:.2f}" + ) + + with self.cmd_lock: + self._current_cmd = B1Command.from_twist(twist, self.current_mode) + + logger.debug(f"Converted to B1Command: {self._current_cmd}") + + self.last_command_time = time.time() + self.timeout_active = False # Reset timeout state since we got a new command + + def handle_mode(self, mode_msg: Int32) -> None: + """Handle mode change message. + + This is called automatically when messages arrive on mode_cmd input. + """ + logger.debug(f"Received mode change: {mode_msg.data}") + if self.test_mode: + logger.info(f"[TEST] Received mode change: {mode_msg.data}") + self.set_mode(mode_msg.data) + + @rpc + def set_mode(self, mode: int) -> bool: + """Set robot mode (0=idle, 1=stand, 2=walk, 6=recovery).""" + self.current_mode = mode + with self.cmd_lock: + self._current_cmd.mode = mode + + # Clear velocities when not in walk mode + if mode != RobotMode.WALK: + self._current_cmd.lx = 0.0 + self._current_cmd.ly = 0.0 + self._current_cmd.rx = 0.0 + self._current_cmd.ry = 0.0 + + mode_names = { + RobotMode.IDLE: "IDLE", + RobotMode.STAND: "STAND", + RobotMode.WALK: "WALK", + RobotMode.RECOVERY: "RECOVERY", + } + logger.info(f"Mode changed to: {mode_names.get(mode, mode)}") + if self.test_mode: + logger.info(f"[TEST] Mode changed to: {mode_names.get(mode, mode)}") + + return True + + def _send_loop(self) -> None: + """Continuously send current command at 50Hz. + + The watchdog thread handles timeout and zeroing commands, so this loop + just sends whatever is in self._current_cmd at 50Hz. + """ + while self.running: + try: + # Watchdog handles timeout, we just send current command + with self.cmd_lock: + cmd_to_send = self._current_cmd + + # Log status every second (50 packets) + if self.packet_count % 50 == 0: + logger.info( + f"Sending B1 commands at 50Hz | Mode: {self.current_mode} | Count: {self.packet_count}" + ) + if not self.test_mode: + logger.debug(f"Current B1Command: {self._current_cmd}") + data = cmd_to_send.to_bytes() + hex_str = " ".join(f"{b:02x}" for b in data) + logger.debug(f"UDP packet ({len(data)} bytes): {hex_str}") + + if self.socket: + data = cmd_to_send.to_bytes() + self.socket.sendto(data, (self.ip, self.port)) + + self.packet_count += 1 + + # 50Hz rate (20ms between packets) + time.sleep(0.020) + + except Exception as e: + if self.running: + logger.error(f"Send error: {e}") + + def _publish_odom_pose(self, msg: Odometry) -> None: + """Convert and publish odometry as PoseStamped. + + This matches G1's approach of receiving external odometry. + """ + if self.odom_pose: + pose_stamped = PoseStamped( + ts=msg.ts, + frame_id=msg.frame_id, + position=msg.pose.pose.position, + orientation=msg.pose.pose.orientation, + ) + self.odom_pose.publish(pose_stamped) + + def _watchdog_loop(self) -> None: + """Single watchdog thread that monitors command freshness.""" + while self.watchdog_running: + try: + time_since_last_cmd = time.time() - self.last_command_time + + if time_since_last_cmd > self.command_timeout: + if not self.timeout_active: + # First time detecting timeout + logger.warning( + f"Watchdog timeout ({time_since_last_cmd:.1f}s) - zeroing commands" + ) + if self.test_mode: + logger.info("[TEST] Watchdog timeout - zeroing commands") + + with self.cmd_lock: + self._current_cmd.lx = 0.0 + self._current_cmd.ly = 0.0 + self._current_cmd.rx = 0.0 + self._current_cmd.ry = 0.0 + + self.timeout_active = True + else: + if self.timeout_active: + logger.info("Watchdog: Commands resumed - control restored") + if self.test_mode: + logger.info("[TEST] Watchdog: Commands resumed") + self.timeout_active = False + + # Check every 50ms + time.sleep(0.05) + + except Exception as e: + if self.watchdog_running: + logger.error(f"Watchdog error: {e}") + + @rpc + def idle(self) -> bool: + """Set robot to idle mode.""" + self.set_mode(RobotMode.IDLE) + return True + + @rpc + def pose(self) -> bool: + """Set robot to stand/pose mode for reaching ground objects with manipulator.""" + self.set_mode(RobotMode.STAND) + return True + + @rpc + def walk(self) -> bool: + """Set robot to walk mode.""" + self.set_mode(RobotMode.WALK) + return True + + @rpc + def recovery(self) -> bool: + """Set robot to recovery mode.""" + self.set_mode(RobotMode.RECOVERY) + return True + + @rpc + def move(self, twist_stamped: TwistStamped, duration: float = 0.0) -> bool: + """Direct RPC method for sending TwistStamped commands. + + Args: + twist_stamped: Timestamped velocity command + duration: Not used, kept for compatibility + """ + self.handle_twist_stamped(twist_stamped) + return True + + +class MockB1ConnectionModule(B1ConnectionModule): + """Test connection module that prints commands instead of sending UDP.""" + + def __init__(self, ip: str = "127.0.0.1", port: int = 9090, *args, **kwargs) -> None: + """Initialize test connection without creating socket.""" + super().__init__(ip, port, test_mode=True, *args, **kwargs) + + def _send_loop(self) -> None: + """Override to provide better test output with timeout detection.""" + timeout_warned = False + + while self.running: + time_since_last_cmd = time.time() - self.last_command_time + is_timeout = time_since_last_cmd > self.command_timeout + + # Show timeout transitions + if is_timeout and not timeout_warned: + logger.info( + f"[TEST] Command timeout! Sending zeros after {time_since_last_cmd:.1f}s" + ) + timeout_warned = True + elif not is_timeout and timeout_warned: + logger.info("[TEST] Commands resumed - control restored") + timeout_warned = False + + # Print current state every 0.5 seconds + if self.packet_count % 25 == 0: + if is_timeout: + logger.info(f"[TEST] B1Cmd[ZEROS] (timeout) | Count: {self.packet_count}") + else: + logger.info(f"[TEST] {self._current_cmd} | Count: {self.packet_count}") + + self.packet_count += 1 + time.sleep(0.020) + + @rpc + def start(self) -> None: + super().start() + + @rpc + def stop(self) -> None: + super().stop() diff --git a/dimos/robot/unitree_webrtc/unitree_b1/joystick_module.py b/dimos/robot/unitree_webrtc/unitree_b1/joystick_module.py new file mode 100644 index 0000000000..9c3c09861c --- /dev/null +++ b/dimos/robot/unitree_webrtc/unitree_b1/joystick_module.py @@ -0,0 +1,282 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright 2025 Dimensional Inc. + +"""Pygame Joystick Module for testing B1 control via LCM.""" + +import os +import threading + +# Force X11 driver to avoid OpenGL threading issues +os.environ["SDL_VIDEODRIVER"] = "x11" + +import time + +from dimos.core import Module, Out, rpc +from dimos.msgs.geometry_msgs import Twist, TwistStamped, Vector3 +from dimos.msgs.std_msgs import Int32 + + +class JoystickModule(Module): + """Pygame-based joystick control module for B1 testing. + + Outputs timestamped Twist messages on /cmd_vel and mode changes on /b1/mode. + This allows testing the same interface that navigation will use. + """ + + twist_out: Out[TwistStamped] = None # Timestamped velocity commands + mode_out: Out[Int32] = None # Mode changes + + def __init__(self, *args, **kwargs) -> None: + Module.__init__(self, *args, **kwargs) + self.pygame_ready = False + self.running = False + self.current_mode = 0 # Start in IDLE mode for safety + + @rpc + def start(self) -> bool: + """Initialize pygame and start control loop.""" + + super().start() + + try: + import pygame + except ImportError: + print("ERROR: pygame not installed. Install with: pip install pygame") + return False + + self.keys_held = set() + self.pygame_ready = True + self.running = True + + # Start pygame loop in background thread - ALL pygame ops will happen there + self._thread = threading.Thread(target=self._pygame_loop, daemon=True) + self._thread.start() + + return True + + @rpc + def stop(self) -> None: + """Stop the joystick module.""" + + self.running = False + self.pygame_ready = False + + # Send stop command + stop_twist = Twist() + stop_twist_stamped = TwistStamped( + ts=time.time(), + frame_id="base_link", + linear=stop_twist.linear, + angular=stop_twist.angular, + ) + self.twist_out.publish(stop_twist_stamped) + + self._thread.join(2) + + super().stop() + + def _pygame_loop(self) -> None: + """Main pygame event loop - ALL pygame operations happen here.""" + import pygame + + # Initialize pygame and create display IN THIS THREAD + pygame.init() + self.screen = pygame.display.set_mode((500, 400), pygame.SWSURFACE) + pygame.display.set_caption("B1 Joystick Control (LCM)") + self.clock = pygame.time.Clock() + self.font = pygame.font.Font(None, 24) + + print("JoystickModule started - Focus pygame window to control") + print("Controls:") + print(" Walk Mode: WASD = Move/Turn, JL = Strafe") + print(" Stand Mode: WASD = Height/Yaw, JL = Roll, IK = Pitch") + print(" 1/2/0 = Stand/Walk/Idle modes") + print(" Space/Q = Emergency Stop") + print(" ESC = Quit (or use Ctrl+C)") + + while self.running and self.pygame_ready: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + self.running = False + elif event.type == pygame.KEYDOWN: + self.keys_held.add(event.key) + + # Mode changes - publish to mode_out for connection module + if event.key == pygame.K_0: + self.current_mode = 0 + mode_msg = Int32() + mode_msg.data = 0 + self.mode_out.publish(mode_msg) + print("Mode: IDLE") + elif event.key == pygame.K_1: + self.current_mode = 1 + mode_msg = Int32() + mode_msg.data = 1 + self.mode_out.publish(mode_msg) + print("Mode: STAND") + elif event.key == pygame.K_2: + self.current_mode = 2 + mode_msg = Int32() + mode_msg.data = 2 + self.mode_out.publish(mode_msg) + print("Mode: WALK") + elif event.key == pygame.K_SPACE or event.key == pygame.K_q: + self.keys_held.clear() + # Send IDLE mode for emergency stop + self.current_mode = 0 + mode_msg = Int32() + mode_msg.data = 0 + self.mode_out.publish(mode_msg) + # Also send zero twist + stop_twist = Twist() + stop_twist.linear = Vector3(0, 0, 0) + stop_twist.angular = Vector3(0, 0, 0) + stop_twist_stamped = TwistStamped( + ts=time.time(), + frame_id="base_link", + linear=stop_twist.linear, + angular=stop_twist.angular, + ) + self.twist_out.publish(stop_twist_stamped) + print("EMERGENCY STOP!") + elif event.key == pygame.K_ESCAPE: + # ESC still quits for development convenience + self.running = False + + elif event.type == pygame.KEYUP: + self.keys_held.discard(event.key) + + # Generate Twist message from held keys + twist = Twist() + twist.linear = Vector3(0, 0, 0) + twist.angular = Vector3(0, 0, 0) + + # Apply controls based on mode + if self.current_mode == 2: # WALK mode - movement control + # Forward/backward (W/S) + if pygame.K_w in self.keys_held: + twist.linear.x = 1.0 # Forward + if pygame.K_s in self.keys_held: + twist.linear.x = -1.0 # Backward + + # Turning (A/D) + if pygame.K_a in self.keys_held: + twist.angular.z = 1.0 # Turn left + if pygame.K_d in self.keys_held: + twist.angular.z = -1.0 # Turn right + + # Strafing (J/L) + if pygame.K_j in self.keys_held: + twist.linear.y = 1.0 # Strafe left + if pygame.K_l in self.keys_held: + twist.linear.y = -1.0 # Strafe right + + elif self.current_mode == 1: # STAND mode - body pose control + # Height control (W/S) - use linear.z for body height + if pygame.K_w in self.keys_held: + twist.linear.z = 1.0 # Raise body + if pygame.K_s in self.keys_held: + twist.linear.z = -1.0 # Lower body + + # Yaw control (A/D) - use angular.z for body yaw + if pygame.K_a in self.keys_held: + twist.angular.z = 1.0 # Rotate body left + if pygame.K_d in self.keys_held: + twist.angular.z = -1.0 # Rotate body right + + # Roll control (J/L) - use angular.x for body roll + if pygame.K_j in self.keys_held: + twist.angular.x = 1.0 # Roll left + if pygame.K_l in self.keys_held: + twist.angular.x = -1.0 # Roll right + + # Pitch control (I/K) - use angular.y for body pitch + if pygame.K_i in self.keys_held: + twist.angular.y = 1.0 # Pitch forward + if pygame.K_k in self.keys_held: + twist.angular.y = -1.0 # Pitch backward + + twist_stamped = TwistStamped( + ts=time.time(), frame_id="base_link", linear=twist.linear, angular=twist.angular + ) + self.twist_out.publish(twist_stamped) + + # Update pygame display + self._update_display(twist) + + # Maintain 50Hz rate + self.clock.tick(50) + + pygame.quit() + print("JoystickModule stopped") + + def _update_display(self, twist) -> None: + """Update pygame window with current status.""" + import pygame + + self.screen.fill((30, 30, 30)) + + # Mode display + y_pos = 20 + mode_text = ["IDLE", "STAND", "WALK"][self.current_mode if self.current_mode < 3 else 0] + mode_color = ( + (0, 255, 0) + if self.current_mode == 2 + else (255, 255, 0) + if self.current_mode == 1 + else (100, 100, 100) + ) + + texts = [ + f"Mode: {mode_text}", + "", + f"Linear X: {twist.linear.x:+.2f}", + f"Linear Y: {twist.linear.y:+.2f}", + f"Linear Z: {twist.linear.z:+.2f}", + f"Angular X: {twist.angular.x:+.2f}", + f"Angular Y: {twist.angular.y:+.2f}", + f"Angular Z: {twist.angular.z:+.2f}", + "Keys: " + ", ".join([pygame.key.name(k).upper() for k in self.keys_held if k < 256]), + ] + + for i, text in enumerate(texts): + if text: + color = mode_color if i == 0 else (255, 255, 255) + surf = self.font.render(text, True, color) + self.screen.blit(surf, (20, y_pos)) + y_pos += 30 + + if ( + twist.linear.x != 0 + or twist.linear.y != 0 + or twist.linear.z != 0 + or twist.angular.x != 0 + or twist.angular.y != 0 + or twist.angular.z != 0 + ): + pygame.draw.circle(self.screen, (255, 0, 0), (450, 30), 15) # Red = moving + else: + pygame.draw.circle(self.screen, (0, 255, 0), (450, 30), 15) # Green = stopped + + y_pos = 300 + help_texts = ["WASD: Move | JL: Strafe | 1/2/0: Modes", "Space/Q: E-Stop | ESC: Quit"] + for text in help_texts: + surf = self.font.render(text, True, (150, 150, 150)) + self.screen.blit(surf, (20, y_pos)) + y_pos += 25 + + pygame.display.flip() diff --git a/dimos/robot/unitree_webrtc/unitree_b1/joystick_server_udp.cpp b/dimos/robot/unitree_webrtc/unitree_b1/joystick_server_udp.cpp new file mode 100644 index 0000000000..56e2b29412 --- /dev/null +++ b/dimos/robot/unitree_webrtc/unitree_b1/joystick_server_udp.cpp @@ -0,0 +1,366 @@ +/***************************************************************** + UDP Joystick Control Server for Unitree B1 Robot + With timeout protection and guaranteed packet boundaries +******************************************************************/ + +#include "unitree_legged_sdk/unitree_legged_sdk.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace UNITREE_LEGGED_SDK; + +// Joystick command structure received over network +struct NetworkJoystickCmd { + float lx; // left stick x (-1 to 1) + float ly; // left stick y (-1 to 1) + float rx; // right stick x (-1 to 1) + float ry; // right stick y (-1 to 1) + uint16_t buttons; // button states + uint8_t mode; // control mode +}; + +class JoystickServer { +public: + JoystickServer(uint8_t level, int server_port) : + safe(LeggedType::B1), + udp(level, 8090, "192.168.123.220", 8082), + server_port_(server_port), + running_(false) { + udp.InitCmdData(cmd); + memset(&joystick_cmd_, 0, sizeof(joystick_cmd_)); + joystick_cmd_.mode = 0; // Start in idle mode + last_packet_time_ = std::chrono::steady_clock::now(); + } + + void Start(); + void Stop(); + +private: + void UDPRecv(); + void UDPSend(); + void RobotControl(); + void NetworkServerThread(); + void ParseJoystickCommand(const NetworkJoystickCmd& net_cmd); + void CheckTimeout(); + + Safety safe; + UDP udp; + HighCmd cmd = {0}; + HighState state = {0}; + + NetworkJoystickCmd joystick_cmd_; + std::mutex cmd_mutex_; + + int server_port_; + int server_socket_; + bool running_; + std::thread server_thread_; + + // Client tracking for debug + struct sockaddr_in last_client_addr_; + bool has_client_ = false; + + // SAFETY: Timeout tracking + std::chrono::steady_clock::time_point last_packet_time_; + const int PACKET_TIMEOUT_MS = 100; // Stop if no packet for 100ms + + float dt = 0.002; + + // Control parameters + const float MAX_FORWARD_SPEED = 0.2f; // m/s + const float MAX_SIDE_SPEED = 0.2f; // m/s + const float MAX_YAW_SPEED = 0.2f; // rad/s + const float MAX_BODY_HEIGHT = 0.1f; // m + const float MAX_EULER_ANGLE = 0.3f; // rad + const float DEADZONE = 0.0f; // joystick deadzone +}; + +void JoystickServer::Start() { + running_ = true; + + // Start network server thread + server_thread_ = std::thread(&JoystickServer::NetworkServerThread, this); + + // Initialize environment + InitEnvironment(); + + // Start control loops + LoopFunc loop_control("control_loop", dt, boost::bind(&JoystickServer::RobotControl, this)); + LoopFunc loop_udpSend("udp_send", dt, 3, boost::bind(&JoystickServer::UDPSend, this)); + LoopFunc loop_udpRecv("udp_recv", dt, 3, boost::bind(&JoystickServer::UDPRecv, this)); + + loop_udpSend.start(); + loop_udpRecv.start(); + loop_control.start(); + + std::cout << "UDP Joystick server started on port " << server_port_ << std::endl; + std::cout << "Timeout protection: " << PACKET_TIMEOUT_MS << "ms" << std::endl; + std::cout << "Expected packet size: 19 bytes" << std::endl; + std::cout << "Robot control loops started" << std::endl; + + // Keep running + while (running_) { + sleep(1); + } +} + +void JoystickServer::Stop() { + running_ = false; + close(server_socket_); + if (server_thread_.joinable()) { + server_thread_.join(); + } +} + +void JoystickServer::NetworkServerThread() { + // Create UDP socket + server_socket_ = socket(AF_INET, SOCK_DGRAM, 0); + if (server_socket_ < 0) { + std::cerr << "Failed to create UDP socket" << std::endl; + return; + } + + // Allow socket reuse + int opt = 1; + setsockopt(server_socket_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + // Bind socket + struct sockaddr_in server_addr; + server_addr.sin_family = AF_INET; + server_addr.sin_addr.s_addr = INADDR_ANY; + server_addr.sin_port = htons(server_port_); + + if (bind(server_socket_, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { + std::cerr << "Failed to bind UDP socket to port " << server_port_ << std::endl; + close(server_socket_); + return; + } + + std::cout << "UDP server listening on port " << server_port_ << std::endl; + std::cout << "Waiting for joystick packets..." << std::endl; + + NetworkJoystickCmd net_cmd; + struct sockaddr_in client_addr; + socklen_t client_len; + + while (running_) { + client_len = sizeof(client_addr); + + // Receive UDP datagram (blocks until packet arrives) + ssize_t bytes = recvfrom(server_socket_, &net_cmd, sizeof(net_cmd), + 0, (struct sockaddr*)&client_addr, &client_len); + + if (bytes == 19) { + // Perfect packet size from Python client + if (!has_client_) { + std::cout << "Client connected from " << inet_ntoa(client_addr.sin_addr) + << ":" << ntohs(client_addr.sin_port) << std::endl; + has_client_ = true; + last_client_addr_ = client_addr; + } + ParseJoystickCommand(net_cmd); + } else if (bytes == sizeof(NetworkJoystickCmd)) { + // C++ client with padding (20 bytes) + if (!has_client_) { + std::cout << "C++ Client connected from " << inet_ntoa(client_addr.sin_addr) + << ":" << ntohs(client_addr.sin_port) << std::endl; + has_client_ = true; + last_client_addr_ = client_addr; + } + ParseJoystickCommand(net_cmd); + } else if (bytes > 0) { + // Wrong packet size - ignore but log + static int error_count = 0; + if (error_count++ < 5) { // Only log first 5 errors + std::cerr << "Ignored packet with wrong size: " << bytes + << " bytes (expected 19)" << std::endl; + } + } + // Note: recvfrom returns -1 on error, which we ignore + } +} + +void JoystickServer::ParseJoystickCommand(const NetworkJoystickCmd& net_cmd) { + std::lock_guard lock(cmd_mutex_); + joystick_cmd_ = net_cmd; + + // SAFETY: Update timestamp for timeout tracking + last_packet_time_ = std::chrono::steady_clock::now(); + + // Apply deadzone to analog sticks + if (fabs(joystick_cmd_.lx) < DEADZONE) joystick_cmd_.lx = 0; + if (fabs(joystick_cmd_.ly) < DEADZONE) joystick_cmd_.ly = 0; + if (fabs(joystick_cmd_.rx) < DEADZONE) joystick_cmd_.rx = 0; + if (fabs(joystick_cmd_.ry) < DEADZONE) joystick_cmd_.ry = 0; +} + +void JoystickServer::CheckTimeout() { + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast( + now - last_packet_time_).count(); + + static bool timeout_printed = false; + + if (elapsed > PACKET_TIMEOUT_MS) { + joystick_cmd_.lx = 0; + joystick_cmd_.ly = 0; + joystick_cmd_.rx = 0; + joystick_cmd_.ry = 0; + joystick_cmd_.buttons = 0; + + if (!timeout_printed) { + std::cout << "SAFETY: Packet timeout - stopping movement!" << std::endl; + timeout_printed = true; + } + } else { + // Reset flag when packets resume + if (timeout_printed) { + std::cout << "Packets resumed - control restored" << std::endl; + timeout_printed = false; + } + } +} + +void JoystickServer::UDPRecv() { + udp.Recv(); +} + +void JoystickServer::UDPSend() { + udp.Send(); +} + +void JoystickServer::RobotControl() { + udp.GetRecv(state); + + // SAFETY: Check for packet timeout + NetworkJoystickCmd current_cmd; + { + std::lock_guard lock(cmd_mutex_); + CheckTimeout(); // This may zero movement if timeout + current_cmd = joystick_cmd_; + } + + cmd.mode = 0; + cmd.gaitType = 0; + cmd.speedLevel = 0; + cmd.footRaiseHeight = 0; + cmd.bodyHeight = 0; + cmd.euler[0] = 0; + cmd.euler[1] = 0; + cmd.euler[2] = 0; + cmd.velocity[0] = 0.0f; + cmd.velocity[1] = 0.0f; + cmd.yawSpeed = 0.0f; + cmd.reserve = 0; + + // Set mode from joystick + cmd.mode = current_cmd.mode; + + // Map joystick to robot control based on mode + switch (current_cmd.mode) { + case 0: // Idle + // Robot stops + break; + + case 1: // Force stand with body control + // Left stick controls body height and yaw + cmd.bodyHeight = current_cmd.ly * MAX_BODY_HEIGHT; + cmd.euler[2] = current_cmd.lx * MAX_EULER_ANGLE; + + // Right stick controls pitch and roll + cmd.euler[1] = current_cmd.ry * MAX_EULER_ANGLE; + cmd.euler[0] = current_cmd.rx * MAX_EULER_ANGLE; + break; + + case 2: // Walk mode + cmd.velocity[0] = std::clamp(current_cmd.ly * MAX_FORWARD_SPEED, -MAX_FORWARD_SPEED, MAX_FORWARD_SPEED); + cmd.yawSpeed = std::clamp(-current_cmd.lx * MAX_YAW_SPEED, -MAX_YAW_SPEED, MAX_YAW_SPEED); + cmd.velocity[1] = std::clamp(-current_cmd.rx * MAX_SIDE_SPEED, -MAX_SIDE_SPEED, MAX_SIDE_SPEED); + + // Check button states for gait type + if (current_cmd.buttons & 0x0001) { // Button A + cmd.gaitType = 0; // Trot + } else if (current_cmd.buttons & 0x0002) { // Button B + cmd.gaitType = 1; // Trot running + } else if (current_cmd.buttons & 0x0004) { // Button X + cmd.gaitType = 2; // Climb mode + } else if (current_cmd.buttons & 0x0008) { // Button Y + cmd.gaitType = 3; // Trot obstacle + } + break; + + case 5: // Damping mode + case 6: // Recovery stand up + break; + + default: + cmd.mode = 0; // Default to idle for safety + break; + } + + // Debug output + static int counter = 0; + if (counter++ % 500 == 0) { // Print every second + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast( + now - last_packet_time_).count(); + + std::cout << "Mode: " << (int)cmd.mode + << " Vel: [" << cmd.velocity[0] << ", " << cmd.velocity[1] << "]" + << " Yaw: " << cmd.yawSpeed + << " Last packet: " << elapsed << "ms ago" + << " IMU: " << state.imu.rpy[2] << std::endl; + } + + udp.SetSend(cmd); +} + +// Signal handler for clean shutdown +JoystickServer* g_server = nullptr; + +void signal_handler(int sig) { + if (g_server) { + std::cout << "\nShutting down server..." << std::endl; + g_server->Stop(); + } + exit(0); +} + +int main(int argc, char* argv[]) { + int port = 9090; // Default port + + if (argc > 1) { + port = atoi(argv[1]); + } + + std::cout << "UDP Unitree B1 Joystick Control Server" << std::endl; + std::cout << "Communication level: HIGH-level" << std::endl; + std::cout << "Protocol: UDP (datagram)" << std::endl; + std::cout << "Server port: " << port << std::endl; + std::cout << "Packet size: 19 bytes (Python) or 20 bytes (C++)" << std::endl; + std::cout << "Update rate: 50Hz expected" << std::endl; + std::cout << "WARNING: Make sure the robot is standing on the ground." << std::endl; + std::cout << "Press Enter to continue..." << std::endl; + std::cin.ignore(); + + JoystickServer server(HIGHLEVEL, port); + g_server = &server; + + // Set up signal handler + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); + + server.Start(); + + return 0; +} \ No newline at end of file diff --git a/dimos/robot/unitree_webrtc/unitree_b1/test_connection.py b/dimos/robot/unitree_webrtc/unitree_b1/test_connection.py new file mode 100644 index 0000000000..49421c85e0 --- /dev/null +++ b/dimos/robot/unitree_webrtc/unitree_b1/test_connection.py @@ -0,0 +1,431 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright 2025 Dimensional Inc. + +"""Comprehensive tests for Unitree B1 connection module Timer implementation.""" + +# TODO: These tests are reaching too much into `conn` by setting and shutting +# down threads manually. That code is already in the connection module, and +# should be used and tested. Additionally, tests should always use `try-finally` +# to clean up even if the test fails. + +import threading +import time + +from dimos.msgs.geometry_msgs import TwistStamped, Vector3 +from dimos.msgs.std_msgs.Int32 import Int32 + +from .connection import MockB1ConnectionModule + + +class TestB1Connection: + """Test suite for B1 connection module with Timer implementation.""" + + def test_watchdog_actually_zeros_commands(self) -> None: + """Test that watchdog thread zeros commands after timeout.""" + conn = MockB1ConnectionModule(ip="127.0.0.1", port=9090) + conn.running = True + conn.watchdog_running = True + conn.send_thread = threading.Thread(target=conn._send_loop, daemon=True) + conn.send_thread.start() + conn.watchdog_thread = threading.Thread(target=conn._watchdog_loop, daemon=True) + conn.watchdog_thread.start() + + # Send a forward command + twist_stamped = TwistStamped( + ts=time.time(), + frame_id="base_link", + linear=Vector3(1.0, 0, 0), + angular=Vector3(0, 0, 0), + ) + conn.handle_twist_stamped(twist_stamped) + + # Verify command is set + assert conn._current_cmd.ly == 1.0 + assert conn._current_cmd.mode == 2 + assert not conn.timeout_active + + # Wait for watchdog timeout (200ms + buffer) + time.sleep(0.3) + + # Verify commands were zeroed by watchdog + assert conn._current_cmd.ly == 0.0 + assert conn._current_cmd.lx == 0.0 + assert conn._current_cmd.rx == 0.0 + assert conn._current_cmd.ry == 0.0 + assert conn._current_cmd.mode == 2 # Mode maintained + assert conn.timeout_active + + conn.running = False + conn.watchdog_running = False + conn.send_thread.join(timeout=0.5) + conn.watchdog_thread.join(timeout=0.5) + conn._close_module() + + def test_watchdog_resets_on_new_command(self) -> None: + """Test that watchdog timeout resets when new command arrives.""" + conn = MockB1ConnectionModule(ip="127.0.0.1", port=9090) + conn.running = True + conn.watchdog_running = True + conn.send_thread = threading.Thread(target=conn._send_loop, daemon=True) + conn.send_thread.start() + conn.watchdog_thread = threading.Thread(target=conn._watchdog_loop, daemon=True) + conn.watchdog_thread.start() + + # Send first command + twist1 = TwistStamped( + ts=time.time(), + frame_id="base_link", + linear=Vector3(1.0, 0, 0), + angular=Vector3(0, 0, 0), + ) + conn.handle_twist_stamped(twist1) + assert conn._current_cmd.ly == 1.0 + + # Wait 150ms (not enough to trigger timeout) + time.sleep(0.15) + + # Send second command before timeout + twist2 = TwistStamped( + ts=time.time(), + frame_id="base_link", + linear=Vector3(0.5, 0, 0), + angular=Vector3(0, 0, 0), + ) + conn.handle_twist_stamped(twist2) + + # Command should be updated and no timeout + assert conn._current_cmd.ly == 0.5 + assert not conn.timeout_active + + # Wait another 150ms (total 300ms from second command) + time.sleep(0.15) + # Should still not timeout since we reset the timer + assert not conn.timeout_active + assert conn._current_cmd.ly == 0.5 + + conn.running = False + conn.watchdog_running = False + conn.send_thread.join(timeout=0.5) + conn.watchdog_thread.join(timeout=0.5) + conn._close_module() + + def test_watchdog_thread_efficiency(self) -> None: + """Test that watchdog uses only one thread regardless of command rate.""" + conn = MockB1ConnectionModule(ip="127.0.0.1", port=9090) + conn.running = True + conn.watchdog_running = True + conn.send_thread = threading.Thread(target=conn._send_loop, daemon=True) + conn.send_thread.start() + conn.watchdog_thread = threading.Thread(target=conn._watchdog_loop, daemon=True) + conn.watchdog_thread.start() + + # Count threads before sending commands + initial_thread_count = threading.active_count() + + # Send many commands rapidly (would create many Timer threads in old implementation) + for i in range(50): + twist = TwistStamped( + ts=time.time(), + frame_id="base_link", + linear=Vector3(i * 0.01, 0, 0), + angular=Vector3(0, 0, 0), + ) + conn.handle_twist_stamped(twist) + time.sleep(0.01) # 100Hz command rate + + # Thread count should be same (no new threads created) + final_thread_count = threading.active_count() + assert final_thread_count == initial_thread_count, "No new threads should be created" + + conn.running = False + conn.watchdog_running = False + conn.send_thread.join(timeout=0.5) + conn.watchdog_thread.join(timeout=0.5) + conn._close_module() + + def test_watchdog_with_send_loop_blocking(self) -> None: + """Test that watchdog still works if send loop blocks.""" + conn = MockB1ConnectionModule(ip="127.0.0.1", port=9090) + + # Mock the send loop to simulate blocking + original_send_loop = conn._send_loop + block_event = threading.Event() + + def blocking_send_loop() -> None: + # Block immediately + block_event.wait() + # Then run normally + original_send_loop() + + conn._send_loop = blocking_send_loop + conn.running = True + conn.watchdog_running = True + conn.send_thread = threading.Thread(target=conn._send_loop, daemon=True) + conn.send_thread.start() + conn.watchdog_thread = threading.Thread(target=conn._watchdog_loop, daemon=True) + conn.watchdog_thread.start() + + # Send command + twist = TwistStamped( + ts=time.time(), + frame_id="base_link", + linear=Vector3(1.0, 0, 0), + angular=Vector3(0, 0, 0), + ) + conn.handle_twist_stamped(twist) + assert conn._current_cmd.ly == 1.0 + + # Wait for watchdog timeout + time.sleep(0.3) + + # Watchdog should have zeroed commands despite blocked send loop + assert conn._current_cmd.ly == 0.0 + assert conn.timeout_active + + # Unblock send loop + block_event.set() + conn.running = False + conn.watchdog_running = False + conn.send_thread.join(timeout=0.5) + conn.watchdog_thread.join(timeout=0.5) + conn._close_module() + + def test_continuous_commands_prevent_timeout(self) -> None: + """Test that continuous commands prevent watchdog timeout.""" + conn = MockB1ConnectionModule(ip="127.0.0.1", port=9090) + conn.running = True + conn.watchdog_running = True + conn.send_thread = threading.Thread(target=conn._send_loop, daemon=True) + conn.send_thread.start() + conn.watchdog_thread = threading.Thread(target=conn._watchdog_loop, daemon=True) + conn.watchdog_thread.start() + + # Send commands continuously for 500ms (should prevent timeout) + start = time.time() + commands_sent = 0 + while time.time() - start < 0.5: + twist = TwistStamped( + ts=time.time(), + frame_id="base_link", + linear=Vector3(0.5, 0, 0), + angular=Vector3(0, 0, 0), + ) + conn.handle_twist_stamped(twist) + commands_sent += 1 + time.sleep(0.05) # 50ms between commands (well under 200ms timeout) + + # Should never timeout + assert not conn.timeout_active, "Should not timeout with continuous commands" + assert conn._current_cmd.ly == 0.5, "Commands should still be active" + assert commands_sent >= 9, f"Should send at least 9 commands in 500ms, sent {commands_sent}" + + conn.running = False + conn.watchdog_running = False + conn.send_thread.join(timeout=0.5) + conn.watchdog_thread.join(timeout=0.5) + conn._close_module() + + def test_watchdog_timing_accuracy(self) -> None: + """Test that watchdog zeros commands at approximately 200ms.""" + conn = MockB1ConnectionModule(ip="127.0.0.1", port=9090) + conn.running = True + conn.watchdog_running = True + conn.send_thread = threading.Thread(target=conn._send_loop, daemon=True) + conn.send_thread.start() + conn.watchdog_thread = threading.Thread(target=conn._watchdog_loop, daemon=True) + conn.watchdog_thread.start() + + # Send command and record time + start_time = time.time() + twist = TwistStamped( + ts=time.time(), + frame_id="base_link", + linear=Vector3(1.0, 0, 0), + angular=Vector3(0, 0, 0), + ) + conn.handle_twist_stamped(twist) + + # Wait for timeout checking periodically + timeout_time = None + while time.time() - start_time < 0.5: + if conn.timeout_active: + timeout_time = time.time() + break + time.sleep(0.01) + + assert timeout_time is not None, "Watchdog should timeout within 500ms" + + # Check timing (should be close to 200ms + up to 50ms watchdog interval) + elapsed = timeout_time - start_time + print(f"\nWatchdog timeout occurred at exactly {elapsed:.3f} seconds") + assert 0.19 <= elapsed <= 0.3, f"Watchdog timed out at {elapsed:.3f}s, expected ~0.2-0.25s" + + conn.running = False + conn.watchdog_running = False + conn.send_thread.join(timeout=0.5) + conn.watchdog_thread.join(timeout=0.5) + conn._close_module() + + def test_mode_changes_with_watchdog(self) -> None: + """Test that mode changes work correctly with watchdog.""" + conn = MockB1ConnectionModule(ip="127.0.0.1", port=9090) + conn.running = True + conn.watchdog_running = True + conn.send_thread = threading.Thread(target=conn._send_loop, daemon=True) + conn.send_thread.start() + conn.watchdog_thread = threading.Thread(target=conn._watchdog_loop, daemon=True) + conn.watchdog_thread.start() + + # Give threads time to initialize + time.sleep(0.05) + + # Send walk command + twist = TwistStamped( + ts=time.time(), + frame_id="base_link", + linear=Vector3(1.0, 0, 0), + angular=Vector3(0, 0, 0), + ) + conn.handle_twist_stamped(twist) + assert conn.current_mode == 2 + assert conn._current_cmd.ly == 1.0 + + # Wait for timeout first (0.2s timeout + 0.15s margin for reliability) + time.sleep(0.35) + assert conn.timeout_active + assert conn._current_cmd.ly == 0.0 # Watchdog zeroed it + + # Now change mode to STAND + mode_msg = Int32() + mode_msg.data = 1 # STAND + conn.handle_mode(mode_msg) + assert conn.current_mode == 1 + assert conn._current_cmd.mode == 1 + # timeout_active stays true since we didn't send new movement commands + + conn.running = False + conn.watchdog_running = False + conn.send_thread.join(timeout=0.5) + conn.watchdog_thread.join(timeout=0.5) + conn._close_module() + + def test_watchdog_stops_movement_when_commands_stop(self) -> None: + """Verify watchdog zeros commands when packets stop being sent.""" + conn = MockB1ConnectionModule(ip="127.0.0.1", port=9090) + conn.running = True + conn.watchdog_running = True + conn.send_thread = threading.Thread(target=conn._send_loop, daemon=True) + conn.send_thread.start() + conn.watchdog_thread = threading.Thread(target=conn._watchdog_loop, daemon=True) + conn.watchdog_thread.start() + + # Simulate sending movement commands for a while + for _i in range(5): + twist = TwistStamped( + ts=time.time(), + frame_id="base_link", + linear=Vector3(1.0, 0, 0), + angular=Vector3(0, 0, 0.5), # Forward and turning + ) + conn.handle_twist_stamped(twist) + time.sleep(0.05) # Send at 20Hz + + # Verify robot is moving + assert conn._current_cmd.ly == 1.0 + assert conn._current_cmd.lx == -0.25 # angular.z * 0.5 -> lx (for turning) + assert conn.current_mode == 2 # WALK mode + assert not conn.timeout_active + + # Wait for watchdog to detect timeout (200ms + buffer) + time.sleep(0.3) + + assert conn.timeout_active, "Watchdog should have detected timeout" + assert conn._current_cmd.ly == 0.0, "Forward velocity should be zeroed" + assert conn._current_cmd.lx == 0.0, "Lateral velocity should be zeroed" + assert conn._current_cmd.rx == 0.0, "Rotation X should be zeroed" + assert conn._current_cmd.ry == 0.0, "Rotation Y should be zeroed" + assert conn.current_mode == 2, "Mode should stay as WALK" + + # Verify recovery works - send new command + twist = TwistStamped( + ts=time.time(), + frame_id="base_link", + linear=Vector3(0.5, 0, 0), + angular=Vector3(0, 0, 0), + ) + conn.handle_twist_stamped(twist) + + # Give watchdog time to detect recovery + time.sleep(0.1) + + assert not conn.timeout_active, "Should recover from timeout" + assert conn._current_cmd.ly == 0.5, "Should accept new commands" + + conn.running = False + conn.watchdog_running = False + conn.send_thread.join(timeout=0.5) + conn.watchdog_thread.join(timeout=0.5) + conn._close_module() + + def test_rapid_command_thread_safety(self) -> None: + """Test thread safety with rapid commands from multiple threads.""" + conn = MockB1ConnectionModule(ip="127.0.0.1", port=9090) + conn.running = True + conn.watchdog_running = True + conn.send_thread = threading.Thread(target=conn._send_loop, daemon=True) + conn.send_thread.start() + conn.watchdog_thread = threading.Thread(target=conn._watchdog_loop, daemon=True) + conn.watchdog_thread.start() + + # Count initial threads + initial_threads = threading.active_count() + + # Send commands from multiple threads rapidly + def send_commands(thread_id) -> None: + for _i in range(10): + twist = TwistStamped( + ts=time.time(), + frame_id="base_link", + linear=Vector3(thread_id * 0.1, 0, 0), + angular=Vector3(0, 0, 0), + ) + conn.handle_twist_stamped(twist) + time.sleep(0.01) + + threads = [] + for i in range(3): + t = threading.Thread(target=send_commands, args=(i,)) + threads.append(t) + t.start() + + for t in threads: + t.join() + + # Thread count should only increase by the 3 sender threads we created + # No additional Timer threads should be created + final_threads = threading.active_count() + assert final_threads <= initial_threads, "No extra threads should be created by watchdog" + + # Commands should still work correctly + assert conn._current_cmd.ly >= 0, "Last command should be set" + assert not conn.timeout_active, "Should not be in timeout with recent commands" + + conn.running = False + conn.watchdog_running = False + conn.send_thread.join(timeout=0.5) + conn.watchdog_thread.join(timeout=0.5) + conn._close_module() diff --git a/dimos/robot/unitree_webrtc/unitree_b1/unitree_b1.py b/dimos/robot/unitree_webrtc/unitree_b1/unitree_b1.py new file mode 100644 index 0000000000..04390c2e9e --- /dev/null +++ b/dimos/robot/unitree_webrtc/unitree_b1/unitree_b1.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright 2025 Dimensional Inc. + +""" +Unitree B1 quadruped robot with simplified UDP control. +Uses standard Twist interface for velocity commands. +""" + +import logging +import os + +from dimos import core +from dimos.core.module_coordinator import ModuleCoordinator +from dimos.core.resource import Resource +from dimos.msgs.geometry_msgs import PoseStamped, TwistStamped +from dimos.msgs.nav_msgs.Odometry import Odometry +from dimos.msgs.std_msgs import Int32 +from dimos.msgs.tf2_msgs.TFMessage import TFMessage +from dimos.robot.robot import Robot +from dimos.robot.ros_bridge import BridgeDirection, ROSBridge +from dimos.robot.unitree_webrtc.unitree_b1.connection import ( + B1ConnectionModule, + MockB1ConnectionModule, +) +from dimos.skills.skills import SkillLibrary +from dimos.types.robot_capabilities import RobotCapability +from dimos.utils.logging_config import setup_logger + +# Handle ROS imports for environments where ROS is not available like CI +try: + from geometry_msgs.msg import TwistStamped as ROSTwistStamped + from nav_msgs.msg import Odometry as ROSOdometry + from tf2_msgs.msg import TFMessage as ROSTFMessage + + ROS_AVAILABLE = True +except ImportError: + ROSTwistStamped = None + ROSOdometry = None + ROSTFMessage = None + ROS_AVAILABLE = False + +logger = setup_logger("dimos.robot.unitree_webrtc.unitree_b1", level=logging.INFO) + + +class UnitreeB1(Robot, Resource): + """Unitree B1 quadruped robot with UDP control. + + Simplified architecture: + - Connection module handles Twist → B1Command conversion + - Standard /cmd_vel interface for navigation compatibility + - Optional joystick module for testing + """ + + def __init__( + self, + ip: str = "192.168.123.14", + port: int = 9090, + output_dir: str | None = None, + skill_library: SkillLibrary | None = None, + enable_joystick: bool = False, + enable_ros_bridge: bool = True, + test_mode: bool = False, + ) -> None: + """Initialize the B1 robot. + + Args: + ip: Robot IP address (or server running joystick_server_udp) + port: UDP port for joystick server (default 9090) + output_dir: Directory for saving outputs + skill_library: Skill library instance (optional) + enable_joystick: Enable pygame joystick control module + enable_ros_bridge: Enable ROS bridge for external control + test_mode: Test mode - print commands instead of sending UDP + """ + super().__init__() + self.ip = ip + self.port = port + self.output_dir = output_dir or os.path.join(os.getcwd(), "assets", "output") + self.enable_joystick = enable_joystick + self.enable_ros_bridge = enable_ros_bridge + self.test_mode = test_mode + self.capabilities = [RobotCapability.LOCOMOTION] + self.connection = None + self.joystick = None + self.ros_bridge = None + self._dimos = ModuleCoordinator(n=2) + + os.makedirs(self.output_dir, exist_ok=True) + logger.info(f"Robot outputs will be saved to: {self.output_dir}") + + def start(self) -> None: + """Start the B1 robot - initialize DimOS, deploy modules, and start them.""" + + logger.info("Initializing DimOS...") + self._dimos.start() + + logger.info("Deploying connection module...") + if self.test_mode: + self.connection = self._dimos.deploy(MockB1ConnectionModule, self.ip, self.port) + else: + self.connection = self._dimos.deploy(B1ConnectionModule, self.ip, self.port) + + # Configure LCM transports for connection (matching G1 pattern) + self.connection.cmd_vel.transport = core.LCMTransport("/cmd_vel", TwistStamped) + self.connection.mode_cmd.transport = core.LCMTransport("/b1/mode", Int32) + self.connection.odom_in.transport = core.LCMTransport("/state_estimation", Odometry) + self.connection.odom_pose.transport = core.LCMTransport("/odom", PoseStamped) + + # Deploy joystick move_vel control + if self.enable_joystick: + from dimos.robot.unitree_webrtc.unitree_b1.joystick_module import JoystickModule + + self.joystick = self._dimos.deploy(JoystickModule) + self.joystick.twist_out.transport = core.LCMTransport("/cmd_vel", TwistStamped) + self.joystick.mode_out.transport = core.LCMTransport("/b1/mode", Int32) + logger.info("Joystick module deployed - pygame window will open") + + self._dimos.start_all_modules() + + self.connection.idle() # Start in IDLE mode for safety + logger.info("B1 started in IDLE mode (safety)") + + # Deploy ROS bridge if enabled (matching G1 pattern) + if self.enable_ros_bridge: + self._deploy_ros_bridge() + + logger.info(f"UnitreeB1 initialized - UDP control to {self.ip}:{self.port}") + if self.enable_joystick: + logger.info("Pygame joystick module enabled for testing") + if self.enable_ros_bridge: + logger.info("ROS bridge enabled for external control") + + def stop(self) -> None: + self._dimos.stop() + if self.ros_bridge: + self.ros_bridge.stop() + + def _deploy_ros_bridge(self) -> None: + """Deploy and configure ROS bridge (matching G1 implementation).""" + self.ros_bridge = ROSBridge("b1_ros_bridge") + + # Add /cmd_vel topic from ROS to DIMOS + self.ros_bridge.add_topic( + "/cmd_vel", TwistStamped, ROSTwistStamped, direction=BridgeDirection.ROS_TO_DIMOS + ) + + # Add /state_estimation topic from ROS to DIMOS (external odometry) + self.ros_bridge.add_topic( + "/state_estimation", Odometry, ROSOdometry, direction=BridgeDirection.ROS_TO_DIMOS + ) + + # Add /tf topic from ROS to DIMOS + self.ros_bridge.add_topic( + "/tf", TFMessage, ROSTFMessage, direction=BridgeDirection.ROS_TO_DIMOS + ) + + self.ros_bridge.start() + + logger.info("ROS bridge deployed: /cmd_vel, /state_estimation, /tf (ROS → DIMOS)") + + # Robot control methods (standard interface) + def move(self, twist_stamped: TwistStamped, duration: float = 0.0) -> None: + """Send movement command to robot using timestamped Twist. + + Args: + twist_stamped: TwistStamped message with linear and angular velocities + duration: How long to move (not used for B1) + """ + if self.connection: + self.connection.move(twist_stamped, duration) + + def stand(self) -> None: + """Put robot in stand mode.""" + if self.connection: + self.connection.stand() + logger.info("B1 switched to STAND mode") + + def walk(self) -> None: + """Put robot in walk mode.""" + if self.connection: + self.connection.walk() + logger.info("B1 switched to WALK mode") + + def idle(self) -> None: + """Put robot in idle mode.""" + if self.connection: + self.connection.idle() + logger.info("B1 switched to IDLE mode") + + +def main() -> None: + """Main entry point for testing B1 robot.""" + import argparse + + parser = argparse.ArgumentParser(description="Unitree B1 Robot Control") + parser.add_argument("--ip", default="192.168.12.1", help="Robot IP address") + parser.add_argument("--port", type=int, default=9090, help="UDP port") + parser.add_argument("--joystick", action="store_true", help="Enable pygame joystick control") + parser.add_argument("--ros-bridge", action="store_true", default=True, help="Enable ROS bridge") + parser.add_argument( + "--no-ros-bridge", dest="ros_bridge", action="store_false", help="Disable ROS bridge" + ) + parser.add_argument("--output-dir", help="Output directory for logs/data") + parser.add_argument( + "--test", action="store_true", help="Test mode - print commands instead of UDP" + ) + + args = parser.parse_args() + + robot = UnitreeB1( + ip=args.ip, + port=args.port, + output_dir=args.output_dir, + enable_joystick=args.joystick, + enable_ros_bridge=args.ros_bridge, + test_mode=args.test, + ) + + robot.start() + + try: + if args.joystick: + print("\n" + "=" * 50) + print("B1 JOYSTICK CONTROL") + print("=" * 50) + print("Focus the pygame window to control") + print("Press keys in pygame window:") + print(" 0/1/2 = Idle/Stand/Walk modes") + print(" WASD = Move/Turn") + print(" JL = Strafe") + print(" Space/Q = Emergency Stop") + print(" ESC = Quit pygame (then Ctrl+C to exit)") + print("=" * 50 + "\n") + + import time + + while True: + time.sleep(1) + else: + # Manual control example + print("\nB1 Robot ready for commands") + print("Use robot.idle(), robot.stand(), robot.walk() to change modes") + if args.ros_bridge: + print("ROS bridge active - listening for /cmd_vel and /state_estimation") + else: + print("Use robot.move(TwistStamped(...)) to send velocity commands") + print("Press Ctrl+C to exit\n") + + import time + + while True: + time.sleep(1) + + except KeyboardInterrupt: + print("\nShutting down...") + finally: + robot.stop() + + +if __name__ == "__main__": + main() diff --git a/dimos/robot/unitree_webrtc/unitree_g1.py b/dimos/robot/unitree_webrtc/unitree_g1.py new file mode 100644 index 0000000000..fc148c54c3 --- /dev/null +++ b/dimos/robot/unitree_webrtc/unitree_g1.py @@ -0,0 +1,549 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Unitree G1 humanoid robot. +Minimal implementation using WebRTC connection for robot control. +""" + +import logging +import os +import time + +from dimos_lcm.foxglove_msgs import SceneUpdate +from geometry_msgs.msg import PoseStamped as ROSPoseStamped, TwistStamped as ROSTwistStamped +from nav_msgs.msg import Odometry as ROSOdometry +from reactivex.disposable import Disposable +from sensor_msgs.msg import Joy as ROSJoy, PointCloud2 as ROSPointCloud2 +from tf2_msgs.msg import TFMessage as ROSTFMessage + +from dimos import core +from dimos.agents2 import Agent +from dimos.agents2.cli.human import HumanInput +from dimos.agents2.skills.ros_navigation import RosNavigation +from dimos.agents2.spec import Model, Provider +from dimos.core import In, Module, Out, rpc +from dimos.core.module_coordinator import ModuleCoordinator +from dimos.core.resource import Resource +from dimos.hardware.camera import zed +from dimos.hardware.camera.module import CameraModule +from dimos.hardware.camera.webcam import Webcam +from dimos.msgs.foxglove_msgs import ImageAnnotations +from dimos.msgs.geometry_msgs import ( + PoseStamped, + Quaternion, + Transform, + Twist, + TwistStamped, + Vector3, +) +from dimos.msgs.nav_msgs.Odometry import Odometry +from dimos.msgs.sensor_msgs import CameraInfo, Image, Joy, PointCloud2 +from dimos.msgs.std_msgs.Bool import Bool +from dimos.msgs.tf2_msgs.TFMessage import TFMessage +from dimos.msgs.vision_msgs import Detection2DArray +from dimos.perception.detection.moduleDB import ObjectDBModule +from dimos.perception.spatial_perception import SpatialMemory +from dimos.protocol import pubsub +from dimos.protocol.pubsub.lcmpubsub import LCM +from dimos.robot.foxglove_bridge import FoxgloveBridge +from dimos.robot.robot import Robot +from dimos.robot.ros_bridge import BridgeDirection, ROSBridge +from dimos.robot.unitree_webrtc.connection import UnitreeWebRTCConnection +from dimos.robot.unitree_webrtc.rosnav import NavigationModule +from dimos.robot.unitree_webrtc.unitree_g1_skill_container import UnitreeG1SkillContainer +from dimos.robot.unitree_webrtc.unitree_skills import MyUnitreeSkills +from dimos.skills.skills import SkillLibrary +from dimos.types.robot_capabilities import RobotCapability +from dimos.utils.logging_config import setup_logger +from dimos.web.websocket_vis.websocket_vis_module import WebsocketVisModule + +logger = setup_logger("dimos.robot.unitree_webrtc.unitree_g1", level=logging.INFO) + +# Suppress verbose loggers +logging.getLogger("aiortc.codecs.h264").setLevel(logging.ERROR) +logging.getLogger("lcm_foxglove_bridge").setLevel(logging.ERROR) +logging.getLogger("websockets.server").setLevel(logging.ERROR) +logging.getLogger("FoxgloveServer").setLevel(logging.ERROR) +logging.getLogger("asyncio").setLevel(logging.ERROR) + + +class G1ConnectionModule(Module): + """Simplified connection module for G1 - uses WebRTC for control.""" + + movecmd: In[TwistStamped] = None + odom_in: In[Odometry] = None + + odom_pose: Out[PoseStamped] = None + ip: str + connection_type: str = "webrtc" + + def __init__( + self, ip: str | None = None, connection_type: str = "webrtc", *args, **kwargs + ) -> None: + self.ip = ip + self.connection_type = connection_type + self.connection = None + Module.__init__(self, *args, **kwargs) + + @rpc + def start(self) -> None: + """Start the connection and subscribe to sensor streams.""" + + super().start() + + # Use the exact same UnitreeWebRTCConnection as Go2 + self.connection = UnitreeWebRTCConnection(self.ip) + self.connection.start() + unsub = self.movecmd.subscribe(self.move) + self._disposables.add(Disposable(unsub)) + unsub = self.odom_in.subscribe(self._publish_odom_pose) + self._disposables.add(Disposable(unsub)) + + @rpc + def stop(self) -> None: + self.connection.stop() + super().stop() + + def _publish_odom_pose(self, msg: Odometry) -> None: + self.odom_pose.publish( + PoseStamped( + ts=msg.ts, + frame_id=msg.frame_id, + position=msg.pose.pose.position, + orientation=msg.pose.orientation, + ) + ) + + @rpc + def move(self, twist_stamped: TwistStamped, duration: float = 0.0) -> None: + """Send movement command to robot.""" + twist = Twist(linear=twist_stamped.linear, angular=twist_stamped.angular) + self.connection.move(twist, duration) + + @rpc + def publish_request(self, topic: str, data: dict): + """Forward WebRTC publish requests to connection.""" + return self.connection.publish_request(topic, data) + + +class UnitreeG1(Robot, Resource): + """Unitree G1 humanoid robot.""" + + def __init__( + self, + ip: str, + output_dir: str | None = None, + websocket_port: int = 7779, + skill_library: SkillLibrary | None = None, + recording_path: str | None = None, + replay_path: str | None = None, + enable_joystick: bool = False, + enable_connection: bool = True, + enable_ros_bridge: bool = True, + enable_perception: bool = False, + enable_camera: bool = False, + ) -> None: + """Initialize the G1 robot. + + Args: + ip: Robot IP address + output_dir: Directory for saving outputs + websocket_port: Port for web visualization + skill_library: Skill library instance + recording_path: Path to save recordings (if recording) + replay_path: Path to replay recordings from (if replaying) + enable_joystick: Enable pygame joystick control + enable_connection: Enable robot connection module + enable_ros_bridge: Enable ROS bridge + enable_camera: Enable web camera module + """ + super().__init__() + self.ip = ip + self.output_dir = output_dir or os.path.join(os.getcwd(), "assets", "output") + self.recording_path = recording_path + self.replay_path = replay_path + self.enable_joystick = enable_joystick + self.enable_connection = enable_connection + self.enable_ros_bridge = enable_ros_bridge + self.enable_perception = enable_perception + self.enable_camera = enable_camera + self.websocket_port = websocket_port + self.lcm = LCM() + + # Initialize skill library with G1 robot type + if skill_library is None: + from dimos.robot.unitree_webrtc.unitree_skills import MyUnitreeSkills + + skill_library = MyUnitreeSkills(robot_type="g1") + self.skill_library = skill_library + + # Set robot capabilities + self.capabilities = [RobotCapability.LOCOMOTION] + + # Module references + self._dimos = ModuleCoordinator(n=4) + self.connection = None + self.websocket_vis = None + self.foxglove_bridge = None + self.spatial_memory_module = None + self.joystick = None + self.ros_bridge = None + self.camera = None + self._ros_nav = None + self._setup_directories() + + def _setup_directories(self) -> None: + """Setup directories for spatial memory storage.""" + os.makedirs(self.output_dir, exist_ok=True) + logger.info(f"Robot outputs will be saved to: {self.output_dir}") + + # Initialize memory directories + self.memory_dir = os.path.join(self.output_dir, "memory") + os.makedirs(self.memory_dir, exist_ok=True) + + # Initialize spatial memory properties + self.spatial_memory_dir = os.path.join(self.memory_dir, "spatial_memory") + self.spatial_memory_collection = "spatial_memory" + self.db_path = os.path.join(self.spatial_memory_dir, "chromadb_data") + self.visual_memory_path = os.path.join(self.spatial_memory_dir, "visual_memory.pkl") + + # Create spatial memory directories + os.makedirs(self.spatial_memory_dir, exist_ok=True) + os.makedirs(self.db_path, exist_ok=True) + + def _deploy_detection(self, goto) -> None: + detection = self._dimos.deploy( + ObjectDBModule, goto=goto, camera_info=zed.CameraInfo.SingleWebcam + ) + + detection.image.connect(self.camera.image) + detection.pointcloud.transport = core.LCMTransport("/map", PointCloud2) + + detection.annotations.transport = core.LCMTransport("/annotations", ImageAnnotations) + detection.detections.transport = core.LCMTransport("/detections", Detection2DArray) + + detection.scene_update.transport = core.LCMTransport("/scene_update", SceneUpdate) + detection.target.transport = core.LCMTransport("/target", PoseStamped) + detection.detected_pointcloud_0.transport = core.LCMTransport( + "/detected/pointcloud/0", PointCloud2 + ) + detection.detected_pointcloud_1.transport = core.LCMTransport( + "/detected/pointcloud/1", PointCloud2 + ) + detection.detected_pointcloud_2.transport = core.LCMTransport( + "/detected/pointcloud/2", PointCloud2 + ) + + detection.detected_image_0.transport = core.LCMTransport("/detected/image/0", Image) + detection.detected_image_1.transport = core.LCMTransport("/detected/image/1", Image) + detection.detected_image_2.transport = core.LCMTransport("/detected/image/2", Image) + + self.detection = detection + + def start(self) -> None: + self.lcm.start() + self._dimos.start() + + if self.enable_connection: + self._deploy_connection() + + self._deploy_visualization() + + if self.enable_joystick: + self._deploy_joystick() + + if self.enable_ros_bridge: + self._deploy_ros_bridge() + + self.nav = self._dimos.deploy(NavigationModule) + self.nav.goal_reached.transport = core.LCMTransport("/goal_reached", Bool) + self.nav.goal_pose.transport = core.LCMTransport("/goal_pose", PoseStamped) + self.nav.cancel_goal.transport = core.LCMTransport("/cancel_goal", Bool) + self.nav.joy.transport = core.LCMTransport("/joy", Joy) + self.nav.start() + + self._deploy_camera() + self._deploy_detection(self.nav.go_to) + + if self.enable_perception: + self._deploy_perception() + + self.lcm.start() + + # Setup agent with G1 skills + logger.info("Setting up agent with G1 skills...") + + agent = Agent( + system_prompt="You are a helpful assistant controlling a Unitree G1 humanoid robot. You can control the robot's arms, movement modes, and navigation.", + model=Model.GPT_4O, + provider=Provider.OPENAI, + ) + + # Register G1-specific skill container + g1_skills = UnitreeG1SkillContainer(robot=self) + agent.register_skills(g1_skills) + + human_input = self._dimos.deploy(HumanInput) + agent.register_skills(human_input) + + if self.enable_perception: + agent.register_skills(self.detection) + + # Register ROS navigation + self._ros_nav = RosNavigation(self) + self._ros_nav.start() + agent.register_skills(self._ros_nav) + + agent.run_implicit_skill("human") + agent.start() + + # For logging + skills = [tool.name for tool in agent.get_tools()] + logger.info(f"Agent configured with {len(skills)} skills: {', '.join(skills)}") + + agent.loop_thread() + + logger.info("UnitreeG1 initialized and started") + logger.info(f"WebSocket visualization available at http://localhost:{self.websocket_port}") + self._start_modules() + + def stop(self) -> None: + self._dimos.stop() + if self._ros_nav: + self._ros_nav.stop() + self.lcm.stop() + + def _deploy_connection(self) -> None: + """Deploy and configure the connection module.""" + self.connection = self._dimos.deploy(G1ConnectionModule, self.ip) + + # Configure LCM transports + self.connection.movecmd.transport = core.LCMTransport("/cmd_vel", TwistStamped) + self.connection.odom_in.transport = core.LCMTransport("/state_estimation", Odometry) + self.connection.odom_pose.transport = core.LCMTransport("/odom", PoseStamped) + + def _deploy_camera(self) -> None: + """Deploy and configure a standard webcam module.""" + logger.info("Deploying standard webcam module...") + + self.camera = self._dimos.deploy( + CameraModule, + transform=Transform( + translation=Vector3(0.05, 0.0, 0.0), + rotation=Quaternion.from_euler(Vector3(0.0, 0.2, 0.0)), + frame_id="sensor", + child_frame_id="camera_link", + ), + hardware=lambda: Webcam( + camera_index=0, + frequency=15, + stereo_slice="left", + camera_info=zed.CameraInfo.SingleWebcam, + ), + ) + + self.camera.image.transport = core.LCMTransport("/image", Image) + self.camera.camera_info.transport = core.LCMTransport("/camera_info", CameraInfo) + logger.info("Webcam module configured") + + def _deploy_visualization(self) -> None: + """Deploy and configure visualization modules.""" + # Deploy WebSocket visualization module + self.websocket_vis = self._dimos.deploy(WebsocketVisModule, port=self.websocket_port) + self.websocket_vis.movecmd_stamped.transport = core.LCMTransport("/cmd_vel", TwistStamped) + + # Note: robot_pose connection removed since odom was removed from G1ConnectionModule + + # Deploy Foxglove bridge + self.foxglove_bridge = FoxgloveBridge( + shm_channels=[ + "/zed/color_image#sensor_msgs.Image", + "/zed/depth_image#sensor_msgs.Image", + ] + ) + self.foxglove_bridge.start() + + def _deploy_perception(self) -> None: + self.spatial_memory_module = self._dimos.deploy( + SpatialMemory, + collection_name=self.spatial_memory_collection, + db_path=self.db_path, + visual_memory_path=self.visual_memory_path, + output_dir=self.spatial_memory_dir, + ) + + self.spatial_memory_module.color_image.connect(self.camera.image) + self.spatial_memory_module.odom.transport = core.LCMTransport("/odom", PoseStamped) + + logger.info("Spatial memory module deployed and connected") + + def _deploy_joystick(self) -> None: + """Deploy joystick control module.""" + from dimos.robot.unitree_webrtc.g1_joystick_module import G1JoystickModule + + logger.info("Deploying G1 joystick module...") + self.joystick = self._dimos.deploy(G1JoystickModule) + self.joystick.twist_out.transport = core.LCMTransport("/cmd_vel", Twist) + logger.info("Joystick module deployed - pygame window will open") + + def _deploy_ros_bridge(self) -> None: + """Deploy and configure ROS bridge.""" + self.ros_bridge = ROSBridge("g1_ros_bridge") + + # Add /cmd_vel topic from ROS to DIMOS + self.ros_bridge.add_topic( + "/cmd_vel", TwistStamped, ROSTwistStamped, direction=BridgeDirection.ROS_TO_DIMOS + ) + + # Add /state_estimation topic from ROS to DIMOS + self.ros_bridge.add_topic( + "/state_estimation", Odometry, ROSOdometry, direction=BridgeDirection.ROS_TO_DIMOS + ) + + # Add /tf topic from ROS to DIMOS + self.ros_bridge.add_topic( + "/tf", TFMessage, ROSTFMessage, direction=BridgeDirection.ROS_TO_DIMOS + ) + + from std_msgs.msg import Bool as ROSBool + + from dimos.msgs.std_msgs import Bool + + # Navigation control topics from autonomy stack + self.ros_bridge.add_topic( + "/goal_pose", PoseStamped, ROSPoseStamped, direction=BridgeDirection.DIMOS_TO_ROS + ) + self.ros_bridge.add_topic( + "/cancel_goal", Bool, ROSBool, direction=BridgeDirection.DIMOS_TO_ROS + ) + self.ros_bridge.add_topic( + "/goal_reached", Bool, ROSBool, direction=BridgeDirection.ROS_TO_DIMOS + ) + + self.ros_bridge.add_topic("/joy", Joy, ROSJoy, direction=BridgeDirection.DIMOS_TO_ROS) + + self.ros_bridge.add_topic( + "/registered_scan", + PointCloud2, + ROSPointCloud2, + direction=BridgeDirection.ROS_TO_DIMOS, + remap_topic="/map", + ) + + self.ros_bridge.start() + + logger.info( + "ROS bridge deployed: /cmd_vel, /state_estimation, /tf, /registered_scan (ROS → DIMOS)" + ) + + def _start_modules(self) -> None: + """Start all deployed modules.""" + self._dimos.start_all_modules() + + # Initialize skills after connection is established + if self.skill_library is not None: + for skill in self.skill_library: + if hasattr(skill, "__name__"): + self.skill_library.create_instance(skill.__name__, robot=self) + if isinstance(self.skill_library, MyUnitreeSkills): + self.skill_library._robot = self + self.skill_library.init() + self.skill_library.initialize_skills() + + def move(self, twist_stamped: TwistStamped, duration: float = 0.0) -> None: + """Send movement command to robot.""" + self.connection.move(twist_stamped, duration) + + def get_odom(self) -> PoseStamped: + """Get the robot's odometry.""" + # Note: odom functionality removed from G1ConnectionModule + return None + + @property + def spatial_memory(self) -> SpatialMemory | None: + return self.spatial_memory_module + + +def main() -> None: + """Main entry point for testing.""" + import argparse + import os + + from dotenv import load_dotenv + + load_dotenv() + + parser = argparse.ArgumentParser(description="Unitree G1 Humanoid Robot Control") + parser.add_argument("--ip", default=os.getenv("ROBOT_IP"), help="Robot IP address") + parser.add_argument("--joystick", action="store_true", help="Enable pygame joystick control") + parser.add_argument("--camera", action="store_true", help="Enable usb camera module") + parser.add_argument("--output-dir", help="Output directory for logs/data") + parser.add_argument("--record", help="Path to save recording") + parser.add_argument("--replay", help="Path to replay recording from") + + args = parser.parse_args() + + pubsub.lcm.autoconf() + + robot = UnitreeG1( + ip=args.ip, + output_dir=args.output_dir, + recording_path=args.record, + replay_path=args.replay, + enable_joystick=args.joystick, + enable_camera=args.camera, + enable_connection=os.getenv("ROBOT_IP") is not None, + enable_ros_bridge=True, + enable_perception=True, + ) + robot.start() + + # time.sleep(7) + # print("Starting navigation...") + # print( + # robot.nav.go_to( + # PoseStamped( + # ts=time.time(), + # frame_id="map", + # position=Vector3(0.0, 0.0, 0.03), + # orientation=Quaternion(0, 0, 0, 0), + # ), + # timeout=10, + # ), + # ) + try: + if args.joystick: + print("\n" + "=" * 50) + print("G1 HUMANOID JOYSTICK CONTROL") + print("=" * 50) + print("Focus the pygame window to control") + print("Keys:") + print(" WASD = Forward/Back/Strafe") + print(" QE = Turn Left/Right") + print(" Space = Emergency Stop") + print(" ESC = Quit pygame (then Ctrl+C to exit)") + print("=" * 50 + "\n") + + logger.info("G1 robot running. Press Ctrl+C to stop.") + while True: + time.sleep(1) + except KeyboardInterrupt: + logger.info("Shutting down...") + robot.stop() + + +if __name__ == "__main__": + main() diff --git a/dimos/robot/unitree_webrtc/unitree_g1_skill_container.py b/dimos/robot/unitree_webrtc/unitree_g1_skill_container.py new file mode 100644 index 0000000000..170b577c21 --- /dev/null +++ b/dimos/robot/unitree_webrtc/unitree_g1_skill_container.py @@ -0,0 +1,231 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Unitree G1 skill container for the new agents2 framework. +Dynamically generates skills for G1 humanoid robot including arm controls and movement modes. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from dimos.core.core import rpc +from dimos.msgs.geometry_msgs import TwistStamped, Vector3 +from dimos.protocol.skill.skill import skill +from dimos.robot.unitree_webrtc.unitree_skill_container import UnitreeSkillContainer +from dimos.utils.logging_config import setup_logger + +if TYPE_CHECKING: + from dimos.robot.unitree_webrtc.unitree_g1 import UnitreeG1 + from dimos.robot.unitree_webrtc.unitree_go2 import UnitreeGo2 + +logger = setup_logger("dimos.robot.unitree_webrtc.unitree_g1_skill_container") + +# G1 Arm Actions - all use api_id 7106 on topic "rt/api/arm/request" +G1_ARM_CONTROLS = [ + ("Handshake", 27, "Perform a handshake gesture with the right hand."), + ("HighFive", 18, "Give a high five with the right hand."), + ("Hug", 19, "Perform a hugging gesture with both arms."), + ("HighWave", 26, "Wave with the hand raised high."), + ("Clap", 17, "Clap hands together."), + ("FaceWave", 25, "Wave near the face level."), + ("LeftKiss", 12, "Blow a kiss with the left hand."), + ("ArmHeart", 20, "Make a heart shape with both arms overhead."), + ("RightHeart", 21, "Make a heart gesture with the right hand."), + ("HandsUp", 15, "Raise both hands up in the air."), + ("XRay", 24, "Hold arms in an X-ray pose position."), + ("RightHandUp", 23, "Raise only the right hand up."), + ("Reject", 22, "Make a rejection or 'no' gesture."), + ("CancelAction", 99, "Cancel any current arm action and return hands to neutral position."), +] + +# G1 Movement Modes - all use api_id 7101 on topic "rt/api/sport/request" +G1_MODE_CONTROLS = [ + ("WalkMode", 500, "Switch to normal walking mode."), + ("WalkControlWaist", 501, "Switch to walking mode with waist control."), + ("RunMode", 801, "Switch to running mode."), +] + + +class UnitreeG1SkillContainer(UnitreeSkillContainer): + """Container for Unitree G1 humanoid robot skills. + + Inherits all Go2 skills and adds G1-specific arm controls and movement modes. + """ + + def __init__(self, robot: UnitreeG1 | UnitreeGo2 | None = None) -> None: + """Initialize the skill container with robot reference. + + Args: + robot: The UnitreeG1 or UnitreeGo2 robot instance + """ + # Initialize parent class to get all base Unitree skills + super().__init__(robot) + + # Add G1-specific skills on top + self._generate_arm_skills() + self._generate_mode_skills() + + @rpc + def start(self) -> None: + super().start() + + @rpc + def stop(self) -> None: + super().stop() + + def _generate_arm_skills(self) -> None: + """Dynamically generate arm control skills from G1_ARM_CONTROLS list.""" + logger.info(f"Generating {len(G1_ARM_CONTROLS)} G1 arm control skills") + + for name, data_value, description in G1_ARM_CONTROLS: + skill_name = self._convert_to_snake_case(name) + self._create_arm_skill(skill_name, data_value, description, name) + + def _generate_mode_skills(self) -> None: + """Dynamically generate movement mode skills from G1_MODE_CONTROLS list.""" + logger.info(f"Generating {len(G1_MODE_CONTROLS)} G1 movement mode skills") + + for name, data_value, description in G1_MODE_CONTROLS: + skill_name = self._convert_to_snake_case(name) + self._create_mode_skill(skill_name, data_value, description, name) + + def _create_arm_skill( + self, skill_name: str, data_value: int, description: str, original_name: str + ) -> None: + """Create a dynamic arm control skill method with the @skill decorator. + + Args: + skill_name: Snake_case name for the method + data_value: The arm action data value + description: Human-readable description + original_name: Original CamelCase name for display + """ + + def dynamic_skill_func(self) -> str: + """Dynamic arm skill function.""" + return self._execute_arm_command(data_value, original_name) + + # Set the function's metadata + dynamic_skill_func.__name__ = skill_name + dynamic_skill_func.__doc__ = description + + # Apply the @skill decorator + decorated_skill = skill()(dynamic_skill_func) + + # Bind the method to the instance + bound_method = decorated_skill.__get__(self, self.__class__) + + # Add it as an attribute + setattr(self, skill_name, bound_method) + + logger.debug(f"Generated arm skill: {skill_name} (data={data_value})") + + def _create_mode_skill( + self, skill_name: str, data_value: int, description: str, original_name: str + ) -> None: + """Create a dynamic movement mode skill method with the @skill decorator. + + Args: + skill_name: Snake_case name for the method + data_value: The mode data value + description: Human-readable description + original_name: Original CamelCase name for display + """ + + def dynamic_skill_func(self) -> str: + """Dynamic mode skill function.""" + return self._execute_mode_command(data_value, original_name) + + # Set the function's metadata + dynamic_skill_func.__name__ = skill_name + dynamic_skill_func.__doc__ = description + + # Apply the @skill decorator + decorated_skill = skill()(dynamic_skill_func) + + # Bind the method to the instance + bound_method = decorated_skill.__get__(self, self.__class__) + + # Add it as an attribute + setattr(self, skill_name, bound_method) + + logger.debug(f"Generated mode skill: {skill_name} (data={data_value})") + + # ========== Override Skills for G1 ========== + + @skill() + def move(self, x: float, y: float = 0.0, yaw: float = 0.0, duration: float = 0.0) -> str: + """Move the robot using direct velocity commands (G1 version with TwistStamped). + + Args: + x: Forward velocity (m/s) + y: Left/right velocity (m/s) + yaw: Rotational velocity (rad/s) + duration: How long to move (seconds) + """ + if self._robot is None: + return "Error: Robot not connected" + + # G1 uses TwistStamped instead of Twist + twist_stamped = TwistStamped(linear=Vector3(x, y, 0), angular=Vector3(0, 0, yaw)) + self._robot.move(twist_stamped, duration=duration) + return f"Started moving with velocity=({x}, {y}, {yaw}) for {duration} seconds" + + # ========== Helper Methods ========== + + def _execute_arm_command(self, data_value: int, name: str) -> str: + """Execute an arm command through WebRTC interface. + + Args: + data_value: The arm action data value + name: Human-readable name of the command + """ + if self._robot is None: + return f"Error: Robot not connected (cannot execute {name})" + + try: + self._robot.connection.publish_request( + "rt/api/arm/request", {"api_id": 7106, "parameter": {"data": data_value}} + ) + message = f"G1 arm action {name} executed successfully (data={data_value})" + logger.info(message) + return message + except Exception as e: + error_msg = f"Failed to execute G1 arm action {name}: {e}" + logger.error(error_msg) + return error_msg + + def _execute_mode_command(self, data_value: int, name: str) -> str: + """Execute a movement mode command through WebRTC interface. + + Args: + data_value: The mode data value + name: Human-readable name of the command + """ + if self._robot is None: + return f"Error: Robot not connected (cannot execute {name})" + + try: + self._robot.connection.publish_request( + "rt/api/sport/request", {"api_id": 7101, "parameter": {"data": data_value}} + ) + message = f"G1 mode {name} activated successfully (data={data_value})" + logger.info(message) + return message + except Exception as e: + error_msg = f"Failed to execute G1 mode {name}: {e}" + logger.error(error_msg) + return error_msg diff --git a/dimos/robot/unitree_webrtc/unitree_go2.py b/dimos/robot/unitree_webrtc/unitree_go2.py index f6e4c1b47c..b91433ead8 100644 --- a/dimos/robot/unitree_webrtc/unitree_go2.py +++ b/dimos/robot/unitree_webrtc/unitree_go2.py @@ -1,104 +1,705 @@ -from dataclasses import dataclass -from dimos.types.path import Path -from dimos.types.vector import Vector -from typing import Union, Optional -from dimos.robot.unitree_webrtc.type.map import Map -from dimos.robot.unitree_webrtc.connection import WebRTCRobot -from dimos.robot.global_planner.planner import AstarPlanner -from dimos.utils.reactive import backpressure -from dimos.utils.reactive import getter_streaming -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills -from dimos.skills.skills import AbstractRobotSkill, AbstractSkill, SkillLibrary +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import functools +import logging import os -from go2_webrtc_driver.constants import VUI_COLOR -from dimos.robot.local_planner import VFHPurePursuitPlanner, navigate_path_local +import time +import warnings + +from dimos_lcm.sensor_msgs import CameraInfo +from dimos_lcm.std_msgs import Bool, String +from reactivex import Observable +from reactivex.disposable import CompositeDisposable + +from dimos import core +from dimos.constants import DEFAULT_CAPACITY_COLOR_IMAGE +from dimos.core import In, Module, Out, rpc +from dimos.core.global_config import GlobalConfig +from dimos.core.module_coordinator import ModuleCoordinator +from dimos.core.resource import Resource +from dimos.mapping.types import LatLon +from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Transform, Twist, Vector3 +from dimos.msgs.nav_msgs import OccupancyGrid, Path +from dimos.msgs.sensor_msgs import Image +from dimos.msgs.std_msgs import Header +from dimos.msgs.vision_msgs import Detection2DArray +from dimos.navigation.bbox_navigation import BBoxNavigationModule +from dimos.navigation.bt_navigator.navigator import BehaviorTreeNavigator, NavigatorState +from dimos.navigation.frontier_exploration import WavefrontFrontierExplorer +from dimos.navigation.global_planner import AstarPlanner +from dimos.navigation.local_planner.holonomic_local_planner import HolonomicLocalPlanner +from dimos.perception.common.utils import ( + load_camera_info, + load_camera_info_opencv, + rectify_image, +) +from dimos.perception.object_tracker_2d import ObjectTracker2D +from dimos.perception.spatial_perception import SpatialMemory +from dimos.protocol import pubsub +from dimos.protocol.pubsub.lcmpubsub import LCM +from dimos.protocol.tf import TF +from dimos.robot.foxglove_bridge import FoxgloveBridge +from dimos.robot.robot import UnitreeRobot +from dimos.robot.unitree_webrtc.connection import UnitreeWebRTCConnection +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.robot.unitree_webrtc.type.map import Map +from dimos.robot.unitree_webrtc.type.odometry import Odometry +from dimos.robot.unitree_webrtc.unitree_skills import MyUnitreeSkills +from dimos.skills.skills import AbstractRobotSkill, SkillLibrary +from dimos.types.robot_capabilities import RobotCapability +from dimos.utils.data import get_data +from dimos.utils.logging_config import setup_logger +from dimos.utils.monitoring import UtilizationModule +from dimos.utils.testing import TimedSensorReplay +from dimos.web.websocket_vis.websocket_vis_module import WebsocketVisModule + +logger = setup_logger(__file__, level=logging.INFO) + +# Suppress verbose loggers +logging.getLogger("aiortc.codecs.h264").setLevel(logging.ERROR) +logging.getLogger("lcm_foxglove_bridge").setLevel(logging.ERROR) +logging.getLogger("websockets.server").setLevel(logging.ERROR) +logging.getLogger("FoxgloveServer").setLevel(logging.ERROR) +logging.getLogger("asyncio").setLevel(logging.ERROR) +logging.getLogger("root").setLevel(logging.WARNING) + +# Suppress warnings +warnings.filterwarnings("ignore", message="coroutine.*was never awaited") +warnings.filterwarnings("ignore", message="H264Decoder.*failed to decode") + + +class ReplayRTC(Resource): + """Replay WebRTC connection for testing with recorded data.""" + + def __init__(self, *args, **kwargs) -> None: + get_data("unitree_office_walk") # Preload data for testing + + def start(self) -> None: + pass + + def stop(self) -> None: + pass + + def standup(self) -> None: + print("standup suppressed") + + def liedown(self) -> None: + print("liedown suppressed") + + @functools.cache + def lidar_stream(self): + print("lidar stream start") + lidar_store = TimedSensorReplay("unitree_office_walk/lidar", autocast=LidarMessage.from_msg) + return lidar_store.stream() + + @functools.cache + def odom_stream(self): + print("odom stream start") + odom_store = TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) + return odom_store.stream() + + @functools.cache + def video_stream(self): + print("video stream start") + video_store = TimedSensorReplay( + "unitree_office_walk/video", autocast=lambda x: Image.from_numpy(x).to_rgb() + ) + return video_store.stream() + + def move(self, twist: Twist, duration: float = 0.0) -> None: + pass + + def publish_request(self, topic: str, data: dict): + """Fake publish request for testing.""" + return {"status": "ok", "message": "Fake publish"} + + +class ConnectionModule(Module): + """Module that handles robot sensor data, movement commands, and camera information.""" + + cmd_vel: In[Twist] = None + odom: Out[PoseStamped] = None + gps_location: Out[LatLon] = None + lidar: Out[LidarMessage] = None + color_image: Out[Image] = None + camera_info: Out[CameraInfo] = None + camera_pose: Out[PoseStamped] = None + ip: str + connection_type: str = "webrtc" + + _odom: PoseStamped = None + _lidar: LidarMessage = None + _last_image: Image = None + + def __init__( + self, + ip: str | None = None, + connection_type: str | None = None, + rectify_image: bool = True, + global_config: GlobalConfig | None = None, + *args, + **kwargs, + ) -> None: + cfg = global_config or GlobalConfig() + self.ip = ip if ip is not None else cfg.robot_ip + self.connection_type = connection_type or cfg.unitree_connection_type + self.rectify_image = not cfg.use_simulation + self.tf = TF() + self.connection = None + + # Load camera parameters from YAML + base_dir = os.path.dirname(os.path.abspath(__file__)) + + # Use sim camera parameters for mujoco, real camera for others + if connection_type == "mujoco": + camera_params_path = os.path.join(base_dir, "params", "sim_camera.yaml") + else: + camera_params_path = os.path.join(base_dir, "params", "front_camera_720.yaml") + + self.lcm_camera_info = load_camera_info(camera_params_path, frame_id="camera_link") + + # Load OpenCV matrices for rectification if enabled + if rectify_image: + self.camera_matrix, self.dist_coeffs = load_camera_info_opencv(camera_params_path) + self.lcm_camera_info.D = [0.0] * len( + self.lcm_camera_info.D + ) # zero out distortion coefficients for rectification + else: + self.camera_matrix = None + self.dist_coeffs = None + + Module.__init__(self, *args, **kwargs) + + @rpc + def start(self) -> None: + """Start the connection and subscribe to sensor streams.""" + super().start() + + match self.connection_type: + case "webrtc": + self.connection = UnitreeWebRTCConnection(self.ip) + case "replay": + self.connection = ReplayRTC(self.ip) + case "mujoco": + from dimos.robot.unitree_webrtc.mujoco_connection import MujocoConnection + self.connection = MujocoConnection() + case _: + raise ValueError(f"Unknown connection type: {self.connection_type}") -class Color(VUI_COLOR): ... + self.connection.start() + # Connect sensor streams to outputs + unsub = self.connection.lidar_stream().subscribe(self.lidar.publish) + self._disposables.add(unsub) + + unsub = self.connection.odom_stream().subscribe(self._publish_tf) + self._disposables.add(unsub) + + if self.connection_type == "mujoco": + unsub = self.connection.gps_stream().subscribe(self._publish_gps_location) + self._disposables.add(unsub) + + unsub = self.connection.video_stream().subscribe(self._on_video) + self._disposables.add(unsub) + + unsub = self.cmd_vel.subscribe(self.move) + self._disposables.add(unsub) + + @rpc + def stop(self) -> None: + if self.connection: + self.connection.stop() + super().stop() + + def _on_video(self, msg: Image) -> None: + """Handle incoming video frames and publish synchronized camera data.""" + # Apply rectification if enabled + if self.rectify_image: + rectified_msg = rectify_image(msg, self.camera_matrix, self.dist_coeffs) + self._last_image = rectified_msg + self.color_image.publish(rectified_msg) + else: + self._last_image = msg + self.color_image.publish(msg) + + # Publish camera info and pose synchronized with video + timestamp = msg.ts if msg.ts else time.time() + self._publish_camera_info(timestamp) + self._publish_camera_pose(timestamp) + + def _publish_gps_location(self, msg: LatLon) -> None: + self.gps_location.publish(msg) + + def _publish_tf(self, msg) -> None: + self._odom = msg + self.odom.publish(msg) + self.tf.publish(Transform.from_pose("base_link", msg)) + camera_link = Transform( + translation=Vector3(0.3, 0.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="base_link", + child_frame_id="camera_link", + ts=time.time(), + ) + self.tf.publish(camera_link) + + def _publish_camera_info(self, timestamp: float) -> None: + header = Header(timestamp, "camera_link") + self.lcm_camera_info.header = header + self.camera_info.publish(self.lcm_camera_info) + + def _publish_camera_pose(self, timestamp: float) -> None: + """Publish camera pose from TF lookup.""" + try: + # Look up transform from world to camera_link + transform = self.tf.get( + parent_frame="world", + child_frame="camera_link", + time_point=timestamp, + time_tolerance=1.0, + ) + + if transform: + pose_msg = PoseStamped( + ts=timestamp, + frame_id="camera_link", + position=transform.translation, + orientation=transform.rotation, + ) + self.camera_pose.publish(pose_msg) + else: + logger.debug("Could not find transform from world to camera_link") + + except Exception as e: + logger.error(f"Error publishing camera pose: {e}") + + @rpc + def get_odom(self) -> PoseStamped | None: + """Get the robot's odometry. + + Returns: + The robot's odometry + """ + return self._odom + + @rpc + def move(self, twist: Twist, duration: float = 0.0) -> None: + """Send movement command to robot.""" + self.connection.move(twist, duration) + + @rpc + def standup(self): + """Make the robot stand up.""" + return self.connection.standup() + + @rpc + def liedown(self): + """Make the robot lie down.""" + return self.connection.liedown() + + @rpc + def publish_request(self, topic: str, data: dict): + """Publish a request to the WebRTC connection. + Args: + topic: The RTC topic to publish to + data: The data dictionary to publish + Returns: + The result of the publish request + """ + return self.connection.publish_request(topic, data) + + +connection = ConnectionModule.blueprint + + +class UnitreeGo2(UnitreeRobot, Resource): + """Full Unitree Go2 robot with navigation and perception capabilities.""" + + _dimos: ModuleCoordinator + _disposables: CompositeDisposable = CompositeDisposable() -class UnitreeGo2(WebRTCRobot): def __init__( self, - ip: str, - mode: str = "ai", - skills: Optional[Union[MyUnitreeSkills, AbstractSkill]] = None, - skill_library: SkillLibrary = None, - output_dir: str = os.path.join(os.getcwd(), "assets", "output"), - ): - super().__init__(ip=ip, mode=mode) - - self.odom = getter_streaming(self.odom_stream()) - self.map = Map() - self.map_stream = self.map.consume(self.lidar_stream()) - - self.global_planner = AstarPlanner( - set_local_nav=lambda path, stop_event=None, goal_theta=None: navigate_path_local( - self, path, timeout=120.0, goal_theta=goal_theta, stop_event=stop_event - ), - get_costmap=lambda: self.map.costmap, - get_robot_pos=lambda: self.odom().pos, - ) - - # # Initialize skills - # if skills is None: - # skills = MyUnitreeSkills(robot=self) - - # self.skill_library = skills if skills else SkillLibrary() - - # if self.skill_library is not None: - # for skill in self.skill_library: - # if isinstance(skill, AbstractRobotSkill): - # self.skill_library.create_instance(skill.__name__, robot=self) - # if isinstance(self.skill_library, MyUnitreeSkills): - # self.skill_library._robot = self - # self.skill_library.init() - # self.skill_library.initialize_skills() - - # # Camera stuff - # self.camera_intrinsics = [819.553492, 820.646595, 625.284099, 336.808987] - # self.camera_pitch = np.deg2rad(0) # negative for downward pitch - # self.camera_height = 0.44 # meters - - # os.makedirs(self.output_dir, exist_ok=True) - - # # Initialize visual servoing if enabled - # if self.get_video_stream() is not None: - # self.person_tracker = PersonTrackingStream( - # camera_intrinsics=self.camera_intrinsics, - # camera_pitch=self.camera_pitch, - # camera_height=self.camera_height, - # ) - # self.object_tracker = ObjectTrackingStream( - # camera_intrinsics=self.camera_intrinsics, - # camera_pitch=self.camera_pitch, - # camera_height=self.camera_height, - # ) - # person_tracking_stream = self.person_tracker.create_stream(self.get_video_stream()) - # object_tracking_stream = self.object_tracker.create_stream(self.get_video_stream()) - - # self.person_tracking_stream = person_tracking_stream - # self.object_tracking_stream = object_tracking_stream - - # Initialize the local planner and create BEV visualization stream - # self.local_planner = VFHPurePursuitPlanner( - # robot=self, - # robot_width=0.36, # Unitree Go2 width in meters - # robot_length=0.6, # Unitree Go2 length in meters - # max_linear_vel=0.5, - # lookahead_distance=0.6, - # visualization_size=500, # 500x500 pixel visualization - # ) - - # Create the visualization stream at 5Hz - # self.local_planner_viz_stream = self.local_planner.create_stream(frequency_hz=5.0) - - def move(self, vector: Vector): - super().move(vector) - - def get_skills(self) -> Optional[SkillLibrary]: - return self.skill_library + ip: str | None, + output_dir: str | None = None, + websocket_port: int = 7779, + skill_library: SkillLibrary | None = None, + connection_type: str | None = "webrtc", + ) -> None: + """Initialize the robot system. + + Args: + ip: Robot IP address (or None for replay connection) + output_dir: Directory for saving outputs (default: assets/output) + websocket_port: Port for web visualization + skill_library: Skill library instance + connection_type: webrtc, replay, or mujoco + """ + super().__init__() + self._dimos = ModuleCoordinator(n=8, memory_limit="8GiB") + self.ip = ip + self.connection_type = connection_type or "webrtc" + if ip is None and self.connection_type == "webrtc": + self.connection_type = "replay" # Auto-enable playback if no IP provided + self.output_dir = output_dir or os.path.join(os.getcwd(), "assets", "output") + self.websocket_port = websocket_port + self.lcm = LCM() + + # Initialize skill library + if skill_library is None: + skill_library = MyUnitreeSkills() + self.skill_library = skill_library + + # Set capabilities + self.capabilities = [RobotCapability.LOCOMOTION, RobotCapability.VISION] + + self.connection = None + self.mapper = None + self.global_planner = None + self.local_planner = None + self.navigator = None + self.frontier_explorer = None + self.websocket_vis = None + self.foxglove_bridge = None + self.spatial_memory_module = None + self.object_tracker = None + self.utilization_module = None + + self._setup_directories() + + def _setup_directories(self) -> None: + """Setup directories for spatial memory storage.""" + os.makedirs(self.output_dir, exist_ok=True) + logger.info(f"Robot outputs will be saved to: {self.output_dir}") + + # Initialize memory directories + self.memory_dir = os.path.join(self.output_dir, "memory") + os.makedirs(self.memory_dir, exist_ok=True) + + # Initialize spatial memory properties + self.spatial_memory_dir = os.path.join(self.memory_dir, "spatial_memory") + self.spatial_memory_collection = "spatial_memory" + self.db_path = os.path.join(self.spatial_memory_dir, "chromadb_data") + self.visual_memory_path = os.path.join(self.spatial_memory_dir, "visual_memory.pkl") + + # Create spatial memory directories + os.makedirs(self.spatial_memory_dir, exist_ok=True) + os.makedirs(self.db_path, exist_ok=True) + + def start(self) -> None: + self.lcm.start() + self._dimos.start() + + self._deploy_connection() + self._deploy_mapping() + self._deploy_navigation() + self._deploy_visualization() + self._deploy_foxglove_bridge() + self._deploy_perception() + self._deploy_camera() + + self._start_modules() + logger.info("UnitreeGo2 initialized and started") + + def stop(self) -> None: + if self.foxglove_bridge: + self.foxglove_bridge.stop() + self._disposables.dispose() + self._dimos.stop() + self.lcm.stop() + + def _deploy_connection(self) -> None: + """Deploy and configure the connection module.""" + self.connection = self._dimos.deploy( + ConnectionModule, self.ip, connection_type=self.connection_type + ) + + self.connection.lidar.transport = core.LCMTransport("/lidar", LidarMessage) + self.connection.odom.transport = core.LCMTransport("/odom", PoseStamped) + self.connection.gps_location.transport = core.pLCMTransport("/gps_location") + self.connection.color_image.transport = core.pSHMTransport( + "/go2/color_image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE + ) + self.connection.cmd_vel.transport = core.LCMTransport("/cmd_vel", Twist) + self.connection.camera_info.transport = core.LCMTransport("/go2/camera_info", CameraInfo) + self.connection.camera_pose.transport = core.LCMTransport("/go2/camera_pose", PoseStamped) + + def _deploy_mapping(self) -> None: + """Deploy and configure the mapping module.""" + min_height = 0.3 if self.connection_type == "mujoco" else 0.15 + self.mapper = self._dimos.deploy( + Map, voxel_size=0.5, global_publish_interval=2.5, min_height=min_height + ) + + self.mapper.global_map.transport = core.LCMTransport("/global_map", LidarMessage) + self.mapper.global_costmap.transport = core.LCMTransport("/global_costmap", OccupancyGrid) + self.mapper.local_costmap.transport = core.LCMTransport("/local_costmap", OccupancyGrid) + + self.mapper.lidar.connect(self.connection.lidar) + + def _deploy_navigation(self) -> None: + """Deploy and configure navigation modules.""" + self.global_planner = self._dimos.deploy(AstarPlanner) + self.local_planner = self._dimos.deploy(HolonomicLocalPlanner) + self.navigator = self._dimos.deploy( + BehaviorTreeNavigator, + reset_local_planner=self.local_planner.reset, + check_goal_reached=self.local_planner.is_goal_reached, + ) + self.frontier_explorer = self._dimos.deploy(WavefrontFrontierExplorer) + + self.navigator.target.transport = core.LCMTransport("/navigation_goal", PoseStamped) + self.navigator.goal_request.transport = core.LCMTransport("/goal_request", PoseStamped) + self.navigator.goal_reached.transport = core.LCMTransport("/goal_reached", Bool) + self.navigator.navigation_state.transport = core.LCMTransport("/navigation_state", String) + self.navigator.global_costmap.transport = core.LCMTransport( + "/global_costmap", OccupancyGrid + ) + self.global_planner.path.transport = core.LCMTransport("/global_path", Path) + self.local_planner.cmd_vel.transport = core.LCMTransport("/cmd_vel", Twist) + self.frontier_explorer.goal_request.transport = core.LCMTransport( + "/goal_request", PoseStamped + ) + self.frontier_explorer.goal_reached.transport = core.LCMTransport("/goal_reached", Bool) + self.frontier_explorer.explore_cmd.transport = core.LCMTransport("/explore_cmd", Bool) + self.frontier_explorer.stop_explore_cmd.transport = core.LCMTransport( + "/stop_explore_cmd", Bool + ) + + self.global_planner.target.connect(self.navigator.target) + + self.global_planner.global_costmap.connect(self.mapper.global_costmap) + self.global_planner.odom.connect(self.connection.odom) + + self.local_planner.path.connect(self.global_planner.path) + self.local_planner.local_costmap.connect(self.mapper.local_costmap) + self.local_planner.odom.connect(self.connection.odom) + + self.connection.cmd_vel.connect(self.local_planner.cmd_vel) + + self.navigator.odom.connect(self.connection.odom) + + self.frontier_explorer.global_costmap.connect(self.mapper.global_costmap) + self.frontier_explorer.odom.connect(self.connection.odom) + + def _deploy_visualization(self) -> None: + """Deploy and configure visualization modules.""" + self.websocket_vis = self._dimos.deploy(WebsocketVisModule, port=self.websocket_port) + self.websocket_vis.goal_request.transport = core.LCMTransport("/goal_request", PoseStamped) + self.websocket_vis.gps_goal.transport = core.pLCMTransport("/gps_goal") + self.websocket_vis.explore_cmd.transport = core.LCMTransport("/explore_cmd", Bool) + self.websocket_vis.stop_explore_cmd.transport = core.LCMTransport("/stop_explore_cmd", Bool) + self.websocket_vis.cmd_vel.transport = core.LCMTransport("/cmd_vel", Twist) + + self.websocket_vis.odom.connect(self.connection.odom) + self.websocket_vis.gps_location.connect(self.connection.gps_location) + self.websocket_vis.path.connect(self.global_planner.path) + self.websocket_vis.global_costmap.connect(self.mapper.global_costmap) + + def _deploy_foxglove_bridge(self) -> None: + self.foxglove_bridge = FoxgloveBridge( + shm_channels=[ + "/go2/color_image#sensor_msgs.Image", + "/go2/tracked_overlay#sensor_msgs.Image", + ] + ) + self.foxglove_bridge.start() + + def _deploy_perception(self) -> None: + """Deploy and configure perception modules.""" + # Deploy spatial memory + self.spatial_memory_module = self._dimos.deploy( + SpatialMemory, + collection_name=self.spatial_memory_collection, + db_path=self.db_path, + visual_memory_path=self.visual_memory_path, + output_dir=self.spatial_memory_dir, + ) + + self.spatial_memory_module.color_image.transport = core.pSHMTransport( + "/go2/color_image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE + ) + self.spatial_memory_module.odom.transport = core.LCMTransport( + "/go2/camera_pose", PoseStamped + ) + + logger.info("Spatial memory module deployed and connected") + + # Deploy 2D object tracker + self.object_tracker = self._dimos.deploy( + ObjectTracker2D, + frame_id="camera_link", + ) + + # Deploy bbox navigation module + self.bbox_navigator = self._dimos.deploy(BBoxNavigationModule, goal_distance=1.0) + + self.utilization_module = self._dimos.deploy(UtilizationModule) + + # Set up transports for object tracker + self.object_tracker.detection2darray.transport = core.LCMTransport( + "/go2/detection2d", Detection2DArray + ) + self.object_tracker.tracked_overlay.transport = core.pSHMTransport( + "/go2/tracked_overlay", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE + ) + + # Set up transports for bbox navigator + self.bbox_navigator.goal_request.transport = core.LCMTransport("/goal_request", PoseStamped) + + logger.info("Object tracker and bbox navigator modules deployed") + + def _deploy_camera(self) -> None: + """Deploy and configure the camera module.""" + # Connect object tracker inputs + if self.object_tracker: + self.object_tracker.color_image.connect(self.connection.color_image) + logger.info("Object tracker connected to camera") + + # Connect bbox navigator inputs + if self.bbox_navigator: + self.bbox_navigator.detection2d.connect(self.object_tracker.detection2darray) + self.bbox_navigator.camera_info.connect(self.connection.camera_info) + self.bbox_navigator.goal_request.connect(self.navigator.goal_request) + logger.info("BBox navigator connected") + + def _start_modules(self) -> None: + """Start all deployed modules in the correct order.""" + self._dimos.start_all_modules() + + # Initialize skills after connection is established + if self.skill_library is not None: + for skill in self.skill_library: + if isinstance(skill, AbstractRobotSkill): + self.skill_library.create_instance(skill.__name__, robot=self) + if isinstance(self.skill_library, MyUnitreeSkills): + self.skill_library._robot = self + self.skill_library.init() + self.skill_library.initialize_skills() + + def move(self, twist: Twist, duration: float = 0.0) -> None: + """Send movement command to robot.""" + self.connection.move(twist, duration) + + def explore(self) -> bool: + """Start autonomous frontier exploration. + + Returns: + True if exploration started successfully + """ + return self.frontier_explorer.explore() + + def navigate_to(self, pose: PoseStamped, blocking: bool = True) -> bool: + """Navigate to a target pose. + + Args: + pose: Target pose to navigate to + blocking: If True, block until goal is reached. If False, return immediately. + + Returns: + If blocking=True: True if navigation was successful, False otherwise + If blocking=False: True if goal was accepted, False otherwise + """ + + logger.info( + f"Navigating to pose: ({pose.position.x:.2f}, {pose.position.y:.2f}, {pose.position.z:.2f})" + ) + self.navigator.set_goal(pose) + time.sleep(1.0) + + if blocking: + while self.navigator.get_state() == NavigatorState.FOLLOWING_PATH: + time.sleep(0.25) + + time.sleep(1.0) + if not self.navigator.is_goal_reached(): + logger.info("Navigation was cancelled or failed") + return False + else: + logger.info("Navigation goal reached") + return True + + return True + + def stop_exploration(self) -> bool: + """Stop autonomous exploration. + + Returns: + True if exploration was stopped + """ + self.navigator.cancel_goal() + return self.frontier_explorer.stop_exploration() + + def is_exploration_active(self) -> bool: + return self.frontier_explorer.is_exploration_active() + + def cancel_navigation(self) -> bool: + """Cancel the current navigation goal. + + Returns: + True if goal was cancelled + """ + return self.navigator.cancel_goal() @property - def costmap(self): - return self.map.costmap + def spatial_memory(self) -> SpatialMemory | None: + """Get the robot's spatial memory module. + + Returns: + SpatialMemory module instance or None if perception is disabled + """ + return self.spatial_memory_module + + @functools.cached_property + def gps_position_stream(self) -> Observable[LatLon]: + return self.connection.gps_location.transport.pure_observable() + + def get_odom(self) -> PoseStamped: + """Get the robot's odometry. + + Returns: + The robot's odometry + """ + return self.connection.get_odom() + + +def main() -> None: + """Main entry point.""" + ip = os.getenv("ROBOT_IP") + connection_type = os.getenv("CONNECTION_TYPE", "webrtc") + + pubsub.lcm.autoconf() + + robot = UnitreeGo2(ip=ip, websocket_port=7779, connection_type=connection_type) + robot.start() + + try: + while True: + time.sleep(0.1) + except KeyboardInterrupt: + pass + finally: + robot.stop() + + +if __name__ == "__main__": + main() + + +__all__ = ["ConnectionModule", "ReplayRTC", "UnitreeGo2", "connection"] diff --git a/dimos/robot/unitree_webrtc/unitree_go2_blueprints.py b/dimos/robot/unitree_webrtc/unitree_go2_blueprints.py new file mode 100644 index 0000000000..b74756cf84 --- /dev/null +++ b/dimos/robot/unitree_webrtc/unitree_go2_blueprints.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos_lcm.sensor_msgs import CameraInfo + +from dimos.agents2.agent import llm_agent +from dimos.agents2.cli.human import human_input +from dimos.agents2.skills.navigation import navigation_skill +from dimos.constants import DEFAULT_CAPACITY_COLOR_IMAGE, DEFAULT_CAPACITY_DEPTH_IMAGE +from dimos.core.blueprints import autoconnect +from dimos.core.transport import JpegLcmTransport, JpegShmTransport, LCMTransport, pSHMTransport +from dimos.msgs.geometry_msgs import PoseStamped +from dimos.msgs.sensor_msgs import Image +from dimos.navigation.bt_navigator.navigator import ( + behavior_tree_navigator, +) +from dimos.navigation.frontier_exploration import ( + wavefront_frontier_explorer, +) +from dimos.navigation.global_planner import astar_planner +from dimos.navigation.local_planner.holonomic_local_planner import ( + holonomic_local_planner, +) +from dimos.perception.object_tracker import object_tracking +from dimos.perception.spatial_perception import spatial_memory +from dimos.robot.foxglove_bridge import foxglove_bridge +from dimos.robot.unitree_webrtc.depth_module import depth_module +from dimos.robot.unitree_webrtc.type.map import mapper +from dimos.robot.unitree_webrtc.unitree_go2 import connection +from dimos.utils.monitoring import utilization +from dimos.web.websocket_vis.websocket_vis_module import websocket_vis + +basic = ( + autoconnect( + connection(), + mapper(voxel_size=0.5, global_publish_interval=2.5), + astar_planner(), + holonomic_local_planner(), + behavior_tree_navigator(), + wavefront_frontier_explorer(), + websocket_vis(), + foxglove_bridge(), + ) + .global_config(n_dask_workers=4) + .transports( + # These are kept the same so that we don't have to change foxglove configs. + # Although we probably should. + { + ("color_image", Image): LCMTransport("/go2/color_image", Image), + ("camera_pose", PoseStamped): LCMTransport("/go2/camera_pose", PoseStamped), + ("camera_info", CameraInfo): LCMTransport("/go2/camera_info", CameraInfo), + } + ) +) + +standard = ( + autoconnect( + basic, + spatial_memory(), + object_tracking(frame_id="camera_link"), + depth_module(), + utilization(), + ) + .global_config(n_dask_workers=8) + .transports( + { + ("depth_image", Image): LCMTransport("/go2/depth_image", Image), + } + ) +) + +standard_with_shm = autoconnect( + standard.transports( + { + ("color_image", Image): pSHMTransport( + "/go2/color_image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE + ), + ("depth_image", Image): pSHMTransport( + "/go2/depth_image", default_capacity=DEFAULT_CAPACITY_DEPTH_IMAGE + ), + } + ), + foxglove_bridge( + shm_channels=[ + "/go2/color_image#sensor_msgs.Image", + "/go2/depth_image#sensor_msgs.Image", + ] + ), +) + +standard_with_jpeglcm = standard.transports( + { + ("color_image", Image): JpegLcmTransport("/go2/color_image", Image), + } +) + +standard_with_jpegshm = autoconnect( + standard.transports( + { + ("color_image", Image): JpegShmTransport("/go2/color_image", quality=75), + } + ), + foxglove_bridge( + jpeg_shm_channels=[ + "/go2/color_image#sensor_msgs.Image", + ] + ), +) + +agentic = autoconnect( + standard, + llm_agent(), + human_input(), + navigation_skill(), +) diff --git a/dimos/robot/unitree_webrtc/unitree_skill_container.py b/dimos/robot/unitree_webrtc/unitree_skill_container.py new file mode 100644 index 0000000000..e6179adcbb --- /dev/null +++ b/dimos/robot/unitree_webrtc/unitree_skill_container.py @@ -0,0 +1,189 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Unitree skill container for the new agents2 framework. +Dynamically generates skills from UNITREE_WEBRTC_CONTROLS list. +""" + +from __future__ import annotations + +import datetime +import time +from typing import TYPE_CHECKING + +from go2_webrtc_driver.constants import RTC_TOPIC + +from dimos.core import Module +from dimos.core.core import rpc +from dimos.msgs.geometry_msgs import Twist, Vector3 +from dimos.protocol.skill.skill import skill +from dimos.protocol.skill.type import Reducer, Stream +from dimos.robot.unitree_webrtc.unitree_skills import UNITREE_WEBRTC_CONTROLS +from dimos.utils.logging_config import setup_logger + +if TYPE_CHECKING: + from dimos.robot.unitree_webrtc.unitree_go2 import UnitreeGo2 + +logger = setup_logger("dimos.robot.unitree_webrtc.unitree_skill_container") + + +class UnitreeSkillContainer(Module): + """Container for Unitree Go2 robot skills using the new framework.""" + + def __init__(self, robot: UnitreeGo2 | None = None) -> None: + """Initialize the skill container with robot reference. + + Args: + robot: The UnitreeGo2 robot instance + """ + super().__init__() + self._robot = robot + + # Dynamically generate skills from UNITREE_WEBRTC_CONTROLS + self._generate_unitree_skills() + + @rpc + def start(self) -> None: + super().start() + + @rpc + def stop(self) -> None: + # TODO: Do I need to clean up dynamic skills? + super().stop() + + def _generate_unitree_skills(self) -> None: + """Dynamically generate skills from the UNITREE_WEBRTC_CONTROLS list.""" + logger.info(f"Generating {len(UNITREE_WEBRTC_CONTROLS)} dynamic Unitree skills") + + for name, api_id, description in UNITREE_WEBRTC_CONTROLS: + if name not in ["Reverse", "Spin"]: # Exclude reverse and spin as in original + # Convert CamelCase to snake_case for method name + skill_name = self._convert_to_snake_case(name) + self._create_dynamic_skill(skill_name, api_id, description, name) + + def _convert_to_snake_case(self, name: str) -> str: + """Convert CamelCase to snake_case. + + Examples: + StandUp -> stand_up + RecoveryStand -> recovery_stand + FrontFlip -> front_flip + """ + result = [] + for i, char in enumerate(name): + if i > 0 and char.isupper(): + result.append("_") + result.append(char.lower()) + return "".join(result) + + def _create_dynamic_skill( + self, skill_name: str, api_id: int, description: str, original_name: str + ) -> None: + """Create a dynamic skill method with the @skill decorator. + + Args: + skill_name: Snake_case name for the method + api_id: The API command ID + description: Human-readable description + original_name: Original CamelCase name for display + """ + + # Define the skill function + def dynamic_skill_func(self) -> str: + """Dynamic skill function.""" + return self._execute_sport_command(api_id, original_name) + + # Set the function's metadata + dynamic_skill_func.__name__ = skill_name + dynamic_skill_func.__doc__ = description + + # Apply the @skill decorator + decorated_skill = skill()(dynamic_skill_func) + + # Bind the method to the instance + bound_method = decorated_skill.__get__(self, self.__class__) + + # Add it as an attribute + setattr(self, skill_name, bound_method) + + logger.debug(f"Generated skill: {skill_name} (API ID: {api_id})") + + # ========== Explicit Skills ========== + + @skill() + def move(self, x: float, y: float = 0.0, yaw: float = 0.0, duration: float = 0.0) -> str: + """Move the robot using direct velocity commands. Determine duration required based on user distance instructions. + + Example call: + args = { "x": 0.5, "y": 0.0, "yaw": 0.0, "duration": 2.0 } + move(**args) + + Args: + x: Forward velocity (m/s) + y: Left/right velocity (m/s) + yaw: Rotational velocity (rad/s) + duration: How long to move (seconds) + """ + if self._robot is None: + return "Error: Robot not connected" + + twist = Twist(linear=Vector3(x, y, 0), angular=Vector3(0, 0, yaw)) + self._robot.move(twist, duration=duration) + return f"Started moving with velocity=({x}, {y}, {yaw}) for {duration} seconds" + + @skill() + def wait(self, seconds: float) -> str: + """Wait for a specified amount of time. + + Args: + seconds: Seconds to wait + """ + time.sleep(seconds) + return f"Wait completed with length={seconds}s" + + @skill(stream=Stream.passive, reducer=Reducer.latest) + def current_time(self): + """Provides current time implicitly, don't call this skill directly.""" + print("Starting current_time skill") + while True: + yield str(datetime.datetime.now()) + time.sleep(1) + + @skill() + def speak(self, text: str) -> str: + """Speak text out loud through the robot's speakers.""" + return f"This is being said aloud: {text}" + + # ========== Helper Methods ========== + + def _execute_sport_command(self, api_id: int, name: str) -> str: + """Execute a sport command through WebRTC interface. + + Args: + api_id: The API command ID + name: Human-readable name of the command + """ + if self._robot is None: + return f"Error: Robot not connected (cannot execute {name})" + + try: + self._robot.connection.publish_request(RTC_TOPIC["SPORT_MOD"], {"api_id": api_id}) + message = f"{name} command executed successfully (id={api_id})" + logger.info(message) + return message + except Exception as e: + error_msg = f"Failed to execute {name}: {e}" + logger.error(error_msg) + return error_msg diff --git a/dimos/robot/unitree_webrtc/unitree_skills.py b/dimos/robot/unitree_webrtc/unitree_skills.py new file mode 100644 index 0000000000..2bba4caa53 --- /dev/null +++ b/dimos/robot/unitree_webrtc/unitree_skills.py @@ -0,0 +1,357 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import time +from typing import TYPE_CHECKING + +from pydantic import Field + +if TYPE_CHECKING: + from dimos.robot.robot import MockRobot, Robot +else: + Robot = "Robot" + MockRobot = "MockRobot" + +from go2_webrtc_driver.constants import RTC_TOPIC + +from dimos.msgs.geometry_msgs import Twist, Vector3 +from dimos.skills.skills import AbstractRobotSkill, AbstractSkill, SkillLibrary +from dimos.types.constants import Colors + +# Module-level constant for Unitree Go2 WebRTC control definitions +UNITREE_WEBRTC_CONTROLS: list[tuple[str, int, str]] = [ + ("Damp", 1001, "Lowers the robot to the ground fully."), + ( + "BalanceStand", + 1002, + "Activates a mode that maintains the robot in a balanced standing position.", + ), + ( + "StandUp", + 1004, + "Commands the robot to transition from a sitting or prone position to a standing posture.", + ), + ( + "StandDown", + 1005, + "Instructs the robot to move from a standing position to a sitting or prone posture.", + ), + ( + "RecoveryStand", + 1006, + "Recovers the robot to a state from which it can take more commands. Useful to run after multiple dynamic commands like front flips, Must run after skills like sit and jump and standup.", + ), + ("Sit", 1009, "Commands the robot to sit down from a standing or moving stance."), + ( + "RiseSit", + 1010, + "Commands the robot to rise back to a standing position from a sitting posture.", + ), + ( + "SwitchGait", + 1011, + "Switches the robot's walking pattern or style dynamically, suitable for different terrains or speeds.", + ), + ("Trigger", 1012, "Triggers a specific action or custom routine programmed into the robot."), + ( + "BodyHeight", + 1013, + "Adjusts the height of the robot's body from the ground, useful for navigating various obstacles.", + ), + ( + "FootRaiseHeight", + 1014, + "Controls how high the robot lifts its feet during movement, which can be adjusted for different surfaces.", + ), + ( + "SpeedLevel", + 1015, + "Sets or adjusts the speed at which the robot moves, with various levels available for different operational needs.", + ), + ( + "Hello", + 1016, + "Performs a greeting action, which could involve a wave or other friendly gesture.", + ), + ("Stretch", 1017, "Engages the robot in a stretching routine."), + ( + "TrajectoryFollow", + 1018, + "Directs the robot to follow a predefined trajectory, which could involve complex paths or maneuvers.", + ), + ( + "ContinuousGait", + 1019, + "Enables a mode for continuous walking or running, ideal for long-distance travel.", + ), + ("Content", 1020, "To display or trigger when the robot is happy."), + ("Wallow", 1021, "The robot falls onto its back and rolls around."), + ( + "Dance1", + 1022, + "Performs a predefined dance routine 1, programmed for entertainment or demonstration.", + ), + ("Dance2", 1023, "Performs another variant of a predefined dance routine 2."), + ("GetBodyHeight", 1024, "Retrieves the current height of the robot's body from the ground."), + ( + "GetFootRaiseHeight", + 1025, + "Retrieves the current height at which the robot's feet are being raised during movement.", + ), + ( + "GetSpeedLevel", + 1026, + "Retrieves the current speed level setting of the robot.", + ), + ( + "SwitchJoystick", + 1027, + "Switches the robot's control mode to respond to joystick input for manual operation.", + ), + ( + "Pose", + 1028, + "Commands the robot to assume a specific pose or posture as predefined in its programming.", + ), + ("Scrape", 1029, "The robot performs a scraping motion."), + ( + "FrontFlip", + 1030, + "Commands the robot to perform a front flip, showcasing its agility and dynamic movement capabilities.", + ), + ( + "FrontJump", + 1031, + "Instructs the robot to jump forward, demonstrating its explosive movement capabilities.", + ), + ( + "FrontPounce", + 1032, + "Commands the robot to perform a pouncing motion forward.", + ), + ( + "WiggleHips", + 1033, + "The robot performs a hip wiggling motion, often used for entertainment or demonstration purposes.", + ), + ( + "GetState", + 1034, + "Retrieves the current operational state of the robot, including its mode, position, and status.", + ), + ( + "EconomicGait", + 1035, + "Engages a more energy-efficient walking or running mode to conserve battery life.", + ), + ("FingerHeart", 1036, "Performs a finger heart gesture while on its hind legs."), + ( + "Handstand", + 1301, + "Commands the robot to perform a handstand, demonstrating balance and control.", + ), + ( + "CrossStep", + 1302, + "Commands the robot to perform cross-step movements.", + ), + ( + "OnesidedStep", + 1303, + "Commands the robot to perform one-sided step movements.", + ), + ("Bound", 1304, "Commands the robot to perform bounding movements."), + ("MoonWalk", 1305, "Commands the robot to perform a moonwalk motion."), + ("LeftFlip", 1042, "Executes a flip towards the left side."), + ("RightFlip", 1043, "Performs a flip towards the right side."), + ("Backflip", 1044, "Executes a backflip, a complex and dynamic maneuver."), +] + +# Module-level constants for Unitree G1 WebRTC control definitions +# G1 Arm Actions - all use api_id 7106 on topic "rt/api/arm/request" +G1_ARM_CONTROLS: list[tuple[str, int, str]] = [ + ("Handshake", 27, "Perform a handshake gesture with the right hand."), + ("HighFive", 18, "Give a high five with the right hand."), + ("Hug", 19, "Perform a hugging gesture with both arms."), + ("HighWave", 26, "Wave with the hand raised high."), + ("Clap", 17, "Clap hands together."), + ("FaceWave", 25, "Wave near the face level."), + ("LeftKiss", 12, "Blow a kiss with the left hand."), + ("ArmHeart", 20, "Make a heart shape with both arms overhead."), + ("RightHeart", 21, "Make a heart gesture with the right hand."), + ("HandsUp", 15, "Raise both hands up in the air."), + ("XRay", 24, "Hold arms in an X-ray pose position."), + ("RightHandUp", 23, "Raise only the right hand up."), + ("Reject", 22, "Make a rejection or 'no' gesture."), + ("CancelAction", 99, "Cancel any current arm action and return hands to neutral position."), +] + +# G1 Movement Modes - all use api_id 7101 on topic "rt/api/sport/request" +G1_MODE_CONTROLS: list[tuple[str, int, str]] = [ + ("WalkMode", 500, "Switch to normal walking mode."), + ("WalkControlWaist", 501, "Switch to walking mode with waist control."), + ("RunMode", 801, "Switch to running mode."), +] + +# region MyUnitreeSkills + + +class MyUnitreeSkills(SkillLibrary): + """My Unitree Skills for WebRTC interface.""" + + def __init__(self, robot: Robot | None = None, robot_type: str = "go2") -> None: + """Initialize Unitree skills library. + + Args: + robot: Optional robot instance + robot_type: Type of robot ("go2" or "g1"), defaults to "go2" + """ + super().__init__() + self._robot: Robot = None + self.robot_type = robot_type.lower() + + if self.robot_type not in ["go2", "g1"]: + raise ValueError(f"Unsupported robot type: {robot_type}. Must be 'go2' or 'g1'") + + # Add dynamic skills to this class based on robot type + dynamic_skills = self.create_skills_live() + self.register_skills(dynamic_skills) + + @classmethod + def register_skills(cls, skill_classes: AbstractSkill | list[AbstractSkill]) -> None: + """Add multiple skill classes as class attributes. + + Args: + skill_classes: List of skill classes to add + """ + if not isinstance(skill_classes, list): + skill_classes = [skill_classes] + + for skill_class in skill_classes: + # Add to the class as a skill + setattr(cls, skill_class.__name__, skill_class) + + def initialize_skills(self) -> None: + for skill_class in self.get_class_skills(): + self.create_instance(skill_class.__name__, robot=self._robot) + + # Refresh the class skills + self.refresh_class_skills() + + def create_skills_live(self) -> list[AbstractRobotSkill]: + # ================================================ + # Procedurally created skills + # ================================================ + class BaseUnitreeSkill(AbstractRobotSkill): + """Base skill for dynamic skill creation.""" + + def __call__(self) -> str: + super().__call__() + + # For Go2: Simple api_id based call + if hasattr(self, "_app_id"): + string = f"{Colors.GREEN_PRINT_COLOR}Executing Go2 skill: {self.__class__.__name__} with api_id={self._app_id}{Colors.RESET_COLOR}" + print(string) + self._robot.connection.publish_request( + RTC_TOPIC["SPORT_MOD"], {"api_id": self._app_id} + ) + return f"{self.__class__.__name__} executed successfully" + + # For G1: Fixed api_id with parameter data + elif hasattr(self, "_data_value"): + string = f"{Colors.GREEN_PRINT_COLOR}Executing G1 skill: {self.__class__.__name__} with data={self._data_value}{Colors.RESET_COLOR}" + print(string) + self._robot.connection.publish_request( + self._topic, + {"api_id": self._api_id, "parameter": {"data": self._data_value}}, + ) + return f"{self.__class__.__name__} executed successfully" + else: + raise RuntimeError( + f"Skill {self.__class__.__name__} missing required attributes" + ) + + skills_classes = [] + + if self.robot_type == "g1": + # Create G1 arm skills + for name, data_value, description in G1_ARM_CONTROLS: + skill_class = type( + name, + (BaseUnitreeSkill,), + { + "__doc__": description, + "_topic": "rt/api/arm/request", + "_api_id": 7106, + "_data_value": data_value, + }, + ) + skills_classes.append(skill_class) + + # Create G1 mode skills + for name, data_value, description in G1_MODE_CONTROLS: + skill_class = type( + name, + (BaseUnitreeSkill,), + { + "__doc__": description, + "_topic": "rt/api/sport/request", + "_api_id": 7101, + "_data_value": data_value, + }, + ) + skills_classes.append(skill_class) + else: + # Go2 skills (existing code) + for name, app_id, description in UNITREE_WEBRTC_CONTROLS: + if name not in ["Reverse", "Spin"]: # Exclude reverse and spin skills + skill_class = type( + name, (BaseUnitreeSkill,), {"__doc__": description, "_app_id": app_id} + ) + skills_classes.append(skill_class) + + return skills_classes + + # region Class-based Skills + + class Move(AbstractRobotSkill): + """Move the robot using direct velocity commands. Determine duration required based on user distance instructions.""" + + x: float = Field(..., description="Forward velocity (m/s).") + y: float = Field(default=0.0, description="Left/right velocity (m/s)") + yaw: float = Field(default=0.0, description="Rotational velocity (rad/s)") + duration: float = Field(default=0.0, description="How long to move (seconds).") + + def __call__(self) -> str: + self._robot.move( + Twist(linear=Vector3(self.x, self.y, 0.0), angular=Vector3(0.0, 0.0, self.yaw)), + duration=self.duration, + ) + return f"started moving with velocity={self.x}, {self.y}, {self.yaw} for {self.duration} seconds" + + class Wait(AbstractSkill): + """Wait for a specified amount of time.""" + + seconds: float = Field(..., description="Seconds to wait") + + def __call__(self) -> str: + time.sleep(self.seconds) + return f"Wait completed with length={self.seconds}s" + + # endregion + + +# endregion diff --git a/dimos/robot/utils/README.md b/dimos/robot/utils/README.md new file mode 100644 index 0000000000..5a84b20c4a --- /dev/null +++ b/dimos/robot/utils/README.md @@ -0,0 +1,38 @@ +# Robot Utils + +## RobotDebugger + +The `RobotDebugger` provides a way to debug a running robot through the python shell. + +Requirements: + +```bash +pip install rpyc +``` + +### Usage + +1. **Add to your robot application:** + ```python + from dimos.robot.utils.robot_debugger import RobotDebugger + + # In your robot application's context manager or main loop: + with RobotDebugger(robot): + # Your robot code here + pass + + # Or better, with an exit stack. + exit_stack.enter_context(RobotDebugger(robot)) + ``` + +2. **Start your robot with debugging enabled:** + ```bash + ROBOT_DEBUGGER=true python your_robot_script.py + ``` + +3. **Open the python shell:** + ```bash + ./bin/robot-debugger + >>> robot.explore() + True + ``` diff --git a/dimos/robot/utils/robot_debugger.py b/dimos/robot/utils/robot_debugger.py new file mode 100644 index 0000000000..b3cfb195ce --- /dev/null +++ b/dimos/robot/utils/robot_debugger.py @@ -0,0 +1,59 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +from dimos.core.resource import Resource +from dimos.utils.logging_config import setup_logger + +logger = setup_logger(__file__) + + +class RobotDebugger(Resource): + def __init__(self, robot) -> None: + self._robot = robot + self._threaded_server = None + + def start(self) -> None: + if not os.getenv("ROBOT_DEBUGGER"): + return + + try: + import rpyc + from rpyc.utils.server import ThreadedServer + except ImportError: + return + + logger.info( + "Starting the robot debugger. You can open a python shell with `./bin/robot-debugger`" + ) + + robot = self._robot + + class RobotService(rpyc.Service): + def exposed_robot(self): + return robot + + self._threaded_server = ThreadedServer( + RobotService, + port=18861, + protocol_config={ + "allow_all_attrs": True, + }, + ) + self._threaded_server.start() + + def stop(self) -> None: + if self._threaded_server: + self._threaded_server.close() diff --git a/dimos/simulation/__init__.py b/dimos/simulation/__init__.py index 7b35329862..2b77f47097 100644 --- a/dimos/simulation/__init__.py +++ b/dimos/simulation/__init__.py @@ -12,9 +12,4 @@ GenesisSimulator = None # type: ignore GenesisStream = None # type: ignore -__all__ = [ - 'IsaacSimulator', - 'IsaacStream', - 'GenesisSimulator', - 'GenesisStream' -] \ No newline at end of file +__all__ = ["GenesisSimulator", "GenesisStream", "IsaacSimulator", "IsaacStream"] diff --git a/dimos/simulation/base/simulator_base.py b/dimos/simulation/base/simulator_base.py index 8fd048b026..777893d74c 100644 --- a/dimos/simulation/base/simulator_base.py +++ b/dimos/simulation/base/simulator_base.py @@ -1,19 +1,32 @@ -from typing import Optional, Union, List, Dict +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from abc import ABC, abstractmethod -from pathlib import Path + class SimulatorBase(ABC): """Base class for simulators.""" - + @abstractmethod def __init__( - self, + self, headless: bool = True, - open_usd: Optional[str] = None, # Keep for Isaac compatibility - entities: Optional[List[Dict[str, Union[str, dict]]]] = None # Add for Genesis - ): + open_usd: str | None = None, # Keep for Isaac compatibility + entities: list[dict[str, str | dict]] | None = None, # Add for Genesis + ) -> None: """Initialize the simulator. - + Args: headless: Whether to run without visualization open_usd: Path to USD file (for Isaac) @@ -22,13 +35,13 @@ def __init__( self.headless = headless self.open_usd = open_usd self.stage = None - + @abstractmethod def get_stage(self): """Get the current stage/scene.""" pass - + @abstractmethod def close(self): """Close the simulation.""" - pass \ No newline at end of file + pass diff --git a/dimos/simulation/base/stream_base.py b/dimos/simulation/base/stream_base.py index 6308207419..1fb0e86add 100644 --- a/dimos/simulation/base/stream_base.py +++ b/dimos/simulation/base/stream_base.py @@ -1,14 +1,29 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from abc import ABC, abstractmethod -from typing import Literal, Optional, Union from pathlib import Path import subprocess +from typing import Literal + +AnnotatorType = Literal["rgb", "normals", "bounding_box_3d", "motion_vectors"] +TransportType = Literal["tcp", "udp"] -AnnotatorType = Literal['rgb', 'normals', 'bounding_box_3d', 'motion_vectors'] -TransportType = Literal['tcp', 'udp'] class StreamBase(ABC): """Base class for simulation streaming.""" - + @abstractmethod def __init__( self, @@ -17,13 +32,13 @@ def __init__( height: int = 1080, fps: int = 60, camera_path: str = "/World/camera", - annotator_type: AnnotatorType = 'rgb', - transport: TransportType = 'tcp', + annotator_type: AnnotatorType = "rgb", + transport: TransportType = "tcp", rtsp_url: str = "rtsp://mediamtx:8554/stream", - usd_path: Optional[Union[str, Path]] = None - ): + usd_path: str | Path | None = None, + ) -> None: """Initialize the stream. - + Args: simulator: Simulator instance width: Stream width in pixels @@ -44,48 +59,58 @@ def __init__( self.transport = transport self.rtsp_url = rtsp_url self.proc = None - + @abstractmethod - def _load_stage(self, usd_path: Union[str, Path]): + def _load_stage(self, usd_path: str | Path): """Load stage from file.""" pass - + @abstractmethod def _setup_camera(self): """Setup and validate camera.""" pass - - def _setup_ffmpeg(self): + + def _setup_ffmpeg(self) -> None: """Setup FFmpeg process for streaming.""" command = [ - 'ffmpeg', - '-y', - '-f', 'rawvideo', - '-vcodec', 'rawvideo', - '-pix_fmt', 'bgr24', - '-s', f"{self.width}x{self.height}", - '-r', str(self.fps), - '-i', '-', - '-an', - '-c:v', 'h264_nvenc', - '-preset', 'fast', - '-f', 'rtsp', - '-rtsp_transport', self.transport, - self.rtsp_url + "ffmpeg", + "-y", + "-f", + "rawvideo", + "-vcodec", + "rawvideo", + "-pix_fmt", + "bgr24", + "-s", + f"{self.width}x{self.height}", + "-r", + str(self.fps), + "-i", + "-", + "-an", + "-c:v", + "h264_nvenc", + "-preset", + "fast", + "-f", + "rtsp", + "-rtsp_transport", + self.transport, + self.rtsp_url, ] self.proc = subprocess.Popen(command, stdin=subprocess.PIPE) - + @abstractmethod def _setup_annotator(self): """Setup annotator.""" pass - + @abstractmethod def stream(self): """Start streaming.""" pass - + @abstractmethod def cleanup(self): """Cleanup resources.""" - pass \ No newline at end of file + pass diff --git a/dimos/simulation/genesis/__init__.py b/dimos/simulation/genesis/__init__.py index f1d88ebc9f..5657d9167b 100644 --- a/dimos/simulation/genesis/__init__.py +++ b/dimos/simulation/genesis/__init__.py @@ -1,7 +1,4 @@ from .simulator import GenesisSimulator from .stream import GenesisStream -__all__ = [ - 'GenesisSimulator', - 'GenesisStream' -] \ No newline at end of file +__all__ = ["GenesisSimulator", "GenesisStream"] diff --git a/dimos/simulation/genesis/simulator.py b/dimos/simulation/genesis/simulator.py index 7982a9f810..f3a73be08b 100644 --- a/dimos/simulation/genesis/simulator.py +++ b/dimos/simulation/genesis/simulator.py @@ -1,19 +1,34 @@ -from typing import Optional, Union, List, Dict +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + import genesis as gs # type: ignore -from pathlib import Path + from ..base.simulator_base import SimulatorBase + class GenesisSimulator(SimulatorBase): """Genesis simulator implementation.""" - + def __init__( - self, + self, headless: bool = True, - open_usd: Optional[str] = None, # Keep for compatibility - entities: Optional[List[Dict[str, Union[str, dict]]]] = None - ): + open_usd: str | None = None, # Keep for compatibility + entities: list[dict[str, str | dict]] | None = None, + ) -> None: """Initialize the Genesis simulation. - + Args: headless: Whether to run without visualization open_usd: Path to USD file (for Isaac) @@ -23,10 +38,10 @@ def __init__( - params: dict (parameters for primitives or loading options) """ super().__init__(headless, open_usd, entities) - + # Initialize Genesis gs.init() - + # Create scene with viewer options self.scene = gs.Scene( show_viewer=not headless, @@ -47,98 +62,98 @@ def __init__( ), renderer=gs.renderers.Rasterizer(), ) - + # Handle USD parameter for compatibility if open_usd: print(f"[Warning] USD files not supported in Genesis. Ignoring: {open_usd}") - + # Load entities if provided if entities: self._load_entities(entities) - + # Don't build scene yet - let stream add camera first self.is_built = False - - def _load_entities(self, entities: List[Dict[str, Union[str, dict]]]): + + def _load_entities(self, entities: list[dict[str, str | dict]]): """Load multiple entities into the scene.""" for entity in entities: - entity_type = entity.get('type', '').lower() - path = entity.get('path', '') - params = entity.get('params', {}) - + entity_type = entity.get("type", "").lower() + path = entity.get("path", "") + params = entity.get("params", {}) + try: - if entity_type == 'mesh': + if entity_type == "mesh": mesh = gs.morphs.Mesh( file=path, # Explicit file argument - **params + **params, ) self.scene.add_entity(mesh) print(f"[Genesis] Added mesh from {path}") - - elif entity_type == 'urdf': + + elif entity_type == "urdf": robot = gs.morphs.URDF( file=path, # Explicit file argument - **params + **params, ) self.scene.add_entity(robot) print(f"[Genesis] Added URDF robot from {path}") - - elif entity_type == 'mjcf': + + elif entity_type == "mjcf": mujoco = gs.morphs.MJCF( file=path, # Explicit file argument - **params + **params, ) self.scene.add_entity(mujoco) print(f"[Genesis] Added MJCF model from {path}") - - elif entity_type == 'primitive': - shape_type = params.pop('shape', 'plane') - if shape_type == 'plane': + + elif entity_type == "primitive": + shape_type = params.pop("shape", "plane") + if shape_type == "plane": morph = gs.morphs.Plane(**params) - elif shape_type == 'box': + elif shape_type == "box": morph = gs.morphs.Box(**params) - elif shape_type == 'sphere': + elif shape_type == "sphere": morph = gs.morphs.Sphere(**params) else: raise ValueError(f"Unsupported primitive shape: {shape_type}") - + # Add position if not specified - if 'pos' not in params: - if shape_type == 'plane': + if "pos" not in params: + if shape_type == "plane": morph.pos = [0, 0, 0] else: morph.pos = [0, 0, 1] # Lift objects above ground - + self.scene.add_entity(morph) print(f"[Genesis] Added {shape_type} at position {morph.pos}") - + else: raise ValueError(f"Unsupported entity type: {entity_type}") - + except Exception as e: - print(f"[Warning] Failed to load entity {entity}: {str(e)}") - - def add_entity(self, entity_type: str, path: str = '', **params): + print(f"[Warning] Failed to load entity {entity}: {e!s}") + + def add_entity(self, entity_type: str, path: str = "", **params) -> None: """Add a single entity to the scene. - + Args: entity_type: Type of entity ('mesh', 'urdf', 'mjcf', 'primitive') path: File path for mesh/urdf/mjcf entities **params: Additional parameters for entity creation """ - self._load_entities([{'type': entity_type, 'path': path, 'params': params}]) - + self._load_entities([{"type": entity_type, "path": path, "params": params}]) + def get_stage(self): """Get the current stage/scene.""" return self.scene - - def build(self): + + def build(self) -> None: """Build the scene if not already built.""" if not self.is_built: self.scene.build() self.is_built = True - - def close(self): + + def close(self) -> None: """Close the simulation.""" # Genesis handles cleanup automatically - pass \ No newline at end of file + pass diff --git a/dimos/simulation/genesis/stream.py b/dimos/simulation/genesis/stream.py index 7ff6b44f7f..d24b254b38 100644 --- a/dimos/simulation/genesis/stream.py +++ b/dimos/simulation/genesis/stream.py @@ -1,14 +1,29 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path +import time + import cv2 import numpy as np -import time -import subprocess -from typing import Literal, Optional, Union -from pathlib import Path -from ..base.stream_base import StreamBase, AnnotatorType, TransportType + +from ..base.stream_base import AnnotatorType, StreamBase, TransportType + class GenesisStream(StreamBase): """Genesis stream implementation.""" - + def __init__( self, simulator, @@ -16,11 +31,11 @@ def __init__( height: int = 1080, fps: int = 60, camera_path: str = "/camera", - annotator_type: AnnotatorType = 'rgb', - transport: TransportType = 'tcp', + annotator_type: AnnotatorType = "rgb", + transport: TransportType = "tcp", rtsp_url: str = "rtsp://mediamtx:8554/stream", - usd_path: Optional[Union[str, Path]] = None - ): + usd_path: str | Path | None = None, + ) -> None: """Initialize the Genesis stream.""" super().__init__( simulator=simulator, @@ -31,27 +46,27 @@ def __init__( annotator_type=annotator_type, transport=transport, rtsp_url=rtsp_url, - usd_path=usd_path + usd_path=usd_path, ) - + self.scene = simulator.get_stage() - + # Initialize components if usd_path: self._load_stage(usd_path) self._setup_camera() self._setup_ffmpeg() self._setup_annotator() - + # Build scene after camera is set up simulator.build() - - def _load_stage(self, usd_path: Union[str, Path]): + + def _load_stage(self, usd_path: str | Path) -> None: """Load stage from file.""" # Genesis handles stage loading through simulator pass - - def _setup_camera(self): + + def _setup_camera(self) -> None: """Setup and validate camera.""" self.camera = self.scene.add_camera( res=(self.width, self.height), @@ -60,63 +75,65 @@ def _setup_camera(self): fov=30, GUI=False, ) - - def _setup_annotator(self): + + def _setup_annotator(self) -> None: """Setup the specified annotator.""" # Genesis handles different render types through camera.render() pass - - def stream(self): + + def stream(self) -> None: """Start the streaming loop.""" try: print("[Stream] Starting Genesis camera stream...") frame_count = 0 start_time = time.time() - + while True: frame_start = time.time() - + # Step simulation and get frame step_start = time.time() self.scene.step() step_time = time.time() - step_start - print(f"[Stream] Simulation step took {step_time*1000:.2f}ms") + print(f"[Stream] Simulation step took {step_time * 1000:.2f}ms") # Get frame based on annotator type - if self.annotator_type == 'rgb': + if self.annotator_type == "rgb": frame, _, _, _ = self.camera.render(rgb=True) - elif self.annotator_type == 'normals': + elif self.annotator_type == "normals": _, _, _, frame = self.camera.render(normal=True) else: frame, _, _, _ = self.camera.render(rgb=True) # Default to RGB - + # Convert frame format if needed if isinstance(frame, np.ndarray): frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) - + # Write to FFmpeg self.proc.stdin.write(frame.tobytes()) self.proc.stdin.flush() - + # Log metrics frame_time = time.time() - frame_start - print(f"[Stream] Total frame processing took {frame_time*1000:.2f}ms") + print(f"[Stream] Total frame processing took {frame_time * 1000:.2f}ms") frame_count += 1 - + if frame_count % 100 == 0: elapsed_time = time.time() - start_time current_fps = frame_count / elapsed_time - print(f"[Stream] Processed {frame_count} frames | Current FPS: {current_fps:.2f}") - + print( + f"[Stream] Processed {frame_count} frames | Current FPS: {current_fps:.2f}" + ) + except KeyboardInterrupt: print("\n[Stream] Received keyboard interrupt, stopping stream...") finally: self.cleanup() - - def cleanup(self): + + def cleanup(self) -> None: """Cleanup resources.""" print("[Cleanup] Stopping FFmpeg process...") - if hasattr(self, 'proc'): + if hasattr(self, "proc"): self.proc.stdin.close() self.proc.wait() print("[Cleanup] Closing simulation...") @@ -124,4 +141,4 @@ def cleanup(self): self.simulator.close() except AttributeError: print("[Cleanup] Warning: Could not close simulator properly") - print("[Cleanup] Successfully cleaned up resources") \ No newline at end of file + print("[Cleanup] Successfully cleaned up resources") diff --git a/dimos/simulation/isaac/__init__.py b/dimos/simulation/isaac/__init__.py index 85e42aa9c1..2b9bdc082d 100644 --- a/dimos/simulation/isaac/__init__.py +++ b/dimos/simulation/isaac/__init__.py @@ -1,7 +1,4 @@ from .simulator import IsaacSimulator from .stream import IsaacStream -__all__ = [ - 'IsaacSimulator', - 'IsaacStream' -] \ No newline at end of file +__all__ = ["IsaacSimulator", "IsaacStream"] diff --git a/dimos/simulation/isaac/simulator.py b/dimos/simulation/isaac/simulator.py index 050c12c80b..0d49b9145e 100644 --- a/dimos/simulation/isaac/simulator.py +++ b/dimos/simulation/isaac/simulator.py @@ -1,30 +1,44 @@ -from typing import Optional, List, Dict, Union +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + from isaacsim import SimulationApp + from ..base.simulator_base import SimulatorBase + class IsaacSimulator(SimulatorBase): """Isaac Sim simulator implementation.""" - + def __init__( - self, - headless: bool = True, - open_usd: Optional[str] = None, - entities: Optional[List[Dict[str, Union[str, dict]]]] = None # Add but ignore -): + self, + headless: bool = True, + open_usd: str | None = None, + entities: list[dict[str, str | dict]] | None = None, # Add but ignore + ) -> None: """Initialize the Isaac Sim simulation.""" super().__init__(headless, open_usd) - self.app = SimulationApp({ - "headless": headless, - "open_usd": open_usd - }) - + self.app = SimulationApp({"headless": headless, "open_usd": open_usd}) + def get_stage(self): """Get the current USD stage.""" import omni.usd + self.stage = omni.usd.get_context().get_stage() return self.stage - - def close(self): + + def close(self) -> None: """Close the simulation.""" - if hasattr(self, 'app'): - self.app.close() \ No newline at end of file + if hasattr(self, "app"): + self.app.close() diff --git a/dimos/simulation/isaac/stream.py b/dimos/simulation/isaac/stream.py index b535b35608..eb85ba8815 100644 --- a/dimos/simulation/isaac/stream.py +++ b/dimos/simulation/isaac/stream.py @@ -12,17 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -from isaacsim import SimulationApp -import cv2 -import numpy as np -import time -from typing import Literal, Optional, Union from pathlib import Path -from ..base.stream_base import StreamBase, AnnotatorType, TransportType +import time + +import cv2 + +from ..base.stream_base import AnnotatorType, StreamBase, TransportType + class IsaacStream(StreamBase): """Isaac Sim stream implementation.""" - + def __init__( self, simulator, @@ -30,11 +30,11 @@ def __init__( height: int = 1080, fps: int = 60, camera_path: str = "/World/alfred_parent_prim/alfred_base_descr/chest_cam_rgb_camera_frame/chest_cam", - annotator_type: AnnotatorType = 'rgb', - transport: TransportType = 'tcp', + annotator_type: AnnotatorType = "rgb", + transport: TransportType = "tcp", rtsp_url: str = "rtsp://mediamtx:8554/stream", - usd_path: Optional[Union[str, Path]] = None - ): + usd_path: str | Path | None = None, + ) -> None: """Initialize the Isaac Sim stream.""" super().__init__( simulator=simulator, @@ -45,90 +45,93 @@ def __init__( annotator_type=annotator_type, transport=transport, rtsp_url=rtsp_url, - usd_path=usd_path + usd_path=usd_path, ) - + # Import omni.replicator after SimulationApp initialization import omni.replicator.core as rep + self.rep = rep - + # Initialize components if usd_path: self._load_stage(usd_path) self._setup_camera() self._setup_ffmpeg() self._setup_annotator() - - def _load_stage(self, usd_path: Union[str, Path]): + + def _load_stage(self, usd_path: str | Path): """Load USD stage from file.""" import omni.usd + abs_path = str(Path(usd_path).resolve()) omni.usd.get_context().open_stage(abs_path) self.stage = self.simulator.get_stage() if not self.stage: raise RuntimeError(f"Failed to load stage: {abs_path}") - + def _setup_camera(self): """Setup and validate camera.""" self.stage = self.simulator.get_stage() camera_prim = self.stage.GetPrimAtPath(self.camera_path) if not camera_prim: raise RuntimeError(f"Failed to find camera at path: {self.camera_path}") - + self.render_product = self.rep.create.render_product( - self.camera_path, - resolution=(self.width, self.height) + self.camera_path, resolution=(self.width, self.height) ) - - def _setup_annotator(self): + + def _setup_annotator(self) -> None: """Setup the specified annotator.""" self.annotator = self.rep.AnnotatorRegistry.get_annotator(self.annotator_type) self.annotator.attach(self.render_product) - - def stream(self): + + def stream(self) -> None: """Start the streaming loop.""" try: print("[Stream] Starting camera stream loop...") frame_count = 0 start_time = time.time() - + while True: frame_start = time.time() - + # Step simulation and get frame step_start = time.time() self.rep.orchestrator.step() step_time = time.time() - step_start - print(f"[Stream] Simulation step took {step_time*1000:.2f}ms") - + print(f"[Stream] Simulation step took {step_time * 1000:.2f}ms") + frame = self.annotator.get_data() frame = cv2.cvtColor(frame, cv2.COLOR_RGBA2BGR) - + # Write to FFmpeg self.proc.stdin.write(frame.tobytes()) self.proc.stdin.flush() - + # Log metrics frame_time = time.time() - frame_start - print(f"[Stream] Total frame processing took {frame_time*1000:.2f}ms") + print(f"[Stream] Total frame processing took {frame_time * 1000:.2f}ms") frame_count += 1 - + if frame_count % 100 == 0: elapsed_time = time.time() - start_time current_fps = frame_count / elapsed_time - print(f"[Stream] Processed {frame_count} frames | Current FPS: {current_fps:.2f}") - + print( + f"[Stream] Processed {frame_count} frames | Current FPS: {current_fps:.2f}" + ) + except KeyboardInterrupt: print("\n[Stream] Received keyboard interrupt, stopping stream...") finally: self.cleanup() - - def cleanup(self): + + def cleanup(self) -> None: """Cleanup resources.""" print("[Cleanup] Stopping FFmpeg process...") - if hasattr(self, 'proc'): + if hasattr(self, "proc"): self.proc.stdin.close() self.proc.wait() print("[Cleanup] Closing simulation...") self.simulator.close() - print("[Cleanup] Successfully cleaned up resources") \ No newline at end of file + print("[Cleanup] Successfully cleaned up resources") diff --git a/dimos/simulation/mujoco/depth_camera.py b/dimos/simulation/mujoco/depth_camera.py new file mode 100644 index 0000000000..bb7cc34047 --- /dev/null +++ b/dimos/simulation/mujoco/depth_camera.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math + +import numpy as np +import open3d as o3d + +MAX_RANGE = 3 +MIN_RANGE = 0.2 +MAX_HEIGHT = 1.2 + + +def depth_image_to_point_cloud( + depth_image: np.ndarray, + camera_pos: np.ndarray, + camera_mat: np.ndarray, + fov_degrees: float = 120, +) -> np.ndarray: + """ + Convert a depth image from a camera to a 3D point cloud using perspective projection. + + Args: + depth_image: 2D numpy array of depth values in meters + camera_pos: 3D position of camera in world coordinates + camera_mat: 3x3 camera rotation matrix in world coordinates + fov_degrees: Vertical field of view of the camera in degrees + min_range: Minimum distance from camera to include points (meters) + + Returns: + numpy array of 3D points in world coordinates, shape (N, 3) + """ + height, width = depth_image.shape + + # Calculate camera intrinsics similar to StackOverflow approach + fovy = math.radians(fov_degrees) + f = height / (2 * math.tan(fovy / 2)) # focal length in pixels + cx = width / 2 # principal point x + cy = height / 2 # principal point y + + # Create Open3D camera intrinsics + cam_intrinsics = o3d.camera.PinholeCameraIntrinsic(width, height, f, f, cx, cy) + + # Convert numpy depth array to Open3D Image + o3d_depth = o3d.geometry.Image(depth_image.astype(np.float32)) + + # Create point cloud from depth image using Open3D + o3d_cloud = o3d.geometry.PointCloud.create_from_depth_image(o3d_depth, cam_intrinsics) + + # Convert Open3D point cloud to numpy array + camera_points = np.asarray(o3d_cloud.points) + + if camera_points.size == 0: + return np.array([]).reshape(0, 3) + + # Flip y and z axes + camera_points[:, 1] = -camera_points[:, 1] + camera_points[:, 2] = -camera_points[:, 2] + + # y (index 1) is up here + valid_mask = ( + (np.abs(camera_points[:, 0]) <= MAX_RANGE) + & (np.abs(camera_points[:, 1]) <= MAX_HEIGHT) + & (np.abs(camera_points[:, 2]) >= MIN_RANGE) + & (np.abs(camera_points[:, 2]) <= MAX_RANGE) + ) + camera_points = camera_points[valid_mask] + + if camera_points.size == 0: + return np.array([]).reshape(0, 3) + + # Transform to world coordinates + world_points = (camera_mat @ camera_points.T).T + camera_pos + + return world_points diff --git a/dimos/simulation/mujoco/model.py b/dimos/simulation/mujoco/model.py new file mode 100644 index 0000000000..12d97181b2 --- /dev/null +++ b/dimos/simulation/mujoco/model.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from etils import epath +import mujoco +from mujoco_playground._src import mjx_env +import numpy as np + +from dimos.simulation.mujoco.policy import OnnxController +from dimos.simulation.mujoco.types import InputController + +_HERE = epath.Path(__file__).parent + + +def get_assets() -> dict[str, bytes]: + # Assets used from https://sketchfab.com/3d-models/mersus-office-8714be387bcd406898b2615f7dae3a47 + # Created by Ryan Cassidy and Coleman Costello + assets: dict[str, bytes] = {} + assets_path = _HERE / "../../../data/mujoco_sim/go1" + mjx_env.update_assets(assets, assets_path, "*.xml") + mjx_env.update_assets(assets, assets_path / "assets") + path = mjx_env.MENAGERIE_PATH / "unitree_go1" + mjx_env.update_assets(assets, path, "*.xml") + mjx_env.update_assets(assets, path / "assets") + return assets + + +def load_model(input_device: InputController, model=None, data=None): + mujoco.set_mjcb_control(None) + + model = mujoco.MjModel.from_xml_path( + (_HERE / "../../../data/mujoco_sim/go1/robot.xml").as_posix(), + assets=get_assets(), + ) + data = mujoco.MjData(model) + + mujoco.mj_resetDataKeyframe(model, data, 0) + + ctrl_dt = 0.02 + sim_dt = 0.01 + n_substeps = round(ctrl_dt / sim_dt) + model.opt.timestep = sim_dt + + policy = OnnxController( + policy_path=(_HERE / "../../../data/mujoco_sim/go1/go1_policy.onnx").as_posix(), + default_angles=np.array(model.keyframe("home").qpos[7:]), + n_substeps=n_substeps, + action_scale=0.5, + input_controller=input_device, + ) + + mujoco.set_mjcb_control(policy.get_control) + + return model, data diff --git a/dimos/simulation/mujoco/mujoco.py b/dimos/simulation/mujoco/mujoco.py new file mode 100644 index 0000000000..5e867a26d1 --- /dev/null +++ b/dimos/simulation/mujoco/mujoco.py @@ -0,0 +1,399 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import atexit +import logging +import threading +import time + +import mujoco +from mujoco import viewer +import numpy as np +import open3d as o3d + +from dimos.msgs.geometry_msgs import Quaternion, Twist, Vector3 +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.robot.unitree_webrtc.type.odometry import Odometry +from dimos.simulation.mujoco.depth_camera import depth_image_to_point_cloud +from dimos.simulation.mujoco.model import load_model + +LIDAR_RESOLUTION = 0.05 +DEPTH_CAMERA_FOV = 160 +STEPS_PER_FRAME = 2 +VIDEO_FPS = 20 +LIDAR_FPS = 4 + +logger = logging.getLogger(__name__) + + +class MujocoThread(threading.Thread): + def __init__(self) -> None: + super().__init__(daemon=True) + self.shared_pixels = None + self.pixels_lock = threading.RLock() + self.shared_depth_front = None + self.depth_lock_front = threading.RLock() + self.shared_depth_left = None + self.depth_left_lock = threading.RLock() + self.shared_depth_right = None + self.depth_right_lock = threading.RLock() + self.odom_data = None + self.odom_lock = threading.RLock() + self.lidar_lock = threading.RLock() + self.model = None + self.data = None + self._command = np.zeros(3, dtype=np.float32) + self._command_lock = threading.RLock() + self._is_running = True + self._stop_timer: threading.Timer | None = None + self._viewer = None + self._rgb_renderer = None + self._depth_renderer = None + self._depth_left_renderer = None + self._depth_right_renderer = None + self._cleanup_registered = False + + # Register cleanup on exit + atexit.register(self.cleanup) + + def run(self) -> None: + try: + self.run_simulation() + except Exception as e: + logger.error(f"MuJoCo simulation thread error: {e}") + finally: + self._cleanup_resources() + + def run_simulation(self) -> None: + self.model, self.data = load_model(self) + + camera_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_CAMERA, "head_camera") + lidar_camera_id = mujoco.mj_name2id( + self.model, mujoco.mjtObj.mjOBJ_CAMERA, "lidar_front_camera" + ) + lidar_left_camera_id = mujoco.mj_name2id( + self.model, mujoco.mjtObj.mjOBJ_CAMERA, "lidar_left_camera" + ) + lidar_right_camera_id = mujoco.mj_name2id( + self.model, mujoco.mjtObj.mjOBJ_CAMERA, "lidar_right_camera" + ) + + with viewer.launch_passive( + self.model, self.data, show_left_ui=False, show_right_ui=False + ) as m_viewer: + self._viewer = m_viewer + camera_size = (320, 240) + + # Create separate renderers for RGB and depth + self._rgb_renderer = mujoco.Renderer( + self.model, height=camera_size[1], width=camera_size[0] + ) + self._depth_renderer = mujoco.Renderer( + self.model, height=camera_size[1], width=camera_size[0] + ) + # Enable depth rendering only for depth renderer + self._depth_renderer.enable_depth_rendering() + + # Create renderers for left and right depth cameras + self._depth_left_renderer = mujoco.Renderer( + self.model, height=camera_size[1], width=camera_size[0] + ) + self._depth_left_renderer.enable_depth_rendering() + + self._depth_right_renderer = mujoco.Renderer( + self.model, height=camera_size[1], width=camera_size[0] + ) + self._depth_right_renderer.enable_depth_rendering() + + scene_option = mujoco.MjvOption() + + # Timing control variables + last_video_time = 0 + last_lidar_time = 0 + video_interval = 1.0 / VIDEO_FPS + lidar_interval = 1.0 / LIDAR_FPS + + while m_viewer.is_running() and self._is_running: + step_start = time.time() + + for _ in range(STEPS_PER_FRAME): + mujoco.mj_step(self.model, self.data) + + m_viewer.sync() + + # Odometry happens every loop + with self.odom_lock: + # base position + pos = self.data.qpos[0:3] + # base orientation + quat = self.data.qpos[3:7] # (w, x, y, z) + self.odom_data = (pos.copy(), quat.copy()) + + current_time = time.time() + + # Video rendering + if current_time - last_video_time >= video_interval: + self._rgb_renderer.update_scene( + self.data, camera=camera_id, scene_option=scene_option + ) + pixels = self._rgb_renderer.render() + + with self.pixels_lock: + self.shared_pixels = pixels.copy() + + last_video_time = current_time + + # Lidar rendering + if current_time - last_lidar_time >= lidar_interval: + # Render fisheye camera for depth/lidar data + self._depth_renderer.update_scene( + self.data, camera=lidar_camera_id, scene_option=scene_option + ) + # When depth rendering is enabled, render() returns depth as float array in meters + depth = self._depth_renderer.render() + + with self.depth_lock_front: + self.shared_depth_front = depth.copy() + + # Render left depth camera + self._depth_left_renderer.update_scene( + self.data, camera=lidar_left_camera_id, scene_option=scene_option + ) + depth_left = self._depth_left_renderer.render() + + with self.depth_left_lock: + self.shared_depth_left = depth_left.copy() + + # Render right depth camera + self._depth_right_renderer.update_scene( + self.data, camera=lidar_right_camera_id, scene_option=scene_option + ) + depth_right = self._depth_right_renderer.render() + + with self.depth_right_lock: + self.shared_depth_right = depth_right.copy() + + last_lidar_time = current_time + + # Control the simulation speed + time_until_next_step = self.model.opt.timestep - (time.time() - step_start) + if time_until_next_step > 0: + time.sleep(time_until_next_step) + + def _process_depth_camera(self, camera_name: str, depth_data, depth_lock) -> np.ndarray | None: + """Process a single depth camera and return point cloud points.""" + with depth_lock: + if depth_data is None: + return None + + depth_image = depth_data.copy() + camera_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_CAMERA, camera_name) + if camera_id == -1: + return None + + camera_pos = self.data.cam_xpos[camera_id] + camera_mat = self.data.cam_xmat[camera_id].reshape(3, 3) + points = depth_image_to_point_cloud( + depth_image, + camera_pos, + camera_mat, + fov_degrees=DEPTH_CAMERA_FOV, + ) + return points if points.size > 0 else None + + def get_lidar_message(self) -> LidarMessage | None: + all_points = [] + origin = None + + with self.lidar_lock: + if self.model is not None and self.data is not None: + pos = self.data.qpos[0:3] + origin = Vector3(pos[0], pos[1], pos[2]) + + cameras = [ + ("lidar_front_camera", self.shared_depth_front, self.depth_lock_front), + ("lidar_left_camera", self.shared_depth_left, self.depth_left_lock), + ("lidar_right_camera", self.shared_depth_right, self.depth_right_lock), + ] + + for camera_name, depth_data, depth_lock in cameras: + points = self._process_depth_camera(camera_name, depth_data, depth_lock) + if points is not None: + all_points.append(points) + + # Combine all point clouds + if not all_points: + return None + + combined_points = np.vstack(all_points) + pcd = o3d.geometry.PointCloud() + pcd.points = o3d.utility.Vector3dVector(combined_points) + + # Apply voxel downsampling to remove overlapping points + pcd = pcd.voxel_down_sample(voxel_size=LIDAR_RESOLUTION) + lidar_to_publish = LidarMessage( + pointcloud=pcd, + ts=time.time(), + origin=origin, + resolution=LIDAR_RESOLUTION, + ) + return lidar_to_publish + + def get_odom_message(self) -> Odometry | None: + with self.odom_lock: + if self.odom_data is None: + return None + pos, quat_wxyz = self.odom_data + + # MuJoCo uses (w, x, y, z) for quaternions. + # ROS and Dimos use (x, y, z, w). + orientation = Quaternion(quat_wxyz[1], quat_wxyz[2], quat_wxyz[3], quat_wxyz[0]) + + odom_to_publish = Odometry( + position=Vector3(pos[0], pos[1], pos[2]), + orientation=orientation, + ts=time.time(), + frame_id="world", + ) + return odom_to_publish + + def _stop_move(self) -> None: + with self._command_lock: + self._command = np.zeros(3, dtype=np.float32) + self._stop_timer = None + + def move(self, twist: Twist, duration: float = 0.0) -> None: + if self._stop_timer: + self._stop_timer.cancel() + + with self._command_lock: + self._command = np.array( + [twist.linear.x, twist.linear.y, twist.angular.z], dtype=np.float32 + ) + + if duration > 0: + self._stop_timer = threading.Timer(duration, self._stop_move) + self._stop_timer.daemon = True + self._stop_timer.start() + else: + self._stop_timer = None + + def get_command(self) -> np.ndarray: + with self._command_lock: + return self._command.copy() + + def stop(self) -> None: + """Stop the simulation thread gracefully.""" + self._is_running = False + + # Cancel any pending timers + if self._stop_timer: + self._stop_timer.cancel() + self._stop_timer = None + + # Wait for thread to finish + if self.is_alive(): + self.join(timeout=5.0) + if self.is_alive(): + logger.warning("MuJoCo thread did not stop gracefully within timeout") + + def cleanup(self) -> None: + """Clean up all resources. Can be called multiple times safely.""" + if self._cleanup_registered: + return + self._cleanup_registered = True + + logger.debug("Cleaning up MuJoCo resources") + self.stop() + self._cleanup_resources() + + def _cleanup_resources(self) -> None: + """Internal method to clean up MuJoCo-specific resources.""" + try: + # Cancel any timers + if self._stop_timer: + self._stop_timer.cancel() + self._stop_timer = None + + # Clean up renderers + if self._rgb_renderer is not None: + try: + self._rgb_renderer.close() + except Exception as e: + logger.debug(f"Error closing RGB renderer: {e}") + finally: + self._rgb_renderer = None + + if self._depth_renderer is not None: + try: + self._depth_renderer.close() + except Exception as e: + logger.debug(f"Error closing depth renderer: {e}") + finally: + self._depth_renderer = None + + if self._depth_left_renderer is not None: + try: + self._depth_left_renderer.close() + except Exception as e: + logger.debug(f"Error closing left depth renderer: {e}") + finally: + self._depth_left_renderer = None + + if self._depth_right_renderer is not None: + try: + self._depth_right_renderer.close() + except Exception as e: + logger.debug(f"Error closing right depth renderer: {e}") + finally: + self._depth_right_renderer = None + + # Clear data references + with self.pixels_lock: + self.shared_pixels = None + + with self.depth_lock_front: + self.shared_depth_front = None + + with self.depth_left_lock: + self.shared_depth_left = None + + with self.depth_right_lock: + self.shared_depth_right = None + + with self.odom_lock: + self.odom_data = None + + # Clear model and data + self.model = None + self.data = None + + # Reset MuJoCo control callback + try: + mujoco.set_mjcb_control(None) + except Exception as e: + logger.debug(f"Error resetting MuJoCo control callback: {e}") + + except Exception as e: + logger.error(f"Error during resource cleanup: {e}") + + def __del__(self) -> None: + """Destructor to ensure cleanup on object deletion.""" + try: + self.cleanup() + except Exception: + pass diff --git a/dimos/simulation/mujoco/policy.py b/dimos/simulation/mujoco/policy.py new file mode 100644 index 0000000000..2ea974f0be --- /dev/null +++ b/dimos/simulation/mujoco/policy.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import mujoco +import numpy as np +import onnxruntime as rt + +from dimos.simulation.mujoco.types import InputController + + +class OnnxController: + """ONNX controller for the Go-1 robot.""" + + def __init__( + self, + policy_path: str, + default_angles: np.ndarray, + n_substeps: int, + action_scale: float, + input_controller: InputController, + ) -> None: + self._output_names = ["continuous_actions"] + self._policy = rt.InferenceSession(policy_path, providers=["CPUExecutionProvider"]) + + self._action_scale = action_scale + self._default_angles = default_angles + self._last_action = np.zeros_like(default_angles, dtype=np.float32) + + self._counter = 0 + self._n_substeps = n_substeps + self._input_controller = input_controller + + def get_obs(self, model, data) -> np.ndarray: + linvel = data.sensor("local_linvel").data + gyro = data.sensor("gyro").data + imu_xmat = data.site_xmat[model.site("imu").id].reshape(3, 3) + gravity = imu_xmat.T @ np.array([0, 0, -1]) + joint_angles = data.qpos[7:] - self._default_angles + joint_velocities = data.qvel[6:] + obs = np.hstack( + [ + linvel, + gyro, + gravity, + joint_angles, + joint_velocities, + self._last_action, + self._input_controller.get_command(), + ] + ) + return obs.astype(np.float32) + + def get_control(self, model: mujoco.MjModel, data: mujoco.MjData) -> None: + self._counter += 1 + if self._counter % self._n_substeps == 0: + obs = self.get_obs(model, data) + onnx_input = {"obs": obs.reshape(1, -1)} + onnx_pred = self._policy.run(self._output_names, onnx_input)[0][0] + self._last_action = onnx_pred.copy() + data.ctrl[:] = onnx_pred * self._action_scale + self._default_angles diff --git a/dimos/simulation/mujoco/types.py b/dimos/simulation/mujoco/types.py new file mode 100644 index 0000000000..42fd28efd2 --- /dev/null +++ b/dimos/simulation/mujoco/types.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from typing import Protocol + +import numpy as np + + +class InputController(Protocol): + """A protocol for input devices to control the robot.""" + + def get_command(self) -> np.ndarray: ... + def stop(self) -> None: ... diff --git a/dimos/skills/kill_skill.py b/dimos/skills/kill_skill.py index 1ca050484f..b9d02729f5 100644 --- a/dimos/skills/kill_skill.py +++ b/dimos/skills/kill_skill.py @@ -19,8 +19,6 @@ particularly those running in separate threads like the monitor skill. """ -import logging -from typing import Optional, Dict, Any, List from pydantic import Field from dimos.skills.skills import AbstractSkill, SkillLibrary @@ -28,35 +26,36 @@ logger = setup_logger("dimos.skills.kill_skill") + class KillSkill(AbstractSkill): """ A skill that terminates other running skills. - + This skill can be used to stop long-running or background skills like the monitor skill. It uses the centralized process management in the SkillLibrary to track and terminate skills. """ - + skill_name: str = Field(..., description="Name of the skill to terminate") - - def __init__(self, skill_library: Optional[SkillLibrary] = None, **data): + + def __init__(self, skill_library: SkillLibrary | None = None, **data) -> None: """ Initialize the kill skill. - + Args: skill_library: The skill library instance **data: Additional data for configuration """ super().__init__(**data) self._skill_library = skill_library - + def __call__(self): """ Terminate the specified skill. - + Returns: A message indicating whether the skill was successfully terminated - """ + """ print("running skills", self._skill_library.get_running_skills()) # Terminate the skill using the skill library - return self._skill_library.terminate_skill(self.skill_name) \ No newline at end of file + return self._skill_library.terminate_skill(self.skill_name) diff --git a/dimos/skills/manipulation/abstract_manipulation_skill.py b/dimos/skills/manipulation/abstract_manipulation_skill.py new file mode 100644 index 0000000000..e3f6e719fa --- /dev/null +++ b/dimos/skills/manipulation/abstract_manipulation_skill.py @@ -0,0 +1,58 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Abstract base class for manipulation skills.""" + +from dimos.manipulation.manipulation_interface import ManipulationInterface +from dimos.robot.robot import Robot +from dimos.skills.skills import AbstractRobotSkill +from dimos.types.robot_capabilities import RobotCapability + + +class AbstractManipulationSkill(AbstractRobotSkill): + """Base class for all manipulation-related skills. + + This abstract class provides access to the robot's manipulation memory system. + """ + + def __init__(self, *args, robot: Robot | None = None, **kwargs) -> None: + """Initialize the manipulation skill. + + Args: + robot: The robot instance to associate with this skill + """ + super().__init__(*args, robot=robot, **kwargs) + + if self._robot and not self._robot.manipulation_interface: + raise NotImplementedError( + "This robot does not have a manipulation interface implemented" + ) + + @property + def manipulation_interface(self) -> ManipulationInterface | None: + """Get the robot's manipulation interface. + + Returns: + ManipulationInterface: The robot's manipulation interface or None if not available + + Raises: + RuntimeError: If the robot doesn't have the MANIPULATION capability + """ + if self._robot is None: + return None + + if not self._robot.has_capability(RobotCapability.MANIPULATION): + raise RuntimeError("This robot does not have manipulation capabilities") + + return self._robot.manipulation_interface diff --git a/dimos/skills/manipulation/force_constraint_skill.py b/dimos/skills/manipulation/force_constraint_skill.py new file mode 100644 index 0000000000..72616c32a3 --- /dev/null +++ b/dimos/skills/manipulation/force_constraint_skill.py @@ -0,0 +1,72 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from pydantic import Field + +from dimos.skills.manipulation.abstract_manipulation_skill import AbstractManipulationSkill +from dimos.types.manipulation import ForceConstraint, Vector +from dimos.utils.logging_config import setup_logger + +# Initialize logger +logger = setup_logger("dimos.skills.force_constraint_skill") + + +class ForceConstraintSkill(AbstractManipulationSkill): + """ + Skill for generating force constraints for robot manipulation. + + This skill generates force constraints and adds them to the ManipulationInterface's + agent_constraints list for tracking constraints created by the Agent. + """ + + # Constraint parameters + min_force: float = Field(0.0, description="Minimum force magnitude in Newtons") + max_force: float = Field(100.0, description="Maximum force magnitude in Newtons to apply") + + # Force direction as (x,y) tuple + force_direction: tuple[float, float] | None = Field( + None, description="Force direction vector (x,y)" + ) + + # Description + description: str = Field("", description="Description of the force constraint") + + def __call__(self) -> ForceConstraint: + """ + Generate a force constraint based on the parameters. + + Returns: + ForceConstraint: The generated constraint + """ + # Create force direction vector if provided (convert 2D point to 3D vector with z=0) + force_direction_vector = None + if self.force_direction: + force_direction_vector = Vector(self.force_direction[0], self.force_direction[1], 0.0) + + # Create and return the constraint + constraint = ForceConstraint( + max_force=self.max_force, + min_force=self.min_force, + force_direction=force_direction_vector, + description=self.description, + ) + + # Add constraint to manipulation interface for Agent recall + self.manipulation_interface.add_constraint(constraint) + + # Log the constraint creation + logger.info(f"Generated force constraint: {self.description}") + + return constraint diff --git a/dimos/skills/manipulation/manipulate_skill.py b/dimos/skills/manipulation/manipulate_skill.py new file mode 100644 index 0000000000..7905d4f76c --- /dev/null +++ b/dimos/skills/manipulation/manipulate_skill.py @@ -0,0 +1,173 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +from typing import Any +import uuid + +from pydantic import Field + +from dimos.skills.manipulation.abstract_manipulation_skill import AbstractManipulationSkill +from dimos.types.manipulation import ( + AbstractConstraint, + ManipulationMetadata, + ManipulationTask, + ManipulationTaskConstraint, +) +from dimos.utils.logging_config import setup_logger + +# Initialize logger +logger = setup_logger("dimos.skills.manipulate_skill") + + +class Manipulate(AbstractManipulationSkill): + """ + Skill for executing manipulation tasks with constraints. + Can be called by an LLM with a list of manipulation constraints. + """ + + description: str = Field("", description="Description of the manipulation task") + + # Target object information + target_object: str = Field( + "", description="Semantic label of the target object (e.g., 'cup', 'box')" + ) + + target_point: str = Field( + "", description="(X,Y) point in pixel-space of the point to manipulate on target object" + ) + + # Constraints - can be set directly + constraints: list[str] = Field( + [], + description="List of AbstractConstraint constraint IDs from AgentMemory to apply to the manipulation task", + ) + + # Object movement tolerances + object_tolerances: dict[str, float] = Field( + {}, # Empty dict as default + description="Dictionary mapping object IDs to movement tolerances (0.0 = immovable, 1.0 = freely movable)", + ) + + def __call__(self) -> dict[str, Any]: + """ + Execute a manipulation task with the given constraints. + + Returns: + Dict[str, Any]: Result of the manipulation operation + """ + # Get the manipulation constraint + constraint = self._build_manipulation_constraint() + + # Create task with unique ID + task_id = f"{str(uuid.uuid4())[:4]}" + timestamp = time.time() + + # Build metadata with environment state + metadata = self._build_manipulation_metadata() + + task = ManipulationTask( + description=self.description, + target_object=self.target_object, + target_point=tuple(map(int, self.target_point.strip("()").split(","))), + constraints=constraint, + metadata=metadata, + timestamp=timestamp, + task_id=task_id, + result=None, + ) + + # Add task to manipulation interface + self.manipulation_interface.add_manipulation_task(task) + + # Execute the manipulation + result = self._execute_manipulation(task) + + # Log the execution + logger.info( + f"Executed manipulation '{self.description}' with constraints: {self.constraints}" + ) + + return result + + def _build_manipulation_metadata(self) -> ManipulationMetadata: + """ + Build metadata for the current environment state, including object data and movement tolerances. + """ + # Get detected objects from the manipulation interface + detected_objects = [] + try: + detected_objects = self.manipulation_interface.get_latest_objects() or [] + except Exception as e: + logger.warning(f"Failed to get detected objects: {e}") + + # Create dictionary of objects keyed by ID for easier lookup + objects_by_id = {} + for obj in detected_objects: + obj_id = str(obj.get("object_id", -1)) + objects_by_id[obj_id] = dict(obj) # Make a copy to avoid modifying original + + # Create objects_data dictionary with tolerances applied + objects_data: dict[str, Any] = {} + + # First, apply all specified tolerances + for object_id, tolerance in self.object_tolerances.items(): + if object_id in objects_by_id: + # Object exists in detected objects, update its tolerance + obj_data = objects_by_id[object_id] + obj_data["movement_tolerance"] = tolerance + objects_data[object_id] = obj_data + + # Add any detected objects not explicitly given tolerances + for obj_id, obj in objects_by_id.items(): + if obj_id not in self.object_tolerances: + obj["movement_tolerance"] = 0.0 # Default to immovable + objects_data[obj_id] = obj + + # Create properly typed ManipulationMetadata + metadata: ManipulationMetadata = {"timestamp": time.time(), "objects": objects_data} + + return metadata + + def _build_manipulation_constraint(self) -> ManipulationTaskConstraint: + """ + Build a ManipulationTaskConstraint object from the provided parameters. + """ + + constraint = ManipulationTaskConstraint() + + # Add constraints directly or resolve from IDs + for c in self.constraints: + if isinstance(c, AbstractConstraint): + constraint.add_constraint(c) + elif isinstance(c, str) and self.manipulation_interface: + # Try to load constraint from ID + saved_constraint = self.manipulation_interface.get_constraint(c) + if saved_constraint: + constraint.add_constraint(saved_constraint) + + return constraint + + # TODO: Implement + def _execute_manipulation(self, task: ManipulationTask) -> dict[str, Any]: + """ + Execute the manipulation with the given constraint. + + Args: + task: The manipulation task to execute + + Returns: + Dict[str, Any]: Result of the manipulation operation + """ + return {"success": True} diff --git a/dimos/skills/manipulation/pick_and_place.py b/dimos/skills/manipulation/pick_and_place.py new file mode 100644 index 0000000000..1143ce073a --- /dev/null +++ b/dimos/skills/manipulation/pick_and_place.py @@ -0,0 +1,440 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Pick and place skill for Piper Arm robot. + +This module provides a skill that uses Qwen VLM to identify pick and place +locations based on natural language queries, then executes the manipulation. +""" + +import json +import os +from typing import Any + +import cv2 +import numpy as np +from pydantic import Field + +from dimos.models.qwen.video_query import query_single_frame +from dimos.skills.skills import AbstractRobotSkill +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.skills.manipulation.pick_and_place") + + +def parse_qwen_points_response(response: str) -> tuple[tuple[int, int], tuple[int, int]] | None: + """ + Parse Qwen's response containing two points. + + Args: + response: Qwen's response containing JSON with two points + + Returns: + Tuple of (pick_point, place_point) where each point is (x, y), or None if parsing fails + """ + try: + # Try to extract JSON from the response + start_idx = response.find("{") + end_idx = response.rfind("}") + 1 + + if start_idx >= 0 and end_idx > start_idx: + json_str = response[start_idx:end_idx] + result = json.loads(json_str) + + # Extract pick and place points + if "pick_point" in result and "place_point" in result: + pick = result["pick_point"] + place = result["place_point"] + + # Validate points have x,y coordinates + if ( + isinstance(pick, list | tuple) + and len(pick) >= 2 + and isinstance(place, list | tuple) + and len(place) >= 2 + ): + return (int(pick[0]), int(pick[1])), (int(place[0]), int(place[1])) + + except Exception as e: + logger.error(f"Error parsing Qwen points response: {e}") + logger.debug(f"Raw response: {response}") + + return None + + +def save_debug_image_with_points( + image: np.ndarray, + pick_point: tuple[int, int] | None = None, + place_point: tuple[int, int] | None = None, + filename_prefix: str = "qwen_debug", +) -> str: + """ + Save debug image with crosshairs marking pick and/or place points. + + Args: + image: RGB image array + pick_point: (x, y) coordinates for pick location + place_point: (x, y) coordinates for place location + filename_prefix: Prefix for the saved filename + + Returns: + Path to the saved image + """ + # Create a copy to avoid modifying original + debug_image = image.copy() + + # Draw pick point crosshair (green) + if pick_point: + x, y = pick_point + # Draw crosshair + cv2.drawMarker(debug_image, (x, y), (0, 255, 0), cv2.MARKER_CROSS, 30, 2) + # Draw circle + cv2.circle(debug_image, (x, y), 5, (0, 255, 0), -1) + # Add label + cv2.putText( + debug_image, "PICK", (x + 10, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2 + ) + + # Draw place point crosshair (cyan) + if place_point: + x, y = place_point + # Draw crosshair + cv2.drawMarker(debug_image, (x, y), (255, 255, 0), cv2.MARKER_CROSS, 30, 2) + # Draw circle + cv2.circle(debug_image, (x, y), 5, (255, 255, 0), -1) + # Add label + cv2.putText( + debug_image, "PLACE", (x + 10, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 0), 2 + ) + + # Draw arrow from pick to place if both exist + if pick_point and place_point: + cv2.arrowedLine(debug_image, pick_point, place_point, (255, 0, 255), 2, tipLength=0.03) + + # Generate filename with timestamp + filename = f"{filename_prefix}.png" + filepath = os.path.join(os.getcwd(), filename) + + # Save image + cv2.imwrite(filepath, debug_image) + logger.info(f"Debug image saved to: {filepath}") + + return filepath + + +def parse_qwen_single_point_response(response: str) -> tuple[int, int] | None: + """ + Parse Qwen's response containing a single point. + + Args: + response: Qwen's response containing JSON with a point + + Returns: + Tuple of (x, y) or None if parsing fails + """ + try: + # Try to extract JSON from the response + start_idx = response.find("{") + end_idx = response.rfind("}") + 1 + + if start_idx >= 0 and end_idx > start_idx: + json_str = response[start_idx:end_idx] + result = json.loads(json_str) + + # Try different possible keys + point = None + for key in ["point", "location", "position", "coordinates"]: + if key in result: + point = result[key] + break + + # Validate point has x,y coordinates + if point and isinstance(point, list | tuple) and len(point) >= 2: + return int(point[0]), int(point[1]) + + except Exception as e: + logger.error(f"Error parsing Qwen single point response: {e}") + logger.debug(f"Raw response: {response}") + + return None + + +class PickAndPlace(AbstractRobotSkill): + """ + A skill that performs pick and place operations using vision-language guidance. + + This skill uses Qwen VLM to identify objects and locations based on natural + language queries, then executes pick and place operations using the robot's + manipulation interface. + + Example usage: + # Just pick the object + skill = PickAndPlace(robot=robot, object_query="red mug") + + # Pick and place the object + skill = PickAndPlace(robot=robot, object_query="red mug", target_query="on the coaster") + + The skill uses the robot's stereo camera to capture RGB images and its manipulation + interface to execute the pick and place operation. It automatically handles coordinate + transformation from 2D pixel coordinates to 3D world coordinates. + """ + + object_query: str = Field( + "mug", + description="Natural language description of the object to pick (e.g., 'red mug', 'small box')", + ) + + target_query: str | None = Field( + None, + description="Natural language description of where to place the object (e.g., 'on the table', 'in the basket'). If not provided, only pick operation will be performed.", + ) + + model_name: str = Field( + "qwen2.5-vl-72b-instruct", description="Qwen model to use for visual queries" + ) + + def __init__(self, robot=None, **data) -> None: + """ + Initialize the PickAndPlace skill. + + Args: + robot: The PiperArmRobot instance + **data: Additional configuration data + """ + super().__init__(robot=robot, **data) + + def _get_camera_frame(self) -> np.ndarray | None: + """ + Get a single RGB frame from the robot's camera. + + Returns: + RGB image as numpy array or None if capture fails + """ + if not self._robot or not self._robot.manipulation_interface: + logger.error("Robot or stereo camera not available") + return None + + try: + # Use the RPC call to get a single RGB frame + rgb_frame = self._robot.manipulation_interface.get_single_rgb_frame() + if rgb_frame is None: + logger.error("Failed to capture RGB frame from camera") + return rgb_frame + except Exception as e: + logger.error(f"Error getting camera frame: {e}") + return None + + def _query_pick_and_place_points( + self, frame: np.ndarray + ) -> tuple[tuple[int, int], tuple[int, int]] | None: + """ + Query Qwen to get both pick and place points in a single query. + + Args: + frame: RGB image array + + Returns: + Tuple of (pick_point, place_point) or None if query fails + """ + # This method is only called when both object and target are specified + prompt = ( + f"Look at this image carefully. I need you to identify two specific locations:\n" + f"1. Find the {self.object_query} - this is the object I want to pick up\n" + f"2. Identify where to place it {self.target_query}\n\n" + "Instructions:\n" + "- The pick_point should be at the center or graspable part of the object\n" + "- The place_point should be a stable, flat surface at the target location\n" + "- Consider the object's size when choosing the placement point\n\n" + "Return ONLY a JSON object with this exact format:\n" + "{'pick_point': [x, y], 'place_point': [x, y]}\n" + "where [x, y] are pixel coordinates in the image." + ) + + try: + response = query_single_frame(frame, prompt, model_name=self.model_name) + return parse_qwen_points_response(response) + except Exception as e: + logger.error(f"Error querying Qwen for pick and place points: {e}") + return None + + def _query_single_point( + self, frame: np.ndarray, query: str, point_type: str + ) -> tuple[int, int] | None: + """ + Query Qwen to get a single point location. + + Args: + frame: RGB image array + query: Natural language description of what to find + point_type: Type of point ('pick' or 'place') for context + + Returns: + Tuple of (x, y) pixel coordinates or None if query fails + """ + if point_type == "pick": + prompt = ( + f"Look at this image carefully and find the {query}.\n\n" + "Instructions:\n" + "- Identify the exact object matching the description\n" + "- Choose the center point or the most graspable location on the object\n" + "- If multiple matching objects exist, choose the most prominent or accessible one\n" + "- Consider the object's shape and material when selecting the grasp point\n\n" + "Return ONLY a JSON object with this exact format:\n" + "{'point': [x, y]}\n" + "where [x, y] are the pixel coordinates of the optimal grasping point on the object." + ) + else: # place + prompt = ( + f"Look at this image and identify where to place an object {query}.\n\n" + "Instructions:\n" + "- Find a stable, flat surface at the specified location\n" + "- Ensure the placement spot is clear of obstacles\n" + "- Consider the size of the object being placed\n" + "- If the query specifies a container or specific spot, center the placement there\n" + "- Otherwise, find the most appropriate nearby surface\n\n" + "Return ONLY a JSON object with this exact format:\n" + "{'point': [x, y]}\n" + "where [x, y] are the pixel coordinates of the optimal placement location." + ) + + try: + response = query_single_frame(frame, prompt, model_name=self.model_name) + return parse_qwen_single_point_response(response) + except Exception as e: + logger.error(f"Error querying Qwen for {point_type} point: {e}") + return None + + def __call__(self) -> dict[str, Any]: + """ + Execute the pick and place operation. + + Returns: + Dictionary with operation results + """ + super().__call__() + + if not self._robot: + error_msg = "No robot instance provided to PickAndPlace skill" + logger.error(error_msg) + return {"success": False, "error": error_msg} + + # Register skill as running + skill_library = self._robot.get_skills() + self.register_as_running("PickAndPlace", skill_library) + + # Get camera frame + frame = self._get_camera_frame() + if frame is None: + return {"success": False, "error": "Failed to capture camera frame"} + + # Convert RGB to BGR for OpenCV if needed + if len(frame.shape) == 3 and frame.shape[2] == 3: + frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) + + # Get pick and place points from Qwen + pick_point = None + place_point = None + + # Determine mode based on whether target_query is provided + if self.target_query is None: + # Pick only mode + logger.info("Pick-only mode (no target specified)") + + # Query for pick point + pick_point = self._query_single_point(frame, self.object_query, "pick") + if not pick_point: + return {"success": False, "error": f"Failed to find {self.object_query}"} + + # No place point needed for pick-only + place_point = None + else: + # Pick and place mode - can use either single or dual query + logger.info("Pick and place mode (target specified)") + + # Try single query first for efficiency + points = self._query_pick_and_place_points(frame) + pick_point, place_point = points + + logger.info(f"Pick point: {pick_point}, Place point: {place_point}") + + # Save debug image with marked points + if pick_point or place_point: + save_debug_image_with_points(frame, pick_point, place_point) + + # Execute pick (and optionally place) using the robot's interface + try: + if place_point: + # Pick and place + result = self._robot.pick_and_place( + pick_x=pick_point[0], + pick_y=pick_point[1], + place_x=place_point[0], + place_y=place_point[1], + ) + else: + # Pick only + result = self._robot.pick_and_place( + pick_x=pick_point[0], pick_y=pick_point[1], place_x=None, place_y=None + ) + + if result: + if self.target_query: + message = ( + f"Successfully picked {self.object_query} and placed it {self.target_query}" + ) + else: + message = f"Successfully picked {self.object_query}" + + return { + "success": True, + "pick_point": pick_point, + "place_point": place_point, + "object": self.object_query, + "target": self.target_query, + "message": message, + } + else: + operation = "Pick and place" if self.target_query else "Pick" + return { + "success": False, + "pick_point": pick_point, + "place_point": place_point, + "error": f"{operation} operation failed", + } + + except Exception as e: + logger.error(f"Error executing pick and place: {e}") + return { + "success": False, + "error": f"Execution error: {e!s}", + "pick_point": pick_point, + "place_point": place_point, + } + finally: + # Always unregister skill when done + self.stop() + + def stop(self) -> None: + """ + Stop the pick and place operation and perform cleanup. + """ + logger.info("Stopping PickAndPlace skill") + + # Unregister skill from skill library + if self._robot: + skill_library = self._robot.get_skills() + self.unregister_as_running("PickAndPlace", skill_library) + + logger.info("PickAndPlace skill stopped successfully") diff --git a/dimos/skills/manipulation/rotation_constraint_skill.py b/dimos/skills/manipulation/rotation_constraint_skill.py new file mode 100644 index 0000000000..ae1bdbb57d --- /dev/null +++ b/dimos/skills/manipulation/rotation_constraint_skill.py @@ -0,0 +1,109 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Literal + +from pydantic import Field + +from dimos.skills.manipulation.abstract_manipulation_skill import AbstractManipulationSkill +from dimos.types.manipulation import RotationConstraint +from dimos.types.vector import Vector +from dimos.utils.logging_config import setup_logger + +# Initialize logger +logger = setup_logger("dimos.skills.rotation_constraint_skill") + + +class RotationConstraintSkill(AbstractManipulationSkill): + """ + Skill for generating rotation constraints for robot manipulation. + + This skill generates rotation constraints and adds them to the ManipulationInterface's + agent_constraints list for tracking constraints created by the Agent. + """ + + # Rotation axis parameter + rotation_axis: Literal["roll", "pitch", "yaw"] = Field( + "roll", + description="Axis to rotate around: 'roll' (x-axis), 'pitch' (y-axis), or 'yaw' (z-axis)", + ) + + # Simple angle values for rotation (in degrees) + start_angle: float | None = Field(None, description="Starting angle in degrees") + end_angle: float | None = Field(None, description="Ending angle in degrees") + + # Pivot points as (x,y) tuples + pivot_point: tuple[float, float] | None = Field( + None, description="Pivot point (x,y) for rotation" + ) + + # TODO: Secondary pivot point for more complex rotations + secondary_pivot_point: tuple[float, float] | None = Field( + None, description="Secondary pivot point (x,y) for double-pivot rotation" + ) + + def __call__(self) -> RotationConstraint: + """ + Generate a rotation constraint based on the parameters. + + This implementation supports rotation around a single axis (roll, pitch, or yaw). + + Returns: + RotationConstraint: The generated constraint + """ + # rotation_axis is guaranteed to be one of "roll", "pitch", or "yaw" due to Literal type constraint + + # Create angle vectors more efficiently + start_angle_vector = None + if self.start_angle is not None: + # Build rotation vector on correct axis + values = [0.0, 0.0, 0.0] + axis_index = {"roll": 0, "pitch": 1, "yaw": 2}[self.rotation_axis] + values[axis_index] = self.start_angle + start_angle_vector = Vector(*values) + + end_angle_vector = None + if self.end_angle is not None: + values = [0.0, 0.0, 0.0] + axis_index = {"roll": 0, "pitch": 1, "yaw": 2}[self.rotation_axis] + values[axis_index] = self.end_angle + end_angle_vector = Vector(*values) + + # Create pivot point vector if provided (convert 2D point to 3D vector with z=0) + pivot_point_vector = None + if self.pivot_point: + pivot_point_vector = Vector(self.pivot_point[0], self.pivot_point[1], 0.0) + + # Create secondary pivot point vector if provided + secondary_pivot_vector = None + if self.secondary_pivot_point: + secondary_pivot_vector = Vector( + self.secondary_pivot_point[0], self.secondary_pivot_point[1], 0.0 + ) + + constraint = RotationConstraint( + rotation_axis=self.rotation_axis, + start_angle=start_angle_vector, + end_angle=end_angle_vector, + pivot_point=pivot_point_vector, + secondary_pivot_point=secondary_pivot_vector, + ) + + # Add constraint to manipulation interface + self.manipulation_interface.add_constraint(constraint) + + # Log the constraint creation + logger.info(f"Generated rotation constraint around {self.rotation_axis} axis") + + return constraint diff --git a/dimos/skills/manipulation/translation_constraint_skill.py b/dimos/skills/manipulation/translation_constraint_skill.py new file mode 100644 index 0000000000..6e1808744f --- /dev/null +++ b/dimos/skills/manipulation/translation_constraint_skill.py @@ -0,0 +1,100 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Literal + +from pydantic import Field + +from dimos.skills.manipulation.abstract_manipulation_skill import AbstractManipulationSkill +from dimos.types.manipulation import TranslationConstraint, Vector +from dimos.utils.logging_config import setup_logger + +# Initialize logger +logger = setup_logger("dimos.skills.translation_constraint_skill") + + +class TranslationConstraintSkill(AbstractManipulationSkill): + """ + Skill for generating translation constraints for robot manipulation. + + This skill generates translation constraints and adds them to the ManipulationInterface's + agent_constraints list for tracking constraints created by the Agent. + """ + + # Constraint parameters + translation_axis: Literal["x", "y", "z"] = Field( + "x", description="Axis to translate along: 'x', 'y', or 'z'" + ) + + reference_point: tuple[float, float] | None = Field( + None, description="Reference point (x,y) on the target object for translation constraining" + ) + + bounds_min: tuple[float, float] | None = Field( + None, description="Minimum bounds (x,y) for bounded translation" + ) + + bounds_max: tuple[float, float] | None = Field( + None, description="Maximum bounds (x,y) for bounded translation" + ) + + target_point: tuple[float, float] | None = Field( + None, description="Final target position (x,y) for translation constraining" + ) + + # Description + description: str = Field("", description="Description of the translation constraint") + + def __call__(self) -> TranslationConstraint: + """ + Generate a translation constraint based on the parameters. + + Returns: + TranslationConstraint: The generated constraint + """ + # Create reference point vector if provided (convert 2D point to 3D vector with z=0) + reference_point = None + if self.reference_point: + reference_point = Vector(self.reference_point[0], self.reference_point[1], 0.0) + + # Create bounds minimum vector if provided + bounds_min = None + if self.bounds_min: + bounds_min = Vector(self.bounds_min[0], self.bounds_min[1], 0.0) + + # Create bounds maximum vector if provided + bounds_max = None + if self.bounds_max: + bounds_max = Vector(self.bounds_max[0], self.bounds_max[1], 0.0) + + # Create relative target vector if provided + target_point = None + if self.target_point: + target_point = Vector(self.target_point[0], self.target_point[1], 0.0) + + constraint = TranslationConstraint( + translation_axis=self.translation_axis, + reference_point=reference_point, + bounds_min=bounds_min, + bounds_max=bounds_max, + target_point=target_point, + ) + + # Add constraint to manipulation interface + self.manipulation_interface.add_constraint(constraint) + + # Log the constraint creation + logger.info(f"Generated translation constraint along {self.translation_axis} axis") + + return {"success": True} diff --git a/dimos/skills/navigation.py b/dimos/skills/navigation.py deleted file mode 100644 index b4aea42416..0000000000 --- a/dimos/skills/navigation.py +++ /dev/null @@ -1,568 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Semantic map skills for building and navigating spatial memory maps. - -This module provides two skills: -1. BuildSemanticMap - Builds a semantic map by recording video frames at different locations -2. Navigate - Queries an existing semantic map using natural language -""" - -import os -import sys -import time -import threading -import logging -import numpy as np -import json -from typing import Optional, Dict, Tuple, Any -from dimos.utils.threadpool import get_scheduler - -import chromadb -from reactivex import operators as ops -from reactivex.subject import Subject -from pydantic import Field - -from dimos.skills.skills import AbstractRobotSkill -from dimos.perception.spatial_perception import SpatialMemory -from dimos.agents.memory.visual_memory import VisualMemory -from dimos.types.robot_location import RobotLocation -from dimos.utils.threadpool import get_scheduler -from dimos.utils.logging_config import setup_logger -from dimos.models.qwen.video_query import get_bbox_from_qwen_frame -from dimos.utils.generic_subscriber import GenericSubscriber -from dimos.utils.ros_utils import distance_angle_to_goal_xy -from dimos.robot.local_planner.local_planner import navigate_to_goal_local - -logger = setup_logger("dimos.skills.semantic_map_skills") - -def get_dimos_base_path(): - """ - Get the DiMOS base path from DIMOS_PATH environment variable or default to user's home directory. - - Returns: - Base path to use for DiMOS assets - """ - dimos_path = os.environ.get('DIMOS_PATH') - if dimos_path: - return dimos_path - # Get the current user's username - user = os.environ.get('USER', os.path.basename(os.path.expanduser('~'))) - return f"/home/{user}/dimos" - - -class NavigateWithText(AbstractRobotSkill): - """ - A skill that queries an existing semantic map using natural language or tries to navigate to an object in view. - - This skill first attempts to locate an object in the robot's camera view using vision. - If the object is found, it navigates to it. If not, it falls back to querying the - semantic map for a location matching the description. For example, "Find the kitchen" - will first look for a kitchen in view, then check the semantic map coordinates where - a kitchen was previously observed. - - CALL THIS SKILL FOR ONE SUBJECT AT A TIME. For example: "Go to the person wearing a blue shirt in the living room", - you should call this skill twice, once for the person wearing a blue shirt and once for the living room. - """ - - query: str = Field("", description="Text query to search for in the semantic map") - - limit: int = Field(1, description="Maximum number of results to return") - distance: float = Field(1.0, description="Desired distance to maintain from object in meters") - timeout: float = Field(40.0, description="Maximum time to spend navigating in seconds") - similarity_threshold: float = Field(0.25, description="Minimum similarity score required for semantic map results to be considered valid") - - def __init__(self, robot=None, **data): - """ - Initialize the Navigate skill. - - Args: - robot: The robot instance - **data: Additional data for configuration - """ - super().__init__(robot=robot, **data) - self._stop_event = threading.Event() - self._spatial_memory = None - self._scheduler = get_scheduler() # Use the shared DiMOS thread pool - self._navigation_disposable = None # Disposable returned by scheduler.schedule() - self._tracking_subscriber = None # For object tracking - - def _navigate_to_object(self): - """ - Helper method that attempts to navigate to an object visible in the camera view. - - Returns: - dict: Result dictionary with success status and details - """ - # Stop any existing operation - self._stop_event.clear() - - try: - logger.warning(f"Attempting to navigate to visible object: {self.query} with desired distance {self.distance}m, timeout {self.timeout} seconds...") - - # Try to get a bounding box from Qwen - only try once - bbox = None - try: - # Capture a single frame from the video stream - frame = self._robot.get_ros_video_stream().pipe(ops.take(1)).run() - # Use the frame-based function - bbox, object_size = get_bbox_from_qwen_frame(frame, object_name=self.query) - except Exception as e: - logger.error(f"Error querying Qwen: {e}") - return {"success": False, "failure_reason": "Perception", "error": f"Could not detect {self.query} in view: {e}"} - - if bbox is None or self._stop_event.is_set(): - logger.error(f"Failed to get bounding box for {self.query}") - return {"success": False, "failure_reason": "Perception", "error": f"Could not find {self.query} in view"} - - logger.info(f"Found {self.query} at {bbox} with size {object_size}") - - # Start the object tracker with the detected bbox - self._robot.object_tracker.track(bbox, frame=frame) - - # Get the first tracking data with valid distance and angle - start_time = time.time() - target_acquired = False - goal_x_robot = 0 - goal_y_robot = 0 - goal_angle = 0 - - while time.time() - start_time < 10.0 and not self._stop_event.is_set() and not target_acquired: - # Get the latest tracking data - tracking_data = self._robot.object_tracking_stream.pipe(ops.take(1)).run() - - if tracking_data and tracking_data.get("targets") and tracking_data["targets"]: - target = tracking_data["targets"][0] - - if "distance" in target and "angle" in target: - # Convert target distance and angle to xy coordinates in robot frame - goal_distance = target["distance"] - self.distance # Subtract desired distance to stop short - goal_angle = -target["angle"] - logger.info(f"Target distance: {goal_distance}, Target angle: {goal_angle}") - - goal_x_robot, goal_y_robot = distance_angle_to_goal_xy(goal_distance, goal_angle) - target_acquired = True - break - - else: - logger.warning(f"No valid target tracking data found. target: {target}") - - else: - logger.warning(f"No valid target tracking data found. tracking_data: {tracking_data}") - - time.sleep(0.1) - - if not target_acquired: - logger.error("Failed to acquire valid target tracking data") - return {"success": False, "failure_reason": "Perception", "error": "Failed to track object"} - - logger.info(f"Navigating to target at local coordinates: ({goal_x_robot:.2f}, {goal_y_robot:.2f}), angle: {goal_angle:.2f}") - - # Use navigate_to_goal_local instead of directly controlling the local planner - success = navigate_to_goal_local( - robot=self._robot, - goal_xy_robot=(goal_x_robot, goal_y_robot), - goal_theta=goal_angle, - distance=0.0, # We already accounted for desired distance - timeout=self.timeout, - stop_event=self._stop_event - ) - - if success: - logger.info(f"Successfully navigated to {self.query}") - return { - "success": True, - "failure_reason": None, - "query": self.query, - "message": f"Successfully navigated to {self.query} in view" - } - else: - logger.warning(f"Failed to reach {self.query} within timeout or operation was stopped") - return { - "success": False, - "failure_reason": "Navigation", - "error": f"Failed to reach {self.query} within timeout" - } - - except Exception as e: - logger.error(f"Error in navigate to object: {e}") - return {"success": False, "failure_reason": "Code Error", "error": f"Error: {e}"} - finally: - # Clean up - self._robot.ros_control.stop() - self._robot.object_tracker.cleanup() - - def _navigate_using_semantic_map(self): - """ - Helper method that attempts to navigate using the semantic map query. - - Returns: - dict: Result dictionary with success status and details - """ - logger.info(f"Querying semantic map for: '{self.query}'") - - try: - self._spatial_memory = self._robot.get_spatial_memory() - - # Run the query - results = self._spatial_memory.query_by_text(self.query, limit=self.limit) - - if not results: - logger.warning(f"No results found for query: '{self.query}'") - return { - "success": False, - "query": self.query, - "error": "No matching location found in semantic map" - } - - # Get the best match - best_match = results[0] - metadata = best_match.get('metadata', {}) - - if isinstance(metadata, list) and metadata: - metadata = metadata[0] - - # Extract coordinates from metadata - if isinstance(metadata, dict) and 'pos_x' in metadata and 'pos_y' in metadata and 'rot_z' in metadata: - pos_x = metadata.get('pos_x', 0) - pos_y = metadata.get('pos_y', 0) - theta = metadata.get('rot_z', 0) - - # Calculate similarity score (distance is inverse of similarity) - similarity = 1.0 - (best_match.get('distance', 0) if best_match.get('distance') is not None else 0) - - logger.info(f"Found match for '{self.query}' at ({pos_x:.2f}, {pos_y:.2f}, rotation {theta:.2f}) with similarity: {similarity:.4f}") - - # Check if similarity is below the threshold - if similarity < self.similarity_threshold: - logger.warning(f"Match found but similarity score ({similarity:.4f}) is below threshold ({self.similarity_threshold})") - return { - "success": False, - "query": self.query, - "position": (pos_x, pos_y), - "rotation": theta, - "similarity": similarity, - "error": f"Match found but similarity score ({similarity:.4f}) is below threshold ({self.similarity_threshold})" - } - - # Reset the stop event before starting navigation - self._stop_event.clear() - - # The scheduler approach isn't working, switch to direct threading - # Define a navigation function that will run on a separate thread - def run_navigation(): - skill_library = self._robot.get_skills() - self.register_as_running("Navigate", skill_library) - - try: - logger.info(f"Starting navigation to ({pos_x:.2f}, {pos_y:.2f}) with rotation {theta:.2f}") - # Pass our stop_event to allow cancellation - result = False - try: - result = self._robot.global_planner.set_goal((pos_x, pos_y), goal_theta = theta, stop_event=self._stop_event) - except Exception as e: - logger.error(f"Error calling global_planner.set_goal: {e}") - - if result: - logger.info("Navigation completed successfully") - else: - logger.error("Navigation did not complete successfully") - return result - except Exception as e: - logger.error(f"Unexpected error in navigation thread: {e}") - return False - finally: - self.stop() - - # Cancel any existing navigation before starting a new one - # Signal stop to any running navigation - self._stop_event.set() - # Clear stop event for new navigation - self._stop_event.clear() - - # Run the navigation in the main thread - run_navigation() - - return { - "success": True, - "query": self.query, - "position": (pos_x, pos_y), - "rotation": theta, - "similarity": similarity, - "metadata": metadata - } - else: - logger.warning(f"No valid position data found for query: '{self.query}'") - return { - "success": False, - "query": self.query, - "error": "No valid position data found in semantic map" - } - except Exception as e: - logger.error(f"Error in semantic map navigation: {e}") - return {"success": False, "error": f"Semantic map error: {e}"} - - def __call__(self): - """ - First attempts to navigate to an object in view, then falls back to querying the semantic map. - - Returns: - A dictionary with the result of the navigation attempt - """ - super().__call__() - - if not self.query: - error_msg = "No query provided to Navigate skill" - logger.error(error_msg) - return {"success": False, "error": error_msg} - - # First, try to find and navigate to the object in camera view - logger.info(f"First attempting to find and navigate to visible object: '{self.query}'") - object_result = self._navigate_to_object() - - if object_result and object_result['success']: - logger.info(f"Successfully navigated to {self.query} in view") - return object_result - - elif object_result and object_result['failure_reason'] == "Navigation": - logger.info(f"Failed to navigate to {self.query} in view: {object_result.get('error', 'Unknown error')}") - return object_result - - # If object navigation failed, fall back to semantic map - logger.info(f"Object not found in view. Falling back to semantic map query for: '{self.query}'") - - return self._navigate_using_semantic_map() - - - def stop(self): - """ - Stop the navigation skill and clean up resources. - - Returns: - A message indicating whether the navigation was stopped successfully - """ - logger.info("Stopping Navigate skill") - - # Signal any running processes to stop via the shared event - self._stop_event.set() - - skill_library = self._robot.get_skills() - self.unregister_as_running("Navigate", skill_library) - - # Dispose of any existing navigation task - if hasattr(self, '_navigation_disposable') and self._navigation_disposable: - logger.info("Disposing navigation task") - try: - self._navigation_disposable.dispose() - except Exception as e: - logger.error(f"Error disposing navigation task: {e}") - self._navigation_disposable = None - - # Clean up spatial memory if it exists - if hasattr(self, '_spatial_memory') and self._spatial_memory is not None: - logger.info("Cleaning up spatial memory") - self._spatial_memory.cleanup() - self._spatial_memory = None - - # Stop robot motion - self._robot.ros_control.stop() - - return "Navigate skill stopped successfully." - -class GetPose(AbstractRobotSkill): - """ - A skill that returns the current position and orientation of the robot. - - This skill is useful for getting the current pose of the robot in the map frame. You call this skill - if you want to remember a location, for example, "remember this is where my favorite chair is" and then - call this skill to get the position and rotation of approximately where the chair is. You can then use - the position to navigate to the chair. - - When location_name is provided, this skill will also remember the current location with that name, - allowing you to navigate back to it later using the Navigate skill. - """ - - location_name: str = Field("", description="Optional name to assign to this location (e.g., 'kitchen', 'office')") - - def __init__(self, robot=None, **data): - """ - Initialize the GetPose skill. - - Args: - robot: The robot instance - **data: Additional data for configuration - """ - super().__init__(robot=robot, **data) - - def __call__(self): - """ - Get the current pose of the robot. - - Returns: - A dictionary containing the position and rotation of the robot - """ - super().__call__() - - if self._robot is None: - error_msg = "No robot instance provided to GetPose skill" - logger.error(error_msg) - return {"success": False, "error": error_msg} - - try: - # Get the current pose using the robot's get_pose method - position, rotation = self._robot.get_pose() - - # Format the response - result = { - "success": True, - "position": { - "x": position[0], - "y": position[1], - "z": position[2] if len(position) > 2 else 0.0 - }, - "rotation": { - "roll": rotation[0], - "pitch": rotation[1], - "yaw": rotation[2] - } - } - - # If location_name is provided, remember this location - if self.location_name: - # Get the spatial memory instance - spatial_memory = self._robot.get_spatial_memory() - - # Create a RobotLocation object - location = RobotLocation( - name=self.location_name, - position=position, - rotation=rotation - ) - - # Add to spatial memory - if spatial_memory.add_robot_location(location): - result["location_saved"] = True - result["location_name"] = self.location_name - logger.info(f"Location '{self.location_name}' saved at {position}") - else: - result["location_saved"] = False - logger.error(f"Failed to save location '{self.location_name}'") - - return result - except Exception as e: - error_msg = f"Error getting robot pose: {e}" - logger.error(error_msg) - return {"success": False, "error": error_msg} - - -class NavigateToGoal(AbstractRobotSkill): - """ - A skill that navigates the robot to a specified position and orientation. - - This skill uses the global planner to generate a path to the target position - and then uses navigate_path_local to follow that path, achieving the desired - orientation at the goal position. - """ - - position: Tuple[float, float] = Field((0.0, 0.0), description="Target position (x, y) in map frame") - rotation: Optional[float] = Field(None, description="Target orientation (yaw) in radians") - frame: str = Field("map", description="Reference frame for the position and rotation") - timeout: float = Field(120.0, description="Maximum time (in seconds) allowed for navigation") - - def __init__(self, robot=None, **data): - """ - Initialize the NavigateToGoal skill. - - Args: - robot: The robot instance - **data: Additional data for configuration - """ - super().__init__(robot=robot, **data) - self._stop_event = threading.Event() - - def __call__(self): - """ - Navigate to the specified goal position and orientation. - - Returns: - A dictionary containing the result of the navigation attempt - """ - super().__call__() - - if self._robot is None: - error_msg = "No robot instance provided to NavigateToGoal skill" - logger.error(error_msg) - return {"success": False, "error": error_msg} - - # Reset stop event to make sure we don't immediately abort - self._stop_event.clear() - - skill_library = self._robot.get_skills() - self.register_as_running("NavigateToGoal", skill_library) - - logger.info(f"Starting navigation to position=({self.position[0]:.2f}, {self.position[1]:.2f}) " - f"with rotation={self.rotation if self.rotation is not None else 'None'} " - f"in frame={self.frame}") - - try: - # Use the global planner to set the goal and generate a path - result = self._robot.global_planner.set_goal( - self.position, - goal_theta=self.rotation, - stop_event=self._stop_event - ) - - if result: - logger.info("Navigation completed successfully") - return { - "success": True, - "position": self.position, - "rotation": self.rotation, - "message": "Goal reached successfully" - } - else: - logger.warning("Navigation did not complete successfully") - return { - "success": False, - "position": self.position, - "rotation": self.rotation, - "message": "Goal could not be reached" - } - - except Exception as e: - error_msg = f"Error during navigation: {e}" - logger.error(error_msg) - return { - "success": False, - "position": self.position, - "rotation": self.rotation, - "error": error_msg - } - finally: - self.stop() - - - def stop(self): - """ - Stop the navigation. - - Returns: - A message indicating that the navigation was stopped - """ - logger.info("Stopping NavigateToGoal") - skill_library = self._robot.get_skills() - self.unregister_as_running("NavigateToGoal", skill_library) - self._stop_event.set() - return "Navigation stopped" diff --git a/dimos/skills/observe_stream.py b/dimos/skills/observe_stream.py deleted file mode 100644 index e0e12e0b9f..0000000000 --- a/dimos/skills/observe_stream.py +++ /dev/null @@ -1,228 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Observer skill for an agent. - -This module provides a skill that periodically sends images from any -Robot Data Stream to an agent for inference. -""" - -import logging -import time -import threading -from typing import Optional, Any, Dict -import base64 -import cv2 -import reactivex as rx -from reactivex import operators as ops -from pydantic import Field - -from dimos.skills.skills import AbstractRobotSkill -from dimos.agents.agent import LLMAgent -from dimos.utils.threadpool import get_scheduler -from dimos.utils.logging_config import setup_logger - -logger = setup_logger("dimos.skills.observe_stream") - -class ObserveStream(AbstractRobotSkill): - """ - A skill that periodically Observes a Robot Video Stream and sends images to current instance of an agent for context. - - This skill runs in a non-halting manner, allowing other skills to run concurrently. - It can be used for continuous perception and passive monitoring, such as waiting for a person to enter a room - or to monitor changes in the environment. - """ - - timestep: float = Field(60.0, description="Time interval in seconds between observation queries") - query_text: str = Field("What do you see in this image? Alert me if you see any people or important changes.", - description="Query text to send to agent with each image") - max_duration: float = Field(0.0, description="Maximum duration to run the observer in seconds (0 for indefinite)") - - def __init__(self, robot=None, agent: Optional[LLMAgent] = None, video_stream = None, **data): - """ - Initialize the ObserveStream skill. - - Args: - robot: The robot instance - agent: The agent to send queries to - **data: Additional data for configuration - """ - super().__init__(robot=robot, **data) - self._agent = agent - self._stop_event = threading.Event() - self._monitor_thread = None - self._scheduler = get_scheduler() - self._subscription = None - - # Get the video stream - # TODO: Use the video stream provided in the constructor for dynamic video_stream selection by the agent - self._video_stream = self._robot.get_ros_video_stream() - if self._video_stream is None: - logger.error("Failed to get video stream from robot") - return - - def __call__(self): - """ - Start the observing process in a separate thread using the threadpool. - - Returns: - A message indicating the observer has started - """ - super().__call__() - - if self._agent is None: - error_msg = "No agent provided to ObserveStream" - logger.error(error_msg) - return error_msg - - if self._robot is None: - error_msg = "No robot instance provided to ObserveStream" - logger.error(error_msg) - return error_msg - - self.stop() - - self._stop_event.clear() - - # Initialize start time for duration tracking - self._start_time = time.time() - - interval_observable = rx.interval(self.timestep, scheduler=self._scheduler).pipe( - ops.take_while(lambda _: not self._stop_event.is_set()) - ) - - # Subscribe to the interval observable - self._subscription = interval_observable.subscribe( - on_next=self._monitor_iteration, - on_error=lambda e: logger.error(f"Error in monitor observable: {e}"), - on_completed=lambda: logger.info("Monitor observable completed") - ) - - skill_library = self._robot.get_skills() - self.register_as_running("ObserveStream", skill_library, self._subscription) - - logger.info(f"Observer started with timestep={self.timestep}s, query='{self.query_text}'") - return f"Observer started with timestep={self.timestep}s, query='{self.query_text}'" - - def _monitor_iteration(self, iteration): - """ - Execute a single observer iteration. - - Args: - iteration: The iteration number (provided by rx.interval) - """ - try: - if self.max_duration > 0: - elapsed_time = time.time() - self._start_time - if elapsed_time > self.max_duration: - logger.info(f"Observer reached maximum duration of {self.max_duration}s") - self.stop() - return - - logger.info(f"Observer iteration {iteration} executing") - - # Get a frame from the video stream - frame = self._get_frame_from_stream() - - if frame is not None: - self._process_frame(frame) - else: - logger.warning("Failed to get frame from video stream") - - except Exception as e: - logger.error(f"Error in monitor iteration {iteration}: {e}") - - def _get_frame_from_stream(self): - """ - Get a single frame from the video stream. - - Args: - video_stream: The ROS video stream observable - - Returns: - A single frame from the video stream, or None if no frame is available - """ - frame = None - - frame_subject = rx.subject.Subject() - - subscription = self._video_stream.pipe( - ops.take(1) # Take just one frame - ).subscribe( - on_next=lambda x: frame_subject.on_next(x), - on_error=lambda e: logger.error(f"Error getting frame: {e}") - ) - - timeout = 5.0 # 5 seconds timeout - start_time = time.time() - - def on_frame(f): - nonlocal frame - frame = f - - frame_subject.subscribe(on_frame) - - while frame is None and time.time() - start_time < timeout: - time.sleep(0.1) - - subscription.dispose() - - return frame - - def _process_frame(self, frame): - """ - Process a frame with the Claude agent. - - Args: - frame: The video frame to process - """ - logger.info("Processing frame with Claude agent") - - try: - _, buffer = cv2.imencode('.jpg', frame) - base64_image = base64.b64encode(buffer).decode('utf-8') - - observable = self._agent.run_observable_query( - f"{self.query_text}\n\nHere is the current camera view from the robot:", - base64_image=base64_image, - thinking_budget_tokens=0, - ) - - # Simple subscription to make sure the query executes - # The actual response content isn't important - observable.subscribe( - on_next=lambda x: logger.info(f"Got response from _observable_query: {x}"), - on_error=lambda e: logger.error(f"Error: {e}"), - on_completed=lambda: logger.info("ObserveStream query completed") - ) - - except Exception as e: - logger.error(f"Error processing frame with agent: {e}") - - def stop(self): - """ - Stop the ObserveStream monitoring process. - - Returns: - A message indicating the observer has stopped - """ - if self._subscription is not None and not self._subscription.is_disposed: - logger.info("Stopping ObserveStream") - self._stop_event.set() - self._subscription.dispose() - self._subscription = None - - return "Observer stopped" - return "Observer was not running" diff --git a/dimos/skills/rest/rest.py b/dimos/skills/rest/rest.py index a88429fba2..a8b5adfeb9 100644 --- a/dimos/skills/rest/rest.py +++ b/dimos/skills/rest/rest.py @@ -12,13 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging + +from pydantic import Field import requests + from dimos.skills.skills import AbstractSkill -from typing import Optional, Dict, Any -from pydantic import Field -import logging + logger = logging.getLogger(__name__) + class GenericRestSkill(AbstractSkill): """Performs a configurable REST API call. @@ -29,9 +32,10 @@ class GenericRestSkill(AbstractSkill): url: The target URL for the API call. method: The HTTP method (e.g., 'GET', 'POST'). Case-insensitive. timeout: Request timeout in seconds. - """ + """ + # TODO: Add query parameters, request body data (form-encoded or JSON), and headers. - #, query + # , query # parameters, request body data (form-encoded or JSON), and headers. # params: Optional dictionary of URL query parameters. # data: Optional dictionary for form-encoded request body data. @@ -46,7 +50,6 @@ class GenericRestSkill(AbstractSkill): # json_payload: Optional[Dict[str, Any]] = Field(default=None, alias="json", description="JSON request body.") # headers: Optional[Dict[str, str]] = Field(default=None, description="HTTP headers.") - def __call__(self) -> str: """Executes the configured REST API call. @@ -68,27 +71,31 @@ def __call__(self) -> str: try: logger.debug( f"Executing {self.method.upper()} request to {self.url} " - f"with timeout={self.timeout}" # , params={self.params}, " + f"with timeout={self.timeout}" # , params={self.params}, " # f"data={self.data}, json={self.json_payload}, headers={self.headers}" ) response = requests.request( - method=self.method.upper(), # Normalize method to uppercase + method=self.method.upper(), # Normalize method to uppercase url=self.url, # params=self.params, # data=self.data, # json=self.json_payload, # Use the attribute name defined in Pydantic # headers=self.headers, - timeout=self.timeout + timeout=self.timeout, ) response.raise_for_status() # Raises HTTPError for bad responses (4xx or 5xx) - logger.debug(f"Request successful. Status: {response.status_code}, Response: {response.text[:100]}...") - return response.text # Return text content directly + logger.debug( + f"Request successful. Status: {response.status_code}, Response: {response.text[:100]}..." + ) + return response.text # Return text content directly except requests.exceptions.HTTPError as http_err: - logger.error(f"HTTP error occurred: {http_err} - Status Code: {http_err.response.status_code}") + logger.error( + f"HTTP error occurred: {http_err} - Status Code: {http_err.response.status_code}" + ) return f"HTTP error making {self.method.upper()} request to {self.url}: {http_err.response.status_code} {http_err.response.reason}" except requests.exceptions.RequestException as req_err: logger.error(f"Request exception occurred: {req_err}") return f"Error making {self.method.upper()} request to {self.url}: {req_err}" except Exception as e: - logger.exception(f"An unexpected error occurred: {e}") # Log the full traceback + logger.exception(f"An unexpected error occurred: {e}") # Log the full traceback return f"An unexpected error occurred: {type(e).__name__}: {e}" diff --git a/dimos/skills/skills.py b/dimos/skills/skills.py index 8ea7262229..197c9e2fe0 100644 --- a/dimos/skills/skills.py +++ b/dimos/skills/skills.py @@ -12,13 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations + import logging -from typing import Any, Optional -from pydantic import BaseModel +from typing import TYPE_CHECKING, Any + from openai import pydantic_function_tool +from pydantic import BaseModel from dimos.types.constants import Colors +if TYPE_CHECKING: + from collections.abc import Iterator + # Configure logging for the module logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -26,61 +32,65 @@ # region SkillLibrary -class SkillLibrary: +class SkillLibrary: # ==== Flat Skill Library ==== - def __init__(self): - self.registered_skills: list["AbstractSkill"] = [] - self.class_skills: list["AbstractSkill"] = [] + def __init__(self) -> None: + self.registered_skills: list[AbstractSkill] = [] + self.class_skills: list[AbstractSkill] = [] self._running_skills = {} # {skill_name: (instance, subscription)} self.init() - - def init(self): + + def init(self) -> None: # Collect all skills from the parent class and update self.skills self.refresh_class_skills() # Temporary self.registered_skills = self.class_skills.copy() - def get_class_skills(self) -> list["AbstractSkill"]: + def get_class_skills(self) -> list[AbstractSkill]: """Extract all AbstractSkill subclasses from a class. - + Returns: List of skill classes found within the class """ skills = [] - + # Loop through all attributes of the class for attr_name in dir(self.__class__): # Skip special/dunder attributes - if attr_name.startswith('__'): + if attr_name.startswith("__"): continue - + try: attr = getattr(self.__class__, attr_name) - + # Check if it's a class and inherits from AbstractSkill - if isinstance(attr, type) and issubclass(attr, AbstractSkill) and attr is not AbstractSkill: + if ( + isinstance(attr, type) + and issubclass(attr, AbstractSkill) + and attr is not AbstractSkill + ): skills.append(attr) except (AttributeError, TypeError): # Skip attributes that can't be accessed or aren't classes continue - + return skills - def refresh_class_skills(self): + def refresh_class_skills(self) -> None: self.class_skills = self.get_class_skills() - def add(self, skill: "AbstractSkill") -> None: + def add(self, skill: AbstractSkill) -> None: if skill not in self.registered_skills: self.registered_skills.append(skill) - def get(self) -> list["AbstractSkill"]: + def get(self) -> list[AbstractSkill]: return self.registered_skills.copy() - def remove(self, skill: "AbstractSkill") -> None: + def remove(self, skill: AbstractSkill) -> None: try: self.registered_skills.remove(skill) except ValueError: @@ -89,72 +99,75 @@ def remove(self, skill: "AbstractSkill") -> None: def clear(self) -> None: self.registered_skills.clear() - def __iter__(self): + def __iter__(self) -> Iterator: return iter(self.registered_skills) def __len__(self) -> int: return len(self.registered_skills) - def __contains__(self, skill: "AbstractSkill") -> bool: + def __contains__(self, skill: AbstractSkill) -> bool: return skill in self.registered_skills - + def __getitem__(self, index): return self.registered_skills[index] - + # ==== Calling a Function ==== - _instances: dict[str, dict] = {} + _instances: dict[str, dict] = {} - def create_instance(self, name, **kwargs): + def create_instance(self, name: str, **kwargs) -> None: # Key based only on the name key = name - - print(f"Preparing to create instance with name: {name} and args: {kwargs}") if key not in self._instances: # Instead of creating an instance, store the args for later use self._instances[key] = kwargs - print(f"Stored args for later instance creation: {name} with args: {kwargs}") - - def call(self, name, **args): - # Get the stored args if available; otherwise, use an empty dict - stored_args = self._instances.get(name, {}) - - # Merge the arguments with priority given to stored arguments - complete_args = {**args, **stored_args} - - # Dynamically get the class from the module or current script - skill_class = getattr(self, name, None) - if skill_class is None: - for skill in self.get(): - if name == skill.__name__: - skill_class = skill - break + + def call(self, name: str, **args): + try: + # Get the stored args if available; otherwise, use an empty dict + stored_args = self._instances.get(name, {}) + + # Merge the arguments with priority given to stored arguments + complete_args = {**args, **stored_args} + + # Dynamically get the class from the module or current script + skill_class = getattr(self, name, None) if skill_class is None: - raise ValueError(f"Skill class not found: {name}") - - # Initialize the instance with the merged arguments - instance = skill_class(**complete_args) - print(f"Instance created and function called for: {name} with args: {complete_args}") - - # Call the instance directly - return instance() - + for skill in self.get(): + if name == skill.__name__: + skill_class = skill + break + if skill_class is None: + error_msg = f"Skill '{name}' is not available. Please check if it's properly registered." + logger.error(f"Skill class not found: {name}") + return error_msg + + # Initialize the instance with the merged arguments + instance = skill_class(**complete_args) + print(f"Instance created and function called for: {name} with args: {complete_args}") + + # Call the instance directly + return instance() + except Exception as e: + error_msg = f"Error executing skill '{name}': {e!s}" + logger.error(error_msg) + return error_msg + # ==== Tools ==== def get_tools(self) -> Any: tools_json = self.get_list_of_skills_as_json(list_of_skills=self.registered_skills) # print(f"{Colors.YELLOW_PRINT_COLOR}Tools JSON: {tools_json}{Colors.RESET_COLOR}") return tools_json - - def get_list_of_skills_as_json(self, list_of_skills: list["AbstractSkill"]) -> list[str]: + + def get_list_of_skills_as_json(self, list_of_skills: list[AbstractSkill]) -> list[str]: return list(map(pydantic_function_tool, list_of_skills)) - - - def register_running_skill(self, name: str, instance: Any, subscription=None): + + def register_running_skill(self, name: str, instance: Any, subscription=None) -> None: """ Register a running skill with its subscription. - + Args: name: Name of the skill (will be converted to lowercase) instance: Instance of the running skill @@ -163,14 +176,14 @@ def register_running_skill(self, name: str, instance: Any, subscription=None): name = name.lower() self._running_skills[name] = (instance, subscription) logger.info(f"Registered running skill: {name}") - - def unregister_running_skill(self, name: str): + + def unregister_running_skill(self, name: str) -> bool: """ Unregister a running skill. - + Args: name: Name of the skill to remove (will be converted to lowercase) - + Returns: True if the skill was found and removed, False otherwise """ @@ -180,49 +193,53 @@ def unregister_running_skill(self, name: str): logger.info(f"Unregistered running skill: {name}") return True return False - + def get_running_skills(self): """ Get all running skills. - + Returns: A dictionary of running skill names and their (instance, subscription) tuples """ return self._running_skills.copy() - + def terminate_skill(self, name: str): """ Terminate a running skill. - + Args: name: Name of the skill to terminate (will be converted to lowercase) - + Returns: A message indicating whether the skill was successfully terminated """ name = name.lower() if name in self._running_skills: instance, subscription = self._running_skills[name] - + try: # Call the stop method if it exists - if hasattr(instance, 'stop') and callable(instance.stop): - result = instance.stop() + if hasattr(instance, "stop") and callable(instance.stop): + instance.stop() logger.info(f"Stopped skill: {name}") else: logger.warning(f"Skill {name} does not have a stop method") - + # Also dispose the subscription if it exists - if subscription is not None and hasattr(subscription, 'dispose') and callable(subscription.dispose): + if ( + subscription is not None + and hasattr(subscription, "dispose") + and callable(subscription.dispose) + ): subscription.dispose() logger.info(f"Disposed subscription for skill: {name}") elif subscription is not None: logger.warning(f"Skill {name} has a subscription but it's not disposable") - + # unregister the skill self.unregister_running_skill(name) return f"Successfully terminated skill: {name}" - + except Exception as e: error_msg = f"Error terminating skill {name}: {e}" logger.error(error_msg) @@ -232,38 +249,40 @@ def terminate_skill(self, name: str): else: return f"No running skill found with name: {name}" + # endregion SkillLibrary # region AbstractSkill -class AbstractSkill(BaseModel): - def __init__(self, *args, **kwargs): +class AbstractSkill(BaseModel): + def __init__(self, *args, **kwargs) -> None: print("Initializing AbstractSkill Class") super().__init__(*args, **kwargs) self._instances = {} self._list_of_skills = [] # Initialize the list of skills print(f"Instances: {self._instances}") - def clone(self) -> "AbstractSkill": + def clone(self) -> AbstractSkill: return AbstractSkill() - - - def register_as_running(self, name: str, skill_library: SkillLibrary, subscription=None): + + def register_as_running( + self, name: str, skill_library: SkillLibrary, subscription=None + ) -> None: """ Register this skill as running in the skill library. - + Args: name: Name of the skill (will be converted to lowercase) skill_library: The skill library to register with subscription: Optional subscription associated with the skill """ skill_library.register_running_skill(name, self, subscription) - - def unregister_as_running(self, name: str, skill_library: SkillLibrary): + + def unregister_as_running(self, name: str, skill_library: SkillLibrary) -> None: """ Unregister this skill from the skill library. - + Args: name: Name of the skill to remove (will be converted to lowercase) skill_library: The skill library to unregister from @@ -276,33 +295,35 @@ def get_tools(self) -> Any: # print(f"Tools JSON: {tools_json}") return tools_json - def get_list_of_skills_as_json(self, list_of_skills: list["AbstractSkill"]) -> list[str]: + def get_list_of_skills_as_json(self, list_of_skills: list[AbstractSkill]) -> list[str]: return list(map(pydantic_function_tool, list_of_skills)) + # endregion AbstractSkill # region Abstract Robot Skill -from typing import Optional, TYPE_CHECKING +from typing import TYPE_CHECKING + if TYPE_CHECKING: from dimos.robot.robot import Robot else: - Robot = 'Robot' + Robot = "Robot" -class AbstractRobotSkill(AbstractSkill): +class AbstractRobotSkill(AbstractSkill): _robot: Robot = None - - def __init__(self, *args, robot: Optional[Robot] = None, **kwargs): + + def __init__(self, *args, robot: Robot | None = None, **kwargs) -> None: super().__init__(*args, **kwargs) self._robot = robot - print(f"{Colors.BLUE_PRINT_COLOR}" - f"Robot Skill Initialized with Robot: {robot}" - f"{Colors.RESET_COLOR}") + print( + f"{Colors.BLUE_PRINT_COLOR}Robot Skill Initialized with Robot: {robot}{Colors.RESET_COLOR}" + ) def set_robot(self, robot: Robot) -> None: """Set the robot reference for this skills instance. - + Args: robot: The robot instance to associate with these skills. """ @@ -313,8 +334,12 @@ def __call__(self): raise RuntimeError( f"{Colors.RED_PRINT_COLOR}" f"No Robot instance provided to Robot Skill: {self.__class__.__name__}" - f"{Colors.RESET_COLOR}") + f"{Colors.RESET_COLOR}" + ) else: - print(f"{Colors.BLUE_PRINT_COLOR}Robot Instance provided to Robot Skill: {self.__class__.__name__}{Colors.RESET_COLOR}") - + print( + f"{Colors.BLUE_PRINT_COLOR}Robot Instance provided to Robot Skill: {self.__class__.__name__}{Colors.RESET_COLOR}" + ) + + # endregion Abstract Robot Skill diff --git a/dimos/skills/speak.py b/dimos/skills/speak.py index 4e3bbbac9b..a1e3abb078 100644 --- a/dimos/skills/speak.py +++ b/dimos/skills/speak.py @@ -1,12 +1,26 @@ -from dimos.skills.skills import AbstractSkill -from dimos.stream.audio.pipelines import tts -from dimos.stream.audio.node_output import SounddeviceAudioOutput +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import queue +import threading +import time +from typing import Any + from pydantic import Field from reactivex import Subject -from typing import Optional, Any, List -import time -import threading -import queue + +from dimos.skills.skills import AbstractSkill from dimos.utils.logging_config import setup_logger logger = setup_logger("dimos.skills.speak") @@ -20,21 +34,21 @@ _queue_running = False -def _process_audio_queue(): +def _process_audio_queue() -> None: """Background thread to process audio requests sequentially""" global _queue_running - + while _queue_running: try: # Get the next queued audio task with a timeout task = _audio_queue.get(timeout=1.0) if task is None: # Sentinel value to stop the thread break - + # Execute the task (which is a function to be called) task() _audio_queue.task_done() - + except queue.Empty: # No tasks in queue, just continue waiting continue @@ -43,16 +57,14 @@ def _process_audio_queue(): # Continue processing other tasks -def start_audio_queue_processor(): +def start_audio_queue_processor() -> None: """Start the background thread for processing audio requests""" global _queue_processor_thread, _queue_running - + if _queue_processor_thread is None or not _queue_processor_thread.is_alive(): _queue_running = True _queue_processor_thread = threading.Thread( - target=_process_audio_queue, - daemon=True, - name="AudioQueueProcessor" + target=_process_audio_queue, daemon=True, name="AudioQueueProcessor" ) _queue_processor_thread.start() logger.info("Started audio queue processor thread") @@ -61,95 +73,96 @@ def start_audio_queue_processor(): # Start the queue processor when module is imported start_audio_queue_processor() + class Speak(AbstractSkill): """Speak text out loud to humans nearby or to other robots.""" text: str = Field(..., description="Text to speak") - def __init__(self, tts_node: Optional[Any] = None, **data): + def __init__(self, tts_node: Any | None = None, **data) -> None: super().__init__(**data) self._tts_node = tts_node self._audio_complete = threading.Event() self._subscription = None - self._subscriptions: List = [] # Track all subscriptions + self._subscriptions: list = [] # Track all subscriptions def __call__(self): if not self._tts_node: logger.error("No TTS node provided to Speak skill") return "Error: No TTS node available" - + # Create a result queue to get the result back from the audio thread result_queue = queue.Queue(1) - + # Define the speech task to run in the audio queue - def speak_task(): + def speak_task() -> None: try: # Using a lock to ensure exclusive access to audio device with _audio_device_lock: text_subject = Subject() self._audio_complete.clear() self._subscriptions = [] - + # This function will be called when audio processing is complete - def on_complete(): + def on_complete() -> None: logger.info(f"TTS audio playback completed for: {self.text}") self._audio_complete.set() - + # This function will be called if there's an error - def on_error(error): + def on_error(error) -> None: logger.error(f"Error in TTS processing: {error}") self._audio_complete.set() - + # Connect the Subject to the TTS node and keep the subscription self._tts_node.consume_text(text_subject) - + # Subscribe to the audio output to know when it's done self._subscription = self._tts_node.emit_text().subscribe( on_next=lambda text: logger.debug(f"TTS processing: {text}"), on_completed=on_complete, - on_error=on_error + on_error=on_error, ) self._subscriptions.append(self._subscription) - + # Emit the text to the Subject text_subject.on_next(self.text) text_subject.on_completed() # Signal that we're done sending text - + # Wait for audio playback to complete with a timeout # Using a dynamic timeout based on text length timeout = max(5, len(self.text) * 0.1) logger.debug(f"Waiting for TTS completion with timeout {timeout:.1f}s") - + if not self._audio_complete.wait(timeout=timeout): logger.warning(f"TTS timeout reached for: {self.text}") else: # Add a small delay after audio completes to ensure buffers are fully flushed time.sleep(0.3) - + # Clean up all subscriptions for sub in self._subscriptions: if sub: sub.dispose() self._subscriptions = [] - + # Successfully completed result_queue.put(f"Spoke: {self.text} successfully") except Exception as e: logger.error(f"Error in speak task: {e}") - result_queue.put(f"Error speaking text: {str(e)}") - + result_queue.put(f"Error speaking text: {e!s}") + # Add our speech task to the global queue for sequential processing - display_text = self.text[:50] + '...' if len(self.text) > 50 else self.text + display_text = self.text[:50] + "..." if len(self.text) > 50 else self.text logger.info(f"Queueing speech task: '{display_text}'") _audio_queue.put(speak_task) - + # Wait for the result with a timeout try: # Use a longer timeout than the audio playback itself text_len_timeout = len(self.text) * 0.15 # 150ms per character max_timeout = max(10, text_len_timeout) # At least 10 seconds - + return result_queue.get(timeout=max_timeout) except queue.Empty: logger.error("Timed out waiting for speech task to complete") - return f"Error: Timed out while speaking: {self.text}" \ No newline at end of file + return f"Error: Timed out while speaking: {self.text}" diff --git a/dimOS.egg-info/dependency_links.txt b/dimos/skills/unitree/__init__.py similarity index 100% rename from dimOS.egg-info/dependency_links.txt rename to dimos/skills/unitree/__init__.py diff --git a/dimos/skills/unitree/unitree_speak.py b/dimos/skills/unitree/unitree_speak.py new file mode 100644 index 0000000000..539ca0cd29 --- /dev/null +++ b/dimos/skills/unitree/unitree_speak.py @@ -0,0 +1,280 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +import hashlib +import json +import os +import tempfile +import time + +from go2_webrtc_driver.constants import RTC_TOPIC +import numpy as np +from openai import OpenAI +from pydantic import Field +import soundfile as sf + +from dimos.skills.skills import AbstractRobotSkill +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.skills.unitree.unitree_speak") + +# Audio API constants (from go2_webrtc_driver) +AUDIO_API = { + "GET_AUDIO_LIST": 1001, + "SELECT_START_PLAY": 1002, + "PAUSE": 1003, + "UNSUSPEND": 1004, + "SET_PLAY_MODE": 1007, + "UPLOAD_AUDIO_FILE": 2001, + "ENTER_MEGAPHONE": 4001, + "EXIT_MEGAPHONE": 4002, + "UPLOAD_MEGAPHONE": 4003, +} + +PLAY_MODES = {"NO_CYCLE": "no_cycle", "SINGLE_CYCLE": "single_cycle", "LIST_LOOP": "list_loop"} + + +class UnitreeSpeak(AbstractRobotSkill): + """Speak text out loud through the robot's speakers using WebRTC audio upload.""" + + text: str = Field(..., description="Text to speak") + voice: str = Field( + default="echo", description="Voice to use (alloy, echo, fable, onyx, nova, shimmer)" + ) + speed: float = Field(default=1.2, description="Speech speed (0.25 to 4.0)") + use_megaphone: bool = Field( + default=False, description="Use megaphone mode for lower latency (experimental)" + ) + + def __init__(self, **data) -> None: + super().__init__(**data) + self._openai_client = None + + def _get_openai_client(self): + if self._openai_client is None: + self._openai_client = OpenAI() + return self._openai_client + + def _generate_audio(self, text: str) -> bytes: + try: + client = self._get_openai_client() + response = client.audio.speech.create( + model="tts-1", voice=self.voice, input=text, speed=self.speed, response_format="mp3" + ) + return response.content + except Exception as e: + logger.error(f"Error generating audio: {e}") + raise + + def _webrtc_request(self, api_id: int, parameter: dict | None = None): + if parameter is None: + parameter = {} + + request_data = {"api_id": api_id, "parameter": json.dumps(parameter) if parameter else "{}"} + + return self._robot.connection.publish_request(RTC_TOPIC["AUDIO_HUB_REQ"], request_data) + + def _upload_audio_to_robot(self, audio_data: bytes, filename: str) -> str: + try: + file_md5 = hashlib.md5(audio_data).hexdigest() + b64_data = base64.b64encode(audio_data).decode("utf-8") + + chunk_size = 61440 + chunks = [b64_data[i : i + chunk_size] for i in range(0, len(b64_data), chunk_size)] + total_chunks = len(chunks) + + logger.info(f"Uploading audio '{filename}' in {total_chunks} chunks (optimized)") + + for i, chunk in enumerate(chunks, 1): + parameter = { + "file_name": filename, + "file_type": "wav", + "file_size": len(audio_data), + "current_block_index": i, + "total_block_number": total_chunks, + "block_content": chunk, + "current_block_size": len(chunk), + "file_md5": file_md5, + "create_time": int(time.time() * 1000), + } + + logger.debug(f"Sending chunk {i}/{total_chunks}") + self._webrtc_request(AUDIO_API["UPLOAD_AUDIO_FILE"], parameter) + + logger.info(f"Audio upload completed for '{filename}'") + + list_response = self._webrtc_request(AUDIO_API["GET_AUDIO_LIST"], {}) + + if list_response and "data" in list_response: + data_str = list_response.get("data", {}).get("data", "{}") + audio_list = json.loads(data_str).get("audio_list", []) + + for audio in audio_list: + if audio.get("CUSTOM_NAME") == filename: + return audio.get("UNIQUE_ID") + + logger.warning( + f"Could not find uploaded audio '{filename}' in list, using filename as UUID" + ) + return filename + + except Exception as e: + logger.error(f"Error uploading audio to robot: {e}") + raise + + def _play_audio_on_robot(self, uuid: str): + try: + self._webrtc_request(AUDIO_API["SET_PLAY_MODE"], {"play_mode": PLAY_MODES["NO_CYCLE"]}) + time.sleep(0.1) + + parameter = {"unique_id": uuid} + + logger.info(f"Playing audio with UUID: {uuid}") + self._webrtc_request(AUDIO_API["SELECT_START_PLAY"], parameter) + + except Exception as e: + logger.error(f"Error playing audio on robot: {e}") + raise + + def _stop_audio_playback(self) -> None: + try: + logger.debug("Stopping audio playback") + self._webrtc_request(AUDIO_API["PAUSE"], {}) + except Exception as e: + logger.warning(f"Error stopping audio playback: {e}") + + def _upload_and_play_megaphone(self, audio_data: bytes, duration: float): + try: + logger.debug("Entering megaphone mode") + self._webrtc_request(AUDIO_API["ENTER_MEGAPHONE"], {}) + + time.sleep(0.2) + + b64_data = base64.b64encode(audio_data).decode("utf-8") + + chunk_size = 4096 + chunks = [b64_data[i : i + chunk_size] for i in range(0, len(b64_data), chunk_size)] + total_chunks = len(chunks) + + logger.info(f"Uploading megaphone audio in {total_chunks} chunks") + + for i, chunk in enumerate(chunks, 1): + parameter = { + "current_block_size": len(chunk), + "block_content": chunk, + "current_block_index": i, + "total_block_number": total_chunks, + } + + logger.debug(f"Sending megaphone chunk {i}/{total_chunks}") + self._webrtc_request(AUDIO_API["UPLOAD_MEGAPHONE"], parameter) + + if i < total_chunks: + time.sleep(0.05) + + logger.info("Megaphone audio upload completed, waiting for playback") + + time.sleep(duration + 1.0) + + except Exception as e: + logger.error(f"Error in megaphone mode: {e}") + try: + self._webrtc_request(AUDIO_API["EXIT_MEGAPHONE"], {}) + except: + pass + raise + finally: + try: + logger.debug("Exiting megaphone mode") + self._webrtc_request(AUDIO_API["EXIT_MEGAPHONE"], {}) + time.sleep(0.1) + except Exception as e: + logger.warning(f"Error exiting megaphone mode: {e}") + + def __call__(self) -> str: + super().__call__() + + if not self._robot: + logger.error("No robot instance provided to UnitreeSpeak skill") + return "Error: No robot instance available" + + try: + display_text = self.text[:50] + "..." if len(self.text) > 50 else self.text + logger.info(f"Speaking: '{display_text}'") + + logger.debug("Generating audio with OpenAI TTS") + audio_data = self._generate_audio(self.text) + + with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as tmp_mp3: + tmp_mp3.write(audio_data) + tmp_mp3_path = tmp_mp3.name + + try: + audio_array, sample_rate = sf.read(tmp_mp3_path) + + if audio_array.ndim > 1: + audio_array = np.mean(audio_array, axis=1) + + target_sample_rate = 22050 + if sample_rate != target_sample_rate: + logger.debug(f"Resampling from {sample_rate}Hz to {target_sample_rate}Hz") + old_length = len(audio_array) + new_length = int(old_length * target_sample_rate / sample_rate) + old_indices = np.arange(old_length) + new_indices = np.linspace(0, old_length - 1, new_length) + audio_array = np.interp(new_indices, old_indices, audio_array) + sample_rate = target_sample_rate + + audio_array = audio_array / np.max(np.abs(audio_array)) + + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_wav: + sf.write(tmp_wav.name, audio_array, sample_rate, format="WAV", subtype="PCM_16") + tmp_wav.seek(0) + wav_data = open(tmp_wav.name, "rb").read() + os.unlink(tmp_wav.name) + + logger.info( + f"Audio size: {len(wav_data) / 1024:.1f}KB, duration: {len(audio_array) / sample_rate:.1f}s" + ) + + finally: + os.unlink(tmp_mp3_path) + + if self.use_megaphone: + logger.debug("Using megaphone mode for lower latency") + duration = len(audio_array) / sample_rate + self._upload_and_play_megaphone(wav_data, duration) + + return f"Spoke: '{display_text}' on robot successfully (megaphone mode)" + else: + filename = f"speak_{int(time.time() * 1000)}" + + logger.debug("Uploading audio to robot") + uuid = self._upload_audio_to_robot(wav_data, filename) + + logger.debug("Playing audio on robot") + self._play_audio_on_robot(uuid) + + duration = len(audio_array) / sample_rate + logger.debug(f"Waiting {duration:.1f}s for playback to complete") + # time.sleep(duration + 0.2) + + # self._stop_audio_playback() + + return f"Spoke: '{display_text}' on robot successfully" + + except Exception as e: + logger.error(f"Error in speak skill: {e}") + return f"Error speaking text: {e!s}" diff --git a/dimos/skills/visual_navigation_skills.py b/dimos/skills/visual_navigation_skills.py index 1bc6d91341..8064f28cc9 100644 --- a/dimos/skills/visual_navigation_skills.py +++ b/dimos/skills/visual_navigation_skills.py @@ -19,82 +19,96 @@ and navigating to specific objects using computer vision. """ -import time import logging import threading -from typing import Optional, Tuple +import time + +from pydantic import Field +from dimos.perception.visual_servoing import VisualServoing from dimos.skills.skills import AbstractRobotSkill +from dimos.types.vector import Vector from dimos.utils.logging_config import setup_logger -from dimos.perception.visual_servoing import VisualServoing -from pydantic import Field logger = setup_logger("dimos.skills.visual_navigation", level=logging.DEBUG) + class FollowHuman(AbstractRobotSkill): """ - A skill that makes the robot follow a human using visual servoing continuously. - + A skill that makes the robot follow a human using visual servoing continuously. + This skill uses the robot's person tracking stream to follow a human while maintaining a specified distance. It will keep following the human until the timeout is reached or the skill is stopped. Don't use this skill if you want to navigate to a specific person, use NavigateTo instead. """ - - distance: float = Field(1.5, description="Desired distance to maintain from the person in meters") + + distance: float = Field( + 1.5, description="Desired distance to maintain from the person in meters" + ) timeout: float = Field(20.0, description="Maximum time to follow the person in seconds") - point: Optional[Tuple[int, int]] = Field(None, description="Optional point to start tracking (x,y pixel coordinates)") - - def __init__(self, robot=None, **data): + point: tuple[int, int] | None = Field( + None, description="Optional point to start tracking (x,y pixel coordinates)" + ) + + def __init__(self, robot=None, **data) -> None: super().__init__(robot=robot, **data) self._stop_event = threading.Event() self._visual_servoing = None - + def __call__(self): """ Start following a human using visual servoing. - + Returns: bool: True if successful, False otherwise """ super().__call__() - - if not hasattr(self._robot, 'person_tracking_stream') or self._robot.person_tracking_stream is None: + + if ( + not hasattr(self._robot, "person_tracking_stream") + or self._robot.person_tracking_stream is None + ): logger.error("Robot does not have a person tracking stream") return False - + # Stop any existing operation self.stop() self._stop_event.clear() - + success = False - + try: # Initialize visual servoing - self._visual_servoing = VisualServoing(tracking_stream=self._robot.person_tracking_stream) - + self._visual_servoing = VisualServoing( + tracking_stream=self._robot.person_tracking_stream + ) + logger.warning(f"Following human for {self.timeout} seconds...") start_time = time.time() - + # Start tracking - track_success = self._visual_servoing.start_tracking(point=self.point, desired_distance=self.distance) - + track_success = self._visual_servoing.start_tracking( + point=self.point, desired_distance=self.distance + ) + if not track_success: logger.error("Failed to start tracking") return False - + # Main follow loop - while (self._visual_servoing.running and - time.time() - start_time < self.timeout and - not self._stop_event.is_set()): - + while ( + self._visual_servoing.running + and time.time() - start_time < self.timeout + and not self._stop_event.is_set() + ): output = self._visual_servoing.updateTracking() x_vel = output.get("linear_vel") z_vel = output.get("angular_vel") logger.debug(f"Following human: x_vel: {x_vel}, z_vel: {z_vel}") - self._robot.ros_control.move_vel_control(x=x_vel, y=0, yaw=z_vel) + self._robot.move(Vector(x_vel, 0, z_vel)) time.sleep(0.05) - + # If we completed the full timeout duration, consider it success if time.time() - start_time >= self.timeout: success = True @@ -103,9 +117,9 @@ def __call__(self): logger.info("Human following stopped externally") else: logger.info("Human following stopped due to tracking loss") - + return success - + except Exception as e: logger.error(f"Error in follow human: {e}") return False @@ -114,25 +128,21 @@ def __call__(self): if self._visual_servoing: self._visual_servoing.stop_tracking() self._visual_servoing = None - self._robot.ros_control.stop() - - def stop(self): + + def stop(self) -> bool: """ Stop the human following process. - + Returns: bool: True if stopped, False if it wasn't running """ if self._visual_servoing is not None: logger.info("Stopping FollowHuman skill") self._stop_event.set() - + # Clean up visual servoing if it exists self._visual_servoing.stop_tracking() self._visual_servoing = None - - # Stop the robot - self._robot.ros_control.stop() - + return True - return False \ No newline at end of file + return False diff --git a/dimos/stream/audio/base.py b/dimos/stream/audio/base.py index ab368c476c..43c3c13dec 100644 --- a/dimos/stream/audio/base.py +++ b/dimos/stream/audio/base.py @@ -1,6 +1,21 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from abc import ABC, abstractmethod -from reactivex import Observable + import numpy as np +from reactivex import Observable class AbstractAudioEmitter(ABC): @@ -46,7 +61,7 @@ class AudioEvent: def __init__( self, data: np.ndarray, sample_rate: int, timestamp: float, channels: int = 1 - ): + ) -> None: """ Initialize an AudioEvent. diff --git a/dimos/stream/audio/node_key_recorder.py b/dimos/stream/audio/node_key_recorder.py index bc603d3623..5e918bae5c 100644 --- a/dimos/stream/audio/node_key_recorder.py +++ b/dimos/stream/audio/node_key_recorder.py @@ -1,15 +1,28 @@ #!/usr/bin/env python3 -from typing import List -import numpy as np -import time -import threading -import sys +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import select +import sys +import threading +import time + +import numpy as np from reactivex import Observable -from reactivex.subject import Subject, ReplaySubject +from reactivex.subject import ReplaySubject, Subject from dimos.stream.audio.base import AbstractAudioTransform, AudioEvent - from dimos.utils.logging_config import setup_logger logger = setup_logger("dimos.audio.key_recorder") @@ -25,7 +38,7 @@ def __init__( self, max_recording_time: float = 120.0, always_subscribe: bool = False, - ): + ) -> None: """ Initialize KeyRecorder. @@ -99,7 +112,7 @@ def emit_recording(self) -> Observable: """ return self._recording_subject - def stop(self): + def stop(self) -> None: """Stop recording and clean up resources.""" logger.info("Stopping audio recorder") @@ -117,7 +130,7 @@ def stop(self): if self._input_thread.is_alive(): self._input_thread.join(1.0) - def _input_monitor(self): + def _input_monitor(self) -> None: """Monitor for key presses to toggle recording.""" logger.info("Press Enter to start/stop recording...") @@ -134,7 +147,7 @@ def _input_monitor(self): # Sleep a bit to reduce CPU usage time.sleep(0.1) - def _start_recording(self): + def _start_recording(self) -> None: """Start recording audio and subscribe to the audio source if not always subscribed.""" if not self._audio_observable: logger.error("Cannot start recording: No audio source has been set") @@ -154,7 +167,7 @@ def _start_recording(self): self._audio_buffer = [] logger.info("Recording... (press Enter to stop)") - def _stop_recording(self): + def _stop_recording(self) -> None: """Stop recording, unsubscribe from audio source if not always subscribed, and emit the combined audio event.""" self._is_recording = False recording_duration = time.time() - self._recording_start_time @@ -174,7 +187,7 @@ def _stop_recording(self): else: logger.warning("No audio was recorded") - def _process_audio_event(self, audio_event): + def _process_audio_event(self, audio_event) -> None: """Process incoming audio events.""" # Only buffer if recording @@ -198,17 +211,20 @@ def _process_audio_event(self, audio_event): logger.warning(f"Max recording time ({self.max_recording_time}s) reached") self._stop_recording() - def _combine_audio_events(self, audio_events: List[AudioEvent]) -> AudioEvent: + def _combine_audio_events(self, audio_events: list[AudioEvent]) -> AudioEvent: """Combine multiple audio events into a single event.""" if not audio_events: logger.warning("Attempted to combine empty audio events list") return None # Filter out any empty events that might cause broadcasting errors - valid_events = [event for event in audio_events if event is not None and - (hasattr(event, 'data') and event.data is not None and - event.data.size > 0)] - + valid_events = [ + event + for event in audio_events + if event is not None + and (hasattr(event, "data") and event.data is not None and event.data.size > 0) + ] + if not valid_events: logger.warning("No valid audio events to combine") return None @@ -219,12 +235,12 @@ def _combine_audio_events(self, audio_events: List[AudioEvent]) -> AudioEvent: # Calculate total samples only from valid events total_samples = sum(event.data.shape[0] for event in valid_events) - + # Safety check - if somehow we got no samples if total_samples <= 0: logger.warning(f"Combined audio would have {total_samples} samples - aborting") return None - + # For multichannel audio, data shape could be (samples,) or (samples, channels) if len(first_event.data.shape) == 1: # 1D audio data (mono) @@ -250,10 +266,12 @@ def _combine_audio_events(self, audio_events: List[AudioEvent]) -> AudioEvent: combined_data[offset : offset + samples] = event.data offset += samples except ValueError as e: - logger.error(f"Error combining audio events: {e}. " - f"Event shape: {event.data.shape}, " - f"Combined shape: {combined_data.shape}, " - f"Offset: {offset}, Samples: {samples}") + logger.error( + f"Error combining audio events: {e}. " + f"Event shape: {event.data.shape}, " + f"Combined shape: {combined_data.shape}, " + f"Offset: {offset}, Samples: {samples}" + ) # Continue with next event instead of failing completely # Create new audio event with the combined data @@ -268,11 +286,11 @@ def _combine_audio_events(self, audio_events: List[AudioEvent]) -> AudioEvent: logger.warning("Failed to create valid combined audio event") return None - def _handle_error(self, error): + def _handle_error(self, error) -> None: """Handle errors from the observable.""" logger.error(f"Error in audio observable: {error}") - def _handle_completion(self): + def _handle_completion(self) -> None: """Handle completion of the observable.""" logger.info("Audio observable completed") self.stop() @@ -282,9 +300,9 @@ def _handle_completion(self): from dimos.stream.audio.node_microphone import ( SounddeviceAudioSource, ) + from dimos.stream.audio.node_normalizer import AudioNormalizer from dimos.stream.audio.node_output import SounddeviceAudioOutput from dimos.stream.audio.node_volume_monitor import monitor - from dimos.stream.audio.node_normalizer import AudioNormalizer from dimos.stream.audio.utils import keepalive # Create microphone source, recorder, and audio output diff --git a/dimos/stream/audio/node_microphone.py b/dimos/stream/audio/node_microphone.py index 6157344bb3..1f4bf13499 100644 --- a/dimos/stream/audio/node_microphone.py +++ b/dimos/stream/audio/node_microphone.py @@ -1,15 +1,29 @@ #!/usr/bin/env python3 -from dimos.stream.audio.base import ( - AbstractAudioEmitter, - AudioEvent, -) +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +from typing import Any import numpy as np -from typing import Optional, List, Dict, Any from reactivex import Observable, create, disposable -import time import sounddevice as sd +from dimos.stream.audio.base import ( + AbstractAudioEmitter, + AudioEvent, +) from dimos.utils.logging_config import setup_logger logger = setup_logger("dimos.audio.node_microphone") @@ -20,12 +34,12 @@ class SounddeviceAudioSource(AbstractAudioEmitter): def __init__( self, - device_index: Optional[int] = None, + device_index: int | None = None, sample_rate: int = 16000, channels: int = 1, block_size: int = 1024, dtype: np.dtype = np.float32, - ): + ) -> None: """ Initialize SounddeviceAudioSource. @@ -55,7 +69,7 @@ def emit_audio(self) -> Observable: def on_subscribe(observer, scheduler): # Callback function to process audio data - def audio_callback(indata, frames, time_info, status): + def audio_callback(indata, frames, time_info, status) -> None: if status: logger.warning(f"Audio callback status: {status}") @@ -92,7 +106,7 @@ def audio_callback(indata, frames, time_info, status): observer.on_error(e) # Return a disposable to clean up resources - def dispose(): + def dispose() -> None: logger.info("Stopping audio capture") self._running = False if self._stream: @@ -104,7 +118,7 @@ def dispose(): return create(on_subscribe) - def get_available_devices(self) -> List[Dict[str, Any]]: + def get_available_devices(self) -> list[dict[str, Any]]: """Get a list of available audio input devices.""" return sd.query_devices() diff --git a/dimos/stream/audio/node_normalizer.py b/dimos/stream/audio/node_normalizer.py index 7297a44cae..064fc3cf6c 100644 --- a/dimos/stream/audio/node_normalizer.py +++ b/dimos/stream/audio/node_normalizer.py @@ -1,19 +1,32 @@ #!/usr/bin/env python3 -from typing import Callable +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Callable import numpy as np from reactivex import Observable, create, disposable -from dimos.utils.logging_config import setup_logger -from dimos.stream.audio.volume import ( - calculate_rms_volume, - calculate_peak_volume, -) from dimos.stream.audio.base import ( AbstractAudioTransform, AudioEvent, ) - +from dimos.stream.audio.volume import ( + calculate_peak_volume, + calculate_rms_volume, +) +from dimos.utils.logging_config import setup_logger logger = setup_logger("dimos.stream.audio.node_normalizer") @@ -34,7 +47,7 @@ def __init__( decay_factor: float = 0.999, adapt_speed: float = 0.05, volume_func: Callable[[np.ndarray], float] = calculate_peak_volume, - ): + ) -> None: """ Initialize AudioNormalizer. @@ -138,12 +151,11 @@ def on_subscribe(observer, scheduler): ) logger.info( - f"Started audio normalizer with target level: {self.target_level}, " - f"max gain: {self.max_gain}" + f"Started audio normalizer with target level: {self.target_level}, max gain: {self.max_gain}" ) # Return a disposable to clean up resources - def dispose(): + def dispose() -> None: logger.info("Stopping audio normalizer") audio_subscription.dispose() @@ -154,12 +166,13 @@ def dispose(): if __name__ == "__main__": import sys + from dimos.stream.audio.node_microphone import ( SounddeviceAudioSource, ) + from dimos.stream.audio.node_output import SounddeviceAudioOutput from dimos.stream.audio.node_simulated import SimulatedAudioSource from dimos.stream.audio.node_volume_monitor import monitor - from dimos.stream.audio.node_output import SounddeviceAudioOutput from dimos.stream.audio.utils import keepalive # Parse command line arguments @@ -191,9 +204,7 @@ def dispose(): print("Using simulated audio source") # Select volume function - volume_func = ( - calculate_rms_volume if volume_method == "rms" else calculate_peak_volume - ) + volume_func = calculate_rms_volume if volume_method == "rms" else calculate_peak_volume # Create normalizer normalizer = AudioNormalizer(target_level=target_level, volume_func=volume_func) diff --git a/dimos/stream/audio/node_output.py b/dimos/stream/audio/node_output.py index e0b6c7a7bd..3dc93d3757 100644 --- a/dimos/stream/audio/node_output.py +++ b/dimos/stream/audio/node_output.py @@ -1,13 +1,28 @@ #!/usr/bin/env python3 -from typing import Optional, List, Dict, Any +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any + import numpy as np -import sounddevice as sd from reactivex import Observable +import sounddevice as sd -from dimos.utils.logging_config import setup_logger from dimos.stream.audio.base import ( AbstractAudioTransform, ) +from dimos.utils.logging_config import setup_logger logger = setup_logger("dimos.stream.audio.node_output") @@ -23,12 +38,12 @@ class SounddeviceAudioOutput(AbstractAudioTransform): def __init__( self, - device_index: Optional[int] = None, + device_index: int | None = None, sample_rate: int = 16000, channels: int = 1, block_size: int = 1024, dtype: np.dtype = np.float32, - ): + ) -> None: """ Initialize SounddeviceAudioOutput. @@ -104,7 +119,7 @@ def emit_audio(self) -> Observable: return self.audio_observable - def stop(self): + def stop(self) -> None: """Stop audio output and clean up resources.""" logger.info("Stopping audio output") self._running = False @@ -118,7 +133,7 @@ def stop(self): self._stream.close() self._stream = None - def _play_audio_event(self, audio_event): + def _play_audio_event(self, audio_event) -> None: """Play audio from an AudioEvent.""" if not self._running or not self._stream: return @@ -136,11 +151,11 @@ def _play_audio_event(self, audio_event): except Exception as e: logger.error(f"Error playing audio: {e}") - def _handle_error(self, error): + def _handle_error(self, error) -> None: """Handle errors from the observable.""" logger.error(f"Error in audio observable: {error}") - def _handle_completion(self): + def _handle_completion(self) -> None: """Handle completion of the observable.""" logger.info("Audio observable completed") self._running = False @@ -149,7 +164,7 @@ def _handle_completion(self): self._stream.close() self._stream = None - def get_available_devices(self) -> List[Dict[str, Any]]: + def get_available_devices(self) -> list[dict[str, Any]]: """Get a list of available audio output devices.""" return sd.query_devices() diff --git a/dimos/stream/audio/node_simulated.py b/dimos/stream/audio/node_simulated.py index 146f976c03..82de718ced 100644 --- a/dimos/stream/audio/node_simulated.py +++ b/dimos/stream/audio/node_simulated.py @@ -1,12 +1,27 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import threading +import time + +import numpy as np +from reactivex import Observable, create, disposable + from dimos.stream.audio.abstract import ( AbstractAudioEmitter, AudioEvent, ) -import numpy as np -from reactivex import Observable, create, disposable -import threading -import time - from dimos.utils.logging_config import setup_logger logger = setup_logger("dimos.stream.audio.node_simulated") @@ -26,7 +41,7 @@ def __init__( modulation_rate: float = 0.5, # Modulation rate in Hz volume_oscillation: bool = True, # Enable sinusoidal volume changes volume_oscillation_rate: float = 0.2, # Volume oscillation rate in Hz - ): + ) -> None: """ Initialize SimulatedAudioSource. @@ -64,9 +79,7 @@ def _generate_sine_wave(self, time_points: np.ndarray) -> np.ndarray: # Add frequency modulation for more interesting sounds if self.modulation_rate > 0: # Modulate frequency between 0.5x and 1.5x the base frequency - freq_mod = self.frequency * ( - 1.0 + 0.5 * np.sin(2 * np.pi * self.modulation_rate * t) - ) + freq_mod = self.frequency * (1.0 + 0.5 * np.sin(2 * np.pi * self.modulation_rate * t)) else: freq_mod = np.ones_like(t) * self.frequency @@ -80,20 +93,11 @@ def _generate_sine_wave(self, time_points: np.ndarray) -> np.ndarray: wave = np.sign(np.sin(phase_arg)) elif self.waveform == "triangle": wave = ( - 2 - * np.abs( - 2 - * ( - phase_arg / (2 * np.pi) - - np.floor(phase_arg / (2 * np.pi) + 0.5) - ) - ) + 2 * np.abs(2 * (phase_arg / (2 * np.pi) - np.floor(phase_arg / (2 * np.pi) + 0.5))) - 1 ) elif self.waveform == "sawtooth": - wave = 2 * ( - phase_arg / (2 * np.pi) - np.floor(0.5 + phase_arg / (2 * np.pi)) - ) + wave = 2 * (phase_arg / (2 * np.pi) - np.floor(0.5 + phase_arg / (2 * np.pi))) else: # Default to sine wave wave = np.sin(phase_arg) @@ -104,9 +108,7 @@ def _generate_sine_wave(self, time_points: np.ndarray) -> np.ndarray: vol_t = t + self.volume_phase # Volume oscillates between 0.0 and 1.0 using a sine wave (complete silence to full volume) - volume_factor = 0.5 + 0.5 * np.sin( - 2 * np.pi * self.volume_oscillation_rate * vol_t - ) + volume_factor = 0.5 + 0.5 * np.sin(2 * np.pi * self.volume_oscillation_rate * vol_t) # Apply the volume factor wave *= volume_factor * 0.7 @@ -117,9 +119,7 @@ def _generate_sine_wave(self, time_points: np.ndarray) -> np.ndarray: ) # Update phase for next frame - self.phase += ( - time_points[-1] - time_points[0] + (time_points[1] - time_points[0]) - ) + self.phase += time_points[-1] - time_points[0] + (time_points[1] - time_points[0]) # Add a second channel if needed if self.channels == 2: @@ -133,7 +133,7 @@ def _generate_sine_wave(self, time_points: np.ndarray) -> np.ndarray: return wave - def _audio_thread(self, observer, interval: float): + def _audio_thread(self, observer, interval: float) -> None: """Thread function for simulated audio generation.""" try: sample_index = 0 @@ -142,8 +142,7 @@ def _audio_thread(self, observer, interval: float): while self._running: # Calculate time points for this frame time_points = ( - np.arange(sample_index, sample_index + self.frame_length) - / self.sample_rate + np.arange(sample_index, sample_index + self.frame_length) / self.sample_rate ) # Generate audio data @@ -199,7 +198,7 @@ def on_subscribe(observer, scheduler): ) # Return a disposable to clean up - def dispose(): + def dispose() -> None: logger.info("Stopping simulated audio") self._running = False if self._thread and self._thread.is_alive(): @@ -211,9 +210,9 @@ def dispose(): if __name__ == "__main__": - from dimos.stream.audio.utils import keepalive - from dimos.stream.audio.node_volume_monitor import monitor from dimos.stream.audio.node_output import SounddeviceAudioOutput + from dimos.stream.audio.node_volume_monitor import monitor + from dimos.stream.audio.utils import keepalive source = SimulatedAudioSource() speaker = SounddeviceAudioOutput() diff --git a/dimos/stream/audio/node_volume_monitor.py b/dimos/stream/audio/node_volume_monitor.py index af1ddaf098..e1c5b226a4 100644 --- a/dimos/stream/audio/node_volume_monitor.py +++ b/dimos/stream/audio/node_volume_monitor.py @@ -1,8 +1,23 @@ #!/usr/bin/env python3 -from typing import Callable +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Callable + from reactivex import Observable, create, disposable -from dimos.stream.audio.base import AudioEvent, AbstractAudioConsumer +from dimos.stream.audio.base import AbstractAudioConsumer, AudioEvent from dimos.stream.audio.text.base import AbstractTextEmitter from dimos.stream.audio.text.node_stdout import TextPrinterNode from dimos.stream.audio.volume import calculate_peak_volume @@ -21,7 +36,7 @@ def __init__( threshold: float = 0.01, bar_length: int = 50, volume_func: Callable = calculate_peak_volume, - ): + ) -> None: """ Initialize VolumeMonitorNode. @@ -87,7 +102,7 @@ def on_subscribe(observer, scheduler): logger.info(f"Starting volume monitor (method: {self.func_name})") # Subscribe to the audio source - def on_audio_event(event: AudioEvent): + def on_audio_event(event: AudioEvent) -> None: try: # Calculate volume volume = self.volume_func(event.data) @@ -109,7 +124,7 @@ def on_audio_event(event: AudioEvent): ) # Return a disposable to clean up resources - def dispose(): + def dispose() -> None: logger.info("Stopping volume monitor") subscription.dispose() @@ -153,8 +168,8 @@ def monitor( if __name__ == "__main__": - from utils import keepalive from audio.node_simulated import SimulatedAudioSource + from utils import keepalive # Use the monitor function to create and connect the nodes volume_monitor = monitor(SimulatedAudioSource().emit_audio()) diff --git a/dimos/stream/audio/pipelines.py b/dimos/stream/audio/pipelines.py index 1865cdfa38..ceaeb80fac 100644 --- a/dimos/stream/audio/pipelines.py +++ b/dimos/stream/audio/pipelines.py @@ -1,11 +1,25 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.stream.audio.node_key_recorder import KeyRecorder from dimos.stream.audio.node_microphone import SounddeviceAudioSource from dimos.stream.audio.node_normalizer import AudioNormalizer -from dimos.stream.audio.node_volume_monitor import monitor -from dimos.stream.audio.node_key_recorder import KeyRecorder from dimos.stream.audio.node_output import SounddeviceAudioOutput +from dimos.stream.audio.node_volume_monitor import monitor from dimos.stream.audio.stt.node_whisper import WhisperNode -from dimos.stream.audio.tts.node_openai import OpenAITTSNode, Voice from dimos.stream.audio.text.node_stdout import TextPrinterNode +from dimos.stream.audio.tts.node_openai import OpenAITTSNode, Voice def stt(): @@ -28,7 +42,7 @@ def stt(): def tts(): - tts_node = OpenAITTSNode(speed=1.2, voice = Voice.ONYX) + tts_node = OpenAITTSNode(speed=1.2, voice=Voice.ONYX) agent_text_printer = TextPrinterNode(prefix="AGENT: ") agent_text_printer.consume_text(tts_node.emit_text()) diff --git a/dimos/stream/audio/stt/node_whisper.py b/dimos/stream/audio/stt/node_whisper.py index 5f152f7d26..05ec5274c8 100644 --- a/dimos/stream/audio/stt/node_whisper.py +++ b/dimos/stream/audio/stt/node_whisper.py @@ -1,11 +1,26 @@ #!/usr/bin/env python3 -from typing import Dict, Any +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any + from reactivex import Observable, create, disposable import whisper from dimos.stream.audio.base import ( - AudioEvent, AbstractAudioConsumer, + AudioEvent, ) from dimos.stream.audio.text.base import AbstractTextEmitter from dimos.utils.logging_config import setup_logger @@ -21,8 +36,10 @@ class WhisperNode(AbstractAudioConsumer, AbstractTextEmitter): def __init__( self, model: str = "base", - modelopts: Dict[str, Any] = {"language": "en", "fp16": False}, - ): + modelopts: dict[str, Any] | None = None, + ) -> None: + if modelopts is None: + modelopts = {"language": "en", "fp16": False} self.audio_observable = None self.modelopts = modelopts self.model = whisper.load_model(model) @@ -54,11 +71,9 @@ def on_subscribe(observer, scheduler): logger.info("Starting Whisper transcription service") # Subscribe to the audio source - def on_audio_event(event: AudioEvent): + def on_audio_event(event: AudioEvent) -> None: try: - result = self.model.transcribe( - event.data.flatten(), **self.modelopts - ) + result = self.model.transcribe(event.data.flatten(), **self.modelopts) observer.on_next(result["text"].strip()) except Exception as e: logger.error(f"Error processing audio event: {e}") @@ -72,7 +87,7 @@ def on_audio_event(event: AudioEvent): ) # Return a disposable to clean up resources - def dispose(): + def dispose() -> None: subscription.dispose() return disposable.Disposable(dispose) @@ -81,13 +96,13 @@ def dispose(): if __name__ == "__main__": + from dimos.stream.audio.node_key_recorder import KeyRecorder from dimos.stream.audio.node_microphone import ( SounddeviceAudioSource, ) + from dimos.stream.audio.node_normalizer import AudioNormalizer from dimos.stream.audio.node_output import SounddeviceAudioOutput from dimos.stream.audio.node_volume_monitor import monitor - from dimos.stream.audio.node_normalizer import AudioNormalizer - from dimos.stream.audio.node_key_recorder import KeyRecorder from dimos.stream.audio.text.node_stdout import TextPrinterNode from dimos.stream.audio.tts.node_openai import OpenAITTSNode from dimos.stream.audio.utils import keepalive diff --git a/dimos/stream/audio/text/base.py b/dimos/stream/audio/text/base.py index 63620ca8ee..b7305c0bcc 100644 --- a/dimos/stream/audio/text/base.py +++ b/dimos/stream/audio/text/base.py @@ -1,4 +1,19 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from abc import ABC, abstractmethod + from reactivex import Observable diff --git a/dimos/stream/audio/text/node_stdout.py b/dimos/stream/audio/text/node_stdout.py index beb99770f9..b0a5fd4ac8 100644 --- a/dimos/stream/audio/text/node_stdout.py +++ b/dimos/stream/audio/text/node_stdout.py @@ -1,5 +1,20 @@ #!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from reactivex import Observable + from dimos.stream.audio.text.base import AbstractTextConsumer from dimos.utils.logging_config import setup_logger @@ -11,7 +26,7 @@ class TextPrinterNode(AbstractTextConsumer): A node that subscribes to a text observable and prints the text. """ - def __init__(self, prefix: str = "", suffix: str = "", end: str = "\n"): + def __init__(self, prefix: str = "", suffix: str = "", end: str = "\n") -> None: """ Initialize TextPrinterNode. @@ -58,6 +73,7 @@ def consume_text(self, text_observable: Observable) -> "AbstractTextConsumer": if __name__ == "__main__": import time + from reactivex import Subject # Create a simple text subject that we can push values to diff --git a/dimos/stream/audio/tts/node_openai.py b/dimos/stream/audio/tts/node_openai.py index fb9c7d9a39..211b2b0246 100644 --- a/dimos/stream/audio/tts/node_openai.py +++ b/dimos/stream/audio/tts/node_openai.py @@ -1,19 +1,32 @@ #!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from enum import Enum +import io import threading import time -from enum import Enum -from typing import Optional + +from openai import OpenAI from reactivex import Observable, Subject -import io import soundfile as sf -from openai import OpenAI -from dimos.stream.audio.text.base import AbstractTextConsumer, AbstractTextEmitter from dimos.stream.audio.base import ( AbstractAudioEmitter, AudioEvent, ) - +from dimos.stream.audio.text.base import AbstractTextConsumer, AbstractTextEmitter from dimos.utils.logging_config import setup_logger logger = setup_logger("dimos.stream.audio.tts.openai") @@ -41,12 +54,12 @@ class OpenAITTSNode(AbstractTextConsumer, AbstractAudioEmitter, AbstractTextEmit def __init__( self, - api_key: Optional[str] = None, + api_key: str | None = None, voice: Voice = Voice.ECHO, model: str = "tts-1", buffer_size: int = 1024, speed: float = 1.0, - ): + ) -> None: """ Initialize OpenAITTSNode. @@ -205,10 +218,12 @@ def dispose(self) -> None: if __name__ == "__main__": import time - from dimos.stream.audio.utils import keepalive + from reactivex import Subject + from dimos.stream.audio.node_output import SounddeviceAudioOutput from dimos.stream.audio.text.node_stdout import TextPrinterNode + from dimos.stream.audio.utils import keepalive # Create a simple text subject that we can push values to text_subject = Subject() @@ -233,7 +248,7 @@ def dispose(self) -> None: print("Starting OpenAI TTS test...") print("-" * 60) - for i, message in enumerate(test_messages): + for _i, message in enumerate(test_messages): text_subject.on_next(message) keepalive() diff --git a/dimos/stream/audio/tts/node_pytts.py b/dimos/stream/audio/tts/node_pytts.py index bbf33e9326..f1543331ef 100644 --- a/dimos/stream/audio/tts/node_pytts.py +++ b/dimos/stream/audio/tts/node_pytts.py @@ -1,9 +1,22 @@ #!/usr/bin/env python3 -from reactivex import Observable, Subject +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import pyttsx3 +from reactivex import Observable, Subject from dimos.stream.audio.text.abstract import AbstractTextTransform - from dimos.utils.logging_config import setup_logger logger = setup_logger(__name__) @@ -17,7 +30,7 @@ class PyTTSNode(AbstractTextTransform): text observables, allowing it to be inserted into a text processing pipeline. """ - def __init__(self, rate: int = 200, volume: float = 1.0): + def __init__(self, rate: int = 200, volume: float = 1.0) -> None: """ Initialize PyTTSNode. diff --git a/dimos/stream/audio/utils.py b/dimos/stream/audio/utils.py index bc7d15c37f..1a2991467c 100644 --- a/dimos/stream/audio/utils.py +++ b/dimos/stream/audio/utils.py @@ -1,7 +1,21 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import time -def keepalive(): +def keepalive() -> None: try: # Keep the program running print("Press Ctrl+C to exit") diff --git a/dimos/stream/audio/volume.py b/dimos/stream/audio/volume.py index 991f363e31..bd137172b3 100644 --- a/dimos/stream/audio/volume.py +++ b/dimos/stream/audio/volume.py @@ -1,4 +1,18 @@ #!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import numpy as np @@ -55,6 +69,7 @@ def calculate_peak_volume(audio_data: np.ndarray) -> float: if __name__ == "__main__": # Example usage import time + from .node_simulated import SimulatedAudioSource # Create a simulated audio source @@ -63,19 +78,19 @@ def calculate_peak_volume(audio_data: np.ndarray) -> float: # Create observable and subscribe to get a single frame audio_observable = audio_source.capture_audio_as_observable() - def process_frame(frame): + def process_frame(frame) -> None: # Calculate and print both RMS and peak volumes rms_vol = calculate_rms_volume(frame.data) peak_vol = calculate_peak_volume(frame.data) print(f"RMS Volume: {rms_vol:.4f}") print(f"Peak Volume: {peak_vol:.4f}") - print(f"Ratio (Peak/RMS): {peak_vol/rms_vol:.2f}") + print(f"Ratio (Peak/RMS): {peak_vol / rms_vol:.2f}") # Set a flag to track when processing is complete processed = {"done": False} - def process_frame_wrapper(frame): + def process_frame_wrapper(frame) -> None: # Process the frame process_frame(frame) # Mark as processed diff --git a/dimos/stream/data_provider.py b/dimos/stream/data_provider.py index 4f4398b137..f931857fda 100644 --- a/dimos/stream/data_provider.py +++ b/dimos/stream/data_provider.py @@ -12,67 +12,68 @@ # See the License for the specific language governing permissions and # limitations under the License. -from abc import ABC, abstractmethod -from reactivex import Subject, Observable -from reactivex.subject import Subject -from reactivex.scheduler import ThreadPoolScheduler -import multiprocessing +from abc import ABC import logging +import multiprocessing import reactivex as rx -from reactivex import operators as ops +from reactivex import Observable, Subject, operators as ops +from reactivex.scheduler import ThreadPoolScheduler +from reactivex.subject import Subject logging.basicConfig(level=logging.INFO) # Create a thread pool scheduler for concurrent processing pool_scheduler = ThreadPoolScheduler(multiprocessing.cpu_count()) + class AbstractDataProvider(ABC): """Abstract base class for data providers using ReactiveX.""" - - def __init__(self, dev_name: str = "NA"): + + def __init__(self, dev_name: str = "NA") -> None: self.dev_name = dev_name self._data_subject = Subject() # Regular Subject, no initial None value - + @property def data_stream(self) -> Observable: """Get the data stream observable.""" return self._data_subject - - def push_data(self, data): + + def push_data(self, data) -> None: """Push new data to the stream.""" self._data_subject.on_next(data) - - def dispose(self): + + def dispose(self) -> None: """Cleanup resources.""" self._data_subject.dispose() + class ROSDataProvider(AbstractDataProvider): """ReactiveX data provider for ROS topics.""" - - def __init__(self, dev_name: str = "ros_provider"): + + def __init__(self, dev_name: str = "ros_provider") -> None: super().__init__(dev_name) self.logger = logging.getLogger(dev_name) - - def push_data(self, data): + + def push_data(self, data) -> None: """Push new data to the stream.""" print(f"ROSDataProvider pushing data of type: {type(data)}") super().push_data(data) print("Data pushed to subject") - - def capture_data_as_observable(self, fps: int = None) -> Observable: + + def capture_data_as_observable(self, fps: int | None = None) -> Observable: """Get the data stream as an observable. - + Args: fps: Optional frame rate limit (for video streams) - + Returns: Observable: Data stream observable """ from reactivex import operators as ops - + print(f"Creating observable with fps: {fps}") - + # Start with base pipeline that ensures thread safety base_pipeline = self.data_stream.pipe( # Ensure emissions are handled on thread pool @@ -81,10 +82,10 @@ def capture_data_as_observable(self, fps: int = None) -> Observable: ops.do_action( on_next=lambda x: print(f"Got frame in pipeline: {type(x)}"), on_error=lambda e: print(f"Pipeline error: {e}"), - on_completed=lambda: print("Pipeline completed") - ) + on_completed=lambda: print("Pipeline completed"), + ), ) - + # If fps is specified, add rate limiting if fps and fps > 0: print(f"Adding rate limiting at {fps} FPS") @@ -92,53 +93,54 @@ def capture_data_as_observable(self, fps: int = None) -> Observable: # Use scheduler for time-based operations ops.sample(1.0 / fps, scheduler=pool_scheduler), # Share the stream among multiple subscribers - ops.share() + ops.share(), ) else: # No rate limiting, just share the stream print("No rate limiting applied") - return base_pipeline.pipe( - ops.share() - ) - + return base_pipeline.pipe(ops.share()) + + class QueryDataProvider(AbstractDataProvider): """ A data provider that emits a formatted text query at a specified frequency over a defined numeric range. - + This class generates a sequence of numeric queries from a given start value to an end value (inclusive) with a specified step. Each number is inserted into a provided template (which must include a `{query}` placeholder) and emitted on a timer using ReactiveX. - + Attributes: dev_name (str): The name of the data provider. logger (logging.Logger): Logger instance for logging messages. """ - - def __init__(self, dev_name: str = "query_provider"): + + def __init__(self, dev_name: str = "query_provider") -> None: """ Initializes the QueryDataProvider. - + Args: dev_name (str): The name of the data provider. Defaults to "query_provider". """ super().__init__(dev_name) self.logger = logging.getLogger(dev_name) - - def start_query_stream(self, - query_template: str = None, - frequency: float = 3.0, - start_count: int = 0, - end_count: int = 5000, - step: int = 250) -> None: + + def start_query_stream( + self, + query_template: str | None = None, + frequency: float = 3.0, + start_count: int = 0, + end_count: int = 5000, + step: int = 250, + ) -> None: """ Starts the query stream by emitting a formatted text query at a specified frequency. - + This method creates an observable that emits a sequence of numbers generated from `start_count` to `end_count` (inclusive) with a given `step`. Each number is then formatted using the `query_template`. The formatted query is pushed to the internal data stream. - + Args: - query_template (str): The template string for formatting queries. It must contain the + query_template (str): The template string for formatting queries. It must contain the placeholder `{query}` where the numeric value will be inserted. If None, a default template is used. frequency (float): The frequency (in seconds) at which queries are emitted. Defaults to 3.0. @@ -155,14 +157,14 @@ def start_query_stream(self, "If the number is equal to or above 2000, then clear debris. " "IF YOU DO NOT FOLLOW THESE INSTRUCTIONS EXACTLY, YOU WILL DIE!!!" ) - + # Generate the sequence of numeric queries. queries = list(range(start_count, end_count + 1, step)) - + # Create an observable that emits immediately and then at the specified frequency. timer = rx.timer(0, frequency) query_source = rx.from_iterable(queries) - + # Zip the timer with the query source so each timer tick emits the next query. query_stream = timer.pipe( ops.zip(query_source), @@ -173,8 +175,8 @@ def start_query_stream(self, # on_error=lambda e: self.logger.error(f"Query stream error: {e}"), # on_completed=lambda: self.logger.info("Query stream completed") # ), - ops.share() + ops.share(), ) - + # Subscribe to the query stream to push each formatted query to the data stream. query_stream.subscribe(lambda q: self.push_data(q)) diff --git a/dimos/stream/frame_processor.py b/dimos/stream/frame_processor.py index 73d418af25..fda13ece61 100644 --- a/dimos/stream/frame_processor.py +++ b/dimos/stream/frame_processor.py @@ -12,17 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -from datetime import timedelta +import os + import cv2 import numpy as np -import os -from reactivex import Observable -from reactivex import operators as ops -from typing import Callable, Tuple, Optional +from reactivex import Observable, operators as ops + # TODO: Reorganize, filenaming - Consider merger with VideoOperators class class FrameProcessor: - def __init__(self, output_dir=f'{os.getcwd()}/assets/output/frames', delete_on_init=False): + def __init__( + self, output_dir: str = f"{os.getcwd()}/assets/output/frames", delete_on_init: bool = False + ) -> None: """Initializes the FrameProcessor. Sets up the output directory for frame storage and optionally cleans up @@ -43,8 +44,7 @@ def __init__(self, output_dir=f'{os.getcwd()}/assets/output/frames', delete_on_i if delete_on_init: try: - jpg_files = [f for f in os.listdir(self.output_dir) - if f.lower().endswith('.jpg')] + jpg_files = [f for f in os.listdir(self.output_dir) if f.lower().endswith(".jpg")] for file in jpg_files: file_path = os.path.join(self.output_dir, file) os.remove(file_path) @@ -52,9 +52,9 @@ def __init__(self, output_dir=f'{os.getcwd()}/assets/output/frames', delete_on_i except Exception as e: print(f"Error cleaning up JPG files: {e}") raise - - self.image_count = 1 - # TODO: Add randomness to jpg folder storage naming. + + self.image_count = 1 + # TODO: Add randomness to jpg folder storage naming. # Will overwrite between sessions. def to_grayscale(self, frame): @@ -66,14 +66,14 @@ def to_grayscale(self, frame): def edge_detection(self, frame): return cv2.Canny(frame, 100, 200) - def resize(self, frame, scale=0.5): + def resize(self, frame, scale: float = 0.5): return cv2.resize(frame, None, fx=scale, fy=scale, interpolation=cv2.INTER_AREA) - def export_to_jpeg(self, frame, save_limit=100, loop=False, suffix=""): + def export_to_jpeg(self, frame, save_limit: int = 100, loop: bool = False, suffix: str = ""): if frame is None: print("Error: Attempted to save a None image.") return None - + # Check if the image has an acceptable number of channels if len(frame.shape) == 3 and frame.shape[2] not in [1, 3, 4]: print(f"Error: Frame with shape {frame.shape} has unsupported number of channels.") @@ -85,18 +85,18 @@ def export_to_jpeg(self, frame, save_limit=100, loop=False, suffix=""): self.image_count = 1 else: return frame - - filepath = os.path.join(self.output_dir, f'{self.image_count}_{suffix}.jpg') + + filepath = os.path.join(self.output_dir, f"{self.image_count}_{suffix}.jpg") cv2.imwrite(filepath, frame) self.image_count += 1 return frame def compute_optical_flow( self, - acc: Tuple[np.ndarray, np.ndarray, Optional[float]], + acc: tuple[np.ndarray, np.ndarray, float | None], current_frame: np.ndarray, - compute_relevancy: bool = True - ) -> Tuple[np.ndarray, np.ndarray, Optional[float]]: + compute_relevancy: bool = True, + ) -> tuple[np.ndarray, np.ndarray, float | None]: """Computes optical flow between consecutive frames. Uses the Farneback algorithm to compute dense optical flow between the @@ -122,7 +122,7 @@ def compute_optical_flow( ValueError: If input frames have invalid dimensions or types. TypeError: If acc is not a tuple of correct types. """ - prev_frame, prev_flow, prev_relevancy = acc + prev_frame, _prev_flow, _prev_relevancy = acc if prev_frame is None: return (current_frame, None, None) @@ -171,10 +171,7 @@ def process_stream_to_greyscale(self, frame_stream): ops.map(self.to_grayscale), ) - def process_stream_optical_flow( - self, - frame_stream: Observable - ) -> Observable: + def process_stream_optical_flow(self, frame_stream: Observable) -> Observable: """Processes video stream to compute and visualize optical flow. Computes optical flow between consecutive frames and generates a color-coded @@ -207,20 +204,17 @@ def process_stream_optical_flow( return frame_stream.pipe( ops.scan( lambda acc, frame: self.compute_optical_flow(acc, frame, compute_relevancy=False), - (None, None, None) + (None, None, None), ), ops.map(lambda result: result[1]), # Extract flow component ops.filter(lambda flow: flow is not None), ops.map(self.visualize_flow), ) - - def process_stream_optical_flow_with_relevancy( - self, - frame_stream: Observable - ) -> Observable: + + def process_stream_optical_flow_with_relevancy(self, frame_stream: Observable) -> Observable: """Processes video stream to compute optical flow with movement relevancy. - Applies optical flow computation to each frame and returns both the + Applies optical flow computation to each frame and returns both the visualized flow and a relevancy score indicating the amount of movement. The relevancy score is calculated as the mean magnitude of flow vectors. This method includes relevancy computation for motion detection. @@ -254,22 +248,21 @@ def process_stream_optical_flow_with_relevancy( return frame_stream.pipe( ops.scan( lambda acc, frame: self.compute_optical_flow(acc, frame, compute_relevancy=True), - (None, None, None) + (None, None, None), ), # Result is (current_frame, flow, relevancy) ops.filter(lambda result: result[1] is not None), # Filter out None flows - ops.map(lambda result: ( - self.visualize_flow(result[1]), # Visualized flow - result[2] # Relevancy score - )), - ops.filter(lambda result: result[0] is not None) # Ensure valid visualization + ops.map( + lambda result: ( + self.visualize_flow(result[1]), # Visualized flow + result[2], # Relevancy score + ) + ), + ops.filter(lambda result: result[0] is not None), # Ensure valid visualization ) def process_stream_with_jpeg_export( - self, - frame_stream: Observable, - suffix: str = "", - loop: bool = False + self, frame_stream: Observable, suffix: str = "", loop: bool = False ) -> Observable: """Processes stream by saving frames as JPEGs while passing them through. diff --git a/dimos/stream/ros_video_provider.py b/dimos/stream/ros_video_provider.py index c7d0ca999b..5182ca79f8 100644 --- a/dimos/stream/ros_video_provider.py +++ b/dimos/stream/ros_video_provider.py @@ -18,15 +18,13 @@ and makes them available as an Observable stream. """ -from reactivex import Subject, Observable -from reactivex import operators as ops -from reactivex.scheduler import ThreadPoolScheduler import logging import time -from typing import Any, Optional + import numpy as np +from reactivex import Observable, Subject, operators as ops +from reactivex.scheduler import ThreadPoolScheduler -from dimos.utils.threadpool import get_scheduler from dimos.stream.video_provider import AbstractVideoProvider logging.basicConfig(level=logging.INFO) @@ -34,21 +32,21 @@ class ROSVideoProvider(AbstractVideoProvider): """Video provider that uses a Subject to broadcast frames pushed by ROS. - + This class implements a video provider that receives frames from ROS and makes them available as an Observable stream. It uses ReactiveX's Subject to broadcast frames. - + Attributes: logger: Logger instance for this provider. _subject: ReactiveX Subject that broadcasts frames. _last_frame_time: Timestamp of the last received frame. """ - def __init__(self, - dev_name: str = "ros_video", - pool_scheduler: Optional[ThreadPoolScheduler] = None): + def __init__( + self, dev_name: str = "ros_video", pool_scheduler: ThreadPoolScheduler | None = None + ) -> None: """Initialize the ROS video provider. - + Args: dev_name: A string identifying this provider. pool_scheduler: Optional ThreadPoolScheduler for multithreading. @@ -61,11 +59,11 @@ def __init__(self, def push_data(self, frame: np.ndarray) -> None: """Push a new frame into the provider. - + Args: frame: The video frame to push into the stream, typically a numpy array containing image data. - + Raises: Exception: If there's an error pushing the frame. """ @@ -74,7 +72,7 @@ def push_data(self, frame: np.ndarray) -> None: if self._last_frame_time: frame_interval = current_time - self._last_frame_time self.logger.debug( - f"Frame interval: {frame_interval:.3f}s ({1/frame_interval:.1f} FPS)" + f"Frame interval: {frame_interval:.3f}s ({1 / frame_interval:.1f} FPS)" ) self._last_frame_time = current_time @@ -87,15 +85,15 @@ def push_data(self, frame: np.ndarray) -> None: def capture_video_as_observable(self, fps: int = 30) -> Observable: """Return an observable of video frames. - + Args: fps: Frames per second rate limit (default: 30; ignored for now). - + Returns: Observable: An observable stream of video frames (numpy.ndarray objects), with each emission containing a single video frame. The frames are multicast to all subscribers. - + Note: The fps parameter is currently not enforced. See implementation note below. """ diff --git a/dimos/stream/rtsp_video_provider.py b/dimos/stream/rtsp_video_provider.py index a6650c52b1..3aeb651a4d 100644 --- a/dimos/stream/rtsp_video_provider.py +++ b/dimos/stream/rtsp_video_provider.py @@ -14,11 +14,9 @@ """RTSP video provider using ffmpeg for robust stream handling.""" -import logging import subprocess import threading import time -from typing import Optional import ffmpeg # ffmpeg-python wrapper import numpy as np @@ -29,11 +27,9 @@ from reactivex.scheduler import ThreadPoolScheduler from dimos.utils.logging_config import setup_logger -from dimos.stream.frame_processor import FrameProcessor -from dimos.stream.video_operators import VideoOperators as vops + # Assuming AbstractVideoProvider and exceptions are in the sibling file -from .video_provider import (AbstractVideoProvider, VideoFrameError, - VideoSourceError, get_scheduler) +from .video_provider import AbstractVideoProvider, VideoFrameError, VideoSourceError logger = setup_logger("dimos.stream.rtsp_video_provider") @@ -46,10 +42,9 @@ class RtspVideoProvider(AbstractVideoProvider): built-in VideoCapture for RTSP. """ - def __init__(self, - dev_name: str, - rtsp_url: str, - pool_scheduler: Optional[ThreadPoolScheduler] = None) -> None: + def __init__( + self, dev_name: str, rtsp_url: str, pool_scheduler: ThreadPoolScheduler | None = None + ) -> None: """Initializes the RTSP video provider. Args: @@ -60,7 +55,7 @@ def __init__(self, super().__init__(dev_name, pool_scheduler) self.rtsp_url = rtsp_url # Holds the currently active ffmpeg process Popen object - self._ffmpeg_process: Optional[subprocess.Popen] = None + self._ffmpeg_process: subprocess.Popen | None = None # Lock to protect access to the ffmpeg process object self._lock = threading.Lock() @@ -71,46 +66,50 @@ def _get_stream_info(self) -> dict: # Probe the stream without the problematic timeout argument probe = ffmpeg.probe(self.rtsp_url) except ffmpeg.Error as e: - stderr = e.stderr.decode('utf8', errors='ignore') if e.stderr else 'No stderr' + stderr = e.stderr.decode("utf8", errors="ignore") if e.stderr else "No stderr" msg = f"({self.dev_name}) Failed to probe RTSP stream {self.rtsp_url}: {stderr}" logger.error(msg) raise VideoSourceError(msg) from e except Exception as e: - msg = f"({self.dev_name}) Unexpected error during probing {self.rtsp_url}: {e}" - logger.error(msg) - raise VideoSourceError(msg) from e - + msg = f"({self.dev_name}) Unexpected error during probing {self.rtsp_url}: {e}" + logger.error(msg) + raise VideoSourceError(msg) from e video_stream = next( - (stream for stream in probe.get('streams', []) if stream.get('codec_type') == 'video'), - None) + (stream for stream in probe.get("streams", []) if stream.get("codec_type") == "video"), + None, + ) if video_stream is None: msg = f"({self.dev_name}) No video stream found in {self.rtsp_url}" logger.error(msg) raise VideoSourceError(msg) - width = video_stream.get('width') - height = video_stream.get('height') - fps_str = video_stream.get('avg_frame_rate', '0/1') + width = video_stream.get("width") + height = video_stream.get("height") + fps_str = video_stream.get("avg_frame_rate", "0/1") if not width or not height: - msg = f"({self.dev_name}) Could not determine resolution for {self.rtsp_url}. Stream info: {video_stream}" - logger.error(msg) - raise VideoSourceError(msg) + msg = f"({self.dev_name}) Could not determine resolution for {self.rtsp_url}. Stream info: {video_stream}" + logger.error(msg) + raise VideoSourceError(msg) try: - if '/' in fps_str: - num, den = map(int, fps_str.split('/')) + if "/" in fps_str: + num, den = map(int, fps_str.split("/")) fps = float(num) / den if den != 0 else 30.0 else: fps = float(fps_str) if fps <= 0: - logger.warning(f"({self.dev_name}) Invalid avg_frame_rate '{fps_str}', defaulting FPS to 30.") + logger.warning( + f"({self.dev_name}) Invalid avg_frame_rate '{fps_str}', defaulting FPS to 30." + ) fps = 30.0 except ValueError: - logger.warning(f"({self.dev_name}) Could not parse FPS '{fps_str}', defaulting FPS to 30.") - fps = 30.0 + logger.warning( + f"({self.dev_name}) Could not parse FPS '{fps_str}', defaulting FPS to 30." + ) + fps = 30.0 logger.info(f"({self.dev_name}) Stream info: {width}x{height} @ {fps:.2f} FPS") return {"width": width, "height": height, "fps": fps} @@ -121,27 +120,26 @@ def _start_ffmpeg_process(self, width: int, height: int) -> subprocess.Popen: try: # Configure ffmpeg input: prefer TCP, set timeout, reduce buffering/delay input_options = { - 'rtsp_transport': 'tcp', - 'stimeout': '5000000', # 5 seconds timeout for RTSP server responses - 'fflags': 'nobuffer', # Reduce input buffering - 'flags': 'low_delay', # Reduce decoding delay + "rtsp_transport": "tcp", + "stimeout": "5000000", # 5 seconds timeout for RTSP server responses + "fflags": "nobuffer", # Reduce input buffering + "flags": "low_delay", # Reduce decoding delay # 'timeout': '10000000' # Removed: This was misinterpreted as listen timeout } process = ( - ffmpeg - .input(self.rtsp_url, **input_options) - .output('pipe:', format='rawvideo', pix_fmt='bgr24') # Output raw BGR frames - .global_args('-loglevel', 'warning') # Reduce ffmpeg log spam, use 'error' for less - .run_async(pipe_stdout=True, pipe_stderr=True) # Capture stdout and stderr + ffmpeg.input(self.rtsp_url, **input_options) + .output("pipe:", format="rawvideo", pix_fmt="bgr24") # Output raw BGR frames + .global_args("-loglevel", "warning") # Reduce ffmpeg log spam, use 'error' for less + .run_async(pipe_stdout=True, pipe_stderr=True) # Capture stdout and stderr ) logger.info(f"({self.dev_name}) ffmpeg process started (PID: {process.pid})") return process except ffmpeg.Error as e: - stderr = e.stderr.decode('utf8', errors='ignore') if e.stderr else 'No stderr' + stderr = e.stderr.decode("utf8", errors="ignore") if e.stderr else "No stderr" msg = f"({self.dev_name}) Failed to start ffmpeg for {self.rtsp_url}: {stderr}" logger.error(msg) raise VideoSourceError(msg) from e - except Exception as e: # Catch other errors like ffmpeg executable not found + except Exception as e: # Catch other errors like ffmpeg executable not found msg = f"({self.dev_name}) An unexpected error occurred starting ffmpeg: {e}" logger.error(msg) raise VideoSourceError(msg) from e @@ -165,15 +163,17 @@ def capture_video_as_observable(self, fps: int = 0) -> Observable: VideoFrameError: If there's an error reading or processing frames. """ if fps != 0: - logger.warning(f"({self.dev_name}) The 'fps' argument ({fps}) is currently ignored. Using stream native FPS.") + logger.warning( + f"({self.dev_name}) The 'fps' argument ({fps}) is currently ignored. Using stream native FPS." + ) def emit_frames(observer, scheduler): """Function executed by rx.create to emit frames.""" - process: Optional[subprocess.Popen] = None + process: subprocess.Popen | None = None # Event to signal the processing loop should stop (e.g., on dispose) should_stop = threading.Event() - def cleanup_process(): + def cleanup_process() -> None: """Safely terminate the ffmpeg process if it's running.""" nonlocal process logger.debug(f"({self.dev_name}) Cleanup requested.") @@ -181,38 +181,45 @@ def cleanup_process(): with self._lock: # Check if the process exists and is still running if process and process.poll() is None: - logger.info(f"({self.dev_name}) Terminating ffmpeg process (PID: {process.pid}).") + logger.info( + f"({self.dev_name}) Terminating ffmpeg process (PID: {process.pid})." + ) try: - process.terminate() # Ask ffmpeg to exit gracefully - process.wait(timeout=1.0) # Wait up to 1 second + process.terminate() # Ask ffmpeg to exit gracefully + process.wait(timeout=1.0) # Wait up to 1 second except subprocess.TimeoutExpired: - logger.warning(f"({self.dev_name}) ffmpeg (PID: {process.pid}) did not terminate gracefully, killing.") - process.kill() # Force kill if it didn't exit + logger.warning( + f"({self.dev_name}) ffmpeg (PID: {process.pid}) did not terminate gracefully, killing." + ) + process.kill() # Force kill if it didn't exit except Exception as e: logger.error(f"({self.dev_name}) Error during ffmpeg termination: {e}") finally: - # Ensure we clear the process variable even if wait/kill fails - process = None - # Also clear the shared class attribute if this was the active process - if self._ffmpeg_process and self._ffmpeg_process.pid == process.pid: - self._ffmpeg_process = None + # Ensure we clear the process variable even if wait/kill fails + process = None + # Also clear the shared class attribute if this was the active process + if self._ffmpeg_process and self._ffmpeg_process.pid == process.pid: + self._ffmpeg_process = None elif process and process.poll() is not None: # Process exists but already terminated - logger.debug(f"({self.dev_name}) ffmpeg process (PID: {process.pid}) already terminated (exit code: {process.poll()}).") - process = None # Clear the variable + logger.debug( + f"({self.dev_name}) ffmpeg process (PID: {process.pid}) already terminated (exit code: {process.poll()})." + ) + process = None # Clear the variable # Clear shared attribute if it matches if self._ffmpeg_process and self._ffmpeg_process.pid == process.pid: - self._ffmpeg_process = None + self._ffmpeg_process = None else: # Process variable is already None or doesn't match _ffmpeg_process - logger.debug(f"({self.dev_name}) No active ffmpeg process found needing termination in cleanup.") - + logger.debug( + f"({self.dev_name}) No active ffmpeg process found needing termination in cleanup." + ) try: # 1. Probe the stream to get essential info (width, height) stream_info = self._get_stream_info() - width = stream_info['width'] - height = stream_info['height'] + width = stream_info["width"] + height = stream_info["height"] # Calculate expected bytes per frame (BGR format = 3 bytes per pixel) frame_size = width * height * 3 @@ -223,7 +230,9 @@ def cleanup_process(): with self._lock: # Check if another thread/subscription already started the process if self._ffmpeg_process and self._ffmpeg_process.poll() is None: - logger.warning(f"({self.dev_name}) ffmpeg process (PID: {self._ffmpeg_process.pid}) seems to be already running. Reusing.") + logger.warning( + f"({self.dev_name}) ffmpeg process (PID: {self._ffmpeg_process.pid}) seems to be already running. Reusing." + ) process = self._ffmpeg_process else: # Start a new ffmpeg process @@ -238,26 +247,33 @@ def cleanup_process(): if len(in_bytes) == 0: # End of stream or process terminated unexpectedly - logger.warning(f"({self.dev_name}) ffmpeg stdout returned 0 bytes. EOF or process terminated.") - process.wait(timeout=0.5) # Allow stderr to flush - stderr_data = process.stderr.read().decode('utf8', errors='ignore') + logger.warning( + f"({self.dev_name}) ffmpeg stdout returned 0 bytes. EOF or process terminated." + ) + process.wait(timeout=0.5) # Allow stderr to flush + stderr_data = process.stderr.read().decode("utf8", errors="ignore") exit_code = process.poll() - logger.warning(f"({self.dev_name}) ffmpeg process (PID: {process.pid}) exited with code {exit_code}. Stderr: {stderr_data}") + logger.warning( + f"({self.dev_name}) ffmpeg process (PID: {process.pid}) exited with code {exit_code}. Stderr: {stderr_data}" + ) # Break inner loop to trigger cleanup and potential restart with self._lock: # Clear the shared process handle if it matches the one that just exited - if self._ffmpeg_process and self._ffmpeg_process.pid == process.pid: + if ( + self._ffmpeg_process + and self._ffmpeg_process.pid == process.pid + ): self._ffmpeg_process = None - process = None # Clear local process variable - break # Exit frame reading loop + process = None # Clear local process variable + break # Exit frame reading loop elif len(in_bytes) != frame_size: # Received incomplete frame data - indicates a problem msg = f"({self.dev_name}) Incomplete frame read. Expected {frame_size}, got {len(in_bytes)}. Stopping." logger.error(msg) observer.on_error(VideoFrameError(msg)) - should_stop.set() # Signal outer loop to stop - break # Exit frame reading loop + should_stop.set() # Signal outer loop to stop + break # Exit frame reading loop # Convert the raw bytes to a NumPy array (height, width, channels) frame = np.frombuffer(in_bytes, np.uint8).reshape((height, width, 3)) @@ -266,35 +282,41 @@ def cleanup_process(): # 4. Handle ffmpeg process exit/crash (if not stopping deliberately) if not should_stop.is_set() and process is None: - logger.info(f"({self.dev_name}) ffmpeg process ended. Attempting reconnection in 5 seconds...") + logger.info( + f"({self.dev_name}) ffmpeg process ended. Attempting reconnection in 5 seconds..." + ) # Wait for a few seconds before trying to restart time.sleep(5) # Continue to the next iteration of the outer loop to restart - except (VideoSourceError, ffmpeg.Error) as e: # Errors during ffmpeg process start or severe runtime errors - logger.error(f"({self.dev_name}) Unrecoverable ffmpeg error: {e}. Stopping emission.") + logger.error( + f"({self.dev_name}) Unrecoverable ffmpeg error: {e}. Stopping emission." + ) observer.on_error(e) - should_stop.set() # Stop retrying + should_stop.set() # Stop retrying except Exception as e: # Catch other unexpected errors during frame reading/processing - logger.error(f"({self.dev_name}) Unexpected error processing stream: {e}", exc_info=True) + logger.error( + f"({self.dev_name}) Unexpected error processing stream: {e}", + exc_info=True, + ) observer.on_error(VideoFrameError(f"Frame processing failed: {e}")) - should_stop.set() # Stop retrying + should_stop.set() # Stop retrying # 5. Loop finished (likely due to should_stop being set) logger.info(f"({self.dev_name}) Frame emission loop stopped.") observer.on_completed() except VideoSourceError as e: - # Handle errors during the initial probing phase - logger.error(f"({self.dev_name}) Failed initial setup: {e}") - observer.on_error(e) + # Handle errors during the initial probing phase + logger.error(f"({self.dev_name}) Failed initial setup: {e}") + observer.on_error(e) except Exception as e: - # Catch-all for unexpected errors before the main loop starts - logger.error(f"({self.dev_name}) Unexpected setup error: {e}", exc_info=True) - observer.on_error(VideoSourceError(f"Setup failed: {e}")) + # Catch-all for unexpected errors before the main loop starts + logger.error(f"({self.dev_name}) Unexpected setup error: {e}", exc_info=True) + observer.on_error(VideoSourceError(f"Setup failed: {e}")) finally: # Crucial: Ensure the ffmpeg process is terminated when the observable # is completed, errored, or disposed. @@ -305,12 +327,11 @@ def cleanup_process(): # signals the loop to stop. The finally block handles the actual cleanup. return Disposable(should_stop.set) - # Create the observable using rx.create, applying scheduling and sharing return rx.create(emit_frames).pipe( - ops.subscribe_on(self.pool_scheduler), # Run the emit_frames logic on the pool + ops.subscribe_on(self.pool_scheduler), # Run the emit_frames logic on the pool # ops.observe_on(self.pool_scheduler), # Optional: Switch thread for downstream operators - ops.share() # Ensure multiple subscribers share the same ffmpeg process + ops.share(), # Ensure multiple subscribers share the same ffmpeg process ) def dispose_all(self) -> None: @@ -318,24 +339,34 @@ def dispose_all(self) -> None: logger.info(f"({self.dev_name}) dispose_all called.") # Terminate the ffmpeg process using the same locked logic as cleanup with self._lock: - process = self._ffmpeg_process # Get the current process handle + process = self._ffmpeg_process # Get the current process handle if process and process.poll() is None: - logger.info(f"({self.dev_name}) Terminating ffmpeg process (PID: {process.pid}) via dispose_all.") - try: - process.terminate() - process.wait(timeout=1.0) - except subprocess.TimeoutExpired: - logger.warning(f"({self.dev_name}) ffmpeg process (PID: {process.pid}) kill required in dispose_all.") - process.kill() - except Exception as e: - logger.error(f"({self.dev_name}) Error during ffmpeg termination in dispose_all: {e}") - finally: - self._ffmpeg_process = None # Clear handle after attempting termination - elif process: # Process exists but already terminated - logger.debug(f"({self.dev_name}) ffmpeg process (PID: {process.pid}) already terminated in dispose_all.") + logger.info( + f"({self.dev_name}) Terminating ffmpeg process (PID: {process.pid}) via dispose_all." + ) + try: + process.terminate() + process.wait(timeout=1.0) + except subprocess.TimeoutExpired: + logger.warning( + f"({self.dev_name}) ffmpeg process (PID: {process.pid}) kill required in dispose_all." + ) + process.kill() + except Exception as e: + logger.error( + f"({self.dev_name}) Error during ffmpeg termination in dispose_all: {e}" + ) + finally: + self._ffmpeg_process = None # Clear handle after attempting termination + elif process: # Process exists but already terminated + logger.debug( + f"({self.dev_name}) ffmpeg process (PID: {process.pid}) already terminated in dispose_all." + ) self._ffmpeg_process = None else: - logger.debug(f"({self.dev_name}) No active ffmpeg process found during dispose_all.") + logger.debug( + f"({self.dev_name}) No active ffmpeg process found during dispose_all." + ) # Call the parent class's dispose_all to handle Rx Disposables super().dispose_all() diff --git a/dimos/stream/stream_merger.py b/dimos/stream/stream_merger.py index 1e3f436bbd..b59c78fa96 100644 --- a/dimos/stream/stream_merger.py +++ b/dimos/stream/stream_merger.py @@ -1,14 +1,28 @@ -from typing import List, TypeVar, Tuple, Any -from reactivex import Observable -from reactivex import operators as ops +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import TypeVar + +from reactivex import Observable, operators as ops + +T = TypeVar("T") +Q = TypeVar("Q") -T = TypeVar('T') -Q = TypeVar('Q') def create_stream_merger( - data_input_stream: Observable[T], - text_query_stream: Observable[Q] -) -> Observable[Tuple[Q, List[T]]]: + data_input_stream: Observable[T], text_query_stream: Observable[Q] +) -> Observable[tuple[Q, list[T]]]: """ Creates a merged stream that combines the latest value from data_input_stream with each value from text_query_stream. @@ -26,8 +40,6 @@ def create_stream_merger( # This avoids any boolean evaluation of arrays ops.map(lambda x: [x]) ) - + # Use safe_data_stream instead of raw data_input_stream - return text_query_stream.pipe( - ops.with_latest_from(safe_data_stream) - ) + return text_query_stream.pipe(ops.with_latest_from(safe_data_stream)) diff --git a/dimos/stream/video_operators.py b/dimos/stream/video_operators.py index a8e36fc6b6..d7299f3dce 100644 --- a/dimos/stream/video_operators.py +++ b/dimos/stream/video_operators.py @@ -12,29 +12,27 @@ # See the License for the specific language governing permissions and # limitations under the License. +import base64 +from collections.abc import Callable from datetime import datetime, timedelta +from enum import Enum +from typing import TYPE_CHECKING, Any + import cv2 import numpy as np -import os -from reactivex import Observable, Observer, create -from reactivex import operators as ops -from typing import Any, Callable, Tuple, Optional - +from reactivex import Observable, Observer, create, operators as ops import zmq -import base64 -from enum import Enum -from dimos.stream.frame_processor import FrameProcessor +if TYPE_CHECKING: + from dimos.stream.frame_processor import FrameProcessor + class VideoOperators: """Collection of video processing operators for reactive video streams.""" - + @staticmethod def with_fps_sampling( - fps: int = 25, - *, - sample_interval: Optional[timedelta] = None, - use_latest: bool = True + fps: int = 25, *, sample_interval: timedelta | None = None, use_latest: bool = True ) -> Callable[[Observable], Observable]: """Creates an operator that samples frames at a specified rate. @@ -51,7 +49,7 @@ def with_fps_sampling( If False, uses the first frame. Defaults to True. Returns: - A function that transforms an Observable[np.ndarray] stream to a sampled + A function that transforms an Observable[np.ndarray] stream to a sampled Observable[np.ndarray] stream with controlled frame rate. Raises: @@ -74,7 +72,7 @@ def with_fps_sampling( Note: This operator helps manage high-speed video streams through time-based - frame selection. It reduces the frame rate by selecting frames at + frame selection. It reduces the frame rate by selecting frames at specified intervals. When use_latest=True: @@ -101,14 +99,15 @@ def _operator(source: Observable) -> Observable: return source.pipe( ops.sample(sample_interval) if use_latest else ops.throttle_first(sample_interval) ) + return _operator @staticmethod def with_jpeg_export( - frame_processor: 'FrameProcessor', + frame_processor: "FrameProcessor", save_limit: int = 100, suffix: str = "", - loop: bool = False + loop: bool = False, ) -> Callable[[Observable], Observable]: """Creates an operator that saves video frames as JPEG files. @@ -123,7 +122,7 @@ def with_jpeg_export( suffix: Optional string to append to filename before index. Example: "raw" creates "1_raw.jpg". Defaults to empty string. - loop: If True, when save_limit is reached, the files saved are + loop: If True, when save_limit is reached, the files saved are loopbacked and overwritten with the most recent frame. Defaults to False. Returns: @@ -139,16 +138,18 @@ def with_jpeg_export( ... VideoOperators.with_jpeg_export(processor, suffix="raw") ... ) """ + def _operator(source: Observable) -> Observable: return source.pipe( - ops.map(lambda frame: frame_processor.export_to_jpeg(frame, save_limit, loop, suffix)) + ops.map( + lambda frame: frame_processor.export_to_jpeg(frame, save_limit, loop, suffix) + ) ) + return _operator - + @staticmethod - def with_optical_flow_filtering( - threshold: float = 1.0 - ) -> Callable[[Observable], Observable]: + def with_optical_flow_filtering(threshold: float = 1.0) -> Callable[[Observable], Observable]: """Creates an operator that filters optical flow frames by relevancy score. Filters a stream of optical flow results (frame, relevancy_score) tuples, @@ -185,12 +186,12 @@ def with_optical_flow_filtering( return lambda source: source.pipe( ops.filter(lambda result: result[1] is not None), ops.filter(lambda result: result[1] > threshold), - ops.map(lambda result: result[0]) + ops.map(lambda result: result[0]), ) @staticmethod def with_edge_detection( - frame_processor: 'FrameProcessor', + frame_processor: "FrameProcessor", ) -> Callable[[Observable], Observable]: return lambda source: source.pipe( ops.map(lambda frame: frame_processor.edge_detection(frame)) @@ -198,12 +199,14 @@ def with_edge_detection( @staticmethod def with_optical_flow( - frame_processor: 'FrameProcessor', + frame_processor: "FrameProcessor", ) -> Callable[[Observable], Observable]: return lambda source: source.pipe( ops.scan( - lambda acc, frame: frame_processor.compute_optical_flow(acc, frame, compute_relevancy=False), - (None, None, None) + lambda acc, frame: frame_processor.compute_optical_flow( + acc, frame, compute_relevancy=False + ), + (None, None, None), ), ops.map(lambda result: result[1]), # Extract flow component ops.filter(lambda flow: flow is not None), @@ -212,17 +215,17 @@ def with_optical_flow( @staticmethod def with_zmq_socket( - socket: zmq.Socket, - scheduler: Optional[Any] = None + socket: zmq.Socket, scheduler: Any | None = None ) -> Callable[[Observable], Observable]: - def send_frame(frame, socket): - _, img_encoded = cv2.imencode('.jpg', frame) + def send_frame(frame, socket) -> None: + _, img_encoded = cv2.imencode(".jpg", frame) socket.send(img_encoded.tobytes()) # print(f"Frame received: {frame.shape}") # Use a default scheduler if none is provided if scheduler is None: from reactivex.scheduler import ThreadPoolScheduler + scheduler = ThreadPoolScheduler(1) # Single-threaded pool for isolation return lambda source: source.pipe( @@ -239,30 +242,31 @@ def encode_image() -> Callable[[Observable], Observable]: A function that transforms an Observable of images into an Observable of tuples containing the Base64 string of the encoded image and its dimensions. """ + def _operator(source: Observable) -> Observable: - def _encode_image(image: np.ndarray) -> Tuple[str, Tuple[int, int]]: + def _encode_image(image: np.ndarray) -> tuple[str, tuple[int, int]]: try: width, height = image.shape[:2] - _, buffer = cv2.imencode('.jpg', image) + _, buffer = cv2.imencode(".jpg", image) if buffer is None: raise ValueError("Failed to encode image") - base64_image = base64.b64encode(buffer).decode('utf-8') + base64_image = base64.b64encode(buffer).decode("utf-8") return base64_image, (width, height) except Exception as e: raise e - return source.pipe( - ops.map(_encode_image) - ) + return source.pipe(ops.map(_encode_image)) return _operator -from reactivex.disposable import Disposable -from reactivex import Observable, create + from threading import Lock -class Operators: +from reactivex import Observable +from reactivex.disposable import Disposable + +class Operators: @staticmethod def exhaust_lock(process_item): """ @@ -270,6 +274,7 @@ def exhaust_lock(process_item): - If we're busy processing the previous one, skip new items. - Use a lock to ensure concurrency safety across threads. """ + def _exhaust_lock(source: Observable) -> Observable: def _subscribe(observer, scheduler=None): in_flight = False @@ -279,13 +284,13 @@ def _subscribe(observer, scheduler=None): upstream_disp = None active_inner_disp = None - def dispose_all(): + def dispose_all() -> None: if upstream_disp: upstream_disp.dispose() if active_inner_disp: active_inner_disp.dispose() - def on_next(value): + def on_next(value) -> None: nonlocal in_flight, active_inner_disp lock.acquire() try: @@ -305,16 +310,16 @@ def on_next(value): observer.on_error(ex) return - def inner_on_next(ivalue): + def inner_on_next(ivalue) -> None: observer.on_next(ivalue) - def inner_on_error(err): + def inner_on_error(err) -> None: nonlocal in_flight with lock: in_flight = False observer.on_error(err) - def inner_on_completed(): + def inner_on_completed() -> None: nonlocal in_flight with lock: in_flight = False @@ -327,14 +332,14 @@ def inner_on_completed(): on_next=inner_on_next, on_error=inner_on_error, on_completed=inner_on_completed, - scheduler=scheduler + scheduler=scheduler, ) - def on_error(err): + def on_error(err) -> None: dispose_all() observer.on_error(err) - def on_completed(): + def on_completed() -> None: nonlocal upstream_done with lock: upstream_done = True @@ -343,16 +348,14 @@ def on_completed(): observer.on_completed() upstream_disp = source.subscribe( - on_next, - on_error, - on_completed, - scheduler=scheduler + on_next, on_error, on_completed, scheduler=scheduler ) return dispose_all return create(_subscribe) + return _exhaust_lock - + @staticmethod def exhaust_lock_per_instance(process_item, lock: Lock): """ @@ -360,6 +363,7 @@ def exhaust_lock_per_instance(process_item, lock: Lock): - If a frame arrives while one is "in flight", discard it. - 'lock' ensures we safely check/modify the 'in_flight' state in a multithreaded environment. """ + def _exhaust_lock(source: Observable) -> Observable: def _subscribe(observer, scheduler=None): in_flight = False @@ -368,13 +372,13 @@ def _subscribe(observer, scheduler=None): upstream_disp = None active_inner_disp = None - def dispose_all(): + def dispose_all() -> None: if upstream_disp: upstream_disp.dispose() if active_inner_disp: active_inner_disp.dispose() - def on_next(value): + def on_next(value) -> None: nonlocal in_flight, active_inner_disp with lock: # If not busy, claim the slot @@ -393,17 +397,17 @@ def on_next(value): observer.on_error(ex) return - def inner_on_next(ivalue): + def inner_on_next(ivalue) -> None: observer.on_next(ivalue) - def inner_on_error(err): + def inner_on_error(err) -> None: nonlocal in_flight with lock: in_flight = False print("\033[34mError in inner on error.\033[0m") observer.on_error(err) - def inner_on_completed(): + def inner_on_completed() -> None: nonlocal in_flight with lock: in_flight = False @@ -417,14 +421,14 @@ def inner_on_completed(): on_next=inner_on_next, on_error=inner_on_error, on_completed=inner_on_completed, - scheduler=scheduler + scheduler=scheduler, ) - def on_error(e): + def on_error(e) -> None: dispose_all() observer.on_error(e) - def on_completed(): + def on_completed() -> None: nonlocal upstream_done with lock: upstream_done = True @@ -436,12 +440,13 @@ def on_completed(): on_next=on_next, on_error=on_error, on_completed=on_completed, - scheduler=scheduler + scheduler=scheduler, ) return Disposable(dispose_all) return create(_subscribe) + return _exhaust_lock @staticmethod @@ -450,76 +455,79 @@ def _exhaust_map(source: Observable): def subscribe(observer, scheduler=None): is_processing = False - def on_next(item): + def on_next(item) -> None: nonlocal is_processing if not is_processing: is_processing = True - print(f"\033[35mProcessing item.\033[0m") + print("\033[35mProcessing item.\033[0m") try: inner_observable = project(item) # Create the inner observable inner_observable.subscribe( on_next=observer.on_next, on_error=observer.on_error, on_completed=lambda: set_not_processing(), - scheduler=scheduler + scheduler=scheduler, ) except Exception as e: observer.on_error(e) else: - print(f"\033[35mSkipping item, already processing.\033[0m") - - def set_not_processing(): + print("\033[35mSkipping item, already processing.\033[0m") + + def set_not_processing() -> None: nonlocal is_processing is_processing = False - print(f"\033[35mItem processed.\033[0m") + print("\033[35mItem processed.\033[0m") return source.subscribe( - on_next=on_next, - on_error=observer.on_error, - on_completed=observer.on_completed, - scheduler=scheduler + on_next=on_next, + on_error=observer.on_error, + on_completed=observer.on_completed, + scheduler=scheduler, ) return create(subscribe) return _exhaust_map - + @staticmethod def with_lock(lock: Lock): def operator(source: Observable): def subscribe(observer, scheduler=None): - def on_next(item): + def on_next(item) -> None: if not lock.locked(): # Check if the lock is free - if lock.acquire(blocking=False): # Non-blocking acquire + if lock.acquire(blocking=False): # Non-blocking acquire try: - print(f"\033[32mAcquired lock, processing item.\033[0m") + print("\033[32mAcquired lock, processing item.\033[0m") observer.on_next(item) - finally: # Ensure lock release even if observer.on_next throws + finally: # Ensure lock release even if observer.on_next throws lock.release() else: - print(f"\033[34mLock busy, skipping item.\033[0m") + print("\033[34mLock busy, skipping item.\033[0m") else: - print(f"\033[34mLock busy, skipping item.\033[0m") + print("\033[34mLock busy, skipping item.\033[0m") - def on_error(error): + def on_error(error) -> None: observer.on_error(error) - def on_completed(): + def on_completed() -> None: observer.on_completed() return source.subscribe( - on_next=on_next, on_error=on_error, on_completed=on_completed, scheduler=scheduler + on_next=on_next, + on_error=on_error, + on_completed=on_completed, + scheduler=scheduler, ) return Observable(subscribe) return operator - + @staticmethod def with_lock_check(lock: Lock): # Renamed for clarity def operator(source: Observable): def subscribe(observer, scheduler=None): - def on_next(item): + def on_next(item) -> None: if not lock.locked(): # Check if the lock is held WITHOUT acquiring print(f"\033[32mLock is free, processing item: {item}\033[0m") observer.on_next(item) @@ -527,14 +535,17 @@ def on_next(item): print(f"\033[34mLock is busy, skipping item: {item}\033[0m") # observer.on_completed() - def on_error(error): + def on_error(error) -> None: observer.on_error(error) - def on_completed(): + def on_completed() -> None: observer.on_completed() return source.subscribe( - on_next=on_next, on_error=on_error, on_completed=on_completed, scheduler=scheduler + on_next=on_next, + on_error=on_error, + on_completed=on_completed, + scheduler=scheduler, ) return Observable(subscribe) @@ -553,14 +564,16 @@ class PrintColor(Enum): RESET = "\033[0m" @staticmethod - def print_emission(id: str, - dev_name: str = "NA", - counts: dict = None, - color: 'Operators.PrintColor' = None, - enabled: bool = True): + def print_emission( + id: str, + dev_name: str = "NA", + counts: dict | None = None, + color: "Operators.PrintColor" = None, + enabled: bool = True, + ): """ Creates an operator that prints the emission with optional counts for debugging. - + Args: id: Identifier for the emission point (e.g., 'A', 'B') dev_name: Device or component name for context @@ -573,36 +586,39 @@ def print_emission(id: str, # If enabled is false, return the source unchanged if not enabled: return lambda source: source - + # Use RED as default if no color provided if color is None: color = Operators.PrintColor.RED def _operator(source: Observable) -> Observable: def _subscribe(observer: Observer, scheduler=None): - def on_next(value): + def on_next(value) -> None: if counts is not None: # Initialize count if necessary if id not in counts: counts[id] = 0 - + # Increment and print counts[id] += 1 - print(f"{color.value}({dev_name} - {id}) Emission Count - {counts[id]} {datetime.now()}{Operators.PrintColor.RESET.value}") - else: - print(f"{color.value}({dev_name} - {id}) Emitted - {datetime.now()}{Operators.PrintColor.RESET.value}") + print( + f"{color.value}({dev_name} - {id}) Emission Count - {counts[id]} {datetime.now()}{Operators.PrintColor.RESET.value}" + ) + else: + print( + f"{color.value}({dev_name} - {id}) Emitted - {datetime.now()}{Operators.PrintColor.RESET.value}" + ) # Pass value through unchanged observer.on_next(value) - + return source.subscribe( on_next=on_next, on_error=observer.on_error, on_completed=observer.on_completed, - scheduler=scheduler + scheduler=scheduler, ) - + return create(_subscribe) - + return _operator - \ No newline at end of file diff --git a/dimos/stream/video_provider.py b/dimos/stream/video_provider.py index eb6b2da0e8..0b7e815ae2 100644 --- a/dimos/stream/video_provider.py +++ b/dimos/stream/video_provider.py @@ -20,12 +20,11 @@ """ # Standard library imports +from abc import ABC, abstractmethod import logging import os -import time -from abc import ABC, abstractmethod from threading import Lock -from typing import Optional +import time # Third-party imports import cv2 @@ -46,20 +45,22 @@ # Specific exception classes class VideoSourceError(Exception): """Raised when there's an issue with the video source.""" + pass class VideoFrameError(Exception): """Raised when there's an issue with frame acquisition.""" + pass class AbstractVideoProvider(ABC): """Abstract base class for video providers managing video capture resources.""" - def __init__(self, - dev_name: str = "NA", - pool_scheduler: Optional[ThreadPoolScheduler] = None) -> None: + def __init__( + self, dev_name: str = "NA", pool_scheduler: ThreadPoolScheduler | None = None + ) -> None: """Initializes the video provider with a device name. Args: @@ -68,20 +69,19 @@ def __init__(self, If None, the global scheduler from get_scheduler() will be used. """ self.dev_name = dev_name - self.pool_scheduler = (pool_scheduler - if pool_scheduler else get_scheduler()) + self.pool_scheduler = pool_scheduler if pool_scheduler else get_scheduler() self.disposables = CompositeDisposable() @abstractmethod def capture_video_as_observable(self, fps: int = 30) -> Observable: """Create an observable from video capture. - + Args: fps: Frames per second to emit. Defaults to 30fps. - + Returns: Observable: An observable emitting frames at the specified rate. - + Raises: VideoSourceError: If the video source cannot be opened. VideoFrameError: If frames cannot be read properly. @@ -103,10 +103,12 @@ def __del__(self) -> None: class VideoProvider(AbstractVideoProvider): """Video provider implementation for capturing video as an observable.""" - def __init__(self, - dev_name: str, - video_source: str = f"{os.getcwd()}/assets/video-f30-480p.mp4", - pool_scheduler: Optional[ThreadPoolScheduler] = None) -> None: + def __init__( + self, + dev_name: str, + video_source: str = f"{os.getcwd()}/assets/video-f30-480p.mp4", + pool_scheduler: ThreadPoolScheduler | None = None, + ) -> None: """Initializes the video provider with a device name and video source. Args: @@ -122,7 +124,7 @@ def __init__(self, def _initialize_capture(self) -> None: """Initializes the video capture object if not already initialized. - + Raises: VideoSourceError: If the video source cannot be opened. """ @@ -141,28 +143,26 @@ def _initialize_capture(self) -> None: logger.info(f"Opened new capture: {self.video_source}") - def capture_video_as_observable(self, - realtime: bool = True, - fps: int = 30) -> Observable: + def capture_video_as_observable(self, realtime: bool = True, fps: int = 30) -> Observable: """Creates an observable from video capture. - Creates an observable that emits frames at specified FPS or the video's + Creates an observable that emits frames at specified FPS or the video's native FPS, with proper resource management and error handling. Args: realtime: If True, use the video's native FPS. Defaults to True. - fps: Frames per second to emit. Defaults to 30fps. Only used if + fps: Frames per second to emit. Defaults to 30fps. Only used if realtime is False or the video's native FPS is not available. Returns: Observable: An observable emitting frames at the configured rate. - + Raises: VideoSourceError: If the video source cannot be opened. VideoFrameError: If frames cannot be read properly. """ - def emit_frames(observer, scheduler): + def emit_frames(observer, scheduler) -> None: try: self._initialize_capture() @@ -173,9 +173,7 @@ def emit_frames(observer, scheduler): if native_fps > 0: local_fps = native_fps else: - logger.warning( - "Native FPS not available, defaulting to specified FPS" - ) + logger.warning("Native FPS not available, defaulting to specified FPS") frame_interval: float = 1.0 / local_fps frame_time: float = time.monotonic() @@ -187,8 +185,7 @@ def emit_frames(observer, scheduler): if not ret: # Loop video when we reach the end - logger.warning( - "End of video reached, restarting playback") + logger.warning("End of video reached, restarting playback") with self.lock: self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0) continue @@ -209,8 +206,7 @@ def emit_frames(observer, scheduler): observer.on_error(e) except Exception as e: logger.error(f"Unexpected error during frame emission: {e}") - observer.on_error( - VideoFrameError(f"Frame acquisition failed: {e}")) + observer.on_error(VideoFrameError(f"Frame acquisition failed: {e}")) finally: # Clean up resources regardless of success or failure with self.lock: diff --git a/dimos/stream/video_providers/unitree.py b/dimos/stream/video_providers/unitree.py index fefbf669f3..ba28cb1d6f 100644 --- a/dimos/stream/video_providers/unitree.py +++ b/dimos/stream/video_providers/unitree.py @@ -12,22 +12,29 @@ # See the License for the specific language governing permissions and # limitations under the License. -from dimos.stream.video_provider import AbstractVideoProvider - -from queue import Queue -from dimos.robot.unitree.external.go2_webrtc_connect.go2_webrtc_driver.constants import RTC_TOPIC, SPORT_CMD, WebRTCConnectionMethod -from dimos.robot.unitree.external.go2_webrtc_connect.go2_webrtc_driver.webrtc_driver import Go2WebRTCConnection -from aiortc import MediaStreamTrack import asyncio -from reactivex import Observable, create, operators as ops import logging +from queue import Queue import threading import time +from aiortc import MediaStreamTrack +from go2_webrtc_driver.webrtc_driver import Go2WebRTCConnection, WebRTCConnectionMethod +from reactivex import Observable, create, operators as ops + +from dimos.stream.video_provider import AbstractVideoProvider + + class UnitreeVideoProvider(AbstractVideoProvider): - def __init__(self, dev_name: str = "UnitreeGo2", connection_method: WebRTCConnectionMethod = WebRTCConnectionMethod.LocalSTA, serial_number: str = None, ip: str = None): + def __init__( + self, + dev_name: str = "UnitreeGo2", + connection_method: WebRTCConnectionMethod = WebRTCConnectionMethod.LocalSTA, + serial_number: str | None = None, + ip: str | None = None, + ) -> None: """Initialize the Unitree video stream with WebRTC connection. - + Args: dev_name: Name of the device connection_method: WebRTC connection method (LocalSTA, LocalAP, Remote) @@ -38,7 +45,7 @@ def __init__(self, dev_name: str = "UnitreeGo2", connection_method: WebRTCConnec self.frame_queue = Queue() self.loop = None self.asyncio_thread = None - + # Initialize WebRTC connection based on method if connection_method == WebRTCConnectionMethod.LocalSTA: if serial_number: @@ -46,13 +53,15 @@ def __init__(self, dev_name: str = "UnitreeGo2", connection_method: WebRTCConnec elif ip: self.conn = Go2WebRTCConnection(connection_method, ip=ip) else: - raise ValueError("Either serial_number or ip must be provided for LocalSTA connection") + raise ValueError( + "Either serial_number or ip must be provided for LocalSTA connection" + ) elif connection_method == WebRTCConnectionMethod.LocalAP: self.conn = Go2WebRTCConnection(connection_method) else: raise ValueError("Unsupported connection method") - async def _recv_camera_stream(self, track: MediaStreamTrack): + async def _recv_camera_stream(self, track: MediaStreamTrack) -> None: """Receive video frames from WebRTC and put them in the queue.""" while True: frame = await track.recv() @@ -60,10 +69,10 @@ async def _recv_camera_stream(self, track: MediaStreamTrack): img = frame.to_ndarray(format="bgr24") self.frame_queue.put(img) - def _run_asyncio_loop(self, loop): + def _run_asyncio_loop(self, loop) -> None: """Run the asyncio event loop in a separate thread.""" asyncio.set_event_loop(loop) - + async def setup(): try: await self.conn.connect() @@ -78,17 +87,17 @@ async def setup(): # await self.conn.datachannel.sendDamp() # await asyncio.sleep(5) # await self.conn.datachannel.sendStandUp() - # await asyncio.sleep(5) + # await asyncio.sleep(5) # Wiggle the robot # await self.conn.datachannel.switchToNormalMode() # await self.conn.datachannel.sendWiggle() - #await asyncio.sleep(3) + # await asyncio.sleep(3) # Stretch the robot # await self.conn.datachannel.sendStretch() # await asyncio.sleep(3) - + except Exception as e: logging.error(f"Error in WebRTC connection: {e}") raise @@ -98,40 +107,39 @@ async def setup(): def capture_video_as_observable(self, fps: int = 30) -> Observable: """Create an observable that emits video frames at the specified FPS. - + Args: fps: Frames per second to emit (default: 30) - + Returns: Observable emitting video frames """ frame_interval = 1.0 / fps - def emit_frames(observer, scheduler): + def emit_frames(observer, scheduler) -> None: try: # Start asyncio loop if not already running if not self.loop: self.loop = asyncio.new_event_loop() self.asyncio_thread = threading.Thread( - target=self._run_asyncio_loop, - args=(self.loop,) + target=self._run_asyncio_loop, args=(self.loop,) ) self.asyncio_thread.start() frame_time = time.monotonic() - + while True: if not self.frame_queue.empty(): frame = self.frame_queue.get() - + # Control frame rate now = time.monotonic() next_frame_time = frame_time + frame_interval sleep_time = next_frame_time - now - + if sleep_time > 0: time.sleep(sleep_time) - + observer.on_next(frame) frame_time = next_frame_time else: @@ -151,7 +159,7 @@ def emit_frames(observer, scheduler): ops.share() # Share the stream among multiple subscribers ) - def dispose_all(self): + def dispose_all(self) -> None: """Clean up resources.""" if self.loop: self.loop.call_soon_threadsafe(self.loop.stop) diff --git a/dimos/stream/videostream.py b/dimos/stream/videostream.py index e2262120fb..9c99ddea3a 100644 --- a/dimos/stream/videostream.py +++ b/dimos/stream/videostream.py @@ -12,13 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +from collections.abc import Iterator + import cv2 + class VideoStream: - def __init__(self, source=0): + def __init__(self, source: int = 0) -> None: """ Initialize the video stream from a camera source. - + Args: source (int or str): Camera index or video file path. """ @@ -26,7 +29,7 @@ def __init__(self, source=0): if not self.capture.isOpened(): raise ValueError(f"Unable to open video source {source}") - def __iter__(self): + def __iter__(self) -> Iterator: return self def __next__(self): @@ -36,5 +39,5 @@ def __next__(self): raise StopIteration return frame - def release(self): + def release(self) -> None: self.capture.release() diff --git a/dimos/types/constants.py b/dimos/types/constants.py index ec8dbb0a4e..91841e8bef 100644 --- a/dimos/types/constants.py +++ b/dimos/types/constants.py @@ -1,3 +1,17 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + class Colors: GREEN_PRINT_COLOR: str = "\033[32m" diff --git a/dimos/types/costmap.py b/dimos/types/costmap.py deleted file mode 100644 index de80b2d6a6..0000000000 --- a/dimos/types/costmap.py +++ /dev/null @@ -1,326 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import base64 -import pickle -import math -import numpy as np -from typing import Optional -from scipy import ndimage -from nav_msgs.msg import OccupancyGrid -from dimos.types.vector import Vector, VectorLike, x, y, to_vector - -DTYPE2STR = { - np.float32: "f32", - np.float64: "f64", - np.int32: "i32", - np.int8: "i8", -} - -STR2DTYPE = {v: k for k, v in DTYPE2STR.items()} - - -def encode_ndarray(arr: np.ndarray, compress: bool = False): - arr_c = np.ascontiguousarray(arr) - payload = arr_c.tobytes() - b64 = base64.b64encode(payload).decode("ascii") - - return { - "type": "grid", - "shape": arr_c.shape, - "dtype": DTYPE2STR[arr_c.dtype.type], - "data": b64, - } - - -class Costmap: - """Class to hold ROS OccupancyGrid data.""" - - def __init__( - self, - grid: np.ndarray, - origin_theta: float, - origin: VectorLike, - resolution: float = 0.05, - ): - """Initialize Costmap with its core attributes.""" - self.grid = grid - self.resolution = resolution - self.origin = to_vector(origin).to_2d() - self.origin_theta = origin_theta - self.width = self.grid.shape[1] - self.height = self.grid.shape[0] - - def serialize(self) -> tuple: - """Serialize the Costmap instance to a tuple.""" - return { - "type": "costmap", - "grid": encode_ndarray(self.grid), - "origin": self.origin.serialize(), - "resolution": self.resolution, - "origin_theta": self.origin_theta, - } - - @classmethod - def from_msg(cls, costmap_msg: OccupancyGrid) -> "Costmap": - """Create a Costmap instance from a ROS OccupancyGrid message.""" - if costmap_msg is None: - raise Exception("need costmap msg") - - # Extract info from the message - width = costmap_msg.info.width - height = costmap_msg.info.height - resolution = costmap_msg.info.resolution - - # Get origin position as a vector-like object - origin = ( - costmap_msg.info.origin.position.x, - costmap_msg.info.origin.position.y, - ) - - # Calculate orientation from quaternion - qx = costmap_msg.info.origin.orientation.x - qy = costmap_msg.info.origin.orientation.y - qz = costmap_msg.info.origin.orientation.z - qw = costmap_msg.info.origin.orientation.w - origin_theta = math.atan2(2.0 * (qw * qz + qx * qy), 1.0 - 2.0 * (qy * qy + qz * qz)) - - # Convert to numpy array - data = np.array(costmap_msg.data, dtype=np.int8) - grid = data.reshape((height, width)) - - return cls( - grid=grid, - resolution=resolution, - origin=origin, - origin_theta=origin_theta, - ) - - def save_pickle(self, pickle_path: str): - """Save costmap to a pickle file. - - Args: - pickle_path: Path to save the pickle file - """ - with open(pickle_path, "wb") as f: - pickle.dump(self, f) - - @classmethod - def from_pickle(cls, pickle_path: str) -> "Costmap": - """Load costmap from a pickle file containing a ROS OccupancyGrid message.""" - with open(pickle_path, "rb") as f: - data = pickle.load(f) - costmap = cls(*data) - return costmap - - @classmethod - def create_empty(cls, width: int = 100, height: int = 100, resolution: float = 0.1) -> "Costmap": - """Create an empty costmap with specified dimensions.""" - return cls( - grid=np.zeros((height, width), dtype=np.int8), - resolution=resolution, - origin=(0.0, 0.0), - origin_theta=0.0, - ) - - def world_to_grid(self, point: VectorLike) -> Vector: - """Convert world coordinates to grid coordinates. - - Args: - point: A vector-like object containing X,Y coordinates - - Returns: - Tuple of (grid_x, grid_y) as integers - """ - return (to_vector(point) - self.origin) / self.resolution - - def grid_to_world(self, grid_point: VectorLike) -> Vector: - return to_vector(grid_point) * self.resolution + self.origin - - def is_occupied(self, point: VectorLike, threshold: int = 50) -> bool: - """Check if a position in world coordinates is occupied. - - Args: - point: Vector-like object containing X,Y coordinates - threshold: Cost threshold above which a cell is considered occupied (0-100) - - Returns: - True if position is occupied or out of bounds, False otherwise - """ - grid_point = self.world_to_grid(point) - grid_x, grid_y = int(grid_point.x), int(grid_point.y) - if 0 <= grid_x < self.width and 0 <= grid_y < self.height: - # Consider unknown (-1) as unoccupied for navigation purposes - value = self.grid[grid_y, grid_x] - return value >= threshold - return True # Consider out-of-bounds as occupied - - def get_value(self, point: VectorLike) -> Optional[int]: - point = self.world_to_grid(point) - - if 0 <= point.x < self.width and 0 <= point.y < self.height: - return int(self.grid[point.y, point.x]) - return None - - def set_value(self, point: VectorLike, value: int = 0) -> bool: - point = self.world_to_grid(point) - - if 0 <= point.x < self.width and 0 <= point.y < self.height: - self.grid[point.y, point.x] = value - return value - return False - - def smudge( - self, - kernel_size: int = 6, - iterations: int = 20, - decay_factor: float = 0.9, - threshold: int = 90, - preserve_unknown: bool = False, - ) -> "Costmap": - """ - Creates a new costmap with expanded obstacles (smudged). - - Args: - kernel_size: Size of the convolution kernel for dilation (must be odd) - iterations: Number of dilation iterations - decay_factor: Factor to reduce cost as distance increases (0.0-1.0) - threshold: Minimum cost value to consider as an obstacle for expansion - preserve_unknown: Whether to keep unknown (-1) cells as unknown - - Returns: - A new Costmap instance with expanded obstacles - """ - # Make sure kernel size is odd - if kernel_size % 2 == 0: - kernel_size += 1 - - # Create a copy of the grid for processing - grid_copy = self.grid.copy() - - # Create a mask of unknown cells if needed - unknown_mask = None - if preserve_unknown: - unknown_mask = grid_copy == -1 - # Temporarily replace unknown cells with 0 for processing - # This allows smudging to go over unknown areas - grid_copy[unknown_mask] = 0 - - # Create a mask of cells that are above the threshold - obstacle_mask = grid_copy >= threshold - - # Create a binary map of obstacles - binary_map = obstacle_mask.astype(np.uint8) * 100 - - # Create a circular kernel for dilation (instead of square) - y, x = np.ogrid[ - -kernel_size // 2 : kernel_size // 2 + 1, - -kernel_size // 2 : kernel_size // 2 + 1, - ] - kernel = (x * x + y * y <= (kernel_size // 2) * (kernel_size // 2)).astype(np.uint8) - - # Create distance map using dilation - # Each iteration adds one 'ring' of cells around obstacles - dilated_map = binary_map.copy() - - # Store each layer of dilation with decreasing values - layers = [] - - # First layer is the original obstacle cells - layers.append(binary_map.copy()) - - for i in range(iterations): - # Dilate the binary map - dilated = ndimage.binary_dilation(dilated_map > 0, structure=kernel, iterations=1).astype(np.uint8) - - # Calculate the new layer (cells that were just added in this iteration) - new_layer = (dilated - (dilated_map > 0).astype(np.uint8)) * 100 - - # Apply decay factor based on distance from obstacle - new_layer = new_layer * (decay_factor ** (i + 1)) - - # Add to layers list - layers.append(new_layer) - - # Update dilated map for next iteration - dilated_map = dilated * 100 - - # Combine all layers to create a distance-based cost map - smudged_map = np.zeros_like(grid_copy) - for layer in layers: - # For each cell, keep the maximum value across all layers - smudged_map = np.maximum(smudged_map, layer) - - # Preserve original obstacles - smudged_map[obstacle_mask] = grid_copy[obstacle_mask] - - # When preserve_unknown is true, restore all original unknown cells - # This overlays unknown cells on top of the smudged map - if preserve_unknown and unknown_mask is not None: - smudged_map[unknown_mask] = -1 - - # Ensure cost values are in valid range (0-100) except for unknown (-1) - if preserve_unknown: - valid_cells = ~unknown_mask - smudged_map[valid_cells] = np.clip(smudged_map[valid_cells], 0, 100) - else: - smudged_map = np.clip(smudged_map, 0, 100) - - # Create a new costmap with the smudged grid - return Costmap( - grid=smudged_map.astype(np.int8), - resolution=self.resolution, - origin=self.origin, - origin_theta=self.origin_theta, - ) - - def __str__(self) -> str: - """ - Create a string representation of the Costmap. - - Returns: - A formatted string with key costmap information - """ - # Calculate occupancy statistics - total_cells = self.width * self.height - occupied_cells = np.sum(self.grid >= 50) - unknown_cells = np.sum(self.grid == -1) - free_cells = total_cells - occupied_cells - unknown_cells - - # Calculate percentages - occupied_percent = (occupied_cells / total_cells) * 100 - unknown_percent = (unknown_cells / total_cells) * 100 - free_percent = (free_cells / total_cells) * 100 - - cell_info = [ - "▦ Costmap", - f"{self.width}x{self.height}", - f"({self.width * self.resolution:.1f}x{self.height * self.resolution:.1f}m @", - f"{1 / self.resolution:.0f}cm res)", - f"Origin: ({x(self.origin):.2f}, {y(self.origin):.2f})", - f"▣ {occupied_percent:.1f}%", - f"□ {free_percent:.1f}%", - f"◌ {unknown_percent:.1f}%", - ] - - return " ".join(cell_info) - - -if __name__ == "__main__": - costmap = Costmap.from_pickle("costmapMsg.pickle") - print(costmap) - - # Create a smudged version of the costmap for better planning - smudged_costmap = costmap.smudge(kernel_size=10, iterations=10, threshold=80, preserve_unknown=False) - - print(costmap) diff --git a/dimos/types/depth_map.py b/dimos/types/depth_map.py deleted file mode 100644 index 5f7e003933..0000000000 --- a/dimos/types/depth_map.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Any -import numpy as np - -class DepthMapType: - def __init__(self, depth_data: np.ndarray, metadata: Any = None): - """ - Initializes a standardized depth map type. - - Args: - depth_data (np.ndarray): The depth map data as a numpy array. - metadata (Any, optional): Additional metadata related to the depth map. - """ - self.depth_data = depth_data - self.metadata = metadata - - def normalize(self): - """Normalize the depth data to a 0-1 range.""" - min_val = np.min(self.depth_data) - max_val = np.max(self.depth_data) - self.depth_data = (self.depth_data - min_val) / (max_val - min_val) - - def save_to_file(self, filepath: str): - """Save the depth map to a file.""" - np.save(filepath, self.depth_data) \ No newline at end of file diff --git a/dimos/types/label.py b/dimos/types/label.py index 1a5acd8324..83b91c8152 100644 --- a/dimos/types/label.py +++ b/dimos/types/label.py @@ -12,10 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict, Any +from typing import Any + class LabelType: - def __init__(self, labels: Dict[str, Any], metadata: Any = None): + def __init__(self, labels: dict[str, Any], metadata: Any = None) -> None: """ Initializes a standardized label type. @@ -24,14 +25,15 @@ def __init__(self, labels: Dict[str, Any], metadata: Any = None): metadata (Any, optional): Additional metadata related to the labels. """ self.labels = labels - self.metadata = metadata + self.metadata = metadata def get_label_descriptions(self): """Return a list of label descriptions.""" - return [desc['description'] for desc in self.labels.values()] + return [desc["description"] for desc in self.labels.values()] - def save_to_json(self, filepath: str): + def save_to_json(self, filepath: str) -> None: """Save the labels to a JSON file.""" import json - with open(filepath, 'w') as f: - json.dump(self.labels, f, indent=4) \ No newline at end of file + + with open(filepath, "w") as f: + json.dump(self.labels, f, indent=4) diff --git a/dimos/types/manipulation.py b/dimos/types/manipulation.py new file mode 100644 index 0000000000..0df62362a4 --- /dev/null +++ b/dimos/types/manipulation.py @@ -0,0 +1,168 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC +from dataclasses import dataclass, field +from enum import Enum +import time +from typing import TYPE_CHECKING, Any, Literal, TypedDict +import uuid + +import numpy as np + +from dimos.types.vector import Vector + +if TYPE_CHECKING: + import open3d as o3d + + +class ConstraintType(Enum): + """Types of manipulation constraints.""" + + TRANSLATION = "translation" + ROTATION = "rotation" + FORCE = "force" + + +@dataclass +class AbstractConstraint(ABC): + """Base class for all manipulation constraints.""" + + description: str = "" + id: str = field(default_factory=lambda: str(uuid.uuid4())[:8]) + + +@dataclass +class TranslationConstraint(AbstractConstraint): + """Constraint parameters for translational movement along a single axis.""" + + translation_axis: Literal["x", "y", "z"] = None # Axis to translate along + reference_point: Vector | None = None + bounds_min: Vector | None = None # For bounded translation + bounds_max: Vector | None = None # For bounded translation + target_point: Vector | None = None # For relative positioning + + +@dataclass +class RotationConstraint(AbstractConstraint): + """Constraint parameters for rotational movement around a single axis.""" + + rotation_axis: Literal["roll", "pitch", "yaw"] = None # Axis to rotate around + start_angle: Vector | None = None # Angle values applied to the specified rotation axis + end_angle: Vector | None = None # Angle values applied to the specified rotation axis + pivot_point: Vector | None = None # Point of rotation + secondary_pivot_point: Vector | None = None # For double point rotations + + +@dataclass +class ForceConstraint(AbstractConstraint): + """Constraint parameters for force application.""" + + max_force: float = 0.0 # Maximum force in newtons + min_force: float = 0.0 # Minimum force in newtons + force_direction: Vector | None = None # Direction of force application + + +class ObjectData(TypedDict, total=False): + """Data about an object in the manipulation scene.""" + + # Basic detection information + object_id: int # Unique ID for the object + bbox: list[float] # Bounding box [x1, y1, x2, y2] + depth: float # Depth in meters from Metric3d + confidence: float # Detection confidence + class_id: int # Class ID from the detector + label: str # Semantic label (e.g., 'cup', 'table') + movement_tolerance: float # (0.0 = immovable, 1.0 = freely movable) + segmentation_mask: np.ndarray # Binary mask of the object's pixels + + # 3D pose and dimensions + position: dict[str, float] | Vector # 3D position {x, y, z} or Vector + rotation: dict[str, float] | Vector # 3D rotation {roll, pitch, yaw} or Vector + size: dict[str, float] # Object dimensions {width, height, depth} + + # Point cloud data + point_cloud: "o3d.geometry.PointCloud" # Open3D point cloud object + point_cloud_numpy: np.ndarray # Nx6 array of XYZRGB points + color: np.ndarray # RGB color for visualization [R, G, B] + + +class ManipulationMetadata(TypedDict, total=False): + """Typed metadata for manipulation constraints.""" + + timestamp: float + objects: dict[str, ObjectData] + + +@dataclass +class ManipulationTaskConstraint: + """Set of constraints for a specific manipulation action.""" + + constraints: list[AbstractConstraint] = field(default_factory=list) + + def add_constraint(self, constraint: AbstractConstraint) -> None: + """Add a constraint to this set.""" + if constraint not in self.constraints: + self.constraints.append(constraint) + + def get_constraints(self) -> list[AbstractConstraint]: + """Get all constraints in this set.""" + return self.constraints + + +@dataclass +class ManipulationTask: + """Complete definition of a manipulation task.""" + + description: str + target_object: str # Semantic label of target object + target_point: tuple[float, float] | None = ( + None # (X,Y) point in pixel-space of the point to manipulate on target object + ) + metadata: ManipulationMetadata = field(default_factory=dict) + timestamp: float = field(default_factory=time.time) + task_id: str = "" + result: dict[str, Any] | None = None # Any result data from the task execution + constraints: list[AbstractConstraint] | ManipulationTaskConstraint | AbstractConstraint = field( + default_factory=list + ) + + def add_constraint(self, constraint: AbstractConstraint) -> None: + """Add a constraint to this manipulation task.""" + # If constraints is a ManipulationTaskConstraint object + if isinstance(self.constraints, ManipulationTaskConstraint): + self.constraints.add_constraint(constraint) + return + + # If constraints is a single AbstractConstraint, convert to list + if isinstance(self.constraints, AbstractConstraint): + self.constraints = [self.constraints, constraint] + return + + # If constraints is a list, append to it + # This will also handle empty lists (the default case) + self.constraints.append(constraint) + + def get_constraints(self) -> list[AbstractConstraint]: + """Get all constraints in this manipulation task.""" + # If constraints is a ManipulationTaskConstraint object + if isinstance(self.constraints, ManipulationTaskConstraint): + return self.constraints.get_constraints() + + # If constraints is a single AbstractConstraint, return as list + if isinstance(self.constraints, AbstractConstraint): + return [self.constraints] + + # If constraints is a list (including empty list), return it + return self.constraints diff --git a/dimos/types/path.py b/dimos/types/path.py deleted file mode 100644 index 2e20924f4d..0000000000 --- a/dimos/types/path.py +++ /dev/null @@ -1,412 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy as np -from typing import List, Union, Tuple, Iterator, TypeVar -from dimos.types.vector import Vector - -T = TypeVar("T", bound="Path") - - -class Path: - """A class representing a path as a sequence of points.""" - - def __init__( - self, - points: Union[List[Vector], List[np.ndarray], List[Tuple], np.ndarray, None] = None, - ): - """Initialize a path from a list of points. - - Args: - points: List of Vector objects, numpy arrays, tuples, or a 2D numpy array where each row is a point. - If None, creates an empty path. - - Examples: - Path([Vector(1, 2), Vector(3, 4)]) # from Vector objects - Path([(1, 2), (3, 4)]) # from tuples - Path(np.array([[1, 2], [3, 4]])) # from 2D numpy array - """ - if points is None: - self._points = np.zeros((0, 0), dtype=float) - return - - if isinstance(points, np.ndarray) and points.ndim == 2: - # If already a 2D numpy array, use it directly - self._points = points.astype(float) - else: - # Convert various input types to numpy array - converted = [] - for p in points: - if isinstance(p, Vector): - converted.append(p.data) - else: - converted.append(p) - self._points = np.array(converted, dtype=float) - - def serialize(self) -> Tuple: - """Serialize the vector to a tuple.""" - return { - "type": "path", - "points": self._points.tolist(), - } - - @property - def points(self) -> np.ndarray: - """Get the path points as a numpy array.""" - return self._points - - def as_vectors(self) -> List[Vector]: - """Get the path points as Vector objects.""" - return [Vector(p) for p in self._points] - - def append(self, point: Union[Vector, np.ndarray, Tuple]) -> None: - """Append a point to the path. - - Args: - point: A Vector, numpy array, or tuple representing a point - """ - if isinstance(point, Vector): - point_data = point.data - else: - point_data = np.array(point, dtype=float) - - if len(self._points) == 0: - # If empty, create with correct dimensionality - self._points = np.array([point_data]) - else: - self._points = np.vstack((self._points, point_data)) - - def extend(self, points: Union[List[Vector], List[np.ndarray], List[Tuple], "Path"]) -> None: - """Extend the path with more points. - - Args: - points: List of points or another Path object - """ - if isinstance(points, Path): - if len(self._points) == 0: - self._points = points.points.copy() - else: - self._points = np.vstack((self._points, points.points)) - else: - for point in points: - self.append(point) - - def insert(self, index: int, point: Union[Vector, np.ndarray, Tuple]) -> None: - """Insert a point at a specific index. - - Args: - index: The index at which to insert the point - point: A Vector, numpy array, or tuple representing a point - """ - if isinstance(point, Vector): - point_data = point.data - else: - point_data = np.array(point, dtype=float) - - if len(self._points) == 0: - self._points = np.array([point_data]) - else: - self._points = np.insert(self._points, index, point_data, axis=0) - - def remove(self, index: int) -> np.ndarray: - """Remove and return the point at the given index. - - Args: - index: The index of the point to remove - - Returns: - The removed point as a numpy array - """ - point = self._points[index].copy() - self._points = np.delete(self._points, index, axis=0) - return point - - def clear(self) -> None: - """Remove all points from the path.""" - self._points = np.zeros((0, self._points.shape[1] if len(self._points) > 0 else 0), dtype=float) - - def length(self) -> float: - """Calculate the total length of the path. - - Returns: - The sum of the distances between consecutive points - """ - if len(self._points) < 2: - return 0.0 - - # Efficient vector calculation of consecutive point distances - diff = self._points[1:] - self._points[:-1] - segment_lengths = np.sqrt(np.sum(diff * diff, axis=1)) - return float(np.sum(segment_lengths)) - - def resample(self: T, point_spacing: float) -> T: - """Resample the path with approximately uniform spacing between points. - - Args: - point_spacing: The desired distance between consecutive points - - Returns: - A new Path object with resampled points - """ - if len(self._points) < 2 or point_spacing <= 0: - return self.__class__(self._points.copy()) - - resampled_points = [self._points[0].copy()] - accumulated_distance = 0.0 - - for i in range(1, len(self._points)): - current_point = self._points[i] - prev_point = self._points[i - 1] - segment_vector = current_point - prev_point - segment_length = np.linalg.norm(segment_vector) - - if segment_length < 1e-10: - continue - - direction = segment_vector / segment_length - - # Add points along this segment until we reach the end - while accumulated_distance + segment_length >= point_spacing: - # How far along this segment the next point should be - dist_along_segment = point_spacing - accumulated_distance - if dist_along_segment < 0: - break - - # Create the new point - new_point = prev_point + direction * dist_along_segment - resampled_points.append(new_point) - - # Update for next iteration - accumulated_distance = 0 - segment_length -= dist_along_segment - prev_point = new_point - - # Update the accumulated distance for the next segment - accumulated_distance += segment_length - - # Add the last point if it's not already there - if len(self._points) > 1: - last_point = self._points[-1] - if not np.array_equal(resampled_points[-1], last_point): - resampled_points.append(last_point.copy()) - - return self.__class__(np.array(resampled_points)) - - def simplify(self: T, tolerance: float) -> T: - """Simplify the path using the Ramer-Douglas-Peucker algorithm. - - Args: - tolerance: The maximum distance a point can deviate from the simplified path - - Returns: - A new simplified Path object - """ - if len(self._points) <= 2: - return self.__class__(self._points.copy()) - - # Implementation of Ramer-Douglas-Peucker algorithm - def rdp(points, epsilon, start, end): - if end <= start + 1: - return [start] - - # Find point with max distance from line - line_vec = points[end] - points[start] - line_length = np.linalg.norm(line_vec) - - if line_length < 1e-10: # If start and end points are the same - # Distance from next point to start point - max_dist = np.linalg.norm(points[start + 1] - points[start]) - max_idx = start + 1 - else: - max_dist = 0 - max_idx = start - - for i in range(start + 1, end): - # Distance from point to line - p_vec = points[i] - points[start] - - # Project p_vec onto line_vec - proj_scalar = np.dot(p_vec, line_vec) / (line_length * line_length) - proj = points[start] + proj_scalar * line_vec - - # Calculate perpendicular distance - dist = np.linalg.norm(points[i] - proj) - - if dist > max_dist: - max_dist = dist - max_idx = i - - # Recursive call - result = [] - if max_dist > epsilon: - result_left = rdp(points, epsilon, start, max_idx) - result_right = rdp(points, epsilon, max_idx, end) - result = result_left + result_right[1:] - else: - result = [start, end] - - return result - - indices = rdp(self._points, tolerance, 0, len(self._points) - 1) - indices.append(len(self._points) - 1) # Make sure the last point is included - indices = sorted(set(indices)) # Remove duplicates and sort - - return self.__class__(self._points[indices]) - - def smooth(self: T, weight: float = 0.5, iterations: int = 1) -> T: - """Smooth the path using a moving average filter. - - Args: - weight: How much to weight the neighboring points (0-1) - iterations: Number of smoothing passes to apply - - Returns: - A new smoothed Path object - """ - if len(self._points) <= 2 or weight <= 0 or iterations <= 0: - return self.__class__(self._points.copy()) - - smoothed_points = self._points.copy() - - for _ in range(iterations): - new_points = np.zeros_like(smoothed_points) - new_points[0] = smoothed_points[0] # Keep first point unchanged - - # Apply weighted average to middle points - for i in range(1, len(smoothed_points) - 1): - neighbor_avg = 0.5 * (smoothed_points[i - 1] + smoothed_points[i + 1]) - new_points[i] = (1 - weight) * smoothed_points[i] + weight * neighbor_avg - - new_points[-1] = smoothed_points[-1] # Keep last point unchanged - smoothed_points = new_points - - return self.__class__(smoothed_points) - - def nearest_point_index(self, point: Union[Vector, np.ndarray, Tuple]) -> int: - """Find the index of the closest point on the path to the given point. - - Args: - point: The reference point - - Returns: - Index of the closest point on the path - """ - if len(self._points) == 0: - raise ValueError("Cannot find nearest point in an empty path") - - if isinstance(point, Vector): - point_data = point.data - else: - point_data = np.array(point, dtype=float) - - # Calculate squared distances to all points - diff = self._points - point_data - sq_distances = np.sum(diff * diff, axis=1) - - # Return index of minimum distance - return int(np.argmin(sq_distances)) - - def reverse(self: T) -> T: - """Reverse the path direction. - - Returns: - A new Path object with points in reverse order - """ - return self.__class__(self._points[::-1].copy()) - - def __len__(self) -> int: - """Return the number of points in the path.""" - return len(self._points) - - def __getitem__(self, idx) -> Union[np.ndarray, "Path"]: - """Get a point or slice of points from the path.""" - if isinstance(idx, slice): - return self.__class__(self._points[idx]) - return self._points[idx].copy() - - def get_vector(self, idx: int) -> Vector: - """Get a point at the given index as a Vector object.""" - return Vector(self._points[idx]) - - def last(self) -> Vector: - """Get the first point in the path as a Vector object.""" - if len(self._points) == 0: - return None - return Vector(self._points[-1]) - - def head(self) -> Vector: - """Get the first point in the path as a Vector object.""" - if len(self._points) == 0: - return None - return Vector(self._points[0]) - - def tail(self) -> "Path": - """Get all points except the first point as a new Path object.""" - if len(self._points) <= 1: - return None - return self.__class__(self._points[1:].copy()) - - def __iter__(self) -> Iterator[np.ndarray]: - """Iterate over the points in the path.""" - for point in self._points: - yield point.copy() - - def __repr__(self) -> str: - """String representation of the path.""" - return f"↶ Path ({len(self._points)} Points)" - - def ipush(self, point: Union[Vector, np.ndarray, Tuple]) -> "Path": - """Return a new Path with `point` appended.""" - if isinstance(point, Vector): - p = point.data - else: - p = np.asarray(point, dtype=float) - - if len(self._points) == 0: - new_pts = p.reshape(1, -1) - else: - new_pts = np.vstack((self._points, p)) - return self.__class__(new_pts) - - def iclip_tail(self, max_len: int) -> "Path": - """Return a new Path containing only the last `max_len` points.""" - if max_len < 0: - raise ValueError("max_len must be ≥ 0") - return self.__class__(self._points[-max_len:]) - - def __add__(self, point): - """path + vec -> path.pushed(vec)""" - return self.pushed(point) - - -if __name__ == "__main__": - # Test vectors in various directions - print( - Path( - [ - Vector(1, 0), # Right - Vector(1, 1), # Up-Right - Vector(0, 1), # Up - Vector(-1, 1), # Up-Left - Vector(-1, 0), # Left - Vector(-1, -1), # Down-Left - Vector(0, -1), # Down - Vector(1, -1), # Down-Right - Vector(0.5, 0.5), # Up-Right (shorter) - Vector(-3, 4), # Up-Left (longer) - ] - ) - ) - - print(Path()) diff --git a/dimos/types/pointcloud.py b/dimos/types/pointcloud.py deleted file mode 100644 index 7fbdaa0203..0000000000 --- a/dimos/types/pointcloud.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import open3d as o3d -from typing import Any - -class PointCloudType: - def __init__(self, point_cloud: o3d.geometry.PointCloud, metadata: Any = None): - """ - Initializes a standardized point cloud type. - - Args: - point_cloud (o3d.geometry.PointCloud): The point cloud data. - metadata (Any, optional): Additional metadata related to the point cloud. - """ - self.point_cloud = point_cloud - self.metadata = metadata - - def downsample(self, voxel_size: float): - """Downsample the point cloud using a voxel grid filter.""" - self.point_cloud = self.point_cloud.voxel_down_sample(voxel_size) - - def save_to_file(self, filepath: str): - """Save the point cloud to a file.""" - o3d.io.write_point_cloud(filepath, self.point_cloud) \ No newline at end of file diff --git a/dimos/types/position.py b/dimos/types/position.py deleted file mode 100644 index d32820b92c..0000000000 --- a/dimos/types/position.py +++ /dev/null @@ -1,14 +0,0 @@ -from dataclasses import dataclass -from dimos.types.vector import Vector - - -@dataclass -class Position: - pos: Vector - rot: Vector - - def __repr__(self) -> str: - return f"pos({self.pos}), rot({self.rot})" - - def __str__(self) -> str: - return self.__repr__() diff --git a/dimos/types/robot_capabilities.py b/dimos/types/robot_capabilities.py new file mode 100644 index 0000000000..8c9a7fcd41 --- /dev/null +++ b/dimos/types/robot_capabilities.py @@ -0,0 +1,27 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Robot capabilities module for defining robot functionality.""" + +from enum import Enum, auto + + +class RobotCapability(Enum): + """Enum defining possible robot capabilities.""" + + MANIPULATION = auto() + VISION = auto() + AUDIO = auto() + SPEECH = auto() + LOCOMOTION = auto() diff --git a/dimos/types/robot_location.py b/dimos/types/robot_location.py index 6a7796193c..59a780daf5 100644 --- a/dimos/types/robot_location.py +++ b/dimos/types/robot_location.py @@ -17,18 +17,19 @@ """ from dataclasses import dataclass, field -from typing import Dict, Any, Optional, Tuple import time +from typing import Any import uuid + @dataclass class RobotLocation: """ Represents a named location in the robot's spatial memory. - + This class stores the position, rotation, and descriptive metadata for locations that the robot can remember and navigate to. - + Attributes: name: Human-readable name of the location (e.g., "kitchen", "office") position: 3D position coordinates (x, y, z) @@ -38,36 +39,37 @@ class RobotLocation: location_id: Unique identifier for this location metadata: Additional metadata for the location """ + name: str - position: Tuple[float, float, float] - rotation: Tuple[float, float, float] - frame_id: Optional[str] = None + position: tuple[float, float, float] + rotation: tuple[float, float, float] + frame_id: str | None = None timestamp: float = field(default_factory=time.time) location_id: str = field(default_factory=lambda: f"loc_{uuid.uuid4().hex[:8]}") - metadata: Dict[str, Any] = field(default_factory=dict) - - def __post_init__(self): + metadata: dict[str, Any] = field(default_factory=dict) + + def __post_init__(self) -> None: """Validate and normalize the position and rotation tuples.""" # Ensure position is a tuple of 3 floats if len(self.position) == 2: self.position = (self.position[0], self.position[1], 0.0) else: self.position = tuple(float(x) for x in self.position) - + # Ensure rotation is a tuple of 3 floats if len(self.rotation) == 1: self.rotation = (0.0, 0.0, self.rotation[0]) else: self.rotation = tuple(float(x) for x in self.rotation) - - def to_vector_metadata(self) -> Dict[str, Any]: + + def to_vector_metadata(self) -> dict[str, Any]: """ Convert the location to metadata format for storing in a vector database. - + Returns: Dictionary with metadata fields compatible with vector DB storage """ - return { + metadata = { "pos_x": float(self.position[0]), "pos_y": float(self.position[1]), "pos_z": float(self.position[2]), @@ -76,19 +78,24 @@ def to_vector_metadata(self) -> Dict[str, Any]: "rot_z": float(self.rotation[2]), "timestamp": self.timestamp, "location_id": self.location_id, - "frame_id": self.frame_id, "location_name": self.name, - "description": self.name # Makes it searchable by text + "description": self.name, # Makes it searchable by text } - + + # Only add frame_id if it's not None + if self.frame_id is not None: + metadata["frame_id"] = self.frame_id + + return metadata + @classmethod - def from_vector_metadata(cls, metadata: Dict[str, Any]) -> 'RobotLocation': + def from_vector_metadata(cls, metadata: dict[str, Any]) -> "RobotLocation": """ Create a RobotLocation object from vector database metadata. - + Args: metadata: Dictionary with metadata from vector database - + Returns: RobotLocation object """ @@ -97,18 +104,35 @@ def from_vector_metadata(cls, metadata: Dict[str, Any]) -> 'RobotLocation': position=( metadata.get("pos_x", 0.0), metadata.get("pos_y", 0.0), - metadata.get("pos_z", 0.0) + metadata.get("pos_z", 0.0), ), rotation=( metadata.get("rot_x", 0.0), metadata.get("rot_y", 0.0), - metadata.get("rot_z", 0.0) + metadata.get("rot_z", 0.0), ), frame_id=metadata.get("frame_id"), timestamp=metadata.get("timestamp", time.time()), location_id=metadata.get("location_id", f"loc_{uuid.uuid4().hex[:8]}"), - metadata={k: v for k, v in metadata.items() if k not in [ - "pos_x", "pos_y", "pos_z", "rot_x", "rot_y", "rot_z", - "timestamp", "location_id", "frame_id", "location_name", "description" - ]} + metadata={ + k: v + for k, v in metadata.items() + if k + not in [ + "pos_x", + "pos_y", + "pos_z", + "rot_x", + "rot_y", + "rot_z", + "timestamp", + "location_id", + "frame_id", + "location_name", + "description", + ] + }, ) + + def __str__(self) -> str: + return f"[RobotPosition name:{self.name} pos:{self.position} rot:{self.rotation})]" diff --git a/dimos/types/ros_polyfill.py b/dimos/types/ros_polyfill.py new file mode 100644 index 0000000000..c8919caec3 --- /dev/null +++ b/dimos/types/ros_polyfill.py @@ -0,0 +1,38 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +try: + from geometry_msgs.msg import Vector3 +except ImportError: + from dimos.msgs.geometry_msgs import Vector3 # type: ignore[import] + +try: + from geometry_msgs.msg import Point, Pose, Quaternion, Twist + from nav_msgs.msg import OccupancyGrid, Odometry + from std_msgs.msg import Header +except ImportError: + from dimos_lcm.geometry_msgs import Point, Pose, Quaternion, Twist + from dimos_lcm.nav_msgs import OccupancyGrid, Odometry + from dimos_lcm.std_msgs import Header + +__all__ = [ + "Header", + "OccupancyGrid", + "Odometry", + "Point", + "Pose", + "Quaternion", + "Twist", + "Vector3", +] diff --git a/dimos/types/sample.py b/dimos/types/sample.py index d21ae240c6..6d84942c55 100644 --- a/dimos/types/sample.py +++ b/dimos/types/sample.py @@ -12,24 +12,24 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json -import logging +import builtins from collections import OrderedDict +from collections.abc import Sequence from enum import Enum +import json +import logging from pathlib import Path -from typing import Any, Dict, List, Literal, Sequence, Union, get_origin +from typing import Annotated, Any, Literal, Union, get_origin -import numpy as np from datasets import Dataset from gymnasium import spaces from jsonref import replace_refs +from mbodied.data.utils import to_features +from mbodied.utils.import_utils import smart_import +import numpy as np from pydantic import BaseModel, ConfigDict, ValidationError from pydantic.fields import FieldInfo from pydantic_core import from_json -from typing_extensions import Annotated - -from mbodied.data.utils import to_features -from mbodied.utils.import_utils import smart_import Flattenable = Annotated[Literal["dict", "np", "pt", "list"], "Numpy, PyTorch, list, or dict"] @@ -81,7 +81,7 @@ class Sample(BaseModel): arbitrary_types_allowed=True, ) - def __init__(self, datum=None, **data): + def __init__(self, datum=None, **data) -> None: """Accepts an arbitrary datum as well as keyword arguments.""" if datum is not None: if isinstance(datum, Sample): @@ -100,7 +100,7 @@ def __str__(self) -> str: """Return a string representation of the Sample instance.""" return f"{self.__class__.__name__}({', '.join([f'{k}={v}' for k, v in self.dict().items() if v is not None])})" - def dict(self, exclude_none=True, exclude: set[str] = None) -> Dict[str, Any]: + def dict(self, exclude_none: bool = True, exclude: set[str] | None = None) -> dict[str, Any]: """Return the Sample object as a dictionary with None values excluded. Args: @@ -142,7 +142,7 @@ def unflatten(cls, one_d_array_or_dict, schema=None) -> "Sample": else: flat_data = list(one_d_array_or_dict) - def unflatten_recursive(schema_part, index=0): + def unflatten_recursive(schema_part, index: int = 0): if schema_part["type"] == "object": result = {} for prop, prop_schema in schema_part["properties"].items(): @@ -165,10 +165,10 @@ def flatten( self, output_type: Flattenable = "dict", non_numerical: Literal["ignore", "forbid", "allow"] = "allow", - ) -> Dict[str, Any] | np.ndarray | "torch.Tensor" | List: + ) -> builtins.dict[str, Any] | np.ndarray | "torch.Tensor" | list: accumulator = {} if output_type == "dict" else [] - def flatten_recursive(obj, path=""): + def flatten_recursive(obj, path: str = "") -> None: if isinstance(obj, Sample): for k, v in obj.dict().items(): flatten_recursive(v, path + k + "/") @@ -196,7 +196,9 @@ def flatten_recursive(obj, path=""): flatten_recursive(self) accumulator = accumulator.values() if output_type == "dict" else accumulator - if non_numerical == "forbid" and any(not isinstance(v, int | float | bool) for v in accumulator): + if non_numerical == "forbid" and any( + not isinstance(v, int | float | bool) for v in accumulator + ): raise ValueError("Non-numerical values found in flattened data.") if output_type == "np": return np.array(accumulator) @@ -206,7 +208,7 @@ def flatten_recursive(obj, path=""): return accumulator @staticmethod - def obj_to_schema(value: Any) -> Dict: + def obj_to_schema(value: Any) -> builtins.dict: """Generates a simplified JSON schema from a dictionary. Args: @@ -216,7 +218,10 @@ def obj_to_schema(value: Any) -> Dict: dict: A simplified JSON schema representing the structure of the dictionary. """ if isinstance(value, dict): - return {"type": "object", "properties": {k: Sample.obj_to_schema(v) for k, v in value.items()}} + return { + "type": "object", + "properties": {k: Sample.obj_to_schema(v) for k, v in value.items()}, + } if isinstance(value, list | tuple | np.ndarray): if len(value) > 0: return {"type": "array", "items": Sample.obj_to_schema(value[0])} @@ -231,7 +236,9 @@ def obj_to_schema(value: Any) -> Dict: return {"type": "boolean"} return {} - def schema(self, resolve_refs: bool = True, include_descriptions=False) -> Dict: + def schema( + self, resolve_refs: bool = True, include_descriptions: bool = False + ) -> builtins.dict: """Returns a simplified json schema. Removing additionalProperties, @@ -260,7 +267,9 @@ def schema(self, resolve_refs: bool = True, include_descriptions=False) -> Dict: if key not in properties: properties[key] = Sample.obj_to_schema(value) if isinstance(value, Sample): - properties[key] = value.schema(resolve_refs=resolve_refs, include_descriptions=include_descriptions) + properties[key] = value.schema( + resolve_refs=resolve_refs, include_descriptions=include_descriptions + ) else: properties[key] = Sample.obj_to_schema(value) return schema @@ -399,10 +408,10 @@ def space_for( raise ValueError(f"Unsupported object {value} of type: {type(value)} for space generation") @classmethod - def init_from(cls, d: Any, pack=False) -> "Sample": + def init_from(cls, d: Any, pack: bool = False) -> "Sample": if isinstance(d, spaces.Space): return cls.from_space(d) - if isinstance(d, Union[Sequence, np.ndarray]): # noqa: UP007 + if isinstance(d, Union[Sequence, np.ndarray]): if pack: return cls.pack_from(d) return cls.unflatten(d) @@ -420,7 +429,9 @@ def init_from(cls, d: Any, pack=False) -> "Sample": return cls(d) @classmethod - def from_flat_dict(cls, flat_dict: Dict[str, Any], schema: Dict = None) -> "Sample": + def from_flat_dict( + cls, flat_dict: builtins.dict[str, Any], schema: builtins.dict | None = None + ) -> "Sample": """Initialize a Sample instance from a flattened dictionary.""" """ Reconstructs the original JSON object from a flattened dictionary using the provided schema. @@ -459,7 +470,7 @@ def from_space(cls, space: spaces.Space) -> "Sample": return cls(sampled) @classmethod - def pack_from(cls, samples: List[Union["Sample", Dict]]) -> "Sample": + def pack_from(cls, samples: list[Union["Sample", builtins.dict]]) -> "Sample": """Pack a list of samples into a single sample with lists for attributes. Args: @@ -489,7 +500,7 @@ def pack_from(cls, samples: List[Union["Sample", Dict]]) -> "Sample": aggregated[attr].append(getattr(sample, attr, None)) return cls(**aggregated) - def unpack(self, to_dicts=False) -> List[Union["Sample", Dict]]: + def unpack(self, to_dicts: bool = False) -> list[Union["Sample", builtins.dict]]: """Unpack the packed Sample object into a list of Sample objects or dictionaries.""" attributes = list(self.model_extra.keys()) + list(self.model_fields.keys()) attributes = [attr for attr in attributes if getattr(self, attr) is not None] @@ -497,7 +508,9 @@ def unpack(self, to_dicts=False) -> List[Union["Sample", Dict]]: return [] # Ensure all attributes are lists and have the same length - list_sizes = {len(getattr(self, attr)) for attr in attributes if isinstance(getattr(self, attr), list)} + list_sizes = { + len(getattr(self, attr)) for attr in attributes if isinstance(getattr(self, attr), list) + } if len(list_sizes) != 1: raise ValueError("Not all attribute lists have the same length.") list_size = list_sizes.pop() @@ -505,7 +518,10 @@ def unpack(self, to_dicts=False) -> List[Union["Sample", Dict]]: if to_dicts: return [{key: getattr(self, key)[i] for key in attributes} for i in range(list_size)] - return [self.__class__(**{key: getattr(self, key)[i] for key in attributes}) for i in range(list_size)] + return [ + self.__class__(**{key: getattr(self, key)[i] for key in attributes}) + for i in range(list_size) + ] @classmethod def default_space(cls) -> spaces.Dict: @@ -513,7 +529,9 @@ def default_space(cls) -> spaces.Dict: return cls().space() @classmethod - def default_sample(cls, output_type="Sample") -> Union["Sample", Dict[str, Any]]: + def default_sample( + cls, output_type: str = "Sample" + ) -> Union["Sample", builtins.dict[str, Any]]: """Generate a default Sample instance from its class attributes. Useful for padding. This is the "no-op" instance and should be overriden as needed. @@ -542,8 +560,10 @@ def space(self) -> spaces.Dict: for key, value in self.dict().items(): logging.debug("Generating space for key: '%s', value: %s", key, value) info = self.model_field_info(key) - value = getattr(self, key) if hasattr(self, key) else value # noqa: PLW2901 - space_dict[key] = value.space() if isinstance(value, Sample) else self.space_for(value, info=info) + value = getattr(self, key) if hasattr(self, key) else value + space_dict[key] = ( + value.space() if isinstance(value, Sample) else self.space_for(value, info=info) + ) return spaces.Dict(space_dict) def random_sample(self) -> "Sample": @@ -555,4 +575,4 @@ def random_sample(self) -> "Sample": if __name__ == "__main__": - sample = Sample(x=1, y=2, z={"a": 3, "b": 4}, extra_field=5) \ No newline at end of file + sample = Sample(x=1, y=2, z={"a": 3, "b": 4}, extra_field=5) diff --git a/dimos/types/segmentation.py b/dimos/types/segmentation.py index 4b2c53f3a4..1f3c2a0773 100644 --- a/dimos/types/segmentation.py +++ b/dimos/types/segmentation.py @@ -12,11 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import List, Any +from typing import Any + import numpy as np + class SegmentationType: - def __init__(self, masks: List[np.ndarray], metadata: Any = None): + def __init__(self, masks: list[np.ndarray], metadata: Any = None) -> None: """ Initializes a standardized segmentation type. @@ -25,7 +27,7 @@ def __init__(self, masks: List[np.ndarray], metadata: Any = None): metadata (Any, optional): Additional metadata related to the segmentations. """ self.masks = masks - self.metadata = metadata + self.metadata = metadata def combine_masks(self): """Combine all masks into a single mask.""" @@ -34,9 +36,10 @@ def combine_masks(self): combined_mask = np.logical_or(combined_mask, mask) return combined_mask - def save_masks(self, directory: str): + def save_masks(self, directory: str) -> None: """Save each mask to a separate file.""" import os + os.makedirs(directory, exist_ok=True) for i, mask in enumerate(self.masks): - np.save(os.path.join(directory, f"mask_{i}.npy"), mask) \ No newline at end of file + np.save(os.path.join(directory, f"mask_{i}.npy"), mask) diff --git a/dimos/types/test_timestamped.py b/dimos/types/test_timestamped.py new file mode 100644 index 0000000000..7eae7a8ad3 --- /dev/null +++ b/dimos/types/test_timestamped.py @@ -0,0 +1,578 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import datetime, timezone +import time + +import pytest +from reactivex import operators as ops +from reactivex.scheduler import ThreadPoolScheduler + +from dimos.msgs.sensor_msgs import Image +from dimos.types.timestamped import ( + Timestamped, + TimestampedBufferCollection, + TimestampedCollection, + align_timestamped, + to_datetime, + to_ros_stamp, +) +from dimos.utils import testing +from dimos.utils.data import get_data +from dimos.utils.reactive import backpressure + + +def test_timestamped_dt_method() -> None: + ts = 1751075203.4120464 + timestamped = Timestamped(ts) + dt = timestamped.dt() + assert isinstance(dt, datetime) + assert abs(dt.timestamp() - ts) < 1e-6 + assert dt.tzinfo is not None, "datetime should be timezone-aware" + + +def test_to_ros_stamp() -> None: + """Test the to_ros_stamp function with different input types.""" + + # Test with float timestamp + ts_float = 1234567890.123456789 + result = to_ros_stamp(ts_float) + assert result.sec == 1234567890 + # Float precision limitation - check within reasonable range + assert abs(result.nanosec - 123456789) < 1000 + + # Test with integer timestamp + ts_int = 1234567890 + result = to_ros_stamp(ts_int) + assert result.sec == 1234567890 + assert result.nanosec == 0 + + # Test with datetime object + dt = datetime(2009, 2, 13, 23, 31, 30, 123456, tzinfo=timezone.utc) + result = to_ros_stamp(dt) + assert result.sec == 1234567890 + assert abs(result.nanosec - 123456000) < 1000 # Allow small rounding error + + +def test_to_datetime() -> None: + """Test the to_datetime function with different input types.""" + + # Test with float timestamp + ts_float = 1234567890.123456 + dt = to_datetime(ts_float) + assert isinstance(dt, datetime) + assert dt.tzinfo is not None # Should have timezone + assert abs(dt.timestamp() - ts_float) < 1e-6 + + # Test with integer timestamp + ts_int = 1234567890 + dt = to_datetime(ts_int) + assert isinstance(dt, datetime) + assert dt.tzinfo is not None + assert dt.timestamp() == ts_int + + # Test with RosStamp + ros_stamp = {"sec": 1234567890, "nanosec": 123456000} + dt = to_datetime(ros_stamp) + assert isinstance(dt, datetime) + assert dt.tzinfo is not None + expected_ts = 1234567890.123456 + assert abs(dt.timestamp() - expected_ts) < 1e-6 + + # Test with datetime (already has timezone) + dt_input = datetime(2009, 2, 13, 23, 31, 30, tzinfo=timezone.utc) + dt_result = to_datetime(dt_input) + assert dt_result.tzinfo is not None + # Should convert to local timezone by default + + # Test with naive datetime (no timezone) + dt_naive = datetime(2009, 2, 13, 23, 31, 30) + dt_result = to_datetime(dt_naive) + assert dt_result.tzinfo is not None + + # Test with specific timezone + dt_utc = to_datetime(ts_float, tz=timezone.utc) + assert dt_utc.tzinfo == timezone.utc + assert abs(dt_utc.timestamp() - ts_float) < 1e-6 + + +class SimpleTimestamped(Timestamped): + def __init__(self, ts: float, data: str) -> None: + super().__init__(ts) + self.data = data + + +@pytest.fixture +def test_scheduler(): + """Fixture that provides a ThreadPoolScheduler and cleans it up after the test.""" + scheduler = ThreadPoolScheduler(max_workers=6) + yield scheduler + # Cleanup after test + scheduler.executor.shutdown(wait=True) + time.sleep(0.2) # Give threads time to finish cleanup + + +@pytest.fixture +def sample_items(): + return [ + SimpleTimestamped(1.0, "first"), + SimpleTimestamped(3.0, "third"), + SimpleTimestamped(5.0, "fifth"), + SimpleTimestamped(7.0, "seventh"), + ] + + +@pytest.fixture +def collection(sample_items): + return TimestampedCollection(sample_items) + + +def test_empty_collection() -> None: + collection = TimestampedCollection() + assert len(collection) == 0 + assert collection.duration() == 0.0 + assert collection.time_range() is None + assert collection.find_closest(1.0) is None + + +def test_add_items() -> None: + collection = TimestampedCollection() + item1 = SimpleTimestamped(2.0, "two") + item2 = SimpleTimestamped(1.0, "one") + + collection.add(item1) + collection.add(item2) + + assert len(collection) == 2 + assert collection[0].data == "one" # Should be sorted by timestamp + assert collection[1].data == "two" + + +def test_find_closest(collection) -> None: + # Exact match + assert collection.find_closest(3.0).data == "third" + + # Between items (closer to left) + assert collection.find_closest(1.5, tolerance=1.0).data == "first" + + # Between items (closer to right) + assert collection.find_closest(3.5, tolerance=1.0).data == "third" + + # Exactly in the middle (should pick the later one due to >= comparison) + assert ( + collection.find_closest(4.0, tolerance=1.0).data == "fifth" + ) # 4.0 is equidistant from 3.0 and 5.0 + + # Before all items + assert collection.find_closest(0.0, tolerance=1.0).data == "first" + + # After all items + assert collection.find_closest(10.0, tolerance=4.0).data == "seventh" + + # low tolerance, should return None + assert collection.find_closest(10.0, tolerance=2.0) is None + + +def test_find_before_after(collection) -> None: + # Find before + assert collection.find_before(2.0).data == "first" + assert collection.find_before(5.5).data == "fifth" + assert collection.find_before(1.0) is None # Nothing before first item + + # Find after + assert collection.find_after(2.0).data == "third" + assert collection.find_after(5.0).data == "seventh" + assert collection.find_after(7.0) is None # Nothing after last item + + +def test_merge_collections() -> None: + collection1 = TimestampedCollection( + [ + SimpleTimestamped(1.0, "a"), + SimpleTimestamped(3.0, "c"), + ] + ) + collection2 = TimestampedCollection( + [ + SimpleTimestamped(2.0, "b"), + SimpleTimestamped(4.0, "d"), + ] + ) + + merged = collection1.merge(collection2) + + assert len(merged) == 4 + assert [item.data for item in merged] == ["a", "b", "c", "d"] + + +def test_duration_and_range(collection) -> None: + assert collection.duration() == 6.0 # 7.0 - 1.0 + assert collection.time_range() == (1.0, 7.0) + + +def test_slice_by_time(collection) -> None: + # Slice inclusive of boundaries + sliced = collection.slice_by_time(2.0, 6.0) + assert len(sliced) == 2 + assert sliced[0].data == "third" + assert sliced[1].data == "fifth" + + # Empty slice + empty_slice = collection.slice_by_time(8.0, 10.0) + assert len(empty_slice) == 0 + + # Slice all + all_slice = collection.slice_by_time(0.0, 10.0) + assert len(all_slice) == 4 + + +def test_iteration(collection) -> None: + items = list(collection) + assert len(items) == 4 + assert [item.ts for item in items] == [1.0, 3.0, 5.0, 7.0] + + +def test_single_item_collection() -> None: + single = TimestampedCollection([SimpleTimestamped(5.0, "only")]) + assert single.duration() == 0.0 + assert single.time_range() == (5.0, 5.0) + + +def test_time_window_collection() -> None: + # Create a collection with a 2-second window + window = TimestampedBufferCollection[SimpleTimestamped](window_duration=2.0) + + # Add messages at different timestamps + window.add(SimpleTimestamped(1.0, "msg1")) + window.add(SimpleTimestamped(2.0, "msg2")) + window.add(SimpleTimestamped(3.0, "msg3")) + + # At this point, all messages should be present (within 2s window) + assert len(window) == 3 + + # Add a message at t=4.0, should keep messages from t=2.0 onwards + window.add(SimpleTimestamped(4.0, "msg4")) + assert len(window) == 3 # msg1 should be dropped + assert window[0].data == "msg2" # oldest is now msg2 + assert window[-1].data == "msg4" # newest is msg4 + + # Add a message at t=5.5, should drop msg2 and msg3 + window.add(SimpleTimestamped(5.5, "msg5")) + assert len(window) == 2 # only msg4 and msg5 remain + assert window[0].data == "msg4" + assert window[1].data == "msg5" + + # Verify time range + assert window.start_ts == 4.0 + assert window.end_ts == 5.5 + + +def test_timestamp_alignment(test_scheduler) -> None: + speed = 5.0 + + # ensure that lfs package is downloaded + get_data("unitree_office_walk") + + raw_frames = [] + + def spy(image): + raw_frames.append(image.ts) + print(image.ts) + return image + + # sensor reply of raw video frames + video_raw = ( + testing.TimedSensorReplay( + "unitree_office_walk/video", autocast=lambda x: Image.from_numpy(x).to_rgb() + ) + .stream(speed) + .pipe(ops.take(30)) + ) + + processed_frames = [] + + def process_video_frame(frame): + processed_frames.append(frame.ts) + time.sleep(0.5 / speed) + return frame + + # fake reply of some 0.5s processor of video frames that drops messages + # Pass the scheduler to backpressure to manage threads properly + fake_video_processor = backpressure( + video_raw.pipe(ops.map(spy)), scheduler=test_scheduler + ).pipe(ops.map(process_video_frame)) + + aligned_frames = align_timestamped(fake_video_processor, video_raw).pipe(ops.to_list()).run() + + assert len(raw_frames) == 30 + assert len(processed_frames) > 2 + assert len(aligned_frames) > 2 + + # Due to async processing, the last frame might not be aligned before completion + assert len(aligned_frames) >= len(processed_frames) - 1 + + for value in aligned_frames: + [primary, secondary] = value + diff = abs(primary.ts - secondary.ts) + print( + f"Aligned pair: primary={primary.ts:.6f}, secondary={secondary.ts:.6f}, diff={diff:.6f}s" + ) + assert diff <= 0.05 + + assert len(aligned_frames) > 2 + + +def test_timestamp_alignment_primary_first() -> None: + """Test alignment when primary messages arrive before secondary messages.""" + from reactivex import Subject + + primary_subject = Subject() + secondary_subject = Subject() + + results = [] + + # Set up alignment with a 2-second buffer + aligned = align_timestamped( + primary_subject, secondary_subject, buffer_size=2.0, match_tolerance=0.1 + ) + + # Subscribe to collect results + aligned.subscribe(lambda x: results.append(x)) + + # Send primary messages first + primary1 = SimpleTimestamped(1.0, "primary1") + primary2 = SimpleTimestamped(2.0, "primary2") + primary3 = SimpleTimestamped(3.0, "primary3") + + primary_subject.on_next(primary1) + primary_subject.on_next(primary2) + primary_subject.on_next(primary3) + + # At this point, no results should be emitted (no secondaries yet) + assert len(results) == 0 + + # Send secondary messages that match primary1 and primary2 + secondary1 = SimpleTimestamped(1.05, "secondary1") # Matches primary1 + secondary2 = SimpleTimestamped(2.02, "secondary2") # Matches primary2 + + secondary_subject.on_next(secondary1) + assert len(results) == 1 # primary1 should now be matched + assert results[0][0].data == "primary1" + assert results[0][1].data == "secondary1" + + secondary_subject.on_next(secondary2) + assert len(results) == 2 # primary2 should now be matched + assert results[1][0].data == "primary2" + assert results[1][1].data == "secondary2" + + # Send a secondary that's too far from primary3 + secondary_far = SimpleTimestamped(3.5, "secondary_far") # Too far from primary3 + secondary_subject.on_next(secondary_far) + # At this point primary3 is removed as unmatchable since secondary progressed past it + assert len(results) == 2 # primary3 should not match (outside tolerance) + + # Send a new primary that can match with the future secondary + primary4 = SimpleTimestamped(3.45, "primary4") + primary_subject.on_next(primary4) + assert len(results) == 3 # Should match with secondary_far + assert results[2][0].data == "primary4" + assert results[2][1].data == "secondary_far" + + # Complete the streams + primary_subject.on_completed() + secondary_subject.on_completed() + + +def test_timestamp_alignment_multiple_secondaries() -> None: + """Test alignment with multiple secondary observables.""" + from reactivex import Subject + + primary_subject = Subject() + secondary1_subject = Subject() + secondary2_subject = Subject() + + results = [] + + # Set up alignment with two secondary streams + aligned = align_timestamped( + primary_subject, + secondary1_subject, + secondary2_subject, + buffer_size=1.0, + match_tolerance=0.05, + ) + + # Subscribe to collect results + aligned.subscribe(lambda x: results.append(x)) + + # Send a primary message + primary1 = SimpleTimestamped(1.0, "primary1") + primary_subject.on_next(primary1) + + # No results yet (waiting for both secondaries) + assert len(results) == 0 + + # Send first secondary + sec1_msg1 = SimpleTimestamped(1.01, "sec1_msg1") + secondary1_subject.on_next(sec1_msg1) + + # Still no results (waiting for secondary2) + assert len(results) == 0 + + # Send second secondary + sec2_msg1 = SimpleTimestamped(1.02, "sec2_msg1") + secondary2_subject.on_next(sec2_msg1) + + # Now we should have a result + assert len(results) == 1 + assert results[0][0].data == "primary1" + assert results[0][1].data == "sec1_msg1" + assert results[0][2].data == "sec2_msg1" + + # Test partial match (one secondary missing) + primary2 = SimpleTimestamped(2.0, "primary2") + primary_subject.on_next(primary2) + + # Send only one secondary + sec1_msg2 = SimpleTimestamped(2.01, "sec1_msg2") + secondary1_subject.on_next(sec1_msg2) + + # No result yet + assert len(results) == 1 + + # Send a secondary2 that's too far + sec2_far = SimpleTimestamped(2.1, "sec2_far") # Outside tolerance + secondary2_subject.on_next(sec2_far) + + # Still no result (secondary2 is outside tolerance) + assert len(results) == 1 + + # Complete the streams + primary_subject.on_completed() + secondary1_subject.on_completed() + secondary2_subject.on_completed() + + +def test_timestamp_alignment_delayed_secondary() -> None: + """Test alignment when secondary messages arrive late but still within tolerance.""" + from reactivex import Subject + + primary_subject = Subject() + secondary_subject = Subject() + + results = [] + + # Set up alignment with a 2-second buffer + aligned = align_timestamped( + primary_subject, secondary_subject, buffer_size=2.0, match_tolerance=0.1 + ) + + # Subscribe to collect results + aligned.subscribe(lambda x: results.append(x)) + + # Send primary messages + primary1 = SimpleTimestamped(1.0, "primary1") + primary2 = SimpleTimestamped(2.0, "primary2") + primary3 = SimpleTimestamped(3.0, "primary3") + + primary_subject.on_next(primary1) + primary_subject.on_next(primary2) + primary_subject.on_next(primary3) + + # No results yet + assert len(results) == 0 + + # Send delayed secondaries (in timestamp order) + secondary1 = SimpleTimestamped(1.05, "secondary1") # Matches primary1 + secondary_subject.on_next(secondary1) + assert len(results) == 1 # primary1 matched + assert results[0][0].data == "primary1" + assert results[0][1].data == "secondary1" + + secondary2 = SimpleTimestamped(2.02, "secondary2") # Matches primary2 + secondary_subject.on_next(secondary2) + assert len(results) == 2 # primary2 matched + assert results[1][0].data == "primary2" + assert results[1][1].data == "secondary2" + + # Now send a secondary that's past primary3's match window + secondary_future = SimpleTimestamped(3.2, "secondary_future") # Too far from primary3 + secondary_subject.on_next(secondary_future) + # At this point, primary3 should be removed as unmatchable + assert len(results) == 2 # No new matches + + # Send a new primary that can match with secondary_future + primary4 = SimpleTimestamped(3.15, "primary4") + primary_subject.on_next(primary4) + assert len(results) == 3 # Should match immediately + assert results[2][0].data == "primary4" + assert results[2][1].data == "secondary_future" + + # Complete the streams + primary_subject.on_completed() + secondary_subject.on_completed() + + +def test_timestamp_alignment_buffer_cleanup() -> None: + """Test that old buffered primaries are cleaned up.""" + import time as time_module + + from reactivex import Subject + + primary_subject = Subject() + secondary_subject = Subject() + + results = [] + + # Set up alignment with a 0.5-second buffer + aligned = align_timestamped( + primary_subject, secondary_subject, buffer_size=0.5, match_tolerance=0.05 + ) + + # Subscribe to collect results + aligned.subscribe(lambda x: results.append(x)) + + # Use real timestamps for this test + now = time_module.time() + + # Send an old primary + old_primary = Timestamped(now - 1.0) # 1 second ago + old_primary.data = "old" + primary_subject.on_next(old_primary) + + # Send a recent secondary to trigger cleanup + recent_secondary = Timestamped(now) + recent_secondary.data = "recent" + secondary_subject.on_next(recent_secondary) + + # Old primary should not match (outside buffer window) + assert len(results) == 0 + + # Send a matching pair within buffer + new_primary = Timestamped(now + 0.1) + new_primary.data = "new_primary" + new_secondary = Timestamped(now + 0.11) + new_secondary.data = "new_secondary" + + primary_subject.on_next(new_primary) + secondary_subject.on_next(new_secondary) + + # Should have one match + assert len(results) == 1 + assert results[0][0].data == "new_primary" + assert results[0][1].data == "new_secondary" + + # Complete the streams + primary_subject.on_completed() + secondary_subject.on_completed() diff --git a/dimos/types/test_vector.py b/dimos/types/test_vector.py new file mode 100644 index 0000000000..5462fda9a4 --- /dev/null +++ b/dimos/types/test_vector.py @@ -0,0 +1,384 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import numpy as np +import pytest + +from dimos.types.vector import Vector + + +def test_vector_default_init() -> None: + """Test that default initialization of Vector() has x,y,z components all zero.""" + v = Vector() + assert v.x == 0.0 + assert v.y == 0.0 + assert v.z == 0.0 + assert v.dim == 0 + assert len(v.data) == 0 + assert v.to_list() == [] + assert v.is_zero() # Empty vector should be considered zero + + +def test_vector_specific_init() -> None: + """Test initialization with specific values.""" + # 2D vector + v1 = Vector(1.0, 2.0) + assert v1.x == 1.0 + assert v1.y == 2.0 + assert v1.z == 0.0 + assert v1.dim == 2 + + # 3D vector + v2 = Vector(3.0, 4.0, 5.0) + assert v2.x == 3.0 + assert v2.y == 4.0 + assert v2.z == 5.0 + assert v2.dim == 3 + + # From list + v3 = Vector([6.0, 7.0, 8.0]) + assert v3.x == 6.0 + assert v3.y == 7.0 + assert v3.z == 8.0 + assert v3.dim == 3 + + # From numpy array + v4 = Vector(np.array([9.0, 10.0, 11.0])) + assert v4.x == 9.0 + assert v4.y == 10.0 + assert v4.z == 11.0 + assert v4.dim == 3 + + +def test_vector_addition() -> None: + """Test vector addition.""" + v1 = Vector(1.0, 2.0, 3.0) + v2 = Vector(4.0, 5.0, 6.0) + + v_add = v1 + v2 + assert v_add.x == 5.0 + assert v_add.y == 7.0 + assert v_add.z == 9.0 + + +def test_vector_subtraction() -> None: + """Test vector subtraction.""" + v1 = Vector(1.0, 2.0, 3.0) + v2 = Vector(4.0, 5.0, 6.0) + + v_sub = v2 - v1 + assert v_sub.x == 3.0 + assert v_sub.y == 3.0 + assert v_sub.z == 3.0 + + +def test_vector_scalar_multiplication() -> None: + """Test vector multiplication by a scalar.""" + v1 = Vector(1.0, 2.0, 3.0) + + v_mul = v1 * 2.0 + assert v_mul.x == 2.0 + assert v_mul.y == 4.0 + assert v_mul.z == 6.0 + + # Test right multiplication + v_rmul = 2.0 * v1 + assert v_rmul.x == 2.0 + assert v_rmul.y == 4.0 + assert v_rmul.z == 6.0 + + +def test_vector_scalar_division() -> None: + """Test vector division by a scalar.""" + v2 = Vector(4.0, 5.0, 6.0) + + v_div = v2 / 2.0 + assert v_div.x == 2.0 + assert v_div.y == 2.5 + assert v_div.z == 3.0 + + +def test_vector_dot_product() -> None: + """Test vector dot product.""" + v1 = Vector(1.0, 2.0, 3.0) + v2 = Vector(4.0, 5.0, 6.0) + + dot = v1.dot(v2) + assert dot == 32.0 + + +def test_vector_length() -> None: + """Test vector length calculation.""" + # 2D vector with length 5 + v1 = Vector(3.0, 4.0) + assert v1.length() == 5.0 + + # 3D vector + v2 = Vector(2.0, 3.0, 6.0) + assert v2.length() == pytest.approx(7.0, 0.001) + + # Test length_squared + assert v1.length_squared() == 25.0 + assert v2.length_squared() == 49.0 + + +def test_vector_normalize() -> None: + """Test vector normalization.""" + v = Vector(2.0, 3.0, 6.0) + assert not v.is_zero() + + v_norm = v.normalize() + length = v.length() + expected_x = 2.0 / length + expected_y = 3.0 / length + expected_z = 6.0 / length + + assert np.isclose(v_norm.x, expected_x) + assert np.isclose(v_norm.y, expected_y) + assert np.isclose(v_norm.z, expected_z) + assert np.isclose(v_norm.length(), 1.0) + assert not v_norm.is_zero() + + # Test normalizing a zero vector + v_zero = Vector(0.0, 0.0, 0.0) + assert v_zero.is_zero() + v_zero_norm = v_zero.normalize() + assert v_zero_norm.x == 0.0 + assert v_zero_norm.y == 0.0 + assert v_zero_norm.z == 0.0 + assert v_zero_norm.is_zero() + + +def test_vector_to_2d() -> None: + """Test conversion to 2D vector.""" + v = Vector(2.0, 3.0, 6.0) + + v_2d = v.to_2d() + assert v_2d.x == 2.0 + assert v_2d.y == 3.0 + assert v_2d.z == 0.0 + assert v_2d.dim == 2 + + # Already 2D vector + v2 = Vector(4.0, 5.0) + v2_2d = v2.to_2d() + assert v2_2d.x == 4.0 + assert v2_2d.y == 5.0 + assert v2_2d.dim == 2 + + +def test_vector_distance() -> None: + """Test distance calculations between vectors.""" + v1 = Vector(1.0, 2.0, 3.0) + v2 = Vector(4.0, 6.0, 8.0) + + # Distance + dist = v1.distance(v2) + expected_dist = np.sqrt(9.0 + 16.0 + 25.0) # sqrt((4-1)² + (6-2)² + (8-3)²) + assert dist == pytest.approx(expected_dist) + + # Distance squared + dist_sq = v1.distance_squared(v2) + assert dist_sq == 50.0 # 9 + 16 + 25 + + +def test_vector_cross_product() -> None: + """Test vector cross product.""" + v1 = Vector(1.0, 0.0, 0.0) # Unit x vector + v2 = Vector(0.0, 1.0, 0.0) # Unit y vector + + # v1 × v2 should be unit z vector + cross = v1.cross(v2) + assert cross.x == 0.0 + assert cross.y == 0.0 + assert cross.z == 1.0 + + # Test with more complex vectors + a = Vector(2.0, 3.0, 4.0) + b = Vector(5.0, 6.0, 7.0) + c = a.cross(b) + + # Cross product manually calculated: + # (3*7-4*6, 4*5-2*7, 2*6-3*5) + assert c.x == -3.0 + assert c.y == 6.0 + assert c.z == -3.0 + + # Test with 2D vectors (should raise error) + v_2d = Vector(1.0, 2.0) + with pytest.raises(ValueError): + v_2d.cross(v2) + + +def test_vector_zeros() -> None: + """Test Vector.zeros class method.""" + # 3D zero vector + v_zeros = Vector.zeros(3) + assert v_zeros.x == 0.0 + assert v_zeros.y == 0.0 + assert v_zeros.z == 0.0 + assert v_zeros.dim == 3 + assert v_zeros.is_zero() + + # 2D zero vector + v_zeros_2d = Vector.zeros(2) + assert v_zeros_2d.x == 0.0 + assert v_zeros_2d.y == 0.0 + assert v_zeros_2d.z == 0.0 + assert v_zeros_2d.dim == 2 + assert v_zeros_2d.is_zero() + + +def test_vector_ones() -> None: + """Test Vector.ones class method.""" + # 3D ones vector + v_ones = Vector.ones(3) + assert v_ones.x == 1.0 + assert v_ones.y == 1.0 + assert v_ones.z == 1.0 + assert v_ones.dim == 3 + + # 2D ones vector + v_ones_2d = Vector.ones(2) + assert v_ones_2d.x == 1.0 + assert v_ones_2d.y == 1.0 + assert v_ones_2d.z == 0.0 + assert v_ones_2d.dim == 2 + + +def test_vector_conversion_methods() -> None: + """Test vector conversion methods (to_list, to_tuple, to_numpy).""" + v = Vector(1.0, 2.0, 3.0) + + # to_list + assert v.to_list() == [1.0, 2.0, 3.0] + + # to_tuple + assert v.to_tuple() == (1.0, 2.0, 3.0) + + # to_numpy + np_array = v.to_numpy() + assert isinstance(np_array, np.ndarray) + assert np.array_equal(np_array, np.array([1.0, 2.0, 3.0])) + + +def test_vector_equality() -> None: + """Test vector equality.""" + v1 = Vector(1, 2, 3) + v2 = Vector(1, 2, 3) + v3 = Vector(4, 5, 6) + + assert v1 == v2 + assert v1 != v3 + assert v1 != Vector(1, 2) # Different dimensions + assert v1 != Vector(1.1, 2, 3) # Different values + assert v1 != [1, 2, 3] + + +def test_vector_is_zero() -> None: + """Test is_zero method for vectors.""" + # Default empty vector + v0 = Vector() + assert v0.is_zero() + + # Explicit zero vector + v1 = Vector(0.0, 0.0, 0.0) + assert v1.is_zero() + + # Zero vector with different dimensions + v2 = Vector(0.0, 0.0) + assert v2.is_zero() + + # Non-zero vectors + v3 = Vector(1.0, 0.0, 0.0) + assert not v3.is_zero() + + v4 = Vector(0.0, 2.0, 0.0) + assert not v4.is_zero() + + v5 = Vector(0.0, 0.0, 3.0) + assert not v5.is_zero() + + # Almost zero (within tolerance) + v6 = Vector(1e-10, 1e-10, 1e-10) + assert v6.is_zero() + + # Almost zero (outside tolerance) + v7 = Vector(1e-6, 1e-6, 1e-6) + assert not v7.is_zero() + + +def test_vector_bool_conversion(): + """Test boolean conversion of vectors.""" + # Zero vectors should be False + v0 = Vector() + assert not bool(v0) + + v1 = Vector(0.0, 0.0, 0.0) + assert not bool(v1) + + # Almost zero vectors should be False + v2 = Vector(1e-10, 1e-10, 1e-10) + assert not bool(v2) + + # Non-zero vectors should be True + v3 = Vector(1.0, 0.0, 0.0) + assert bool(v3) + + v4 = Vector(0.0, 2.0, 0.0) + assert bool(v4) + + v5 = Vector(0.0, 0.0, 3.0) + assert bool(v5) + + # Direct use in if statements + if v0: + raise AssertionError("Zero vector should be False in boolean context") + else: + pass # Expected path + + if v3: + pass # Expected path + else: + raise AssertionError("Non-zero vector should be True in boolean context") + + +def test_vector_add() -> None: + """Test vector addition operator.""" + v1 = Vector(1.0, 2.0, 3.0) + v2 = Vector(4.0, 5.0, 6.0) + + # Using __add__ method + v_add = v1.__add__(v2) + assert v_add.x == 5.0 + assert v_add.y == 7.0 + assert v_add.z == 9.0 + + # Using + operator + v_add_op = v1 + v2 + assert v_add_op.x == 5.0 + assert v_add_op.y == 7.0 + assert v_add_op.z == 9.0 + + # Adding zero vector should return original vector + v_zero = Vector.zeros(3) + assert (v1 + v_zero) == v1 + + +def test_vector_add_dim_mismatch() -> None: + """Test vector addition operator.""" + v1 = Vector(1.0, 2.0) + v2 = Vector(4.0, 5.0, 6.0) + + # Using + operator + v1 + v2 diff --git a/dimos/types/test_weaklist.py b/dimos/types/test_weaklist.py new file mode 100644 index 0000000000..a37d893de9 --- /dev/null +++ b/dimos/types/test_weaklist.py @@ -0,0 +1,165 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for WeakList implementation.""" + +import gc + +import pytest + +from dimos.types.weaklist import WeakList + + +class SampleObject: + """Simple test object.""" + + def __init__(self, value) -> None: + self.value = value + + def __repr__(self) -> str: + return f"SampleObject({self.value})" + + +def test_weaklist_basic_operations() -> None: + """Test basic append, iterate, and length operations.""" + wl = WeakList() + + # Add objects + obj1 = SampleObject(1) + obj2 = SampleObject(2) + obj3 = SampleObject(3) + + wl.append(obj1) + wl.append(obj2) + wl.append(obj3) + + # Check length and iteration + assert len(wl) == 3 + assert list(wl) == [obj1, obj2, obj3] + + # Check contains + assert obj1 in wl + assert obj2 in wl + assert SampleObject(4) not in wl + + +def test_weaklist_auto_removal() -> None: + """Test that objects are automatically removed when garbage collected.""" + wl = WeakList() + + obj1 = SampleObject(1) + obj2 = SampleObject(2) + obj3 = SampleObject(3) + + wl.append(obj1) + wl.append(obj2) + wl.append(obj3) + + assert len(wl) == 3 + + # Delete one object and force garbage collection + del obj2 + gc.collect() + + # Should only have 2 objects now + assert len(wl) == 2 + assert list(wl) == [obj1, obj3] + + +def test_weaklist_explicit_remove() -> None: + """Test explicit removal of objects.""" + wl = WeakList() + + obj1 = SampleObject(1) + obj2 = SampleObject(2) + + wl.append(obj1) + wl.append(obj2) + + # Remove obj1 + wl.remove(obj1) + assert len(wl) == 1 + assert obj1 not in wl + assert obj2 in wl + + # Try to remove non-existent object + with pytest.raises(ValueError): + wl.remove(SampleObject(3)) + + +def test_weaklist_indexing() -> None: + """Test index access.""" + wl = WeakList() + + obj1 = SampleObject(1) + obj2 = SampleObject(2) + obj3 = SampleObject(3) + + wl.append(obj1) + wl.append(obj2) + wl.append(obj3) + + assert wl[0] is obj1 + assert wl[1] is obj2 + assert wl[2] is obj3 + + # Test index out of range + with pytest.raises(IndexError): + _ = wl[3] + + +def test_weaklist_clear() -> None: + """Test clearing the list.""" + wl = WeakList() + + obj1 = SampleObject(1) + obj2 = SampleObject(2) + + wl.append(obj1) + wl.append(obj2) + + assert len(wl) == 2 + + wl.clear() + assert len(wl) == 0 + assert obj1 not in wl + + +def test_weaklist_iteration_during_modification() -> None: + """Test that iteration works even if objects are deleted during iteration.""" + wl = WeakList() + + objects = [SampleObject(i) for i in range(5)] + for obj in objects: + wl.append(obj) + + # Verify initial state + assert len(wl) == 5 + + # Iterate and check that we can safely delete objects + seen_values = [] + for obj in wl: + seen_values.append(obj.value) + if obj.value == 2: + # Delete another object (not the current one) + del objects[3] # Delete SampleObject(3) + gc.collect() + + # The object with value 3 gets garbage collected during iteration + # so we might not see it (depends on timing) + assert len(seen_values) in [4, 5] + assert all(v in [0, 1, 2, 3, 4] for v in seen_values) + + # After iteration, the list should have 4 objects (one was deleted) + assert len(wl) == 4 diff --git a/dimos/types/timestamped.py b/dimos/types/timestamped.py new file mode 100644 index 0000000000..0045c73ef4 --- /dev/null +++ b/dimos/types/timestamped.py @@ -0,0 +1,410 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from collections import defaultdict +from collections.abc import Iterable, Iterator +from datetime import datetime, timezone +from typing import Generic, TypeVar, Union + +from dimos_lcm.builtin_interfaces import Time as ROSTime +from reactivex import create +from reactivex.disposable import CompositeDisposable + +# from dimos_lcm.std_msgs import Time as ROSTime +from reactivex.observable import Observable +from sortedcontainers import SortedKeyList + +from dimos.types.weaklist import WeakList +from dimos.utils.logging_config import setup_logger + +logger = setup_logger("dimos.timestampAlignment") + +# any class that carries a timestamp should inherit from this +# this allows us to work with timeseries in consistent way, allign messages, replay etc +# aditional functionality will come to this class soon + + +# class RosStamp(TypedDict): +# sec: int +# nanosec: int + + +TimeLike = Union[int, float, datetime, ROSTime] + + +def to_timestamp(ts: TimeLike) -> float: + """Convert TimeLike to a timestamp in seconds.""" + if isinstance(ts, datetime): + return ts.timestamp() + if isinstance(ts, int | float): + return float(ts) + if isinstance(ts, dict) and "sec" in ts and "nanosec" in ts: + return ts["sec"] + ts["nanosec"] / 1e9 + # Check for ROS Time-like objects by attributes + if hasattr(ts, "sec") and (hasattr(ts, "nanosec") or hasattr(ts, "nsec")): + # Handle both std_msgs.Time (nsec) and builtin_interfaces.Time (nanosec) + if hasattr(ts, "nanosec"): + return ts.sec + ts.nanosec / 1e9 + else: # has nsec + return ts.sec + ts.nsec / 1e9 + raise TypeError("unsupported timestamp type") + + +def to_ros_stamp(ts: TimeLike) -> ROSTime: + """Convert TimeLike to a ROS-style timestamp dictionary.""" + if isinstance(ts, dict) and "sec" in ts and "nanosec" in ts: + return ts + + timestamp = to_timestamp(ts) + sec = int(timestamp) + nanosec = int((timestamp - sec) * 1_000_000_000) + return ROSTime(sec=sec, nanosec=nanosec) + + +def to_human_readable(ts: float) -> str: + """Convert timestamp to human-readable format with date and time.""" + import time + + return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ts)) + + +def to_datetime(ts: TimeLike, tz=None) -> datetime: + if isinstance(ts, datetime): + if ts.tzinfo is None: + # Assume UTC for naive datetime + ts = ts.replace(tzinfo=timezone.utc) + if tz is not None: + return ts.astimezone(tz) + return ts.astimezone() # Convert to local tz + + # Convert to timestamp first + timestamp = to_timestamp(ts) + + # Create datetime from timestamp + if tz is not None: + return datetime.fromtimestamp(timestamp, tz=tz) + else: + # Use local timezone by default + return datetime.fromtimestamp(timestamp).astimezone() + + +class Timestamped: + ts: float + + def __init__(self, ts: float) -> None: + self.ts = ts + + def dt(self) -> datetime: + return datetime.fromtimestamp(self.ts, tz=timezone.utc).astimezone() + + def ros_timestamp(self) -> list[int]: + """Convert timestamp to ROS-style list [sec, nanosec].""" + sec = int(self.ts) + nanosec = int((self.ts - sec) * 1_000_000_000) + return [sec, nanosec] + + +T = TypeVar("T", bound=Timestamped) + + +class TimestampedCollection(Generic[T]): + """A collection of timestamped objects with efficient time-based operations.""" + + def __init__(self, items: Iterable[T] | None = None) -> None: + self._items = SortedKeyList(items or [], key=lambda x: x.ts) + + def add(self, item: T) -> None: + """Add a timestamped item to the collection.""" + self._items.add(item) + + def find_closest(self, timestamp: float, tolerance: float | None = None) -> T | None: + """Find the timestamped object closest to the given timestamp.""" + if not self._items: + return None + + # Use binary search to find insertion point + idx = self._items.bisect_key_left(timestamp) + + # Check exact match + if idx < len(self._items) and self._items[idx].ts == timestamp: + return self._items[idx] + + # Find candidates: item before and after + candidates = [] + + # Item before + if idx > 0: + candidates.append((idx - 1, abs(self._items[idx - 1].ts - timestamp))) + + # Item after + if idx < len(self._items): + candidates.append((idx, abs(self._items[idx].ts - timestamp))) + + if not candidates: + return None + + # Find closest + # When distances are equal, prefer the later item (higher index) + closest_idx, closest_distance = min(candidates, key=lambda x: (x[1], -x[0])) + + # Check tolerance if provided + if tolerance is not None and closest_distance > tolerance: + return None + + return self._items[closest_idx] + + def find_before(self, timestamp: float) -> T | None: + """Find the last item before the given timestamp.""" + idx = self._items.bisect_key_left(timestamp) + return self._items[idx - 1] if idx > 0 else None + + def find_after(self, timestamp: float) -> T | None: + """Find the first item after the given timestamp.""" + idx = self._items.bisect_key_right(timestamp) + return self._items[idx] if idx < len(self._items) else None + + def merge(self, other: "TimestampedCollection[T]") -> "TimestampedCollection[T]": + """Merge two timestamped collections into a new one.""" + result = TimestampedCollection[T]() + result._items = SortedKeyList(self._items + other._items, key=lambda x: x.ts) + return result + + def duration(self) -> float: + """Get the duration of the collection in seconds.""" + if len(self._items) < 2: + return 0.0 + return self._items[-1].ts - self._items[0].ts + + def time_range(self) -> tuple[float, float] | None: + """Get the time range (start, end) of the collection.""" + if not self._items: + return None + return (self._items[0].ts, self._items[-1].ts) + + def slice_by_time(self, start: float, end: float) -> "TimestampedCollection[T]": + """Get a subset of items within the given time range.""" + start_idx = self._items.bisect_key_left(start) + end_idx = self._items.bisect_key_right(end) + return TimestampedCollection(self._items[start_idx:end_idx]) + + @property + def start_ts(self) -> float | None: + """Get the start timestamp of the collection.""" + return self._items[0].ts if self._items else None + + @property + def end_ts(self) -> float | None: + """Get the end timestamp of the collection.""" + return self._items[-1].ts if self._items else None + + def __len__(self) -> int: + return len(self._items) + + def __iter__(self) -> Iterator: + return iter(self._items) + + def __getitem__(self, idx: int) -> T: + return self._items[idx] + + +PRIMARY = TypeVar("PRIMARY", bound=Timestamped) +SECONDARY = TypeVar("SECONDARY", bound=Timestamped) + + +class TimestampedBufferCollection(TimestampedCollection[T]): + """A timestamped collection that maintains a sliding time window, dropping old messages.""" + + def __init__(self, window_duration: float, items: Iterable[T] | None = None) -> None: + """ + Initialize with a time window duration in seconds. + + Args: + window_duration: Maximum age of messages to keep in seconds + items: Optional initial items + """ + super().__init__(items) + self.window_duration = window_duration + + def add(self, item: T) -> None: + """Add a timestamped item and remove any items outside the time window.""" + super().add(item) + self._prune_old_messages(item.ts) + + def _prune_old_messages(self, current_ts: float) -> None: + """Remove messages older than window_duration from the given timestamp.""" + cutoff_ts = current_ts - self.window_duration + + # Find the index of the first item that should be kept + keep_idx = self._items.bisect_key_left(cutoff_ts) + + # Remove old items + if keep_idx > 0: + del self._items[:keep_idx] + + def remove_by_timestamp(self, timestamp: float) -> bool: + """Remove an item with the given timestamp. Returns True if item was found and removed.""" + idx = self._items.bisect_key_left(timestamp) + + if idx < len(self._items) and self._items[idx].ts == timestamp: + del self._items[idx] + return True + return False + + def remove(self, item: T) -> bool: + """Remove a timestamped item from the collection. Returns True if item was found and removed.""" + return self.remove_by_timestamp(item.ts) + + +class MatchContainer(Timestamped, Generic[PRIMARY, SECONDARY]): + """ + This class stores a primary item along with its partial matches to secondary items, + tracking which secondaries are still missing to avoid redundant searches. + """ + + def __init__(self, primary: PRIMARY, matches: list[SECONDARY | None]) -> None: + super().__init__(primary.ts) + self.primary = primary + self.matches = matches # Direct list with None for missing matches + + def message_received(self, secondary_idx: int, secondary_item: SECONDARY) -> None: + """Process a secondary message and check if it matches this primary.""" + if self.matches[secondary_idx] is None: + self.matches[secondary_idx] = secondary_item + + def is_complete(self) -> bool: + """Check if all secondary matches have been found.""" + return all(match is not None for match in self.matches) + + def get_tuple(self) -> tuple[PRIMARY, ...]: + """Get the result tuple for emission.""" + return (self.primary, *self.matches) + + +def align_timestamped( + primary_observable: Observable[PRIMARY], + *secondary_observables: Observable[SECONDARY], + buffer_size: float = 1.0, # seconds + match_tolerance: float = 0.1, # seconds +) -> Observable[tuple[PRIMARY, ...]]: + """Align a primary observable with one or more secondary observables. + + Args: + primary_observable: The primary stream to align against + *secondary_observables: One or more secondary streams to align + buffer_size: Time window to keep messages in seconds + match_tolerance: Maximum time difference for matching in seconds + + Returns: + If single secondary observable: Observable that emits tuples of (primary_item, secondary_item) + If multiple secondary observables: Observable that emits tuples of (primary_item, secondary1, secondary2, ...) + Each secondary item is the closest match from the corresponding + secondary observable, or None if no match within tolerance. + """ + + def subscribe(observer, scheduler=None): + # Create a timed buffer collection for each secondary observable + secondary_collections: list[TimestampedBufferCollection[SECONDARY]] = [ + TimestampedBufferCollection(buffer_size) for _ in secondary_observables + ] + + # WeakLists to track subscribers to each secondary observable + secondary_stakeholders = defaultdict(WeakList) + + # Buffer for unmatched MatchContainers - automatically expires old items + primary_buffer: TimestampedBufferCollection[MatchContainer[PRIMARY, SECONDARY]] = ( + TimestampedBufferCollection(buffer_size) + ) + + # Subscribe to all secondary observables + secondary_subs = [] + + def has_secondary_progressed_past(secondary_ts: float, primary_ts: float) -> bool: + """Check if secondary stream has progressed past the primary + tolerance.""" + return secondary_ts > primary_ts + match_tolerance + + def remove_stakeholder(stakeholder: MatchContainer) -> None: + """Remove a stakeholder from all tracking structures.""" + primary_buffer.remove(stakeholder) + for weak_list in secondary_stakeholders.values(): + weak_list.discard(stakeholder) + + def on_secondary(i: int, secondary_item: SECONDARY) -> None: + # Add the secondary item to its collection + secondary_collections[i].add(secondary_item) + + # Check all stakeholders for this secondary stream + for stakeholder in secondary_stakeholders[i]: + # If the secondary stream has progressed past this primary, + # we won't be able to match it anymore + if has_secondary_progressed_past(secondary_item.ts, stakeholder.ts): + logger.debug(f"secondary progressed, giving up {stakeholder.ts}") + + remove_stakeholder(stakeholder) + continue + + # Check if this secondary is within tolerance of the primary + if abs(stakeholder.ts - secondary_item.ts) <= match_tolerance: + stakeholder.message_received(i, secondary_item) + + # If all secondaries matched, emit result + if stakeholder.is_complete(): + logger.debug(f"Emitting deferred match {stakeholder.ts}") + observer.on_next(stakeholder.get_tuple()) + remove_stakeholder(stakeholder) + + for i, secondary_obs in enumerate(secondary_observables): + secondary_subs.append( + secondary_obs.subscribe( + lambda x, idx=i: on_secondary(idx, x), on_error=observer.on_error + ) + ) + + def on_primary(primary_item: PRIMARY) -> None: + # Try to find matches in existing secondary collections + matches = [None] * len(secondary_observables) + + for i, collection in enumerate(secondary_collections): + closest = collection.find_closest(primary_item.ts, tolerance=match_tolerance) + if closest is not None: + matches[i] = closest + else: + # Check if this secondary stream has already progressed past this primary + if collection.end_ts is not None and has_secondary_progressed_past( + collection.end_ts, primary_item.ts + ): + # This secondary won't match, so don't buffer this primary + return + + # If all matched, emit immediately without creating MatchContainer + if all(match is not None for match in matches): + logger.debug(f"Immadiate match {primary_item.ts}") + result = (primary_item, *matches) + observer.on_next(result) + else: + logger.debug(f"Deferred match attempt {primary_item.ts}") + match_container = MatchContainer(primary_item, matches) + primary_buffer.add(match_container) + + for i, match in enumerate(matches): + if match is None: + secondary_stakeholders[i].append(match_container) + + # Subscribe to primary observable + primary_sub = primary_observable.subscribe( + on_primary, on_error=observer.on_error, on_completed=observer.on_completed + ) + + # Return a CompositeDisposable for proper cleanup + return CompositeDisposable(primary_sub, *secondary_subs) + + return create(subscribe) diff --git a/dimos/types/vector.py b/dimos/types/vector.py index e8bb5b8308..161084fc2c 100644 --- a/dimos/types/vector.py +++ b/dimos/types/vector.py @@ -12,23 +12,24 @@ # See the License for the specific language governing permissions and # limitations under the License. +import builtins +from collections.abc import Sequence +from typing import TypeVar, Union + import numpy as np -from geometry_msgs.msg import Vector3 -from typing import ( - Tuple, - List, - TypeVar, - Protocol, - runtime_checkable, -) + +from dimos.types.ros_polyfill import Vector3 T = TypeVar("T", bound="Vector") +# Vector-like types that can be converted to/from Vector +VectorLike = Union[Sequence[int | float], Vector3, "Vector", np.ndarray] + class Vector: """A wrapper around numpy arrays for vector operations with intuitive syntax.""" - def __init__(self, *args): + def __init__(self, *args: VectorLike) -> None: """Initialize a vector from components or another iterable. Examples: @@ -39,6 +40,7 @@ def __init__(self, *args): """ if len(args) == 1 and hasattr(args[0], "__iter__"): self._data = np.array(args[0], dtype=float) + elif len(args) == 1: self._data = np.array([args[0].x, args[0].y, args[0].z], dtype=float) @@ -50,7 +52,7 @@ def yaw(self) -> float: return self.x @property - def tuple(self) -> Tuple[float, ...]: + def tuple(self) -> tuple[float, ...]: """Tuple representation of the vector.""" return tuple(self._data) @@ -79,18 +81,11 @@ def data(self) -> np.ndarray: """Get the underlying numpy array.""" return self._data - def __len__(self) -> int: - return len(self._data) - - def __getitem__(self, idx): + def __getitem__(self, idx: int): return self._data[idx] - def __iter__(self): - return iter(self._data) - def __repr__(self) -> str: - components = ",".join(f"{x:.6g}" for x in self._data) - return f"({components})" + return f"Vector({self.data})" def __str__(self) -> str: if self.dim < 2: @@ -99,7 +94,7 @@ def __str__(self) -> str: def getArrow(): repr = ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"] - if self.y == 0 and self.x == 0: + if self.x == 0 and self.y == 0: return "·" # Calculate angle in radians and convert to directional index @@ -111,24 +106,31 @@ def getArrow(): return f"{getArrow()} Vector {self.__repr__()}" - def serialize(self) -> Tuple: + def serialize(self) -> builtins.tuple: """Serialize the vector to a tuple.""" return {"type": "vector", "c": self._data.tolist()} def __eq__(self, other) -> bool: - if isinstance(other, Vector): - return np.array_equal(self._data, other._data) - return np.array_equal(self._data, np.array(other, dtype=float)) - - def __add__(self: T, other) -> T: - if isinstance(other, Vector): - return self.__class__(self._data + other._data) - return self.__class__(self._data + np.array(other, dtype=float)) - - def __sub__(self: T, other) -> T: - if isinstance(other, Vector): - return self.__class__(self._data - other._data) - return self.__class__(self._data - np.array(other, dtype=float)) + """Check if two vectors are equal using numpy's allclose for floating point comparison.""" + if not isinstance(other, Vector): + return False + if len(self._data) != len(other._data): + return False + return np.allclose(self._data, other._data) + + def __add__(self: T, other: VectorLike) -> T: + other = to_vector(other) + if self.dim != other.dim: + max_dim = max(self.dim, other.dim) + return self.pad(max_dim) + other.pad(max_dim) + return self.__class__(self._data + other._data) + + def __sub__(self: T, other: VectorLike) -> T: + other = to_vector(other) + if self.dim != other.dim: + max_dim = max(self.dim, other.dim) + return self.pad(max_dim) - other.pad(max_dim) + return self.__class__(self._data - other._data) def __mul__(self: T, scalar: float) -> T: return self.__class__(self._data * scalar) @@ -142,26 +144,21 @@ def __truediv__(self: T, scalar: float) -> T: def __neg__(self: T) -> T: return self.__class__(-self._data) - def dot(self, other) -> float: + def dot(self, other: VectorLike) -> float: """Compute dot product.""" - if isinstance(other, Vector): - return float(np.dot(self._data, other._data)) - return float(np.dot(self._data, np.array(other, dtype=float))) + other = to_vector(other) + return float(np.dot(self._data, other._data)) - def cross(self: T, other) -> T: + def cross(self: T, other: VectorLike) -> T: """Compute cross product (3D vectors only).""" if self.dim != 3: raise ValueError("Cross product is only defined for 3D vectors") - if isinstance(other, Vector): - other_data = other._data - else: - other_data = np.array(other, dtype=float) - - if len(other_data) != 3: + other = to_vector(other) + if other.dim != 3: raise ValueError("Cross product requires two 3D vectors") - return self.__class__(np.cross(self._data, other_data)) + return self.__class__(np.cross(self._data, other._data)) def length(self) -> float: """Compute the Euclidean length (magnitude) of the vector.""" @@ -182,50 +179,52 @@ def to_2d(self: T) -> T: """Convert a vector to a 2D vector by taking only the x and y components.""" return self.__class__(self._data[:2]) - def distance(self, other) -> float: + def pad(self: T, dim: int) -> T: + """Pad a vector with zeros to reach the specified dimension. + + If vector already has dimension >= dim, it is returned unchanged. + """ + if self.dim >= dim: + return self + + padded = np.zeros(dim, dtype=float) + padded[: len(self._data)] = self._data + return self.__class__(padded) + + def distance(self, other: VectorLike) -> float: """Compute Euclidean distance to another vector.""" - if isinstance(other, Vector): - return float(np.linalg.norm(self._data - other._data)) - return float(np.linalg.norm(self._data - np.array(other, dtype=float))) + other = to_vector(other) + return float(np.linalg.norm(self._data - other._data)) - def distance_squared(self, other) -> float: + def distance_squared(self, other: VectorLike) -> float: """Compute squared Euclidean distance to another vector (faster than distance()).""" - if isinstance(other, Vector): - diff = self._data - other._data - else: - diff = self._data - np.array(other, dtype=float) + other = to_vector(other) + diff = self._data - other._data return float(np.sum(diff * diff)) - def angle(self, other) -> float: + def angle(self, other: VectorLike) -> float: """Compute the angle (in radians) between this vector and another.""" - if self.length() < 1e-10 or (isinstance(other, Vector) and other.length() < 1e-10): + other = to_vector(other) + if self.length() < 1e-10 or other.length() < 1e-10: return 0.0 - if isinstance(other, Vector): - other_data = other._data - else: - other_data = np.array(other, dtype=float) - cos_angle = np.clip( - np.dot(self._data, other_data) / (np.linalg.norm(self._data) * np.linalg.norm(other_data)), + np.dot(self._data, other._data) + / (np.linalg.norm(self._data) * np.linalg.norm(other._data)), -1.0, 1.0, ) return float(np.arccos(cos_angle)) - def project(self: T, onto) -> T: + def project(self: T, onto: VectorLike) -> T: """Project this vector onto another vector.""" - if isinstance(onto, Vector): - onto_data = onto._data - else: - onto_data = np.array(onto, dtype=float) - - onto_length_sq = np.sum(onto_data * onto_data) + onto = to_vector(onto) + onto_length_sq = np.sum(onto._data * onto._data) if onto_length_sq < 1e-10: return self.__class__(np.zeros_like(self._data)) - scalar_projection = np.dot(self._data, onto_data) / onto_length_sq - return self.__class__(scalar_projection * onto_data) + scalar_projection = np.dot(self._data, onto._data) / onto_length_sq + return self.__class__(scalar_projection * onto._data) # this is here to test ros_observable_topic # doesn't happen irl afaik that we want a vector from ros message @@ -265,11 +264,11 @@ def unit_z(cls: type[T], dim: int = 3) -> T: v[2] = 1.0 return cls(v) - def to_list(self) -> List[float]: + def to_list(self) -> list[float]: """Convert the vector to a list.""" return self._data.tolist() - def to_tuple(self) -> Tuple[float, ...]: + def to_tuple(self) -> builtins.tuple[float, ...]: """Convert the vector to a tuple.""" return tuple(self._data) @@ -277,14 +276,24 @@ def to_numpy(self) -> np.ndarray: """Convert the vector to a numpy array.""" return self._data + def is_zero(self) -> bool: + """Check if this is a zero vector (all components are zero). -# Protocol approach for static type checking -@runtime_checkable -class VectorLike(Protocol): - """Protocol for types that can be treated as vectors.""" + Returns: + True if all components are zero, False otherwise + """ + return np.allclose(self._data, 0.0) + + def __bool__(self) -> bool: + """Boolean conversion for Vector. + + A Vector is considered False if it's a zero vector (all components are zero), + and True otherwise. - def __getitem__(self, key: int) -> float: ... - def __len__(self) -> int: ... + Returns: + False if vector is zero, True otherwise + """ + return not self.is_zero() def to_numpy(value: VectorLike) -> np.ndarray: @@ -321,7 +330,7 @@ def to_vector(value: VectorLike) -> Vector: return Vector(value) -def to_tuple(value: VectorLike) -> Tuple[float, ...]: +def to_tuple(value: VectorLike) -> tuple[float, ...]: """Convert a vector-compatible value to a tuple. Args: @@ -342,7 +351,7 @@ def to_tuple(value: VectorLike) -> Tuple[float, ...]: return tuple(value) -def to_list(value: VectorLike) -> List[float]: +def to_list(value: VectorLike) -> list[float]: """Convert a vector-compatible value to a list. Args: @@ -452,143 +461,3 @@ def z(value: VectorLike) -> float: else: arr = to_numpy(value) return float(arr[2]) if len(arr) > 2 else 0.0 - - -if __name__ == "__main__": - # Test vectors in various directions - test_vectors = [ - Vector(1, 0), # Right - Vector(1, 1), # Up-Right - Vector(0, 1), # Up - Vector(-1, 1), # Up-Left - Vector(-1, 0), # Left - Vector(-1, -1), # Down-Left - Vector(0, -1), # Down - Vector(1, -1), # Down-Right - Vector(0.5, 0.5), # Up-Right (shorter) - Vector(-3, 4), # Up-Left (longer) - Vector(Vector3(x=2.0, y=3.0, z=4.0)), - ] - - for v in test_vectors: - print(str(v)) - - # Test the vector compatibility functions - print("Testing vectortypes.py conversion functions\n") - - # Create test vectors in different formats - vector_obj = Vector(1.0, 2.0, 3.0) - numpy_arr = np.array([4.0, 5.0, 6.0]) - tuple_vec = (7.0, 8.0, 9.0) - list_vec = [10.0, 11.0, 12.0] - - print("Original values:") - print(f"Vector: {vector_obj}") - print(f"NumPy: {numpy_arr}") - print(f"Tuple: {tuple_vec}") - print(f"List: {list_vec}") - print() - - # Test to_numpy - print("to_numpy() conversions:") - print(f"Vector → NumPy: {to_numpy(vector_obj)}") - print(f"NumPy → NumPy: {to_numpy(numpy_arr)}") - print(f"Tuple → NumPy: {to_numpy(tuple_vec)}") - print(f"List → NumPy: {to_numpy(list_vec)}") - print() - - # Test to_vector - print("to_vector() conversions:") - print(f"Vector → Vector: {to_vector(vector_obj)}") - print(f"NumPy → Vector: {to_vector(numpy_arr)}") - print(f"Tuple → Vector: {to_vector(tuple_vec)}") - print(f"List → Vector: {to_vector(list_vec)}") - print() - - # Test to_tuple - print("to_tuple() conversions:") - print(f"Vector → Tuple: {to_tuple(vector_obj)}") - print(f"NumPy → Tuple: {to_tuple(numpy_arr)}") - print(f"Tuple → Tuple: {to_tuple(tuple_vec)}") - print(f"List → Tuple: {to_tuple(list_vec)}") - print() - - # Test to_list - print("to_list() conversions:") - print(f"Vector → List: {to_list(vector_obj)}") - print(f"NumPy → List: {to_list(numpy_arr)}") - print(f"Tuple → List: {to_list(tuple_vec)}") - print(f"List → List: {to_list(list_vec)}") - print() - - # Test component extraction - print("Component extraction:") - print("x() function:") - print(f"x(Vector): {x(vector_obj)}") - print(f"x(NumPy): {x(numpy_arr)}") - print(f"x(Tuple): {x(tuple_vec)}") - print(f"x(List): {x(list_vec)}") - print() - - print("y() function:") - print(f"y(Vector): {y(vector_obj)}") - print(f"y(NumPy): {y(numpy_arr)}") - print(f"y(Tuple): {y(tuple_vec)}") - print(f"y(List): {y(list_vec)}") - print() - - print("z() function:") - print(f"z(Vector): {z(vector_obj)}") - print(f"z(NumPy): {z(numpy_arr)}") - print(f"z(Tuple): {z(tuple_vec)}") - print(f"z(List): {z(list_vec)}") - print() - - # Test dimension checking - print("Dimension checking:") - vec2d = Vector(1.0, 2.0) - vec3d = Vector(1.0, 2.0, 3.0) - arr2d = np.array([1.0, 2.0]) - arr3d = np.array([1.0, 2.0, 3.0]) - - print(f"is_2d(Vector(1,2)): {is_2d(vec2d)}") - print(f"is_2d(Vector(1,2,3)): {is_2d(vec3d)}") - print(f"is_2d(np.array([1,2])): {is_2d(arr2d)}") - print(f"is_2d(np.array([1,2,3])): {is_2d(arr3d)}") - print(f"is_2d((1,2)): {is_2d((1.0, 2.0))}") - print(f"is_2d((1,2,3)): {is_2d((1.0, 2.0, 3.0))}") - print() - - print(f"is_3d(Vector(1,2)): {is_3d(vec2d)}") - print(f"is_3d(Vector(1,2,3)): {is_3d(vec3d)}") - print(f"is_3d(np.array([1,2])): {is_3d(arr2d)}") - print(f"is_3d(np.array([1,2,3])): {is_3d(arr3d)}") - print(f"is_3d((1,2)): {is_3d((1.0, 2.0))}") - print(f"is_3d((1,2,3)): {is_3d((1.0, 2.0, 3.0))}") - print() - - # Test the Protocol interface - print("Testing VectorLike Protocol:") - print(f"isinstance(Vector(1,2), VectorLike): {isinstance(vec2d, VectorLike)}") - print(f"isinstance(np.array([1,2]), VectorLike): {isinstance(arr2d, VectorLike)}") - print(f"isinstance((1,2), VectorLike): {isinstance((1.0, 2.0), VectorLike)}") - print(f"isinstance([1,2], VectorLike): {isinstance([1.0, 2.0], VectorLike)}") - print() - - # Test mixed operations using different vector types - # These functions aren't defined in vectortypes, but demonstrate the concept - def distance(a, b): - a_np = to_numpy(a) - b_np = to_numpy(b) - diff = a_np - b_np - return np.sqrt(np.sum(diff * diff)) - - def midpoint(a, b): - a_np = to_numpy(a) - b_np = to_numpy(b) - return (a_np + b_np) / 2 - - print("Mixed operations between different vector types:") - print(f"distance(Vector(1,2,3), [4,5,6]): {distance(vec3d, [4.0, 5.0, 6.0])}") - print(f"distance(np.array([1,2,3]), (4,5,6)): {distance(arr3d, (4.0, 5.0, 6.0))}") - print(f"midpoint(Vector(1,2,3), np.array([4,5,6])): {midpoint(vec3d, numpy_arr)}") diff --git a/dimos/types/weaklist.py b/dimos/types/weaklist.py new file mode 100644 index 0000000000..e09b36157c --- /dev/null +++ b/dimos/types/weaklist.py @@ -0,0 +1,86 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Weak reference list implementation that automatically removes dead references.""" + +from collections.abc import Iterator +from typing import Any +import weakref + + +class WeakList: + """A list that holds weak references to objects. + + Objects are automatically removed when garbage collected. + Supports iteration, append, remove, and length operations. + """ + + def __init__(self) -> None: + self._refs = [] + + def append(self, obj: Any) -> None: + """Add an object to the list (stored as weak reference).""" + + def _cleanup(ref) -> None: + try: + self._refs.remove(ref) + except ValueError: + pass + + self._refs.append(weakref.ref(obj, _cleanup)) + + def remove(self, obj: Any) -> None: + """Remove an object from the list.""" + for i, ref in enumerate(self._refs): + if ref() is obj: + del self._refs[i] + return + raise ValueError(f"{obj} not in WeakList") + + def discard(self, obj: Any) -> None: + """Remove an object from the list if present, otherwise do nothing.""" + try: + self.remove(obj) + except ValueError: + pass + + def __iter__(self) -> Iterator[Any]: + """Iterate over live objects, skipping dead references.""" + # Create a copy to avoid modification during iteration + for ref in self._refs[:]: + obj = ref() + if obj is not None: + yield obj + + def __len__(self) -> int: + """Return count of live objects.""" + return sum(1 for _ in self) + + def __contains__(self, obj: Any) -> bool: + """Check if object is in the list.""" + return any(ref() is obj for ref in self._refs) + + def clear(self) -> None: + """Remove all references.""" + self._refs.clear() + + def __getitem__(self, index: int) -> Any: + """Get object at index (only counting live objects).""" + for i, obj in enumerate(self): + if i == index: + return obj + raise IndexError("WeakList index out of range") + + def __repr__(self) -> str: + return f"WeakList({list(self)})" diff --git a/dimos/utils/actor_registry.py b/dimos/utils/actor_registry.py new file mode 100644 index 0000000000..9cd589bed2 --- /dev/null +++ b/dimos/utils/actor_registry.py @@ -0,0 +1,84 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Shared memory registry for tracking actor deployments across processes.""" + +import json +from multiprocessing import shared_memory + + +class ActorRegistry: + """Shared memory registry of actor deployments.""" + + SHM_NAME = "dimos_actor_registry" + SHM_SIZE = 65536 # 64KB should be enough for most deployments + + @staticmethod + def update(actor_name: str, worker_id: str) -> None: + """Update registry with new actor deployment.""" + try: + shm = shared_memory.SharedMemory(name=ActorRegistry.SHM_NAME) + except FileNotFoundError: + shm = shared_memory.SharedMemory( + name=ActorRegistry.SHM_NAME, create=True, size=ActorRegistry.SHM_SIZE + ) + + # Read existing data + data = ActorRegistry._read_from_shm(shm) + + # Update with new actor + data[actor_name] = worker_id + + # Write back + ActorRegistry._write_to_shm(shm, data) + shm.close() + + @staticmethod + def get_all() -> dict[str, str]: + """Get all actor->worker mappings.""" + try: + shm = shared_memory.SharedMemory(name=ActorRegistry.SHM_NAME) + data = ActorRegistry._read_from_shm(shm) + shm.close() + return data + except FileNotFoundError: + return {} + + @staticmethod + def clear() -> None: + """Clear the registry and free shared memory.""" + try: + shm = shared_memory.SharedMemory(name=ActorRegistry.SHM_NAME) + ActorRegistry._write_to_shm(shm, {}) + shm.close() + shm.unlink() + except FileNotFoundError: + pass + + @staticmethod + def _read_from_shm(shm) -> dict[str, str]: + """Read JSON data from shared memory.""" + raw = bytes(shm.buf[:]).rstrip(b"\x00") + if not raw: + return {} + return json.loads(raw.decode("utf-8")) + + @staticmethod + def _write_to_shm(shm, data: dict[str, str]): + """Write JSON data to shared memory.""" + json_bytes = json.dumps(data).encode("utf-8") + if len(json_bytes) > ActorRegistry.SHM_SIZE: + raise ValueError("Registry data too large for shared memory") + shm.buf[: len(json_bytes)] = json_bytes + shm.buf[len(json_bytes) :] = b"\x00" * (ActorRegistry.SHM_SIZE - len(json_bytes)) diff --git a/dimos/utils/cli/__init__.py b/dimos/utils/cli/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/dimos/utils/cli/agentspy/agentspy.py b/dimos/utils/cli/agentspy/agentspy.py new file mode 100644 index 0000000000..84f68c10af --- /dev/null +++ b/dimos/utils/cli/agentspy/agentspy.py @@ -0,0 +1,238 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from collections import deque +from dataclasses import dataclass +import time +from typing import Any, Union + +from langchain_core.messages import ( + AIMessage, + HumanMessage, + SystemMessage, + ToolMessage, +) +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual.widgets import Footer, RichLog + +from dimos.protocol.pubsub.lcmpubsub import PickleLCM +from dimos.utils.cli import theme + +# Type alias for all message types we might receive +AnyMessage = Union[SystemMessage, ToolMessage, AIMessage, HumanMessage] + + +@dataclass +class MessageEntry: + """Store a single message with metadata.""" + + timestamp: float + message: AnyMessage + + def __post_init__(self) -> None: + """Initialize timestamp if not provided.""" + if self.timestamp is None: + self.timestamp = time.time() + + +class AgentMessageMonitor: + """Monitor agent messages published via LCM.""" + + def __init__(self, topic: str = "/agent", max_messages: int = 1000) -> None: + self.topic = topic + self.max_messages = max_messages + self.messages: deque[MessageEntry] = deque(maxlen=max_messages) + self.transport = PickleLCM() + self.transport.start() + self.callbacks: list[callable] = [] + pass + + def start(self) -> None: + """Start monitoring messages.""" + self.transport.subscribe(self.topic, self._handle_message) + + def stop(self) -> None: + """Stop monitoring.""" + # PickleLCM doesn't have explicit stop method + pass + + def _handle_message(self, msg: Any, topic: str) -> None: + """Handle incoming messages.""" + # Check if it's one of the message types we care about + if isinstance(msg, SystemMessage | ToolMessage | AIMessage | HumanMessage): + entry = MessageEntry(timestamp=time.time(), message=msg) + self.messages.append(entry) + + # Notify callbacks + for callback in self.callbacks: + callback(entry) + else: + pass + + def subscribe(self, callback: callable) -> None: + """Subscribe to new messages.""" + self.callbacks.append(callback) + + def get_messages(self) -> list[MessageEntry]: + """Get all stored messages.""" + return list(self.messages) + + +def format_timestamp(timestamp: float) -> str: + """Format timestamp as HH:MM:SS.mmm.""" + return ( + time.strftime("%H:%M:%S", time.localtime(timestamp)) + f".{int((timestamp % 1) * 1000):03d}" + ) + + +def get_message_type_and_style(msg: AnyMessage) -> tuple[str, str]: + """Get message type name and style color.""" + if isinstance(msg, HumanMessage): + return "Human ", "green" + elif isinstance(msg, AIMessage): + if hasattr(msg, "metadata") and msg.metadata.get("state"): + return "State ", "blue" + return "Agent ", "yellow" + elif isinstance(msg, ToolMessage): + return "Tool ", "red" + elif isinstance(msg, SystemMessage): + return "System", "red" + else: + return "Unkn ", "white" + + +def format_message_content(msg: AnyMessage) -> str: + """Format message content for display.""" + if isinstance(msg, ToolMessage): + return f"{msg.name}() -> {msg.content}" + elif isinstance(msg, AIMessage) and msg.tool_calls: + # Include tool calls in content + tool_info = [] + for tc in msg.tool_calls: + args_str = str(tc.get("args", {})) + tool_info.append(f"{tc.get('name')}({args_str})") + content = msg.content or "" + if content and tool_info: + return f"{content}\n[Tool Calls: {', '.join(tool_info)}]" + elif tool_info: + return f"[Tool Calls: {', '.join(tool_info)}]" + return content + else: + return str(msg.content) if hasattr(msg, "content") else str(msg) + + +class AgentSpyApp(App): + """TUI application for monitoring agent messages.""" + + CSS_PATH = theme.CSS_PATH + + CSS = f""" + Screen {{ + layout: vertical; + background: {theme.BACKGROUND}; + }} + + RichLog {{ + height: 1fr; + border: none; + background: {theme.BACKGROUND}; + padding: 0 1; + }} + + Footer {{ + dock: bottom; + height: 1; + }} + """ + + BINDINGS = [ + Binding("q", "quit", "Quit"), + Binding("c", "clear", "Clear"), + Binding("ctrl+c", "quit", show=False), + ] + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.monitor = AgentMessageMonitor() + self.message_log: RichLog | None = None + + def compose(self) -> ComposeResult: + """Compose the UI.""" + self.message_log = RichLog(wrap=True, highlight=True, markup=True) + yield self.message_log + yield Footer() + + def on_mount(self) -> None: + """Start monitoring when app mounts.""" + self.theme = "flexoki" + + # Subscribe to new messages + self.monitor.subscribe(self.on_new_message) + self.monitor.start() + + # Write existing messages to the log + for entry in self.monitor.get_messages(): + self.on_new_message(entry) + + def on_unmount(self) -> None: + """Stop monitoring when app unmounts.""" + self.monitor.stop() + + def on_new_message(self, entry: MessageEntry) -> None: + """Handle new messages.""" + if self.message_log: + msg = entry.message + msg_type, style = get_message_type_and_style(msg) + content = format_message_content(msg) + + # Format the message for the log + timestamp = format_timestamp(entry.timestamp) + self.message_log.write( + f"[dim white]{timestamp}[/dim white] | " + f"[bold {style}]{msg_type}[/bold {style}] | " + f"[{style}]{content}[/{style}]" + ) + + def refresh_display(self) -> None: + """Refresh the message display.""" + # Not needed anymore as messages are written directly to the log + + def action_clear(self) -> None: + """Clear message history.""" + self.monitor.messages.clear() + if self.message_log: + self.message_log.clear() + + +def main() -> None: + """Main entry point for agentspy.""" + import sys + + if len(sys.argv) > 1 and sys.argv[1] == "web": + import os + + from textual_serve.server import Server + + server = Server(f"python {os.path.abspath(__file__)}") + server.serve() + else: + app = AgentSpyApp() + app.run() + + +if __name__ == "__main__": + main() diff --git a/dimos/utils/cli/agentspy/demo_agentspy.py b/dimos/utils/cli/agentspy/demo_agentspy.py new file mode 100755 index 0000000000..100f22522d --- /dev/null +++ b/dimos/utils/cli/agentspy/demo_agentspy.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Demo script to test agent message publishing and agentspy reception.""" + +import time + +from langchain_core.messages import ( + AIMessage, + HumanMessage, + SystemMessage, + ToolMessage, +) + +from dimos.protocol.pubsub import lcm +from dimos.protocol.pubsub.lcmpubsub import PickleLCM + + +def test_publish_messages() -> None: + """Publish test messages to verify agentspy is working.""" + print("Starting agent message publisher demo...") + + # Create transport + transport = PickleLCM() + topic = lcm.Topic("/agent") + + print(f"Publishing to topic: {topic}") + + # Test messages + messages = [ + SystemMessage("System initialized for testing"), + HumanMessage("Hello agent, can you help me?"), + AIMessage( + "Of course! I'm here to help.", + tool_calls=[{"name": "get_info", "args": {"query": "test"}, "id": "1"}], + ), + ToolMessage(name="get_info", content="Test result: success", tool_call_id="1"), + AIMessage("The test was successful!", metadata={"state": True}), + ] + + # Publish messages with delays + for i, msg in enumerate(messages): + print(f"\nPublishing message {i + 1}: {type(msg).__name__}") + print(f"Content: {msg.content if hasattr(msg, 'content') else msg}") + + transport.publish(topic, msg) + time.sleep(1) # Wait 1 second between messages + + print("\nAll messages published! Check agentspy to see if they were received.") + print("Keeping publisher alive for 10 more seconds...") + time.sleep(10) + + +if __name__ == "__main__": + test_publish_messages() diff --git a/dimos/utils/cli/boxglove/boxglove.py b/dimos/utils/cli/boxglove/boxglove.py new file mode 100644 index 0000000000..1e0e09a277 --- /dev/null +++ b/dimos/utils/cli/boxglove/boxglove.py @@ -0,0 +1,292 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +import reactivex.operators as ops +from rich.text import Text +from textual.app import App, ComposeResult +from textual.containers import Container +from textual.reactive import reactive +from textual.widgets import Footer, Static + +from dimos import core +from dimos.msgs.nav_msgs import OccupancyGrid +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage + +if TYPE_CHECKING: + from reactivex.disposable import Disposable + + from dimos.msgs.nav_msgs import OccupancyGrid + from dimos.utils.cli.boxglove.connection import Connection + + +blocks = "█▗▖▝▘" +shades = "█░░░░" +crosses = "┼┌┐└┘" +quadrant = "█▟▙▜▛" +triangles = "◼◢◣◥◤" # 45-degree triangular blocks + + +alphabet = crosses + +# Box drawing characters for smooth edges +top_left = alphabet[1] # Quadrant lower right +top_right = alphabet[2] # Quadrant lower left +bottom_left = alphabet[3] # Quadrant upper right +bottom_right = alphabet[4] # Quadrant upper left +full = alphabet[0] # Full block + + +class OccupancyGridApp(App): + """A Textual app for visualizing OccupancyGrid data in real-time.""" + + CSS = """ + Screen { + layout: vertical; + overflow: hidden; + } + + #grid-container { + width: 100%; + height: 1fr; + overflow: hidden; + margin: 0; + padding: 0; + } + + #grid-display { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + } + + Footer { + dock: bottom; + height: 1; + } + """ + + # Reactive properties + grid_data: reactive[OccupancyGrid | None] = reactive(None) + + BINDINGS = [ + ("q", "quit", "Quit"), + ("ctrl+c", "quit", "Quit"), + ] + + def __init__(self, connection: Connection, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.connection = connection + self.subscription: Disposable | None = None + self.grid_display: Static | None = None + self.cached_grid: OccupancyGrid | None = None + + def compose(self) -> ComposeResult: + """Create the app layout.""" + # Container for the grid (no scrolling since we scale to fit) + with Container(id="grid-container"): + self.grid_display = Static("", id="grid-display") + yield self.grid_display + + yield Footer() + + def on_mount(self) -> None: + """Subscribe to the connection when the app starts.""" + self.theme = "flexoki" + + # Subscribe to the OccupancyGrid stream + def on_grid(grid: OccupancyGrid) -> None: + self.grid_data = grid + + def on_error(error: Exception) -> None: + self.notify(f"Error: {error}", severity="error") + + self.subscription = self.connection().subscribe(on_next=on_grid, on_error=on_error) + + async def on_unmount(self) -> None: + """Clean up subscription when app closes.""" + if self.subscription: + self.subscription.dispose() + + def watch_grid_data(self, grid: OccupancyGrid | None) -> None: + """Update display when new grid data arrives.""" + if grid is None: + return + + # Cache the grid for rerendering on terminal resize + self.cached_grid = grid + + # Render the grid as ASCII art + grid_text = self.render_grid(grid) + self.grid_display.update(grid_text) + + def on_resize(self, event) -> None: + """Handle terminal resize events.""" + if self.cached_grid: + # Re-render with new terminal dimensions + grid_text = self.render_grid(self.cached_grid) + self.grid_display.update(grid_text) + + def render_grid(self, grid: OccupancyGrid) -> Text: + """Render the OccupancyGrid as colored ASCII art, scaled to fit terminal.""" + text = Text() + + # Get the actual container dimensions + container = self.query_one("#grid-container") + content_width = container.content_size.width + content_height = container.content_size.height + + # Each cell will be 2 chars wide to make square pixels + terminal_width = max(1, content_width // 2) + terminal_height = max(1, content_height) + + # Handle edge cases + if grid.width == 0 or grid.height == 0: + return text # Return empty text for empty grid + + # Calculate scaling factors (as floats for smoother scaling) + scale_x = grid.width / terminal_width + scale_y = grid.height / terminal_height + + # Use the larger scale to ensure the grid fits + scale_float = max(1.0, max(scale_x, scale_y)) + + # For smoother resizing, we'll use fractional scaling + # This means we might sample between grid cells + render_width = min(int(grid.width / scale_float), terminal_width) + render_height = min(int(grid.height / scale_float), terminal_height) + + # Store both integer and float scale for different uses + int(np.ceil(scale_float)) # For legacy compatibility + + # Adjust render dimensions to use all available space + # This reduces jumping by allowing fractional cell sizes + actual_scale_x = grid.width / render_width if render_width > 0 else 1 + actual_scale_y = grid.height / render_height if render_height > 0 else 1 + + # Function to get value with fractional scaling + def get_cell_value(grid_data: np.ndarray, x: int, y: int) -> int: + # Use fractional coordinates for smoother scaling + y_center = int((y + 0.5) * actual_scale_y) + x_center = int((x + 0.5) * actual_scale_x) + + # Clamp to grid bounds + y_center = max(0, min(y_center, grid.height - 1)) + x_center = max(0, min(x_center, grid.width - 1)) + + # For now, just sample the center point + # Could do area averaging for smoother results + return grid_data[y_center, x_center] + + # Helper function to check if a cell is an obstacle + def is_obstacle(grid_data: np.ndarray, x: int, y: int) -> bool: + if x < 0 or x >= render_width or y < 0 or y >= render_height: + return False + value = get_cell_value(grid_data, x, y) + return value > 90 # Consider cells with >90% probability as obstacles + + # Character and color mapping with intelligent obstacle rendering + def get_cell_char_and_style(grid_data: np.ndarray, x: int, y: int) -> tuple[str, str]: + value = get_cell_value(grid_data, x, y) + norm_value = min(value, 100) / 100.0 + + if norm_value > 0.9: + # Check neighbors for intelligent character selection + top = is_obstacle(grid_data, x, y + 1) + bottom = is_obstacle(grid_data, x, y - 1) + left = is_obstacle(grid_data, x - 1, y) + right = is_obstacle(grid_data, x + 1, y) + + # Count neighbors + neighbor_count = sum([top, bottom, left, right]) + + # Select character based on neighbor configuration + if neighbor_count == 4: + # All neighbors are obstacles - use full block + symbol = full + full + elif neighbor_count == 3: + # Three neighbors - use full block (interior edge) + symbol = full + full + elif neighbor_count == 2: + # Two neighbors - check configuration + if top and bottom: + symbol = full + full # Vertical corridor + elif left and right: + symbol = full + full # Horizontal corridor + elif top and left: + symbol = bottom_right + " " + elif top and right: + symbol = " " + bottom_left + elif bottom and left: + symbol = top_right + " " + elif bottom and right: + symbol = " " + top_left + else: + symbol = full + full + elif neighbor_count == 1: + # One neighbor - point towards it + if top: + symbol = bottom_left + bottom_right + elif bottom: + symbol = top_left + top_right + elif left: + symbol = top_right + bottom_right + elif right: + symbol = top_left + bottom_left + else: + symbol = full + full + else: + # No neighbors - isolated obstacle + symbol = full + full + + return symbol, None + else: + return " ", None + + # Render the scaled grid row by row (flip Y axis for proper display) + for y in range(render_height - 1, -1, -1): + for x in range(render_width): + char, style = get_cell_char_and_style(grid.grid, x, y) + text.append(char, style=style) + if y > 0: # Add newline except for last row + text.append("\n") + + # Could show scale info in footer status if needed + + return text + + +def main() -> None: + """Run the OccupancyGrid visualizer with a connection.""" + # app = OccupancyGridApp(core.LCMTransport("/global_costmap", OccupancyGrid).observable) + + app = OccupancyGridApp( + lambda: core.LCMTransport("/lidar", LidarMessage) + .observable() + .pipe(ops.map(lambda msg: msg.costmap())) + ) + app.run() + import time + + while True: + time.sleep(1) + + +if __name__ == "__main__": + main() diff --git a/dimos/utils/cli/boxglove/connection.py b/dimos/utils/cli/boxglove/connection.py new file mode 100644 index 0000000000..5d3b3f8806 --- /dev/null +++ b/dimos/utils/cli/boxglove/connection.py @@ -0,0 +1,71 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Callable +import pickle + +import reactivex as rx +from reactivex import operators as ops +from reactivex.disposable import Disposable +from reactivex.observable import Observable + +from dimos.msgs.nav_msgs import OccupancyGrid +from dimos.msgs.sensor_msgs import PointCloud2 +from dimos.protocol.pubsub import lcm +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.robot.unitree_webrtc.type.map import Map +from dimos.utils.data import get_data +from dimos.utils.reactive import backpressure +from dimos.utils.testing import TimedSensorReplay + +Connection = Callable[[], Observable[OccupancyGrid]] + + +def live_connection() -> Observable[OccupancyGrid]: + def subscribe(observer, scheduler=None): + lcm.autoconf() + l = lcm.LCM() + + def on_message(grid: OccupancyGrid, _) -> None: + observer.on_next(grid) + + l.subscribe(lcm.Topic("/global_costmap", OccupancyGrid), on_message) + l.start() + + def dispose() -> None: + l.stop() + + return Disposable(dispose) + + return rx.create(subscribe) + + +def recorded_connection() -> Observable[OccupancyGrid]: + lidar_store = TimedSensorReplay("unitree_office_walk/lidar", autocast=LidarMessage.from_msg) + mapper = Map() + return backpressure( + lidar_store.stream(speed=1).pipe( + ops.map(mapper.add_frame), + ops.map(lambda _: mapper.costmap().inflate(0.1).gradient()), + ) + ) + + +def single_message() -> Observable[OccupancyGrid]: + pointcloud_pickle = get_data("lcm_msgs") / "sensor_msgs/PointCloud2.pickle" + with open(pointcloud_pickle, "rb") as f: + pointcloud = PointCloud2.lcm_decode(pickle.load(f)) + mapper = Map() + mapper.add_frame(pointcloud) + return rx.just(mapper.costmap()) diff --git a/dimos/utils/cli/dimos.tcss b/dimos/utils/cli/dimos.tcss new file mode 100644 index 0000000000..3ccbde957d --- /dev/null +++ b/dimos/utils/cli/dimos.tcss @@ -0,0 +1,91 @@ +/* DimOS Base Theme for Textual CLI Applications + * Based on colors.json - Official DimOS color palette + */ + +/* Base Color Palette (from colors.json) */ +$black: #0b0f0f; +$red: #ff0000; +$green: #00eeee; +$yellow: #ffcc00; +$blue: #5c9ff0; +$purple: #00eeee; +$cyan: #00eeee; +$white: #b5e4f4; + +/* Bright Colors */ +$bright-black: #404040; +$bright-red: #ff0000; +$bright-green: #00eeee; +$bright-yellow: #f2ea8c; +$bright-blue: #8cbdf2; +$bright-purple: #00eeee; +$bright-cyan: #00eeee; +$bright-white: #ffffff; + +/* Core Theme Colors */ +$background: #0b0f0f; +$foreground: #b5e4f4; +$cursor: #00eeee; + +/* Semantic Aliases */ +$bg: $black; +$border: $cyan; +$accent: $white; +$dim: $bright-black; +$timestamp: $bright-white; + +/* Message Type Colors */ +$system: $red; +$agent: #88ff88; +$tool: $cyan; +$tool-result: $yellow; +$human: $bright-white; + +/* Status Colors */ +$success: $green; +$error: $red; +$warning: $yellow; +$info: $cyan; + +/* Base Screen */ +Screen { + background: $bg; +} + +/* Default Container */ +Container { + background: $bg; +} + +/* Input Widget */ +Input { + background: $bg; + border: solid $border; + color: $accent; +} + +Input:focus { + border: solid $border; +} + +/* RichLog Widget */ +RichLog { + background: $bg; + border: solid $border; +} + +/* Button Widget */ +Button { + background: $bg; + border: solid $border; + color: $accent; +} + +Button:hover { + background: $dim; + border: solid $accent; +} + +Button:focus { + border: double $accent; +} diff --git a/dimos/utils/cli/foxglove_bridge/run_foxglove_bridge.py b/dimos/utils/cli/foxglove_bridge/run_foxglove_bridge.py new file mode 100644 index 0000000000..8244d16d39 --- /dev/null +++ b/dimos/utils/cli/foxglove_bridge/run_foxglove_bridge.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +use lcm_foxglove_bridge as a module from dimos_lcm +""" + +import asyncio +import os +import threading + +import dimos_lcm +from dimos_lcm.foxglove_bridge import FoxgloveBridge + +dimos_lcm_path = os.path.dirname(os.path.abspath(dimos_lcm.__file__)) +print(f"Using dimos_lcm from: {dimos_lcm_path}") + + +def run_bridge_example() -> None: + """Example of running the bridge in a separate thread""" + + def bridge_thread() -> None: + """Thread function to run the bridge""" + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + bridge_instance = FoxgloveBridge(host="0.0.0.0", port=8765, debug=True, num_threads=4) + + loop.run_until_complete(bridge_instance.run()) + except Exception as e: + print(f"Bridge error: {e}") + finally: + loop.close() + + thread = threading.Thread(target=bridge_thread, daemon=True) + thread.start() + + print("Bridge started in background thread") + print("Open Foxglove Studio and connect to ws://localhost:8765") + print("Press Ctrl+C to exit") + + try: + while True: + threading.Event().wait(1) + except KeyboardInterrupt: + print("Shutting down...") + + +def main() -> None: + run_bridge_example() + + +if __name__ == "__main__": + main() diff --git a/dimos/utils/cli/human/humancli.py b/dimos/utils/cli/human/humancli.py new file mode 100644 index 0000000000..4c474b88d2 --- /dev/null +++ b/dimos/utils/cli/human/humancli.py @@ -0,0 +1,315 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +import textwrap +import threading +from typing import TYPE_CHECKING + +from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolCall, ToolMessage +from rich.highlighter import JSONHighlighter +from rich.theme import Theme +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual.containers import Container +from textual.widgets import Input, RichLog + +from dimos.core import pLCMTransport +from dimos.utils.cli import theme +from dimos.utils.generic import truncate_display_string + +if TYPE_CHECKING: + from textual.events import Key + +# Custom theme for JSON highlighting +JSON_THEME = Theme( + { + "json.key": theme.CYAN, + "json.str": theme.ACCENT, + "json.number": theme.ACCENT, + "json.bool_true": theme.ACCENT, + "json.bool_false": theme.ACCENT, + "json.null": theme.DIM, + "json.brace": theme.BRIGHT_WHITE, + } +) + + +class HumanCLIApp(App): + """IRC-like interface for interacting with DimOS agents.""" + + CSS_PATH = theme.CSS_PATH + + CSS = f""" + Screen {{ + background: {theme.BACKGROUND}; + }} + + #chat-container {{ + height: 1fr; + }} + + RichLog {{ + scrollbar-size: 0 0; + }} + + Input {{ + dock: bottom; + }} + """ + + BINDINGS = [ + Binding("q", "quit", "Quit", show=False), + Binding("ctrl+c", "quit", "Quit"), + Binding("ctrl+l", "clear", "Clear chat"), + ] + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.human_transport = pLCMTransport("/human_input") + self.agent_transport = pLCMTransport("/agent") + self.chat_log: RichLog | None = None + self.input_widget: Input | None = None + self._subscription_thread: threading.Thread | None = None + self._running = False + + def compose(self) -> ComposeResult: + """Compose the IRC-like interface.""" + with Container(id="chat-container"): + self.chat_log = RichLog(highlight=True, markup=True, wrap=False) + yield self.chat_log + + self.input_widget = Input(placeholder="Type a message...") + yield self.input_widget + + def on_mount(self) -> None: + """Initialize the app when mounted.""" + self._running = True + + # Apply custom JSON theme to app console + self.console.push_theme(JSON_THEME) + + # Set custom highlighter for RichLog + self.chat_log.highlighter = JSONHighlighter() + + # Start subscription thread + self._subscription_thread = threading.Thread(target=self._subscribe_to_agent, daemon=True) + self._subscription_thread.start() + + # Focus on input + self.input_widget.focus() + + # Display ASCII art banner + ascii_art = """ + ██████╗ ██╗███╗ ███╗███████╗███╗ ██╗███████╗██╗ ██████╗ ███╗ ██╗ █████╗ ██╗ + ██╔══██╗██║████╗ ████║██╔════╝████╗ ██║██╔════╝██║██╔═══██╗████╗ ██║██╔══██╗██║ + ██║ ██║██║██╔████╔██║█████╗ ██╔██╗ ██║███████╗██║██║ ██║██╔██╗ ██║███████║██║ + ██║ ██║██║██║╚██╔╝██║██╔══╝ ██║╚██╗██║╚════██║██║██║ ██║██║╚██╗██║██╔══██║██║ + ██████╔╝██║██║ ╚═╝ ██║███████╗██║ ╚████║███████║██║╚██████╔╝██║ ╚████║██║ ██║███████╗ + ╚═════╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝ +""" + self.chat_log.write(f"[{theme.ACCENT}]{ascii_art}[/{theme.ACCENT}]") + + # Welcome message + self._add_system_message("Connected to DimOS Agent Interface") + + def on_unmount(self) -> None: + """Clean up when unmounting.""" + self._running = False + + def _subscribe_to_agent(self) -> None: + """Subscribe to agent messages in a separate thread.""" + + def receive_msg(msg) -> None: + if not self._running: + return + + timestamp = datetime.now().strftime("%H:%M:%S") + + if isinstance(msg, SystemMessage): + self.call_from_thread( + self._add_message, + timestamp, + "system", + truncate_display_string(msg.content, 1000), + theme.YELLOW, + ) + elif isinstance(msg, AIMessage): + content = msg.content or "" + tool_calls = msg.additional_kwargs.get("tool_calls", []) + + # Display the main content first + if content: + self.call_from_thread( + self._add_message, timestamp, "agent", content, theme.AGENT + ) + + # Display tool calls separately with different formatting + if tool_calls: + for tc in tool_calls: + tool_info = self._format_tool_call(tc) + self.call_from_thread( + self._add_message, timestamp, "tool", tool_info, theme.TOOL + ) + + # If neither content nor tool calls, show a placeholder + if not content and not tool_calls: + self.call_from_thread( + self._add_message, timestamp, "agent", "", theme.DIM + ) + elif isinstance(msg, ToolMessage): + self.call_from_thread( + self._add_message, timestamp, "tool", msg.content, theme.TOOL_RESULT + ) + elif isinstance(msg, HumanMessage): + self.call_from_thread( + self._add_message, timestamp, "human", msg.content, theme.HUMAN + ) + + self.agent_transport.subscribe(receive_msg) + + def _format_tool_call(self, tool_call: ToolCall) -> str: + """Format a tool call for display.""" + f = tool_call.get("function", {}) + name = f.get("name", "unknown") + return f"▶ {name}({f.get('arguments', '')})" + + def _add_message(self, timestamp: str, sender: str, content: str, color: str) -> None: + """Add a message to the chat log.""" + # Strip leading/trailing whitespace from content + content = content.strip() if content else "" + + # Format timestamp with nicer colors - split into hours, minutes, seconds + time_parts = timestamp.split(":") + if len(time_parts) == 3: + # Format as HH:MM:SS with colored colons + timestamp_formatted = f" [{theme.TIMESTAMP}]{time_parts[0]}:{time_parts[1]}:{time_parts[2]}[/{theme.TIMESTAMP}]" + else: + timestamp_formatted = f" [{theme.TIMESTAMP}]{timestamp}[/{theme.TIMESTAMP}]" + + # Format sender with consistent width + sender_formatted = f"[{color}]{sender:>8}[/{color}]" + + # Calculate the prefix length for proper indentation + # space (1) + timestamp (8) + space (1) + sender (8) + space (1) + separator (1) + space (1) = 21 + prefix = f"{timestamp_formatted} {sender_formatted} │ " + indent = " " * 19 # Spaces to align with the content after the separator + + # Get the width of the chat area (accounting for borders and padding) + width = self.chat_log.size.width - 4 if self.chat_log.size else 76 + + # Calculate the available width for text (subtract prefix length) + text_width = max(width - 20, 40) # Minimum 40 chars for text + + # Split content into lines first (respecting explicit newlines) + lines = content.split("\n") + + for line_idx, line in enumerate(lines): + # Wrap each line to fit the available width + if line_idx == 0: + # First line includes the full prefix + wrapped = textwrap.wrap( + line, width=text_width, initial_indent="", subsequent_indent="" + ) + if wrapped: + self.chat_log.write(prefix + f"[{color}]{wrapped[0]}[/{color}]") + for wrapped_line in wrapped[1:]: + self.chat_log.write(indent + f"│ [{color}]{wrapped_line}[/{color}]") + else: + # Empty line + self.chat_log.write(prefix) + else: + # Subsequent lines from explicit newlines + wrapped = textwrap.wrap( + line, width=text_width, initial_indent="", subsequent_indent="" + ) + if wrapped: + for wrapped_line in wrapped: + self.chat_log.write(indent + f"│ [{color}]{wrapped_line}[/{color}]") + else: + # Empty line + self.chat_log.write(indent + "│") + + def _add_system_message(self, content: str) -> None: + """Add a system message to the chat.""" + timestamp = datetime.now().strftime("%H:%M:%S") + self._add_message(timestamp, "system", content, theme.YELLOW) + + def on_key(self, event: Key) -> None: + """Handle key events.""" + if event.key == "ctrl+c": + self.exit() + event.prevent_default() + + def on_input_submitted(self, event: Input.Submitted) -> None: + """Handle input submission.""" + message = event.value.strip() + if not message: + return + + # Clear input + self.input_widget.value = "" + + # Check for commands + if message.lower() in ["/exit", "/quit"]: + self.exit() + return + elif message.lower() == "/clear": + self.action_clear() + return + elif message.lower() == "/help": + help_text = """Commands: + /clear - Clear the chat log + /help - Show this help message + /exit - Exit the application + /quit - Exit the application + +Tool calls are displayed in cyan with ▶ prefix""" + self._add_system_message(help_text) + return + + # Send to agent (message will be displayed when received back) + self.human_transport.publish(message) + + def action_clear(self) -> None: + """Clear the chat log.""" + self.chat_log.clear() + + def action_quit(self) -> None: + """Quit the application.""" + self._running = False + self.exit() + + +def main() -> None: + """Main entry point for the human CLI.""" + import sys + + if len(sys.argv) > 1 and sys.argv[1] == "web": + # Support for textual-serve web mode + import os + + from textual_serve.server import Server + + server = Server(f"python {os.path.abspath(__file__)}") + server.serve() + else: + app = HumanCLIApp() + app.run() + + +if __name__ == "__main__": + main() diff --git a/dimos/utils/cli/lcmspy/lcmspy.py b/dimos/utils/cli/lcmspy/lcmspy.py new file mode 100755 index 0000000000..42f811ffbc --- /dev/null +++ b/dimos/utils/cli/lcmspy/lcmspy.py @@ -0,0 +1,214 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections import deque +from dataclasses import dataclass +from enum import Enum +import threading +import time + +import lcm + +from dimos.protocol.service.lcmservice import LCMConfig, LCMService + + +class BandwidthUnit(Enum): + BP = "B" + KBP = "kB" + MBP = "MB" + GBP = "GB" + + +def human_readable_bytes(bytes_value: float, round_to: int = 2) -> tuple[float, BandwidthUnit]: + """Convert bytes to human-readable format with appropriate units""" + if bytes_value >= 1024**3: # GB + return round(bytes_value / (1024**3), round_to), BandwidthUnit.GBP + elif bytes_value >= 1024**2: # MB + return round(bytes_value / (1024**2), round_to), BandwidthUnit.MBP + elif bytes_value >= 1024: # KB + return round(bytes_value / 1024, round_to), BandwidthUnit.KBP + else: + return round(bytes_value, round_to), BandwidthUnit.BP + + +class Topic: + history_window: float = 60.0 + + def __init__(self, name: str, history_window: float = 60.0) -> None: + self.name = name + # Store (timestamp, data_size) tuples for statistics + self.message_history = deque() + self.history_window = history_window + # Total traffic accumulator (doesn't get cleaned up) + self.total_traffic_bytes = 0 + + def msg(self, data: bytes) -> None: + # print(f"> msg {self.__str__()} {len(data)} bytes") + datalen = len(data) + self.message_history.append((time.time(), datalen)) + self.total_traffic_bytes += datalen + self._cleanup_old_messages() + + def _cleanup_old_messages(self, max_age: float | None = None) -> None: + """Remove messages older than max_age seconds""" + current_time = time.time() + while self.message_history and current_time - self.message_history[0][0] > ( + max_age or self.history_window + ): + self.message_history.popleft() + + def _get_messages_in_window(self, time_window: float): + """Get messages within the specified time window""" + current_time = time.time() + cutoff_time = current_time - time_window + return [(ts, size) for ts, size in self.message_history if ts >= cutoff_time] + + # avg msg freq in the last n seconds + def freq(self, time_window: float) -> float: + messages = self._get_messages_in_window(time_window) + if not messages: + return 0.0 + return len(messages) / time_window + + # avg bandwidth in kB/s in the last n seconds + def kbps(self, time_window: float) -> float: + messages = self._get_messages_in_window(time_window) + if not messages: + return 0.0 + total_bytes = sum(size for _, size in messages) + total_kbytes = total_bytes / 1000 # Convert bytes to kB + return total_kbytes / time_window + + def kbps_hr(self, time_window: float, round_to: int = 2) -> tuple[float, BandwidthUnit]: + """Return human-readable bandwidth with appropriate units""" + kbps_val = self.kbps(time_window) + # Convert kB/s to B/s for human_readable_bytes + bps = kbps_val * 1000 + return human_readable_bytes(bps, round_to) + + # avg msg size in the last n seconds + def size(self, time_window: float) -> float: + messages = self._get_messages_in_window(time_window) + if not messages: + return 0.0 + total_size = sum(size for _, size in messages) + return total_size / len(messages) + + def total_traffic(self) -> int: + """Return total traffic passed in bytes since the beginning""" + return self.total_traffic_bytes + + def total_traffic_hr(self) -> tuple[float, BandwidthUnit]: + """Return human-readable total traffic with appropriate units""" + total_bytes = self.total_traffic() + return human_readable_bytes(total_bytes) + + def __str__(self) -> str: + return f"topic({self.name})" + + +@dataclass +class LCMSpyConfig(LCMConfig): + topic_history_window: float = 60.0 + + +class LCMSpy(LCMService, Topic): + default_config = LCMSpyConfig + topic = dict[str, Topic] + graph_log_window: float = 1.0 + topic_class: type[Topic] = Topic + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + Topic.__init__(self, name="total", history_window=self.config.topic_history_window) + self.topic = {} + self.l = lcm.LCM(self.config.url) if self.config.url else lcm.LCM() + + def start(self) -> None: + super().start() + self.l.subscribe(".*", self.msg) + + def stop(self) -> None: + """Stop the LCM spy and clean up resources""" + super().stop() + + def msg(self, topic, data) -> None: + Topic.msg(self, data) + + if topic not in self.topic: + print(self.config) + self.topic[topic] = self.topic_class( + topic, history_window=self.config.topic_history_window + ) + self.topic[topic].msg(data) + + +class GraphTopic(Topic): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.freq_history = deque(maxlen=20) + self.bandwidth_history = deque(maxlen=20) + + def update_graphs(self, step_window: float = 1.0) -> None: + """Update historical data for graphing""" + freq = self.freq(step_window) + kbps = self.kbps(step_window) + self.freq_history.append(freq) + self.bandwidth_history.append(kbps) + + +@dataclass +class GraphLCMSpyConfig(LCMSpyConfig): + graph_log_window: float = 1.0 + + +class GraphLCMSpy(LCMSpy, GraphTopic): + default_config = GraphLCMSpyConfig + + graph_log_thread: threading.Thread | None = None + graph_log_stop_event: threading.Event = threading.Event() + topic_class: type[Topic] = GraphTopic + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + GraphTopic.__init__(self, name="total", history_window=self.config.topic_history_window) + + def start(self) -> None: + super().start() + self.graph_log_thread = threading.Thread(target=self.graph_log, daemon=True) + self.graph_log_thread.start() + + def graph_log(self) -> None: + while not self.graph_log_stop_event.is_set(): + self.update_graphs(self.config.graph_log_window) # Update global history + for topic in self.topic.values(): + topic.update_graphs(self.config.graph_log_window) + time.sleep(self.config.graph_log_window) + + def stop(self) -> None: + """Stop the graph logging and LCM spy""" + self.graph_log_stop_event.set() + if self.graph_log_thread and self.graph_log_thread.is_alive(): + self.graph_log_thread.join(timeout=1.0) + super().stop() + + +if __name__ == "__main__": + lcm_spy = LCMSpy() + lcm_spy.start() + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + print("LCM Spy stopped.") diff --git a/dimos/utils/cli/lcmspy/run_lcmspy.py b/dimos/utils/cli/lcmspy/run_lcmspy.py new file mode 100644 index 0000000000..2e96156852 --- /dev/null +++ b/dimos/utils/cli/lcmspy/run_lcmspy.py @@ -0,0 +1,135 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from rich.text import Text +from textual.app import App, ComposeResult +from textual.color import Color +from textual.widgets import DataTable + +from dimos.utils.cli import theme +from dimos.utils.cli.lcmspy.lcmspy import GraphLCMSpy, GraphTopic as SpyTopic + + +def gradient(max_value: float, value: float) -> str: + """Gradient from cyan (low) to yellow (high) using DimOS theme colors""" + ratio = min(value / max_value, 1.0) + # Parse hex colors from theme + cyan = Color.parse(theme.CYAN) + yellow = Color.parse(theme.YELLOW) + color = cyan.blend(yellow, ratio) + + return color.hex + + +def topic_text(topic_name: str) -> Text: + """Format topic name with DimOS theme colors""" + if "#" in topic_name: + parts = topic_name.split("#", 1) + return Text(parts[0], style=theme.BRIGHT_WHITE) + Text("#" + parts[1], style=theme.BLUE) + + if topic_name[:4] == "/rpc": + return Text(topic_name[:4], style=theme.BLUE) + Text( + topic_name[4:], style=theme.BRIGHT_WHITE + ) + + return Text(topic_name, style=theme.BRIGHT_WHITE) + + +class LCMSpyApp(App): + """A real-time CLI dashboard for LCM traffic statistics using Textual.""" + + CSS_PATH = "../dimos.tcss" + + CSS = f""" + Screen {{ + layout: vertical; + background: {theme.BACKGROUND}; + }} + DataTable {{ + height: 2fr; + width: 1fr; + border: solid {theme.BORDER}; + background: {theme.BG}; + scrollbar-size: 0 0; + }} + DataTable > .datatable--header {{ + color: {theme.ACCENT}; + background: transparent; + }} + """ + + refresh_interval: float = 0.5 # seconds + + BINDINGS = [ + ("q", "quit"), + ("ctrl+c", "quit"), + ] + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.spy = GraphLCMSpy(autoconf=True, graph_log_window=0.5) + self.table: DataTable | None = None + + def compose(self) -> ComposeResult: + self.table = DataTable(zebra_stripes=False, cursor_type=None) + self.table.add_column("Topic") + self.table.add_column("Freq (Hz)") + self.table.add_column("Bandwidth") + self.table.add_column("Total Traffic") + yield self.table + + def on_mount(self) -> None: + self.spy.start() + self.set_interval(self.refresh_interval, self.refresh_table) + + async def on_unmount(self) -> None: + self.spy.stop() + + def refresh_table(self) -> None: + topics: list[SpyTopic] = list(self.spy.topic.values()) + topics.sort(key=lambda t: t.total_traffic(), reverse=True) + self.table.clear(columns=False) + + for t in topics: + freq = t.freq(5.0) + kbps = t.kbps(5.0) + bw_val, bw_unit = t.kbps_hr(5.0) + total_val, total_unit = t.total_traffic_hr() + + self.table.add_row( + topic_text(t.name), + Text(f"{freq:.1f}", style=gradient(10, freq)), + Text(f"{bw_val} {bw_unit.value}/s", style=gradient(1024 * 3, kbps)), + Text(f"{total_val} {total_unit.value}"), + ) + + +def main() -> None: + import sys + + if len(sys.argv) > 1 and sys.argv[1] == "web": + import os + + from textual_serve.server import Server + + server = Server(f"python {os.path.abspath(__file__)}") + server.serve() + else: + LCMSpyApp().run() + + +if __name__ == "__main__": + main() diff --git a/dimos/utils/cli/lcmspy/test_lcmspy.py b/dimos/utils/cli/lcmspy/test_lcmspy.py new file mode 100644 index 0000000000..56e8e72c3b --- /dev/null +++ b/dimos/utils/cli/lcmspy/test_lcmspy.py @@ -0,0 +1,221 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +import pytest + +from dimos.protocol.pubsub.lcmpubsub import PickleLCM, Topic +from dimos.utils.cli.lcmspy.lcmspy import GraphLCMSpy, GraphTopic, LCMSpy, Topic as TopicSpy + + +@pytest.mark.lcm +def test_spy_basic() -> None: + lcm = PickleLCM(autoconf=True) + lcm.start() + + lcmspy = LCMSpy(autoconf=True) + lcmspy.start() + + video_topic = Topic(topic="/video") + odom_topic = Topic(topic="/odom") + + for i in range(5): + lcm.publish(video_topic, f"video frame {i}") + time.sleep(0.1) + if i % 2 == 0: + lcm.publish(odom_topic, f"odometry data {i / 2}") + + # Wait a bit for messages to be processed + time.sleep(0.5) + + # Test statistics for video topic + video_topic_spy = lcmspy.topic["/video"] + assert video_topic_spy is not None + + # Test frequency (should be around 10 Hz for 5 messages in ~0.5 seconds) + freq = video_topic_spy.freq(1.0) + assert freq > 0 + print(f"Video topic frequency: {freq:.2f} Hz") + + # Test bandwidth + kbps = video_topic_spy.kbps(1.0) + assert kbps > 0 + print(f"Video topic bandwidth: {kbps:.2f} kbps") + + # Test average message size + avg_size = video_topic_spy.size(1.0) + assert avg_size > 0 + print(f"Video topic average message size: {avg_size:.2f} bytes") + + # Test statistics for odom topic + odom_topic_spy = lcmspy.topic["/odom"] + assert odom_topic_spy is not None + + freq = odom_topic_spy.freq(1.0) + assert freq > 0 + print(f"Odom topic frequency: {freq:.2f} Hz") + + kbps = odom_topic_spy.kbps(1.0) + assert kbps > 0 + print(f"Odom topic bandwidth: {kbps:.2f} kbps") + + avg_size = odom_topic_spy.size(1.0) + assert avg_size > 0 + print(f"Odom topic average message size: {avg_size:.2f} bytes") + + print(f"Video topic: {video_topic_spy}") + print(f"Odom topic: {odom_topic_spy}") + + +@pytest.mark.lcm +def test_topic_statistics_direct() -> None: + """Test Topic statistics directly without LCM""" + + topic = TopicSpy("/test") + + # Add some test messages + test_data = [b"small", b"medium sized message", b"very long message for testing purposes"] + + for _i, data in enumerate(test_data): + topic.msg(data) + time.sleep(0.1) # Simulate time passing + + # Test statistics over 1 second window + freq = topic.freq(1.0) + kbps = topic.kbps(1.0) + avg_size = topic.size(1.0) + + assert freq > 0 + assert kbps > 0 + assert avg_size > 0 + + print(f"Direct test - Frequency: {freq:.2f} Hz") + print(f"Direct test - Bandwidth: {kbps:.2f} kbps") + print(f"Direct test - Avg size: {avg_size:.2f} bytes") + + +def test_topic_cleanup() -> None: + """Test that old messages are properly cleaned up""" + + topic = TopicSpy("/test") + + # Add a message + topic.msg(b"test message") + initial_count = len(topic.message_history) + assert initial_count == 1 + + # Simulate time passing by manually adding old timestamps + old_time = time.time() - 70 # 70 seconds ago + topic.message_history.appendleft((old_time, 10)) + + # Trigger cleanup + topic._cleanup_old_messages(max_age=60.0) + + # Should only have the recent message + assert len(topic.message_history) == 1 + assert topic.message_history[0][0] > time.time() - 10 # Recent message + + +@pytest.mark.lcm +def test_graph_topic_basic() -> None: + """Test GraphTopic basic functionality""" + topic = GraphTopic("/test_graph") + + # Add some messages and update graphs + topic.msg(b"test message") + topic.update_graphs(1.0) + + # Should have history data + assert len(topic.freq_history) == 1 + assert len(topic.bandwidth_history) == 1 + assert topic.freq_history[0] > 0 + assert topic.bandwidth_history[0] > 0 + + +@pytest.mark.lcm +def test_graph_lcmspy_basic() -> None: + """Test GraphLCMSpy basic functionality""" + spy = GraphLCMSpy(autoconf=True, graph_log_window=0.1) + spy.start() + time.sleep(0.2) # Wait for thread to start + + # Simulate a message + spy.msg("/test", b"test data") + time.sleep(0.2) # Wait for graph update + + # Should create GraphTopic with history + topic = spy.topic["/test"] + assert isinstance(topic, GraphTopic) + assert len(topic.freq_history) > 0 + assert len(topic.bandwidth_history) > 0 + + spy.stop() + + +@pytest.mark.lcm +def test_lcmspy_global_totals() -> None: + """Test that LCMSpy tracks global totals as a Topic itself""" + spy = LCMSpy(autoconf=True) + spy.start() + + # Send messages to different topics + spy.msg("/video", b"video frame data") + spy.msg("/odom", b"odometry data") + spy.msg("/imu", b"imu data") + + # The spy itself should have accumulated all messages + assert len(spy.message_history) == 3 + + # Check global statistics + global_freq = spy.freq(1.0) + global_kbps = spy.kbps(1.0) + global_size = spy.size(1.0) + + assert global_freq > 0 + assert global_kbps > 0 + assert global_size > 0 + + print(f"Global frequency: {global_freq:.2f} Hz") + print(f"Global bandwidth: {spy.kbps_hr(1.0)}") + print(f"Global avg message size: {global_size:.0f} bytes") + + spy.stop() + + +@pytest.mark.lcm +def test_graph_lcmspy_global_totals() -> None: + """Test that GraphLCMSpy tracks global totals with history""" + spy = GraphLCMSpy(autoconf=True, graph_log_window=0.1) + spy.start() + time.sleep(0.2) + + # Send messages + spy.msg("/video", b"video frame data") + spy.msg("/odom", b"odometry data") + time.sleep(0.2) # Wait for graph update + + # Update global graphs + spy.update_graphs(1.0) + + # Should have global history + assert len(spy.freq_history) == 1 + assert len(spy.bandwidth_history) == 1 + assert spy.freq_history[0] > 0 + assert spy.bandwidth_history[0] > 0 + + print(f"Global frequency history: {spy.freq_history[0]:.2f} Hz") + print(f"Global bandwidth history: {spy.bandwidth_history[0]:.2f} kB/s") + + spy.stop() diff --git a/dimos/utils/cli/skillspy/demo_skillspy.py b/dimos/utils/cli/skillspy/demo_skillspy.py new file mode 100644 index 0000000000..f7d4875e01 --- /dev/null +++ b/dimos/utils/cli/skillspy/demo_skillspy.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Demo script that runs skills in the background while agentspy monitors them.""" + +import threading +import time + +from dimos.protocol.skill.coordinator import SkillCoordinator +from dimos.protocol.skill.skill import SkillContainer, skill + + +class DemoSkills(SkillContainer): + @skill() + def count_to(self, n: int) -> str: + """Count to n with delays.""" + for _i in range(n): + time.sleep(0.5) + return f"Counted to {n}" + + @skill() + def compute_fibonacci(self, n: int) -> int: + """Compute nth fibonacci number.""" + if n <= 1: + return n + a, b = 0, 1 + for _ in range(2, n + 1): + time.sleep(0.1) # Simulate computation + a, b = b, a + b + return b + + @skill() + def simulate_error(self) -> None: + """Skill that always errors.""" + time.sleep(0.3) + raise RuntimeError("Simulated error for testing") + + @skill() + def quick_task(self, name: str) -> str: + """Quick task that completes fast.""" + time.sleep(0.1) + return f"Quick task '{name}' done!" + + +def run_demo_skills() -> None: + """Run demo skills in background.""" + # Create and start agent interface + agent_interface = SkillCoordinator() + agent_interface.start() + + # Register skills + demo_skills = DemoSkills() + agent_interface.register_skills(demo_skills) + + # Run various skills periodically + def skill_runner() -> None: + counter = 0 + while True: + time.sleep(2) + + # Generate unique call_id for each invocation + call_id = f"demo-{counter}" + + # Run different skills based on counter + if counter % 4 == 0: + # Run multiple count_to in parallel to show parallel execution + agent_interface.call_skill(f"{call_id}-count-1", "count_to", {"args": [3]}) + agent_interface.call_skill(f"{call_id}-count-2", "count_to", {"args": [5]}) + agent_interface.call_skill(f"{call_id}-count-3", "count_to", {"args": [2]}) + elif counter % 4 == 1: + agent_interface.call_skill(f"{call_id}-fib", "compute_fibonacci", {"args": [10]}) + elif counter % 4 == 2: + agent_interface.call_skill( + f"{call_id}-quick", "quick_task", {"args": [f"task-{counter}"]} + ) + else: + agent_interface.call_skill(f"{call_id}-error", "simulate_error", {}) + + counter += 1 + + # Start skill runner in background + thread = threading.Thread(target=skill_runner, daemon=True) + thread.start() + + print("Demo skills running in background. Start agentspy in another terminal to monitor.") + print("Run: agentspy") + + # Keep running + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + print("\nDemo stopped.") + + agent_interface.stop() + + +if __name__ == "__main__": + run_demo_skills() diff --git a/dimos/utils/cli/skillspy/skillspy.py b/dimos/utils/cli/skillspy/skillspy.py new file mode 100644 index 0000000000..769478b00e --- /dev/null +++ b/dimos/utils/cli/skillspy/skillspy.py @@ -0,0 +1,281 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import threading +import time +from typing import TYPE_CHECKING + +from rich.text import Text +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual.widgets import DataTable, Footer + +from dimos.protocol.skill.coordinator import SkillCoordinator, SkillState, SkillStateEnum +from dimos.utils.cli import theme + +if TYPE_CHECKING: + from collections.abc import Callable + + from dimos.protocol.skill.comms import SkillMsg + + +class AgentSpy: + """Spy on agent skill executions via LCM messages.""" + + def __init__(self) -> None: + self.agent_interface = SkillCoordinator() + self.message_callbacks: list[Callable[[dict[str, SkillState]], None]] = [] + self._lock = threading.Lock() + self._latest_state: dict[str, SkillState] = {} + self._running = False + + def start(self) -> None: + """Start spying on agent messages.""" + self._running = True + # Start the agent interface + self.agent_interface.start() + + # Subscribe to the agent interface's comms + self.agent_interface.skill_transport.subscribe(self._handle_message) + + def stop(self) -> None: + """Stop spying.""" + self._running = False + # Give threads a moment to finish processing + time.sleep(0.2) + self.agent_interface.stop() + + def _handle_message(self, msg: SkillMsg) -> None: + """Handle incoming skill messages.""" + if not self._running: + return + + # Small delay to ensure agent_interface has processed the message + def delayed_update() -> None: + time.sleep(0.1) + if not self._running: + return + with self._lock: + self._latest_state = self.agent_interface.generate_snapshot(clear=False) + for callback in self.message_callbacks: + callback(self._latest_state) + + # Run in separate thread to not block LCM + threading.Thread(target=delayed_update, daemon=True).start() + + def subscribe(self, callback: Callable[[dict[str, SkillState]], None]) -> None: + """Subscribe to state updates.""" + self.message_callbacks.append(callback) + + def get_state(self) -> dict[str, SkillState]: + """Get current state snapshot.""" + with self._lock: + return self._latest_state.copy() + + +def state_color(state: SkillStateEnum) -> str: + """Get color for skill state.""" + if state == SkillStateEnum.pending: + return theme.WARNING + elif state == SkillStateEnum.running: + return theme.AGENT + elif state == SkillStateEnum.completed: + return theme.SUCCESS + elif state == SkillStateEnum.error: + return theme.ERROR + return theme.FOREGROUND + + +def format_duration(duration: float) -> str: + """Format duration in human readable format.""" + if duration < 1: + return f"{duration * 1000:.0f}ms" + elif duration < 60: + return f"{duration:.1f}s" + elif duration < 3600: + return f"{duration / 60:.1f}m" + else: + return f"{duration / 3600:.1f}h" + + +class AgentSpyApp(App): + """A real-time CLI dashboard for agent skill monitoring using Textual.""" + + CSS_PATH = theme.CSS_PATH + + CSS = f""" + Screen {{ + layout: vertical; + background: {theme.BACKGROUND}; + }} + DataTable {{ + height: 100%; + border: solid $border; + background: {theme.BACKGROUND}; + }} + DataTable > .datatable--header {{ + background: transparent; + }} + Footer {{ + background: transparent; + }} + """ + + BINDINGS = [ + Binding("q", "quit", "Quit"), + Binding("c", "clear", "Clear History"), + Binding("ctrl+c", "quit", "Quit", show=False), + ] + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.spy = AgentSpy() + self.table: DataTable | None = None + self.skill_history: list[tuple[str, SkillState, float]] = [] # (call_id, state, start_time) + + def compose(self) -> ComposeResult: + self.table = DataTable(zebra_stripes=False, cursor_type=None) + self.table.add_column("Call ID") + self.table.add_column("Skill Name") + self.table.add_column("State") + self.table.add_column("Duration") + self.table.add_column("Messages") + self.table.add_column("Details") + + yield self.table + yield Footer() + + def on_mount(self) -> None: + """Start the spy when app mounts.""" + self.spy.subscribe(self.update_state) + self.spy.start() + + # Set up periodic refresh to update durations + self.set_interval(1.0, self.refresh_table) + + def on_unmount(self) -> None: + """Stop the spy when app unmounts.""" + self.spy.stop() + + def update_state(self, state: dict[str, SkillState]) -> None: + """Update state from spy callback. State dict is keyed by call_id.""" + # Update history with current state + current_time = time.time() + + # Add new skills or update existing ones + for call_id, skill_state in state.items(): + # Find if this call_id already in history + found = False + for i, (existing_call_id, _old_state, start_time) in enumerate(self.skill_history): + if existing_call_id == call_id: + # Update existing entry + self.skill_history[i] = (call_id, skill_state, start_time) + found = True + break + + if not found: + # Add new entry with current time as start + start_time = current_time + if skill_state.start_msg: + # Use start message timestamp if available + start_time = skill_state.start_msg.ts + self.skill_history.append((call_id, skill_state, start_time)) + + # Schedule UI update + self.call_from_thread(self.refresh_table) + + def refresh_table(self) -> None: + """Refresh the table display.""" + if not self.table: + return + + # Clear table + self.table.clear(columns=False) + + # Sort by start time (newest first) + sorted_history = sorted(self.skill_history, key=lambda x: x[2], reverse=True) + + # Get terminal height and calculate how many rows we can show + height = self.size.height - 6 # Account for header, footer, column headers + max_rows = max(1, height) + + # Show only top N entries + for call_id, skill_state, start_time in sorted_history[:max_rows]: + # Calculate how long ago it started (for progress indicator) + time_ago = time.time() - start_time + + # Duration + duration_str = format_duration(skill_state.duration()) + + # Message count + msg_count = len(skill_state) + + # Details based on state and last message + details = "" + if skill_state.state == SkillStateEnum.error and skill_state.error_msg: + # Show error message + error_content = skill_state.error_msg.content + if isinstance(error_content, dict): + details = error_content.get("msg", "Error")[:40] + else: + details = str(error_content)[:40] + elif skill_state.state == SkillStateEnum.completed and skill_state.ret_msg: + # Show return value + details = f"→ {str(skill_state.ret_msg.content)[:37]}" + elif skill_state.state == SkillStateEnum.running: + # Show progress indicator + details = "⋯ " + "▸" * min(int(time_ago), 20) + + # Format call_id for display (truncate if too long) + display_call_id = call_id + if len(call_id) > 16: + display_call_id = call_id[:13] + "..." + + # Add row with colored state + self.table.add_row( + Text(display_call_id, style=theme.BRIGHT_BLUE), + Text(skill_state.name, style=theme.YELLOW), + Text(skill_state.state.name, style=state_color(skill_state.state)), + Text(duration_str, style=theme.WHITE), + Text(str(msg_count), style=theme.YELLOW), + Text(details, style=theme.FOREGROUND), + ) + + def action_clear(self) -> None: + """Clear the skill history.""" + self.skill_history.clear() + self.refresh_table() + + +def main() -> None: + """Main entry point for agentspy CLI.""" + import sys + + # Check if running in web mode + if len(sys.argv) > 1 and sys.argv[1] == "web": + import os + + from textual_serve.server import Server + + server = Server(f"python {os.path.abspath(__file__)}") + server.serve() + else: + app = AgentSpyApp() + app.run() + + +if __name__ == "__main__": + main() diff --git a/dimos/utils/cli/theme.py b/dimos/utils/cli/theme.py new file mode 100644 index 0000000000..e3d98b07de --- /dev/null +++ b/dimos/utils/cli/theme.py @@ -0,0 +1,99 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Parse DimOS theme from tcss file.""" + +from __future__ import annotations + +from pathlib import Path +import re + + +def parse_tcss_colors(tcss_path: str | Path) -> dict[str, str]: + """Parse color variables from a tcss file. + + Args: + tcss_path: Path to the tcss file + + Returns: + Dictionary mapping variable names to color values + """ + tcss_path = Path(tcss_path) + content = tcss_path.read_text() + + # Match $variable: value; patterns + pattern = r"\$([a-zA-Z0-9_-]+)\s*:\s*(#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3});" + matches = re.findall(pattern, content) + + return {name: value for name, value in matches} + + +# Load DimOS theme colors +_THEME_PATH = Path(__file__).parent / "dimos.tcss" +COLORS = parse_tcss_colors(_THEME_PATH) + +# Export CSS path for Textual apps +CSS_PATH = str(_THEME_PATH) + + +# Convenience accessors for common colors +def get(name: str, default: str = "#ffffff") -> str: + """Get a color by variable name.""" + return COLORS.get(name, default) + + +# Base color palette +BLACK = COLORS.get("black", "#0b0f0f") +RED = COLORS.get("red", "#ff0000") +GREEN = COLORS.get("green", "#00eeee") +YELLOW = COLORS.get("yellow", "#ffcc00") +BLUE = COLORS.get("blue", "#5c9ff0") +PURPLE = COLORS.get("purple", "#00eeee") +CYAN = COLORS.get("cyan", "#00eeee") +WHITE = COLORS.get("white", "#b5e4f4") + +# Bright colors +BRIGHT_BLACK = COLORS.get("bright-black", "#404040") +BRIGHT_RED = COLORS.get("bright-red", "#ff0000") +BRIGHT_GREEN = COLORS.get("bright-green", "#00eeee") +BRIGHT_YELLOW = COLORS.get("bright-yellow", "#f2ea8c") +BRIGHT_BLUE = COLORS.get("bright-blue", "#8cbdf2") +BRIGHT_PURPLE = COLORS.get("bright-purple", "#00eeee") +BRIGHT_CYAN = COLORS.get("bright-cyan", "#00eeee") +BRIGHT_WHITE = COLORS.get("bright-white", "#ffffff") + +# Core theme colors +BACKGROUND = COLORS.get("background", "#0b0f0f") +FOREGROUND = COLORS.get("foreground", "#b5e4f4") +CURSOR = COLORS.get("cursor", "#00eeee") + +# Semantic aliases +BG = COLORS.get("bg", "#0b0f0f") +BORDER = COLORS.get("border", "#00eeee") +ACCENT = COLORS.get("accent", "#b5e4f4") +DIM = COLORS.get("dim", "#404040") +TIMESTAMP = COLORS.get("timestamp", "#ffffff") + +# Message type colors +SYSTEM = COLORS.get("system", "#ff0000") +AGENT = COLORS.get("agent", "#88ff88") +TOOL = COLORS.get("tool", "#00eeee") +TOOL_RESULT = COLORS.get("tool-result", "#ffff00") +HUMAN = COLORS.get("human", "#ffffff") + +# Status colors +SUCCESS = COLORS.get("success", "#00eeee") +ERROR = COLORS.get("error", "#ff0000") +WARNING = COLORS.get("warning", "#ffcc00") +INFO = COLORS.get("info", "#00eeee") diff --git a/dimos/utils/data.py b/dimos/utils/data.py new file mode 100644 index 0000000000..8b70c2ad27 --- /dev/null +++ b/dimos/utils/data.py @@ -0,0 +1,159 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from functools import cache +from pathlib import Path +import subprocess +import tarfile + + +@cache +def _get_repo_root() -> Path: + try: + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], capture_output=True, check=True, text=True + ) + return Path(result.stdout.strip()) + except subprocess.CalledProcessError: + raise RuntimeError("Not in a Git repository") + + +@cache +def _get_data_dir(extra_path: str | None = None) -> Path: + if extra_path: + return _get_repo_root() / "data" / extra_path + return _get_repo_root() / "data" + + +@cache +def _get_lfs_dir() -> Path: + return _get_data_dir() / ".lfs" + + +def _check_git_lfs_available() -> bool: + try: + subprocess.run(["git", "lfs", "version"], capture_output=True, check=True, text=True) + except (subprocess.CalledProcessError, FileNotFoundError): + raise RuntimeError( + "Git LFS is not installed. Please install git-lfs to use test data utilities.\n" + "Installation instructions: https://git-lfs.github.io/" + ) + return True + + +def _is_lfs_pointer_file(file_path: Path) -> bool: + try: + # LFS pointer files are small (typically < 200 bytes) and start with specific text + if file_path.stat().st_size > 1024: # LFS pointers are much smaller + return False + + with open(file_path, encoding="utf-8") as f: + first_line = f.readline().strip() + return first_line.startswith("version https://git-lfs.github.com/spec/") + + except (UnicodeDecodeError, OSError): + return False + + +def _lfs_pull(file_path: Path, repo_root: Path) -> None: + try: + relative_path = file_path.relative_to(repo_root) + + subprocess.run( + ["git", "lfs", "pull", "--include", str(relative_path)], + cwd=repo_root, + check=True, + capture_output=True, + ) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Failed to pull LFS file {file_path}: {e}") + + return None + + +def _decompress_archive(filename: str | Path) -> Path: + target_dir = _get_data_dir() + filename_path = Path(filename) + with tarfile.open(filename_path, "r:gz") as tar: + tar.extractall(target_dir) + return target_dir / filename_path.name.replace(".tar.gz", "") + + +def _pull_lfs_archive(filename: str | Path) -> Path: + # Check Git LFS availability first + _check_git_lfs_available() + + # Find repository root + repo_root = _get_repo_root() + + # Construct path to test data file + file_path = _get_lfs_dir() / (str(filename) + ".tar.gz") + + # Check if file exists + if not file_path.exists(): + raise FileNotFoundError( + f"Test file '{filename}' not found at {file_path}. " + f"Make sure the file is committed to Git LFS in the tests/data directory." + ) + + # If it's an LFS pointer file, ensure LFS is set up and pull the file + if _is_lfs_pointer_file(file_path): + _lfs_pull(file_path, repo_root) + + # Verify the file was actually downloaded + if _is_lfs_pointer_file(file_path): + raise RuntimeError( + f"Failed to download LFS file '{filename}'. The file is still a pointer after attempting to pull." + ) + + return file_path + + +def get_data(filename: str | Path) -> Path: + """ + Get the path to a test data, downloading from LFS if needed. + + This function will: + 1. Check that Git LFS is available + 2. Locate the file in the tests/data directory + 3. Initialize Git LFS if needed + 4. Download the file from LFS if it's a pointer file + 5. Return the Path object to the actual file or dir + + Args: + filename: Name of the test file (e.g., "lidar_sample.bin") + + Returns: + Path: Path object to the test file + + Raises: + RuntimeError: If Git LFS is not available or LFS operations fail + FileNotFoundError: If the test file doesn't exist + + Usage: + # As string path + file_path = str(testFile("sample.bin")) + + # As context manager for file operations + with testFile("sample.bin").open('rb') as f: + data = f.read() + """ + data_dir = _get_data_dir() + file_path = data_dir / filename + + # already pulled and decompressed, return it directly + if file_path.exists(): + return file_path + + return _decompress_archive(_pull_lfs_archive(filename)) diff --git a/dimos/utils/decorators/__init__.py b/dimos/utils/decorators/__init__.py new file mode 100644 index 0000000000..ee17260c20 --- /dev/null +++ b/dimos/utils/decorators/__init__.py @@ -0,0 +1,12 @@ +"""Decorators and accumulators for rate limiting and other utilities.""" + +from .accumulators import Accumulator, LatestAccumulator, RollingAverageAccumulator +from .decorators import limit, retry + +__all__ = [ + "Accumulator", + "LatestAccumulator", + "RollingAverageAccumulator", + "limit", + "retry", +] diff --git a/dimos/utils/decorators/accumulators.py b/dimos/utils/decorators/accumulators.py new file mode 100644 index 0000000000..7672ff7033 --- /dev/null +++ b/dimos/utils/decorators/accumulators.py @@ -0,0 +1,106 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC, abstractmethod +import threading +from typing import Generic, TypeVar + +T = TypeVar("T") + + +class Accumulator(ABC, Generic[T]): + """Base class for accumulating messages between rate-limited calls.""" + + @abstractmethod + def add(self, *args, **kwargs) -> None: + """Add args and kwargs to the accumulator.""" + pass + + @abstractmethod + def get(self) -> tuple[tuple, dict] | None: + """Get the accumulated args and kwargs and reset the accumulator.""" + pass + + @abstractmethod + def __len__(self) -> int: + """Return the number of accumulated items.""" + pass + + +class LatestAccumulator(Accumulator[T]): + """Simple accumulator that remembers only the latest args and kwargs.""" + + def __init__(self) -> None: + self._latest: tuple[tuple, dict] | None = None + self._lock = threading.Lock() + + def add(self, *args, **kwargs) -> None: + with self._lock: + self._latest = (args, kwargs) + + def get(self) -> tuple[tuple, dict] | None: + with self._lock: + result = self._latest + self._latest = None + return result + + def __len__(self) -> int: + with self._lock: + return 1 if self._latest is not None else 0 + + +class RollingAverageAccumulator(Accumulator[T]): + """Accumulator that maintains a rolling average of the first argument. + + This accumulator expects the first argument to be numeric and maintains + a rolling average without storing individual values. + """ + + def __init__(self) -> None: + self._sum: float = 0.0 + self._count: int = 0 + self._latest_kwargs: dict = {} + self._lock = threading.Lock() + + def add(self, *args, **kwargs) -> None: + if not args: + raise ValueError("RollingAverageAccumulator requires at least one argument") + + with self._lock: + try: + value = float(args[0]) + self._sum += value + self._count += 1 + self._latest_kwargs = kwargs + except (TypeError, ValueError): + raise TypeError(f"First argument must be numeric, got {type(args[0])}") + + def get(self) -> tuple[tuple, dict] | None: + with self._lock: + if self._count == 0: + return None + + average = self._sum / self._count + result = ((average,), self._latest_kwargs) + + # Reset accumulator + self._sum = 0.0 + self._count = 0 + self._latest_kwargs = {} + + return result + + def __len__(self) -> int: + with self._lock: + return self._count diff --git a/dimos/utils/decorators/decorators.py b/dimos/utils/decorators/decorators.py new file mode 100644 index 0000000000..4511aea309 --- /dev/null +++ b/dimos/utils/decorators/decorators.py @@ -0,0 +1,201 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Callable +from functools import wraps +import threading +import time + +from .accumulators import Accumulator, LatestAccumulator + + +def limit(max_freq: float, accumulator: Accumulator | None = None): + """ + Decorator that limits function call frequency. + + If calls come faster than max_freq, they are skipped. + If calls come slower than max_freq, they pass through immediately. + + Args: + max_freq: Maximum frequency in Hz (calls per second) + accumulator: Optional accumulator to collect skipped calls (defaults to LatestAccumulator) + + Returns: + Decorated function that respects the frequency limit + """ + if max_freq <= 0: + raise ValueError("Frequency must be positive") + + min_interval = 1.0 / max_freq + + # Create default accumulator if none provided + if accumulator is None: + accumulator = LatestAccumulator() + + def decorator(func: Callable) -> Callable: + last_call_time = 0.0 + lock = threading.Lock() + timer: threading.Timer | None = None + + def execute_accumulated() -> None: + nonlocal last_call_time, timer + with lock: + if len(accumulator): + acc_args, acc_kwargs = accumulator.get() + last_call_time = time.time() + timer = None + func(*acc_args, **acc_kwargs) + + @wraps(func) + def wrapper(*args, **kwargs): + nonlocal last_call_time, timer + current_time = time.time() + + with lock: + time_since_last = current_time - last_call_time + + if time_since_last >= min_interval: + # Cancel any pending timer + if timer is not None: + timer.cancel() + timer = None + + # Enough time has passed, execute the function + last_call_time = current_time + + # if we have accumulated data, we get a compound value + if len(accumulator): + accumulator.add(*args, **kwargs) + acc_args, acc_kwargs = accumulator.get() # accumulator resets here + return func(*acc_args, **acc_kwargs) + + # No accumulated data, normal call + return func(*args, **kwargs) + + else: + # Too soon, skip this call + accumulator.add(*args, **kwargs) + + # Schedule execution for when the interval expires + if timer is not None: + timer.cancel() + + time_to_wait = min_interval - time_since_last + timer = threading.Timer(time_to_wait, execute_accumulated) + timer.start() + + return None + + return wrapper + + return decorator + + +def simple_mcache(method: Callable) -> Callable: + """ + Decorator to cache the result of a method call on the instance. + + The cached value is stored as an attribute on the instance with the name + `_cached_`. Subsequent calls to the method will return the + cached value instead of recomputing it. + + Thread-safe: Uses a lock per instance to ensure the cached value is + computed only once even in multi-threaded environments. + + Args: + method: The method to be decorated. + + Returns: + The decorated method with caching behavior. + """ + + attr_name = f"_cached_{method.__name__}" + lock_name = f"_lock_{method.__name__}" + + @wraps(method) + def getter(self): + # Get or create the lock for this instance + if not hasattr(self, lock_name): + # This is a one-time operation, race condition here is acceptable + # as worst case we create multiple locks but only one gets stored + setattr(self, lock_name, threading.Lock()) + + lock = getattr(self, lock_name) + + if hasattr(self, attr_name): + return getattr(self, attr_name) + + with lock: + # Check again inside the lock + if not hasattr(self, attr_name): + setattr(self, attr_name, method(self)) + return getattr(self, attr_name) + + return getter + + +def retry(max_retries: int = 3, on_exception: type[Exception] = Exception, delay: float = 0.0): + """ + Decorator that retries a function call if it raises an exception. + + Args: + max_retries: Maximum number of retry attempts (default: 3) + on_exception: Exception type to catch and retry on (default: Exception) + delay: Fixed delay in seconds between retries (default: 0.0) + + Returns: + Decorated function that will retry on failure + + Example: + @retry(max_retries=5, on_exception=ConnectionError, delay=0.5) + def connect_to_server(): + # connection logic that might fail + pass + + @retry() # Use defaults: 3 retries on any Exception, no delay + def risky_operation(): + # might fail occasionally + pass + """ + if max_retries < 0: + raise ValueError("max_retries must be non-negative") + if delay < 0: + raise ValueError("delay must be non-negative") + + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs): + last_exception = None + + for attempt in range(max_retries + 1): + try: + return func(*args, **kwargs) + except on_exception as e: + last_exception = e + if attempt < max_retries: + # Still have retries left + if delay > 0: + time.sleep(delay) + continue + else: + # Out of retries, re-raise the last exception + raise + + # This should never be reached, but just in case + if last_exception: + raise last_exception + + return wrapper + + return decorator diff --git a/dimos/utils/decorators/test_decorators.py b/dimos/utils/decorators/test_decorators.py new file mode 100644 index 0000000000..fdad670454 --- /dev/null +++ b/dimos/utils/decorators/test_decorators.py @@ -0,0 +1,262 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +import pytest + +from dimos.utils.decorators import RollingAverageAccumulator, limit, retry + + +def test_limit() -> None: + """Test limit decorator with keyword arguments.""" + calls = [] + + @limit(20) # 20 Hz + def process(msg: str, keyword: int = 0) -> str: + calls.append((msg, keyword)) + return f"{msg}:{keyword}" + + # First call goes through + result1 = process("first", keyword=1) + assert result1 == "first:1" + assert calls == [("first", 1)] + + # Quick calls get accumulated + result2 = process("second", keyword=2) + assert result2 is None + + result3 = process("third", keyword=3) + assert result3 is None + + # Wait for interval, expect to be called after it passes + time.sleep(0.6) + + result4 = process("fourth") + assert result4 == "fourth:0" + + assert calls == [("first", 1), ("third", 3), ("fourth", 0)] + + +def test_latest_rolling_average() -> None: + """Test RollingAverageAccumulator with limit decorator.""" + calls = [] + + accumulator = RollingAverageAccumulator() + + @limit(20, accumulator=accumulator) # 20 Hz + def process(value: float, label: str = "") -> str: + calls.append((value, label)) + return f"{value}:{label}" + + # First call goes through + result1 = process(10.0, label="first") + assert result1 == "10.0:first" + assert calls == [(10.0, "first")] + + # Quick calls get accumulated + result2 = process(20.0, label="second") + assert result2 is None + + result3 = process(30.0, label="third") + assert result3 is None + + # Wait for interval + time.sleep(0.6) + + # Should see the average of accumulated values + assert calls == [(10.0, "first"), (25.0, "third")] # (20+30)/2 = 25 + + +def test_retry_success_after_failures() -> None: + """Test that retry decorator retries on failure and eventually succeeds.""" + attempts = [] + + @retry(max_retries=3) + def flaky_function(fail_times: int = 2) -> str: + attempts.append(len(attempts)) + if len(attempts) <= fail_times: + raise ValueError(f"Attempt {len(attempts)} failed") + return "success" + + result = flaky_function() + assert result == "success" + assert len(attempts) == 3 # Failed twice, succeeded on third attempt + + +def test_retry_exhausted() -> None: + """Test that retry decorator raises exception when retries are exhausted.""" + attempts = [] + + @retry(max_retries=2) + def always_fails(): + attempts.append(len(attempts)) + raise RuntimeError(f"Attempt {len(attempts)} failed") + + with pytest.raises(RuntimeError) as exc_info: + always_fails() + + assert "Attempt 3 failed" in str(exc_info.value) + assert len(attempts) == 3 # Initial attempt + 2 retries + + +def test_retry_specific_exception() -> None: + """Test that retry only catches specified exception types.""" + attempts = [] + + @retry(max_retries=3, on_exception=ValueError) + def raises_different_exceptions() -> str: + attempts.append(len(attempts)) + if len(attempts) == 1: + raise ValueError("First attempt") + elif len(attempts) == 2: + raise TypeError("Second attempt - should not be retried") + return "success" + + # Should fail on TypeError (not retried) + with pytest.raises(TypeError) as exc_info: + raises_different_exceptions() + + assert "Second attempt" in str(exc_info.value) + assert len(attempts) == 2 # First attempt with ValueError, second with TypeError + + +def test_retry_no_failures() -> None: + """Test that retry decorator works when function succeeds immediately.""" + attempts = [] + + @retry(max_retries=5) + def always_succeeds() -> str: + attempts.append(len(attempts)) + return "immediate success" + + result = always_succeeds() + assert result == "immediate success" + assert len(attempts) == 1 # Only one attempt needed + + +def test_retry_with_delay() -> None: + """Test that retry decorator applies delay between attempts.""" + attempts = [] + times = [] + + @retry(max_retries=2, delay=0.1) + def delayed_failures() -> str: + times.append(time.time()) + attempts.append(len(attempts)) + if len(attempts) < 2: + raise ValueError(f"Attempt {len(attempts)}") + return "success" + + start = time.time() + result = delayed_failures() + duration = time.time() - start + + assert result == "success" + assert len(attempts) == 2 + assert duration >= 0.1 # At least one delay occurred + + # Check that delays were applied + if len(times) >= 2: + assert times[1] - times[0] >= 0.1 + + +def test_retry_zero_retries() -> None: + """Test retry with max_retries=0 (no retries, just one attempt).""" + attempts = [] + + @retry(max_retries=0) + def single_attempt(): + attempts.append(len(attempts)) + raise ValueError("Failed") + + with pytest.raises(ValueError): + single_attempt() + + assert len(attempts) == 1 # Only the initial attempt + + +def test_retry_invalid_parameters() -> None: + """Test that retry decorator validates parameters.""" + with pytest.raises(ValueError): + + @retry(max_retries=-1) + def invalid_retries() -> None: + pass + + with pytest.raises(ValueError): + + @retry(delay=-0.5) + def invalid_delay() -> None: + pass + + +def test_retry_with_methods() -> None: + """Test that retry decorator works with class methods, instance methods, and static methods.""" + + class TestClass: + def __init__(self) -> None: + self.instance_attempts = [] + self.instance_value = 42 + + @retry(max_retries=3) + def instance_method(self, fail_times: int = 2) -> str: + """Test retry on instance method.""" + self.instance_attempts.append(len(self.instance_attempts)) + if len(self.instance_attempts) <= fail_times: + raise ValueError(f"Instance attempt {len(self.instance_attempts)} failed") + return f"instance success with value {self.instance_value}" + + @classmethod + @retry(max_retries=2) + def class_method(cls, attempts_list, fail_times: int = 1) -> str: + """Test retry on class method.""" + attempts_list.append(len(attempts_list)) + if len(attempts_list) <= fail_times: + raise ValueError(f"Class attempt {len(attempts_list)} failed") + return f"class success from {cls.__name__}" + + @staticmethod + @retry(max_retries=2) + def static_method(attempts_list, fail_times: int = 1) -> str: + """Test retry on static method.""" + attempts_list.append(len(attempts_list)) + if len(attempts_list) <= fail_times: + raise ValueError(f"Static attempt {len(attempts_list)} failed") + return "static success" + + # Test instance method + obj = TestClass() + result = obj.instance_method() + assert result == "instance success with value 42" + assert len(obj.instance_attempts) == 3 # Failed twice, succeeded on third + + # Test class method + class_attempts = [] + result = TestClass.class_method(class_attempts) + assert result == "class success from TestClass" + assert len(class_attempts) == 2 # Failed once, succeeded on second + + # Test static method + static_attempts = [] + result = TestClass.static_method(static_attempts) + assert result == "static success" + assert len(static_attempts) == 2 # Failed once, succeeded on second + + # Test that self is properly maintained across retries + obj2 = TestClass() + obj2.instance_value = 100 + result = obj2.instance_method() + assert result == "instance success with value 100" + assert len(obj2.instance_attempts) == 3 diff --git a/dimos/utils/demo_image_encoding.py b/dimos/utils/demo_image_encoding.py new file mode 100644 index 0000000000..a98924260c --- /dev/null +++ b/dimos/utils/demo_image_encoding.py @@ -0,0 +1,127 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +# Usage + +Run it with uncompressed LCM: + + python dimos/utils/demo_image_encoding.py + +Run it with JPEG LCM: + + python dimos/utils/demo_image_encoding.py --use-jpeg +""" + +import argparse +import threading +import time + +from reactivex.disposable import Disposable + +from dimos.core.module import Module +from dimos.core.module_coordinator import ModuleCoordinator +from dimos.core.stream import In, Out +from dimos.core.transport import JpegLcmTransport, LCMTransport +from dimos.msgs.sensor_msgs import Image +from dimos.robot.foxglove_bridge import FoxgloveBridge +from dimos.utils.fast_image_generator import random_image + + +class EmitterModule(Module): + image: Out[Image] = None + + _thread: threading.Thread | None = None + _stop_event: threading.Event | None = None + + def start(self): + super().start() + self._stop_event = threading.Event() + self._thread = threading.Thread(target=self._publish_image, daemon=True) + self._thread.start() + + def stop(self): + if self._thread: + self._stop_event.set() + self._thread.join(timeout=2) + super().stop() + + def _publish_image(self): + open_file = open("/tmp/emitter-times", "w") + while not self._stop_event.is_set(): + start = time.time() + data = random_image(1280, 720) + total = time.time() - start + print("took", total) + open_file.write(str(time.time()) + "\n") + self.image.publish(Image(data=data)) + open_file.close() + + +class ReceiverModule(Module): + image: In[Image] = None + + _open_file = None + + def start(self): + super().start() + self._disposables.add(Disposable(self.image.subscribe(self._on_image))) + self._open_file = open("/tmp/receiver-times", "w") + + def stop(self): + self._open_file.close() + super().stop() + + def _on_image(self, image: Image): + self._open_file.write(str(time.time()) + "\n") + print("image") + + +def main(): + parser = argparse.ArgumentParser(description="Demo image encoding with transport options") + parser.add_argument( + "--use-jpeg", + action="store_true", + help="Use JPEG LCM transport instead of regular LCM transport", + ) + args = parser.parse_args() + + dimos = ModuleCoordinator(n=2) + dimos.start() + emitter = dimos.deploy(EmitterModule) + receiver = dimos.deploy(ReceiverModule) + + if args.use_jpeg: + emitter.image.transport = JpegLcmTransport("/go2/color_image", Image) + else: + emitter.image.transport = LCMTransport("/go2/color_image", Image) + receiver.image.connect(emitter.image) + + foxglove_bridge = FoxgloveBridge() + foxglove_bridge.start() + + dimos.start_all_modules() + + try: + while True: + time.sleep(0.1) + except KeyboardInterrupt: + pass + finally: + foxglove_bridge.stop() + dimos.close() + + +if __name__ == "__main__": + main() diff --git a/dimos/utils/deprecation.py b/dimos/utils/deprecation.py new file mode 100644 index 0000000000..3c4dd5929e --- /dev/null +++ b/dimos/utils/deprecation.py @@ -0,0 +1,36 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import functools +import warnings + + +def deprecated(reason: str): + """ + This function itself is deprecated as we can use `from warnings import deprecated` in Python 3.13+. + """ + + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + warnings.warn( + f"{func.__name__} is deprecated: {reason}", + category=DeprecationWarning, + stacklevel=2, + ) + return func(*args, **kwargs) + + return wrapper + + return decorator diff --git a/dimos/utils/extract_frames.py b/dimos/utils/extract_frames.py index d5a564930e..d57b0641cd 100644 --- a/dimos/utils/extract_frames.py +++ b/dimos/utils/extract_frames.py @@ -12,12 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import cv2 -import os import argparse from pathlib import Path -def extract_frames(video_path, output_dir, frame_rate): +import cv2 + + +def extract_frames(video_path, output_dir, frame_rate) -> None: """ Extract frames from a video file at a specified frame rate. @@ -40,7 +41,7 @@ def extract_frames(video_path, output_dir, frame_rate): return # Calculate the interval between frames to capture - frame_interval = int(round(original_frame_rate / frame_rate)) + frame_interval = round(original_frame_rate / frame_rate) if frame_interval == 0: frame_interval = 1 @@ -63,11 +64,19 @@ def extract_frames(video_path, output_dir, frame_rate): cap.release() print(f"Extracted {saved_frame_count} frames to {output_dir}") + if __name__ == "__main__": parser = argparse.ArgumentParser(description="Extract frames from a video file.") parser.add_argument("video_path", type=str, help="Path to the input .mov or .mp4 video file.") - parser.add_argument("--output_dir", type=str, default="frames", help="Directory to save extracted frames.") - parser.add_argument("--frame_rate", type=float, default=1.0, help="Frame rate at which to extract frames (frames per second).") + parser.add_argument( + "--output_dir", type=str, default="frames", help="Directory to save extracted frames." + ) + parser.add_argument( + "--frame_rate", + type=float, + default=1.0, + help="Frame rate at which to extract frames (frames per second).", + ) args = parser.parse_args() diff --git a/dimos/utils/fast_image_generator.py b/dimos/utils/fast_image_generator.py new file mode 100644 index 0000000000..f8e02cb71b --- /dev/null +++ b/dimos/utils/fast_image_generator.py @@ -0,0 +1,273 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Fast stateful image generator with visual features for encoding tests.""" + +import numpy as np + + +class FastImageGenerator: + """ + Stateful image generator that creates images with visual features + suitable for testing image/video encoding at 30+ FPS. + + Features generated: + - Moving geometric shapes (tests motion vectors) + - Color gradients (tests gradient compression) + - Sharp edges and corners (tests edge preservation) + - Textured regions (tests detail retention) + - Smooth regions (tests flat area compression) + - High contrast boundaries (tests blocking artifacts) + """ + + def __init__(self, width: int = 1280, height: int = 720): + """Initialize the generator with pre-computed elements.""" + self.width = width + self.height = height + self.frame_count = 0 + + # Pre-allocate the main canvas + self.canvas = np.zeros((height, width, 3), dtype=np.float32) + + # Pre-compute coordinate grids for fast gradient generation + self.x_grid, self.y_grid = np.meshgrid( + np.linspace(0, 1, width, dtype=np.float32), np.linspace(0, 1, height, dtype=np.float32) + ) + + # Pre-compute base gradient patterns + self._init_gradients() + + # Initialize moving objects with their properties + self._init_moving_objects() + + # Pre-compute static texture pattern + self._init_texture() + + # Pre-allocate shape masks for reuse + self._init_shape_masks() + + def _init_gradients(self): + """Pre-compute gradient patterns.""" + # Diagonal gradient + self.diag_gradient = (self.x_grid + self.y_grid) * 0.5 + + # Radial gradient from center + cx, cy = 0.5, 0.5 + self.radial_gradient = np.sqrt((self.x_grid - cx) ** 2 + (self.y_grid - cy) ** 2) + self.radial_gradient = np.clip(1.0 - self.radial_gradient * 1.5, 0, 1) + + # Horizontal and vertical gradients + self.h_gradient = self.x_grid + self.v_gradient = self.y_grid + + def _init_moving_objects(self): + """Initialize properties of moving objects.""" + self.objects = [ + { + "type": "circle", + "x": 0.2, + "y": 0.3, + "vx": 0.002, + "vy": 0.003, + "radius": 60, + "color": np.array([255, 100, 100], dtype=np.float32), + }, + { + "type": "rect", + "x": 0.7, + "y": 0.6, + "vx": -0.003, + "vy": 0.002, + "width": 100, + "height": 80, + "color": np.array([100, 255, 100], dtype=np.float32), + }, + { + "type": "circle", + "x": 0.5, + "y": 0.5, + "vx": 0.004, + "vy": -0.002, + "radius": 40, + "color": np.array([100, 100, 255], dtype=np.float32), + }, + ] + + def _init_texture(self): + """Pre-compute a texture pattern.""" + # Create a simple checkerboard pattern at lower resolution + checker_size = 20 + checker_h = self.height // checker_size + checker_w = self.width // checker_size + + # Create small checkerboard + checker = np.indices((checker_h, checker_w)).sum(axis=0) % 2 + + # Upscale using repeat (fast) + self.texture = np.repeat(np.repeat(checker, checker_size, axis=0), checker_size, axis=1) + self.texture = self.texture[: self.height, : self.width].astype(np.float32) * 30 + + def _init_shape_masks(self): + """Pre-allocate reusable masks for shapes.""" + # Pre-allocate a mask array + self.temp_mask = np.zeros((self.height, self.width), dtype=np.float32) + + # Pre-compute indices for the entire image + self.y_indices, self.x_indices = np.indices((self.height, self.width)) + + def _draw_circle_fast(self, cx: int, cy: int, radius: int, color: np.ndarray): + """Draw a circle using vectorized operations - optimized version without anti-aliasing.""" + # Compute bounding box to minimize calculations + y1 = max(0, cy - radius - 1) + y2 = min(self.height, cy + radius + 2) + x1 = max(0, cx - radius - 1) + x2 = min(self.width, cx + radius + 2) + + # Work only on the bounding box region + if y1 < y2 and x1 < x2: + y_local, x_local = np.ogrid[y1:y2, x1:x2] + dist_sq = (x_local - cx) ** 2 + (y_local - cy) ** 2 + mask = dist_sq <= radius**2 + self.canvas[y1:y2, x1:x2][mask] = color + + def _draw_rect_fast(self, x: int, y: int, w: int, h: int, color: np.ndarray): + """Draw a rectangle using slicing.""" + # Clip to canvas boundaries + x1 = max(0, x) + y1 = max(0, y) + x2 = min(self.width, x + w) + y2 = min(self.height, y + h) + + if x1 < x2 and y1 < y2: + self.canvas[y1:y2, x1:x2] = color + + def _update_objects(self): + """Update positions of moving objects.""" + for obj in self.objects: + # Update position + obj["x"] += obj["vx"] + obj["y"] += obj["vy"] + + # Bounce off edges + if obj["type"] == "circle": + r = obj["radius"] / self.width + if obj["x"] - r <= 0 or obj["x"] + r >= 1: + obj["vx"] *= -1 + obj["x"] = np.clip(obj["x"], r, 1 - r) + + r = obj["radius"] / self.height + if obj["y"] - r <= 0 or obj["y"] + r >= 1: + obj["vy"] *= -1 + obj["y"] = np.clip(obj["y"], r, 1 - r) + + elif obj["type"] == "rect": + w = obj["width"] / self.width + h = obj["height"] / self.height + if obj["x"] <= 0 or obj["x"] + w >= 1: + obj["vx"] *= -1 + obj["x"] = np.clip(obj["x"], 0, 1 - w) + + if obj["y"] <= 0 or obj["y"] + h >= 1: + obj["vy"] *= -1 + obj["y"] = np.clip(obj["y"], 0, 1 - h) + + def generate_frame(self) -> np.ndarray: + """ + Generate a single frame with visual features - optimized for 30+ FPS. + + Returns: + numpy array of shape (height, width, 3) with uint8 values + """ + # Fast gradient background - use only one gradient per frame + if self.frame_count % 2 == 0: + base_gradient = self.h_gradient + else: + base_gradient = self.v_gradient + + # Simple color mapping + self.canvas[:, :, 0] = base_gradient * 150 + 50 + self.canvas[:, :, 1] = base_gradient * 120 + 70 + self.canvas[:, :, 2] = (1 - base_gradient) * 140 + 60 + + # Add texture in corner - simplified without per-channel scaling + tex_size = self.height // 3 + self.canvas[:tex_size, :tex_size] += self.texture[:tex_size, :tex_size, np.newaxis] + + # Add test pattern bars - vectorized + bar_width = 50 + bar_start = self.width // 3 + for i in range(3): # Reduced from 5 to 3 bars + x1 = bar_start + i * bar_width * 2 + x2 = min(x1 + bar_width, self.width) + if x1 < self.width: + color_val = 180 + i * 30 + self.canvas[self.height // 2 :, x1:x2] = color_val + + # Update and draw only 2 moving objects (reduced from 3) + self._update_objects() + + # Draw only first 2 objects for speed + for obj in self.objects[:2]: + if obj["type"] == "circle": + cx = int(obj["x"] * self.width) + cy = int(obj["y"] * self.height) + self._draw_circle_fast(cx, cy, obj["radius"], obj["color"]) + elif obj["type"] == "rect": + x = int(obj["x"] * self.width) + y = int(obj["y"] * self.height) + self._draw_rect_fast(x, y, obj["width"], obj["height"], obj["color"]) + + # Simple horizontal lines pattern (faster than sine wave) + line_y = int(self.height * 0.8) + line_spacing = 10 + for i in range(0, 5): + y = line_y + i * line_spacing + if y < self.height: + self.canvas[y : y + 2, :] = [255, 200, 100] + + # Increment frame counter + self.frame_count += 1 + + # Direct conversion to uint8 (already in valid range) + return self.canvas.astype(np.uint8) + + def reset(self): + """Reset the generator to initial state.""" + self.frame_count = 0 + self._init_moving_objects() + + +# Convenience function for backward compatibility +_generator = None + + +def random_image(width: int, height: int) -> np.ndarray: + """ + Generate an image with visual features suitable for encoding tests. + Maintains state for efficient stream generation. + + Args: + width: Image width in pixels + height: Image height in pixels + + Returns: + numpy array of shape (height, width, 3) with uint8 values + """ + global _generator + + # Initialize or reinitialize if dimensions changed + if _generator is None or _generator.width != width or _generator.height != height: + _generator = FastImageGenerator(width, height) + + return _generator.generate_frame() diff --git a/dimos/utils/generic.py b/dimos/utils/generic.py new file mode 100644 index 0000000000..adbb18988f --- /dev/null +++ b/dimos/utils/generic.py @@ -0,0 +1,78 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import hashlib +import json +import os +import string +from typing import Any +import uuid + + +def truncate_display_string(arg: Any, max: int | None = None) -> str: + """ + If we print strings that are too long that potentially obscures more important logs. + + Use this function to truncate it to a reasonable length (configurable from the env). + """ + string = str(arg) + + if max is not None: + max_chars = max + else: + max_chars = int(os.getenv("TRUNCATE_MAX", "2000")) + + if max_chars == 0 or len(string) <= max_chars: + return string + + return string[:max_chars] + "...(truncated)..." + + +def extract_json_from_llm_response(response: str) -> Any: + start_idx = response.find("{") + end_idx = response.rfind("}") + 1 + + if start_idx >= 0 and end_idx > start_idx: + json_str = response[start_idx:end_idx] + try: + return json.loads(json_str) + except Exception: + pass + + return None + + +def short_id(from_string: str | None = None) -> str: + alphabet = string.digits + string.ascii_letters + base = len(alphabet) + + if from_string is None: + num = uuid.uuid4().int + else: + hash_bytes = hashlib.sha1(from_string.encode()).digest()[:16] + num = int.from_bytes(hash_bytes, "big") + + min_chars = 18 + + chars: list[str] = [] + while num > 0 or len(chars) < min_chars: + num, rem = divmod(num, base) + chars.append(alphabet[rem]) + + return "".join(reversed(chars))[:min_chars] + + +class classproperty(property): + def __get__(self, obj, cls): + return self.fget(cls) diff --git a/dimos/utils/generic_subscriber.py b/dimos/utils/generic_subscriber.py index fd0cb5841f..5f687c494a 100644 --- a/dimos/utils/generic_subscriber.py +++ b/dimos/utils/generic_subscriber.py @@ -1,10 +1,27 @@ #!/usr/bin/env python3 -import threading +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import logging -from typing import Optional, Any +import threading +from typing import TYPE_CHECKING, Any + from reactivex import Observable -from reactivex.disposable import Disposable + +if TYPE_CHECKING: + from reactivex.disposable import Disposable logger = logging.getLogger(__name__) @@ -12,51 +29,49 @@ class GenericSubscriber: """Subscribes to an RxPy Observable stream and stores the latest message.""" - def __init__(self, stream: Observable): + def __init__(self, stream: Observable) -> None: """Initialize the subscriber and subscribe to the stream. Args: stream: The RxPy Observable stream to subscribe to. """ - self.latest_message: Optional[Any] = None + self.latest_message: Any | None = None self._lock = threading.Lock() - self._subscription: Optional[Disposable] = None + self._subscription: Disposable | None = None self._stream_completed = threading.Event() - self._stream_error: Optional[Exception] = None + self._stream_error: Exception | None = None if stream is not None: try: self._subscription = stream.subscribe( - on_next=self._on_next, - on_error=self._on_error, - on_completed=self._on_completed + on_next=self._on_next, on_error=self._on_error, on_completed=self._on_completed ) logger.debug(f"Subscribed to stream {stream}") except Exception as e: logger.error(f"Error subscribing to stream {stream}: {e}") - self._stream_error = e # Store error if subscription fails immediately + self._stream_error = e # Store error if subscription fails immediately else: logger.warning("Initialized GenericSubscriber with a None stream.") - def _on_next(self, message: Any): + def _on_next(self, message: Any) -> None: """Callback for receiving a new message.""" with self._lock: self.latest_message = message # logger.debug("Received new message") # Can be noisy - def _on_error(self, error: Exception): + def _on_error(self, error: Exception) -> None: """Callback for stream error.""" logger.error(f"Stream error: {error}") with self._lock: self._stream_error = error - self._stream_completed.set() # Signal completion/error + self._stream_completed.set() # Signal completion/error - def _on_completed(self): + def _on_completed(self) -> None: """Callback for stream completion.""" logger.info("Stream completed.") self._stream_completed.set() - def get_data(self) -> Optional[Any]: + def get_data(self) -> Any | None: """Get the latest message received from the stream. Returns: @@ -77,7 +92,7 @@ def is_completed(self) -> bool: """Check if the stream has completed or encountered an error.""" return self._stream_completed.is_set() - def dispose(self): + def dispose(self) -> None: """Dispose of the subscription to stop receiving messages.""" if self._subscription is not None: try: @@ -86,8 +101,8 @@ def dispose(self): self._subscription = None except Exception as e: logger.error(f"Error disposing subscription: {e}") - self._stream_completed.set() # Ensure completed flag is set on manual dispose + self._stream_completed.set() # Ensure completed flag is set on manual dispose - def __del__(self): + def __del__(self) -> None: """Ensure cleanup on object deletion.""" self.dispose() diff --git a/dimos/utils/gpu_utils.py b/dimos/utils/gpu_utils.py new file mode 100644 index 0000000000..e0a1a23734 --- /dev/null +++ b/dimos/utils/gpu_utils.py @@ -0,0 +1,23 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def is_cuda_available(): + try: + import pycuda.driver as cuda + + cuda.init() + return cuda.Device.count() > 0 + except Exception: + return False diff --git a/dimos/utils/llm_utils.py b/dimos/utils/llm_utils.py new file mode 100644 index 0000000000..124169e794 --- /dev/null +++ b/dimos/utils/llm_utils.py @@ -0,0 +1,74 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import re + + +def extract_json(response: str) -> dict | list: + """Extract JSON from potentially messy LLM response. + + Tries multiple strategies: + 1. Parse the entire response as JSON + 2. Find and parse JSON arrays in the response + 3. Find and parse JSON objects in the response + + Args: + response: Raw text response that may contain JSON + + Returns: + Parsed JSON object (dict or list) + + Raises: + json.JSONDecodeError: If no valid JSON can be extracted + """ + # First try to parse the whole response as JSON + try: + return json.loads(response) + except json.JSONDecodeError: + pass + + # If that fails, try to extract JSON from the messy response + # Look for JSON arrays or objects in the text + + # Pattern to match JSON arrays (including nested arrays/objects) + # This finds the outermost [...] structure + array_pattern = r"\[(?:[^\[\]]*|\[(?:[^\[\]]*|\[[^\[\]]*\])*\])*\]" + + # Pattern to match JSON objects + object_pattern = r"\{(?:[^{}]*|\{(?:[^{}]*|\{[^{}]*\})*\})*\}" + + # Try to find JSON arrays first (most common for detections) + matches = re.findall(array_pattern, response, re.DOTALL) + for match in matches: + try: + parsed = json.loads(match) + # For detection arrays, we expect a list + if isinstance(parsed, list): + return parsed + except json.JSONDecodeError: + continue + + # Try JSON objects if no arrays found + matches = re.findall(object_pattern, response, re.DOTALL) + for match in matches: + try: + return json.loads(match) + except json.JSONDecodeError: + continue + + # If nothing worked, raise an error with the original response + raise json.JSONDecodeError( + f"Could not extract valid JSON from response: {response[:200]}...", response, 0 + ) diff --git a/dimos/utils/logging_config.py b/dimos/utils/logging_config.py index 66877658ef..d0a347f2cd 100644 --- a/dimos/utils/logging_config.py +++ b/dimos/utils/logging_config.py @@ -17,15 +17,17 @@ This module sets up a logger with color output for different log levels. """ -import os import logging +import os + import colorlog -from typing import Optional logging.basicConfig(format="%(name)s - %(levelname)s - %(message)s") -def setup_logger(name: str, level: Optional[int] = None, log_format: Optional[str] = None) -> logging.Logger: +def setup_logger( + name: str, level: int | None = None, log_format: str | None = None +) -> logging.Logger: """Set up a logger with color output. Args: diff --git a/dimos/utils/monitoring.py b/dimos/utils/monitoring.py new file mode 100644 index 0000000000..17415781b5 --- /dev/null +++ b/dimos/utils/monitoring.py @@ -0,0 +1,307 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Note, to enable ps-spy to run without sudo you need: + + echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope +""" + +from functools import cache +import os +import re +import shutil +import subprocess +import threading + +from distributed import get_client +from distributed.client import Client + +from dimos.core import Module, rpc +from dimos.utils.actor_registry import ActorRegistry +from dimos.utils.logging_config import setup_logger + +logger = setup_logger(__file__) + + +def print_data_table(data) -> None: + headers = [ + "cpu_percent", + "active_percent", + "gil_percent", + "n_threads", + "pid", + "worker_id", + "modules", + ] + numeric_headers = {"cpu_percent", "active_percent", "gil_percent", "n_threads", "pid"} + + # Add registered modules. + modules = ActorRegistry.get_all() + for worker in data: + worker["modules"] = ", ".join( + module_name.split("-", 1)[0] + for module_name, worker_id_str in modules.items() + if worker_id_str == str(worker["worker_id"]) + ) + + # Determine column widths + col_widths = [] + for h in headers: + max_len = max(len(str(d[h])) for d in data) + col_widths.append(max(len(h), max_len)) + + # Print header with DOS box characters + header_row = " │ ".join(h.ljust(col_widths[i]) for i, h in enumerate(headers)) + border_parts = ["─" * w for w in col_widths] + border_line = "─┼─".join(border_parts) + print(border_line) + print(header_row) + print(border_line) + + # Print rows + for row in data: + formatted_cells = [] + for i, h in enumerate(headers): + value = str(row[h]) + if h in numeric_headers: + formatted_cells.append(value.rjust(col_widths[i])) + else: + formatted_cells.append(value.ljust(col_widths[i])) + print(" │ ".join(formatted_cells)) + + +class UtilizationThread(threading.Thread): + _module: "UtilizationModule" + _stop_event: threading.Event + _monitors: dict + + def __init__(self, module) -> None: + super().__init__(daemon=True) + self._module = module + self._stop_event = threading.Event() + self._monitors = {} + + def run(self) -> None: + while not self._stop_event.is_set(): + workers = self._module.client.scheduler_info()["workers"] + pids = {pid: None for pid in get_worker_pids()} + for worker, info in workers.items(): + pid = get_pid_by_port(worker.rsplit(":", 1)[-1]) + if pid is None: + continue + pids[pid] = info["id"] + data = [] + for pid, worker_id in pids.items(): + if pid not in self._monitors: + self._monitors[pid] = GilMonitorThread(pid) + self._monitors[pid].start() + cpu, gil, active, n_threads = self._monitors[pid].get_values() + data.append( + { + "cpu_percent": cpu, + "worker_id": worker_id, + "pid": pid, + "gil_percent": gil, + "active_percent": active, + "n_threads": n_threads, + } + ) + data.sort(key=lambda x: x["pid"]) + self._fix_missing_ids(data) + print_data_table(data) + self._stop_event.wait(1) + + def stop(self) -> None: + self._stop_event.set() + for monitor in self._monitors.values(): + monitor.stop() + monitor.join(timeout=2) + + def _fix_missing_ids(self, data) -> None: + """ + Some worker IDs are None. But if we order the workers by PID and all + non-None ids are in order, then we can deduce that the None ones are the + missing indices. + """ + if all(x["worker_id"] in (i, None) for i, x in enumerate(data)): + for i, worker in enumerate(data): + worker["worker_id"] = i + + +class UtilizationModule(Module): + client: Client | None + _utilization_thread: UtilizationThread | None + + def __init__(self) -> None: + super().__init__() + self.client = None + self._utilization_thread = None + + if not os.getenv("MEASURE_GIL_UTILIZATION"): + logger.info("Set `MEASURE_GIL_UTILIZATION=true` to print GIL utilization.") + return + + if not _can_use_py_spy(): + logger.warning( + "Cannot start UtilizationModule because in order to run py-spy without " + "being root you need to enable this:\n" + "\n" + " echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope" + ) + return + + if not shutil.which("py-spy"): + logger.warning("Cannot start UtilizationModule because `py-spy` is not installed.") + return + + self.client = get_client() + self._utilization_thread = UtilizationThread(self) + + @rpc + def start(self) -> None: + super().start() + + if self._utilization_thread: + self._utilization_thread.start() + + @rpc + def stop(self) -> None: + if self._utilization_thread: + self._utilization_thread.stop() + self._utilization_thread.join(timeout=2) + super().stop() + + +utilization = UtilizationModule.blueprint + + +__all__ = ["UtilizationModule", "utilization"] + + +def _can_use_py_spy(): + try: + with open("/proc/sys/kernel/yama/ptrace_scope") as f: + value = f.read().strip() + return value == "0" + except Exception: + pass + return False + + +@cache +def get_pid_by_port(port: int) -> int | None: + try: + result = subprocess.run( + ["lsof", "-ti", f":{port}"], capture_output=True, text=True, check=True + ) + pid_str = result.stdout.strip() + return int(pid_str) if pid_str else None + except subprocess.CalledProcessError: + return None + + +def get_worker_pids(): + pids = [] + for pid in os.listdir("/proc"): + if not pid.isdigit(): + continue + try: + with open(f"/proc/{pid}/cmdline") as f: + cmdline = f.read().replace("\x00", " ") + if "spawn_main" in cmdline: + pids.append(int(pid)) + except (FileNotFoundError, PermissionError): + continue + return pids + + +class GilMonitorThread(threading.Thread): + pid: int + _latest_values: tuple[float, float, float, int] + _stop_event: threading.Event + _lock: threading.Lock + + def __init__(self, pid: int) -> None: + super().__init__(daemon=True) + self.pid = pid + self._latest_values = (-1.0, -1.0, -1.0, -1) + self._stop_event = threading.Event() + self._lock = threading.Lock() + + def run(self): + command = ["py-spy", "top", "--pid", str(self.pid), "--rate", "100"] + process = None + try: + process = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1, # Line-buffered output + ) + + for line in iter(process.stdout.readline, ""): + if self._stop_event.is_set(): + break + + if "GIL:" not in line: + continue + + match = re.search( + r"GIL:\s*([\d.]+?)%,\s*Active:\s*([\d.]+?)%,\s*Threads:\s*(\d+)", line + ) + if not match: + continue + + try: + cpu_percent = _get_cpu_percent(self.pid) + gil_percent = float(match.group(1)) + active_percent = float(match.group(2)) + num_threads = int(match.group(3)) + + with self._lock: + self._latest_values = ( + cpu_percent, + gil_percent, + active_percent, + num_threads, + ) + except (ValueError, IndexError): + pass + except Exception as e: + logger.error(f"An error occurred in GilMonitorThread for PID {self.pid}: {e}") + raise + finally: + if process: + process.terminate() + process.wait(timeout=1) + self._stop_event.set() + + def get_values(self): + with self._lock: + return self._latest_values + + def stop(self) -> None: + self._stop_event.set() + + +def _get_cpu_percent(pid: int) -> float: + try: + result = subprocess.run( + ["ps", "-p", str(pid), "-o", "%cpu="], capture_output=True, text=True, check=True + ) + return float(result.stdout.strip()) + except Exception: + return -1.0 diff --git a/dimos/utils/path_utils.py b/dimos/utils/path_utils.py new file mode 100644 index 0000000000..d60014d068 --- /dev/null +++ b/dimos/utils/path_utils.py @@ -0,0 +1,22 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + + +def get_project_root() -> Path: + """ + Returns the absolute path to the project root directory. + """ + return Path(__file__).resolve().parent.parent.parent diff --git a/dimos/utils/reactive.py b/dimos/utils/reactive.py index 0a609dd23e..f7885d3129 100644 --- a/dimos/utils/reactive.py +++ b/dimos/utils/reactive.py @@ -1,11 +1,26 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Callable import threading -from typing import Optional, TypeVar, Generic, Any, Callable +from typing import Any, Generic, TypeVar import reactivex as rx from reactivex import operators as ops -from reactivex.scheduler import ThreadPoolScheduler from reactivex.disposable import Disposable from reactivex.observable import Observable +from reactivex.scheduler import ThreadPoolScheduler from rxpy_backpressure import BackPressure from dimos.utils.threadpool import get_scheduler @@ -18,7 +33,7 @@ # └──► observe_on(pool) ─► backpressure.latest ─► sub3 (slower) def backpressure( observable: Observable[T], - scheduler: Optional[ThreadPoolScheduler] = None, + scheduler: ThreadPoolScheduler | None = None, drop_unprocessed: bool = True, ) -> Observable[T]: if scheduler is None: @@ -51,7 +66,7 @@ def _subscribe(observer, sch=None): class LatestReader(Generic[T]): """A callable object that returns the latest value from an observable.""" - def __init__(self, initial_value: T, subscription, connection=None): + def __init__(self, initial_value: T, subscription, connection=None) -> None: self._value = initial_value self._subscription = subscription self._connection = connection @@ -67,14 +82,44 @@ def dispose(self) -> None: self._connection.dispose() -def getter_ondemand(observable: Observable[T], timeout: Optional[float] = 30.0) -> T: +def getter_ondemand(observable: Observable[T], timeout: float | None = 30.0) -> T: def getter(): + result = [] + error = [] + event = threading.Event() + + def on_next(value) -> None: + result.append(value) + event.set() + + def on_error(e) -> None: + error.append(e) + event.set() + + def on_completed() -> None: + event.set() + + # Subscribe and wait for first value + subscription = observable.pipe(ops.first()).subscribe( + on_next=on_next, on_error=on_error, on_completed=on_completed + ) + try: - # Wait for first value with optional timeout - value = observable.pipe(ops.first(), *([ops.timeout(timeout)] if timeout is not None else [])).run() - return value - except Exception as e: - raise Exception(f"No value received after {timeout} seconds") from e + if timeout is not None: + if not event.wait(timeout): + raise TimeoutError(f"No value received after {timeout} seconds") + else: + event.wait() + + if error: + raise error[0] + + if not result: + raise Exception("Observable completed without emitting a value") + + return result[0] + finally: + subscription.dispose() return getter @@ -84,7 +129,7 @@ def getter(): def getter_streaming( source: Observable[T], - timeout: Optional[float] = 30.0, + timeout: float | None = 30.0, *, nonblocking: bool = False, ) -> LatestReader[T]: @@ -138,10 +183,48 @@ def callback_to_observable( stop: Callable[[CB[T]], Any], ) -> Observable[T]: def _subscribe(observer, _scheduler=None): - def _on_msg(value: T): + def _on_msg(value: T) -> None: observer.on_next(value) start(_on_msg) return Disposable(lambda: stop(_on_msg)) return rx.create(_subscribe) + + +def spy(name: str): + def spyfun(x): + print(f"SPY {name}:", x) + return x + + return ops.map(spyfun) + + +def quality_barrier(quality_func: Callable[[T], float], target_frequency: float): + """ + RxPY pipe operator that selects the highest quality item within each time window. + + Args: + quality_func: Function to compute quality score for each item + target_frequency: Output frequency in Hz (e.g., 1.0 for 1 item per second) + + Returns: + A pipe operator that can be used with .pipe() + """ + window_duration = 1.0 / target_frequency # Duration of each window in seconds + + def _quality_barrier(source: Observable[T]) -> Observable[T]: + return source.pipe( + # Create non-overlapping time-based windows + ops.window_with_time(window_duration, window_duration), + # For each window, find the highest quality item + ops.flat_map( + lambda window: window.pipe( + ops.to_list(), + ops.map(lambda items: max(items, key=quality_func) if items else None), + ops.filter(lambda x: x is not None), + ) + ), + ) + + return _quality_barrier diff --git a/dimos/utils/ros_utils.py b/dimos/utils/ros_utils.py deleted file mode 100644 index 9423dfe6a1..0000000000 --- a/dimos/utils/ros_utils.py +++ /dev/null @@ -1,16 +0,0 @@ -import math -import numpy as np -from typing import Tuple -from scipy.spatial.transform import Rotation as R -import logging - -logger = logging.getLogger(__name__) - -def normalize_angle(angle: float) -> float: - """Normalize angle to [-pi, pi] range""" - return np.arctan2(np.sin(angle), np.cos(angle)) - -def distance_angle_to_goal_xy(distance: float, angle: float) -> Tuple[float, float]: - """Convert distance and angle to goal x, y in robot frame""" - return distance * np.cos(angle), distance * np.sin(angle) - diff --git a/dimos/utils/s3_utils.py b/dimos/utils/s3_utils.py index efca0eb2fb..f4c3227a71 100644 --- a/dimos/utils/s3_utils.py +++ b/dimos/utils/s3_utils.py @@ -12,39 +12,40 @@ # See the License for the specific language governing permissions and # limitations under the License. -import boto3 import os -from io import BytesIO + +import boto3 + try: import open3d as o3d except Exception as e: print(f"Open3D not importing, assuming to be running outside of docker. {e}") + class S3Utils: - def __init__(self, bucket_name): - self.s3 = boto3.client('s3') + def __init__(self, bucket_name: str) -> None: + self.s3 = boto3.client("s3") self.bucket_name = bucket_name - def download_file(self, s3_key, local_path): + def download_file(self, s3_key, local_path) -> None: try: self.s3.download_file(self.bucket_name, s3_key, local_path) print(f"Downloaded {s3_key} to {local_path}") except Exception as e: print(f"Error downloading {s3_key}: {e}") - def upload_file(self, local_path, s3_key): + def upload_file(self, local_path, s3_key) -> None: try: self.s3.upload_file(local_path, self.bucket_name, s3_key) print(f"Uploaded {local_path} to {s3_key}") except Exception as e: print(f"Error uploading {local_path}: {e}") - def save_pointcloud_to_s3(self, inlier_cloud, s3_key): - + def save_pointcloud_to_s3(self, inlier_cloud, s3_key) -> None: try: temp_pcd_file = "/tmp/temp_pointcloud.pcd" o3d.io.write_point_cloud(temp_pcd_file, inlier_cloud) - with open(temp_pcd_file, 'rb') as pcd_file: + with open(temp_pcd_file, "rb") as pcd_file: self.s3.put_object(Bucket=self.bucket_name, Key=s3_key, Body=pcd_file.read()) os.remove(temp_pcd_file) print(f"Saved pointcloud to {s3_key}") @@ -57,11 +58,11 @@ def restore_pointcloud_from_s3(self, pointcloud_paths): for path in pointcloud_paths: # Download the point cloud file from S3 to memory pcd_obj = self.s3.get_object(Bucket=self.bucket_name, Key=path) - pcd_data = pcd_obj['Body'].read() + pcd_data = pcd_obj["Body"].read() # Save the point cloud data to a temporary file temp_pcd_file = "/tmp/temp_pointcloud.pcd" - with open(temp_pcd_file, 'wb') as f: + with open(temp_pcd_file, "wb") as f: f.write(pcd_data) # Read the point cloud from the temporary file @@ -72,22 +73,23 @@ def restore_pointcloud_from_s3(self, pointcloud_paths): os.remove(temp_pcd_file) return restored_pointclouds + @staticmethod - def upload_text_file(bucket_name, local_path, s3_key): - s3 = boto3.client('s3') + def upload_text_file(bucket_name: str, local_path, s3_key) -> None: + s3 = boto3.client("s3") try: - with open(local_path, 'r') as file: + with open(local_path) as file: content = file.read() # Ensure the s3_key includes the file name - if not s3_key.endswith('/'): - s3_key = s3_key + '/' + if not s3_key.endswith("/"): + s3_key = s3_key + "/" # Extract the file name from the local_path - file_name = local_path.split('/')[-1] + file_name = local_path.split("/")[-1] full_s3_key = s3_key + file_name s3.put_object(Bucket=bucket_name, Key=full_s3_key, Body=content) print(f"Uploaded text file {local_path} to {full_s3_key}") except Exception as e: - print(f"Error uploading text file {local_path}: {e}") \ No newline at end of file + print(f"Error uploading text file {local_path}: {e}") diff --git a/dimos/utils/simple_controller.py b/dimos/utils/simple_controller.py index f2b2d8ba03..dd92ae0c55 100644 --- a/dimos/utils/simple_controller.py +++ b/dimos/utils/simple_controller.py @@ -1,17 +1,43 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import math -def normalize_angle(angle): + +def normalize_angle(angle: float): """Normalize angle to the range [-pi, pi].""" return math.atan2(math.sin(angle), math.cos(angle)) + # ---------------------------- # PID Controller Class # ---------------------------- class PIDController: - def __init__(self, kp, ki=0.0, kd=0.0, output_limits=(None, None), integral_limit=None, deadband=0.0, output_deadband=0.0, inverse_output=False): + def __init__( + self, + kp, + ki: float = 0.0, + kd: float = 0.0, + output_limits=(None, None), + integral_limit=None, + deadband: float = 0.0, + output_deadband: float = 0.0, + inverse_output: bool = False, + ) -> None: """ Initialize the PID controller. - + Args: kp (float): Proportional gain. ki (float): Integral gain. @@ -39,7 +65,7 @@ def update(self, error, dt): self.integral += error * dt if self.integral_limit is not None: self.integral = max(-self.integral_limit, min(self.integral, self.integral_limit)) - + # Compute derivative term. derivative = (error - self.prev_error) / dt if dt > 0 else 0.0 @@ -47,34 +73,34 @@ def update(self, error, dt): # Prevent integral windup by not increasing integral term when error is small. self.integral = 0.0 derivative = 0.0 - + # Compute raw output. output = self.kp * error + self.ki * self.integral + self.kd * derivative - + # Apply deadband compensation to the output output = self._apply_output_deadband_compensation(output) - + # Apply output limits if specified. if self.max_output is not None: output = min(self.max_output, output) if self.min_output is not None: output = max(self.min_output, output) - + self.prev_error = error if self.inverse_output: return -output return output - + def _apply_output_deadband_compensation(self, output): """ Apply deadband compensation to the output. - + This simply adds the deadband value to the magnitude of the output while preserving the sign, ensuring we overcome the physical deadband. """ if self.output_deadband == 0.0 or output == 0.0: return output - + if output > self.max_output * 0.05: # For positive output, add the deadband return output + self.output_deadband @@ -83,24 +109,25 @@ def _apply_output_deadband_compensation(self, output): return output - self.output_deadband else: return output - + def _apply_deadband_compensation(self, error): """ Apply deadband compensation to the error. - + This maintains the original error value, as the deadband compensation will be applied to the output, not the error. """ return error + # ---------------------------- # Visual Servoing Controller Class # ---------------------------- class VisualServoingController: - def __init__(self, distance_pid_params, angle_pid_params): + def __init__(self, distance_pid_params, angle_pid_params) -> None: """ Initialize the visual servoing controller using enhanced PID controllers. - + Args: distance_pid_params (tuple): (kp, ki, kd, output_limits, integral_limit, deadband) for distance. angle_pid_params (tuple): (kp, ki, kd, output_limits, integral_limit, deadband) for angle. @@ -109,30 +136,32 @@ def __init__(self, distance_pid_params, angle_pid_params): self.angle_pid = PIDController(*angle_pid_params) self.prev_measured_angle = 0.0 # Used for angular feed-forward damping - def compute_control(self, measured_distance, measured_angle, desired_distance, desired_angle, dt): + def compute_control( + self, measured_distance, measured_angle, desired_distance, desired_angle, dt + ): """ Compute the forward (x) and angular (z) commands. - + Args: measured_distance (float): Current distance to target (from camera). measured_angle (float): Current angular offset to target (radians). desired_distance (float): Desired distance to target. desired_angle (float): Desired angular offset (e.g., 0 for centered). dt (float): Timestep. - + Returns: tuple: (forward_command, angular_command) """ # Compute the errors. error_distance = measured_distance - desired_distance error_angle = normalize_angle(measured_angle - desired_angle) - + # Get raw PID outputs. forward_command_raw = self.distance_pid.update(error_distance, dt) angular_command_raw = self.angle_pid.update(error_angle, dt) - #print("forward: {} angular: {}".format(forward_command_raw, angular_command_raw)) - + # print("forward: {} angular: {}".format(forward_command_raw, angular_command_raw)) + angular_command = angular_command_raw # Couple forward command to angular error: @@ -140,4 +169,4 @@ def compute_control(self, measured_distance, measured_angle, desired_distance, d scaling_factor = max(0.0, min(1.0, math.exp(-2.0 * abs(error_angle)))) forward_command = forward_command_raw * scaling_factor - return forward_command, angular_command \ No newline at end of file + return forward_command, angular_command diff --git a/dimos/utils/test_data.py b/dimos/utils/test_data.py new file mode 100644 index 0000000000..b6df8e1a12 --- /dev/null +++ b/dimos/utils/test_data.py @@ -0,0 +1,130 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import hashlib +import os +import subprocess + +import pytest + +from dimos.utils import data + + +@pytest.mark.heavy +def test_pull_file() -> None: + repo_root = data._get_repo_root() + test_file_name = "cafe.jpg" + test_file_compressed = data._get_lfs_dir() / (test_file_name + ".tar.gz") + test_file_decompressed = data._get_data_dir() / test_file_name + + # delete decompressed test file if it exists + if test_file_decompressed.exists(): + test_file_decompressed.unlink() + + # delete lfs archive file if it exists + if test_file_compressed.exists(): + test_file_compressed.unlink() + + assert not test_file_compressed.exists() + assert not test_file_decompressed.exists() + + # pull the lfs file reference from git + env = os.environ.copy() + env["GIT_LFS_SKIP_SMUDGE"] = "1" + subprocess.run( + ["git", "checkout", "HEAD", "--", test_file_compressed], + cwd=repo_root, + env=env, + check=True, + capture_output=True, + ) + + # ensure we have a pointer file from git (small ASCII text file) + assert test_file_compressed.exists() + assert test_file_compressed.stat().st_size < 200 + + # trigger a data file pull + assert data.get_data(test_file_name) == test_file_decompressed + + # validate data is received + assert test_file_compressed.exists() + assert test_file_decompressed.exists() + + # validate hashes + with test_file_compressed.open("rb") as f: + assert test_file_compressed.stat().st_size > 200 + compressed_sha256 = hashlib.sha256(f.read()).hexdigest() + assert ( + compressed_sha256 == "b8cf30439b41033ccb04b09b9fc8388d18fb544d55b85c155dbf85700b9e7603" + ) + + with test_file_decompressed.open("rb") as f: + decompressed_sha256 = hashlib.sha256(f.read()).hexdigest() + assert ( + decompressed_sha256 + == "55d451dde49b05e3ad386fdd4ae9e9378884b8905bff1ca8aaea7d039ff42ddd" + ) + + +@pytest.mark.heavy +def test_pull_dir() -> None: + repo_root = data._get_repo_root() + test_dir_name = "ab_lidar_frames" + test_dir_compressed = data._get_lfs_dir() / (test_dir_name + ".tar.gz") + test_dir_decompressed = data._get_data_dir() / test_dir_name + + # delete decompressed test directory if it exists + if test_dir_decompressed.exists(): + for item in test_dir_decompressed.iterdir(): + item.unlink() + test_dir_decompressed.rmdir() + + # delete lfs archive file if it exists + if test_dir_compressed.exists(): + test_dir_compressed.unlink() + + # pull the lfs file reference from git + env = os.environ.copy() + env["GIT_LFS_SKIP_SMUDGE"] = "1" + subprocess.run( + ["git", "checkout", "HEAD", "--", test_dir_compressed], + cwd=repo_root, + env=env, + check=True, + capture_output=True, + ) + + # ensure we have a pointer file from git (small ASCII text file) + assert test_dir_compressed.exists() + assert test_dir_compressed.stat().st_size < 200 + + # trigger a data file pull + assert data.get_data(test_dir_name) == test_dir_decompressed + assert test_dir_compressed.stat().st_size > 200 + + # validate data is received + assert test_dir_compressed.exists() + assert test_dir_decompressed.exists() + + for [file, expected_hash] in zip( + sorted(test_dir_decompressed.iterdir()), + [ + "6c3aaa9a79853ea4a7453c7db22820980ceb55035777f7460d05a0fa77b3b1b3", + "456cc2c23f4ffa713b4e0c0d97143c27e48bbe6ef44341197b31ce84b3650e74", + ], + strict=False, + ): + with file.open("rb") as f: + sha256 = hashlib.sha256(f.read()).hexdigest() + assert sha256 == expected_hash diff --git a/dimos/utils/test_foxglove_bridge.py b/dimos/utils/test_foxglove_bridge.py new file mode 100644 index 0000000000..ad597c8720 --- /dev/null +++ b/dimos/utils/test_foxglove_bridge.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Test for foxglove bridge import and basic functionality +""" + +import warnings + +import pytest + +warnings.filterwarnings("ignore", category=DeprecationWarning, module="websockets.server") +warnings.filterwarnings("ignore", category=DeprecationWarning, module="websockets.legacy") + + +def test_foxglove_bridge_import() -> None: + """Test that the foxglove bridge can be imported successfully.""" + try: + from dimos_lcm.foxglove_bridge import FoxgloveBridge + except ImportError as e: + pytest.fail(f"Failed to import foxglove bridge: {e}") + + +def test_foxglove_bridge_runner_init() -> None: + """Test that LcmFoxgloveBridge can be initialized with default parameters.""" + try: + from dimos_lcm.foxglove_bridge import FoxgloveBridge + + runner = FoxgloveBridge(host="localhost", port=8765, debug=False, num_threads=2) + + # Check that the runner was created successfully + assert runner is not None + + except Exception as e: + pytest.fail(f"Failed to initialize LcmFoxgloveBridge: {e}") + + +def test_foxglove_bridge_runner_params() -> None: + """Test that LcmFoxgloveBridge accepts various parameter configurations.""" + try: + from dimos_lcm.foxglove_bridge import FoxgloveBridge + + configs = [ + {"host": "0.0.0.0", "port": 8765, "debug": True, "num_threads": 1}, + {"host": "127.0.0.1", "port": 9090, "debug": False, "num_threads": 4}, + {"host": "localhost", "port": 8080, "debug": True, "num_threads": 2}, + ] + + for config in configs: + runner = FoxgloveBridge(**config) + assert runner is not None + + except Exception as e: + pytest.fail(f"Failed to create runner with different configs: {e}") + + +def test_bridge_runner_has_run_method() -> None: + """Test that the bridge runner has a run method that can be called.""" + try: + from dimos_lcm.foxglove_bridge import FoxgloveBridge + + runner = FoxgloveBridge(host="localhost", port=8765, debug=False, num_threads=1) + + # Check that the run method exists + assert hasattr(runner, "run") + assert callable(runner.run) + + except Exception as e: + pytest.fail(f"Failed to verify run method: {e}") diff --git a/dimos/utils/test_generic.py b/dimos/utils/test_generic.py new file mode 100644 index 0000000000..51e7a2007a --- /dev/null +++ b/dimos/utils/test_generic.py @@ -0,0 +1,31 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from uuid import UUID + +from dimos.utils.generic import short_id + + +def test_short_id_hello_world() -> None: + assert short_id("HelloWorld") == "6GgJmzi1KYf4iaHVxk" + + +def test_short_id_uuid_one(mocker) -> None: + mocker.patch("uuid.uuid4", return_value=UUID("11111111-1111-1111-1111-111111111111")) + assert short_id() == "wcFtOGNXQnQFZ8QRh1" + + +def test_short_id_uuid_zero(mocker) -> None: + mocker.patch("uuid.uuid4", return_value=UUID("00000000-0000-0000-0000-000000000000")) + assert short_id() == "000000000000000000" diff --git a/dimos/utils/test_llm_utils.py b/dimos/utils/test_llm_utils.py new file mode 100644 index 0000000000..2eb2da9867 --- /dev/null +++ b/dimos/utils/test_llm_utils.py @@ -0,0 +1,123 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for LLM utility functions.""" + +import json + +import pytest + +from dimos.utils.llm_utils import extract_json + + +def test_extract_json_clean_response() -> None: + """Test extract_json with clean JSON response.""" + clean_json = '[["object", 1, 2, 3, 4]]' + result = extract_json(clean_json) + assert result == [["object", 1, 2, 3, 4]] + + +def test_extract_json_with_text_before_after() -> None: + """Test extract_json with text before and after JSON.""" + messy = """Here's what I found: + [ + ["person", 10, 20, 30, 40], + ["car", 50, 60, 70, 80] + ] + Hope this helps!""" + result = extract_json(messy) + assert result == [["person", 10, 20, 30, 40], ["car", 50, 60, 70, 80]] + + +def test_extract_json_with_emojis() -> None: + """Test extract_json with emojis and markdown code blocks.""" + messy = """Sure! 😊 Here are the detections: + + ```json + [["human", 100, 200, 300, 400]] + ``` + + Let me know if you need anything else! 👍""" + result = extract_json(messy) + assert result == [["human", 100, 200, 300, 400]] + + +def test_extract_json_multiple_json_blocks() -> None: + """Test extract_json when there are multiple JSON blocks.""" + messy = """First attempt (wrong format): + {"error": "not what we want"} + + Correct format: + [ + ["cat", 10, 10, 50, 50], + ["dog", 60, 60, 100, 100] + ] + + Another block: {"also": "not needed"}""" + result = extract_json(messy) + # Should return the first valid array + assert result == [["cat", 10, 10, 50, 50], ["dog", 60, 60, 100, 100]] + + +def test_extract_json_object() -> None: + """Test extract_json with JSON object instead of array.""" + response = 'The result is: {"status": "success", "count": 5}' + result = extract_json(response) + assert result == {"status": "success", "count": 5} + + +def test_extract_json_nested_structures() -> None: + """Test extract_json with nested arrays and objects.""" + response = """Processing complete: + [ + ["label1", 1, 2, 3, 4], + {"nested": {"value": 10}}, + ["label2", 5, 6, 7, 8] + ]""" + result = extract_json(response) + assert result[0] == ["label1", 1, 2, 3, 4] + assert result[1] == {"nested": {"value": 10}} + assert result[2] == ["label2", 5, 6, 7, 8] + + +def test_extract_json_invalid() -> None: + """Test extract_json raises error when no valid JSON found.""" + response = "This response has no valid JSON at all!" + with pytest.raises(json.JSONDecodeError) as exc_info: + extract_json(response) + assert "Could not extract valid JSON" in str(exc_info.value) + + +# Test with actual LLM response format +MOCK_LLM_RESPONSE = """ + Yes :) + + [ + ["humans", 76, 368, 219, 580], + ["humans", 354, 372, 512, 525], + ["humans", 409, 370, 615, 748], + ["humans", 628, 350, 762, 528], + ["humans", 785, 323, 960, 650] + ] + + Hope this helps!😀😊 :)""" + + +def test_extract_json_with_real_llm_response() -> None: + """Test extract_json with actual messy LLM response.""" + result = extract_json(MOCK_LLM_RESPONSE) + assert isinstance(result, list) + assert len(result) == 5 + assert result[0] == ["humans", 76, 368, 219, 580] + assert result[-1] == ["humans", 785, 323, 960, 650] diff --git a/dimos/utils/test_reactive.py b/dimos/utils/test_reactive.py index 977863826a..8fae6de0db 100644 --- a/dimos/utils/test_reactive.py +++ b/dimos/utils/test_reactive.py @@ -1,11 +1,34 @@ -import pytest +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Callable import time +from typing import Any, TypeVar + import numpy as np +import pytest import reactivex as rx from reactivex import operators as ops -from typing import Callable, TypeVar, Any from reactivex.disposable import Disposable -from dimos.utils.reactive import backpressure, getter_streaming, getter_ondemand, callback_to_observable +from reactivex.scheduler import ThreadPoolScheduler + +from dimos.utils.reactive import ( + backpressure, + callback_to_observable, + getter_ondemand, + getter_streaming, +) def measure_time(func: Callable[[], Any], iterations: int = 1) -> float: @@ -16,17 +39,23 @@ def measure_time(func: Callable[[], Any], iterations: int = 1) -> float: return result, total_time -def assert_time(func: Callable[[], Any], assertion: Callable[[int], bool], assert_fail_msg=None) -> None: +def assert_time( + func: Callable[[], Any], assertion: Callable[[int], bool], assert_fail_msg=None +) -> None: [result, total_time] = measure_time(func) assert assertion(total_time), assert_fail_msg + f", took {round(total_time, 2)}s" return result -def min_time(func: Callable[[], Any], min_t: int, assert_fail_msg="Function returned too fast"): - return assert_time(func, (lambda t: t > min_t), assert_fail_msg + f", min: {min_t} seconds") +def min_time( + func: Callable[[], Any], min_t: int, assert_fail_msg: str = "Function returned too fast" +): + return assert_time( + func, (lambda t: t >= min_t * 0.98), assert_fail_msg + f", min: {min_t} seconds" + ) -def max_time(func: Callable[[], Any], max_t: int, assert_fail_msg="Function took too long"): +def max_time(func: Callable[[], Any], max_t: int, assert_fail_msg: str = "Function took too long"): return assert_time(func, (lambda t: t < max_t), assert_fail_msg + f", max: {max_t} seconds") @@ -40,7 +69,7 @@ def factory(observer, scheduler=None): state["active"] += 1 upstream = source.subscribe(observer, scheduler=scheduler) - def _dispose(): + def _dispose() -> None: upstream.dispose() state["active"] -= 1 @@ -52,57 +81,79 @@ def _dispose(): return proxy -def test_backpressure_handling(): - received_fast = [] - received_slow = [] - # Create an observable that emits numpy arrays instead of integers - source = dispose_spy(rx.interval(0.1).pipe(ops.map(lambda i: np.array([i, i + 1, i + 2])), ops.take(50))) - - # Wrap with backpressure handling - safe_source = backpressure(source) - - # Fast sub - subscription1 = safe_source.subscribe(lambda x: received_fast.append(x)) - - # Slow sub (shouldn't block above) - subscription2 = safe_source.subscribe(lambda x: (time.sleep(0.25), received_slow.append(x))) - - time.sleep(2.5) - - subscription1.dispose() - assert not source.is_disposed(), "Observable should not be disposed yet" - subscription2.dispose() - time.sleep(0.1) - assert source.is_disposed(), "Observable should be disposed" - - # Check results - print("Fast observer received:", len(received_fast), [arr[0] for arr in received_fast]) - print("Slow observer received:", len(received_slow), [arr[0] for arr in received_slow]) - - # Fast observer should get all or nearly all items - assert len(received_fast) > 15, f"Expected fast observer to receive most items, got {len(received_fast)}" - - # Slow observer should get fewer items due to backpressure handling - assert len(received_slow) < len(received_fast), "Slow observer should receive fewer items than fast observer" - # Specifically, processing at 0.25s means ~4 items per second, so expect 8-10 items - assert 7 <= len(received_slow) <= 11, f"Expected 7-11 items, got {len(received_slow)}" - - # The slow observer should skip items (not process them in sequence) - # We test this by checking that the difference between consecutive arrays is sometimes > 1 - has_skips = False - for i in range(1, len(received_slow)): - if received_slow[i][0] - received_slow[i - 1][0] > 1: - has_skips = True - break - assert has_skips, "Slow observer should skip items due to backpressure" - - -def test_getter_streaming_blocking(): - source = dispose_spy(rx.interval(0.2).pipe(ops.map(lambda i: np.array([i, i + 1, i + 2])), ops.take(50))) +def test_backpressure_handling() -> None: + # Create a dedicated scheduler for this test to avoid thread leaks + test_scheduler = ThreadPoolScheduler(max_workers=8) + try: + received_fast = [] + received_slow = [] + # Create an observable that emits numpy arrays instead of integers + source = dispose_spy( + rx.interval(0.1).pipe(ops.map(lambda i: np.array([i, i + 1, i + 2])), ops.take(50)) + ) + + # Wrap with backpressure handling + safe_source = backpressure(source, scheduler=test_scheduler) + + # Fast sub + subscription1 = safe_source.subscribe(lambda x: received_fast.append(x)) + + # Slow sub (shouldn't block above) + subscription2 = safe_source.subscribe(lambda x: (time.sleep(0.25), received_slow.append(x))) + + time.sleep(2.5) + + subscription1.dispose() + assert not source.is_disposed(), "Observable should not be disposed yet" + subscription2.dispose() + # Wait longer to ensure background threads finish processing + # (the slow subscriber sleeps for 0.25s, so we need to wait at least that long) + time.sleep(0.5) + assert source.is_disposed(), "Observable should be disposed" + + # Check results + print("Fast observer received:", len(received_fast), [arr[0] for arr in received_fast]) + print("Slow observer received:", len(received_slow), [arr[0] for arr in received_slow]) + + # Fast observer should get all or nearly all items + assert len(received_fast) > 15, ( + f"Expected fast observer to receive most items, got {len(received_fast)}" + ) + + # Slow observer should get fewer items due to backpressure handling + assert len(received_slow) < len(received_fast), ( + "Slow observer should receive fewer items than fast observer" + ) + # Specifically, processing at 0.25s means ~4 items per second, so expect 8-10 items + assert 7 <= len(received_slow) <= 11, f"Expected 7-11 items, got {len(received_slow)}" + + # The slow observer should skip items (not process them in sequence) + # We test this by checking that the difference between consecutive arrays is sometimes > 1 + has_skips = False + for i in range(1, len(received_slow)): + if received_slow[i][0] - received_slow[i - 1][0] > 1: + has_skips = True + break + assert has_skips, "Slow observer should skip items due to backpressure" + finally: + # Always shutdown the scheduler to clean up threads + test_scheduler.executor.shutdown(wait=True) + + +def test_getter_streaming_blocking() -> None: + source = dispose_spy( + rx.interval(0.2).pipe(ops.map(lambda i: np.array([i, i + 1, i + 2])), ops.take(50)) + ) assert source.is_disposed() - getter = min_time(lambda: getter_streaming(source), 0.2, "Latest getter needs to block until first msg is ready") - assert np.array_equal(getter(), np.array([0, 1, 2])), f"Expected to get the first array [0,1,2], got {getter()}" + getter = min_time( + lambda: getter_streaming(source), + 0.2, + "Latest getter needs to block until first msg is ready", + ) + assert np.array_equal(getter(), np.array([0, 1, 2])), ( + f"Expected to get the first array [0,1,2], got {getter()}" + ) time.sleep(0.5) assert getter()[0] >= 2, f"Expected array with first value >= 2, got {getter()}" @@ -110,24 +161,28 @@ def test_getter_streaming_blocking(): assert getter()[0] >= 4, f"Expected array with first value >= 4, got {getter()}" getter.dispose() + time.sleep(0.3) # Wait for background interval timer threads to finish assert source.is_disposed(), "Observable should be disposed" -def test_getter_streaming_blocking_timeout(): +def test_getter_streaming_blocking_timeout() -> None: source = dispose_spy(rx.interval(0.2).pipe(ops.take(50))) with pytest.raises(Exception): getter = getter_streaming(source, timeout=0.1) getter.dispose() + time.sleep(0.3) # Wait for background interval timer threads to finish assert source.is_disposed() -def test_getter_streaming_nonblocking(): +def test_getter_streaming_nonblocking() -> None: source = dispose_spy(rx.interval(0.2).pipe(ops.take(50))) getter = max_time( - lambda: getter_streaming(source, nonblocking=True), 0.1, "nonblocking getter init shouldn't block" + lambda: getter_streaming(source, nonblocking=True), + 0.1, + "nonblocking getter init shouldn't block", ) - min_time(getter, 0.2, "Expected for first value call to block if cache is empty") + min_time(getter, 0.1, "Expected for first value call to block if cache is empty") assert getter() == 0 time.sleep(0.5) @@ -140,10 +195,11 @@ def test_getter_streaming_nonblocking(): assert getter() >= 4, f"Expected value >= 4, got {getter()}" getter.dispose() + time.sleep(0.3) # Wait for background interval timer threads to finish assert source.is_disposed(), "Observable should be disposed" -def test_getter_streaming_nonblocking_timeout(): +def test_getter_streaming_nonblocking_timeout() -> None: source = dispose_spy(rx.interval(0.2).pipe(ops.take(50))) getter = getter_streaming(source, timeout=0.1, nonblocking=True) with pytest.raises(Exception): @@ -151,32 +207,51 @@ def test_getter_streaming_nonblocking_timeout(): assert not source.is_disposed(), "is not disposed, this is a job of the caller" - -def test_getter_ondemand(): - source = dispose_spy(rx.interval(0.1).pipe(ops.take(50))) - getter = getter_ondemand(source) - assert source.is_disposed(), "Observable should be disposed" - assert min_time(getter, 0.05) == 0, f"Expected to get the first value of 0, got {getter()}" - assert source.is_disposed(), "Observable should be disposed" - assert getter() == 0, f"Expected to get the first value of 0, got {getter()}" - assert source.is_disposed(), "Observable should be disposed" - - -def test_getter_ondemand_timeout(): + # Clean up the subscription to avoid thread leak + getter.dispose() + time.sleep(0.3) # Wait for background threads to finish + assert source.is_disposed(), "Observable should be disposed after cleanup" + + +def test_getter_ondemand() -> None: + # Create a controlled scheduler to avoid thread leaks from rx.interval + test_scheduler = ThreadPoolScheduler(max_workers=4) + try: + source = dispose_spy(rx.interval(0.1, scheduler=test_scheduler).pipe(ops.take(50))) + getter = getter_ondemand(source) + assert source.is_disposed(), "Observable should be disposed" + result = min_time(getter, 0.05) + assert result == 0, f"Expected to get the first value of 0, got {result}" + # Wait for background threads to clean up + time.sleep(0.3) + assert source.is_disposed(), "Observable should be disposed" + result2 = getter() + assert result2 == 0, f"Expected to get the first value of 0, got {result2}" + assert source.is_disposed(), "Observable should be disposed" + # Wait for threads to finish + time.sleep(0.3) + finally: + # Explicitly shutdown the scheduler to clean up threads + test_scheduler.executor.shutdown(wait=True) + + +def test_getter_ondemand_timeout() -> None: source = dispose_spy(rx.interval(0.2).pipe(ops.take(50))) getter = getter_ondemand(source, timeout=0.1) with pytest.raises(Exception): getter() assert source.is_disposed(), "Observable should be disposed" + # Wait for background interval timer threads to finish + time.sleep(0.3) -def test_callback_to_observable(): +def test_callback_to_observable() -> None: # Test converting a callback-based API to an Observable received = [] callback = None # Mock start function that captures the callback - def start_fn(cb): + def start_fn(cb) -> str: nonlocal callback callback = cb return "start_result" @@ -184,7 +259,7 @@ def start_fn(cb): # Mock stop function stop_called = False - def stop_fn(cb): + def stop_fn(cb) -> None: nonlocal stop_called stop_called = True diff --git a/dimos/utils/test_testing.py b/dimos/utils/test_testing.py new file mode 100644 index 0000000000..3684031170 --- /dev/null +++ b/dimos/utils/test_testing.py @@ -0,0 +1,279 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re + +from reactivex import operators as ops + +from dimos.robot.unitree_webrtc.type.lidar import LidarMessage +from dimos.robot.unitree_webrtc.type.odometry import Odometry +from dimos.utils import testing +from dimos.utils.data import get_data + + +def test_sensor_replay() -> None: + counter = 0 + for message in testing.SensorReplay(name="office_lidar").iterate(): + counter += 1 + assert isinstance(message, dict) + assert counter == 500 + + +def test_sensor_replay_cast() -> None: + counter = 0 + for message in testing.SensorReplay( + name="office_lidar", autocast=LidarMessage.from_msg + ).iterate(): + counter += 1 + assert isinstance(message, LidarMessage) + assert counter == 500 + + +def test_timed_sensor_replay() -> None: + get_data("unitree_office_walk") + odom_store = testing.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) + + itermsgs = [] + for msg in odom_store.iterate(): + itermsgs.append(msg) + if len(itermsgs) > 9: + break + + assert len(itermsgs) == 10 + + print("\n") + + timed_msgs = [] + + for msg in odom_store.stream().pipe(ops.take(10), ops.to_list()).run(): + timed_msgs.append(msg) + + assert len(timed_msgs) == 10 + + for i in range(10): + print(itermsgs[i], timed_msgs[i]) + assert itermsgs[i] == timed_msgs[i] + + +def test_iterate_ts_no_seek() -> None: + """Test iterate_ts without seek (start_timestamp=None)""" + odom_store = testing.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) + + # Test without seek + ts_msgs = [] + for ts, msg in odom_store.iterate_ts(): + ts_msgs.append((ts, msg)) + if len(ts_msgs) >= 5: + break + + assert len(ts_msgs) == 5 + # Check that we get tuples of (timestamp, data) + for ts, msg in ts_msgs: + assert isinstance(ts, float) + assert isinstance(msg, Odometry) + + +def test_iterate_ts_with_from_timestamp() -> None: + """Test iterate_ts with from_timestamp (absolute timestamp)""" + odom_store = testing.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) + + # First get all messages to find a good seek point + all_msgs = [] + for ts, msg in odom_store.iterate_ts(): + all_msgs.append((ts, msg)) + if len(all_msgs) >= 10: + break + + # Seek to timestamp of 5th message + seek_timestamp = all_msgs[4][0] + + # Test with from_timestamp + seeked_msgs = [] + for ts, msg in odom_store.iterate_ts(from_timestamp=seek_timestamp): + seeked_msgs.append((ts, msg)) + if len(seeked_msgs) >= 5: + break + + assert len(seeked_msgs) == 5 + # First message should be at or after seek timestamp + assert seeked_msgs[0][0] >= seek_timestamp + # Should match the data from position 5 onward + assert seeked_msgs[0][1] == all_msgs[4][1] + + +def test_iterate_ts_with_relative_seek() -> None: + """Test iterate_ts with seek (relative seconds after first timestamp)""" + odom_store = testing.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) + + # Get first few messages to understand timing + all_msgs = [] + for ts, msg in odom_store.iterate_ts(): + all_msgs.append((ts, msg)) + if len(all_msgs) >= 10: + break + + # Calculate relative seek time (e.g., 0.5 seconds after start) + first_ts = all_msgs[0][0] + seek_seconds = 0.5 + expected_start_ts = first_ts + seek_seconds + + # Test with relative seek + seeked_msgs = [] + for ts, msg in odom_store.iterate_ts(seek=seek_seconds): + seeked_msgs.append((ts, msg)) + if len(seeked_msgs) >= 5: + break + + # First message should be at or after expected timestamp + assert seeked_msgs[0][0] >= expected_start_ts + # Make sure we're actually skipping some messages + assert seeked_msgs[0][0] > first_ts + + +def test_stream_with_seek() -> None: + """Test stream method with seek parameters""" + odom_store = testing.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) + + # Test stream with relative seek + msgs_with_seek = [] + for msg in odom_store.stream(seek=0.2).pipe(ops.take(5), ops.to_list()).run(): + msgs_with_seek.append(msg) + + assert len(msgs_with_seek) == 5 + + # Test stream with from_timestamp + # First get a reference timestamp + first_msgs = [] + for msg in odom_store.stream().pipe(ops.take(3), ops.to_list()).run(): + first_msgs.append(msg) + + # Now test from_timestamp (would need actual timestamps from iterate_ts to properly test) + # This is a basic test to ensure the parameter is accepted + msgs_with_timestamp = [] + for msg in ( + odom_store.stream(from_timestamp=1000000000.0).pipe(ops.take(3), ops.to_list()).run() + ): + msgs_with_timestamp.append(msg) + + +def test_duration_with_loop() -> None: + """Test duration parameter with looping in TimedSensorReplay""" + odom_store = testing.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) + + # Collect timestamps from a small duration window + collected_ts = [] + duration = 0.3 # 300ms window + + # First pass: collect timestamps in the duration window + for ts, _msg in odom_store.iterate_ts(duration=duration): + collected_ts.append(ts) + if len(collected_ts) >= 100: # Safety limit + break + + # Should have some messages but not too many + assert len(collected_ts) > 0 + assert len(collected_ts) < 20 # Assuming ~30Hz data + + # Test looping with duration - should repeat the same window + loop_count = 0 + prev_ts = None + + for ts, _msg in odom_store.iterate_ts(duration=duration, loop=True): + if prev_ts is not None and ts < prev_ts: + # We've looped back to the beginning + loop_count += 1 + if loop_count >= 2: # Stop after 2 full loops + break + prev_ts = ts + + assert loop_count >= 2 # Verify we actually looped + + +def test_first_methods() -> None: + """Test first() and first_timestamp() methods""" + + # Test SensorReplay.first() + lidar_replay = testing.SensorReplay("office_lidar", autocast=LidarMessage.from_msg) + + print("first file", lidar_replay.files[0]) + # Verify the first file ends with 000.pickle using regex + assert re.search(r"000\.pickle$", str(lidar_replay.files[0])), ( + f"Expected first file to end with 000.pickle, got {lidar_replay.files[0]}" + ) + + first_msg = lidar_replay.first() + assert first_msg is not None + assert isinstance(first_msg, LidarMessage) + + # Verify it's the same type as first item from iterate() + first_from_iterate = next(lidar_replay.iterate()) + print("DONE") + assert type(first_msg) is type(first_from_iterate) + # Since LidarMessage.from_msg uses time.time(), timestamps will be slightly different + assert abs(first_msg.ts - first_from_iterate.ts) < 1.0 # Within 1 second tolerance + + # Test TimedSensorReplay.first_timestamp() + odom_store = testing.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) + first_ts = odom_store.first_timestamp() + assert first_ts is not None + assert isinstance(first_ts, float) + + # Verify it matches the timestamp from iterate_ts + ts_from_iterate, _ = next(odom_store.iterate_ts()) + assert first_ts == ts_from_iterate + + # Test that first() returns just the data + first_data = odom_store.first() + assert first_data is not None + assert isinstance(first_data, Odometry) + + +def test_find_closest() -> None: + """Test find_closest method in TimedSensorReplay""" + odom_store = testing.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) + + # Get some reference timestamps + timestamps = [] + for ts, _msg in odom_store.iterate_ts(): + timestamps.append(ts) + if len(timestamps) >= 10: + break + + # Test exact match + target_ts = timestamps[5] + result = odom_store.find_closest(target_ts) + assert result is not None + assert isinstance(result, Odometry) + + # Test between timestamps + mid_ts = (timestamps[3] + timestamps[4]) / 2 + result = odom_store.find_closest(mid_ts) + assert result is not None + + # Test with tolerance + far_future = timestamps[-1] + 100.0 + result = odom_store.find_closest(far_future, tolerance=1.0) + assert result is None # Too far away + + result = odom_store.find_closest(timestamps[0] - 0.001, tolerance=0.01) + assert result is not None # Within tolerance + + # Test find_closest_seek + result = odom_store.find_closest_seek(0.5) # 0.5 seconds from start + assert result is not None + assert isinstance(result, Odometry) + + # Test with negative seek (before start) + result = odom_store.find_closest_seek(-1.0) + assert result is not None # Should still return closest (first frame) diff --git a/dimos/utils/test_transform_utils.py b/dimos/utils/test_transform_utils.py new file mode 100644 index 0000000000..8054971d3f --- /dev/null +++ b/dimos/utils/test_transform_utils.py @@ -0,0 +1,678 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pytest +from scipy.spatial.transform import Rotation as R + +from dimos.msgs.geometry_msgs import Pose, Quaternion, Transform, Vector3 +from dimos.utils import transform_utils + + +class TestNormalizeAngle: + def test_normalize_angle_zero(self) -> None: + assert transform_utils.normalize_angle(0) == 0 + + def test_normalize_angle_pi(self) -> None: + assert np.isclose(transform_utils.normalize_angle(np.pi), np.pi) + + def test_normalize_angle_negative_pi(self) -> None: + assert np.isclose(transform_utils.normalize_angle(-np.pi), -np.pi) + + def test_normalize_angle_two_pi(self) -> None: + # 2*pi should normalize to 0 + assert np.isclose(transform_utils.normalize_angle(2 * np.pi), 0, atol=1e-10) + + def test_normalize_angle_large_positive(self) -> None: + # Large positive angle should wrap to [-pi, pi] + angle = 5 * np.pi + normalized = transform_utils.normalize_angle(angle) + assert -np.pi <= normalized <= np.pi + assert np.isclose(normalized, np.pi) + + def test_normalize_angle_large_negative(self) -> None: + # Large negative angle should wrap to [-pi, pi] + angle = -5 * np.pi + normalized = transform_utils.normalize_angle(angle) + assert -np.pi <= normalized <= np.pi + # -5*pi = -pi (odd multiple of pi wraps to -pi) + assert np.isclose(normalized, -np.pi) or np.isclose(normalized, np.pi) + + +# Tests for distance_angle_to_goal_xy removed as function doesn't exist in the module + + +class TestPoseToMatrix: + def test_identity_pose(self) -> None: + pose = Pose(Vector3(0, 0, 0), Quaternion(0, 0, 0, 1)) + T = transform_utils.pose_to_matrix(pose) + assert np.allclose(T, np.eye(4)) + + def test_translation_only(self) -> None: + pose = Pose(Vector3(1, 2, 3), Quaternion(0, 0, 0, 1)) + T = transform_utils.pose_to_matrix(pose) + expected = np.eye(4) + expected[:3, 3] = [1, 2, 3] + assert np.allclose(T, expected) + + def test_rotation_only_90_degrees_z(self) -> None: + # 90 degree rotation around z-axis + quat = R.from_euler("z", np.pi / 2).as_quat() + pose = Pose(Vector3(0, 0, 0), Quaternion(quat[0], quat[1], quat[2], quat[3])) + T = transform_utils.pose_to_matrix(pose) + + # Check rotation part + expected_rot = R.from_euler("z", np.pi / 2).as_matrix() + assert np.allclose(T[:3, :3], expected_rot) + + # Check translation is zero + assert np.allclose(T[:3, 3], [0, 0, 0]) + + def test_translation_and_rotation(self) -> None: + quat = R.from_euler("xyz", [np.pi / 4, np.pi / 6, np.pi / 3]).as_quat() + pose = Pose(Vector3(5, -3, 2), Quaternion(quat[0], quat[1], quat[2], quat[3])) + T = transform_utils.pose_to_matrix(pose) + + # Check translation + assert np.allclose(T[:3, 3], [5, -3, 2]) + + # Check rotation + expected_rot = R.from_euler("xyz", [np.pi / 4, np.pi / 6, np.pi / 3]).as_matrix() + assert np.allclose(T[:3, :3], expected_rot) + + # Check bottom row + assert np.allclose(T[3, :], [0, 0, 0, 1]) + + def test_zero_norm_quaternion(self) -> None: + # Test handling of zero norm quaternion + pose = Pose(Vector3(1, 2, 3), Quaternion(0, 0, 0, 0)) + T = transform_utils.pose_to_matrix(pose) + + # Should use identity rotation + expected = np.eye(4) + expected[:3, 3] = [1, 2, 3] + assert np.allclose(T, expected) + + +class TestMatrixToPose: + def test_identity_matrix(self) -> None: + T = np.eye(4) + pose = transform_utils.matrix_to_pose(T) + assert pose.position.x == 0 + assert pose.position.y == 0 + assert pose.position.z == 0 + assert np.isclose(pose.orientation.w, 1) + assert np.isclose(pose.orientation.x, 0) + assert np.isclose(pose.orientation.y, 0) + assert np.isclose(pose.orientation.z, 0) + + def test_translation_only(self) -> None: + T = np.eye(4) + T[:3, 3] = [1, 2, 3] + pose = transform_utils.matrix_to_pose(T) + assert pose.position.x == 1 + assert pose.position.y == 2 + assert pose.position.z == 3 + assert np.isclose(pose.orientation.w, 1) + + def test_rotation_only(self) -> None: + T = np.eye(4) + T[:3, :3] = R.from_euler("z", np.pi / 2).as_matrix() + pose = transform_utils.matrix_to_pose(T) + + # Check position is zero + assert pose.position.x == 0 + assert pose.position.y == 0 + assert pose.position.z == 0 + + # Check rotation + quat = [pose.orientation.x, pose.orientation.y, pose.orientation.z, pose.orientation.w] + recovered_rot = R.from_quat(quat).as_matrix() + assert np.allclose(recovered_rot, T[:3, :3]) + + def test_round_trip_conversion(self) -> None: + # Test that pose -> matrix -> pose gives same result + # Use a properly normalized quaternion + quat = R.from_euler("xyz", [0.1, 0.2, 0.3]).as_quat() + original_pose = Pose( + Vector3(1.5, -2.3, 0.7), Quaternion(quat[0], quat[1], quat[2], quat[3]) + ) + T = transform_utils.pose_to_matrix(original_pose) + recovered_pose = transform_utils.matrix_to_pose(T) + + assert np.isclose(recovered_pose.position.x, original_pose.position.x) + assert np.isclose(recovered_pose.position.y, original_pose.position.y) + assert np.isclose(recovered_pose.position.z, original_pose.position.z) + assert np.isclose(recovered_pose.orientation.x, original_pose.orientation.x, atol=1e-6) + assert np.isclose(recovered_pose.orientation.y, original_pose.orientation.y, atol=1e-6) + assert np.isclose(recovered_pose.orientation.z, original_pose.orientation.z, atol=1e-6) + assert np.isclose(recovered_pose.orientation.w, original_pose.orientation.w, atol=1e-6) + + +class TestApplyTransform: + def test_identity_transform(self) -> None: + pose = Pose(Vector3(1, 2, 3), Quaternion(0, 0, 0, 1)) + T_identity = np.eye(4) + result = transform_utils.apply_transform(pose, T_identity) + + assert np.isclose(result.position.x, pose.position.x) + assert np.isclose(result.position.y, pose.position.y) + assert np.isclose(result.position.z, pose.position.z) + + def test_translation_transform(self) -> None: + pose = Pose(Vector3(1, 0, 0), Quaternion(0, 0, 0, 1)) + T = np.eye(4) + T[:3, 3] = [2, 3, 4] + result = transform_utils.apply_transform(pose, T) + + assert np.isclose(result.position.x, 3) # 2 + 1 + assert np.isclose(result.position.y, 3) # 3 + 0 + assert np.isclose(result.position.z, 4) # 4 + 0 + + def test_rotation_transform(self) -> None: + pose = Pose(Vector3(1, 0, 0), Quaternion(0, 0, 0, 1)) + T = np.eye(4) + T[:3, :3] = R.from_euler("z", np.pi / 2).as_matrix() # 90 degree rotation + result = transform_utils.apply_transform(pose, T) + + # After 90 degree rotation around z, point (1,0,0) becomes (0,1,0) + assert np.isclose(result.position.x, 0, atol=1e-10) + assert np.isclose(result.position.y, 1) + assert np.isclose(result.position.z, 0) + + def test_transform_with_transform_object(self) -> None: + pose = Pose(Vector3(1, 0, 0), Quaternion(0, 0, 0, 1)) + pose.frame_id = "base" + + transform = Transform() + transform.frame_id = "world" + transform.child_frame_id = "base" + transform.translation = Vector3(2, 3, 4) + transform.rotation = Quaternion(0, 0, 0, 1) + + result = transform_utils.apply_transform(pose, transform) + assert np.isclose(result.position.x, 3) + assert np.isclose(result.position.y, 3) + assert np.isclose(result.position.z, 4) + + def test_transform_frame_mismatch_raises(self) -> None: + pose = Pose(Vector3(1, 0, 0), Quaternion(0, 0, 0, 1)) + pose.frame_id = "base" + + transform = Transform() + transform.frame_id = "world" + transform.child_frame_id = "different_frame" + transform.translation = Vector3(2, 3, 4) + transform.rotation = Quaternion(0, 0, 0, 1) + + with pytest.raises(ValueError, match="does not match"): + transform_utils.apply_transform(pose, transform) + + +class TestOpticalToRobotFrame: + def test_identity_at_origin(self) -> None: + pose = Pose(Vector3(0, 0, 0), Quaternion(0, 0, 0, 1)) + result = transform_utils.optical_to_robot_frame(pose) + assert result.position.x == 0 + assert result.position.y == 0 + assert result.position.z == 0 + + def test_position_transformation(self) -> None: + # Optical: X=right(1), Y=down(0), Z=forward(0) + pose = Pose(Vector3(1, 0, 0), Quaternion(0, 0, 0, 1)) + result = transform_utils.optical_to_robot_frame(pose) + + # Robot: X=forward(0), Y=left(-1), Z=up(0) + assert np.isclose(result.position.x, 0) # Forward = Camera Z + assert np.isclose(result.position.y, -1) # Left = -Camera X + assert np.isclose(result.position.z, 0) # Up = -Camera Y + + def test_forward_position(self) -> None: + # Optical: X=right(0), Y=down(0), Z=forward(2) + pose = Pose(Vector3(0, 0, 2), Quaternion(0, 0, 0, 1)) + result = transform_utils.optical_to_robot_frame(pose) + + # Robot: X=forward(2), Y=left(0), Z=up(0) + assert np.isclose(result.position.x, 2) + assert np.isclose(result.position.y, 0) + assert np.isclose(result.position.z, 0) + + def test_down_position(self) -> None: + # Optical: X=right(0), Y=down(3), Z=forward(0) + pose = Pose(Vector3(0, 3, 0), Quaternion(0, 0, 0, 1)) + result = transform_utils.optical_to_robot_frame(pose) + + # Robot: X=forward(0), Y=left(0), Z=up(-3) + assert np.isclose(result.position.x, 0) + assert np.isclose(result.position.y, 0) + assert np.isclose(result.position.z, -3) + + def test_round_trip_optical_robot(self) -> None: + original_pose = Pose(Vector3(1, 2, 3), Quaternion(0.1, 0.2, 0.3, 0.9165151389911680)) + robot_pose = transform_utils.optical_to_robot_frame(original_pose) + recovered_pose = transform_utils.robot_to_optical_frame(robot_pose) + + assert np.isclose(recovered_pose.position.x, original_pose.position.x, atol=1e-10) + assert np.isclose(recovered_pose.position.y, original_pose.position.y, atol=1e-10) + assert np.isclose(recovered_pose.position.z, original_pose.position.z, atol=1e-10) + + +class TestRobotToOpticalFrame: + def test_position_transformation(self) -> None: + # Robot: X=forward(1), Y=left(0), Z=up(0) + pose = Pose(Vector3(1, 0, 0), Quaternion(0, 0, 0, 1)) + result = transform_utils.robot_to_optical_frame(pose) + + # Optical: X=right(0), Y=down(0), Z=forward(1) + assert np.isclose(result.position.x, 0) + assert np.isclose(result.position.y, 0) + assert np.isclose(result.position.z, 1) + + def test_left_position(self) -> None: + # Robot: X=forward(0), Y=left(2), Z=up(0) + pose = Pose(Vector3(0, 2, 0), Quaternion(0, 0, 0, 1)) + result = transform_utils.robot_to_optical_frame(pose) + + # Optical: X=right(-2), Y=down(0), Z=forward(0) + assert np.isclose(result.position.x, -2) + assert np.isclose(result.position.y, 0) + assert np.isclose(result.position.z, 0) + + def test_up_position(self) -> None: + # Robot: X=forward(0), Y=left(0), Z=up(3) + pose = Pose(Vector3(0, 0, 3), Quaternion(0, 0, 0, 1)) + result = transform_utils.robot_to_optical_frame(pose) + + # Optical: X=right(0), Y=down(-3), Z=forward(0) + assert np.isclose(result.position.x, 0) + assert np.isclose(result.position.y, -3) + assert np.isclose(result.position.z, 0) + + +class TestYawTowardsPoint: + def test_yaw_from_origin(self) -> None: + # Point at (1, 0) from origin should have yaw = 0 + position = Vector3(1, 0, 0) + yaw = transform_utils.yaw_towards_point(position) + assert np.isclose(yaw, 0) + + def test_yaw_ninety_degrees(self) -> None: + # Point at (0, 1) from origin should have yaw = pi/2 + position = Vector3(0, 1, 0) + yaw = transform_utils.yaw_towards_point(position) + assert np.isclose(yaw, np.pi / 2) + + def test_yaw_negative_ninety_degrees(self) -> None: + # Point at (0, -1) from origin should have yaw = -pi/2 + position = Vector3(0, -1, 0) + yaw = transform_utils.yaw_towards_point(position) + assert np.isclose(yaw, -np.pi / 2) + + def test_yaw_forty_five_degrees(self) -> None: + # Point at (1, 1) from origin should have yaw = pi/4 + position = Vector3(1, 1, 0) + yaw = transform_utils.yaw_towards_point(position) + assert np.isclose(yaw, np.pi / 4) + + def test_yaw_with_custom_target(self) -> None: + # Point at (3, 2) from target (1, 1) + position = Vector3(3, 2, 0) + target = Vector3(1, 1, 0) + yaw = transform_utils.yaw_towards_point(position, target) + # Direction is (2, 1), so yaw = atan2(1, 2) + expected = np.arctan2(1, 2) + assert np.isclose(yaw, expected) + + +# Tests for transform_robot_to_map removed as function doesn't exist in the module + + +class TestCreateTransformFrom6DOF: + def test_identity_transform(self) -> None: + trans = Vector3(0, 0, 0) + euler = Vector3(0, 0, 0) + T = transform_utils.create_transform_from_6dof(trans, euler) + assert np.allclose(T, np.eye(4)) + + def test_translation_only(self) -> None: + trans = Vector3(1, 2, 3) + euler = Vector3(0, 0, 0) + T = transform_utils.create_transform_from_6dof(trans, euler) + + expected = np.eye(4) + expected[:3, 3] = [1, 2, 3] + assert np.allclose(T, expected) + + def test_rotation_only(self) -> None: + trans = Vector3(0, 0, 0) + euler = Vector3(np.pi / 4, np.pi / 6, np.pi / 3) + T = transform_utils.create_transform_from_6dof(trans, euler) + + expected_rot = R.from_euler("xyz", [np.pi / 4, np.pi / 6, np.pi / 3]).as_matrix() + assert np.allclose(T[:3, :3], expected_rot) + assert np.allclose(T[:3, 3], [0, 0, 0]) + assert np.allclose(T[3, :], [0, 0, 0, 1]) + + def test_translation_and_rotation(self) -> None: + trans = Vector3(5, -3, 2) + euler = Vector3(0.1, 0.2, 0.3) + T = transform_utils.create_transform_from_6dof(trans, euler) + + expected_rot = R.from_euler("xyz", [0.1, 0.2, 0.3]).as_matrix() + assert np.allclose(T[:3, :3], expected_rot) + assert np.allclose(T[:3, 3], [5, -3, 2]) + + def test_small_angles_threshold(self) -> None: + trans = Vector3(1, 2, 3) + euler = Vector3(1e-7, 1e-8, 1e-9) # Very small angles + T = transform_utils.create_transform_from_6dof(trans, euler) + + # Should be effectively identity rotation + expected = np.eye(4) + expected[:3, 3] = [1, 2, 3] + assert np.allclose(T, expected, atol=1e-6) + + +class TestInvertTransform: + def test_identity_inverse(self) -> None: + T = np.eye(4) + T_inv = transform_utils.invert_transform(T) + assert np.allclose(T_inv, np.eye(4)) + + def test_translation_inverse(self) -> None: + T = np.eye(4) + T[:3, 3] = [1, 2, 3] + T_inv = transform_utils.invert_transform(T) + + # Inverse should negate translation + expected = np.eye(4) + expected[:3, 3] = [-1, -2, -3] + assert np.allclose(T_inv, expected) + + def test_rotation_inverse(self) -> None: + T = np.eye(4) + T[:3, :3] = R.from_euler("z", np.pi / 2).as_matrix() + T_inv = transform_utils.invert_transform(T) + + # Inverse rotation is transpose + expected = np.eye(4) + expected[:3, :3] = R.from_euler("z", -np.pi / 2).as_matrix() + assert np.allclose(T_inv, expected) + + def test_general_transform_inverse(self) -> None: + T = np.eye(4) + T[:3, :3] = R.from_euler("xyz", [0.1, 0.2, 0.3]).as_matrix() + T[:3, 3] = [1, 2, 3] + + T_inv = transform_utils.invert_transform(T) + + # T @ T_inv should be identity + result = T @ T_inv + assert np.allclose(result, np.eye(4)) + + # T_inv @ T should also be identity + result2 = T_inv @ T + assert np.allclose(result2, np.eye(4)) + + +class TestComposeTransforms: + def test_no_transforms(self) -> None: + result = transform_utils.compose_transforms() + assert np.allclose(result, np.eye(4)) + + def test_single_transform(self) -> None: + T = np.eye(4) + T[:3, 3] = [1, 2, 3] + result = transform_utils.compose_transforms(T) + assert np.allclose(result, T) + + def test_two_translations(self) -> None: + T1 = np.eye(4) + T1[:3, 3] = [1, 0, 0] + + T2 = np.eye(4) + T2[:3, 3] = [0, 2, 0] + + result = transform_utils.compose_transforms(T1, T2) + + expected = np.eye(4) + expected[:3, 3] = [1, 2, 0] + assert np.allclose(result, expected) + + def test_three_transforms(self) -> None: + T1 = np.eye(4) + T1[:3, 3] = [1, 0, 0] + + T2 = np.eye(4) + T2[:3, :3] = R.from_euler("z", np.pi / 2).as_matrix() + + T3 = np.eye(4) + T3[:3, 3] = [1, 0, 0] + + result = transform_utils.compose_transforms(T1, T2, T3) + expected = T1 @ T2 @ T3 + assert np.allclose(result, expected) + + +class TestEulerToQuaternion: + def test_zero_euler(self) -> None: + euler = Vector3(0, 0, 0) + quat = transform_utils.euler_to_quaternion(euler) + assert np.isclose(quat.w, 1) + assert np.isclose(quat.x, 0) + assert np.isclose(quat.y, 0) + assert np.isclose(quat.z, 0) + + def test_roll_only(self) -> None: + euler = Vector3(np.pi / 2, 0, 0) + quat = transform_utils.euler_to_quaternion(euler) + + # Verify by converting back + recovered = R.from_quat([quat.x, quat.y, quat.z, quat.w]).as_euler("xyz") + assert np.isclose(recovered[0], np.pi / 2) + assert np.isclose(recovered[1], 0) + assert np.isclose(recovered[2], 0) + + def test_pitch_only(self) -> None: + euler = Vector3(0, np.pi / 3, 0) + quat = transform_utils.euler_to_quaternion(euler) + + recovered = R.from_quat([quat.x, quat.y, quat.z, quat.w]).as_euler("xyz") + assert np.isclose(recovered[0], 0) + assert np.isclose(recovered[1], np.pi / 3) + assert np.isclose(recovered[2], 0) + + def test_yaw_only(self) -> None: + euler = Vector3(0, 0, np.pi / 4) + quat = transform_utils.euler_to_quaternion(euler) + + recovered = R.from_quat([quat.x, quat.y, quat.z, quat.w]).as_euler("xyz") + assert np.isclose(recovered[0], 0) + assert np.isclose(recovered[1], 0) + assert np.isclose(recovered[2], np.pi / 4) + + def test_degrees_mode(self) -> None: + euler = Vector3(45, 30, 60) # degrees + quat = transform_utils.euler_to_quaternion(euler, degrees=True) + + recovered = R.from_quat([quat.x, quat.y, quat.z, quat.w]).as_euler("xyz", degrees=True) + assert np.isclose(recovered[0], 45) + assert np.isclose(recovered[1], 30) + assert np.isclose(recovered[2], 60) + + +class TestQuaternionToEuler: + def test_identity_quaternion(self) -> None: + quat = Quaternion(0, 0, 0, 1) + euler = transform_utils.quaternion_to_euler(quat) + assert np.isclose(euler.x, 0) + assert np.isclose(euler.y, 0) + assert np.isclose(euler.z, 0) + + def test_90_degree_yaw(self) -> None: + # Create quaternion for 90 degree yaw rotation + r = R.from_euler("z", np.pi / 2) + q = r.as_quat() + quat = Quaternion(q[0], q[1], q[2], q[3]) + + euler = transform_utils.quaternion_to_euler(quat) + assert np.isclose(euler.x, 0) + assert np.isclose(euler.y, 0) + assert np.isclose(euler.z, np.pi / 2) + + def test_round_trip_euler_quaternion(self) -> None: + original_euler = Vector3(0.3, 0.5, 0.7) + quat = transform_utils.euler_to_quaternion(original_euler) + recovered_euler = transform_utils.quaternion_to_euler(quat) + + assert np.isclose(recovered_euler.x, original_euler.x, atol=1e-10) + assert np.isclose(recovered_euler.y, original_euler.y, atol=1e-10) + assert np.isclose(recovered_euler.z, original_euler.z, atol=1e-10) + + def test_degrees_mode(self) -> None: + # Create quaternion for 45 degree yaw rotation + r = R.from_euler("z", 45, degrees=True) + q = r.as_quat() + quat = Quaternion(q[0], q[1], q[2], q[3]) + + euler = transform_utils.quaternion_to_euler(quat, degrees=True) + assert np.isclose(euler.x, 0) + assert np.isclose(euler.y, 0) + assert np.isclose(euler.z, 45) + + def test_angle_normalization(self) -> None: + # Test that angles are normalized to [-pi, pi] + r = R.from_euler("xyz", [3 * np.pi, -3 * np.pi, 2 * np.pi]) + q = r.as_quat() + quat = Quaternion(q[0], q[1], q[2], q[3]) + + euler = transform_utils.quaternion_to_euler(quat) + assert -np.pi <= euler.x <= np.pi + assert -np.pi <= euler.y <= np.pi + assert -np.pi <= euler.z <= np.pi + + +class TestGetDistance: + def test_same_pose(self) -> None: + pose1 = Pose(Vector3(1, 2, 3), Quaternion(0, 0, 0, 1)) + pose2 = Pose(Vector3(1, 2, 3), Quaternion(0.1, 0.2, 0.3, 0.9)) + distance = transform_utils.get_distance(pose1, pose2) + assert np.isclose(distance, 0) + + def test_vector_distance(self) -> None: + pose1 = Vector3(1, 2, 3) + pose2 = Vector3(4, 5, 6) + distance = transform_utils.get_distance(pose1, pose2) + assert np.isclose(distance, np.sqrt(3**2 + 3**2 + 3**2)) + + def test_distance_x_axis(self) -> None: + pose1 = Pose(Vector3(0, 0, 0), Quaternion(0, 0, 0, 1)) + pose2 = Pose(Vector3(5, 0, 0), Quaternion(0, 0, 0, 1)) + distance = transform_utils.get_distance(pose1, pose2) + assert np.isclose(distance, 5) + + def test_distance_y_axis(self) -> None: + pose1 = Pose(Vector3(0, 0, 0), Quaternion(0, 0, 0, 1)) + pose2 = Pose(Vector3(0, 3, 0), Quaternion(0, 0, 0, 1)) + distance = transform_utils.get_distance(pose1, pose2) + assert np.isclose(distance, 3) + + def test_distance_z_axis(self) -> None: + pose1 = Pose(Vector3(0, 0, 0), Quaternion(0, 0, 0, 1)) + pose2 = Pose(Vector3(0, 0, 4), Quaternion(0, 0, 0, 1)) + distance = transform_utils.get_distance(pose1, pose2) + assert np.isclose(distance, 4) + + def test_3d_distance(self) -> None: + pose1 = Pose(Vector3(0, 0, 0), Quaternion(0, 0, 0, 1)) + pose2 = Pose(Vector3(3, 4, 0), Quaternion(0, 0, 0, 1)) + distance = transform_utils.get_distance(pose1, pose2) + assert np.isclose(distance, 5) # 3-4-5 triangle + + def test_negative_coordinates(self) -> None: + pose1 = Pose(Vector3(-1, -2, -3), Quaternion(0, 0, 0, 1)) + pose2 = Pose(Vector3(1, 2, 3), Quaternion(0, 0, 0, 1)) + distance = transform_utils.get_distance(pose1, pose2) + expected = np.sqrt(4 + 16 + 36) # sqrt(56) + assert np.isclose(distance, expected) + + +class TestRetractDistance: + def test_retract_along_negative_z(self) -> None: + # Default case: gripper approaches along -z axis + # Positive distance moves away from the surface (opposite to approach direction) + target_pose = Pose(Vector3(0, 0, 1), Quaternion(0, 0, 0, 1)) + retracted = transform_utils.offset_distance(target_pose, 0.5) + + # Moving along -z approach vector with positive distance = retracting upward + # Since approach is -z and we retract (positive distance), we move in +z + assert np.isclose(retracted.position.x, 0) + assert np.isclose(retracted.position.y, 0) + assert np.isclose(retracted.position.z, 0.5) # 1 + 0.5 * (-1) = 0.5 + + # Orientation should remain unchanged + assert retracted.orientation.x == target_pose.orientation.x + assert retracted.orientation.y == target_pose.orientation.y + assert retracted.orientation.z == target_pose.orientation.z + assert retracted.orientation.w == target_pose.orientation.w + + def test_retract_with_rotation(self) -> None: + # Test with a rotated pose (90 degrees around x-axis) + r = R.from_euler("x", np.pi / 2) + q = r.as_quat() + target_pose = Pose(Vector3(0, 0, 1), Quaternion(q[0], q[1], q[2], q[3])) + + retracted = transform_utils.offset_distance(target_pose, 0.5) + + # After 90 degree rotation around x, -z becomes +y + assert np.isclose(retracted.position.x, 0) + assert np.isclose(retracted.position.y, 0.5) # Move along +y + assert np.isclose(retracted.position.z, 1) + + def test_retract_negative_distance(self) -> None: + # Negative distance should move forward (toward the approach direction) + target_pose = Pose(Vector3(0, 0, 1), Quaternion(0, 0, 0, 1)) + retracted = transform_utils.offset_distance(target_pose, -0.3) + + # Moving along -z approach vector with negative distance = moving downward + assert np.isclose(retracted.position.x, 0) + assert np.isclose(retracted.position.y, 0) + assert np.isclose(retracted.position.z, 1.3) # 1 + (-0.3) * (-1) = 1.3 + + def test_retract_arbitrary_pose(self) -> None: + # Test with arbitrary position and rotation + r = R.from_euler("xyz", [0.1, 0.2, 0.3]) + q = r.as_quat() + target_pose = Pose(Vector3(5, 3, 2), Quaternion(q[0], q[1], q[2], q[3])) + + distance = 1.0 + retracted = transform_utils.offset_distance(target_pose, distance) + + # Verify the distance between original and retracted is as expected + # (approximately, due to the approach vector direction) + T_target = transform_utils.pose_to_matrix(target_pose) + rotation_matrix = T_target[:3, :3] + approach_vector = rotation_matrix @ np.array([0, 0, -1]) + + expected_x = target_pose.position.x + distance * approach_vector[0] + expected_y = target_pose.position.y + distance * approach_vector[1] + expected_z = target_pose.position.z + distance * approach_vector[2] + + assert np.isclose(retracted.position.x, expected_x) + assert np.isclose(retracted.position.y, expected_y) + assert np.isclose(retracted.position.z, expected_z) + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/dimos/utils/testing.py b/dimos/utils/testing.py new file mode 100644 index 0000000000..5e3725bc81 --- /dev/null +++ b/dimos/utils/testing.py @@ -0,0 +1,375 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from collections.abc import Callable, Iterator +import functools +import glob +import os +from pathlib import Path +import pickle +import re +import time +from typing import Any, Generic, TypeVar + +from reactivex import ( + from_iterable, + interval, + operators as ops, +) +from reactivex.observable import Observable +from reactivex.scheduler import TimeoutScheduler + +from dimos.utils.data import _get_data_dir, get_data + +T = TypeVar("T") + + +class SensorReplay(Generic[T]): + """Generic sensor data replay utility. + + Args: + name: The name of the test dataset + autocast: Optional function that takes unpickled data and returns a processed result. + For example: lambda data: LidarMessage.from_msg(data) + """ + + def __init__(self, name: str, autocast: Callable[[Any], T] | None = None) -> None: + self.root_dir = get_data(name) + self.autocast = autocast + + def load(self, *names: int | str) -> T | Any | list[T] | list[Any]: + if len(names) == 1: + return self.load_one(names[0]) + return list(map(lambda name: self.load_one(name), names)) + + def load_one(self, name: int | str | Path) -> T | Any: + if isinstance(name, int): + full_path = self.root_dir / f"/{name:03d}.pickle" + elif isinstance(name, Path): + full_path = name + else: + full_path = self.root_dir / Path(f"{name}.pickle") + + with open(full_path, "rb") as f: + data = pickle.load(f) + if self.autocast: + return self.autocast(data) + return data + + def first(self) -> T | Any | None: + try: + return next(self.iterate()) + except StopIteration: + return None + + @functools.cached_property + def files(self) -> list[Path]: + def extract_number(filepath): + """Extract last digits before .pickle extension""" + basename = os.path.basename(filepath) + match = re.search(r"(\d+)\.pickle$", basename) + return int(match.group(1)) if match else 0 + + return sorted( + glob.glob(os.path.join(self.root_dir, "*")), + key=extract_number, + ) + + def iterate(self, loop: bool = False) -> Iterator[T | Any]: + while True: + for file_path in self.files: + yield self.load_one(Path(file_path)) + if not loop: + break + + def stream(self, rate_hz: float | None = None, loop: bool = False) -> Observable[T | Any]: + if rate_hz is None: + return from_iterable(self.iterate(loop=loop)) + + sleep_time = 1.0 / rate_hz + + return from_iterable(self.iterate(loop=loop)).pipe( + ops.zip(interval(sleep_time)), + ops.map(lambda x: x[0] if isinstance(x, tuple) else x), + ) + + +class SensorStorage(Generic[T]): + """Generic sensor data storage utility + . + Creates a directory in the test data directory and stores pickled sensor data. + + Args: + name: The name of the storage directory + autocast: Optional function that takes data and returns a processed result before storage. + """ + + def __init__(self, name: str, autocast: Callable[[T], Any] | None = None) -> None: + self.name = name + self.autocast = autocast + self.cnt = 0 + + # Create storage directory in the data dir + self.root_dir = _get_data_dir() / name + + # Check if directory exists and is not empty + if self.root_dir.exists(): + existing_files = list(self.root_dir.glob("*.pickle")) + if existing_files: + raise RuntimeError( + f"Storage directory '{name}' already exists and contains {len(existing_files)} files. " + f"Please use a different name or clean the directory first." + ) + else: + # Create the directory + self.root_dir.mkdir(parents=True, exist_ok=True) + + def consume_stream(self, observable: Observable[T | Any]) -> None: + """Consume an observable stream of sensor data without saving.""" + return observable.subscribe(self.save_one) + + def save_stream(self, observable: Observable[T | Any]) -> Observable[int]: + """Save an observable stream of sensor data to pickle files.""" + return observable.pipe(ops.map(lambda frame: self.save_one(frame))) + + def save(self, *frames) -> int: + """Save one or more frames to pickle files.""" + for frame in frames: + self.save_one(frame) + return self.cnt + + def save_one(self, frame) -> int: + """Save a single frame to a pickle file.""" + file_name = f"{self.cnt:03d}.pickle" + full_path = self.root_dir / file_name + + if full_path.exists(): + raise RuntimeError(f"File {full_path} already exists") + + # Apply autocast if provided + data_to_save = frame + if self.autocast: + data_to_save = self.autocast(frame) + # Convert to raw message if frame has a raw_msg attribute + elif hasattr(frame, "raw_msg"): + data_to_save = frame.raw_msg + + with open(full_path, "wb") as f: + pickle.dump(data_to_save, f) + + self.cnt += 1 + return self.cnt + + +class TimedSensorStorage(SensorStorage[T]): + def save_one(self, frame: T) -> int: + return super().save_one((time.time(), frame)) + + +class TimedSensorReplay(SensorReplay[T]): + def load_one(self, name: int | str | Path) -> T | Any: + if isinstance(name, int): + full_path = self.root_dir / f"/{name:03d}.pickle" + elif isinstance(name, Path): + full_path = name + else: + full_path = self.root_dir / Path(f"{name}.pickle") + + with open(full_path, "rb") as f: + data = pickle.load(f) + if self.autocast: + return (data[0], self.autocast(data[1])) + return data + + def find_closest(self, timestamp: float, tolerance: float | None = None) -> T | Any | None: + """Find the frame closest to the given timestamp. + + Args: + timestamp: The target timestamp to search for + tolerance: Optional maximum time difference allowed + + Returns: + The data frame closest to the timestamp, or None if no match within tolerance + """ + closest_data = None + closest_diff = float("inf") + + # Check frames before and after the timestamp + for ts, data in self.iterate_ts(): + diff = abs(ts - timestamp) + + if diff < closest_diff: + closest_diff = diff + closest_data = data + elif diff > closest_diff: + # We're moving away from the target, can stop + break + + if tolerance is not None and closest_diff > tolerance: + return None + + return closest_data + + def find_closest_seek( + self, relative_seconds: float, tolerance: float | None = None + ) -> T | Any | None: + """Find the frame closest to a time relative to the start. + + Args: + relative_seconds: Seconds from the start of the dataset + tolerance: Optional maximum time difference allowed + + Returns: + The data frame closest to the relative timestamp, or None if no match within tolerance + """ + # Get the first timestamp + first_ts = self.first_timestamp() + if first_ts is None: + return None + + # Calculate absolute timestamp and use find_closest + target_timestamp = first_ts + relative_seconds + return self.find_closest(target_timestamp, tolerance) + + def first_timestamp(self) -> float | None: + """Get the timestamp of the first item in the dataset. + + Returns: + The first timestamp, or None if dataset is empty + """ + try: + ts, _ = next(self.iterate_ts()) + return ts + except StopIteration: + return None + + def iterate(self, loop: bool = False) -> Iterator[T | Any]: + return (x[1] for x in super().iterate(loop=loop)) + + def iterate_ts( + self, + seek: float | None = None, + duration: float | None = None, + from_timestamp: float | None = None, + loop: bool = False, + ) -> Iterator[tuple[float, T] | Any]: + first_ts = None + if (seek is not None) or (duration is not None): + first_ts = self.first_timestamp() + if first_ts is None: + return + + if seek is not None: + from_timestamp = first_ts + seek + + end_timestamp = None + if duration is not None: + end_timestamp = (from_timestamp if from_timestamp else first_ts) + duration + + while True: + for ts, data in super().iterate(): + if from_timestamp is None or ts >= from_timestamp: + if end_timestamp is not None and ts >= end_timestamp: + break + yield (ts, data) + if not loop: + break + + def stream( + self, + speed: float = 1.0, + seek: float | None = None, + duration: float | None = None, + from_timestamp: float | None = None, + loop: bool = False, + ) -> Observable[T | Any]: + def _subscribe(observer, scheduler=None): + from reactivex.disposable import CompositeDisposable, Disposable + + scheduler = scheduler or TimeoutScheduler() + disp = CompositeDisposable() + is_disposed = False + + iterator = self.iterate_ts( + seek=seek, duration=duration, from_timestamp=from_timestamp, loop=loop + ) + + # Get first message + try: + first_ts, first_data = next(iterator) + except StopIteration: + observer.on_completed() + return Disposable() + + # Establish timing reference + start_local_time = time.time() + start_replay_time = first_ts + + # Emit first sample immediately + observer.on_next(first_data) + + # Pre-load next message + try: + next_message = next(iterator) + except StopIteration: + observer.on_completed() + return disp + + def schedule_emission(message) -> None: + nonlocal next_message, is_disposed + + if is_disposed: + return + + ts, data = message + + # Pre-load the following message while we have time + try: + next_message = next(iterator) + except StopIteration: + next_message = None + + # Calculate absolute emission time + target_time = start_local_time + (ts - start_replay_time) / speed + delay = max(0.0, target_time - time.time()) + + def emit() -> None: + if is_disposed: + return + observer.on_next(data) + if next_message is not None: + schedule_emission(next_message) + else: + observer.on_completed() + # Dispose of the scheduler to clean up threads + if hasattr(scheduler, "dispose"): + scheduler.dispose() + + disp.add(scheduler.schedule_relative(delay, lambda sc, _: emit())) + + schedule_emission(next_message) + + # Create a custom disposable that properly cleans up + def dispose() -> None: + nonlocal is_disposed + is_disposed = True + disp.dispose() + # Ensure scheduler is disposed to clean up any threads + if hasattr(scheduler, "dispose"): + scheduler.dispose() + + return Disposable(dispose) + + from reactivex import create + + return create(_subscribe) diff --git a/dimos/utils/threadpool.py b/dimos/utils/threadpool.py index 53b6f0de1f..45625e9980 100644 --- a/dimos/utils/threadpool.py +++ b/dimos/utils/threadpool.py @@ -18,9 +18,11 @@ ReactiveX scheduler, ensuring consistent thread management across the application. """ -import os import multiprocessing +import os + from reactivex.scheduler import ThreadPoolScheduler + from .logging_config import logger @@ -31,15 +33,15 @@ def get_max_workers() -> int: int: The number of workers, configurable via the DIMOS_MAX_WORKERS environment variable, defaulting to 4 times the CPU count. """ - env_value = os.getenv('DIMOS_MAX_WORKERS', '') - return int(env_value) if env_value.strip() else multiprocessing.cpu_count() * 4 + env_value = os.getenv("DIMOS_MAX_WORKERS", "") + return int(env_value) if env_value.strip() else multiprocessing.cpu_count() # Create a ThreadPoolScheduler with a configurable number of workers. try: max_workers = get_max_workers() scheduler = ThreadPoolScheduler(max_workers=max_workers) - logger.info(f"Using {max_workers} workers") + # logger.info(f"Using {max_workers} workers") except Exception as e: logger.error(f"Failed to initialize ThreadPoolScheduler: {e}") raise @@ -57,6 +59,7 @@ def get_scheduler() -> ThreadPoolScheduler: """ return scheduler + def make_single_thread_scheduler() -> ThreadPoolScheduler: """Create a new ThreadPoolScheduler with a single worker. diff --git a/dimos/utils/transform_utils.py b/dimos/utils/transform_utils.py new file mode 100644 index 0000000000..21421b4390 --- /dev/null +++ b/dimos/utils/transform_utils.py @@ -0,0 +1,386 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import numpy as np +from scipy.spatial.transform import Rotation as R + +from dimos.msgs.geometry_msgs import Pose, Quaternion, Transform, Vector3 + + +def normalize_angle(angle: float) -> float: + """Normalize angle to [-pi, pi] range""" + return np.arctan2(np.sin(angle), np.cos(angle)) + + +def pose_to_matrix(pose: Pose) -> np.ndarray: + """ + Convert pose to 4x4 homogeneous transform matrix. + + Args: + pose: Pose object with position and orientation (quaternion) + + Returns: + 4x4 transformation matrix + """ + # Extract position + tx, ty, tz = pose.position.x, pose.position.y, pose.position.z + + # Create rotation matrix from quaternion using scipy + quat = [pose.orientation.x, pose.orientation.y, pose.orientation.z, pose.orientation.w] + + # Check for zero norm quaternion and use identity if invalid + quat_norm = np.linalg.norm(quat) + if quat_norm == 0.0: + # Use identity quaternion [0, 0, 0, 1] if zero norm detected + quat = [0.0, 0.0, 0.0, 1.0] + + rotation = R.from_quat(quat) + Rot = rotation.as_matrix() + + # Create 4x4 transform + T = np.eye(4) + T[:3, :3] = Rot + T[:3, 3] = [tx, ty, tz] + + return T + + +def matrix_to_pose(T: np.ndarray) -> Pose: + """ + Convert 4x4 transformation matrix to Pose object. + + Args: + T: 4x4 transformation matrix + + Returns: + Pose object with position and orientation (quaternion) + """ + # Extract position + pos = Vector3(T[0, 3], T[1, 3], T[2, 3]) + + # Extract rotation matrix and convert to quaternion + Rot = T[:3, :3] + rotation = R.from_matrix(Rot) + quat = rotation.as_quat() # Returns [x, y, z, w] + + orientation = Quaternion(quat[0], quat[1], quat[2], quat[3]) + + return Pose(pos, orientation) + + +def apply_transform(pose: Pose, transform: np.ndarray | Transform) -> Pose: + """ + Apply a transformation matrix to a pose. + + Args: + pose: Input pose + transform_matrix: 4x4 transformation matrix to apply + + Returns: + Transformed pose + """ + if isinstance(transform, Transform): + if transform.child_frame_id != pose.frame_id: + raise ValueError( + f"Transform frame_id {transform.frame_id} does not match pose frame_id {pose.frame_id}" + ) + transform = pose_to_matrix(transform.to_pose()) + + # Convert pose to matrix + T_pose = pose_to_matrix(pose) + + # Apply transform + T_result = transform @ T_pose + + # Convert back to pose + return matrix_to_pose(T_result) + + +def optical_to_robot_frame(pose: Pose) -> Pose: + """ + Convert pose from optical camera frame to robot frame convention. + + Optical Camera Frame (e.g., ZED): + - X: Right + - Y: Down + - Z: Forward (away from camera) + + Robot Frame (ROS/REP-103): + - X: Forward + - Y: Left + - Z: Up + + Args: + pose: Pose in optical camera frame + + Returns: + Pose in robot frame + """ + # Position transformation + robot_x = pose.position.z # Forward = Camera Z + robot_y = -pose.position.x # Left = -Camera X + robot_z = -pose.position.y # Up = -Camera Y + + # Rotation transformation using quaternions + # First convert quaternion to rotation matrix + quat_optical = [pose.orientation.x, pose.orientation.y, pose.orientation.z, pose.orientation.w] + R_optical = R.from_quat(quat_optical).as_matrix() + + # Coordinate frame transformation matrix from optical to robot + # X_robot = Z_optical, Y_robot = -X_optical, Z_robot = -Y_optical + T_frame = np.array( + [ + [0, 0, 1], # X_robot = Z_optical + [-1, 0, 0], # Y_robot = -X_optical + [0, -1, 0], # Z_robot = -Y_optical + ] + ) + + # Transform the rotation matrix + R_robot = T_frame @ R_optical @ T_frame.T + + # Convert back to quaternion + quat_robot = R.from_matrix(R_robot).as_quat() # [x, y, z, w] + + return Pose( + Vector3(robot_x, robot_y, robot_z), + Quaternion(quat_robot[0], quat_robot[1], quat_robot[2], quat_robot[3]), + ) + + +def robot_to_optical_frame(pose: Pose) -> Pose: + """ + Convert pose from robot frame to optical camera frame convention. + This is the inverse of optical_to_robot_frame. + + Args: + pose: Pose in robot frame + + Returns: + Pose in optical camera frame + """ + # Position transformation (inverse) + optical_x = -pose.position.y # Right = -Left + optical_y = -pose.position.z # Down = -Up + optical_z = pose.position.x # Forward = Forward + + # Rotation transformation using quaternions + quat_robot = [pose.orientation.x, pose.orientation.y, pose.orientation.z, pose.orientation.w] + R_robot = R.from_quat(quat_robot).as_matrix() + + # Coordinate frame transformation matrix from Robot to optical (inverse of optical to Robot) + # This is the transpose of the forward transformation + T_frame_inv = np.array( + [ + [0, -1, 0], # X_optical = -Y_robot + [0, 0, -1], # Y_optical = -Z_robot + [1, 0, 0], # Z_optical = X_robot + ] + ) + + # Transform the rotation matrix + R_optical = T_frame_inv @ R_robot @ T_frame_inv.T + + # Convert back to quaternion + quat_optical = R.from_matrix(R_optical).as_quat() # [x, y, z, w] + + return Pose( + Vector3(optical_x, optical_y, optical_z), + Quaternion(quat_optical[0], quat_optical[1], quat_optical[2], quat_optical[3]), + ) + + +def yaw_towards_point(position: Vector3, target_point: Vector3 = None) -> float: + """ + Calculate yaw angle from target point to position (away from target). + This is commonly used for object orientation in grasping applications. + Assumes robot frame where X is forward and Y is left. + + Args: + position: Current position in robot frame + target_point: Reference point (default: origin) + + Returns: + Yaw angle in radians pointing from target_point to position + """ + if target_point is None: + target_point = Vector3(0.0, 0.0, 0.0) + direction_x = position.x - target_point.x + direction_y = position.y - target_point.y + return np.arctan2(direction_y, direction_x) + + +def create_transform_from_6dof(translation: Vector3, euler_angles: Vector3) -> np.ndarray: + """ + Create a 4x4 transformation matrix from 6DOF parameters. + + Args: + translation: Translation vector [x, y, z] in meters + euler_angles: Euler angles [rx, ry, rz] in radians (XYZ convention) + + Returns: + 4x4 transformation matrix + """ + # Create transformation matrix + T = np.eye(4) + + # Set translation + T[0:3, 3] = [translation.x, translation.y, translation.z] + + # Set rotation using scipy + if np.linalg.norm([euler_angles.x, euler_angles.y, euler_angles.z]) > 1e-6: + rotation = R.from_euler("xyz", [euler_angles.x, euler_angles.y, euler_angles.z]) + T[0:3, 0:3] = rotation.as_matrix() + + return T + + +def invert_transform(T: np.ndarray) -> np.ndarray: + """ + Invert a 4x4 transformation matrix efficiently. + + Args: + T: 4x4 transformation matrix + + Returns: + Inverted 4x4 transformation matrix + """ + # For homogeneous transform matrices, we can use the special structure: + # [R t]^-1 = [R^T -R^T*t] + # [0 1] [0 1 ] + + Rot = T[:3, :3] + t = T[:3, 3] + + T_inv = np.eye(4) + T_inv[:3, :3] = Rot.T + T_inv[:3, 3] = -Rot.T @ t + + return T_inv + + +def compose_transforms(*transforms: np.ndarray) -> np.ndarray: + """ + Compose multiple transformation matrices. + + Args: + *transforms: Variable number of 4x4 transformation matrices + + Returns: + Composed 4x4 transformation matrix (T1 @ T2 @ ... @ Tn) + """ + result = np.eye(4) + for T in transforms: + result = result @ T + return result + + +def euler_to_quaternion(euler_angles: Vector3, degrees: bool = False) -> Quaternion: + """ + Convert euler angles to quaternion. + + Args: + euler_angles: Euler angles as Vector3 [roll, pitch, yaw] in radians (XYZ convention) + + Returns: + Quaternion object [x, y, z, w] + """ + rotation = R.from_euler( + "xyz", [euler_angles.x, euler_angles.y, euler_angles.z], degrees=degrees + ) + quat = rotation.as_quat() # Returns [x, y, z, w] + return Quaternion(quat[0], quat[1], quat[2], quat[3]) + + +def quaternion_to_euler(quaternion: Quaternion, degrees: bool = False) -> Vector3: + """ + Convert quaternion to euler angles. + + Args: + quaternion: Quaternion object [x, y, z, w] + + Returns: + Euler angles as Vector3 [roll, pitch, yaw] in radians (XYZ convention) + """ + quat = [quaternion.x, quaternion.y, quaternion.z, quaternion.w] + rotation = R.from_quat(quat) + euler = rotation.as_euler("xyz", degrees=degrees) # Returns [roll, pitch, yaw] + if not degrees: + return Vector3( + normalize_angle(euler[0]), normalize_angle(euler[1]), normalize_angle(euler[2]) + ) + else: + return Vector3(euler[0], euler[1], euler[2]) + + +def get_distance(pose1: Pose | Vector3, pose2: Pose | Vector3) -> float: + """ + Calculate Euclidean distance between two poses. + + Args: + pose1: First pose + pose2: Second pose + + Returns: + Euclidean distance between the two poses in meters + """ + if hasattr(pose1, "position"): + pose1 = pose1.position + if hasattr(pose2, "position"): + pose2 = pose2.position + + dx = pose1.x - pose2.x + dy = pose1.y - pose2.y + dz = pose1.z - pose2.z + + return np.linalg.norm(np.array([dx, dy, dz])) + + +def offset_distance( + target_pose: Pose, distance: float, approach_vector: Vector3 = Vector3(0, 0, -1) +) -> Pose: + """ + Apply distance offset to target pose along its approach direction. + + This is commonly used in grasping to offset the gripper by a certain distance + along the approach vector before or after grasping. + + Args: + target_pose: Target pose (e.g., grasp pose) + distance: Distance to offset along the approach direction (meters) + + Returns: + Target pose offset by the specified distance along its approach direction + """ + # Convert pose to transformation matrix to extract rotation + T_target = pose_to_matrix(target_pose) + rotation_matrix = T_target[:3, :3] + + # Define the approach vector based on the target pose orientation + # Assuming the gripper approaches along its local -z axis (common for downward grasps) + # You can change this to [1, 0, 0] for x-axis or [0, 1, 0] for y-axis based on your gripper + approach_vector_local = np.array([approach_vector.x, approach_vector.y, approach_vector.z]) + + # Transform approach vector to world coordinates + approach_vector_world = rotation_matrix @ approach_vector_local + + # Apply offset along the approach direction + offset_position = Vector3( + target_pose.position.x + distance * approach_vector_world[0], + target_pose.position.y + distance * approach_vector_world[1], + target_pose.position.z + distance * approach_vector_world[2], + ) + + return Pose(position=offset_position, orientation=target_pose.orientation) diff --git a/dimos/web/command-center-extension/.gitignore b/dimos/web/command-center-extension/.gitignore new file mode 100644 index 0000000000..3f7224ed26 --- /dev/null +++ b/dimos/web/command-center-extension/.gitignore @@ -0,0 +1,5 @@ +*.foxe +/dist +/node_modules +!/package.json +!/package-lock.json diff --git a/dimos/web/command-center-extension/.prettierrc.yaml b/dimos/web/command-center-extension/.prettierrc.yaml new file mode 100644 index 0000000000..e57cc20758 --- /dev/null +++ b/dimos/web/command-center-extension/.prettierrc.yaml @@ -0,0 +1,5 @@ +arrowParens: always +printWidth: 100 +trailingComma: "all" +tabWidth: 2 +semi: true diff --git a/dimos/web/command-center-extension/CHANGELOG.md b/dimos/web/command-center-extension/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/dimos/web/command-center-extension/README.md b/dimos/web/command-center-extension/README.md new file mode 100644 index 0000000000..efee4ec11d --- /dev/null +++ b/dimos/web/command-center-extension/README.md @@ -0,0 +1,17 @@ +# command-center-extension + +This is a Foxglove extension for visualizing robot data and controlling the robot. See `dimos/web/websocket_vis/README.md` for how to use the module in your robot. + +## Build and use + +Install the Foxglove Studio desktop application. + +Install the Node dependencies: + + npm install + +Build the package and install it into Foxglove: + + npm run build && npm run local-install + +To add the panel, go to Foxglove Studio, click on the "Add panel" icon on the top right and select "command-center [local]". diff --git a/dimos/web/command-center-extension/eslint.config.js b/dimos/web/command-center-extension/eslint.config.js new file mode 100644 index 0000000000..63cc3a243a --- /dev/null +++ b/dimos/web/command-center-extension/eslint.config.js @@ -0,0 +1,23 @@ +// @ts-check + +const foxglove = require("@foxglove/eslint-plugin"); +const globals = require("globals"); +const tseslint = require("typescript-eslint"); + +module.exports = tseslint.config({ + files: ["src/**/*.ts", "src/**/*.tsx"], + extends: [foxglove.configs.base, foxglove.configs.react, foxglove.configs.typescript], + languageOptions: { + globals: { + ...globals.es2020, + ...globals.browser, + }, + parserOptions: { + project: "tsconfig.json", + tsconfigRootDir: __dirname, + }, + }, + rules: { + "react-hooks/exhaustive-deps": "error", + }, +}); diff --git a/dimos/web/command-center-extension/package-lock.json b/dimos/web/command-center-extension/package-lock.json new file mode 100644 index 0000000000..771bae9aaa --- /dev/null +++ b/dimos/web/command-center-extension/package-lock.json @@ -0,0 +1,7181 @@ +{ + "name": "command-center-extension", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "command-center-extension", + "version": "0.0.0", + "license": "UNLICENSED", + "dependencies": { + "@types/pako": "^2.0.4", + "d3": "^7.9.0", + "pako": "^2.1.0", + "react-leaflet": "^4.2.1", + "socket.io-client": "^4.8.1" + }, + "devDependencies": { + "@foxglove/eslint-plugin": "2.1.0", + "@foxglove/extension": "2.34.0", + "@types/d3": "^7.4.3", + "@types/leaflet": "^1.9.20", + "@types/react": "18.3.24", + "@types/react-dom": "18.3.7", + "create-foxglove-extension": "1.0.6", + "eslint": "9.34.0", + "prettier": "3.6.2", + "react": "18.3.1", + "react-dom": "^18.3.1", + "typescript": "5.9.2" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/compat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.3.2.tgz", + "integrity": "sha512-jRNwzTbd6p2Rw4sZ1CgWRS8YMtqG15YyZf7zvb6gY2rB2u6n+2Z+ELW0GtL0fQgyl0pr4Y/BzBfng/BdsereRA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^8.40 || 9" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", + "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@foxglove/eslint-plugin": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@foxglove/eslint-plugin/-/eslint-plugin-2.1.0.tgz", + "integrity": "sha512-EQrEns2BneSY7ODsOnJ6YIvn6iOVhwypHT4OwrzuPX2jqncghF7BXypkdDP3KlFtyDGC1+ff3+VXZMmyc8vpfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint/compat": "^1", + "@eslint/js": "^9", + "@typescript-eslint/utils": "^8", + "eslint-config-prettier": "^9", + "eslint-plugin-es": "^4", + "eslint-plugin-filenames": "^1", + "eslint-plugin-import": "^2", + "eslint-plugin-jest": "^28", + "eslint-plugin-prettier": "^5", + "eslint-plugin-react": "^7", + "eslint-plugin-react-hooks": "^5", + "tsutils": "^3", + "typescript-eslint": "^8" + }, + "peerDependencies": { + "eslint": "^9.27.0" + } + }, + "node_modules/@foxglove/extension": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@foxglove/extension/-/extension-2.34.0.tgz", + "integrity": "sha512-muZGa//A4gsNVRjwZevwvnSqQdabCJePdh75VFm5LhEb0fkP7VXjU3Rzh84EHRJvkUctiV7IbiI9OAPJmENGeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@react-leaflet/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", + "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/leaflet": { + "version": "1.9.20", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.20.tgz", + "integrity": "sha512-rooalPMlk61LCaLOvBF2VIf9M47HgMQqi5xQ9QRi7c8PkdIe0WrIi5IxXUXQjAdL0c+vcQ01mYWbthzmp9GHWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.24", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz", + "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz", + "integrity": "sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/type-utils": "8.40.0", + "@typescript-eslint/utils": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.40.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz", + "integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz", + "integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.40.0", + "@typescript-eslint/types": "^8.40.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz", + "integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz", + "integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz", + "integrity": "sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0", + "@typescript-eslint/utils": "8.40.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz", + "integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz", + "integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.40.0", + "@typescript-eslint/tsconfig-utils": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.40.0.tgz", + "integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz", + "integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.40.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", + "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz", + "integrity": "sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001735", + "electron-to-chromium": "^1.5.204", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001737", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz", + "integrity": "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clean-webpack-plugin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0.tgz", + "integrity": "sha512-WuWE1nyTNAyW5T7oNyys2EN0cfP2fdRxhxnIQWiAp0bMabPdHhoGxM8A6YL2GhqwgrPnnaemVE7nv5XJ2Fhh2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "del": "^4.1.1" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "webpack": ">=4.0.0 <6.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-foxglove-extension": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/create-foxglove-extension/-/create-foxglove-extension-1.0.6.tgz", + "integrity": "sha512-Gp0qOQ+nU6dkqgpQlEdqdYVL4PJtdG+HXnfw09npEJCGT9M+5KFLj9V6Xt07oV3rSO/vthoTKPLR6xAD/+nPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-webpack-plugin": "4.0.0", + "commander": "12.1.0", + "jszip": "3.10.1", + "mkdirp": "3.0.1", + "ncp": "2.0.0", + "node-fetch": "2.7.0", + "path-browserify": "1.0.1", + "rimraf": "6.0.1", + "sanitize-filename": "1.6.3", + "ts-loader": "9.5.1", + "webpack": "5.96.1" + }, + "bin": { + "create-foxglove-extension": "dist/bin/create-foxglove-extension.js", + "foxglove-extension": "dist/bin/foxglove-extension.js" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/del/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.208", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.208.tgz", + "integrity": "sha512-ozZyibehoe7tOhNaf16lKmljVf+3npZcJIEbJRVftVsmAg5TeA1mGS9dVCZzOwr2xT7xK15V0p7+GZqSPgkuPg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", + "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.34.0", + "@eslint/plugin-kit": "^0.3.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-es": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", + "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-filenames": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-filenames/-/eslint-plugin-filenames-1.3.2.tgz", + "integrity": "sha512-tqxJTiEM5a0JmRCUYQmxw23vtTxrb2+a3Q2mMOPhFxvt7ZQQJmdiuMby9B/vUAuVMghyP7oET+nIf6EO6CBd/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.camelcase": "4.3.0", + "lodash.kebabcase": "4.1.1", + "lodash.snakecase": "4.1.1", + "lodash.upperfirst": "4.3.1" + }, + "peerDependencies": { + "eslint": "*" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "28.14.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.14.0.tgz", + "integrity": "sha512-P9s/qXSMTpRTerE2FQ0qJet2gKbcGyFTPAJipoKxmWqR6uuFqIqk8FuEfg5yBieOezVrEfAMZrEwJ6yEp+1MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "engines": { + "node": "^16.10.0 || ^18.12.0 || >=20.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/globby/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-path-inside": "^2.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-is-inside": "^1.0.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "dev": true, + "license": "MIT", + "bin": { + "ncp": "bin/ncp" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-leaflet": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz", + "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==", + "license": "Hippocratic-2.1", + "dependencies": { + "@react-leaflet/core": "^2.1.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tapable": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", + "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.43.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", + "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-loader": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", + "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.40.0.tgz", + "integrity": "sha512-Xvd2l+ZmFDPEt4oj1QEXzA4A2uUK6opvKu3eGN9aGjB8au02lIVcLyi375w94hHyejTOmzIU77L8ol2sRg9n7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.40.0", + "@typescript-eslint/parser": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0", + "@typescript-eslint/utils": "8.40.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.96.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", + "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/dimos/web/command-center-extension/package.json b/dimos/web/command-center-extension/package.json new file mode 100644 index 0000000000..36eb7854c4 --- /dev/null +++ b/dimos/web/command-center-extension/package.json @@ -0,0 +1,42 @@ +{ + "name": "command-center-extension", + "displayName": "command-center-extension", + "description": "", + "publisher": "dimensional", + "homepage": "", + "version": "0.0.0", + "license": "UNLICENSED", + "main": "./dist/extension.js", + "keywords": [], + "scripts": { + "build": "foxglove-extension build", + "foxglove:prepublish": "foxglove-extension build --mode production", + "lint": "eslint .", + "lint:ci": "eslint .", + "lint:fix": "eslint --fix .", + "local-install": "foxglove-extension install", + "package": "foxglove-extension package", + "pretest": "foxglove-extension pretest" + }, + "devDependencies": { + "@foxglove/eslint-plugin": "2.1.0", + "@foxglove/extension": "2.34.0", + "@types/d3": "^7.4.3", + "@types/leaflet": "^1.9.20", + "@types/react": "18.3.24", + "@types/react-dom": "18.3.7", + "create-foxglove-extension": "1.0.6", + "eslint": "9.34.0", + "prettier": "3.6.2", + "react": "18.3.1", + "react-dom": "^18.3.1", + "typescript": "5.9.2" + }, + "dependencies": { + "@types/pako": "^2.0.4", + "d3": "^7.9.0", + "pako": "^2.1.0", + "react-leaflet": "^4.2.1", + "socket.io-client": "^4.8.1" + } +} diff --git a/dimos/web/command-center-extension/src/App.tsx b/dimos/web/command-center-extension/src/App.tsx new file mode 100644 index 0000000000..838f15df59 --- /dev/null +++ b/dimos/web/command-center-extension/src/App.tsx @@ -0,0 +1,115 @@ +import * as React from "react"; + +import Connection from "./Connection"; +import ExplorePanel from "./ExplorePanel"; +import GpsButton from "./GpsButton"; +import KeyboardControlPanel from "./KeyboardControlPanel"; +import VisualizerWrapper from "./components/VisualizerWrapper"; +import LeafletMap from "./components/LeafletMap"; +import { AppAction, AppState, LatLon } from "./types"; + +function appReducer(state: AppState, action: AppAction): AppState { + switch (action.type) { + case "SET_COSTMAP": + return { ...state, costmap: action.payload }; + case "SET_ROBOT_POSE": + return { ...state, robotPose: action.payload }; + case "SET_GPS_LOCATION": + return { ...state, gpsLocation: action.payload }; + case "SET_GPS_TRAVEL_GOAL_POINTS": + return { ...state, gpsTravelGoalPoints: action.payload }; + case "SET_PATH": + return { ...state, path: action.payload }; + case "SET_FULL_STATE": + return { ...state, ...action.payload }; + default: + return state; + } +} + +const initialState: AppState = { + costmap: null, + robotPose: null, + gpsLocation: null, + gpsTravelGoalPoints: null, + path: null, +}; + +export default function App(): React.ReactElement { + const [state, dispatch] = React.useReducer(appReducer, initialState); + const [isGpsMode, setIsGpsMode] = React.useState(false); + const connectionRef = React.useRef(null); + + React.useEffect(() => { + connectionRef.current = new Connection(dispatch); + + return () => { + if (connectionRef.current) { + connectionRef.current.disconnect(); + } + }; + }, []); + + const handleWorldClick = React.useCallback((worldX: number, worldY: number) => { + connectionRef.current?.worldClick(worldX, worldY); + }, []); + + const handleStartExplore = React.useCallback(() => { + connectionRef.current?.startExplore(); + }, []); + + const handleStopExplore = React.useCallback(() => { + connectionRef.current?.stopExplore(); + }, []); + + const handleGpsGoal = React.useCallback((goal: LatLon) => { + connectionRef.current?.sendGpsGoal(goal); + }, []); + + const handleSendMoveCommand = React.useCallback( + (linear: [number, number, number], angular: [number, number, number]) => { + connectionRef.current?.sendMoveCommand(linear, angular); + }, + [], + ); + + const handleStopMoveCommand = React.useCallback(() => { + connectionRef.current?.stopMoveCommand(); + }, []); + + return ( +
+ {isGpsMode ? ( + + ) : ( + + )} +
+ setIsGpsMode(true)} + onUseCostmap={() => setIsGpsMode(false)} + > + + +
+
+ ); +} diff --git a/dimos/web/command-center-extension/src/Button.tsx b/dimos/web/command-center-extension/src/Button.tsx new file mode 100644 index 0000000000..8714bb8611 --- /dev/null +++ b/dimos/web/command-center-extension/src/Button.tsx @@ -0,0 +1,24 @@ +interface ButtonProps { + onClick: () => void; + isActive: boolean; + children: React.ReactNode; +} + +export default function Button({ onClick, isActive, children }: ButtonProps): React.ReactElement { + return ( + + ); +} diff --git a/dimos/web/command-center-extension/src/Connection.ts b/dimos/web/command-center-extension/src/Connection.ts new file mode 100644 index 0000000000..7a23c6b98c --- /dev/null +++ b/dimos/web/command-center-extension/src/Connection.ts @@ -0,0 +1,110 @@ +import { io, Socket } from "socket.io-client"; + +import { + AppAction, + Costmap, + EncodedCostmap, + EncodedPath, + EncodedVector, + FullStateData, + LatLon, + Path, + TwistCommand, + Vector, +} from "./types"; + +export default class Connection { + socket: Socket; + dispatch: React.Dispatch; + + constructor(dispatch: React.Dispatch) { + this.dispatch = dispatch; + this.socket = io("ws://localhost:7779"); + + this.socket.on("costmap", (data: EncodedCostmap) => { + const costmap = Costmap.decode(data); + this.dispatch({ type: "SET_COSTMAP", payload: costmap }); + }); + + this.socket.on("robot_pose", (data: EncodedVector) => { + const robotPose = Vector.decode(data); + this.dispatch({ type: "SET_ROBOT_POSE", payload: robotPose }); + }); + + this.socket.on("gps_location", (data: LatLon) => { + this.dispatch({ type: "SET_GPS_LOCATION", payload: data }); + }); + + this.socket.on("gps_travel_goal_points", (data: LatLon[]) => { + this.dispatch({ type: "SET_GPS_TRAVEL_GOAL_POINTS", payload: data }); + }); + + this.socket.on("path", (data: EncodedPath) => { + const path = Path.decode(data); + this.dispatch({ type: "SET_PATH", payload: path }); + }); + + this.socket.on("full_state", (data: FullStateData) => { + const state: Partial<{ costmap: Costmap; robotPose: Vector; gpsLocation: LatLon; gpsTravelGoalPoints: LatLon[]; path: Path }> = {}; + + if (data.costmap != undefined) { + state.costmap = Costmap.decode(data.costmap); + } + if (data.robot_pose != undefined) { + state.robotPose = Vector.decode(data.robot_pose); + } + if (data.gps_location != undefined) { + state.gpsLocation = data.gps_location; + } + if (data.path != undefined) { + state.path = Path.decode(data.path); + } + + this.dispatch({ type: "SET_FULL_STATE", payload: state }); + }); + } + + worldClick(worldX: number, worldY: number): void { + this.socket.emit("click", [worldX, worldY]); + } + + startExplore(): void { + this.socket.emit("start_explore"); + } + + stopExplore(): void { + this.socket.emit("stop_explore"); + } + + sendMoveCommand(linear: [number, number, number], angular: [number, number, number]): void { + const twist: TwistCommand = { + linear: { + x: linear[0], + y: linear[1], + z: linear[2], + }, + angular: { + x: angular[0], + y: angular[1], + z: angular[2], + }, + }; + this.socket.emit("move_command", twist); + } + + sendGpsGoal(goal: LatLon): void { + this.socket.emit("gps_goal", goal); + } + + stopMoveCommand(): void { + const twist: TwistCommand = { + linear: { x: 0, y: 0, z: 0 }, + angular: { x: 0, y: 0, z: 0 }, + }; + this.socket.emit("move_command", twist); + } + + disconnect(): void { + this.socket.disconnect(); + } +} diff --git a/dimos/web/command-center-extension/src/ExplorePanel.tsx b/dimos/web/command-center-extension/src/ExplorePanel.tsx new file mode 100644 index 0000000000..6210664591 --- /dev/null +++ b/dimos/web/command-center-extension/src/ExplorePanel.tsx @@ -0,0 +1,41 @@ +import * as React from "react"; + +import Button from "./Button"; + +interface ExplorePanelProps { + onStartExplore: () => void; + onStopExplore: () => void; +} + +export default function ExplorePanel({ + onStartExplore, + onStopExplore, +}: ExplorePanelProps): React.ReactElement { + const [exploring, setExploring] = React.useState(false); + + return ( +
+ {exploring ? ( + + ) : ( + + )} +
+ ); +} diff --git a/dimos/web/command-center-extension/src/GpsButton.tsx b/dimos/web/command-center-extension/src/GpsButton.tsx new file mode 100644 index 0000000000..74f0d73dfd --- /dev/null +++ b/dimos/web/command-center-extension/src/GpsButton.tsx @@ -0,0 +1,41 @@ +import * as React from "react"; + +import Button from "./Button"; + +interface GpsButtonProps { + onUseGps: () => void; + onUseCostmap: () => void; +} + +export default function GpsButton({ + onUseGps, + onUseCostmap, +}: GpsButtonProps): React.ReactElement { + const [gps, setGps] = React.useState(false); + + return ( +
+ {gps ? ( + + ) : ( + + )} +
+ ); +} diff --git a/dimos/web/command-center-extension/src/KeyboardControlPanel.tsx b/dimos/web/command-center-extension/src/KeyboardControlPanel.tsx new file mode 100644 index 0000000000..d4f5402557 --- /dev/null +++ b/dimos/web/command-center-extension/src/KeyboardControlPanel.tsx @@ -0,0 +1,167 @@ +import * as React from "react"; + +import Button from "./Button"; + +interface KeyboardControlPanelProps { + onSendMoveCommand: (linear: [number, number, number], angular: [number, number, number]) => void; + onStopMoveCommand: () => void; +} + +const linearSpeed = 0.5; +const angularSpeed = 0.8; +const publishRate = 10.0; // Hz + +function calculateVelocities(keys: Set) { + let linearX = 0.0; + let linearY = 0.0; + let angularY = 0.0; + let angularZ = 0.0; + + let speedMultiplier = 1.0; + if (keys.has("Shift")) { + speedMultiplier = 2.0; // Boost mode + } else if (keys.has("Control")) { + speedMultiplier = 0.5; // Slow mode + } + + // Check for stop command (space) + if (keys.has(" ")) { + return { linearX: 0, linearY: 0, angularY: 0, angularZ: 0 }; + } + + // Linear X (forward/backward) - W/S + if (keys.has("w")) { + linearX = linearSpeed * speedMultiplier; + } else if (keys.has("s")) { + linearX = -linearSpeed * speedMultiplier; + } + + // Angular Z (yaw/turn) - A/D + if (keys.has("a")) { + angularZ = angularSpeed * speedMultiplier; + } else if (keys.has("d")) { + angularZ = -angularSpeed * speedMultiplier; + } + + // Linear Y (strafe) - Left/Right arrows + if (keys.has("ArrowLeft")) { + linearY = linearSpeed * speedMultiplier; + } else if (keys.has("ArrowRight")) { + linearY = -linearSpeed * speedMultiplier; + } + + // Angular Y (pitch) - Up/Down arrows + if (keys.has("ArrowUp")) { + angularY = angularSpeed * speedMultiplier; + } else if (keys.has("ArrowDown")) { + angularY = -angularSpeed * speedMultiplier; + } + + return { linearX, linearY, angularY, angularZ }; +} + +export default function KeyboardControlPanel({ + onSendMoveCommand, + onStopMoveCommand, +}: KeyboardControlPanelProps): React.ReactElement { + const [isActive, setIsActive] = React.useState(false); + const keysPressed = React.useRef>(new Set()); + const intervalRef = React.useRef(null); + + const handleKeyDown = React.useCallback((event: KeyboardEvent) => { + // Prevent default for arrow keys and space to avoid scrolling + if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", " "].includes(event.key)) { + event.preventDefault(); + } + + const normalizedKey = event.key.length === 1 ? event.key.toLowerCase() : event.key; + keysPressed.current.add(normalizedKey); + }, []); + + const handleKeyUp = React.useCallback((event: KeyboardEvent) => { + const normalizedKey = event.key.length === 1 ? event.key.toLowerCase() : event.key; + keysPressed.current.delete(normalizedKey); + }, []); + + // Start/stop keyboard control + React.useEffect(() => { + keysPressed.current.clear(); + + if (!isActive) { + return undefined; + } + + document.addEventListener("keydown", handleKeyDown); + document.addEventListener("keyup", handleKeyUp); + + // Start publishing loop + intervalRef.current = setInterval(() => { + const velocities = calculateVelocities(keysPressed.current); + + onSendMoveCommand( + [velocities.linearX, velocities.linearY, 0], + [0, velocities.angularY, velocities.angularZ], + ); + }, 1000 / publishRate); + + return () => { + document.removeEventListener("keydown", handleKeyDown); + document.removeEventListener("keyup", handleKeyUp); + + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + + keysPressed.current.clear(); + onStopMoveCommand(); + }; + }, [isActive, handleKeyDown, handleKeyUp, onSendMoveCommand, onStopMoveCommand]); + + const toggleKeyboardControl = () => { + if (isActive) { + keysPressed.current.clear(); + setIsActive(false); + } else { + setIsActive(true); + } + }; + + React.useEffect(() => { + const handleBlur = () => { + if (isActive) { + keysPressed.current.clear(); + setIsActive(false); + } + }; + + const handleFocus = () => { + // Clear keys when window regains focus to avoid stuck keys + keysPressed.current.clear(); + }; + + window.addEventListener("blur", handleBlur); + window.addEventListener("focus", handleFocus); + + return () => { + window.removeEventListener("blur", handleBlur); + window.removeEventListener("focus", handleFocus); + }; + }, [isActive]); + + return ( +
+ {isActive && ( +
+
Controls:
+
W/S: Forward/Backward | A/D: Turn
+
Arrows: Strafe/Pitch | Space: Stop
+
Shift: Boost | Ctrl: Slow
+
+ )} + +
+ ); +} diff --git a/dimos/web/command-center-extension/src/components/CostmapLayer.tsx b/dimos/web/command-center-extension/src/components/CostmapLayer.tsx new file mode 100644 index 0000000000..3881f6f0d5 --- /dev/null +++ b/dimos/web/command-center-extension/src/components/CostmapLayer.tsx @@ -0,0 +1,165 @@ +import * as d3 from "d3"; +import * as React from "react"; + +import { Costmap } from "../types"; +import GridLayer from "./GridLayer"; + +interface CostmapLayerProps { + costmap: Costmap; + width: number; + height: number; +} + +const CostmapLayer = React.memo(({ costmap, width, height }) => { + const canvasRef = React.useRef(null); + const { grid, origin, resolution } = costmap; + const rows = Math.max(1, grid.shape[0] || 1); + const cols = Math.max(1, grid.shape[1] || 1); + + const axisMargin = { left: 60, bottom: 40 }; + const availableWidth = Math.max(1, width - axisMargin.left); + const availableHeight = Math.max(1, height - axisMargin.bottom); + + const cell = Math.max(0, Math.min(availableWidth / cols, availableHeight / rows)); + const gridW = Math.max(0, cols * cell); + const gridH = Math.max(0, rows * cell); + const offsetX = axisMargin.left + (availableWidth - gridW) / 2; + const offsetY = (availableHeight - gridH) / 2; + + // Pre-compute color lookup table using exact D3 colors (computed once on mount) + const colorLookup = React.useMemo(() => { + const lookup = new Uint8ClampedArray(256 * 3); // RGB values for -1 to 254 (255 total values) + + const customColorScale = (t: number) => { + if (t === 0) { + return "black"; + } + if (t < 0) { + return "#2d2136"; + } + if (t > 0.95) { + return "#000000"; + } + + const color = d3.interpolateTurbo(t * 2 - 1); + const hsl = d3.hsl(color); + hsl.s *= 0.75; + return hsl.toString(); + }; + + const colour = d3.scaleSequential(customColorScale).domain([-1, 100]); + + // Pre-compute all 256 possible color values + for (let i = 0; i < 256; i++) { + const value = i === 255 ? -1 : i; + const colorStr = colour(value); + const c = d3.color(colorStr); + + if (c) { + const rgb = c as d3.RGBColor; + lookup[i * 3] = rgb.r; + lookup[i * 3 + 1] = rgb.g; + lookup[i * 3 + 2] = rgb.b; + } else { + lookup[i * 3] = 0; + lookup[i * 3 + 1] = 0; + lookup[i * 3 + 2] = 0; + } + } + + return lookup; + }, []); + + React.useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) { + return; + } + + // Validate grid data length matches dimensions + const expectedLength = rows * cols; + if (grid.data.length !== expectedLength) { + console.warn( + `Grid data length mismatch: expected ${expectedLength}, got ${grid.data.length} (rows=${rows}, cols=${cols})` + ); + } + + canvas.width = cols; + canvas.height = rows; + const ctx = canvas.getContext("2d"); + if (!ctx) { + return; + } + + const img = ctx.createImageData(cols, rows); + const data = grid.data; + const imgData = img.data; + + for (let i = 0; i < data.length && i < rows * cols; i++) { + const row = Math.floor(i / cols); + const col = i % cols; + const invertedRow = rows - 1 - row; + const srcIdx = invertedRow * cols + col; + + if (srcIdx < 0 || srcIdx >= data.length) { + continue; + } + + const value = data[i]!; + // Map value to lookup index (handle -1 -> 255 mapping) + const lookupIdx = value === -1 ? 255 : Math.min(254, Math.max(0, value)); + + const o = srcIdx * 4; + if (o < 0 || o + 3 >= imgData.length) { + continue; + } + + // Use pre-computed colors from lookup table + const colorOffset = lookupIdx * 3; + imgData[o] = colorLookup[colorOffset]!; + imgData[o + 1] = colorLookup[colorOffset + 1]!; + imgData[o + 2] = colorLookup[colorOffset + 2]!; + imgData[o + 3] = 255; + } + + ctx.putImageData(img, 0, 0); + }, [grid.data, cols, rows, colorLookup]); + + return ( + + +
+ +
+
+ +
+ ); +}); + +CostmapLayer.displayName = "CostmapLayer"; + +export default CostmapLayer; diff --git a/dimos/web/command-center-extension/src/components/GridLayer.tsx b/dimos/web/command-center-extension/src/components/GridLayer.tsx new file mode 100644 index 0000000000..87018cd3af --- /dev/null +++ b/dimos/web/command-center-extension/src/components/GridLayer.tsx @@ -0,0 +1,105 @@ +import * as d3 from "d3"; +import * as React from "react"; + +import { Vector } from "../types"; + +interface GridLayerProps { + width: number; + height: number; + origin: Vector; + resolution: number; + rows: number; + cols: number; +} + +const GridLayer = React.memo( + ({ width, height, origin, resolution, rows, cols }) => { + const minX = origin.coords[0]!; + const minY = origin.coords[1]!; + const maxX = minX + cols * resolution; + const maxY = minY + rows * resolution; + + const xScale = d3.scaleLinear().domain([minX, maxX]).range([0, width]); + const yScale = d3.scaleLinear().domain([minY, maxY]).range([height, 0]); + + const gridSize = 1 / resolution; + const gridLines = React.useMemo(() => { + const lines = []; + for (const x of d3.range(Math.ceil(minX / gridSize) * gridSize, maxX, gridSize)) { + lines.push( + , + ); + } + for (const y of d3.range(Math.ceil(minY / gridSize) * gridSize, maxY, gridSize)) { + lines.push( + , + ); + } + return lines; + }, [minX, minY, maxX, maxY, gridSize, xScale, yScale, width, height]); + + const xAxisRef = React.useRef(null); + const yAxisRef = React.useRef(null); + + React.useEffect(() => { + if (xAxisRef.current) { + const xAxis = d3.axisBottom(xScale).ticks(7); + d3.select(xAxisRef.current).call(xAxis); + d3.select(xAxisRef.current) + .selectAll("line,path") + .attr("stroke", "#ffffff") + .attr("stroke-width", 1); + d3.select(xAxisRef.current).selectAll("text").attr("fill", "#ffffff"); + } + if (yAxisRef.current) { + const yAxis = d3.axisLeft(yScale).ticks(7); + d3.select(yAxisRef.current).call(yAxis); + d3.select(yAxisRef.current) + .selectAll("line,path") + .attr("stroke", "#ffffff") + .attr("stroke-width", 1); + d3.select(yAxisRef.current).selectAll("text").attr("fill", "#ffffff"); + } + }, [xScale, yScale]); + + const showOrigin = minX <= 0 && 0 <= maxX && minY <= 0 && 0 <= maxY; + + return ( + <> + {gridLines} + + + {showOrigin && ( + + + + World Origin (0,0) + + + )} + + ); + }, +); + +GridLayer.displayName = "GridLayer"; + +export default GridLayer; diff --git a/dimos/web/command-center-extension/src/components/LeafletMap.tsx b/dimos/web/command-center-extension/src/components/LeafletMap.tsx new file mode 100644 index 0000000000..79ba4b25da --- /dev/null +++ b/dimos/web/command-center-extension/src/components/LeafletMap.tsx @@ -0,0 +1,150 @@ +import * as React from "react"; +import { MapContainer, TileLayer, Marker, Popup, useMapEvents } from "react-leaflet"; +import L, { LatLngExpression } from "leaflet"; +import { LatLon } from "../types"; + +// Fix for default marker icons in react-leaflet +// Using CDN URLs since webpack can't handle the image imports directly +const DefaultIcon = L.icon({ + iconUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png", + shadowUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png", + iconSize: [25, 41], + iconAnchor: [12, 41], +}); + +L.Marker.prototype.options.icon = DefaultIcon; + +// Component to handle map click events +function MapClickHandler({ onMapClick }: { onMapClick: (lat: number, lng: number) => void }) { + useMapEvents({ + click: (e) => { + onMapClick(e.latlng.lat, e.latlng.lng); + }, + }); + return null; +} + +interface LeafletMapProps { + gpsLocation: LatLon | null; + gpsTravelGoalPoints: LatLon[] | null; + onGpsGoal: (goal: LatLon) => void; +} + +const LeafletMap: React.FC = ({ gpsLocation, gpsTravelGoalPoints, onGpsGoal }) => { + if (!gpsLocation) { + return ( +
+ GPS location not received yet. +
+ ); + } + + const center: LatLngExpression = [gpsLocation.lat, gpsLocation.lon]; + + return ( +
+ + + + { + onGpsGoal({ lat, lon: lng }); + }} /> + + Current GPS Location + + {gpsTravelGoalPoints !== null && ( + gpsTravelGoalPoints.map(p => ( + + )) + )} + +
+ ); +}; + +const leafletCss = ` +.leaflet-control-container { + display: none; +} +.leaflet-container { + width: 100%; + height: 100%; + position: relative; +} +.leaflet-pane, +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-tile-container, +.leaflet-pane > svg, +.leaflet-pane > canvas, +.leaflet-zoom-box, +.leaflet-image-layer, +.leaflet-layer { + position: absolute; + left: 0; + top: 0; +} +.leaflet-container { + overflow: hidden; + -webkit-tap-highlight-color: transparent; + background: #ddd; + outline: 0; + font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; +} +.leaflet-tile { + filter: inherit; + visibility: hidden; +} +.leaflet-tile-loaded { + visibility: inherit; +} +.leaflet-zoom-box { + width: 0; + height: 0; + -moz-box-sizing: border-box; + box-sizing: border-box; + z-index: 800; +} +.leaflet-control { + position: relative; + z-index: 800; + pointer-events: visiblePainted; + pointer-events: auto; +} +.leaflet-top, +.leaflet-bottom { + position: absolute; + z-index: 1000; + pointer-events: none; +} +.leaflet-top { + top: 0; +} +.leaflet-right { + right: 0; +} +.leaflet-bottom { + bottom: 0; +} +.leaflet-left { + left: 0; +} +`; + +export default LeafletMap; \ No newline at end of file diff --git a/dimos/web/command-center-extension/src/components/PathLayer.tsx b/dimos/web/command-center-extension/src/components/PathLayer.tsx new file mode 100644 index 0000000000..969c9cf7dc --- /dev/null +++ b/dimos/web/command-center-extension/src/components/PathLayer.tsx @@ -0,0 +1,57 @@ +import * as d3 from "d3"; +import * as React from "react"; + +import { Path } from "../types"; + +interface PathLayerProps { + path: Path; + worldToPx: (x: number, y: number) => [number, number]; +} + +const PathLayer = React.memo(({ path, worldToPx }) => { + const points = React.useMemo( + () => path.coords.map(([x, y]) => worldToPx(x, y)), + [path.coords, worldToPx], + ); + + const pathData = React.useMemo(() => { + const line = d3.line(); + return line(points); + }, [points]); + + const gradientId = React.useMemo(() => `path-gradient-${Date.now()}`, []); + + if (path.coords.length < 2) { + return null; + } + + return ( + <> + + + + + + + + + ); +}); + +PathLayer.displayName = "PathLayer"; + +export default PathLayer; diff --git a/dimos/web/command-center-extension/src/components/VectorLayer.tsx b/dimos/web/command-center-extension/src/components/VectorLayer.tsx new file mode 100644 index 0000000000..87b932d0a4 --- /dev/null +++ b/dimos/web/command-center-extension/src/components/VectorLayer.tsx @@ -0,0 +1,41 @@ +import * as React from "react"; + +import { Vector } from "../types"; + +interface VectorLayerProps { + vector: Vector; + label: string; + worldToPx: (x: number, y: number) => [number, number]; +} + +const VectorLayer = React.memo(({ vector, label, worldToPx }) => { + const [cx, cy] = worldToPx(vector.coords[0]!, vector.coords[1]!); + const text = `${label} (${vector.coords[0]!.toFixed(2)}, ${vector.coords[1]!.toFixed(2)})`; + + return ( + <> + + + + + + + + {text} + + + + ); +}); + +VectorLayer.displayName = "VectorLayer"; + +export default VectorLayer; diff --git a/dimos/web/command-center-extension/src/components/VisualizerComponent.tsx b/dimos/web/command-center-extension/src/components/VisualizerComponent.tsx new file mode 100644 index 0000000000..e5bdb7f58e --- /dev/null +++ b/dimos/web/command-center-extension/src/components/VisualizerComponent.tsx @@ -0,0 +1,102 @@ +import * as d3 from "d3"; +import * as React from "react"; + +import { Costmap, Path, Vector } from "../types"; +import CostmapLayer from "./CostmapLayer"; +import PathLayer from "./PathLayer"; +import VectorLayer from "./VectorLayer"; + +interface VisualizerComponentProps { + costmap: Costmap | null; + robotPose: Vector | null; + path: Path | null; +} + +const VisualizerComponent: React.FC = ({ costmap, robotPose, path }) => { + const svgRef = React.useRef(null); + const [dimensions, setDimensions] = React.useState({ width: 800, height: 600 }); + const { width, height } = dimensions; + + React.useEffect(() => { + if (!svgRef.current?.parentElement) { + return; + } + + const updateDimensions = () => { + const rect = svgRef.current?.parentElement?.getBoundingClientRect(); + if (rect) { + setDimensions({ width: rect.width, height: rect.height }); + } + }; + + updateDimensions(); + const observer = new ResizeObserver(updateDimensions); + observer.observe(svgRef.current.parentElement); + + return () => { + observer.disconnect(); + }; + }, []); + + const { worldToPx } = React.useMemo(() => { + if (!costmap) { + return { worldToPx: undefined }; + } + + const { + grid: { shape }, + origin, + resolution, + } = costmap; + const rows = shape[0]!; + const cols = shape[1]!; + + const axisMargin = { left: 60, bottom: 40 }; + const availableWidth = width - axisMargin.left; + const availableHeight = height - axisMargin.bottom; + + const cell = Math.min(availableWidth / cols, availableHeight / rows); + const gridW = cols * cell; + const gridH = rows * cell; + const offsetX = axisMargin.left + (availableWidth - gridW) / 2; + const offsetY = (availableHeight - gridH) / 2; + + const xScale = d3 + .scaleLinear() + .domain([origin.coords[0]!, origin.coords[0]! + cols * resolution]) + .range([offsetX, offsetX + gridW]); + + const yScale = d3 + .scaleLinear() + .domain([origin.coords[1]!, origin.coords[1]! + rows * resolution]) + .range([offsetY + gridH, offsetY]); + + const worldToPxFn = (x: number, y: number): [number, number] => [xScale(x), yScale(y)]; + + return { worldToPx: worldToPxFn }; + }, [costmap, width, height]); + + return ( +
+ + {costmap && } + {path && worldToPx && } + {robotPose && worldToPx && ( + + )} + +
+ ); +}; + +export default React.memo(VisualizerComponent); diff --git a/dimos/web/command-center-extension/src/components/VisualizerWrapper.tsx b/dimos/web/command-center-extension/src/components/VisualizerWrapper.tsx new file mode 100644 index 0000000000..e137019ae1 --- /dev/null +++ b/dimos/web/command-center-extension/src/components/VisualizerWrapper.tsx @@ -0,0 +1,86 @@ +import * as d3 from "d3"; +import * as React from "react"; + +import { AppState } from "../types"; +import VisualizerComponent from "./VisualizerComponent"; + +interface VisualizerWrapperProps { + data: AppState; + onWorldClick: (worldX: number, worldY: number) => void; +} + +const VisualizerWrapper: React.FC = ({ data, onWorldClick }) => { + const containerRef = React.useRef(null); + const lastClickTime = React.useRef(0); + const clickThrottleMs = 150; + + const handleClick = React.useCallback( + (event: React.MouseEvent) => { + if (!data.costmap || !containerRef.current) { + return; + } + + event.stopPropagation(); + + const now = Date.now(); + if (now - lastClickTime.current < clickThrottleMs) { + console.log("Click throttled"); + return; + } + lastClickTime.current = now; + + const svgElement = containerRef.current.querySelector("svg"); + if (!svgElement) { + return; + } + + const svgRect = svgElement.getBoundingClientRect(); + const clickX = event.clientX - svgRect.left; + const clickY = event.clientY - svgRect.top; + + const costmap = data.costmap; + const { + grid: { shape }, + origin, + resolution, + } = costmap; + const rows = shape[0]!; + const cols = shape[1]!; + const width = svgRect.width; + const height = svgRect.height; + + const axisMargin = { left: 60, bottom: 40 }; + const availableWidth = width - axisMargin.left; + const availableHeight = height - axisMargin.bottom; + + const cell = Math.min(availableWidth / cols, availableHeight / rows); + const gridW = cols * cell; + const gridH = rows * cell; + const offsetX = axisMargin.left + (availableWidth - gridW) / 2; + const offsetY = (availableHeight - gridH) / 2; + + const xScale = d3 + .scaleLinear() + .domain([origin.coords[0]!, origin.coords[0]! + cols * resolution]) + .range([offsetX, offsetX + gridW]); + const yScale = d3 + .scaleLinear() + .domain([origin.coords[1]!, origin.coords[1]! + rows * resolution]) + .range([offsetY + gridH, offsetY]); + + const worldX = xScale.invert(clickX); + const worldY = yScale.invert(clickY); + + onWorldClick(worldX, worldY); + }, + [data.costmap, onWorldClick], + ); + + return ( +
+ +
+ ); +}; + +export default VisualizerWrapper; diff --git a/dimos/web/command-center-extension/src/index.ts b/dimos/web/command-center-extension/src/index.ts new file mode 100644 index 0000000000..052f967e37 --- /dev/null +++ b/dimos/web/command-center-extension/src/index.ts @@ -0,0 +1,14 @@ +import { PanelExtensionContext, ExtensionContext } from "@foxglove/extension"; + +import { initializeApp } from "./init"; + +export function activate(extensionContext: ExtensionContext): void { + extensionContext.registerPanel({ name: "command-center", initPanel }); +} + +export function initPanel(context: PanelExtensionContext): () => void { + initializeApp(context.panelElement); + return () => { + // Cleanup function + }; +} diff --git a/dimos/web/command-center-extension/src/init.ts b/dimos/web/command-center-extension/src/init.ts new file mode 100644 index 0000000000..f57f3aa582 --- /dev/null +++ b/dimos/web/command-center-extension/src/init.ts @@ -0,0 +1,9 @@ +import * as React from "react"; +import * as ReactDOMClient from "react-dom/client"; + +import App from "./App"; + +export function initializeApp(element: HTMLElement): void { + const root = ReactDOMClient.createRoot(element); + root.render(React.createElement(App)); +} diff --git a/dimos/web/command-center-extension/src/optimizedCostmap.ts b/dimos/web/command-center-extension/src/optimizedCostmap.ts new file mode 100644 index 0000000000..2244437eab --- /dev/null +++ b/dimos/web/command-center-extension/src/optimizedCostmap.ts @@ -0,0 +1,120 @@ +import * as pako from 'pako'; + +export interface EncodedOptimizedGrid { + update_type: "full" | "delta"; + shape: [number, number]; + dtype: string; + compressed: boolean; + compression?: "zlib" | "none"; + data?: string; + chunks?: Array<{ + pos: [number, number]; + size: [number, number]; + data: string; + }>; +} + +export class OptimizedGrid { + private fullGrid: Uint8Array | null = null; + private shape: [number, number] = [0, 0]; + + decode(msg: EncodedOptimizedGrid): Float32Array { + if (msg.update_type === "full") { + return this.decodeFull(msg); + } else { + return this.decodeDelta(msg); + } + } + + private decodeFull(msg: EncodedOptimizedGrid): Float32Array { + if (!msg.data) { + throw new Error("Missing data for full update"); + } + + const binaryString = atob(msg.data); + const compressed = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + compressed[i] = binaryString.charCodeAt(i); + } + + // Decompress if needed + let decompressed: Uint8Array; + if (msg.compressed && msg.compression === "zlib") { + decompressed = pako.inflate(compressed); + } else { + decompressed = compressed; + } + + // Store for delta updates + this.fullGrid = decompressed; + this.shape = msg.shape; + + // Convert uint8 back to float32 costmap values + const float32Data = new Float32Array(decompressed.length); + for (let i = 0; i < decompressed.length; i++) { + // Map 255 back to -1 for unknown cells + const val = decompressed[i]!; + float32Data[i] = val === 255 ? -1 : val; + } + + return float32Data; + } + + private decodeDelta(msg: EncodedOptimizedGrid): Float32Array { + if (!this.fullGrid) { + console.warn("No full grid available for delta update - skipping until full update arrives"); + const size = msg.shape[0] * msg.shape[1]; + return new Float32Array(size).fill(-1); + } + + if (!msg.chunks) { + throw new Error("Missing chunks for delta update"); + } + + // Apply delta updates to the full grid + for (const chunk of msg.chunks) { + const [y, x] = chunk.pos; + const [h, w] = chunk.size; + + // Decode chunk data + const binaryString = atob(chunk.data); + const compressed = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + compressed[i] = binaryString.charCodeAt(i); + } + + let decompressed: Uint8Array; + if (msg.compressed && msg.compression === "zlib") { + decompressed = pako.inflate(compressed); + } else { + decompressed = compressed; + } + + // Update the full grid with chunk data + const width = this.shape[1]; + let chunkIdx = 0; + for (let cy = 0; cy < h; cy++) { + for (let cx = 0; cx < w; cx++) { + const gridIdx = (y + cy) * width + (x + cx); + const val = decompressed[chunkIdx++]; + if (val !== undefined) { + this.fullGrid[gridIdx] = val; + } + } + } + } + + // Convert to float32 + const float32Data = new Float32Array(this.fullGrid.length); + for (let i = 0; i < this.fullGrid.length; i++) { + const val = this.fullGrid[i]!; + float32Data[i] = val === 255 ? -1 : val; + } + + return float32Data; + } + + getShape(): [number, number] { + return this.shape; + } +} diff --git a/dimos/web/command-center-extension/src/types.ts b/dimos/web/command-center-extension/src/types.ts new file mode 100644 index 0000000000..5f3a804a9c --- /dev/null +++ b/dimos/web/command-center-extension/src/types.ts @@ -0,0 +1,127 @@ +import { EncodedOptimizedGrid, OptimizedGrid } from './optimizedCostmap'; + +export type EncodedVector = Encoded<"vector"> & { + c: number[]; +}; + +export class Vector { + coords: number[]; + constructor(...coords: number[]) { + this.coords = coords; + } + + static decode(data: EncodedVector): Vector { + return new Vector(...data.c); + } +} + +export interface LatLon { + lat: number; + lon: number; + alt?: number; +} + +export type EncodedPath = Encoded<"path"> & { + points: Array<[number, number]>; +}; + +export class Path { + constructor(public coords: Array<[number, number]>) {} + + static decode(data: EncodedPath): Path { + return new Path(data.points); + } +} + +export type EncodedCostmap = Encoded<"costmap"> & { + grid: EncodedOptimizedGrid; + origin: EncodedVector; + resolution: number; + origin_theta: number; +}; + +export class Costmap { + constructor( + public grid: Grid, + public origin: Vector, + public resolution: number, + public origin_theta: number, + ) { + this.grid = grid; + this.origin = origin; + this.resolution = resolution; + this.origin_theta = origin_theta; + } + + private static decoder: OptimizedGrid | null = null; + + static decode(data: EncodedCostmap): Costmap { + // Use a singleton decoder to maintain state for delta updates + if (!Costmap.decoder) { + Costmap.decoder = new OptimizedGrid(); + } + + const float32Data = Costmap.decoder.decode(data.grid); + const shape = data.grid.shape; + + // Create a Grid object from the decoded data + const grid = new Grid(float32Data, shape); + + return new Costmap( + grid, + Vector.decode(data.origin), + data.resolution, + data.origin_theta, + ); + } +} + +export class Grid { + constructor( + public data: Float32Array | Float64Array | Int32Array | Int8Array, + public shape: number[], + ) {} +} + +export type Drawable = Costmap | Vector | Path; + +export type Encoded = { + type: T; +}; + +export interface FullStateData { + costmap?: EncodedCostmap; + robot_pose?: EncodedVector; + gps_location?: LatLon; + gps_travel_goal_points?: LatLon[]; + path?: EncodedPath; +} + +export interface TwistCommand { + linear: { + x: number; + y: number; + z: number; + }; + angular: { + x: number; + y: number; + z: number; + }; +} + +export interface AppState { + costmap: Costmap | null; + robotPose: Vector | null; + gpsLocation: LatLon | null; + gpsTravelGoalPoints: LatLon[] | null; + path: Path | null; +} + +export type AppAction = + | { type: "SET_COSTMAP"; payload: Costmap } + | { type: "SET_ROBOT_POSE"; payload: Vector } + | { type: "SET_GPS_LOCATION"; payload: LatLon } + | { type: "SET_GPS_TRAVEL_GOAL_POINTS"; payload: LatLon[] } + | { type: "SET_PATH"; payload: Path } + | { type: "SET_FULL_STATE"; payload: Partial }; diff --git a/dimos/web/command-center-extension/tsconfig.json b/dimos/web/command-center-extension/tsconfig.json new file mode 100644 index 0000000000..b4ead7c4a8 --- /dev/null +++ b/dimos/web/command-center-extension/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "create-foxglove-extension/tsconfig/tsconfig.json", + "include": [ + "./src/**/*" + ], + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "lib": [ + "dom" + ], + "composite": false, + "declaration": false, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "forceConsistentCasingInFileNames": true + } +} diff --git a/dimos/web/dimos_interface/__init__.py b/dimos/web/dimos_interface/__init__.py index d3aa928a0d..5ca28b30e5 100644 --- a/dimos/web/dimos_interface/__init__.py +++ b/dimos/web/dimos_interface/__init__.py @@ -4,4 +4,4 @@ from .api.server import FastAPIServer -__all__ = ['FastAPIServer'] +__all__ = ["FastAPIServer"] diff --git a/dimos/web/dimos_interface/api/server.py b/dimos/web/dimos_interface/api/server.py index 14007ca4f4..4f9979c085 100644 --- a/dimos/web/dimos_interface/api/server.py +++ b/dimos/web/dimos_interface/api/server.py @@ -25,41 +25,50 @@ # browser like Safari. # Fast Api & Uvicorn +import asyncio + +# For audio processing +import io +from pathlib import Path +from queue import Empty, Queue +from threading import Lock +import time + import cv2 -from dimos.web.edge_io import EdgeIO -from fastapi import FastAPI, Request, Response, Form, HTTPException -from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse -from sse_starlette.sse import EventSourceResponse +from fastapi import FastAPI, File, Form, HTTPException, Request, UploadFile +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse from fastapi.templating import Jinja2Templates +import ffmpeg +import numpy as np +import reactivex as rx +from reactivex import operators as ops +from reactivex.disposable import SingleAssignmentDisposable +import soundfile as sf +from sse_starlette.sse import EventSourceResponse import uvicorn -from threading import Lock -from pathlib import Path -from queue import Queue, Empty -import asyncio -from reactivex.disposable import SingleAssignmentDisposable -from reactivex import operators as ops -import reactivex as rx -from fastapi.middleware.cors import CORSMiddleware +from dimos.stream.audio.base import AudioEvent +from dimos.web.edge_io import EdgeIO # TODO: Resolve threading, start/stop stream functionality. class FastAPIServer(EdgeIO): - - - - def __init__(self, - dev_name="FastAPI Server", - edge_type="Bidirectional", - host="0.0.0.0", - port=5555, - text_streams=None, - **streams): + def __init__( + self, + dev_name: str = "FastAPI Server", + edge_type: str = "Bidirectional", + host: str = "0.0.0.0", + port: int = 5555, + text_streams=None, + audio_subject=None, + **streams, + ) -> None: print("Starting FastAPIServer initialization...") # Debug print super().__init__(dev_name, edge_type) self.app = FastAPI() - + # Add CORS middleware with more permissive settings for development self.app.add_middleware( CORSMiddleware, @@ -67,13 +76,13 @@ def __init__(self, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], - expose_headers=["*"] + expose_headers=["*"], ) - + self.port = port self.host = host BASE_DIR = Path(__file__).resolve().parent - self.templates = Jinja2Templates(directory=str(BASE_DIR / 'templates')) + self.templates = Jinja2Templates(directory=str(BASE_DIR / "templates")) self.streams = streams self.active_streams = {} self.stream_locks = {key: Lock() for key in self.streams} @@ -89,21 +98,22 @@ def __init__(self, # Create a Subject for text queries self.query_subject = rx.subject.Subject() self.query_stream = self.query_subject.pipe(ops.share()) + self.audio_subject = audio_subject for key in self.streams: if self.streams[key] is not None: self.active_streams[key] = self.streams[key].pipe( - ops.map(self.process_frame_fastapi), ops.share()) + ops.map(self.process_frame_fastapi), ops.share() + ) # Set up text stream subscriptions for key, stream in self.text_streams.items(): if stream is not None: self.text_queues[key] = Queue(maxsize=100) disposable = stream.subscribe( - lambda text, k=key: self.text_queues[k].put(text) - if text is not None else None, + lambda text, k=key: self.text_queues[k].put(text) if text is not None else None, lambda e, k=key: self.text_queues[k].put(None), - lambda k=key: self.text_queues[k].put(None) + lambda k=key: self.text_queues[k].put(None), ) self.text_disposables[key] = disposable self.disposables.add(disposable) @@ -114,7 +124,7 @@ def __init__(self, def process_frame_fastapi(self, frame): """Convert frame to JPEG format for streaming.""" - _, buffer = cv2.imencode('.jpg', frame) + _, buffer = cv2.imencode(".jpg", frame) return buffer.tobytes() def stream_generator(self, key): @@ -144,10 +154,10 @@ def generate(): break disposable.disposable = self.active_streams[key].subscribe( - lambda frame: frame_queue.put(frame) - if frame is not None else None, + lambda frame: frame_queue.put(frame) if frame is not None else None, lambda e: frame_queue.put(None), - lambda: frame_queue.put(None)) + lambda: frame_queue.put(None), + ) try: while True: @@ -155,9 +165,7 @@ def generate(): frame = frame_queue.get(timeout=1) if frame is None: break - yield (b'--frame\r\n' - b'Content-Type: image/jpeg\r\n\r\n' + frame + - b'\r\n') + yield (b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + frame + b"\r\n") except Empty: # Instead of breaking, continue waiting for new frames continue @@ -172,8 +180,8 @@ def create_video_feed_route(self, key): async def video_feed(): return StreamingResponse( - self.stream_generator(key)(), - media_type="multipart/x-mixed-replace; boundary=frame") + self.stream_generator(key)(), media_type="multipart/x-mixed-replace; boundary=frame" + ) return video_feed @@ -181,22 +189,18 @@ async def text_stream_generator(self, key): """Generate SSE events for text stream.""" client_id = id(object()) self.text_clients.add(client_id) - + try: while True: if key not in self.text_queues: yield {"event": "ping", "data": ""} await asyncio.sleep(0.1) continue - + try: text = self.text_queues[key].get_nowait() if text is not None: - yield { - "event": "message", - "id": key, - "data": text - } + yield {"event": "message", "id": key, "data": text} else: break except Empty: @@ -205,7 +209,34 @@ async def text_stream_generator(self, key): finally: self.text_clients.remove(client_id) - def setup_routes(self): + @staticmethod + def _decode_audio(raw: bytes) -> tuple[np.ndarray, int]: + """Convert the webm/opus blob sent by the browser into mono 16-kHz PCM.""" + try: + # Use ffmpeg to convert to 16-kHz mono 16-bit PCM WAV in memory + out, _ = ( + ffmpeg.input("pipe:0") + .output( + "pipe:1", + format="wav", + acodec="pcm_s16le", + ac=1, + ar="16000", + loglevel="quiet", + ) + .run(input=raw, capture_stdout=True, capture_stderr=True) + ) + # Load with soundfile (returns float32 by default) + audio, sr = sf.read(io.BytesIO(out), dtype="float32") + # Ensure 1-D array (mono) + if audio.ndim > 1: + audio = audio[:, 0] + return np.array(audio), sr + except Exception as exc: + print(f"ffmpeg decoding failed: {exc}") + return None, None + + def setup_routes(self) -> None: """Set up FastAPI routes.""" @self.app.get("/streams") @@ -222,12 +253,16 @@ async def get_text_streams(): async def index(request: Request): stream_keys = list(self.streams.keys()) text_stream_keys = list(self.text_streams.keys()) - return self.templates.TemplateResponse("index_fastapi.html", { - "request": request, - "stream_keys": stream_keys, - "text_stream_keys": text_stream_keys - }) - + return self.templates.TemplateResponse( + "index_fastapi.html", + { + "request": request, + "stream_keys": stream_keys, + "text_stream_keys": text_stream_keys, + "has_voice": self.audio_subject is not None, + }, + ) + @self.app.post("/submit_query") async def submit_query(query: str = Form(...)): # Using Form directly as a dependency ensures proper form handling @@ -235,29 +270,53 @@ async def submit_query(query: str = Form(...)): if query: # Emit the query through our Subject self.query_subject.on_next(query) - return JSONResponse({ - "success": True, - "message": "Query received" - }) - return JSONResponse({ - "success": False, - "message": "No query provided" - }) + return JSONResponse({"success": True, "message": "Query received"}) + return JSONResponse({"success": False, "message": "No query provided"}) except Exception as e: # Ensure we always return valid JSON even on error - return JSONResponse(status_code=500, - content={ - "success": False, - "message": f"Server error: {str(e)}" - }) + return JSONResponse( + status_code=500, + content={"success": False, "message": f"Server error: {e!s}"}, + ) + + @self.app.post("/upload_audio") + async def upload_audio(file: UploadFile = File(...)): + """Handle audio upload from the browser.""" + if self.audio_subject is None: + return JSONResponse( + status_code=400, + content={"success": False, "message": "Voice input not configured"}, + ) + + try: + data = await file.read() + audio_np, sr = self._decode_audio(data) + if audio_np is None: + return JSONResponse( + status_code=400, + content={"success": False, "message": "Unable to decode audio"}, + ) + + event = AudioEvent( + data=audio_np, + sample_rate=sr, + timestamp=time.time(), + channels=1 if audio_np.ndim == 1 else audio_np.shape[1], + ) + + # Push to reactive stream + self.audio_subject.on_next(event) + print(f"Received audio – {event.data.shape[0] / sr:.2f} s, {sr} Hz") + return {"success": True} + except Exception as e: + print(f"Failed to process uploaded audio: {e}") + return JSONResponse(status_code=500, content={"success": False, "message": str(e)}) + # Unitree API endpoints @self.app.get("/unitree/status") async def unitree_status(): """Check the status of the Unitree API server""" - return JSONResponse({ - "status": "online", - "service": "unitree" - }) + return JSONResponse({"status": "online", "service": "unitree"}) @self.app.post("/unitree/command") async def unitree_command(request: Request): @@ -265,25 +324,22 @@ async def unitree_command(request: Request): try: data = await request.json() command_text = data.get("command", "") - + # Emit the command through the query_subject self.query_subject.on_next(command_text) - + response = { "success": True, "command": command_text, - "result": f"Processed command: {command_text}" + "result": f"Processed command: {command_text}", } - + return JSONResponse(response) except Exception as e: - print(f"Error processing command: {str(e)}") + print(f"Error processing command: {e!s}") return JSONResponse( status_code=500, - content={ - "success": False, - "message": f"Error processing command: {str(e)}" - } + content={"success": False, "message": f"Error processing command: {e!s}"}, ) @self.app.get("/text_stream/{key}") @@ -292,16 +348,16 @@ async def text_stream(key: str): raise HTTPException(status_code=404, detail=f"Text stream '{key}' not found") return EventSourceResponse(self.text_stream_generator(key)) - for key in self.streams: - self.app.get(f"/video_feed/{key}")( - self.create_video_feed_route(key)) + self.app.get(f"/video_feed/{key}")(self.create_video_feed_route(key)) - def run(self): + def run(self) -> None: """Run the FastAPI server.""" - uvicorn.run(self.app, host=self.host, port=self.port - ) # TODO: Translate structure to enable in-built workers' - + uvicorn.run( + self.app, host=self.host, port=self.port + ) # TODO: Translate structure to enable in-built workers' + + if __name__ == "__main__": server = FastAPIServer() - server.run() \ No newline at end of file + server.run() diff --git a/dimos/web/dimos_interface/api/templates/index_fastapi.html b/dimos/web/dimos_interface/api/templates/index_fastapi.html index 94cf0be328..406557c04a 100644 --- a/dimos/web/dimos_interface/api/templates/index_fastapi.html +++ b/dimos/web/dimos_interface/api/templates/index_fastapi.html @@ -130,6 +130,7 @@ .query-form { display: flex; gap: 10px; + align-items: center; } .query-input { @@ -155,6 +156,81 @@ background-color: #218838; } + /* Voice button styles */ + .voice-button { + width: 50px; + height: 50px; + border-radius: 50%; + background-color: #dc3545; + color: white; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + transition: all 0.3s ease; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); + position: relative; + } + + .voice-button:hover { + transform: scale(1.1); + box-shadow: 0 4px 8px rgba(0,0,0,0.3); + } + + .voice-button.recording { + background-color: #ff0000; + animation: pulse 1.5s infinite; + } + + .voice-button.recording::after { + content: ''; + position: absolute; + top: -10px; + left: -10px; + right: -10px; + bottom: -10px; + border: 3px solid rgba(255, 0, 0, 0.5); + border-radius: 50%; + animation: ripple 1.5s infinite; + } + + @keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); } + } + + @keyframes ripple { + 0% { + transform: scale(1); + opacity: 1; + } + 100% { + transform: scale(1.2); + opacity: 0; + } + } + + .voice-status { + position: absolute; + top: -25px; + left: 50%; + transform: translateX(-50%); + background-color: rgba(0,0,0,0.8); + color: white; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + white-space: nowrap; + display: none; + } + + .voice-button.recording .voice-status { + display: block; + } + .query-response { margin-top: 15px; padding: 10px; @@ -218,6 +294,12 @@

Ask a Question

+ {% if has_voice %} + + {% endif %}
@@ -277,6 +359,72 @@

{{ key.replace('_', ' ').title() }}

window.location.reload(true); } + // Voice recording functionality + {% if has_voice %} + let mediaRecorder; + let chunks = []; + const voiceBtn = document.getElementById('voiceButton'); + const queryResponse = document.getElementById('queryResponse'); + + voiceBtn.addEventListener('click', async () => { + if (mediaRecorder && mediaRecorder.state === 'recording') { + // Stop recording + mediaRecorder.stop(); + voiceBtn.classList.remove('recording'); + } else { + // Start recording + try { + if (!mediaRecorder) { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + mediaRecorder = new MediaRecorder(stream); + + mediaRecorder.ondataavailable = e => chunks.push(e.data); + + mediaRecorder.onstop = async () => { + const blob = new Blob(chunks, { type: 'audio/webm' }); + chunks = []; + + // Show uploading status + queryResponse.textContent = 'Processing voice command...'; + queryResponse.className = 'query-response'; + queryResponse.style.display = 'block'; + + const formData = new FormData(); + formData.append('file', blob, 'recording.webm'); + + try { + const res = await fetch('/upload_audio', { + method: 'POST', + body: formData + }); + const json = await res.json(); + + if (json.success) { + queryResponse.textContent = 'Voice command received!'; + queryResponse.className = 'query-response success'; + setTimeout(() => { + queryResponse.style.display = 'none'; + }, 3000); + } else { + queryResponse.textContent = 'Error: ' + json.message; + queryResponse.className = 'query-response error'; + } + } catch (err) { + queryResponse.textContent = 'Upload failed: ' + err.message; + queryResponse.className = 'query-response error'; + } + }; + } + + mediaRecorder.start(); + voiceBtn.classList.add('recording'); + } catch (err) { + alert('Microphone access denied. Please allow microphone access to use voice commands.'); + } + } + }); + {% endif %} + // Handle query form submission document.getElementById('queryForm').addEventListener('submit', async function(e) { e.preventDefault(); diff --git a/dimos/web/dimos_interface/src/App.svelte b/dimos/web/dimos_interface/src/App.svelte index a1ddae4931..c249f3e3ea 100644 --- a/dimos/web/dimos_interface/src/App.svelte +++ b/dimos/web/dimos_interface/src/App.svelte @@ -3,7 +3,27 @@ import Input from './components/Input.svelte'; import History from './components/History.svelte'; import StreamViewer from './components/StreamViewer.svelte'; + import VoiceButton from './components/VoiceButton.svelte'; import { theme } from './stores/theme'; + import { history } from './stores/history'; + + const handleVoiceCommand = async (event: CustomEvent) => { + if (event.detail.success) { + // Show voice processing message + history.update(h => [...h, { + command: '[voice command]', + outputs: ['Processing voice command...'] + }]); + + // The actual command will be processed by the agent through the audio pipeline + // and will appear in the text stream + } else { + history.update(h => [...h, { + command: '[voice command]', + outputs: [`Error: ${event.detail.error}`] + }]); + } + }; @@ -29,3 +49,5 @@ + + diff --git a/dimos/web/dimos_interface/src/components/Input.svelte b/dimos/web/dimos_interface/src/components/Input.svelte index 86eba9184d..3a2b515f3d 100644 --- a/dimos/web/dimos_interface/src/components/Input.svelte +++ b/dimos/web/dimos_interface/src/components/Input.svelte @@ -29,6 +29,32 @@ const handleKeyDown = async (event: KeyboardEvent) => { if (event.key === 'Enter') { + await executeCommand(); + } else if (event.key === 'ArrowUp') { + if (historyIndex < $history.length - 1) { + historyIndex++; + command = $history[$history.length - 1 - historyIndex].command; + } + event.preventDefault(); + } else if (event.key === 'ArrowDown') { + if (historyIndex > -1) { + historyIndex--; + command = historyIndex >= 0 ? $history[$history.length - 1 - historyIndex].command : ''; + } + event.preventDefault(); + } else if (event.key === 'Tab') { + event.preventDefault(); + const autoCompleteCommand = Object.keys(commands).find((cmd) => cmd.startsWith(command)); + if (autoCompleteCommand) { + command = autoCompleteCommand; + } + } else if (event.ctrlKey && event.key === 'l') { + event.preventDefault(); + $history = []; + } + }; + + const executeCommand = async () => { const [commandName, ...args] = command.split(' '); if (import.meta.env.VITE_TRACKING_ENABLED === 'true') { @@ -57,28 +83,6 @@ command = ''; historyIndex = -1; - } else if (event.key === 'ArrowUp') { - if (historyIndex < $history.length - 1) { - historyIndex++; - command = $history[$history.length - 1 - historyIndex].command; - } - event.preventDefault(); - } else if (event.key === 'ArrowDown') { - if (historyIndex > -1) { - historyIndex--; - command = historyIndex >= 0 ? $history[$history.length - 1 - historyIndex].command : ''; - } - event.preventDefault(); - } else if (event.key === 'Tab') { - event.preventDefault(); - const autoCompleteCommand = Object.keys(commands).find((cmd) => cmd.startsWith(command)); - if (autoCompleteCommand) { - command = autoCompleteCommand; - } - } else if (event.ctrlKey && event.key === 'l') { - event.preventDefault(); - $history = []; - } }; diff --git a/dimos/web/dimos_interface/src/components/VoiceButton.svelte b/dimos/web/dimos_interface/src/components/VoiceButton.svelte new file mode 100644 index 0000000000..0f9682519a --- /dev/null +++ b/dimos/web/dimos_interface/src/components/VoiceButton.svelte @@ -0,0 +1,262 @@ + + + + + + + + + \ No newline at end of file diff --git a/dimos/web/dimos_interface/src/stores/stream.ts b/dimos/web/dimos_interface/src/stores/stream.ts index 4ea07a71d7..eee46f84bf 100644 --- a/dimos/web/dimos_interface/src/stores/stream.ts +++ b/dimos/web/dimos_interface/src/stores/stream.ts @@ -18,6 +18,13 @@ import { writable, derived, get } from 'svelte/store'; import { simulationManager, simulationStore } from '../utils/simulation'; import { history } from './history'; +// Get the server URL dynamically based on current location +const getServerUrl = () => { + // In production, use the same host as the frontend but on port 5555 + const hostname = window.location.hostname; + return `http://${hostname}:5555`; +}; + interface StreamState { isVisible: boolean; url: string | null; @@ -65,7 +72,7 @@ export const combinedStreamState = derived( // Function to fetch available streams async function fetchAvailableStreams(): Promise { try { - const response = await fetch('http://0.0.0.0:5555/streams', { + const response = await fetch(`${getServerUrl()}/streams`, { headers: { 'Accept': 'application/json' } @@ -100,7 +107,7 @@ export const showStream = async (streamKey?: string) => { streamStore.set({ isVisible: true, - url: 'http://0.0.0.0:5555', + url: getServerUrl(), streamKeys: selectedStreams, isLoading: false, error: null, @@ -134,7 +141,7 @@ export const connectTextStream = (key: string): void => { } // Create new EventSource - const eventSource = new EventSource(`http://0.0.0.0:5555/text_stream/${key}`); + const eventSource = new EventSource(`${getServerUrl()}/text_stream/${key}`); textEventSources[key] = eventSource; // Handle incoming messages eventSource.addEventListener('message', (event) => { diff --git a/dimos/web/dimos_interface/tsconfig.json b/dimos/web/dimos_interface/tsconfig.json index 772ce46b79..4bf29f39d2 100644 --- a/dimos/web/dimos_interface/tsconfig.json +++ b/dimos/web/dimos_interface/tsconfig.json @@ -5,17 +5,21 @@ "useDefineForClassFields": true, "module": "ESNext", "resolveJsonModule": true, - /** - * Typecheck JS in `.svelte` and `.js` files by default. - * Disable checkJs if you'd like to use dynamic types in JS. - * Note that setting allowJs false does not prevent the use - * of JS in `.svelte` files. - */ "allowJs": true, "checkJs": true, "isolatedModules": true, - "types": ["node"] + "types": [ + "node" + ] }, - "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], - "references": [{ "path": "./tsconfig.node.json" }] + "include": [ + "src/**/*.ts", + "src/**/*.js", + "src/**/*.svelte" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] } diff --git a/dimos/web/dimos_interface/tsconfig.node.json b/dimos/web/dimos_interface/tsconfig.node.json index 494bfe0835..ad883d0eb4 100644 --- a/dimos/web/dimos_interface/tsconfig.node.json +++ b/dimos/web/dimos_interface/tsconfig.node.json @@ -5,5 +5,7 @@ "module": "ESNext", "moduleResolution": "bundler" }, - "include": ["vite.config.ts"] + "include": [ + "vite.config.ts" + ] } diff --git a/dimos/web/dimos_interface/vite.config.ts b/dimos/web/dimos_interface/vite.config.ts index 296050256a..29be79dd4a 100644 --- a/dimos/web/dimos_interface/vite.config.ts +++ b/dimos/web/dimos_interface/vite.config.ts @@ -22,6 +22,7 @@ export default defineConfig({ plugins: [svelte()], server: { port: 3000, + host: '0.0.0.0', watch: { // Exclude node_modules, .git and other large directories ignored: ['**/node_modules/**', '**/.git/**', '**/dist/**', 'lambda/**'], diff --git a/dimos/web/edge_io.py b/dimos/web/edge_io.py index de17f0220f..ad15614623 100644 --- a/dimos/web/edge_io.py +++ b/dimos/web/edge_io.py @@ -14,12 +14,13 @@ from reactivex.disposable import CompositeDisposable -class EdgeIO(): - def __init__(self, dev_name:str="NA", edge_type:str="Base"): + +class EdgeIO: + def __init__(self, dev_name: str = "NA", edge_type: str = "Base") -> None: self.dev_name = dev_name self.edge_type = edge_type self.disposables = CompositeDisposable() - def dispose_all(self): + def dispose_all(self) -> None: """Disposes of all active subscriptions managed by this agent.""" self.disposables.dispose() diff --git a/dimos/web/fastapi_server.py b/dimos/web/fastapi_server.py index d639510150..6c8a85344a 100644 --- a/dimos/web/fastapi_server.py +++ b/dimos/web/fastapi_server.py @@ -23,46 +23,48 @@ # browser like Safari. # Fast Api & Uvicorn +import asyncio +from pathlib import Path +from queue import Empty, Queue +from threading import Lock + import cv2 -from dimos.web.edge_io import EdgeIO -from fastapi import FastAPI, Request, Response, Form, HTTPException -from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse -from sse_starlette.sse import EventSourceResponse +from fastapi import FastAPI, Form, HTTPException, Request +from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse from fastapi.templating import Jinja2Templates +import reactivex as rx +from reactivex import operators as ops +from reactivex.disposable import SingleAssignmentDisposable +from sse_starlette.sse import EventSourceResponse import uvicorn -from threading import Lock -from pathlib import Path -from queue import Queue, Empty -import asyncio -from reactivex.disposable import SingleAssignmentDisposable -from reactivex import operators as ops -import reactivex as rx +from dimos.web.edge_io import EdgeIO # TODO: Resolve threading, start/stop stream functionality. class FastAPIServer(EdgeIO): - - def __init__(self, - dev_name="FastAPI Server", - edge_type="Bidirectional", - host="0.0.0.0", - port=5555, - text_streams=None, - **streams): + def __init__( + self, + dev_name: str = "FastAPI Server", + edge_type: str = "Bidirectional", + host: str = "0.0.0.0", + port: int = 5555, + text_streams=None, + **streams, + ) -> None: super().__init__(dev_name, edge_type) self.app = FastAPI() self.port = port self.host = host BASE_DIR = Path(__file__).resolve().parent - self.templates = Jinja2Templates(directory=str(BASE_DIR / 'templates')) + self.templates = Jinja2Templates(directory=str(BASE_DIR / "templates")) self.streams = streams self.active_streams = {} self.stream_locks = {key: Lock() for key in self.streams} self.stream_queues = {} self.stream_disposables = {} - + # Initialize text streams self.text_streams = text_streams or {} self.text_queues = {} @@ -76,17 +78,17 @@ def __init__(self, for key in self.streams: if self.streams[key] is not None: self.active_streams[key] = self.streams[key].pipe( - ops.map(self.process_frame_fastapi), ops.share()) - + ops.map(self.process_frame_fastapi), ops.share() + ) + # Set up text stream subscriptions for key, stream in self.text_streams.items(): if stream is not None: self.text_queues[key] = Queue(maxsize=100) disposable = stream.subscribe( - lambda text, k=key: self.text_queues[k].put(text) - if text is not None else None, + lambda text, k=key: self.text_queues[k].put(text) if text is not None else None, lambda e, k=key: self.text_queues[k].put(None), - lambda k=key: self.text_queues[k].put(None) + lambda k=key: self.text_queues[k].put(None), ) self.text_disposables[key] = disposable self.disposables.add(disposable) @@ -95,7 +97,7 @@ def __init__(self, def process_frame_fastapi(self, frame): """Convert frame to JPEG format for streaming.""" - _, buffer = cv2.imencode('.jpg', frame) + _, buffer = cv2.imencode(".jpg", frame) return buffer.tobytes() def stream_generator(self, key): @@ -125,10 +127,10 @@ def generate(): break disposable.disposable = self.active_streams[key].subscribe( - lambda frame: frame_queue.put(frame) - if frame is not None else None, + lambda frame: frame_queue.put(frame) if frame is not None else None, lambda e: frame_queue.put(None), - lambda: frame_queue.put(None)) + lambda: frame_queue.put(None), + ) try: while True: @@ -136,9 +138,7 @@ def generate(): frame = frame_queue.get(timeout=1) if frame is None: break - yield (b'--frame\r\n' - b'Content-Type: image/jpeg\r\n\r\n' + frame + - b'\r\n') + yield (b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + frame + b"\r\n") except Empty: # Instead of breaking, continue waiting for new frames continue @@ -153,8 +153,8 @@ def create_video_feed_route(self, key): async def video_feed(): return StreamingResponse( - self.stream_generator(key)(), - media_type="multipart/x-mixed-replace; boundary=frame") + self.stream_generator(key)(), media_type="multipart/x-mixed-replace; boundary=frame" + ) return video_feed @@ -162,40 +162,36 @@ async def text_stream_generator(self, key): """Generate SSE events for text stream.""" client_id = id(object()) self.text_clients.add(client_id) - + try: while True: if key in self.text_queues: try: text = self.text_queues[key].get(timeout=1) if text is not None: - yield { - "event": "message", - "id": key, - "data": text - } + yield {"event": "message", "id": key, "data": text} except Empty: # Send a keep-alive comment - yield { - "event": "ping", - "data": "" - } + yield {"event": "ping", "data": ""} await asyncio.sleep(0.1) finally: self.text_clients.remove(client_id) - - def setup_routes(self): + + def setup_routes(self) -> None: """Set up FastAPI routes.""" @self.app.get("/", response_class=HTMLResponse) async def index(request: Request): stream_keys = list(self.streams.keys()) text_stream_keys = list(self.text_streams.keys()) - return self.templates.TemplateResponse("index_fastapi.html", { - "request": request, - "stream_keys": stream_keys, - "text_stream_keys": text_stream_keys - }) + return self.templates.TemplateResponse( + "index_fastapi.html", + { + "request": request, + "stream_keys": stream_keys, + "text_stream_keys": text_stream_keys, + }, + ) @self.app.post("/submit_query") async def submit_query(query: str = Form(...)): @@ -204,21 +200,14 @@ async def submit_query(query: str = Form(...)): if query: # Emit the query through our Subject self.query_subject.on_next(query) - return JSONResponse({ - "success": True, - "message": "Query received" - }) - return JSONResponse({ - "success": False, - "message": "No query provided" - }) + return JSONResponse({"success": True, "message": "Query received"}) + return JSONResponse({"success": False, "message": "No query provided"}) except Exception as e: # Ensure we always return valid JSON even on error - return JSONResponse(status_code=500, - content={ - "success": False, - "message": f"Server error: {str(e)}" - }) + return JSONResponse( + status_code=500, + content={"success": False, "message": f"Server error: {e!s}"}, + ) @self.app.get("/text_stream/{key}") async def text_stream(key: str): @@ -227,10 +216,10 @@ async def text_stream(key: str): return EventSourceResponse(self.text_stream_generator(key)) for key in self.streams: - self.app.get(f"/video_feed/{key}")( - self.create_video_feed_route(key)) + self.app.get(f"/video_feed/{key}")(self.create_video_feed_route(key)) - def run(self): + def run(self) -> None: """Run the FastAPI server.""" - uvicorn.run(self.app, host=self.host, port=self.port - ) # TODO: Translate structure to enable in-built workers' + uvicorn.run( + self.app, host=self.host, port=self.port + ) # TODO: Translate structure to enable in-built workers' diff --git a/dimos/web/flask_server.py b/dimos/web/flask_server.py index a7a5c59acf..b0cf6fc143 100644 --- a/dimos/web/flask_server.py +++ b/dimos/web/flask_server.py @@ -12,16 +12,24 @@ # See the License for the specific language governing permissions and # limitations under the License. -from flask import Flask, Response, render_template +from queue import Queue + import cv2 +from flask import Flask, Response, render_template from reactivex import operators as ops from reactivex.disposable import SingleAssignmentDisposable -from queue import Queue from dimos.web.edge_io import EdgeIO + class FlaskServer(EdgeIO): - def __init__(self, dev_name="Flask Server", edge_type="Bidirectional", port=5555, **streams): + def __init__( + self, + dev_name: str = "Flask Server", + edge_type: str = "Bidirectional", + port: int = 5555, + **streams, + ) -> None: super().__init__(dev_name, edge_type) self.app = Flask(__name__) self.port = port @@ -33,22 +41,21 @@ def __init__(self, dev_name="Flask Server", edge_type="Bidirectional", port=5555 if self.streams[key] is not None: # Apply share and ref_count to manage subscriptions self.active_streams[key] = self.streams[key].pipe( - ops.map(self.process_frame_flask), - ops.share() + ops.map(self.process_frame_flask), ops.share() ) self.setup_routes() - + def process_frame_flask(self, frame): """Convert frame to JPEG format for streaming.""" - _, buffer = cv2.imencode('.jpg', frame) + _, buffer = cv2.imencode(".jpg", frame) return buffer.tobytes() - def setup_routes(self): - @self.app.route('/') + def setup_routes(self) -> None: + @self.app.route("/") def index(): stream_keys = list(self.streams.keys()) # Get the keys from the streams dictionary - return render_template('index_flask.html', stream_keys=stream_keys) + return render_template("index_flask.html", stream_keys=stream_keys) # Function to create a streaming response def stream_generator(key): @@ -61,7 +68,7 @@ def generate(): disposable.disposable = self.active_streams[key].subscribe( lambda frame: frame_queue.put(frame) if frame is not None else None, lambda e: frame_queue.put(None), - lambda: frame_queue.put(None) + lambda: frame_queue.put(None), ) try: @@ -69,24 +76,27 @@ def generate(): frame = frame_queue.get() if frame is None: break - yield (b'--frame\r\n' - b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n') + yield (b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + frame + b"\r\n") finally: disposable.dispose() return generate - + def make_response_generator(key): def response_generator(): - return Response(stream_generator(key)(), mimetype='multipart/x-mixed-replace; boundary=frame') + return Response( + stream_generator(key)(), mimetype="multipart/x-mixed-replace; boundary=frame" + ) + return response_generator # Dynamically adding routes using add_url_rule for key in self.streams: - endpoint = f'video_feed_{key}' + endpoint = f"video_feed_{key}" self.app.add_url_rule( - f'/video_feed/{key}', endpoint, view_func=make_response_generator(key)) + f"/video_feed/{key}", endpoint, view_func=make_response_generator(key) + ) - def run(self, host='0.0.0.0', port=5555, threaded=True): + def run(self, host: str = "0.0.0.0", port: int = 5555, threaded: bool = True) -> None: self.port = port self.app.run(host=host, port=self.port, debug=False, threaded=threaded) diff --git a/dimos/web/robot_web_interface.py b/dimos/web/robot_web_interface.py index ea773994d9..0dc7636ac9 100644 --- a/dimos/web/robot_web_interface.py +++ b/dimos/web/robot_web_interface.py @@ -17,20 +17,19 @@ Provides a clean interface to the dimensional-interface FastAPI server. """ -import os -import sys - from dimos.web.dimos_interface.api.server import FastAPIServer + class RobotWebInterface(FastAPIServer): """Wrapper class for the dimos-interface FastAPI server.""" - - def __init__(self, port=5555, text_streams=None, **streams): + + def __init__(self, port: int = 5555, text_streams=None, audio_subject=None, **streams) -> None: super().__init__( dev_name="Robot Web Interface", edge_type="Bidirectional", host="0.0.0.0", port=port, text_streams=text_streams, - **streams - ) \ No newline at end of file + audio_subject=audio_subject, + **streams, + ) diff --git a/dimos/web/websocket_vis/README.md b/dimos/web/websocket_vis/README.md new file mode 100644 index 0000000000..c04235958e --- /dev/null +++ b/dimos/web/websocket_vis/README.md @@ -0,0 +1,66 @@ +# WebSocket Visualization Module + +The `WebsocketVisModule` provides a real-time data for visualization and control of the robot in Foxglove (see `dimos/web/command-center-extension/README.md`). + +## Overview + +Visualization: + +- Robot position and orientation +- Navigation paths +- Costmaps + +Control: + +- Set navigation goal +- Set GPS location goal +- Keyboard teleop (WASD) +- Trigger exploration + +## What it Provides + +### Inputs (Subscribed Topics) +- `robot_pose` (PoseStamped): Current robot position and orientation +- `gps_location` (LatLon): GPS coordinates of the robot +- `path` (Path): Planned navigation path +- `global_costmap` (OccupancyGrid): Global costmap for visualization + +### Outputs (Published Topics) +- `click_goal` (PoseStamped): Goal positions set by user clicks in the web interface +- `gps_goal` (LatLon): GPS goal coordinates set through the interface +- `explore_cmd` (Bool): Command to start autonomous exploration +- `stop_explore_cmd` (Bool): Command to stop exploration +- `movecmd` (Twist): Direct movement commands from the interface +- `movecmd_stamped` (TwistStamped): Timestamped movement commands + +## How to Use + +### Basic Usage + +```python +from dimos.web.websocket_vis.websocket_vis_module import WebsocketVisModule +from dimos import core + +# Deploy the WebSocket visualization module +websocket_vis = dimos.deploy(WebsocketVisModule, port=7779) + +# Receive control from the Foxglove plugin. +websocket_vis.click_goal.transport = core.LCMTransport("/goal_request", PoseStamped) +websocket_vis.explore_cmd.transport = core.LCMTransport("/explore_cmd", Bool) +websocket_vis.stop_explore_cmd.transport = core.LCMTransport("/stop_explore_cmd", Bool) +websocket_vis.movecmd.transport = core.LCMTransport("/cmd_vel", Twist) +websocket_vis.gps_goal.transport = core.pLCMTransport("/gps_goal") + +# Send visualization data to the Foxglove plugin. +websocket_vis.robot_pose.connect(connection.odom) +websocket_vis.path.connect(global_planner.path) +websocket_vis.global_costmap.connect(mapper.global_costmap) +websocket_vis.gps_location.connect(connection.gps_location) + +# Start the module +websocket_vis.start() +``` + +### Accessing the Interface + +See `dimos/web/command-center-extension/README.md` for how to add the command-center plugin in Foxglove. diff --git a/dimos/web/websocket_vis/build.ts b/dimos/web/websocket_vis/build.ts deleted file mode 100644 index 1464eac2c5..0000000000 --- a/dimos/web/websocket_vis/build.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as esbuild from "npm:esbuild" -import { denoPlugins } from "jsr:@luca/esbuild-deno-loader" -import type { BuildOptions } from "npm:esbuild" - -const args = Deno.args -const watchMode = args.includes("--watch") - -const buildOptions: BuildOptions = { - plugins: [...denoPlugins()], - conditions: ["browser", "deno", "node"], - entryPoints: [ - "./clientside/init.ts", - // vs2.tsx is imported by init.ts, so we don't need to add it here - ], - outfile: "./static/js/clientside.js", - bundle: true, - format: "esm", - target: ["es2020"], - define: { - "import.meta.url": '""', - "import.meta": "false", - "process.env.NODE_ENV": '"production"', - }, - loader: { - ".tsx": "tsx", - ".ts": "ts", - }, - jsx: "transform", // Use transform instead of automatic - jsxFactory: "React.createElement", - jsxFragment: "React.Fragment", - platform: "browser", - // Generate source maps - sourcemap: true, -} - -async function build() { - try { - const timestamp = new Date().toLocaleTimeString() - await esbuild.build(buildOptions) - console.log(`[${timestamp}] Build completed successfully`) - } catch (error) { - console.error(`Build failed:`, error) - } -} - -if (watchMode) { - // Use Deno's built-in watch functionality - const watcher = Deno.watchFs(["./clientside"], { recursive: true }) - - // Initial build - await build() - console.log("Watching for changes...") - - for await (const event of watcher) { - if (["create", "modify"].includes(event.kind)) { - console.log(`Changes detected in ${event.paths}`) - await build() - } - } -} else { - await build() - esbuild.stop() -} diff --git a/dimos/web/websocket_vis/clientside/decoder.ts b/dimos/web/websocket_vis/clientside/decoder.ts deleted file mode 100644 index ff4439d799..0000000000 --- a/dimos/web/websocket_vis/clientside/decoder.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Costmap, EncodedSomething, Grid, Path, Vector } from "./types.ts"; - -export function decode(data: EncodedSomething) { - // console.log("decoding", data) - if (data.type == "costmap") { - return Costmap.decode(data); - } - if (data.type == "vector") { - return Vector.decode(data); - } - if (data.type == "grid") { - return Grid.decode(data); - } - if (data.type == "path") { - return Path.decode(data); - } - - return "UNKNOWN"; -} diff --git a/dimos/web/websocket_vis/clientside/init.ts b/dimos/web/websocket_vis/clientside/init.ts deleted file mode 100644 index 4367d89819..0000000000 --- a/dimos/web/websocket_vis/clientside/init.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { io } from "npm:socket.io-client"; -import { decode } from "./decoder.ts"; -import { Drawable, EncodedSomething } from "./types.ts"; -import { Visualizer as ReactVisualizer } from "./vis2.tsx"; - -// Store server state locally -let serverState = { - status: "disconnected", - connected_clients: 0, - data: {}, - draw: {}, -}; - -let reactVisualizer: ReactVisualizer | null = null; - -const socket = io(); - -socket.on("connect", () => { - console.log("Connected to server"); - serverState.status = "connected"; -}); - -socket.on("disconnect", () => { - console.log("Disconnected from server"); - serverState.status = "disconnected"; -}); - -socket.on("message", (data) => { - //console.log("Received message:", data) -}); - -// Deep merge function for client-side state updates -function deepMerge(source: any, destination: any): any { - for (const key in source) { - // If both source and destination have the property and both are objects, merge them - if ( - key in destination && - typeof source[key] === "object" && - source[key] !== null && - typeof destination[key] === "object" && - destination[key] !== null && - !Array.isArray(source[key]) && - !Array.isArray(destination[key]) - ) { - deepMerge(source[key], destination[key]); - } else { - // Otherwise, just copy the value - destination[key] = source[key]; - } - } - return destination; -} - -type DrawConfig = { [key: string]: any }; - -type EncodedDrawable = EncodedSomething; -type EncodedDrawables = { - [key: string]: EncodedDrawable; -}; -type Drawables = { - [key: string]: Drawable; -}; - -function decodeDrawables(encoded: EncodedDrawables): Drawables { - const drawables: Drawables = {}; - for (const [key, value] of Object.entries(encoded)) { - // @ts-ignore - drawables[key] = decode(value); - } - return drawables; -} - -function state_update(state: { [key: string]: any }) { - //console.log("Received state update:", state) - // Use deep merge to update nested properties - - if (state.draw) { - state.draw = decodeDrawables(state.draw); - } - - // console.log("Decoded state update:", state); - // Create a fresh copy of the server state to trigger rerenders properly - serverState = { ...deepMerge(state, { ...serverState }) }; - - updateUI(); -} - -socket.on("state_update", state_update); -socket.on("full_state", state_update); - -// Function to send data to server -function emitMessage(data: any) { - socket.emit("message", data); -} - -// Function to update UI based on state -function updateUI() { - // console.log("Current state:", serverState); - - // Update both visualizers if they exist and there's data to display - if (serverState.draw && Object.keys(serverState.draw).length > 0) { - if (reactVisualizer) { - reactVisualizer.visualizeState(serverState.draw); - } - } -} - -// Initialize the application -function initializeApp() { - console.log("DOM loaded, initializing UI"); - reactVisualizer = new ReactVisualizer("#vis"); - - // Set up click handler to convert clicks to world coordinates and send to server - reactVisualizer.onWorldClick((worldX, worldY) => { - emitMessage({ type: "click", position: [worldX, worldY] }); - }); - - updateUI(); -} - -console.log("Socket.IO client initialized"); - -// Call initialization once when the DOM is loaded -document.addEventListener("DOMContentLoaded", initializeApp); diff --git a/dimos/web/websocket_vis/clientside/types.ts b/dimos/web/websocket_vis/clientside/types.ts deleted file mode 100644 index 8f7a03c3b9..0000000000 --- a/dimos/web/websocket_vis/clientside/types.ts +++ /dev/null @@ -1,97 +0,0 @@ -type EncodedVector = Encoded<"vector"> & { - c: number[] -} - -export class Vector { - coords: number[] - constructor(...coords: number[]) { - this.coords = coords - } - - static decode(data: EncodedVector): Vector { - return new Vector(...data.c) - } -} - -type EncodedPath = Encoded<"path"> & { - points: Array<[number, number]> -} - -export class Path { - constructor(public coords: Array<[number, number]>) { - } - - static decode(data: EncodedPath): Path { - return new Path(data.points) - } -} - -type EncodedCostmap = Encoded<"costmap"> & { - grid: EncodedGrid - origin: EncodedVector - resolution: number - origin_theta: number -} - -export class Costmap { - constructor( - public grid: Grid, - public origin: Vector, - public resolution: number, - public origin_theta: number, - ) { - this.grid = grid - this.origin = origin - this.resolution = resolution - this.origin_theta = origin_theta - } - - static decode(data: EncodedCostmap): Costmap { - return new Costmap( - Grid.decode(data.grid), - Vector.decode(data.origin), - data.resolution, - data.origin_theta, - ) - } -} - -const DTYPE = { - f32: Float32Array, - f64: Float64Array, - i32: Int32Array, - i8: Int8Array, -} - -type EncodedGrid = Encoded<"grid"> & { - shape: [number, number] - dtype: keyof typeof DTYPE - compressed: boolean - data: string -} - -export class Grid { - constructor( - public data: Float32Array | Float64Array | Int32Array | Int8Array, - public shape: number[], - ) {} - - static decode(msg: EncodedGrid): Grid { - const bytes = Uint8Array.from(atob(msg.data), (c) => c.charCodeAt(0)) - const raw = bytes - const Arr = DTYPE[msg.dtype] || Uint8Array // fallback - return new Grid(new Arr(raw.buffer), msg.shape) - } -} - -export type Drawable = Costmap | Vector | Path - -export type Encoded = { - type: T -} - -export type EncodedSomething = - | EncodedCostmap - | EncodedVector - | EncodedGrid - | EncodedPath diff --git a/dimos/web/websocket_vis/clientside/vis.ts b/dimos/web/websocket_vis/clientside/vis.ts deleted file mode 100644 index 29cb77563a..0000000000 --- a/dimos/web/websocket_vis/clientside/vis.ts +++ /dev/null @@ -1,309 +0,0 @@ -import * as d3 from "npm:d3" -import { Costmap, Drawable, Grid, Vector } from "./types.ts" - -export class CostmapVisualizer { - private svg: d3.Selection - private canvas: HTMLCanvasElement | null = null - private width: number - private height: number - private colorScale: d3.ScaleSequential - private cellSize: number = 4 // Default cell size - - constructor( - selector: string, - width: number = 800, - height: number = 600, - ) { - this.width = width - this.height = height - - // Create or select SVG element with responsive dimensions - this.svg = d3.select(selector) - .append("svg") - .attr("width", "100%") - .attr("height", "100%") - .attr("viewBox", `0 0 ${width} ${height}`) - .attr("preserveAspectRatio", "xMidYMid meet") - .style("background-color", "#f8f9fa") - - this.colorScale = d3.scaleSequential(d3.interpolateGreys) - } - - public visualize( - costmap: Costmap, - ): void { - const { grid, origin, resolution, origin_theta } = costmap - const [rows, cols] = grid.shape - - // Adjust cell size based on grid dimensions and container size - this.cellSize = Math.min( - this.width / cols, - this.height / rows, - ) - - // Calculate the required area for the grid - const gridWidth = cols * this.cellSize - const gridHeight = rows * this.cellSize - - // Clear previous visualization - this.svg.selectAll("*").remove() - - // Add transformation group for the entire costmap - const costmapGroup = this.svg - .append("g") - .attr( - "transform", - `translate(${(this.width - gridWidth) / 2}, ${ - (this.height - gridHeight) / 2 - })`, - ) - - // Determine value range for proper coloring - const minValue = 0 - const maxValue = 100 - - this.colorScale.domain([minValue, maxValue]) - - // Create a canvas element for fast rendering that fills the container - const foreignObject = costmapGroup.append("foreignObject") - .attr("width", gridWidth) - .attr("height", gridHeight) - - // Add a canvas element inside the foreignObject, ensuring it fills the container - const canvasDiv = foreignObject.append("xhtml:div") - .style("width", "100%") - .style("height", "100%") - .style("display", "flex") - .style("align-items", "center") - .style("justify-content", "center") - - // Create canvas element if not exists - if (!this.canvas) { - this.canvas = document.createElement("canvas") - canvasDiv.node()?.appendChild(this.canvas) - } else { - // Reuse existing canvas - canvasDiv.node()?.appendChild(this.canvas) - } - - // Set canvas size - physical pixel dimensions for rendering - this.canvas.width = cols - this.canvas.height = rows - - // Set canvas display size to fill the available space - this.canvas.style.width = "100%" - this.canvas.style.height = "100%" - this.canvas.style.objectFit = "contain" // Maintains aspect ratio - - // Get canvas context and render the grid - const ctx = this.canvas.getContext("2d") - if (ctx) { - // Create ImageData directly from the grid data - const imageData = ctx.createImageData(cols, rows) - const typedArray = grid.data - - // Fill the image data with colors based on the grid values - for (let i = 0; i < typedArray.length; i++) { - const value = typedArray[i] - // Get color from scale - const color = d3.color(this.colorScale(value)) - if (color) { - const idx = i * 4 - imageData.data[idx] = color.r || 0 // Red - imageData.data[idx + 1] = color.g || 0 // Green - imageData.data[idx + 2] = color.b || 0 // Blue - imageData.data[idx + 3] = 255 // Alpha (fully opaque) - } - } - - // Put the image data on the canvas - ctx.putImageData(imageData, 0, 0) - } - - // Add coordinates/scale - this.addCoordinateSystem( - costmapGroup, - gridWidth, - gridHeight, - origin, - resolution, - ) - } - - private addCoordinateSystem( - group: d3.Selection, - width: number, - height: number, - origin: Vector, - resolution: number, - ): void { - // Add axes at the bottom and left edge - const xScale = d3.scaleLinear() - .domain([origin.coords[0], origin.coords[0] + width * resolution]) - .range([0, width]) - - const yScale = d3.scaleLinear() - .domain([origin.coords[1], origin.coords[1] + height * resolution]) - .range([height, 0]) - - // Add x-axis at the bottom - const xAxis = d3.axisBottom(xScale).ticks(5) - group.append("g") - .attr("transform", `translate(0, ${height})`) - .call(xAxis) - .attr("class", "axis") - - // Add y-axis at the left - const yAxis = d3.axisLeft(yScale).ticks(5) - group.append("g") - .call(yAxis) - .attr("class", "axis") - } - - /** - * @deprecated Use visualize with interpolator parameter directly - */ - public setColorScale(interpolator: (t: number) => string): void { - console.warn( - "setColorScale is deprecated, pass the interpolator directly to visualize", - ) - } - - // Method to add a legend for the costmap values - public addLegend(minValue: number, maxValue: number): void { - // Create a gradient definition - const defs = this.svg.append("defs") - const gradient = defs.append("linearGradient") - .attr("id", "costmap-gradient") - .attr("x1", "0%") - .attr("y1", "0%") - .attr("x2", "100%") - .attr("y2", "0%") - - // Add color stops - const steps = 10 - for (let i = 0; i <= steps; i++) { - const t = i / steps - gradient.append("stop") - .attr("offset", `${t * 100}%`) - .attr( - "stop-color", - this.colorScale(minValue + t * (maxValue - minValue)), - ) - } - - // Add a rectangle with the gradient - const legendWidth = 200 - const legendHeight = 20 - - const legend = this.svg.append("g") - .attr("class", "legend") - .attr( - "transform", - `translate(${this.width - legendWidth - 20}, 20)`, - ) - - legend.append("rect") - .attr("width", legendWidth) - .attr("height", legendHeight) - .style("fill", "url(#costmap-gradient)") - - // Add labels - const legendScale = d3.scaleLinear() - .domain([minValue, maxValue]) - .range([0, legendWidth]) - - const legendAxis = d3.axisBottom(legendScale).ticks(5) - - legend.append("g") - .attr("transform", `translate(0, ${legendHeight})`) - .call(legendAxis) - } -} - -// Helper function to create and hook up visualization -export function createCostmapVis( - selector: string, - width: number = 800, - height: number = 600, -): CostmapVisualizer { - return new CostmapVisualizer(selector, width, height) -} - -// Extension to visualize multiple drawables -export class RobotStateVisualizer { - private costmapVis: CostmapVisualizer - private svg: d3.Selection - private containerSelector: string - private resizeObserver: ResizeObserver | null = null - - constructor( - selector: string, - width: number = 800, - height: number = 600, - ) { - this.containerSelector = selector - - // Create base SVG with responsive sizing - this.svg = d3.select(selector) - .append("svg") - .attr("width", "100%") - .attr("height", "100%") - .attr("viewBox", `0 0 ${width} ${height}`) - .attr("preserveAspectRatio", "xMidYMid meet") - .style("background-color", "#f8f9fa") - - // Create costmap visualizer that will render to the same SVG - this.costmapVis = new CostmapVisualizer(selector, width, height) - - // Set up resize observer to update when container size changes - const container = document.querySelector(selector) - if (container && window.ResizeObserver) { - this.resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - const { width, height } = entry.contentRect - if (width > 0 && height > 0) { - this.updateSize(width, height) - } - } - }) - this.resizeObserver.observe(container) - } - } - - private updateSize(width: number, height: number): void { - // Update viewBox to maintain aspect ratio - this.svg.attr("viewBox", `0 0 ${width} ${height}`) - } - - public visualizeState( - state: { [key: string]: Drawable }, - ): void { - // Clear previous visualization - this.svg.selectAll("*").remove() - - // Visualize each drawable based on its type - for (const [key, drawable] of Object.entries(state)) { - if (drawable instanceof Costmap) { - this.costmapVis.visualize(drawable) - } else if (drawable instanceof Vector) { - this.visualizeVector(drawable, key) - } - } - } - - private visualizeVector(vector: Vector, label: string): void { - // Implement vector visualization (arrows, points, etc.) - // This is a simple implementation showing vectors as points - const [x, y] = vector.coords - - console.log("VIS VECTOR", vector) - this.svg.append("circle") - .attr("cx", 200) - .attr("cy", 200) - .attr("r", 20) - .attr("fill", "red") - .append("title") - .text(`${label}: (${x}, ${y})`) - } -} diff --git a/dimos/web/websocket_vis/clientside/vis2.tsx b/dimos/web/websocket_vis/clientside/vis2.tsx deleted file mode 100644 index e0052a210d..0000000000 --- a/dimos/web/websocket_vis/clientside/vis2.tsx +++ /dev/null @@ -1,625 +0,0 @@ -import * as d3 from "npm:d3"; -import * as React from "npm:react"; -import * as ReactDOMClient from "npm:react-dom/client"; -import { Costmap, Drawable, Path, Vector } from "./types.ts"; - -// ─────────────────────────────────────────────────────────────────────────────── -// React component -// ─────────────────────────────────────────────────────────────────────────────── -const VisualizerComponent: React.FC<{ state: Record }> = ({ - state, -}) => { - const svgRef = React.useRef(null); - const [dimensions, setDimensions] = React.useState({ - width: 800, - height: 600, - }); - const { width, height } = dimensions; - - // Update dimensions when container size changes - React.useEffect(() => { - if (!svgRef.current) return; - - const updateDimensions = () => { - const rect = svgRef.current?.parentElement?.getBoundingClientRect(); - if (rect) { - setDimensions({ width: rect.width, height: rect.height }); - } - }; - - // Initial update - updateDimensions(); - - // Create resize observer - const observer = new ResizeObserver(updateDimensions); - observer.observe(svgRef.current.parentElement as Element); - - return () => observer.disconnect(); - }, []); - - /** Build a world→pixel transformer from the *first* cost‑map we see. */ - const { worldToPx, pxToWorld } = React.useMemo(() => { - const ref = Object.values(state).find( - (d): d is Costmap => d instanceof Costmap, - ); - if (!ref) return { worldToPx: undefined, pxToWorld: undefined }; - - const { - grid: { shape }, - origin, - resolution, - } = ref; - const [rows, cols] = shape; - - // Same sizing/centering logic used in visualiseCostmap - const cell = Math.min(width / cols, height / rows); - const gridW = cols * cell; - const gridH = rows * cell; - const offsetX = (width - gridW) / 2; - const offsetY = (height - gridH) / 2; - - const xScale = d3 - .scaleLinear() - .domain([origin.coords[0], origin.coords[0] + cols * resolution]) - .range([offsetX, offsetX + gridW]); - - const yScale = d3 - .scaleLinear() - .domain([origin.coords[1], origin.coords[1] + rows * resolution]) - .range([offsetY + gridH, offsetY]); // invert y (world ↑ => svg ↑) - - // World coordinates to pixel coordinates - const worldToPxFn = ( - x: number, - y: number, - ): [number, number] => [xScale(x), yScale(y)]; - - // Pixel coordinates to world coordinates (inverse transform) - const pxToWorldFn = ( - x: number, - y: number, - ): [number, number] => [ - xScale.invert(x), - yScale.invert(y), - ]; - - return { - worldToPx: worldToPxFn, - pxToWorld: pxToWorldFn, - }; - }, [state]); - - // Removed component-level click handler as we're using the global one in Visualizer class - - React.useEffect(() => { - if (!svgRef.current) return; - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - // 1. maps (bottom layer) - Object.values(state).forEach((d) => { - if (d instanceof Costmap) visualiseCostmap(svg, d, width, height); - }); - - // 2. paths (middle layer) - Object.entries(state).forEach(([key, d]) => { - if (d instanceof Path) { - visualisePath(svg, d, key, worldToPx, width, height); - } - }); - - // 3. vectors (top layer) - Object.entries(state).forEach(([key, d]) => { - if (d instanceof Vector) { - visualiseVector(svg, d, key, worldToPx, width, height); - } - }); - - // Removed click handler as we're using the global one in Visualizer class - }, [state, worldToPx]); - - return ( -
- -
- ); -}; - -// ─────────────────────────────────────────────────────────────────────────────── -// Helper: costmap -// ─────────────────────────────────────────────────────────────────────────────── -function visualiseCostmap( - svg: d3.Selection, - costmap: Costmap, - width: number, - height: number, -): void { - const { grid, origin, resolution } = costmap; - const [rows, cols] = grid.shape; - - const cell = Math.min(width / cols, height / rows); - const gridW = cols * cell; - const gridH = rows * cell; - - const group = svg - .append("g") - .attr( - "transform", - `translate(${(width - gridW) / 2}, ${(height - gridH) / 2})`, - ); - - // Custom color interpolation function that maps 0 to white and other values to Inferno scale - const customColorScale = (t: number) => { - // If value is 0 (or very close to it), return dark bg color - // bluest #2d2136 - if (t == 0) return "white"; - if (t < 0) return "#2d2136"; - if (t > 0.95) return "#000000"; - - const color = d3.interpolateTurbo((t * 2) - 1); - const hsl = d3.hsl(color); - hsl.s *= 0.75; - return hsl.toString(); - }; - - const colour = d3.scaleSequential(customColorScale).domain([ - -1, - 100, - ]); - - const fo = group.append("foreignObject").attr("width", gridW).attr( - "height", - gridH, - ); - - const canvas = document.createElement("canvas"); - canvas.width = cols; - canvas.height = rows; - Object.assign(canvas.style, { - width: "100%", - height: "100%", - objectFit: "contain", - backgroundColor: "black", - }); - - fo.append("xhtml:div") - .style("width", "100%") - .style("height", "100%") - .style("display", "flex") - .style("alignItems", "center") - .style("justifyContent", "center") - .node() - ?.appendChild(canvas); - - const ctx = canvas.getContext("2d"); - if (ctx) { - const img = ctx.createImageData(cols, rows); - const data = grid.data; // row‑major, (0,0) = world south‑west - - // Flip vertically so world north appears at top of SVG - for (let i = 0; i < data.length; i++) { - const row = Math.floor(i / cols); - const col = i % cols; - - // Flip Y coordinate (invert row) to put origin at bottom-left - const invertedRow = rows - 1 - row; - const srcIdx = invertedRow * cols + col; - - const value = data[i]; // Get value from original index - const c = d3.color(colour(value)); - if (!c) continue; - const o = srcIdx * 4; // Write to flipped position - img.data[o] = c.r ?? 0; - img.data[o + 1] = c.g ?? 0; - img.data[o + 2] = c.b ?? 0; - img.data[o + 3] = 255; - } - ctx.putImageData(img, 0, 0); - } - - addCoordinateSystem(group, gridW, gridH, origin, resolution); -} - -// ─────────────────────────────────────────────────────────────────────────────── -// Helper: coordinate system -// ─────────────────────────────────────────────────────────────────────────────── -function addCoordinateSystem( - group: d3.Selection, - width: number, - height: number, - origin: Vector, - resolution: number, -): void { - const minX = origin.coords[0]; - const minY = origin.coords[1]; - - const maxX = minX + (width * resolution); - const maxY = minY + (height * resolution); - //console.log(group, width, origin, maxX); - - const xScale = d3.scaleLinear().domain([ - minX, - maxX, - ]).range([0, width]); - const yScale = d3.scaleLinear().domain([ - minY, - maxY, - ]).range([height, 0]); - - const gridSize = 1 / resolution; - const gridColour = "#000"; - const gridGroup = group.append("g").attr("class", "grid"); - - for ( - const x of d3.range( - Math.ceil(minX / gridSize) * gridSize, - maxX, - gridSize, - ) - ) { - gridGroup - .append("line") - .attr("x1", xScale(x)) - .attr("y1", 0) - .attr("x2", xScale(x)) - .attr("y2", height) - .attr("stroke", gridColour) - .attr("stroke-width", 0.5) - .attr("opacity", 0.25); - } - - for ( - const y of d3.range( - Math.ceil(minY / gridSize) * gridSize, - maxY, - gridSize, - ) - ) { - gridGroup - .append("line") - .attr("x1", 0) - .attr("y1", yScale(y)) - .attr("x2", width) - .attr("y2", yScale(y)) - .attr("stroke", gridColour) - .attr("stroke-width", 0.5) - .attr("opacity", 0.25); - } - - const stylise = ( - sel: d3.Selection, - ) => { - sel.selectAll("line,path") - .attr("stroke", "#ffffff") - .attr("stroke-width", 1); - sel.selectAll("text") - .attr("fill", "#ffffff"); // Change the color here - }; - - group - .append("g") - .attr("transform", `translate(0, ${height})`) - .call(d3.axisBottom(xScale).ticks(7)) - .call(stylise); - group.append("g").call(d3.axisLeft(yScale).ticks(7)).call(stylise); - - if (minX <= 0 && 0 <= maxX && minY <= 0 && 0 <= maxY) { - const originPoint = group.append("g") - .attr("class", "origin-marker") - .attr("transform", `translate(${xScale(0)}, ${yScale(0)})`); - - // Add outer ring - originPoint.append("circle") - .attr("r", 8) - .attr("fill", "none") - .attr("stroke", "#00e676") - .attr("stroke-width", 1) - .attr("opacity", 0.5); - - // Add center point - originPoint.append("circle") - .attr("r", 4) - .attr("fill", "#00e676") - .attr("opacity", 0.9) - .append("title") - .text("World Origin (0,0)"); - } -} - -// ─────────────────────────────────────────────────────────────────────────────── -// Helper: path -// ─────────────────────────────────────────────────────────────────────────────── -function visualisePath( - svg: d3.Selection, - path: Path, - label: string, - wp: ((x: number, y: number) => [number, number]) | undefined, - width: number, - height: number, -): void { - if (path.coords.length < 2) return; - - const points = path.coords.map(([x, y]) => { - return wp ? wp(x, y) : [width / 2 + x, height / 2 - y]; - }); - - // Create a path line - const line = d3.line(); - - // Create a gradient for the path - const pathId = `path-gradient-${label.replace(/\s+/g, "-")}`; - - svg.append("defs") - .append("linearGradient") - .attr("id", pathId) - .attr("gradientUnits", "userSpaceOnUse") - .attr("x1", points[0][0]) - .attr("y1", points[0][1]) - .attr("x2", points[points.length - 1][0]) - .attr("y2", points[points.length - 1][1]) - .selectAll("stop") - .data([ - //{ offset: "0%", color: "#4fc3f7" }, - //{ offset: "100%", color: "#f06292" }, - { offset: "0%", color: "#ff3333" }, - { offset: "100%", color: "#ff3333" }, - ]) - .enter().append("stop") - .attr("offset", (d) => d.offset) - .attr("stop-color", (d) => d.color); - - // Create the path with gradient and animation - svg.append("path") - .datum(points) - .attr("fill", "none") - .attr("stroke", `url(#${pathId})`) - .attr("stroke-width", 5) - .attr("stroke-linecap", "round") - .attr("filter", "url(#glow)") - .attr("opacity", 0.9) - .attr("d", line); -} - -// ─────────────────────────────────────────────────────────────────────────────── -// Helper: vector -// ─────────────────────────────────────────────────────────────────────────────── -function visualiseVector( - svg: d3.Selection, - vector: Vector, - label: string, - wp: ((x: number, y: number) => [number, number]) | undefined, - width: number, - height: number, -): void { - const [cx, cy] = wp - ? wp(vector.coords[0], vector.coords[1]) - : [width / 2 + vector.coords[0], height / 2 - vector.coords[1]]; - - // Create a vector marker group - const vectorGroup = svg.append("g") - .attr("class", "vector-marker") - .attr("transform", `translate(${cx}, ${cy})`); - - // Add a glowing outer ring - vectorGroup.append("circle") - .attr("r", ".7em") - .attr("fill", "none") - // .attr("stroke", "#4fc3f7") - .attr("stroke", "red") - .attr("stroke-width", "1") - .attr("opacity", 0.9); - - // Add inner dot - vectorGroup.append("circle") - .attr("r", ".4em") - // .attr("fill", "#4fc3f7") - .attr("fill", "red"); - - // Add text with background - const text = `${label} (${vector.coords[0].toFixed(2)}, ${ - vector.coords[1].toFixed(2) - })`; - - // Create a group for the text and background - const textGroup = svg.append("g"); - - // Add text element - const textElement = textGroup - .append("text") - .attr("x", cx + 25) - .attr("y", cy + 25) - .attr("font-size", "1em") - .attr("fill", "white") - .text(text); - - // Add background rect - const bbox = textElement.node()?.getBBox(); - if (bbox) { - textGroup - .insert("rect", "text") - .attr("x", bbox.x - 1) - .attr("y", bbox.y - 1) - .attr("width", bbox.width + 2) - .attr("height", bbox.height + 2) - .attr("fill", "black") - .attr("stroke", "black") - .attr("opacity", 0.75); - } -} - -// ─────────────────────────────────────────────────────────────────────────────── -// Wrapper class -// ─────────────────────────────────────────────────────────────────────────────── -export class Visualizer { - private container: HTMLElement | null; - private state: Record = {}; - private resizeObserver: ResizeObserver | null = null; - private root: ReactDOMClient.Root; - private onClickCallback: ((worldX: number, worldY: number) => void) | null = null; - private lastClickTime: number = 0; - private clickThrottleMs: number = 150; // Minimum ms between processed clicks - - constructor(selector: string) { - this.container = document.querySelector(selector); - if (!this.container) { - throw new Error(`Container not found: ${selector}`); - } - this.root = ReactDOMClient.createRoot(this.container); - - // First paint - this.render(); - - // Keep canvas responsive - if (window.ResizeObserver) { - this.resizeObserver = new ResizeObserver(() => this.render()); - this.resizeObserver.observe(this.container); - } - - // Bind the click handler once to preserve reference for cleanup - this.handleGlobalClick = this.handleGlobalClick.bind(this); - - // Set up click handler directly on the container with capture phase - // This ensures we get the event before any SVG elements - if (this.container) { - this.container.addEventListener("click", this.handleGlobalClick, true); - } - } - - /** Register a callback for when user clicks on the visualization */ - public onWorldClick( - callback: (worldX: number, worldY: number) => void, - ): void { - this.onClickCallback = callback; - } - - /** Handle global click events, filtering for clicks within our SVG */ - private handleGlobalClick(event: MouseEvent): void { - if (!this.onClickCallback || !this.container) return; - - // Stop propagation to prevent other handlers from interfering - event.stopPropagation(); - - // Throttle clicks to prevent issues with high refresh rates - const now = Date.now(); - if (now - this.lastClickTime < this.clickThrottleMs) { - console.log("Click throttled"); - return; - } - this.lastClickTime = now; - - // We don't need to check if click was inside container since we're attaching - // the event listener directly to the container - - console.log("Processing click at", event.clientX, event.clientY); - - // Find our SVG element - const svgElement = this.container.querySelector("svg"); - if (!svgElement) return; - - // Calculate click position relative to SVG viewport - const svgRect = svgElement.getBoundingClientRect(); - const viewportX = event.clientX - svgRect.left; - const viewportY = event.clientY - svgRect.top; - - // Convert to SVG coordinate space (accounting for viewBox) - const svgPoint = new DOMPoint(viewportX, viewportY); - const transformedPoint = svgPoint.matrixTransform( - svgElement.getScreenCTM()?.inverse() || new DOMMatrix(), - ); - - // Find a costmap to use for coordinate conversion - const costmap = Object.values(this.state).find( - (d): d is Costmap => d instanceof Costmap, - ); - - if (!costmap) return; - - const { - grid: { shape }, - origin, - resolution, - } = costmap; - const [rows, cols] = shape; - // Use the current SVG dimensions instead of hardcoded values - const width = svgRect.width; - const height = svgRect.height; - - // Calculate scales (same logic as in the component) - const cell = Math.min(width / cols, height / rows); - const gridW = cols * cell; - const gridH = rows * cell; - const offsetX = (width - gridW) / 2; - const offsetY = (height - gridH) / 2; - - const xScale = d3 - .scaleLinear() - .domain([offsetX, offsetX + gridW]) - .range([origin.coords[0], origin.coords[0] + cols * resolution]); - const yScale = d3 - .scaleLinear() - .domain([offsetY + gridH, offsetY]) - .range([origin.coords[1], origin.coords[1] + rows * resolution]); - - // Convert to world coordinates - const worldX = xScale(transformedPoint.x); - const worldY = yScale(transformedPoint.y); - - console.log("Calling callback with world coords:", worldX.toFixed(2), worldY.toFixed(2)); - - // Call the callback with the world coordinates - this.onClickCallback(worldX, worldY); - } - - /** Push a new application‑state snapshot to the visualiser */ - public visualizeState(state: Record): void { - // Store reference to current state before updating - const prevState = this.state; - this.state = { ...state }; - - // Don't re-render if we're currently processing a click - const timeSinceLastClick = Date.now() - this.lastClickTime; - if (timeSinceLastClick < this.clickThrottleMs) { - console.log("Skipping render during click processing"); - return; - } - - this.render(); - } - - /** React‑render the component tree */ - private render(): void { - this.root.render(); - } - - /** Tear down listeners and free resources */ - public cleanup(): void { - if (this.resizeObserver && this.container) { - this.resizeObserver.unobserve(this.container); - this.resizeObserver.disconnect(); - } - - if (this.container) { - this.container.removeEventListener("click", this.handleGlobalClick, true); - } - } -} - -// Convenience factory ---------------------------------------------------------- -export function createReactVis(selector: string): Visualizer { - return new Visualizer(selector); -} diff --git a/dimos/web/websocket_vis/clientside/vis3.tsx b/dimos/web/websocket_vis/clientside/vis3.tsx deleted file mode 100644 index d0e5b615c9..0000000000 --- a/dimos/web/websocket_vis/clientside/vis3.tsx +++ /dev/null @@ -1,640 +0,0 @@ -import * as React from "npm:react" -import * as ReactDOMClient from "npm:react-dom/client" -import * as THREE from "npm:three" -import { Canvas, extend, Object3DNode, useThree } from "npm:@react-three/fiber" -import { - Billboard, - Line, - OrbitControls, - Plane, - Text, -} from "npm:@react-three/drei" -import { Costmap, Drawable, Path, Vector } from "./types.ts" - -// ─────────────────────────────────────────────────────────────────────────────── -// Extend with OrbitControls -// ─────────────────────────────────────────────────────────────────────────────── -extend({ OrbitControls }) -declare global { - namespace JSX { - interface IntrinsicElements { - orbitControls: Object3DNode - } - } -} - -// ─────────────────────────────────────────────────────────────────────────────── -// Camera Controls -// ─────────────────────────────────────────────────────────────────────────────── -function CameraControls() { - const { camera, gl } = useThree() - const controlsRef = React.useRef(null) - - React.useEffect(() => { - if (controlsRef.current) { - // Set initial camera position to better show the 3D effect - camera.position.set(5, 8, 5) - camera.lookAt(0, 0, 0) - - // Update controls with better settings for 3D viewing - controlsRef.current.minDistance = 2 - controlsRef.current.maxDistance = 50 - controlsRef.current.update() - } - }, [camera]) - - return ( - - ) -} - -// ─────────────────────────────────────────────────────────────────────────────── -// Grid Component -// ─────────────────────────────────────────────────────────────────────────────── -interface GridProps { - size: number - divisions: number - color?: string -} - -function Grid({ size = 10, divisions = 10, color = "#666666" }: GridProps) { - return ( - - ) -} - -// ─────────────────────────────────────────────────────────────────────────────── -// Costmap Component -// ─────────────────────────────────────────────────────────────────────────────── -interface CostmapMeshProps { - costmap: Costmap -} - -function CostmapMesh({ costmap }: CostmapMeshProps) { - const { grid, origin, resolution } = costmap - const [rows, cols] = grid.shape - - // Calculate dimensions - const width = cols * resolution - const height = rows * resolution - - // Position is at the center of the grid - const posX = origin.coords[0] + (width / 2) - const posY = 0 - const posZ = origin.coords[1] + (height / 2) - - // Generate a 3D mesh directly from the costmap data - const meshRef = React.useRef(null) - - // Create the mesh - const colorData = React.useMemo(() => { - const vertices: number[] = [] - const indices: number[] = [] - const colors: number[] = [] - - // Cell size - const cellWidth = width / cols - const cellHeight = height / rows - - // Create vertices and colors - for (let row = 0; row < rows; row++) { - for (let col = 0; col < cols; col++) { - const x = col * cellWidth - width / 2 - const z = row * cellHeight - height / 2 - - // Get cost value - const idx = row * cols + col - const value = grid.data[idx] - - // Map cost value to height (y) with minimal elevation - let y = 0 - if (value < 5) { - y = 0 // Flat for clear paths - } else if (value < 20) { - y = (value / 20) * 0.1 // Barely visible bumps - } else if (value > 80) { - y = 0.3 + ((value - 80) / 20) * 0.2 // Small obstacles - } else { - y = 0.1 + ((value - 20) / 60) * 0.2 // Very gentle elevation - } - - // Add the vertex - vertices.push(x, y, z) - - // Determine color based on cost - original monochromatic scheme - let r, g, b - if (value < 5) { - // Very low cost - light gray (easily passable) - r = 0.9 - g = 0.9 - b = 0.9 - } else if (value < 20) { - // Low cost - slightly darker gray - const t = value / 20 - const val = 0.9 - (t * 0.3) - r = val - g = val - b = val - } else if (value > 80) { - // High cost - dark gray to black (obstacle) - const t = (value - 80) / 20 - const val = 0.25 - (t * 0.25) - r = val - g = val - b = val - } else { - // Medium cost - medium grays - const t = (value - 20) / 60 - const val = 0.6 - (t * 0.35) - r = val - g = val - b = val - } - - colors.push(r, g, b) - } - } - - // Create indices for triangles - for (let row = 0; row < rows - 1; row++) { - for (let col = 0; col < cols - 1; col++) { - const a = row * cols + col - const b = row * cols + col + 1 - const c = (row + 1) * cols + col - const d = (row + 1) * cols + col + 1 - - // First triangle - indices.push(a, c, b) - // Second triangle - indices.push(b, c, d) - } - } - - return { vertices, indices, colors } - }, [grid, rows, cols, width, height]) - - return ( - - {/* Custom Mesh */} - - - - - - - - - - {/* Optional wireframe overlay */} - - - - - - - - - {/* Grid overlay - 10x coarser */} - - - ) -} - -// ─────────────────────────────────────────────────────────────────────────────── -// Path Component -// ─────────────────────────────────────────────────────────────────────────────── -interface PathProps { - path: Path - color?: string - label: string -} - -function PathLine({ path, color = "#ff3333", label }: PathProps) { - if (path.coords.length < 2) return null - - // Path height offset to float above terrain (33% lower) - const pathHeight = 0.47 - - // Convert 2D path coordinates to 3D points - const points = path.coords.map(([x, y]) => - new THREE.Vector3(x, pathHeight, y) - ) - - // Calculate midpoint for label placement - const midIdx = Math.floor(points.length / 2) - const midPoint = points[midIdx] - - return ( - - {/* The path line */} - - - {/* Path label - always faces camera */} - - - - {`${label} (${path.coords.length})`} - - - - - {/* Start point marker */} - - - - - - {/* End point marker */} - - - - - - {/* Vertical connectors to terrain - thicker */} - {path.coords.map(([x, y], idx) => ( - - )).filter((_, idx) => - idx % 8 === 0 || idx === 0 || idx === path.coords.length - 1 - )} {/* Only show some connectors */} - - ) -} - -// ─────────────────────────────────────────────────────────────────────────────── -// Vector Component -// ─────────────────────────────────────────────────────────────────────────────── -interface VectorProps { - vector: Vector - label: string - color?: string -} - -function VectorMarker({ vector, label, color = "#00aaff" }: VectorProps) { - const [x, y] = vector.coords - const markerHeight = 0.47 // Same height as paths (33% lower) - - return ( - - {/* Vector Marker - larger, no ring */} - - - - - - {/* Label - always faces camera */} - - - - {`${label} (${vector.coords[0].toFixed(2)}, ${ - vector.coords[1].toFixed(2) - })`} - - - - - {/* Vertical connector to terrain - thicker */} - - - ) -} - -// ─────────────────────────────────────────────────────────────────────────────── -// Click Detector Component -// ─────────────────────────────────────────────────────────────────────────────── -interface ClickDetectorProps { - onWorldClick: (x: number, y: number) => void -} - -function ClickDetector({ onWorldClick }: ClickDetectorProps) { - const planeRef = React.useRef(null) - - const handleClick = (event: THREE.ThreeEvent) => { - if (planeRef.current && event.intersections.length > 0) { - // Get the intersection point with our invisible plane - const intersection = event.intersections.find( - (i) => i.object === planeRef.current, - ) - - if (intersection) { - const point = intersection.point - onWorldClick(point.x, point.z) - } - } - } - - return ( - - - - - ) -} - -// ─────────────────────────────────────────────────────────────────────────────── -// Color Utilities -// ─────────────────────────────────────────────────────────────────────────────── -// Tron-inspired color palette -const tronColors = [ - "#00FFFF", // Cyan - "#00BFFF", // Deep Sky Blue - "#1E90FF", // Dodger Blue - "#40E0D0", // Turquoise - "#00FF7F", // Spring Green - "#7FFFD4", // Aquamarine - "#48D1CC", // Medium Turquoise - "#87CEFA", // Light Sky Blue - "#0000FF", // Blue - "#007FFF", // Azure - "#4169E1", // Royal Blue -] - -// Generate a consistent color based on the vector name -function getTronColor(name: string): string { - // Hash the string to get a consistent index - let hash = 0 - for (let i = 0; i < name.length; i++) { - hash = ((hash << 5) - hash) + name.charCodeAt(i) - hash |= 0 // Convert to 32bit integer - } - // Get positive index in the color array range - const index = Math.abs(hash) % tronColors.length - return tronColors[index] -} - -// ─────────────────────────────────────────────────────────────────────────────── -// Main Scene Component -// ─────────────────────────────────────────────────────────────────────────────── -interface VisualizerSceneProps { - state: Record - onWorldClick: (x: number, y: number) => void -} - -function VisualizerScene({ state, onWorldClick }: VisualizerSceneProps) { - // Extract the costmaps, paths, and vectors from state - const costmaps = Object.values(state).filter( - (item): item is Costmap => item instanceof Costmap, - ) - - const pathEntries = Object.entries(state).filter( - ([_, item]): item is Path => item instanceof Path, - ) as [string, Path][] - - const vectorEntries = Object.entries(state).filter( - ([_, item]): item is Vector => item instanceof Vector, - ) as [string, Vector][] - - return ( - - {/* Ambient light for basic illumination */} - - - {/* Main directional light */} - - - {/* Additional lights for better 3D effect visibility */} - - - {/* Add a point light to highlight elevation differences */} - - - {/* Camera controls */} - - - {/* Click detector */} - - - {/* Render costmaps (bottom layer) */} - {costmaps.map((costmap, index) => ( - - ))} - - {/* Render paths (middle layer) */} - {pathEntries.map(([key, path]) => ( - - ))} - - {/* Render vectors (top layer) */} - {vectorEntries.map(([key, vector]) => ( - - ))} - - ) -} - -// ─────────────────────────────────────────────────────────────────────────────── -// React Component -// ─────────────────────────────────────────────────────────────────────────────── -const VisualizerComponent: React.FC<{ - state: Record - onWorldClick: (x: number, y: number) => void -}> = ({ - state, - onWorldClick, -}) => { - return ( -
- -
- ) -} - -// ─────────────────────────────────────────────────────────────────────────────── -// Wrapper class (maintains API compatibility with previous visualizer) -// ─────────────────────────────────────────────────────────────────────────────── -export class Visualizer { - private container: HTMLElement | null - private state: Record = {} - private resizeObserver: ResizeObserver | null = null - private root: ReactDOMClient.Root - private onClickCallback: ((worldX: number, worldY: number) => void) | null = - null - - constructor(selector: string) { - this.container = document.querySelector(selector) - if (!this.container) throw new Error(`Container not found: ${selector}`) - this.root = ReactDOMClient.createRoot(this.container) - - // First paint - this.render() - - // Keep canvas responsive - if (window.ResizeObserver) { - this.resizeObserver = new ResizeObserver(() => this.render()) - this.resizeObserver.observe(this.container) - } - } - - /** Register a callback for when user clicks on the visualization */ - public onWorldClick( - callback: (worldX: number, worldY: number) => void, - ): void { - this.onClickCallback = callback - this.render() // Re-render to apply the new callback - } - - /** Handle click event from the 3D scene */ - private handleWorldClick = (x: number, y: number): void => { - if (this.onClickCallback) { - this.onClickCallback(x, y) - } - } - - /** Push a new application‑state snapshot to the visualiser */ - public visualizeState(state: Record): void { - this.state = { ...state } - this.render() - } - - /** React‑render the component tree */ - private render(): void { - this.root.render( - , - ) - } - - /** Tear down listeners and free resources */ - public cleanup(): void { - if (this.resizeObserver && this.container) { - this.resizeObserver.unobserve(this.container) - this.resizeObserver.disconnect() - } - } -} - -// Convenience factory ---------------------------------------------------------- -export function createReactVis(selector: string): Visualizer { - return new Visualizer(selector) -} diff --git a/dimos/web/websocket_vis/costmap_viz.py b/dimos/web/websocket_vis/costmap_viz.py new file mode 100644 index 0000000000..ec2088b3b8 --- /dev/null +++ b/dimos/web/websocket_vis/costmap_viz.py @@ -0,0 +1,65 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Simple costmap wrapper for visualization purposes. +This is a minimal implementation to support websocket visualization. +""" + +import numpy as np + +from dimos.msgs.nav_msgs import OccupancyGrid + + +class CostmapViz: + """A wrapper around OccupancyGrid for visualization compatibility.""" + + def __init__(self, occupancy_grid: OccupancyGrid | None = None) -> None: + """Initialize from an OccupancyGrid.""" + self.occupancy_grid = occupancy_grid + + @property + def data(self) -> np.ndarray | None: + """Get the costmap data as a numpy array.""" + if self.occupancy_grid: + return self.occupancy_grid.grid + return None + + @property + def width(self) -> int: + """Get the width of the costmap.""" + if self.occupancy_grid: + return self.occupancy_grid.width + return 0 + + @property + def height(self) -> int: + """Get the height of the costmap.""" + if self.occupancy_grid: + return self.occupancy_grid.height + return 0 + + @property + def resolution(self) -> float: + """Get the resolution of the costmap.""" + if self.occupancy_grid: + return self.occupancy_grid.resolution + return 1.0 + + @property + def origin(self): + """Get the origin pose of the costmap.""" + if self.occupancy_grid: + return self.occupancy_grid.origin + return None diff --git a/dimos/web/websocket_vis/deno.json b/dimos/web/websocket_vis/deno.json deleted file mode 100644 index 401e578474..0000000000 --- a/dimos/web/websocket_vis/deno.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "nodeModulesDir": "auto", - "tasks": { - "build": "deno run -A build.ts", - "watch": "deno run -A build.ts --watch" - }, - "lint": { - "rules": { - "exclude": ["require-await", "ban-ts-comment"] - } - }, - "fmt": { - "indentWidth": 4, - "useTabs": false, - "semiColons": false - }, - - "compilerOptions": { - "lib": ["dom", "deno.ns"] - } -} - diff --git a/dimos/web/websocket_vis/deno.lock b/dimos/web/websocket_vis/deno.lock deleted file mode 100644 index 279c3fd6c3..0000000000 --- a/dimos/web/websocket_vis/deno.lock +++ /dev/null @@ -1,136 +0,0 @@ -{ - "version": "4", - "specifiers": { - "jsr:@luca/esbuild-deno-loader@*": "0.11.1", - "jsr:@std/bytes@^1.0.2": "1.0.5", - "jsr:@std/encoding@^1.0.5": "1.0.7", - "jsr:@std/path@^1.0.6": "1.0.8", - "npm:esbuild@*": "0.25.2" - }, - "jsr": { - "@luca/esbuild-deno-loader@0.11.1": { - "integrity": "dc020d16d75b591f679f6b9288b10f38bdb4f24345edb2f5732affa1d9885267", - "dependencies": [ - "jsr:@std/bytes", - "jsr:@std/encoding", - "jsr:@std/path" - ] - }, - "@std/bytes@1.0.5": { - "integrity": "4465dd739d7963d964c809202ebea6d5c6b8e3829ef25c6a224290fbb8a1021e" - }, - "@std/encoding@1.0.7": { - "integrity": "f631247c1698fef289f2de9e2a33d571e46133b38d042905e3eac3715030a82d" - }, - "@std/path@1.0.8": { - "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" - } - }, - "npm": { - "@esbuild/aix-ppc64@0.25.2": { - "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==" - }, - "@esbuild/android-arm64@0.25.2": { - "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==" - }, - "@esbuild/android-arm@0.25.2": { - "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==" - }, - "@esbuild/android-x64@0.25.2": { - "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==" - }, - "@esbuild/darwin-arm64@0.25.2": { - "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==" - }, - "@esbuild/darwin-x64@0.25.2": { - "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==" - }, - "@esbuild/freebsd-arm64@0.25.2": { - "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==" - }, - "@esbuild/freebsd-x64@0.25.2": { - "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==" - }, - "@esbuild/linux-arm64@0.25.2": { - "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==" - }, - "@esbuild/linux-arm@0.25.2": { - "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==" - }, - "@esbuild/linux-ia32@0.25.2": { - "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==" - }, - "@esbuild/linux-loong64@0.25.2": { - "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==" - }, - "@esbuild/linux-mips64el@0.25.2": { - "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==" - }, - "@esbuild/linux-ppc64@0.25.2": { - "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==" - }, - "@esbuild/linux-riscv64@0.25.2": { - "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==" - }, - "@esbuild/linux-s390x@0.25.2": { - "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==" - }, - "@esbuild/linux-x64@0.25.2": { - "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==" - }, - "@esbuild/netbsd-arm64@0.25.2": { - "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==" - }, - "@esbuild/netbsd-x64@0.25.2": { - "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==" - }, - "@esbuild/openbsd-arm64@0.25.2": { - "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==" - }, - "@esbuild/openbsd-x64@0.25.2": { - "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==" - }, - "@esbuild/sunos-x64@0.25.2": { - "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==" - }, - "@esbuild/win32-arm64@0.25.2": { - "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==" - }, - "@esbuild/win32-ia32@0.25.2": { - "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==" - }, - "@esbuild/win32-x64@0.25.2": { - "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==" - }, - "esbuild@0.25.2": { - "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", - "dependencies": [ - "@esbuild/aix-ppc64", - "@esbuild/android-arm", - "@esbuild/android-arm64", - "@esbuild/android-x64", - "@esbuild/darwin-arm64", - "@esbuild/darwin-x64", - "@esbuild/freebsd-arm64", - "@esbuild/freebsd-x64", - "@esbuild/linux-arm", - "@esbuild/linux-arm64", - "@esbuild/linux-ia32", - "@esbuild/linux-loong64", - "@esbuild/linux-mips64el", - "@esbuild/linux-ppc64", - "@esbuild/linux-riscv64", - "@esbuild/linux-s390x", - "@esbuild/linux-x64", - "@esbuild/netbsd-arm64", - "@esbuild/netbsd-x64", - "@esbuild/openbsd-arm64", - "@esbuild/openbsd-x64", - "@esbuild/sunos-x64", - "@esbuild/win32-arm64", - "@esbuild/win32-ia32", - "@esbuild/win32-x64" - ] - } - } -} diff --git a/dimos/web/websocket_vis/helpers.py b/dimos/web/websocket_vis/helpers.py deleted file mode 100644 index af77380c1a..0000000000 --- a/dimos/web/websocket_vis/helpers.py +++ /dev/null @@ -1,45 +0,0 @@ -import threading -import time -from dataclasses import dataclass, field -from abc import ABC -from typing import Tuple, Callable, Optional -from dimos.types.path import Path -from dimos.types.vector import Vector - -import reactivex as rx -from reactivex import operators as ops -from reactivex.observable import Observable -from reactivex.subject import Subject -from dimos.web.websocket_vis.types import Drawable - - -class Visualizable(ABC): - """ - Base class for objects that can provide visualization data. - """ - - def vis_stream(self) -> Observable[Tuple[str, Drawable]]: - if not hasattr(self, "_vis_subject"): - self._vis_subject = Subject() - return self._vis_subject - - def vis(self, name: str, drawable: Drawable) -> None: - if not hasattr(self, "_vis_subject"): - return - self._vis_subject.on_next((name, drawable)) - - -def vector_stream( - name: str, pos: Callable[[], Vector], update_interval=0.1, precision=0.25, history=10 -) -> Observable[Tuple[str, Drawable]]: - return rx.interval(update_interval).pipe( - ops.map(lambda _: pos()), - ops.distinct_until_changed( - comparer=lambda a, b: (a - b).length() < precision, - ), - ops.scan( - lambda hist, cur: hist.ipush(cur).iclip_tail(history), - seed=Path(), - ), - ops.flat_map(lambda path: rx.from_([(f"{name}_hst", path), (name, path.last())])), - ) diff --git a/dimos/web/websocket_vis/main.ts b/dimos/web/websocket_vis/main.ts deleted file mode 100644 index 292ce5f6dc..0000000000 --- a/dimos/web/websocket_vis/main.ts +++ /dev/null @@ -1,8 +0,0 @@ -export function add(a: number, b: number): number { - return a + b; -} - -// Learn more at https://docs.deno.com/runtime/manual/examples/module_metadata#concepts -if (import.meta.main) { - console.log("Add 2 + 3 =", add(2, 3)); -} diff --git a/dimos/web/websocket_vis/main_test.ts b/dimos/web/websocket_vis/main_test.ts deleted file mode 100644 index 3d981e9bed..0000000000 --- a/dimos/web/websocket_vis/main_test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { assertEquals } from "@std/assert"; -import { add } from "./main.ts"; - -Deno.test(function addTest() { - assertEquals(add(2, 3), 5); -}); diff --git a/dimos/web/websocket_vis/optimized_costmap.py b/dimos/web/websocket_vis/optimized_costmap.py new file mode 100644 index 0000000000..03307ff2c0 --- /dev/null +++ b/dimos/web/websocket_vis/optimized_costmap.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 + +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright 2025 Dimensional Inc. + +import base64 +import hashlib +import time +from typing import Any +import zlib + +import numpy as np + + +class OptimizedCostmapEncoder: + """Handles optimized encoding of costmaps with delta compression.""" + + def __init__(self, chunk_size: int = 64) -> None: + self.chunk_size = chunk_size + self.last_full_grid: np.ndarray | None = None + self.last_full_sent_time: float = 0 # Track when last full update was sent + self.chunk_hashes: dict[tuple[int, int], str] = {} + self.full_update_interval = 3.0 # Send full update every 3 seconds + + def encode_costmap(self, grid: np.ndarray, force_full: bool = False) -> dict[str, Any]: + """Encode a costmap grid with optimizations. + + Args: + grid: The costmap grid as numpy array + force_full: Force sending a full update + + Returns: + Encoded costmap data + """ + current_time = time.time() + + # Determine if we need a full update + send_full = ( + force_full + or self.last_full_grid is None + or self.last_full_grid.shape != grid.shape + or (current_time - self.last_full_sent_time) > self.full_update_interval + ) + + if send_full: + return self._encode_full(grid, current_time) + else: + return self._encode_delta(grid, current_time) + + def _encode_full(self, grid: np.ndarray, current_time: float) -> dict[str, Any]: + height, width = grid.shape + + # Convert to uint8 for better compression (costmap values are -1 to 100) + # Map -1 to 255 for unknown cells + grid_uint8 = grid.astype(np.int16) + grid_uint8[grid_uint8 == -1] = 255 + grid_uint8 = grid_uint8.astype(np.uint8) + + # Compress the data + compressed = zlib.compress(grid_uint8.tobytes(), level=6) + + # Base64 encode + encoded = base64.b64encode(compressed).decode("ascii") + + # Update state + self.last_full_grid = grid.copy() + self.last_full_sent_time = current_time + self._update_chunk_hashes(grid) + + return { + "update_type": "full", + "shape": [height, width], + "dtype": "u8", # uint8 + "compressed": True, + "compression": "zlib", + "data": encoded, + } + + def _encode_delta(self, grid: np.ndarray, current_time: float) -> dict[str, Any]: + height, width = grid.shape + changed_chunks = [] + + # Divide grid into chunks and check for changes + for y in range(0, height, self.chunk_size): + for x in range(0, width, self.chunk_size): + # Get chunk bounds + y_end = min(y + self.chunk_size, height) + x_end = min(x + self.chunk_size, width) + + # Extract chunk + chunk = grid[y:y_end, x:x_end] + + # Compute hash of chunk + chunk_hash = hashlib.md5(chunk.tobytes()).hexdigest() + chunk_key = (y, x) + + # Check if chunk has changed + if chunk_key not in self.chunk_hashes or self.chunk_hashes[chunk_key] != chunk_hash: + # Chunk has changed, encode it + chunk_uint8 = chunk.astype(np.int16) + chunk_uint8[chunk_uint8 == -1] = 255 + chunk_uint8 = chunk_uint8.astype(np.uint8) + + # Compress chunk + compressed = zlib.compress(chunk_uint8.tobytes(), level=6) + encoded = base64.b64encode(compressed).decode("ascii") + + changed_chunks.append( + {"pos": [y, x], "size": [y_end - y, x_end - x], "data": encoded} + ) + + # Update hash + self.chunk_hashes[chunk_key] = chunk_hash + + # Update state - only update the grid, not the timer + self.last_full_grid = grid.copy() + + # If too many chunks changed, send full update instead + total_chunks = ((height + self.chunk_size - 1) // self.chunk_size) * ( + (width + self.chunk_size - 1) // self.chunk_size + ) + + if len(changed_chunks) > total_chunks * 0.5: + # More than 50% changed, send full update + return self._encode_full(grid, current_time) + + return { + "update_type": "delta", + "shape": [height, width], + "dtype": "u8", + "compressed": True, + "compression": "zlib", + "chunks": changed_chunks, + } + + def _update_chunk_hashes(self, grid: np.ndarray) -> None: + """Update all chunk hashes for the grid.""" + self.chunk_hashes.clear() + height, width = grid.shape + + for y in range(0, height, self.chunk_size): + for x in range(0, width, self.chunk_size): + y_end = min(y + self.chunk_size, height) + x_end = min(x + self.chunk_size, width) + chunk = grid[y:y_end, x:x_end] + chunk_hash = hashlib.md5(chunk.tobytes()).hexdigest() + self.chunk_hashes[(y, x)] = chunk_hash diff --git a/dimos/web/websocket_vis/path_history.py b/dimos/web/websocket_vis/path_history.py new file mode 100644 index 0000000000..f60031bc51 --- /dev/null +++ b/dimos/web/websocket_vis/path_history.py @@ -0,0 +1,75 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Simple path history class for visualization purposes. +This is a minimal implementation to support websocket visualization. +""" + +from dimos.msgs.geometry_msgs import Vector3 + + +class PathHistory: + """A simple container for storing a history of positions for visualization.""" + + def __init__(self, points: list[Vector3 | tuple | list] | None = None) -> None: + """Initialize with optional list of points.""" + self.points: list[Vector3] = [] + if points: + for p in points: + if isinstance(p, Vector3): + self.points.append(p) + else: + self.points.append(Vector3(*p)) + + def ipush(self, point: Vector3 | tuple | list) -> "PathHistory": + """Add a point to the history (in-place) and return self.""" + if isinstance(point, Vector3): + self.points.append(point) + else: + self.points.append(Vector3(*point)) + return self + + def iclip_tail(self, max_length: int) -> "PathHistory": + """Keep only the last max_length points (in-place) and return self.""" + if max_length > 0 and len(self.points) > max_length: + self.points = self.points[-max_length:] + return self + + def last(self) -> Vector3 | None: + """Return the last point in the history, or None if empty.""" + return self.points[-1] if self.points else None + + def length(self) -> float: + """Calculate the total length of the path.""" + if len(self.points) < 2: + return 0.0 + + total = 0.0 + for i in range(1, len(self.points)): + p1 = self.points[i - 1] + p2 = self.points[i] + dx = p2.x - p1.x + dy = p2.y - p1.y + dz = p2.z - p1.z + total += (dx * dx + dy * dy + dz * dz) ** 0.5 + return total + + def __len__(self) -> int: + """Return the number of points in the history.""" + return len(self.points) + + def __getitem__(self, index: int) -> Vector3: + """Get a point by index.""" + return self.points[index] diff --git a/dimos/web/websocket_vis/server.py b/dimos/web/websocket_vis/server.py deleted file mode 100644 index ea9fbb00d5..0000000000 --- a/dimos/web/websocket_vis/server.py +++ /dev/null @@ -1,208 +0,0 @@ -import socketio -import uvicorn -import threading -import os -import sys -import asyncio -from typing import Tuple -from starlette.routing import Route -from starlette.responses import HTMLResponse -from starlette.applications import Starlette -from starlette.staticfiles import StaticFiles -from dimos.web.websocket_vis.types import Drawable -from reactivex import Observable - - -async def serve_index(request): - # Read the index.html file directly - index_path = os.path.join(os.path.dirname(__file__), "static", "index.html") - with open(index_path, "r") as f: - content = f.read() - return HTMLResponse(content) - - -# Create global socketio server -sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*") - -# Create Starlette app with route for root path -routes = [Route("/", serve_index)] -starlette_app = Starlette(routes=routes) - - -static_dir = os.path.join(os.path.dirname(__file__), "static") -starlette_app.mount("/", StaticFiles(directory=static_dir), name="static") - -# Create the ASGI app -app = socketio.ASGIApp(sio, starlette_app) - -main_state = { - "status": "idle", - "connected_clients": 0, -} - - -@sio.event -async def connect(sid, environ): - print(f"Client connected: {sid}") - await update_state({"connected_clients": main_state["connected_clients"] + 1}) - await sio.emit("full_state", main_state, room=sid) - - -@sio.event -async def disconnect(sid): - print(f"Client disconnected: {sid}") - await update_state({"connected_clients": main_state["connected_clients"] - 1}) - - -@sio.event -async def message(sid, data): - # print(f"Message received from {sid}: {data}") - # Call WebsocketVis.handle_message if there's an active instance - if hasattr(sio, "vis_instance") and sio.vis_instance: - msgtype = data.get("type", "unknown") - sio.vis_instance.handle_message(msgtype, data) - # await sio.emit("message", {"response": "Server received your message"}, room=sid) - - -# Deep merge function for nested dictionaries -def deep_merge(source, destination): - """ - Deep merge two dictionaries recursively. - Updates destination in-place with values from source. - Lists are replaced, not merged. - """ - for key, value in source.items(): - if key in destination and isinstance(destination[key], dict) and isinstance(value, dict): - # If both values are dictionaries, recursively deep merge them - deep_merge(value, destination[key]) - else: - # Otherwise, just update the value - destination[key] = value - return destination - - -# Utility function to update state and broadcast to all clients -async def update_state(new_data): - """Update main_state and broadcast only the new data to all connected clients""" - # Deep merge the new data into main_state - deep_merge(new_data, main_state) - # Broadcast only the new data to all connected clients - await sio.emit("state_update", new_data) - - -class WebsocketVis: - def __init__(self, port=7778, use_reload=False, msg_handler=None): - self.port = port - self.server = None - self.server_thread = None - self.sio = sio # Use the global sio instance - self.use_reload = use_reload - self.main_state = main_state # Reference to global main_state - self.msg_handler = msg_handler - - # Store reference to this instance on the sio object for message handling - sio.vis_instance = self - - def handle_message(self, msgtype, msg): - """Handle incoming messages from the client""" - if self.msg_handler: - self.msg_handler(msgtype, msg) - else: - print("No message handler defined. Ignoring message.") - - def start(self): - # If reload is requested, run in main thread - if self.use_reload: - print("Starting server with hot reload in main thread") - uvicorn.run( - "server:app", # Use import string for reload to work - host="0.0.0.0", - port=self.port, - reload=True, - reload_dirs=[os.path.dirname(__file__)], - ) - return self - - # Otherwise, run in background thread - else: - print("Starting server in background thread") - self.server_thread = threading.Thread( - target=uvicorn.run, - kwargs={ - "app": app, # Use direct app object for thread mode - "host": "0.0.0.0", - "port": self.port, - }, - daemon=True, - ) - self.server_thread.start() - return self - - def process_drawable(self, drawable: Drawable): - """Process a drawable object and return a dictionary representation""" - if isinstance(drawable, tuple): - obj, config = drawable - return [obj.serialize(), config] - else: - return drawable.serialize() - - def connect(self, obs: Observable[Tuple[str, Drawable]], window_name: str = "main"): - """Connect to an Observable stream and update state on new data""" - - def new_update(data): - [name, drawable] = data - self.update_state({"draw": {name: self.process_drawable(drawable)}}) - - obs.subscribe( - on_next=new_update, - on_error=lambda e: print(f"Error in stream: {e}"), - on_completed=lambda: print("Stream completed"), - ) - - def stop(self): - if self.server_thread and self.server_thread.is_alive(): - self.server_thread.join() - self.sio.disconnect() - - async def update_state_async(self, new_data): - """Update main_state and broadcast to all connected clients""" - await update_state(new_data) - - def update_state(self, new_data): - """Synchronous wrapper for update_state""" - - # Get or create an event loop - try: - loop = asyncio.get_event_loop() - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - # Run the coroutine in the loop - if loop.is_running(): - # Create a future and run it in the existing loop - future = asyncio.run_coroutine_threadsafe(update_state(new_data), loop) - return future.result() - else: - # Run the coroutine in a new loop - return loop.run_until_complete(update_state(new_data)) - - -# Test timer function that updates state with current Unix time -async def start_time_counter(server): - """Start a background task that updates state with current Unix time every second""" - import time - - while True: - # Update state with current Unix timestamp - await server.update_state_async({"time": int(time.time())}) - # Wait for 1 second - await asyncio.sleep(1) - - -# For direct execution with uvicorn CLI -if __name__ == "__main__": - # Check if --reload flag is passed - use_reload = "--reload" in sys.argv - server = WebsocketVis(port=7778, use_reload=use_reload) - server_instance = server.start() diff --git a/dimos/web/websocket_vis/static/index.css b/dimos/web/websocket_vis/static/index.css deleted file mode 100644 index fb9b901f06..0000000000 --- a/dimos/web/websocket_vis/static/index.css +++ /dev/null @@ -1,301 +0,0 @@ -:root, -svg { - --color-red: #fd5548; - --color-green: #73e3bb; - --color-blue: #555; - --color-lightblue: #adf7f6; - --color-orange: #ffa500; - --color-bgblue: #141d22; - - --text-color: #fff; - --text-color-alt: #aaa; - --text-color-p: #ccc; - --background-color: #000; - --background-color-alt: #111; - - --font-family: "JetBrains Mono", monospace; - --line-height: 1.2rem; - --border-thickness: 1px; - - --font-weight-normal: 500; - --font-weight-medium: 600; - --font-weight-bold: 800; - - fill: var(--text-color); - font-family: var(--font-family); - font-optical-sizing: auto; - font-weight: var(--font-weight-normal); - font-style: normal; - font-variant-numeric: tabular-nums lining-nums; - font-size: 16px; - width: 100%; -} - -body { - position: relative; - width: 100%; - margin: 0; - padding: 0; - max-width: calc(min(100ch, round(down, 100%, 1ch))); - line-height: var(--line-height); - background-color: black; -} - -canvas { - background-color: rgba(0, 0, 0, 1); - width: 100%; - height: auto; -} - -.left-section { - flex: 1; /* Takes up 50% of the container width */ -} - -.right-section { - flex: 1; /* Takes up 50% of the container width */ -} - -.skelesvg { - background-color: #000000; - width: 100%; - height: auto; - border: 1px solid white; -} - -video { - width: 100%; - height: auto; -} - -#plotly_container { - filter: invert(100%) hue-rotate(180deg); -} - -button { - font-family: var(--font-family); - white-space: nowrap; - border: none; - padding: 5px; - cursor: pointer; - margin: 0; - background-color: black; - color: white; - height: 2em; - border: 1px solid white; -} - -button:hover { - background-color: white; - color: black; -} - -button.selected { - background-color: white; - color: black; -} - -button.checkbox-button { - position: relative; - padding-left: 20px; - text-align: left; - margin: 2px; - width: auto; - min-width: 80px; -} - -button.checkbox-button::before { - content: ""; - position: absolute; - left: 5px; - top: 50%; - transform: translateY(-50%); - width: 10px; - height: 10px; - border: 1px solid white; - background-color: black; -} - -button.checkbox-button.checked::before { - background-color: white; -} - -button.checkbox-button:hover::before { - border-color: var(--color-green); -} - -/* Adjustments for grid layout */ -.controls.grid button.checkbox-button { - flex: 0 0 auto; -} - -.controls { - position: absolute; - bottom: 5px; - left: 5px; - width: calc(100% - 10px); - display: flex; - flex-wrap: wrap; - gap: 5px; - padding: 5px; - max-height: calc(100% - 30px); - overflow-y: auto; -} - -/* For grid layout (multiple items per row) */ -.controls.grid { - flex-direction: row; - justify-content: flex-start; - align-items: flex-start; -} - -/* For horizontal controls layout (default) */ -.controls.horizontal { - flex-direction: row; - align-items: center; - justify-content: flex-start; -} - -/* For vertical controls layout */ -.controls.vertical { - flex-direction: column; - align-items: flex-start; -} - -input[type="range"] { - -webkit-appearance: none; - width: 100%; - background: black; - outline: 1px solid white; - padding-left: 5px; - padding-right: 5px; - margin-right: 12px; -} - -input[type="range"]::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 1.5em; - height: 1em; - background: black; - cursor: pointer; - border: 1px solid white; -} - -input[type="range"]::-moz-range-thumb { - width: 1.5em; - height: 1em; - background: black; - cursor: pointer; - border: 1px solid white; -} - -input[type="range"]::-webkit-slider-thumb:hover { - background: white; -} - -input[type="range"]::-moz-range-thumb:hover { - background: white; -} - -#window-container { - display: flex; - flex-wrap: wrap; - width: 100vw; -} - -.window { - position: relative; - border: 1px solid #ccc; - box-sizing: border-box; - min-width: 30vw; - flex: 1 1 30vh; - min-height: 33vh; - display: flex; - flex-direction: column; -} - -#vis { - min-height: 100vh; - min-width: 100vw; -} - -.window-title { - position: absolute; - top: 5px; - right: 5px; - background-color: black; - color: white; - padding: 5px; - border: 1px solid white; - z-index: 100; -} - -.window:has(.window) > .window-title { - top: 5px; - left: 5px; - width: fit-content; -} - -.window-content { - flex: 1; - overflow: hidden; - position: relative; - display: flex; - flex-wrap: wrap; -} - -svg { - width: 100%; - height: 100%; - position: absolute; - top: 0; - left: 0; -} - -/* Graph styling */ -.keypoint-path { - fill: none; - stroke-width: 1.5px; - vector-effect: non-scaling-stroke; -} - -.annotation-line { - stroke-width: 1px; - vector-effect: non-scaling-stroke; -} - -.annotation-line.vertical { - stroke-dasharray: none; -} - -.annotation-line.horizontal { - stroke-dasharray: none; -} - -.annotation-region { - opacity: 0.3; -} - -.annotation-point { - r: 4px; -} - -.annotation-text { - font-size: 12px; - font-weight: normal; -} - -.window:has(.window) { - border: 0px; -} - -.clickable-keypoint { - cursor: pointer; /* Indicates clickability */ - transition: stroke, r 0.5s; /* Smooth hover transition */ -} - -.clickable-keypoint:hover { - r: 10px; - stroke-width: 1; - stroke: white; -} diff --git a/dimos/web/websocket_vis/static/index.html b/dimos/web/websocket_vis/static/index.html deleted file mode 100644 index 2d5705ab1e..0000000000 --- a/dimos/web/websocket_vis/static/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - dimos websocket vis - - - - - - -
-
vis
-
- - - - diff --git a/dimos/web/websocket_vis/static/js/clientside.js b/dimos/web/websocket_vis/static/js/clientside.js deleted file mode 100644 index 6aaaa7089c..0000000000 --- a/dimos/web/websocket_vis/static/js/clientside.js +++ /dev/null @@ -1,20021 +0,0 @@ -var __create = Object.create; -var __defProp = Object.defineProperty; -var __getOwnPropDesc = Object.getOwnPropertyDescriptor; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __getProtoOf = Object.getPrototypeOf; -var __hasOwnProp = Object.prototype.hasOwnProperty; -var __defNormalProp = (obj, key, value2) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value: value2 }) : obj[key] = value2; -var __commonJS = (cb, mod) => function __require() { - return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; -}; -var __export = (target, all) => { - for (var name in all) - __defProp(target, name, { get: all[name], enumerable: true }); -}; -var __copyProps = (to, from, except, desc) => { - if (from && typeof from === "object" || typeof from === "function") { - for (let key of __getOwnPropNames(from)) - if (!__hasOwnProp.call(to, key) && key !== except) - __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); - } - return to; -}; -var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( - // If the importer is in node compatibility mode or this is not an ESM - // file that has been converted to a CommonJS file using a Babel- - // compatible transform (i.e. "__esModule" has not been set), then set - // "default" to the CommonJS "module.exports" for node compatibility. - isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, - mod -)); -var __publicField = (obj, key, value2) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value2); - -// node_modules/.deno/ms@2.1.3/node_modules/ms/index.js -var require_ms = __commonJS({ - "node_modules/.deno/ms@2.1.3/node_modules/ms/index.js"(exports, module) { - var s = 1e3; - var m = s * 60; - var h = m * 60; - var d = h * 24; - var w = d * 7; - var y2 = d * 365.25; - module.exports = function(val, options) { - options = options || {}; - var type2 = typeof val; - if (type2 === "string" && val.length > 0) { - return parse2(val); - } else if (type2 === "number" && isFinite(val)) { - return options.long ? fmtLong(val) : fmtShort(val); - } - throw new Error( - "val is not a non-empty string or a valid number. val=" + JSON.stringify(val) - ); - }; - function parse2(str) { - str = String(str); - if (str.length > 100) { - return; - } - var match = /^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec( - str - ); - if (!match) { - return; - } - var n = parseFloat(match[1]); - var type2 = (match[2] || "ms").toLowerCase(); - switch (type2) { - case "years": - case "year": - case "yrs": - case "yr": - case "y": - return n * y2; - case "weeks": - case "week": - case "w": - return n * w; - case "days": - case "day": - case "d": - return n * d; - case "hours": - case "hour": - case "hrs": - case "hr": - case "h": - return n * h; - case "minutes": - case "minute": - case "mins": - case "min": - case "m": - return n * m; - case "seconds": - case "second": - case "secs": - case "sec": - case "s": - return n * s; - case "milliseconds": - case "millisecond": - case "msecs": - case "msec": - case "ms": - return n; - default: - return void 0; - } - } - function fmtShort(ms) { - var msAbs = Math.abs(ms); - if (msAbs >= d) { - return Math.round(ms / d) + "d"; - } - if (msAbs >= h) { - return Math.round(ms / h) + "h"; - } - if (msAbs >= m) { - return Math.round(ms / m) + "m"; - } - if (msAbs >= s) { - return Math.round(ms / s) + "s"; - } - return ms + "ms"; - } - function fmtLong(ms) { - var msAbs = Math.abs(ms); - if (msAbs >= d) { - return plural(ms, msAbs, d, "day"); - } - if (msAbs >= h) { - return plural(ms, msAbs, h, "hour"); - } - if (msAbs >= m) { - return plural(ms, msAbs, m, "minute"); - } - if (msAbs >= s) { - return plural(ms, msAbs, s, "second"); - } - return ms + " ms"; - } - function plural(ms, msAbs, n, name) { - var isPlural = msAbs >= n * 1.5; - return Math.round(ms / n) + " " + name + (isPlural ? "s" : ""); - } - } -}); - -// node_modules/.deno/debug@4.3.7/node_modules/debug/src/common.js -var require_common = __commonJS({ - "node_modules/.deno/debug@4.3.7/node_modules/debug/src/common.js"(exports, module) { - function setup(env) { - createDebug.debug = createDebug; - createDebug.default = createDebug; - createDebug.coerce = coerce; - createDebug.disable = disable; - createDebug.enable = enable; - createDebug.enabled = enabled; - createDebug.humanize = require_ms(); - createDebug.destroy = destroy; - Object.keys(env).forEach((key) => { - createDebug[key] = env[key]; - }); - createDebug.names = []; - createDebug.skips = []; - createDebug.formatters = {}; - function selectColor(namespace) { - let hash = 0; - for (let i = 0; i < namespace.length; i++) { - hash = (hash << 5) - hash + namespace.charCodeAt(i); - hash |= 0; - } - return createDebug.colors[Math.abs(hash) % createDebug.colors.length]; - } - createDebug.selectColor = selectColor; - function createDebug(namespace) { - let prevTime; - let enableOverride = null; - let namespacesCache; - let enabledCache; - function debug12(...args) { - if (!debug12.enabled) { - return; - } - const self2 = debug12; - const curr = Number(/* @__PURE__ */ new Date()); - const ms = curr - (prevTime || curr); - self2.diff = ms; - self2.prev = prevTime; - self2.curr = curr; - prevTime = curr; - args[0] = createDebug.coerce(args[0]); - if (typeof args[0] !== "string") { - args.unshift("%O"); - } - let index = 0; - args[0] = args[0].replace(/%([a-zA-Z%])/g, (match, format2) => { - if (match === "%%") { - return "%"; - } - index++; - const formatter = createDebug.formatters[format2]; - if (typeof formatter === "function") { - const val = args[index]; - match = formatter.call(self2, val); - args.splice(index, 1); - index--; - } - return match; - }); - createDebug.formatArgs.call(self2, args); - const logFn = self2.log || createDebug.log; - logFn.apply(self2, args); - } - debug12.namespace = namespace; - debug12.useColors = createDebug.useColors(); - debug12.color = createDebug.selectColor(namespace); - debug12.extend = extend2; - debug12.destroy = createDebug.destroy; - Object.defineProperty(debug12, "enabled", { - enumerable: true, - configurable: false, - get: () => { - if (enableOverride !== null) { - return enableOverride; - } - if (namespacesCache !== createDebug.namespaces) { - namespacesCache = createDebug.namespaces; - enabledCache = createDebug.enabled(namespace); - } - return enabledCache; - }, - set: (v) => { - enableOverride = v; - } - }); - if (typeof createDebug.init === "function") { - createDebug.init(debug12); - } - return debug12; - } - function extend2(namespace, delimiter) { - const newDebug = createDebug(this.namespace + (typeof delimiter === "undefined" ? ":" : delimiter) + namespace); - newDebug.log = this.log; - return newDebug; - } - function enable(namespaces) { - createDebug.save(namespaces); - createDebug.namespaces = namespaces; - createDebug.names = []; - createDebug.skips = []; - let i; - const split = (typeof namespaces === "string" ? namespaces : "").split(/[\s,]+/); - const len = split.length; - for (i = 0; i < len; i++) { - if (!split[i]) { - continue; - } - namespaces = split[i].replace(/\*/g, ".*?"); - if (namespaces[0] === "-") { - createDebug.skips.push(new RegExp("^" + namespaces.slice(1) + "$")); - } else { - createDebug.names.push(new RegExp("^" + namespaces + "$")); - } - } - } - function disable() { - const namespaces = [ - ...createDebug.names.map(toNamespace), - ...createDebug.skips.map(toNamespace).map((namespace) => "-" + namespace) - ].join(","); - createDebug.enable(""); - return namespaces; - } - function enabled(name) { - if (name[name.length - 1] === "*") { - return true; - } - let i; - let len; - for (i = 0, len = createDebug.skips.length; i < len; i++) { - if (createDebug.skips[i].test(name)) { - return false; - } - } - for (i = 0, len = createDebug.names.length; i < len; i++) { - if (createDebug.names[i].test(name)) { - return true; - } - } - return false; - } - function toNamespace(regexp) { - return regexp.toString().substring(2, regexp.toString().length - 2).replace(/\.\*\?$/, "*"); - } - function coerce(val) { - if (val instanceof Error) { - return val.stack || val.message; - } - return val; - } - function destroy() { - console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`."); - } - createDebug.enable(createDebug.load()); - return createDebug; - } - module.exports = setup; - } -}); - -// node_modules/.deno/debug@4.3.7/node_modules/debug/src/browser.js -var require_browser = __commonJS({ - "node_modules/.deno/debug@4.3.7/node_modules/debug/src/browser.js"(exports, module) { - exports.formatArgs = formatArgs; - exports.save = save; - exports.load = load; - exports.useColors = useColors; - exports.storage = localstorage(); - exports.destroy = /* @__PURE__ */ (() => { - let warned = false; - return () => { - if (!warned) { - warned = true; - console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`."); - } - }; - })(); - exports.colors = [ - "#0000CC", - "#0000FF", - "#0033CC", - "#0033FF", - "#0066CC", - "#0066FF", - "#0099CC", - "#0099FF", - "#00CC00", - "#00CC33", - "#00CC66", - "#00CC99", - "#00CCCC", - "#00CCFF", - "#3300CC", - "#3300FF", - "#3333CC", - "#3333FF", - "#3366CC", - "#3366FF", - "#3399CC", - "#3399FF", - "#33CC00", - "#33CC33", - "#33CC66", - "#33CC99", - "#33CCCC", - "#33CCFF", - "#6600CC", - "#6600FF", - "#6633CC", - "#6633FF", - "#66CC00", - "#66CC33", - "#9900CC", - "#9900FF", - "#9933CC", - "#9933FF", - "#99CC00", - "#99CC33", - "#CC0000", - "#CC0033", - "#CC0066", - "#CC0099", - "#CC00CC", - "#CC00FF", - "#CC3300", - "#CC3333", - "#CC3366", - "#CC3399", - "#CC33CC", - "#CC33FF", - "#CC6600", - "#CC6633", - "#CC9900", - "#CC9933", - "#CCCC00", - "#CCCC33", - "#FF0000", - "#FF0033", - "#FF0066", - "#FF0099", - "#FF00CC", - "#FF00FF", - "#FF3300", - "#FF3333", - "#FF3366", - "#FF3399", - "#FF33CC", - "#FF33FF", - "#FF6600", - "#FF6633", - "#FF9900", - "#FF9933", - "#FFCC00", - "#FFCC33" - ]; - function useColors() { - if (typeof window !== "undefined" && window.process && (window.process.type === "renderer" || window.process.__nwjs)) { - return true; - } - if (typeof navigator !== "undefined" && navigator.userAgent && navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/)) { - return false; - } - let m; - return typeof document !== "undefined" && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance || // Is firebug? http://stackoverflow.com/a/398120/376773 - typeof window !== "undefined" && window.console && (window.console.firebug || window.console.exception && window.console.table) || // Is firefox >= v31? - // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages - typeof navigator !== "undefined" && navigator.userAgent && (m = navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)) && parseInt(m[1], 10) >= 31 || // Double check webkit in userAgent just in case we are in a worker - typeof navigator !== "undefined" && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/); - } - function formatArgs(args) { - args[0] = (this.useColors ? "%c" : "") + this.namespace + (this.useColors ? " %c" : " ") + args[0] + (this.useColors ? "%c " : " ") + "+" + module.exports.humanize(this.diff); - if (!this.useColors) { - return; - } - const c = "color: " + this.color; - args.splice(1, 0, c, "color: inherit"); - let index = 0; - let lastC = 0; - args[0].replace(/%[a-zA-Z%]/g, (match) => { - if (match === "%%") { - return; - } - index++; - if (match === "%c") { - lastC = index; - } - }); - args.splice(lastC, 0, c); - } - exports.log = console.debug || console.log || (() => { - }); - function save(namespaces) { - try { - if (namespaces) { - exports.storage.setItem("debug", namespaces); - } else { - exports.storage.removeItem("debug"); - } - } catch (error) { - } - } - function load() { - let r; - try { - r = exports.storage.getItem("debug"); - } catch (error) { - } - if (!r && typeof process !== "undefined" && "env" in process) { - r = process.env.DEBUG; - } - return r; - } - function localstorage() { - try { - return localStorage; - } catch (error) { - } - } - module.exports = require_common()(exports); - var { formatters } = module.exports; - formatters.j = function(v) { - try { - return JSON.stringify(v); - } catch (error) { - return "[UnexpectedJSONParseError]: " + error.message; - } - }; - } -}); - -// node_modules/.deno/react@19.1.0/node_modules/react/cjs/react.production.js -var require_react_production = __commonJS({ - "node_modules/.deno/react@19.1.0/node_modules/react/cjs/react.production.js"(exports) { - "use strict"; - var REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"); - var REACT_PORTAL_TYPE = Symbol.for("react.portal"); - var REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"); - var REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode"); - var REACT_PROFILER_TYPE = Symbol.for("react.profiler"); - var REACT_CONSUMER_TYPE = Symbol.for("react.consumer"); - var REACT_CONTEXT_TYPE = Symbol.for("react.context"); - var REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref"); - var REACT_SUSPENSE_TYPE = Symbol.for("react.suspense"); - var REACT_MEMO_TYPE = Symbol.for("react.memo"); - var REACT_LAZY_TYPE = Symbol.for("react.lazy"); - var MAYBE_ITERATOR_SYMBOL = Symbol.iterator; - function getIteratorFn(maybeIterable) { - if (null === maybeIterable || "object" !== typeof maybeIterable) return null; - maybeIterable = MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL] || maybeIterable["@@iterator"]; - return "function" === typeof maybeIterable ? maybeIterable : null; - } - var ReactNoopUpdateQueue = { - isMounted: function() { - return false; - }, - enqueueForceUpdate: function() { - }, - enqueueReplaceState: function() { - }, - enqueueSetState: function() { - } - }; - var assign = Object.assign; - var emptyObject = {}; - function Component(props, context, updater) { - this.props = props; - this.context = context; - this.refs = emptyObject; - this.updater = updater || ReactNoopUpdateQueue; - } - Component.prototype.isReactComponent = {}; - Component.prototype.setState = function(partialState, callback) { - if ("object" !== typeof partialState && "function" !== typeof partialState && null != partialState) - throw Error( - "takes an object of state variables to update or a function which returns an object of state variables." - ); - this.updater.enqueueSetState(this, partialState, callback, "setState"); - }; - Component.prototype.forceUpdate = function(callback) { - this.updater.enqueueForceUpdate(this, callback, "forceUpdate"); - }; - function ComponentDummy() { - } - ComponentDummy.prototype = Component.prototype; - function PureComponent(props, context, updater) { - this.props = props; - this.context = context; - this.refs = emptyObject; - this.updater = updater || ReactNoopUpdateQueue; - } - var pureComponentPrototype = PureComponent.prototype = new ComponentDummy(); - pureComponentPrototype.constructor = PureComponent; - assign(pureComponentPrototype, Component.prototype); - pureComponentPrototype.isPureReactComponent = true; - var isArrayImpl = Array.isArray; - var ReactSharedInternals = { H: null, A: null, T: null, S: null, V: null }; - var hasOwnProperty = Object.prototype.hasOwnProperty; - function ReactElement(type2, key, self2, source, owner, props) { - self2 = props.ref; - return { - $$typeof: REACT_ELEMENT_TYPE, - type: type2, - key, - ref: void 0 !== self2 ? self2 : null, - props - }; - } - function cloneAndReplaceKey(oldElement, newKey) { - return ReactElement( - oldElement.type, - newKey, - void 0, - void 0, - void 0, - oldElement.props - ); - } - function isValidElement(object) { - return "object" === typeof object && null !== object && object.$$typeof === REACT_ELEMENT_TYPE; - } - function escape(key) { - var escaperLookup = { "=": "=0", ":": "=2" }; - return "$" + key.replace(/[=:]/g, function(match) { - return escaperLookup[match]; - }); - } - var userProvidedKeyEscapeRegex = /\/+/g; - function getElementKey(element, index) { - return "object" === typeof element && null !== element && null != element.key ? escape("" + element.key) : index.toString(36); - } - function noop$1() { - } - function resolveThenable(thenable) { - switch (thenable.status) { - case "fulfilled": - return thenable.value; - case "rejected": - throw thenable.reason; - default: - switch ("string" === typeof thenable.status ? thenable.then(noop$1, noop$1) : (thenable.status = "pending", thenable.then( - function(fulfilledValue) { - "pending" === thenable.status && (thenable.status = "fulfilled", thenable.value = fulfilledValue); - }, - function(error) { - "pending" === thenable.status && (thenable.status = "rejected", thenable.reason = error); - } - )), thenable.status) { - case "fulfilled": - return thenable.value; - case "rejected": - throw thenable.reason; - } - } - throw thenable; - } - function mapIntoArray(children2, array2, escapedPrefix, nameSoFar, callback) { - var type2 = typeof children2; - if ("undefined" === type2 || "boolean" === type2) children2 = null; - var invokeCallback = false; - if (null === children2) invokeCallback = true; - else - switch (type2) { - case "bigint": - case "string": - case "number": - invokeCallback = true; - break; - case "object": - switch (children2.$$typeof) { - case REACT_ELEMENT_TYPE: - case REACT_PORTAL_TYPE: - invokeCallback = true; - break; - case REACT_LAZY_TYPE: - return invokeCallback = children2._init, mapIntoArray( - invokeCallback(children2._payload), - array2, - escapedPrefix, - nameSoFar, - callback - ); - } - } - if (invokeCallback) - return callback = callback(children2), invokeCallback = "" === nameSoFar ? "." + getElementKey(children2, 0) : nameSoFar, isArrayImpl(callback) ? (escapedPrefix = "", null != invokeCallback && (escapedPrefix = invokeCallback.replace(userProvidedKeyEscapeRegex, "$&/") + "/"), mapIntoArray(callback, array2, escapedPrefix, "", function(c) { - return c; - })) : null != callback && (isValidElement(callback) && (callback = cloneAndReplaceKey( - callback, - escapedPrefix + (null == callback.key || children2 && children2.key === callback.key ? "" : ("" + callback.key).replace( - userProvidedKeyEscapeRegex, - "$&/" - ) + "/") + invokeCallback - )), array2.push(callback)), 1; - invokeCallback = 0; - var nextNamePrefix = "" === nameSoFar ? "." : nameSoFar + ":"; - if (isArrayImpl(children2)) - for (var i = 0; i < children2.length; i++) - nameSoFar = children2[i], type2 = nextNamePrefix + getElementKey(nameSoFar, i), invokeCallback += mapIntoArray( - nameSoFar, - array2, - escapedPrefix, - type2, - callback - ); - else if (i = getIteratorFn(children2), "function" === typeof i) - for (children2 = i.call(children2), i = 0; !(nameSoFar = children2.next()).done; ) - nameSoFar = nameSoFar.value, type2 = nextNamePrefix + getElementKey(nameSoFar, i++), invokeCallback += mapIntoArray( - nameSoFar, - array2, - escapedPrefix, - type2, - callback - ); - else if ("object" === type2) { - if ("function" === typeof children2.then) - return mapIntoArray( - resolveThenable(children2), - array2, - escapedPrefix, - nameSoFar, - callback - ); - array2 = String(children2); - throw Error( - "Objects are not valid as a React child (found: " + ("[object Object]" === array2 ? "object with keys {" + Object.keys(children2).join(", ") + "}" : array2) + "). If you meant to render a collection of children, use an array instead." - ); - } - return invokeCallback; - } - function mapChildren(children2, func, context) { - if (null == children2) return children2; - var result = [], count = 0; - mapIntoArray(children2, result, "", "", function(child) { - return func.call(context, child, count++); - }); - return result; - } - function lazyInitializer(payload) { - if (-1 === payload._status) { - var ctor = payload._result; - ctor = ctor(); - ctor.then( - function(moduleObject) { - if (0 === payload._status || -1 === payload._status) - payload._status = 1, payload._result = moduleObject; - }, - function(error) { - if (0 === payload._status || -1 === payload._status) - payload._status = 2, payload._result = error; - } - ); - -1 === payload._status && (payload._status = 0, payload._result = ctor); - } - if (1 === payload._status) return payload._result.default; - throw payload._result; - } - var reportGlobalError = "function" === typeof reportError ? reportError : function(error) { - if ("object" === typeof window && "function" === typeof window.ErrorEvent) { - var event = new window.ErrorEvent("error", { - bubbles: true, - cancelable: true, - message: "object" === typeof error && null !== error && "string" === typeof error.message ? String(error.message) : String(error), - error - }); - if (!window.dispatchEvent(event)) return; - } else if ("object" === typeof process && "function" === typeof process.emit) { - process.emit("uncaughtException", error); - return; - } - console.error(error); - }; - function noop2() { - } - exports.Children = { - map: mapChildren, - forEach: function(children2, forEachFunc, forEachContext) { - mapChildren( - children2, - function() { - forEachFunc.apply(this, arguments); - }, - forEachContext - ); - }, - count: function(children2) { - var n = 0; - mapChildren(children2, function() { - n++; - }); - return n; - }, - toArray: function(children2) { - return mapChildren(children2, function(child) { - return child; - }) || []; - }, - only: function(children2) { - if (!isValidElement(children2)) - throw Error( - "React.Children.only expected to receive a single React element child." - ); - return children2; - } - }; - exports.Component = Component; - exports.Fragment = REACT_FRAGMENT_TYPE; - exports.Profiler = REACT_PROFILER_TYPE; - exports.PureComponent = PureComponent; - exports.StrictMode = REACT_STRICT_MODE_TYPE; - exports.Suspense = REACT_SUSPENSE_TYPE; - exports.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = ReactSharedInternals; - exports.__COMPILER_RUNTIME = { - __proto__: null, - c: function(size) { - return ReactSharedInternals.H.useMemoCache(size); - } - }; - exports.cache = function(fn) { - return function() { - return fn.apply(null, arguments); - }; - }; - exports.cloneElement = function(element, config, children2) { - if (null === element || void 0 === element) - throw Error( - "The argument must be a React element, but you passed " + element + "." - ); - var props = assign({}, element.props), key = element.key, owner = void 0; - if (null != config) - for (propName in void 0 !== config.ref && (owner = void 0), void 0 !== config.key && (key = "" + config.key), config) - !hasOwnProperty.call(config, propName) || "key" === propName || "__self" === propName || "__source" === propName || "ref" === propName && void 0 === config.ref || (props[propName] = config[propName]); - var propName = arguments.length - 2; - if (1 === propName) props.children = children2; - else if (1 < propName) { - for (var childArray = Array(propName), i = 0; i < propName; i++) - childArray[i] = arguments[i + 2]; - props.children = childArray; - } - return ReactElement(element.type, key, void 0, void 0, owner, props); - }; - exports.createContext = function(defaultValue) { - defaultValue = { - $$typeof: REACT_CONTEXT_TYPE, - _currentValue: defaultValue, - _currentValue2: defaultValue, - _threadCount: 0, - Provider: null, - Consumer: null - }; - defaultValue.Provider = defaultValue; - defaultValue.Consumer = { - $$typeof: REACT_CONSUMER_TYPE, - _context: defaultValue - }; - return defaultValue; - }; - exports.createElement = function(type2, config, children2) { - var propName, props = {}, key = null; - if (null != config) - for (propName in void 0 !== config.key && (key = "" + config.key), config) - hasOwnProperty.call(config, propName) && "key" !== propName && "__self" !== propName && "__source" !== propName && (props[propName] = config[propName]); - var childrenLength = arguments.length - 2; - if (1 === childrenLength) props.children = children2; - else if (1 < childrenLength) { - for (var childArray = Array(childrenLength), i = 0; i < childrenLength; i++) - childArray[i] = arguments[i + 2]; - props.children = childArray; - } - if (type2 && type2.defaultProps) - for (propName in childrenLength = type2.defaultProps, childrenLength) - void 0 === props[propName] && (props[propName] = childrenLength[propName]); - return ReactElement(type2, key, void 0, void 0, null, props); - }; - exports.createRef = function() { - return { current: null }; - }; - exports.forwardRef = function(render) { - return { $$typeof: REACT_FORWARD_REF_TYPE, render }; - }; - exports.isValidElement = isValidElement; - exports.lazy = function(ctor) { - return { - $$typeof: REACT_LAZY_TYPE, - _payload: { _status: -1, _result: ctor }, - _init: lazyInitializer - }; - }; - exports.memo = function(type2, compare) { - return { - $$typeof: REACT_MEMO_TYPE, - type: type2, - compare: void 0 === compare ? null : compare - }; - }; - exports.startTransition = function(scope) { - var prevTransition = ReactSharedInternals.T, currentTransition = {}; - ReactSharedInternals.T = currentTransition; - try { - var returnValue = scope(), onStartTransitionFinish = ReactSharedInternals.S; - null !== onStartTransitionFinish && onStartTransitionFinish(currentTransition, returnValue); - "object" === typeof returnValue && null !== returnValue && "function" === typeof returnValue.then && returnValue.then(noop2, reportGlobalError); - } catch (error) { - reportGlobalError(error); - } finally { - ReactSharedInternals.T = prevTransition; - } - }; - exports.unstable_useCacheRefresh = function() { - return ReactSharedInternals.H.useCacheRefresh(); - }; - exports.use = function(usable) { - return ReactSharedInternals.H.use(usable); - }; - exports.useActionState = function(action, initialState, permalink) { - return ReactSharedInternals.H.useActionState(action, initialState, permalink); - }; - exports.useCallback = function(callback, deps) { - return ReactSharedInternals.H.useCallback(callback, deps); - }; - exports.useContext = function(Context) { - return ReactSharedInternals.H.useContext(Context); - }; - exports.useDebugValue = function() { - }; - exports.useDeferredValue = function(value2, initialValue) { - return ReactSharedInternals.H.useDeferredValue(value2, initialValue); - }; - exports.useEffect = function(create2, createDeps, update) { - var dispatcher = ReactSharedInternals.H; - if ("function" === typeof update) - throw Error( - "useEffect CRUD overload is not enabled in this build of React." - ); - return dispatcher.useEffect(create2, createDeps); - }; - exports.useId = function() { - return ReactSharedInternals.H.useId(); - }; - exports.useImperativeHandle = function(ref, create2, deps) { - return ReactSharedInternals.H.useImperativeHandle(ref, create2, deps); - }; - exports.useInsertionEffect = function(create2, deps) { - return ReactSharedInternals.H.useInsertionEffect(create2, deps); - }; - exports.useLayoutEffect = function(create2, deps) { - return ReactSharedInternals.H.useLayoutEffect(create2, deps); - }; - exports.useMemo = function(create2, deps) { - return ReactSharedInternals.H.useMemo(create2, deps); - }; - exports.useOptimistic = function(passthrough, reducer) { - return ReactSharedInternals.H.useOptimistic(passthrough, reducer); - }; - exports.useReducer = function(reducer, initialArg, init2) { - return ReactSharedInternals.H.useReducer(reducer, initialArg, init2); - }; - exports.useRef = function(initialValue) { - return ReactSharedInternals.H.useRef(initialValue); - }; - exports.useState = function(initialState) { - return ReactSharedInternals.H.useState(initialState); - }; - exports.useSyncExternalStore = function(subscribe, getSnapshot, getServerSnapshot) { - return ReactSharedInternals.H.useSyncExternalStore( - subscribe, - getSnapshot, - getServerSnapshot - ); - }; - exports.useTransition = function() { - return ReactSharedInternals.H.useTransition(); - }; - exports.version = "19.1.0"; - } -}); - -// node_modules/.deno/react@19.1.0/node_modules/react/index.js -var require_react = __commonJS({ - "node_modules/.deno/react@19.1.0/node_modules/react/index.js"(exports, module) { - "use strict"; - if (true) { - module.exports = require_react_production(); - } else { - module.exports = null; - } - } -}); - -// node_modules/.deno/scheduler@0.26.0/node_modules/scheduler/cjs/scheduler.production.js -var require_scheduler_production = __commonJS({ - "node_modules/.deno/scheduler@0.26.0/node_modules/scheduler/cjs/scheduler.production.js"(exports) { - "use strict"; - function push(heap, node) { - var index = heap.length; - heap.push(node); - a: for (; 0 < index; ) { - var parentIndex = index - 1 >>> 1, parent = heap[parentIndex]; - if (0 < compare(parent, node)) - heap[parentIndex] = node, heap[index] = parent, index = parentIndex; - else break a; - } - } - function peek(heap) { - return 0 === heap.length ? null : heap[0]; - } - function pop(heap) { - if (0 === heap.length) return null; - var first = heap[0], last = heap.pop(); - if (last !== first) { - heap[0] = last; - a: for (var index = 0, length = heap.length, halfLength = length >>> 1; index < halfLength; ) { - var leftIndex = 2 * (index + 1) - 1, left2 = heap[leftIndex], rightIndex = leftIndex + 1, right2 = heap[rightIndex]; - if (0 > compare(left2, last)) - rightIndex < length && 0 > compare(right2, left2) ? (heap[index] = right2, heap[rightIndex] = last, index = rightIndex) : (heap[index] = left2, heap[leftIndex] = last, index = leftIndex); - else if (rightIndex < length && 0 > compare(right2, last)) - heap[index] = right2, heap[rightIndex] = last, index = rightIndex; - else break a; - } - } - return first; - } - function compare(a, b) { - var diff = a.sortIndex - b.sortIndex; - return 0 !== diff ? diff : a.id - b.id; - } - exports.unstable_now = void 0; - if ("object" === typeof performance && "function" === typeof performance.now) { - localPerformance = performance; - exports.unstable_now = function() { - return localPerformance.now(); - }; - } else { - localDate = Date, initialTime = localDate.now(); - exports.unstable_now = function() { - return localDate.now() - initialTime; - }; - } - var localPerformance; - var localDate; - var initialTime; - var taskQueue = []; - var timerQueue = []; - var taskIdCounter = 1; - var currentTask = null; - var currentPriorityLevel = 3; - var isPerformingWork = false; - var isHostCallbackScheduled = false; - var isHostTimeoutScheduled = false; - var needsPaint = false; - var localSetTimeout = "function" === typeof setTimeout ? setTimeout : null; - var localClearTimeout = "function" === typeof clearTimeout ? clearTimeout : null; - var localSetImmediate = "undefined" !== typeof setImmediate ? setImmediate : null; - function advanceTimers(currentTime) { - for (var timer2 = peek(timerQueue); null !== timer2; ) { - if (null === timer2.callback) pop(timerQueue); - else if (timer2.startTime <= currentTime) - pop(timerQueue), timer2.sortIndex = timer2.expirationTime, push(taskQueue, timer2); - else break; - timer2 = peek(timerQueue); - } - } - function handleTimeout(currentTime) { - isHostTimeoutScheduled = false; - advanceTimers(currentTime); - if (!isHostCallbackScheduled) - if (null !== peek(taskQueue)) - isHostCallbackScheduled = true, isMessageLoopRunning || (isMessageLoopRunning = true, schedulePerformWorkUntilDeadline()); - else { - var firstTimer = peek(timerQueue); - null !== firstTimer && requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime); - } - } - var isMessageLoopRunning = false; - var taskTimeoutID = -1; - var frameInterval = 5; - var startTime = -1; - function shouldYieldToHost() { - return needsPaint ? true : exports.unstable_now() - startTime < frameInterval ? false : true; - } - function performWorkUntilDeadline() { - needsPaint = false; - if (isMessageLoopRunning) { - var currentTime = exports.unstable_now(); - startTime = currentTime; - var hasMoreWork = true; - try { - a: { - isHostCallbackScheduled = false; - isHostTimeoutScheduled && (isHostTimeoutScheduled = false, localClearTimeout(taskTimeoutID), taskTimeoutID = -1); - isPerformingWork = true; - var previousPriorityLevel = currentPriorityLevel; - try { - b: { - advanceTimers(currentTime); - for (currentTask = peek(taskQueue); null !== currentTask && !(currentTask.expirationTime > currentTime && shouldYieldToHost()); ) { - var callback = currentTask.callback; - if ("function" === typeof callback) { - currentTask.callback = null; - currentPriorityLevel = currentTask.priorityLevel; - var continuationCallback = callback( - currentTask.expirationTime <= currentTime - ); - currentTime = exports.unstable_now(); - if ("function" === typeof continuationCallback) { - currentTask.callback = continuationCallback; - advanceTimers(currentTime); - hasMoreWork = true; - break b; - } - currentTask === peek(taskQueue) && pop(taskQueue); - advanceTimers(currentTime); - } else pop(taskQueue); - currentTask = peek(taskQueue); - } - if (null !== currentTask) hasMoreWork = true; - else { - var firstTimer = peek(timerQueue); - null !== firstTimer && requestHostTimeout( - handleTimeout, - firstTimer.startTime - currentTime - ); - hasMoreWork = false; - } - } - break a; - } finally { - currentTask = null, currentPriorityLevel = previousPriorityLevel, isPerformingWork = false; - } - hasMoreWork = void 0; - } - } finally { - hasMoreWork ? schedulePerformWorkUntilDeadline() : isMessageLoopRunning = false; - } - } - } - var schedulePerformWorkUntilDeadline; - if ("function" === typeof localSetImmediate) - schedulePerformWorkUntilDeadline = function() { - localSetImmediate(performWorkUntilDeadline); - }; - else if ("undefined" !== typeof MessageChannel) { - channel = new MessageChannel(), port = channel.port2; - channel.port1.onmessage = performWorkUntilDeadline; - schedulePerformWorkUntilDeadline = function() { - port.postMessage(null); - }; - } else - schedulePerformWorkUntilDeadline = function() { - localSetTimeout(performWorkUntilDeadline, 0); - }; - var channel; - var port; - function requestHostTimeout(callback, ms) { - taskTimeoutID = localSetTimeout(function() { - callback(exports.unstable_now()); - }, ms); - } - exports.unstable_IdlePriority = 5; - exports.unstable_ImmediatePriority = 1; - exports.unstable_LowPriority = 4; - exports.unstable_NormalPriority = 3; - exports.unstable_Profiling = null; - exports.unstable_UserBlockingPriority = 2; - exports.unstable_cancelCallback = function(task) { - task.callback = null; - }; - exports.unstable_forceFrameRate = function(fps) { - 0 > fps || 125 < fps ? console.error( - "forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported" - ) : frameInterval = 0 < fps ? Math.floor(1e3 / fps) : 5; - }; - exports.unstable_getCurrentPriorityLevel = function() { - return currentPriorityLevel; - }; - exports.unstable_next = function(eventHandler) { - switch (currentPriorityLevel) { - case 1: - case 2: - case 3: - var priorityLevel = 3; - break; - default: - priorityLevel = currentPriorityLevel; - } - var previousPriorityLevel = currentPriorityLevel; - currentPriorityLevel = priorityLevel; - try { - return eventHandler(); - } finally { - currentPriorityLevel = previousPriorityLevel; - } - }; - exports.unstable_requestPaint = function() { - needsPaint = true; - }; - exports.unstable_runWithPriority = function(priorityLevel, eventHandler) { - switch (priorityLevel) { - case 1: - case 2: - case 3: - case 4: - case 5: - break; - default: - priorityLevel = 3; - } - var previousPriorityLevel = currentPriorityLevel; - currentPriorityLevel = priorityLevel; - try { - return eventHandler(); - } finally { - currentPriorityLevel = previousPriorityLevel; - } - }; - exports.unstable_scheduleCallback = function(priorityLevel, callback, options) { - var currentTime = exports.unstable_now(); - "object" === typeof options && null !== options ? (options = options.delay, options = "number" === typeof options && 0 < options ? currentTime + options : currentTime) : options = currentTime; - switch (priorityLevel) { - case 1: - var timeout2 = -1; - break; - case 2: - timeout2 = 250; - break; - case 5: - timeout2 = 1073741823; - break; - case 4: - timeout2 = 1e4; - break; - default: - timeout2 = 5e3; - } - timeout2 = options + timeout2; - priorityLevel = { - id: taskIdCounter++, - callback, - priorityLevel, - startTime: options, - expirationTime: timeout2, - sortIndex: -1 - }; - options > currentTime ? (priorityLevel.sortIndex = options, push(timerQueue, priorityLevel), null === peek(taskQueue) && priorityLevel === peek(timerQueue) && (isHostTimeoutScheduled ? (localClearTimeout(taskTimeoutID), taskTimeoutID = -1) : isHostTimeoutScheduled = true, requestHostTimeout(handleTimeout, options - currentTime))) : (priorityLevel.sortIndex = timeout2, push(taskQueue, priorityLevel), isHostCallbackScheduled || isPerformingWork || (isHostCallbackScheduled = true, isMessageLoopRunning || (isMessageLoopRunning = true, schedulePerformWorkUntilDeadline()))); - return priorityLevel; - }; - exports.unstable_shouldYield = shouldYieldToHost; - exports.unstable_wrapCallback = function(callback) { - var parentPriorityLevel = currentPriorityLevel; - return function() { - var previousPriorityLevel = currentPriorityLevel; - currentPriorityLevel = parentPriorityLevel; - try { - return callback.apply(this, arguments); - } finally { - currentPriorityLevel = previousPriorityLevel; - } - }; - }; - } -}); - -// node_modules/.deno/scheduler@0.26.0/node_modules/scheduler/index.js -var require_scheduler = __commonJS({ - "node_modules/.deno/scheduler@0.26.0/node_modules/scheduler/index.js"(exports, module) { - "use strict"; - if (true) { - module.exports = require_scheduler_production(); - } else { - module.exports = null; - } - } -}); - -// node_modules/.deno/react-dom@19.1.0/node_modules/react-dom/cjs/react-dom.production.js -var require_react_dom_production = __commonJS({ - "node_modules/.deno/react-dom@19.1.0/node_modules/react-dom/cjs/react-dom.production.js"(exports) { - "use strict"; - var React2 = require_react(); - function formatProdErrorMessage(code) { - var url2 = "https://react.dev/errors/" + code; - if (1 < arguments.length) { - url2 += "?args[]=" + encodeURIComponent(arguments[1]); - for (var i = 2; i < arguments.length; i++) - url2 += "&args[]=" + encodeURIComponent(arguments[i]); - } - return "Minified React error #" + code + "; visit " + url2 + " for the full message or use the non-minified dev environment for full errors and additional helpful warnings."; - } - function noop2() { - } - var Internals = { - d: { - f: noop2, - r: function() { - throw Error(formatProdErrorMessage(522)); - }, - D: noop2, - C: noop2, - L: noop2, - m: noop2, - X: noop2, - S: noop2, - M: noop2 - }, - p: 0, - findDOMNode: null - }; - var REACT_PORTAL_TYPE = Symbol.for("react.portal"); - function createPortal$1(children2, containerInfo, implementation) { - var key = 3 < arguments.length && void 0 !== arguments[3] ? arguments[3] : null; - return { - $$typeof: REACT_PORTAL_TYPE, - key: null == key ? null : "" + key, - children: children2, - containerInfo, - implementation - }; - } - var ReactSharedInternals = React2.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE; - function getCrossOriginStringAs(as, input) { - if ("font" === as) return ""; - if ("string" === typeof input) - return "use-credentials" === input ? input : ""; - } - exports.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = Internals; - exports.createPortal = function(children2, container) { - var key = 2 < arguments.length && void 0 !== arguments[2] ? arguments[2] : null; - if (!container || 1 !== container.nodeType && 9 !== container.nodeType && 11 !== container.nodeType) - throw Error(formatProdErrorMessage(299)); - return createPortal$1(children2, container, null, key); - }; - exports.flushSync = function(fn) { - var previousTransition = ReactSharedInternals.T, previousUpdatePriority = Internals.p; - try { - if (ReactSharedInternals.T = null, Internals.p = 2, fn) return fn(); - } finally { - ReactSharedInternals.T = previousTransition, Internals.p = previousUpdatePriority, Internals.d.f(); - } - }; - exports.preconnect = function(href, options) { - "string" === typeof href && (options ? (options = options.crossOrigin, options = "string" === typeof options ? "use-credentials" === options ? options : "" : void 0) : options = null, Internals.d.C(href, options)); - }; - exports.prefetchDNS = function(href) { - "string" === typeof href && Internals.d.D(href); - }; - exports.preinit = function(href, options) { - if ("string" === typeof href && options && "string" === typeof options.as) { - var as = options.as, crossOrigin = getCrossOriginStringAs(as, options.crossOrigin), integrity = "string" === typeof options.integrity ? options.integrity : void 0, fetchPriority = "string" === typeof options.fetchPriority ? options.fetchPriority : void 0; - "style" === as ? Internals.d.S( - href, - "string" === typeof options.precedence ? options.precedence : void 0, - { - crossOrigin, - integrity, - fetchPriority - } - ) : "script" === as && Internals.d.X(href, { - crossOrigin, - integrity, - fetchPriority, - nonce: "string" === typeof options.nonce ? options.nonce : void 0 - }); - } - }; - exports.preinitModule = function(href, options) { - if ("string" === typeof href) - if ("object" === typeof options && null !== options) { - if (null == options.as || "script" === options.as) { - var crossOrigin = getCrossOriginStringAs( - options.as, - options.crossOrigin - ); - Internals.d.M(href, { - crossOrigin, - integrity: "string" === typeof options.integrity ? options.integrity : void 0, - nonce: "string" === typeof options.nonce ? options.nonce : void 0 - }); - } - } else null == options && Internals.d.M(href); - }; - exports.preload = function(href, options) { - if ("string" === typeof href && "object" === typeof options && null !== options && "string" === typeof options.as) { - var as = options.as, crossOrigin = getCrossOriginStringAs(as, options.crossOrigin); - Internals.d.L(href, as, { - crossOrigin, - integrity: "string" === typeof options.integrity ? options.integrity : void 0, - nonce: "string" === typeof options.nonce ? options.nonce : void 0, - type: "string" === typeof options.type ? options.type : void 0, - fetchPriority: "string" === typeof options.fetchPriority ? options.fetchPriority : void 0, - referrerPolicy: "string" === typeof options.referrerPolicy ? options.referrerPolicy : void 0, - imageSrcSet: "string" === typeof options.imageSrcSet ? options.imageSrcSet : void 0, - imageSizes: "string" === typeof options.imageSizes ? options.imageSizes : void 0, - media: "string" === typeof options.media ? options.media : void 0 - }); - } - }; - exports.preloadModule = function(href, options) { - if ("string" === typeof href) - if (options) { - var crossOrigin = getCrossOriginStringAs(options.as, options.crossOrigin); - Internals.d.m(href, { - as: "string" === typeof options.as && "script" !== options.as ? options.as : void 0, - crossOrigin, - integrity: "string" === typeof options.integrity ? options.integrity : void 0 - }); - } else Internals.d.m(href); - }; - exports.requestFormReset = function(form) { - Internals.d.r(form); - }; - exports.unstable_batchedUpdates = function(fn, a) { - return fn(a); - }; - exports.useFormState = function(action, initialState, permalink) { - return ReactSharedInternals.H.useFormState(action, initialState, permalink); - }; - exports.useFormStatus = function() { - return ReactSharedInternals.H.useHostTransitionStatus(); - }; - exports.version = "19.1.0"; - } -}); - -// node_modules/.deno/react-dom@19.1.0/node_modules/react-dom/index.js -var require_react_dom = __commonJS({ - "node_modules/.deno/react-dom@19.1.0/node_modules/react-dom/index.js"(exports, module) { - "use strict"; - function checkDCE() { - if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === "undefined" || typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE !== "function") { - return; - } - if (false) { - throw new Error("^_^"); - } - try { - __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(checkDCE); - } catch (err) { - console.error(err); - } - } - if (true) { - checkDCE(); - module.exports = require_react_dom_production(); - } else { - module.exports = null; - } - } -}); - -// node_modules/.deno/react-dom@19.1.0/node_modules/react-dom/cjs/react-dom-client.production.js -var require_react_dom_client_production = __commonJS({ - "node_modules/.deno/react-dom@19.1.0/node_modules/react-dom/cjs/react-dom-client.production.js"(exports) { - "use strict"; - var Scheduler = require_scheduler(); - var React2 = require_react(); - var ReactDOM = require_react_dom(); - function formatProdErrorMessage(code) { - var url2 = "https://react.dev/errors/" + code; - if (1 < arguments.length) { - url2 += "?args[]=" + encodeURIComponent(arguments[1]); - for (var i = 2; i < arguments.length; i++) - url2 += "&args[]=" + encodeURIComponent(arguments[i]); - } - return "Minified React error #" + code + "; visit " + url2 + " for the full message or use the non-minified dev environment for full errors and additional helpful warnings."; - } - function isValidContainer(node) { - return !(!node || 1 !== node.nodeType && 9 !== node.nodeType && 11 !== node.nodeType); - } - function getNearestMountedFiber(fiber) { - var node = fiber, nearestMounted = fiber; - if (fiber.alternate) for (; node.return; ) node = node.return; - else { - fiber = node; - do - node = fiber, 0 !== (node.flags & 4098) && (nearestMounted = node.return), fiber = node.return; - while (fiber); - } - return 3 === node.tag ? nearestMounted : null; - } - function getSuspenseInstanceFromFiber(fiber) { - if (13 === fiber.tag) { - var suspenseState = fiber.memoizedState; - null === suspenseState && (fiber = fiber.alternate, null !== fiber && (suspenseState = fiber.memoizedState)); - if (null !== suspenseState) return suspenseState.dehydrated; - } - return null; - } - function assertIsMounted(fiber) { - if (getNearestMountedFiber(fiber) !== fiber) - throw Error(formatProdErrorMessage(188)); - } - function findCurrentFiberUsingSlowPath(fiber) { - var alternate = fiber.alternate; - if (!alternate) { - alternate = getNearestMountedFiber(fiber); - if (null === alternate) throw Error(formatProdErrorMessage(188)); - return alternate !== fiber ? null : fiber; - } - for (var a = fiber, b = alternate; ; ) { - var parentA = a.return; - if (null === parentA) break; - var parentB = parentA.alternate; - if (null === parentB) { - b = parentA.return; - if (null !== b) { - a = b; - continue; - } - break; - } - if (parentA.child === parentB.child) { - for (parentB = parentA.child; parentB; ) { - if (parentB === a) return assertIsMounted(parentA), fiber; - if (parentB === b) return assertIsMounted(parentA), alternate; - parentB = parentB.sibling; - } - throw Error(formatProdErrorMessage(188)); - } - if (a.return !== b.return) a = parentA, b = parentB; - else { - for (var didFindChild = false, child$0 = parentA.child; child$0; ) { - if (child$0 === a) { - didFindChild = true; - a = parentA; - b = parentB; - break; - } - if (child$0 === b) { - didFindChild = true; - b = parentA; - a = parentB; - break; - } - child$0 = child$0.sibling; - } - if (!didFindChild) { - for (child$0 = parentB.child; child$0; ) { - if (child$0 === a) { - didFindChild = true; - a = parentB; - b = parentA; - break; - } - if (child$0 === b) { - didFindChild = true; - b = parentB; - a = parentA; - break; - } - child$0 = child$0.sibling; - } - if (!didFindChild) throw Error(formatProdErrorMessage(189)); - } - } - if (a.alternate !== b) throw Error(formatProdErrorMessage(190)); - } - if (3 !== a.tag) throw Error(formatProdErrorMessage(188)); - return a.stateNode.current === a ? fiber : alternate; - } - function findCurrentHostFiberImpl(node) { - var tag = node.tag; - if (5 === tag || 26 === tag || 27 === tag || 6 === tag) return node; - for (node = node.child; null !== node; ) { - tag = findCurrentHostFiberImpl(node); - if (null !== tag) return tag; - node = node.sibling; - } - return null; - } - var assign = Object.assign; - var REACT_LEGACY_ELEMENT_TYPE = Symbol.for("react.element"); - var REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"); - var REACT_PORTAL_TYPE = Symbol.for("react.portal"); - var REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"); - var REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode"); - var REACT_PROFILER_TYPE = Symbol.for("react.profiler"); - var REACT_PROVIDER_TYPE = Symbol.for("react.provider"); - var REACT_CONSUMER_TYPE = Symbol.for("react.consumer"); - var REACT_CONTEXT_TYPE = Symbol.for("react.context"); - var REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref"); - var REACT_SUSPENSE_TYPE = Symbol.for("react.suspense"); - var REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list"); - var REACT_MEMO_TYPE = Symbol.for("react.memo"); - var REACT_LAZY_TYPE = Symbol.for("react.lazy"); - Symbol.for("react.scope"); - var REACT_ACTIVITY_TYPE = Symbol.for("react.activity"); - Symbol.for("react.legacy_hidden"); - Symbol.for("react.tracing_marker"); - var REACT_MEMO_CACHE_SENTINEL = Symbol.for("react.memo_cache_sentinel"); - Symbol.for("react.view_transition"); - var MAYBE_ITERATOR_SYMBOL = Symbol.iterator; - function getIteratorFn(maybeIterable) { - if (null === maybeIterable || "object" !== typeof maybeIterable) return null; - maybeIterable = MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL] || maybeIterable["@@iterator"]; - return "function" === typeof maybeIterable ? maybeIterable : null; - } - var REACT_CLIENT_REFERENCE = Symbol.for("react.client.reference"); - function getComponentNameFromType(type2) { - if (null == type2) return null; - if ("function" === typeof type2) - return type2.$$typeof === REACT_CLIENT_REFERENCE ? null : type2.displayName || type2.name || null; - if ("string" === typeof type2) return type2; - switch (type2) { - case REACT_FRAGMENT_TYPE: - return "Fragment"; - case REACT_PROFILER_TYPE: - return "Profiler"; - case REACT_STRICT_MODE_TYPE: - return "StrictMode"; - case REACT_SUSPENSE_TYPE: - return "Suspense"; - case REACT_SUSPENSE_LIST_TYPE: - return "SuspenseList"; - case REACT_ACTIVITY_TYPE: - return "Activity"; - } - if ("object" === typeof type2) - switch (type2.$$typeof) { - case REACT_PORTAL_TYPE: - return "Portal"; - case REACT_CONTEXT_TYPE: - return (type2.displayName || "Context") + ".Provider"; - case REACT_CONSUMER_TYPE: - return (type2._context.displayName || "Context") + ".Consumer"; - case REACT_FORWARD_REF_TYPE: - var innerType = type2.render; - type2 = type2.displayName; - type2 || (type2 = innerType.displayName || innerType.name || "", type2 = "" !== type2 ? "ForwardRef(" + type2 + ")" : "ForwardRef"); - return type2; - case REACT_MEMO_TYPE: - return innerType = type2.displayName || null, null !== innerType ? innerType : getComponentNameFromType(type2.type) || "Memo"; - case REACT_LAZY_TYPE: - innerType = type2._payload; - type2 = type2._init; - try { - return getComponentNameFromType(type2(innerType)); - } catch (x2) { - } - } - return null; - } - var isArrayImpl = Array.isArray; - var ReactSharedInternals = React2.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE; - var ReactDOMSharedInternals = ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE; - var sharedNotPendingObject = { - pending: false, - data: null, - method: null, - action: null - }; - var valueStack = []; - var index = -1; - function createCursor(defaultValue) { - return { current: defaultValue }; - } - function pop(cursor) { - 0 > index || (cursor.current = valueStack[index], valueStack[index] = null, index--); - } - function push(cursor, value2) { - index++; - valueStack[index] = cursor.current; - cursor.current = value2; - } - var contextStackCursor = createCursor(null); - var contextFiberStackCursor = createCursor(null); - var rootInstanceStackCursor = createCursor(null); - var hostTransitionProviderCursor = createCursor(null); - function pushHostContainer(fiber, nextRootInstance) { - push(rootInstanceStackCursor, nextRootInstance); - push(contextFiberStackCursor, fiber); - push(contextStackCursor, null); - switch (nextRootInstance.nodeType) { - case 9: - case 11: - fiber = (fiber = nextRootInstance.documentElement) ? (fiber = fiber.namespaceURI) ? getOwnHostContext(fiber) : 0 : 0; - break; - default: - if (fiber = nextRootInstance.tagName, nextRootInstance = nextRootInstance.namespaceURI) - nextRootInstance = getOwnHostContext(nextRootInstance), fiber = getChildHostContextProd(nextRootInstance, fiber); - else - switch (fiber) { - case "svg": - fiber = 1; - break; - case "math": - fiber = 2; - break; - default: - fiber = 0; - } - } - pop(contextStackCursor); - push(contextStackCursor, fiber); - } - function popHostContainer() { - pop(contextStackCursor); - pop(contextFiberStackCursor); - pop(rootInstanceStackCursor); - } - function pushHostContext(fiber) { - null !== fiber.memoizedState && push(hostTransitionProviderCursor, fiber); - var context = contextStackCursor.current; - var JSCompiler_inline_result = getChildHostContextProd(context, fiber.type); - context !== JSCompiler_inline_result && (push(contextFiberStackCursor, fiber), push(contextStackCursor, JSCompiler_inline_result)); - } - function popHostContext(fiber) { - contextFiberStackCursor.current === fiber && (pop(contextStackCursor), pop(contextFiberStackCursor)); - hostTransitionProviderCursor.current === fiber && (pop(hostTransitionProviderCursor), HostTransitionContext._currentValue = sharedNotPendingObject); - } - var hasOwnProperty = Object.prototype.hasOwnProperty; - var scheduleCallback$3 = Scheduler.unstable_scheduleCallback; - var cancelCallback$1 = Scheduler.unstable_cancelCallback; - var shouldYield = Scheduler.unstable_shouldYield; - var requestPaint = Scheduler.unstable_requestPaint; - var now2 = Scheduler.unstable_now; - var getCurrentPriorityLevel = Scheduler.unstable_getCurrentPriorityLevel; - var ImmediatePriority = Scheduler.unstable_ImmediatePriority; - var UserBlockingPriority = Scheduler.unstable_UserBlockingPriority; - var NormalPriority$1 = Scheduler.unstable_NormalPriority; - var LowPriority = Scheduler.unstable_LowPriority; - var IdlePriority = Scheduler.unstable_IdlePriority; - var log$1 = Scheduler.log; - var unstable_setDisableYieldValue = Scheduler.unstable_setDisableYieldValue; - var rendererID = null; - var injectedHook = null; - function setIsStrictModeForDevtools(newIsStrictMode) { - "function" === typeof log$1 && unstable_setDisableYieldValue(newIsStrictMode); - if (injectedHook && "function" === typeof injectedHook.setStrictMode) - try { - injectedHook.setStrictMode(rendererID, newIsStrictMode); - } catch (err) { - } - } - var clz32 = Math.clz32 ? Math.clz32 : clz32Fallback; - var log = Math.log; - var LN2 = Math.LN2; - function clz32Fallback(x2) { - x2 >>>= 0; - return 0 === x2 ? 32 : 31 - (log(x2) / LN2 | 0) | 0; - } - var nextTransitionLane = 256; - var nextRetryLane = 4194304; - function getHighestPriorityLanes(lanes) { - var pendingSyncLanes = lanes & 42; - if (0 !== pendingSyncLanes) return pendingSyncLanes; - switch (lanes & -lanes) { - case 1: - return 1; - case 2: - return 2; - case 4: - return 4; - case 8: - return 8; - case 16: - return 16; - case 32: - return 32; - case 64: - return 64; - case 128: - return 128; - case 256: - case 512: - case 1024: - case 2048: - case 4096: - case 8192: - case 16384: - case 32768: - case 65536: - case 131072: - case 262144: - case 524288: - case 1048576: - case 2097152: - return lanes & 4194048; - case 4194304: - case 8388608: - case 16777216: - case 33554432: - return lanes & 62914560; - case 67108864: - return 67108864; - case 134217728: - return 134217728; - case 268435456: - return 268435456; - case 536870912: - return 536870912; - case 1073741824: - return 0; - default: - return lanes; - } - } - function getNextLanes(root3, wipLanes, rootHasPendingCommit) { - var pendingLanes = root3.pendingLanes; - if (0 === pendingLanes) return 0; - var nextLanes = 0, suspendedLanes = root3.suspendedLanes, pingedLanes = root3.pingedLanes; - root3 = root3.warmLanes; - var nonIdlePendingLanes = pendingLanes & 134217727; - 0 !== nonIdlePendingLanes ? (pendingLanes = nonIdlePendingLanes & ~suspendedLanes, 0 !== pendingLanes ? nextLanes = getHighestPriorityLanes(pendingLanes) : (pingedLanes &= nonIdlePendingLanes, 0 !== pingedLanes ? nextLanes = getHighestPriorityLanes(pingedLanes) : rootHasPendingCommit || (rootHasPendingCommit = nonIdlePendingLanes & ~root3, 0 !== rootHasPendingCommit && (nextLanes = getHighestPriorityLanes(rootHasPendingCommit))))) : (nonIdlePendingLanes = pendingLanes & ~suspendedLanes, 0 !== nonIdlePendingLanes ? nextLanes = getHighestPriorityLanes(nonIdlePendingLanes) : 0 !== pingedLanes ? nextLanes = getHighestPriorityLanes(pingedLanes) : rootHasPendingCommit || (rootHasPendingCommit = pendingLanes & ~root3, 0 !== rootHasPendingCommit && (nextLanes = getHighestPriorityLanes(rootHasPendingCommit)))); - return 0 === nextLanes ? 0 : 0 !== wipLanes && wipLanes !== nextLanes && 0 === (wipLanes & suspendedLanes) && (suspendedLanes = nextLanes & -nextLanes, rootHasPendingCommit = wipLanes & -wipLanes, suspendedLanes >= rootHasPendingCommit || 32 === suspendedLanes && 0 !== (rootHasPendingCommit & 4194048)) ? wipLanes : nextLanes; - } - function checkIfRootIsPrerendering(root3, renderLanes2) { - return 0 === (root3.pendingLanes & ~(root3.suspendedLanes & ~root3.pingedLanes) & renderLanes2); - } - function computeExpirationTime(lane, currentTime) { - switch (lane) { - case 1: - case 2: - case 4: - case 8: - case 64: - return currentTime + 250; - case 16: - case 32: - case 128: - case 256: - case 512: - case 1024: - case 2048: - case 4096: - case 8192: - case 16384: - case 32768: - case 65536: - case 131072: - case 262144: - case 524288: - case 1048576: - case 2097152: - return currentTime + 5e3; - case 4194304: - case 8388608: - case 16777216: - case 33554432: - return -1; - case 67108864: - case 134217728: - case 268435456: - case 536870912: - case 1073741824: - return -1; - default: - return -1; - } - } - function claimNextTransitionLane() { - var lane = nextTransitionLane; - nextTransitionLane <<= 1; - 0 === (nextTransitionLane & 4194048) && (nextTransitionLane = 256); - return lane; - } - function claimNextRetryLane() { - var lane = nextRetryLane; - nextRetryLane <<= 1; - 0 === (nextRetryLane & 62914560) && (nextRetryLane = 4194304); - return lane; - } - function createLaneMap(initial) { - for (var laneMap = [], i = 0; 31 > i; i++) laneMap.push(initial); - return laneMap; - } - function markRootUpdated$1(root3, updateLane) { - root3.pendingLanes |= updateLane; - 268435456 !== updateLane && (root3.suspendedLanes = 0, root3.pingedLanes = 0, root3.warmLanes = 0); - } - function markRootFinished(root3, finishedLanes, remainingLanes, spawnedLane, updatedLanes, suspendedRetryLanes) { - var previouslyPendingLanes = root3.pendingLanes; - root3.pendingLanes = remainingLanes; - root3.suspendedLanes = 0; - root3.pingedLanes = 0; - root3.warmLanes = 0; - root3.expiredLanes &= remainingLanes; - root3.entangledLanes &= remainingLanes; - root3.errorRecoveryDisabledLanes &= remainingLanes; - root3.shellSuspendCounter = 0; - var entanglements = root3.entanglements, expirationTimes = root3.expirationTimes, hiddenUpdates = root3.hiddenUpdates; - for (remainingLanes = previouslyPendingLanes & ~remainingLanes; 0 < remainingLanes; ) { - var index$5 = 31 - clz32(remainingLanes), lane = 1 << index$5; - entanglements[index$5] = 0; - expirationTimes[index$5] = -1; - var hiddenUpdatesForLane = hiddenUpdates[index$5]; - if (null !== hiddenUpdatesForLane) - for (hiddenUpdates[index$5] = null, index$5 = 0; index$5 < hiddenUpdatesForLane.length; index$5++) { - var update = hiddenUpdatesForLane[index$5]; - null !== update && (update.lane &= -536870913); - } - remainingLanes &= ~lane; - } - 0 !== spawnedLane && markSpawnedDeferredLane(root3, spawnedLane, 0); - 0 !== suspendedRetryLanes && 0 === updatedLanes && 0 !== root3.tag && (root3.suspendedLanes |= suspendedRetryLanes & ~(previouslyPendingLanes & ~finishedLanes)); - } - function markSpawnedDeferredLane(root3, spawnedLane, entangledLanes) { - root3.pendingLanes |= spawnedLane; - root3.suspendedLanes &= ~spawnedLane; - var spawnedLaneIndex = 31 - clz32(spawnedLane); - root3.entangledLanes |= spawnedLane; - root3.entanglements[spawnedLaneIndex] = root3.entanglements[spawnedLaneIndex] | 1073741824 | entangledLanes & 4194090; - } - function markRootEntangled(root3, entangledLanes) { - var rootEntangledLanes = root3.entangledLanes |= entangledLanes; - for (root3 = root3.entanglements; rootEntangledLanes; ) { - var index$6 = 31 - clz32(rootEntangledLanes), lane = 1 << index$6; - lane & entangledLanes | root3[index$6] & entangledLanes && (root3[index$6] |= entangledLanes); - rootEntangledLanes &= ~lane; - } - } - function getBumpedLaneForHydrationByLane(lane) { - switch (lane) { - case 2: - lane = 1; - break; - case 8: - lane = 4; - break; - case 32: - lane = 16; - break; - case 256: - case 512: - case 1024: - case 2048: - case 4096: - case 8192: - case 16384: - case 32768: - case 65536: - case 131072: - case 262144: - case 524288: - case 1048576: - case 2097152: - case 4194304: - case 8388608: - case 16777216: - case 33554432: - lane = 128; - break; - case 268435456: - lane = 134217728; - break; - default: - lane = 0; - } - return lane; - } - function lanesToEventPriority(lanes) { - lanes &= -lanes; - return 2 < lanes ? 8 < lanes ? 0 !== (lanes & 134217727) ? 32 : 268435456 : 8 : 2; - } - function resolveUpdatePriority() { - var updatePriority = ReactDOMSharedInternals.p; - if (0 !== updatePriority) return updatePriority; - updatePriority = window.event; - return void 0 === updatePriority ? 32 : getEventPriority(updatePriority.type); - } - function runWithPriority(priority, fn) { - var previousPriority = ReactDOMSharedInternals.p; - try { - return ReactDOMSharedInternals.p = priority, fn(); - } finally { - ReactDOMSharedInternals.p = previousPriority; - } - } - var randomKey = Math.random().toString(36).slice(2); - var internalInstanceKey = "__reactFiber$" + randomKey; - var internalPropsKey = "__reactProps$" + randomKey; - var internalContainerInstanceKey = "__reactContainer$" + randomKey; - var internalEventHandlersKey = "__reactEvents$" + randomKey; - var internalEventHandlerListenersKey = "__reactListeners$" + randomKey; - var internalEventHandlesSetKey = "__reactHandles$" + randomKey; - var internalRootNodeResourcesKey = "__reactResources$" + randomKey; - var internalHoistableMarker = "__reactMarker$" + randomKey; - function detachDeletedInstance(node) { - delete node[internalInstanceKey]; - delete node[internalPropsKey]; - delete node[internalEventHandlersKey]; - delete node[internalEventHandlerListenersKey]; - delete node[internalEventHandlesSetKey]; - } - function getClosestInstanceFromNode(targetNode) { - var targetInst = targetNode[internalInstanceKey]; - if (targetInst) return targetInst; - for (var parentNode = targetNode.parentNode; parentNode; ) { - if (targetInst = parentNode[internalContainerInstanceKey] || parentNode[internalInstanceKey]) { - parentNode = targetInst.alternate; - if (null !== targetInst.child || null !== parentNode && null !== parentNode.child) - for (targetNode = getParentSuspenseInstance(targetNode); null !== targetNode; ) { - if (parentNode = targetNode[internalInstanceKey]) return parentNode; - targetNode = getParentSuspenseInstance(targetNode); - } - return targetInst; - } - targetNode = parentNode; - parentNode = targetNode.parentNode; - } - return null; - } - function getInstanceFromNode(node) { - if (node = node[internalInstanceKey] || node[internalContainerInstanceKey]) { - var tag = node.tag; - if (5 === tag || 6 === tag || 13 === tag || 26 === tag || 27 === tag || 3 === tag) - return node; - } - return null; - } - function getNodeFromInstance(inst) { - var tag = inst.tag; - if (5 === tag || 26 === tag || 27 === tag || 6 === tag) return inst.stateNode; - throw Error(formatProdErrorMessage(33)); - } - function getResourcesFromRoot(root3) { - var resources = root3[internalRootNodeResourcesKey]; - resources || (resources = root3[internalRootNodeResourcesKey] = { hoistableStyles: /* @__PURE__ */ new Map(), hoistableScripts: /* @__PURE__ */ new Map() }); - return resources; - } - function markNodeAsHoistable(node) { - node[internalHoistableMarker] = true; - } - var allNativeEvents = /* @__PURE__ */ new Set(); - var registrationNameDependencies = {}; - function registerTwoPhaseEvent(registrationName, dependencies) { - registerDirectEvent(registrationName, dependencies); - registerDirectEvent(registrationName + "Capture", dependencies); - } - function registerDirectEvent(registrationName, dependencies) { - registrationNameDependencies[registrationName] = dependencies; - for (registrationName = 0; registrationName < dependencies.length; registrationName++) - allNativeEvents.add(dependencies[registrationName]); - } - var VALID_ATTRIBUTE_NAME_REGEX = RegExp( - "^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD][:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040]*$" - ); - var illegalAttributeNameCache = {}; - var validatedAttributeNameCache = {}; - function isAttributeNameSafe(attributeName) { - if (hasOwnProperty.call(validatedAttributeNameCache, attributeName)) - return true; - if (hasOwnProperty.call(illegalAttributeNameCache, attributeName)) return false; - if (VALID_ATTRIBUTE_NAME_REGEX.test(attributeName)) - return validatedAttributeNameCache[attributeName] = true; - illegalAttributeNameCache[attributeName] = true; - return false; - } - function setValueForAttribute(node, name, value2) { - if (isAttributeNameSafe(name)) - if (null === value2) node.removeAttribute(name); - else { - switch (typeof value2) { - case "undefined": - case "function": - case "symbol": - node.removeAttribute(name); - return; - case "boolean": - var prefix$8 = name.toLowerCase().slice(0, 5); - if ("data-" !== prefix$8 && "aria-" !== prefix$8) { - node.removeAttribute(name); - return; - } - } - node.setAttribute(name, "" + value2); - } - } - function setValueForKnownAttribute(node, name, value2) { - if (null === value2) node.removeAttribute(name); - else { - switch (typeof value2) { - case "undefined": - case "function": - case "symbol": - case "boolean": - node.removeAttribute(name); - return; - } - node.setAttribute(name, "" + value2); - } - } - function setValueForNamespacedAttribute(node, namespace, name, value2) { - if (null === value2) node.removeAttribute(name); - else { - switch (typeof value2) { - case "undefined": - case "function": - case "symbol": - case "boolean": - node.removeAttribute(name); - return; - } - node.setAttributeNS(namespace, name, "" + value2); - } - } - var prefix; - var suffix; - function describeBuiltInComponentFrame(name) { - if (void 0 === prefix) - try { - throw Error(); - } catch (x2) { - var match = x2.stack.trim().match(/\n( *(at )?)/); - prefix = match && match[1] || ""; - suffix = -1 < x2.stack.indexOf("\n at") ? " ()" : -1 < x2.stack.indexOf("@") ? "@unknown:0:0" : ""; - } - return "\n" + prefix + name + suffix; - } - var reentry = false; - function describeNativeComponentFrame(fn, construct) { - if (!fn || reentry) return ""; - reentry = true; - var previousPrepareStackTrace = Error.prepareStackTrace; - Error.prepareStackTrace = void 0; - try { - var RunInRootFrame = { - DetermineComponentFrameRoot: function() { - try { - if (construct) { - var Fake = function() { - throw Error(); - }; - Object.defineProperty(Fake.prototype, "props", { - set: function() { - throw Error(); - } - }); - if ("object" === typeof Reflect && Reflect.construct) { - try { - Reflect.construct(Fake, []); - } catch (x2) { - var control = x2; - } - Reflect.construct(fn, [], Fake); - } else { - try { - Fake.call(); - } catch (x$9) { - control = x$9; - } - fn.call(Fake.prototype); - } - } else { - try { - throw Error(); - } catch (x$10) { - control = x$10; - } - (Fake = fn()) && "function" === typeof Fake.catch && Fake.catch(function() { - }); - } - } catch (sample) { - if (sample && control && "string" === typeof sample.stack) - return [sample.stack, control.stack]; - } - return [null, null]; - } - }; - RunInRootFrame.DetermineComponentFrameRoot.displayName = "DetermineComponentFrameRoot"; - var namePropDescriptor = Object.getOwnPropertyDescriptor( - RunInRootFrame.DetermineComponentFrameRoot, - "name" - ); - namePropDescriptor && namePropDescriptor.configurable && Object.defineProperty( - RunInRootFrame.DetermineComponentFrameRoot, - "name", - { value: "DetermineComponentFrameRoot" } - ); - var _RunInRootFrame$Deter = RunInRootFrame.DetermineComponentFrameRoot(), sampleStack = _RunInRootFrame$Deter[0], controlStack = _RunInRootFrame$Deter[1]; - if (sampleStack && controlStack) { - var sampleLines = sampleStack.split("\n"), controlLines = controlStack.split("\n"); - for (namePropDescriptor = RunInRootFrame = 0; RunInRootFrame < sampleLines.length && !sampleLines[RunInRootFrame].includes("DetermineComponentFrameRoot"); ) - RunInRootFrame++; - for (; namePropDescriptor < controlLines.length && !controlLines[namePropDescriptor].includes( - "DetermineComponentFrameRoot" - ); ) - namePropDescriptor++; - if (RunInRootFrame === sampleLines.length || namePropDescriptor === controlLines.length) - for (RunInRootFrame = sampleLines.length - 1, namePropDescriptor = controlLines.length - 1; 1 <= RunInRootFrame && 0 <= namePropDescriptor && sampleLines[RunInRootFrame] !== controlLines[namePropDescriptor]; ) - namePropDescriptor--; - for (; 1 <= RunInRootFrame && 0 <= namePropDescriptor; RunInRootFrame--, namePropDescriptor--) - if (sampleLines[RunInRootFrame] !== controlLines[namePropDescriptor]) { - if (1 !== RunInRootFrame || 1 !== namePropDescriptor) { - do - if (RunInRootFrame--, namePropDescriptor--, 0 > namePropDescriptor || sampleLines[RunInRootFrame] !== controlLines[namePropDescriptor]) { - var frame2 = "\n" + sampleLines[RunInRootFrame].replace(" at new ", " at "); - fn.displayName && frame2.includes("") && (frame2 = frame2.replace("", fn.displayName)); - return frame2; - } - while (1 <= RunInRootFrame && 0 <= namePropDescriptor); - } - break; - } - } - } finally { - reentry = false, Error.prepareStackTrace = previousPrepareStackTrace; - } - return (previousPrepareStackTrace = fn ? fn.displayName || fn.name : "") ? describeBuiltInComponentFrame(previousPrepareStackTrace) : ""; - } - function describeFiber(fiber) { - switch (fiber.tag) { - case 26: - case 27: - case 5: - return describeBuiltInComponentFrame(fiber.type); - case 16: - return describeBuiltInComponentFrame("Lazy"); - case 13: - return describeBuiltInComponentFrame("Suspense"); - case 19: - return describeBuiltInComponentFrame("SuspenseList"); - case 0: - case 15: - return describeNativeComponentFrame(fiber.type, false); - case 11: - return describeNativeComponentFrame(fiber.type.render, false); - case 1: - return describeNativeComponentFrame(fiber.type, true); - case 31: - return describeBuiltInComponentFrame("Activity"); - default: - return ""; - } - } - function getStackByFiberInDevAndProd(workInProgress2) { - try { - var info = ""; - do - info += describeFiber(workInProgress2), workInProgress2 = workInProgress2.return; - while (workInProgress2); - return info; - } catch (x2) { - return "\nError generating stack: " + x2.message + "\n" + x2.stack; - } - } - function getToStringValue(value2) { - switch (typeof value2) { - case "bigint": - case "boolean": - case "number": - case "string": - case "undefined": - return value2; - case "object": - return value2; - default: - return ""; - } - } - function isCheckable(elem) { - var type2 = elem.type; - return (elem = elem.nodeName) && "input" === elem.toLowerCase() && ("checkbox" === type2 || "radio" === type2); - } - function trackValueOnNode(node) { - var valueField = isCheckable(node) ? "checked" : "value", descriptor = Object.getOwnPropertyDescriptor( - node.constructor.prototype, - valueField - ), currentValue = "" + node[valueField]; - if (!node.hasOwnProperty(valueField) && "undefined" !== typeof descriptor && "function" === typeof descriptor.get && "function" === typeof descriptor.set) { - var get3 = descriptor.get, set3 = descriptor.set; - Object.defineProperty(node, valueField, { - configurable: true, - get: function() { - return get3.call(this); - }, - set: function(value2) { - currentValue = "" + value2; - set3.call(this, value2); - } - }); - Object.defineProperty(node, valueField, { - enumerable: descriptor.enumerable - }); - return { - getValue: function() { - return currentValue; - }, - setValue: function(value2) { - currentValue = "" + value2; - }, - stopTracking: function() { - node._valueTracker = null; - delete node[valueField]; - } - }; - } - } - function track(node) { - node._valueTracker || (node._valueTracker = trackValueOnNode(node)); - } - function updateValueIfChanged(node) { - if (!node) return false; - var tracker = node._valueTracker; - if (!tracker) return true; - var lastValue = tracker.getValue(); - var value2 = ""; - node && (value2 = isCheckable(node) ? node.checked ? "true" : "false" : node.value); - node = value2; - return node !== lastValue ? (tracker.setValue(node), true) : false; - } - function getActiveElement(doc) { - doc = doc || ("undefined" !== typeof document ? document : void 0); - if ("undefined" === typeof doc) return null; - try { - return doc.activeElement || doc.body; - } catch (e) { - return doc.body; - } - } - var escapeSelectorAttributeValueInsideDoubleQuotesRegex = /[\n"\\]/g; - function escapeSelectorAttributeValueInsideDoubleQuotes(value2) { - return value2.replace( - escapeSelectorAttributeValueInsideDoubleQuotesRegex, - function(ch) { - return "\\" + ch.charCodeAt(0).toString(16) + " "; - } - ); - } - function updateInput(element, value2, defaultValue, lastDefaultValue, checked, defaultChecked, type2, name) { - element.name = ""; - null != type2 && "function" !== typeof type2 && "symbol" !== typeof type2 && "boolean" !== typeof type2 ? element.type = type2 : element.removeAttribute("type"); - if (null != value2) - if ("number" === type2) { - if (0 === value2 && "" === element.value || element.value != value2) - element.value = "" + getToStringValue(value2); - } else - element.value !== "" + getToStringValue(value2) && (element.value = "" + getToStringValue(value2)); - else - "submit" !== type2 && "reset" !== type2 || element.removeAttribute("value"); - null != value2 ? setDefaultValue(element, type2, getToStringValue(value2)) : null != defaultValue ? setDefaultValue(element, type2, getToStringValue(defaultValue)) : null != lastDefaultValue && element.removeAttribute("value"); - null == checked && null != defaultChecked && (element.defaultChecked = !!defaultChecked); - null != checked && (element.checked = checked && "function" !== typeof checked && "symbol" !== typeof checked); - null != name && "function" !== typeof name && "symbol" !== typeof name && "boolean" !== typeof name ? element.name = "" + getToStringValue(name) : element.removeAttribute("name"); - } - function initInput(element, value2, defaultValue, checked, defaultChecked, type2, name, isHydrating2) { - null != type2 && "function" !== typeof type2 && "symbol" !== typeof type2 && "boolean" !== typeof type2 && (element.type = type2); - if (null != value2 || null != defaultValue) { - if (!("submit" !== type2 && "reset" !== type2 || void 0 !== value2 && null !== value2)) - return; - defaultValue = null != defaultValue ? "" + getToStringValue(defaultValue) : ""; - value2 = null != value2 ? "" + getToStringValue(value2) : defaultValue; - isHydrating2 || value2 === element.value || (element.value = value2); - element.defaultValue = value2; - } - checked = null != checked ? checked : defaultChecked; - checked = "function" !== typeof checked && "symbol" !== typeof checked && !!checked; - element.checked = isHydrating2 ? element.checked : !!checked; - element.defaultChecked = !!checked; - null != name && "function" !== typeof name && "symbol" !== typeof name && "boolean" !== typeof name && (element.name = name); - } - function setDefaultValue(node, type2, value2) { - "number" === type2 && getActiveElement(node.ownerDocument) === node || node.defaultValue === "" + value2 || (node.defaultValue = "" + value2); - } - function updateOptions(node, multiple, propValue, setDefaultSelected) { - node = node.options; - if (multiple) { - multiple = {}; - for (var i = 0; i < propValue.length; i++) - multiple["$" + propValue[i]] = true; - for (propValue = 0; propValue < node.length; propValue++) - i = multiple.hasOwnProperty("$" + node[propValue].value), node[propValue].selected !== i && (node[propValue].selected = i), i && setDefaultSelected && (node[propValue].defaultSelected = true); - } else { - propValue = "" + getToStringValue(propValue); - multiple = null; - for (i = 0; i < node.length; i++) { - if (node[i].value === propValue) { - node[i].selected = true; - setDefaultSelected && (node[i].defaultSelected = true); - return; - } - null !== multiple || node[i].disabled || (multiple = node[i]); - } - null !== multiple && (multiple.selected = true); - } - } - function updateTextarea(element, value2, defaultValue) { - if (null != value2 && (value2 = "" + getToStringValue(value2), value2 !== element.value && (element.value = value2), null == defaultValue)) { - element.defaultValue !== value2 && (element.defaultValue = value2); - return; - } - element.defaultValue = null != defaultValue ? "" + getToStringValue(defaultValue) : ""; - } - function initTextarea(element, value2, defaultValue, children2) { - if (null == value2) { - if (null != children2) { - if (null != defaultValue) throw Error(formatProdErrorMessage(92)); - if (isArrayImpl(children2)) { - if (1 < children2.length) throw Error(formatProdErrorMessage(93)); - children2 = children2[0]; - } - defaultValue = children2; - } - null == defaultValue && (defaultValue = ""); - value2 = defaultValue; - } - defaultValue = getToStringValue(value2); - element.defaultValue = defaultValue; - children2 = element.textContent; - children2 === defaultValue && "" !== children2 && null !== children2 && (element.value = children2); - } - function setTextContent(node, text) { - if (text) { - var firstChild = node.firstChild; - if (firstChild && firstChild === node.lastChild && 3 === firstChild.nodeType) { - firstChild.nodeValue = text; - return; - } - } - node.textContent = text; - } - var unitlessNumbers = new Set( - "animationIterationCount aspectRatio borderImageOutset borderImageSlice borderImageWidth boxFlex boxFlexGroup boxOrdinalGroup columnCount columns flex flexGrow flexPositive flexShrink flexNegative flexOrder gridArea gridRow gridRowEnd gridRowSpan gridRowStart gridColumn gridColumnEnd gridColumnSpan gridColumnStart fontWeight lineClamp lineHeight opacity order orphans scale tabSize widows zIndex zoom fillOpacity floodOpacity stopOpacity strokeDasharray strokeDashoffset strokeMiterlimit strokeOpacity strokeWidth MozAnimationIterationCount MozBoxFlex MozBoxFlexGroup MozLineClamp msAnimationIterationCount msFlex msZoom msFlexGrow msFlexNegative msFlexOrder msFlexPositive msFlexShrink msGridColumn msGridColumnSpan msGridRow msGridRowSpan WebkitAnimationIterationCount WebkitBoxFlex WebKitBoxFlexGroup WebkitBoxOrdinalGroup WebkitColumnCount WebkitColumns WebkitFlex WebkitFlexGrow WebkitFlexPositive WebkitFlexShrink WebkitLineClamp".split( - " " - ) - ); - function setValueForStyle(style2, styleName, value2) { - var isCustomProperty = 0 === styleName.indexOf("--"); - null == value2 || "boolean" === typeof value2 || "" === value2 ? isCustomProperty ? style2.setProperty(styleName, "") : "float" === styleName ? style2.cssFloat = "" : style2[styleName] = "" : isCustomProperty ? style2.setProperty(styleName, value2) : "number" !== typeof value2 || 0 === value2 || unitlessNumbers.has(styleName) ? "float" === styleName ? style2.cssFloat = value2 : style2[styleName] = ("" + value2).trim() : style2[styleName] = value2 + "px"; - } - function setValueForStyles(node, styles, prevStyles) { - if (null != styles && "object" !== typeof styles) - throw Error(formatProdErrorMessage(62)); - node = node.style; - if (null != prevStyles) { - for (var styleName in prevStyles) - !prevStyles.hasOwnProperty(styleName) || null != styles && styles.hasOwnProperty(styleName) || (0 === styleName.indexOf("--") ? node.setProperty(styleName, "") : "float" === styleName ? node.cssFloat = "" : node[styleName] = ""); - for (var styleName$16 in styles) - styleName = styles[styleName$16], styles.hasOwnProperty(styleName$16) && prevStyles[styleName$16] !== styleName && setValueForStyle(node, styleName$16, styleName); - } else - for (var styleName$17 in styles) - styles.hasOwnProperty(styleName$17) && setValueForStyle(node, styleName$17, styles[styleName$17]); - } - function isCustomElement(tagName) { - if (-1 === tagName.indexOf("-")) return false; - switch (tagName) { - case "annotation-xml": - case "color-profile": - case "font-face": - case "font-face-src": - case "font-face-uri": - case "font-face-format": - case "font-face-name": - case "missing-glyph": - return false; - default: - return true; - } - } - var aliases = /* @__PURE__ */ new Map([ - ["acceptCharset", "accept-charset"], - ["htmlFor", "for"], - ["httpEquiv", "http-equiv"], - ["crossOrigin", "crossorigin"], - ["accentHeight", "accent-height"], - ["alignmentBaseline", "alignment-baseline"], - ["arabicForm", "arabic-form"], - ["baselineShift", "baseline-shift"], - ["capHeight", "cap-height"], - ["clipPath", "clip-path"], - ["clipRule", "clip-rule"], - ["colorInterpolation", "color-interpolation"], - ["colorInterpolationFilters", "color-interpolation-filters"], - ["colorProfile", "color-profile"], - ["colorRendering", "color-rendering"], - ["dominantBaseline", "dominant-baseline"], - ["enableBackground", "enable-background"], - ["fillOpacity", "fill-opacity"], - ["fillRule", "fill-rule"], - ["floodColor", "flood-color"], - ["floodOpacity", "flood-opacity"], - ["fontFamily", "font-family"], - ["fontSize", "font-size"], - ["fontSizeAdjust", "font-size-adjust"], - ["fontStretch", "font-stretch"], - ["fontStyle", "font-style"], - ["fontVariant", "font-variant"], - ["fontWeight", "font-weight"], - ["glyphName", "glyph-name"], - ["glyphOrientationHorizontal", "glyph-orientation-horizontal"], - ["glyphOrientationVertical", "glyph-orientation-vertical"], - ["horizAdvX", "horiz-adv-x"], - ["horizOriginX", "horiz-origin-x"], - ["imageRendering", "image-rendering"], - ["letterSpacing", "letter-spacing"], - ["lightingColor", "lighting-color"], - ["markerEnd", "marker-end"], - ["markerMid", "marker-mid"], - ["markerStart", "marker-start"], - ["overlinePosition", "overline-position"], - ["overlineThickness", "overline-thickness"], - ["paintOrder", "paint-order"], - ["panose-1", "panose-1"], - ["pointerEvents", "pointer-events"], - ["renderingIntent", "rendering-intent"], - ["shapeRendering", "shape-rendering"], - ["stopColor", "stop-color"], - ["stopOpacity", "stop-opacity"], - ["strikethroughPosition", "strikethrough-position"], - ["strikethroughThickness", "strikethrough-thickness"], - ["strokeDasharray", "stroke-dasharray"], - ["strokeDashoffset", "stroke-dashoffset"], - ["strokeLinecap", "stroke-linecap"], - ["strokeLinejoin", "stroke-linejoin"], - ["strokeMiterlimit", "stroke-miterlimit"], - ["strokeOpacity", "stroke-opacity"], - ["strokeWidth", "stroke-width"], - ["textAnchor", "text-anchor"], - ["textDecoration", "text-decoration"], - ["textRendering", "text-rendering"], - ["transformOrigin", "transform-origin"], - ["underlinePosition", "underline-position"], - ["underlineThickness", "underline-thickness"], - ["unicodeBidi", "unicode-bidi"], - ["unicodeRange", "unicode-range"], - ["unitsPerEm", "units-per-em"], - ["vAlphabetic", "v-alphabetic"], - ["vHanging", "v-hanging"], - ["vIdeographic", "v-ideographic"], - ["vMathematical", "v-mathematical"], - ["vectorEffect", "vector-effect"], - ["vertAdvY", "vert-adv-y"], - ["vertOriginX", "vert-origin-x"], - ["vertOriginY", "vert-origin-y"], - ["wordSpacing", "word-spacing"], - ["writingMode", "writing-mode"], - ["xmlnsXlink", "xmlns:xlink"], - ["xHeight", "x-height"] - ]); - var isJavaScriptProtocol = /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i; - function sanitizeURL(url2) { - return isJavaScriptProtocol.test("" + url2) ? "javascript:throw new Error('React has blocked a javascript: URL as a security precaution.')" : url2; - } - var currentReplayingEvent = null; - function getEventTarget(nativeEvent) { - nativeEvent = nativeEvent.target || nativeEvent.srcElement || window; - nativeEvent.correspondingUseElement && (nativeEvent = nativeEvent.correspondingUseElement); - return 3 === nativeEvent.nodeType ? nativeEvent.parentNode : nativeEvent; - } - var restoreTarget = null; - var restoreQueue = null; - function restoreStateOfTarget(target) { - var internalInstance = getInstanceFromNode(target); - if (internalInstance && (target = internalInstance.stateNode)) { - var props = target[internalPropsKey] || null; - a: switch (target = internalInstance.stateNode, internalInstance.type) { - case "input": - updateInput( - target, - props.value, - props.defaultValue, - props.defaultValue, - props.checked, - props.defaultChecked, - props.type, - props.name - ); - internalInstance = props.name; - if ("radio" === props.type && null != internalInstance) { - for (props = target; props.parentNode; ) props = props.parentNode; - props = props.querySelectorAll( - 'input[name="' + escapeSelectorAttributeValueInsideDoubleQuotes( - "" + internalInstance - ) + '"][type="radio"]' - ); - for (internalInstance = 0; internalInstance < props.length; internalInstance++) { - var otherNode = props[internalInstance]; - if (otherNode !== target && otherNode.form === target.form) { - var otherProps = otherNode[internalPropsKey] || null; - if (!otherProps) throw Error(formatProdErrorMessage(90)); - updateInput( - otherNode, - otherProps.value, - otherProps.defaultValue, - otherProps.defaultValue, - otherProps.checked, - otherProps.defaultChecked, - otherProps.type, - otherProps.name - ); - } - } - for (internalInstance = 0; internalInstance < props.length; internalInstance++) - otherNode = props[internalInstance], otherNode.form === target.form && updateValueIfChanged(otherNode); - } - break a; - case "textarea": - updateTextarea(target, props.value, props.defaultValue); - break a; - case "select": - internalInstance = props.value, null != internalInstance && updateOptions(target, !!props.multiple, internalInstance, false); - } - } - } - var isInsideEventHandler = false; - function batchedUpdates$1(fn, a, b) { - if (isInsideEventHandler) return fn(a, b); - isInsideEventHandler = true; - try { - var JSCompiler_inline_result = fn(a); - return JSCompiler_inline_result; - } finally { - if (isInsideEventHandler = false, null !== restoreTarget || null !== restoreQueue) { - if (flushSyncWork$1(), restoreTarget && (a = restoreTarget, fn = restoreQueue, restoreQueue = restoreTarget = null, restoreStateOfTarget(a), fn)) - for (a = 0; a < fn.length; a++) restoreStateOfTarget(fn[a]); - } - } - } - function getListener(inst, registrationName) { - var stateNode = inst.stateNode; - if (null === stateNode) return null; - var props = stateNode[internalPropsKey] || null; - if (null === props) return null; - stateNode = props[registrationName]; - a: switch (registrationName) { - case "onClick": - case "onClickCapture": - case "onDoubleClick": - case "onDoubleClickCapture": - case "onMouseDown": - case "onMouseDownCapture": - case "onMouseMove": - case "onMouseMoveCapture": - case "onMouseUp": - case "onMouseUpCapture": - case "onMouseEnter": - (props = !props.disabled) || (inst = inst.type, props = !("button" === inst || "input" === inst || "select" === inst || "textarea" === inst)); - inst = !props; - break a; - default: - inst = false; - } - if (inst) return null; - if (stateNode && "function" !== typeof stateNode) - throw Error( - formatProdErrorMessage(231, registrationName, typeof stateNode) - ); - return stateNode; - } - var canUseDOM = !("undefined" === typeof window || "undefined" === typeof window.document || "undefined" === typeof window.document.createElement); - var passiveBrowserEventsSupported = false; - if (canUseDOM) - try { - options = {}; - Object.defineProperty(options, "passive", { - get: function() { - passiveBrowserEventsSupported = true; - } - }); - window.addEventListener("test", options, options); - window.removeEventListener("test", options, options); - } catch (e) { - passiveBrowserEventsSupported = false; - } - var options; - var root2 = null; - var startText = null; - var fallbackText = null; - function getData() { - if (fallbackText) return fallbackText; - var start2, startValue = startText, startLength = startValue.length, end, endValue = "value" in root2 ? root2.value : root2.textContent, endLength = endValue.length; - for (start2 = 0; start2 < startLength && startValue[start2] === endValue[start2]; start2++) ; - var minEnd = startLength - start2; - for (end = 1; end <= minEnd && startValue[startLength - end] === endValue[endLength - end]; end++) ; - return fallbackText = endValue.slice(start2, 1 < end ? 1 - end : void 0); - } - function getEventCharCode(nativeEvent) { - var keyCode = nativeEvent.keyCode; - "charCode" in nativeEvent ? (nativeEvent = nativeEvent.charCode, 0 === nativeEvent && 13 === keyCode && (nativeEvent = 13)) : nativeEvent = keyCode; - 10 === nativeEvent && (nativeEvent = 13); - return 32 <= nativeEvent || 13 === nativeEvent ? nativeEvent : 0; - } - function functionThatReturnsTrue() { - return true; - } - function functionThatReturnsFalse() { - return false; - } - function createSyntheticEvent(Interface) { - function SyntheticBaseEvent(reactName, reactEventType, targetInst, nativeEvent, nativeEventTarget) { - this._reactName = reactName; - this._targetInst = targetInst; - this.type = reactEventType; - this.nativeEvent = nativeEvent; - this.target = nativeEventTarget; - this.currentTarget = null; - for (var propName in Interface) - Interface.hasOwnProperty(propName) && (reactName = Interface[propName], this[propName] = reactName ? reactName(nativeEvent) : nativeEvent[propName]); - this.isDefaultPrevented = (null != nativeEvent.defaultPrevented ? nativeEvent.defaultPrevented : false === nativeEvent.returnValue) ? functionThatReturnsTrue : functionThatReturnsFalse; - this.isPropagationStopped = functionThatReturnsFalse; - return this; - } - assign(SyntheticBaseEvent.prototype, { - preventDefault: function() { - this.defaultPrevented = true; - var event = this.nativeEvent; - event && (event.preventDefault ? event.preventDefault() : "unknown" !== typeof event.returnValue && (event.returnValue = false), this.isDefaultPrevented = functionThatReturnsTrue); - }, - stopPropagation: function() { - var event = this.nativeEvent; - event && (event.stopPropagation ? event.stopPropagation() : "unknown" !== typeof event.cancelBubble && (event.cancelBubble = true), this.isPropagationStopped = functionThatReturnsTrue); - }, - persist: function() { - }, - isPersistent: functionThatReturnsTrue - }); - return SyntheticBaseEvent; - } - var EventInterface = { - eventPhase: 0, - bubbles: 0, - cancelable: 0, - timeStamp: function(event) { - return event.timeStamp || Date.now(); - }, - defaultPrevented: 0, - isTrusted: 0 - }; - var SyntheticEvent = createSyntheticEvent(EventInterface); - var UIEventInterface = assign({}, EventInterface, { view: 0, detail: 0 }); - var SyntheticUIEvent = createSyntheticEvent(UIEventInterface); - var lastMovementX; - var lastMovementY; - var lastMouseEvent; - var MouseEventInterface = assign({}, UIEventInterface, { - screenX: 0, - screenY: 0, - clientX: 0, - clientY: 0, - pageX: 0, - pageY: 0, - ctrlKey: 0, - shiftKey: 0, - altKey: 0, - metaKey: 0, - getModifierState: getEventModifierState, - button: 0, - buttons: 0, - relatedTarget: function(event) { - return void 0 === event.relatedTarget ? event.fromElement === event.srcElement ? event.toElement : event.fromElement : event.relatedTarget; - }, - movementX: function(event) { - if ("movementX" in event) return event.movementX; - event !== lastMouseEvent && (lastMouseEvent && "mousemove" === event.type ? (lastMovementX = event.screenX - lastMouseEvent.screenX, lastMovementY = event.screenY - lastMouseEvent.screenY) : lastMovementY = lastMovementX = 0, lastMouseEvent = event); - return lastMovementX; - }, - movementY: function(event) { - return "movementY" in event ? event.movementY : lastMovementY; - } - }); - var SyntheticMouseEvent = createSyntheticEvent(MouseEventInterface); - var DragEventInterface = assign({}, MouseEventInterface, { dataTransfer: 0 }); - var SyntheticDragEvent = createSyntheticEvent(DragEventInterface); - var FocusEventInterface = assign({}, UIEventInterface, { relatedTarget: 0 }); - var SyntheticFocusEvent = createSyntheticEvent(FocusEventInterface); - var AnimationEventInterface = assign({}, EventInterface, { - animationName: 0, - elapsedTime: 0, - pseudoElement: 0 - }); - var SyntheticAnimationEvent = createSyntheticEvent(AnimationEventInterface); - var ClipboardEventInterface = assign({}, EventInterface, { - clipboardData: function(event) { - return "clipboardData" in event ? event.clipboardData : window.clipboardData; - } - }); - var SyntheticClipboardEvent = createSyntheticEvent(ClipboardEventInterface); - var CompositionEventInterface = assign({}, EventInterface, { data: 0 }); - var SyntheticCompositionEvent = createSyntheticEvent(CompositionEventInterface); - var normalizeKey = { - Esc: "Escape", - Spacebar: " ", - Left: "ArrowLeft", - Up: "ArrowUp", - Right: "ArrowRight", - Down: "ArrowDown", - Del: "Delete", - Win: "OS", - Menu: "ContextMenu", - Apps: "ContextMenu", - Scroll: "ScrollLock", - MozPrintableKey: "Unidentified" - }; - var translateToKey = { - 8: "Backspace", - 9: "Tab", - 12: "Clear", - 13: "Enter", - 16: "Shift", - 17: "Control", - 18: "Alt", - 19: "Pause", - 20: "CapsLock", - 27: "Escape", - 32: " ", - 33: "PageUp", - 34: "PageDown", - 35: "End", - 36: "Home", - 37: "ArrowLeft", - 38: "ArrowUp", - 39: "ArrowRight", - 40: "ArrowDown", - 45: "Insert", - 46: "Delete", - 112: "F1", - 113: "F2", - 114: "F3", - 115: "F4", - 116: "F5", - 117: "F6", - 118: "F7", - 119: "F8", - 120: "F9", - 121: "F10", - 122: "F11", - 123: "F12", - 144: "NumLock", - 145: "ScrollLock", - 224: "Meta" - }; - var modifierKeyToProp = { - Alt: "altKey", - Control: "ctrlKey", - Meta: "metaKey", - Shift: "shiftKey" - }; - function modifierStateGetter(keyArg) { - var nativeEvent = this.nativeEvent; - return nativeEvent.getModifierState ? nativeEvent.getModifierState(keyArg) : (keyArg = modifierKeyToProp[keyArg]) ? !!nativeEvent[keyArg] : false; - } - function getEventModifierState() { - return modifierStateGetter; - } - var KeyboardEventInterface = assign({}, UIEventInterface, { - key: function(nativeEvent) { - if (nativeEvent.key) { - var key = normalizeKey[nativeEvent.key] || nativeEvent.key; - if ("Unidentified" !== key) return key; - } - return "keypress" === nativeEvent.type ? (nativeEvent = getEventCharCode(nativeEvent), 13 === nativeEvent ? "Enter" : String.fromCharCode(nativeEvent)) : "keydown" === nativeEvent.type || "keyup" === nativeEvent.type ? translateToKey[nativeEvent.keyCode] || "Unidentified" : ""; - }, - code: 0, - location: 0, - ctrlKey: 0, - shiftKey: 0, - altKey: 0, - metaKey: 0, - repeat: 0, - locale: 0, - getModifierState: getEventModifierState, - charCode: function(event) { - return "keypress" === event.type ? getEventCharCode(event) : 0; - }, - keyCode: function(event) { - return "keydown" === event.type || "keyup" === event.type ? event.keyCode : 0; - }, - which: function(event) { - return "keypress" === event.type ? getEventCharCode(event) : "keydown" === event.type || "keyup" === event.type ? event.keyCode : 0; - } - }); - var SyntheticKeyboardEvent = createSyntheticEvent(KeyboardEventInterface); - var PointerEventInterface = assign({}, MouseEventInterface, { - pointerId: 0, - width: 0, - height: 0, - pressure: 0, - tangentialPressure: 0, - tiltX: 0, - tiltY: 0, - twist: 0, - pointerType: 0, - isPrimary: 0 - }); - var SyntheticPointerEvent = createSyntheticEvent(PointerEventInterface); - var TouchEventInterface = assign({}, UIEventInterface, { - touches: 0, - targetTouches: 0, - changedTouches: 0, - altKey: 0, - metaKey: 0, - ctrlKey: 0, - shiftKey: 0, - getModifierState: getEventModifierState - }); - var SyntheticTouchEvent = createSyntheticEvent(TouchEventInterface); - var TransitionEventInterface = assign({}, EventInterface, { - propertyName: 0, - elapsedTime: 0, - pseudoElement: 0 - }); - var SyntheticTransitionEvent = createSyntheticEvent(TransitionEventInterface); - var WheelEventInterface = assign({}, MouseEventInterface, { - deltaX: function(event) { - return "deltaX" in event ? event.deltaX : "wheelDeltaX" in event ? -event.wheelDeltaX : 0; - }, - deltaY: function(event) { - return "deltaY" in event ? event.deltaY : "wheelDeltaY" in event ? -event.wheelDeltaY : "wheelDelta" in event ? -event.wheelDelta : 0; - }, - deltaZ: 0, - deltaMode: 0 - }); - var SyntheticWheelEvent = createSyntheticEvent(WheelEventInterface); - var ToggleEventInterface = assign({}, EventInterface, { - newState: 0, - oldState: 0 - }); - var SyntheticToggleEvent = createSyntheticEvent(ToggleEventInterface); - var END_KEYCODES = [9, 13, 27, 32]; - var canUseCompositionEvent = canUseDOM && "CompositionEvent" in window; - var documentMode = null; - canUseDOM && "documentMode" in document && (documentMode = document.documentMode); - var canUseTextInputEvent = canUseDOM && "TextEvent" in window && !documentMode; - var useFallbackCompositionData = canUseDOM && (!canUseCompositionEvent || documentMode && 8 < documentMode && 11 >= documentMode); - var SPACEBAR_CHAR = String.fromCharCode(32); - var hasSpaceKeypress = false; - function isFallbackCompositionEnd(domEventName, nativeEvent) { - switch (domEventName) { - case "keyup": - return -1 !== END_KEYCODES.indexOf(nativeEvent.keyCode); - case "keydown": - return 229 !== nativeEvent.keyCode; - case "keypress": - case "mousedown": - case "focusout": - return true; - default: - return false; - } - } - function getDataFromCustomEvent(nativeEvent) { - nativeEvent = nativeEvent.detail; - return "object" === typeof nativeEvent && "data" in nativeEvent ? nativeEvent.data : null; - } - var isComposing = false; - function getNativeBeforeInputChars(domEventName, nativeEvent) { - switch (domEventName) { - case "compositionend": - return getDataFromCustomEvent(nativeEvent); - case "keypress": - if (32 !== nativeEvent.which) return null; - hasSpaceKeypress = true; - return SPACEBAR_CHAR; - case "textInput": - return domEventName = nativeEvent.data, domEventName === SPACEBAR_CHAR && hasSpaceKeypress ? null : domEventName; - default: - return null; - } - } - function getFallbackBeforeInputChars(domEventName, nativeEvent) { - if (isComposing) - return "compositionend" === domEventName || !canUseCompositionEvent && isFallbackCompositionEnd(domEventName, nativeEvent) ? (domEventName = getData(), fallbackText = startText = root2 = null, isComposing = false, domEventName) : null; - switch (domEventName) { - case "paste": - return null; - case "keypress": - if (!(nativeEvent.ctrlKey || nativeEvent.altKey || nativeEvent.metaKey) || nativeEvent.ctrlKey && nativeEvent.altKey) { - if (nativeEvent.char && 1 < nativeEvent.char.length) - return nativeEvent.char; - if (nativeEvent.which) return String.fromCharCode(nativeEvent.which); - } - return null; - case "compositionend": - return useFallbackCompositionData && "ko" !== nativeEvent.locale ? null : nativeEvent.data; - default: - return null; - } - } - var supportedInputTypes = { - color: true, - date: true, - datetime: true, - "datetime-local": true, - email: true, - month: true, - number: true, - password: true, - range: true, - search: true, - tel: true, - text: true, - time: true, - url: true, - week: true - }; - function isTextInputElement(elem) { - var nodeName = elem && elem.nodeName && elem.nodeName.toLowerCase(); - return "input" === nodeName ? !!supportedInputTypes[elem.type] : "textarea" === nodeName ? true : false; - } - function createAndAccumulateChangeEvent(dispatchQueue, inst, nativeEvent, target) { - restoreTarget ? restoreQueue ? restoreQueue.push(target) : restoreQueue = [target] : restoreTarget = target; - inst = accumulateTwoPhaseListeners(inst, "onChange"); - 0 < inst.length && (nativeEvent = new SyntheticEvent( - "onChange", - "change", - null, - nativeEvent, - target - ), dispatchQueue.push({ event: nativeEvent, listeners: inst })); - } - var activeElement$1 = null; - var activeElementInst$1 = null; - function runEventInBatch(dispatchQueue) { - processDispatchQueue(dispatchQueue, 0); - } - function getInstIfValueChanged(targetInst) { - var targetNode = getNodeFromInstance(targetInst); - if (updateValueIfChanged(targetNode)) return targetInst; - } - function getTargetInstForChangeEvent(domEventName, targetInst) { - if ("change" === domEventName) return targetInst; - } - var isInputEventSupported = false; - if (canUseDOM) { - if (canUseDOM) { - isSupported$jscomp$inline_417 = "oninput" in document; - if (!isSupported$jscomp$inline_417) { - element$jscomp$inline_418 = document.createElement("div"); - element$jscomp$inline_418.setAttribute("oninput", "return;"); - isSupported$jscomp$inline_417 = "function" === typeof element$jscomp$inline_418.oninput; - } - JSCompiler_inline_result$jscomp$282 = isSupported$jscomp$inline_417; - } else JSCompiler_inline_result$jscomp$282 = false; - isInputEventSupported = JSCompiler_inline_result$jscomp$282 && (!document.documentMode || 9 < document.documentMode); - } - var JSCompiler_inline_result$jscomp$282; - var isSupported$jscomp$inline_417; - var element$jscomp$inline_418; - function stopWatchingForValueChange() { - activeElement$1 && (activeElement$1.detachEvent("onpropertychange", handlePropertyChange), activeElementInst$1 = activeElement$1 = null); - } - function handlePropertyChange(nativeEvent) { - if ("value" === nativeEvent.propertyName && getInstIfValueChanged(activeElementInst$1)) { - var dispatchQueue = []; - createAndAccumulateChangeEvent( - dispatchQueue, - activeElementInst$1, - nativeEvent, - getEventTarget(nativeEvent) - ); - batchedUpdates$1(runEventInBatch, dispatchQueue); - } - } - function handleEventsForInputEventPolyfill(domEventName, target, targetInst) { - "focusin" === domEventName ? (stopWatchingForValueChange(), activeElement$1 = target, activeElementInst$1 = targetInst, activeElement$1.attachEvent("onpropertychange", handlePropertyChange)) : "focusout" === domEventName && stopWatchingForValueChange(); - } - function getTargetInstForInputEventPolyfill(domEventName) { - if ("selectionchange" === domEventName || "keyup" === domEventName || "keydown" === domEventName) - return getInstIfValueChanged(activeElementInst$1); - } - function getTargetInstForClickEvent(domEventName, targetInst) { - if ("click" === domEventName) return getInstIfValueChanged(targetInst); - } - function getTargetInstForInputOrChangeEvent(domEventName, targetInst) { - if ("input" === domEventName || "change" === domEventName) - return getInstIfValueChanged(targetInst); - } - function is(x2, y2) { - return x2 === y2 && (0 !== x2 || 1 / x2 === 1 / y2) || x2 !== x2 && y2 !== y2; - } - var objectIs = "function" === typeof Object.is ? Object.is : is; - function shallowEqual(objA, objB) { - if (objectIs(objA, objB)) return true; - if ("object" !== typeof objA || null === objA || "object" !== typeof objB || null === objB) - return false; - var keysA = Object.keys(objA), keysB = Object.keys(objB); - if (keysA.length !== keysB.length) return false; - for (keysB = 0; keysB < keysA.length; keysB++) { - var currentKey = keysA[keysB]; - if (!hasOwnProperty.call(objB, currentKey) || !objectIs(objA[currentKey], objB[currentKey])) - return false; - } - return true; - } - function getLeafNode(node) { - for (; node && node.firstChild; ) node = node.firstChild; - return node; - } - function getNodeForCharacterOffset(root3, offset) { - var node = getLeafNode(root3); - root3 = 0; - for (var nodeEnd; node; ) { - if (3 === node.nodeType) { - nodeEnd = root3 + node.textContent.length; - if (root3 <= offset && nodeEnd >= offset) - return { node, offset: offset - root3 }; - root3 = nodeEnd; - } - a: { - for (; node; ) { - if (node.nextSibling) { - node = node.nextSibling; - break a; - } - node = node.parentNode; - } - node = void 0; - } - node = getLeafNode(node); - } - } - function containsNode(outerNode, innerNode) { - return outerNode && innerNode ? outerNode === innerNode ? true : outerNode && 3 === outerNode.nodeType ? false : innerNode && 3 === innerNode.nodeType ? containsNode(outerNode, innerNode.parentNode) : "contains" in outerNode ? outerNode.contains(innerNode) : outerNode.compareDocumentPosition ? !!(outerNode.compareDocumentPosition(innerNode) & 16) : false : false; - } - function getActiveElementDeep(containerInfo) { - containerInfo = null != containerInfo && null != containerInfo.ownerDocument && null != containerInfo.ownerDocument.defaultView ? containerInfo.ownerDocument.defaultView : window; - for (var element = getActiveElement(containerInfo.document); element instanceof containerInfo.HTMLIFrameElement; ) { - try { - var JSCompiler_inline_result = "string" === typeof element.contentWindow.location.href; - } catch (err) { - JSCompiler_inline_result = false; - } - if (JSCompiler_inline_result) containerInfo = element.contentWindow; - else break; - element = getActiveElement(containerInfo.document); - } - return element; - } - function hasSelectionCapabilities(elem) { - var nodeName = elem && elem.nodeName && elem.nodeName.toLowerCase(); - return nodeName && ("input" === nodeName && ("text" === elem.type || "search" === elem.type || "tel" === elem.type || "url" === elem.type || "password" === elem.type) || "textarea" === nodeName || "true" === elem.contentEditable); - } - var skipSelectionChangeEvent = canUseDOM && "documentMode" in document && 11 >= document.documentMode; - var activeElement = null; - var activeElementInst = null; - var lastSelection = null; - var mouseDown = false; - function constructSelectEvent(dispatchQueue, nativeEvent, nativeEventTarget) { - var doc = nativeEventTarget.window === nativeEventTarget ? nativeEventTarget.document : 9 === nativeEventTarget.nodeType ? nativeEventTarget : nativeEventTarget.ownerDocument; - mouseDown || null == activeElement || activeElement !== getActiveElement(doc) || (doc = activeElement, "selectionStart" in doc && hasSelectionCapabilities(doc) ? doc = { start: doc.selectionStart, end: doc.selectionEnd } : (doc = (doc.ownerDocument && doc.ownerDocument.defaultView || window).getSelection(), doc = { - anchorNode: doc.anchorNode, - anchorOffset: doc.anchorOffset, - focusNode: doc.focusNode, - focusOffset: doc.focusOffset - }), lastSelection && shallowEqual(lastSelection, doc) || (lastSelection = doc, doc = accumulateTwoPhaseListeners(activeElementInst, "onSelect"), 0 < doc.length && (nativeEvent = new SyntheticEvent( - "onSelect", - "select", - null, - nativeEvent, - nativeEventTarget - ), dispatchQueue.push({ event: nativeEvent, listeners: doc }), nativeEvent.target = activeElement))); - } - function makePrefixMap(styleProp, eventName) { - var prefixes2 = {}; - prefixes2[styleProp.toLowerCase()] = eventName.toLowerCase(); - prefixes2["Webkit" + styleProp] = "webkit" + eventName; - prefixes2["Moz" + styleProp] = "moz" + eventName; - return prefixes2; - } - var vendorPrefixes = { - animationend: makePrefixMap("Animation", "AnimationEnd"), - animationiteration: makePrefixMap("Animation", "AnimationIteration"), - animationstart: makePrefixMap("Animation", "AnimationStart"), - transitionrun: makePrefixMap("Transition", "TransitionRun"), - transitionstart: makePrefixMap("Transition", "TransitionStart"), - transitioncancel: makePrefixMap("Transition", "TransitionCancel"), - transitionend: makePrefixMap("Transition", "TransitionEnd") - }; - var prefixedEventNames = {}; - var style = {}; - canUseDOM && (style = document.createElement("div").style, "AnimationEvent" in window || (delete vendorPrefixes.animationend.animation, delete vendorPrefixes.animationiteration.animation, delete vendorPrefixes.animationstart.animation), "TransitionEvent" in window || delete vendorPrefixes.transitionend.transition); - function getVendorPrefixedEventName(eventName) { - if (prefixedEventNames[eventName]) return prefixedEventNames[eventName]; - if (!vendorPrefixes[eventName]) return eventName; - var prefixMap = vendorPrefixes[eventName], styleProp; - for (styleProp in prefixMap) - if (prefixMap.hasOwnProperty(styleProp) && styleProp in style) - return prefixedEventNames[eventName] = prefixMap[styleProp]; - return eventName; - } - var ANIMATION_END = getVendorPrefixedEventName("animationend"); - var ANIMATION_ITERATION = getVendorPrefixedEventName("animationiteration"); - var ANIMATION_START = getVendorPrefixedEventName("animationstart"); - var TRANSITION_RUN = getVendorPrefixedEventName("transitionrun"); - var TRANSITION_START = getVendorPrefixedEventName("transitionstart"); - var TRANSITION_CANCEL = getVendorPrefixedEventName("transitioncancel"); - var TRANSITION_END = getVendorPrefixedEventName("transitionend"); - var topLevelEventsToReactNames = /* @__PURE__ */ new Map(); - var simpleEventPluginEvents = "abort auxClick beforeToggle cancel canPlay canPlayThrough click close contextMenu copy cut drag dragEnd dragEnter dragExit dragLeave dragOver dragStart drop durationChange emptied encrypted ended error gotPointerCapture input invalid keyDown keyPress keyUp load loadedData loadedMetadata loadStart lostPointerCapture mouseDown mouseMove mouseOut mouseOver mouseUp paste pause play playing pointerCancel pointerDown pointerMove pointerOut pointerOver pointerUp progress rateChange reset resize seeked seeking stalled submit suspend timeUpdate touchCancel touchEnd touchStart volumeChange scroll toggle touchMove waiting wheel".split( - " " - ); - simpleEventPluginEvents.push("scrollEnd"); - function registerSimpleEvent(domEventName, reactName) { - topLevelEventsToReactNames.set(domEventName, reactName); - registerTwoPhaseEvent(reactName, [domEventName]); - } - var CapturedStacks = /* @__PURE__ */ new WeakMap(); - function createCapturedValueAtFiber(value2, source) { - if ("object" === typeof value2 && null !== value2) { - var existing = CapturedStacks.get(value2); - if (void 0 !== existing) return existing; - source = { - value: value2, - source, - stack: getStackByFiberInDevAndProd(source) - }; - CapturedStacks.set(value2, source); - return source; - } - return { - value: value2, - source, - stack: getStackByFiberInDevAndProd(source) - }; - } - var concurrentQueues = []; - var concurrentQueuesIndex = 0; - var concurrentlyUpdatedLanes = 0; - function finishQueueingConcurrentUpdates() { - for (var endIndex = concurrentQueuesIndex, i = concurrentlyUpdatedLanes = concurrentQueuesIndex = 0; i < endIndex; ) { - var fiber = concurrentQueues[i]; - concurrentQueues[i++] = null; - var queue = concurrentQueues[i]; - concurrentQueues[i++] = null; - var update = concurrentQueues[i]; - concurrentQueues[i++] = null; - var lane = concurrentQueues[i]; - concurrentQueues[i++] = null; - if (null !== queue && null !== update) { - var pending = queue.pending; - null === pending ? update.next = update : (update.next = pending.next, pending.next = update); - queue.pending = update; - } - 0 !== lane && markUpdateLaneFromFiberToRoot(fiber, update, lane); - } - } - function enqueueUpdate$1(fiber, queue, update, lane) { - concurrentQueues[concurrentQueuesIndex++] = fiber; - concurrentQueues[concurrentQueuesIndex++] = queue; - concurrentQueues[concurrentQueuesIndex++] = update; - concurrentQueues[concurrentQueuesIndex++] = lane; - concurrentlyUpdatedLanes |= lane; - fiber.lanes |= lane; - fiber = fiber.alternate; - null !== fiber && (fiber.lanes |= lane); - } - function enqueueConcurrentHookUpdate(fiber, queue, update, lane) { - enqueueUpdate$1(fiber, queue, update, lane); - return getRootForUpdatedFiber(fiber); - } - function enqueueConcurrentRenderForLane(fiber, lane) { - enqueueUpdate$1(fiber, null, null, lane); - return getRootForUpdatedFiber(fiber); - } - function markUpdateLaneFromFiberToRoot(sourceFiber, update, lane) { - sourceFiber.lanes |= lane; - var alternate = sourceFiber.alternate; - null !== alternate && (alternate.lanes |= lane); - for (var isHidden = false, parent = sourceFiber.return; null !== parent; ) - parent.childLanes |= lane, alternate = parent.alternate, null !== alternate && (alternate.childLanes |= lane), 22 === parent.tag && (sourceFiber = parent.stateNode, null === sourceFiber || sourceFiber._visibility & 1 || (isHidden = true)), sourceFiber = parent, parent = parent.return; - return 3 === sourceFiber.tag ? (parent = sourceFiber.stateNode, isHidden && null !== update && (isHidden = 31 - clz32(lane), sourceFiber = parent.hiddenUpdates, alternate = sourceFiber[isHidden], null === alternate ? sourceFiber[isHidden] = [update] : alternate.push(update), update.lane = lane | 536870912), parent) : null; - } - function getRootForUpdatedFiber(sourceFiber) { - if (50 < nestedUpdateCount) - throw nestedUpdateCount = 0, rootWithNestedUpdates = null, Error(formatProdErrorMessage(185)); - for (var parent = sourceFiber.return; null !== parent; ) - sourceFiber = parent, parent = sourceFiber.return; - return 3 === sourceFiber.tag ? sourceFiber.stateNode : null; - } - var emptyContextObject = {}; - function FiberNode(tag, pendingProps, key, mode) { - this.tag = tag; - this.key = key; - this.sibling = this.child = this.return = this.stateNode = this.type = this.elementType = null; - this.index = 0; - this.refCleanup = this.ref = null; - this.pendingProps = pendingProps; - this.dependencies = this.memoizedState = this.updateQueue = this.memoizedProps = null; - this.mode = mode; - this.subtreeFlags = this.flags = 0; - this.deletions = null; - this.childLanes = this.lanes = 0; - this.alternate = null; - } - function createFiberImplClass(tag, pendingProps, key, mode) { - return new FiberNode(tag, pendingProps, key, mode); - } - function shouldConstruct(Component) { - Component = Component.prototype; - return !(!Component || !Component.isReactComponent); - } - function createWorkInProgress(current, pendingProps) { - var workInProgress2 = current.alternate; - null === workInProgress2 ? (workInProgress2 = createFiberImplClass( - current.tag, - pendingProps, - current.key, - current.mode - ), workInProgress2.elementType = current.elementType, workInProgress2.type = current.type, workInProgress2.stateNode = current.stateNode, workInProgress2.alternate = current, current.alternate = workInProgress2) : (workInProgress2.pendingProps = pendingProps, workInProgress2.type = current.type, workInProgress2.flags = 0, workInProgress2.subtreeFlags = 0, workInProgress2.deletions = null); - workInProgress2.flags = current.flags & 65011712; - workInProgress2.childLanes = current.childLanes; - workInProgress2.lanes = current.lanes; - workInProgress2.child = current.child; - workInProgress2.memoizedProps = current.memoizedProps; - workInProgress2.memoizedState = current.memoizedState; - workInProgress2.updateQueue = current.updateQueue; - pendingProps = current.dependencies; - workInProgress2.dependencies = null === pendingProps ? null : { lanes: pendingProps.lanes, firstContext: pendingProps.firstContext }; - workInProgress2.sibling = current.sibling; - workInProgress2.index = current.index; - workInProgress2.ref = current.ref; - workInProgress2.refCleanup = current.refCleanup; - return workInProgress2; - } - function resetWorkInProgress(workInProgress2, renderLanes2) { - workInProgress2.flags &= 65011714; - var current = workInProgress2.alternate; - null === current ? (workInProgress2.childLanes = 0, workInProgress2.lanes = renderLanes2, workInProgress2.child = null, workInProgress2.subtreeFlags = 0, workInProgress2.memoizedProps = null, workInProgress2.memoizedState = null, workInProgress2.updateQueue = null, workInProgress2.dependencies = null, workInProgress2.stateNode = null) : (workInProgress2.childLanes = current.childLanes, workInProgress2.lanes = current.lanes, workInProgress2.child = current.child, workInProgress2.subtreeFlags = 0, workInProgress2.deletions = null, workInProgress2.memoizedProps = current.memoizedProps, workInProgress2.memoizedState = current.memoizedState, workInProgress2.updateQueue = current.updateQueue, workInProgress2.type = current.type, renderLanes2 = current.dependencies, workInProgress2.dependencies = null === renderLanes2 ? null : { - lanes: renderLanes2.lanes, - firstContext: renderLanes2.firstContext - }); - return workInProgress2; - } - function createFiberFromTypeAndProps(type2, key, pendingProps, owner, mode, lanes) { - var fiberTag = 0; - owner = type2; - if ("function" === typeof type2) shouldConstruct(type2) && (fiberTag = 1); - else if ("string" === typeof type2) - fiberTag = isHostHoistableType( - type2, - pendingProps, - contextStackCursor.current - ) ? 26 : "html" === type2 || "head" === type2 || "body" === type2 ? 27 : 5; - else - a: switch (type2) { - case REACT_ACTIVITY_TYPE: - return type2 = createFiberImplClass(31, pendingProps, key, mode), type2.elementType = REACT_ACTIVITY_TYPE, type2.lanes = lanes, type2; - case REACT_FRAGMENT_TYPE: - return createFiberFromFragment(pendingProps.children, mode, lanes, key); - case REACT_STRICT_MODE_TYPE: - fiberTag = 8; - mode |= 24; - break; - case REACT_PROFILER_TYPE: - return type2 = createFiberImplClass(12, pendingProps, key, mode | 2), type2.elementType = REACT_PROFILER_TYPE, type2.lanes = lanes, type2; - case REACT_SUSPENSE_TYPE: - return type2 = createFiberImplClass(13, pendingProps, key, mode), type2.elementType = REACT_SUSPENSE_TYPE, type2.lanes = lanes, type2; - case REACT_SUSPENSE_LIST_TYPE: - return type2 = createFiberImplClass(19, pendingProps, key, mode), type2.elementType = REACT_SUSPENSE_LIST_TYPE, type2.lanes = lanes, type2; - default: - if ("object" === typeof type2 && null !== type2) - switch (type2.$$typeof) { - case REACT_PROVIDER_TYPE: - case REACT_CONTEXT_TYPE: - fiberTag = 10; - break a; - case REACT_CONSUMER_TYPE: - fiberTag = 9; - break a; - case REACT_FORWARD_REF_TYPE: - fiberTag = 11; - break a; - case REACT_MEMO_TYPE: - fiberTag = 14; - break a; - case REACT_LAZY_TYPE: - fiberTag = 16; - owner = null; - break a; - } - fiberTag = 29; - pendingProps = Error( - formatProdErrorMessage(130, null === type2 ? "null" : typeof type2, "") - ); - owner = null; - } - key = createFiberImplClass(fiberTag, pendingProps, key, mode); - key.elementType = type2; - key.type = owner; - key.lanes = lanes; - return key; - } - function createFiberFromFragment(elements, mode, lanes, key) { - elements = createFiberImplClass(7, elements, key, mode); - elements.lanes = lanes; - return elements; - } - function createFiberFromText(content, mode, lanes) { - content = createFiberImplClass(6, content, null, mode); - content.lanes = lanes; - return content; - } - function createFiberFromPortal(portal, mode, lanes) { - mode = createFiberImplClass( - 4, - null !== portal.children ? portal.children : [], - portal.key, - mode - ); - mode.lanes = lanes; - mode.stateNode = { - containerInfo: portal.containerInfo, - pendingChildren: null, - implementation: portal.implementation - }; - return mode; - } - var forkStack = []; - var forkStackIndex = 0; - var treeForkProvider = null; - var treeForkCount = 0; - var idStack = []; - var idStackIndex = 0; - var treeContextProvider = null; - var treeContextId = 1; - var treeContextOverflow = ""; - function pushTreeFork(workInProgress2, totalChildren) { - forkStack[forkStackIndex++] = treeForkCount; - forkStack[forkStackIndex++] = treeForkProvider; - treeForkProvider = workInProgress2; - treeForkCount = totalChildren; - } - function pushTreeId(workInProgress2, totalChildren, index2) { - idStack[idStackIndex++] = treeContextId; - idStack[idStackIndex++] = treeContextOverflow; - idStack[idStackIndex++] = treeContextProvider; - treeContextProvider = workInProgress2; - var baseIdWithLeadingBit = treeContextId; - workInProgress2 = treeContextOverflow; - var baseLength = 32 - clz32(baseIdWithLeadingBit) - 1; - baseIdWithLeadingBit &= ~(1 << baseLength); - index2 += 1; - var length = 32 - clz32(totalChildren) + baseLength; - if (30 < length) { - var numberOfOverflowBits = baseLength - baseLength % 5; - length = (baseIdWithLeadingBit & (1 << numberOfOverflowBits) - 1).toString(32); - baseIdWithLeadingBit >>= numberOfOverflowBits; - baseLength -= numberOfOverflowBits; - treeContextId = 1 << 32 - clz32(totalChildren) + baseLength | index2 << baseLength | baseIdWithLeadingBit; - treeContextOverflow = length + workInProgress2; - } else - treeContextId = 1 << length | index2 << baseLength | baseIdWithLeadingBit, treeContextOverflow = workInProgress2; - } - function pushMaterializedTreeId(workInProgress2) { - null !== workInProgress2.return && (pushTreeFork(workInProgress2, 1), pushTreeId(workInProgress2, 1, 0)); - } - function popTreeContext(workInProgress2) { - for (; workInProgress2 === treeForkProvider; ) - treeForkProvider = forkStack[--forkStackIndex], forkStack[forkStackIndex] = null, treeForkCount = forkStack[--forkStackIndex], forkStack[forkStackIndex] = null; - for (; workInProgress2 === treeContextProvider; ) - treeContextProvider = idStack[--idStackIndex], idStack[idStackIndex] = null, treeContextOverflow = idStack[--idStackIndex], idStack[idStackIndex] = null, treeContextId = idStack[--idStackIndex], idStack[idStackIndex] = null; - } - var hydrationParentFiber = null; - var nextHydratableInstance = null; - var isHydrating = false; - var hydrationErrors = null; - var rootOrSingletonContext = false; - var HydrationMismatchException = Error(formatProdErrorMessage(519)); - function throwOnHydrationMismatch(fiber) { - var error = Error(formatProdErrorMessage(418, "")); - queueHydrationError(createCapturedValueAtFiber(error, fiber)); - throw HydrationMismatchException; - } - function prepareToHydrateHostInstance(fiber) { - var instance = fiber.stateNode, type2 = fiber.type, props = fiber.memoizedProps; - instance[internalInstanceKey] = fiber; - instance[internalPropsKey] = props; - switch (type2) { - case "dialog": - listenToNonDelegatedEvent("cancel", instance); - listenToNonDelegatedEvent("close", instance); - break; - case "iframe": - case "object": - case "embed": - listenToNonDelegatedEvent("load", instance); - break; - case "video": - case "audio": - for (type2 = 0; type2 < mediaEventTypes.length; type2++) - listenToNonDelegatedEvent(mediaEventTypes[type2], instance); - break; - case "source": - listenToNonDelegatedEvent("error", instance); - break; - case "img": - case "image": - case "link": - listenToNonDelegatedEvent("error", instance); - listenToNonDelegatedEvent("load", instance); - break; - case "details": - listenToNonDelegatedEvent("toggle", instance); - break; - case "input": - listenToNonDelegatedEvent("invalid", instance); - initInput( - instance, - props.value, - props.defaultValue, - props.checked, - props.defaultChecked, - props.type, - props.name, - true - ); - track(instance); - break; - case "select": - listenToNonDelegatedEvent("invalid", instance); - break; - case "textarea": - listenToNonDelegatedEvent("invalid", instance), initTextarea(instance, props.value, props.defaultValue, props.children), track(instance); - } - type2 = props.children; - "string" !== typeof type2 && "number" !== typeof type2 && "bigint" !== typeof type2 || instance.textContent === "" + type2 || true === props.suppressHydrationWarning || checkForUnmatchedText(instance.textContent, type2) ? (null != props.popover && (listenToNonDelegatedEvent("beforetoggle", instance), listenToNonDelegatedEvent("toggle", instance)), null != props.onScroll && listenToNonDelegatedEvent("scroll", instance), null != props.onScrollEnd && listenToNonDelegatedEvent("scrollend", instance), null != props.onClick && (instance.onclick = noop$1), instance = true) : instance = false; - instance || throwOnHydrationMismatch(fiber); - } - function popToNextHostParent(fiber) { - for (hydrationParentFiber = fiber.return; hydrationParentFiber; ) - switch (hydrationParentFiber.tag) { - case 5: - case 13: - rootOrSingletonContext = false; - return; - case 27: - case 3: - rootOrSingletonContext = true; - return; - default: - hydrationParentFiber = hydrationParentFiber.return; - } - } - function popHydrationState(fiber) { - if (fiber !== hydrationParentFiber) return false; - if (!isHydrating) return popToNextHostParent(fiber), isHydrating = true, false; - var tag = fiber.tag, JSCompiler_temp; - if (JSCompiler_temp = 3 !== tag && 27 !== tag) { - if (JSCompiler_temp = 5 === tag) - JSCompiler_temp = fiber.type, JSCompiler_temp = !("form" !== JSCompiler_temp && "button" !== JSCompiler_temp) || shouldSetTextContent(fiber.type, fiber.memoizedProps); - JSCompiler_temp = !JSCompiler_temp; - } - JSCompiler_temp && nextHydratableInstance && throwOnHydrationMismatch(fiber); - popToNextHostParent(fiber); - if (13 === tag) { - fiber = fiber.memoizedState; - fiber = null !== fiber ? fiber.dehydrated : null; - if (!fiber) throw Error(formatProdErrorMessage(317)); - a: { - fiber = fiber.nextSibling; - for (tag = 0; fiber; ) { - if (8 === fiber.nodeType) - if (JSCompiler_temp = fiber.data, "/$" === JSCompiler_temp) { - if (0 === tag) { - nextHydratableInstance = getNextHydratable(fiber.nextSibling); - break a; - } - tag--; - } else - "$" !== JSCompiler_temp && "$!" !== JSCompiler_temp && "$?" !== JSCompiler_temp || tag++; - fiber = fiber.nextSibling; - } - nextHydratableInstance = null; - } - } else - 27 === tag ? (tag = nextHydratableInstance, isSingletonScope(fiber.type) ? (fiber = previousHydratableOnEnteringScopedSingleton, previousHydratableOnEnteringScopedSingleton = null, nextHydratableInstance = fiber) : nextHydratableInstance = tag) : nextHydratableInstance = hydrationParentFiber ? getNextHydratable(fiber.stateNode.nextSibling) : null; - return true; - } - function resetHydrationState() { - nextHydratableInstance = hydrationParentFiber = null; - isHydrating = false; - } - function upgradeHydrationErrorsToRecoverable() { - var queuedErrors = hydrationErrors; - null !== queuedErrors && (null === workInProgressRootRecoverableErrors ? workInProgressRootRecoverableErrors = queuedErrors : workInProgressRootRecoverableErrors.push.apply( - workInProgressRootRecoverableErrors, - queuedErrors - ), hydrationErrors = null); - return queuedErrors; - } - function queueHydrationError(error) { - null === hydrationErrors ? hydrationErrors = [error] : hydrationErrors.push(error); - } - var valueCursor = createCursor(null); - var currentlyRenderingFiber$1 = null; - var lastContextDependency = null; - function pushProvider(providerFiber, context, nextValue) { - push(valueCursor, context._currentValue); - context._currentValue = nextValue; - } - function popProvider(context) { - context._currentValue = valueCursor.current; - pop(valueCursor); - } - function scheduleContextWorkOnParentPath(parent, renderLanes2, propagationRoot) { - for (; null !== parent; ) { - var alternate = parent.alternate; - (parent.childLanes & renderLanes2) !== renderLanes2 ? (parent.childLanes |= renderLanes2, null !== alternate && (alternate.childLanes |= renderLanes2)) : null !== alternate && (alternate.childLanes & renderLanes2) !== renderLanes2 && (alternate.childLanes |= renderLanes2); - if (parent === propagationRoot) break; - parent = parent.return; - } - } - function propagateContextChanges(workInProgress2, contexts, renderLanes2, forcePropagateEntireTree) { - var fiber = workInProgress2.child; - null !== fiber && (fiber.return = workInProgress2); - for (; null !== fiber; ) { - var list = fiber.dependencies; - if (null !== list) { - var nextFiber = fiber.child; - list = list.firstContext; - a: for (; null !== list; ) { - var dependency = list; - list = fiber; - for (var i = 0; i < contexts.length; i++) - if (dependency.context === contexts[i]) { - list.lanes |= renderLanes2; - dependency = list.alternate; - null !== dependency && (dependency.lanes |= renderLanes2); - scheduleContextWorkOnParentPath( - list.return, - renderLanes2, - workInProgress2 - ); - forcePropagateEntireTree || (nextFiber = null); - break a; - } - list = dependency.next; - } - } else if (18 === fiber.tag) { - nextFiber = fiber.return; - if (null === nextFiber) throw Error(formatProdErrorMessage(341)); - nextFiber.lanes |= renderLanes2; - list = nextFiber.alternate; - null !== list && (list.lanes |= renderLanes2); - scheduleContextWorkOnParentPath(nextFiber, renderLanes2, workInProgress2); - nextFiber = null; - } else nextFiber = fiber.child; - if (null !== nextFiber) nextFiber.return = fiber; - else - for (nextFiber = fiber; null !== nextFiber; ) { - if (nextFiber === workInProgress2) { - nextFiber = null; - break; - } - fiber = nextFiber.sibling; - if (null !== fiber) { - fiber.return = nextFiber.return; - nextFiber = fiber; - break; - } - nextFiber = nextFiber.return; - } - fiber = nextFiber; - } - } - function propagateParentContextChanges(current, workInProgress2, renderLanes2, forcePropagateEntireTree) { - current = null; - for (var parent = workInProgress2, isInsidePropagationBailout = false; null !== parent; ) { - if (!isInsidePropagationBailout) { - if (0 !== (parent.flags & 524288)) isInsidePropagationBailout = true; - else if (0 !== (parent.flags & 262144)) break; - } - if (10 === parent.tag) { - var currentParent = parent.alternate; - if (null === currentParent) throw Error(formatProdErrorMessage(387)); - currentParent = currentParent.memoizedProps; - if (null !== currentParent) { - var context = parent.type; - objectIs(parent.pendingProps.value, currentParent.value) || (null !== current ? current.push(context) : current = [context]); - } - } else if (parent === hostTransitionProviderCursor.current) { - currentParent = parent.alternate; - if (null === currentParent) throw Error(formatProdErrorMessage(387)); - currentParent.memoizedState.memoizedState !== parent.memoizedState.memoizedState && (null !== current ? current.push(HostTransitionContext) : current = [HostTransitionContext]); - } - parent = parent.return; - } - null !== current && propagateContextChanges( - workInProgress2, - current, - renderLanes2, - forcePropagateEntireTree - ); - workInProgress2.flags |= 262144; - } - function checkIfContextChanged(currentDependencies) { - for (currentDependencies = currentDependencies.firstContext; null !== currentDependencies; ) { - if (!objectIs( - currentDependencies.context._currentValue, - currentDependencies.memoizedValue - )) - return true; - currentDependencies = currentDependencies.next; - } - return false; - } - function prepareToReadContext(workInProgress2) { - currentlyRenderingFiber$1 = workInProgress2; - lastContextDependency = null; - workInProgress2 = workInProgress2.dependencies; - null !== workInProgress2 && (workInProgress2.firstContext = null); - } - function readContext(context) { - return readContextForConsumer(currentlyRenderingFiber$1, context); - } - function readContextDuringReconciliation(consumer, context) { - null === currentlyRenderingFiber$1 && prepareToReadContext(consumer); - return readContextForConsumer(consumer, context); - } - function readContextForConsumer(consumer, context) { - var value2 = context._currentValue; - context = { context, memoizedValue: value2, next: null }; - if (null === lastContextDependency) { - if (null === consumer) throw Error(formatProdErrorMessage(308)); - lastContextDependency = context; - consumer.dependencies = { lanes: 0, firstContext: context }; - consumer.flags |= 524288; - } else lastContextDependency = lastContextDependency.next = context; - return value2; - } - var AbortControllerLocal = "undefined" !== typeof AbortController ? AbortController : function() { - var listeners = [], signal = this.signal = { - aborted: false, - addEventListener: function(type2, listener) { - listeners.push(listener); - } - }; - this.abort = function() { - signal.aborted = true; - listeners.forEach(function(listener) { - return listener(); - }); - }; - }; - var scheduleCallback$2 = Scheduler.unstable_scheduleCallback; - var NormalPriority = Scheduler.unstable_NormalPriority; - var CacheContext = { - $$typeof: REACT_CONTEXT_TYPE, - Consumer: null, - Provider: null, - _currentValue: null, - _currentValue2: null, - _threadCount: 0 - }; - function createCache() { - return { - controller: new AbortControllerLocal(), - data: /* @__PURE__ */ new Map(), - refCount: 0 - }; - } - function releaseCache(cache2) { - cache2.refCount--; - 0 === cache2.refCount && scheduleCallback$2(NormalPriority, function() { - cache2.controller.abort(); - }); - } - var currentEntangledListeners = null; - var currentEntangledPendingCount = 0; - var currentEntangledLane = 0; - var currentEntangledActionThenable = null; - function entangleAsyncAction(transition2, thenable) { - if (null === currentEntangledListeners) { - var entangledListeners = currentEntangledListeners = []; - currentEntangledPendingCount = 0; - currentEntangledLane = requestTransitionLane(); - currentEntangledActionThenable = { - status: "pending", - value: void 0, - then: function(resolve) { - entangledListeners.push(resolve); - } - }; - } - currentEntangledPendingCount++; - thenable.then(pingEngtangledActionScope, pingEngtangledActionScope); - return thenable; - } - function pingEngtangledActionScope() { - if (0 === --currentEntangledPendingCount && null !== currentEntangledListeners) { - null !== currentEntangledActionThenable && (currentEntangledActionThenable.status = "fulfilled"); - var listeners = currentEntangledListeners; - currentEntangledListeners = null; - currentEntangledLane = 0; - currentEntangledActionThenable = null; - for (var i = 0; i < listeners.length; i++) (0, listeners[i])(); - } - } - function chainThenableValue(thenable, result) { - var listeners = [], thenableWithOverride = { - status: "pending", - value: null, - reason: null, - then: function(resolve) { - listeners.push(resolve); - } - }; - thenable.then( - function() { - thenableWithOverride.status = "fulfilled"; - thenableWithOverride.value = result; - for (var i = 0; i < listeners.length; i++) (0, listeners[i])(result); - }, - function(error) { - thenableWithOverride.status = "rejected"; - thenableWithOverride.reason = error; - for (error = 0; error < listeners.length; error++) - (0, listeners[error])(void 0); - } - ); - return thenableWithOverride; - } - var prevOnStartTransitionFinish = ReactSharedInternals.S; - ReactSharedInternals.S = function(transition2, returnValue) { - "object" === typeof returnValue && null !== returnValue && "function" === typeof returnValue.then && entangleAsyncAction(transition2, returnValue); - null !== prevOnStartTransitionFinish && prevOnStartTransitionFinish(transition2, returnValue); - }; - var resumedCache = createCursor(null); - function peekCacheFromPool() { - var cacheResumedFromPreviousRender = resumedCache.current; - return null !== cacheResumedFromPreviousRender ? cacheResumedFromPreviousRender : workInProgressRoot.pooledCache; - } - function pushTransition(offscreenWorkInProgress, prevCachePool) { - null === prevCachePool ? push(resumedCache, resumedCache.current) : push(resumedCache, prevCachePool.pool); - } - function getSuspendedCache() { - var cacheFromPool = peekCacheFromPool(); - return null === cacheFromPool ? null : { parent: CacheContext._currentValue, pool: cacheFromPool }; - } - var SuspenseException = Error(formatProdErrorMessage(460)); - var SuspenseyCommitException = Error(formatProdErrorMessage(474)); - var SuspenseActionException = Error(formatProdErrorMessage(542)); - var noopSuspenseyCommitThenable = { then: function() { - } }; - function isThenableResolved(thenable) { - thenable = thenable.status; - return "fulfilled" === thenable || "rejected" === thenable; - } - function noop$3() { - } - function trackUsedThenable(thenableState2, thenable, index2) { - index2 = thenableState2[index2]; - void 0 === index2 ? thenableState2.push(thenable) : index2 !== thenable && (thenable.then(noop$3, noop$3), thenable = index2); - switch (thenable.status) { - case "fulfilled": - return thenable.value; - case "rejected": - throw thenableState2 = thenable.reason, checkIfUseWrappedInAsyncCatch(thenableState2), thenableState2; - default: - if ("string" === typeof thenable.status) thenable.then(noop$3, noop$3); - else { - thenableState2 = workInProgressRoot; - if (null !== thenableState2 && 100 < thenableState2.shellSuspendCounter) - throw Error(formatProdErrorMessage(482)); - thenableState2 = thenable; - thenableState2.status = "pending"; - thenableState2.then( - function(fulfilledValue) { - if ("pending" === thenable.status) { - var fulfilledThenable = thenable; - fulfilledThenable.status = "fulfilled"; - fulfilledThenable.value = fulfilledValue; - } - }, - function(error) { - if ("pending" === thenable.status) { - var rejectedThenable = thenable; - rejectedThenable.status = "rejected"; - rejectedThenable.reason = error; - } - } - ); - } - switch (thenable.status) { - case "fulfilled": - return thenable.value; - case "rejected": - throw thenableState2 = thenable.reason, checkIfUseWrappedInAsyncCatch(thenableState2), thenableState2; - } - suspendedThenable = thenable; - throw SuspenseException; - } - } - var suspendedThenable = null; - function getSuspendedThenable() { - if (null === suspendedThenable) throw Error(formatProdErrorMessage(459)); - var thenable = suspendedThenable; - suspendedThenable = null; - return thenable; - } - function checkIfUseWrappedInAsyncCatch(rejectedReason) { - if (rejectedReason === SuspenseException || rejectedReason === SuspenseActionException) - throw Error(formatProdErrorMessage(483)); - } - var hasForceUpdate = false; - function initializeUpdateQueue(fiber) { - fiber.updateQueue = { - baseState: fiber.memoizedState, - firstBaseUpdate: null, - lastBaseUpdate: null, - shared: { pending: null, lanes: 0, hiddenCallbacks: null }, - callbacks: null - }; - } - function cloneUpdateQueue(current, workInProgress2) { - current = current.updateQueue; - workInProgress2.updateQueue === current && (workInProgress2.updateQueue = { - baseState: current.baseState, - firstBaseUpdate: current.firstBaseUpdate, - lastBaseUpdate: current.lastBaseUpdate, - shared: current.shared, - callbacks: null - }); - } - function createUpdate(lane) { - return { lane, tag: 0, payload: null, callback: null, next: null }; - } - function enqueueUpdate(fiber, update, lane) { - var updateQueue = fiber.updateQueue; - if (null === updateQueue) return null; - updateQueue = updateQueue.shared; - if (0 !== (executionContext & 2)) { - var pending = updateQueue.pending; - null === pending ? update.next = update : (update.next = pending.next, pending.next = update); - updateQueue.pending = update; - update = getRootForUpdatedFiber(fiber); - markUpdateLaneFromFiberToRoot(fiber, null, lane); - return update; - } - enqueueUpdate$1(fiber, updateQueue, update, lane); - return getRootForUpdatedFiber(fiber); - } - function entangleTransitions(root3, fiber, lane) { - fiber = fiber.updateQueue; - if (null !== fiber && (fiber = fiber.shared, 0 !== (lane & 4194048))) { - var queueLanes = fiber.lanes; - queueLanes &= root3.pendingLanes; - lane |= queueLanes; - fiber.lanes = lane; - markRootEntangled(root3, lane); - } - } - function enqueueCapturedUpdate(workInProgress2, capturedUpdate) { - var queue = workInProgress2.updateQueue, current = workInProgress2.alternate; - if (null !== current && (current = current.updateQueue, queue === current)) { - var newFirst = null, newLast = null; - queue = queue.firstBaseUpdate; - if (null !== queue) { - do { - var clone = { - lane: queue.lane, - tag: queue.tag, - payload: queue.payload, - callback: null, - next: null - }; - null === newLast ? newFirst = newLast = clone : newLast = newLast.next = clone; - queue = queue.next; - } while (null !== queue); - null === newLast ? newFirst = newLast = capturedUpdate : newLast = newLast.next = capturedUpdate; - } else newFirst = newLast = capturedUpdate; - queue = { - baseState: current.baseState, - firstBaseUpdate: newFirst, - lastBaseUpdate: newLast, - shared: current.shared, - callbacks: current.callbacks - }; - workInProgress2.updateQueue = queue; - return; - } - workInProgress2 = queue.lastBaseUpdate; - null === workInProgress2 ? queue.firstBaseUpdate = capturedUpdate : workInProgress2.next = capturedUpdate; - queue.lastBaseUpdate = capturedUpdate; - } - var didReadFromEntangledAsyncAction = false; - function suspendIfUpdateReadFromEntangledAsyncAction() { - if (didReadFromEntangledAsyncAction) { - var entangledActionThenable = currentEntangledActionThenable; - if (null !== entangledActionThenable) throw entangledActionThenable; - } - } - function processUpdateQueue(workInProgress$jscomp$0, props, instance$jscomp$0, renderLanes2) { - didReadFromEntangledAsyncAction = false; - var queue = workInProgress$jscomp$0.updateQueue; - hasForceUpdate = false; - var firstBaseUpdate = queue.firstBaseUpdate, lastBaseUpdate = queue.lastBaseUpdate, pendingQueue = queue.shared.pending; - if (null !== pendingQueue) { - queue.shared.pending = null; - var lastPendingUpdate = pendingQueue, firstPendingUpdate = lastPendingUpdate.next; - lastPendingUpdate.next = null; - null === lastBaseUpdate ? firstBaseUpdate = firstPendingUpdate : lastBaseUpdate.next = firstPendingUpdate; - lastBaseUpdate = lastPendingUpdate; - var current = workInProgress$jscomp$0.alternate; - null !== current && (current = current.updateQueue, pendingQueue = current.lastBaseUpdate, pendingQueue !== lastBaseUpdate && (null === pendingQueue ? current.firstBaseUpdate = firstPendingUpdate : pendingQueue.next = firstPendingUpdate, current.lastBaseUpdate = lastPendingUpdate)); - } - if (null !== firstBaseUpdate) { - var newState = queue.baseState; - lastBaseUpdate = 0; - current = firstPendingUpdate = lastPendingUpdate = null; - pendingQueue = firstBaseUpdate; - do { - var updateLane = pendingQueue.lane & -536870913, isHiddenUpdate = updateLane !== pendingQueue.lane; - if (isHiddenUpdate ? (workInProgressRootRenderLanes & updateLane) === updateLane : (renderLanes2 & updateLane) === updateLane) { - 0 !== updateLane && updateLane === currentEntangledLane && (didReadFromEntangledAsyncAction = true); - null !== current && (current = current.next = { - lane: 0, - tag: pendingQueue.tag, - payload: pendingQueue.payload, - callback: null, - next: null - }); - a: { - var workInProgress2 = workInProgress$jscomp$0, update = pendingQueue; - updateLane = props; - var instance = instance$jscomp$0; - switch (update.tag) { - case 1: - workInProgress2 = update.payload; - if ("function" === typeof workInProgress2) { - newState = workInProgress2.call(instance, newState, updateLane); - break a; - } - newState = workInProgress2; - break a; - case 3: - workInProgress2.flags = workInProgress2.flags & -65537 | 128; - case 0: - workInProgress2 = update.payload; - updateLane = "function" === typeof workInProgress2 ? workInProgress2.call(instance, newState, updateLane) : workInProgress2; - if (null === updateLane || void 0 === updateLane) break a; - newState = assign({}, newState, updateLane); - break a; - case 2: - hasForceUpdate = true; - } - } - updateLane = pendingQueue.callback; - null !== updateLane && (workInProgress$jscomp$0.flags |= 64, isHiddenUpdate && (workInProgress$jscomp$0.flags |= 8192), isHiddenUpdate = queue.callbacks, null === isHiddenUpdate ? queue.callbacks = [updateLane] : isHiddenUpdate.push(updateLane)); - } else - isHiddenUpdate = { - lane: updateLane, - tag: pendingQueue.tag, - payload: pendingQueue.payload, - callback: pendingQueue.callback, - next: null - }, null === current ? (firstPendingUpdate = current = isHiddenUpdate, lastPendingUpdate = newState) : current = current.next = isHiddenUpdate, lastBaseUpdate |= updateLane; - pendingQueue = pendingQueue.next; - if (null === pendingQueue) - if (pendingQueue = queue.shared.pending, null === pendingQueue) - break; - else - isHiddenUpdate = pendingQueue, pendingQueue = isHiddenUpdate.next, isHiddenUpdate.next = null, queue.lastBaseUpdate = isHiddenUpdate, queue.shared.pending = null; - } while (1); - null === current && (lastPendingUpdate = newState); - queue.baseState = lastPendingUpdate; - queue.firstBaseUpdate = firstPendingUpdate; - queue.lastBaseUpdate = current; - null === firstBaseUpdate && (queue.shared.lanes = 0); - workInProgressRootSkippedLanes |= lastBaseUpdate; - workInProgress$jscomp$0.lanes = lastBaseUpdate; - workInProgress$jscomp$0.memoizedState = newState; - } - } - function callCallback(callback, context) { - if ("function" !== typeof callback) - throw Error(formatProdErrorMessage(191, callback)); - callback.call(context); - } - function commitCallbacks(updateQueue, context) { - var callbacks = updateQueue.callbacks; - if (null !== callbacks) - for (updateQueue.callbacks = null, updateQueue = 0; updateQueue < callbacks.length; updateQueue++) - callCallback(callbacks[updateQueue], context); - } - var currentTreeHiddenStackCursor = createCursor(null); - var prevEntangledRenderLanesCursor = createCursor(0); - function pushHiddenContext(fiber, context) { - fiber = entangledRenderLanes; - push(prevEntangledRenderLanesCursor, fiber); - push(currentTreeHiddenStackCursor, context); - entangledRenderLanes = fiber | context.baseLanes; - } - function reuseHiddenContextOnStack() { - push(prevEntangledRenderLanesCursor, entangledRenderLanes); - push(currentTreeHiddenStackCursor, currentTreeHiddenStackCursor.current); - } - function popHiddenContext() { - entangledRenderLanes = prevEntangledRenderLanesCursor.current; - pop(currentTreeHiddenStackCursor); - pop(prevEntangledRenderLanesCursor); - } - var renderLanes = 0; - var currentlyRenderingFiber = null; - var currentHook = null; - var workInProgressHook = null; - var didScheduleRenderPhaseUpdate = false; - var didScheduleRenderPhaseUpdateDuringThisPass = false; - var shouldDoubleInvokeUserFnsInHooksDEV = false; - var localIdCounter = 0; - var thenableIndexCounter$1 = 0; - var thenableState$1 = null; - var globalClientIdCounter = 0; - function throwInvalidHookError() { - throw Error(formatProdErrorMessage(321)); - } - function areHookInputsEqual(nextDeps, prevDeps) { - if (null === prevDeps) return false; - for (var i = 0; i < prevDeps.length && i < nextDeps.length; i++) - if (!objectIs(nextDeps[i], prevDeps[i])) return false; - return true; - } - function renderWithHooks(current, workInProgress2, Component, props, secondArg, nextRenderLanes) { - renderLanes = nextRenderLanes; - currentlyRenderingFiber = workInProgress2; - workInProgress2.memoizedState = null; - workInProgress2.updateQueue = null; - workInProgress2.lanes = 0; - ReactSharedInternals.H = null === current || null === current.memoizedState ? HooksDispatcherOnMount : HooksDispatcherOnUpdate; - shouldDoubleInvokeUserFnsInHooksDEV = false; - nextRenderLanes = Component(props, secondArg); - shouldDoubleInvokeUserFnsInHooksDEV = false; - didScheduleRenderPhaseUpdateDuringThisPass && (nextRenderLanes = renderWithHooksAgain( - workInProgress2, - Component, - props, - secondArg - )); - finishRenderingHooks(current); - return nextRenderLanes; - } - function finishRenderingHooks(current) { - ReactSharedInternals.H = ContextOnlyDispatcher; - var didRenderTooFewHooks = null !== currentHook && null !== currentHook.next; - renderLanes = 0; - workInProgressHook = currentHook = currentlyRenderingFiber = null; - didScheduleRenderPhaseUpdate = false; - thenableIndexCounter$1 = 0; - thenableState$1 = null; - if (didRenderTooFewHooks) throw Error(formatProdErrorMessage(300)); - null === current || didReceiveUpdate || (current = current.dependencies, null !== current && checkIfContextChanged(current) && (didReceiveUpdate = true)); - } - function renderWithHooksAgain(workInProgress2, Component, props, secondArg) { - currentlyRenderingFiber = workInProgress2; - var numberOfReRenders = 0; - do { - didScheduleRenderPhaseUpdateDuringThisPass && (thenableState$1 = null); - thenableIndexCounter$1 = 0; - didScheduleRenderPhaseUpdateDuringThisPass = false; - if (25 <= numberOfReRenders) throw Error(formatProdErrorMessage(301)); - numberOfReRenders += 1; - workInProgressHook = currentHook = null; - if (null != workInProgress2.updateQueue) { - var children2 = workInProgress2.updateQueue; - children2.lastEffect = null; - children2.events = null; - children2.stores = null; - null != children2.memoCache && (children2.memoCache.index = 0); - } - ReactSharedInternals.H = HooksDispatcherOnRerender; - children2 = Component(props, secondArg); - } while (didScheduleRenderPhaseUpdateDuringThisPass); - return children2; - } - function TransitionAwareHostComponent() { - var dispatcher = ReactSharedInternals.H, maybeThenable = dispatcher.useState()[0]; - maybeThenable = "function" === typeof maybeThenable.then ? useThenable(maybeThenable) : maybeThenable; - dispatcher = dispatcher.useState()[0]; - (null !== currentHook ? currentHook.memoizedState : null) !== dispatcher && (currentlyRenderingFiber.flags |= 1024); - return maybeThenable; - } - function checkDidRenderIdHook() { - var didRenderIdHook = 0 !== localIdCounter; - localIdCounter = 0; - return didRenderIdHook; - } - function bailoutHooks(current, workInProgress2, lanes) { - workInProgress2.updateQueue = current.updateQueue; - workInProgress2.flags &= -2053; - current.lanes &= ~lanes; - } - function resetHooksOnUnwind(workInProgress2) { - if (didScheduleRenderPhaseUpdate) { - for (workInProgress2 = workInProgress2.memoizedState; null !== workInProgress2; ) { - var queue = workInProgress2.queue; - null !== queue && (queue.pending = null); - workInProgress2 = workInProgress2.next; - } - didScheduleRenderPhaseUpdate = false; - } - renderLanes = 0; - workInProgressHook = currentHook = currentlyRenderingFiber = null; - didScheduleRenderPhaseUpdateDuringThisPass = false; - thenableIndexCounter$1 = localIdCounter = 0; - thenableState$1 = null; - } - function mountWorkInProgressHook() { - var hook = { - memoizedState: null, - baseState: null, - baseQueue: null, - queue: null, - next: null - }; - null === workInProgressHook ? currentlyRenderingFiber.memoizedState = workInProgressHook = hook : workInProgressHook = workInProgressHook.next = hook; - return workInProgressHook; - } - function updateWorkInProgressHook() { - if (null === currentHook) { - var nextCurrentHook = currentlyRenderingFiber.alternate; - nextCurrentHook = null !== nextCurrentHook ? nextCurrentHook.memoizedState : null; - } else nextCurrentHook = currentHook.next; - var nextWorkInProgressHook = null === workInProgressHook ? currentlyRenderingFiber.memoizedState : workInProgressHook.next; - if (null !== nextWorkInProgressHook) - workInProgressHook = nextWorkInProgressHook, currentHook = nextCurrentHook; - else { - if (null === nextCurrentHook) { - if (null === currentlyRenderingFiber.alternate) - throw Error(formatProdErrorMessage(467)); - throw Error(formatProdErrorMessage(310)); - } - currentHook = nextCurrentHook; - nextCurrentHook = { - memoizedState: currentHook.memoizedState, - baseState: currentHook.baseState, - baseQueue: currentHook.baseQueue, - queue: currentHook.queue, - next: null - }; - null === workInProgressHook ? currentlyRenderingFiber.memoizedState = workInProgressHook = nextCurrentHook : workInProgressHook = workInProgressHook.next = nextCurrentHook; - } - return workInProgressHook; - } - function createFunctionComponentUpdateQueue() { - return { lastEffect: null, events: null, stores: null, memoCache: null }; - } - function useThenable(thenable) { - var index2 = thenableIndexCounter$1; - thenableIndexCounter$1 += 1; - null === thenableState$1 && (thenableState$1 = []); - thenable = trackUsedThenable(thenableState$1, thenable, index2); - index2 = currentlyRenderingFiber; - null === (null === workInProgressHook ? index2.memoizedState : workInProgressHook.next) && (index2 = index2.alternate, ReactSharedInternals.H = null === index2 || null === index2.memoizedState ? HooksDispatcherOnMount : HooksDispatcherOnUpdate); - return thenable; - } - function use(usable) { - if (null !== usable && "object" === typeof usable) { - if ("function" === typeof usable.then) return useThenable(usable); - if (usable.$$typeof === REACT_CONTEXT_TYPE) return readContext(usable); - } - throw Error(formatProdErrorMessage(438, String(usable))); - } - function useMemoCache(size) { - var memoCache = null, updateQueue = currentlyRenderingFiber.updateQueue; - null !== updateQueue && (memoCache = updateQueue.memoCache); - if (null == memoCache) { - var current = currentlyRenderingFiber.alternate; - null !== current && (current = current.updateQueue, null !== current && (current = current.memoCache, null != current && (memoCache = { - data: current.data.map(function(array2) { - return array2.slice(); - }), - index: 0 - }))); - } - null == memoCache && (memoCache = { data: [], index: 0 }); - null === updateQueue && (updateQueue = createFunctionComponentUpdateQueue(), currentlyRenderingFiber.updateQueue = updateQueue); - updateQueue.memoCache = memoCache; - updateQueue = memoCache.data[memoCache.index]; - if (void 0 === updateQueue) - for (updateQueue = memoCache.data[memoCache.index] = Array(size), current = 0; current < size; current++) - updateQueue[current] = REACT_MEMO_CACHE_SENTINEL; - memoCache.index++; - return updateQueue; - } - function basicStateReducer(state, action) { - return "function" === typeof action ? action(state) : action; - } - function updateReducer(reducer) { - var hook = updateWorkInProgressHook(); - return updateReducerImpl(hook, currentHook, reducer); - } - function updateReducerImpl(hook, current, reducer) { - var queue = hook.queue; - if (null === queue) throw Error(formatProdErrorMessage(311)); - queue.lastRenderedReducer = reducer; - var baseQueue = hook.baseQueue, pendingQueue = queue.pending; - if (null !== pendingQueue) { - if (null !== baseQueue) { - var baseFirst = baseQueue.next; - baseQueue.next = pendingQueue.next; - pendingQueue.next = baseFirst; - } - current.baseQueue = baseQueue = pendingQueue; - queue.pending = null; - } - pendingQueue = hook.baseState; - if (null === baseQueue) hook.memoizedState = pendingQueue; - else { - current = baseQueue.next; - var newBaseQueueFirst = baseFirst = null, newBaseQueueLast = null, update = current, didReadFromEntangledAsyncAction$32 = false; - do { - var updateLane = update.lane & -536870913; - if (updateLane !== update.lane ? (workInProgressRootRenderLanes & updateLane) === updateLane : (renderLanes & updateLane) === updateLane) { - var revertLane = update.revertLane; - if (0 === revertLane) - null !== newBaseQueueLast && (newBaseQueueLast = newBaseQueueLast.next = { - lane: 0, - revertLane: 0, - action: update.action, - hasEagerState: update.hasEagerState, - eagerState: update.eagerState, - next: null - }), updateLane === currentEntangledLane && (didReadFromEntangledAsyncAction$32 = true); - else if ((renderLanes & revertLane) === revertLane) { - update = update.next; - revertLane === currentEntangledLane && (didReadFromEntangledAsyncAction$32 = true); - continue; - } else - updateLane = { - lane: 0, - revertLane: update.revertLane, - action: update.action, - hasEagerState: update.hasEagerState, - eagerState: update.eagerState, - next: null - }, null === newBaseQueueLast ? (newBaseQueueFirst = newBaseQueueLast = updateLane, baseFirst = pendingQueue) : newBaseQueueLast = newBaseQueueLast.next = updateLane, currentlyRenderingFiber.lanes |= revertLane, workInProgressRootSkippedLanes |= revertLane; - updateLane = update.action; - shouldDoubleInvokeUserFnsInHooksDEV && reducer(pendingQueue, updateLane); - pendingQueue = update.hasEagerState ? update.eagerState : reducer(pendingQueue, updateLane); - } else - revertLane = { - lane: updateLane, - revertLane: update.revertLane, - action: update.action, - hasEagerState: update.hasEagerState, - eagerState: update.eagerState, - next: null - }, null === newBaseQueueLast ? (newBaseQueueFirst = newBaseQueueLast = revertLane, baseFirst = pendingQueue) : newBaseQueueLast = newBaseQueueLast.next = revertLane, currentlyRenderingFiber.lanes |= updateLane, workInProgressRootSkippedLanes |= updateLane; - update = update.next; - } while (null !== update && update !== current); - null === newBaseQueueLast ? baseFirst = pendingQueue : newBaseQueueLast.next = newBaseQueueFirst; - if (!objectIs(pendingQueue, hook.memoizedState) && (didReceiveUpdate = true, didReadFromEntangledAsyncAction$32 && (reducer = currentEntangledActionThenable, null !== reducer))) - throw reducer; - hook.memoizedState = pendingQueue; - hook.baseState = baseFirst; - hook.baseQueue = newBaseQueueLast; - queue.lastRenderedState = pendingQueue; - } - null === baseQueue && (queue.lanes = 0); - return [hook.memoizedState, queue.dispatch]; - } - function rerenderReducer(reducer) { - var hook = updateWorkInProgressHook(), queue = hook.queue; - if (null === queue) throw Error(formatProdErrorMessage(311)); - queue.lastRenderedReducer = reducer; - var dispatch2 = queue.dispatch, lastRenderPhaseUpdate = queue.pending, newState = hook.memoizedState; - if (null !== lastRenderPhaseUpdate) { - queue.pending = null; - var update = lastRenderPhaseUpdate = lastRenderPhaseUpdate.next; - do - newState = reducer(newState, update.action), update = update.next; - while (update !== lastRenderPhaseUpdate); - objectIs(newState, hook.memoizedState) || (didReceiveUpdate = true); - hook.memoizedState = newState; - null === hook.baseQueue && (hook.baseState = newState); - queue.lastRenderedState = newState; - } - return [newState, dispatch2]; - } - function updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) { - var fiber = currentlyRenderingFiber, hook = updateWorkInProgressHook(), isHydrating$jscomp$0 = isHydrating; - if (isHydrating$jscomp$0) { - if (void 0 === getServerSnapshot) throw Error(formatProdErrorMessage(407)); - getServerSnapshot = getServerSnapshot(); - } else getServerSnapshot = getSnapshot(); - var snapshotChanged = !objectIs( - (currentHook || hook).memoizedState, - getServerSnapshot - ); - snapshotChanged && (hook.memoizedState = getServerSnapshot, didReceiveUpdate = true); - hook = hook.queue; - var create2 = subscribeToStore.bind(null, fiber, hook, subscribe); - updateEffectImpl(2048, 8, create2, [subscribe]); - if (hook.getSnapshot !== getSnapshot || snapshotChanged || null !== workInProgressHook && workInProgressHook.memoizedState.tag & 1) { - fiber.flags |= 2048; - pushSimpleEffect( - 9, - createEffectInstance(), - updateStoreInstance.bind( - null, - fiber, - hook, - getServerSnapshot, - getSnapshot - ), - null - ); - if (null === workInProgressRoot) throw Error(formatProdErrorMessage(349)); - isHydrating$jscomp$0 || 0 !== (renderLanes & 124) || pushStoreConsistencyCheck(fiber, getSnapshot, getServerSnapshot); - } - return getServerSnapshot; - } - function pushStoreConsistencyCheck(fiber, getSnapshot, renderedSnapshot) { - fiber.flags |= 16384; - fiber = { getSnapshot, value: renderedSnapshot }; - getSnapshot = currentlyRenderingFiber.updateQueue; - null === getSnapshot ? (getSnapshot = createFunctionComponentUpdateQueue(), currentlyRenderingFiber.updateQueue = getSnapshot, getSnapshot.stores = [fiber]) : (renderedSnapshot = getSnapshot.stores, null === renderedSnapshot ? getSnapshot.stores = [fiber] : renderedSnapshot.push(fiber)); - } - function updateStoreInstance(fiber, inst, nextSnapshot, getSnapshot) { - inst.value = nextSnapshot; - inst.getSnapshot = getSnapshot; - checkIfSnapshotChanged(inst) && forceStoreRerender(fiber); - } - function subscribeToStore(fiber, inst, subscribe) { - return subscribe(function() { - checkIfSnapshotChanged(inst) && forceStoreRerender(fiber); - }); - } - function checkIfSnapshotChanged(inst) { - var latestGetSnapshot = inst.getSnapshot; - inst = inst.value; - try { - var nextValue = latestGetSnapshot(); - return !objectIs(inst, nextValue); - } catch (error) { - return true; - } - } - function forceStoreRerender(fiber) { - var root3 = enqueueConcurrentRenderForLane(fiber, 2); - null !== root3 && scheduleUpdateOnFiber(root3, fiber, 2); - } - function mountStateImpl(initialState) { - var hook = mountWorkInProgressHook(); - if ("function" === typeof initialState) { - var initialStateInitializer = initialState; - initialState = initialStateInitializer(); - if (shouldDoubleInvokeUserFnsInHooksDEV) { - setIsStrictModeForDevtools(true); - try { - initialStateInitializer(); - } finally { - setIsStrictModeForDevtools(false); - } - } - } - hook.memoizedState = hook.baseState = initialState; - hook.queue = { - pending: null, - lanes: 0, - dispatch: null, - lastRenderedReducer: basicStateReducer, - lastRenderedState: initialState - }; - return hook; - } - function updateOptimisticImpl(hook, current, passthrough, reducer) { - hook.baseState = passthrough; - return updateReducerImpl( - hook, - currentHook, - "function" === typeof reducer ? reducer : basicStateReducer - ); - } - function dispatchActionState(fiber, actionQueue, setPendingState, setState, payload) { - if (isRenderPhaseUpdate(fiber)) throw Error(formatProdErrorMessage(485)); - fiber = actionQueue.action; - if (null !== fiber) { - var actionNode = { - payload, - action: fiber, - next: null, - isTransition: true, - status: "pending", - value: null, - reason: null, - listeners: [], - then: function(listener) { - actionNode.listeners.push(listener); - } - }; - null !== ReactSharedInternals.T ? setPendingState(true) : actionNode.isTransition = false; - setState(actionNode); - setPendingState = actionQueue.pending; - null === setPendingState ? (actionNode.next = actionQueue.pending = actionNode, runActionStateAction(actionQueue, actionNode)) : (actionNode.next = setPendingState.next, actionQueue.pending = setPendingState.next = actionNode); - } - } - function runActionStateAction(actionQueue, node) { - var action = node.action, payload = node.payload, prevState = actionQueue.state; - if (node.isTransition) { - var prevTransition = ReactSharedInternals.T, currentTransition = {}; - ReactSharedInternals.T = currentTransition; - try { - var returnValue = action(prevState, payload), onStartTransitionFinish = ReactSharedInternals.S; - null !== onStartTransitionFinish && onStartTransitionFinish(currentTransition, returnValue); - handleActionReturnValue(actionQueue, node, returnValue); - } catch (error) { - onActionError(actionQueue, node, error); - } finally { - ReactSharedInternals.T = prevTransition; - } - } else - try { - prevTransition = action(prevState, payload), handleActionReturnValue(actionQueue, node, prevTransition); - } catch (error$38) { - onActionError(actionQueue, node, error$38); - } - } - function handleActionReturnValue(actionQueue, node, returnValue) { - null !== returnValue && "object" === typeof returnValue && "function" === typeof returnValue.then ? returnValue.then( - function(nextState) { - onActionSuccess(actionQueue, node, nextState); - }, - function(error) { - return onActionError(actionQueue, node, error); - } - ) : onActionSuccess(actionQueue, node, returnValue); - } - function onActionSuccess(actionQueue, actionNode, nextState) { - actionNode.status = "fulfilled"; - actionNode.value = nextState; - notifyActionListeners(actionNode); - actionQueue.state = nextState; - actionNode = actionQueue.pending; - null !== actionNode && (nextState = actionNode.next, nextState === actionNode ? actionQueue.pending = null : (nextState = nextState.next, actionNode.next = nextState, runActionStateAction(actionQueue, nextState))); - } - function onActionError(actionQueue, actionNode, error) { - var last = actionQueue.pending; - actionQueue.pending = null; - if (null !== last) { - last = last.next; - do - actionNode.status = "rejected", actionNode.reason = error, notifyActionListeners(actionNode), actionNode = actionNode.next; - while (actionNode !== last); - } - actionQueue.action = null; - } - function notifyActionListeners(actionNode) { - actionNode = actionNode.listeners; - for (var i = 0; i < actionNode.length; i++) (0, actionNode[i])(); - } - function actionStateReducer(oldState, newState) { - return newState; - } - function mountActionState(action, initialStateProp) { - if (isHydrating) { - var ssrFormState = workInProgressRoot.formState; - if (null !== ssrFormState) { - a: { - var JSCompiler_inline_result = currentlyRenderingFiber; - if (isHydrating) { - if (nextHydratableInstance) { - b: { - var JSCompiler_inline_result$jscomp$0 = nextHydratableInstance; - for (var inRootOrSingleton = rootOrSingletonContext; 8 !== JSCompiler_inline_result$jscomp$0.nodeType; ) { - if (!inRootOrSingleton) { - JSCompiler_inline_result$jscomp$0 = null; - break b; - } - JSCompiler_inline_result$jscomp$0 = getNextHydratable( - JSCompiler_inline_result$jscomp$0.nextSibling - ); - if (null === JSCompiler_inline_result$jscomp$0) { - JSCompiler_inline_result$jscomp$0 = null; - break b; - } - } - inRootOrSingleton = JSCompiler_inline_result$jscomp$0.data; - JSCompiler_inline_result$jscomp$0 = "F!" === inRootOrSingleton || "F" === inRootOrSingleton ? JSCompiler_inline_result$jscomp$0 : null; - } - if (JSCompiler_inline_result$jscomp$0) { - nextHydratableInstance = getNextHydratable( - JSCompiler_inline_result$jscomp$0.nextSibling - ); - JSCompiler_inline_result = "F!" === JSCompiler_inline_result$jscomp$0.data; - break a; - } - } - throwOnHydrationMismatch(JSCompiler_inline_result); - } - JSCompiler_inline_result = false; - } - JSCompiler_inline_result && (initialStateProp = ssrFormState[0]); - } - } - ssrFormState = mountWorkInProgressHook(); - ssrFormState.memoizedState = ssrFormState.baseState = initialStateProp; - JSCompiler_inline_result = { - pending: null, - lanes: 0, - dispatch: null, - lastRenderedReducer: actionStateReducer, - lastRenderedState: initialStateProp - }; - ssrFormState.queue = JSCompiler_inline_result; - ssrFormState = dispatchSetState.bind( - null, - currentlyRenderingFiber, - JSCompiler_inline_result - ); - JSCompiler_inline_result.dispatch = ssrFormState; - JSCompiler_inline_result = mountStateImpl(false); - inRootOrSingleton = dispatchOptimisticSetState.bind( - null, - currentlyRenderingFiber, - false, - JSCompiler_inline_result.queue - ); - JSCompiler_inline_result = mountWorkInProgressHook(); - JSCompiler_inline_result$jscomp$0 = { - state: initialStateProp, - dispatch: null, - action, - pending: null - }; - JSCompiler_inline_result.queue = JSCompiler_inline_result$jscomp$0; - ssrFormState = dispatchActionState.bind( - null, - currentlyRenderingFiber, - JSCompiler_inline_result$jscomp$0, - inRootOrSingleton, - ssrFormState - ); - JSCompiler_inline_result$jscomp$0.dispatch = ssrFormState; - JSCompiler_inline_result.memoizedState = action; - return [initialStateProp, ssrFormState, false]; - } - function updateActionState(action) { - var stateHook = updateWorkInProgressHook(); - return updateActionStateImpl(stateHook, currentHook, action); - } - function updateActionStateImpl(stateHook, currentStateHook, action) { - currentStateHook = updateReducerImpl( - stateHook, - currentStateHook, - actionStateReducer - )[0]; - stateHook = updateReducer(basicStateReducer)[0]; - if ("object" === typeof currentStateHook && null !== currentStateHook && "function" === typeof currentStateHook.then) - try { - var state = useThenable(currentStateHook); - } catch (x2) { - if (x2 === SuspenseException) throw SuspenseActionException; - throw x2; - } - else state = currentStateHook; - currentStateHook = updateWorkInProgressHook(); - var actionQueue = currentStateHook.queue, dispatch2 = actionQueue.dispatch; - action !== currentStateHook.memoizedState && (currentlyRenderingFiber.flags |= 2048, pushSimpleEffect( - 9, - createEffectInstance(), - actionStateActionEffect.bind(null, actionQueue, action), - null - )); - return [state, dispatch2, stateHook]; - } - function actionStateActionEffect(actionQueue, action) { - actionQueue.action = action; - } - function rerenderActionState(action) { - var stateHook = updateWorkInProgressHook(), currentStateHook = currentHook; - if (null !== currentStateHook) - return updateActionStateImpl(stateHook, currentStateHook, action); - updateWorkInProgressHook(); - stateHook = stateHook.memoizedState; - currentStateHook = updateWorkInProgressHook(); - var dispatch2 = currentStateHook.queue.dispatch; - currentStateHook.memoizedState = action; - return [stateHook, dispatch2, false]; - } - function pushSimpleEffect(tag, inst, create2, createDeps) { - tag = { tag, create: create2, deps: createDeps, inst, next: null }; - inst = currentlyRenderingFiber.updateQueue; - null === inst && (inst = createFunctionComponentUpdateQueue(), currentlyRenderingFiber.updateQueue = inst); - create2 = inst.lastEffect; - null === create2 ? inst.lastEffect = tag.next = tag : (createDeps = create2.next, create2.next = tag, tag.next = createDeps, inst.lastEffect = tag); - return tag; - } - function createEffectInstance() { - return { destroy: void 0, resource: void 0 }; - } - function updateRef() { - return updateWorkInProgressHook().memoizedState; - } - function mountEffectImpl(fiberFlags, hookFlags, create2, createDeps) { - var hook = mountWorkInProgressHook(); - createDeps = void 0 === createDeps ? null : createDeps; - currentlyRenderingFiber.flags |= fiberFlags; - hook.memoizedState = pushSimpleEffect( - 1 | hookFlags, - createEffectInstance(), - create2, - createDeps - ); - } - function updateEffectImpl(fiberFlags, hookFlags, create2, deps) { - var hook = updateWorkInProgressHook(); - deps = void 0 === deps ? null : deps; - var inst = hook.memoizedState.inst; - null !== currentHook && null !== deps && areHookInputsEqual(deps, currentHook.memoizedState.deps) ? hook.memoizedState = pushSimpleEffect(hookFlags, inst, create2, deps) : (currentlyRenderingFiber.flags |= fiberFlags, hook.memoizedState = pushSimpleEffect( - 1 | hookFlags, - inst, - create2, - deps - )); - } - function mountEffect(create2, createDeps) { - mountEffectImpl(8390656, 8, create2, createDeps); - } - function updateEffect(create2, createDeps) { - updateEffectImpl(2048, 8, create2, createDeps); - } - function updateInsertionEffect(create2, deps) { - return updateEffectImpl(4, 2, create2, deps); - } - function updateLayoutEffect(create2, deps) { - return updateEffectImpl(4, 4, create2, deps); - } - function imperativeHandleEffect(create2, ref) { - if ("function" === typeof ref) { - create2 = create2(); - var refCleanup = ref(create2); - return function() { - "function" === typeof refCleanup ? refCleanup() : ref(null); - }; - } - if (null !== ref && void 0 !== ref) - return create2 = create2(), ref.current = create2, function() { - ref.current = null; - }; - } - function updateImperativeHandle(ref, create2, deps) { - deps = null !== deps && void 0 !== deps ? deps.concat([ref]) : null; - updateEffectImpl(4, 4, imperativeHandleEffect.bind(null, create2, ref), deps); - } - function mountDebugValue() { - } - function updateCallback(callback, deps) { - var hook = updateWorkInProgressHook(); - deps = void 0 === deps ? null : deps; - var prevState = hook.memoizedState; - if (null !== deps && areHookInputsEqual(deps, prevState[1])) - return prevState[0]; - hook.memoizedState = [callback, deps]; - return callback; - } - function updateMemo(nextCreate, deps) { - var hook = updateWorkInProgressHook(); - deps = void 0 === deps ? null : deps; - var prevState = hook.memoizedState; - if (null !== deps && areHookInputsEqual(deps, prevState[1])) - return prevState[0]; - prevState = nextCreate(); - if (shouldDoubleInvokeUserFnsInHooksDEV) { - setIsStrictModeForDevtools(true); - try { - nextCreate(); - } finally { - setIsStrictModeForDevtools(false); - } - } - hook.memoizedState = [prevState, deps]; - return prevState; - } - function mountDeferredValueImpl(hook, value2, initialValue) { - if (void 0 === initialValue || 0 !== (renderLanes & 1073741824)) - return hook.memoizedState = value2; - hook.memoizedState = initialValue; - hook = requestDeferredLane(); - currentlyRenderingFiber.lanes |= hook; - workInProgressRootSkippedLanes |= hook; - return initialValue; - } - function updateDeferredValueImpl(hook, prevValue, value2, initialValue) { - if (objectIs(value2, prevValue)) return value2; - if (null !== currentTreeHiddenStackCursor.current) - return hook = mountDeferredValueImpl(hook, value2, initialValue), objectIs(hook, prevValue) || (didReceiveUpdate = true), hook; - if (0 === (renderLanes & 42)) - return didReceiveUpdate = true, hook.memoizedState = value2; - hook = requestDeferredLane(); - currentlyRenderingFiber.lanes |= hook; - workInProgressRootSkippedLanes |= hook; - return prevValue; - } - function startTransition(fiber, queue, pendingState, finishedState, callback) { - var previousPriority = ReactDOMSharedInternals.p; - ReactDOMSharedInternals.p = 0 !== previousPriority && 8 > previousPriority ? previousPriority : 8; - var prevTransition = ReactSharedInternals.T, currentTransition = {}; - ReactSharedInternals.T = currentTransition; - dispatchOptimisticSetState(fiber, false, queue, pendingState); - try { - var returnValue = callback(), onStartTransitionFinish = ReactSharedInternals.S; - null !== onStartTransitionFinish && onStartTransitionFinish(currentTransition, returnValue); - if (null !== returnValue && "object" === typeof returnValue && "function" === typeof returnValue.then) { - var thenableForFinishedState = chainThenableValue( - returnValue, - finishedState - ); - dispatchSetStateInternal( - fiber, - queue, - thenableForFinishedState, - requestUpdateLane(fiber) - ); - } else - dispatchSetStateInternal( - fiber, - queue, - finishedState, - requestUpdateLane(fiber) - ); - } catch (error) { - dispatchSetStateInternal( - fiber, - queue, - { then: function() { - }, status: "rejected", reason: error }, - requestUpdateLane() - ); - } finally { - ReactDOMSharedInternals.p = previousPriority, ReactSharedInternals.T = prevTransition; - } - } - function noop$2() { - } - function startHostTransition(formFiber, pendingState, action, formData) { - if (5 !== formFiber.tag) throw Error(formatProdErrorMessage(476)); - var queue = ensureFormComponentIsStateful(formFiber).queue; - startTransition( - formFiber, - queue, - pendingState, - sharedNotPendingObject, - null === action ? noop$2 : function() { - requestFormReset$1(formFiber); - return action(formData); - } - ); - } - function ensureFormComponentIsStateful(formFiber) { - var existingStateHook = formFiber.memoizedState; - if (null !== existingStateHook) return existingStateHook; - existingStateHook = { - memoizedState: sharedNotPendingObject, - baseState: sharedNotPendingObject, - baseQueue: null, - queue: { - pending: null, - lanes: 0, - dispatch: null, - lastRenderedReducer: basicStateReducer, - lastRenderedState: sharedNotPendingObject - }, - next: null - }; - var initialResetState = {}; - existingStateHook.next = { - memoizedState: initialResetState, - baseState: initialResetState, - baseQueue: null, - queue: { - pending: null, - lanes: 0, - dispatch: null, - lastRenderedReducer: basicStateReducer, - lastRenderedState: initialResetState - }, - next: null - }; - formFiber.memoizedState = existingStateHook; - formFiber = formFiber.alternate; - null !== formFiber && (formFiber.memoizedState = existingStateHook); - return existingStateHook; - } - function requestFormReset$1(formFiber) { - var resetStateQueue = ensureFormComponentIsStateful(formFiber).next.queue; - dispatchSetStateInternal(formFiber, resetStateQueue, {}, requestUpdateLane()); - } - function useHostTransitionStatus() { - return readContext(HostTransitionContext); - } - function updateId() { - return updateWorkInProgressHook().memoizedState; - } - function updateRefresh() { - return updateWorkInProgressHook().memoizedState; - } - function refreshCache(fiber) { - for (var provider = fiber.return; null !== provider; ) { - switch (provider.tag) { - case 24: - case 3: - var lane = requestUpdateLane(); - fiber = createUpdate(lane); - var root$41 = enqueueUpdate(provider, fiber, lane); - null !== root$41 && (scheduleUpdateOnFiber(root$41, provider, lane), entangleTransitions(root$41, provider, lane)); - provider = { cache: createCache() }; - fiber.payload = provider; - return; - } - provider = provider.return; - } - } - function dispatchReducerAction(fiber, queue, action) { - var lane = requestUpdateLane(); - action = { - lane, - revertLane: 0, - action, - hasEagerState: false, - eagerState: null, - next: null - }; - isRenderPhaseUpdate(fiber) ? enqueueRenderPhaseUpdate(queue, action) : (action = enqueueConcurrentHookUpdate(fiber, queue, action, lane), null !== action && (scheduleUpdateOnFiber(action, fiber, lane), entangleTransitionUpdate(action, queue, lane))); - } - function dispatchSetState(fiber, queue, action) { - var lane = requestUpdateLane(); - dispatchSetStateInternal(fiber, queue, action, lane); - } - function dispatchSetStateInternal(fiber, queue, action, lane) { - var update = { - lane, - revertLane: 0, - action, - hasEagerState: false, - eagerState: null, - next: null - }; - if (isRenderPhaseUpdate(fiber)) enqueueRenderPhaseUpdate(queue, update); - else { - var alternate = fiber.alternate; - if (0 === fiber.lanes && (null === alternate || 0 === alternate.lanes) && (alternate = queue.lastRenderedReducer, null !== alternate)) - try { - var currentState = queue.lastRenderedState, eagerState = alternate(currentState, action); - update.hasEagerState = true; - update.eagerState = eagerState; - if (objectIs(eagerState, currentState)) - return enqueueUpdate$1(fiber, queue, update, 0), null === workInProgressRoot && finishQueueingConcurrentUpdates(), false; - } catch (error) { - } finally { - } - action = enqueueConcurrentHookUpdate(fiber, queue, update, lane); - if (null !== action) - return scheduleUpdateOnFiber(action, fiber, lane), entangleTransitionUpdate(action, queue, lane), true; - } - return false; - } - function dispatchOptimisticSetState(fiber, throwIfDuringRender, queue, action) { - action = { - lane: 2, - revertLane: requestTransitionLane(), - action, - hasEagerState: false, - eagerState: null, - next: null - }; - if (isRenderPhaseUpdate(fiber)) { - if (throwIfDuringRender) throw Error(formatProdErrorMessage(479)); - } else - throwIfDuringRender = enqueueConcurrentHookUpdate( - fiber, - queue, - action, - 2 - ), null !== throwIfDuringRender && scheduleUpdateOnFiber(throwIfDuringRender, fiber, 2); - } - function isRenderPhaseUpdate(fiber) { - var alternate = fiber.alternate; - return fiber === currentlyRenderingFiber || null !== alternate && alternate === currentlyRenderingFiber; - } - function enqueueRenderPhaseUpdate(queue, update) { - didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true; - var pending = queue.pending; - null === pending ? update.next = update : (update.next = pending.next, pending.next = update); - queue.pending = update; - } - function entangleTransitionUpdate(root3, queue, lane) { - if (0 !== (lane & 4194048)) { - var queueLanes = queue.lanes; - queueLanes &= root3.pendingLanes; - lane |= queueLanes; - queue.lanes = lane; - markRootEntangled(root3, lane); - } - } - var ContextOnlyDispatcher = { - readContext, - use, - useCallback: throwInvalidHookError, - useContext: throwInvalidHookError, - useEffect: throwInvalidHookError, - useImperativeHandle: throwInvalidHookError, - useLayoutEffect: throwInvalidHookError, - useInsertionEffect: throwInvalidHookError, - useMemo: throwInvalidHookError, - useReducer: throwInvalidHookError, - useRef: throwInvalidHookError, - useState: throwInvalidHookError, - useDebugValue: throwInvalidHookError, - useDeferredValue: throwInvalidHookError, - useTransition: throwInvalidHookError, - useSyncExternalStore: throwInvalidHookError, - useId: throwInvalidHookError, - useHostTransitionStatus: throwInvalidHookError, - useFormState: throwInvalidHookError, - useActionState: throwInvalidHookError, - useOptimistic: throwInvalidHookError, - useMemoCache: throwInvalidHookError, - useCacheRefresh: throwInvalidHookError - }; - var HooksDispatcherOnMount = { - readContext, - use, - useCallback: function(callback, deps) { - mountWorkInProgressHook().memoizedState = [ - callback, - void 0 === deps ? null : deps - ]; - return callback; - }, - useContext: readContext, - useEffect: mountEffect, - useImperativeHandle: function(ref, create2, deps) { - deps = null !== deps && void 0 !== deps ? deps.concat([ref]) : null; - mountEffectImpl( - 4194308, - 4, - imperativeHandleEffect.bind(null, create2, ref), - deps - ); - }, - useLayoutEffect: function(create2, deps) { - return mountEffectImpl(4194308, 4, create2, deps); - }, - useInsertionEffect: function(create2, deps) { - mountEffectImpl(4, 2, create2, deps); - }, - useMemo: function(nextCreate, deps) { - var hook = mountWorkInProgressHook(); - deps = void 0 === deps ? null : deps; - var nextValue = nextCreate(); - if (shouldDoubleInvokeUserFnsInHooksDEV) { - setIsStrictModeForDevtools(true); - try { - nextCreate(); - } finally { - setIsStrictModeForDevtools(false); - } - } - hook.memoizedState = [nextValue, deps]; - return nextValue; - }, - useReducer: function(reducer, initialArg, init2) { - var hook = mountWorkInProgressHook(); - if (void 0 !== init2) { - var initialState = init2(initialArg); - if (shouldDoubleInvokeUserFnsInHooksDEV) { - setIsStrictModeForDevtools(true); - try { - init2(initialArg); - } finally { - setIsStrictModeForDevtools(false); - } - } - } else initialState = initialArg; - hook.memoizedState = hook.baseState = initialState; - reducer = { - pending: null, - lanes: 0, - dispatch: null, - lastRenderedReducer: reducer, - lastRenderedState: initialState - }; - hook.queue = reducer; - reducer = reducer.dispatch = dispatchReducerAction.bind( - null, - currentlyRenderingFiber, - reducer - ); - return [hook.memoizedState, reducer]; - }, - useRef: function(initialValue) { - var hook = mountWorkInProgressHook(); - initialValue = { current: initialValue }; - return hook.memoizedState = initialValue; - }, - useState: function(initialState) { - initialState = mountStateImpl(initialState); - var queue = initialState.queue, dispatch2 = dispatchSetState.bind(null, currentlyRenderingFiber, queue); - queue.dispatch = dispatch2; - return [initialState.memoizedState, dispatch2]; - }, - useDebugValue: mountDebugValue, - useDeferredValue: function(value2, initialValue) { - var hook = mountWorkInProgressHook(); - return mountDeferredValueImpl(hook, value2, initialValue); - }, - useTransition: function() { - var stateHook = mountStateImpl(false); - stateHook = startTransition.bind( - null, - currentlyRenderingFiber, - stateHook.queue, - true, - false - ); - mountWorkInProgressHook().memoizedState = stateHook; - return [false, stateHook]; - }, - useSyncExternalStore: function(subscribe, getSnapshot, getServerSnapshot) { - var fiber = currentlyRenderingFiber, hook = mountWorkInProgressHook(); - if (isHydrating) { - if (void 0 === getServerSnapshot) - throw Error(formatProdErrorMessage(407)); - getServerSnapshot = getServerSnapshot(); - } else { - getServerSnapshot = getSnapshot(); - if (null === workInProgressRoot) - throw Error(formatProdErrorMessage(349)); - 0 !== (workInProgressRootRenderLanes & 124) || pushStoreConsistencyCheck(fiber, getSnapshot, getServerSnapshot); - } - hook.memoizedState = getServerSnapshot; - var inst = { value: getServerSnapshot, getSnapshot }; - hook.queue = inst; - mountEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [ - subscribe - ]); - fiber.flags |= 2048; - pushSimpleEffect( - 9, - createEffectInstance(), - updateStoreInstance.bind( - null, - fiber, - inst, - getServerSnapshot, - getSnapshot - ), - null - ); - return getServerSnapshot; - }, - useId: function() { - var hook = mountWorkInProgressHook(), identifierPrefix = workInProgressRoot.identifierPrefix; - if (isHydrating) { - var JSCompiler_inline_result = treeContextOverflow; - var idWithLeadingBit = treeContextId; - JSCompiler_inline_result = (idWithLeadingBit & ~(1 << 32 - clz32(idWithLeadingBit) - 1)).toString(32) + JSCompiler_inline_result; - identifierPrefix = "\xAB" + identifierPrefix + "R" + JSCompiler_inline_result; - JSCompiler_inline_result = localIdCounter++; - 0 < JSCompiler_inline_result && (identifierPrefix += "H" + JSCompiler_inline_result.toString(32)); - identifierPrefix += "\xBB"; - } else - JSCompiler_inline_result = globalClientIdCounter++, identifierPrefix = "\xAB" + identifierPrefix + "r" + JSCompiler_inline_result.toString(32) + "\xBB"; - return hook.memoizedState = identifierPrefix; - }, - useHostTransitionStatus, - useFormState: mountActionState, - useActionState: mountActionState, - useOptimistic: function(passthrough) { - var hook = mountWorkInProgressHook(); - hook.memoizedState = hook.baseState = passthrough; - var queue = { - pending: null, - lanes: 0, - dispatch: null, - lastRenderedReducer: null, - lastRenderedState: null - }; - hook.queue = queue; - hook = dispatchOptimisticSetState.bind( - null, - currentlyRenderingFiber, - true, - queue - ); - queue.dispatch = hook; - return [passthrough, hook]; - }, - useMemoCache, - useCacheRefresh: function() { - return mountWorkInProgressHook().memoizedState = refreshCache.bind( - null, - currentlyRenderingFiber - ); - } - }; - var HooksDispatcherOnUpdate = { - readContext, - use, - useCallback: updateCallback, - useContext: readContext, - useEffect: updateEffect, - useImperativeHandle: updateImperativeHandle, - useInsertionEffect: updateInsertionEffect, - useLayoutEffect: updateLayoutEffect, - useMemo: updateMemo, - useReducer: updateReducer, - useRef: updateRef, - useState: function() { - return updateReducer(basicStateReducer); - }, - useDebugValue: mountDebugValue, - useDeferredValue: function(value2, initialValue) { - var hook = updateWorkInProgressHook(); - return updateDeferredValueImpl( - hook, - currentHook.memoizedState, - value2, - initialValue - ); - }, - useTransition: function() { - var booleanOrThenable = updateReducer(basicStateReducer)[0], start2 = updateWorkInProgressHook().memoizedState; - return [ - "boolean" === typeof booleanOrThenable ? booleanOrThenable : useThenable(booleanOrThenable), - start2 - ]; - }, - useSyncExternalStore: updateSyncExternalStore, - useId: updateId, - useHostTransitionStatus, - useFormState: updateActionState, - useActionState: updateActionState, - useOptimistic: function(passthrough, reducer) { - var hook = updateWorkInProgressHook(); - return updateOptimisticImpl(hook, currentHook, passthrough, reducer); - }, - useMemoCache, - useCacheRefresh: updateRefresh - }; - var HooksDispatcherOnRerender = { - readContext, - use, - useCallback: updateCallback, - useContext: readContext, - useEffect: updateEffect, - useImperativeHandle: updateImperativeHandle, - useInsertionEffect: updateInsertionEffect, - useLayoutEffect: updateLayoutEffect, - useMemo: updateMemo, - useReducer: rerenderReducer, - useRef: updateRef, - useState: function() { - return rerenderReducer(basicStateReducer); - }, - useDebugValue: mountDebugValue, - useDeferredValue: function(value2, initialValue) { - var hook = updateWorkInProgressHook(); - return null === currentHook ? mountDeferredValueImpl(hook, value2, initialValue) : updateDeferredValueImpl( - hook, - currentHook.memoizedState, - value2, - initialValue - ); - }, - useTransition: function() { - var booleanOrThenable = rerenderReducer(basicStateReducer)[0], start2 = updateWorkInProgressHook().memoizedState; - return [ - "boolean" === typeof booleanOrThenable ? booleanOrThenable : useThenable(booleanOrThenable), - start2 - ]; - }, - useSyncExternalStore: updateSyncExternalStore, - useId: updateId, - useHostTransitionStatus, - useFormState: rerenderActionState, - useActionState: rerenderActionState, - useOptimistic: function(passthrough, reducer) { - var hook = updateWorkInProgressHook(); - if (null !== currentHook) - return updateOptimisticImpl(hook, currentHook, passthrough, reducer); - hook.baseState = passthrough; - return [passthrough, hook.queue.dispatch]; - }, - useMemoCache, - useCacheRefresh: updateRefresh - }; - var thenableState = null; - var thenableIndexCounter = 0; - function unwrapThenable(thenable) { - var index2 = thenableIndexCounter; - thenableIndexCounter += 1; - null === thenableState && (thenableState = []); - return trackUsedThenable(thenableState, thenable, index2); - } - function coerceRef(workInProgress2, element) { - element = element.props.ref; - workInProgress2.ref = void 0 !== element ? element : null; - } - function throwOnInvalidObjectType(returnFiber, newChild) { - if (newChild.$$typeof === REACT_LEGACY_ELEMENT_TYPE) - throw Error(formatProdErrorMessage(525)); - returnFiber = Object.prototype.toString.call(newChild); - throw Error( - formatProdErrorMessage( - 31, - "[object Object]" === returnFiber ? "object with keys {" + Object.keys(newChild).join(", ") + "}" : returnFiber - ) - ); - } - function resolveLazy(lazyType) { - var init2 = lazyType._init; - return init2(lazyType._payload); - } - function createChildReconciler(shouldTrackSideEffects) { - function deleteChild(returnFiber, childToDelete) { - if (shouldTrackSideEffects) { - var deletions = returnFiber.deletions; - null === deletions ? (returnFiber.deletions = [childToDelete], returnFiber.flags |= 16) : deletions.push(childToDelete); - } - } - function deleteRemainingChildren(returnFiber, currentFirstChild) { - if (!shouldTrackSideEffects) return null; - for (; null !== currentFirstChild; ) - deleteChild(returnFiber, currentFirstChild), currentFirstChild = currentFirstChild.sibling; - return null; - } - function mapRemainingChildren(currentFirstChild) { - for (var existingChildren = /* @__PURE__ */ new Map(); null !== currentFirstChild; ) - null !== currentFirstChild.key ? existingChildren.set(currentFirstChild.key, currentFirstChild) : existingChildren.set(currentFirstChild.index, currentFirstChild), currentFirstChild = currentFirstChild.sibling; - return existingChildren; - } - function useFiber(fiber, pendingProps) { - fiber = createWorkInProgress(fiber, pendingProps); - fiber.index = 0; - fiber.sibling = null; - return fiber; - } - function placeChild(newFiber, lastPlacedIndex, newIndex) { - newFiber.index = newIndex; - if (!shouldTrackSideEffects) - return newFiber.flags |= 1048576, lastPlacedIndex; - newIndex = newFiber.alternate; - if (null !== newIndex) - return newIndex = newIndex.index, newIndex < lastPlacedIndex ? (newFiber.flags |= 67108866, lastPlacedIndex) : newIndex; - newFiber.flags |= 67108866; - return lastPlacedIndex; - } - function placeSingleChild(newFiber) { - shouldTrackSideEffects && null === newFiber.alternate && (newFiber.flags |= 67108866); - return newFiber; - } - function updateTextNode(returnFiber, current, textContent, lanes) { - if (null === current || 6 !== current.tag) - return current = createFiberFromText(textContent, returnFiber.mode, lanes), current.return = returnFiber, current; - current = useFiber(current, textContent); - current.return = returnFiber; - return current; - } - function updateElement(returnFiber, current, element, lanes) { - var elementType = element.type; - if (elementType === REACT_FRAGMENT_TYPE) - return updateFragment( - returnFiber, - current, - element.props.children, - lanes, - element.key - ); - if (null !== current && (current.elementType === elementType || "object" === typeof elementType && null !== elementType && elementType.$$typeof === REACT_LAZY_TYPE && resolveLazy(elementType) === current.type)) - return current = useFiber(current, element.props), coerceRef(current, element), current.return = returnFiber, current; - current = createFiberFromTypeAndProps( - element.type, - element.key, - element.props, - null, - returnFiber.mode, - lanes - ); - coerceRef(current, element); - current.return = returnFiber; - return current; - } - function updatePortal(returnFiber, current, portal, lanes) { - if (null === current || 4 !== current.tag || current.stateNode.containerInfo !== portal.containerInfo || current.stateNode.implementation !== portal.implementation) - return current = createFiberFromPortal(portal, returnFiber.mode, lanes), current.return = returnFiber, current; - current = useFiber(current, portal.children || []); - current.return = returnFiber; - return current; - } - function updateFragment(returnFiber, current, fragment, lanes, key) { - if (null === current || 7 !== current.tag) - return current = createFiberFromFragment( - fragment, - returnFiber.mode, - lanes, - key - ), current.return = returnFiber, current; - current = useFiber(current, fragment); - current.return = returnFiber; - return current; - } - function createChild(returnFiber, newChild, lanes) { - if ("string" === typeof newChild && "" !== newChild || "number" === typeof newChild || "bigint" === typeof newChild) - return newChild = createFiberFromText( - "" + newChild, - returnFiber.mode, - lanes - ), newChild.return = returnFiber, newChild; - if ("object" === typeof newChild && null !== newChild) { - switch (newChild.$$typeof) { - case REACT_ELEMENT_TYPE: - return lanes = createFiberFromTypeAndProps( - newChild.type, - newChild.key, - newChild.props, - null, - returnFiber.mode, - lanes - ), coerceRef(lanes, newChild), lanes.return = returnFiber, lanes; - case REACT_PORTAL_TYPE: - return newChild = createFiberFromPortal( - newChild, - returnFiber.mode, - lanes - ), newChild.return = returnFiber, newChild; - case REACT_LAZY_TYPE: - var init2 = newChild._init; - newChild = init2(newChild._payload); - return createChild(returnFiber, newChild, lanes); - } - if (isArrayImpl(newChild) || getIteratorFn(newChild)) - return newChild = createFiberFromFragment( - newChild, - returnFiber.mode, - lanes, - null - ), newChild.return = returnFiber, newChild; - if ("function" === typeof newChild.then) - return createChild(returnFiber, unwrapThenable(newChild), lanes); - if (newChild.$$typeof === REACT_CONTEXT_TYPE) - return createChild( - returnFiber, - readContextDuringReconciliation(returnFiber, newChild), - lanes - ); - throwOnInvalidObjectType(returnFiber, newChild); - } - return null; - } - function updateSlot(returnFiber, oldFiber, newChild, lanes) { - var key = null !== oldFiber ? oldFiber.key : null; - if ("string" === typeof newChild && "" !== newChild || "number" === typeof newChild || "bigint" === typeof newChild) - return null !== key ? null : updateTextNode(returnFiber, oldFiber, "" + newChild, lanes); - if ("object" === typeof newChild && null !== newChild) { - switch (newChild.$$typeof) { - case REACT_ELEMENT_TYPE: - return newChild.key === key ? updateElement(returnFiber, oldFiber, newChild, lanes) : null; - case REACT_PORTAL_TYPE: - return newChild.key === key ? updatePortal(returnFiber, oldFiber, newChild, lanes) : null; - case REACT_LAZY_TYPE: - return key = newChild._init, newChild = key(newChild._payload), updateSlot(returnFiber, oldFiber, newChild, lanes); - } - if (isArrayImpl(newChild) || getIteratorFn(newChild)) - return null !== key ? null : updateFragment(returnFiber, oldFiber, newChild, lanes, null); - if ("function" === typeof newChild.then) - return updateSlot( - returnFiber, - oldFiber, - unwrapThenable(newChild), - lanes - ); - if (newChild.$$typeof === REACT_CONTEXT_TYPE) - return updateSlot( - returnFiber, - oldFiber, - readContextDuringReconciliation(returnFiber, newChild), - lanes - ); - throwOnInvalidObjectType(returnFiber, newChild); - } - return null; - } - function updateFromMap(existingChildren, returnFiber, newIdx, newChild, lanes) { - if ("string" === typeof newChild && "" !== newChild || "number" === typeof newChild || "bigint" === typeof newChild) - return existingChildren = existingChildren.get(newIdx) || null, updateTextNode(returnFiber, existingChildren, "" + newChild, lanes); - if ("object" === typeof newChild && null !== newChild) { - switch (newChild.$$typeof) { - case REACT_ELEMENT_TYPE: - return existingChildren = existingChildren.get( - null === newChild.key ? newIdx : newChild.key - ) || null, updateElement(returnFiber, existingChildren, newChild, lanes); - case REACT_PORTAL_TYPE: - return existingChildren = existingChildren.get( - null === newChild.key ? newIdx : newChild.key - ) || null, updatePortal(returnFiber, existingChildren, newChild, lanes); - case REACT_LAZY_TYPE: - var init2 = newChild._init; - newChild = init2(newChild._payload); - return updateFromMap( - existingChildren, - returnFiber, - newIdx, - newChild, - lanes - ); - } - if (isArrayImpl(newChild) || getIteratorFn(newChild)) - return existingChildren = existingChildren.get(newIdx) || null, updateFragment(returnFiber, existingChildren, newChild, lanes, null); - if ("function" === typeof newChild.then) - return updateFromMap( - existingChildren, - returnFiber, - newIdx, - unwrapThenable(newChild), - lanes - ); - if (newChild.$$typeof === REACT_CONTEXT_TYPE) - return updateFromMap( - existingChildren, - returnFiber, - newIdx, - readContextDuringReconciliation(returnFiber, newChild), - lanes - ); - throwOnInvalidObjectType(returnFiber, newChild); - } - return null; - } - function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, lanes) { - for (var resultingFirstChild = null, previousNewFiber = null, oldFiber = currentFirstChild, newIdx = currentFirstChild = 0, nextOldFiber = null; null !== oldFiber && newIdx < newChildren.length; newIdx++) { - oldFiber.index > newIdx ? (nextOldFiber = oldFiber, oldFiber = null) : nextOldFiber = oldFiber.sibling; - var newFiber = updateSlot( - returnFiber, - oldFiber, - newChildren[newIdx], - lanes - ); - if (null === newFiber) { - null === oldFiber && (oldFiber = nextOldFiber); - break; - } - shouldTrackSideEffects && oldFiber && null === newFiber.alternate && deleteChild(returnFiber, oldFiber); - currentFirstChild = placeChild(newFiber, currentFirstChild, newIdx); - null === previousNewFiber ? resultingFirstChild = newFiber : previousNewFiber.sibling = newFiber; - previousNewFiber = newFiber; - oldFiber = nextOldFiber; - } - if (newIdx === newChildren.length) - return deleteRemainingChildren(returnFiber, oldFiber), isHydrating && pushTreeFork(returnFiber, newIdx), resultingFirstChild; - if (null === oldFiber) { - for (; newIdx < newChildren.length; newIdx++) - oldFiber = createChild(returnFiber, newChildren[newIdx], lanes), null !== oldFiber && (currentFirstChild = placeChild( - oldFiber, - currentFirstChild, - newIdx - ), null === previousNewFiber ? resultingFirstChild = oldFiber : previousNewFiber.sibling = oldFiber, previousNewFiber = oldFiber); - isHydrating && pushTreeFork(returnFiber, newIdx); - return resultingFirstChild; - } - for (oldFiber = mapRemainingChildren(oldFiber); newIdx < newChildren.length; newIdx++) - nextOldFiber = updateFromMap( - oldFiber, - returnFiber, - newIdx, - newChildren[newIdx], - lanes - ), null !== nextOldFiber && (shouldTrackSideEffects && null !== nextOldFiber.alternate && oldFiber.delete( - null === nextOldFiber.key ? newIdx : nextOldFiber.key - ), currentFirstChild = placeChild( - nextOldFiber, - currentFirstChild, - newIdx - ), null === previousNewFiber ? resultingFirstChild = nextOldFiber : previousNewFiber.sibling = nextOldFiber, previousNewFiber = nextOldFiber); - shouldTrackSideEffects && oldFiber.forEach(function(child) { - return deleteChild(returnFiber, child); - }); - isHydrating && pushTreeFork(returnFiber, newIdx); - return resultingFirstChild; - } - function reconcileChildrenIterator(returnFiber, currentFirstChild, newChildren, lanes) { - if (null == newChildren) throw Error(formatProdErrorMessage(151)); - for (var resultingFirstChild = null, previousNewFiber = null, oldFiber = currentFirstChild, newIdx = currentFirstChild = 0, nextOldFiber = null, step = newChildren.next(); null !== oldFiber && !step.done; newIdx++, step = newChildren.next()) { - oldFiber.index > newIdx ? (nextOldFiber = oldFiber, oldFiber = null) : nextOldFiber = oldFiber.sibling; - var newFiber = updateSlot(returnFiber, oldFiber, step.value, lanes); - if (null === newFiber) { - null === oldFiber && (oldFiber = nextOldFiber); - break; - } - shouldTrackSideEffects && oldFiber && null === newFiber.alternate && deleteChild(returnFiber, oldFiber); - currentFirstChild = placeChild(newFiber, currentFirstChild, newIdx); - null === previousNewFiber ? resultingFirstChild = newFiber : previousNewFiber.sibling = newFiber; - previousNewFiber = newFiber; - oldFiber = nextOldFiber; - } - if (step.done) - return deleteRemainingChildren(returnFiber, oldFiber), isHydrating && pushTreeFork(returnFiber, newIdx), resultingFirstChild; - if (null === oldFiber) { - for (; !step.done; newIdx++, step = newChildren.next()) - step = createChild(returnFiber, step.value, lanes), null !== step && (currentFirstChild = placeChild(step, currentFirstChild, newIdx), null === previousNewFiber ? resultingFirstChild = step : previousNewFiber.sibling = step, previousNewFiber = step); - isHydrating && pushTreeFork(returnFiber, newIdx); - return resultingFirstChild; - } - for (oldFiber = mapRemainingChildren(oldFiber); !step.done; newIdx++, step = newChildren.next()) - step = updateFromMap(oldFiber, returnFiber, newIdx, step.value, lanes), null !== step && (shouldTrackSideEffects && null !== step.alternate && oldFiber.delete(null === step.key ? newIdx : step.key), currentFirstChild = placeChild(step, currentFirstChild, newIdx), null === previousNewFiber ? resultingFirstChild = step : previousNewFiber.sibling = step, previousNewFiber = step); - shouldTrackSideEffects && oldFiber.forEach(function(child) { - return deleteChild(returnFiber, child); - }); - isHydrating && pushTreeFork(returnFiber, newIdx); - return resultingFirstChild; - } - function reconcileChildFibersImpl(returnFiber, currentFirstChild, newChild, lanes) { - "object" === typeof newChild && null !== newChild && newChild.type === REACT_FRAGMENT_TYPE && null === newChild.key && (newChild = newChild.props.children); - if ("object" === typeof newChild && null !== newChild) { - switch (newChild.$$typeof) { - case REACT_ELEMENT_TYPE: - a: { - for (var key = newChild.key; null !== currentFirstChild; ) { - if (currentFirstChild.key === key) { - key = newChild.type; - if (key === REACT_FRAGMENT_TYPE) { - if (7 === currentFirstChild.tag) { - deleteRemainingChildren( - returnFiber, - currentFirstChild.sibling - ); - lanes = useFiber( - currentFirstChild, - newChild.props.children - ); - lanes.return = returnFiber; - returnFiber = lanes; - break a; - } - } else if (currentFirstChild.elementType === key || "object" === typeof key && null !== key && key.$$typeof === REACT_LAZY_TYPE && resolveLazy(key) === currentFirstChild.type) { - deleteRemainingChildren( - returnFiber, - currentFirstChild.sibling - ); - lanes = useFiber(currentFirstChild, newChild.props); - coerceRef(lanes, newChild); - lanes.return = returnFiber; - returnFiber = lanes; - break a; - } - deleteRemainingChildren(returnFiber, currentFirstChild); - break; - } else deleteChild(returnFiber, currentFirstChild); - currentFirstChild = currentFirstChild.sibling; - } - newChild.type === REACT_FRAGMENT_TYPE ? (lanes = createFiberFromFragment( - newChild.props.children, - returnFiber.mode, - lanes, - newChild.key - ), lanes.return = returnFiber, returnFiber = lanes) : (lanes = createFiberFromTypeAndProps( - newChild.type, - newChild.key, - newChild.props, - null, - returnFiber.mode, - lanes - ), coerceRef(lanes, newChild), lanes.return = returnFiber, returnFiber = lanes); - } - return placeSingleChild(returnFiber); - case REACT_PORTAL_TYPE: - a: { - for (key = newChild.key; null !== currentFirstChild; ) { - if (currentFirstChild.key === key) - if (4 === currentFirstChild.tag && currentFirstChild.stateNode.containerInfo === newChild.containerInfo && currentFirstChild.stateNode.implementation === newChild.implementation) { - deleteRemainingChildren( - returnFiber, - currentFirstChild.sibling - ); - lanes = useFiber(currentFirstChild, newChild.children || []); - lanes.return = returnFiber; - returnFiber = lanes; - break a; - } else { - deleteRemainingChildren(returnFiber, currentFirstChild); - break; - } - else deleteChild(returnFiber, currentFirstChild); - currentFirstChild = currentFirstChild.sibling; - } - lanes = createFiberFromPortal(newChild, returnFiber.mode, lanes); - lanes.return = returnFiber; - returnFiber = lanes; - } - return placeSingleChild(returnFiber); - case REACT_LAZY_TYPE: - return key = newChild._init, newChild = key(newChild._payload), reconcileChildFibersImpl( - returnFiber, - currentFirstChild, - newChild, - lanes - ); - } - if (isArrayImpl(newChild)) - return reconcileChildrenArray( - returnFiber, - currentFirstChild, - newChild, - lanes - ); - if (getIteratorFn(newChild)) { - key = getIteratorFn(newChild); - if ("function" !== typeof key) throw Error(formatProdErrorMessage(150)); - newChild = key.call(newChild); - return reconcileChildrenIterator( - returnFiber, - currentFirstChild, - newChild, - lanes - ); - } - if ("function" === typeof newChild.then) - return reconcileChildFibersImpl( - returnFiber, - currentFirstChild, - unwrapThenable(newChild), - lanes - ); - if (newChild.$$typeof === REACT_CONTEXT_TYPE) - return reconcileChildFibersImpl( - returnFiber, - currentFirstChild, - readContextDuringReconciliation(returnFiber, newChild), - lanes - ); - throwOnInvalidObjectType(returnFiber, newChild); - } - return "string" === typeof newChild && "" !== newChild || "number" === typeof newChild || "bigint" === typeof newChild ? (newChild = "" + newChild, null !== currentFirstChild && 6 === currentFirstChild.tag ? (deleteRemainingChildren(returnFiber, currentFirstChild.sibling), lanes = useFiber(currentFirstChild, newChild), lanes.return = returnFiber, returnFiber = lanes) : (deleteRemainingChildren(returnFiber, currentFirstChild), lanes = createFiberFromText(newChild, returnFiber.mode, lanes), lanes.return = returnFiber, returnFiber = lanes), placeSingleChild(returnFiber)) : deleteRemainingChildren(returnFiber, currentFirstChild); - } - return function(returnFiber, currentFirstChild, newChild, lanes) { - try { - thenableIndexCounter = 0; - var firstChildFiber = reconcileChildFibersImpl( - returnFiber, - currentFirstChild, - newChild, - lanes - ); - thenableState = null; - return firstChildFiber; - } catch (x2) { - if (x2 === SuspenseException || x2 === SuspenseActionException) throw x2; - var fiber = createFiberImplClass(29, x2, null, returnFiber.mode); - fiber.lanes = lanes; - fiber.return = returnFiber; - return fiber; - } finally { - } - }; - } - var reconcileChildFibers = createChildReconciler(true); - var mountChildFibers = createChildReconciler(false); - var suspenseHandlerStackCursor = createCursor(null); - var shellBoundary = null; - function pushPrimaryTreeSuspenseHandler(handler) { - var current = handler.alternate; - push(suspenseStackCursor, suspenseStackCursor.current & 1); - push(suspenseHandlerStackCursor, handler); - null === shellBoundary && (null === current || null !== currentTreeHiddenStackCursor.current ? shellBoundary = handler : null !== current.memoizedState && (shellBoundary = handler)); - } - function pushOffscreenSuspenseHandler(fiber) { - if (22 === fiber.tag) { - if (push(suspenseStackCursor, suspenseStackCursor.current), push(suspenseHandlerStackCursor, fiber), null === shellBoundary) { - var current = fiber.alternate; - null !== current && null !== current.memoizedState && (shellBoundary = fiber); - } - } else reuseSuspenseHandlerOnStack(fiber); - } - function reuseSuspenseHandlerOnStack() { - push(suspenseStackCursor, suspenseStackCursor.current); - push(suspenseHandlerStackCursor, suspenseHandlerStackCursor.current); - } - function popSuspenseHandler(fiber) { - pop(suspenseHandlerStackCursor); - shellBoundary === fiber && (shellBoundary = null); - pop(suspenseStackCursor); - } - var suspenseStackCursor = createCursor(0); - function findFirstSuspended(row) { - for (var node = row; null !== node; ) { - if (13 === node.tag) { - var state = node.memoizedState; - if (null !== state && (state = state.dehydrated, null === state || "$?" === state.data || isSuspenseInstanceFallback(state))) - return node; - } else if (19 === node.tag && void 0 !== node.memoizedProps.revealOrder) { - if (0 !== (node.flags & 128)) return node; - } else if (null !== node.child) { - node.child.return = node; - node = node.child; - continue; - } - if (node === row) break; - for (; null === node.sibling; ) { - if (null === node.return || node.return === row) return null; - node = node.return; - } - node.sibling.return = node.return; - node = node.sibling; - } - return null; - } - function applyDerivedStateFromProps(workInProgress2, ctor, getDerivedStateFromProps, nextProps) { - ctor = workInProgress2.memoizedState; - getDerivedStateFromProps = getDerivedStateFromProps(nextProps, ctor); - getDerivedStateFromProps = null === getDerivedStateFromProps || void 0 === getDerivedStateFromProps ? ctor : assign({}, ctor, getDerivedStateFromProps); - workInProgress2.memoizedState = getDerivedStateFromProps; - 0 === workInProgress2.lanes && (workInProgress2.updateQueue.baseState = getDerivedStateFromProps); - } - var classComponentUpdater = { - enqueueSetState: function(inst, payload, callback) { - inst = inst._reactInternals; - var lane = requestUpdateLane(), update = createUpdate(lane); - update.payload = payload; - void 0 !== callback && null !== callback && (update.callback = callback); - payload = enqueueUpdate(inst, update, lane); - null !== payload && (scheduleUpdateOnFiber(payload, inst, lane), entangleTransitions(payload, inst, lane)); - }, - enqueueReplaceState: function(inst, payload, callback) { - inst = inst._reactInternals; - var lane = requestUpdateLane(), update = createUpdate(lane); - update.tag = 1; - update.payload = payload; - void 0 !== callback && null !== callback && (update.callback = callback); - payload = enqueueUpdate(inst, update, lane); - null !== payload && (scheduleUpdateOnFiber(payload, inst, lane), entangleTransitions(payload, inst, lane)); - }, - enqueueForceUpdate: function(inst, callback) { - inst = inst._reactInternals; - var lane = requestUpdateLane(), update = createUpdate(lane); - update.tag = 2; - void 0 !== callback && null !== callback && (update.callback = callback); - callback = enqueueUpdate(inst, update, lane); - null !== callback && (scheduleUpdateOnFiber(callback, inst, lane), entangleTransitions(callback, inst, lane)); - } - }; - function checkShouldComponentUpdate(workInProgress2, ctor, oldProps, newProps, oldState, newState, nextContext) { - workInProgress2 = workInProgress2.stateNode; - return "function" === typeof workInProgress2.shouldComponentUpdate ? workInProgress2.shouldComponentUpdate(newProps, newState, nextContext) : ctor.prototype && ctor.prototype.isPureReactComponent ? !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState) : true; - } - function callComponentWillReceiveProps(workInProgress2, instance, newProps, nextContext) { - workInProgress2 = instance.state; - "function" === typeof instance.componentWillReceiveProps && instance.componentWillReceiveProps(newProps, nextContext); - "function" === typeof instance.UNSAFE_componentWillReceiveProps && instance.UNSAFE_componentWillReceiveProps(newProps, nextContext); - instance.state !== workInProgress2 && classComponentUpdater.enqueueReplaceState(instance, instance.state, null); - } - function resolveClassComponentProps(Component, baseProps) { - var newProps = baseProps; - if ("ref" in baseProps) { - newProps = {}; - for (var propName in baseProps) - "ref" !== propName && (newProps[propName] = baseProps[propName]); - } - if (Component = Component.defaultProps) { - newProps === baseProps && (newProps = assign({}, newProps)); - for (var propName$73 in Component) - void 0 === newProps[propName$73] && (newProps[propName$73] = Component[propName$73]); - } - return newProps; - } - var reportGlobalError = "function" === typeof reportError ? reportError : function(error) { - if ("object" === typeof window && "function" === typeof window.ErrorEvent) { - var event = new window.ErrorEvent("error", { - bubbles: true, - cancelable: true, - message: "object" === typeof error && null !== error && "string" === typeof error.message ? String(error.message) : String(error), - error - }); - if (!window.dispatchEvent(event)) return; - } else if ("object" === typeof process && "function" === typeof process.emit) { - process.emit("uncaughtException", error); - return; - } - console.error(error); - }; - function defaultOnUncaughtError(error) { - reportGlobalError(error); - } - function defaultOnCaughtError(error) { - console.error(error); - } - function defaultOnRecoverableError(error) { - reportGlobalError(error); - } - function logUncaughtError(root3, errorInfo) { - try { - var onUncaughtError = root3.onUncaughtError; - onUncaughtError(errorInfo.value, { componentStack: errorInfo.stack }); - } catch (e$74) { - setTimeout(function() { - throw e$74; - }); - } - } - function logCaughtError(root3, boundary, errorInfo) { - try { - var onCaughtError = root3.onCaughtError; - onCaughtError(errorInfo.value, { - componentStack: errorInfo.stack, - errorBoundary: 1 === boundary.tag ? boundary.stateNode : null - }); - } catch (e$75) { - setTimeout(function() { - throw e$75; - }); - } - } - function createRootErrorUpdate(root3, errorInfo, lane) { - lane = createUpdate(lane); - lane.tag = 3; - lane.payload = { element: null }; - lane.callback = function() { - logUncaughtError(root3, errorInfo); - }; - return lane; - } - function createClassErrorUpdate(lane) { - lane = createUpdate(lane); - lane.tag = 3; - return lane; - } - function initializeClassErrorUpdate(update, root3, fiber, errorInfo) { - var getDerivedStateFromError = fiber.type.getDerivedStateFromError; - if ("function" === typeof getDerivedStateFromError) { - var error = errorInfo.value; - update.payload = function() { - return getDerivedStateFromError(error); - }; - update.callback = function() { - logCaughtError(root3, fiber, errorInfo); - }; - } - var inst = fiber.stateNode; - null !== inst && "function" === typeof inst.componentDidCatch && (update.callback = function() { - logCaughtError(root3, fiber, errorInfo); - "function" !== typeof getDerivedStateFromError && (null === legacyErrorBoundariesThatAlreadyFailed ? legacyErrorBoundariesThatAlreadyFailed = /* @__PURE__ */ new Set([this]) : legacyErrorBoundariesThatAlreadyFailed.add(this)); - var stack = errorInfo.stack; - this.componentDidCatch(errorInfo.value, { - componentStack: null !== stack ? stack : "" - }); - }); - } - function throwException(root3, returnFiber, sourceFiber, value2, rootRenderLanes) { - sourceFiber.flags |= 32768; - if (null !== value2 && "object" === typeof value2 && "function" === typeof value2.then) { - returnFiber = sourceFiber.alternate; - null !== returnFiber && propagateParentContextChanges( - returnFiber, - sourceFiber, - rootRenderLanes, - true - ); - sourceFiber = suspenseHandlerStackCursor.current; - if (null !== sourceFiber) { - switch (sourceFiber.tag) { - case 13: - return null === shellBoundary ? renderDidSuspendDelayIfPossible() : null === sourceFiber.alternate && 0 === workInProgressRootExitStatus && (workInProgressRootExitStatus = 3), sourceFiber.flags &= -257, sourceFiber.flags |= 65536, sourceFiber.lanes = rootRenderLanes, value2 === noopSuspenseyCommitThenable ? sourceFiber.flags |= 16384 : (returnFiber = sourceFiber.updateQueue, null === returnFiber ? sourceFiber.updateQueue = /* @__PURE__ */ new Set([value2]) : returnFiber.add(value2), attachPingListener(root3, value2, rootRenderLanes)), false; - case 22: - return sourceFiber.flags |= 65536, value2 === noopSuspenseyCommitThenable ? sourceFiber.flags |= 16384 : (returnFiber = sourceFiber.updateQueue, null === returnFiber ? (returnFiber = { - transitions: null, - markerInstances: null, - retryQueue: /* @__PURE__ */ new Set([value2]) - }, sourceFiber.updateQueue = returnFiber) : (sourceFiber = returnFiber.retryQueue, null === sourceFiber ? returnFiber.retryQueue = /* @__PURE__ */ new Set([value2]) : sourceFiber.add(value2)), attachPingListener(root3, value2, rootRenderLanes)), false; - } - throw Error(formatProdErrorMessage(435, sourceFiber.tag)); - } - attachPingListener(root3, value2, rootRenderLanes); - renderDidSuspendDelayIfPossible(); - return false; - } - if (isHydrating) - return returnFiber = suspenseHandlerStackCursor.current, null !== returnFiber ? (0 === (returnFiber.flags & 65536) && (returnFiber.flags |= 256), returnFiber.flags |= 65536, returnFiber.lanes = rootRenderLanes, value2 !== HydrationMismatchException && (root3 = Error(formatProdErrorMessage(422), { cause: value2 }), queueHydrationError(createCapturedValueAtFiber(root3, sourceFiber)))) : (value2 !== HydrationMismatchException && (returnFiber = Error(formatProdErrorMessage(423), { - cause: value2 - }), queueHydrationError( - createCapturedValueAtFiber(returnFiber, sourceFiber) - )), root3 = root3.current.alternate, root3.flags |= 65536, rootRenderLanes &= -rootRenderLanes, root3.lanes |= rootRenderLanes, value2 = createCapturedValueAtFiber(value2, sourceFiber), rootRenderLanes = createRootErrorUpdate( - root3.stateNode, - value2, - rootRenderLanes - ), enqueueCapturedUpdate(root3, rootRenderLanes), 4 !== workInProgressRootExitStatus && (workInProgressRootExitStatus = 2)), false; - var wrapperError = Error(formatProdErrorMessage(520), { cause: value2 }); - wrapperError = createCapturedValueAtFiber(wrapperError, sourceFiber); - null === workInProgressRootConcurrentErrors ? workInProgressRootConcurrentErrors = [wrapperError] : workInProgressRootConcurrentErrors.push(wrapperError); - 4 !== workInProgressRootExitStatus && (workInProgressRootExitStatus = 2); - if (null === returnFiber) return true; - value2 = createCapturedValueAtFiber(value2, sourceFiber); - sourceFiber = returnFiber; - do { - switch (sourceFiber.tag) { - case 3: - return sourceFiber.flags |= 65536, root3 = rootRenderLanes & -rootRenderLanes, sourceFiber.lanes |= root3, root3 = createRootErrorUpdate(sourceFiber.stateNode, value2, root3), enqueueCapturedUpdate(sourceFiber, root3), false; - case 1: - if (returnFiber = sourceFiber.type, wrapperError = sourceFiber.stateNode, 0 === (sourceFiber.flags & 128) && ("function" === typeof returnFiber.getDerivedStateFromError || null !== wrapperError && "function" === typeof wrapperError.componentDidCatch && (null === legacyErrorBoundariesThatAlreadyFailed || !legacyErrorBoundariesThatAlreadyFailed.has(wrapperError)))) - return sourceFiber.flags |= 65536, rootRenderLanes &= -rootRenderLanes, sourceFiber.lanes |= rootRenderLanes, rootRenderLanes = createClassErrorUpdate(rootRenderLanes), initializeClassErrorUpdate( - rootRenderLanes, - root3, - sourceFiber, - value2 - ), enqueueCapturedUpdate(sourceFiber, rootRenderLanes), false; - } - sourceFiber = sourceFiber.return; - } while (null !== sourceFiber); - return false; - } - var SelectiveHydrationException = Error(formatProdErrorMessage(461)); - var didReceiveUpdate = false; - function reconcileChildren(current, workInProgress2, nextChildren, renderLanes2) { - workInProgress2.child = null === current ? mountChildFibers(workInProgress2, null, nextChildren, renderLanes2) : reconcileChildFibers( - workInProgress2, - current.child, - nextChildren, - renderLanes2 - ); - } - function updateForwardRef(current, workInProgress2, Component, nextProps, renderLanes2) { - Component = Component.render; - var ref = workInProgress2.ref; - if ("ref" in nextProps) { - var propsWithoutRef = {}; - for (var key in nextProps) - "ref" !== key && (propsWithoutRef[key] = nextProps[key]); - } else propsWithoutRef = nextProps; - prepareToReadContext(workInProgress2); - nextProps = renderWithHooks( - current, - workInProgress2, - Component, - propsWithoutRef, - ref, - renderLanes2 - ); - key = checkDidRenderIdHook(); - if (null !== current && !didReceiveUpdate) - return bailoutHooks(current, workInProgress2, renderLanes2), bailoutOnAlreadyFinishedWork(current, workInProgress2, renderLanes2); - isHydrating && key && pushMaterializedTreeId(workInProgress2); - workInProgress2.flags |= 1; - reconcileChildren(current, workInProgress2, nextProps, renderLanes2); - return workInProgress2.child; - } - function updateMemoComponent(current, workInProgress2, Component, nextProps, renderLanes2) { - if (null === current) { - var type2 = Component.type; - if ("function" === typeof type2 && !shouldConstruct(type2) && void 0 === type2.defaultProps && null === Component.compare) - return workInProgress2.tag = 15, workInProgress2.type = type2, updateSimpleMemoComponent( - current, - workInProgress2, - type2, - nextProps, - renderLanes2 - ); - current = createFiberFromTypeAndProps( - Component.type, - null, - nextProps, - workInProgress2, - workInProgress2.mode, - renderLanes2 - ); - current.ref = workInProgress2.ref; - current.return = workInProgress2; - return workInProgress2.child = current; - } - type2 = current.child; - if (!checkScheduledUpdateOrContext(current, renderLanes2)) { - var prevProps = type2.memoizedProps; - Component = Component.compare; - Component = null !== Component ? Component : shallowEqual; - if (Component(prevProps, nextProps) && current.ref === workInProgress2.ref) - return bailoutOnAlreadyFinishedWork(current, workInProgress2, renderLanes2); - } - workInProgress2.flags |= 1; - current = createWorkInProgress(type2, nextProps); - current.ref = workInProgress2.ref; - current.return = workInProgress2; - return workInProgress2.child = current; - } - function updateSimpleMemoComponent(current, workInProgress2, Component, nextProps, renderLanes2) { - if (null !== current) { - var prevProps = current.memoizedProps; - if (shallowEqual(prevProps, nextProps) && current.ref === workInProgress2.ref) - if (didReceiveUpdate = false, workInProgress2.pendingProps = nextProps = prevProps, checkScheduledUpdateOrContext(current, renderLanes2)) - 0 !== (current.flags & 131072) && (didReceiveUpdate = true); - else - return workInProgress2.lanes = current.lanes, bailoutOnAlreadyFinishedWork(current, workInProgress2, renderLanes2); - } - return updateFunctionComponent( - current, - workInProgress2, - Component, - nextProps, - renderLanes2 - ); - } - function updateOffscreenComponent(current, workInProgress2, renderLanes2) { - var nextProps = workInProgress2.pendingProps, nextChildren = nextProps.children, prevState = null !== current ? current.memoizedState : null; - if ("hidden" === nextProps.mode) { - if (0 !== (workInProgress2.flags & 128)) { - nextProps = null !== prevState ? prevState.baseLanes | renderLanes2 : renderLanes2; - if (null !== current) { - nextChildren = workInProgress2.child = current.child; - for (prevState = 0; null !== nextChildren; ) - prevState = prevState | nextChildren.lanes | nextChildren.childLanes, nextChildren = nextChildren.sibling; - workInProgress2.childLanes = prevState & ~nextProps; - } else workInProgress2.childLanes = 0, workInProgress2.child = null; - return deferHiddenOffscreenComponent( - current, - workInProgress2, - nextProps, - renderLanes2 - ); - } - if (0 !== (renderLanes2 & 536870912)) - workInProgress2.memoizedState = { baseLanes: 0, cachePool: null }, null !== current && pushTransition( - workInProgress2, - null !== prevState ? prevState.cachePool : null - ), null !== prevState ? pushHiddenContext(workInProgress2, prevState) : reuseHiddenContextOnStack(), pushOffscreenSuspenseHandler(workInProgress2); - else - return workInProgress2.lanes = workInProgress2.childLanes = 536870912, deferHiddenOffscreenComponent( - current, - workInProgress2, - null !== prevState ? prevState.baseLanes | renderLanes2 : renderLanes2, - renderLanes2 - ); - } else - null !== prevState ? (pushTransition(workInProgress2, prevState.cachePool), pushHiddenContext(workInProgress2, prevState), reuseSuspenseHandlerOnStack(workInProgress2), workInProgress2.memoizedState = null) : (null !== current && pushTransition(workInProgress2, null), reuseHiddenContextOnStack(), reuseSuspenseHandlerOnStack(workInProgress2)); - reconcileChildren(current, workInProgress2, nextChildren, renderLanes2); - return workInProgress2.child; - } - function deferHiddenOffscreenComponent(current, workInProgress2, nextBaseLanes, renderLanes2) { - var JSCompiler_inline_result = peekCacheFromPool(); - JSCompiler_inline_result = null === JSCompiler_inline_result ? null : { parent: CacheContext._currentValue, pool: JSCompiler_inline_result }; - workInProgress2.memoizedState = { - baseLanes: nextBaseLanes, - cachePool: JSCompiler_inline_result - }; - null !== current && pushTransition(workInProgress2, null); - reuseHiddenContextOnStack(); - pushOffscreenSuspenseHandler(workInProgress2); - null !== current && propagateParentContextChanges(current, workInProgress2, renderLanes2, true); - return null; - } - function markRef(current, workInProgress2) { - var ref = workInProgress2.ref; - if (null === ref) - null !== current && null !== current.ref && (workInProgress2.flags |= 4194816); - else { - if ("function" !== typeof ref && "object" !== typeof ref) - throw Error(formatProdErrorMessage(284)); - if (null === current || current.ref !== ref) - workInProgress2.flags |= 4194816; - } - } - function updateFunctionComponent(current, workInProgress2, Component, nextProps, renderLanes2) { - prepareToReadContext(workInProgress2); - Component = renderWithHooks( - current, - workInProgress2, - Component, - nextProps, - void 0, - renderLanes2 - ); - nextProps = checkDidRenderIdHook(); - if (null !== current && !didReceiveUpdate) - return bailoutHooks(current, workInProgress2, renderLanes2), bailoutOnAlreadyFinishedWork(current, workInProgress2, renderLanes2); - isHydrating && nextProps && pushMaterializedTreeId(workInProgress2); - workInProgress2.flags |= 1; - reconcileChildren(current, workInProgress2, Component, renderLanes2); - return workInProgress2.child; - } - function replayFunctionComponent(current, workInProgress2, nextProps, Component, secondArg, renderLanes2) { - prepareToReadContext(workInProgress2); - workInProgress2.updateQueue = null; - nextProps = renderWithHooksAgain( - workInProgress2, - Component, - nextProps, - secondArg - ); - finishRenderingHooks(current); - Component = checkDidRenderIdHook(); - if (null !== current && !didReceiveUpdate) - return bailoutHooks(current, workInProgress2, renderLanes2), bailoutOnAlreadyFinishedWork(current, workInProgress2, renderLanes2); - isHydrating && Component && pushMaterializedTreeId(workInProgress2); - workInProgress2.flags |= 1; - reconcileChildren(current, workInProgress2, nextProps, renderLanes2); - return workInProgress2.child; - } - function updateClassComponent(current, workInProgress2, Component, nextProps, renderLanes2) { - prepareToReadContext(workInProgress2); - if (null === workInProgress2.stateNode) { - var context = emptyContextObject, contextType = Component.contextType; - "object" === typeof contextType && null !== contextType && (context = readContext(contextType)); - context = new Component(nextProps, context); - workInProgress2.memoizedState = null !== context.state && void 0 !== context.state ? context.state : null; - context.updater = classComponentUpdater; - workInProgress2.stateNode = context; - context._reactInternals = workInProgress2; - context = workInProgress2.stateNode; - context.props = nextProps; - context.state = workInProgress2.memoizedState; - context.refs = {}; - initializeUpdateQueue(workInProgress2); - contextType = Component.contextType; - context.context = "object" === typeof contextType && null !== contextType ? readContext(contextType) : emptyContextObject; - context.state = workInProgress2.memoizedState; - contextType = Component.getDerivedStateFromProps; - "function" === typeof contextType && (applyDerivedStateFromProps( - workInProgress2, - Component, - contextType, - nextProps - ), context.state = workInProgress2.memoizedState); - "function" === typeof Component.getDerivedStateFromProps || "function" === typeof context.getSnapshotBeforeUpdate || "function" !== typeof context.UNSAFE_componentWillMount && "function" !== typeof context.componentWillMount || (contextType = context.state, "function" === typeof context.componentWillMount && context.componentWillMount(), "function" === typeof context.UNSAFE_componentWillMount && context.UNSAFE_componentWillMount(), contextType !== context.state && classComponentUpdater.enqueueReplaceState(context, context.state, null), processUpdateQueue(workInProgress2, nextProps, context, renderLanes2), suspendIfUpdateReadFromEntangledAsyncAction(), context.state = workInProgress2.memoizedState); - "function" === typeof context.componentDidMount && (workInProgress2.flags |= 4194308); - nextProps = true; - } else if (null === current) { - context = workInProgress2.stateNode; - var unresolvedOldProps = workInProgress2.memoizedProps, oldProps = resolveClassComponentProps(Component, unresolvedOldProps); - context.props = oldProps; - var oldContext = context.context, contextType$jscomp$0 = Component.contextType; - contextType = emptyContextObject; - "object" === typeof contextType$jscomp$0 && null !== contextType$jscomp$0 && (contextType = readContext(contextType$jscomp$0)); - var getDerivedStateFromProps = Component.getDerivedStateFromProps; - contextType$jscomp$0 = "function" === typeof getDerivedStateFromProps || "function" === typeof context.getSnapshotBeforeUpdate; - unresolvedOldProps = workInProgress2.pendingProps !== unresolvedOldProps; - contextType$jscomp$0 || "function" !== typeof context.UNSAFE_componentWillReceiveProps && "function" !== typeof context.componentWillReceiveProps || (unresolvedOldProps || oldContext !== contextType) && callComponentWillReceiveProps( - workInProgress2, - context, - nextProps, - contextType - ); - hasForceUpdate = false; - var oldState = workInProgress2.memoizedState; - context.state = oldState; - processUpdateQueue(workInProgress2, nextProps, context, renderLanes2); - suspendIfUpdateReadFromEntangledAsyncAction(); - oldContext = workInProgress2.memoizedState; - unresolvedOldProps || oldState !== oldContext || hasForceUpdate ? ("function" === typeof getDerivedStateFromProps && (applyDerivedStateFromProps( - workInProgress2, - Component, - getDerivedStateFromProps, - nextProps - ), oldContext = workInProgress2.memoizedState), (oldProps = hasForceUpdate || checkShouldComponentUpdate( - workInProgress2, - Component, - oldProps, - nextProps, - oldState, - oldContext, - contextType - )) ? (contextType$jscomp$0 || "function" !== typeof context.UNSAFE_componentWillMount && "function" !== typeof context.componentWillMount || ("function" === typeof context.componentWillMount && context.componentWillMount(), "function" === typeof context.UNSAFE_componentWillMount && context.UNSAFE_componentWillMount()), "function" === typeof context.componentDidMount && (workInProgress2.flags |= 4194308)) : ("function" === typeof context.componentDidMount && (workInProgress2.flags |= 4194308), workInProgress2.memoizedProps = nextProps, workInProgress2.memoizedState = oldContext), context.props = nextProps, context.state = oldContext, context.context = contextType, nextProps = oldProps) : ("function" === typeof context.componentDidMount && (workInProgress2.flags |= 4194308), nextProps = false); - } else { - context = workInProgress2.stateNode; - cloneUpdateQueue(current, workInProgress2); - contextType = workInProgress2.memoizedProps; - contextType$jscomp$0 = resolveClassComponentProps(Component, contextType); - context.props = contextType$jscomp$0; - getDerivedStateFromProps = workInProgress2.pendingProps; - oldState = context.context; - oldContext = Component.contextType; - oldProps = emptyContextObject; - "object" === typeof oldContext && null !== oldContext && (oldProps = readContext(oldContext)); - unresolvedOldProps = Component.getDerivedStateFromProps; - (oldContext = "function" === typeof unresolvedOldProps || "function" === typeof context.getSnapshotBeforeUpdate) || "function" !== typeof context.UNSAFE_componentWillReceiveProps && "function" !== typeof context.componentWillReceiveProps || (contextType !== getDerivedStateFromProps || oldState !== oldProps) && callComponentWillReceiveProps( - workInProgress2, - context, - nextProps, - oldProps - ); - hasForceUpdate = false; - oldState = workInProgress2.memoizedState; - context.state = oldState; - processUpdateQueue(workInProgress2, nextProps, context, renderLanes2); - suspendIfUpdateReadFromEntangledAsyncAction(); - var newState = workInProgress2.memoizedState; - contextType !== getDerivedStateFromProps || oldState !== newState || hasForceUpdate || null !== current && null !== current.dependencies && checkIfContextChanged(current.dependencies) ? ("function" === typeof unresolvedOldProps && (applyDerivedStateFromProps( - workInProgress2, - Component, - unresolvedOldProps, - nextProps - ), newState = workInProgress2.memoizedState), (contextType$jscomp$0 = hasForceUpdate || checkShouldComponentUpdate( - workInProgress2, - Component, - contextType$jscomp$0, - nextProps, - oldState, - newState, - oldProps - ) || null !== current && null !== current.dependencies && checkIfContextChanged(current.dependencies)) ? (oldContext || "function" !== typeof context.UNSAFE_componentWillUpdate && "function" !== typeof context.componentWillUpdate || ("function" === typeof context.componentWillUpdate && context.componentWillUpdate(nextProps, newState, oldProps), "function" === typeof context.UNSAFE_componentWillUpdate && context.UNSAFE_componentWillUpdate( - nextProps, - newState, - oldProps - )), "function" === typeof context.componentDidUpdate && (workInProgress2.flags |= 4), "function" === typeof context.getSnapshotBeforeUpdate && (workInProgress2.flags |= 1024)) : ("function" !== typeof context.componentDidUpdate || contextType === current.memoizedProps && oldState === current.memoizedState || (workInProgress2.flags |= 4), "function" !== typeof context.getSnapshotBeforeUpdate || contextType === current.memoizedProps && oldState === current.memoizedState || (workInProgress2.flags |= 1024), workInProgress2.memoizedProps = nextProps, workInProgress2.memoizedState = newState), context.props = nextProps, context.state = newState, context.context = oldProps, nextProps = contextType$jscomp$0) : ("function" !== typeof context.componentDidUpdate || contextType === current.memoizedProps && oldState === current.memoizedState || (workInProgress2.flags |= 4), "function" !== typeof context.getSnapshotBeforeUpdate || contextType === current.memoizedProps && oldState === current.memoizedState || (workInProgress2.flags |= 1024), nextProps = false); - } - context = nextProps; - markRef(current, workInProgress2); - nextProps = 0 !== (workInProgress2.flags & 128); - context || nextProps ? (context = workInProgress2.stateNode, Component = nextProps && "function" !== typeof Component.getDerivedStateFromError ? null : context.render(), workInProgress2.flags |= 1, null !== current && nextProps ? (workInProgress2.child = reconcileChildFibers( - workInProgress2, - current.child, - null, - renderLanes2 - ), workInProgress2.child = reconcileChildFibers( - workInProgress2, - null, - Component, - renderLanes2 - )) : reconcileChildren(current, workInProgress2, Component, renderLanes2), workInProgress2.memoizedState = context.state, current = workInProgress2.child) : current = bailoutOnAlreadyFinishedWork( - current, - workInProgress2, - renderLanes2 - ); - return current; - } - function mountHostRootWithoutHydrating(current, workInProgress2, nextChildren, renderLanes2) { - resetHydrationState(); - workInProgress2.flags |= 256; - reconcileChildren(current, workInProgress2, nextChildren, renderLanes2); - return workInProgress2.child; - } - var SUSPENDED_MARKER = { - dehydrated: null, - treeContext: null, - retryLane: 0, - hydrationErrors: null - }; - function mountSuspenseOffscreenState(renderLanes2) { - return { baseLanes: renderLanes2, cachePool: getSuspendedCache() }; - } - function getRemainingWorkInPrimaryTree(current, primaryTreeDidDefer, renderLanes2) { - current = null !== current ? current.childLanes & ~renderLanes2 : 0; - primaryTreeDidDefer && (current |= workInProgressDeferredLane); - return current; - } - function updateSuspenseComponent(current, workInProgress2, renderLanes2) { - var nextProps = workInProgress2.pendingProps, showFallback = false, didSuspend = 0 !== (workInProgress2.flags & 128), JSCompiler_temp; - (JSCompiler_temp = didSuspend) || (JSCompiler_temp = null !== current && null === current.memoizedState ? false : 0 !== (suspenseStackCursor.current & 2)); - JSCompiler_temp && (showFallback = true, workInProgress2.flags &= -129); - JSCompiler_temp = 0 !== (workInProgress2.flags & 32); - workInProgress2.flags &= -33; - if (null === current) { - if (isHydrating) { - showFallback ? pushPrimaryTreeSuspenseHandler(workInProgress2) : reuseSuspenseHandlerOnStack(workInProgress2); - if (isHydrating) { - var nextInstance = nextHydratableInstance, JSCompiler_temp$jscomp$0; - if (JSCompiler_temp$jscomp$0 = nextInstance) { - c: { - JSCompiler_temp$jscomp$0 = nextInstance; - for (nextInstance = rootOrSingletonContext; 8 !== JSCompiler_temp$jscomp$0.nodeType; ) { - if (!nextInstance) { - nextInstance = null; - break c; - } - JSCompiler_temp$jscomp$0 = getNextHydratable( - JSCompiler_temp$jscomp$0.nextSibling - ); - if (null === JSCompiler_temp$jscomp$0) { - nextInstance = null; - break c; - } - } - nextInstance = JSCompiler_temp$jscomp$0; - } - null !== nextInstance ? (workInProgress2.memoizedState = { - dehydrated: nextInstance, - treeContext: null !== treeContextProvider ? { id: treeContextId, overflow: treeContextOverflow } : null, - retryLane: 536870912, - hydrationErrors: null - }, JSCompiler_temp$jscomp$0 = createFiberImplClass( - 18, - null, - null, - 0 - ), JSCompiler_temp$jscomp$0.stateNode = nextInstance, JSCompiler_temp$jscomp$0.return = workInProgress2, workInProgress2.child = JSCompiler_temp$jscomp$0, hydrationParentFiber = workInProgress2, nextHydratableInstance = null, JSCompiler_temp$jscomp$0 = true) : JSCompiler_temp$jscomp$0 = false; - } - JSCompiler_temp$jscomp$0 || throwOnHydrationMismatch(workInProgress2); - } - nextInstance = workInProgress2.memoizedState; - if (null !== nextInstance && (nextInstance = nextInstance.dehydrated, null !== nextInstance)) - return isSuspenseInstanceFallback(nextInstance) ? workInProgress2.lanes = 32 : workInProgress2.lanes = 536870912, null; - popSuspenseHandler(workInProgress2); - } - nextInstance = nextProps.children; - nextProps = nextProps.fallback; - if (showFallback) - return reuseSuspenseHandlerOnStack(workInProgress2), showFallback = workInProgress2.mode, nextInstance = mountWorkInProgressOffscreenFiber( - { mode: "hidden", children: nextInstance }, - showFallback - ), nextProps = createFiberFromFragment( - nextProps, - showFallback, - renderLanes2, - null - ), nextInstance.return = workInProgress2, nextProps.return = workInProgress2, nextInstance.sibling = nextProps, workInProgress2.child = nextInstance, showFallback = workInProgress2.child, showFallback.memoizedState = mountSuspenseOffscreenState(renderLanes2), showFallback.childLanes = getRemainingWorkInPrimaryTree( - current, - JSCompiler_temp, - renderLanes2 - ), workInProgress2.memoizedState = SUSPENDED_MARKER, nextProps; - pushPrimaryTreeSuspenseHandler(workInProgress2); - return mountSuspensePrimaryChildren(workInProgress2, nextInstance); - } - JSCompiler_temp$jscomp$0 = current.memoizedState; - if (null !== JSCompiler_temp$jscomp$0 && (nextInstance = JSCompiler_temp$jscomp$0.dehydrated, null !== nextInstance)) { - if (didSuspend) - workInProgress2.flags & 256 ? (pushPrimaryTreeSuspenseHandler(workInProgress2), workInProgress2.flags &= -257, workInProgress2 = retrySuspenseComponentWithoutHydrating( - current, - workInProgress2, - renderLanes2 - )) : null !== workInProgress2.memoizedState ? (reuseSuspenseHandlerOnStack(workInProgress2), workInProgress2.child = current.child, workInProgress2.flags |= 128, workInProgress2 = null) : (reuseSuspenseHandlerOnStack(workInProgress2), showFallback = nextProps.fallback, nextInstance = workInProgress2.mode, nextProps = mountWorkInProgressOffscreenFiber( - { mode: "visible", children: nextProps.children }, - nextInstance - ), showFallback = createFiberFromFragment( - showFallback, - nextInstance, - renderLanes2, - null - ), showFallback.flags |= 2, nextProps.return = workInProgress2, showFallback.return = workInProgress2, nextProps.sibling = showFallback, workInProgress2.child = nextProps, reconcileChildFibers( - workInProgress2, - current.child, - null, - renderLanes2 - ), nextProps = workInProgress2.child, nextProps.memoizedState = mountSuspenseOffscreenState(renderLanes2), nextProps.childLanes = getRemainingWorkInPrimaryTree( - current, - JSCompiler_temp, - renderLanes2 - ), workInProgress2.memoizedState = SUSPENDED_MARKER, workInProgress2 = showFallback); - else if (pushPrimaryTreeSuspenseHandler(workInProgress2), isSuspenseInstanceFallback(nextInstance)) { - JSCompiler_temp = nextInstance.nextSibling && nextInstance.nextSibling.dataset; - if (JSCompiler_temp) var digest = JSCompiler_temp.dgst; - JSCompiler_temp = digest; - nextProps = Error(formatProdErrorMessage(419)); - nextProps.stack = ""; - nextProps.digest = JSCompiler_temp; - queueHydrationError({ value: nextProps, source: null, stack: null }); - workInProgress2 = retrySuspenseComponentWithoutHydrating( - current, - workInProgress2, - renderLanes2 - ); - } else if (didReceiveUpdate || propagateParentContextChanges(current, workInProgress2, renderLanes2, false), JSCompiler_temp = 0 !== (renderLanes2 & current.childLanes), didReceiveUpdate || JSCompiler_temp) { - JSCompiler_temp = workInProgressRoot; - if (null !== JSCompiler_temp && (nextProps = renderLanes2 & -renderLanes2, nextProps = 0 !== (nextProps & 42) ? 1 : getBumpedLaneForHydrationByLane(nextProps), nextProps = 0 !== (nextProps & (JSCompiler_temp.suspendedLanes | renderLanes2)) ? 0 : nextProps, 0 !== nextProps && nextProps !== JSCompiler_temp$jscomp$0.retryLane)) - throw JSCompiler_temp$jscomp$0.retryLane = nextProps, enqueueConcurrentRenderForLane(current, nextProps), scheduleUpdateOnFiber(JSCompiler_temp, current, nextProps), SelectiveHydrationException; - "$?" === nextInstance.data || renderDidSuspendDelayIfPossible(); - workInProgress2 = retrySuspenseComponentWithoutHydrating( - current, - workInProgress2, - renderLanes2 - ); - } else - "$?" === nextInstance.data ? (workInProgress2.flags |= 192, workInProgress2.child = current.child, workInProgress2 = null) : (current = JSCompiler_temp$jscomp$0.treeContext, nextHydratableInstance = getNextHydratable( - nextInstance.nextSibling - ), hydrationParentFiber = workInProgress2, isHydrating = true, hydrationErrors = null, rootOrSingletonContext = false, null !== current && (idStack[idStackIndex++] = treeContextId, idStack[idStackIndex++] = treeContextOverflow, idStack[idStackIndex++] = treeContextProvider, treeContextId = current.id, treeContextOverflow = current.overflow, treeContextProvider = workInProgress2), workInProgress2 = mountSuspensePrimaryChildren( - workInProgress2, - nextProps.children - ), workInProgress2.flags |= 4096); - return workInProgress2; - } - if (showFallback) - return reuseSuspenseHandlerOnStack(workInProgress2), showFallback = nextProps.fallback, nextInstance = workInProgress2.mode, JSCompiler_temp$jscomp$0 = current.child, digest = JSCompiler_temp$jscomp$0.sibling, nextProps = createWorkInProgress(JSCompiler_temp$jscomp$0, { - mode: "hidden", - children: nextProps.children - }), nextProps.subtreeFlags = JSCompiler_temp$jscomp$0.subtreeFlags & 65011712, null !== digest ? showFallback = createWorkInProgress(digest, showFallback) : (showFallback = createFiberFromFragment( - showFallback, - nextInstance, - renderLanes2, - null - ), showFallback.flags |= 2), showFallback.return = workInProgress2, nextProps.return = workInProgress2, nextProps.sibling = showFallback, workInProgress2.child = nextProps, nextProps = showFallback, showFallback = workInProgress2.child, nextInstance = current.child.memoizedState, null === nextInstance ? nextInstance = mountSuspenseOffscreenState(renderLanes2) : (JSCompiler_temp$jscomp$0 = nextInstance.cachePool, null !== JSCompiler_temp$jscomp$0 ? (digest = CacheContext._currentValue, JSCompiler_temp$jscomp$0 = JSCompiler_temp$jscomp$0.parent !== digest ? { parent: digest, pool: digest } : JSCompiler_temp$jscomp$0) : JSCompiler_temp$jscomp$0 = getSuspendedCache(), nextInstance = { - baseLanes: nextInstance.baseLanes | renderLanes2, - cachePool: JSCompiler_temp$jscomp$0 - }), showFallback.memoizedState = nextInstance, showFallback.childLanes = getRemainingWorkInPrimaryTree( - current, - JSCompiler_temp, - renderLanes2 - ), workInProgress2.memoizedState = SUSPENDED_MARKER, nextProps; - pushPrimaryTreeSuspenseHandler(workInProgress2); - renderLanes2 = current.child; - current = renderLanes2.sibling; - renderLanes2 = createWorkInProgress(renderLanes2, { - mode: "visible", - children: nextProps.children - }); - renderLanes2.return = workInProgress2; - renderLanes2.sibling = null; - null !== current && (JSCompiler_temp = workInProgress2.deletions, null === JSCompiler_temp ? (workInProgress2.deletions = [current], workInProgress2.flags |= 16) : JSCompiler_temp.push(current)); - workInProgress2.child = renderLanes2; - workInProgress2.memoizedState = null; - return renderLanes2; - } - function mountSuspensePrimaryChildren(workInProgress2, primaryChildren) { - primaryChildren = mountWorkInProgressOffscreenFiber( - { mode: "visible", children: primaryChildren }, - workInProgress2.mode - ); - primaryChildren.return = workInProgress2; - return workInProgress2.child = primaryChildren; - } - function mountWorkInProgressOffscreenFiber(offscreenProps, mode) { - offscreenProps = createFiberImplClass(22, offscreenProps, null, mode); - offscreenProps.lanes = 0; - offscreenProps.stateNode = { - _visibility: 1, - _pendingMarkers: null, - _retryCache: null, - _transitions: null - }; - return offscreenProps; - } - function retrySuspenseComponentWithoutHydrating(current, workInProgress2, renderLanes2) { - reconcileChildFibers(workInProgress2, current.child, null, renderLanes2); - current = mountSuspensePrimaryChildren( - workInProgress2, - workInProgress2.pendingProps.children - ); - current.flags |= 2; - workInProgress2.memoizedState = null; - return current; - } - function scheduleSuspenseWorkOnFiber(fiber, renderLanes2, propagationRoot) { - fiber.lanes |= renderLanes2; - var alternate = fiber.alternate; - null !== alternate && (alternate.lanes |= renderLanes2); - scheduleContextWorkOnParentPath(fiber.return, renderLanes2, propagationRoot); - } - function initSuspenseListRenderState(workInProgress2, isBackwards, tail, lastContentRow, tailMode) { - var renderState = workInProgress2.memoizedState; - null === renderState ? workInProgress2.memoizedState = { - isBackwards, - rendering: null, - renderingStartTime: 0, - last: lastContentRow, - tail, - tailMode - } : (renderState.isBackwards = isBackwards, renderState.rendering = null, renderState.renderingStartTime = 0, renderState.last = lastContentRow, renderState.tail = tail, renderState.tailMode = tailMode); - } - function updateSuspenseListComponent(current, workInProgress2, renderLanes2) { - var nextProps = workInProgress2.pendingProps, revealOrder = nextProps.revealOrder, tailMode = nextProps.tail; - reconcileChildren(current, workInProgress2, nextProps.children, renderLanes2); - nextProps = suspenseStackCursor.current; - if (0 !== (nextProps & 2)) - nextProps = nextProps & 1 | 2, workInProgress2.flags |= 128; - else { - if (null !== current && 0 !== (current.flags & 128)) - a: for (current = workInProgress2.child; null !== current; ) { - if (13 === current.tag) - null !== current.memoizedState && scheduleSuspenseWorkOnFiber(current, renderLanes2, workInProgress2); - else if (19 === current.tag) - scheduleSuspenseWorkOnFiber(current, renderLanes2, workInProgress2); - else if (null !== current.child) { - current.child.return = current; - current = current.child; - continue; - } - if (current === workInProgress2) break a; - for (; null === current.sibling; ) { - if (null === current.return || current.return === workInProgress2) - break a; - current = current.return; - } - current.sibling.return = current.return; - current = current.sibling; - } - nextProps &= 1; - } - push(suspenseStackCursor, nextProps); - switch (revealOrder) { - case "forwards": - renderLanes2 = workInProgress2.child; - for (revealOrder = null; null !== renderLanes2; ) - current = renderLanes2.alternate, null !== current && null === findFirstSuspended(current) && (revealOrder = renderLanes2), renderLanes2 = renderLanes2.sibling; - renderLanes2 = revealOrder; - null === renderLanes2 ? (revealOrder = workInProgress2.child, workInProgress2.child = null) : (revealOrder = renderLanes2.sibling, renderLanes2.sibling = null); - initSuspenseListRenderState( - workInProgress2, - false, - revealOrder, - renderLanes2, - tailMode - ); - break; - case "backwards": - renderLanes2 = null; - revealOrder = workInProgress2.child; - for (workInProgress2.child = null; null !== revealOrder; ) { - current = revealOrder.alternate; - if (null !== current && null === findFirstSuspended(current)) { - workInProgress2.child = revealOrder; - break; - } - current = revealOrder.sibling; - revealOrder.sibling = renderLanes2; - renderLanes2 = revealOrder; - revealOrder = current; - } - initSuspenseListRenderState( - workInProgress2, - true, - renderLanes2, - null, - tailMode - ); - break; - case "together": - initSuspenseListRenderState(workInProgress2, false, null, null, void 0); - break; - default: - workInProgress2.memoizedState = null; - } - return workInProgress2.child; - } - function bailoutOnAlreadyFinishedWork(current, workInProgress2, renderLanes2) { - null !== current && (workInProgress2.dependencies = current.dependencies); - workInProgressRootSkippedLanes |= workInProgress2.lanes; - if (0 === (renderLanes2 & workInProgress2.childLanes)) - if (null !== current) { - if (propagateParentContextChanges( - current, - workInProgress2, - renderLanes2, - false - ), 0 === (renderLanes2 & workInProgress2.childLanes)) - return null; - } else return null; - if (null !== current && workInProgress2.child !== current.child) - throw Error(formatProdErrorMessage(153)); - if (null !== workInProgress2.child) { - current = workInProgress2.child; - renderLanes2 = createWorkInProgress(current, current.pendingProps); - workInProgress2.child = renderLanes2; - for (renderLanes2.return = workInProgress2; null !== current.sibling; ) - current = current.sibling, renderLanes2 = renderLanes2.sibling = createWorkInProgress(current, current.pendingProps), renderLanes2.return = workInProgress2; - renderLanes2.sibling = null; - } - return workInProgress2.child; - } - function checkScheduledUpdateOrContext(current, renderLanes2) { - if (0 !== (current.lanes & renderLanes2)) return true; - current = current.dependencies; - return null !== current && checkIfContextChanged(current) ? true : false; - } - function attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress2, renderLanes2) { - switch (workInProgress2.tag) { - case 3: - pushHostContainer(workInProgress2, workInProgress2.stateNode.containerInfo); - pushProvider(workInProgress2, CacheContext, current.memoizedState.cache); - resetHydrationState(); - break; - case 27: - case 5: - pushHostContext(workInProgress2); - break; - case 4: - pushHostContainer(workInProgress2, workInProgress2.stateNode.containerInfo); - break; - case 10: - pushProvider( - workInProgress2, - workInProgress2.type, - workInProgress2.memoizedProps.value - ); - break; - case 13: - var state = workInProgress2.memoizedState; - if (null !== state) { - if (null !== state.dehydrated) - return pushPrimaryTreeSuspenseHandler(workInProgress2), workInProgress2.flags |= 128, null; - if (0 !== (renderLanes2 & workInProgress2.child.childLanes)) - return updateSuspenseComponent(current, workInProgress2, renderLanes2); - pushPrimaryTreeSuspenseHandler(workInProgress2); - current = bailoutOnAlreadyFinishedWork( - current, - workInProgress2, - renderLanes2 - ); - return null !== current ? current.sibling : null; - } - pushPrimaryTreeSuspenseHandler(workInProgress2); - break; - case 19: - var didSuspendBefore = 0 !== (current.flags & 128); - state = 0 !== (renderLanes2 & workInProgress2.childLanes); - state || (propagateParentContextChanges( - current, - workInProgress2, - renderLanes2, - false - ), state = 0 !== (renderLanes2 & workInProgress2.childLanes)); - if (didSuspendBefore) { - if (state) - return updateSuspenseListComponent( - current, - workInProgress2, - renderLanes2 - ); - workInProgress2.flags |= 128; - } - didSuspendBefore = workInProgress2.memoizedState; - null !== didSuspendBefore && (didSuspendBefore.rendering = null, didSuspendBefore.tail = null, didSuspendBefore.lastEffect = null); - push(suspenseStackCursor, suspenseStackCursor.current); - if (state) break; - else return null; - case 22: - case 23: - return workInProgress2.lanes = 0, updateOffscreenComponent(current, workInProgress2, renderLanes2); - case 24: - pushProvider(workInProgress2, CacheContext, current.memoizedState.cache); - } - return bailoutOnAlreadyFinishedWork(current, workInProgress2, renderLanes2); - } - function beginWork(current, workInProgress2, renderLanes2) { - if (null !== current) - if (current.memoizedProps !== workInProgress2.pendingProps) - didReceiveUpdate = true; - else { - if (!checkScheduledUpdateOrContext(current, renderLanes2) && 0 === (workInProgress2.flags & 128)) - return didReceiveUpdate = false, attemptEarlyBailoutIfNoScheduledUpdate( - current, - workInProgress2, - renderLanes2 - ); - didReceiveUpdate = 0 !== (current.flags & 131072) ? true : false; - } - else - didReceiveUpdate = false, isHydrating && 0 !== (workInProgress2.flags & 1048576) && pushTreeId(workInProgress2, treeForkCount, workInProgress2.index); - workInProgress2.lanes = 0; - switch (workInProgress2.tag) { - case 16: - a: { - current = workInProgress2.pendingProps; - var lazyComponent = workInProgress2.elementType, init2 = lazyComponent._init; - lazyComponent = init2(lazyComponent._payload); - workInProgress2.type = lazyComponent; - if ("function" === typeof lazyComponent) - shouldConstruct(lazyComponent) ? (current = resolveClassComponentProps(lazyComponent, current), workInProgress2.tag = 1, workInProgress2 = updateClassComponent( - null, - workInProgress2, - lazyComponent, - current, - renderLanes2 - )) : (workInProgress2.tag = 0, workInProgress2 = updateFunctionComponent( - null, - workInProgress2, - lazyComponent, - current, - renderLanes2 - )); - else { - if (void 0 !== lazyComponent && null !== lazyComponent) { - if (init2 = lazyComponent.$$typeof, init2 === REACT_FORWARD_REF_TYPE) { - workInProgress2.tag = 11; - workInProgress2 = updateForwardRef( - null, - workInProgress2, - lazyComponent, - current, - renderLanes2 - ); - break a; - } else if (init2 === REACT_MEMO_TYPE) { - workInProgress2.tag = 14; - workInProgress2 = updateMemoComponent( - null, - workInProgress2, - lazyComponent, - current, - renderLanes2 - ); - break a; - } - } - workInProgress2 = getComponentNameFromType(lazyComponent) || lazyComponent; - throw Error(formatProdErrorMessage(306, workInProgress2, "")); - } - } - return workInProgress2; - case 0: - return updateFunctionComponent( - current, - workInProgress2, - workInProgress2.type, - workInProgress2.pendingProps, - renderLanes2 - ); - case 1: - return lazyComponent = workInProgress2.type, init2 = resolveClassComponentProps( - lazyComponent, - workInProgress2.pendingProps - ), updateClassComponent( - current, - workInProgress2, - lazyComponent, - init2, - renderLanes2 - ); - case 3: - a: { - pushHostContainer( - workInProgress2, - workInProgress2.stateNode.containerInfo - ); - if (null === current) throw Error(formatProdErrorMessage(387)); - lazyComponent = workInProgress2.pendingProps; - var prevState = workInProgress2.memoizedState; - init2 = prevState.element; - cloneUpdateQueue(current, workInProgress2); - processUpdateQueue(workInProgress2, lazyComponent, null, renderLanes2); - var nextState = workInProgress2.memoizedState; - lazyComponent = nextState.cache; - pushProvider(workInProgress2, CacheContext, lazyComponent); - lazyComponent !== prevState.cache && propagateContextChanges( - workInProgress2, - [CacheContext], - renderLanes2, - true - ); - suspendIfUpdateReadFromEntangledAsyncAction(); - lazyComponent = nextState.element; - if (prevState.isDehydrated) - if (prevState = { - element: lazyComponent, - isDehydrated: false, - cache: nextState.cache - }, workInProgress2.updateQueue.baseState = prevState, workInProgress2.memoizedState = prevState, workInProgress2.flags & 256) { - workInProgress2 = mountHostRootWithoutHydrating( - current, - workInProgress2, - lazyComponent, - renderLanes2 - ); - break a; - } else if (lazyComponent !== init2) { - init2 = createCapturedValueAtFiber( - Error(formatProdErrorMessage(424)), - workInProgress2 - ); - queueHydrationError(init2); - workInProgress2 = mountHostRootWithoutHydrating( - current, - workInProgress2, - lazyComponent, - renderLanes2 - ); - break a; - } else { - current = workInProgress2.stateNode.containerInfo; - switch (current.nodeType) { - case 9: - current = current.body; - break; - default: - current = "HTML" === current.nodeName ? current.ownerDocument.body : current; - } - nextHydratableInstance = getNextHydratable(current.firstChild); - hydrationParentFiber = workInProgress2; - isHydrating = true; - hydrationErrors = null; - rootOrSingletonContext = true; - renderLanes2 = mountChildFibers( - workInProgress2, - null, - lazyComponent, - renderLanes2 - ); - for (workInProgress2.child = renderLanes2; renderLanes2; ) - renderLanes2.flags = renderLanes2.flags & -3 | 4096, renderLanes2 = renderLanes2.sibling; - } - else { - resetHydrationState(); - if (lazyComponent === init2) { - workInProgress2 = bailoutOnAlreadyFinishedWork( - current, - workInProgress2, - renderLanes2 - ); - break a; - } - reconcileChildren( - current, - workInProgress2, - lazyComponent, - renderLanes2 - ); - } - workInProgress2 = workInProgress2.child; - } - return workInProgress2; - case 26: - return markRef(current, workInProgress2), null === current ? (renderLanes2 = getResource( - workInProgress2.type, - null, - workInProgress2.pendingProps, - null - )) ? workInProgress2.memoizedState = renderLanes2 : isHydrating || (renderLanes2 = workInProgress2.type, current = workInProgress2.pendingProps, lazyComponent = getOwnerDocumentFromRootContainer( - rootInstanceStackCursor.current - ).createElement(renderLanes2), lazyComponent[internalInstanceKey] = workInProgress2, lazyComponent[internalPropsKey] = current, setInitialProperties(lazyComponent, renderLanes2, current), markNodeAsHoistable(lazyComponent), workInProgress2.stateNode = lazyComponent) : workInProgress2.memoizedState = getResource( - workInProgress2.type, - current.memoizedProps, - workInProgress2.pendingProps, - current.memoizedState - ), null; - case 27: - return pushHostContext(workInProgress2), null === current && isHydrating && (lazyComponent = workInProgress2.stateNode = resolveSingletonInstance( - workInProgress2.type, - workInProgress2.pendingProps, - rootInstanceStackCursor.current - ), hydrationParentFiber = workInProgress2, rootOrSingletonContext = true, init2 = nextHydratableInstance, isSingletonScope(workInProgress2.type) ? (previousHydratableOnEnteringScopedSingleton = init2, nextHydratableInstance = getNextHydratable( - lazyComponent.firstChild - )) : nextHydratableInstance = init2), reconcileChildren( - current, - workInProgress2, - workInProgress2.pendingProps.children, - renderLanes2 - ), markRef(current, workInProgress2), null === current && (workInProgress2.flags |= 4194304), workInProgress2.child; - case 5: - if (null === current && isHydrating) { - if (init2 = lazyComponent = nextHydratableInstance) - lazyComponent = canHydrateInstance( - lazyComponent, - workInProgress2.type, - workInProgress2.pendingProps, - rootOrSingletonContext - ), null !== lazyComponent ? (workInProgress2.stateNode = lazyComponent, hydrationParentFiber = workInProgress2, nextHydratableInstance = getNextHydratable( - lazyComponent.firstChild - ), rootOrSingletonContext = false, init2 = true) : init2 = false; - init2 || throwOnHydrationMismatch(workInProgress2); - } - pushHostContext(workInProgress2); - init2 = workInProgress2.type; - prevState = workInProgress2.pendingProps; - nextState = null !== current ? current.memoizedProps : null; - lazyComponent = prevState.children; - shouldSetTextContent(init2, prevState) ? lazyComponent = null : null !== nextState && shouldSetTextContent(init2, nextState) && (workInProgress2.flags |= 32); - null !== workInProgress2.memoizedState && (init2 = renderWithHooks( - current, - workInProgress2, - TransitionAwareHostComponent, - null, - null, - renderLanes2 - ), HostTransitionContext._currentValue = init2); - markRef(current, workInProgress2); - reconcileChildren(current, workInProgress2, lazyComponent, renderLanes2); - return workInProgress2.child; - case 6: - if (null === current && isHydrating) { - if (current = renderLanes2 = nextHydratableInstance) - renderLanes2 = canHydrateTextInstance( - renderLanes2, - workInProgress2.pendingProps, - rootOrSingletonContext - ), null !== renderLanes2 ? (workInProgress2.stateNode = renderLanes2, hydrationParentFiber = workInProgress2, nextHydratableInstance = null, current = true) : current = false; - current || throwOnHydrationMismatch(workInProgress2); - } - return null; - case 13: - return updateSuspenseComponent(current, workInProgress2, renderLanes2); - case 4: - return pushHostContainer( - workInProgress2, - workInProgress2.stateNode.containerInfo - ), lazyComponent = workInProgress2.pendingProps, null === current ? workInProgress2.child = reconcileChildFibers( - workInProgress2, - null, - lazyComponent, - renderLanes2 - ) : reconcileChildren( - current, - workInProgress2, - lazyComponent, - renderLanes2 - ), workInProgress2.child; - case 11: - return updateForwardRef( - current, - workInProgress2, - workInProgress2.type, - workInProgress2.pendingProps, - renderLanes2 - ); - case 7: - return reconcileChildren( - current, - workInProgress2, - workInProgress2.pendingProps, - renderLanes2 - ), workInProgress2.child; - case 8: - return reconcileChildren( - current, - workInProgress2, - workInProgress2.pendingProps.children, - renderLanes2 - ), workInProgress2.child; - case 12: - return reconcileChildren( - current, - workInProgress2, - workInProgress2.pendingProps.children, - renderLanes2 - ), workInProgress2.child; - case 10: - return lazyComponent = workInProgress2.pendingProps, pushProvider(workInProgress2, workInProgress2.type, lazyComponent.value), reconcileChildren( - current, - workInProgress2, - lazyComponent.children, - renderLanes2 - ), workInProgress2.child; - case 9: - return init2 = workInProgress2.type._context, lazyComponent = workInProgress2.pendingProps.children, prepareToReadContext(workInProgress2), init2 = readContext(init2), lazyComponent = lazyComponent(init2), workInProgress2.flags |= 1, reconcileChildren(current, workInProgress2, lazyComponent, renderLanes2), workInProgress2.child; - case 14: - return updateMemoComponent( - current, - workInProgress2, - workInProgress2.type, - workInProgress2.pendingProps, - renderLanes2 - ); - case 15: - return updateSimpleMemoComponent( - current, - workInProgress2, - workInProgress2.type, - workInProgress2.pendingProps, - renderLanes2 - ); - case 19: - return updateSuspenseListComponent(current, workInProgress2, renderLanes2); - case 31: - return lazyComponent = workInProgress2.pendingProps, renderLanes2 = workInProgress2.mode, lazyComponent = { - mode: lazyComponent.mode, - children: lazyComponent.children - }, null === current ? (renderLanes2 = mountWorkInProgressOffscreenFiber( - lazyComponent, - renderLanes2 - ), renderLanes2.ref = workInProgress2.ref, workInProgress2.child = renderLanes2, renderLanes2.return = workInProgress2, workInProgress2 = renderLanes2) : (renderLanes2 = createWorkInProgress(current.child, lazyComponent), renderLanes2.ref = workInProgress2.ref, workInProgress2.child = renderLanes2, renderLanes2.return = workInProgress2, workInProgress2 = renderLanes2), workInProgress2; - case 22: - return updateOffscreenComponent(current, workInProgress2, renderLanes2); - case 24: - return prepareToReadContext(workInProgress2), lazyComponent = readContext(CacheContext), null === current ? (init2 = peekCacheFromPool(), null === init2 && (init2 = workInProgressRoot, prevState = createCache(), init2.pooledCache = prevState, prevState.refCount++, null !== prevState && (init2.pooledCacheLanes |= renderLanes2), init2 = prevState), workInProgress2.memoizedState = { - parent: lazyComponent, - cache: init2 - }, initializeUpdateQueue(workInProgress2), pushProvider(workInProgress2, CacheContext, init2)) : (0 !== (current.lanes & renderLanes2) && (cloneUpdateQueue(current, workInProgress2), processUpdateQueue(workInProgress2, null, null, renderLanes2), suspendIfUpdateReadFromEntangledAsyncAction()), init2 = current.memoizedState, prevState = workInProgress2.memoizedState, init2.parent !== lazyComponent ? (init2 = { parent: lazyComponent, cache: lazyComponent }, workInProgress2.memoizedState = init2, 0 === workInProgress2.lanes && (workInProgress2.memoizedState = workInProgress2.updateQueue.baseState = init2), pushProvider(workInProgress2, CacheContext, lazyComponent)) : (lazyComponent = prevState.cache, pushProvider(workInProgress2, CacheContext, lazyComponent), lazyComponent !== init2.cache && propagateContextChanges( - workInProgress2, - [CacheContext], - renderLanes2, - true - ))), reconcileChildren( - current, - workInProgress2, - workInProgress2.pendingProps.children, - renderLanes2 - ), workInProgress2.child; - case 29: - throw workInProgress2.pendingProps; - } - throw Error(formatProdErrorMessage(156, workInProgress2.tag)); - } - function markUpdate(workInProgress2) { - workInProgress2.flags |= 4; - } - function preloadResourceAndSuspendIfNeeded(workInProgress2, resource) { - if ("stylesheet" !== resource.type || 0 !== (resource.state.loading & 4)) - workInProgress2.flags &= -16777217; - else if (workInProgress2.flags |= 16777216, !preloadResource(resource)) { - resource = suspenseHandlerStackCursor.current; - if (null !== resource && ((workInProgressRootRenderLanes & 4194048) === workInProgressRootRenderLanes ? null !== shellBoundary : (workInProgressRootRenderLanes & 62914560) !== workInProgressRootRenderLanes && 0 === (workInProgressRootRenderLanes & 536870912) || resource !== shellBoundary)) - throw suspendedThenable = noopSuspenseyCommitThenable, SuspenseyCommitException; - workInProgress2.flags |= 8192; - } - } - function scheduleRetryEffect(workInProgress2, retryQueue) { - null !== retryQueue && (workInProgress2.flags |= 4); - workInProgress2.flags & 16384 && (retryQueue = 22 !== workInProgress2.tag ? claimNextRetryLane() : 536870912, workInProgress2.lanes |= retryQueue, workInProgressSuspendedRetryLanes |= retryQueue); - } - function cutOffTailIfNeeded(renderState, hasRenderedATailFallback) { - if (!isHydrating) - switch (renderState.tailMode) { - case "hidden": - hasRenderedATailFallback = renderState.tail; - for (var lastTailNode = null; null !== hasRenderedATailFallback; ) - null !== hasRenderedATailFallback.alternate && (lastTailNode = hasRenderedATailFallback), hasRenderedATailFallback = hasRenderedATailFallback.sibling; - null === lastTailNode ? renderState.tail = null : lastTailNode.sibling = null; - break; - case "collapsed": - lastTailNode = renderState.tail; - for (var lastTailNode$113 = null; null !== lastTailNode; ) - null !== lastTailNode.alternate && (lastTailNode$113 = lastTailNode), lastTailNode = lastTailNode.sibling; - null === lastTailNode$113 ? hasRenderedATailFallback || null === renderState.tail ? renderState.tail = null : renderState.tail.sibling = null : lastTailNode$113.sibling = null; - } - } - function bubbleProperties(completedWork) { - var didBailout = null !== completedWork.alternate && completedWork.alternate.child === completedWork.child, newChildLanes = 0, subtreeFlags = 0; - if (didBailout) - for (var child$114 = completedWork.child; null !== child$114; ) - newChildLanes |= child$114.lanes | child$114.childLanes, subtreeFlags |= child$114.subtreeFlags & 65011712, subtreeFlags |= child$114.flags & 65011712, child$114.return = completedWork, child$114 = child$114.sibling; - else - for (child$114 = completedWork.child; null !== child$114; ) - newChildLanes |= child$114.lanes | child$114.childLanes, subtreeFlags |= child$114.subtreeFlags, subtreeFlags |= child$114.flags, child$114.return = completedWork, child$114 = child$114.sibling; - completedWork.subtreeFlags |= subtreeFlags; - completedWork.childLanes = newChildLanes; - return didBailout; - } - function completeWork(current, workInProgress2, renderLanes2) { - var newProps = workInProgress2.pendingProps; - popTreeContext(workInProgress2); - switch (workInProgress2.tag) { - case 31: - case 16: - case 15: - case 0: - case 11: - case 7: - case 8: - case 12: - case 9: - case 14: - return bubbleProperties(workInProgress2), null; - case 1: - return bubbleProperties(workInProgress2), null; - case 3: - renderLanes2 = workInProgress2.stateNode; - newProps = null; - null !== current && (newProps = current.memoizedState.cache); - workInProgress2.memoizedState.cache !== newProps && (workInProgress2.flags |= 2048); - popProvider(CacheContext); - popHostContainer(); - renderLanes2.pendingContext && (renderLanes2.context = renderLanes2.pendingContext, renderLanes2.pendingContext = null); - if (null === current || null === current.child) - popHydrationState(workInProgress2) ? markUpdate(workInProgress2) : null === current || current.memoizedState.isDehydrated && 0 === (workInProgress2.flags & 256) || (workInProgress2.flags |= 1024, upgradeHydrationErrorsToRecoverable()); - bubbleProperties(workInProgress2); - return null; - case 26: - return renderLanes2 = workInProgress2.memoizedState, null === current ? (markUpdate(workInProgress2), null !== renderLanes2 ? (bubbleProperties(workInProgress2), preloadResourceAndSuspendIfNeeded(workInProgress2, renderLanes2)) : (bubbleProperties(workInProgress2), workInProgress2.flags &= -16777217)) : renderLanes2 ? renderLanes2 !== current.memoizedState ? (markUpdate(workInProgress2), bubbleProperties(workInProgress2), preloadResourceAndSuspendIfNeeded(workInProgress2, renderLanes2)) : (bubbleProperties(workInProgress2), workInProgress2.flags &= -16777217) : (current.memoizedProps !== newProps && markUpdate(workInProgress2), bubbleProperties(workInProgress2), workInProgress2.flags &= -16777217), null; - case 27: - popHostContext(workInProgress2); - renderLanes2 = rootInstanceStackCursor.current; - var type2 = workInProgress2.type; - if (null !== current && null != workInProgress2.stateNode) - current.memoizedProps !== newProps && markUpdate(workInProgress2); - else { - if (!newProps) { - if (null === workInProgress2.stateNode) - throw Error(formatProdErrorMessage(166)); - bubbleProperties(workInProgress2); - return null; - } - current = contextStackCursor.current; - popHydrationState(workInProgress2) ? prepareToHydrateHostInstance(workInProgress2, current) : (current = resolveSingletonInstance(type2, newProps, renderLanes2), workInProgress2.stateNode = current, markUpdate(workInProgress2)); - } - bubbleProperties(workInProgress2); - return null; - case 5: - popHostContext(workInProgress2); - renderLanes2 = workInProgress2.type; - if (null !== current && null != workInProgress2.stateNode) - current.memoizedProps !== newProps && markUpdate(workInProgress2); - else { - if (!newProps) { - if (null === workInProgress2.stateNode) - throw Error(formatProdErrorMessage(166)); - bubbleProperties(workInProgress2); - return null; - } - current = contextStackCursor.current; - if (popHydrationState(workInProgress2)) - prepareToHydrateHostInstance(workInProgress2, current); - else { - type2 = getOwnerDocumentFromRootContainer( - rootInstanceStackCursor.current - ); - switch (current) { - case 1: - current = type2.createElementNS( - "http://www.w3.org/2000/svg", - renderLanes2 - ); - break; - case 2: - current = type2.createElementNS( - "http://www.w3.org/1998/Math/MathML", - renderLanes2 - ); - break; - default: - switch (renderLanes2) { - case "svg": - current = type2.createElementNS( - "http://www.w3.org/2000/svg", - renderLanes2 - ); - break; - case "math": - current = type2.createElementNS( - "http://www.w3.org/1998/Math/MathML", - renderLanes2 - ); - break; - case "script": - current = type2.createElement("div"); - current.innerHTML = "